Skip to content

Zap Transport ​

Go ReferenceVersionSourceChangelog

Wraps a *zap.Logger. Metadata nests under a configurable key ("metadata" by default); with Config.FlattenMetadata: true, map metadata becomes individual zap fields. Fatal-level entries are written via a custom CheckWriteHook so the process is not terminated, regardless of zap's defaults.

sh
go get go.loglayer.dev/transports/zap/v3
go get go.uber.org/zap

Basic Usage ​

go
import (
    "go.uber.org/zap"

    "go.loglayer.dev/v3"
    llzap "go.loglayer.dev/transports/zap/v3"
)

z, _ := zap.NewProduction()

log := loglayer.New(loglayer.Config{
    Transport: llzap.New(llzap.Config{Logger: z}),
})

log.Info("hello")
// {"level":"info","ts":...,"caller":"...","msg":"hello"}

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

Config ​

go
type Config struct {
    transport.BaseConfig

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

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")
// {"level":"info","ts":...,"caller":"...","msg":"served","metadata":{"requestId":"abc","n":42}}

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

zap reflects into the map or struct via zap.Any, which is faster than a JSON roundtrip.

Change the nesting key on the core's config:

go
loglayer.New(loglayer.Config{
    Transport:         llzap.New(llzap.Config{Logger: z}),
    MetadataFieldName: "payload",
})

Fatal Behavior ​

zap's Logger.Fatal and dispatch via the default fatal hook both call os.Exit(1). zap.WithFatalHook(zapcore.WriteThenNoop) does not work: zap silently overrides WriteThenNoop back to WriteThenFatal. To prevent zap from exiting before the core's DisableFatalExit check runs, this transport always wraps the supplied logger with a custom no-op hook:

go
logger := userLogger.WithOptions(zap.WithFatalHook(noopFatalHook{}))

The result: zap writes the fatal entry and returns. The core then decides whether to call os.Exit(1) based on Config.DisableFatalExit. See Fatal Exits the Process.

Reaching the Underlying Logger ​

GetLoggerInstance returns the (fatal-hook-wrapped) *zap.Logger:

go
z := log.GetLoggerInstance("zap").(*zap.Logger)
z.Sync()

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

This is the wrapped instance, not the original you passed in. For most operations that doesn't matter: fields, sampling, and hooks set before passing the logger to LogLayer are preserved.

Level Mapping ​

LogLayer Levelzap LevelNote
LogLevelTraceDebugLevelzap has no Trace; mapped to lowest
LogLevelDebugDebugLevel
LogLevelInfoInfoLevel
LogLevelWarnWarnLevel
LogLevelErrorErrorLevel
LogLevelFatalFatalLevelwritten but no os.Exit
LogLevelPanicPanicLevelzap's PanicLevel triggers panic() after write