diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea0653c181d..cf57261570d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
### :rocket: Features
+* feat(sdk-trace): implement span start/end metrics [#1851](https://github.com/open-telemetry/opentelemetry-js/pull/6213) @anuraaga
+
### :bug: Bug Fixes
* fix(opentelemetry-sdk-node): the custom value from env variable for service.instance.id should take priority over random uuid as backup [#6345](https://github.com/open-telemetry/opentelemetry-js/pull/6345) @maryliag
diff --git a/e2e-tests/verify.mjs b/e2e-tests/verify.mjs
index 16bafb7d913..3e22f99ee50 100755
--- a/e2e-tests/verify.mjs
+++ b/e2e-tests/verify.mjs
@@ -32,9 +32,14 @@ for (const line of lines) {
verifiedSpan = true;
}
if (parsed.resourceMetrics) {
- console.log('found metric');
- verifyMetric(parsed.resourceMetrics[0].scopeMetrics[0].metrics[0]);
- verifiedMetric = true;
+ const scopeMetrics = parsed.resourceMetrics[0].scopeMetrics.find(
+ sm => sm.scope.name === 'example-meter'
+ );
+ if (scopeMetrics) {
+ console.log('found metric');
+ verifyMetric(scopeMetrics.metrics[0]);
+ verifiedMetric = true;
+ }
}
if (parsed.resourceLogs) {
console.log('found log');
diff --git a/experimental/packages/opentelemetry-sdk-node/README.md b/experimental/packages/opentelemetry-sdk-node/README.md
index 22033184bfc..c38a8e936ae 100644
--- a/experimental/packages/opentelemetry-sdk-node/README.md
+++ b/experimental/packages/opentelemetry-sdk-node/README.md
@@ -228,6 +228,21 @@ Additionally, you can specify other applicable environment variables that apply
- [OTLP exporter environment configuration](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options)
- [Zipkin exporter environment configuration](https://github.com/open-telemetry/opentelemetry-specification/blob/6ce62202e5407518e19c56c445c13682ef51a51d/specification/sdk-environment-variables.md#zipkin-exporter)
+## Enable OpenTelemetry SDK internal metrics from environment
+
+OpenTelemetry defines [metrics for monitoring SDK components](https://opentelemetry.io/docs/specs/semconv/otel/sdk-metrics/).
+Until this spec is stabilized, the following environment variable must be used
+to enable these metrics:
+
+```bash
+OTEL_NODE_EXPERIMENTAL_SDK_METRICS=true
+```
+
+Currently a subset of the specified metrics are implemented. See the following
+linkes for details:
+
+- Span metrics: [TracerMetrics.ts](../../../packages/opentelemetry-sdk-trace-base/src/TracerMetrics.ts)
+
## Useful links
- For more information on OpenTelemetry, visit:
diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts
index 84fc35cd0ae..26595cdcb6c 100644
--- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts
+++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts
@@ -326,15 +326,43 @@ export class NodeSDK {
})
);
+ if (
+ this._meterProviderConfig?.readers &&
+ // only register if there is a reader, otherwise we waste compute/memory.
+ this._meterProviderConfig.readers.length > 0
+ ) {
+ const meterProvider = new MeterProvider({
+ resource: this._resource,
+ views: this._meterProviderConfig?.views ?? [],
+ readers: this._meterProviderConfig.readers,
+ });
+
+ this._meterProvider = meterProvider;
+ metrics.setGlobalMeterProvider(meterProvider);
+
+ // TODO: This is a workaround to fix https://github.com/open-telemetry/opentelemetry-js/issues/3609
+ // If the MeterProvider is not yet registered when instrumentations are registered, all metrics are dropped.
+ // This code is obsolete once https://github.com/open-telemetry/opentelemetry-js/issues/3622 is implemented.
+ for (const instrumentation of this._instrumentations) {
+ instrumentation.setMeterProvider(metrics.getMeterProvider());
+ }
+ }
+
const spanProcessors = this._tracerProviderConfig
? this._tracerProviderConfig.spanProcessors
: getSpanProcessorsFromEnv();
// Only register if there is a span processor
if (spanProcessors.length > 0) {
+ // While SDK metrics are unstable, we require an opt-in.
+ // https://opentelemetry.io/docs/specs/semconv/otel/sdk-metrics/
+ const sdkMetricsEnabled = getBooleanFromEnv(
+ 'OTEL_NODE_EXPERIMENTAL_SDK_METRICS'
+ );
this._tracerProvider = new NodeTracerProvider({
...this._configuration,
resource: this._resource,
+ meterProvider: sdkMetricsEnabled ? this._meterProvider : undefined,
spanProcessors,
});
trace.setGlobalTracerProvider(this._tracerProvider);
@@ -351,28 +379,6 @@ export class NodeSDK {
logs.setGlobalLoggerProvider(loggerProvider);
}
-
- if (
- this._meterProviderConfig?.readers &&
- // only register if there is a reader, otherwise we waste compute/memory.
- this._meterProviderConfig.readers.length > 0
- ) {
- const meterProvider = new MeterProvider({
- resource: this._resource,
- views: this._meterProviderConfig?.views ?? [],
- readers: this._meterProviderConfig.readers,
- });
-
- this._meterProvider = meterProvider;
- metrics.setGlobalMeterProvider(meterProvider);
-
- // TODO: This is a workaround to fix https://github.com/open-telemetry/opentelemetry-js/issues/3609
- // If the MeterProvider is not yet registered when instrumentations are registered, all metrics are dropped.
- // This code is obsolete once https://github.com/open-telemetry/opentelemetry-js/issues/3622 is implemented.
- for (const instrumentation of this._instrumentations) {
- instrumentation.setMeterProvider(metrics.getMeterProvider());
- }
- }
}
public shutdown(): Promise {
diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts
index dd4a23c44d0..cec88f4409e 100644
--- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts
+++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts
@@ -111,6 +111,12 @@ describe('Node SDK', () => {
setGlobalTracerProviderSpy = Sinon.spy(trace, 'setGlobalTracerProvider');
setGlobalLoggerProviderSpy = Sinon.spy(logs, 'setGlobalLoggerProvider');
+
+ // need to set these to none, since the default value is 'otlp'. Tests either
+ // provide exporters programatically or reset to an appropriate value.
+ process.env.OTEL_TRACES_EXPORTER = 'none';
+ process.env.OTEL_LOGS_EXPORTER = 'none';
+ process.env.OTEL_METRICS_EXPORTER = 'none';
});
afterEach(() => {
@@ -124,13 +130,10 @@ describe('Node SDK', () => {
delete process.env.OTEL_METRICS_EXPORTER;
delete process.env.OTEL_PROPAGATORS;
delete process.env.OTEL_TRACES_EXPORTER;
+ delete process.env.OTEL_NODE_EXPERIMENTAL_SDK_METRICS;
});
it('should not register more than the minimal SDK components', async () => {
- // need to set these to none, since the default value is 'otlp'
- process.env.OTEL_TRACES_EXPORTER = 'none';
- process.env.OTEL_LOGS_EXPORTER = 'none';
- process.env.OTEL_METRICS_EXPORTER = 'none';
const sdk = new NodeSDK({
autoDetectResources: false,
});
@@ -272,9 +275,6 @@ describe('Node SDK', () => {
});
it('should register a meter provider if a reader is provided', async () => {
- // need to set OTEL_TRACES_EXPORTER to none since default value is otlp
- // which sets up an exporter and affects the context manager
- process.env.OTEL_TRACES_EXPORTER = 'none';
const exporter = new ConsoleMetricExporter();
const metricReader = new PeriodicExportingMetricReader({
exporter: exporter,
@@ -303,9 +303,6 @@ describe('Node SDK', () => {
});
it('should register a meter provider if multiple readers are provided', async () => {
- // need to set OTEL_TRACES_EXPORTER to none since default value is otlp
- // which sets up an exporter and affects the context manager
- process.env.OTEL_TRACES_EXPORTER = 'none';
const consoleExporter = new ConsoleMetricExporter();
const inMemoryExporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
@@ -347,9 +344,6 @@ describe('Node SDK', () => {
});
it('should show deprecation warning when using metricReader option', async () => {
- // need to set OTEL_TRACES_EXPORTER to none since default value is otlp
- // which sets up an exporter and affects the context manager
- process.env.OTEL_TRACES_EXPORTER = 'none';
const exporter = new ConsoleMetricExporter();
const metricReader = new PeriodicExportingMetricReader({
exporter: exporter,
@@ -378,9 +372,6 @@ describe('Node SDK', () => {
});
it('should not show deprecation warning when using metricReaders option', async () => {
- // need to set OTEL_TRACES_EXPORTER to none since default value is otlp
- // which sets up an exporter and affects the context manager
- process.env.OTEL_TRACES_EXPORTER = 'none';
const exporter = new ConsoleMetricExporter();
const metricReader = new PeriodicExportingMetricReader({
exporter: exporter,
@@ -409,8 +400,6 @@ describe('Node SDK', () => {
});
it('should not register meter provider when metricReaders is empty array', async () => {
- // need to set OTEL_TRACES_EXPORTER to none since default value is otlp
- process.env.OTEL_TRACES_EXPORTER = 'none';
const sdk = new NodeSDK({
metricReaders: [],
autoDetectResources: false,
@@ -431,8 +420,68 @@ describe('Node SDK', () => {
await sdk.shutdown();
});
+ it('should register a meter provider to the tracer provider if both initialized and metrics enabled', async () => {
+ process.env.OTEL_NODE_EXPERIMENTAL_SDK_METRICS = 'true';
+ const exporter = new ConsoleMetricExporter();
+ const metricReader = new PeriodicExportingMetricReader({
+ exporter: exporter,
+ exportIntervalMillis: 100,
+ exportTimeoutMillis: 100,
+ });
+
+ const sdk = new NodeSDK({
+ metricReader: metricReader,
+ traceExporter: new ConsoleSpanExporter(),
+ autoDetectResources: false,
+ });
+
+ sdk.start();
+
+ assertDefaultContextManagerRegistered();
+ assertDefaultPropagatorRegistered();
+
+ assert.strictEqual(setGlobalTracerProviderSpy.callCount, 1);
+ const tracerProvider = setGlobalTracerProviderSpy.lastCall.args[0];
+ assert.ok(tracerProvider instanceof NodeTracerProvider);
+ assert.ok(
+ (tracerProvider as any)._config.meterProvider instanceof MeterProvider
+ );
+
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider);
+
+ await sdk.shutdown();
+ });
+
+ it('should not register a meter provider to the tracer provider if both initialized but metrics disabled', async () => {
+ const exporter = new ConsoleMetricExporter();
+ const metricReader = new PeriodicExportingMetricReader({
+ exporter: exporter,
+ exportIntervalMillis: 100,
+ exportTimeoutMillis: 100,
+ });
+
+ const sdk = new NodeSDK({
+ metricReader: metricReader,
+ traceExporter: new ConsoleSpanExporter(),
+ autoDetectResources: false,
+ });
+
+ sdk.start();
+
+ assertDefaultContextManagerRegistered();
+ assertDefaultPropagatorRegistered();
+
+ assert.strictEqual(setGlobalTracerProviderSpy.callCount, 1);
+ const tracerProvider = setGlobalTracerProviderSpy.lastCall.args[0];
+ assert.ok(tracerProvider instanceof NodeTracerProvider);
+ assert.equal((tracerProvider as any)._config.meterProvider, undefined);
+
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider);
+
+ await sdk.shutdown();
+ });
+
it('should register a logger provider if a log record processor is provided', async () => {
- process.env.OTEL_TRACES_EXPORTER = 'none';
const logRecordExporter = new InMemoryLogRecordExporter();
const logRecordProcessor = new SimpleLogRecordProcessor(
logRecordExporter
@@ -604,9 +653,6 @@ describe('Node SDK', () => {
}
it('should register meter views when provided', async () => {
- // need to set OTEL_TRACES_EXPORTER to none since default value is otlp
- // which sets up an exporter and affects the context manager
- process.env.OTEL_TRACES_EXPORTER = 'none';
const exporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
@@ -1163,6 +1209,9 @@ describe('Node SDK', () => {
beforeEach(() => {
stubLogger = Sinon.stub(diag, 'info');
+ delete process.env.OTEL_LOGS_EXPORTER;
+ delete process.env.OTEL_METRICS_EXPORTER;
+ delete process.env.OTEL_TRACES_EXPORTER;
});
afterEach(() => {
@@ -1303,8 +1352,6 @@ describe('Node SDK', () => {
it('should apply OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT and OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT', async () => {
// arrange
- process.env.OTEL_TRACES_EXPORTER = 'none';
- process.env.OTEL_METRICS_EXPORTER = 'none';
process.env.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT = '2';
process.env.OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = '10';
@@ -1355,6 +1402,9 @@ describe('Node SDK', () => {
beforeEach(() => {
infoStub = Sinon.stub(diag, 'info');
warnStub = Sinon.stub(diag, 'warn');
+ delete process.env.OTEL_LOGS_EXPORTER;
+ delete process.env.OTEL_METRICS_EXPORTER;
+ delete process.env.OTEL_TRACES_EXPORTER;
});
afterEach(() => {
@@ -1684,6 +1734,9 @@ describe('Node SDK', () => {
beforeEach(() => {
stubLoggerError = Sinon.stub(diag, 'warn');
+ delete process.env.OTEL_LOGS_EXPORTER;
+ delete process.env.OTEL_METRICS_EXPORTER;
+ delete process.env.OTEL_TRACES_EXPORTER;
});
afterEach(() => {
diff --git a/package-lock.json b/package-lock.json
index 325ec03bda5..b460bb4be50 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24588,6 +24588,7 @@
},
"devDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0",
+ "@opentelemetry/sdk-metrics": "2.2.0",
"@types/benchmark": "2.1.5",
"@types/mocha": "10.0.10",
"@types/node": "18.19.130",
@@ -24615,6 +24616,56 @@
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
+ "packages/opentelemetry-sdk-trace-base/node_modules/@opentelemetry/sdk-metrics": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
+ "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "packages/opentelemetry-sdk-trace-base/node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
+ "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "packages/opentelemetry-sdk-trace-base/node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
"packages/opentelemetry-sdk-trace-node": {
"name": "@opentelemetry/sdk-trace-node",
"version": "2.5.0",
diff --git a/packages/opentelemetry-sdk-trace-base/package.json b/packages/opentelemetry-sdk-trace-base/package.json
index 2c8a50e201c..67872e52ddd 100644
--- a/packages/opentelemetry-sdk-trace-base/package.json
+++ b/packages/opentelemetry-sdk-trace-base/package.json
@@ -65,6 +65,7 @@
},
"devDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0",
+ "@opentelemetry/sdk-metrics": "2.2.0",
"@types/benchmark": "2.1.5",
"@types/mocha": "10.0.10",
"@types/node": "18.19.130",
diff --git a/packages/opentelemetry-sdk-trace-base/src/Span.ts b/packages/opentelemetry-sdk-trace-base/src/Span.ts
index c9b1cd9cabe..b65384e705d 100644
--- a/packages/opentelemetry-sdk-trace-base/src/Span.ts
+++ b/packages/opentelemetry-sdk-trace-base/src/Span.ts
@@ -72,6 +72,7 @@ interface SpanOptions {
attributes?: Attributes;
spanLimits: SpanLimits;
spanProcessor: SpanProcessor;
+ recordEndMetrics?: () => void;
}
/**
@@ -105,6 +106,7 @@ export class SpanImpl implements Span {
private readonly _spanProcessor: SpanProcessor;
private readonly _spanLimits: SpanLimits;
private readonly _attributeValueLengthLimit: number;
+ private readonly _recordEndMetrics?: () => void;
private readonly _performanceStartTime: number;
private readonly _performanceOffset: number;
@@ -133,6 +135,7 @@ export class SpanImpl implements Span {
this.startTime = this._getTime(opts.startTime ?? now);
this.resource = opts.resource;
this.instrumentationScope = opts.scope;
+ this._recordEndMetrics = opts.recordEndMetrics;
if (opts.attributes != null) {
this.setAttributes(opts.attributes);
@@ -300,6 +303,7 @@ export class SpanImpl implements Span {
this._spanProcessor.onEnding(this);
}
+ this._recordEndMetrics?.();
this._ended = true;
this._spanProcessor.onEnd(this);
}
diff --git a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts
index ec587a1eed2..eb84fd2055c 100644
--- a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts
+++ b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts
@@ -28,6 +28,8 @@ import { Sampler } from './Sampler';
import { IdGenerator } from './IdGenerator';
import { RandomIdGenerator } from './platform';
import { Resource } from '@opentelemetry/resources';
+import { TracerMetrics } from './TracerMetrics';
+import { VERSION } from './version';
/**
* This class represents a basic tracer.
@@ -41,6 +43,7 @@ export class Tracer implements api.Tracer {
private readonly _resource: Resource;
private readonly _spanProcessor: SpanProcessor;
+ private readonly _tracerMetrics: TracerMetrics;
/**
* Constructs a new Tracer instance.
@@ -59,6 +62,11 @@ export class Tracer implements api.Tracer {
this._resource = resource;
this._spanProcessor = spanProcessor;
this.instrumentationScope = instrumentationScope;
+
+ const meter = localConfig.meterProvider
+ ? localConfig.meterProvider.getMeter('@opentelemetry/sdk-trace', VERSION)
+ : api.createNoopMeter();
+ this._tracerMetrics = new TracerMetrics(meter);
}
/**
@@ -120,6 +128,11 @@ export class Tracer implements api.Tracer {
links
);
+ const recordEndMetrics = this._tracerMetrics.startSpan(
+ parentSpanContext,
+ samplingResult.decision
+ );
+
traceState = samplingResult.traceState ?? traceState;
const traceFlags =
@@ -154,6 +167,7 @@ export class Tracer implements api.Tracer {
startTime: options.startTime,
spanProcessor: this._spanProcessor,
spanLimits: this._spanLimits,
+ recordEndMetrics,
});
return span;
}
diff --git a/packages/opentelemetry-sdk-trace-base/src/TracerMetrics.ts b/packages/opentelemetry-sdk-trace-base/src/TracerMetrics.ts
new file mode 100644
index 00000000000..dd5630ba612
--- /dev/null
+++ b/packages/opentelemetry-sdk-trace-base/src/TracerMetrics.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Counter, Meter, SpanContext, UpDownCounter } from '@opentelemetry/api';
+import { SamplingDecision } from './Sampler';
+import {
+ ATTR_OTEL_SPAN_PARENT_ORIGIN,
+ ATTR_OTEL_SPAN_SAMPLING_RESULT,
+ METRIC_OTEL_SDK_SPAN_LIVE,
+ METRIC_OTEL_SDK_SPAN_STARTED,
+} from './semconv';
+
+/**
+ * Generates `otel.sdk.span.*` metrics.
+ * https://opentelemetry.io/docs/specs/semconv/otel/sdk-metrics/#span-metrics
+ */
+export class TracerMetrics {
+ private readonly startedSpans: Counter;
+ private readonly liveSpans: UpDownCounter;
+
+ constructor(meter: Meter) {
+ this.startedSpans = meter.createCounter(METRIC_OTEL_SDK_SPAN_STARTED, {
+ unit: '{span}',
+ description: 'The number of created spans.',
+ });
+
+ this.liveSpans = meter.createUpDownCounter(METRIC_OTEL_SDK_SPAN_LIVE, {
+ unit: '{span}',
+ description: 'The number of currently live spans.',
+ });
+ }
+
+ startSpan(
+ parentSpanCtx: SpanContext | undefined,
+ samplingDecision: SamplingDecision
+ ): () => void {
+ const samplingDecisionStr = samplingDecisionToString(samplingDecision);
+ this.startedSpans.add(1, {
+ [ATTR_OTEL_SPAN_PARENT_ORIGIN]: parentOrigin(parentSpanCtx),
+ [ATTR_OTEL_SPAN_SAMPLING_RESULT]: samplingDecisionStr,
+ });
+
+ if (samplingDecision === SamplingDecision.NOT_RECORD) {
+ return () => {};
+ }
+
+ const liveSpanAttributes = {
+ [ATTR_OTEL_SPAN_SAMPLING_RESULT]: samplingDecisionStr,
+ };
+ this.liveSpans.add(1, liveSpanAttributes);
+ return () => {
+ this.liveSpans.add(-1, liveSpanAttributes);
+ };
+ }
+}
+
+function parentOrigin(parentSpanContext: SpanContext | undefined): string {
+ if (!parentSpanContext) {
+ return 'none';
+ }
+ if (parentSpanContext.isRemote) {
+ return 'remote';
+ }
+ return 'local';
+}
+
+function samplingDecisionToString(decision: SamplingDecision): string {
+ switch (decision) {
+ case SamplingDecision.RECORD_AND_SAMPLED:
+ return 'RECORD_AND_SAMPLE';
+ case SamplingDecision.RECORD:
+ return 'RECORD_ONLY';
+ case SamplingDecision.NOT_RECORD:
+ return 'DROP';
+ }
+}
diff --git a/packages/opentelemetry-sdk-trace-base/src/semconv.ts b/packages/opentelemetry-sdk-trace-base/src/semconv.ts
new file mode 100644
index 00000000000..7f83cc7ee54
--- /dev/null
+++ b/packages/opentelemetry-sdk-trace-base/src/semconv.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This file contains a copy of unstable semantic convention definitions
+ * used by this package.
+ * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
+ */
+
+/**
+ * Determines whether the span has a parent span, and if so, [whether it is a remote parent](https://opentelemetry.io/docs/specs/otel/trace/api/#isremote)
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_OTEL_SPAN_PARENT_ORIGIN = 'otel.span.parent.origin' as const;
+
+/**
+ * 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' as const;
+
+/**
+ * The number of created spans with `recording=true` for which the end operation has not been called yet.
+ *
+ * @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' as const;
+
+/**
+ * The number of created spans.
+ *
+ * @note Implementations **MUST** record this metric for all spans, even for non-recording ones.
+ *
+ * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const METRIC_OTEL_SDK_SPAN_STARTED = 'otel.sdk.span.started' as const;
diff --git a/packages/opentelemetry-sdk-trace-base/src/types.ts b/packages/opentelemetry-sdk-trace-base/src/types.ts
index 413a5caa1cc..6cd03cfd5d9 100644
--- a/packages/opentelemetry-sdk-trace-base/src/types.ts
+++ b/packages/opentelemetry-sdk-trace-base/src/types.ts
@@ -14,11 +14,15 @@
* limitations under the License.
*/
-import { ContextManager, TextMapPropagator } from '@opentelemetry/api';
-import { Resource } from '@opentelemetry/resources';
-import { IdGenerator } from './IdGenerator';
-import { Sampler } from './Sampler';
-import { SpanProcessor } from './SpanProcessor';
+import type {
+ ContextManager,
+ MeterProvider,
+ TextMapPropagator,
+} from '@opentelemetry/api';
+import type { Resource } from '@opentelemetry/resources';
+import type { IdGenerator } from './IdGenerator';
+import type { Sampler } from './Sampler';
+import type { SpanProcessor } from './SpanProcessor';
/**
* TracerConfig provides an interface for configuring a Basic Tracer.
@@ -54,6 +58,12 @@ export interface TracerConfig {
* List of SpanProcessor for the tracer
*/
spanProcessors?: SpanProcessor[];
+
+ /**
+ * A meter provider to record trace SDK metrics to.
+ * @experimental This option is experimental and is subject to breaking changes in minor releases.
+ */
+ meterProvider?: MeterProvider;
}
/**
diff --git a/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts
index 6a3e86efe99..e283047568c 100644
--- a/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts
+++ b/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts
@@ -27,6 +27,7 @@ import {
defaultResource,
resourceFromAttributes,
} from '@opentelemetry/resources';
+import { MeterProvider } from '@opentelemetry/sdk-metrics';
import * as assert from 'assert';
import * as sinon from 'sinon';
import {
@@ -41,6 +42,8 @@ import {
import { SpanImpl } from '../../src/Span';
import { MultiSpanProcessor } from '../../src/MultiSpanProcessor';
import { Tracer } from '../../src/Tracer';
+import { TestRecordOnlySampler } from './export/TestRecordOnlySampler';
+import { TestMetricReader } from './util';
describe('BasicTracerProvider', () => {
beforeEach(() => {
@@ -564,4 +567,254 @@ describe('BasicTracerProvider', () => {
sinon.assert.calledOnce(shutdownStub);
});
});
+
+ describe('TracerMetrics', () => {
+ it('should record metrics for sampled spans', async () => {
+ const metricReader = new TestMetricReader();
+ const meterProvider = new MeterProvider({
+ readers: [metricReader],
+ });
+ const tracerProvider = new BasicTracerProvider({
+ meterProvider,
+ sampler: new AlwaysOnSampler(),
+ });
+ const tracer = tracerProvider.getTracer('test');
+ const span = tracer.startSpan('span');
+ let { resourceMetrics } = await metricReader.collect();
+ let metrics = resourceMetrics.scopeMetrics[0].metrics;
+ let spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ let spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'none',
+ 'otel.span.sampling_result': 'RECORD_AND_SAMPLE',
+ });
+ assert.ok(spansLiveMetric);
+ assert.strictEqual(spansLiveMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansLiveMetric.dataPoints[0].attributes, {
+ 'otel.span.sampling_result': 'RECORD_AND_SAMPLE',
+ });
+ span.end();
+ ({ resourceMetrics } = await metricReader.collect());
+ metrics = resourceMetrics.scopeMetrics[0].metrics;
+ spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'none',
+ 'otel.span.sampling_result': 'RECORD_AND_SAMPLE',
+ });
+ assert.ok(spansLiveMetric);
+ assert.strictEqual(spansLiveMetric.dataPoints[0].value, 0);
+ assert.deepStrictEqual(spansLiveMetric.dataPoints[0].attributes, {
+ 'otel.span.sampling_result': 'RECORD_AND_SAMPLE',
+ });
+ });
+
+ it('should record metrics for record-only spans', async () => {
+ const metricReader = new TestMetricReader();
+ const meterProvider = new MeterProvider({
+ readers: [metricReader],
+ });
+ const tracerProvider = new BasicTracerProvider({
+ meterProvider,
+ sampler: new TestRecordOnlySampler(),
+ });
+ const tracer = tracerProvider.getTracer('test');
+ const span = tracer.startSpan('span');
+ let { resourceMetrics } = await metricReader.collect();
+ let metrics = resourceMetrics.scopeMetrics[0].metrics;
+ let spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ let spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'none',
+ 'otel.span.sampling_result': 'RECORD_ONLY',
+ });
+ assert.ok(spansLiveMetric);
+ assert.strictEqual(spansLiveMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansLiveMetric.dataPoints[0].attributes, {
+ 'otel.span.sampling_result': 'RECORD_ONLY',
+ });
+ span.end();
+ ({ resourceMetrics } = await metricReader.collect());
+ metrics = resourceMetrics.scopeMetrics[0].metrics;
+ spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'none',
+ 'otel.span.sampling_result': 'RECORD_ONLY',
+ });
+ assert.ok(spansLiveMetric);
+ assert.strictEqual(spansLiveMetric.dataPoints[0].value, 0);
+ assert.deepStrictEqual(spansLiveMetric.dataPoints[0].attributes, {
+ 'otel.span.sampling_result': 'RECORD_ONLY',
+ });
+ });
+
+ it('should record metrics for dropped spans', async () => {
+ const metricReader = new TestMetricReader();
+ const meterProvider = new MeterProvider({
+ readers: [metricReader],
+ });
+ const tracerProvider = new BasicTracerProvider({
+ meterProvider,
+ sampler: new AlwaysOffSampler(),
+ });
+ const tracer = tracerProvider.getTracer('test');
+ const span = tracer.startSpan('span');
+ let { resourceMetrics } = await metricReader.collect();
+ let metrics = resourceMetrics.scopeMetrics[0].metrics;
+ let spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ let spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'none',
+ 'otel.span.sampling_result': 'DROP',
+ });
+ assert.strictEqual(spansLiveMetric, undefined);
+ span.end();
+ ({ resourceMetrics } = await metricReader.collect());
+ metrics = resourceMetrics.scopeMetrics[0].metrics;
+ spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'none',
+ 'otel.span.sampling_result': 'DROP',
+ });
+ assert.strictEqual(spansLiveMetric, undefined);
+ });
+
+ it('should record metrics for dropped spans with remote parent', async () => {
+ const metricReader = new TestMetricReader();
+ const meterProvider = new MeterProvider({
+ readers: [metricReader],
+ });
+ const tracerProvider = new BasicTracerProvider({
+ meterProvider,
+ sampler: new AlwaysOffSampler(),
+ });
+ const tracer = tracerProvider.getTracer('test');
+ const parentContext = trace.setSpanContext(context.active(), {
+ traceId: 'd4cda95b652f4a1592b449d5929fda1b',
+ spanId: '6e0c63257de34c92',
+ traceFlags: TraceFlags.SAMPLED,
+ isRemote: true,
+ });
+ const span = tracer.startSpan('span', undefined, parentContext);
+ let { resourceMetrics } = await metricReader.collect();
+ let metrics = resourceMetrics.scopeMetrics[0].metrics;
+ let spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ let spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'remote',
+ 'otel.span.sampling_result': 'DROP',
+ });
+ assert.strictEqual(spansLiveMetric, undefined);
+ span.end();
+ ({ resourceMetrics } = await metricReader.collect());
+ metrics = resourceMetrics.scopeMetrics[0].metrics;
+ spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'remote',
+ 'otel.span.sampling_result': 'DROP',
+ });
+ assert.strictEqual(spansLiveMetric, undefined);
+ });
+
+ it('should record metrics for dropped spans with local parent', async () => {
+ const metricReader = new TestMetricReader();
+ const meterProvider = new MeterProvider({
+ readers: [metricReader],
+ });
+ const tracerProvider = new BasicTracerProvider({
+ meterProvider,
+ sampler: new AlwaysOffSampler(),
+ });
+ const tracer = tracerProvider.getTracer('test');
+ const parentContext = trace.setSpanContext(context.active(), {
+ traceId: 'd4cda95b652f4a1592b449d5929fda1b',
+ spanId: '6e0c63257de34c92',
+ traceFlags: TraceFlags.SAMPLED,
+ isRemote: false,
+ });
+ const span = tracer.startSpan('span', undefined, parentContext);
+ let { resourceMetrics } = await metricReader.collect();
+ let metrics = resourceMetrics.scopeMetrics[0].metrics;
+ let spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ let spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'local',
+ 'otel.span.sampling_result': 'DROP',
+ });
+ assert.strictEqual(spansLiveMetric, undefined);
+ span.end();
+ ({ resourceMetrics } = await metricReader.collect());
+ metrics = resourceMetrics.scopeMetrics[0].metrics;
+ spansStartedMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.started'
+ );
+ spansLiveMetric = metrics.find(
+ metric => metric.descriptor.name === 'otel.sdk.span.live'
+ );
+ assert.ok(spansStartedMetric);
+ assert.strictEqual(spansStartedMetric.dataPoints[0].value, 1);
+ assert.deepStrictEqual(spansStartedMetric.dataPoints[0].attributes, {
+ 'otel.span.parent.origin': 'local',
+ 'otel.span.sampling_result': 'DROP',
+ });
+ assert.strictEqual(spansLiveMetric, undefined);
+ });
+ });
});
diff --git a/packages/opentelemetry-sdk-trace-base/test/common/util.ts b/packages/opentelemetry-sdk-trace-base/test/common/util.ts
index ab34f2ab1be..60bc657c0cb 100644
--- a/packages/opentelemetry-sdk-trace-base/test/common/util.ts
+++ b/packages/opentelemetry-sdk-trace-base/test/common/util.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { MetricReader } from '@opentelemetry/sdk-metrics';
+
export const validAttributes = {
string: 'string',
number: 0,
@@ -33,3 +35,12 @@ export const invalidAttributes = {
};
export function assertAssignable(val: T): asserts val is T {}
+
+export class TestMetricReader extends MetricReader {
+ protected override onShutdown(): Promise {
+ return Promise.resolve();
+ }
+ protected override onForceFlush(): Promise {
+ return Promise.resolve();
+ }
+}
diff --git a/packages/opentelemetry-sdk-trace-base/tsconfig.esm.json b/packages/opentelemetry-sdk-trace-base/tsconfig.esm.json
index 5c669f84042..1c23f621bf2 100644
--- a/packages/opentelemetry-sdk-trace-base/tsconfig.esm.json
+++ b/packages/opentelemetry-sdk-trace-base/tsconfig.esm.json
@@ -25,6 +25,9 @@
},
{
"path": "../opentelemetry-resources"
+ },
+ {
+ "path": "../sdk-metrics"
}
]
}
diff --git a/packages/opentelemetry-sdk-trace-base/tsconfig.esnext.json b/packages/opentelemetry-sdk-trace-base/tsconfig.esnext.json
index c49b4c14d71..aeedeefeb7b 100644
--- a/packages/opentelemetry-sdk-trace-base/tsconfig.esnext.json
+++ b/packages/opentelemetry-sdk-trace-base/tsconfig.esnext.json
@@ -25,6 +25,9 @@
},
{
"path": "../opentelemetry-resources"
+ },
+ {
+ "path": "../sdk-metrics"
}
]
}
diff --git a/packages/opentelemetry-sdk-trace-base/tsconfig.json b/packages/opentelemetry-sdk-trace-base/tsconfig.json
index b5736caac3c..fa7ab56e53e 100644
--- a/packages/opentelemetry-sdk-trace-base/tsconfig.json
+++ b/packages/opentelemetry-sdk-trace-base/tsconfig.json
@@ -26,6 +26,9 @@
},
{
"path": "../opentelemetry-resources"
+ },
+ {
+ "path": "../sdk-metrics"
}
]
}