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