Skip to content

log/slog Transport

Go ReferenceVersionSourceChangelog

Wraps a stdlib *log/slog.Logger. Metadata nests under a single key via one slog.Attr. Per-call context.Context attached via WithContext is passed through to slog.Logger.LogAttrs so handlers downstream (OpenTelemetry, structured shippers) can extract trace context.

sh
go get go.loglayer.dev/transports/slog/v3

Basic Usage

go
import (
    "log/slog"
    "os"

    "go.loglayer.dev/v3"
    llslog "go.loglayer.dev/transports/slog/v3"
)

handler := slog.NewJSONHandler(os.Stderr, nil)
sl := slog.New(handler)

log := loglayer.New(loglayer.Config{
    Transport: llslog.New(llslog.Config{Logger: sl}),
})

log.Info("hello")
// {"time":"...","level":"INFO","msg":"hello"}

If you don't pass a Logger, the transport constructs one with slog.NewJSONHandler writing to Writer (default os.Stderr).

Config

go
type Config struct {
    transport.BaseConfig

    Logger *slog.Logger // wrap an existing logger
    Writer io.Writer    // used only when Logger is nil
}

Fatal Behavior

slog has no fatal level. This transport maps LogLevelFatal to slog.LevelError + 4 so it sorts above Error in any handler that filters by level. The actual os.Exit(1) decision is made by the LogLayer core based on Config.DisableFatalExit. See Fatal Exits the Process.

Metadata Handling

The core nests the entry's metadata under MetadataFieldName (default "metadata"; set Config.FlattenMetadata: true to restore per-transport v2 placement). This transport honors that key.

Metadata nests under the metadata key

go
log.WithMetadata(loglayer.Metadata{"requestId": "abc", "n": 42}).Info("served")
// {"time":"...","level":"INFO","msg":"served","metadata":{"requestId":"abc","n":42}}

log.WithMetadata(User{ID: 7, Name: "Alice"}).Info("user")
// {"time":"...","level":"INFO","msg":"user","metadata":{"id":7,"name":"Alice"}}

The nested value becomes a single slog.Any(key, value) attribute, so slog renders it via the configured handler (JSON, text, or anything custom). The JSON handler honors json: tags; other handlers may render fields differently.

context.Context Pass-through

The slog transport is special: it forwards WithContext directly to the underlying slog.Logger.LogAttrs call.

go
import "context"

ctx := context.WithValue(context.Background(), traceKey{}, "trace-abc")
log.WithContext(ctx).Info("request received")

If your slog handler is wired to OpenTelemetry (e.g. via slogcontext or a custom handler), the trace context is extracted automatically. See Go Context for the broader pattern.

Reaching the Underlying Logger

GetLoggerInstance returns the underlying *slog.Logger:

go
sl := log.GetLoggerInstance("slog").(*slog.Logger)
sl.With("global", "field").Info("...")

("slog" is whatever you set as BaseConfig.ID; defaults to an auto-generated ID when unset.)

Level Mapping

LogLayer Levelslog LevelNote
LogLevelTraceLevelDebug - 4slog has no Trace; renders as DEBUG-4 in output
LogLevelDebugLevelDebug
LogLevelInfoLevelInfo
LogLevelWarnLevelWarn
LogLevelErrorLevelError
LogLevelFatalLevelError + 4slog has no Fatal; renders as ERROR+4 in output
LogLevelPanicLevelError + 8slog has no Panic; renders as ERROR+8 in output