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

Commit 199cb42

Browse files
authored
feat: add contextHeaderBehavior option (#900)
1 parent 47bb6e5 commit 199cb42

8 files changed

Lines changed: 185 additions & 67 deletions

File tree

src/config.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ const pluginDirectory =
2222
export type CLSMechanism =
2323
'async-hooks'|'async-listener'|'auto'|'none'|'singular';
2424

25-
/** Available configuration options. */
25+
export type ContextHeaderBehavior = 'default'|'ignore'|'require';
26+
27+
/**
28+
* Available configuration options. All fields are optional. See the
29+
* defaultConfig object defined in this file for default assigned values.
30+
*/
2631
export interface Config {
2732
/**
2833
* Log levels: 0=disabled, 1=error, 2=warn, 3=info, 4=debug
@@ -137,6 +142,26 @@ export interface Config {
137142
*/
138143
samplingRate?: number;
139144

145+
/**
146+
* Specifies how to use incoming trace context headers. The following options
147+
* are available:
148+
* 'default' -- Trace context will be propagated for incoming requests that
149+
* contain the context header. A new trace will be created for requests
150+
* without trace context headers. All traces are still subject to local
151+
* sampling and url filter policies.
152+
* 'require' -- Same as default, but traces won't be created for requests
153+
* without trace context headers. This should not be set for end user-facing
154+
* services, as this header is usually set by other traced services rather
155+
* than by users.
156+
* 'ignore' -- Trace context headers will always be ignored, so a new trace
157+
* with a unique ID will be created for every request. This means that a
158+
* sampling decision specified on an incoming request will be ignored.
159+
* This might be useful for aggregating traces generated by different cloud
160+
* platform projects.
161+
* All traces are still subject to local tracing policy.
162+
*/
163+
contextHeaderBehavior?: ContextHeaderBehavior;
164+
140165
/**
141166
* The number of transactions we buffer before we publish to the trace
142167
* API, unless `flushDelaySeconds` seconds have elapsed first.
@@ -166,13 +191,10 @@ export interface Config {
166191
onUncaughtException?: string;
167192

168193
/**
169-
* EXPERIMENTAL:
170-
* Allows to ignore the requests X-Cloud-Trace-Context header if set. Setting
171-
* this to true will cause traces generated by this module to appear
172-
* separately from other distributed work done by other services on behalf of
173-
* the same incoming request. Setting this will also cause sampling decisions
174-
* made by other distributed components to be ignored. This is useful for
175-
* aggregating traces generated by different cloud platform projects.
194+
* Setting this to true or false is the same as setting contextHeaderBehavior
195+
* to 'ignore' or 'default' respectively. If both are explicitly set,
196+
* contextHeaderBehavior will be prioritized over this value.
197+
* Deprecated: This option will be removed in a future release.
176198
*/
177199
ignoreContextHeader?: boolean;
178200

@@ -244,8 +266,8 @@ export const defaultConfig = {
244266
flushDelaySeconds: 30,
245267
ignoreUrls: ['/_ah/health'],
246268
samplingRate: 10,
269+
contextHeaderBehavior: 'default',
247270
bufferSize: 1000,
248271
onUncaughtException: 'ignore',
249-
ignoreContextHeader: false,
250272
serviceContext: {}
251273
};

src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,24 @@ function initConfig(projectConfig: Forceable<Config>):
6060
envSetConfig =
6161
require(path.resolve(process.env.GCLOUD_TRACE_CONFIG!)) as Config;
6262
}
63+
64+
// Internally, ignoreContextHeader is no longer being used, so convert the
65+
// user's value into a value for contextHeaderBehavior. But let this value
66+
// be overridden by the user's explicitly set value for contextHeaderBehavior.
67+
const contextHeaderBehaviorUnderride = {
68+
contextHeaderBehavior: projectConfig.ignoreContextHeader ? 'ignore' :
69+
'default'
70+
};
71+
6372
// Configuration order of precedence:
6473
// 1. Environment Variables
6574
// 2. Project Config
6675
// 3. Environment Variable Set Configuration File (from GCLOUD_TRACE_CONFIG)
6776
// 4. Default Config (as specified in './config')
6877
const config = extend(
6978
true, {[FORCE_NEW]: projectConfig[FORCE_NEW]}, defaultConfig,
70-
envSetConfig, projectConfig, envConfig, {plugins: {}});
79+
envSetConfig, contextHeaderBehaviorUnderride, projectConfig, envConfig,
80+
{plugins: {}});
7181
// The empty plugins object guarantees that plugins is a plain object,
7282
// even if it's explicitly specified in the config to be a non-object.
7383

src/trace-api.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,34 @@ import {traceWriter} from './trace-writer';
2828
import {TracePolicy, TracePolicyConfig} from './tracing-policy';
2929
import * as util from './util';
3030

31+
/**
32+
* An enumeration of the different possible types of behavior when dealing with
33+
* incoming trace context. Requests are still subject to local tracing policy.
34+
*/
35+
export enum TraceContextHeaderBehavior {
36+
/**
37+
* Respect the trace context header if it exists; otherwise, trace the
38+
* request as a new trace.
39+
*/
40+
DEFAULT = 'default',
41+
/**
42+
* Respect the trace context header if it exists; otherwise, treat the
43+
* request as unsampled and don't trace it.
44+
*/
45+
REQUIRE = 'require',
46+
/**
47+
* Trace every request as a new trace, even if trace context exists.
48+
*/
49+
IGNORE = 'ignore'
50+
}
51+
3152
/**
3253
* An interface describing configuration fields read by the StackdriverTracer
3354
* object. This includes fields read by the trace policy.
3455
*/
3556
export interface StackdriverTracerConfig extends TracePolicyConfig {
3657
enhancedDatabaseReporting: boolean;
37-
ignoreContextHeader: boolean;
58+
contextHeaderBehavior: TraceContextHeaderBehavior;
3859
rootSpanNameOverride: (path: string) => string;
3960
spansPerTraceSoftLimit: number;
4061
spansPerTraceHardLimit: number;
@@ -43,7 +64,7 @@ export interface StackdriverTracerConfig extends TracePolicyConfig {
4364
interface IncomingTraceContext {
4465
traceId?: string;
4566
spanId?: string;
46-
options?: number;
67+
options: number;
4768
}
4869

4970
/**
@@ -151,20 +172,26 @@ export class StackdriverTracer implements Tracer {
151172
}
152173

153174
// Attempt to read incoming trace context.
154-
let incomingTraceContext: IncomingTraceContext = {};
155-
if (isString(options.traceContext) && !this.config!.ignoreContextHeader) {
156-
const parsedContext = util.parseContextFromHeader(options.traceContext);
157-
if (parsedContext) {
158-
incomingTraceContext = parsedContext;
159-
}
175+
const incomingTraceContext: IncomingTraceContext = {options: 1};
176+
let parsedContext: util.TraceContext|null = null;
177+
if (isString(options.traceContext) &&
178+
this.config!.contextHeaderBehavior !==
179+
TraceContextHeaderBehavior.IGNORE) {
180+
parsedContext = util.parseContextFromHeader(options.traceContext);
181+
}
182+
if (parsedContext) {
183+
Object.assign(incomingTraceContext, parsedContext);
184+
} else if (
185+
this.config!.contextHeaderBehavior ===
186+
TraceContextHeaderBehavior.REQUIRE) {
187+
incomingTraceContext.options = 0;
160188
}
161189

162190
// Consult the trace policy.
163191
const locallyAllowed = this.policy!.shouldTrace(
164192
{timestamp: Date.now(), url: options.url || ''});
165-
const remotelyAllowed = incomingTraceContext.options === undefined ||
166-
!!(incomingTraceContext.options &
167-
Constants.TRACE_OPTIONS_TRACE_ENABLED);
193+
const remotelyAllowed = !!(
194+
incomingTraceContext.options & Constants.TRACE_OPTIONS_TRACE_ENABLED);
168195

169196
let rootContext: RootSpan&RootContext;
170197
// Don't create a root span if the trace policy disallows it.

src/tracing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface TopLevelConfig {
3232

3333
// PluginLoaderConfig extends TraceAgentConfig
3434
export type NormalizedConfig =
35-
(TraceWriterConfig&PluginLoaderConfig&TopLevelConfig)|{enabled: false};
35+
((TraceWriterConfig&PluginLoaderConfig&TopLevelConfig)|{enabled: false});
3636

3737
/**
3838
* A class that represents automatic tracing.

test/plugins/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import '../override-gcp-metadata';
1919
import { cls, TraceCLS } from '../../src/cls';
20-
import { StackdriverTracer } from '../../src/trace-api';
20+
import { StackdriverTracer, TraceContextHeaderBehavior } from '../../src/trace-api';
2121
import { traceWriter } from '../../src/trace-writer';
2222
import { SpanType } from '../../src/constants';
2323
import { TestLogger } from '../logger';
@@ -66,7 +66,7 @@ shimmer.wrap(trace, 'start', function(original) {
6666
testTraceAgent = new StackdriverTracer('test');
6767
testTraceAgent.enable({
6868
enhancedDatabaseReporting: false,
69-
ignoreContextHeader: false,
69+
contextHeaderBehavior: TraceContextHeaderBehavior.DEFAULT,
7070
rootSpanNameOverride: (name: string) => name,
7171
samplingRate: 0,
7272
ignoreUrls: [],

test/test-config.ts

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { NormalizedConfig } from '../src/tracing';
2525
import { StackdriverTracer } from '../src/trace-api';
2626
import {Logger} from '../src/logger';
2727

28-
describe('Behavior set by config for context propagation mechanism', () => {
28+
describe('Behavior set by config for CLS', () => {
2929
const useAH = semver.satisfies(process.version, '>=8');
3030
const autoMechanism =
3131
useAH ? TraceCLSMechanism.ASYNC_HOOKS : TraceCLSMechanism.ASYNC_LISTENER;
@@ -93,9 +93,25 @@ describe('Behavior set by config for context propagation mechanism', () => {
9393
}
9494
});
9595

96-
describe('Behavior set by config for overriding root span name', () => {
96+
describe('Behavior set by config for Tracer', () => {
9797
let capturedConfig: NormalizedConfig|null;
9898

99+
// Convenience function to assert properties of capturedConfig that we want
100+
// to be true on every test, and return an object with a conveniently
101+
// sanitized type.
102+
const getCapturedConfig = () => {
103+
assert.ok(capturedConfig);
104+
const config = capturedConfig!;
105+
// If !config.enabled, then TSC does not permit access to other fields on
106+
// config. So use this structure instead of assert.ok(config.enabled).
107+
if (config.enabled) {
108+
return config;
109+
} else {
110+
assert.fail('Configuration was not enabled.');
111+
throw new Error(); // unreachable.
112+
}
113+
};
114+
99115
class CaptureConfigTestTracing extends testTraceModule.TestTracing {
100116
constructor(config: NormalizedConfig, traceAgent: StackdriverTracer) {
101117
super(config, traceAgent);
@@ -116,40 +132,67 @@ describe('Behavior set by config for overriding root span name', () => {
116132
testTraceModule.setTracingForTest(testTraceModule.TestTracing);
117133
});
118134

119-
it('should convert a string to a function', () => {
120-
testTraceModule.start({
121-
rootSpanNameOverride: 'hello'
135+
136+
describe('Context header behavior', () => {
137+
it('should copy over an explicitly-set value', () => {
138+
testTraceModule.start({
139+
contextHeaderBehavior: 'require'
140+
});
141+
const config = getCapturedConfig();
142+
assert.strictEqual(config.contextHeaderBehavior, 'require');
143+
});
144+
145+
it('should respect the value of ignoreContextHeader if not set', () => {
146+
testTraceModule.start({
147+
ignoreContextHeader: false
148+
});
149+
let config = getCapturedConfig();
150+
assert.strictEqual(config.contextHeaderBehavior, 'default');
151+
capturedConfig = null;
152+
testTraceModule.start({
153+
ignoreContextHeader: true
154+
});
155+
config = getCapturedConfig();
156+
assert.strictEqual(config.contextHeaderBehavior, 'ignore');
157+
});
158+
159+
it('should override the value of ignoreContextHeader if both set', () => {
160+
testTraceModule.start({
161+
ignoreContextHeader: false,
162+
contextHeaderBehavior: 'require'
163+
});
164+
let config = getCapturedConfig();
165+
assert.strictEqual(config.contextHeaderBehavior, 'require');
166+
capturedConfig = null;
167+
testTraceModule.start({
168+
ignoreContextHeader: true,
169+
contextHeaderBehavior: 'require'
170+
});
171+
config = getCapturedConfig();
172+
assert.strictEqual(config.contextHeaderBehavior, 'require');
122173
});
123-
assert.ok(capturedConfig!);
124-
// Avoid using the ! operator multiple times.
125-
const config = capturedConfig!;
126-
// If !config.enabled, then TSC does not permit access to other fields on
127-
// config. So use this structure instead of assert.ok(config.enabled).
128-
if (config.enabled) {
129-
assert.strictEqual(typeof config.rootSpanNameOverride, 'function');
130-
assert.strictEqual(config.rootSpanNameOverride(''), 'hello');
131-
} else {
132-
assert.fail('Configuration was not enabled.');
133-
}
134174
});
135175

136-
it('should convert a non-string, non-function to the identity fn', () => {
137-
testTraceModule.start({
138-
// We should make sure passing in unsupported values at least doesn't
139-
// result in a crash.
140-
// tslint:disable-next-line:no-any
141-
rootSpanNameOverride: 2 as any
176+
describe('Overriding root span name', () => {
177+
it('should convert a string to a function', () => {
178+
testTraceModule.start({
179+
rootSpanNameOverride: 'hello'
180+
});
181+
const config = getCapturedConfig();
182+
assert.strictEqual(typeof config.rootSpanNameOverride, 'function');
183+
assert.strictEqual(config.rootSpanNameOverride(''), 'hello');
142184
});
143-
assert.ok(capturedConfig!);
144-
// Avoid using the ! operator multiple times.
145-
const config = capturedConfig!;
146-
// If !config.enabled, then TSC does not permit access to other fields on
147-
// config. So use this structure instead of assert.ok(config.enabled).
148-
if (config.enabled) {
185+
186+
it('should convert a non-string, non-function to the identity fn', () => {
187+
testTraceModule.start({
188+
// We should make sure passing in unsupported values at least doesn't
189+
// result in a crash.
190+
// tslint:disable-next-line:no-any
191+
rootSpanNameOverride: 2 as any
192+
});
193+
const config = getCapturedConfig();
149194
assert.strictEqual(typeof config.rootSpanNameOverride, 'function');
150195
assert.strictEqual(config.rootSpanNameOverride('a'), 'a');
151-
} else {
152-
assert.fail('Configuration was not enabled.');
153-
}
196+
});
154197
});
155198
});

test/test-plugin-loader.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as hook from 'require-in-the-middle';
2020
import * as shimmer from 'shimmer';
2121

2222
import {Logger} from '../src/logger';
23+
import {TraceContextHeaderBehavior} from '../src/trace-api';
2324
import {PluginLoader, PluginLoaderState, PluginWrapper} from '../src/trace-plugin-loader';
2425

2526
import {TestLogger} from './logger';
@@ -46,7 +47,7 @@ describe('Trace Plugin Loader', () => {
4647
samplingRate: 0,
4748
ignoreUrls: [],
4849
enhancedDatabaseReporting: false,
49-
ignoreContextHeader: false,
50+
contextHeaderBehavior: TraceContextHeaderBehavior.DEFAULT,
5051
rootSpanNameOverride: (name: string) => name,
5152
projectId: '0',
5253
spansPerTraceSoftLimit: Infinity,

0 commit comments

Comments
 (0)