Pretty Transport
The pretty transport renders log entries with ANSI color, theme support, and three view modes (inline, message-only, expanded). Inspired by loglayer's simple-pretty-terminal.
This is the recommended transport for local development and any human-readable terminal output. For production logging, switch to structured, zerolog, or zap.
go get go.loglayer.dev/transports/pretty/v3This transport pulls in github.com/fatih/color for ANSI handling.
Basic Usage
import (
"go.loglayer.dev/v3"
"go.loglayer.dev/transports/pretty/v3"
)
log := loglayer.New(loglayer.Config{
Transport: pretty.New(pretty.Config{}),
})
log.WithMetadata(loglayer.Metadata{"user": "alice", "n": 42}).Info("served")12:34:56.789 ▶ INFO served metadata={n=42 user=alice}(With colors applied by the default Moonlight theme.)
Multi-line messages
Message strings have control bytes (including \n) stripped to defeat log-forging and terminal-escape smuggling. To author a genuinely multi-line message, wrap it with loglayer.Multiline(...). Each authored line is still sanitized for ANSI / CR / bidi / ZWSP individually; only the boundaries between authored elements are preserved. See Log Sanitization for the full picture of what gets sanitized where.
View Modes
Three rendering modes via Config.ViewMode:
Inline (default)
Single-line output with structured data appended as key=value. Best for high-throughput dev logs.
pretty.New(pretty.Config{ViewMode: pretty.ViewModeInline})12:34:56.789 ▶ INFO served n=42 user=aliceMessage-only
Just timestamp, level, and message. Structured data is dropped from output. Useful when you want a clean stream and don't care about per-call payloads.
pretty.New(pretty.Config{ViewMode: pretty.ViewModeMessageOnly})12:34:56.789 ▶ INFO servedExpanded
Header on the first line, then YAML-like indented data underneath. Best when payloads are deep or wide:
pretty.New(pretty.Config{ViewMode: pretty.ViewModeExpanded})12:34:56.789 ▶ INFO served
items:
- first
- second
request:
method: POST
path: /users
user: aliceThemes
Five themes ship with the package, mirroring the upstream JS palette:
| Theme | Best for |
|---|---|
Moonlight() | Dark terminals (default) |
Sunlight() | Light terminals |
Neon() | Dark + high contrast |
Nature() | Light, organic palette |
Pastel() | Soft, low-strain |
pretty.New(pretty.Config{Theme: pretty.Neon()})Custom Themes
A *pretty.Theme is just a struct of Style functions (func(string) string). Build one with color.RGB(...) or any other color library:
import (
"github.com/fatih/color"
"go.loglayer.dev/transports/pretty/v3"
)
// Style is func(string) string; fatih/color's SprintFunc returns
// func(...any) string, so wrap each color in a one-argument closure.
style := func(c *color.Color) pretty.Style {
return func(s string) string { return c.Sprint(s) }
}
theme := &pretty.Theme{
Debug: style(color.New(color.FgCyan)),
Info: style(color.New(color.FgGreen)),
Warn: style(color.New(color.FgYellow)),
Error: style(color.New(color.FgRed)),
Fatal: style(color.New(color.BgRed, color.FgWhite)),
Timestamp: style(color.New(color.Faint)),
LogID: style(color.New(color.Faint)),
DataKey: style(color.New(color.FgCyan)),
DataValue: style(color.New(color.FgWhite)),
}
pretty.New(pretty.Config{Theme: theme})The style wrapper is required, not optional: color.New(...).SprintFunc() returns func(...any) string, but Style is func(string) string, so the two are not assignment-compatible.
Config
type Config struct {
transport.BaseConfig
ViewMode ViewMode // default: ViewModeInline
Theme *Theme // default: Moonlight()
NoColor bool // disable ANSI escape codes
ShowLogID bool // emit a per-entry [hex-id]
TimestampFormat string // Go time format; default "15:04:05.000"
TimestampFn func(time.Time) string // overrides TimestampFormat
MaxInlineDepth int // default: 4
Writer io.Writer // default: os.Stdout
}NoColor
Set this when piping to a file, sending to CI, or running in a container that doesn't honor ANSI:
pretty.New(pretty.Config{NoColor: true})fatih/color also auto-disables when stdout isn't a TTY, so in most CI scenarios you don't need to set this manually.
ShowLogID
Each entry gets a short pseudo-random identifier prefixed to the message. Useful for cross-referencing multi-line expanded output:
pretty.New(pretty.Config{ShowLogID: true})12:34:56.789 ▶ INFO [00001a] served
12:34:56.790 ▶ WARN [00001b] retryTimestamps
Default is 15:04:05.000 (HH:MM:SS.mmm). Override with TimestampFormat (any Go time layout) or TimestampFn for full control:
pretty.New(pretty.Config{
TimestampFormat: "2006-01-02 15:04:05",
})
pretty.New(pretty.Config{
TimestampFn: func(t time.Time) string {
return strconv.FormatInt(t.Unix(), 10)
},
})MaxInlineDepth
In inline mode, nested objects deeper than this collapse to {...} to keep lines short:
pretty.New(pretty.Config{MaxInlineDepth: 2})
log.WithMetadata(loglayer.Metadata{
"shallow": "ok",
"deep": loglayer.Metadata{"nested": loglayer.Metadata{"more": "stuff"}},
}).Info("depth")
// 12:34:56.789 ▶ INFO depth deep={nested={...}} shallow=okDoesn't affect expanded mode; it always shows the full tree.
Metadata Handling
Every metadata value (map, struct, scalar) nests under MetadataFieldName, which the v3 core defaults to "metadata". Set a different key on loglayer.Config, or set FlattenMetadata: true to restore the v2 shape: map and struct metadata roundtrip to root fields, and non-roundtrippable values fall back to _metadata.
log.WithMetadata(loglayer.Metadata{"user": "alice", "n": 42}).Info("served")
// 12:34:56.789 ▶ INFO served metadata={n=42 user=alice}type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
log.WithMetadata(User{ID: 7, Name: "Alice"}).Info("user")
// 12:34:56.789 ▶ INFO user metadata={id=7 name=Alice}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.
Threat Model: For Terminals (Not Pipelines)
Pretty is a terminal renderer. It writes ANSI color codes for level chevrons, keys, and timestamps directly to the writer (defaults to stdout). The message string is sanitized for control characters before output, but field/metadata values in pretty's rendered output pass through to the terminal in their typed form, including any ANSI escape sequences they happen to contain.
Concretely: an attacker who can place a value in a logged field could include \x1b[31m... in that value, which a viewer's terminal would interpret as red text. They could also use \r to overwrite previous output or \x1b[2J to clear the screen.
If your service ships log lines to a viewer who can be tricked by terminal control sequences, use structured instead. Structured emits JSON; encoding/json escapes all control characters by default, so the threat model is closed. Pretty's contract is "for the developer's terminal during local dev"; that's also where it's useful.
Combining with Other Transports
Use it locally alongside a structured transport that ships to disk or a service:
loglayer.New(loglayer.Config{
Transports: []loglayer.Transport{
pretty.New(pretty.Config{
BaseConfig: transport.BaseConfig{ID: "console"},
}),
structured.New(structured.Config{
BaseConfig: transport.BaseConfig{
ID: "ship",
Level: loglayer.LogLevelWarn,
},
Writer: logFile,
}),
},
})