Skip to content

Structured Transport ​

Go ReferenceVersionSourceChangelog

The structured transport always writes one JSON object per log entry. By default each entry has level, time, and msg fields, with fields merged at the root.

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

Basic Usage ​

go
import (
    "go.loglayer.dev/v3"
    "go.loglayer.dev/transports/structured/v3"
)

log := loglayer.New(loglayer.Config{
    Transport: structured.New(structured.Config{}),
    FieldsKey: "context",
})

log.Info("hello")
// {"level":"info","time":"2026-04-25T12:00:00Z","msg":"hello"}

log.WithFields(loglayer.Fields{"requestId": "abc"}).
    WithMetadata(loglayer.Metadata{"user": "alice"}).
    Info("served")
// {"level":"info","time":"...","msg":"served","context":{"requestId":"abc"},"metadata":{"user":"alice"}}

Config ​

go
type Config struct {
    transport.BaseConfig

    MessageField string                              // default: "msg"
    DateField    string                              // default: "time"
    LevelField   string                              // default: "level"

    DateFn    func() string                          // override timestamp generation
    LevelFn   func(loglayer.LogLevel) string         // override level rendering
    MessageFn func(loglayer.TransportParams) string  // format the message text

    Writer io.Writer                                  // default: os.Stdout
}

Renaming the Standard Fields ​

go
structured.New(structured.Config{
    MessageField: "message",
    DateField:    "timestamp",
    LevelField:   "severity",
})

log.Info("renamed")
// {"severity":"info","timestamp":"...","message":"renamed"}

Custom Timestamp / Level ​

go
structured.New(structured.Config{
    DateFn:  func() string { return strconv.FormatInt(time.Now().Unix(), 10) },
    LevelFn: func(l loglayer.LogLevel) string { return strings.ToUpper(l.String()) },
})

log.Warn("loud")
// {"level":"WARN","time":"1714060800","msg":"loud"}

Writing to a File or Buffer ​

The Writer field accepts any io.Writer. See Writers for recipes covering files, rotating files, bytes.Buffer, io.MultiWriter, and network sockets, plus a concurrency-safety table.

Struct Metadata ​

Metadata (map or struct) nests under MetadataFieldName, which the v3 core defaults to "metadata":

go
type User struct {
    ID    int    `json:"id"`
    Email string `json:"email"`
}

log.WithMetadata(User{ID: 7, Email: "alice@example.com"}).Info("user")
// {"level":"info","time":"...","msg":"user","metadata":{"id":7,"email":"alice@example.com"}}

log.WithMetadata(loglayer.Metadata{"total": 3}).Info("cart")
// {"level":"info","time":"...","msg":"cart","metadata":{"total":3}}

A struct is JSON-marshaled + unmarshaled into a map[string]any before encoding, so json: tags apply. The core LogLayer does not touch the value, see Metadata. With Config.FlattenMetadata: true, the v2 shape returns: struct fields and map entries merge at the root instead of nesting.

Errors ​

Errors are serialized via the logger's ErrorSerializer (default {"message": err.Error()}) and placed under ErrorFieldName (default err):

go
log.WithError(err).Error("failed")
// {"level":"error","time":"...","msg":"failed","err":{"message":"connection refused"}}

See Error Handling.

When Marshaling Fails ​

If json.Marshal returns an error (typically because metadata contains an unsupported type like a channel), the transport writes a fallback JSON error object instead of dropping the entry silently:

json
{"level":"error","msg":"loglayer: failed to marshal log entry","error":"json: unsupported type: chan int"}

Catch these in monitoring, they indicate a code-side bug, not a runtime issue.

Fatal Behavior ​

This transport writes fatal entries normally; whether the process actually exits is the core's decision via Config.DisableFatalExit (default: exit). See Fatal Exits the Process.