Structured Transport
The structured transport always writes one JSON object per log entry. By default each entry has level, time, and msg fields, with fields and metadata merged at the root.
go get go.loglayer.dev/transports/structuredBasic Usage
import (
"go.loglayer.dev"
"go.loglayer.dev/transports/structured"
)
log := loglayer.New(loglayer.Config{
Transport: structured.New(structured.Config{}),
})
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","requestId":"abc","user":"alice"}Config
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
structured.New(structured.Config{
MessageField: "message",
DateField: "timestamp",
LevelField: "severity",
})
log.Info("renamed")
// {"severity":"info","timestamp":"...","message":"renamed"}Custom Timestamp / Level
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
f, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
log := loglayer.New(loglayer.Config{
Transport: structured.New(structured.Config{Writer: f}),
})The transport calls fmt.Fprintln once per entry, make sure your writer is concurrency-safe if multiple goroutines log at once.
Struct Metadata
When you pass a struct to WithMetadata, the transport JSON-marshals + unmarshals it into a map[string]any and merges the fields at the root:
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","id":7,"email":"alice@example.com"}The roundtrip happens once, in the transport. The core LogLayer does not touch the value, see Metadata.
If you want struct payloads under a single key instead of merged at the root, wrap them in a map yourself:
log.WithMetadata(loglayer.Metadata{"user": User{ID: 7, Email: "..."}}).Info("user")
// {"level":"info","time":"...","msg":"user","user":{"id":7,"email":"..."}}Errors
Errors are serialized via the logger's ErrorSerializer (default {"message": err.Error()}) and placed under ErrorFieldName (default err):
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:
{"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.
