Skip to content

Commit 672368f

Browse files
committed
feat(sdk-metrics-base): detect resets on async metrics
1 parent ba3e320 commit 672368f

31 files changed

Lines changed: 685 additions & 283 deletions

experimental/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ All notable changes to experimental packages in this project will be documented
2020
* fix(sdk-metrics-base): misbehaving aggregation temporality selector tolerance #2958 @legendecas
2121
* feat(trace-otlp-grpc): configure security with env vars #2827 @svetlanabrennan
2222
* feat(sdk-metrics-base): async instruments callback timeout #2742 @legendecas
23+
* feat(sdk-metrics-base): detect resets on async metrics #2990 @legendecas
24+
* Added monotonicity support in SumAggregator.
25+
* Added reset and gaps detection for async metric instruments.
26+
* Fixed the start time and end time of an exported metric with regarding to resets and gaps.
2327

2428
### :bug: (Bug Fix)
2529

experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,8 @@ describe('OTLPMetricExporter - node with json over http', () => {
307307
assert.ok(typeof metric3 !== 'undefined', "histogram doesn't exist");
308308
ensureHistogramIsCorrect(
309309
metric3,
310-
core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].endTime),
311-
core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].startTime),
310+
core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].endTime),
311+
core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].startTime),
312312
[0, 100],
313313
[0, 2, 0]
314314
);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import * as api from '@opentelemetry/api';
1818
import * as metrics from '@opentelemetry/api-metrics';
1919
import { ObservableCallback } from '@opentelemetry/api-metrics';
20+
import { hrTime } from '@opentelemetry/core';
2021
import { InstrumentDescriptor } from './InstrumentDescriptor';
2122
import { ObservableRegistry } from './state/ObservableRegistry';
2223
import { AsyncWritableMetricStorage, WritableMetricStorage } from './state/WritableMetricStorage';
@@ -31,7 +32,7 @@ export class SyncInstrument {
3132
);
3233
value = Math.trunc(value);
3334
}
34-
this._writableMetricStorage.record(value, attributes, context);
35+
this._writableMetricStorage.record(value, attributes, context, hrTime());
3536
}
3637
}
3738

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export class DropAggregator implements Aggregator<undefined> {
4545
_descriptor: InstrumentDescriptor,
4646
_aggregationTemporality: AggregationTemporality,
4747
_accumulationByAttributes: AccumulationRecord<undefined>[],
48-
_startTime: HrTime,
4948
_endTime: HrTime): Maybe<MetricData> {
5049
return undefined;
5150
}

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function createNewEmptyCheckpoint(boundaries: number[]): Histogram {
4242

4343
export class HistogramAccumulation implements Accumulation {
4444
constructor(
45+
public startTime: HrTime,
4546
private readonly _boundaries: number[],
4647
private _current: Histogram = createNewEmptyCheckpoint(_boundaries)
4748
) {}
@@ -60,6 +61,10 @@ export class HistogramAccumulation implements Accumulation {
6061
this._current.buckets.counts[this._boundaries.length] += 1;
6162
}
6263

64+
setStartTime(startTime: HrTime): void {
65+
this.startTime = startTime;
66+
}
67+
6368
toPointValue(): Histogram {
6469
return this._current;
6570
}
@@ -77,8 +82,8 @@ export class HistogramAggregator implements Aggregator<HistogramAccumulation> {
7782
*/
7883
constructor(private readonly _boundaries: number[]) {}
7984

80-
createAccumulation() {
81-
return new HistogramAccumulation(this._boundaries);
85+
createAccumulation(startTime: HrTime) {
86+
return new HistogramAccumulation(startTime, this._boundaries);
8287
}
8388

8489
/**
@@ -98,7 +103,7 @@ export class HistogramAggregator implements Aggregator<HistogramAccumulation> {
98103
mergedCounts[idx] = previousCounts[idx] + deltaCounts[idx];
99104
}
100105

101-
return new HistogramAccumulation(previousValue.buckets.boundaries, {
106+
return new HistogramAccumulation(previous.startTime, previousValue.buckets.boundaries, {
102107
buckets: {
103108
boundaries: previousValue.buckets.boundaries,
104109
counts: mergedCounts,
@@ -123,7 +128,7 @@ export class HistogramAggregator implements Aggregator<HistogramAccumulation> {
123128
diffedCounts[idx] = currentCounts[idx] - previousCounts[idx];
124129
}
125130

126-
return new HistogramAccumulation(previousValue.buckets.boundaries, {
131+
return new HistogramAccumulation(current.startTime, previousValue.buckets.boundaries, {
127132
buckets: {
128133
boundaries: previousValue.buckets.boundaries,
129134
counts: diffedCounts,
@@ -137,7 +142,6 @@ export class HistogramAggregator implements Aggregator<HistogramAccumulation> {
137142
descriptor: InstrumentDescriptor,
138143
aggregationTemporality: AggregationTemporality,
139144
accumulationByAttributes: AccumulationRecord<HistogramAccumulation>[],
140-
startTime: HrTime,
141145
endTime: HrTime): Maybe<HistogramMetricData> {
142146
return {
143147
descriptor,
@@ -146,7 +150,7 @@ export class HistogramAggregator implements Aggregator<HistogramAccumulation> {
146150
dataPoints: accumulationByAttributes.map(([attributes, accumulation]) => {
147151
return {
148152
attributes,
149-
startTime,
153+
startTime: accumulation.startTime,
150154
endTime,
151155
value: accumulation.toPointValue(),
152156
};

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ import { Maybe } from '../utils';
2323
import { AggregationTemporality } from '../export/AggregationTemporality';
2424

2525
export class LastValueAccumulation implements Accumulation {
26-
constructor(private _current: number = 0, public sampleTime: HrTime = [0, 0]) {}
26+
constructor(public startTime: HrTime, private _current: number = 0, public sampleTime: HrTime = [0, 0]) {}
2727

2828
record(value: number): void {
2929
this._current = value;
3030
this.sampleTime = hrTime();
3131
}
3232

33+
setStartTime(startTime: HrTime): void {
34+
this.startTime = startTime;
35+
}
36+
3337
toPointValue(): LastValue {
3438
return this._current;
3539
}
@@ -39,8 +43,8 @@ export class LastValueAccumulation implements Accumulation {
3943
export class LastValueAggregator implements Aggregator<LastValueAccumulation> {
4044
public kind: AggregatorKind.LAST_VALUE = AggregatorKind.LAST_VALUE;
4145

42-
createAccumulation() {
43-
return new LastValueAccumulation();
46+
createAccumulation(startTime: HrTime) {
47+
return new LastValueAccumulation(startTime);
4448
}
4549

4650
/**
@@ -51,7 +55,7 @@ export class LastValueAggregator implements Aggregator<LastValueAccumulation> {
5155
merge(previous: LastValueAccumulation, delta: LastValueAccumulation): LastValueAccumulation {
5256
// nanoseconds may lose precisions.
5357
const latestAccumulation = hrTimeToMicroseconds(delta.sampleTime) >= hrTimeToMicroseconds(previous.sampleTime) ? delta : previous;
54-
return new LastValueAccumulation(latestAccumulation.toPointValue(), latestAccumulation.sampleTime);
58+
return new LastValueAccumulation(previous.startTime, latestAccumulation.toPointValue(), latestAccumulation.sampleTime);
5559
}
5660

5761
/**
@@ -63,14 +67,13 @@ export class LastValueAggregator implements Aggregator<LastValueAccumulation> {
6367
diff(previous: LastValueAccumulation, current: LastValueAccumulation): LastValueAccumulation {
6468
// nanoseconds may lose precisions.
6569
const latestAccumulation = hrTimeToMicroseconds(current.sampleTime) >= hrTimeToMicroseconds(previous.sampleTime) ? current : previous;
66-
return new LastValueAccumulation(latestAccumulation.toPointValue(), latestAccumulation.sampleTime);
70+
return new LastValueAccumulation(current.startTime, latestAccumulation.toPointValue(), latestAccumulation.sampleTime);
6771
}
6872

6973
toMetricData(
7074
descriptor: InstrumentDescriptor,
7175
aggregationTemporality: AggregationTemporality,
7276
accumulationByAttributes: AccumulationRecord<LastValueAccumulation>[],
73-
startTime: HrTime,
7477
endTime: HrTime): Maybe<SingularMetricData> {
7578
return {
7679
descriptor,
@@ -79,7 +82,7 @@ export class LastValueAggregator implements Aggregator<LastValueAccumulation> {
7982
dataPoints: accumulationByAttributes.map(([attributes, accumulation]) => {
8083
return {
8184
attributes,
82-
startTime,
85+
startTime: accumulation.startTime,
8386
endTime,
8487
value: accumulation.toPointValue(),
8588
};

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ import { Maybe } from '../utils';
2222
import { AggregationTemporality } from '../export/AggregationTemporality';
2323

2424
export class SumAccumulation implements Accumulation {
25-
constructor(private _current: number = 0) {}
25+
constructor(public startTime: HrTime, public monotonic: boolean, private _current: number = 0, public reset = false) {}
2626

2727
record(value: number): void {
28+
if (this.monotonic && value < 0) {
29+
return;
30+
}
2831
this._current += value;
2932
}
3033

34+
setStartTime(startTime: HrTime): void {
35+
this.startTime = startTime;
36+
}
37+
3138
toPointValue(): Sum {
3239
return this._current;
3340
}
@@ -37,29 +44,43 @@ export class SumAccumulation implements Accumulation {
3744
export class SumAggregator implements Aggregator<SumAccumulation> {
3845
public kind: AggregatorKind.SUM = AggregatorKind.SUM;
3946

40-
createAccumulation() {
41-
return new SumAccumulation();
47+
constructor (public monotonic: boolean) {}
48+
49+
createAccumulation(startTime: HrTime) {
50+
return new SumAccumulation(startTime, this.monotonic);
4251
}
4352

4453
/**
4554
* Returns the result of the merge of the given accumulations.
4655
*/
4756
merge(previous: SumAccumulation, delta: SumAccumulation): SumAccumulation {
48-
return new SumAccumulation(previous.toPointValue() + delta.toPointValue());
57+
if (delta.reset) {
58+
return new SumAccumulation(delta.startTime, this.monotonic, delta.toPointValue(), delta.reset);
59+
}
60+
return new SumAccumulation(previous.startTime, this.monotonic, previous.toPointValue() + delta.toPointValue());
4961
}
5062

5163
/**
5264
* Returns a new DELTA aggregation by comparing two cumulative measurements.
5365
*/
5466
diff(previous: SumAccumulation, current: SumAccumulation): SumAccumulation {
55-
return new SumAccumulation(current.toPointValue() - previous.toPointValue());
67+
const prevPv = previous.toPointValue();
68+
const currPv = current.toPointValue();
69+
/**
70+
* If the SumAggregator is a monotonic one and the previous point value is
71+
* greater than the current one, a reset is deemed to be happened.
72+
* Return the current point value to prevent the value from been reset.
73+
*/
74+
if (this.monotonic && (prevPv > currPv)) {
75+
return new SumAccumulation(current.startTime, this.monotonic, currPv, true);
76+
}
77+
return new SumAccumulation(current.startTime, this.monotonic, current.toPointValue() - previous.toPointValue());
5678
}
5779

5880
toMetricData(
5981
descriptor: InstrumentDescriptor,
6082
aggregationTemporality: AggregationTemporality,
6183
accumulationByAttributes: AccumulationRecord<SumAccumulation>[],
62-
startTime: HrTime,
6384
endTime: HrTime): Maybe<SingularMetricData> {
6485
return {
6586
descriptor,
@@ -68,7 +89,7 @@ export class SumAggregator implements Aggregator<SumAccumulation> {
6889
dataPoints: accumulationByAttributes.map(([attributes, accumulation]) => {
6990
return {
7091
attributes,
71-
startTime,
92+
startTime: accumulation.startTime,
7293
endTime,
7394
value: accumulation.toPointValue(),
7495
};

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface Histogram {
6565
* An Aggregator accumulation state.
6666
*/
6767
export interface Accumulation {
68+
setStartTime(startTime: HrTime): void;
6869
record(value: number): void;
6970
}
7071

@@ -81,7 +82,7 @@ export interface Aggregator<T> {
8182
/**
8283
* Create a clean state of accumulation.
8384
*/
84-
createAccumulation(): T;
85+
createAccumulation(startTime: HrTime): T;
8586

8687
/**
8788
* Returns the result of the merge of the given accumulations.
@@ -109,13 +110,11 @@ export interface Aggregator<T> {
109110
*
110111
* @param descriptor the metric instrument descriptor.
111112
* @param accumulationByAttributes the array of attributes and accumulation pairs.
112-
* @param startTime the start time of the metric data.
113113
* @param endTime the end time of the metric data.
114114
* @return the {@link MetricData} that this {@link Aggregator} will produce.
115115
*/
116116
toMetricData(descriptor: InstrumentDescriptor,
117117
aggregationTemporality: AggregationTemporality,
118118
accumulationByAttributes: AccumulationRecord<T>[],
119-
startTime: HrTime,
120119
endTime: HrTime): Maybe<MetricData>;
121120
}

experimental/packages/opentelemetry-sdk-metrics-base/src/state/AsyncMetricStorage.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ export class AsyncMetricStorage<T extends Maybe<Accumulation>> extends MetricSto
4646
this._temporalMetricStorage = new TemporalMetricProcessor(aggregator);
4747
}
4848

49-
record(measurements: AttributeHashMap<number>) {
49+
record(measurements: AttributeHashMap<number>, observationTime: HrTime) {
5050
const processed = new AttributeHashMap<number>();
5151
Array.from(measurements.entries()).forEach(([attributes, value]) => {
5252
processed.set(this._attributesProcessor.process(attributes), value);
5353
});
54-
this._deltaMetricStorage.batchCumulate(processed);
54+
this._deltaMetricStorage.batchCumulate(processed, observationTime);
5555
}
5656

5757
/**
@@ -64,7 +64,6 @@ export class AsyncMetricStorage<T extends Maybe<Accumulation>> extends MetricSto
6464
collect(
6565
collector: MetricCollectorHandle,
6666
collectors: MetricCollectorHandle[],
67-
sdkStartTime: HrTime,
6867
collectionTime: HrTime,
6968
): Maybe<MetricData> {
7069
const accumulations = this._deltaMetricStorage.collect();
@@ -74,7 +73,6 @@ export class AsyncMetricStorage<T extends Maybe<Accumulation>> extends MetricSto
7473
collectors,
7574
this._instrumentDescriptor,
7675
accumulations,
77-
sdkStartTime,
7876
collectionTime
7977
);
8078
}

experimental/packages/opentelemetry-sdk-metrics-base/src/state/DeltaMetricProcessor.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Context } from '@opentelemetry/api';
17+
import { Context, HrTime } from '@opentelemetry/api';
1818
import { MetricAttributes } from '@opentelemetry/api-metrics';
1919
import { Maybe } from '../utils';
2020
import { Accumulation, Aggregator } from '../aggregator/types';
@@ -35,31 +35,36 @@ export class DeltaMetricProcessor<T extends Maybe<Accumulation>> {
3535

3636
constructor(private _aggregator: Aggregator<T>) {}
3737

38-
/** Bind an efficient storage handle for a set of attributes. */
39-
private bind(attributes: MetricAttributes) {
40-
return this._activeCollectionStorage.getOrDefault(attributes, () => this._aggregator.createAccumulation());
41-
}
42-
43-
record(value: number, attributes: MetricAttributes, _context: Context) {
44-
const accumulation = this.bind(attributes);
38+
record(value: number, attributes: MetricAttributes, _context: Context, collectionTime: HrTime) {
39+
const accumulation = this._activeCollectionStorage.getOrDefault(
40+
attributes,
41+
() => this._aggregator.createAccumulation(collectionTime)
42+
);
4543
accumulation?.record(value);
4644
}
4745

48-
batchCumulate(measurements: AttributeHashMap<number>) {
46+
batchCumulate(measurements: AttributeHashMap<number>, collectionTime: HrTime) {
4947
Array.from(measurements.entries()).forEach(([attributes, value, hashCode]) => {
50-
let accumulation = this._aggregator.createAccumulation();
48+
const accumulation = this._aggregator.createAccumulation(collectionTime);
5149
accumulation?.record(value);
50+
let delta = accumulation;
5251
if (this._cumulativeMemoStorage.has(attributes, hashCode)) {
52+
// previous must present.
5353
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5454
const previous = this._cumulativeMemoStorage.get(attributes, hashCode)!;
55-
accumulation = this._aggregator.diff(previous, accumulation);
55+
delta = this._aggregator.diff(previous, accumulation);
5656
}
5757

58+
// Save the current record and the delta record.
5859
this._cumulativeMemoStorage.set(attributes, accumulation, hashCode);
59-
this._activeCollectionStorage.set(attributes, accumulation, hashCode);
60+
this._activeCollectionStorage.set(attributes, delta, hashCode);
6061
});
6162
}
6263

64+
/**
65+
* Returns a collection of delta metrics. Their start time is the when first
66+
* time event collected.
67+
*/
6368
collect() {
6469
const unreportedDelta = this._activeCollectionStorage;
6570
this._activeCollectionStorage = new AttributeHashMap();

0 commit comments

Comments
 (0)