Skip to content

Commit 1d0eaef

Browse files
pichlermarcdyladan
andauthored
feat(histogram): align collection of optional Histogram properties with spec (#3102)
Co-authored-by: Daniel Dyla <[email protected]>
1 parent d5cf120 commit 1d0eaef

File tree

10 files changed

+221
-47
lines changed

10 files changed

+221
-47
lines changed

experimental/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ All notable changes to experimental packages in this project will be documented
1010
* feat(sdk-metrics-base): split up Singular into Sum and Gauge in MetricData [#3079](https://github.com/open-telemetry/opentelemetry-js/pull/3079) @pichlermarc
1111
* removes `DataPointType.SINGULAR`, and replaces it with `DataPointType.SUM` and `DataPointType.GAUGE`
1212
* removes `SingularMetricData` and replaces it with `SumMetricData` (including an additional `isMonotonic` flag) and `GaugeMetricData`
13+
* feat(histogram): align collection of optional Histogram properties with spec [#3102](https://github.com/open-telemetry/opentelemetry-js/pull/3079) @pichlermarc
14+
* changes type of `sum` property on `Histogram` to `number | undefined`
15+
* changes type of `min` and `max` properties on `Histogram` to `number | undefined`
16+
* removes `hasMinMax` flag on the exported `Histogram` - this is now indicated by `min` and `max` being `undefined`
1317

1418
### :rocket: (Enhancement)
1519

experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import {
2424
DataPoint,
2525
Histogram,
2626
} from '@opentelemetry/sdk-metrics-base';
27-
import type { MetricAttributes, MetricAttributeValue } from '@opentelemetry/api-metrics';
27+
import type {
28+
MetricAttributes,
29+
MetricAttributeValue
30+
} from '@opentelemetry/api-metrics';
2831
import { hrTimeToMilliseconds } from '@opentelemetry/core';
2932

3033
type PrometheusDataTypeLiteral =
@@ -52,6 +55,7 @@ function escapeAttributeValue(str: MetricAttributeValue = '') {
5255
}
5356

5457
const invalidCharacterRegex = /[^a-z0-9_]/gi;
58+
5559
/**
5660
* Ensures metric names are valid Prometheus metric names by removing
5761
* characters allowed by OpenTelemetry but disallowed by Prometheus.
@@ -254,25 +258,28 @@ export class PrometheusSerializer {
254258
let results = '';
255259

256260
name = enforcePrometheusNamingConvention(name, type);
257-
const { value, attributes } = dataPoint;
261+
const attributes = dataPoint.attributes;
262+
const histogram = dataPoint.value;
258263
const timestamp = hrTimeToMilliseconds(dataPoint.endTime);
259264
/** Histogram["bucket"] is not typed with `number` */
260265
for (const key of ['count', 'sum'] as ('count' | 'sum')[]) {
261-
results += stringify(
262-
name + '_' + key,
263-
attributes,
264-
value[key],
265-
this._appendTimestamp ? timestamp : undefined,
266-
undefined
267-
);
266+
const value = histogram[key];
267+
if (value != null)
268+
results += stringify(
269+
name + '_' + key,
270+
attributes,
271+
value,
272+
this._appendTimestamp ? timestamp : undefined,
273+
undefined
274+
);
268275
}
269276

270277
let cumulativeSum = 0;
271-
const countEntries = value.buckets.counts.entries();
278+
const countEntries = histogram.buckets.counts.entries();
272279
let infiniteBoundaryDefined = false;
273280
for (const [idx, val] of countEntries) {
274281
cumulativeSum += val;
275-
const upperBound = value.buckets.boundaries[idx];
282+
const upperBound = histogram.buckets.boundaries[idx];
276283
/** HistogramAggregator is producing different boundary output -
277284
* in one case not including infinity values, in other -
278285
* full, e.g. [0, 100] and [0, 100, Infinity]

experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ describe('PrometheusSerializer', () => {
355355
return result;
356356
}
357357

358-
it('serialize metric record with ExplicitHistogramAggregation aggregator, cumulative', async () => {
358+
it('serialize cumulative metric record', async () => {
359359
const serializer = new PrometheusSerializer();
360360
const result = await testSerializer(serializer);
361361
assert.strictEqual(
@@ -376,6 +376,53 @@ describe('PrometheusSerializer', () => {
376376
`test_bucket{val="2",le="+Inf"} 1 ${mockedHrTimeMs}\n`
377377
);
378378
});
379+
380+
it('serialize cumulative metric record on instrument that allows negative values', async () => {
381+
const serializer = new PrometheusSerializer();
382+
const reader = new TestMetricReader();
383+
const meterProvider = new MeterProvider({
384+
views: [
385+
new View({
386+
aggregation: new ExplicitBucketHistogramAggregation([1, 10, 100]),
387+
instrumentName: '*'
388+
})
389+
]
390+
});
391+
meterProvider.addMetricReader(reader);
392+
const meter = meterProvider.getMeter('test');
393+
394+
const upDownCounter = meter.createUpDownCounter('test', {
395+
description: 'foobar',
396+
});
397+
upDownCounter.add(5, { val: '1' });
398+
upDownCounter.add(50, { val: '1' });
399+
upDownCounter.add(120, { val: '1' });
400+
401+
upDownCounter.add(5, { val: '2' });
402+
403+
const { resourceMetrics, errors } = await reader.collect();
404+
assert.strictEqual(errors.length, 0);
405+
assert.strictEqual(resourceMetrics.scopeMetrics.length, 1);
406+
assert.strictEqual(resourceMetrics.scopeMetrics[0].metrics.length, 1);
407+
const scopeMetrics = resourceMetrics.scopeMetrics[0];
408+
409+
const result = serializer['_serializeScopeMetrics'](scopeMetrics);
410+
assert.strictEqual(
411+
result,
412+
'# HELP test foobar\n' +
413+
'# TYPE test histogram\n' +
414+
`test_count{val="1"} 3 ${mockedHrTimeMs}\n` +
415+
`test_bucket{val="1",le="1"} 0 ${mockedHrTimeMs}\n` +
416+
`test_bucket{val="1",le="10"} 1 ${mockedHrTimeMs}\n` +
417+
`test_bucket{val="1",le="100"} 2 ${mockedHrTimeMs}\n` +
418+
`test_bucket{val="1",le="+Inf"} 3 ${mockedHrTimeMs}\n` +
419+
`test_count{val="2"} 1 ${mockedHrTimeMs}\n` +
420+
`test_bucket{val="2",le="1"} 0 ${mockedHrTimeMs}\n` +
421+
`test_bucket{val="2",le="10"} 1 ${mockedHrTimeMs}\n` +
422+
`test_bucket{val="2",le="100"} 1 ${mockedHrTimeMs}\n` +
423+
`test_bucket{val="2",le="+Inf"} 1 ${mockedHrTimeMs}\n`
424+
);
425+
});
379426
});
380427
});
381428

experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Histogram.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,38 @@ import {
1818
Accumulation,
1919
AccumulationRecord,
2020
Aggregator,
21-
AggregatorKind,
22-
Histogram,
21+
AggregatorKind
2322
} from './types';
24-
import { HistogramMetricData, DataPointType } from '../export/MetricData';
23+
import {
24+
DataPointType,
25+
HistogramMetricData
26+
} from '../export/MetricData';
2527
import { HrTime } from '@opentelemetry/api';
26-
import { InstrumentDescriptor } from '../InstrumentDescriptor';
28+
import {
29+
InstrumentDescriptor,
30+
InstrumentType
31+
} from '../InstrumentDescriptor';
2732
import { Maybe } from '../utils';
2833
import { AggregationTemporality } from '../export/AggregationTemporality';
2934

30-
function createNewEmptyCheckpoint(boundaries: number[]): Histogram {
35+
/**
36+
* Internal value type for HistogramAggregation.
37+
* Differs from the exported type as undefined sum/min/max complicate arithmetic
38+
* performed by this aggregation, but are required to be undefined in the exported types.
39+
*/
40+
interface InternalHistogram {
41+
buckets: {
42+
boundaries: number[];
43+
counts: number[];
44+
};
45+
sum: number;
46+
count: number;
47+
hasMinMax: boolean;
48+
min: number;
49+
max: number;
50+
}
51+
52+
function createNewEmptyCheckpoint(boundaries: number[]): InternalHistogram {
3153
const counts = boundaries.map(() => 0);
3254
counts.push(0);
3355
return {
@@ -48,7 +70,7 @@ export class HistogramAccumulation implements Accumulation {
4870
public startTime: HrTime,
4971
private readonly _boundaries: number[],
5072
private _recordMinMax = true,
51-
private _current: Histogram = createNewEmptyCheckpoint(_boundaries)
73+
private _current: InternalHistogram = createNewEmptyCheckpoint(_boundaries)
5274
) {}
5375

5476
record(value: number): void {
@@ -75,7 +97,7 @@ export class HistogramAccumulation implements Accumulation {
7597
this.startTime = startTime;
7698
}
7799

78-
toPointValue(): Histogram {
100+
toPointValue(): InternalHistogram {
79101
return this._current;
80102
}
81103
}
@@ -181,11 +203,25 @@ export class HistogramAggregator implements Aggregator<HistogramAccumulation> {
181203
aggregationTemporality,
182204
dataPointType: DataPointType.HISTOGRAM,
183205
dataPoints: accumulationByAttributes.map(([attributes, accumulation]) => {
206+
const pointValue = accumulation.toPointValue();
207+
208+
// determine if instrument allows negative values.
209+
const allowsNegativeValues =
210+
(descriptor.type === InstrumentType.UP_DOWN_COUNTER) ||
211+
(descriptor.type === InstrumentType.OBSERVABLE_GAUGE) ||
212+
(descriptor.type === InstrumentType.OBSERVABLE_UP_DOWN_COUNTER);
213+
184214
return {
185215
attributes,
186216
startTime: accumulation.startTime,
187217
endTime,
188-
value: accumulation.toPointValue(),
218+
value: {
219+
min: pointValue.hasMinMax ? pointValue.min : undefined,
220+
max: pointValue.hasMinMax ? pointValue.max : undefined,
221+
sum: !allowsNegativeValues ? pointValue.sum : undefined,
222+
buckets: pointValue.buckets,
223+
count: pointValue.count
224+
},
189225
};
190226
})
191227
};

experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,10 @@ export interface Histogram {
5757
boundaries: number[];
5858
counts: number[];
5959
};
60-
sum: number;
60+
sum?: number;
6161
count: number;
62-
hasMinMax: boolean;
63-
min: number;
64-
max: number;
62+
min?: number;
63+
max?: number;
6564
}
6665

6766
/**

experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { MetricAttributes } from '@opentelemetry/api-metrics';
1919
import { InstrumentationScope } from '@opentelemetry/core';
2020
import { Resource } from '@opentelemetry/resources';
2121
import { InstrumentDescriptor } from '../InstrumentDescriptor';
22-
import { Histogram } from '../aggregator/types';
2322
import { AggregationTemporality } from './AggregationTemporality';
23+
import { Histogram } from '../aggregator/types';
2424

2525
/**
2626
* Basic metric data fields.

experimental/packages/opentelemetry-sdk-metrics-base/test/Instruments.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
MeterProvider,
2626
MetricReader,
2727
DataPoint,
28-
DataPointType
28+
DataPointType,
29+
Histogram
2930
} from '../src';
3031
import { TestMetricReader } from './export/TestMetricReader';
3132
import {
@@ -36,7 +37,6 @@ import {
3637
defaultResource,
3738
defaultInstrumentationScope
3839
} from './util';
39-
import { Histogram } from '../src/aggregator/types';
4040
import { ObservableResult, ValueType } from '@opentelemetry/api-metrics';
4141

4242
describe('Instruments', () => {
@@ -301,7 +301,6 @@ describe('Instruments', () => {
301301
},
302302
count: 2,
303303
sum: 10,
304-
hasMinMax: true,
305304
max: 10,
306305
min: 0
307306
},
@@ -315,7 +314,6 @@ describe('Instruments', () => {
315314
},
316315
count: 2,
317316
sum: 100,
318-
hasMinMax: true,
319317
max: 100,
320318
min: 0
321319
},
@@ -358,7 +356,6 @@ describe('Instruments', () => {
358356
},
359357
count: 2,
360358
sum: 110,
361-
hasMinMax: true,
362359
min: 20,
363360
max: 90
364361
},
@@ -386,7 +383,6 @@ describe('Instruments', () => {
386383
},
387384
count: 4,
388385
sum: 220,
389-
hasMinMax: true,
390386
min: 10,
391387
max: 100
392388
},
@@ -430,7 +426,6 @@ describe('Instruments', () => {
430426
},
431427
count: 2,
432428
sum: 10.1,
433-
hasMinMax: true,
434429
max: 10,
435430
min: 0.1
436431
},
@@ -444,7 +439,6 @@ describe('Instruments', () => {
444439
},
445440
count: 2,
446441
sum: 100.1,
447-
hasMinMax: true,
448442
max: 100,
449443
min: 0.1
450444
},

0 commit comments

Comments
 (0)