Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.

Commit 28ecb16

Browse files
authored
feat!: support user-specified context header propagation (#1029)
BREAKING CHANGE: This change modifies/removes APIs that assume a particular format for trace context headers; in other words, any place where the user would deal with a stringified trace context, they would now deal with a TraceContext object instead. This affects three APIs: `getResponseTraceContext` (input/output has changed from string to TraceContext), `createRootSpan` (input RootSpanOptions now accepts a TraceContext instead of a string in the traceContext field), and `Span#getTraceContext` (output has changed from string to TraceContext).
1 parent c63bb14 commit 28ecb16

25 files changed

Lines changed: 310 additions & 196 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
},
101101
"dependencies": {
102102
"@google-cloud/common": "^1.0.0",
103+
"@opencensus/propagation-stackdriver": "0.0.11",
103104
"builtin-modules": "^3.0.0",
104105
"console-log-level": "^1.4.0",
105106
"continuation-local-storage": "^3.2.1",

src/config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ export interface TracePolicy {
5252
shouldTrace: (requestDetails: RequestDetails) => boolean;
5353
}
5454

55+
export type GetHeaderFunction = {
56+
getHeader: (key: string) => string[]|string|undefined;
57+
};
58+
export type SetHeaderFunction = {
59+
setHeader: (key: string, value: string) => void;
60+
};
61+
export interface OpenCensusPropagation {
62+
extract: (getHeader: GetHeaderFunction) => {
63+
traceId: string;
64+
spanId: string;
65+
options?: number
66+
} | null;
67+
inject: (setHeader: SetHeaderFunction, traceContext: {
68+
traceId: string; spanId: string;
69+
options?: number
70+
}) => void;
71+
}
72+
5573
/**
5674
* Available configuration options. All fields are optional. See the
5775
* defaultConfig object defined in this file for default assigned values.
@@ -195,6 +213,13 @@ export interface Config {
195213
*/
196214
tracePolicy?: TracePolicy;
197215

216+
/**
217+
* If specified, the Trace Agent will use this context header propagation
218+
* implementation instead of @opencensus/propagation-stackdriver, the default
219+
* trace context header format.
220+
*/
221+
propagation?: OpenCensusPropagation;
222+
198223
/**
199224
* Buffer the captured traces for `flushDelaySeconds` seconds before
200225
* publishing to the Stackdriver Trace API, unless the buffer fills up first.

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ function initConfig(userConfig: Forceable<Config>): Forceable<TopLevelConfig> {
146146
contextHeaderBehavior: mergedConfig.contextHeaderBehavior as
147147
TraceContextHeaderBehavior
148148
},
149-
overrides: {tracePolicy: mergedConfig.tracePolicy}
149+
overrides: {
150+
tracePolicy: mergedConfig.tracePolicy,
151+
propagation: mergedConfig.propagation
152+
}
150153
};
151154
}
152155

src/plugin-types.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,10 @@ export interface TraceAgentExtension {
3838
*/
3939
export interface Span {
4040
/**
41-
* Gets the current trace context serialized as a string, or an empty string
42-
* if it can't be generated.
43-
* @return The stringified trace context.
41+
* Gets the current trace context, or null if it can't be retrieved.
42+
* @return The trace context.
4443
*/
45-
getTraceContext(): string;
44+
getTraceContext(): TraceContext|null;
4645

4746
/**
4847
* Adds a key-value pair as a label to the trace span. The value will be
@@ -106,10 +105,9 @@ export interface RootSpanOptions extends SpanOptions {
106105
/* A Method associated with the root span, if applicable. */
107106
method?: string;
108107
/**
109-
* The serialized form of an object that contains information about an
110-
* existing trace context, if it exists.
108+
* An existing trace context, if it exists.
111109
*/
112-
traceContext?: string|null;
110+
traceContext?: TraceContext|null;
113111
}
114112

115113
export interface Tracer {
@@ -191,21 +189,22 @@ export interface Tracer {
191189
isRealSpan(span: Span): boolean;
192190

193191
/**
194-
* Generates a stringified trace context that should be set as the trace
192+
* Generates a trace context object that should be set as the trace
195193
* context header in a response to an incoming web request. This value is
196194
* based on the trace context header value in the corresponding incoming
197195
* request, as well as the result from the local trace policy on whether this
198196
* request will be traced or not.
199197
* @param incomingTraceContext The trace context that was attached to
200198
* the incoming web request, or null if the incoming request didn't have one.
201-
* @param isTraced Whether the incoming was traced. This is determined
199+
* @param isTraced Whether the incoming request was traced. This is determined
202200
* by the local tracing policy.
203201
* @returns If the response should contain the trace context within its
204-
* header, the string to be set as this header's value. Otherwise, an empty
205-
* string.
202+
* header, the context object to be serialized as this header's value.
203+
* Otherwise, null.
206204
*/
207-
getResponseTraceContext(incomingTraceContext: string|null, isTraced: boolean):
208-
string;
205+
getResponseTraceContext(
206+
incomingTraceContext: TraceContext|null, isTraced: boolean): TraceContext
207+
|null;
209208

210209
/**
211210
* Binds the trace context to the given function.
@@ -233,11 +232,21 @@ export interface Tracer {
233232
readonly spanTypes: typeof SpanType;
234233
/** A collection of functions for encoding and decoding trace context. */
235234
readonly traceContextUtils: {
236-
encodeAsString: (ctx: TraceContext) => string;
237-
decodeFromString: (str: string) => TraceContext | null;
238235
encodeAsByteArray: (ctx: TraceContext) => Buffer;
239236
decodeFromByteArray: (buf: Buffer) => TraceContext | null;
240237
};
238+
/**
239+
* A collection of functions for dealing with trace context in HTTP headers.
240+
*/
241+
readonly propagation: Propagation;
242+
}
243+
244+
export type GetHeaderFunction = (key: string) => string[]|string|null|undefined;
245+
export type SetHeaderFunction = (key: string, value: string) => void;
246+
export interface Propagation {
247+
extract: (getHeader: GetHeaderFunction) => TraceContext | null;
248+
inject:
249+
(setHeader: SetHeaderFunction, traceContext: TraceContext|null) => void;
241250
}
242251

243252
export interface Monkeypatch<T> {

src/plugins/plugin-connect.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,23 @@ type Request = IncomingMessage&{originalUrl?: string};
2828

2929
const SUPPORTED_VERSIONS = '3.x';
3030

31-
function getFirstHeader(req: IncomingMessage, key: string): string|null {
32-
let headerValue = req.headers[key] || null;
33-
if (headerValue && typeof headerValue !== 'string') {
34-
headerValue = headerValue[0];
35-
}
36-
return headerValue;
37-
}
38-
3931
function createMiddleware(api: PluginTypes.Tracer):
4032
connect_3.NextHandleFunction {
4133
return function middleware(req: Request, res, next) {
4234
const options = {
4335
name: req.originalUrl ? (urlParse(req.originalUrl).pathname || '') : '',
4436
url: req.originalUrl,
4537
method: req.method,
46-
traceContext:
47-
getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME),
38+
traceContext: api.propagation.extract((key) => req.headers[key]),
4839
skipFrames: 1
4940
};
5041
api.runInRootSpan(options, (root) => {
5142
// Set response trace context.
5243
const responseTraceContext = api.getResponseTraceContext(
53-
options.traceContext || null, api.isRealSpan(root));
44+
options.traceContext, api.isRealSpan(root));
5445
if (responseTraceContext) {
55-
res.setHeader(
56-
api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext);
46+
api.propagation.inject(
47+
(k, v) => res.setHeader(k, v), responseTraceContext);
5748
}
5849

5950
if (!api.isRealSpan(root)) {

src/plugins/plugin-express.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,20 @@ function patchModuleRoot(express: Express4Module, api: PluginTypes.Tracer) {
3535
function middleware(
3636
req: express_4.Request, res: express_4.Response,
3737
next: express_4.NextFunction) {
38-
const options: PluginTypes.RootSpanOptions = {
38+
const options = {
3939
name: req.path,
40-
traceContext: req.get(api.constants.TRACE_CONTEXT_HEADER_NAME),
40+
traceContext: api.propagation.extract((key) => req.get(key)),
4141
url: req.originalUrl,
4242
method: req.method,
4343
skipFrames: 1
4444
};
4545
api.runInRootSpan(options, (rootSpan) => {
4646
// Set response trace context.
4747
const responseTraceContext = api.getResponseTraceContext(
48-
options.traceContext || null, api.isRealSpan(rootSpan));
48+
options.traceContext, api.isRealSpan(rootSpan));
4949
if (responseTraceContext) {
50-
res.set(api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext);
50+
api.propagation.inject(
51+
(k, v) => res.setHeader(k, v), responseTraceContext);
5152
}
5253

5354
if (!api.isRealSpan(rootSpan)) {

src/plugins/plugin-grpc.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as grpcModule from 'grpc'; // for types only.
1919
import {Client, MethodDefinition, ServerReadableStream, ServerUnaryCall, StatusObject} from 'grpc';
2020
import * as shimmer from 'shimmer';
2121

22-
import {Plugin, RootSpan, RootSpanOptions, Span, Tracer} from '../plugin-types';
22+
import {Plugin, RootSpan, RootSpanOptions, Span, TraceContext, Tracer} from '../plugin-types';
2323

2424
// Re-definition of Metadata with private fields
2525
type Metadata = grpcModule.Metadata&{
@@ -116,9 +116,7 @@ function patchClient(client: ClientModule, api: Tracer) {
116116
* a falsey value, metadata will not be modified.
117117
*/
118118
function setTraceContextFromString(
119-
metadata: Metadata, stringifiedTraceContext: string): void {
120-
const traceContext =
121-
api.traceContextUtils.decodeFromString(stringifiedTraceContext);
119+
metadata: Metadata, traceContext: TraceContext|null): void {
122120
if (traceContext) {
123121
const metadataValue =
124122
api.traceContextUtils.encodeAsByteArray(traceContext);
@@ -292,26 +290,19 @@ function unpatchClient(client: ClientModule) {
292290
function patchServer(server: ServerModule, api: Tracer) {
293291
/**
294292
* Returns a trace context on a Metadata object if it exists and is
295-
* well-formed, or null otherwise. The result will be encoded as a string.
293+
* well-formed, or null otherwise.
296294
* @param metadata The Metadata object from which trace context should be
297295
* retrieved.
298296
*/
299-
function getStringifiedTraceContext(metadata: grpcModule.Metadata): string|
300-
null {
297+
function getTraceContext(metadata: grpcModule.Metadata): TraceContext|null {
301298
const metadataValue =
302299
metadata.getMap()[api.constants.TRACE_CONTEXT_GRPC_METADATA_NAME] as
303300
Buffer;
304301
// Entry doesn't exist.
305302
if (!metadataValue) {
306303
return null;
307304
}
308-
const traceContext =
309-
api.traceContextUtils.decodeFromByteArray(metadataValue);
310-
// Value is malformed.
311-
if (!traceContext) {
312-
return null;
313-
}
314-
return api.traceContextUtils.encodeAsString(traceContext);
305+
return api.traceContextUtils.decodeFromByteArray(metadataValue);
315306
}
316307

317308
/**
@@ -356,7 +347,7 @@ function patchServer(server: ServerModule, api: Tracer) {
356347
const rootSpanOptions = {
357348
name: requestName,
358349
url: requestName,
359-
traceContext: getStringifiedTraceContext(call.metadata),
350+
traceContext: getTraceContext(call.metadata),
360351
skipFrames: SKIP_FRAMES
361352
};
362353
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {
@@ -410,7 +401,7 @@ function patchServer(server: ServerModule, api: Tracer) {
410401
const rootSpanOptions = {
411402
name: requestName,
412403
url: requestName,
413-
traceContext: getStringifiedTraceContext(stream.metadata),
404+
traceContext: getTraceContext(stream.metadata),
414405
skipFrames: SKIP_FRAMES
415406
} as RootSpanOptions;
416407
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {
@@ -472,7 +463,7 @@ function patchServer(server: ServerModule, api: Tracer) {
472463
const rootSpanOptions = {
473464
name: requestName,
474465
url: requestName,
475-
traceContext: getStringifiedTraceContext(stream.metadata),
466+
traceContext: getTraceContext(stream.metadata),
476467
skipFrames: SKIP_FRAMES
477468
} as RootSpanOptions;
478469
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {
@@ -532,7 +523,7 @@ function patchServer(server: ServerModule, api: Tracer) {
532523
const rootSpanOptions = {
533524
name: requestName,
534525
url: requestName,
535-
traceContext: getStringifiedTraceContext(stream.metadata),
526+
traceContext: getTraceContext(stream.metadata),
536527
skipFrames: SKIP_FRAMES
537528
} as RootSpanOptions;
538529
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {

src/plugins/plugin-hapi.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,34 +34,26 @@ type Hapi17Request = hapi_17.Request&{
3434
_execute: Hapi17RequestExecutePrivate;
3535
};
3636

37-
function getFirstHeader(req: IncomingMessage, key: string): string|null {
38-
let headerValue = req.headers[key] || null;
39-
if (headerValue && typeof headerValue !== 'string') {
40-
headerValue = headerValue[0];
41-
}
42-
return headerValue;
43-
}
44-
4537
function instrument<T>(
4638
api: PluginTypes.Tracer, request: hapi_16.Request|hapi_17.Request,
4739
continueCb: () => T): T {
4840
const req = request.raw.req;
4941
const res = request.raw.res;
5042
const originalEnd = res.end;
51-
const options: PluginTypes.RootSpanOptions = {
43+
const options = {
5244
name: req.url ? (urlParse(req.url).pathname || '') : '',
5345
url: req.url,
5446
method: req.method,
55-
traceContext: getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME),
47+
traceContext: api.propagation.extract(key => req.headers[key]),
5648
skipFrames: 2
5749
};
5850
return api.runInRootSpan(options, (root) => {
5951
// Set response trace context.
60-
const responseTraceContext = api.getResponseTraceContext(
61-
options.traceContext || null, api.isRealSpan(root));
52+
const responseTraceContext =
53+
api.getResponseTraceContext(options.traceContext, api.isRealSpan(root));
6254
if (responseTraceContext) {
63-
res.setHeader(
64-
api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext);
55+
api.propagation.inject(
56+
(k, v) => res.setHeader(k, v), responseTraceContext);
6557
}
6658

6759
if (!api.isRealSpan(root)) {

src/plugins/plugin-http.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,11 @@ function makeRequestTrace(
142142
// headers.
143143
options = Object.assign({}, options) as ClientRequestArgs;
144144
options.headers = Object.assign({}, options.headers);
145+
const headers = options.headers;
145146
// Inject the trace context header.
146-
options.headers[api.constants.TRACE_CONTEXT_HEADER_NAME] =
147-
span.getTraceContext();
147+
api.propagation.inject((key, value) => {
148+
headers[key] = value;
149+
}, span.getTraceContext());
148150
}
149151

150152
const req = request(options, (res) => {
@@ -188,19 +190,20 @@ function makeRequestTrace(
188190
// Inject the trace context header, but only if it wasn't already injected
189191
// earlier.
190192
if (!traceHeaderPreinjected) {
191-
try {
192-
req.setHeader(
193-
api.constants.TRACE_CONTEXT_HEADER_NAME, span.getTraceContext());
194-
} catch (e) {
195-
if (e.code === ERR_HTTP_HEADERS_SENT ||
196-
e.message === ERR_HTTP_HEADERS_SENT_MSG) {
197-
// Swallow the error.
198-
// This would happen in the pathological case where the Expect header
199-
// exists but is not detected by hasExpectHeader.
200-
} else {
201-
throw e;
193+
api.propagation.inject((key, value) => {
194+
try {
195+
req.setHeader(key, value);
196+
} catch (e) {
197+
if (e.code === ERR_HTTP_HEADERS_SENT ||
198+
e.message === ERR_HTTP_HEADERS_SENT_MSG) {
199+
// Swallow the error.
200+
// This would happen in the pathological case where the Expect
201+
// header exists but is not detected by hasExpectHeader.
202+
} else {
203+
throw e;
204+
}
202205
}
203-
}
206+
}, span.getTraceContext());
204207
}
205208
return req;
206209
};

src/plugins/plugin-http2.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ function makeRequestTrace(
9797
api.labels.HTTP_METHOD_LABEL_KEY, extractMethodName(newHeaders));
9898
requestLifecycleSpan.addLabel(
9999
api.labels.HTTP_URL_LABEL_KEY, extractUrl(authority, newHeaders));
100-
newHeaders[api.constants.TRACE_CONTEXT_HEADER_NAME] =
101-
requestLifecycleSpan.getTraceContext();
100+
api.propagation.inject(
101+
(k, v) => newHeaders[k] = v, requestLifecycleSpan.getTraceContext());
102102
const stream: http2.ClientHttp2Stream = request.call(
103103
this, newHeaders, ...Array.prototype.slice.call(arguments, 1));
104104
api.wrapEmitter(stream);

0 commit comments

Comments
 (0)