logx

Monolog formatter

A PHP/Monolog-style console formatter, with full-path stack traces pulled out of an errs error.

In Go, the first cold shower is often logging: the raw log package's one-line, context-free output, or a structured logger's wall of key=value where a stack trace collapses into a single escaped string. logx/monolog is logx's answer for development: a console handler that, set as the active format, looks exactly like what you know from PHP.

What it looks like

Set Format: "monolog" on logx.Config (or LOG_FORMAT=monolog, if logx.Config is loaded from the environment) and the console reads like this:

[2026-07-15 10:23:44] shop.INFO: order placed {"order_id":"o_8412","total":12990}
[2026-07-15 10:23:45] shop.WARNING: payment provider slow, retrying {"attempt":2,"provider":"stripe"} in /home/you/shop/internal/modules/shop/surfaces/api/service/payments.go:114

And when a wrapped errs error reaches the logger:

[2026-07-15 10:23:46] shop.ERROR: request failed: shop_out_of_stock: shop: place order: inventory: decrement: insufficient stock {"code":"shop_out_of_stock","method":"POST","path":"/api/v1/shop/orders","status":500}
Stack trace:
#0 /home/you/shop/internal/modules/shop/repository/inventory.go(87): repository.(*InventoryRepository).Decrement()
#1 /home/you/shop/internal/modules/shop/orders.go(41): shop.(*Service).PlaceOrder()
#2 /home/you/shop/internal/modules/shop/surfaces/api/service/orders.go(52): service.(*OrderService).PlaceOrder()
#3 /home/you/shop/internal/modules/shop/surfaces/api/http/handlers.go(61): http.(*Handler).PlaceOrder()
#4 {main}

What's at work in these few lines:

  • The channel names the record's source (a service name, typically), so running several services' logs side by side, you can still see who is talking. The WARN level renders Monolog-faithfully as WARNING.
  • The error headline leads with the errs code (shop_out_of_stock: ...), so the failure mode is identifiable without reading the JSON tail; the code also appears in the JSON context.
  • The stack trace is complete, with absolute paths: most terminals and editors make them clickable. Frames run from the error's point of origin upward; frames from dependencies (the Go module cache) are dimmed, and the runtime entry frames are replaced by the terminal {main} line.
  • The error attr is not duplicated: the value of slog.Any("error", err) goes into the headline and the stack block and is excluded from the JSON context.
  • Color only on a terminal: NO_COLOR and TERM=dumb are honored; piped output carries no escape garbage.
  • A WARN/ERROR record without an errs stack still gets a location: its call site is appended as in file:line.

logx/monolog up close

The format driver is a standalone package with a single constructor:

import "github.com/gp-system/logx/monolog"

func NewHandler(w io.Writer, level slog.Level, channel string) *Handler

logx.ConsoleHandler returns this when Config.Format is "monolog", but you can wire it anywhere a slog.Handler is expected, with or without the rest of logx. Worth knowing:

  • It is slog-native. With(...) attrs and WithGroup(...) groups render correctly, as nested JSON in the context part ("req":{"status":200}).
  • Stacks come from errs. The handler pulls out the record's first error-valued attr; if it (or anything in its wrap chain) is an errs error, its full-path stack from errs.FullFrames is rendered. A plain errors.New error gets no stack block: the log call site goes on the line instead.
  • Lines stay atomic under concurrency: the handler's clones share a mutex for writes; there are no torn lines.

Structured errors in logs: none of this without errs

The stack traces above are mined from errs errors, and that is not a console privilege: *errs.Error implements slog.LogValuer, so in any structured slog.Handler, not only logx/monolog, the error unfolds as a group:

"error": {
  "msg": "shop: place order: inventory: decrement: insufficient stock",
  "code": "shop_out_of_stock",
  "public": "One or more items are out of stock.",
  "chain": [ { "msg": "shop: place order", "file": "service/orders.go", "line": 52, "..." : "" }, "" ],
  "stack": [ { "file": "repository/inventory.go", "line": 87, "..." : "" }, "" ]
}

So the same error is a PHP-style stack trace on the dev console, and machine-ready JSON in any other handler you fan it out to (the framework's OTel log bridge and Sentry handler among them, see Telemetry: Logging), all from a single return err.

Where did the file logs go?

Nowhere, deliberately: there is no single/daily file driver here, logx/monolog writes to whatever io.Writer you give NewHandler, typically stdout. This follows the 12-factor principle: logs go to stdout as an event stream, and collection, rotation and retention are the platform's job, not the logger's. In a container the runtime captures stdout anyway; a collector, or a plain shell redirect, takes it from there.

If you still want a local file: go run ./cmd/myapp 2>&1 | tee myapp.log; in a pipe the handler automatically writes without colors.

Monolog concepts, mapped

If you arrive from config/logging.php, this is how the concepts map:

MonologgpsystemWhere
channel namewhatever channel you pass NewHandler (the framework uses OTEL_SERVICE_NAME)the shop.INFO part of the monolog line
stack driver (several channels in one)logx.Fanoutlogx: Overview
a channel's level thresholdlogx.LevelFilterlogx: Overview
single / daily file drivernone (stdout is the only destination)see above
formatter (LineFormatter, JSON)logx.Config.Format (auto / monolog)logx: Overview
Log::info('msg', ['ctx' => ...])slog.InfoContext(ctx, "msg", slog.Any("ctx", ...))app code
Log::withContext([...])logger := slog.Default().With(...)app code
a Sentry / Slack channel in the stackanother handler added to the fanoutyour own wiring, or the framework's fanout, see Telemetry: Logging
  • logx: Overview: install, Config, and the ConsoleHandler/LevelFilter/Fanout composition this formatter plugs into.
  • errs: Overview: the error type this formatter's stack traces come from.
  • Telemetry: Logging: how the framework fans this handler out alongside the OTel log bridge and the Sentry capture handler.
Copyright © 2026