diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index d25839061b..dbdd0d1786 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to experimental packages in this project will be documented * feat(instrumentation-grpc): added grpc metadata client side attributes in instrumentation [#3386](https://github.com/open-telemetry/opentelemetry-js/pull/3386) * feat(instrumentation): add new `_setMeterInstruments` protected method that update the meter instruments every meter provider update. * feat(api-logs): add the `SeverityNumber` enumeration. [#3443](https://github.com/open-telemetry/opentelemetry-js/pull/3443/) @fuaiyi +* feat(sdk-node): configure no-op sdk with `OTEL_SDK_DISABLED` environment variable [#3485](https://github.com/open-telemetry/opentelemetry-js/pull/3485/files/2211c78aec39aeb6b4b3dae71844edf8ce234d20) @RazGvili ### :bug: (Bug Fix) diff --git a/experimental/packages/opentelemetry-sdk-node/README.md b/experimental/packages/opentelemetry-sdk-node/README.md index 0d4f3c0f7a..0bac781143 100644 --- a/experimental/packages/opentelemetry-sdk-node/README.md +++ b/experimental/packages/opentelemetry-sdk-node/README.md @@ -139,6 +139,10 @@ Configure tracing parameters. These are the same trace parameters used to [confi Configure the [service name](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service). +## Disable the SDK from the environment + +Disable the SDK by setting the `OTEL_SDK_DISABLED` environment variable to `true`. + ## Configure Trace Exporter from Environment This is an alternative to programmatically configuring an exporter or span processor. This package will auto setup the default `otlp` exporter with `http/protobuf` protocol if `traceExporter` or `spanProcessor` hasn't been passed into the `NodeSDK` constructor. diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index ca8ae65573..a3af268442 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -39,6 +39,7 @@ import { import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { NodeSDKConfiguration } from './types'; import { TracerProviderWithEnvExporters } from './TracerProviderWithEnvExporter'; +import { getEnv } from '@opentelemetry/core'; /** This class represents everything needed to register a fully configured OpenTelemetry Node.js SDK */ @@ -71,10 +72,18 @@ export class NodeSDK { private _meterProvider?: MeterProvider; private _serviceName?: string; + private _disabled?: boolean; + /** * Create a new NodeJS SDK instance */ public constructor(configuration: Partial = {}) { + if (getEnv().OTEL_SDK_DISABLED) { + this._disabled = true; + // Functions with possible side-effects are set + // to no-op via the _disabled flag + } + this._resource = configuration.resource ?? new Resource({}); this._resourceDetectors = configuration.resourceDetectors ?? [ envDetector, @@ -175,6 +184,10 @@ export class NodeSDK { /** Detect resource attributes */ public async detectResources(): Promise { + if (this._disabled) { + return; + } + const internalConfig: ResourceDetectionConfig = { detectors: this._resourceDetectors, }; @@ -191,6 +204,10 @@ export class NodeSDK { * Once the SDK has been configured, call this method to construct SDK components and register them with the OpenTelemetry API. */ public async start(): Promise { + if (this._disabled) { + return; + } + if (this._autoDetectResources) { await this.detectResources(); } diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index a37f33f809..bad3d7f4b7 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -585,6 +585,78 @@ describe('Node SDK', () => { delete process.env.OTEL_RESOURCE_ATTRIBUTES; }); }); + + describe('A disabled SDK should be no-op', () => { + beforeEach(() => { + env.OTEL_SDK_DISABLED = 'true'; + }); + + afterEach(() => { + delete env.OTEL_SDK_DISABLED; + }); + + it('should not register a trace provider', async () => { + const sdk = new NodeSDK({}); + await sdk.start(); + + assert.strictEqual( + (trace.getTracerProvider() as ProxyTracerProvider).getDelegate(), + delegate, + 'sdk.start() should not change the global tracer provider' + ); + + await sdk.shutdown(); + }); + + it('should not register a meter provider if a reader is provided', async () => { + const exporter = new ConsoleMetricExporter(); + const metricReader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100, + }); + + const sdk = new NodeSDK({ + metricReader: metricReader, + autoDetectResources: false, + }); + await sdk.start(); + + assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + + await sdk.shutdown(); + }); + + describe('detectResources should be no-op', async () => { + beforeEach(() => { + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'service.instance.id=627cc493,service.name=my-service,service.namespace=default,service.version=0.0.1'; + }); + + afterEach(() => { + delete process.env.OTEL_RESOURCE_ATTRIBUTES; + }); + + it('detectResources will not read resources from env or manually', async () => { + const sdk = new NodeSDK({ + autoDetectResources: true, + resourceDetectors: [ + processDetector, + { + async detect(): Promise { + return new Resource({ customAttr: 'someValue' }); + }, + }, + envDetector, + ], + }); + await sdk.detectResources(); + const resource = sdk['_resource']; + + assert.deepStrictEqual(resource, Resource.empty()); + }); + }); + }); }); describe('setup exporter from env', () => { diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index b576c13c8f..36cfe0f702 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -24,6 +24,18 @@ const DEFAULT_LIST_SEPARATOR = ','; * Environment interface to define all names */ +const ENVIRONMENT_BOOLEAN_KEYS = ['OTEL_SDK_DISABLED'] as const; + +type ENVIRONMENT_BOOLEANS = { + [K in typeof ENVIRONMENT_BOOLEAN_KEYS[number]]?: boolean; +}; + +function isEnvVarABoolean(key: unknown): key is keyof ENVIRONMENT_BOOLEANS { + return ( + ENVIRONMENT_BOOLEAN_KEYS.indexOf(key as keyof ENVIRONMENT_BOOLEANS) > -1 + ); +} + const ENVIRONMENT_NUMBERS_KEYS = [ 'OTEL_BSP_EXPORT_TIMEOUT', 'OTEL_BSP_MAX_EXPORT_BATCH_SIZE', @@ -107,7 +119,8 @@ export type ENVIRONMENT = { OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?: string; OTEL_EXPORTER_OTLP_METRICS_PROTOCOL?: string; OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE?: string; -} & ENVIRONMENT_NUMBERS & +} & ENVIRONMENT_BOOLEANS & + ENVIRONMENT_NUMBERS & ENVIRONMENT_LISTS; export type RAW_ENVIRONMENT = { @@ -122,6 +135,7 @@ export const DEFAULT_ATTRIBUTE_COUNT_LIMIT = 128; * Default environment variables */ export const DEFAULT_ENVIRONMENT: Required = { + OTEL_SDK_DISABLED: false, CONTAINER_NAME: '', ECS_CONTAINER_METADATA_URI_V4: '', ECS_CONTAINER_METADATA_URI: '', @@ -182,6 +196,25 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: 'cumulative', }; +/** + * @param key + * @param environment + * @param values + */ +function parseBoolean( + key: keyof ENVIRONMENT_BOOLEANS, + environment: ENVIRONMENT, + values: RAW_ENVIRONMENT +) { + if (typeof values[key] === 'undefined') { + return; + } + + const value = String(values[key]); + // support case-insensitive "true" + environment[key] = value.toLowerCase() === 'true'; +} + /** * Parses a variable as number with number validation * @param name @@ -277,7 +310,9 @@ export function parseEnvironment(values: RAW_ENVIRONMENT): ENVIRONMENT { break; default: - if (isEnvVarANumber(key)) { + if (isEnvVarABoolean(key)) { + parseBoolean(key, environment, values); + } else if (isEnvVarANumber(key)) { parseNumber(key, environment, values); } else if (isEnvVarAList(key)) { parseStringList(key, environment, values); diff --git a/packages/opentelemetry-core/test/utils/environment.test.ts b/packages/opentelemetry-core/test/utils/environment.test.ts index 2517a4e99e..8fa8b08f1e 100644 --- a/packages/opentelemetry-core/test/utils/environment.test.ts +++ b/packages/opentelemetry-core/test/utils/environment.test.ts @@ -75,6 +75,7 @@ describe('environment', () => { HOSTNAME: 'hostname', KUBERNETES_SERVICE_HOST: 'https://k8s.host/', NAMESPACE: 'namespace', + OTEL_SDK_DISABLED: 'true', OTEL_BSP_MAX_EXPORT_BATCH_SIZE: 40, OTEL_BSP_SCHEDULE_DELAY: 50, OTEL_EXPORTER_JAEGER_AGENT_HOST: 'host.domain.com', @@ -98,6 +99,7 @@ describe('environment', () => { }); const env = getEnv(); assert.deepStrictEqual(env.OTEL_NO_PATCH_MODULES, ['a', 'b', 'c']); + assert.strictEqual(env.OTEL_SDK_DISABLED, true); assert.strictEqual(env.OTEL_LOG_LEVEL, DiagLogLevel.ERROR); assert.strictEqual(env.OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, 40); assert.strictEqual(env.OTEL_ATTRIBUTE_COUNT_LIMIT, 50); @@ -134,6 +136,27 @@ describe('environment', () => { assert.strictEqual(env.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, 12000); }); + it('should parse OTEL_SDK_DISABLED truthy value despite casing', () => { + mockEnvironment({ + OTEL_SDK_DISABLED: 'TrUe', + }); + const env = getEnv(); + assert.strictEqual(env.OTEL_SDK_DISABLED, true); + }); + + describe('OTEL_SDK_DISABLED falsy values', () => { + const falsyValues = ['False', '']; + for (const falsyValue of falsyValues) { + it(`should parse falsy value: ${falsyValue}`, () => { + mockEnvironment({ + OTEL_SDK_DISABLED: falsyValue, + }); + const env = getEnv(); + assert.strictEqual(env.OTEL_SDK_DISABLED, false); + }); + } + }); + it('should parse OTEL_LOG_LEVEL despite casing', () => { mockEnvironment({ OTEL_LOG_LEVEL: 'waRn',