Log-Trace Correlation
Automatically inject trace_id and span_id into Go log entries with the Last9 Go Agent for slog and zap
The Last9 Go Agent automatically injects trace_id and span_id into your log entries so you can jump from a log line directly to its trace. Supported for both Go’s standard log/slog and Uber’s zap.
Trace fields are only injected when the context passed to a log call contains an active OpenTelemetry span. Logs without a span context pass through unchanged.
slog
One line replaces the global logger with a trace-aware version:
package main
import ( "context" "log" "log/slog" "os"
"github.com/last9/go-agent" slogagent "github.com/last9/go-agent/instrumentation/slog")
func main() { if err := agent.Start(); err != nil { log.Fatalf("failed to start agent: %v", err) } defer agent.Shutdown()
// Replaces the global slog logger slogagent.SetDefault(os.Stdout, nil, nil)
// Use *Context methods — trace_id and span_id are injected automatically ctx := context.Background() // in practice, this comes from your HTTP handler slog.InfoContext(ctx, "processing request", "user_id", 42) // Output: {"time":"...","level":"INFO","msg":"processing request","user_id":42,"trace_id":"abc123...","span_id":"def456..."}}Wrap any slog.Handler to add trace correlation:
import ( "log/slog" "os"
slogagent "github.com/last9/go-agent/instrumentation/slog")
// JSON handlerbase := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})handler := slogagent.NewHandler(base, nil)logger := slog.New(handler)
// Text handlerhandler = slogagent.NewTextHandler(os.Stdout, nil, nil)
// JSON shorthandhandler = slogagent.NewJSONHandler(os.Stdout, nil, nil)Override the default trace_id and span_id keys to match your log aggregation backend:
import slogagent "github.com/last9/go-agent/instrumentation/slog"
// Datadog-stylehandler := slogagent.NewJSONHandler(os.Stdout, nil, &slogagent.Options{ TraceKey: "dd.trace_id", SpanKey: "dd.span_id",})
logger := slog.New(handler)Using slog in HTTP handlers
Always use *Context methods and pass the request context:
func getUserHandler(w http.ResponseWriter, r *http.Request) { // r.Context() carries the active span set by your framework middleware slog.InfoContext(r.Context(), "fetching user", "user_id", userID)
user, err := db.QueryContext(r.Context(), "SELECT * FROM users WHERE id = ?", userID) if err != nil { slog.ErrorContext(r.Context(), "db query failed", "error", err) http.Error(w, "internal error", http.StatusInternalServerError) return }
slog.InfoContext(r.Context(), "user fetched", "user_id", userID)}zap
The lightest approach — no wrapper needed. Spread trace fields inline into any log call:
package main
import ( "log"
"go.uber.org/zap" "github.com/last9/go-agent" zapagent "github.com/last9/go-agent/instrumentation/zap")
func main() { if err := agent.Start(); err != nil { log.Fatalf("failed to start agent: %v", err) } defer agent.Shutdown()
logger, _ := zap.NewProduction() defer logger.Sync()
// ctx comes from your HTTP handler or message consumer logger.Info("request handled", zap.String("path", "/api/users"), zap.Int("status", 200), zapagent.TraceFields(ctx)..., ) // Output: {"level":"info","msg":"request handled","path":"/api/users","status":200,"trace_id":"abc...","span_id":"def..."}}Use the zapagent.Logger wrapper for context-aware *Context methods that inject trace fields automatically:
import ( "go.uber.org/zap" zapagent "github.com/last9/go-agent/instrumentation/zap")
base, _ := zap.NewProduction()logger := zapagent.New(base, nil)
// trace_id and span_id are injected automaticallylogger.InfoContext(ctx, "user created", zap.String("user_id", "42"))logger.WarnContext(ctx, "rate limit approaching", zap.Int("remaining", 5))logger.ErrorContext(ctx, "payment failed", zap.Error(err))Chaining works as expected:
// Pre-set fields are preserved alongside trace fieldsreqLogger := logger.With(zap.String("request_id", reqID))reqLogger.InfoContext(ctx, "started")
// Named loggersauthLogger := logger.Named("auth")authLogger.WarnContext(ctx, "token expiring")Override the default trace_id and span_id keys:
logger := zapagent.New(base, &zapagent.Options{ TraceKey: "dd.trace_id", SpanKey: "dd.span_id",})Using zap in HTTP handlers
func orderHandler(c *gin.Context) { logger.InfoContext(c.Request.Context(), "processing order", zap.String("order_id", c.Param("id")), )
if err := processOrder(c.Request.Context()); err != nil { logger.ErrorContext(c.Request.Context(), "order failed", zap.String("order_id", c.Param("id")), zap.Error(err), ) c.JSON(http.StatusInternalServerError, gin.H{"error": "order failed"}) return }
logger.InfoContext(c.Request.Context(), "order completed", zap.String("order_id", c.Param("id")), ) c.JSON(http.StatusOK, gin.H{"status": "ok"})}Viewing Correlated Logs and Traces
Once log-trace correlation is active, navigate to Logs in Last9. Each log entry with a trace_id field links directly to its trace in Trace Explorer.
Troubleshooting
Please get in touch with us on Discord or Email if you have any questions.