Skip to content

Choose a logging surface

ax-go exposes its logger through two public import paths. They give you the same logger — literally the same type, backed by the same implementation — but they pull in very different amounts of code.

This guide shows you how to pick one, and what changes if you pick wrong.

You need Import
Logging only, smallest binary github.com/rshade/ax-go/logging
Logging plus Loki direct push root github.com/rshade/ax-go
Logging plus OpenTelemetry export or ax.Execute root github.com/rshade/ax-go

If you are writing a library, or a CLI that ships as a cross-compiled binary and does not export traces, you want logging.

package main
import (
"context"
"github.com/rshade/ax-go/logging"
)
func main() {
ctx := context.Background()
log := logging.NewLogger(ctx,
logging.WithLoggerLabels(logging.Labels{
Application: "my-cli",
Environment: "prod",
}),
)
log.Info(ctx).Str("stage", "startup").Msg("ready")
}

No build tags. No configuration. The line goes to stderr as JSON, carrying trace_id and span_id — the real values when a span is active in ctx, and valid zero-value hex constants when none is, so your log parser never has to branch on a missing field.

Measured on linux/amd64 with Go 1.26.5 and -trimpath -ldflags="-s -w":

Program Linked packages Stripped binary
imports logging 103 2,261,257 bytes
imports root ax 410 12,013,833 bytes
difference −307 −81.2%

The isolated surface links none of the following, and a test asserts it under all four supported build configurations:

  • the OpenTelemetry SDK and every OTLP exporter
  • gRPC and protobuf
  • Cobra
  • net/http and crypto/tls

net/http is the single largest lever. Excluding it is most of the 81%.

Exactly two things, and both for the same reason — they need dependencies the isolation exists to exclude:

Loki direct push. ax.WithLokiFromEnv() and the AX_LOKI_URL / AX_LOKI_AUTH_TOKEN variables live in root ax, because pushing to Loki needs net/http. This is not a gap to be filled later; it is the boundary working as designed. Note that direct push is the opt-in path anyway — ax-go’s default log shipping is decoupled (stderr → Promtail/Alloy), and that works identically from either surface, because it is just stderr.

Span creation and export. logging reads an active span context, which is why correlation works. It cannot create or export spans — ID generation and export need the SDK. If you call ax.StartTelemetry, you are already importing root ax.

logging.Flush exists and always returns nil for a consumer of logging alone: the only destination that buffers anything is the Loki sink, and it is unreachable from here. Calling it unconditionally in your shutdown path is safe, and keeps working unchanged if you later migrate to the root facade.

For a root-facade CLI whose lifecycle runs through ax.Execute, register the drain once at that boundary:

ax.Execute(ctx, root, ax.WithFlushFunc(func(shutdownCtx context.Context) error {
return ax.Flush(shutdownCtx, logger)
}))

Execute supplies a fresh bounded context and reports a flush failure as a sanitized stderr diagnostic without changing the command exit code. Direct logging.Flush/ax.Flush calls below remain the right demonstration of the surfaces’ shared type identity and for programs with another lifecycle owner.

The two surfaces name one logger. ax.Logger and logging.Logger are the same type, not two similar ones, so all of this compiles:

var a ax.Logger = logging.NewLogger(ctx)
var b logging.Logger = ax.NewLogger(ctx)
_ = ax.Flush(ctx, logging.NewLogger(ctx))
_ = logging.Flush(ctx, ax.NewLogger(ctx))
// An option built by root ax, accepted by the isolated constructor:
log := logging.NewLogger(ctx, ax.WithLokiFromEnv())

That last line is the sharpest demonstration: an option manufactured by root ax, driving root-owned machinery, handed to logging.NewLogger. It works because ax.LoggerOption and logging.LoggerOption are aliases of one declaration.

A practical consequence: a library can accept a logging.Logger and stay small, while the application that calls it uses root ax and passes its own Loki-enabled logger straight in. Neither side converts anything.

Confirm nothing forbidden is reachable:

Terminal window
go list -deps github.com/rshade/ax-go/logging | grep -E \
'^github.com/rshade/ax-go$|^go.opentelemetry.io/otel/sdk|^google.golang.org/grpc|^net/http$|^crypto/tls$'

Expect no output. Then confirm the parts that must be there:

Terminal window
go list -deps github.com/rshade/ax-go/logging | grep -E 'zerolog|otel/trace'

Anchor those patterns. A bare internal/telemetry also matches OpenTelemetry’s own otel/trace/internal/telemetry, which is a legitimate part of the trace API and not an ax-go runtime import.