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/prettyThis transport pulls in github.com/fatih/color for ANSI handling.
Basic Usage
import (
"go.loglayer.dev"
"go.loglayer.dev/transports/pretty"
)
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 n=42 user=alice(With colors applied by the default Moonlight theme.)
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 user=alice n=42Message-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
user: alice
request:
method: POST
path: /users
items:
- first
- secondThemes
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"
theme := &pretty.Theme{
Debug: color.New(color.FgCyan).SprintFunc(),
Info: color.New(color.FgGreen).SprintFunc(),
Warn: color.New(color.FgYellow).SprintFunc(),
Error: color.New(color.FgRed).SprintFunc(),
Fatal: color.New(color.BgRed, color.FgWhite).SprintFunc(),
Timestamp: color.New(color.Faint).SprintFunc(),
LogID: color.New(color.Faint).SprintFunc(),
DataKey: color.New(color.FgCyan).SprintFunc(),
DataValue: color.New(color.FgWhite).SprintFunc(),
}
pretty.New(pretty.Config{Theme: theme})(Note: color.New(...).SprintFunc() returns func(...any) string, but Style is func(string) string. Wrap in func(s string) string { return c.Sprint(s) } if needed.)
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
- Maps merge at the root, alongside fields and error fields.
- Structs are JSON-roundtripped into a map (so
json:"foo"tags determine the rendered key) and merged at the root. - Scalars / unknown types fall back to
_metadataas the key.
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 id=7 name=AliceFatal 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,
}),
},
})