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

Commit 000643f

Browse files
authored
feat: add singular cls option (#748)
PR-URL: #748
1 parent 319642a commit 000643f

5 files changed

Lines changed: 108 additions & 15 deletions

File tree

src/cls.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {AsyncHooksCLS} from './cls/async-hooks';
2222
import {AsyncListenerCLS} from './cls/async-listener';
2323
import {CLS, Func} from './cls/base';
2424
import {NullCLS} from './cls/null';
25+
import {SingularCLS} from './cls/singular';
2526
import {SpanDataType} from './constants';
2627
import {SpanData, SpanOptions} from './plugin-types';
2728
import {Trace, TraceSpan} from './trace';
@@ -71,6 +72,11 @@ export enum TraceCLSMechanism {
7172
* Do not use any special mechanism to propagate root span context.
7273
* Only a single root span can be open at a time.
7374
*/
75+
SINGULAR = 'singular',
76+
/**
77+
* Do not write root span context; in other words, querying the current root
78+
* span context will always result in a default value.
79+
*/
7480
NONE = 'none'
7581
}
7682

@@ -120,6 +126,10 @@ export class TraceCLS implements CLS<RootContext> {
120126
this.CLSClass = AsyncListenerCLS;
121127
this.rootSpanStackOffset = 8;
122128
break;
129+
case TraceCLSMechanism.SINGULAR:
130+
this.CLSClass = SingularCLS;
131+
this.rootSpanStackOffset = 4;
132+
break;
123133
case TraceCLSMechanism.NONE:
124134
this.CLSClass = NullCLS;
125135
this.rootSpanStackOffset = 4;

src/cls/singular.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright 2018 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {EventEmitter} from 'events';
18+
19+
import {CLS, Func} from './base';
20+
21+
/**
22+
* A trivial implementation of continuation-local storage where everything is
23+
* in the same continuation. Therefore, only one unique value can be stored at
24+
* a time.
25+
*/
26+
export class SingularCLS<Context> implements CLS<Context> {
27+
private enabled = false;
28+
private currentContext: Context;
29+
30+
constructor(private readonly defaultContext: Context) {
31+
this.currentContext = this.defaultContext;
32+
}
33+
34+
isEnabled(): boolean {
35+
return this.enabled;
36+
}
37+
38+
enable(): void {
39+
this.enabled = true;
40+
}
41+
42+
disable(): void {
43+
this.enabled = false;
44+
this.setContext(this.defaultContext);
45+
}
46+
47+
getContext(): Context {
48+
return this.currentContext;
49+
}
50+
51+
setContext(value: Context): void {
52+
if (this.enabled) {
53+
this.currentContext = value;
54+
}
55+
}
56+
57+
runWithNewContext<T>(fn: Func<T>): T {
58+
return fn();
59+
}
60+
61+
bindWithCurrentContext<T>(fn: Func<T>): Func<T> {
62+
return fn;
63+
}
64+
65+
patchEmitterToPropagateContext(ee: EventEmitter): void {}
66+
}

src/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as path from 'path';
1919
const pluginDirectory =
2020
path.join(path.resolve(__dirname, '..'), 'src', 'plugins');
2121

22-
export type CLSMechanism = 'none'|'auto';
22+
export type CLSMechanism = 'auto'|'none'|'singular';
2323

2424
/** Available configuration options. */
2525
export interface Config {
@@ -30,6 +30,10 @@ export interface Config {
3030
* _and_ the environment variable GCLOUD_TRACE_NEW_CONTEXT is set, in which
3131
* case async_hooks will be used instead.
3232
* - 'none' disables CLS completely.
33+
* - 'singular' allows one root span to exist at a time. This option is meant
34+
* to be used internally by Google Cloud Functions, or in any other
35+
* environment where it is guaranteed that only one request is being served
36+
* at a time.
3337
* The 'auto' mechanism is used by default if this configuration option is
3438
* not explicitly set.
3539
*/

test/test-cls.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {AsyncHooksCLS} from '../src/cls/async-hooks';
2525
import {AsyncListenerCLS} from '../src/cls/async-listener';
2626
import {CLS} from '../src/cls/base';
2727
import {NullCLS} from '../src/cls/null';
28+
import {SingularCLS} from '../src/cls/singular';
2829
import {SpanDataType} from '../src/constants';
2930
import {createStackTrace, FORCE_NEW} from '../src/util';
3031

@@ -236,36 +237,44 @@ describe('Continuation-Local Storage', () => {
236237
});
237238
});
238239
}
240+
241+
describe('SingularCLS', () => {
242+
it('uses a single global context', async () => {
243+
const cls = new SingularCLS('default');
244+
cls.enable();
245+
cls.runWithNewContext(() => {
246+
cls.setContext('modified');
247+
});
248+
await Promise.resolve();
249+
cls.runWithNewContext(() => {
250+
assert.strictEqual(cls.getContext(), 'modified');
251+
});
252+
});
253+
});
239254
});
240255

241256
describe('TraceCLS', () => {
242257
const validTestCases:
243-
Array<{config: TraceCLSConfig, expectedDefaultType: SpanDataType}> =
244-
asyncAwaitSupported ?
245-
[
246-
{
247-
config: {mechanism: TraceCLSMechanism.ASYNC_HOOKS},
248-
expectedDefaultType: SpanDataType.UNCORRELATED
249-
},
258+
Array<{config: TraceCLSConfig, expectedDefaultType: SpanDataType}> = [
250259
{
251260
config: {mechanism: TraceCLSMechanism.ASYNC_LISTENER},
252261
expectedDefaultType: SpanDataType.UNCORRELATED
253262
},
254263
{
255-
config: {mechanism: TraceCLSMechanism.NONE},
256-
expectedDefaultType: SpanDataType.UNTRACED
257-
}
258-
] :
259-
[
260-
{
261-
config: {mechanism: TraceCLSMechanism.ASYNC_LISTENER},
264+
config: {mechanism: TraceCLSMechanism.SINGULAR},
262265
expectedDefaultType: SpanDataType.UNCORRELATED
263266
},
264267
{
265268
config: {mechanism: TraceCLSMechanism.NONE},
266269
expectedDefaultType: SpanDataType.UNTRACED
267270
}
268271
];
272+
if (asyncAwaitSupported) {
273+
validTestCases.push({
274+
config: {mechanism: TraceCLSMechanism.ASYNC_HOOKS},
275+
expectedDefaultType: SpanDataType.UNCORRELATED
276+
});
277+
}
269278
for (const testCase of validTestCases) {
270279
describe(`with configuration ${inspect(testCase)}`, () => {
271280
const logger = new TestLogger();

test/test-config-cls.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ describe('Behavior set by config for context propagation mechanism', () => {
6767
tracingConfig: {},
6868
contextPropagationConfig: {mechanism: autoMechanism}
6969
},
70+
{
71+
tracingConfig: {clsMechanism: 'singular'},
72+
contextPropagationConfig: {mechanism: 'singular'}
73+
},
7074
{
7175
// tslint:disable:no-any
7276
tracingConfig: {clsMechanism: 'unknown' as any},

0 commit comments

Comments
 (0)