diff --git a/packages/opentelemetry-node/lib/processors.js b/packages/opentelemetry-node/lib/processors.js new file mode 100644 index 00000000..d4b857b0 --- /dev/null +++ b/packages/opentelemetry-node/lib/processors.js @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// 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 { + 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 otlpPkgPrefix = '@opentelemetry/exporter-trace-otlp-'; +const otlpProtocol = + getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? + getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') ?? + 'http/protobuf'; + +// 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 + // 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}` + ); + } +} + +/** + * @param {'otlp' | 'zipkin' | 'jaeger' | 'console'} type + * @returns {SpanExporter} + */ +function getSpanExporter(type) { + if (type === 'zipkin') { + const {ZipkinExporter} = require('@opentelemetry/exporter-zipkin'); + return new ZipkinExporter(); + } else if (type === 'jaeger') { + return getJaegerExporter(); + } else if (type === 'console') { + return new ConsoleSpanExporter(); + } + + let exporterPkgName = `${otlpPkgPrefix}`; + 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(); +} + +/** + * @returns {SpanProcessor[]} + */ +function getSpanProcessors() { + // Get from env + const exporters = getStringListFromEnv('OTEL_TRACES_EXPORTER') ?? []; + const result = []; + + if (exporters[0] === 'none') { + log.warn( + 'OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.' + ); + return []; + } + + if (exporters.length === 0) { + log.trace( + '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) { + log.trace(`Initializing "${name}" traces exporter.`); + 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': + result.push(new BatchSpanProcessor(getJaegerExporter())); + break; + default: + log.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${name}.`); + } + } + + return result; +} + +module.exports = { + getSpanProcessors, +}; diff --git a/packages/opentelemetry-node/lib/sdk-metrics.js b/packages/opentelemetry-node/lib/sdk-metrics.js new file mode 100644 index 00000000..e7326bf3 --- /dev/null +++ b/packages/opentelemetry-node/lib/sdk-metrics.js @@ -0,0 +1,130 @@ +/* + * 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, for now: +// - 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_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').Span} Span + * @typedef {import('@opentelemetry/sdk-trace-base').ReadableSpan} ReadableSpan + * @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; + } + // 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; +} + +/** + * @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; +} + +/** + * 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) { + 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} */ +const spanMetricsProcessor = { + forceFlush: function () { + return Promise.resolve(); + }, + onStart: function (span) { + getLiveSpansCounter().add(1, { + [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), + }); + }, + onEnd: function (span) { + const attrs = { + [ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span), + }; + getLiveSpansCounter().add(-1, attrs); + getEndedSpansCounter().add(1, attrs); + }, + shutdown: function () { + return Promise.resolve(); + }, +}; + +/** + * 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 + */ +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 1c799040..cf19a379 100644 --- a/packages/opentelemetry-node/lib/sdk.js +++ b/packages/opentelemetry-node/lib/sdk.js @@ -33,7 +33,9 @@ const luggite = require('./luggite'); const {resolveDetectors} = require('./detectors'); 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; /** @@ -124,8 +126,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: cfg.spanProcessors || getSpanProcessors(), }; const exporterPkgNameFromEnvVar = { @@ -199,6 +200,9 @@ function startNodeSDK(cfg = {}) { const config = Object.assign(defaultConfig, cfg); setupEnvironment(); + if (metricsEnabled) { + 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 fe112210..6912b50c 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,49 @@ 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 "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_RECORD_AND_SAMPLE, + OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY, }; diff --git a/packages/opentelemetry-node/package-lock.json b/packages/opentelemetry-node/package-lock.json index 6feb25f3..a3a54403 100644 --- a/packages/opentelemetry-node/package-lock.json +++ b/packages/opentelemetry-node/package-lock.json @@ -18,6 +18,7 @@ "@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-zipkin": "^2.0.1", "@opentelemetry/host-metrics": "^0.36.0", "@opentelemetry/instrumentation-amqplib": "^0.49.0", "@opentelemetry/instrumentation-aws-sdk": "^0.54.0", @@ -66,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 fe5cb19e..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", @@ -81,6 +81,7 @@ "@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-zipkin": "^2.0.1", "@opentelemetry/host-metrics": "^0.36.0", "@opentelemetry/instrumentation-amqplib": "^0.49.0", "@opentelemetry/instrumentation-aws-sdk": "^0.54.0", @@ -129,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", diff --git a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js index afbeec2e..bb4837cc 100644 --- a/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js +++ b/packages/opentelemetry-node/test/OTEL_METRICS_EXPORTER.test.js @@ -34,6 +34,8 @@ 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: '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 new file mode 100644 index 00000000..24cf1043 --- /dev/null +++ b/packages/opentelemetry-node/test/OTEL_TRACES_EXPORTER.test.js @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * 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'); + +/** @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.')); + }, + }, + { + // 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: 'none, console, zipkin, 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( + '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.')); + }, + }, + { + // 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', + 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(); +}); diff --git a/packages/opentelemetry-node/test/sdk-metrics.test.js b/packages/opentelemetry-node/test/sdk-metrics.test.js new file mode 100644 index 00000000..3433013f --- /dev/null +++ b/packages/opentelemetry-node/test/sdk-metrics.test.js @@ -0,0 +1,75 @@ +/* + * 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 configuration involved (all spans sampled)', + args: ['./fixtures/use-http-server-metrics.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--import=@elastic/opentelemetry-node', + // 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, + 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; + 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_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, + 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(); +}); diff --git a/packages/opentelemetry-node/types/processors.d.ts b/packages/opentelemetry-node/types/processors.d.ts new file mode 100644 index 00000000..6da2a935 --- /dev/null +++ b/packages/opentelemetry-node/types/processors.d.ts @@ -0,0 +1,6 @@ +export type SpanProcessor = import('@opentelemetry/sdk-trace-base').SpanProcessor; +export type SpanExporter = import('@opentelemetry/sdk-trace-base').SpanExporter; +/** + * @returns {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..e1d844a7 --- /dev/null +++ b/packages/opentelemetry-node/types/sdk-metrics.d.ts @@ -0,0 +1,13 @@ +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; +/** + * 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 + */ +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..e14e64f2 100644 --- a/packages/opentelemetry-node/types/semconv.d.ts +++ b/packages/opentelemetry-node/types/semconv.d.ts @@ -24,3 +24,35 @@ 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 "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";