Why TransactionId, RequestId in Logs

The current log stream mixes messages from all tenants and all concurrent API calls with no way to isolate them. A single log line such as:


{"level":"info","time":"2026-06-17T18:28:39.525+0530","caller":"module_name/file_name.go:34","msg":"function_name() Log message"}
    

Cannot answer:
- Which tenant made this call? When system is multitenant
- Is this from Tenant A's user hitting `/Rest-endpoint1` or Tenant B's user hitting `/Rest-endpoint2`?
- If two requests arrive simultaneously, which log lines belong to which request?

The goal is to add two fields — trid and reqid, to every log line so that Kibana filters like `trid=abc123 AND tenantid=5` or `reqid=xyz789` instantly isolate a specific flow.

Terminology

trid (Transaction Id) / session identifier: Scoped to one login session (from login until JWT expiry or new login). All API calls made with the same JWT share the same `trid`.
reqid (Request identifier): Scoped to one **HTTP request-response cycle**. Every log line produced while handling a single API call shares the same `reqid`
TenantId: The numeric tenant identifier

Transaction Id (1 trid for 1 session)

Session = lifetime of one login
Suppose there is a backend application written for CRM(Customer Relationship Management) system, which issues JWT access tokens when employee logs in using his username, password. JWT would be renewed by access token
Session Lifetime = JWT Token Lifetime. That means whatever operations which can be done using same JWT Token will fall in same session


User opens app
     │ provide username,password
     ▼
POST /login  ──►    Server validates correct username, password
     |              Server generates JWT (access token) + refresh token
     |              JWT contains a new trid (UUID generated at login)
     │
     ▼
GET  /Restendpoint1 viewallleads  ──► Bearer JWT sent in header
GET  /Restendpoint2 viewonlymyleads   ──► Bearer JWT sent in header     ◄─── same trid on all these
POST /Restendpoint3 addlead       ──► Bearer JWT sent in header
     │
     ▼  (JWT expires after JWT_ACCESS_EXPIRY_HOURS, default 24 h)
     │
POST /token/refresh  ──► Old refresh token sent by mobile app to backend
     |               ──► Server issues new JWT, carries forward the same trid
     │
     ▼
...more calls...     ──► still same trid (session continues)
     │
     ▼
User closes app / JWT finally expires / user logs in again
     ──► Next login generates a brand-new trid
      

`trid` is the session identifier. It persists across token refreshes so a full user journey (login → work → refresh → more work → logout) is traceable as one cohesive unit.

Why Session is not "until frontend app exits"?

The backend has no mechanism to detect that the frontend app has been closed.
Trying to detect this would require: WebSocket heartbeats, or An explicit logout API call Neither is reliable.
The JWT expiry model is already the correct session boundary for a stateless API. No extra mechanism is needed.

Request Id (1 per HTTP Request)


trid  ────────────────────────────────────────────────────────────────────────────────────────►  (login session)
              reqid-1       reqid-2          reqid-3        reqid-4
restendpoints [/login  ]   [/viewallleads]  [/AddAlead]   [/token/refresh]

Every REST endpoint hit/API call will create a new request id
- Filter by `trid` → see every log line for one user's entire session.
- Filter by `reqid` → zoom into one specific API call.
- Filter by `tenantid` → see everything for one company.

Where trid and reqid are stored?

What is go Context?
For dumping trid and reqid in the logs, we need to generate these upfront, per HTTP request(reqid) and keep trid (per HTTP session).

Storing Place-1. trid, reqid stored in *http.Request.Context


// Go HTTP Server
main.go
func main() {
  // Initialize logger = *zap.SugaredLogger

  // Define REST endpoints
    loginHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    rest_endpoints.HandleLogin(logger)
  })
  router.Handle("/login").Methods("POST")
  ...... Other REST Endpoints ......

  a := Inject_Trid_Reqid(logger)
  handler := c.Handler(a(router))
  err = http.ListenAndServe("0.0.0.0:8080", handler)
}

func Inject_Trid_Reqid(base *zap.SugaredLogger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			reqid := newID()
			trid := newID()

			enriched := base.With(
				"trid", trid,
				"reqid", reqid,
				"tenantid", 0,
			)

			w.Header().Set("X-Request-Id", reqid)

			ctx := logging.InjectLogger(r.Context(), enriched)  // Store trid, reqid
			ctx = logging.InjectReqid(ctx, reqid)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func InjectLogger(ctx context.Context, l *zap.SugaredLogger) context.Context {
	return context.WithValue(ctx, loggerKey, l)
}
      

Storing Place-2. trid also stored in JWT claims struct

Since trid need to persist across multiple sessions, this need to passed to client, which will again send to server in JWT token and would be printed in logs


type JWTClaim struct {
 	Email                string `json:"email"`     // Email of person having token
 	TenantID             int64  `json:"tenant_id"`
 	Trid                 string `json:"trid"` // Session/transaction ID, generated at login, carried across token refreshes
 	jwt.RegisteredClaims        // Embed standard claims
 }
      

Priting the trid, reqid in Logs from Context

**any call** on `enriched` (or on a logger retrieved via `LoggerFromContext`) automatically emits `trid`, `reqid`



enriched.Info("any message")
// → {"level":"info", ..., "trid":"3f8a1c2e9b047d56", "reqid":"a72e4019cf8b3d1e", "tenantid":0, "msg":"any message"}

// Singleton Pattern
// extract a logger from the context (if present)
// Since we have stored trid, reqid into logger, it will return
func LoggerFromContext(ctx context.Context, fallback *zap.SugaredLogger) *zap.SugaredLogger {
	if l, ok := ctx.Value(loggerKey).(*zap.SugaredLogger); ok && l != nil {
		return l
	}
	return fallback
}


func HandleLogin(db *sql.DB, logger *zap.SugaredLogger, w http.ResponseWriter, r *http.Request) {
	logger = LoggerFromContext(r.Context(), logger)
	logger.Info(utils.GetFunctionName())

}