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

Commit 63b13ca

Browse files
authored
feat: use well-known format for propagating trace context thru grpc (#814)
BREAKING CHANGE: The change in distributed trace context propagation across gRPC is not backwards-compatible. In other words, distributed tracing will not work between two Node instances communicating using gRPC with v2 and v3 of the Trace Agent, respectively. Trace context can now be propagated thru gRPC with the `'grpc-trace-bin'` key, and a binary-encoded value.
1 parent f96c827 commit 63b13ca

3 files changed

Lines changed: 88 additions & 42 deletions

File tree

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
/** Constant values. */
2020
// tslint:disable-next-line:variable-name
2121
export const Constants = {
22+
/** The metadata key under which trace context */
23+
TRACE_CONTEXT_GRPC_METADATA_NAME: 'grpc-trace-bin',
24+
2225
/** Header that carries trace context across Google infrastructure. */
2326
TRACE_CONTEXT_HEADER_NAME: 'x-cloud-trace-context',
2427

src/plugins/plugin-grpc.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,25 @@ function unpatchMetadata() {
108108
}
109109

110110
function patchClient(client: ClientModule, api: TraceAgent) {
111+
/**
112+
* Set trace context on a Metadata object if it exists.
113+
* @param metadata The Metadata object to which a trace context should be
114+
* added.
115+
* @param stringifiedTraceContext The stringified trace context. If this is
116+
* a falsey value, metadata will not be modified.
117+
*/
118+
function setTraceContextFromString(
119+
metadata: Metadata, stringifiedTraceContext: string): void {
120+
const traceContext =
121+
api.traceContextUtils.decodeFromString(stringifiedTraceContext);
122+
if (traceContext) {
123+
const metadataValue =
124+
api.traceContextUtils.encodeAsByteArray(traceContext);
125+
metadata.set(
126+
api.constants.TRACE_CONTEXT_GRPC_METADATA_NAME, metadataValue);
127+
}
128+
}
129+
111130
/**
112131
* Wraps a callback so that the current span for this trace is also ended when
113132
* the callback is invoked.
@@ -194,8 +213,7 @@ function patchClient(client: ClientModule, api: TraceAgent) {
194213
// TS: Safe cast as we either found the index of the Metadata argument
195214
// or spliced it in at metaIndex.
196215
const metadata = args[metaIndex] as Metadata;
197-
metadata.set(
198-
api.constants.TRACE_CONTEXT_HEADER_NAME, span.getTraceContext());
216+
setTraceContextFromString(metadata, span.getTraceContext());
199217
const call: EventEmitter = method.apply(this, args);
200218
// Add extra data only when call successfully goes through. At this point
201219
// we know that the arguments are correct.
@@ -264,7 +282,29 @@ function unpatchClient(client: ClientModule) {
264282
}
265283

266284
function patchServer(server: ServerModule, api: TraceAgent) {
267-
const traceContextHeaderName = api.constants.TRACE_CONTEXT_HEADER_NAME;
285+
/**
286+
* Returns a trace context on a Metadata object if it exists and is
287+
* well-formed, or null otherwise. The result will be encoded as a string.
288+
* @param metadata The Metadata object from which trace context should be
289+
* retrieved.
290+
*/
291+
function getStringifiedTraceContext(metadata: grpcModule.Metadata): string|
292+
null {
293+
const metadataValue =
294+
metadata.getMap()[api.constants.TRACE_CONTEXT_GRPC_METADATA_NAME] as
295+
Buffer;
296+
// Entry doesn't exist.
297+
if (!metadataValue) {
298+
return null;
299+
}
300+
const traceContext =
301+
api.traceContextUtils.decodeFromByteArray(metadataValue);
302+
// Value is malformed.
303+
if (!traceContext) {
304+
return null;
305+
}
306+
return api.traceContextUtils.encodeAsString(traceContext);
307+
}
268308

269309
/**
270310
* A helper function to record metadata in a trace span. The return value of
@@ -301,15 +341,12 @@ function patchServer(server: ServerModule, api: TraceAgent) {
301341
return function serverMethodTrace(
302342
this: Server, call: ServerUnaryCall<S>,
303343
callback: ServerUnaryCallback<T>) {
304-
// TODO(kjin): Is it possible for a metadata value to be a buffer?
305-
// This needs to be investigated in order to avoid the cast here and
306-
// in other server wrapper functions.
307344
const rootSpanOptions = {
308345
name: requestName,
309346
url: requestName,
310-
traceContext: call.metadata.getMap()[traceContextHeaderName],
347+
traceContext: getStringifiedTraceContext(call.metadata),
311348
skipFrames: SKIP_FRAMES
312-
} as RootSpanOptions;
349+
};
313350
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {
314351
if (!api.isRealSpan(rootSpan)) {
315352
return serverMethod.call(this, call, callback);
@@ -362,7 +399,7 @@ function patchServer(server: ServerModule, api: TraceAgent) {
362399
const rootSpanOptions = {
363400
name: requestName,
364401
url: requestName,
365-
traceContext: stream.metadata.getMap()[traceContextHeaderName],
402+
traceContext: getStringifiedTraceContext(stream.metadata),
366403
skipFrames: SKIP_FRAMES
367404
} as RootSpanOptions;
368405
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {
@@ -425,7 +462,7 @@ function patchServer(server: ServerModule, api: TraceAgent) {
425462
const rootSpanOptions = {
426463
name: requestName,
427464
url: requestName,
428-
traceContext: stream.metadata.getMap()[traceContextHeaderName],
465+
traceContext: getStringifiedTraceContext(stream.metadata),
429466
skipFrames: SKIP_FRAMES
430467
} as RootSpanOptions;
431468
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {
@@ -486,7 +523,7 @@ function patchServer(server: ServerModule, api: TraceAgent) {
486523
const rootSpanOptions = {
487524
name: requestName,
488525
url: requestName,
489-
traceContext: stream.metadata.getMap()[traceContextHeaderName],
526+
traceContext: getStringifiedTraceContext(stream.metadata),
490527
skipFrames: SKIP_FRAMES
491528
} as RootSpanOptions;
492529
return api.runInRootSpan(rootSpanOptions, (rootSpan) => {

test/plugins/test-trace-grpc.ts

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,28 @@ var SEND_METADATA = 131;
4545
var EMIT_ERROR = 13412;
4646

4747
// Regular expression matching client-side metadata labels
48-
var metadataRegExp =
49-
/^{"a":"b","x-cloud-trace-context":"[a-f0-9]{32}\/[0-9]+;o=1"}$/;
48+
var metadataRegExp = /"a":"b"/;
5049

5150
// Whether asserts in checkServerMetadata should be run
52-
// Turned on only for the test that checks propagated tract context
51+
// Turned on only for the test that checks propagated trace context
5352
var checkMetadata;
5453

54+
// When trace IDs are checked in checkServerMetadata, they should have this
55+
// exact value. This only applies in the test "should support distributed
56+
// context".
57+
const COMMON_TRACE_ID = 'ffeeddccbbaa99887766554433221100';
58+
5559
function checkServerMetadata(metadata) {
5660
if (checkMetadata) {
57-
var traceContext = metadata.getMap()[Constants.TRACE_CONTEXT_HEADER_NAME];
58-
assert.ok(/[a-f0-9]{32}\/[0-9]+;o=1/.test(traceContext));
59-
var parsedContext = util.parseContextFromHeader(traceContext);
61+
var traceContext = metadata.getMap()[Constants.TRACE_CONTEXT_GRPC_METADATA_NAME];
62+
var parsedContext = util.deserializeTraceContext(traceContext);
6063
assert.ok(parsedContext);
6164
var root = asRootSpanData(cls.get().getContext() as Span);
65+
// Check that we were able to propagate trace context.
66+
assert.strictEqual(parsedContext!.traceId, COMMON_TRACE_ID);
67+
assert.strictEqual(root.trace.traceId, COMMON_TRACE_ID);
68+
// Check that we correctly assigned the parent ID of the current span to
69+
// that of the incoming span ID.
6270
assert.strictEqual(root.span.parentSpanId, parsedContext!.spanId);
6371
}
6472
}
@@ -205,7 +213,7 @@ function callClientStream(client, grpc, metadata, cb) {
205213
if (Object.keys(metadata).length > 0) {
206214
var m = new grpc.Metadata();
207215
for (var key in metadata) {
208-
m.set(key, metadata[key]);
216+
m.add(key, metadata[key]);
209217
}
210218
args.unshift(m);
211219
}
@@ -454,9 +462,10 @@ Object.keys(versions).forEach(function(version) {
454462
it('should support distributed trace context', function(done) {
455463
function makeLink(fn, meta, next) {
456464
return function() {
457-
common.runInTransaction(function(terminate) {
465+
agent.runInRootSpan({ name: '', traceContext: `${COMMON_TRACE_ID}/0;o=1` }, function(span) {
466+
assert.strictEqual(span.type, agent.spanTypes.ROOT);
458467
fn(client, grpc, meta, function() {
459-
terminate();
468+
span.endSpan();
460469
next();
461470
});
462471
});
@@ -465,28 +474,25 @@ Object.keys(versions).forEach(function(version) {
465474
// Enable asserting properties of the metdata on the grpc server.
466475
checkMetadata = true;
467476
var next;
468-
common.runInTransaction(function (endTransaction) {
469-
var metadata = { a: 'b' };
470-
next = function() {
471-
endTransaction();
472-
checkMetadata = false;
473-
done();
474-
};
475-
// Try without supplying metadata (call* will not supply metadata to
476-
// the grpc client methods at all if no fields are present).
477-
// The plugin should automatically create a new Metadata object and
478-
// populate it with trace context data accordingly.
479-
next = makeLink(callUnary, {}, next);
480-
next = makeLink(callClientStream, {}, next);
481-
next = makeLink(callServerStream, {}, next);
482-
next = makeLink(callBidi, {}, next);
483-
// Try with metadata. The plugin should simply add trace context data
484-
// to it.
485-
next = makeLink(callUnary, metadata, next);
486-
next = makeLink(callClientStream, metadata, next);
487-
next = makeLink(callServerStream, metadata, next);
488-
next = makeLink(callBidi, metadata, next);
489-
});
477+
var metadata = { a: 'b' };
478+
next = function() {
479+
checkMetadata = false;
480+
done();
481+
};
482+
// Try without supplying metadata (call* will not supply metadata to
483+
// the grpc client methods at all if no fields are present).
484+
// The plugin should automatically create a new Metadata object and
485+
// populate it with trace context data accordingly.
486+
next = makeLink(callUnary, {}, next);
487+
next = makeLink(callClientStream, {}, next);
488+
next = makeLink(callServerStream, {}, next);
489+
next = makeLink(callBidi, {}, next);
490+
// Try with metadata. The plugin should simply add trace context data
491+
// to it.
492+
next = makeLink(callUnary, metadata, next);
493+
next = makeLink(callClientStream, metadata, next);
494+
next = makeLink(callServerStream, metadata, next);
495+
next = makeLink(callBidi, metadata, next);
490496
next();
491497
});
492498

0 commit comments

Comments
 (0)