Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
12 Jul 2025 · 5 min read ·Article 34 / 110
Go

34 Logging Interceptor with Context

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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

go
 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

go
 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

go
1mux := http.NewServeMux()
2mux.HandleFunc("/hello", handlerWithLog)
3
4logged := middleware.LoggingInterceptor(mux)
5http.ListenAndServe(":8080", logged)

4. Downstream Handlers Can Stay Context-Aware!

go
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:

TimeTraceIDUserIDEventInfo
13:01:02abc-123999Incoming requestGET /hello
13:01:02abc-123999Main process
13:01:02abc-123999Completed requestGET /hello

With context, every log entry related to a single request will share the same TraceID and UserID.


Flow Diagram: Logging Interceptor

MERMAID
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

BenefitsChallenges
No need to copy-paste logging in every handlerPropagating context into background routines
Ready to scale out, with automatic tracingPotential issues if context is propagated incorrectly
Easy to debug a single request end-to-endSmall overhead if logging is very verbose
Extensible (user agent, IP address, etc.)Requires team-wide understanding of context

Best Practices

  1. Log only what you need. Don’t log the entire payload if it isn’t necessary.
  2. Mind privacy. Don’t write sensitive info to the logs.
  3. Inject into the context layer as early as possible—ideally at the gateway/middleware.
  4. 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!

Related Articles

💬 Comments