34 Logging Interceptor with Context
When we talk about monitoring and observability in modern applications, logging is the heartbeat of it all. Nothing is more frustrating than a bug that only shows up “sometimes” without enough logs to trace it—a nightmare for any backend developer.
Typically, we add logging across various parts of the application, from the controller and service layer down to the data layer. But if you’re designing a scalable API or backend, you’ll definitely want logging that is flexible, structured, context-aware, and has minimal overhead.
In this article, we’ll discuss how to build a Logging Interceptor with Context Awareness—an interceptor that can log intelligently, read the context (for example, user ID, request ID, trace source), and scale without flooding your logs with noise.
TL;DR
- A logging interceptor is a cross-cutting concern solution for recording requests/responses globally.
- Context keeps logs informative: user, endpoint, trace ID, and more can automatically flow into the logs.
- The simulation is built in Go, but the concepts apply to many languages (Java Spring, .NET Middleware, etc.).
- Includes code examples, a comparison table, and a flow diagram.
What Is a Logging Interceptor?
An interceptor (sometimes called middleware) is a mechanism that lets us do something before or after a request/goroutine is executed—without having to tinker with the code in every handler. It’s a great fit for logging, validation, rate limiting, and so on.
This pattern is common in modern frameworks:
- Go:
http.Handler,grpc.UnaryInterceptor - Java Spring:
HandlerInterceptor,Filter - .NET:
Middleware
Why Use Context?
One of the challenges of traditional logging:
- Context gets lost… For example, “Internal Server Error on endpoint GET /order/123 by user X”—if the log is global, the user isn’t visible.
- It’s hard to connect traces across microservices/modules.
- It’s difficult to debug specific cases, because grouped logs are poorly organized.
With Context, we can:
- Inject a trace ID/request ID.
- Add user ID/session info.
- Attach metadata such as elapsed time.
- Pass this context throughout every handler/layer.
Case Study: Logging Interceptor on an HTTP API (Golang)
Let’s simulate this with Go. However, you can apply these principles in other frameworks.
1. Context-Ready Logging Utility
1package logger
2
3import (
4 "context"
5 "log"
6)
7
8type contextKey string
9
10const (
11 TraceIDKey contextKey = "trace_id"
12 UserIDKey contextKey = "user_id"
13)
14
15func WithTraceID(ctx context.Context, traceID string) context.Context {
16 return context.WithValue(ctx, TraceIDKey, traceID)
17}
18
19func WithUserID(ctx context.Context, userID string) context.Context {
20 return context.WithValue(ctx, UserIDKey, userID)
21}
22
23func LogWithContext(ctx context.Context, msg string, args ...interface{}) {
24 traceID, _ := ctx.Value(TraceIDKey).(string)
25 userID, _ := ctx.Value(UserIDKey).(string)
26 log.Printf("[TraceID:%s][UserID:%s] %s", traceID, userID, fmt.Sprintf(msg, args...))
27}2. Middleware: Logging Interceptor
1package middleware
2
3import (
4 "net/http"
5 "github.com/yourmodule/logger"
6 "github.com/google/uuid"
7)
8
9func LoggingInterceptor(next http.Handler) http.Handler {
10 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11 traceID := uuid.New().String()
12 // Suppose we get the userID from a header
13 userID := r.Header.Get("X-User-ID")
14
15 ctx := logger.WithTraceID(r.Context(), traceID)
16 if userID != "" {
17 ctx = logger.WithUserID(ctx, userID)
18 }
19
20 logger.LogWithContext(ctx, "Incoming request %s %s", r.Method, r.URL.Path)
21 next.ServeHTTP(w, r.WithContext(ctx))
22 logger.LogWithContext(ctx, "Completed request %s %s", r.Method, r.URL.Path)
23 })
24}3. Usage in Main
1mux := http.NewServeMux()
2mux.HandleFunc("/hello", handlerWithLog)
3
4logged := middleware.LoggingInterceptor(mux)
5http.ListenAndServe(":8080", logged)4. Downstream Handlers Can Stay Context-Aware!
1func handlerWithLog(w http.ResponseWriter, r *http.Request) {
2 ctx := r.Context()
3 logger.LogWithContext(ctx, "Main process running...")
4 // ... other code
5 fmt.Fprintln(w, "Hello World")
6}Simulating the Log Output
Let’s look at a simulation of our logging output:
| Time | TraceID | UserID | Event | Info |
|---|---|---|---|---|
| 13:01:02 | abc-123 | 999 | Incoming request | GET /hello |
| 13:01:02 | abc-123 | 999 | Main process | … |
| 13:01:02 | abc-123 | 999 | Completed request | GET /hello |
With context, every log entry related to a single request will share the same TraceID and UserID.
Flow Diagram: Logging Interceptor
sequenceDiagram participant Client participant Interceptor participant Handler Client->>Interceptor: Kirim HTTP Request Interceptor->>Interceptor: Buat TraceID & baca UserID Interceptor->>Interceptor: Inject ke Context Interceptor->>Interceptor: Log "incoming request" Interceptor->>Handler: Lanjutkan ke Handler (dengan Context) Handler->>Interceptor: Selesai Handle Interceptor->>Interceptor: Log "completed request" Interceptor->>Client: Response
Benefits & Challenges of a Context-Aware Logging Interceptor
| Benefits | Challenges |
|---|---|
| No need to copy-paste logging in every handler | Propagating context into background routines |
| Ready to scale out, with automatic tracing | Potential issues if context is propagated incorrectly |
| Easy to debug a single request end-to-end | Small overhead if logging is very verbose |
| Extensible (user agent, IP address, etc.) | Requires team-wide understanding of context |
Best Practices
- Log only what you need. Don’t log the entire payload if it isn’t necessary.
- Mind privacy. Don’t write sensitive info to the logs.
- Inject into the context layer as early as possible—ideally at the gateway/middleware.
- Favor JSON-structured logs for easier parsing by ELK/Splunk.
Conclusion
Applying a Logging Interceptor with Context is a simple yet vital step toward getting logs that are meaningful, structured, and traceable. With this pattern, tracing problems becomes far more productive—no more “mysteries in the server log”—and monitoring microservices becomes scalable.
This pattern is compatible with nearly every modern language and framework. So, as an engineer who cares about observability and efficient debugging? Go refactor your logger to be context-aware right away!
Want to discuss further? Drop your questions in the comments section!