diff --git a/CHANGELOG.md b/CHANGELOG.md index 507cc3b7c2e..abb0fed11c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features -* feat(sdk-logs): implement log creation metrics [#6433](https://github.com/open-telemetry/opentelemetry-js/pull/6433) @anuraaga +* feat(sdk-trace): implement span processor metrics [#6504](https://github.com/open-telemetry/opentelemetry-js/pull/6504) @anuraaga +* feat(sdk-logs): implement log creation metrics [#6433](https://github.com/open-telemetry/opentelemetry-js/pull/6433) @anuraaga ### :bug: Bug Fixes diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index d9e865515ed..f671193be5d 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -39,7 +39,10 @@ import { ConsoleMetricExporter, PeriodicExportingMetricReader, } from '@opentelemetry/sdk-metrics'; -import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import type { + SpanExporter, + SpanProcessor, +} from '@opentelemetry/sdk-trace-base'; import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; import type { NodeTracerConfig } from '@opentelemetry/sdk-trace-node'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; @@ -65,7 +68,8 @@ import { type TracerProviderConfig = { tracerConfig: NodeTracerConfig; - spanProcessors: SpanProcessor[]; + spanProcessors: SpanProcessor[] | undefined; + traceExporter: SpanExporter | undefined; }; export type MeterProviderConfig = { @@ -225,16 +229,16 @@ export class NodeSDK { ); } - const spanProcessor = - configuration.spanProcessor ?? - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - new BatchSpanProcessor(configuration.traceExporter!); - - const spanProcessors = configuration.spanProcessors ?? [spanProcessor]; + const spanProcessors = + configuration.spanProcessors ?? + (configuration.spanProcessor + ? [configuration.spanProcessor] + : undefined); this._tracerProviderConfig = { tracerConfig: tracerProviderConfig, spanProcessors, + traceExporter: configuration.traceExporter, }; } @@ -340,9 +344,23 @@ export class NodeSDK { } } - const spanProcessors = this._tracerProviderConfig - ? this._tracerProviderConfig.spanProcessors - : getSpanProcessorsFromEnv(); + let spanProcessors: SpanProcessor[]; + if (this._tracerProviderConfig) { + // If tracerProviderConfig is set, either spanProcessors or traceExporter is set. + if (this._tracerProviderConfig.spanProcessors) { + spanProcessors = this._tracerProviderConfig.spanProcessors; + } else { + spanProcessors = [ + new BatchSpanProcessor(this._tracerProviderConfig.traceExporter!, { + meterProvider: sdkMetricsEnabled ? this._meterProvider : undefined, + }), + ]; + } + } else { + spanProcessors = getSpanProcessorsFromEnv( + sdkMetricsEnabled ? this._meterProvider : undefined + ); + } // Only register if there is a span processor if (spanProcessors.length > 0) { diff --git a/experimental/packages/opentelemetry-sdk-node/src/utils.ts b/experimental/packages/opentelemetry-sdk-node/src/utils.ts index e819a1f0bee..d7fba460dfb 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/utils.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/utils.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { ContextManager, TextMapPropagator } from '@opentelemetry/api'; +import type { + ContextManager, + MeterProvider, + TextMapPropagator, +} from '@opentelemetry/api'; import { context, diag, propagation } from '@opentelemetry/api'; import { CompositePropagator, @@ -177,7 +181,9 @@ function getOtlpExporterFromEnv(): SpanExporter { } } -export function getSpanProcessorsFromEnv(): SpanProcessor[] { +export function getSpanProcessorsFromEnv( + meterProvider: MeterProvider | undefined +): SpanProcessor[] { const exportersMap = new Map SpanExporter>([ ['otlp', () => getOtlpExporterFromEnv()], ['zipkin', () => new ZipkinExporter()], @@ -220,9 +226,9 @@ export function getSpanProcessorsFromEnv(): SpanProcessor[] { for (const exp of exporters) { if (exp instanceof ConsoleSpanExporter) { - processors.push(new SimpleSpanProcessor(exp)); + processors.push(new SimpleSpanProcessor(exp, { meterProvider })); } else { - processors.push(new BatchSpanProcessor(exp)); + processors.push(new BatchSpanProcessor(exp, { meterProvider })); } } diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 212a2a5ef78..e686504783a 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -419,7 +419,7 @@ describe('Node SDK', () => { }); const sdk = new NodeSDK({ - metricReader: metricReader, + metricReaders: [metricReader], traceExporter: new ConsoleSpanExporter(), logRecordProcessors: [ new SimpleLogRecordProcessor(new InMemoryLogRecordExporter()), @@ -438,6 +438,11 @@ describe('Node SDK', () => { assert.ok( (tracerProvider as any)._config.meterProvider instanceof MeterProvider ); + assert.notDeepEqual( + (tracerProvider as any)._activeSpanProcessor._spanProcessors[0]._metrics + .processedSpans, + NOOP_COUNTER_METRIC + ); const loggerProvider = setGlobalLoggerProviderSpy.lastCall.args[0]; assert.notDeepEqual( @@ -476,6 +481,11 @@ describe('Node SDK', () => { const tracerProvider = setGlobalTracerProviderSpy.lastCall.args[0]; assert.ok(tracerProvider instanceof NodeTracerProvider); assert.equal((tracerProvider as any)._config.meterProvider, undefined); + assert.deepEqual( + (tracerProvider as any)._activeSpanProcessor._spanProcessors[0]._metrics + .processedSpans, + NOOP_COUNTER_METRIC + ); const loggerProvider = setGlobalLoggerProviderSpy.lastCall.args[0]; assert.deepEqual( diff --git a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts index d05ee3aa86e..0bb01cf6778 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts @@ -4,7 +4,7 @@ */ import type { Context } from '@opentelemetry/api'; -import { context, diag, TraceFlags } from '@opentelemetry/api'; +import { context, createNoopMeter, diag, TraceFlags } from '@opentelemetry/api'; import { BindOnceFuture, ExportResultCode, @@ -14,9 +14,12 @@ import { } from '@opentelemetry/core'; import type { Span } from '../Span'; import type { SpanProcessor } from '../SpanProcessor'; +import type { SpanProcessorConfig } from './SpanProcessorConfig'; import type { BufferConfig } from '../types'; import type { ReadableSpan } from './ReadableSpan'; import type { SpanExporter } from './SpanExporter'; +import { SpanProcessorMetrics } from './SpanProcessorMetrics'; +import { OTEL_COMPONENT_TYPE_VALUE_BATCHING_SPAN_PROCESSOR } from '../semconv'; /** * Implementation of the {@link SpanProcessor} that batches spans exported by @@ -30,6 +33,7 @@ export abstract class BatchSpanProcessorBase private readonly _scheduledDelayMillis: number; private readonly _exportTimeoutMillis: number; private readonly _exporter: SpanExporter; + private readonly _metrics: SpanProcessorMetrics; private _isExporting = false; private _finishedSpans: ReadableSpan[] = []; @@ -37,7 +41,7 @@ export abstract class BatchSpanProcessorBase private _shutdownOnce: BindOnceFuture; private _droppedSpansCount: number = 0; - constructor(exporter: SpanExporter, config?: T) { + constructor(exporter: SpanExporter, config?: T & SpanProcessorConfig) { this._exporter = exporter; this._maxExportBatchSize = typeof config?.maxExportBatchSize === 'number' @@ -64,6 +68,18 @@ export abstract class BatchSpanProcessorBase ); this._maxExportBatchSize = this._maxQueueSize; } + + const meter = config?.meterProvider + ? config.meterProvider.getMeter('@opentelemetry/sdk-trace') + : createNoopMeter(); + this._metrics = new SpanProcessorMetrics( + OTEL_COMPONENT_TYPE_VALUE_BATCHING_SPAN_PROCESSOR, + meter, + { + capacity: this._maxQueueSize, + getQueueSize: () => this._finishedSpans.length, + } + ); } forceFlush(): Promise { @@ -101,6 +117,7 @@ export abstract class BatchSpanProcessorBase return this._flushAll(); }) .then(() => { + this._metrics.shutdown(); return this._exporter.shutdown(); }); } @@ -114,6 +131,7 @@ export abstract class BatchSpanProcessorBase diag.debug('maxQueueSize reached, dropping spans'); } this._droppedSpansCount++; + this._metrics.dropSpans(1); return; } @@ -179,6 +197,7 @@ export abstract class BatchSpanProcessorBase const doExport = () => this._exporter.export(spans, result => { clearTimeout(timer); + this._metrics.finishSpans(spans.length, result.error); if (result.code === ExportResultCode.SUCCESS) { resolve(); } else { diff --git a/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts b/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts index dceca3be24b..6c89e6afe83 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts @@ -4,7 +4,7 @@ */ import type { Context } from '@opentelemetry/api'; -import { TraceFlags } from '@opentelemetry/api'; +import { createNoopMeter, TraceFlags } from '@opentelemetry/api'; import { internal, ExportResultCode, @@ -13,8 +13,11 @@ import { } from '@opentelemetry/core'; import type { Span } from '../Span'; import type { SpanProcessor } from '../SpanProcessor'; +import type { SpanProcessorConfig } from './SpanProcessorConfig'; import type { ReadableSpan } from './ReadableSpan'; import type { SpanExporter } from './SpanExporter'; +import { SpanProcessorMetrics } from './SpanProcessorMetrics'; +import { OTEL_COMPONENT_TYPE_VALUE_SIMPLE_SPAN_PROCESSOR } from '../semconv'; /** * An implementation of the {@link SpanProcessor} that converts the {@link Span} @@ -26,13 +29,22 @@ import type { SpanExporter } from './SpanExporter'; */ export class SimpleSpanProcessor implements SpanProcessor { private readonly _exporter: SpanExporter; + private readonly _metrics: SpanProcessorMetrics; private _shutdownOnce: BindOnceFuture; private _pendingExports: Set>; - constructor(exporter: SpanExporter) { + constructor(exporter: SpanExporter, config?: SpanProcessorConfig) { this._exporter = exporter; this._shutdownOnce = new BindOnceFuture(this._shutdown, this); this._pendingExports = new Set>(); + + const meter = config?.meterProvider + ? config.meterProvider.getMeter('@opentelemetry/sdk-trace') + : createNoopMeter(); + this._metrics = new SpanProcessorMetrics( + OTEL_COMPONENT_TYPE_VALUE_SIMPLE_SPAN_PROCESSOR, + meter + ); } async forceFlush(): Promise { @@ -70,6 +82,7 @@ export class SimpleSpanProcessor implements SpanProcessor { } const result = await internal._export(this._exporter, [span]); + this._metrics.finishSpans(1, result.error); if (result.code !== ExportResultCode.SUCCESS) { throw ( result.error ?? @@ -83,6 +96,7 @@ export class SimpleSpanProcessor implements SpanProcessor { } private _shutdown(): Promise { + this._metrics.shutdown(); return this._exporter.shutdown(); } } diff --git a/packages/opentelemetry-sdk-trace-base/src/export/SpanProcessorConfig.ts b/packages/opentelemetry-sdk-trace-base/src/export/SpanProcessorConfig.ts new file mode 100644 index 00000000000..b3cee8bdaae --- /dev/null +++ b/packages/opentelemetry-sdk-trace-base/src/export/SpanProcessorConfig.ts @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { MeterProvider } from '@opentelemetry/api'; + +/** + * Common options for SDK span processors. + */ +export interface SpanProcessorConfig { + meterProvider?: MeterProvider; +} diff --git a/packages/opentelemetry-sdk-trace-base/src/export/SpanProcessorMetrics.ts b/packages/opentelemetry-sdk-trace-base/src/export/SpanProcessorMetrics.ts new file mode 100644 index 00000000000..b581703dac4 --- /dev/null +++ b/packages/opentelemetry-sdk-trace-base/src/export/SpanProcessorMetrics.ts @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + Attributes, + Counter, + Meter, + ObservableCallback, + ObservableUpDownCounter, +} from '@opentelemetry/api'; +import { + ATTR_ERROR_TYPE, + ATTR_OTEL_COMPONENT_NAME, + ATTR_OTEL_COMPONENT_TYPE, + METRIC_OTEL_SDK_PROCESSOR_SPAN_PROCESSED, + METRIC_OTEL_SDK_PROCESSOR_SPAN_QUEUE_CAPACITY, + METRIC_OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE, +} from '../semconv'; + +const componentCounter = new Map(); + +interface QueueConfig { + capacity: number; + getQueueSize: () => number; +} + +export class SpanProcessorMetrics { + private readonly processedSpans: Counter; + private readonly queueSize: ObservableUpDownCounter | undefined; + private readonly queueSizeCallback: ObservableCallback | undefined; + + private readonly standardAttrs: Attributes; + private readonly droppedAttrs: Attributes; + + constructor(componentType: string, meter: Meter, queueConfig?: QueueConfig) { + const counter = componentCounter.get(componentType) ?? 0; + componentCounter.set(componentType, counter + 1); + + this.standardAttrs = { + [ATTR_OTEL_COMPONENT_TYPE]: componentType, + [ATTR_OTEL_COMPONENT_NAME]: `${componentType}/${counter}`, + }; + + this.droppedAttrs = { + ...this.standardAttrs, + [ATTR_ERROR_TYPE]: 'queue_full', + }; + + this.processedSpans = meter.createCounter( + METRIC_OTEL_SDK_PROCESSOR_SPAN_PROCESSED, + { + unit: '{span}', + description: + 'The number of spans for which the processing has finished, either successful or failed.', + } + ); + + if (queueConfig) { + const { capacity, getQueueSize } = queueConfig; + const queueCapacity = meter.createUpDownCounter( + METRIC_OTEL_SDK_PROCESSOR_SPAN_QUEUE_CAPACITY, + { + unit: '{span}', + description: + 'The maximum number of spans the queue of a given instance of an SDK span processor can hold.', + } + ); + queueCapacity.add(capacity, this.standardAttrs); + + this.queueSize = meter.createObservableUpDownCounter( + METRIC_OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE, + { + unit: '{span}', + description: + 'The number of spans in the queue of a given instance of an SDK span processor.', + } + ); + this.queueSizeCallback = result => + result.observe(getQueueSize(), this.standardAttrs); + this.queueSize.addCallback(this.queueSizeCallback); + } + } + + dropSpans(count: number) { + this.processedSpans.add(count, this.droppedAttrs); + } + + finishSpans(count: number, error: Error | undefined) { + if (!error) { + this.processedSpans.add(count, this.standardAttrs); + return; + } + + const attrs = { + ...this.standardAttrs, + [ATTR_ERROR_TYPE]: error.name, + }; + this.processedSpans.add(count, attrs); + } + + shutdown() { + if (this.queueSize && this.queueSizeCallback) { + this.queueSize.removeCallback(this.queueSizeCallback); + } + } +} diff --git a/packages/opentelemetry-sdk-trace-base/src/semconv.ts b/packages/opentelemetry-sdk-trace-base/src/semconv.ts index e4eb8fd8bbd..82e0fcbb8a2 100644 --- a/packages/opentelemetry-sdk-trace-base/src/semconv.ts +++ b/packages/opentelemetry-sdk-trace-base/src/semconv.ts @@ -39,3 +39,121 @@ export const METRIC_OTEL_SDK_SPAN_LIVE = 'otel.sdk.span.live' as const; * @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; + +/** + * The number of spans for which the processing has finished, either successful or failed. + * + * @note For successful processing, `error.type` **MUST NOT** be set. For failed processing, `error.type` **MUST** contain the failure cause. + * For the SDK Simple and Batching Span Processor a span is considered to be processed already when it has been submitted to the exporter, not when the corresponding export call has finished. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const METRIC_OTEL_SDK_PROCESSOR_SPAN_PROCESSED = + 'otel.sdk.processor.span.processed' as const; + +/** + * The maximum number of spans the queue of a given instance of an SDK span processor can hold. + * + * @note Only applies to span processors which use a queue, e.g. the SDK Batching Span Processor. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const METRIC_OTEL_SDK_PROCESSOR_SPAN_QUEUE_CAPACITY = + 'otel.sdk.processor.span.queue.capacity' as const; + +/** + * The number of spans in the queue of a given instance of an SDK span processor. + * + * @note Only applies to span processors which use a queue, e.g. the SDK Batching Span Processor. + * + * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const METRIC_OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE = + 'otel.sdk.processor.span.queue.size' as const; + +/** + * A name uniquely identifying the instance of the OpenTelemetry component within its containing SDK instance. + * + * @example otlp_grpc_span_exporter/0 + * @example custom-name + * + * @note Implementations **SHOULD** ensure a low cardinality for this attribute, even across application or SDK restarts. + * E.g. implementations **MUST NOT** use UUIDs as values for this attribute. + * + * Implementations **MAY** achieve these goals by following a `/` pattern, e.g. `batching_span_processor/0`. + * Hereby `otel.component.type` refers to the corresponding attribute value of the component. + * + * The value of `instance-counter` **MAY** be automatically assigned by the component and uniqueness within the enclosing SDK instance **MUST** be guaranteed. + * For example, `` **MAY** be implemented by using a monotonically increasing counter (starting with `0`), which is incremented every time an + * instance of the given component type is started. + * + * With this implementation, for example the first Batching Span Processor would have `batching_span_processor/0` + * as `otel.component.name`, the second one `batching_span_processor/1` and so on. + * These values will therefore be reused in the case of an application restart. + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_OTEL_COMPONENT_NAME = 'otel.component.name' as const; + +/** + * A name identifying the type of the OpenTelemetry component. + * + * @example batching_span_processor + * @example com.example.MySpanExporter + * + * @note If none of the standardized values apply, implementations **SHOULD** use the language-defined name of the type. + * E.g. for Java the fully qualified classname **SHOULD** be used in this case. + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_OTEL_COMPONENT_TYPE = 'otel.component.type' as const; + +/** + * Enum value "batching_span_processor" for attribute {@link ATTR_OTEL_COMPONENT_TYPE}. + * + * The builtin SDK batching span processor + * + * @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const OTEL_COMPONENT_TYPE_VALUE_BATCHING_SPAN_PROCESSOR = + 'batching_span_processor' as const; + +/** + * Enum value "simple_span_processor" for attribute {@link ATTR_OTEL_COMPONENT_TYPE}. + * + * The builtin SDK simple span processor + * + * @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const OTEL_COMPONENT_TYPE_VALUE_SIMPLE_SPAN_PROCESSOR = + 'simple_span_processor' as const; + +/** + * Describes a class of error the operation ended with. + * + * @example timeout + * @example java.net.UnknownHostException + * @example server_certificate_invalid + * @example 500 + * + * @note The `error.type` **SHOULD** be predictable, and **SHOULD** have low cardinality. + * + * When `error.type` is set to a type (e.g., an exception type), its + * canonical class name identifying the type within the artifact **SHOULD** be used. + * + * Instrumentations **SHOULD** document the list of errors they report. + * + * The cardinality of `error.type` within one instrumentation library **SHOULD** be low. + * Telemetry consumers that aggregate data from multiple instrumentation libraries and applications + * should be prepared for `error.type` to have high cardinality at query time when no + * additional filters are applied. + * + * If the operation has completed successfully, instrumentations **SHOULD NOT** set `error.type`. + * + * If a specific domain defines its own set of error identifiers (such as HTTP or RPC status codes), + * it's **RECOMMENDED** to: + * + * - Use a domain-specific attribute + * - Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. + */ +export const ATTR_ERROR_TYPE = 'error.type' as const; diff --git a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts index 49e5b40a813..3717d341042 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts @@ -24,11 +24,13 @@ import { InMemorySpanExporter, } from '../../../src'; import { context } from '@opentelemetry/api'; +import { MeterProvider } from '@opentelemetry/sdk-metrics'; import { TestRecordOnlySampler } from './TestRecordOnlySampler'; import { TestTracingSpanExporter } from './TestTracingSpanExporter'; import { TestStackContextManager } from './TestStackContextManager'; import { BatchSpanProcessorBase } from '../../../src/export/BatchSpanProcessorBase'; import { resourceFromAttributes } from '@opentelemetry/resources'; +import { TestMetricReader, withResolvers } from '../util'; function createSampledSpan(spanName: string): Span { const tracer = new BasicTracerProvider({ @@ -561,4 +563,216 @@ describe('BatchSpanProcessorBase', () => { assert.equal(spans.length, 10); }); }); + + describe('Metrics', () => { + it('should record metrics', async () => { + const metricReader = new TestMetricReader(); + const meterProvider = new MeterProvider({ + readers: [metricReader], + }); + const processor = new BatchSpanProcessor(exporter, { + maxQueueSize: 1, + maxExportBatchSize: 1, + scheduledDelayMillis: 1_000_000_000, // Manually flush + meterProvider, + }); + + const exportStub = sinon.stub(exporter, 'export'); + + const { resolve: resolveExport1, promise: export1Promise } = + withResolvers(); + const { resolve: resolveExport2, promise: export2Promise } = + withResolvers(); + + // Signal for when export has started + const { resolve: resolveFirstExport, promise: firstExportPromise } = + withResolvers(); + + exportStub + .onFirstCall() + .callsFake((_spans, resultCallback: (result: ExportResult) => void) => { + resolveFirstExport(); + export1Promise.then(result => resultCallback(result)); + }) + .onSecondCall() + .callsFake((_spans, resultCallback: (result: ExportResult) => void) => { + export2Promise.then(result => resultCallback(result)); + }); + + const span1 = createSampledSpan('span1'); + // Immediately processed + processor.onStart(span1, ROOT_CONTEXT); + processor.onEnd(span1); + + // Wait for span to be sent to exporter. + await firstExportPromise; + + // Queue empty, export in progress, this span is queued. + const span2 = createSampledSpan('span2'); + processor.onStart(span2, ROOT_CONTEXT); + processor.onEnd(span2); + + // Queue full, this span is dropped. + const span3 = createSampledSpan('span3'); + processor.onStart(span3, ROOT_CONTEXT); + processor.onEnd(span3); + + let { resourceMetrics } = await metricReader.collect(); + let scopeMetrics = resourceMetrics.scopeMetrics.find( + sm => sm.scope.name === '@opentelemetry/sdk-trace' + ); + assert.ok(scopeMetrics); + let processedSpansMetric = scopeMetrics.metrics.find( + m => m.descriptor.name === 'otel.sdk.processor.span.processed' + ); + assert.ok(processedSpansMetric); + assert.strictEqual(processedSpansMetric.dataPoints[0].value, 1); + assert.strictEqual( + processedSpansMetric.dataPoints[0].attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + processedSpansMetric.dataPoints[0].attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + assert.strictEqual( + processedSpansMetric.dataPoints[0].attributes['error.type'], + 'queue_full' + ); + let spanCapacityMetric = scopeMetrics.metrics.find( + m => m.descriptor.name === 'otel.sdk.processor.span.queue.capacity' + ); + assert.ok(spanCapacityMetric); + assert.strictEqual(spanCapacityMetric.dataPoints[0].value, 1); + assert.strictEqual( + spanCapacityMetric.dataPoints[0].attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + spanCapacityMetric.dataPoints[0].attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + assert.strictEqual(spanCapacityMetric.dataPoints[0].value, 1); + let spanQueueSizeMetric = scopeMetrics.metrics.find( + m => m.descriptor.name === 'otel.sdk.processor.span.queue.size' + ); + assert.ok(spanQueueSizeMetric); + assert.strictEqual(spanQueueSizeMetric.dataPoints[0].value, 1); + assert.strictEqual( + spanQueueSizeMetric.dataPoints[0].attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + spanQueueSizeMetric.dataPoints[0].attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + sinon.assert.calledOnce(exportStub); + + resolveExport1({ code: ExportResultCode.SUCCESS }); + const error = new Error('Export failed'); + error.name = 'BackendError'; + resolveExport2({ + code: ExportResultCode.FAILED, + error, + }); + + await assert.rejects(processor.forceFlush(), err => { + assert.strictEqual(err, error); + return true; + }); + sinon.assert.calledTwice(exportStub); + + ({ resourceMetrics } = await metricReader.collect()); + scopeMetrics = resourceMetrics.scopeMetrics.find( + sm => sm.scope.name === '@opentelemetry/sdk-trace' + ); + assert.ok(scopeMetrics); + + processedSpansMetric = scopeMetrics.metrics.find( + m => m.descriptor.name === 'otel.sdk.processor.span.processed' + ); + assert.ok(processedSpansMetric); + const processedSpansDataPoints = + processedSpansMetric.dataPoints as Array<{ + value: number; + attributes: Record; + }>; + const queueFullPoint = processedSpansDataPoints.find( + dataPoint => dataPoint.attributes['error.type'] === 'queue_full' + ); + assert.ok(queueFullPoint); + assert.strictEqual(queueFullPoint.value, 1); + assert.strictEqual( + queueFullPoint.attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + queueFullPoint.attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + const successPoint = processedSpansDataPoints.find( + dataPoint => dataPoint.attributes['error.type'] === undefined + ); + assert.ok(successPoint); + assert.strictEqual(successPoint.value, 1); + assert.strictEqual( + successPoint.attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + successPoint.attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + const failedPoint = processedSpansDataPoints.find( + dataPoint => dataPoint.attributes['error.type'] === 'BackendError' + ); + assert.ok(failedPoint); + assert.strictEqual(failedPoint.value, 1); + assert.strictEqual( + failedPoint.attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + failedPoint.attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + + spanCapacityMetric = scopeMetrics.metrics.find( + m => m.descriptor.name === 'otel.sdk.processor.span.queue.capacity' + ); + assert.ok(spanCapacityMetric); + assert.strictEqual(spanCapacityMetric.dataPoints[0].value, 1); + assert.strictEqual( + spanCapacityMetric.dataPoints[0].attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + spanCapacityMetric.dataPoints[0].attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + assert.strictEqual(spanCapacityMetric.dataPoints[0].value, 1); + + spanQueueSizeMetric = scopeMetrics.metrics.find( + m => m.descriptor.name === 'otel.sdk.processor.span.queue.size' + ); + assert.ok(spanQueueSizeMetric); + assert.strictEqual(spanQueueSizeMetric.dataPoints[0].value, 0); + assert.strictEqual( + spanQueueSizeMetric.dataPoints[0].attributes['otel.component.type'], + 'batching_span_processor' + ); + assert.ok( + spanQueueSizeMetric.dataPoints[0].attributes['otel.component.name'] + ?.toString() + .startsWith('batching_span_processor/') + ); + }); + }); }); diff --git a/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts index 806059e8b09..c413c34b4bf 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts @@ -11,10 +11,12 @@ import { TraceFlags, } from '@opentelemetry/api'; import { + type ExportResult, ExportResultCode, loggingErrorHandler, setGlobalErrorHandler, } from '@opentelemetry/core'; +import { MeterProvider } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { @@ -28,6 +30,7 @@ import { TestTracingSpanExporter } from './TestTracingSpanExporter'; import { TestExporterWithDelay } from './TestExporterWithDelay'; import type { Tracer } from '../../../src/Tracer'; import { resourceFromAttributes } from '@opentelemetry/resources'; +import { TestMetricReader } from '../util'; describe('SimpleSpanProcessor', () => { let provider: BasicTracerProvider; @@ -336,4 +339,95 @@ describe('SimpleSpanProcessor', () => { assert.equal(exporterCreatedSpans.length, 0); }); }); + + describe('Metrics', () => { + it('should record metrics', async () => { + const metricReader = new TestMetricReader(); + const meterProvider = new MeterProvider({ + readers: [metricReader], + }); + const processor = new SimpleSpanProcessor(exporter, { + meterProvider, + }); + + const exportStub = sinon.stub(exporter, 'export'); + exportStub + .onFirstCall() + .callsFake((_spans, resultCallback: (result: ExportResult) => void) => { + resultCallback({ code: ExportResultCode.SUCCESS }); + }) + .onSecondCall() + .callsFake((_spans, resultCallback: (result: ExportResult) => void) => { + const error = new Error('Export failed'); + error.name = 'SystemError'; + resultCallback({ code: ExportResultCode.FAILED, error }); + }); + + const spanContext: SpanContext = { + traceId: 'a3cda95b652f4a1592b449d5929fda1b', + spanId: '5e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const tracer = provider.getTracer('default') as Tracer; + const span = new SpanImpl({ + scope: tracer.instrumentationScope, + resource: tracer['_resource'], + context: ROOT_CONTEXT, + spanContext, + name: 'span-name', + kind: SpanKind.CLIENT, + spanLimits: tracer.getSpanLimits(), + spanProcessor: tracer['_spanProcessor'], + }); + processor.onStart(span, ROOT_CONTEXT); + processor.onEnd(span); + processor.onStart(span, ROOT_CONTEXT); + processor.onEnd(span); + + await processor.forceFlush(); + + const { resourceMetrics } = await metricReader.collect(); + const scopeMetrics = resourceMetrics.scopeMetrics.find( + sm => sm.scope.name === '@opentelemetry/sdk-trace' + ); + assert.ok(scopeMetrics); + const processedSpansMetric = scopeMetrics.metrics.find( + m => m.descriptor.name === 'otel.sdk.processor.span.processed' + ); + assert.ok(processedSpansMetric); + const processedSpansDataPoints = + processedSpansMetric.dataPoints as Array<{ + value: number; + attributes: Record; + }>; + const successPoint = processedSpansDataPoints.find( + dataPoint => dataPoint.attributes['error.type'] === undefined + ); + assert.ok(successPoint); + assert.strictEqual(successPoint.value, 1); + assert.strictEqual( + successPoint.attributes['otel.component.type'], + 'simple_span_processor' + ); + assert.ok( + successPoint.attributes['otel.component.name'] + ?.toString() + .startsWith('simple_span_processor/') + ); + const failedPoint = processedSpansDataPoints.find( + dataPoint => dataPoint.attributes['error.type'] === 'SystemError' + ); + assert.ok(failedPoint); + assert.strictEqual(failedPoint.value, 1); + assert.strictEqual( + failedPoint.attributes['otel.component.type'], + 'simple_span_processor' + ); + assert.ok( + failedPoint.attributes['otel.component.name'] + ?.toString() + .startsWith('simple_span_processor/') + ); + }); + }); }); diff --git a/packages/opentelemetry-sdk-trace-base/test/common/util.ts b/packages/opentelemetry-sdk-trace-base/test/common/util.ts index af36681b8be..ad606ee3fcc 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/util.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/util.ts @@ -33,3 +33,25 @@ export class TestMetricReader extends MetricReader { return Promise.resolve(); } } + +interface Resolvers { + promise: Promise; + resolve: (value: T) => void; + reject: (reason: any) => void; +} + +// Use Promise.withResolvers when we can +export function withResolvers(): Resolvers { + let resolve: (value: T) => void; + let reject: (reason: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { + promise, + resolve: resolve!, + reject: reject!, + }; +}