diff --git a/CHANGELOG.md b/CHANGELOG.md index 640b1072f27..fa83fa0bf79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ ### :rocket: (Enhancement) -* feat(sdk-metrics): add exponential histogram accumulation / aggregator [#3505](https://github.com/open-telemetry/opentelemetry-js/pull/3505) @mwear +* feat(sdk-metrics): add exponential histogram support [#3505](https://github.com/open-telemetry/opentelemetry-js/pull/3505), [#3506](https://github.com/open-telemetry/opentelemetry-js/pull/3506) @mwear * feat(resources): collect additional process attributes [#3605](https://github.com/open-telemetry/opentelemetry-js/pull/3605) @mwear ### :bug: (Bug Fix) diff --git a/experimental/packages/otlp-transformer/src/metrics/internal.ts b/experimental/packages/otlp-transformer/src/metrics/internal.ts index 98a32e33d0d..7ba7722b6a4 100644 --- a/experimental/packages/otlp-transformer/src/metrics/internal.ts +++ b/experimental/packages/otlp-transformer/src/metrics/internal.ts @@ -28,6 +28,7 @@ import { import { toAttributes } from '../common/internal'; import { EAggregationTemporality, + IExponentialHistogramDataPoint, IHistogramDataPoint, IMetric, INumberDataPoint, @@ -91,6 +92,11 @@ export function toMetric(metricData: MetricData): IMetric { aggregationTemporality, dataPoints: toHistogramDataPoints(metricData), }; + } else if (metricData.dataPointType === DataPointType.EXPONENTIAL_HISTOGRAM) { + out.exponentialHistogram = { + aggregationTemporality, + dataPoints: toExponentialHistogramDataPoints(metricData), + }; } return out; @@ -141,6 +147,33 @@ function toHistogramDataPoints(metricData: MetricData): IHistogramDataPoint[] { }); } +function toExponentialHistogramDataPoints( + metricData: MetricData +): IExponentialHistogramDataPoint[] { + return metricData.dataPoints.map(dataPoint => { + const histogram = dataPoint.value as ExponentialHistogram; + return { + attributes: toAttributes(dataPoint.attributes), + count: histogram.count, + min: histogram.min, + max: histogram.max, + sum: histogram.sum, + positive: { + offset: histogram.positive.offset, + bucketCounts: histogram.positive.bucketCounts, + }, + negative: { + offset: histogram.negative.offset, + bucketCounts: histogram.negative.bucketCounts, + }, + scale: histogram.scale, + zeroCount: histogram.zeroCount, + startTimeUnixNano: hrTimeToNanoseconds(dataPoint.startTime), + timeUnixNano: hrTimeToNanoseconds(dataPoint.endTime), + }; + }); +} + function toAggregationTemporality( temporality: AggregationTemporality ): EAggregationTemporality { diff --git a/experimental/packages/otlp-transformer/src/metrics/types.ts b/experimental/packages/otlp-transformer/src/metrics/types.ts index d3efd29c2e4..6db9c17a9be 100644 --- a/experimental/packages/otlp-transformer/src/metrics/types.ts +++ b/experimental/packages/otlp-transformer/src/metrics/types.ts @@ -184,7 +184,7 @@ export interface IExponentialHistogramDataPoint { startTimeUnixNano?: number; /** ExponentialHistogramDataPoint timeUnixNano */ - timeUnixNano?: string; + timeUnixNano?: number; /** ExponentialHistogramDataPoint count */ count?: number; @@ -209,6 +209,12 @@ export interface IExponentialHistogramDataPoint { /** ExponentialHistogramDataPoint exemplars */ exemplars?: IExemplar[]; + + /** ExponentialHistogramDataPoint min */ + min?: number; + + /** ExponentialHistogramDataPoint max */ + max?: number; } /** Properties of a SummaryDataPoint. */ diff --git a/experimental/packages/otlp-transformer/test/metrics.test.ts b/experimental/packages/otlp-transformer/test/metrics.test.ts index 325c3eee4ab..748ef758891 100644 --- a/experimental/packages/otlp-transformer/test/metrics.test.ts +++ b/experimental/packages/otlp-transformer/test/metrics.test.ts @@ -265,6 +265,47 @@ describe('Metrics', () => { }; } + function createExponentialHistogramMetrics( + count: number, + sum: number, + scale: number, + zeroCount: number, + positive: { offset: number; bucketCounts: number[] }, + negative: { offset: number; bucketCounts: number[] }, + aggregationTemporality: AggregationTemporality, + min?: number, + max?: number + ): MetricData { + return { + descriptor: { + description: 'this is a description', + type: InstrumentType.HISTOGRAM, + name: 'xhist', + unit: '1', + valueType: ValueType.INT, + }, + aggregationTemporality, + dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM, + dataPoints: [ + { + value: { + sum: sum, + count: count, + min: min, + max: max, + zeroCount: zeroCount, + scale: scale, + positive: positive, + negative: negative, + }, + startTime: START_TIME, + endTime: END_TIME, + attributes: ATTRIBUTES, + }, + ], + }; + } + function createResourceMetrics(metricData: MetricData[]): ResourceMetrics { const resource = new Resource({ 'resource-attribute': 'resource attribute value', @@ -608,5 +649,133 @@ describe('Metrics', () => { }); }); }); + + describe('serializes an exponential histogram metric record', () => { + it('with min/max', () => { + const exportRequest = createExportMetricsServiceRequest([ + createResourceMetrics([ + createExponentialHistogramMetrics( + 3, + 10, + 1, + 0, + { offset: 0, bucketCounts: [1, 0, 0, 0, 1, 0, 1, 0] }, + { offset: 0, bucketCounts: [0] }, + AggregationTemporality.CUMULATIVE, + 1, + 8 + ), + ]), + ]); + + assert.ok(exportRequest); + + assert.deepStrictEqual(exportRequest, { + resourceMetrics: [ + { + resource: expectedResource, + schemaUrl: undefined, + scopeMetrics: [ + { + scope: expectedScope, + schemaUrl: expectedSchemaUrl, + metrics: [ + { + name: 'xhist', + description: 'this is a description', + unit: '1', + exponentialHistogram: { + aggregationTemporality: + EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + dataPoints: [ + { + attributes: expectedAttributes, + count: 3, + sum: 10, + min: 1, + max: 8, + zeroCount: 0, + scale: 1, + positive: { + offset: 0, + bucketCounts: [1, 0, 0, 0, 1, 0, 1, 0], + }, + negative: { offset: 0, bucketCounts: [0] }, + startTimeUnixNano: hrTimeToNanoseconds(START_TIME), + timeUnixNano: hrTimeToNanoseconds(END_TIME), + }, + ], + }, + }, + ], + }, + ], + }, + ], + }); + }); + + it('without min/max', () => { + const exportRequest = createExportMetricsServiceRequest([ + createResourceMetrics([ + createExponentialHistogramMetrics( + 3, + 10, + 1, + 0, + { offset: 0, bucketCounts: [1, 0, 0, 0, 1, 0, 1, 0] }, + { offset: 0, bucketCounts: [0] }, + AggregationTemporality.CUMULATIVE + ), + ]), + ]); + + assert.ok(exportRequest); + + assert.deepStrictEqual(exportRequest, { + resourceMetrics: [ + { + resource: expectedResource, + schemaUrl: undefined, + scopeMetrics: [ + { + scope: expectedScope, + schemaUrl: expectedSchemaUrl, + metrics: [ + { + name: 'xhist', + description: 'this is a description', + unit: '1', + exponentialHistogram: { + aggregationTemporality: + EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + dataPoints: [ + { + attributes: expectedAttributes, + count: 3, + sum: 10, + min: undefined, + max: undefined, + zeroCount: 0, + scale: 1, + positive: { + offset: 0, + bucketCounts: [1, 0, 0, 0, 1, 0, 1, 0], + }, + negative: { offset: 0, bucketCounts: [0] }, + startTimeUnixNano: hrTimeToNanoseconds(START_TIME), + timeUnixNano: hrTimeToNanoseconds(END_TIME), + }, + ], + }, + }, + ], + }, + ], + }, + ], + }); + }); + }); }); }); diff --git a/packages/sdk-metrics/src/aggregator/index.ts b/packages/sdk-metrics/src/aggregator/index.ts index 8906c4563a1..b304997dd6f 100644 --- a/packages/sdk-metrics/src/aggregator/index.ts +++ b/packages/sdk-metrics/src/aggregator/index.ts @@ -16,6 +16,7 @@ export * from './Drop'; export * from './Histogram'; +export * from './ExponentialHistogram'; export * from './LastValue'; export * from './Sum'; export { Aggregator } from './types'; diff --git a/packages/sdk-metrics/src/index.ts b/packages/sdk-metrics/src/index.ts index 45559a54b8d..4760da3ccce 100644 --- a/packages/sdk-metrics/src/index.ts +++ b/packages/sdk-metrics/src/index.ts @@ -34,6 +34,7 @@ export { SumMetricData, GaugeMetricData, HistogramMetricData, + ExponentialHistogramMetricData, ResourceMetrics, ScopeMetrics, MetricData, @@ -60,6 +61,7 @@ export { MeterProvider, MeterProviderOptions } from './MeterProvider'; export { DefaultAggregation, ExplicitBucketHistogramAggregation, + ExponentialHistogramAggregation, DropAggregation, HistogramAggregation, LastValueAggregation, diff --git a/packages/sdk-metrics/src/view/Aggregation.ts b/packages/sdk-metrics/src/view/Aggregation.ts index 1b73b7c0a40..755cd175318 100644 --- a/packages/sdk-metrics/src/view/Aggregation.ts +++ b/packages/sdk-metrics/src/view/Aggregation.ts @@ -21,6 +21,7 @@ import { DropAggregator, LastValueAggregator, HistogramAggregator, + ExponentialHistogramAggregator, } from '../aggregator'; import { Accumulation } from '../aggregator/types'; import { InstrumentDescriptor, InstrumentType } from '../InstrumentDescriptor'; @@ -52,6 +53,10 @@ export abstract class Aggregation { return HISTOGRAM_AGGREGATION; } + static ExponentialHistogram(): Aggregation { + return EXPONENTIAL_HISTOGRAM_AGGREGATION; + } + static Default(): Aggregation { return DEFAULT_AGGREGATION; } @@ -144,6 +149,21 @@ export class ExplicitBucketHistogramAggregation extends Aggregation { } } +export class ExponentialHistogramAggregation extends Aggregation { + constructor( + private readonly _maxSize: number = 160, + private readonly _recordMinMax = true + ) { + super(); + } + createAggregator(_instrument: InstrumentDescriptor) { + return new ExponentialHistogramAggregator( + this._maxSize, + this._recordMinMax + ); + } +} + /** * The default aggregation. */ @@ -179,4 +199,5 @@ const DROP_AGGREGATION = new DropAggregation(); const SUM_AGGREGATION = new SumAggregation(); const LAST_VALUE_AGGREGATION = new LastValueAggregation(); const HISTOGRAM_AGGREGATION = new HistogramAggregation(); +const EXPONENTIAL_HISTOGRAM_AGGREGATION = new ExponentialHistogramAggregation(); const DEFAULT_AGGREGATION = new DefaultAggregation();