Logging and Trace ID
Logging and traceability are not optional features in production systems.
They are the minimum requirements for understanding behavior, diagnosing failures, and operating services with confidence.
Plumego does not provide logging or tracing by default.
This is intentional.
Instead, Plumego provides clear extension points so that logging and trace identifiers can be implemented explicitly and correctly.
Design Principles
Before looking at code, it is important to understand the guiding principles:
- Logging is a cross-cutting concern → implemented as middleware
- Trace IDs are request-scoped → stored in context
- No global magic → everything is explicit
- Framework-agnostic logging libraries → Plumego does not prescribe one
This keeps observability flexible and replaceable.
What Is a Trace ID?
A Trace ID is a unique identifier assigned to a single request.
It allows you to:
- Correlate logs across layers
- Track a request through middleware and handlers
- Associate downstream calls with the originating request
- Debug production issues reliably
Every request should have exactly one Trace ID.
Where Trace IDs Live
In Plumego:
- The Trace ID is generated (or extracted) in middleware
- Stored in the request-scoped context
- Read by handlers, services, and logging utilities
- Never stored globally
This ensures isolation and correctness.
Step 1: Decide the Trace ID Strategy
A common strategy is:
- If the incoming request has a Trace ID header → reuse it
- Otherwise → generate a new Trace ID
Typical header names:
X-Request-IDX-Trace-ID
The exact name is a system-level decision.
Step 2: Trace ID Middleware (Example)
Below is a conceptual example of a Trace ID middleware.
func TraceIDMiddleware() plumego.Middleware {
return func(ctx *plumego.Context, next plumego.NextFunc) {
traceID := ctx.Request().Header.Get("X-Trace-ID")
if traceID == "" {
traceID = generateTraceID()
}
ctx.Set("trace_id", traceID)
// Expose Trace ID to the client
ctx.ResponseWriter().Header().Set("X-Trace-ID", traceID)
next()
}
}
Key points:
- Trace ID is generated once per request
- Stored in context
- Propagated to the response
- Middleware controls the lifecycle
Step 3: Accessing Trace ID in Handlers
Handlers can retrieve the Trace ID from context:
traceID, _ := ctx.Get("trace_id")
This value can be:
- Included in logs
- Passed to downstream calls
- Returned in error responses
Handlers should treat Trace ID as read-only metadata.
Step 4: Structured Logging
Plumego does not prescribe a logging library.
However, it strongly encourages structured logging.
A typical logging call might include:
- Timestamp
- Log level
- Message
- Trace ID
- Request metadata
Example (conceptual):
logger.Info("request completed",
"trace_id", ctx.Get("trace_id"),
"method", ctx.Request().Method,
"path", ctx.Request().URL.Path,
"status", statusCode,
"duration_ms", duration,
)
The important part is not the library —
it is consistent fields across logs.
Logging Middleware Pattern
Logging is best implemented as middleware that:
- Records start time
- Calls
next() - Logs request outcome
Example structure:
func LoggingMiddleware(logger Logger) plumego.Middleware {
return func(ctx *plumego.Context, next plumego.NextFunc) {
start := time.Now()
next()
duration := time.Since(start)
logger.Info("request",
"trace_id", ctx.Get("trace_id"),
"method", ctx.Request().Method,
"path", ctx.Request().URL.Path,
"duration_ms", duration.Milliseconds(),
)
}
}
This keeps logging:
- Centralized
- Consistent
- Decoupled from handlers
Ordering Matters
Middleware order is critical.
Recommended order:
- Trace ID middleware
- Logging middleware
- Authentication / Authorization
- Business handlers
This ensures:
- All logs include a Trace ID
- Logging captures authentication failures
- Timing is accurate
Middleware order should be treated as part of system design.
Avoiding Common Logging Mistakes
Logging Inside Domain Logic
Domain code should not log.
Reasons:
- It pollutes core logic
- It couples domain to infrastructure
- It reduces testability
Logging belongs at the edges.
Generating Trace IDs in Multiple Places
There must be exactly one source of truth.
Multiple Trace IDs per request destroy observability.
Using Global Context for Trace IDs
Trace IDs must be request-scoped.
Global storage leads to data races and incorrect correlation.
Propagating Trace IDs Downstream
When calling external systems:
- Include the Trace ID in outgoing headers
- Use the same header name consistently
This enables cross-service tracing even without a full tracing system.
Trace ID vs Distributed Tracing
Trace IDs are a foundation.
They can later integrate with:
- OpenTelemetry
- Jaeger
- Zipkin
- Cloud provider tracing
Plumego’s explicit approach makes this integration straightforward.
Minimal Is Enough
A simple Trace ID + structured logging setup provides:
- Debuggability
- Correlation
- Operational confidence
You do not need a full tracing stack on day one.
Plumego encourages incremental observability.
Summary
In Plumego:
- Logging is explicit middleware
- Trace IDs are request-scoped
- Context is the carrier
- Libraries are replaceable
- Behavior is predictable
Observability is not magic — it is structure.
Next
With logging and traceability in place, the next practical concern is:
→ Panic Recovery
This explains how to prevent unexpected crashes from taking down your service.