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

Commit f34aac5

Browse files
authored
feat: add options to set the cls mechanism to async-hooks or async-listener (#741)
PR-URL: #741
1 parent ebd33d8 commit f34aac5

6 files changed

Lines changed: 238 additions & 155 deletions

File tree

src/config.ts

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

22-
export type CLSMechanism = 'auto'|'none'|'singular';
22+
export type CLSMechanism =
23+
'async-hooks'|'async-listener'|'auto'|'none'|'singular';
2324

2425
/** Available configuration options. */
2526
export interface Config {
2627
/**
2728
* The trace context propagation mechanism to use. The following options are
2829
* available:
29-
* - 'auto' uses continuation-local-storage, unless async_hooks is available
30-
* _and_ the environment variable GCLOUD_TRACE_NEW_CONTEXT is set, in which
31-
* case async_hooks will be used instead.
30+
* - 'async-hooks' uses an implementation of CLS on top of the Node core
31+
* `async_hooks` module in Node 8+. This option should not be used if the
32+
* Node binary version requirements are not met.
33+
* - 'async-listener' uses an implementation of CLS on top of the
34+
* `continuation-local-storage` module.
35+
* - 'auto' behaves like 'async-hooks' on Node 8+ when the
36+
* GCLOUD_TRACE_NEW_CONTEXT env variable is set, and 'async-listener'
37+
* otherwise.
3238
* - 'none' disables CLS completely.
3339
* - 'singular' allows one root span to exist at a time. This option is meant
3440
* to be used internally by Google Cloud Functions, or in any other

src/index.ts

Lines changed: 52 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -16,51 +16,21 @@
1616

1717
const filesLoadedBeforeTrace = Object.keys(require.cache);
1818

19-
// semver does not require any core modules.
19+
// This file's top-level imports must not transitively depend on modules that
20+
// do I/O, or continuation-local-storage will not work.
2021
import * as semver from 'semver';
21-
22-
const useAH = !!process.env.GCLOUD_TRACE_NEW_CONTEXT &&
23-
semver.satisfies(process.version, '>=8');
24-
if (!useAH) {
25-
// This should be loaded before any core modules.
26-
require('continuation-local-storage');
27-
}
28-
29-
import * as common from '@google-cloud/common';
30-
import {cls, TraceCLSConfig, TraceCLSMechanism} from './cls';
31-
import {Constants} from './constants';
32-
import {Config, defaultConfig, CLSMechanism} from './config';
22+
import {Config, defaultConfig} from './config';
3323
import * as extend from 'extend';
3424
import * as path from 'path';
3525
import * as PluginTypes from './plugin-types';
36-
import {PluginLoaderConfig} from './trace-plugin-loader';
37-
import {pluginLoader} from './trace-plugin-loader';
26+
import {tracing, Tracing, NormalizedConfig} from './tracing';
27+
import {Singleton, FORCE_NEW, Forceable} from './util';
28+
import {Constants} from './constants';
3829
import {TraceAgent} from './trace-api';
39-
import {traceWriter, TraceWriterConfig} from './trace-writer';
40-
import {Forceable, FORCE_NEW, packageNameFromPath} from './util';
4130

4231
export {Config, PluginTypes};
4332

44-
const traceAgent: TraceAgent = new TraceAgent('Custom Trace API');
45-
46-
const modulesLoadedBeforeTrace: string[] = [];
47-
const traceModuleName = path.join('@google-cloud', 'trace-agent');
48-
for (let i = 0; i < filesLoadedBeforeTrace.length; i++) {
49-
const moduleName = packageNameFromPath(filesLoadedBeforeTrace[i]);
50-
if (moduleName && moduleName !== traceModuleName &&
51-
modulesLoadedBeforeTrace.indexOf(moduleName) === -1) {
52-
modulesLoadedBeforeTrace.push(moduleName);
53-
}
54-
}
55-
56-
interface TopLevelConfig {
57-
enabled: boolean;
58-
logLevel: number;
59-
clsMechanism: CLSMechanism;
60-
}
61-
62-
// PluginLoaderConfig extends TraceAgentConfig
63-
type NormalizedConfig = TraceWriterConfig&PluginLoaderConfig&TopLevelConfig;
33+
let traceAgent: TraceAgent;
6434

6535
/**
6636
* Normalizes the user-provided configuration object by adding default values
@@ -106,122 +76,73 @@ function initConfig(projectConfig: Forceable<Config>):
10676
Constants.TRACE_SERVICE_LABEL_VALUE_LIMIT) {
10777
config.maximumLabelValueSize = Constants.TRACE_SERVICE_LABEL_VALUE_LIMIT;
10878
}
109-
// Clamp the logger level.
110-
if (config.logLevel < 0) {
111-
config.logLevel = 0;
112-
} else if (config.logLevel >= common.logger.LEVELS.length) {
113-
config.logLevel = common.logger.LEVELS.length - 1;
114-
}
115-
return config;
116-
}
11779

118-
/**
119-
* Stops the Trace Agent. This disables the publicly exposed agent instance,
120-
* as well as any instances passed to plugins. This also prevents the Trace
121-
* Writer from publishing additional traces.
122-
*/
123-
function stop() {
124-
if (pluginLoader.exists()) {
125-
pluginLoader.get().deactivate();
126-
}
127-
if (traceAgent && traceAgent.isActive()) {
128-
traceAgent.disable();
129-
}
130-
if (cls.exists()) {
131-
cls.get().disable();
132-
}
133-
if (traceWriter.exists()) {
134-
traceWriter.get().stop();
80+
// If the CLS mechanism is set to auto-determined, decide now what it should
81+
// be.
82+
const ahAvailable = semver.satisfies(process.version, '>=8') &&
83+
process.env.GCLOUD_TRACE_NEW_CONTEXT;
84+
if (config.clsMechanism === 'auto') {
85+
config.clsMechanism = ahAvailable ? 'async-hooks' : 'async-listener';
13586
}
87+
88+
return config;
13689
}
13790

13891
/**
139-
* Start the Trace agent that will make your application available for
140-
* tracing with Stackdriver Trace.
141-
*
142-
* @param config - Trace configuration
92+
* Start the Stackdriver Trace Agent with the given configuration (if provided).
93+
* This function should only be called once, and before any other modules are
94+
* loaded.
95+
* @param config A configuration object.
96+
* @returns An object exposing functions for creating custom spans.
14397
*
14498
* @resource [Introductory video]{@link
14599
* https://www.youtube.com/watch?v=NCFDqeo7AeY}
146100
*
147101
* @example
148102
* trace.start();
149103
*/
150-
export function start(projectConfig?: Config): PluginTypes.TraceAgent {
151-
const config = initConfig(projectConfig || {});
152-
153-
if (traceAgent.isActive() && !config[FORCE_NEW]) { // already started.
154-
throw new Error('Cannot call start on an already started agent.');
155-
} else if (traceAgent.isActive()) {
156-
// For unit tests only.
157-
// Undoes initialization that occurred last time start() was called.
158-
stop();
104+
export function start(config?: Config): PluginTypes.TraceAgent {
105+
const normalizedConfig = initConfig(config || {});
106+
// Determine the preferred context propagation mechanism, as
107+
// continuation-local-storage should be loaded before any modules that do I/O.
108+
if (normalizedConfig.enabled &&
109+
normalizedConfig.clsMechanism === 'async-listener') {
110+
// This is the earliest we can load continuation-local-storage.
111+
require('continuation-local-storage');
159112
}
160113

161-
if (!config.enabled) {
162-
return traceAgent;
163-
}
164-
165-
const logger = common.logger({
166-
level: common.logger.LEVELS[config.logLevel],
167-
tag: '@google-cloud/trace-agent'
168-
});
169-
170-
if (modulesLoadedBeforeTrace.length > 0) {
171-
logger.error(
172-
'TraceAgent#start: Tracing might not work as the following modules',
173-
'were loaded before the trace agent was initialized:',
174-
`[${modulesLoadedBeforeTrace.sort().join(', ')}]`);
175-
// Stop storing these entries in memory
176-
filesLoadedBeforeTrace.length = 0;
177-
modulesLoadedBeforeTrace.length = 0;
114+
if (!traceAgent) {
115+
traceAgent = new (require('./trace-api').TraceAgent)();
178116
}
179117

180118
try {
181-
// Initialize context propagation mechanism.
182-
const m = config.clsMechanism;
183-
const clsConfig: Forceable<TraceCLSConfig> = {
184-
mechanism: m === 'auto' ? (useAH ? TraceCLSMechanism.ASYNC_HOOKS :
185-
TraceCLSMechanism.ASYNC_LISTENER) :
186-
m as TraceCLSMechanism,
187-
[FORCE_NEW]: config[FORCE_NEW]
188-
};
189-
cls.create(clsConfig, logger).enable();
190-
191-
traceWriter.create(config, logger).initialize((err) => {
192-
if (err) {
193-
stop();
194-
}
195-
});
196-
197-
traceAgent.enable(config, logger);
198-
199-
pluginLoader.create(config, logger).activate();
200-
} catch (e) {
201-
logger.error(
202-
'TraceAgent#start: Disabling the Trace Agent for the',
203-
`following reason: ${e.message}`);
204-
stop();
205-
return traceAgent;
206-
}
207-
208-
if (typeof config.projectId !== 'string' &&
209-
typeof config.projectId !== 'undefined') {
210-
logger.error(
211-
'TraceAgent#start: config.projectId, if provided, must be a string.',
212-
'Disabling trace agent.');
213-
stop();
119+
let tracing: Tracing;
120+
try {
121+
tracing =
122+
require('./tracing').tracing.create(normalizedConfig, traceAgent);
123+
} catch (e) {
124+
// An error could be thrown if create() is called multiple times.
125+
// It's not a helpful error message for the end user, so make it more
126+
// useful here.
127+
throw new Error('Cannot call start on an already created agent.');
128+
}
129+
tracing.enable();
130+
tracing.logModulesLoadedBeforeTrace(filesLoadedBeforeTrace);
214131
return traceAgent;
132+
} finally {
133+
// Stop storing these entries in memory
134+
filesLoadedBeforeTrace.length = 0;
215135
}
216-
217-
// Make trace agent available globally without requiring package
218-
global._google_trace_agent = traceAgent;
219-
220-
logger.info('TraceAgent#start: Trace Agent activated.');
221-
return traceAgent;
222136
}
223137

138+
/**
139+
* Get the previously created TraceAgent object.
140+
* @returns An object exposing functions for creating custom spans.
141+
*/
224142
export function get(): PluginTypes.TraceAgent {
143+
if (!traceAgent) {
144+
traceAgent = new (require('./trace-api').TraceAgent)();
145+
}
225146
return traceAgent;
226147
}
227148

src/trace-plugin-loader.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,6 @@ export class PluginLoader {
420420
}
421421
this.internalState = PluginLoaderState.DEACTIVATED;
422422
this.logger.info(`PluginLoader#deactivate: Deactivated.`);
423-
} else {
424-
throw new Error('Plugin loader is not activated.');
425423
}
426424
return this;
427425
}

0 commit comments

Comments
 (0)