Skip to content

Commit d07df8e

Browse files
committed
add request ID injection to context to enable tracking reqeusts across downstream services
Signed-off-by: Erlan Zholdubai uulu <[email protected]>
1 parent 9861229 commit d07df8e

23 files changed

+438
-192
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
* [ENHANCEMENT] Querier: Support query limits in parquet queryable. #6870
6060
* [ENHANCEMENT] Ring: Add zone label to ring_members metric. #6900
6161
* [ENHANCEMENT] Ingester: Add new metric `cortex_ingester_push_errors_total` to track reasons for ingester request failures. #6901
62+
* [ENHANCEMENT] API: add request ID injection to context to enable tracking reqeusts across downstream services. #6895
6263
* [BUGFIX] Ingester: Avoid error or early throttling when READONLY ingesters are present in the ring #6517
6364
* [BUGFIX] Ingester: Fix labelset data race condition. #6573
6465
* [BUGFIX] Compactor: Cleaner should not put deletion marker for blocks with no-compact marker. #6576

docs/configuration/config-file-reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ api:
102102
# CLI flag: -api.http-request-headers-to-log
103103
[http_request_headers_to_log: <list of string> | default = []]
104104

105+
# HTTP header that can be used as request id
106+
# CLI flag: -api.request-id-header
107+
[request_id_header: <string> | default = ""]
108+
105109
# Regex for CORS origin. It is fully anchored. Example:
106110
# 'https?://(domain1|domain2)\.com'
107111
# CLI flag: -server.cors-origin

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ require (
7979
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3
8080
github.com/cespare/xxhash/v2 v2.3.0
8181
github.com/google/go-cmp v0.7.0
82+
github.com/google/uuid v1.6.0
8283
github.com/hashicorp/golang-lru/v2 v2.0.7
8384
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
8485
github.com/oklog/ulid/v2 v2.1.1
@@ -170,7 +171,6 @@ require (
170171
github.com/google/btree v1.1.3 // indirect
171172
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a // indirect
172173
github.com/google/s2a-go v0.1.9 // indirect
173-
github.com/google/uuid v1.6.0 // indirect
174174
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
175175
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
176176
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect

pkg/api/api.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ type Config struct {
7171
// Allows and is used to configure the addition of HTTP Header fields to logs
7272
HTTPRequestHeadersToLog flagext.StringSlice `yaml:"http_request_headers_to_log"`
7373

74+
// HTTP header that can be used as request id. It will always be included in logs
75+
// If it's not provided, or this header is empty, then random requestId will be generated
76+
RequestIdHeader string `yaml:"request_id_header"`
77+
7478
// This sets the Origin header value
7579
corsRegexString string `yaml:"cors_origin"`
7680

@@ -87,6 +91,7 @@ var (
8791
func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
8892
f.BoolVar(&cfg.ResponseCompression, "api.response-compression-enabled", false, "Use GZIP compression for API responses. Some endpoints serve large YAML or JSON blobs which can benefit from compression.")
8993
f.Var(&cfg.HTTPRequestHeadersToLog, "api.http-request-headers-to-log", "Which HTTP Request headers to add to logs")
94+
f.StringVar(&cfg.RequestIdHeader, "api.request-id-header", "", "HTTP header that can be used as request id")
9095
f.BoolVar(&cfg.buildInfoEnabled, "api.build-info-enabled", false, "If enabled, build Info API will be served by query frontend or querier.")
9196
f.StringVar(&cfg.QuerierDefaultCodec, "api.querier-default-codec", "json", "Choose default codec for querier response serialization. Supports 'json' and 'protobuf'.")
9297
cfg.RegisterFlagsWithPrefix("", f)
@@ -169,8 +174,9 @@ func New(cfg Config, serverCfg server.Config, s *server.Server, logger log.Logge
169174
if cfg.HTTPAuthMiddleware == nil {
170175
api.AuthMiddleware = middleware.AuthenticateUser
171176
}
172-
if len(cfg.HTTPRequestHeadersToLog) > 0 {
173-
api.HTTPHeaderMiddleware = &HTTPHeaderMiddleware{TargetHeaders: cfg.HTTPRequestHeadersToLog}
177+
api.HTTPHeaderMiddleware = &HTTPHeaderMiddleware{
178+
TargetHeaders: cfg.HTTPRequestHeadersToLog,
179+
RequestIdHeader: cfg.RequestIdHeader,
174180
}
175181

176182
return api, nil

pkg/api/api_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func TestNewApiWithHeaderLogging(t *testing.T) {
8989

9090
}
9191

92+
// HTTPHeaderMiddleware should be added even if no headers are specified to log because it also handles request ID injection.
9293
func TestNewApiWithoutHeaderLogging(t *testing.T) {
9394
cfg := Config{
9495
HTTPRequestHeadersToLog: []string{},
@@ -102,7 +103,8 @@ func TestNewApiWithoutHeaderLogging(t *testing.T) {
102103

103104
api, err := New(cfg, serverCfg, server, &FakeLogger{})
104105
require.NoError(t, err)
105-
require.Nil(t, api.HTTPHeaderMiddleware)
106+
require.NotNil(t, api.HTTPHeaderMiddleware)
107+
require.Empty(t, api.HTTPHeaderMiddleware.TargetHeaders)
106108

107109
}
108110

pkg/api/middlewares.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,51 @@
11
package api
22

33
import (
4-
"context"
54
"net/http"
65

7-
util_log "github.com/cortexproject/cortex/pkg/util/log"
6+
"github.com/google/uuid"
7+
8+
"github.com/cortexproject/cortex/pkg/util/requestmeta"
89
)
910

1011
// HTTPHeaderMiddleware adds specified HTTPHeaders to the request context
1112
type HTTPHeaderMiddleware struct {
12-
TargetHeaders []string
13+
TargetHeaders []string
14+
RequestIdHeader string
1315
}
1416

15-
// InjectTargetHeadersIntoHTTPRequest injects specified HTTPHeaders into the request context
16-
func (h HTTPHeaderMiddleware) InjectTargetHeadersIntoHTTPRequest(r *http.Request) context.Context {
17-
headerMap := make(map[string]string)
17+
// injectRequestContext injects request related metadata into the request context
18+
func (h HTTPHeaderMiddleware) injectRequestContext(r *http.Request) *http.Request {
19+
requestContextMap := make(map[string]string)
1820

19-
// Check to make sure that Headers have not already been injected
20-
checkMapInContext := util_log.HeaderMapFromContext(r.Context())
21+
// Check to make sure that request context have not already been injected
22+
checkMapInContext := requestmeta.MapFromContext(r.Context())
2123
if checkMapInContext != nil {
22-
return r.Context()
24+
return r
2325
}
2426

2527
for _, target := range h.TargetHeaders {
2628
contents := r.Header.Get(target)
2729
if contents != "" {
28-
headerMap[target] = contents
30+
requestContextMap[target] = contents
2931
}
3032
}
31-
return util_log.ContextWithHeaderMap(r.Context(), headerMap)
33+
requestContextMap[requestmeta.LoggingHeadersKey] = requestmeta.LoggingHeaderKeysToString(h.TargetHeaders)
34+
35+
reqId := r.Header.Get(h.RequestIdHeader)
36+
if reqId == "" {
37+
reqId = uuid.NewString()
38+
}
39+
requestContextMap[requestmeta.RequestIdKey] = reqId
40+
41+
ctx := requestmeta.ContextWithRequestMetadataMap(r.Context(), requestContextMap)
42+
return r.WithContext(ctx)
3243
}
3344

3445
// Wrap implements Middleware
3546
func (h HTTPHeaderMiddleware) Wrap(next http.Handler) http.Handler {
3647
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37-
ctx := h.InjectTargetHeadersIntoHTTPRequest(r)
38-
next.ServeHTTP(w, r.WithContext(ctx))
48+
r = h.injectRequestContext(r)
49+
next.ServeHTTP(w, r)
3950
})
4051
}

pkg/api/middlewares_test.go

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import (
77

88
"github.com/stretchr/testify/require"
99

10-
util_log "github.com/cortexproject/cortex/pkg/util/log"
10+
"github.com/cortexproject/cortex/pkg/util/requestmeta"
1111
)
1212

13-
var HTTPTestMiddleware = HTTPHeaderMiddleware{TargetHeaders: []string{"TestHeader1", "TestHeader2", "Test3"}}
14-
1513
func TestHeaderInjection(t *testing.T) {
14+
middleware := HTTPHeaderMiddleware{TargetHeaders: []string{"TestHeader1", "TestHeader2", "Test3"}}
1615
ctx := context.Background()
1716
h := http.Header{}
1817
contentsMap := make(map[string]string)
@@ -32,12 +31,12 @@ func TestHeaderInjection(t *testing.T) {
3231
}
3332

3433
req = req.WithContext(ctx)
35-
ctx = HTTPTestMiddleware.InjectTargetHeadersIntoHTTPRequest(req)
34+
req = middleware.injectRequestContext(req)
3635

37-
headerMap := util_log.HeaderMapFromContext(ctx)
36+
headerMap := requestmeta.MapFromContext(req.Context())
3837
require.NotNil(t, headerMap)
3938

40-
for _, header := range HTTPTestMiddleware.TargetHeaders {
39+
for _, header := range middleware.TargetHeaders {
4140
require.Equal(t, contentsMap[header], headerMap[header])
4241
}
4342
for header, contents := range contentsMap {
@@ -46,6 +45,7 @@ func TestHeaderInjection(t *testing.T) {
4645
}
4746

4847
func TestExistingHeaderInContextIsNotOverridden(t *testing.T) {
48+
middleware := HTTPHeaderMiddleware{TargetHeaders: []string{"TestHeader1", "TestHeader2", "Test3"}}
4949
ctx := context.Background()
5050

5151
h := http.Header{}
@@ -58,7 +58,7 @@ func TestExistingHeaderInContextIsNotOverridden(t *testing.T) {
5858
h.Add("TestHeader2", "Fail2")
5959
h.Add("Test3", "Fail3")
6060

61-
ctx = util_log.ContextWithHeaderMap(ctx, contentsMap)
61+
ctx = requestmeta.ContextWithRequestMetadataMap(ctx, contentsMap)
6262
req := &http.Request{
6363
Method: "GET",
6464
RequestURI: "/HTTPHeaderTest",
@@ -67,8 +67,77 @@ func TestExistingHeaderInContextIsNotOverridden(t *testing.T) {
6767
}
6868

6969
req = req.WithContext(ctx)
70-
ctx = HTTPTestMiddleware.InjectTargetHeadersIntoHTTPRequest(req)
70+
req = middleware.injectRequestContext(req)
71+
72+
require.Equal(t, contentsMap, requestmeta.MapFromContext(req.Context()))
73+
74+
}
75+
76+
func TestRequestIdInjection(t *testing.T) {
77+
middleware := HTTPHeaderMiddleware{
78+
RequestIdHeader: "X-Request-ID",
79+
}
80+
81+
req := &http.Request{
82+
Method: "GET",
83+
RequestURI: "/test",
84+
Body: http.NoBody,
85+
Header: http.Header{},
86+
}
87+
req = req.WithContext(context.Background())
88+
req = middleware.injectRequestContext(req)
89+
90+
requestID := requestmeta.RequestIdFromContext(req.Context())
91+
require.NotEmpty(t, requestID, "Request ID should be generated if not provided")
92+
}
93+
94+
func TestRequestIdFromHeaderIsUsed(t *testing.T) {
95+
const providedID = "my-test-id-123"
96+
97+
middleware := HTTPHeaderMiddleware{
98+
RequestIdHeader: "X-Request-ID",
99+
}
100+
101+
h := http.Header{}
102+
h.Add("X-Request-ID", providedID)
71103

72-
require.Equal(t, contentsMap, util_log.HeaderMapFromContext(ctx))
104+
req := &http.Request{
105+
Method: "GET",
106+
RequestURI: "/test",
107+
Body: http.NoBody,
108+
Header: h,
109+
}
110+
req = req.WithContext(context.Background())
111+
req = middleware.injectRequestContext(req)
112+
113+
requestID := requestmeta.RequestIdFromContext(req.Context())
114+
require.Equal(t, providedID, requestID, "Request ID from header should be used")
115+
}
116+
117+
func TestTargetHeaderAndRequestIdHeaderOverlap(t *testing.T) {
118+
const headerKey = "X-Request-ID"
119+
const providedID = "overlap-id-456"
120+
121+
middleware := HTTPHeaderMiddleware{
122+
TargetHeaders: []string{headerKey, "Other-Header"},
123+
RequestIdHeader: headerKey,
124+
}
125+
126+
h := http.Header{}
127+
h.Add(headerKey, providedID)
128+
h.Add("Other-Header", "some-value")
129+
130+
req := &http.Request{
131+
Method: "GET",
132+
RequestURI: "/test",
133+
Body: http.NoBody,
134+
Header: h,
135+
}
136+
req = req.WithContext(context.Background())
137+
req = middleware.injectRequestContext(req)
73138

139+
ctxMap := requestmeta.MapFromContext(req.Context())
140+
requestID := requestmeta.RequestIdFromContext(req.Context())
141+
require.Equal(t, providedID, ctxMap[headerKey], "Header value should be correctly stored")
142+
require.Equal(t, providedID, requestID, "Request ID should come from the overlapping header")
74143
}

pkg/cortex/cortex.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,10 +392,8 @@ func (t *Cortex) setupThanosTracing() {
392392
// setupGRPCHeaderForwarding appends a gRPC middleware used to enable the propagation of
393393
// HTTP Headers through child gRPC calls
394394
func (t *Cortex) setupGRPCHeaderForwarding() {
395-
if len(t.Cfg.API.HTTPRequestHeadersToLog) > 0 {
396-
t.Cfg.Server.GRPCMiddleware = append(t.Cfg.Server.GRPCMiddleware, grpcutil.HTTPHeaderPropagationServerInterceptor)
397-
t.Cfg.Server.GRPCStreamMiddleware = append(t.Cfg.Server.GRPCStreamMiddleware, grpcutil.HTTPHeaderPropagationStreamServerInterceptor)
398-
}
395+
t.Cfg.Server.GRPCMiddleware = append(t.Cfg.Server.GRPCMiddleware, grpcutil.HTTPHeaderPropagationServerInterceptor)
396+
t.Cfg.Server.GRPCStreamMiddleware = append(t.Cfg.Server.GRPCStreamMiddleware, grpcutil.HTTPHeaderPropagationStreamServerInterceptor)
399397
}
400398

401399
func (t *Cortex) setupRequestSigning() {

pkg/cortex/modules.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,9 +402,7 @@ func (t *Cortex) initQuerier() (serv services.Service, err error) {
402402
// request context.
403403
internalQuerierRouter = t.API.AuthMiddleware.Wrap(internalQuerierRouter)
404404

405-
if len(t.Cfg.API.HTTPRequestHeadersToLog) > 0 {
406-
internalQuerierRouter = t.API.HTTPHeaderMiddleware.Wrap(internalQuerierRouter)
407-
}
405+
internalQuerierRouter = t.API.HTTPHeaderMiddleware.Wrap(internalQuerierRouter)
408406
}
409407

410408
// If neither frontend address or scheduler address is configured, no worker is needed.

pkg/distributor/distributor.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/cortexproject/cortex/pkg/util/limiter"
4141
util_log "github.com/cortexproject/cortex/pkg/util/log"
4242
util_math "github.com/cortexproject/cortex/pkg/util/math"
43+
"github.com/cortexproject/cortex/pkg/util/requestmeta"
4344
"github.com/cortexproject/cortex/pkg/util/services"
4445
"github.com/cortexproject/cortex/pkg/util/validation"
4546
)
@@ -892,9 +893,9 @@ func (d *Distributor) doBatch(ctx context.Context, req *cortexpb.WriteRequest, s
892893
if sp := opentracing.SpanFromContext(ctx); sp != nil {
893894
localCtx = opentracing.ContextWithSpan(localCtx, sp)
894895
}
895-
// Get any HTTP headers that are supposed to be added to logs and add to localCtx for later use
896-
if headerMap := util_log.HeaderMapFromContext(ctx); headerMap != nil {
897-
localCtx = util_log.ContextWithHeaderMap(localCtx, headerMap)
896+
// Get any HTTP request metadata that are supposed to be added to logs and add to localCtx for later use
897+
if requestContextMap := requestmeta.MapFromContext(ctx); requestContextMap != nil {
898+
localCtx = requestmeta.ContextWithRequestMetadataMap(localCtx, requestContextMap)
898899
}
899900
// Get clientIP(s) from Context and add it to localCtx
900901
source := util.GetSourceIPsFromOutgoingCtx(ctx)

0 commit comments

Comments
 (0)