Skip to content

Logger slog-first boundary

pkg/logger.Logger has been redefined so its method set mirrors the standard library's *slog.Logger exactly. A *slog.Logger now satisfies logger.Logger directly, so it can be assigned straight to props.Props.Logger, and any type with the same method set works as a custom logger.

This is an intentional pre-1.0 breaking change to the Logger interface and to Props.Logger's type.

What was removed from the interface

The following left the Logger interface because *slog.Logger does not have them. They were application/UI concerns, not logging-boundary concerns:

Removed Replacement
Debugf/Infof/Warnf/Errorf Structured logging: log.Debug("msg", "key", val), or log.Debug(fmt.Sprintf(...)) for pre-formatted strings
Fatal/Fatalf Return an error and let pkg/errorhandling / the command layer exit
Print Write to cmd.OutOrStdout() / pkg/output
WithPrefix(string) With("prefix", value) (a structured attribute)
SetLevel(logger.Level) / GetLevel() logger.SetLevel(log, slog.Level) helper; branch on log.Enabled(ctx, slog.LevelDebug)
SetFormatter(Formatter) (on the interface) logger.SetFormatter(log, Formatter) helper

Runtime level and format control

SetLevel/SetFormatter are no longer interface methods. They are now optional capabilities, exposed via two interfaces and two helpers:

type Leveller interface{ SetLevel(level slog.Level) }
type Reformatter interface{ SetFormatter(f Formatter) }

func SetLevel(log Logger, level slog.Level) bool     // no-op + false if log is not a Leveller
func SetFormatter(log Logger, f Formatter) bool       // no-op + false if log is not a Reformatter

GTB's default logger (logger.NewCharm) implements both, so --debug, log.level, and log.format continue to work. A plain *slog.Logger injected by a consumer owns its own level via its handler, so the helpers no-op for it.

Constructors

  • NewCharm(w, opts...) — GTB's default; returns a *slog.Logger-backed logger that also implements Leveller/Reformatter. Use this for Props.Logger when you want --debug/log.level/log.format to take effect.
  • NewCharmSlog(w, opts...) *slog.Logger / NewCharmHandler(w, opts...) slog.Handler — slog-native construction with GTB's Charm output.
  • NewLevelGate(handler, *slog.LevelVar) — wrap a handler for runtime level control via a shared LevelVar.
  • NewNoop() — a discarding *slog.Logger.
  • NewSlog(handler)slog.New(handler) for ecosystem handlers (OTel, etc.).
  • NewBuffer() and NewCaptureHandler() — capture records for test assertions.

Migrating call sites

// before
log.Infof("installed %s to %s", from, to)
log.WithPrefix("[svc] ").Warn("degraded")
if log.GetLevel() == logger.DebugLevel { ... }

// after
log.Info("installed", "from", from, "to", to)
log.With("component", "svc").Warn("degraded")
if log.Enabled(ctx, slog.LevelDebug) { ... }

Note that slog normalises integer attributes to int64; tests that assert on captured integer key/values should compare by value (assert.EqualValues).

Constructors across GTB now take *slog.Logger

The bigger downstream break: reusable package constructors that previously accepted a logger.Logger now accept a *slog.Logger, so the logging seam is the standard library type at every package boundary. If you call one of these directly while holding a logger.Logger (for example props.Logger), convert at the call site with logger.ToSlog(...) — it is nil-safe, returns a *slog.Logger unchanged, and otherwise builds one over the logger's Handler():

// before
props.ErrorHandler = errorhandling.New(props.Logger, props.Tool.Help)
srv, _ := grpc.Register(ctx, "grpc", controller, cfg, props.Logger)

// after — wrap the application logger.Logger at the boundary
props.ErrorHandler = errorhandling.New(logger.ToSlog(props.Logger), props.Tool.Help)
srv, _ := grpc.RegisterFromContainable(ctx, "grpc", controller, cfg, props.Logger)

Affected constructors (non-exhaustive): errorhandling.New; config.WithLogger and config.NewContainerFromViper; controls.WithLogger / Controller.SetLogger; the pkg/http and pkg/grpc core Register/Start/Stop, middleware and interceptors (LoggingMiddleware, RateLimitMiddleware, WithCircuitBreaker, LoggingInterceptor, …); pkg/gateway Register; pkg/http.WithAuthLogger; and telemetry.NewCollector plus the datadog/posthog backends.

Not affected — these deliberately keep taking logger.Logger: the config- reading adapters (*FromContainable / *FromConfig / *FromProps, e.g. grpc.RegisterFromContainable) and pkg/setup (WithTiming, WithRecovery, NewOfflineUpdater). See Typed config section adapters for the settings-struct side of the same refactor.

Generated projects

Regeneration handles the one root-wiring change: scaffolded projects still build Props.Logger with logger.NewCharm(...) (unchanged), but the root now wraps it for the error handler — errorhandling.New(logger.ToSlog(l), p.Tool.Help). Run gtb regenerate project to pick this up; no manual edit to the logger construction itself is needed.