charmbracelet/log Transport
Wraps an existing *charmbracelet/log.Logger. Metadata nests under a single configurable key.
go get go.loglayer.dev/transports/charmlog/v3
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/v3"
llcharm "go.loglayer.dev/transports/charmlog/v3"
)
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 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
log.WithMetadata(loglayer.Metadata{"requestId": "xyz", "n": 42}).Info("served")
// INFO served metadata="{\"requestId\":\"xyz\",\"n\":42}"
log.WithMetadata(User{ID: 7, Name: "Alice"}).Info("user")
// INFO user metadata="{ID:7 Name:Alice}"With Config.FlattenMetadata: true, map metadata instead becomes (key, value) pairs in the variadic keyvals argument to Log.
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)("charmlog" is whatever you set as BaseConfig.ID; defaults to an auto-generated ID when unset.)
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 |
