Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2

### :rocket: Features

* feat(sdk-metrics): implement metric reader metrics [#6449](https://github.com/open-telemetry/opentelemetry-js/pull/6449) @anuraaga
Comment thread
trentm marked this conversation as resolved.
Outdated
* feat(sdk-trace): implement span start/end metrics [#6213](https://github.com/open-telemetry/opentelemetry-js/pull/6213) @anuraaga
Comment thread
anuraaga marked this conversation as resolved.

### :bug: Bug Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { IncomingMessage, Server, ServerResponse } from 'http';
import { createServer } from 'http';
import type { ExporterConfig } from './export/types';
import { PrometheusSerializer } from './PrometheusSerializer';
import { OTEL_COMPONENT_TYPE_VALUE_PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER } from './semconv';
/** Node.js v8.x compat */
import { URL } from 'url';

Expand Down Expand Up @@ -60,6 +61,8 @@ export class PrometheusExporter extends MetricReader {
},
aggregationTemporalitySelector: _instrumentType =>
AggregationTemporality.CUMULATIVE,
otelComponentType:
OTEL_COMPONENT_TYPE_VALUE_PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER,
metricProducers: config.metricProducers,
});
this._host =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

/*
* 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
*/

/**
* Enum value "prometheus_http_text_metric_exporter" for attribute {@link ATTR_OTEL_COMPONENT_TYPE}.
*
* Prometheus metric exporter over HTTP with the default text-based format
*
* @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_PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER =
'prometheus_http_text_metric_exporter' as const;
1 change: 1 addition & 0 deletions experimental/packages/opentelemetry-sdk-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ OTEL_NODE_EXPERIMENTAL_SDK_METRICS=true
Currently a subset of the specified metrics are implemented. See the following
linkes for details:

- Metric reader metrics: [MetricReaderMetrics](../../../packages//sdk-metrics/src/export/MetricReaderMetrics.ts)
- Span metrics: [TracerMetrics.ts](../../../packages/opentelemetry-sdk-trace-base/src/TracerMetrics.ts)

## Useful links
Expand Down
12 changes: 7 additions & 5 deletions experimental/packages/opentelemetry-sdk-node/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@ export class NodeSDK {
})
);

// 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'
);

if (
this._meterProviderConfig?.readers &&
// only register if there is a reader, otherwise we waste compute/memory.
Expand All @@ -321,6 +327,7 @@ export class NodeSDK {
resource: this._resource,
views: this._meterProviderConfig?.views ?? [],
readers: this._meterProviderConfig.readers,
sdkMetricsEnabled,
});

this._meterProvider = meterProvider;
Expand All @@ -340,11 +347,6 @@ export class NodeSDK {

// 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,
Expand Down
17 changes: 13 additions & 4 deletions experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/expor
import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';

import { ATTR_HOST_NAME, ATTR_PROCESS_PID } from '../src/semconv';
import { NOOP_HISTOGRAM_METRIC } from '../../../../api/src/metrics/NoopMeter';

function assertDefaultContextManagerRegistered() {
assert.ok(
Expand Down Expand Up @@ -408,7 +409,7 @@ describe('Node SDK', () => {
await sdk.shutdown();
});

it('should register a meter provider to the tracer provider if both initialized and metrics enabled', async () => {
it('should configure components for SDK metrics if enabled', async () => {
process.env.OTEL_NODE_EXPERIMENTAL_SDK_METRICS = 'true';
const exporter = new ConsoleMetricExporter();
const metricReader = new PeriodicExportingMetricReader({
Expand All @@ -418,7 +419,7 @@ describe('Node SDK', () => {
});

const sdk = new NodeSDK({
metricReader: metricReader,
metricReaders: [metricReader],
traceExporter: new ConsoleSpanExporter(),
autoDetectResources: false,
});
Expand All @@ -436,11 +437,15 @@ describe('Node SDK', () => {
);

assert.ok(metrics.getMeterProvider() instanceof MeterProvider);
assert.notDeepEqual(
(metricReader as any)._metrics.collectionDuration,
NOOP_HISTOGRAM_METRIC
);

await sdk.shutdown();
});

it('should not register a meter provider to the tracer provider if both initialized but metrics disabled', async () => {
it('should not configure components for SDK metrics if disabled', async () => {
const exporter = new ConsoleMetricExporter();
const metricReader = new PeriodicExportingMetricReader({
exporter: exporter,
Expand All @@ -449,7 +454,7 @@ describe('Node SDK', () => {
});

const sdk = new NodeSDK({
metricReader: metricReader,
metricReaders: [metricReader],
traceExporter: new ConsoleSpanExporter(),
autoDetectResources: false,
});
Expand All @@ -465,6 +470,10 @@ describe('Node SDK', () => {
assert.equal((tracerProvider as any)._config.meterProvider, undefined);

assert.ok(metrics.getMeterProvider() instanceof MeterProvider);
assert.deepEqual(
(metricReader as any)._metrics.collectionDuration,
NOOP_HISTOGRAM_METRIC
);

await sdk.shutdown();
});
Expand Down
15 changes: 11 additions & 4 deletions packages/opentelemetry-core/src/common/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ export function hrTimeToNanoseconds(time: api.HrTime): number {
return time[0] * SECOND_TO_NANOSECONDS + time[1];
}

/**
* Convert hrTime to microseconds.
* @param time
*/
export function hrTimeToMicroseconds(time: api.HrTime): number {
return time[0] * 1e6 + time[1] / 1e3;
}

/**
* Convert hrTime to milliseconds.
* @param time
Expand All @@ -119,13 +127,12 @@ export function hrTimeToMilliseconds(time: api.HrTime): number {
}

/**
* Convert hrTime to microseconds.
* Convert hrTime to seconds.
* @param time
*/
export function hrTimeToMicroseconds(time: api.HrTime): number {
return time[0] * 1e6 + time[1] / 1e3;
export function hrTimeToSeconds(time: api.HrTime): number {
return time[0] + time[1] / SECOND_TO_NANOSECONDS;
}

/**
* check if time is HrTime
* @param value
Expand Down
1 change: 1 addition & 0 deletions packages/opentelemetry-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
hrTimeToMicroseconds,
hrTimeToMilliseconds,
hrTimeToNanoseconds,
hrTimeToSeconds,
hrTimeToTimeStamp,
isTimeInput,
isTimeInputHrTime,
Expand Down
19 changes: 14 additions & 5 deletions packages/opentelemetry-core/test/common/time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
timeInputToHrTime,
hrTimeDuration,
hrTimeToNanoseconds,
hrTimeToMilliseconds,
hrTimeToMicroseconds,
hrTimeToMilliseconds,
hrTimeToSeconds,
hrTimeToTimeStamp,
isTimeInput,
addHrTimes,
Expand Down Expand Up @@ -164,19 +165,27 @@ describe('time', () => {
});
});

describe('#hrTimeToMicroseconds', () => {
it('should return microseconds', () => {
const output = hrTimeToMicroseconds([1, 200000000]);
assert.deepStrictEqual(output, 1200000);
});
});

describe('#hrTimeToMilliseconds', () => {
it('should return milliseconds', () => {
const output = hrTimeToMilliseconds([1, 200000000]);
assert.deepStrictEqual(output, 1200);
});
});

describe('#hrTimeToMicroseconds', () => {
it('should return microseconds', () => {
const output = hrTimeToMicroseconds([1, 200000000]);
assert.deepStrictEqual(output, 1200000);
describe('#hrTimeToSeconds', () => {
it('should return seconds', () => {
const output = hrTimeToSeconds([1, 200000000]);
assert.deepStrictEqual(output, 1.2);
});
});

describe('#isTimeInput', () => {
it('should return true for a number', () => {
assert.strictEqual(isTimeInput(12), true);
Expand Down
9 changes: 9 additions & 0 deletions packages/sdk-metrics/src/MeterProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export interface MeterProviderOptions {
resource?: Resource;
views?: ViewOptions[];
readers?: IMetricReader[];

/**
* Whether to enable SDK metrics for this meter provider.
* @experimental This option is experimental and is subject to breaking changes in minor releases.
*/
sdkMetricsEnabled?: boolean;
}

/**
Expand All @@ -50,6 +56,9 @@ export class MeterProvider implements IMeterProvider {
const collector = new MetricCollector(this._sharedState, metricReader);
metricReader.setMetricProducer(collector);
this._sharedState.metricCollectors.push(collector);
if (options.sdkMetricsEnabled) {
metricReader.setMeterProvider?.(this);
}
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions packages/sdk-metrics/src/export/MetricReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import {
} from './AggregationSelector';
import type { AggregationOption } from '../view/AggregationOption';
import type { CardinalitySelector } from './CardinalitySelector';
import { MetricReaderMetrics } from './MetricReaderMetrics';
import { VERSION } from '../version';
import { hrTime, hrTimeDuration, hrTimeToSeconds } from '@opentelemetry/core';

export interface MetricReaderOptions {
/**
Expand Down Expand Up @@ -54,6 +57,11 @@ export interface MetricReaderOptions {
* @experimental
*/
metricProducers?: MetricProducer[];
/**
* The component type used for reporting SDK metrics.
* @experimental This option is experimental and is subject to breaking changes in minor releases.
*/
otelComponentType?: string;
}

/**
Expand Down Expand Up @@ -118,6 +126,12 @@ export interface IMetricReader {
* @param options options with timeout.
*/
forceFlush(options?: ForceFlushOptions): Promise<void>;

/**
* Sets the MeterProvider to use for reporting metrics for this reader.
* @experimental This option is experimental and is subject to breaking changes in minor releases.
*/
setMeterProvider?(meterProvider: api.MeterProvider): void;
}

/**
Expand All @@ -132,9 +146,12 @@ export abstract class MetricReader implements IMetricReader {
private _metricProducers: MetricProducer[];
// MetricProducer used by this instance which produces metrics from the SDK
private _sdkMetricProducer?: MetricProducer;
// Metrics about the MetricReader itself
private _metrics: MetricReaderMetrics;
private readonly _aggregationTemporalitySelector: AggregationTemporalitySelector;
private readonly _aggregationSelector: AggregationSelector;
private readonly _cardinalitySelector?: CardinalitySelector;
private readonly _otelComponentType: string;

constructor(options?: MetricReaderOptions) {
this._aggregationSelector =
Expand All @@ -144,6 +161,12 @@ export abstract class MetricReader implements IMetricReader {
DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR;
this._metricProducers = options?.metricProducers ?? [];
this._cardinalitySelector = options?.cardinalitySelector;
this._otelComponentType =
options?.otelComponentType ?? this.constructor.name;
this._metrics = new MetricReaderMetrics(
this._otelComponentType,
api.createNoopMeter()
);
}

setMetricProducer(metricProducer: MetricProducer) {
Expand All @@ -156,6 +179,11 @@ export abstract class MetricReader implements IMetricReader {
this.onInitialized();
}

setMeterProvider(meterProvider: api.MeterProvider): void {
const meter = meterProvider.getMeter('@opentelemetry/sdk-metrics', VERSION);
this._metrics = new MetricReaderMetrics(this._otelComponentType, meter);
}

selectAggregation(instrumentType: InstrumentType): AggregationOption {
return this._aggregationSelector(instrumentType);
}
Expand Down Expand Up @@ -207,6 +235,7 @@ export abstract class MetricReader implements IMetricReader {
throw new Error('MetricReader is shutdown');
}

const startTime = hrTime();
const [sdkCollectionResults, ...additionalCollectionResults] =
await Promise.all([
this._sdkMetricProducer.collect({
Expand All @@ -218,11 +247,21 @@ export abstract class MetricReader implements IMetricReader {
})
),
]);
const endTime = hrTime();

// Merge the results, keeping the SDK's Resource
const errors = sdkCollectionResults.errors.concat(
additionalCollectionResults.flatMap(result => result.errors)
);

const collectDuration = hrTimeToSeconds(hrTimeDuration(startTime, endTime));
this._metrics.recordCollection(
collectDuration,
errors.length > 0
? ((errors[0] as Error).name ?? 'collect_error')
: undefined
);

const resource = sdkCollectionResults.resourceMetrics.resource;
const scopeMetrics =
sdkCollectionResults.resourceMetrics.scopeMetrics.concat(
Expand Down
Loading