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

Commit d0009ff

Browse files
authored
feat: add rootSpan.createChildSpan and change none CLS semantics (#731)
PR-URL: #731
1 parent 6e46ed1 commit d0009ff

12 files changed

Lines changed: 289 additions & 88 deletions

src/cls.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import * as semver from 'semver';
2121
import {AsyncHooksCLS} from './cls/async-hooks';
2222
import {AsyncListenerCLS} from './cls/async-listener';
2323
import {CLS, Func} from './cls/base';
24-
import {UniversalCLS} from './cls/universal';
24+
import {NullCLS} from './cls/null';
2525
import {SpanDataType} from './constants';
26+
import {SpanData, SpanOptions} from './plugin-types';
2627
import {Trace, TraceSpan} from './trace';
2728
import {Singleton} from './util';
2829

@@ -31,6 +32,7 @@ const asyncHooksAvailable = semver.satisfies(process.version, '>=8');
3132
export interface RealRootContext {
3233
readonly span: TraceSpan;
3334
readonly trace: Trace;
35+
createChildSpan(options: SpanOptions): SpanData;
3436
readonly type: SpanDataType.ROOT;
3537
}
3638

@@ -119,7 +121,7 @@ export class TraceCLS implements CLS<RootContext> {
119121
this.rootSpanStackOffset = 8;
120122
break;
121123
case TraceCLSMechanism.NONE:
122-
this.CLSClass = UniversalCLS;
124+
this.CLSClass = NullCLS;
123125
this.rootSpanStackOffset = 4;
124126
break;
125127
default:
@@ -128,7 +130,7 @@ export class TraceCLS implements CLS<RootContext> {
128130
}
129131
this.logger.info(
130132
`TraceCLS#constructor: Created [${config.mechanism}] CLS instance.`);
131-
this.currentCLS = new UniversalCLS(TraceCLS.UNTRACED);
133+
this.currentCLS = new NullCLS(TraceCLS.UNTRACED);
132134
this.currentCLS.enable();
133135
}
134136

@@ -137,23 +139,25 @@ export class TraceCLS implements CLS<RootContext> {
137139
}
138140

139141
enable(): void {
140-
if (!this.enabled) {
142+
// if this.CLSClass = NullCLS, the user specifically asked not to use
143+
// any context propagation mechanism. So nothing should change.
144+
if (!this.enabled && this.CLSClass !== NullCLS) {
141145
this.logger.info('TraceCLS#enable: Enabling CLS.');
142-
this.enabled = true;
143146
this.currentCLS.disable();
144147
this.currentCLS = new this.CLSClass(TraceCLS.UNCORRELATED);
145148
this.currentCLS.enable();
146149
}
150+
this.enabled = true;
147151
}
148152

149153
disable(): void {
150-
if (this.enabled) {
154+
if (this.enabled && this.CLSClass !== NullCLS) {
151155
this.logger.info('TraceCLS#disable: Disabling CLS.');
152-
this.enabled = false;
153156
this.currentCLS.disable();
154-
this.currentCLS = new UniversalCLS(TraceCLS.UNTRACED);
157+
this.currentCLS = new NullCLS(TraceCLS.UNTRACED);
155158
this.currentCLS.enable();
156159
}
160+
this.enabled = false;
157161
}
158162

159163
getContext(): RootContext {
Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,13 @@ import {EventEmitter} from 'events';
1919
import {CLS, Func} from './base';
2020

2121
/**
22-
* A trivial implementation of continuation-local storage where everything is
23-
* in the same continuation.
22+
* A trivial implementation of continuation-local storage where context takes on
23+
* a default, immutable value.
2424
*/
25-
export class UniversalCLS<Context> implements CLS<Context> {
25+
export class NullCLS<Context> implements CLS<Context> {
2626
private enabled = false;
27-
private currentContext: Context;
2827

29-
constructor(private readonly defaultContext: Context) {
30-
this.currentContext = this.defaultContext;
31-
}
28+
constructor(private readonly defaultContext: Context) {}
3229

3330
isEnabled(): boolean {
3431
return this.enabled;
@@ -40,18 +37,13 @@ export class UniversalCLS<Context> implements CLS<Context> {
4037

4138
disable(): void {
4239
this.enabled = false;
43-
this.setContext(this.defaultContext);
4440
}
4541

4642
getContext(): Context {
47-
return this.currentContext;
43+
return this.defaultContext;
4844
}
4945

50-
setContext(value: Context): void {
51-
if (this.enabled) {
52-
this.currentContext = value;
53-
}
54-
}
46+
setContext(value: Context): void {}
5547

5648
runWithNewContext<T>(fn: Func<T>): T {
5749
return fn();

src/plugin-types.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface TraceAgentExtension { _google_trace_patched: boolean; }
2626

2727
/**
2828
* Represents a trace span.
29+
* TODO(kjin): This should be called `Span`.
2930
*/
3031
export interface SpanData {
3132
/**
@@ -55,6 +56,22 @@ export interface SpanData {
5556
endSpan(): void;
5657
}
5758

59+
/**
60+
* Represents the root span within a trace.
61+
*/
62+
export interface RootSpanData extends SpanData {
63+
/**
64+
* Creates and starts a child span under this root span.
65+
* If the root span is a real span (type = ROOT), the child span will be as
66+
* well (type = CHILD).
67+
* Otherwise, if the root span's type is UNTRACED or UNCORRELATED, the child
68+
* span will be of the same type.
69+
* @param options Options for creating the child span.
70+
* @returns A new SpanData object.
71+
*/
72+
createChildSpan(options?: SpanOptions): SpanData;
73+
}
74+
5875
/**
5976
* An interface that describes the available options for creating a span in
6077
* general.
@@ -105,7 +122,7 @@ export interface TraceAgent {
105122
* a phantom SpanData object.
106123
* @returns The return value of calling fn.
107124
*/
108-
runInRootSpan<T>(options: RootSpanOptions, fn: (span: SpanData) => T): T;
125+
runInRootSpan<T>(options: RootSpanOptions, fn: (span: RootSpanData) => T): T;
109126

110127
/**
111128
* Returns a unique identifier for the currently active context. This can be
@@ -125,14 +142,14 @@ export interface TraceAgent {
125142
getWriterProjectId(): string|null;
126143

127144
/**
128-
* Creates and returns a new SpanData object nested within the root span.
129-
* If there is no root span, a phantom SpanData object will be
130-
* returned instead.
131-
* @param options An object that specifies options for how the child
132-
* span is created and propagated.
145+
* Creates and returns a new SpanData object nested within the current root
146+
* span, which is detected automatically.
147+
* If the root span is a phantom span or doesn't exist, the child span will
148+
* be a phantom span as well.
149+
* @param options Options for creating the child span.
133150
* @returns A new SpanData object.
134151
*/
135-
createChildSpan(options: SpanOptions): SpanData;
152+
createChildSpan(options?: SpanOptions): SpanData;
136153

137154
/**
138155
* Returns whether a given span is real or not by checking its SpanDataType.

src/span-data.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import * as crypto from 'crypto';
1818
import * as util from 'util';
1919

2020
import {Constants, SpanDataType} from './constants';
21-
import {SpanData as SpanData} from './plugin-types';
21+
import * as types from './plugin-types';
22+
import {SpanData, SpanOptions} from './plugin-types';
2223
import {SpanKind, Trace, TraceSpan} from './trace';
2324
import {TraceLabels} from './trace-labels';
2425
import {traceWriter} from './trace-writer';
@@ -110,7 +111,7 @@ export abstract class BaseSpanData implements SpanData {
110111
/**
111112
* Represents a real root span, which corresponds to an incoming request.
112113
*/
113-
export class RootSpanData extends BaseSpanData {
114+
export class RootSpanData extends BaseSpanData implements types.RootSpanData {
114115
readonly type = SpanDataType.ROOT;
115116

116117
constructor(
@@ -120,6 +121,16 @@ export class RootSpanData extends BaseSpanData {
120121
this.span.kind = SpanKind.RPC_SERVER;
121122
}
122123

124+
createChildSpan(options?: SpanOptions): SpanData {
125+
options = options || {name: ''};
126+
const skipFrames = options.skipFrames ? options.skipFrames + 1 : 1;
127+
return new ChildSpanData(
128+
this.trace, /* Trace object */
129+
options.name, /* Span name */
130+
this.span.spanId, /* Parent's span ID */
131+
skipFrames); /* # of frames to skip in stack trace */
132+
}
133+
123134
endSpan() {
124135
super.endSpan();
125136
traceWriter.get().writeSpan(this.trace);
@@ -156,14 +167,39 @@ function createPhantomSpanData<T extends SpanDataType>(spanType: T): SpanData&
156167
}
157168

158169
/**
159-
* A virtual trace span that indicates that a real trace span couldn't be
160-
* created because context was lost.
170+
* A virtual trace span that indicates that a real child span couldn't be
171+
* created because the correct root span couldn't be determined.
161172
*/
162-
export const UNCORRELATED_SPAN =
173+
export const UNCORRELATED_CHILD_SPAN =
163174
createPhantomSpanData(SpanDataType.UNCORRELATED);
164175

165176
/**
166-
* A virtual trace span that indicates that a real trace span couldn't be
177+
* A virtual trace span that indicates that a real child span couldn't be
178+
* created because the corresponding root span was disallowed by user
179+
* configuration.
180+
*/
181+
export const UNTRACED_CHILD_SPAN = createPhantomSpanData(SpanDataType.UNTRACED);
182+
183+
/**
184+
* A virtual trace span that indicates that a real root span couldn't be
185+
* created because an active root span context already exists.
186+
*/
187+
export const UNCORRELATED_ROOT_SPAN = Object.freeze(Object.assign(
188+
{
189+
createChildSpan() {
190+
return UNCORRELATED_CHILD_SPAN;
191+
}
192+
},
193+
UNCORRELATED_CHILD_SPAN));
194+
195+
/**
196+
* A virtual trace span that indicates that a real root span couldn't be
167197
* created because it was disallowed by user configuration.
168198
*/
169-
export const UNTRACED_SPAN = createPhantomSpanData(SpanDataType.UNTRACED);
199+
export const UNTRACED_ROOT_SPAN = Object.freeze(Object.assign(
200+
{
201+
createChildSpan() {
202+
return UNTRACED_CHILD_SPAN;
203+
}
204+
},
205+
UNTRACED_CHILD_SPAN));

src/trace-api.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import * as uuid from 'uuid';
2121

2222
import {cls} from './cls';
2323
import {Constants, SpanDataType} from './constants';
24-
import {Func, RootSpanOptions, SpanData, SpanOptions, TraceAgent as TraceAgentInterface} from './plugin-types';
25-
import {ChildSpanData, RootSpanData, UNCORRELATED_SPAN, UNTRACED_SPAN} from './span-data';
24+
import {Func, RootSpanData as RootSpanDataInterface, RootSpanOptions, SpanData, SpanOptions, TraceAgent as TraceAgentInterface} from './plugin-types';
25+
import {ChildSpanData, RootSpanData, UNCORRELATED_CHILD_SPAN, UNCORRELATED_ROOT_SPAN, UNTRACED_CHILD_SPAN, UNTRACED_ROOT_SPAN} from './span-data';
2626
import {SpanKind, Trace} from './trace';
2727
import {TraceLabels} from './trace-labels';
2828
import * as TracingPolicy from './tracing-policy';
@@ -121,18 +121,20 @@ export class TraceAgent implements TraceAgentInterface {
121121
return !!this.config && this.config.enhancedDatabaseReporting;
122122
}
123123

124-
runInRootSpan<T>(options: RootSpanOptions, fn: (span: SpanData) => T): T {
124+
runInRootSpan<T>(
125+
options: RootSpanOptions, fn: (span: RootSpanDataInterface) => T): T {
125126
if (!this.isActive()) {
126-
return fn(UNTRACED_SPAN);
127+
return fn(UNTRACED_ROOT_SPAN);
127128
}
128129

129-
// TODO validate options
130+
options = options || {name: ''};
131+
130132
// Don't create a root span if we are already in a root span
131133
const rootSpan = cls.get().getContext();
132134
if (rootSpan.type === SpanDataType.ROOT && !rootSpan.span.endTime) {
133135
this.logger!.warn(`TraceApi#runInRootSpan: [${
134136
this.pluginName}] Cannot create nested root spans.`);
135-
return fn(UNCORRELATED_SPAN);
137+
return fn(UNCORRELATED_ROOT_SPAN);
136138
}
137139

138140
return cls.get().runWithNewContext(() => {
@@ -155,8 +157,8 @@ export class TraceAgent implements TraceAgentInterface {
155157
!!(incomingTraceContext.options &
156158
Constants.TRACE_OPTIONS_TRACE_ENABLED);
157159
if (!locallyAllowed || !remotelyAllowed) {
158-
cls.get().setContext(UNTRACED_SPAN);
159-
return fn(UNTRACED_SPAN);
160+
cls.get().setContext(UNTRACED_ROOT_SPAN);
161+
return fn(UNTRACED_ROOT_SPAN);
160162
}
161163

162164
// Create a new root span, and invoke fn with it.
@@ -193,11 +195,12 @@ export class TraceAgent implements TraceAgentInterface {
193195
}
194196
}
195197

196-
createChildSpan(options: SpanOptions): SpanData {
198+
createChildSpan(options?: SpanOptions): SpanData {
197199
if (!this.isActive()) {
198-
return UNTRACED_SPAN;
200+
return UNTRACED_CHILD_SPAN;
199201
}
200202

203+
options = options || {name: ''};
201204
const rootSpan = cls.get().getContext();
202205
if (rootSpan.type === SpanDataType.ROOT) {
203206
if (!!rootSpan.span.endTime) {
@@ -212,29 +215,26 @@ export class TraceAgent implements TraceAgentInterface {
212215
this.pluginName}] Creating phantom child span [${
213216
options.name}] because root span [${
214217
rootSpan.span.name}] was already closed.`);
215-
return UNCORRELATED_SPAN;
218+
return UNCORRELATED_CHILD_SPAN;
216219
}
217220
// Create a new child span and return it.
218-
options = options || {name: ''};
219-
const skipFrames = options.skipFrames ? options.skipFrames + 1 : 1;
220-
const childContext = new ChildSpanData(
221-
rootSpan.trace, /* Trace object */
222-
options.name, /* Span name */
223-
rootSpan.span.spanId, /* Parent's span ID */
224-
skipFrames); /* # of frames to skip in stack trace */
221+
const childContext = rootSpan.createChildSpan({
222+
name: options.name,
223+
skipFrames: options.skipFrames ? options.skipFrames + 1 : 1
224+
});
225225
this.logger!.info(`TraceApi#createChildSpan: [${
226226
this.pluginName}] Created child span [${options.name}]`);
227227
return childContext;
228228
} else if (rootSpan.type === SpanDataType.UNTRACED) {
229229
// Context wasn't lost, but there's no root span, indicating that this
230230
// request should not be traced.
231-
return UNTRACED_SPAN;
231+
return UNTRACED_CHILD_SPAN;
232232
} else {
233233
// Context was lost.
234234
this.logger!.warn(`TraceApi#createChildSpan: [${
235235
this.pluginName}] Creating phantom child span [${
236236
options.name}] because there is no root span.`);
237-
return UNCORRELATED_SPAN;
237+
return UNCORRELATED_CHILD_SPAN;
238238
}
239239
}
240240

test/plugins/test-trace-grpc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { TraceLabels } from '../../src/trace-labels';
2121
import * as TracingPolicy from '../../src/tracing-policy';
2222
import * as util from '../../src/util';
2323
import * as assert from 'assert';
24-
import { asBaseSpanData } from '../utils';
24+
import { asRootSpanData } from '../utils';
2525
import { SpanData } from '../../src/plugin-types';
2626
import { FORCE_NEW } from '../../src/util';
2727

@@ -55,7 +55,7 @@ function checkServerMetadata(metadata) {
5555
assert.ok(/[a-f0-9]{32}\/[0-9]+;o=1/.test(traceContext));
5656
var parsedContext = util.parseContextFromHeader(traceContext);
5757
assert.ok(parsedContext);
58-
var root = asBaseSpanData(cls.get().getContext() as SpanData);
58+
var root = asRootSpanData(cls.get().getContext() as SpanData);
5959
assert.strictEqual(root.span.parentSpanId, parsedContext!.spanId);
6060
}
6161
}

test/plugins/test-trace-http.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ for (const nodule of Object.keys(servers) as Array<keyof typeof servers>) {
116116
describe(`${nodule} client tracing`, () => {
117117
let http: {get: HttpRequest; request: HttpRequest;};
118118
before(() => {
119+
trace.setCLS();
119120
trace.setPluginLoader();
120121
trace.start({
121122
plugins: {

0 commit comments

Comments
 (0)