Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit fd92679

Browse files
authored
Add support for recording exemplars (#405)
* Add exemplar support * Fix review comments Fix typos (THe -> The). Use const and a ternary expression instead of let. Remove unwanted template backticks. Add separate function to create/get metric Bucket (getMetricBucket).
1 parent 55efec4 commit fd92679

File tree

7 files changed

+253
-26
lines changed

7 files changed

+253
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
2020
- Add an API `globalStats.unregisterExporter()`.
2121
- Add support for overriding sampling for a span.
2222
- Enforce `--strictNullChecks` and `--noUnusedLocals` Compiler Options on [opencensus-exporter-jaeger] packages.
23+
- Add support for recording Exemplars.
2324

2425
## 0.0.9 - 2019-02-12
2526
- Add Metrics API.

packages/opencensus-core/src/stats/recorder.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ const UNKNOWN_TAG_VALUE: TagValue = null;
2121

2222
export class Recorder {
2323
static addMeasurement(
24-
aggregationData: AggregationData,
25-
measurement: Measurement): AggregationData {
24+
aggregationData: AggregationData, measurement: Measurement,
25+
attachments?: {[key: string]: string}): AggregationData {
2626
aggregationData.timestamp = Date.now();
2727
const value = measurement.measure.type === MeasureType.DOUBLE ?
2828
measurement.value :
2929
Math.trunc(measurement.value);
3030

3131
switch (aggregationData.type) {
3232
case AggregationType.DISTRIBUTION:
33-
return this.addToDistribution(aggregationData, value);
33+
return this.addToDistribution(aggregationData, value, attachments);
3434

3535
case AggregationType.SUM:
3636
return this.addToSum(aggregationData, value);
@@ -53,7 +53,8 @@ export class Recorder {
5353
}
5454

5555
private static addToDistribution(
56-
distributionData: DistributionData, value: number): DistributionData {
56+
distributionData: DistributionData, value: number,
57+
attachments?: {[key: string]: string}): DistributionData {
5758
distributionData.count += 1;
5859

5960
let bucketIndex =
@@ -79,6 +80,15 @@ export class Recorder {
7980
distributionData.stdDeviation = Math.sqrt(
8081
distributionData.sumOfSquaredDeviation / distributionData.count);
8182

83+
// No implicit recording for exemplars - if there are no attachments
84+
// (contextual information), don't record exemplars.
85+
if (attachments) {
86+
distributionData.exemplars[bucketIndex] = {
87+
value,
88+
timestamp: distributionData.timestamp,
89+
attachments
90+
};
91+
}
8292
return distributionData;
8393
}
8494

packages/opencensus-core/src/stats/stats.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,15 @@ export class BaseStats implements Stats {
172172
* Updates all views with the new measurements.
173173
* @param measurements A list of measurements to record
174174
* @param tags optional The tags to which the value is applied.
175-
* tags could either be explicitly passed to the method, or implicitly
176-
* read from current execution context.
175+
* tags could either be explicitly passed to the method, or implicitly
176+
* read from current execution context.
177+
* @param attachments optional The contextual information associated with an
178+
* example value. THe contextual information is represented as key - value
179+
* string pairs.
177180
*/
178-
record(measurements: Measurement[], tags?: TagMap): void {
181+
record(
182+
measurements: Measurement[], tags?: TagMap,
183+
attachments?: {[key: string]: string}): void {
179184
if (this.hasNegativeValue(measurements)) {
180185
this.logger.warn(`Dropping measurments ${measurements}, value to record
181186
must be non-negative.`);
@@ -194,7 +199,7 @@ export class BaseStats implements Stats {
194199
}
195200
// Updates all views
196201
for (const view of views) {
197-
view.recordMeasurement(measurement, tags);
202+
view.recordMeasurement(measurement, tags, attachments);
198203
}
199204

200205
// Notifies all exporters

packages/opencensus-core/src/stats/types.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,15 @@ export interface Stats {
6565
* Updates all views with the new measurements.
6666
* @param measurements A list of measurements to record
6767
* @param tags optional The tags to which the value is applied.
68-
* tags could either be explicitly passed to the method, or implicitly
69-
* read from current execution context.
68+
* tags could either be explicitly passed to the method, or implicitly
69+
* read from current execution context.
70+
* @param attachments optional The contextual information associated with an
71+
* example value. The contextual information is represented as key - value
72+
* string pairs.
7073
*/
71-
record(measurements: Measurement[], tags?: TagMap): void;
74+
record(
75+
measurements: Measurement[], tags?: TagMap,
76+
attachments?: {[key: string]: string}): void;
7277

7378
/**
7479
* Remove all registered Views and exporters from the stats.
@@ -181,8 +186,13 @@ export interface View {
181186
* Measurements with measurement type INT64 will have its value truncated.
182187
* @param measurement The measurement to record
183188
* @param tags The tags to which the value is applied
189+
* @param attachments optional The contextual information associated with an
190+
* example value. THe contextual information is represented as key - value
191+
* string pairs.
184192
*/
185-
recordMeasurement(measurement: Measurement, tags: TagMap): void;
193+
recordMeasurement(
194+
measurement: Measurement, tags: TagMap,
195+
attachments?: {[key: string]: string}): void;
186196
/**
187197
* Returns a snapshot of an AggregationData for that tags/labels values.
188198
* @param tagValues The desired data's tag values.
@@ -269,6 +279,25 @@ export interface DistributionData extends AggregationMetadata {
269279
buckets: Bucket[];
270280
/** Buckets count */
271281
bucketCounts?: number[];
282+
/** If the distribution does not have a histogram, then omit this field. */
283+
exemplars?: StatsExemplar[];
284+
}
285+
286+
/**
287+
* Exemplars are example points that may be used to annotate aggregated
288+
* Distribution values. They are metadata that gives information about a
289+
* particular value added to a Distribution bucket.
290+
*/
291+
export interface StatsExemplar {
292+
/**
293+
* Value of the exemplar point. It determines which bucket the exemplar
294+
* belongs to.
295+
*/
296+
readonly value: number;
297+
/** The observation (sampling) time of the above value. */
298+
readonly timestamp: number;
299+
/** Contextual information about the example value. */
300+
readonly attachments: {[key: string]: string};
272301
}
273302

274303
export type Bucket = number;

packages/opencensus-core/src/stats/view.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
import * as defaultLogger from '../common/console-logger';
1818
import {getTimestampWithProcessHRTime, timestampFromMillis} from '../common/time-util';
1919
import * as loggerTypes from '../common/types';
20-
import {DistributionValue, LabelValue, Metric, MetricDescriptor, MetricDescriptorType, Point, TimeSeries, Timestamp} from '../metrics/export/types';
20+
import {Bucket as metricBucket, DistributionValue, LabelValue, Metric, MetricDescriptor, MetricDescriptorType, Point, TimeSeries, Timestamp} from '../metrics/export/types';
2121
import {TagMap} from '../tags/tag-map';
2222
import {TagKey, TagValue} from '../tags/types';
2323
import {isValidTagKey} from '../tags/validation';
24+
2425
import {BucketBoundaries} from './bucket-boundaries';
2526
import {MetricUtils} from './metric-utils';
2627
import {Recorder} from './recorder';
27-
import {AggregationData, AggregationType, Measure, Measurement, View} from './types';
28+
import {AggregationData, AggregationType, Measure, Measurement, StatsExemplar, View} from './types';
2829

2930
const RECORD_SEPARATOR = String.fromCharCode(30);
3031

@@ -114,8 +115,13 @@ export class BaseView implements View {
114115
* Measurements with measurement type INT64 will have its value truncated.
115116
* @param measurement The measurement to record
116117
* @param tags The tags to which the value is applied
118+
* @param attachments optional The contextual information associated with an
119+
* example value. The contextual information is represented as key - value
120+
* string pairs.
117121
*/
118-
recordMeasurement(measurement: Measurement, tags: TagMap) {
122+
recordMeasurement(
123+
measurement: Measurement, tags: TagMap,
124+
attachments?: {[key: string]: string}) {
119125
const tagValues = Recorder.getTagValues(tags.tags, this.columns);
120126
const encodedTags = this.encodeTagValues(tagValues);
121127
if (!this.tagValueAggregationMap[encodedTags]) {
@@ -124,7 +130,7 @@ export class BaseView implements View {
124130
}
125131

126132
Recorder.addMeasurement(
127-
this.tagValueAggregationMap[encodedTags], measurement);
133+
this.tagValueAggregationMap[encodedTags], measurement, attachments);
128134
}
129135

130136
/**
@@ -143,12 +149,14 @@ export class BaseView implements View {
143149
*/
144150
private createAggregationData(tagValues: TagValue[]): AggregationData {
145151
const aggregationMetadata = {tagValues, timestamp: Date.now()};
146-
const {buckets, bucketCounts} = this.bucketBoundaries;
147-
const bucketsCopy = Object.assign([], buckets);
148-
const bucketCountsCopy = Object.assign([], bucketCounts);
149152

150153
switch (this.aggregation) {
151154
case AggregationType.DISTRIBUTION:
155+
const {buckets, bucketCounts} = this.bucketBoundaries;
156+
const bucketsCopy = Object.assign([], buckets);
157+
const bucketCountsCopy = Object.assign([], bucketCounts);
158+
const exemplars = new Array(bucketCounts.length);
159+
152160
return {
153161
...aggregationMetadata,
154162
type: AggregationType.DISTRIBUTION,
@@ -159,7 +167,8 @@ export class BaseView implements View {
159167
stdDeviation: null as number,
160168
sumOfSquaredDeviation: null as number,
161169
buckets: bucketsCopy,
162-
bucketCounts: bucketCountsCopy
170+
bucketCounts: bucketCountsCopy,
171+
exemplars
163172
};
164173
case AggregationType.SUM:
165174
return {...aggregationMetadata, type: AggregationType.SUM, value: 0};
@@ -221,18 +230,21 @@ export class BaseView implements View {
221230
*/
222231
private toPoint(timestamp: Timestamp, data: AggregationData): Point {
223232
let value;
224-
225233
if (data.type === AggregationType.DISTRIBUTION) {
226-
// TODO: Add examplar transition
227-
const {count, sum, sumOfSquaredDeviation} = data;
234+
const {count, sum, sumOfSquaredDeviation, exemplars} = data;
235+
const buckets = [];
236+
for (let bucket = 0; bucket < data.bucketCounts.length; bucket++) {
237+
const bucketCount = data.bucketCounts[bucket];
238+
const statsExemplar = exemplars ? exemplars[bucket] : undefined;
239+
buckets.push(this.getMetricBucket(statsExemplar, bucketCount));
240+
}
241+
228242
value = {
229243
count,
230244
sum,
231245
sumOfSquaredDeviation,
246+
buckets,
232247
bucketOptions: {explicit: {bounds: data.buckets}},
233-
// Bucket without an Exemplar.
234-
buckets:
235-
data.bucketCounts.map(bucketCount => ({count: bucketCount}))
236248
} as DistributionValue;
237249
} else {
238250
value = data.value as number;
@@ -249,6 +261,24 @@ export class BaseView implements View {
249261
return this.tagValueAggregationMap[this.encodeTagValues(tagValues)];
250262
}
251263

264+
/** Returns a Bucket with count and examplar (if present) */
265+
private getMetricBucket(statsExemplar: StatsExemplar, bucketCount: number):
266+
metricBucket {
267+
if (statsExemplar) {
268+
// Bucket with an Exemplar.
269+
return {
270+
count: bucketCount,
271+
exemplar: {
272+
value: statsExemplar.value,
273+
timestamp: timestampFromMillis(statsExemplar.timestamp),
274+
attachments: statsExemplar.attachments
275+
}
276+
};
277+
}
278+
// Bucket with no Exemplar.
279+
return {count: bucketCount};
280+
}
281+
252282
/** Determines whether the given TagKeys are valid. */
253283
private validateTagKeys(tagKeys: TagKey[]): TagKey[] {
254284
const tagKeysCopy = Object.assign([], tagKeys);

packages/opencensus-core/test/test-recorder.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,41 @@ describe('Recorder', () => {
176176
}
177177
});
178178

179+
describe('for distribution aggregation data with attachments', () => {
180+
const attachments = {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'};
181+
it('should record measurements and attachments correctly', () => {
182+
const distributionData: DistributionData = {
183+
type: AggregationType.DISTRIBUTION,
184+
tagValues,
185+
timestamp: Date.now(),
186+
startTime: Date.now(),
187+
count: 0,
188+
sum: 0,
189+
mean: 0,
190+
stdDeviation: 0,
191+
sumOfSquaredDeviation: 0,
192+
buckets: [2, 4, 6],
193+
bucketCounts: [0, 0, 0, 0],
194+
exemplars: [undefined, undefined, undefined, undefined]
195+
};
196+
const value = 5;
197+
const measurement: Measurement = {measure, value};
198+
const aggregationData =
199+
Recorder.addMeasurement(
200+
distributionData, measurement, attachments) as DistributionData;
201+
202+
assert.equal(aggregationData.sum, 5);
203+
assert.equal(aggregationData.mean, 5);
204+
assert.deepStrictEqual(aggregationData.buckets, [2, 4, 6]);
205+
assert.deepStrictEqual(aggregationData.bucketCounts, [0, 0, 1, 0]);
206+
assert.deepStrictEqual(aggregationData.exemplars, [
207+
undefined, undefined,
208+
{value: 5, timestamp: aggregationData.timestamp, attachments},
209+
undefined
210+
]);
211+
});
212+
});
213+
179214
describe('getTagValues()', () => {
180215
const CALLER = {name: 'caller'};
181216
const METHOD = {name: 'method'};

0 commit comments

Comments
 (0)