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())
}