Skip to content

charmbracelet/log Transport

Go ReferenceVersionSourceChangelog

Wraps an existing *charmbracelet/log.Logger. Metadata nests under a single configurable key.

sh
go get go.loglayer.dev/transports/charmlog/v3
go get github.com/charmbracelet/log

The 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

go
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 hello

If you don't pass a Logger, the transport constructs one writing to Writer (default os.Stderr).

Config

go
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

go
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:

go
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):

go
loglayer.New(loglayer.Config{
    Transport:         llcharm.New(llcharm.Config{Logger: cl}),
    MetadataFieldName: "payload",
})

Reaching the Underlying Logger

GetLoggerInstance returns the wrapped *charmbracelet/log.Logger:

go
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 Levelcharmbracelet LevelNote
LogLevelTraceDebugLevelcharmbracelet has no Trace; mapped to lowest
LogLevelDebugDebugLevel
LogLevelInfoInfoLevel
LogLevelWarnWarnLevel
LogLevelErrorErrorLevel
LogLevelFatalFatalLevel
LogLevelPanicFatalLevelcharmbracelet has no Panic; surfaced as Fatal