Documentation
¶
Overview ¶
Package traceid solves the distributed-tracing correlation problem at the service boundary: capturing a request-scoped trace ID from an inbound HTTP header, propagating it through the context.Context for the lifetime of the request, and writing it back into outbound HTTP headers when calling downstream services — all without coupling business logic to any particular tracing framework.
Problem ¶
In distributed systems every request needs a stable identifier that survives hops across services so that logs, metrics, and traces can be correlated after the fact. The naive approach — passing the ID as an explicit function parameter — pollutes every call site. Storing it ad-hoc under an untyped string key in the context risks collisions with other packages. And extracting the ID from an HTTP header requires consistent validation to prevent header-injection attacks. This package encapsulates all three concerns in a handful of small, focused functions.
How It Works ¶
The package uses an unexported struct type (ctxKey) as the context key, guaranteeing no collision with any other package's context values.
- NewContext stores the trace ID in the context. If an ID is already present the context is returned unchanged, making it safe to call at every layer without overwriting an upstream-supplied ID.
- ForceContext stores the trace ID unconditionally, overwriting any ID already present; use it when the authoritative ID has just been determined and the context must agree with what is propagated downstream.
- FromContext retrieves the stored ID, falling back to a caller-supplied default (typically DefaultValue) when none is set.
- FromHTTPRequestHeader extracts the trace ID from an incoming HTTP request header and validates it with Valid (1 to MaxIDLen characters from the set [0-9A-Za-z._-]). Any value that does not match — including an empty or absent header — is replaced with the caller-supplied default, preventing header-injection.
- SetHTTPRequestHeaderFromContext reads the ID from the context and writes it onto an outgoing *http.Request header, completing the propagation loop to downstream services.
The context stored under the key is not validated (see NewContext); callers that ingest an ID from an untrusted origin should validate it with Valid, or obtain it through FromHTTPRequestHeader, before storing or logging it.
The package also exports the conventional defaults DefaultHeader ("X-Request-ID"), DefaultValue (""), and DefaultLogKey ("traceid") so all services in a system can share consistent naming without hardcoding strings.
Usage ¶
At the inbound boundary (e.g. an HTTP middleware):
id := traceid.FromHTTPRequestHeader(r, traceid.DefaultHeader, traceid.DefaultValue) ctx := traceid.NewContext(r.Context(), id) // pass ctx to all downstream handlers
Anywhere in the call chain — logging, metrics, business logic:
id := traceid.FromContext(ctx, traceid.DefaultValue)
logger.With(traceid.DefaultLogKey, id).Info("processing request")
At an outbound boundary (e.g. before calling a downstream service):
traceid.SetHTTPRequestHeaderFromContext(ctx, outboundReq, traceid.DefaultHeader, traceid.DefaultValue)
This package is ideal for any Go HTTP service that participates in a distributed system and needs lightweight, framework-agnostic trace ID propagation.
Index ¶
- Constants
- func ForceContext(ctx context.Context, id string) context.Context
- func FromContext(ctx context.Context, defaultValue string) string
- func FromHTTPRequestHeader(r *http.Request, header, defaultValue string) string
- func NewContext(ctx context.Context, id string) context.Context
- func SetHTTPRequestHeaderFromContext(ctx context.Context, r *http.Request, header, defaultValue string) string
- func Valid(id string) bool
Examples ¶
Constants ¶
const ( // DefaultHeader is the default header name for the trace ID. DefaultHeader = "X-Request-ID" // DefaultValue is the default trace ID value. DefaultValue = "" // DefaultLogKey is the default log field key for the Trace ID. DefaultLogKey = "traceid" // MaxIDLen is the maximum number of bytes accepted in a valid trace ID. MaxIDLen = 64 )
Variables ¶
This section is empty.
Functions ¶
func ForceContext ¶
ForceContext stores the trace ID in the context, overwriting any value that may already be present. Unlike NewContext, which preserves an already-stored ID, this guarantees the returned context reflects the supplied ID. It is meant for callers that have just determined the authoritative ID (for example after replacing an invalid stored ID with a freshly generated one) and need the context to agree with what was propagated downstream. An empty ID is not stored and the context is returned unchanged; if the stored ID already equals id the context is returned unchanged to avoid an unnecessary allocation. Like NewContext, the ID is stored verbatim and is not validated.
func FromContext ¶
FromContext retrieves the trace ID from context, returning the defaultValue if not found. The returned value is whatever was stored (see NewContext); it is not re-validated, so pass a value from an untrusted origin through Valid before logging or otherwise trusting it.
Example ¶
package main
import (
"context"
"fmt"
"github.com/tecnickcom/nurago/pkg/traceid"
)
func main() {
// context without set id, should return the default value
id1 := traceid.FromContext(context.Background(), "default-1-206951")
fmt.Println(id1)
// context with set id, should return the existing value
ctx := traceid.NewContext(context.Background(), "default-2-616841")
id2 := traceid.FromContext(ctx, "default-3-67890")
fmt.Println(id2)
}
Output: default-1-206951 default-2-616841
func FromHTTPRequestHeader ¶
FromHTTPRequestHeader extracts the trace ID from the HTTP request header and validates it with Valid; returns defaultValue if the value is invalid, missing, or r is nil.
Example ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/tecnickcom/nurago/pkg/traceid"
)
func main() {
ctx := context.Background()
// header not set should return default
r1, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)
if err != nil {
log.Fatal(err)
}
v1 := traceid.FromHTTPRequestHeader(r1, traceid.DefaultHeader, "default-1-103993")
fmt.Println(v1)
// header set should return actual value
r2, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)
if err != nil {
log.Fatal(err)
}
r2.Header.Add(traceid.DefaultHeader, "test-1-413579")
v2 := traceid.FromHTTPRequestHeader(r2, traceid.DefaultHeader, "default-2-968041")
fmt.Println(v2)
}
Output: default-1-103993 test-1-413579
func NewContext ¶
NewContext stores the trace ID value in the context if not already present, making it safe to call at multiple layers. An empty ID is not stored, so that a later real ID is not shadowed by an earlier empty one. The ID is stored verbatim and is not validated; validate untrusted input with Valid (or obtain it via FromHTTPRequestHeader) before storing it, because callers may log the value later returned by FromContext.
Example ¶
package main
import (
"context"
"fmt"
"github.com/tecnickcom/nurago/pkg/traceid"
)
func main() {
// store value in context
ctx := traceid.NewContext(context.Background(), "test-1-218549")
// load the value from context and ignore default
el1 := traceid.FromContext(ctx, "default-104173")
fmt.Println(el1)
// do not override the value in context
ctx1 := traceid.NewContext(ctx, "test-2-563011")
fmt.Println(ctx1)
}
Output: test-1-218549 context.Background.WithValue(traceid.ctxKey, test-1-218549)
func SetHTTPRequestHeaderFromContext ¶
func SetHTTPRequestHeaderFromContext(ctx context.Context, r *http.Request, header, defaultValue string) string
SetHTTPRequestHeaderFromContext retrieves the trace ID from context and sets it onto the HTTP request header, validating it first; returns the ID that was set. The outbound ID is validated with Valid to prevent header injection; any value that does not match is replaced with defaultValue before being written. The defaultValue is subject to the same validation: when the final value is empty or invalid no header is written (so no empty or unvalidated header is transmitted) and an empty string is returned; any value already present on the request header is left unchanged. If r is nil the function is a no-op and returns an empty string.
Example ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/tecnickcom/nurago/pkg/traceid"
)
func main() {
ctx := context.Background()
// header not set
r1, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)
if err != nil {
log.Fatal(err)
}
id1 := traceid.SetHTTPRequestHeaderFromContext(context.Background(), r1, traceid.DefaultHeader, traceid.DefaultValue)
fmt.Println(id1)
fmt.Println(r1.Header.Get(traceid.DefaultHeader))
// header set
r2, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)
if err != nil {
log.Fatal(err)
}
ctx = traceid.NewContext(ctx, "test-904117")
r2 = r2.WithContext(ctx)
id2 := traceid.SetHTTPRequestHeaderFromContext(ctx, r2, traceid.DefaultHeader, traceid.DefaultValue)
fmt.Println(id2)
fmt.Println(r2.Header.Get(traceid.DefaultHeader))
}
Output: test-904117 test-904117
func Valid ¶
Valid reports whether id is a well-formed trace ID: between 1 and MaxIDLen bytes drawn exclusively from the set [0-9A-Za-z._-]. The set deliberately excludes CR, LF, spaces and ':' so that a validated ID is always safe to write into an HTTP header value or a structured log field without further escaping.
Example ¶
package main
import (
"fmt"
"github.com/tecnickcom/nurago/pkg/traceid"
)
func main() {
// a well-formed trace ID (1 to MaxIDLen chars from [0-9A-Za-z._-])
fmt.Println(traceid.Valid("0191b2f1-8f3a-7c2d-9e4b-1a2b3c4d5e6f"))
// an id carrying control characters is rejected, preventing header/log injection
fmt.Println(traceid.Valid("evil\r\nX-Injected: 1"))
}
Output: true false
Types ¶
This section is empty.