From 236aa41d158d70279ecb45aa2ff338e292700218 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 20 Jun 2025 14:07:32 +0200 Subject: [PATCH 01/19] feat: add span metrics processor --- packages/opentelemetry-node/lib/processors.js | 131 ++++++++++++++++++ packages/opentelemetry-node/lib/sdk.js | 4 +- 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 packages/opentelemetry-node/lib/processors.js diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js new file mode 100644 index 00000000..b26043a6 --- /dev/null +++ b/packages/opentelemetry-node/lib/processors.js @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// This replicates the SDKs logic of getting processors from env +// and also adds a new one to collect span metrics like: +// - metric.otel.sdk.span.live.count +// - metric.otel.sdk.span.ended.count + +const {getStringListFromEnv, getStringFromEnv} = require('@opentelemetry/core'); +const {BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter} = require('@opentelemetry/sdk-trace-base'); +const {log} = require('./logging'); + +/** + * @typedef {import('@opentelemetry/sdk-trace-base').SpanProcessor} SpanProcessor + */ +/** + * @typedef {import('@opentelemetry/sdk-trace-base').SpanExporter} SpanExporter + */ + +const otlpPkgPreffix = '@opentelemetry/exporter-trace-otlp-'; +const otlpProtocol = getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? + getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') ?? + 'http/protobuf'; + + +/** @type {SpanProcessor} */ +const spanMetricsPrcessor = { + forceFlush: function () { + return Promise.resolve(); + }, + onStart: function (span, parentContext) { + // TODO: + console.log('processor onStart'); + }, + onEnd: function (span) { + // TODO: update metrics + console.log('processor onEnd'); + }, + shutdown: function () { + // TODO: shutdown meter? + return Promise.resolve(); + } +}; + +/** + * @returns {SpanExporter} + */ +function getOtlpExporter() { + let exporterPkgName = `${otlpPkgPreffix}`; + switch(otlpProtocol) { + case 'grpc': + exporterPkgName += 'grpc'; + break; + case 'http/json': + exporterPkgName += 'http'; + break; + case 'http/protobuf': + exporterPkgName += 'proto'; + break; + default: + log.warn(`Unsupported OTLP traces protocol: ${otlpProtocol}. Using http/protobuf.`); + exporterPkgName += 'proto'; + } + const {OTLPTraceExporter} = require(exporterPkgName); + return new OTLPTraceExporter(); +} + +/** + * @param {SpanProcessor[]} [processors] + */ +function getSpanProcessors(processors) { + const metricsExporters = getStringListFromEnv('OTEL_METRICS_EXPORTER') || []; + const metricsEnabled = metricsExporters.every((e) => e !== 'none'); + + console.log('getSpanProcessors', metricsEnabled, Array.isArray(processors)) + + if (Array.isArray(processors)) { + if (metricsEnabled) { + processors.push(spanMetricsPrcessor); + } + return processors; + } + + // Get from env + const exporters = getStringListFromEnv('OTEL_TRACES_EXPORTER') ?? []; + const result = metricsEnabled ? [spanMetricsPrcessor] : []; + + if (exporters.some(exp => exp === 'none')) { + log.warn('OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.'); + return []; + } + + if (exporters.length === 0) { + log.debug('OTEL_TRACES_EXPORTER is empty. Using the default "otlp" exporter.') + exporters.push('otlp'); + } + + for (const name of exporters) { + switch(name) { + case 'otlp': + result.push(new BatchSpanProcessor(getOtlpExporter())); + break; + case 'console': + result.push(new SimpleSpanProcessor(new ConsoleSpanExporter())); + break; + case 'zipkin': + const {ZipkinExporter} = require('@opentelemetry/exporter-zipkin'); + result.push(new BatchSpanProcessor(new ZipkinExporter())); + break; + case 'jaeger': + // TODO: this shold be installed and there is a possible issue with bundlers. refs: + // - is a dev-dependency? https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/package.json#L76 + // - surreunded with try catch in https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/src/utils.ts#L120 + // const {JaegerExporter} = require('@opentelemetry/exporter-jaeger'); + // result.push(new BatchSpanProcessor(new JaegerExporter())); + log.warn(`OTEL_TRACES_EXPORTER value "${name}" not available yet.`); + break; + default: + log.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${name}.`); + } + } + + console.log('getSpanProcessors.length', result) + return result; +} + +module.exports = { + getSpanProcessors, +}; diff --git a/packages/opentelemetry-node/lib/sdk.js b/packages/opentelemetry-node/lib/sdk.js index 76d06c36..a59b4315 100644 --- a/packages/opentelemetry-node/lib/sdk.js +++ b/packages/opentelemetry-node/lib/sdk.js @@ -33,6 +33,7 @@ const luggite = require('./luggite'); const {resolveDetectors} = require('./detectors'); const {setupEnvironment, restoreEnvironment} = require('./environment'); const {getInstrumentations} = require('./instrumentations'); +const {getSpanProcessors} = require('./processors'); const DISTRO_VERSION = require('../package.json').version; /** @@ -123,8 +124,7 @@ function startNodeSDK(cfg = {}) { const defaultConfig = { resourceDetectors: resolveDetectors(cfg.resourceDetectors), instrumentations: cfg.instrumentations || getInstrumentations(), - // Avoid setting `spanProcessor` or `traceExporter` to have NodeSDK - // use its `TracerProviderWithEnvExporters` for tracing setup. + spanProcessors: getSpanProcessors(cfg.spanProcessors), }; const exporterPkgNameFromEnvVar = { From 8417ad55ca6cfb7feed678eb2510630d67cd688c Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 20 Jun 2025 15:56:49 +0200 Subject: [PATCH 02/19] chore(otel-node): fix lint issues --- packages/opentelemetry-node/lib/processors.js | 188 ++++++++++-------- 1 file changed, 105 insertions(+), 83 deletions(-) diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index b26043a6..f2281c70 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -9,7 +9,11 @@ // - metric.otel.sdk.span.ended.count const {getStringListFromEnv, getStringFromEnv} = require('@opentelemetry/core'); -const {BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter} = require('@opentelemetry/sdk-trace-base'); +const { + BatchSpanProcessor, + SimpleSpanProcessor, + ConsoleSpanExporter, +} = require('@opentelemetry/sdk-trace-base'); const {log} = require('./logging'); /** @@ -20,112 +24,130 @@ const {log} = require('./logging'); */ const otlpPkgPreffix = '@opentelemetry/exporter-trace-otlp-'; -const otlpProtocol = getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? +const otlpProtocol = + getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') ?? 'http/protobuf'; - /** @type {SpanProcessor} */ const spanMetricsPrcessor = { - forceFlush: function () { - return Promise.resolve(); - }, - onStart: function (span, parentContext) { - // TODO: - console.log('processor onStart'); - }, - onEnd: function (span) { - // TODO: update metrics - console.log('processor onEnd'); - }, - shutdown: function () { - // TODO: shutdown meter? - return Promise.resolve(); - } + forceFlush: function () { + return Promise.resolve(); + }, + onStart: function (span, parentContext) { + // TODO: + // console.log('processor onStart'); + }, + onEnd: function (span) { + // TODO: update metrics + // console.log('processor onEnd'); + }, + shutdown: function () { + // TODO: shutdown meter? + return Promise.resolve(); + }, }; /** + * @param {'otlp' | 'zipkin' | 'jaeger' | 'console'} type * @returns {SpanExporter} */ -function getOtlpExporter() { - let exporterPkgName = `${otlpPkgPreffix}`; - switch(otlpProtocol) { - case 'grpc': - exporterPkgName += 'grpc'; - break; - case 'http/json': - exporterPkgName += 'http'; - break; - case 'http/protobuf': - exporterPkgName += 'proto'; - break; - default: - log.warn(`Unsupported OTLP traces protocol: ${otlpProtocol}. Using http/protobuf.`); - exporterPkgName += 'proto'; - } - const {OTLPTraceExporter} = require(exporterPkgName); - return new OTLPTraceExporter(); +function getSpanExporter(type) { + if (type === 'zipkin') { + const {ZipkinExporter} = require('@opentelemetry/exporter-zipkin'); + return new ZipkinExporter(); + } else if (type === 'jaeger') { + // TODO: this shold be installed and there is a possible issue with bundlers. refs: + // - is a dev-dependency? https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/package.json#L76 + // - surreunded with try catch in https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/src/utils.ts#L120 + // const {JaegerExporter} = require('@opentelemetry/exporter-jaeger'); + // result.push(new BatchSpanProcessor(new JaegerExporter())); + } else if (type === 'console') { + return new ConsoleSpanExporter(); + } + + let exporterPkgName = `${otlpPkgPreffix}`; + switch (otlpProtocol) { + case 'grpc': + exporterPkgName += 'grpc'; + break; + case 'http/json': + exporterPkgName += 'http'; + break; + case 'http/protobuf': + exporterPkgName += 'proto'; + break; + default: + log.warn( + `Unsupported OTLP traces protocol: ${otlpProtocol}. Using http/protobuf.` + ); + exporterPkgName += 'proto'; + } + const {OTLPTraceExporter} = require(exporterPkgName); + return new OTLPTraceExporter(); } /** * @param {SpanProcessor[]} [processors] */ function getSpanProcessors(processors) { - const metricsExporters = getStringListFromEnv('OTEL_METRICS_EXPORTER') || []; - const metricsEnabled = metricsExporters.every((e) => e !== 'none'); - - console.log('getSpanProcessors', metricsEnabled, Array.isArray(processors)) - - if (Array.isArray(processors)) { - if (metricsEnabled) { - processors.push(spanMetricsPrcessor); + const metricsExporters = + getStringListFromEnv('OTEL_METRICS_EXPORTER') || []; + const metricsEnabled = metricsExporters.every((e) => e !== 'none'); + + if (Array.isArray(processors)) { + if (metricsEnabled) { + processors.push(spanMetricsPrcessor); + } + return processors; } - return processors; - } - // Get from env - const exporters = getStringListFromEnv('OTEL_TRACES_EXPORTER') ?? []; - const result = metricsEnabled ? [spanMetricsPrcessor] : []; + // Get from env + const exporters = getStringListFromEnv('OTEL_TRACES_EXPORTER') ?? []; + const result = metricsEnabled ? [spanMetricsPrcessor] : []; - if (exporters.some(exp => exp === 'none')) { - log.warn('OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.'); - return []; - } + if (exporters.some((exp) => exp === 'none')) { + log.warn( + 'OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.' + ); + return []; + } - if (exporters.length === 0) { - log.debug('OTEL_TRACES_EXPORTER is empty. Using the default "otlp" exporter.') - exporters.push('otlp'); - } + if (exporters.length === 0) { + log.debug( + 'OTEL_TRACES_EXPORTER is empty. Using the default "otlp" exporter.' + ); + exporters.push('otlp'); + } - for (const name of exporters) { - switch(name) { - case 'otlp': - result.push(new BatchSpanProcessor(getOtlpExporter())); - break; - case 'console': - result.push(new SimpleSpanProcessor(new ConsoleSpanExporter())); - break; - case 'zipkin': - const {ZipkinExporter} = require('@opentelemetry/exporter-zipkin'); - result.push(new BatchSpanProcessor(new ZipkinExporter())); - break; - case 'jaeger': - // TODO: this shold be installed and there is a possible issue with bundlers. refs: - // - is a dev-dependency? https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/package.json#L76 - // - surreunded with try catch in https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/src/utils.ts#L120 - // const {JaegerExporter} = require('@opentelemetry/exporter-jaeger'); - // result.push(new BatchSpanProcessor(new JaegerExporter())); - log.warn(`OTEL_TRACES_EXPORTER value "${name}" not available yet.`); - break; - default: - log.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${name}.`); + for (const name of exporters) { + switch (name) { + case 'otlp': + result.push(new BatchSpanProcessor(getSpanExporter('otlp'))); + break; + case 'console': + result.push( + new SimpleSpanProcessor(getSpanExporter('console')) + ); + break; + case 'zipkin': + result.push(new BatchSpanProcessor(getSpanExporter('zipkin'))); + break; + case 'jaeger': + // TODO: check comment in `getSpanExporter` function + // result.push(new BatchSpanProcessor(getSpanExporter('zipkin'))); + log.warn( + `OTEL_TRACES_EXPORTER value "${name}" not available yet.` + ); + break; + default: + log.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${name}.`); + } } - } - console.log('getSpanProcessors.length', result) - return result; + return result; } module.exports = { - getSpanProcessors, + getSpanProcessors, }; From 75cea924a6f8f6023117c37619a2497fd43781f8 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 23 Jun 2025 18:36:19 +0200 Subject: [PATCH 03/19] chore: add metrics and collect --- packages/opentelemetry-node/lib/processors.js | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index f2281c70..d82e02a1 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -8,6 +8,7 @@ // - metric.otel.sdk.span.live.count // - metric.otel.sdk.span.ended.count +const {metrics} = require('@opentelemetry/api'); const {getStringListFromEnv, getStringFromEnv} = require('@opentelemetry/core'); const { BatchSpanProcessor, @@ -17,9 +18,10 @@ const { const {log} = require('./logging'); /** + * @typedef {import('@opentelemetry/api').Meter} Meter + * @typedef {import('@opentelemetry/api').UpDownCounter} UpDownCounter + * @typedef {import('@opentelemetry/api').Counter} Counter * @typedef {import('@opentelemetry/sdk-trace-base').SpanProcessor} SpanProcessor - */ -/** * @typedef {import('@opentelemetry/sdk-trace-base').SpanExporter} SpanExporter */ @@ -29,18 +31,66 @@ const otlpProtocol = getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') ?? 'http/protobuf'; +// NOTE: assuming the meter provider is not going to be replaced once +// the EDOT is started we can cache the meter and metrics in these vars +/** @type {Meter} */ +let selfMetricsMeter; +/** @type {UpDownCounter} */ +let liveSpans; +/** @type {Counter} */ +let closedSpans; + +/** + * @returns {Meter} + */ +function getSpanMeter() { + if (selfMetricsMeter) { + return selfMetricsMeter; + } + // TODO: name & verison + selfMetricsMeter = metrics.getMeter('edot-nodejs', '1.2.3'); + return selfMetricsMeter; +} + +/** + * @returns {UpDownCounter} + */ +function getLiveSpans() { + if (liveSpans) { + return liveSpans; + } + liveSpans = getSpanMeter().createUpDownCounter('otel.sdk.span.live.count', { + description: + 'Number of created spans for which the end operation has not been called yet', + }); + return liveSpans; +} + +/** + * @returns {Counter} + */ +function getClosedSpans() { + if (closedSpans) { + return closedSpans; + } + closedSpans = getSpanMeter().createCounter('otel.sdk.span.closed.count', { + description: + 'Number of created spans for which the end operation was called', + }); + return closedSpans; +} + /** @type {SpanProcessor} */ const spanMetricsPrcessor = { forceFlush: function () { return Promise.resolve(); }, onStart: function (span, parentContext) { - // TODO: - // console.log('processor onStart'); + getLiveSpans().add(1); }, onEnd: function (span) { - // TODO: update metrics - // console.log('processor onEnd'); + getLiveSpans().add(-1); + getClosedSpans().add(1); }, shutdown: function () { // TODO: shutdown meter? @@ -57,7 +107,7 @@ function getSpanExporter(type) { const {ZipkinExporter} = require('@opentelemetry/exporter-zipkin'); return new ZipkinExporter(); } else if (type === 'jaeger') { - // TODO: this shold be installed and there is a possible issue with bundlers. refs: + // TODO: this should be installed and there is a possible issue with bundlers. refs: // - is a dev-dependency? https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/package.json#L76 // - surreunded with try catch in https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/src/utils.ts#L120 // const {JaegerExporter} = require('@opentelemetry/exporter-jaeger'); From e584385038f26a6f5c0dec8e4bc57c27e8acec48 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 23 Jun 2025 19:30:38 +0200 Subject: [PATCH 04/19] chore: add types for processors.js file --- packages/opentelemetry-node/types/processors.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/opentelemetry-node/types/processors.d.ts diff --git a/packages/opentelemetry-node/types/processors.d.ts b/packages/opentelemetry-node/types/processors.d.ts new file mode 100644 index 00000000..71ced19c --- /dev/null +++ b/packages/opentelemetry-node/types/processors.d.ts @@ -0,0 +1,9 @@ +export type Meter = import('@opentelemetry/api').Meter; +export type UpDownCounter = import('@opentelemetry/api').UpDownCounter; +export type Counter = import('@opentelemetry/api').Counter; +export type SpanProcessor = import('@opentelemetry/sdk-trace-base').SpanProcessor; +export type SpanExporter = import('@opentelemetry/sdk-trace-base').SpanExporter; +/** + * @param {SpanProcessor[]} [processors] + */ +export function getSpanProcessors(processors?: SpanProcessor[]): import("@opentelemetry/sdk-trace-base").SpanProcessor[]; From e64765db44d72a908462e774897ceaf346d62e73 Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 25 Jun 2025 10:22:35 +0200 Subject: [PATCH 05/19] chore: add test assertions for SDK span metrics --- packages/opentelemetry-node/lib/processors.js | 34 +++++++++++-------- .../test/OTEL_METRICS_EXPORTER.test.js | 3 ++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index d82e02a1..092f27c7 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -25,6 +25,11 @@ const {log} = require('./logging'); * @typedef {import('@opentelemetry/sdk-trace-base').SpanExporter} SpanExporter */ +// @ts-ignore - compiler options do not allow lookp outside `lib` folder +const ELASTIC_PKG = require('../package.json'); +const ELASTIC_SDK_VERSION = ELASTIC_PKG.version; +const ELASTIC_SDK_SCOPE = ELASTIC_PKG.name; + const otlpPkgPreffix = '@opentelemetry/exporter-trace-otlp-'; const otlpProtocol = getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? @@ -43,37 +48,39 @@ let closedSpans; /** * @returns {Meter} */ -function getSpanMeter() { +function getSpansMeter() { if (selfMetricsMeter) { return selfMetricsMeter; } - // TODO: name & verison - selfMetricsMeter = metrics.getMeter('edot-nodejs', '1.2.3'); + selfMetricsMeter = metrics.getMeter(ELASTIC_SDK_SCOPE, ELASTIC_SDK_VERSION); return selfMetricsMeter; } /** * @returns {UpDownCounter} */ -function getLiveSpans() { +function getLiveSpansCounter() { if (liveSpans) { return liveSpans; } - liveSpans = getSpanMeter().createUpDownCounter('otel.sdk.span.live.count', { - description: - 'Number of created spans for which the end operation has not been called yet', - }); + liveSpans = getSpansMeter().createUpDownCounter( + 'otel.sdk.span.live.count', + { + description: + 'Number of created spans for which the end operation has not been called yet', + } + ); return liveSpans; } /** * @returns {Counter} */ -function getClosedSpans() { +function getClosedSpansCounter() { if (closedSpans) { return closedSpans; } - closedSpans = getSpanMeter().createCounter('otel.sdk.span.closed.count', { + closedSpans = getSpansMeter().createCounter('otel.sdk.span.closed.count', { description: 'Number of created spans for which the end operation was called', }); @@ -86,14 +93,13 @@ const spanMetricsPrcessor = { return Promise.resolve(); }, onStart: function (span, parentContext) { - getLiveSpans().add(1); + getLiveSpansCounter().add(1); }, onEnd: function (span) { - getLiveSpans().add(-1); - getClosedSpans().add(1); + getLiveSpansCounter().add(-1); + getClosedSpansCounter().add(1); }, shutdown: function () { - // TODO: shutdown meter? return Promise.resolve(); }, }; diff --git a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js index afbeec2e..c06324bf 100644 --- a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js +++ b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js @@ -34,6 +34,9 @@ const testFixtures = [ t.ok(hasLog(`name: 'nodejs.eventloop.delay.min'`)); t.ok(hasLog(`name: 'nodejs.eventloop.delay.max'`)); t.ok(hasLog(`name: 'process.cpu.utilization'`)); + t.ok(hasLog(`name: 'process.cpu.utilization'`)); + t.ok(hasLog(`name: 'otel.sdk.span.live.count'`)); + t.ok(hasLog(`name: 'otel.sdk.span.closed.count'`)); }, checkTelemetry: (t, col) => { t.ok(col.metrics.length > 0); From 28573fa62432e98c38decd3a6a59ca457cb47a4c Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 25 Jun 2025 11:09:38 +0200 Subject: [PATCH 06/19] chore: add tests for OTLP_TRACES_EXPORTER --- packages/opentelemetry-node/lib/processors.js | 3 +- .../test/OTEL_TRACES_EXPORTER.test.js | 97 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index 092f27c7..8b2a579f 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -170,13 +170,14 @@ function getSpanProcessors(processors) { } if (exporters.length === 0) { - log.debug( + log.trace( 'OTEL_TRACES_EXPORTER is empty. Using the default "otlp" exporter.' ); exporters.push('otlp'); } for (const name of exporters) { + log.trace(`Initializing "${name}" traces exporter.`); switch (name) { case 'otlp': result.push(new BatchSpanProcessor(getSpanExporter('otlp'))); diff --git a/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js b/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js new file mode 100644 index 00000000..717a3554 --- /dev/null +++ b/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const {test} = require('tape'); +const {runTestFixtures} = require('./testutils'); + +/** @type {import('./testutils').TestFixture[]} */ +const testFixtures = [ + { + name: 'basic scenario with no values in env', + // Using an existing fixture since we're only interested in startup logs + // from the distro. + args: ['./fixtures/use-exporter-protocol.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--import=@elastic/opentelemetry-node', + OTEL_LOG_LEVEL: 'verbose', + }, + // verbose: true, + checkResult: (t, err, stdout, stderr) => { + t.error(err); + const lines = stdout.split('\n'); + const hasLog = (text) => + getLogs(lines).some((log) => log.msg.includes(text)); + t.ok(hasLog('Initializing "otlp" traces exporter.')); + t.ok(!hasLog('Initializing "zipkin" traces exporter.')); + t.ok(!hasLog('Initializing "jaeger" traces exporter.')); + t.ok(!hasLog('Initializing "console" traces exporter.')); + }, + }, + { + name: 'scenario with "none" in OTEL_TRACES_EXPORTER', + // Using an existing fixture since we're only interested in startup logs + // from the distro. + args: ['./fixtures/use-exporter-protocol.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--import=@elastic/opentelemetry-node', + OTEL_LOG_LEVEL: 'verbose', + OTEL_TRACES_EXPORTER: 'console, zipkin, otlp, none', + }, + // verbose: true, + checkResult: (t, err, stdout, stderr) => { + t.error(err); + const lines = stdout.split('\n'); + const hasLog = (text) => + getLogs(lines).some((log) => log.msg.includes(text)); + t.ok(hasLog('OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.')); + t.ok(!hasLog('Initializing "otlp" traces exporter.')); + t.ok(!hasLog('Initializing "zipkin" traces exporter.')); + t.ok(!hasLog('Initializing "jaeger" traces exporter.')); + t.ok(!hasLog('Initializing "console" traces exporter.')); + }, + }, + { + name: 'scenario with values in OTEL_TRACES_EXPORTER', + // Using an existing fixture since we're only interested in startup logs + // from the distro. + args: ['./fixtures/use-exporter-protocol.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--import=@elastic/opentelemetry-node', + OTEL_LOG_LEVEL: 'verbose', + OTEL_TRACES_EXPORTER: 'console, otlp', + }, + // verbose: true, + checkResult: (t, err, stdout, stderr) => { + t.error(err); + const lines = stdout.split('\n'); + const hasLog = (text) => + getLogs(lines).some((log) => log.msg.includes(text)); + t.ok(hasLog('Initializing "otlp" traces exporter.')); + t.ok(hasLog('Initializing "console" traces exporter.')); + t.ok(!hasLog('Initializing "zipkin" traces exporter.')); + t.ok(!hasLog('Initializing "jaeger" traces exporter.')); + }, + }, +]; + +// ----- helper functions ----- + +/** + * @param {Array} lines + * @returns {Array<{name: string, lelvel: number; msg: string}>} + */ +function getLogs(lines) { + return lines.filter((l) => l.startsWith('{')).map(JSON.parse); +} + +// ----- main line ----- + +test('OTEL_TRACES_EXPORTER', (suite) => { + runTestFixtures(suite, testFixtures); + suite.end(); +}); From 76363ee9aaefaa653f1cf77d84e324592c571c53 Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 25 Jun 2025 11:09:57 +0200 Subject: [PATCH 07/19] chore: fix lint issues --- .../opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js b/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js index 717a3554..57bf4fc4 100644 --- a/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js +++ b/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js @@ -47,7 +47,11 @@ const testFixtures = [ const lines = stdout.split('\n'); const hasLog = (text) => getLogs(lines).some((log) => log.msg.includes(text)); - t.ok(hasLog('OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.')); + t.ok( + hasLog( + 'OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.' + ) + ); t.ok(!hasLog('Initializing "otlp" traces exporter.')); t.ok(!hasLog('Initializing "zipkin" traces exporter.')); t.ok(!hasLog('Initializing "jaeger" traces exporter.')); From 59cb9ed241a7106f165c2d1cd32ccb5bcbc18352 Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 25 Jun 2025 11:17:25 +0200 Subject: [PATCH 08/19] chore(otel-node): update package.json --- packages/opentelemetry-node/package-lock.json | 4 ++++ packages/opentelemetry-node/package.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/opentelemetry-node/package-lock.json b/packages/opentelemetry-node/package-lock.json index 1f97cbae..9045c0ae 100644 --- a/packages/opentelemetry-node/package-lock.json +++ b/packages/opentelemetry-node/package-lock.json @@ -18,6 +18,10 @@ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.202.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.202.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.202.0", + "@opentelemetry/exporter-zipkin": "^2.0.1", "@opentelemetry/host-metrics": "^0.36.0", "@opentelemetry/instrumentation-amqplib": "^0.49.0", "@opentelemetry/instrumentation-aws-sdk": "^0.54.0", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 31a4b673..523f0b6b 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -81,6 +81,10 @@ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.202.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.202.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.202.0", + "@opentelemetry/exporter-zipkin": "^2.0.1", "@opentelemetry/host-metrics": "^0.36.0", "@opentelemetry/instrumentation-amqplib": "^0.49.0", "@opentelemetry/instrumentation-aws-sdk": "^0.54.0", From 942941c48201b366bce4870ad6038a3006e3450d Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 25 Jun 2025 11:41:09 +0200 Subject: [PATCH 09/19] chore(otel-node): fix lint issues --- packages/opentelemetry-node/package-lock.json | 3 --- packages/opentelemetry-node/package.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/opentelemetry-node/package-lock.json b/packages/opentelemetry-node/package-lock.json index 9045c0ae..881f1146 100644 --- a/packages/opentelemetry-node/package-lock.json +++ b/packages/opentelemetry-node/package-lock.json @@ -18,9 +18,6 @@ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.202.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.202.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.202.0", "@opentelemetry/exporter-zipkin": "^2.0.1", "@opentelemetry/host-metrics": "^0.36.0", "@opentelemetry/instrumentation-amqplib": "^0.49.0", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 523f0b6b..15eba1a2 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -81,9 +81,6 @@ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.202.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.202.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.202.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.202.0", "@opentelemetry/exporter-zipkin": "^2.0.1", "@opentelemetry/host-metrics": "^0.36.0", "@opentelemetry/instrumentation-amqplib": "^0.49.0", From 9fbe68480cda280fabc7e25057425c84a5b215ab Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 25 Jun 2025 12:00:53 +0200 Subject: [PATCH 10/19] chore(otel-node): fix lint issues --- packages/opentelemetry-node/package-lock.json | 1 + packages/opentelemetry-node/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/opentelemetry-node/package-lock.json b/packages/opentelemetry-node/package-lock.json index 881f1146..bfd29c62 100644 --- a/packages/opentelemetry-node/package-lock.json +++ b/packages/opentelemetry-node/package-lock.json @@ -67,6 +67,7 @@ "@opentelemetry/sdk-logs": "^0.202.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-node": "^0.202.0", + "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.30.0", "@opentelemetry/winston-transport": "^0.13.0", "import-in-the-middle": "^1.12.0", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 15eba1a2..b5501442 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -130,6 +130,7 @@ "@opentelemetry/sdk-logs": "^0.202.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-node": "^0.202.0", + "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.30.0", "@opentelemetry/winston-transport": "^0.13.0", "import-in-the-middle": "^1.12.0", From ebd0802d5b66cbba9055fa98c6dca70a9441070b Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 26 Jun 2025 09:30:16 +0200 Subject: [PATCH 11/19] chore: separate processors from SDK metrics --- packages/opentelemetry-node/lib/processors.js | 126 ++++------------ .../opentelemetry-node/lib/sdk-metrics.js | 138 ++++++++++++++++++ packages/opentelemetry-node/lib/sdk.js | 7 +- packages/opentelemetry-node/lib/semconv.js | 49 ++++++- .../test/OTEL_METRICS_EXPORTER.test.js | 4 +- .../test/OTEL_TRACES_EXPORTER.test.js | 44 +++++- 6 files changed, 259 insertions(+), 109 deletions(-) create mode 100644 packages/opentelemetry-node/lib/sdk-metrics.js diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index 8b2a579f..03db9093 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -4,11 +4,7 @@ */ // This replicates the SDKs logic of getting processors from env -// and also adds a new one to collect span metrics like: -// - metric.otel.sdk.span.live.count -// - metric.otel.sdk.span.ended.count -const {metrics} = require('@opentelemetry/api'); const {getStringListFromEnv, getStringFromEnv} = require('@opentelemetry/core'); const { BatchSpanProcessor, @@ -18,92 +14,33 @@ const { const {log} = require('./logging'); /** - * @typedef {import('@opentelemetry/api').Meter} Meter - * @typedef {import('@opentelemetry/api').UpDownCounter} UpDownCounter - * @typedef {import('@opentelemetry/api').Counter} Counter * @typedef {import('@opentelemetry/sdk-trace-base').SpanProcessor} SpanProcessor * @typedef {import('@opentelemetry/sdk-trace-base').SpanExporter} SpanExporter */ -// @ts-ignore - compiler options do not allow lookp outside `lib` folder -const ELASTIC_PKG = require('../package.json'); -const ELASTIC_SDK_VERSION = ELASTIC_PKG.version; -const ELASTIC_SDK_SCOPE = ELASTIC_PKG.name; - -const otlpPkgPreffix = '@opentelemetry/exporter-trace-otlp-'; +const otlpPkgPrefix = '@opentelemetry/exporter-trace-otlp-'; const otlpProtocol = getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') ?? 'http/protobuf'; -// NOTE: assuming the meter provider is not going to be replaced once -// the EDOT is started we can cache the meter and metrics in these vars -/** @type {Meter} */ -let selfMetricsMeter; -/** @type {UpDownCounter} */ -let liveSpans; -/** @type {Counter} */ -let closedSpans; - -/** - * @returns {Meter} - */ -function getSpansMeter() { - if (selfMetricsMeter) { - return selfMetricsMeter; - } - selfMetricsMeter = metrics.getMeter(ELASTIC_SDK_SCOPE, ELASTIC_SDK_VERSION); - return selfMetricsMeter; -} - -/** - * @returns {UpDownCounter} - */ -function getLiveSpansCounter() { - if (liveSpans) { - return liveSpans; - } - liveSpans = getSpansMeter().createUpDownCounter( - 'otel.sdk.span.live.count', - { - description: - 'Number of created spans for which the end operation has not been called yet', - } - ); - return liveSpans; -} - -/** - * @returns {Counter} - */ -function getClosedSpansCounter() { - if (closedSpans) { - return closedSpans; +// Jeager exporter is deprecated but upstream stills support it (for now) +// ref: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/CHANGELOG.md#0440 +function getJaegerExporter() { + // The JaegerExporter does not support being required in bundled + // environments. By delaying the require statement to here, we only crash when + // the exporter is actually used in such an environment. + try { + // @ts-ignore + const {JaegerExporter} = require('@opentelemetry/exporter-jaeger'); + return new JaegerExporter(); + } catch (e) { + throw new Error( + `Could not instantiate JaegerExporter. This could be due to the JaegerExporter's lack of support for bundling. If possible, use @opentelemetry/exporter-trace-otlp-proto instead. Original Error: ${e}` + ); } - closedSpans = getSpansMeter().createCounter('otel.sdk.span.closed.count', { - description: - 'Number of created spans for which the end operation was called', - }); - return closedSpans; } -/** @type {SpanProcessor} */ -const spanMetricsPrcessor = { - forceFlush: function () { - return Promise.resolve(); - }, - onStart: function (span, parentContext) { - getLiveSpansCounter().add(1); - }, - onEnd: function (span) { - getLiveSpansCounter().add(-1); - getClosedSpansCounter().add(1); - }, - shutdown: function () { - return Promise.resolve(); - }, -}; - /** * @param {'otlp' | 'zipkin' | 'jaeger' | 'console'} type * @returns {SpanExporter} @@ -122,7 +59,7 @@ function getSpanExporter(type) { return new ConsoleSpanExporter(); } - let exporterPkgName = `${otlpPkgPreffix}`; + let exporterPkgName = `${otlpPkgPrefix}`; switch (otlpProtocol) { case 'grpc': exporterPkgName += 'grpc'; @@ -144,25 +81,14 @@ function getSpanExporter(type) { } /** - * @param {SpanProcessor[]} [processors] + * @returns {SpanProcessor[]} */ -function getSpanProcessors(processors) { - const metricsExporters = - getStringListFromEnv('OTEL_METRICS_EXPORTER') || []; - const metricsEnabled = metricsExporters.every((e) => e !== 'none'); - - if (Array.isArray(processors)) { - if (metricsEnabled) { - processors.push(spanMetricsPrcessor); - } - return processors; - } - +function getSpanProcessors() { // Get from env const exporters = getStringListFromEnv('OTEL_TRACES_EXPORTER') ?? []; - const result = metricsEnabled ? [spanMetricsPrcessor] : []; + const result = []; - if (exporters.some((exp) => exp === 'none')) { + if (exporters[0] === 'none') { log.warn( 'OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.' ); @@ -174,6 +100,12 @@ function getSpanProcessors(processors) { 'OTEL_TRACES_EXPORTER is empty. Using the default "otlp" exporter.' ); exporters.push('otlp'); + } else if (exporters.length > 1 && exporters.includes('none')) { + log.warn( + 'OTEL_TRACES_EXPORTER contains "none" along with other exporters. Using default otlp exporter.' + ); + exporters.length = 0; + exporters.push('otlp'); } for (const name of exporters) { @@ -191,11 +123,7 @@ function getSpanProcessors(processors) { result.push(new BatchSpanProcessor(getSpanExporter('zipkin'))); break; case 'jaeger': - // TODO: check comment in `getSpanExporter` function - // result.push(new BatchSpanProcessor(getSpanExporter('zipkin'))); - log.warn( - `OTEL_TRACES_EXPORTER value "${name}" not available yet.` - ); + result.push(new BatchSpanProcessor(getJaegerExporter())); break; default: log.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${name}.`); diff --git a/packages/opentelemetry-node/lib/sdk-metrics.js b/packages/opentelemetry-node/lib/sdk-metrics.js new file mode 100644 index 00000000..ff537667 --- /dev/null +++ b/packages/opentelemetry-node/lib/sdk-metrics.js @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Gets the SDK configuration and updates it to have instruments +// to collect metrics related to the SDK like: +// - otel.sdk.span.live +// - otel.sdk.span.ended + +const {metrics, TraceFlags} = require('@opentelemetry/api'); +const { + METRIC_OTEL_SDK_SPAN_ENDED, + METRIC_OTEL_SDK_SPAN_LIVE, + ATTR_OTEL_SPAN_SAMPLING_RESULT, + OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP, + OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE, + OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY, +} = require('./semconv'); + +/** + * @typedef {import('@opentelemetry/api').Meter} Meter + * @typedef {import('@opentelemetry/api').UpDownCounter} UpDownCounter + * @typedef {import('@opentelemetry/api').Counter} Counter + * @typedef {import('@opentelemetry/sdk-trace-base').SpanProcessor} SpanProcessor + */ + +// @ts-ignore - compiler options do not allow lookp outside `lib` folder +const ELASTIC_PKG = require('../package.json'); +const ELASTIC_SDK_VERSION = ELASTIC_PKG.version; +const ELASTIC_SDK_SCOPE = ELASTIC_PKG.name; + +// NOTE: assuming the meter provider is not going to be replaced once +// the EDOT is started we can cache the meter and metrics in these vars +/** @type {Meter} */ +let selfMetricsMeter; +/** @type {UpDownCounter} */ +let liveSpans; +/** @type {Counter} */ +let closedSpans; + +/** + * @returns {Meter} + */ +function getSpansMeter() { + if (selfMetricsMeter) { + return selfMetricsMeter; + } + selfMetricsMeter = metrics.getMeter(ELASTIC_SDK_SCOPE, ELASTIC_SDK_VERSION); + return selfMetricsMeter; +} + +/** + * @returns {UpDownCounter} + */ +function getLiveSpansCounter() { + if (liveSpans) { + return liveSpans; + } + liveSpans = getSpansMeter().createUpDownCounter(METRIC_OTEL_SDK_SPAN_LIVE, { + description: + 'Number of created spans for which the end operation has not been called yet', + }); + return liveSpans; +} + +/** + * @returns {Counter} + */ +function getEndedSpansCounter() { + if (closedSpans) { + return closedSpans; + } + closedSpans = getSpansMeter().createCounter(METRIC_OTEL_SDK_SPAN_ENDED, { + description: + 'Number of created spans for which the end operation was called', + }); + return closedSpans; +} + +/** + * @param {any} span - an Span form the API or a ReadableSpan from the trace SDK + * @returns {string} + */ +function getSamplingResult(span) { + let result; + const isRecording = + typeof span.isRecording === 'function' + ? span.isRecording() + : !span.ended; + + if (isRecording) { + const isSampled = span.spanContext().traceFlags & TraceFlags.SAMPLED; + result = isSampled + ? OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE + : OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY; + } else { + result = OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP; + } + + return result; +} + +/** @type {SpanProcessor} */ +const spanMetricsProcessor = { + forceFlush: function () { + return Promise.resolve(); + }, + onStart: function (span, parentContext) { + getLiveSpansCounter().add(1, { + [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), + }); + }, + onEnd: function (span) { + getLiveSpansCounter().add(-1, { + [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), + }); + getEndedSpansCounter().add(1, { + [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), + }); + }, + shutdown: function () { + return Promise.resolve(); + }, +}; + +/** + * + * @param {Partial} cfg + * @returns + */ +function setupSdkMetrics(cfg) { + cfg.spanProcessors.push(spanMetricsProcessor); +} + +module.exports = { + setupSdkMetrics, +}; diff --git a/packages/opentelemetry-node/lib/sdk.js b/packages/opentelemetry-node/lib/sdk.js index b97a6f2b..837b18d9 100644 --- a/packages/opentelemetry-node/lib/sdk.js +++ b/packages/opentelemetry-node/lib/sdk.js @@ -35,6 +35,7 @@ const {setupEnvironment, restoreEnvironment} = require('./environment'); const {getInstrumentations} = require('./instrumentations'); const {getSpanProcessors} = require('./processors'); const {setupCentralConfig} = require('./central-config'); +const {setupSdkMetrics} = require('./sdk-metrics'); const DISTRO_VERSION = require('../package.json').version; /** @@ -125,7 +126,7 @@ function startNodeSDK(cfg = {}) { const defaultConfig = { resourceDetectors: resolveDetectors(cfg.resourceDetectors), instrumentations: cfg.instrumentations || getInstrumentations(), - spanProcessors: getSpanProcessors(cfg.spanProcessors), + spanProcessors: cfg.spanProcessors || getSpanProcessors(), }; const exporterPkgNameFromEnvVar = { @@ -194,6 +195,10 @@ function startNodeSDK(cfg = {}) { aggregation: {type: AggregationType.DROP}, }, ]; + + // Configure OTEL SDK metrics + // ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml + setupSdkMetrics(defaultConfig); } const config = Object.assign(defaultConfig, cfg); diff --git a/packages/opentelemetry-node/lib/semconv.js b/packages/opentelemetry-node/lib/semconv.js index fe112210..16ca59f6 100644 --- a/packages/opentelemetry-node/lib/semconv.js +++ b/packages/opentelemetry-node/lib/semconv.js @@ -26,7 +26,6 @@ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. */ const ATTR_DEPLOYMENT_ENVIRONMENT_NAME = 'deployment.environment.name'; - /** * The name of the deployment. * @@ -37,7 +36,55 @@ const ATTR_DEPLOYMENT_ENVIRONMENT_NAME = 'deployment.environment.name'; */ const ATTR_DEPLOYMENT_NAME = 'deployment.name'; +/** + * The number of created spans for which the end operation was called + * + * @note For spans with `recording=true`: Implementations **MUST** record both `otel.sdk.span.live` and `otel.sdk.span.ended`. + * For spans with `recording=false`: If implementations decide to record this metric, they **MUST** also record `otel.sdk.span.live`. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +const METRIC_OTEL_SDK_SPAN_ENDED = 'otel.sdk.span.ended'; + +/** + * The number of created spans for which the end operation has not been called yet + * + * @note For spans with `recording=true`: Implementations **MUST** record both `otel.sdk.span.live` and `otel.sdk.span.ended`. + * For spans with `recording=false`: If implementations decide to record this metric, they **MUST** also record `otel.sdk.span.ended`. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +const METRIC_OTEL_SDK_SPAN_LIVE = 'otel.sdk.span.live'; + +/** + * The result value of the sampler for this span + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +const ATTR_OTEL_SPAN_SAMPLING_RESULT = 'otel.span.sampling_result'; + +/** + * Enum value "DROP" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. + */ +const OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP = 'DROP'; + +/** + * Enum value "RECORD_AND_SAMPLE" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. + */ +const OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE = 'RECORD_AND_SAMPLE'; + +/** + * Enum value "RECORD_ONLY" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. + */ +const OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY = 'RECORD_ONLY'; + module.exports = { ATTR_DEPLOYMENT_ENVIRONMENT_NAME, ATTR_DEPLOYMENT_NAME, + METRIC_OTEL_SDK_SPAN_ENDED, + METRIC_OTEL_SDK_SPAN_LIVE, + ATTR_OTEL_SPAN_SAMPLING_RESULT, + OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP, + OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE, + OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY, }; diff --git a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js index c06324bf..e8f2b17b 100644 --- a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js +++ b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js @@ -35,8 +35,8 @@ const testFixtures = [ t.ok(hasLog(`name: 'nodejs.eventloop.delay.max'`)); t.ok(hasLog(`name: 'process.cpu.utilization'`)); t.ok(hasLog(`name: 'process.cpu.utilization'`)); - t.ok(hasLog(`name: 'otel.sdk.span.live.count'`)); - t.ok(hasLog(`name: 'otel.sdk.span.closed.count'`)); + t.ok(hasLog(`name: 'otel.sdk.span.live'`)); + t.ok(hasLog(`name: 'otel.sdk.span.ended'`)); }, checkTelemetry: (t, col) => { t.ok(col.metrics.length > 0); diff --git a/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js b/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js index 57bf4fc4..24cf1043 100644 --- a/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js +++ b/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js @@ -3,6 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +// Test that the exporters and processors are properly set +// via environment variables. thi test uses an existing fixture +// since we're only interested in startup logs from the distro. + const {test} = require('tape'); const {runTestFixtures} = require('./testutils'); @@ -31,15 +35,16 @@ const testFixtures = [ }, }, { - name: 'scenario with "none" in OTEL_TRACES_EXPORTER', - // Using an existing fixture since we're only interested in startup logs - // from the distro. + // NOTE: we believe the presence of "none" should disable all exporters + // (like metrics exporters) but this is not how the upstream SDK works. + // We keep behavior and will comment in the JS SIG + name: 'scenario with "none" in the 1st value of OTEL_TRACES_EXPORTER', args: ['./fixtures/use-exporter-protocol.js'], cwd: __dirname, env: { NODE_OPTIONS: '--import=@elastic/opentelemetry-node', OTEL_LOG_LEVEL: 'verbose', - OTEL_TRACES_EXPORTER: 'console, zipkin, otlp, none', + OTEL_TRACES_EXPORTER: 'none, console, zipkin, otlp', }, // verbose: true, checkResult: (t, err, stdout, stderr) => { @@ -58,10 +63,37 @@ const testFixtures = [ t.ok(!hasLog('Initializing "console" traces exporter.')); }, }, + { + // NOTE: same here. The "none" value has a different effect when not in the + // 1st position of the list. + // ref: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-sdk-node/src/utils.ts#L144-L162 + name: 'scenario with "none" note in the 1st value of OTEL_TRACES_EXPORTER', + args: ['./fixtures/use-exporter-protocol.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--import=@elastic/opentelemetry-node', + OTEL_LOG_LEVEL: 'verbose', + OTEL_TRACES_EXPORTER: 'console, zipkin, otlp, none', + }, + // verbose: true, + checkResult: (t, err, stdout, stderr) => { + t.error(err); + const lines = stdout.split('\n'); + const hasLog = (text) => + getLogs(lines).some((log) => log.msg.includes(text)); + t.ok( + hasLog( + 'OTEL_TRACES_EXPORTER contains "none" along with other exporters. Using default otlp exporter.' + ) + ); + t.ok(hasLog('Initializing "otlp" traces exporter.')); + t.ok(!hasLog('Initializing "zipkin" traces exporter.')); + t.ok(!hasLog('Initializing "jaeger" traces exporter.')); + t.ok(!hasLog('Initializing "console" traces exporter.')); + }, + }, { name: 'scenario with values in OTEL_TRACES_EXPORTER', - // Using an existing fixture since we're only interested in startup logs - // from the distro. args: ['./fixtures/use-exporter-protocol.js'], cwd: __dirname, env: { From 35bcb2b2463715149a657d94e7993debaa1dcfcc Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 26 Jun 2025 09:46:12 +0200 Subject: [PATCH 12/19] chore: update types --- .../opentelemetry-node/types/processors.d.ts | 7 ++-- .../opentelemetry-node/types/sdk-metrics.d.ts | 10 ++++++ .../opentelemetry-node/types/semconv.d.ts | 36 +++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 packages/opentelemetry-node/types/sdk-metrics.d.ts diff --git a/packages/opentelemetry-node/types/processors.d.ts b/packages/opentelemetry-node/types/processors.d.ts index 71ced19c..6da2a935 100644 --- a/packages/opentelemetry-node/types/processors.d.ts +++ b/packages/opentelemetry-node/types/processors.d.ts @@ -1,9 +1,6 @@ -export type Meter = import('@opentelemetry/api').Meter; -export type UpDownCounter = import('@opentelemetry/api').UpDownCounter; -export type Counter = import('@opentelemetry/api').Counter; export type SpanProcessor = import('@opentelemetry/sdk-trace-base').SpanProcessor; export type SpanExporter = import('@opentelemetry/sdk-trace-base').SpanExporter; /** - * @param {SpanProcessor[]} [processors] + * @returns {SpanProcessor[]} */ -export function getSpanProcessors(processors?: SpanProcessor[]): import("@opentelemetry/sdk-trace-base").SpanProcessor[]; +export function getSpanProcessors(): SpanProcessor[]; diff --git a/packages/opentelemetry-node/types/sdk-metrics.d.ts b/packages/opentelemetry-node/types/sdk-metrics.d.ts new file mode 100644 index 00000000..6c32d2e4 --- /dev/null +++ b/packages/opentelemetry-node/types/sdk-metrics.d.ts @@ -0,0 +1,10 @@ +export type Meter = import('@opentelemetry/api').Meter; +export type UpDownCounter = import('@opentelemetry/api').UpDownCounter; +export type Counter = import('@opentelemetry/api').Counter; +export type SpanProcessor = import('@opentelemetry/sdk-trace-base').SpanProcessor; +/** + * + * @param {Partial} cfg + * @returns + */ +export function setupSdkMetrics(cfg: Partial): void; diff --git a/packages/opentelemetry-node/types/semconv.d.ts b/packages/opentelemetry-node/types/semconv.d.ts index 1ab1512a..89b695d6 100644 --- a/packages/opentelemetry-node/types/semconv.d.ts +++ b/packages/opentelemetry-node/types/semconv.d.ts @@ -24,3 +24,39 @@ export const ATTR_DEPLOYMENT_ENVIRONMENT_NAME: "deployment.environment.name"; * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. */ export const ATTR_DEPLOYMENT_NAME: "deployment.name"; +/** + * The number of created spans for which the end operation was called + * + * @note For spans with `recording=true`: Implementations **MUST** record both `otel.sdk.span.live` and `otel.sdk.span.ended`. + * For spans with `recording=false`: If implementations decide to record this metric, they **MUST** also record `otel.sdk.span.live`. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const METRIC_OTEL_SDK_SPAN_ENDED: "otel.sdk.span.ended"; +/** + * The number of created spans for which the end operation has not been called yet + * + * @note For spans with `recording=true`: Implementations **MUST** record both `otel.sdk.span.live` and `otel.sdk.span.ended`. + * For spans with `recording=false`: If implementations decide to record this metric, they **MUST** also record `otel.sdk.span.ended`. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const METRIC_OTEL_SDK_SPAN_LIVE: "otel.sdk.span.live"; +/** + * The result value of the sampler for this span + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_OTEL_SPAN_SAMPLING_RESULT: "otel.span.sampling_result"; +/** + * Enum value "DROP" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. + */ +export const OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP: "DROP"; +/** + * Enum value "RECORD_AND_SAMPLE" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. + */ +export const OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE: "RECORD_AND_SAMPLE"; +/** + * Enum value "RECORD_ONLY" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. + */ +export const OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY: "RECORD_ONLY"; From a389f5c8c24c67069f21f2caf990328214cd591c Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 26 Jun 2025 11:07:32 +0200 Subject: [PATCH 13/19] chore: ignore jaeger exporter from lint:deps --- packages/opentelemetry-node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 81b459cc..71d44c82 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -47,7 +47,7 @@ "lint:eslint": "eslint --ext=js,mjs,cjs . # requires node >=16.0.0", "lint:types": "rm -rf build/lint-types && tsc --outDir build/lint-types && diff -ur types build/lint-types", "lint:fix": "eslint --ext=js,mjs,cjs --fix . # requires node >=16.0.0", - "lint:deps": "dependency-check require.js import.mjs 'lib/**/*.js' 'test/**/*.js' '!test/fixtures/a-ts-proj' '!test/fixtures/an-esm-pkg' -e mjs:../../scripts/parse-mjs-source -i @types/tape -i dotenv -i @opentelemetry/winston-transport -i @opentelemetry/exporter-logs-* -i @opentelemetry/exporter-metrics-*", + "lint:deps": "dependency-check require.js import.mjs 'lib/**/*.js' 'test/**/*.js' '!test/fixtures/a-ts-proj' '!test/fixtures/an-esm-pkg' -e mjs:../../scripts/parse-mjs-source -i @types/tape -i dotenv -i @opentelemetry/winston-transport -i @opentelemetry/exporter-logs-* -i @opentelemetry/exporter-metrics-* -i @opentelemetry/exporter-jaeger", "lint:license-files": "../../scripts/gen-notice.sh --lint . # requires node >=16", "lint:changelog": "../../scripts/extract-release-notes.js .", "test": "NODE_OPTIONS='-r dotenv/config' DOTENV_CONFIG_PATH=./test/test-services.env tape test/**/*.test.js", From e25c31a7814962468599e1fe97dd6dfde72daf47 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 26 Jun 2025 20:21:29 +0200 Subject: [PATCH 14/19] chore(otel-node): fix samping result attribute --- .../opentelemetry-node/lib/sdk-metrics.js | 42 ++++++++----------- packages/opentelemetry-node/lib/sdk.js | 9 ++-- packages/opentelemetry-node/lib/semconv.js | 6 --- 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/packages/opentelemetry-node/lib/sdk-metrics.js b/packages/opentelemetry-node/lib/sdk-metrics.js index ff537667..ad2888cc 100644 --- a/packages/opentelemetry-node/lib/sdk-metrics.js +++ b/packages/opentelemetry-node/lib/sdk-metrics.js @@ -4,7 +4,7 @@ */ // Gets the SDK configuration and updates it to have instruments -// to collect metrics related to the SDK like: +// to collect metrics related to the SDK, for now: // - otel.sdk.span.live // - otel.sdk.span.ended @@ -13,7 +13,6 @@ const { METRIC_OTEL_SDK_SPAN_ENDED, METRIC_OTEL_SDK_SPAN_LIVE, ATTR_OTEL_SPAN_SAMPLING_RESULT, - OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP, OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE, OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY, } = require('./semconv'); @@ -22,6 +21,8 @@ const { * @typedef {import('@opentelemetry/api').Meter} Meter * @typedef {import('@opentelemetry/api').UpDownCounter} UpDownCounter * @typedef {import('@opentelemetry/api').Counter} Counter + * @typedef {import('@opentelemetry/sdk-trace-base').Span} Span + * @typedef {import('@opentelemetry/sdk-trace-base').ReadableSpan} ReadableSpan * @typedef {import('@opentelemetry/sdk-trace-base').SpanProcessor} SpanProcessor */ @@ -46,6 +47,8 @@ function getSpansMeter() { if (selfMetricsMeter) { return selfMetricsMeter; } + // NOTE: we have a metter for a single scope which is the EDOT package + // TODO: check WWJD (what would Java do?) selfMetricsMeter = metrics.getMeter(ELASTIC_SDK_SCOPE, ELASTIC_SDK_VERSION); return selfMetricsMeter; } @@ -79,26 +82,16 @@ function getEndedSpansCounter() { } /** - * @param {any} span - an Span form the API or a ReadableSpan from the trace SDK + * All Spans treated in SpanProcessors are recording so here none will have + * a "DROP" sampling result + * @param {Span | ReadableSpan} span * @returns {string} */ function getSamplingResult(span) { - let result; - const isRecording = - typeof span.isRecording === 'function' - ? span.isRecording() - : !span.ended; - - if (isRecording) { - const isSampled = span.spanContext().traceFlags & TraceFlags.SAMPLED; - result = isSampled - ? OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE - : OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY; - } else { - result = OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP; - } - - return result; + const isSampled = span.spanContext().traceFlags & TraceFlags.SAMPLED; + return isSampled + ? OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE + : OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY; } /** @type {SpanProcessor} */ @@ -106,18 +99,17 @@ const spanMetricsProcessor = { forceFlush: function () { return Promise.resolve(); }, - onStart: function (span, parentContext) { + onStart: function (span) { getLiveSpansCounter().add(1, { [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), }); }, onEnd: function (span) { - getLiveSpansCounter().add(-1, { + const attrs = { [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), - }); - getEndedSpansCounter().add(1, { - [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), - }); + }; + getLiveSpansCounter().add(-1, attrs); + getEndedSpansCounter().add(1, attrs); }, shutdown: function () { return Promise.resolve(); diff --git a/packages/opentelemetry-node/lib/sdk.js b/packages/opentelemetry-node/lib/sdk.js index 837b18d9..a77891c0 100644 --- a/packages/opentelemetry-node/lib/sdk.js +++ b/packages/opentelemetry-node/lib/sdk.js @@ -195,15 +195,16 @@ function startNodeSDK(cfg = {}) { aggregation: {type: AggregationType.DROP}, }, ]; - - // Configure OTEL SDK metrics - // ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml - setupSdkMetrics(defaultConfig); } const config = Object.assign(defaultConfig, cfg); setupEnvironment(); + if (metricsEnabled) { + // Configure OTEL SDK metrics + // ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml + setupSdkMetrics(config); + } const sdk = new NodeSDK(config); // TODO perhaps include some choice resource attrs in this log (sync ones): service.name, deployment.environment.name diff --git a/packages/opentelemetry-node/lib/semconv.js b/packages/opentelemetry-node/lib/semconv.js index 16ca59f6..6912b50c 100644 --- a/packages/opentelemetry-node/lib/semconv.js +++ b/packages/opentelemetry-node/lib/semconv.js @@ -63,11 +63,6 @@ const METRIC_OTEL_SDK_SPAN_LIVE = 'otel.sdk.span.live'; */ const ATTR_OTEL_SPAN_SAMPLING_RESULT = 'otel.span.sampling_result'; -/** - * Enum value "DROP" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. - */ -const OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP = 'DROP'; - /** * Enum value "RECORD_AND_SAMPLE" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. */ @@ -84,7 +79,6 @@ module.exports = { METRIC_OTEL_SDK_SPAN_ENDED, METRIC_OTEL_SDK_SPAN_LIVE, ATTR_OTEL_SPAN_SAMPLING_RESULT, - OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP, OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE, OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY, }; From d4b503c9598239dfbb7256728cec53ec4f3305ca Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 27 Jun 2025 09:21:32 +0200 Subject: [PATCH 15/19] chore: fix metric attribs --- packages/opentelemetry-node/lib/processors.js | 6 +----- packages/opentelemetry-node/types/sdk-metrics.d.ts | 2 ++ packages/opentelemetry-node/types/semconv.d.ts | 4 ---- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index 03db9093..c8524e01 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -50,11 +50,7 @@ function getSpanExporter(type) { const {ZipkinExporter} = require('@opentelemetry/exporter-zipkin'); return new ZipkinExporter(); } else if (type === 'jaeger') { - // TODO: this should be installed and there is a possible issue with bundlers. refs: - // - is a dev-dependency? https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/package.json#L76 - // - surreunded with try catch in https://github.com/open-telemetry/opentelemetry-js/blob/ec17ce48d0e5a99a122da5add612a20e2dd84ed5/experimental/packages/opentelemetry-sdk-node/src/utils.ts#L120 - // const {JaegerExporter} = require('@opentelemetry/exporter-jaeger'); - // result.push(new BatchSpanProcessor(new JaegerExporter())); + return getJaegerExporter(); } else if (type === 'console') { return new ConsoleSpanExporter(); } diff --git a/packages/opentelemetry-node/types/sdk-metrics.d.ts b/packages/opentelemetry-node/types/sdk-metrics.d.ts index 6c32d2e4..e3a62820 100644 --- a/packages/opentelemetry-node/types/sdk-metrics.d.ts +++ b/packages/opentelemetry-node/types/sdk-metrics.d.ts @@ -1,6 +1,8 @@ export type Meter = import('@opentelemetry/api').Meter; export type UpDownCounter = import('@opentelemetry/api').UpDownCounter; export type Counter = import('@opentelemetry/api').Counter; +export type Span = import('@opentelemetry/sdk-trace-base').Span; +export type ReadableSpan = import('@opentelemetry/sdk-trace-base').ReadableSpan; export type SpanProcessor = import('@opentelemetry/sdk-trace-base').SpanProcessor; /** * diff --git a/packages/opentelemetry-node/types/semconv.d.ts b/packages/opentelemetry-node/types/semconv.d.ts index 89b695d6..e14e64f2 100644 --- a/packages/opentelemetry-node/types/semconv.d.ts +++ b/packages/opentelemetry-node/types/semconv.d.ts @@ -48,10 +48,6 @@ export const METRIC_OTEL_SDK_SPAN_LIVE: "otel.sdk.span.live"; * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. */ export const ATTR_OTEL_SPAN_SAMPLING_RESULT: "otel.span.sampling_result"; -/** - * Enum value "DROP" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. - */ -export const OTEL_SPAN_SAMPLING_RESULT_VALUE_DROP: "DROP"; /** * Enum value "RECORD_AND_SAMPLE" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}. */ From e82db6872dfa112d2eb8beafc48615fbd3e39630 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 27 Jun 2025 10:35:51 +0200 Subject: [PATCH 16/19] chore: add sdk metrics test --- .../test/OTEL_SDK_METRICS.test.js | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/opentelemetry-node/test/OTEL_SDK_METRICS.test.js diff --git a/packages/opentelemetry-node/test/OTEL_SDK_METRICS.test.js b/packages/opentelemetry-node/test/OTEL_SDK_METRICS.test.js new file mode 100644 index 00000000..da0989dd --- /dev/null +++ b/packages/opentelemetry-node/test/OTEL_SDK_METRICS.test.js @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const {test} = require('tape'); +const {runTestFixtures} = require('./testutils'); + +/** @type {import('./testutils').TestFixture[]} */ +const testFixtures = [ + { + name: 'basic scenario with no sampling configutarion involved (all spans sampled)', + args: ['./fixtures/use-http-server-metrics.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--import=@elastic/opentelemetry-node', + OTEL_METRICS_EXPORTER: 'otlp,console', + // The default metrics interval is 30s, which makes for a slow test. + // However, too low a value runs into possible: + // PeriodicExportingMetricReader: metrics collection errors TimeoutError: Operation timed out. + // which can lead to test data surprises. + OTEL_METRIC_EXPORT_INTERVAL: '1000', + OTEL_METRIC_EXPORT_TIMEOUT: '900', + }, + // verbose: true, + checkResult: (t, err, stdout) => { + t.error(err); + }, + checkTelemetry: (t, col) => { + t.ok(col.metrics.length > 0); + + const spanMetrics = col.metrics.filter((m) => + m.name.startsWith('otel.sdk.span') + ); + t.ok( + spanMetrics.every((m) => { + const dataPoints = m.sum.dataPoints; + return dataPoints.every( + (p) => + p.attributes['otel.span.sampling_result'] === + 'RECORD_AND_SAMPLE' + ); + }) + ); + }, + }, + { + name: 'scenario with span sampling configuration involved (not sampling any spans)', + args: ['./fixtures/use-http-server-metrics.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--import=@elastic/opentelemetry-node', + OTEL_METRICS_EXPORTER: 'otlp,console', + OTEL_TRACES_SAMPLER: 'always_off', + // The default metrics interval is 30s, which makes for a slow test. + // However, too low a value runs into possible: + // PeriodicExportingMetricReader: metrics collection errors TimeoutError: Operation timed out. + // which can lead to test data surprises. + OTEL_METRIC_EXPORT_INTERVAL: '1000', + OTEL_METRIC_EXPORT_TIMEOUT: '900', + }, + // verbose: true, + checkResult: (t, err, stdout) => { + t.error(err); + }, + checkTelemetry: (t, col) => { + t.ok(col.metrics.length > 0); + + const spanMetrics = col.metrics.filter((m) => + m.name.startsWith('otel.sdk.span') + ); + t.equal(spanMetrics.length, 0); + }, + }, +]; + +// ----- main line ----- + +test('OTEL_SDK_METRICS', (suite) => { + runTestFixtures(suite, testFixtures); + suite.end(); +}); From e3f7ca0b2e72eabd2cef8a6d9520f4e470f796f4 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 3 Jul 2025 09:54:55 +0200 Subject: [PATCH 17/19] Apply suggestions from code review Co-authored-by: Trent Mick --- packages/opentelemetry-node/lib/processors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index c8524e01..6cc3fffa 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -24,7 +24,7 @@ const otlpProtocol = getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') ?? 'http/protobuf'; -// Jeager exporter is deprecated but upstream stills support it (for now) +// Jaeger exporter is deprecated but upstream stills support it (for now) // ref: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/CHANGELOG.md#0440 function getJaegerExporter() { // The JaegerExporter does not support being required in bundled From 001a8174b47ccfbc55a73e4d65484f8cdddfe88b Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 3 Jul 2025 09:55:09 +0200 Subject: [PATCH 18/19] chore: rename test file and fix comments and typos --- packages/opentelemetry-node/lib/sdk-metrics.js | 6 +++--- packages/opentelemetry-node/lib/sdk.js | 2 -- .../test/OTEL_METRICS_EXPORTER.test.js | 1 - .../{OTEL_SDK_METRICS.test.js => sdk-metrics.test.js} | 11 ++--------- packages/opentelemetry-node/types/sdk-metrics.d.ts | 3 ++- 5 files changed, 7 insertions(+), 16 deletions(-) rename packages/opentelemetry-node/test/{OTEL_SDK_METRICS.test.js => sdk-metrics.test.js} (88%) diff --git a/packages/opentelemetry-node/lib/sdk-metrics.js b/packages/opentelemetry-node/lib/sdk-metrics.js index ad2888cc..e7326bf3 100644 --- a/packages/opentelemetry-node/lib/sdk-metrics.js +++ b/packages/opentelemetry-node/lib/sdk-metrics.js @@ -47,8 +47,7 @@ function getSpansMeter() { if (selfMetricsMeter) { return selfMetricsMeter; } - // NOTE: we have a metter for a single scope which is the EDOT package - // TODO: check WWJD (what would Java do?) + // NOTE: we have a meter for a single scope which is the EDOT package selfMetricsMeter = metrics.getMeter(ELASTIC_SDK_SCOPE, ELASTIC_SDK_VERSION); return selfMetricsMeter; } @@ -117,7 +116,8 @@ const spanMetricsProcessor = { }; /** - * + * Updates the configuration to add OTEL SDK metrics + * ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml * @param {Partial} cfg * @returns */ diff --git a/packages/opentelemetry-node/lib/sdk.js b/packages/opentelemetry-node/lib/sdk.js index a77891c0..cf19a379 100644 --- a/packages/opentelemetry-node/lib/sdk.js +++ b/packages/opentelemetry-node/lib/sdk.js @@ -201,8 +201,6 @@ function startNodeSDK(cfg = {}) { setupEnvironment(); if (metricsEnabled) { - // Configure OTEL SDK metrics - // ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml setupSdkMetrics(config); } const sdk = new NodeSDK(config); diff --git a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js index e8f2b17b..bb4837cc 100644 --- a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js +++ b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js @@ -34,7 +34,6 @@ const testFixtures = [ t.ok(hasLog(`name: 'nodejs.eventloop.delay.min'`)); t.ok(hasLog(`name: 'nodejs.eventloop.delay.max'`)); t.ok(hasLog(`name: 'process.cpu.utilization'`)); - t.ok(hasLog(`name: 'process.cpu.utilization'`)); t.ok(hasLog(`name: 'otel.sdk.span.live'`)); t.ok(hasLog(`name: 'otel.sdk.span.ended'`)); }, diff --git a/packages/opentelemetry-node/test/OTEL_SDK_METRICS.test.js b/packages/opentelemetry-node/test/sdk-metrics.test.js similarity index 88% rename from packages/opentelemetry-node/test/OTEL_SDK_METRICS.test.js rename to packages/opentelemetry-node/test/sdk-metrics.test.js index da0989dd..3433013f 100644 --- a/packages/opentelemetry-node/test/OTEL_SDK_METRICS.test.js +++ b/packages/opentelemetry-node/test/sdk-metrics.test.js @@ -9,12 +9,11 @@ const {runTestFixtures} = require('./testutils'); /** @type {import('./testutils').TestFixture[]} */ const testFixtures = [ { - name: 'basic scenario with no sampling configutarion involved (all spans sampled)', + name: 'basic scenario with no sampling configuration involved (all spans sampled)', args: ['./fixtures/use-http-server-metrics.js'], cwd: __dirname, env: { NODE_OPTIONS: '--import=@elastic/opentelemetry-node', - OTEL_METRICS_EXPORTER: 'otlp,console', // The default metrics interval is 30s, which makes for a slow test. // However, too low a value runs into possible: // PeriodicExportingMetricReader: metrics collection errors TimeoutError: Operation timed out. @@ -23,15 +22,13 @@ const testFixtures = [ OTEL_METRIC_EXPORT_TIMEOUT: '900', }, // verbose: true, - checkResult: (t, err, stdout) => { - t.error(err); - }, checkTelemetry: (t, col) => { t.ok(col.metrics.length > 0); const spanMetrics = col.metrics.filter((m) => m.name.startsWith('otel.sdk.span') ); + t.ok(spanMetrics.length > 0); t.ok( spanMetrics.every((m) => { const dataPoints = m.sum.dataPoints; @@ -50,7 +47,6 @@ const testFixtures = [ cwd: __dirname, env: { NODE_OPTIONS: '--import=@elastic/opentelemetry-node', - OTEL_METRICS_EXPORTER: 'otlp,console', OTEL_TRACES_SAMPLER: 'always_off', // The default metrics interval is 30s, which makes for a slow test. // However, too low a value runs into possible: @@ -60,9 +56,6 @@ const testFixtures = [ OTEL_METRIC_EXPORT_TIMEOUT: '900', }, // verbose: true, - checkResult: (t, err, stdout) => { - t.error(err); - }, checkTelemetry: (t, col) => { t.ok(col.metrics.length > 0); diff --git a/packages/opentelemetry-node/types/sdk-metrics.d.ts b/packages/opentelemetry-node/types/sdk-metrics.d.ts index e3a62820..e1d844a7 100644 --- a/packages/opentelemetry-node/types/sdk-metrics.d.ts +++ b/packages/opentelemetry-node/types/sdk-metrics.d.ts @@ -5,7 +5,8 @@ export type Span = import('@opentelemetry/sdk-trace-base').Span; export type ReadableSpan = import('@opentelemetry/sdk-trace-base').ReadableSpan; export type SpanProcessor = import('@opentelemetry/sdk-trace-base').SpanProcessor; /** - * + * Updates the configuration to add OTEL SDK metrics + * ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml * @param {Partial} cfg * @returns */ From cebfcd03e6b39176e2fabec3a5ac088cfaad4f03 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 4 Jul 2025 15:31:25 +0200 Subject: [PATCH 19/19] Update packages/opentelemetry-node/lib/processors.js Co-authored-by: Trent Mick --- packages/opentelemetry-node/lib/processors.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js index 6cc3fffa..d4b857b0 100644 --- a/packages/opentelemetry-node/lib/processors.js +++ b/packages/opentelemetry-node/lib/processors.js @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -// This replicates the SDKs logic of getting processors from env +// This replicates the sdk-node logic of getting processors from env, because +// the created NodeSDK doesn't expose the created processors and EDOT Node.js +// wants to customize the default. const {getStringListFromEnv, getStringFromEnv} = require('@opentelemetry/core'); const {