diff --git a/CHANGELOG.md b/CHANGELOG.md index 99dac5dea0e..53d4bdf5555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ## Unreleased +* feat(b3-propagator)!: implement case-insensitive carrier handling [#5859](https://github.com/open-telemetry/opentelemetry-js/pull/5859) @YangJonghun + * feat(instrumentation-http): Added support for redacting specific url query string values and url credentials in instrumentations [#5743](https://github.com/open-telemetry/opentelemetry-js/pull/5743) @rads-1996 ### :boom: Breaking Changes diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts index 5a49397f104..bcd9939362a 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts @@ -303,51 +303,43 @@ describe('fetch', () => { exportedSpans = []; }); - const assertPropagationHeaders = async ( - response: Response - ): Promise> => { - const { request } = await response.json(); - + const assertPropagationHeaders = (response: Response): Headers => { const span: tracing.ReadableSpan = exportedSpans[0]; assert.strictEqual( - request.headers[X_B3_TRACE_ID], + response.headers.get(X_B3_TRACE_ID), span.spanContext().traceId, `trace header '${X_B3_TRACE_ID}' not set` ); assert.strictEqual( - request.headers[X_B3_SPAN_ID], + response.headers.get(X_B3_SPAN_ID), span.spanContext().spanId, `trace header '${X_B3_SPAN_ID}' not set` ); assert.strictEqual( - request.headers[X_B3_SAMPLED], + response.headers.get(X_B3_SAMPLED), String(span.spanContext().traceFlags), `trace header '${X_B3_SAMPLED}' not set` ); - return request.headers; + return response.headers; }; - const assertNoPropagationHeaders = async ( - response: Response - ): Promise> => { - const { request } = await response.json(); - + const assertNoPropagationHeaders = (response: Response): Headers => { assert.ok( - !(X_B3_TRACE_ID in request.headers), + !response.headers.has(X_B3_TRACE_ID), `trace header '${X_B3_TRACE_ID}' should not be set` ); assert.ok( - !(X_B3_SPAN_ID in request.headers), + !response.headers.has(X_B3_SPAN_ID), `trace header '${X_B3_SPAN_ID}' should not be set` ); assert.ok( - !(X_B3_SAMPLED in request.headers), + !response.headers.has(X_B3_SAMPLED), `trace header '${X_B3_SAMPLED}' should not be set` ); - return request.headers; + return response.headers; }; describe('same origin requests', () => { @@ -357,11 +349,10 @@ describe('fetch', () => { return msw.HttpResponse.json({ ok: true }); }), msw.http.get('/api/echo-headers.json', ({ request }) => { - return msw.HttpResponse.json({ - request: { - headers: Object.fromEntries(request.headers), - }, - }); + return msw.HttpResponse.json( + { ok: true }, + { headers: request.headers } + ); }), msw.http.get('/no-such-path', () => { return new msw.HttpResponse(null, { status: 404 }); @@ -718,7 +709,7 @@ describe('fetch', () => { callback: () => fetch('/api/echo-headers.json'), }); - await assertPropagationHeaders(response); + assertPropagationHeaders(response); }); it('should set trace propagation headers with a request object', async () => { @@ -726,7 +717,7 @@ describe('fetch', () => { callback: () => fetch(new Request('/api/echo-headers.json')), }); - await assertPropagationHeaders(response); + assertPropagationHeaders(response); }); it('should keep custom headers with a request object and a headers object', async () => { @@ -739,9 +730,9 @@ describe('fetch', () => { ), }); - const headers = await assertPropagationHeaders(response); + const headers = assertPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); it('should keep custom headers with url, untyped request object and typed (Headers) headers object', async () => { @@ -752,9 +743,9 @@ describe('fetch', () => { }), }); - const headers = await assertPropagationHeaders(response); + const headers = assertPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); it('should keep custom headers with url, untyped request object and untyped headers object', async () => { @@ -765,9 +756,9 @@ describe('fetch', () => { }), }); - const headers = await assertPropagationHeaders(response); + const headers = assertPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); it('should keep custom headers with url, untyped request object and typed (Map) headers object', async () => { @@ -779,9 +770,9 @@ describe('fetch', () => { }), }); - const headers = await assertPropagationHeaders(response); + const headers = assertPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); }); @@ -791,7 +782,7 @@ describe('fetch', () => { callback: () => fetch('/api/echo-headers.json'), }); - await assertNoPropagationHeaders(response); + assertNoPropagationHeaders(response); }); it('should not set trace propagation headers with a request object', async () => { @@ -799,7 +790,7 @@ describe('fetch', () => { callback: () => fetch(new Request('/api/echo-headers.json')), }); - await assertNoPropagationHeaders(response); + assertNoPropagationHeaders(response); }); it('should keep custom headers with a request object and a headers object', async () => { @@ -814,7 +805,7 @@ describe('fetch', () => { const headers = await assertNoPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); it('should keep custom headers with url, untyped request object and typed (Headers) headers object', async () => { @@ -827,7 +818,7 @@ describe('fetch', () => { const headers = await assertNoPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); it('should keep custom headers with url, untyped request object and untyped headers object', async () => { @@ -840,7 +831,7 @@ describe('fetch', () => { const headers = await assertNoPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); it('should keep custom headers with url, untyped request object and typed (Map) headers object', async () => { @@ -854,7 +845,7 @@ describe('fetch', () => { const headers = await assertNoPropagationHeaders(response); - assert.strictEqual(headers['foo'], 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); }); }); }); @@ -919,11 +910,10 @@ describe('fetch', () => { msw.http.get( 'http://example.com/api/echo-headers.json', ({ request }) => { - return msw.HttpResponse.json({ - request: { - headers: Object.fromEntries(request.headers), - }, - }); + return msw.HttpResponse.json( + { ok: true }, + { headers: request.headers } + ); } ), ], @@ -1161,7 +1151,7 @@ describe('fetch', () => { }, }); - await assertPropagationHeaders(response); + assertPropagationHeaders(response); assertNoDebugMessages(); }); diff --git a/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts b/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts index f20b0d75514..ff98708ccd7 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts @@ -66,11 +66,11 @@ describe('getPropagatorFromEnv', function () { 'tracestate', 'baggage', 'b3', - 'x-b3-traceid', - 'x-b3-spanid', - 'x-b3-flags', - 'x-b3-sampled', - 'x-b3-parentspanid', + 'X-B3-TraceId', + 'X-B3-SpanId', + 'X-B3-Flags', + 'X-B3-Sampled', + 'X-B3-ParentSpanId', 'uber-trace-id', ]); }); diff --git a/packages/opentelemetry-propagator-b3/src/B3MultiPropagator.ts b/packages/opentelemetry-propagator-b3/src/B3MultiPropagator.ts index 63189e1578c..2be2eb42bde 100644 --- a/packages/opentelemetry-propagator-b3/src/B3MultiPropagator.ts +++ b/packages/opentelemetry-propagator-b3/src/B3MultiPropagator.ts @@ -47,7 +47,8 @@ function parseHeader(header: unknown) { } function getHeaderValue(carrier: unknown, getter: TextMapGetter, key: string) { - const header = getter.get(carrier, key); + const header = + getter.get(carrier, key) ?? getter.get(carrier, key.toLowerCase()); return parseHeader(header); } diff --git a/packages/opentelemetry-propagator-b3/src/constants.ts b/packages/opentelemetry-propagator-b3/src/constants.ts index cd60c865cfa..dcecbd34178 100644 --- a/packages/opentelemetry-propagator-b3/src/constants.ts +++ b/packages/opentelemetry-propagator-b3/src/constants.ts @@ -18,8 +18,8 @@ export const B3_CONTEXT_HEADER = 'b3'; /* b3 multi-header keys */ -export const X_B3_TRACE_ID = 'x-b3-traceid'; -export const X_B3_SPAN_ID = 'x-b3-spanid'; -export const X_B3_SAMPLED = 'x-b3-sampled'; -export const X_B3_PARENT_SPAN_ID = 'x-b3-parentspanid'; -export const X_B3_FLAGS = 'x-b3-flags'; +export const X_B3_TRACE_ID = 'X-B3-TraceId'; +export const X_B3_SPAN_ID = 'X-B3-SpanId'; +export const X_B3_SAMPLED = 'X-B3-Sampled'; +export const X_B3_PARENT_SPAN_ID = 'X-B3-ParentSpanId'; +export const X_B3_FLAGS = 'X-B3-Flags'; diff --git a/packages/opentelemetry-propagator-b3/test/B3MultiPropagator.test.ts b/packages/opentelemetry-propagator-b3/test/B3MultiPropagator.test.ts index a314385b57b..e157aab760b 100644 --- a/packages/opentelemetry-propagator-b3/test/B3MultiPropagator.test.ts +++ b/packages/opentelemetry-propagator-b3/test/B3MultiPropagator.test.ts @@ -155,6 +155,30 @@ describe('B3MultiPropagator', () => { assert.strictEqual(carrier[X_B3_FLAGS], undefined); assert.strictEqual(carrier[X_B3_PARENT_SPAN_ID], undefined); }); + + it('should inject headers with proper case format', () => { + const spanContext: SpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + b3Propagator.inject( + trace.setSpanContext(ROOT_CONTEXT, spanContext), + carrier, + defaultTextMapSetter + ); + + // Verify headers are injected with standardized case format + assert.strictEqual( + carrier['X-B3-TraceId'], + 'd4cda95b652f4a1592b449d5929fda1b' + ); + assert.strictEqual(carrier['X-B3-SpanId'], '6e0c63257de34c92'); + assert.strictEqual(carrier['X-B3-Sampled'], '1'); + assert.strictEqual(carrier['x-b3-traceid'], undefined); + assert.strictEqual(carrier['x-b3-spanid'], undefined); + }); }); describe('.extract()', () => { @@ -522,5 +546,82 @@ describe('B3MultiPropagator', () => { }); assert.equal(context.getValue(B3_DEBUG_FLAG_KEY), undefined); }); + + describe('case insensitive header extraction', () => { + it('should extract context from lowercase headers', () => { + carrier['x-b3-traceid'] = '0af7651916cd43dd8448eb211c80319c'; + carrier['x-b3-spanid'] = 'b7ad6b7169203331'; + carrier['x-b3-sampled'] = '1'; + const context = b3Propagator.extract( + ROOT_CONTEXT, + carrier, + defaultTextMapGetter + ); + const extractedSpanContext = trace.getSpanContext(context); + assert.deepStrictEqual(extractedSpanContext, { + spanId: 'b7ad6b7169203331', + traceId: '0af7651916cd43dd8448eb211c80319c', + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }); + }); + + it('should extract context from mixed case headers', () => { + carrier['X-B3-TraceId'] = '0af7651916cd43dd8448eb211c80319c'; + carrier['x-b3-spanid'] = 'b7ad6b7169203331'; + carrier['X-b3-Sampled'] = '0'; + const context = b3Propagator.extract( + ROOT_CONTEXT, + carrier, + defaultTextMapGetter + ); + const extractedSpanContext = trace.getSpanContext(context); + assert.deepStrictEqual(extractedSpanContext, { + spanId: 'b7ad6b7169203331', + traceId: '0af7651916cd43dd8448eb211c80319c', + isRemote: true, + traceFlags: TraceFlags.NONE, + }); + }); + + it('should prioritize standard case over lowercase when both exist', () => { + carrier[X_B3_TRACE_ID] = '0af7651916cd43dd8448eb211c80319c'; + carrier['x-b3-traceid'] = 'ffffffffffffffffffffffffffffffff'; + carrier[X_B3_SPAN_ID] = 'b7ad6b7169203331'; + carrier[X_B3_SAMPLED] = '1'; + const context = b3Propagator.extract( + ROOT_CONTEXT, + carrier, + defaultTextMapGetter + ); + const extractedSpanContext = trace.getSpanContext(context); + // Should use the standard case header value, not lowercase + assert.deepStrictEqual(extractedSpanContext, { + spanId: 'b7ad6b7169203331', + traceId: '0af7651916cd43dd8448eb211c80319c', + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }); + }); + + it('should extract context from lowercase debug flag', () => { + carrier['x-b3-traceid'] = '0af7651916cd43dd8448eb211c80319c'; + carrier['x-b3-spanid'] = 'b7ad6b7169203331'; + carrier['x-b3-flags'] = '1'; + const context = b3Propagator.extract( + ROOT_CONTEXT, + carrier, + defaultTextMapGetter + ); + const extractedSpanContext = trace.getSpanContext(context); + assert.deepStrictEqual(extractedSpanContext, { + spanId: 'b7ad6b7169203331', + traceId: '0af7651916cd43dd8448eb211c80319c', + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }); + assert.strictEqual(context.getValue(B3_DEBUG_FLAG_KEY), '1'); + }); + }); }); });