Structured Logging

Logging is one of the pillars of observability alongside distributed tracing and metrics. Microbus uses Go’s standard log/slog for logging with output directed to stderr.

Logs are printed in human-friendly format in the LOCAL and TESTING deployment environments and in JSON in PROD and LAB.

Logs are automatically enriched with the microservice’s hostname, version number and ID, as well by the distributed trace ID, if applicable.

In addition, logs are metered on a per-message basis to make them visible in Grafana. For this reason, the message part should be a fixed string, and all variable parts added as arguments.

The Connector supports 4 methods for logging at different severity levels: LogDebug, LogInfo, LogWarn and LogError. Debug logs are ignored unless the MICROBUS_LOG_DEBUG environment variable is set.

Example:

c.LogInfo(ctx, "Fixed message",
	"key1", "value",
	"key2", 1234,
)

Exporting Logs to OpenTelemetry

When a logs endpoint is configured via OTEL_EXPORTER_OTLP_LOGS_ENDPOINT (or the generic OTEL_EXPORTER_OTLP_ENDPOINT that drives all three signals), each record is also exported to the OpenTelemetry collector over OTLP, alongside the console output. No code change is required — every record logged through LogInfo/LogWarn/LogError/LogDebug (or through Logger()) is mirrored to the collector.

OTLP logs are automatically correlated with their distributed trace: the bridge stamps the native trace and span IDs from the context, so logs land already linked to their traces in Grafana’s LGTM stack. Export is best-effort sideband and never affects service health — a configured-but-unreachable collector simply drops exports after the OTEL_EXPORTER_OTLP_TIMEOUT rather than stalling startup or shutdown.

The Logger() Accessor

Connector.Logger() returns the microservice’s structured *slog.Logger, letting application code instrument third-party libraries against the same logging pipeline and resource as the framework. Logging through the logger’s context-aware methods enriches each record identically to LogInfo/LogError, because both flow through the same handler:

svc.Logger().InfoContext(ctx, "Fixed message", "key1", "value")

The context-aware methods (DebugContext, InfoContext, WarnContext, ErrorContext) carry the trace and actor identity from the context; the non-context methods log without that enrichment. Logger() never returns nil — before Startup it returns a logger that discards all records — but the handle should be fetched at point of use rather than cached across Startup, since the discard logger is replaced with the real one during Startup.