From 6dcde289ca9bd9afdf372c87ea19ab2ec972e04e Mon Sep 17 00:00:00 2001 From: maryliag Date: Wed, 10 Apr 2024 16:40:43 -0400 Subject: [PATCH] feat(sdk-node): add serviceInstanceIDDetector to NodeSDK Follow up from #4608 Adds the resource detector ServiceInstanceIDDetector on the NodeSDK constructor. It only gets added by default on any of those conditions: - the value `serviceinstance` is part of the list `OTEL_NODE_RESOURCE_DETECTORS` - `OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID` is set to `true` --- CHANGELOG.md | 8 ++ .../opentelemetry-sdk-node/src/sdk.ts | 23 ++++-- .../opentelemetry-sdk-node/src/utils.ts | 50 ++++++++++++ .../opentelemetry-sdk-node/test/sdk.test.ts | 81 ++++++++++++++++--- .../test/util/resource-assertions.ts | 12 +++ .../src/utils/environment.ts | 8 +- .../src/platform/browser/index.ts | 3 +- .../src/platform/node/index.ts | 3 +- 8 files changed, 167 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5acc2d69ee0..abbcf3e721c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ * feat(sdk-trace-base): log resource attributes in ConsoleSpanExporter [#4605](https://github.com/open-telemetry/opentelemetry-js/pull/4605) @pichlermarc * feat(resources): new detector ServiceInstanceIDDetector that sets the value for `service.instance.id` as random UUID. +* feat(resources): add usage for the detector ServiceInstanceIDDetector. + * The resource detector can be added to default resource detector list by + * setting the environment variable `OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID` as `true` + * adding the value `serviceinstance` to the list of resource detectors on the environment variable `OTEL_NODE_RESOURCE_DETECTORS`, e.g `OTEL_NODE_RESOURCE_DETECTORS=env,host,os,serviceinstance` + * The value can be overwritten by + * merging a resource containing the `service.instance.id` attribute + * setting `service.instance.id` via the `OTEL_RESOURCE_ATTRIBUTES` environment variable when using `envDetector` + * using another resource detector which writes `service.instance.id` ### :bug: (Bug Fix) diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index c5f38d63a91..4a5dbc255e6 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -36,6 +36,7 @@ import { processDetector, Resource, ResourceDetectionConfig, + serviceInstanceIDDetector, } from '@opentelemetry/resources'; import { LogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs'; import { MeterProvider, MetricReader, View } from '@opentelemetry/sdk-metrics'; @@ -51,7 +52,10 @@ import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; import { NodeSDKConfiguration } from './types'; import { TracerProviderWithEnvExporters } from './TracerProviderWithEnvExporter'; import { getEnv, getEnvWithoutDefaults } from '@opentelemetry/core'; -import { parseInstrumentationOptions } from './utils'; +import { + getResourceDetectorsFromEnv, + parseInstrumentationOptions, +} from './utils'; /** This class represents everything needed to register a fully configured OpenTelemetry Node.js SDK */ @@ -121,11 +125,18 @@ export class NodeSDK { this._configuration = configuration; this._resource = configuration.resource ?? new Resource({}); - this._resourceDetectors = configuration.resourceDetectors ?? [ - envDetector, - processDetector, - hostDetector, - ]; + let defaultDetectors: (Detector | DetectorSync)[] = []; + if (env.OTEL_NODE_RESOURCE_DETECTORS.length > 0) { + defaultDetectors = getResourceDetectorsFromEnv(); + } else { + defaultDetectors = [envDetector, processDetector, hostDetector]; + if (env.OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID) { + defaultDetectors.push(serviceInstanceIDDetector); + } + } + + this._resourceDetectors = + configuration.resourceDetectors ?? defaultDetectors; this._serviceName = configuration.serviceName; diff --git a/experimental/packages/opentelemetry-sdk-node/src/utils.ts b/experimental/packages/opentelemetry-sdk-node/src/utils.ts index a3d83147477..446995134d8 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/utils.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/utils.ts @@ -14,10 +14,20 @@ * limitations under the License. */ +import { diag } from '@opentelemetry/api'; import { Instrumentation, InstrumentationOption, } from '@opentelemetry/instrumentation'; +import { + Detector, + DetectorSync, + envDetectorSync, + hostDetectorSync, + osDetectorSync, + processDetectorSync, + serviceInstanceIDDetectorSync, +} from '@opentelemetry/resources'; // TODO: This part of a workaround to fix https://github.com/open-telemetry/opentelemetry-js/issues/3609 // If the MeterProvider is not yet registered when instrumentations are registered, all metrics are dropped. @@ -41,3 +51,43 @@ export function parseInstrumentationOptions( return instrumentations; } + +const RESOURCE_DETECTOR_ENVIRONMENT = 'env'; +const RESOURCE_DETECTOR_HOST = 'host'; +const RESOURCE_DETECTOR_OS = 'os'; +const RESOURCE_DETECTOR_PROCESS = 'process'; +const RESOURCE_DETECTOR_SERVICE_INSTANCE_ID = 'serviceinstance'; + +export function getResourceDetectorsFromEnv(): Array { + const resourceDetectors = new Map< + string, + Detector | DetectorSync | Detector[] + >([ + [RESOURCE_DETECTOR_ENVIRONMENT, envDetectorSync], + [RESOURCE_DETECTOR_HOST, hostDetectorSync], + [RESOURCE_DETECTOR_OS, osDetectorSync], + [RESOURCE_DETECTOR_SERVICE_INSTANCE_ID, serviceInstanceIDDetectorSync], + [RESOURCE_DETECTOR_PROCESS, processDetectorSync], + ]); + + const resourceDetectorsFromEnv = + process.env.OTEL_NODE_RESOURCE_DETECTORS?.split(',') ?? ['all']; + + if (resourceDetectorsFromEnv.includes('all')) { + return [...resourceDetectors.values()].flat(); + } + + if (resourceDetectorsFromEnv.includes('none')) { + return []; + } + + return resourceDetectorsFromEnv.flatMap(detector => { + const resourceDetector = resourceDetectors.get(detector); + if (!resourceDetector) { + diag.error( + `Invalid resource detector "${detector}" specified in the environment variable OTEL_NODE_RESOURCE_DETECTORS` + ); + } + return resourceDetector || []; + }); +} diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index cdb99684aae..7459dad3a0a 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -39,7 +39,10 @@ import { View, } from '@opentelemetry/sdk-metrics'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; -import { assertServiceResource } from './util/resource-assertions'; +import { + assertServiceInstanceIDIsUUID, + assertServiceResource, +} from './util/resource-assertions'; import { ConsoleSpanExporter, SimpleSpanProcessor, @@ -71,7 +74,6 @@ import { import { SEMRESATTRS_HOST_NAME, SEMRESATTRS_PROCESS_PID, - SEMRESATTRS_SERVICE_INSTANCE_ID, } from '@opentelemetry/semantic-conventions'; const DefaultContextManager = semver.gte(process.version, '14.8.0') @@ -682,7 +684,7 @@ describe('Node SDK', () => { describe('configureServiceInstanceId', async () => { it('should configure service instance id via OTEL_RESOURCE_ATTRIBUTES env var', async () => { process.env.OTEL_RESOURCE_ATTRIBUTES = - 'service.instance.id=627cc493,service.name=my-service'; + 'service.instance.id=627cc493,service.name=my-service,service.namespace'; const sdk = new NodeSDK(); sdk.start(); @@ -694,7 +696,20 @@ describe('Node SDK', () => { instanceId: '627cc493', }); delete process.env.OTEL_RESOURCE_ATTRIBUTES; - sdk.shutdown(); + await sdk.shutdown(); + }); + + it('should configure service instance id via OTEL_NODE_RESOURCE_DETECTORS env var', async () => { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'env,host,os,serviceinstance'; + const sdk = new NodeSDK(); + + sdk.start(); + const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); + + assertServiceInstanceIDIsUUID(resource); + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + await sdk.shutdown(); }); it('should configure service instance id with random UUID', async () => { @@ -712,14 +727,56 @@ describe('Node SDK', () => { const resource = sdk['_resource']; await resource.waitForAsyncAttributes?.(); - const UUID_REGEX = - /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - assert.equal( - UUID_REGEX.test( - resource.attributes[SEMRESATTRS_SERVICE_INSTANCE_ID]?.toString() || '' - ), - true - ); + assertServiceInstanceIDIsUUID(resource); + delete process.env.OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID; + await sdk.shutdown(); + }); + + it('should configure service instance id with random UUID with OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID env var', async () => { + process.env.OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID = 'true'; + const sdk = new NodeSDK(); + + sdk.start(); + const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); + + assertServiceInstanceIDIsUUID(resource); + delete process.env.OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID; + await sdk.shutdown(); + }); + + it('should configure service instance id via OTEL_RESOURCE_ATTRIBUTES env var even with OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID env var', async () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'service.instance.id=627cc493,service.name=my-service'; + process.env.OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID = 'true'; + const sdk = new NodeSDK(); + + sdk.start(); + const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); + + assertServiceResource(resource, { + name: 'my-service', + instanceId: '627cc493', + }); + delete process.env.OTEL_RESOURCE_ATTRIBUTES; + delete process.env.OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID; + sdk.shutdown(); + }); + + it('should not configure service instance id with no value for it on OTEL_RESOURCE_ATTRIBUTES env var and OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID env var as false', async () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.name=my-service'; + process.env.OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID = 'false'; + const sdk = new NodeSDK(); + + sdk.start(); + const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); + + assertServiceResource(resource, { + name: 'my-service', + }); + delete process.env.OTEL_RESOURCE_ATTRIBUTES; await sdk.shutdown(); }); }); diff --git a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts index 80c8c04ecca..0212b717fc5 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts @@ -18,6 +18,7 @@ import { SDK_INFO } from '@opentelemetry/core'; import * as assert from 'assert'; import { IResource, Resource } from '@opentelemetry/resources'; import { + SEMRESATTRS_SERVICE_INSTANCE_ID, SEMRESATTRS_TELEMETRY_SDK_LANGUAGE, SEMRESATTRS_TELEMETRY_SDK_NAME, SEMRESATTRS_TELEMETRY_SDK_VERSION, @@ -336,3 +337,14 @@ const assertHasOneLabel = (prefix: string, resource: Resource): void => { JSON.stringify(Object.keys(SemanticResourceAttributes)) ); }; + +export const assertServiceInstanceIDIsUUID = (resource: Resource): void => { + const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + assert.equal( + UUID_REGEX.test( + resource.attributes[SEMRESATTRS_SERVICE_INSTANCE_ID]?.toString() || '' + ), + true + ); +}; diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index fda6e103b7d..723611537f2 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -24,7 +24,10 @@ const DEFAULT_LIST_SEPARATOR = ','; * Environment interface to define all names */ -const ENVIRONMENT_BOOLEAN_KEYS = ['OTEL_SDK_DISABLED'] as const; +const ENVIRONMENT_BOOLEAN_KEYS = [ + 'OTEL_SDK_DISABLED', + 'OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID', +] as const; type ENVIRONMENT_BOOLEANS = { [K in (typeof ENVIRONMENT_BOOLEAN_KEYS)[number]]?: boolean; @@ -75,6 +78,7 @@ function isEnvVarANumber(key: unknown): key is keyof ENVIRONMENT_NUMBERS { const ENVIRONMENT_LISTS_KEYS = [ 'OTEL_NO_PATCH_MODULES', 'OTEL_PROPAGATORS', + 'OTEL_NODE_RESOURCE_DETECTORS', ] as const; type ENVIRONMENT_LISTS = { @@ -192,6 +196,7 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_LOG_LEVEL: DiagLogLevel.INFO, OTEL_NO_PATCH_MODULES: [], OTEL_PROPAGATORS: ['tracecontext', 'baggage'], + OTEL_NODE_RESOURCE_DETECTORS: [], OTEL_RESOURCE_ATTRIBUTES: '', OTEL_SERVICE_NAME: '', OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, @@ -236,6 +241,7 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: 'http/protobuf', OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: 'http/protobuf', OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: 'cumulative', + OTEL_NODE_EXPERIMENTAL_DEFAULT_SERVICE_INSTANCE_ID: false, }; /** diff --git a/packages/opentelemetry-resources/src/platform/browser/index.ts b/packages/opentelemetry-resources/src/platform/browser/index.ts index 757d0a116cc..5e48609f694 100644 --- a/packages/opentelemetry-resources/src/platform/browser/index.ts +++ b/packages/opentelemetry-resources/src/platform/browser/index.ts @@ -16,9 +16,10 @@ export * from './default-service-name'; export * from './HostDetector'; -export * from './OSDetector'; export * from './HostDetectorSync'; +export * from './OSDetector'; export * from './OSDetectorSync'; export * from './ProcessDetector'; export * from './ProcessDetectorSync'; export * from './ServiceInstanceIDDetector'; +export * from './ServiceInstanceIDDetectorSync'; diff --git a/packages/opentelemetry-resources/src/platform/node/index.ts b/packages/opentelemetry-resources/src/platform/node/index.ts index 757d0a116cc..5e48609f694 100644 --- a/packages/opentelemetry-resources/src/platform/node/index.ts +++ b/packages/opentelemetry-resources/src/platform/node/index.ts @@ -16,9 +16,10 @@ export * from './default-service-name'; export * from './HostDetector'; -export * from './OSDetector'; export * from './HostDetectorSync'; +export * from './OSDetector'; export * from './OSDetectorSync'; export * from './ProcessDetector'; export * from './ProcessDetectorSync'; export * from './ServiceInstanceIDDetector'; +export * from './ServiceInstanceIDDetectorSync';