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

Commit b37aa3d

Browse files
authored
feat!: allow users to specify a trace policy impl (#1027)
BREAKING CHANGE: contextHeaderBehavior and ignoreContextHeader now act independently of one another. The former controls how a sampling decision is made based on incoming context header, and the latter controls whether trace context is propagated to the current request.
1 parent e956d45 commit b37aa3d

16 files changed

Lines changed: 475 additions & 360 deletions

src/config.ts

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,34 @@ export type CLSMechanism =
2424

2525
export type ContextHeaderBehavior = 'default'|'ignore'|'require';
2626

27+
export interface RequestDetails {
28+
/**
29+
* The request timestamp.
30+
*/
31+
timestamp: number;
32+
/**
33+
* The request URL.
34+
*/
35+
url: string;
36+
/**
37+
* The request method.
38+
*/
39+
method: string;
40+
/**
41+
* The parsed trace context, if it exists.
42+
*/
43+
traceContext: {traceId: string; spanId: string; options: number;}|null;
44+
/**
45+
* The original options object used to create the root span that corresponds
46+
* to this request.
47+
*/
48+
options: {};
49+
}
50+
51+
export interface TracePolicy {
52+
shouldTrace: (requestDetails: RequestDetails) => boolean;
53+
}
54+
2755
/**
2856
* Available configuration options. All fields are optional. See the
2957
* defaultConfig object defined in this file for default assigned values.
@@ -144,25 +172,29 @@ export interface Config {
144172
samplingRate?: number;
145173

146174
/**
147-
* Specifies how to use incoming trace context headers. The following options
148-
* are available:
149-
* 'default' -- Trace context will be propagated for incoming requests that
150-
* contain the context header. A new trace will be created for requests
151-
* without trace context headers. All traces are still subject to local
152-
* sampling and url filter policies.
153-
* 'require' -- Same as default, but traces won't be created for requests
154-
* without trace context headers. This should not be set for end user-facing
155-
* services, as this header is usually set by other traced services rather
156-
* than by users.
157-
* 'ignore' -- Trace context headers will always be ignored, so a new trace
158-
* with a unique ID will be created for every request. This means that a
159-
* sampling decision specified on an incoming request will be ignored.
175+
* Specifies whether to trace based on the 'traced' bit specified on incoming
176+
* trace context headers. The following options are available:
177+
* 'default' -- Don't trace incoming requests that have a trace context
178+
* header with its 'traced' bit set to 0.
179+
* 'require' -- Don't trace incoming requests that have a trace context
180+
* header with its 'traced' bit set to 0, or incoming requests without a
181+
* trace context header.
182+
* 'ignore' -- The 'traced' bit will be ignored. In other words, the context
183+
* header isn't used to determine whether a request will be traced at all.
160184
* This might be useful for aggregating traces generated by different cloud
161185
* platform projects.
162-
* All traces are still subject to local tracing policy.
163186
*/
164187
contextHeaderBehavior?: ContextHeaderBehavior;
165188

189+
/**
190+
* For advanced usage only.
191+
* If specified, overrides the built-in trace policy object.
192+
* Note that if any of ignoreUrls, ignoreMethods, samplingRate, or
193+
* contextHeaderBehavior is specified, an error will be thrown when start()
194+
* is called.
195+
*/
196+
tracePolicy?: TracePolicy;
197+
166198
/**
167199
* Buffer the captured traces for `flushDelaySeconds` seconds before
168200
* publishing to the Stackdriver Trace API, unless the buffer fills up first.
@@ -199,14 +231,6 @@ export interface Config {
199231
*/
200232
onUncaughtException?: string;
201233

202-
/**
203-
* Setting this to true or false is the same as setting contextHeaderBehavior
204-
* to 'ignore' or 'default' respectively. If both are explicitly set,
205-
* contextHeaderBehavior will be prioritized over this value.
206-
* Deprecated: This option will be removed in a future release.
207-
*/
208-
ignoreContextHeader?: boolean;
209-
210234
/**
211235
* The ID of the Google Cloud Platform project with which traces should
212236
* be associated. The value of GCLOUD_PROJECT takes precedence over this

src/index.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ const filesLoadedBeforeTrace = Object.keys(require.cache);
1919
// This file's top-level imports must not transitively depend on modules that
2020
// do I/O, or continuation-local-storage will not work.
2121
import * as semver from 'semver';
22-
import {Config, defaultConfig} from './config';
22+
import {Config, defaultConfig, TracePolicy} from './config';
2323
import * as extend from 'extend';
2424
import * as path from 'path';
2525
import * as PluginTypes from './plugin-types';
2626
import {Tracing, TopLevelConfig} from './tracing';
2727
import {FORCE_NEW, Forceable, lastOf} from './util';
2828
import {Constants} from './constants';
29-
import {StackdriverTracer, TraceContextHeaderBehavior} from './trace-api';
3029
import {TraceCLSMechanism} from './cls';
30+
import {StackdriverTracer} from './trace-api';
31+
import {BuiltinTracePolicy, TraceContextHeaderBehavior} from './tracing-policy';
32+
import {config} from './plugins/types/bluebird_3';
3133

3234
export {Config, PluginTypes};
3335

@@ -55,6 +57,22 @@ function initConfig(userConfig: Forceable<Config>): Forceable<TopLevelConfig> {
5557
extend(true, {}, defaultConfig, envSetConfig, userConfig);
5658
const forceNew = userConfig[FORCE_NEW];
5759

60+
// Throw for improper configurations.
61+
const userSetKeys =
62+
new Set([...Object.keys(envSetConfig), ...Object.keys(userConfig)]);
63+
if (userSetKeys.has('tracePolicy')) {
64+
// If the user specified tracePolicy, they should not have also set these
65+
// other fields.
66+
const forbiddenKeys =
67+
['ignoreUrls', 'ignoreMethods', 'samplingRate', 'contextHeaderBehavior']
68+
.filter(key => userSetKeys.has(key))
69+
.map(key => `config.${key}`);
70+
if (forbiddenKeys.length > 0) {
71+
throw new Error(`config.tracePolicy and any of [${
72+
forbiddenKeys.join('\, ')}] can't be specified at the same time.`);
73+
}
74+
}
75+
5876
const getInternalClsMechanism = (clsMechanism: string): TraceCLSMechanism => {
5977
// If the CLS mechanism is set to auto-determined, decide now
6078
// what it should be.
@@ -115,27 +133,20 @@ function initConfig(userConfig: Forceable<Config>): Forceable<TopLevelConfig> {
115133
plugins: {...mergedConfig.plugins},
116134
tracerConfig: {
117135
enhancedDatabaseReporting: mergedConfig.enhancedDatabaseReporting,
118-
contextHeaderBehavior: lastOf<TraceContextHeaderBehavior>(
119-
defaultConfig.contextHeaderBehavior as TraceContextHeaderBehavior,
120-
// Internally, ignoreContextHeader is no longer being used, so
121-
// convert the user's value into a value for contextHeaderBehavior.
122-
// But let this value be overridden by the user's explicitly set
123-
// value for contextHeaderBehavior.
124-
mergedConfig.ignoreContextHeader ?
125-
TraceContextHeaderBehavior.IGNORE :
126-
TraceContextHeaderBehavior.DEFAULT,
127-
userConfig.contextHeaderBehavior as TraceContextHeaderBehavior),
128136
rootSpanNameOverride:
129137
getInternalRootSpanNameOverride(mergedConfig.rootSpanNameOverride),
130138
spansPerTraceHardLimit: mergedConfig.spansPerTraceHardLimit,
131-
spansPerTraceSoftLimit: mergedConfig.spansPerTraceSoftLimit,
132-
tracePolicyConfig: {
133-
samplingRate: mergedConfig.samplingRate,
134-
ignoreMethods: mergedConfig.ignoreMethods,
135-
ignoreUrls: mergedConfig.ignoreUrls
136-
}
139+
spansPerTraceSoftLimit: mergedConfig.spansPerTraceSoftLimit
137140
}
138-
}
141+
},
142+
tracePolicyConfig: {
143+
samplingRate: mergedConfig.samplingRate,
144+
ignoreMethods: mergedConfig.ignoreMethods,
145+
ignoreUrls: mergedConfig.ignoreUrls,
146+
contextHeaderBehavior: mergedConfig.contextHeaderBehavior as
147+
TraceContextHeaderBehavior
148+
},
149+
overrides: {tracePolicy: mergedConfig.tracePolicy}
139150
};
140151
}
141152

src/trace-api.ts

Lines changed: 35 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,53 +19,25 @@ import * as is from 'is';
1919
import * as uuid from 'uuid';
2020

2121
import {cls, RootContext} from './cls';
22+
import {TracePolicy} from './config';
2223
import {Constants, SpanType} from './constants';
2324
import {Logger} from './logger';
2425
import {Func, RootSpan, RootSpanOptions, Span, SpanOptions, Tracer} from './plugin-types';
2526
import {RootSpanData, UNCORRELATED_CHILD_SPAN, UNCORRELATED_ROOT_SPAN, UNTRACED_CHILD_SPAN, UNTRACED_ROOT_SPAN} from './span-data';
2627
import {TraceLabels} from './trace-labels';
2728
import {traceWriter} from './trace-writer';
28-
import {TracePolicy, TracePolicyConfig} from './tracing-policy';
29+
import {neverTrace} from './tracing-policy';
2930
import * as util from './util';
3031

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-
5232
/**
5333
* An interface describing configuration fields read by the StackdriverTracer
5434
* object. This includes fields read by the trace policy.
5535
*/
5636
export interface StackdriverTracerConfig {
5737
enhancedDatabaseReporting: boolean;
58-
contextHeaderBehavior: TraceContextHeaderBehavior;
5938
rootSpanNameOverride: (path: string) => string;
6039
spansPerTraceSoftLimit: number;
6140
spansPerTraceHardLimit: number;
62-
tracePolicyConfig: TracePolicyConfig;
63-
}
64-
65-
interface IncomingTraceContext {
66-
traceId?: string;
67-
spanId?: string;
68-
options: number;
6941
}
7042

7143
/**
@@ -117,10 +89,10 @@ export class StackdriverTracer implements Tracer {
11789
* @param logger A logger object.
11890
* @private
11991
*/
120-
enable(config: StackdriverTracerConfig, logger: Logger) {
92+
enable(config: StackdriverTracerConfig, policy: TracePolicy, logger: Logger) {
12193
this.logger = logger;
12294
this.config = config;
123-
this.policy = new TracePolicy(config.tracePolicyConfig);
95+
this.policy = policy;
12496
this.enabled = true;
12597
}
12698

@@ -134,7 +106,7 @@ export class StackdriverTracer implements Tracer {
134106
// never generates traces allows persisting wrapped methods (either because
135107
// they are already instantiated or the plugin doesn't unpatch them) to
136108
// short-circuit out of trace generation logic.
137-
this.policy = TracePolicy.never();
109+
this.policy = neverTrace();
138110
this.enabled = false;
139111
}
140112

@@ -175,48 +147,49 @@ export class StackdriverTracer implements Tracer {
175147
}
176148

177149
// Attempt to read incoming trace context.
178-
const incomingTraceContext: IncomingTraceContext = {options: 1};
179-
let parsedContext: util.TraceContext|null = null;
180-
if (isString(options.traceContext) &&
181-
this.config!.contextHeaderBehavior !==
182-
TraceContextHeaderBehavior.IGNORE) {
183-
parsedContext = util.parseContextFromHeader(options.traceContext);
184-
}
185-
if (parsedContext) {
186-
if (parsedContext.options === undefined) {
187-
// If there are no incoming option flags, default to 0x1.
188-
parsedContext.options = 1;
150+
const parseContext = (stringifiedTraceContext?: string|null) => {
151+
const parsedContext = isString(stringifiedTraceContext) ?
152+
util.parseContextFromHeader(stringifiedTraceContext) :
153+
null;
154+
if (parsedContext) {
155+
if (parsedContext.options === undefined) {
156+
// If there are no incoming option flags, default to 0x1.
157+
parsedContext.options = 1;
158+
}
189159
}
190-
Object.assign(incomingTraceContext, parsedContext);
191-
} else if (
192-
this.config!.contextHeaderBehavior ===
193-
TraceContextHeaderBehavior.REQUIRE) {
194-
incomingTraceContext.options = 0;
195-
}
160+
return parsedContext as Required<util.TraceContext>| null;
161+
};
162+
const traceContext = parseContext(options.traceContext);
196163

197164
// Consult the trace policy.
198-
const locallyAllowed = this.policy!.shouldTrace({
165+
const shouldTrace = this.policy!.shouldTrace({
199166
timestamp: Date.now(),
200167
url: options.url || '',
201-
method: options.method || ''
168+
method: options.method || '',
169+
traceContext,
170+
options
202171
});
203-
const remotelyAllowed = !!(
204-
incomingTraceContext.options & Constants.TRACE_OPTIONS_TRACE_ENABLED);
205172

206173
let rootContext: RootSpan&RootContext;
174+
207175
// Don't create a root span if the trace policy disallows it.
208-
if (!locallyAllowed || !remotelyAllowed) {
176+
if (!shouldTrace) {
209177
rootContext = UNTRACED_ROOT_SPAN;
210178
} else {
211179
// Create a new root span, and invoke fn with it.
212-
const traceId =
213-
incomingTraceContext.traceId || (uuid.v4().split('-').join(''));
214-
const parentId = incomingTraceContext.spanId || '0';
215-
const name = this.config!.rootSpanNameOverride(options.name);
216180
rootContext = new RootSpanData(
217-
{projectId: '', traceId, spans: []}, /* Trace object */
218-
name, /* Span name */
219-
parentId, /* Parent's span ID */
181+
// Trace object
182+
{
183+
projectId: '',
184+
traceId: traceContext ? traceContext.traceId :
185+
uuid.v4().split('-').join(''),
186+
spans: []
187+
},
188+
// Span name
189+
this.config!.rootSpanNameOverride(options.name),
190+
// Parent span ID
191+
traceContext ? traceContext.spanId : '0',
192+
// Number of stack frames to skip
220193
options.skipFrames || 0);
221194
}
222195

0 commit comments

Comments
 (0)