diff --git a/src/index.ts b/src/index.ts index 2f808e613..6a163134a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,16 +19,74 @@ const filesLoadedBeforeTrace = Object.keys(require.cache); // This file's top-level imports must not transitively depend on modules that // do I/O, or continuation-local-storage will not work. import * as semver from 'semver'; -import {Config} from './config'; +import {Config, defaultConfig} from './config'; +import * as extend from 'extend'; import * as path from 'path'; import * as PluginTypes from './plugin-types'; -import {tracing, Tracing} from './tracing'; -import {Singleton} from './util'; +import {tracing, Tracing, NormalizedConfig} from './tracing'; +import {Singleton, FORCE_NEW, Forceable} from './util'; +import {Constants} from './constants'; export {Config, PluginTypes}; let tracingSingleton: typeof tracing; +/** + * Normalizes the user-provided configuration object by adding default values + * and overriding with env variables when they are provided. + * @param projectConfig The user-provided configuration object. It will not + * be modified. + * @return A normalized configuration object. + */ +function initConfig(projectConfig: Forceable): + Forceable { + // `|| undefined` prevents environmental variables that are empty strings + // from overriding values provided in the config object passed to start(). + const envConfig = { + logLevel: Number(process.env.GCLOUD_TRACE_LOGLEVEL) || undefined, + projectId: process.env.GCLOUD_PROJECT || undefined, + serviceContext: { + service: + process.env.GAE_SERVICE || process.env.GAE_MODULE_NAME || undefined, + version: process.env.GAE_VERSION || process.env.GAE_MODULE_VERSION || + undefined, + minorVersion: process.env.GAE_MINOR_VERSION || undefined + } + }; + + let envSetConfig: Config = {}; + if (!!process.env.GCLOUD_TRACE_CONFIG) { + envSetConfig = + require(path.resolve(process.env.GCLOUD_TRACE_CONFIG!)) as Config; + } + // Configuration order of precedence: + // 1. Environment Variables + // 2. Project Config + // 3. Environment Variable Set Configuration File (from GCLOUD_TRACE_CONFIG) + // 4. Default Config (as specified in './config') + const config = extend( + true, {[FORCE_NEW]: projectConfig[FORCE_NEW]}, defaultConfig, + envSetConfig, projectConfig, envConfig, {plugins: {}}); + // The empty plugins object guarantees that plugins is a plain object, + // even if it's explicitly specified in the config to be a non-object. + + // Enforce the upper limit for the label value size. + if (config.maximumLabelValueSize > + Constants.TRACE_SERVICE_LABEL_VALUE_LIMIT) { + config.maximumLabelValueSize = Constants.TRACE_SERVICE_LABEL_VALUE_LIMIT; + } + + // If the CLS mechanism is set to auto-determined, decide now what it should + // be. + const ahAvailable = semver.satisfies(process.version, '>=8') && + process.env.GCLOUD_TRACE_NEW_CONTEXT; + if (config.clsMechanism === 'auto') { + config.clsMechanism = ahAvailable ? 'async-hooks' : 'async-listener'; + } + + return config; +} + /** * Start the Stackdriver Trace Agent with the given configuration (if provided). * This function should only be called once, and before any other modules are @@ -43,18 +101,15 @@ let tracingSingleton: typeof tracing; * trace.start(); */ export function start(config?: Config): PluginTypes.TraceAgent { + const normalizedConfig = initConfig(config || {}); // Determine the preferred context propagation mechanism, as // continuation-local-storage should be loaded before any modules that do I/O. - const ahAvailable = semver.satisfies(process.version, '>=8') && - process.env.GCLOUD_TRACE_NEW_CONTEXT; - const agentEnabled = !config || config.enabled !== false; - const alAutoPreferred = - !ahAvailable && (!config || config.clsMechanism === 'auto'); - const alUserPreferred = config && (config.clsMechanism === 'async-listener'); - if (agentEnabled && (alAutoPreferred || alUserPreferred)) { + if (normalizedConfig.enabled && + normalizedConfig.clsMechanism === 'async-listener') { // This is the earliest we can load continuation-local-storage. require('continuation-local-storage'); } + if (!tracingSingleton) { tracingSingleton = require('./tracing').tracing; } @@ -62,7 +117,7 @@ export function start(config?: Config): PluginTypes.TraceAgent { try { let tracing: Tracing; try { - tracing = tracingSingleton.create(config || {}, {}); + tracing = tracingSingleton.create(normalizedConfig, {}); } catch (e) { // An error could be thrown if create() is called multiple times. // It's not a helpful error message for the end user, so make it more diff --git a/src/tracing.ts b/src/tracing.ts index 2c3383016..3341b5dae 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -15,7 +15,6 @@ */ import * as common from '@google-cloud/common'; -import * as extend from 'extend'; import * as path from 'path'; import * as semver from 'semver'; @@ -28,14 +27,15 @@ import {pluginLoader, PluginLoaderConfig} from './trace-plugin-loader'; import {traceWriter, TraceWriterConfig} from './trace-writer'; import {Component, FORCE_NEW, Forceable, packageNameFromPath, Singleton} from './util'; -interface TopLevelConfig { +export interface TopLevelConfig { enabled: boolean; logLevel: number; clsMechanism: CLSMechanism; } // PluginLoaderConfig extends TraceAgentConfig -type NormalizedConfig = TraceWriterConfig&PluginLoaderConfig&TopLevelConfig; +export type NormalizedConfig = + (TraceWriterConfig&PluginLoaderConfig&TopLevelConfig)|{enabled: false}; /** * A class that represents automatic tracing. @@ -50,66 +50,22 @@ export class Tracing implements Component { * Constructs a new Tracing instance. * @param config The configuration for this instance. */ - constructor(config: Config) { - this.config = Tracing.initConfig(config); + constructor(config: NormalizedConfig) { + this.config = config; + let logLevel = config.enabled ? config.logLevel : 0; + // Clamp the logger level. + if (logLevel < 0) { + logLevel = 0; + } else if (logLevel >= common.logger.LEVELS.length) { + logLevel = common.logger.LEVELS.length - 1; + } this.logger = common.logger({ - level: common.logger.LEVELS[this.config.logLevel], + level: common.logger.LEVELS[logLevel], tag: '@google-cloud/trace-agent' }); } - /** - * Normalizes the user-provided configuration object by adding default values - * and overriding with env variables when they are provided. - * @param projectConfig The user-provided configuration object. It will not - * be modified. - * @return A normalized configuration object. - */ - private static initConfig(projectConfig: Forceable): - Forceable { - // `|| undefined` prevents environmental variables that are empty strings - // from overriding values provided in the config object passed to start(). - const envConfig = { - logLevel: Number(process.env.GCLOUD_TRACE_LOGLEVEL) || undefined, - projectId: process.env.GCLOUD_PROJECT || undefined, - serviceContext: { - service: - process.env.GAE_SERVICE || process.env.GAE_MODULE_NAME || undefined, - version: process.env.GAE_VERSION || process.env.GAE_MODULE_VERSION || - undefined, - minorVersion: process.env.GAE_MINOR_VERSION || undefined - } - }; - let envSetConfig: Config = {}; - if (!!process.env.GCLOUD_TRACE_CONFIG) { - envSetConfig = - require(path.resolve(process.env.GCLOUD_TRACE_CONFIG!)) as Config; - } - // Configuration order of precedence: - // 1. Environment Variables - // 2. Project Config - // 3. Environment Variable Set Configuration File (from GCLOUD_TRACE_CONFIG) - // 4. Default Config (as specified in './config') - const config = extend( - true, {[FORCE_NEW]: projectConfig[FORCE_NEW]}, defaultConfig, - envSetConfig, projectConfig, envConfig, {plugins: {}}); - // The empty plugins object guarantees that plugins is a plain object, - // even if it's explicitly specified in the config to be a non-object. - - // Enforce the upper limit for the label value size. - if (config.maximumLabelValueSize > - Constants.TRACE_SERVICE_LABEL_VALUE_LIMIT) { - config.maximumLabelValueSize = Constants.TRACE_SERVICE_LABEL_VALUE_LIMIT; - } - // Clamp the logger level. - if (config.logLevel < 0) { - config.logLevel = 0; - } else if (config.logLevel >= common.logger.LEVELS.length) { - config.logLevel = common.logger.LEVELS.length - 1; - } - return config; - } /** * Logs an error message detailing the list of modules that were loaded before @@ -152,14 +108,8 @@ export class Tracing implements Component { try { // Initialize context propagation mechanism. - const m = this.config.clsMechanism; - const ahAvailable = semver.satisfies(process.version, '>=8') && - process.env.GCLOUD_TRACE_NEW_CONTEXT; const clsConfig: Forceable = { - mechanism: m === 'auto' ? - (ahAvailable ? TraceCLSMechanism.ASYNC_HOOKS : - TraceCLSMechanism.ASYNC_LISTENER) : - m as TraceCLSMechanism, + mechanism: this.config.clsMechanism as TraceCLSMechanism, [FORCE_NEW]: this.config[FORCE_NEW] }; cls.create(clsConfig, this.logger).enable();