Skip to content

Introducing mtlog

Logs humans can read at 2 AM. Structured data machines can query at 9 AM. In Go.

• 5 min read

Go developers often face a trade-off when it comes to logging: readable logs that are great for humans but lack structured data for queries, or structured logs that are great for machines but verbose and awkward to read in raw form.

With mtlog, you don't have to choose.

Why mtlog?

mtlog brings message template logging—popularized by Serilog in the .NET ecosystem—to Go. The result: logs that read like prose at 2 AM and contain rich structured properties for 9 AM production queries.

// Human-friendly and structured
log.Info("User {UserId} purchased {@Order} from {IP} in {Duration:F2}ms",
    userId, order, ipAddress, duration)

Humans see:

User 123 purchased {Id:456 Total:99.95 Items:[...]} from 192.168.1.5 in 153ms

Machines see:

{"UserId":123,"Order":{"Id":456,"Total":99.95,"Items":[...]},"IP":"192.168.1.5","Duration":153}

Key Features

  • Message templates with {Property}, OTEL-style {http.method} names, capture hints (@ for destructuring, $ for stringification), and format specifiers.
  • Clear syntax for built-ins: ${Message}, ${Timestamp}, ${Level}—no collisions with user properties.
  • Zero allocations for simple log events (≈17 ns/op).
  • Adapters for slog and logr so you can integrate without rewriting your whole app.
  • A full sink ecosystem: console (themes), rolling file, Seq (CLEF), Elasticsearch, Splunk, async, and durable buffering.
  • Dynamic level control at runtime—locally or via Seq.

Built with Lessons Learned

mtlog's design is informed by years of structured logging experience in other ecosystems:

  • OTEL dotted names ({http.method}, {service.name}) treated as flat property names for compatibility.
  • ${...} output template syntax avoids {Message} collisions and keeps future extensibility open.
  • Format specifiers that actually work—{Duration:F2}, {Price:F2}, {Percent:P1}—not just afterthoughts.
  • Clear capture semantics—@ means capture structured data, $ means stringify.

Tooling From Day One

Logging isn't just about API design—it's about using it correctly, everywhere.

mtlog ships with the mtlog-analyzer, a static analysis tool that:

  • Catches template/argument mismatches
  • Flags duplicate or misnamed properties
  • Suggests capture hints for complex types
  • Warns about error logs without error values
  • Detects dynamic templates that can't be analyzed

And it's not just CLI—real-time feedback is available via:

VS Code extension

Squiggly underlines & quick fixes

GoLand plugin

Real-time annotations

Neovim plugin

LSP integration & code actions

In all of them, you'll see squiggly underlines, quick-fixes, and even interactive help with wrong/right examples.

Output Templates Without Collisions

In most logging libraries, output templates share syntax with message templates, which can lead to conflicts. mtlog avoids this entirely:

// Built-ins are `${...}`
mtlog.WithConsoleTemplate("[${Timestamp:HH:mm:ss} ${Level:u3}] {SourceContext}: ${Message}")
  • ${Message} — the rendered message
  • {SourceContext} — a property from your event
  • No risk of accidentally overwriting your own data

Context That Flows

mtlog's LogContext lets you attach scoped properties to a context.Context that automatically flow through log events:

ctx := mtlog.PushProperty(context.Background(), "RequestId", "abc-123")
logger := baseLogger.WithContext(ctx)

logger.Info("Processing request") // Includes RequestId=abc-123

Useful for request tracing, async workflows, and multi-tenant apps.

ForType — Strongly-Typed SourceContext

Avoid hard-coded strings for source categories:

serviceLogger := mtlog.ForType[UserService](logger)
serviceLogger.Info("User created")
// SourceContext = "UserService"

Cached for near-zero overhead after the first call.

Example: From Verbose to Clear

Before:

log.Info("user logged in", "user_id", 123, "ip", "192.168.1.5")

After (mtlog):

log.Info("User {UserId} logged in from {IP}", 123, "192.168.1.5")

Same structure in JSON output, but dramatically more readable in plain text.

Getting Started

go get github.com/willibrandon/mtlog
log := mtlog.New(mtlog.WithConsole())

log.Info("App started")
log.Info("User {UserId} purchased {@Order}", 42, order)

See the Quick Start guide and examples directory for more.

Try mtlog

mtlog is feature-complete and already being used in real projects.
We're in the final review phase for the public API before the 1.0 release — so now is the perfect time to try it, kick the tires, and help shape the final version.

Docs & Examples

mtlog.dev

GitHub

github.com/willibrandon/mtlog

Static Analyzer

mtlog-analyzer

Readable logs. Structured data. Static analysis. All in Go.

Brandon Williams