Skip to content

Commit 3e66c66

Browse files
committed
docs(api): clarify TextMapPropagator API requirements
The current interface places the generic paramter on the interface itself. This implies that the implementors of `TextMapPropagator` can specify what carrier types it accepts (and that each implementor only work with one specific type of carrier). ```ts interface TextMapPropagator<Carrier> { inject(context: Context, carrier: Carrier, setter: TextMapSetter<Carrier>): void; extract(context: Context, carrier: Carrier, getter: TextMapGetter<Carrier>): void; } ``` In reality, this is not the case. The propagator API is designed to be called by participating code around the various transport layers (such as the `fetch` inst on the browser, or integration with the HTTP server library on the backend), and it is these callers that ultimately controls what carrier type the currently configured propagator is called with. Therefore, a correct implementation of this interface must treat the carrier as an opaque value, and only work with it using the provided getter/setter. Ideally, the interface should look like this instead: ```ts interface TextMapPropagator { inject<Carrier>(context: Context, carrier: Carrier, setter: TextMapSetter<Carrier>): void; extract<Carrier>(context: Context, carrier: Carrier, getter: TextMapGetter<Carrier>): void; } ``` This communicates and enforces the contract. Unfortunately, that would be a breking change we are not currently prepared to make. Instead, this commit updates the documentation to explicitly document the discrapancy and advice implemntors the correct way forward. It also updates our own implementations to follow the recommended pattern, as well as updating the tests to be more well-behaved around this, as some of them are written to rely on this exact behavior that would be problematic in the real world. Ref open-telemetry#5365 Ref open-telemetry#5368
1 parent f927e82 commit 3e66c66

File tree

16 files changed

+264
-117
lines changed

16 files changed

+264
-117
lines changed

api/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file.
1313

1414
### :books: (Refine Doc)
1515

16+
* docs(api): Clarify `TextMapPropagator` interface implementation requirements around the `Carrier` type [#5370](https://github.com/open-telemetry/opentelemetry-js/pull/5370) @chancancode
17+
1618
### :house: (Internal)
1719

1820
* refactor(api): remove "export *" in favor of explicit named exports [#4880](https://github.com/open-telemetry/opentelemetry-js/pull/4880) @robbkidd

api/src/propagation/TextMapPropagator.ts

+37-10
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,36 @@ import { Context } from '../context/types';
2222
* HTTP Header Field semantics. Values are often encoded as RPC/HTTP request
2323
* headers.
2424
*
25-
* The carrier of propagated data on both the client (injector) and server
26-
* (extractor) side is usually an object such as http headers. Propagation is
27-
* usually implemented via library-specific request interceptors, where the
28-
* client-side injects values and the server-side extracts them.
25+
* Propagation is usually implemented via library-specific request
26+
* interceptors, where the client-side injects values and the server-side
27+
* extracts them.
28+
*
29+
* @template Carrier **It is strongly recommended to set the `Carrier` type
30+
* parameter to `unknown`, as it is the only correct type here.**
31+
*
32+
* The carrier is the medium for communicating propagated data between the
33+
* client (injector) and server (extractor) side, such as HTTP headers. To
34+
* work with `TextMapPropagator`, the carrier type must semantically function
35+
* as an abstract map data structure, supporting string keys and values.
36+
*
37+
* This type parameter exists on the interface for historical reasons. While
38+
* it may be suggest that implementors have a choice over what type of carrier
39+
* medium it accepts, this is not true in practice.
40+
*
41+
* The propagator API is designed to be called by participating code around the
42+
* various transport layers (such as the `fetch` instrumentation on the browser
43+
* client, or integration with the HTTP server library on the backend), and it
44+
* is these callers that ultimately controls what carrier type your propagator
45+
* is called with.
46+
*
47+
* Therefore, a correct implementation of this interface must treat the carrier
48+
* as an opaque value, and only work with it using the provided getter/setter.
2949
*
3050
* @since 1.0.0
3151
*/
52+
// TODO: move this generic parameter into the methods in API 2.0 and remove
53+
// the default to `any`
54+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3255
export interface TextMapPropagator<Carrier = any> {
3356
/**
3457
* Injects values from a given `Context` into a carrier.
@@ -38,10 +61,10 @@ export interface TextMapPropagator<Carrier = any> {
3861
*
3962
* @param context the Context from which to extract values to transmit over
4063
* the wire.
41-
* @param carrier the carrier of propagation fields, such as http request
64+
* @param carrier the carrier of propagation fields, such as HTTP request
4265
* headers.
43-
* @param setter an optional {@link TextMapSetter}. If undefined, values will be
44-
* set by direct object assignment.
66+
* @param setter a {@link TextMapSetter} guaranteed to work with the given
67+
* carrier, implementors must use this to set values on the carrier.
4568
*/
4669
inject(
4770
context: Context,
@@ -56,10 +79,10 @@ export interface TextMapPropagator<Carrier = any> {
5679
*
5780
* @param context the Context from which to extract values to transmit over
5881
* the wire.
59-
* @param carrier the carrier of propagation fields, such as http request
82+
* @param carrier the carrier of propagation fields, such as HTTP request
6083
* headers.
61-
* @param getter an optional {@link TextMapGetter}. If undefined, keys will be all
62-
* own properties, and keys will be accessed by direct object access.
84+
* @param getter a {@link TextMapGetter} guaranteed to work with the given
85+
* carrier, implementors must use this to read values from the carrier.
6386
*/
6487
extract(
6588
context: Context,
@@ -79,6 +102,8 @@ export interface TextMapPropagator<Carrier = any> {
79102
*
80103
* @since 1.0.0
81104
*/
105+
// TODO: remove the default to `any` in API 2.0
106+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
82107
export interface TextMapSetter<Carrier = any> {
83108
/**
84109
* Callback used to set a key/value pair on an object.
@@ -99,6 +124,8 @@ export interface TextMapSetter<Carrier = any> {
99124
*
100125
* @since 1.0.0
101126
*/
127+
// TODO: remove the default to `any` in API 2.0
128+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
102129
export interface TextMapGetter<Carrier = any> {
103130
/**
104131
* Get a list of all keys available on the carrier.

api/test/common/api/api.test.ts

+31-42
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import * as assert from 'assert';
1818
import api, {
1919
context,
2020
Context,
21-
defaultTextMapGetter,
22-
defaultTextMapSetter,
2321
diag,
2422
metrics,
2523
propagation,
@@ -137,31 +135,23 @@ describe('API', () => {
137135
describe('should use the global propagation', () => {
138136
const testKey = Symbol('kTestKey');
139137

140-
interface Carrier {
141-
context?: Context;
142-
setter?: TextMapSetter;
143-
}
138+
type Carrier = Record<string, string | undefined>;
144139

145-
class TestTextMapPropagation implements TextMapPropagator<Carrier> {
140+
class TestTextMapPropagation implements TextMapPropagator<unknown> {
146141
inject(
147142
context: Context,
148-
carrier: Carrier,
149-
setter: TextMapSetter
143+
carrier: unknown,
144+
setter: TextMapSetter<unknown>
150145
): void {
151-
carrier.context = context;
152-
carrier.setter = setter;
146+
setter.set(carrier, 'TestField', String(context.getValue(testKey)));
153147
}
154148

155-
extract(
149+
extract<C>(
156150
context: Context,
157-
carrier: Carrier,
158-
getter: TextMapGetter
151+
carrier: unknown,
152+
getter: TextMapGetter<unknown>
159153
): Context {
160-
return context.setValue(testKey, {
161-
context,
162-
carrier,
163-
getter,
164-
});
154+
return context.setValue(testKey, getter.get(carrier, 'TestField'));
165155
}
166156

167157
fields(): string[] {
@@ -172,41 +162,40 @@ describe('API', () => {
172162
it('inject', () => {
173163
api.propagation.setGlobalPropagator(new TestTextMapPropagation());
174164

175-
const context = ROOT_CONTEXT.setValue(testKey, 15);
165+
const context = ROOT_CONTEXT.setValue(testKey, 'test-value');
176166
const carrier: Carrier = {};
177167
api.propagation.inject(context, carrier);
178-
assert.strictEqual(carrier.context, context);
179-
assert.strictEqual(carrier.setter, defaultTextMapSetter);
168+
assert.strictEqual(carrier['TestField'], 'test-value');
180169

181-
const setter: TextMapSetter = {
182-
set: () => {},
170+
const setter: TextMapSetter<Carrier> = {
171+
set: (carrier, key, value) => {
172+
carrier[key.toLowerCase()] = value.toUpperCase();
173+
},
183174
};
184175
api.propagation.inject(context, carrier, setter);
185-
assert.strictEqual(carrier.context, context);
186-
assert.strictEqual(carrier.setter, setter);
176+
assert.strictEqual(carrier['testfield'], 'TEST-VALUE');
187177
});
188178

189179
it('extract', () => {
190180
api.propagation.setGlobalPropagator(new TestTextMapPropagation());
191181

192-
const carrier: Carrier = {};
193-
let context = api.propagation.extract(ROOT_CONTEXT, carrier);
194-
let data: any = context.getValue(testKey);
195-
assert.ok(data != null);
196-
assert.strictEqual(data.context, ROOT_CONTEXT);
197-
assert.strictEqual(data.carrier, carrier);
198-
assert.strictEqual(data.getter, defaultTextMapGetter);
199-
200-
const getter: TextMapGetter = {
201-
keys: () => [],
202-
get: () => undefined,
182+
let context = api.propagation.extract(ROOT_CONTEXT, {
183+
TestField: 'test-value',
184+
});
185+
let data = context.getValue(testKey);
186+
assert.strictEqual(data, 'test-value');
187+
188+
const getter: TextMapGetter<Carrier> = {
189+
keys: carrier => Object.keys(carrier),
190+
get: (carrier, key) => carrier[key.toLowerCase()],
203191
};
204-
context = api.propagation.extract(ROOT_CONTEXT, carrier, getter);
192+
context = api.propagation.extract(
193+
ROOT_CONTEXT,
194+
{ testfield: 'TEST-VALUE' },
195+
getter
196+
);
205197
data = context.getValue(testKey);
206-
assert.ok(data != null);
207-
assert.strictEqual(data.context, ROOT_CONTEXT);
208-
assert.strictEqual(data.carrier, carrier);
209-
assert.strictEqual(data.getter, getter);
198+
assert.strictEqual(data, 'TEST-VALUE');
210199
});
211200

212201
it('fields', () => {

doc/propagation.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ import { isTracingSuppressed } from '@opentelemetry/core';
3232
// Example header, the content format can be `<trace-id>:<span-id>`
3333
const MyHeader = 'my-header';
3434

35-
export class MyPropagator implements TextMapPropagator {
35+
export class MyPropagator implements TextMapPropagator<unknown> {
3636
// Inject the header to the outgoing request.
37-
inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
37+
inject(
38+
context: Context,
39+
carrier: unknown,
40+
setter: TextMapSetter<unknown>
41+
): void {
3842
const spanContext = trace.getSpanContext(context);
3943
// Skip if the current span context is not valid or suppressed.
4044
if (
@@ -51,7 +55,11 @@ export class MyPropagator implements TextMapPropagator {
5155
}
5256

5357
// Extract the header from the incoming request.
54-
extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
58+
extract(
59+
context: Context,
60+
carrier: unknown,
61+
getter: TextMapGetter<unknown>
62+
): Context {
5563
const headers = getter.get(carrier, MyHeader);
5664
const header = Array.isArray(headers) ? headers[0] : headers;
5765
if (typeof header !== 'string') return context;

experimental/packages/opentelemetry-instrumentation-http/test/utils/DummyPropagation.ts

+35-8
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,60 @@
1515
*/
1616
import {
1717
Context,
18+
TextMapGetter,
1819
TextMapPropagator,
20+
TextMapSetter,
1921
trace,
2022
TraceFlags,
2123
} from '@opentelemetry/api';
22-
import type * as http from 'http';
2324

24-
export class DummyPropagation implements TextMapPropagator {
25+
export class DummyPropagation implements TextMapPropagator<unknown> {
2526
static TRACE_CONTEXT_KEY = 'x-dummy-trace-id';
2627
static SPAN_CONTEXT_KEY = 'x-dummy-span-id';
27-
extract(context: Context, carrier: http.OutgoingHttpHeaders) {
28+
29+
extract(context: Context, carrier: unknown, getter: TextMapGetter<unknown>) {
30+
const traceId = getter.get(carrier, DummyPropagation.TRACE_CONTEXT_KEY);
31+
32+
if (typeof traceId !== 'string') {
33+
throw new Error('expecting traceId to be a string');
34+
}
35+
36+
const spanId = getter.get(carrier, DummyPropagation.SPAN_CONTEXT_KEY);
37+
38+
if (typeof spanId !== 'string') {
39+
throw new Error('expecting spanId to be a string');
40+
}
41+
2842
const extractedSpanContext = {
29-
traceId: carrier[DummyPropagation.TRACE_CONTEXT_KEY] as string,
30-
spanId: carrier[DummyPropagation.SPAN_CONTEXT_KEY] as string,
43+
traceId,
44+
spanId,
3145
traceFlags: TraceFlags.SAMPLED,
3246
isRemote: true,
3347
};
48+
3449
if (extractedSpanContext.traceId && extractedSpanContext.spanId) {
3550
return trace.setSpanContext(context, extractedSpanContext);
3651
}
52+
3753
return context;
3854
}
39-
inject(context: Context, headers: { [custom: string]: string }): void {
55+
56+
inject(
57+
context: Context,
58+
carrier: unknown,
59+
setter: TextMapSetter<unknown>
60+
): void {
4061
const spanContext = trace.getSpanContext(context);
4162
if (!spanContext) return;
42-
headers[DummyPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId;
43-
headers[DummyPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId;
63+
64+
setter.set(
65+
carrier,
66+
DummyPropagation.TRACE_CONTEXT_KEY,
67+
spanContext.traceId
68+
);
69+
setter.set(carrier, DummyPropagation.SPAN_CONTEXT_KEY, spanContext.spanId);
4470
}
71+
4572
fields(): string[] {
4673
return [
4774
DummyPropagation.TRACE_CONTEXT_KEY,

packages/opentelemetry-core/src/baggage/propagation/W3CBaggagePropagator.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@ import { getKeyPairs, parsePairKeyValue, serializeKeyPairs } from '../utils';
3838
* Based on the Baggage specification:
3939
* https://w3c.github.io/baggage/
4040
*/
41-
export class W3CBaggagePropagator implements TextMapPropagator {
42-
inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
41+
export class W3CBaggagePropagator implements TextMapPropagator<unknown> {
42+
inject(
43+
context: Context,
44+
carrier: unknown,
45+
setter: TextMapSetter<unknown>
46+
): void {
4347
const baggage = propagation.getBaggage(context);
4448
if (!baggage || isTracingSuppressed(context)) return;
4549
const keyPairs = getKeyPairs(baggage)
@@ -53,7 +57,11 @@ export class W3CBaggagePropagator implements TextMapPropagator {
5357
}
5458
}
5559

56-
extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
60+
extract(
61+
context: Context,
62+
carrier: unknown,
63+
getter: TextMapGetter<unknown>
64+
): Context {
5765
const headerValue = getter.get(carrier, BAGGAGE_HEADER);
5866
const baggageString = Array.isArray(headerValue)
5967
? headerValue.join(BAGGAGE_ITEMS_SEPARATOR)

packages/opentelemetry-core/src/propagation/composite.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface CompositePropagatorConfig {
3333
}
3434

3535
/** Combines multiple propagators into a single propagator. */
36-
export class CompositePropagator implements TextMapPropagator {
36+
export class CompositePropagator implements TextMapPropagator<unknown> {
3737
private readonly _propagators: TextMapPropagator[];
3838
private readonly _fields: string[];
3939

@@ -64,7 +64,11 @@ export class CompositePropagator implements TextMapPropagator {
6464
* @param context Context to inject
6565
* @param carrier Carrier into which context will be injected
6666
*/
67-
inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
67+
inject(
68+
context: Context,
69+
carrier: unknown,
70+
setter: TextMapSetter<unknown>
71+
): void {
6872
for (const propagator of this._propagators) {
6973
try {
7074
propagator.inject(context, carrier, setter);
@@ -85,7 +89,11 @@ export class CompositePropagator implements TextMapPropagator {
8589
* @param context Context to add values to
8690
* @param carrier Carrier from which to extract context
8791
*/
88-
extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
92+
extract(
93+
context: Context,
94+
carrier: unknown,
95+
getter: TextMapGetter<unknown>
96+
): Context {
8997
return this._propagators.reduce((ctx, propagator) => {
9098
try {
9199
return propagator.extract(ctx, carrier, getter);

packages/opentelemetry-core/src/trace/W3CTraceContextPropagator.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,12 @@ export function parseTraceParent(traceParent: string): SpanContext | null {
7171
* Based on the Trace Context specification:
7272
* https://www.w3.org/TR/trace-context/
7373
*/
74-
export class W3CTraceContextPropagator implements TextMapPropagator {
75-
inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
74+
export class W3CTraceContextPropagator implements TextMapPropagator<unknown> {
75+
inject(
76+
context: Context,
77+
carrier: unknown,
78+
setter: TextMapSetter<unknown>
79+
): void {
7680
const spanContext = trace.getSpanContext(context);
7781
if (
7882
!spanContext ||
@@ -95,7 +99,11 @@ export class W3CTraceContextPropagator implements TextMapPropagator {
9599
}
96100
}
97101

98-
extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
102+
extract(
103+
context: Context,
104+
carrier: unknown,
105+
getter: TextMapGetter<unknown>
106+
): Context {
99107
const traceParentHeader = getter.get(carrier, TRACE_PARENT_HEADER);
100108
if (!traceParentHeader) return context;
101109
const traceParent = Array.isArray(traceParentHeader)

0 commit comments

Comments
 (0)