charmbracelet/log Transport
Wraps an existing *charmbracelet/log.Logger. Map metadata flattens to alternating key/value pairs; struct metadata lands under a configurable key.
go get go.loglayer.dev/transports/charmlog/v2
go get github.com/charmbracelet/logThe package name is charmlog (since both the import path's last element and the package itself are log, which would collide with the stdlib).
Basic Usage
import (
"os"
clog "github.com/charmbracelet/log"
"go.loglayer.dev/v2"
llcharm "go.loglayer.dev/transports/charmlog/v2"
)
cl := clog.NewWithOptions(os.Stderr, clog.Options{
Level: clog.InfoLevel,
ReportTimestamp: true,
})
log := loglayer.New(loglayer.Config{
Transport: llcharm.New(llcharm.Config{Logger: cl}),
})
log.Info("hello")
// 2026-04-25 12:00:00 INFO helloIf you don't pass a Logger, the transport constructs one writing to Writer (default os.Stderr).
Config
type Config struct {
transport.BaseConfig
Logger *charmbracelet/log.Logger // wrap an existing logger
Writer io.Writer // used only when Logger is nil
}Fatal Behavior
charmbracelet's Logger.Fatal() calls os.Exit(1), but Logger.Log(FatalLevel, msg, keyvals...) does not. This wrapper always dispatches via Log(level, ...), so charmbracelet writes the fatal entry and returns. The core then decides whether os.Exit(1) is called after dispatch. See Fatal Exits the Process.
Metadata Handling
The placement key for non-map metadata is controlled by the core via MetadataFieldName. When unset, this transport defaults to nesting non-map metadata under "metadata".
Map metadata → individual key/value pairs
log.WithMetadata(loglayer.Metadata{"requestId": "xyz", "n": 42}).Info("served")
// INFO served requestId=xyz n=42Each map entry becomes a (key, value) pair in the variadic keyvals argument to Log.
Struct metadata nests under the metadata key
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
log.WithMetadata(User{ID: 7, Name: "Alice"}).Info("user")
// INFO user metadata={ID:7 Name:Alice}charmbracelet renders the struct via its default formatter. Exact output shape depends on whether you've configured JSONFormatter, TextFormatter, or the default colored output.
To use a different key per call, wrap in a map:
log.WithMetadata(loglayer.Metadata{"user": User{ID: 7, Name: "Alice"}}).Info("user")Or globally via the core's MetadataFieldName (which also nests map metadata under the same key):
loglayer.New(loglayer.Config{
Transport: llcharm.New(llcharm.Config{Logger: cl}),
MetadataFieldName: "payload",
})Reaching the Underlying Logger
GetLoggerInstance returns the wrapped *charmbracelet/log.Logger:
cl := log.GetLoggerInstance("charmlog").(*clog.Logger)
cl.SetLevel(clog.DebugLevel)Level Mapping
| LogLayer Level | charmbracelet Level | Note |
|---|---|---|
LogLevelTrace | DebugLevel | charmbracelet has no Trace; mapped to lowest |
LogLevelDebug | DebugLevel | |
LogLevelInfo | InfoLevel | |
LogLevelWarn | WarnLevel | |
LogLevelError | ErrorLevel | |
LogLevelFatal | FatalLevel | |
LogLevelPanic | FatalLevel | charmbracelet has no Panic; surfaced as Fatal |
