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

Add support for recording exemplars #405

Merged
merged 2 commits into from
Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
- Add an API `globalStats.unregisterExporter()`.
- Add support for overriding sampling for a span.
- Enforce `--strictNullChecks` and `--noUnusedLocals` Compiler Options on [opencensus-exporter-jaeger] packages.
- Add support for recording Exemplars.

## 0.0.9 - 2019-02-12
- Add Metrics API.
Expand Down
18 changes: 14 additions & 4 deletions packages/opencensus-core/src/stats/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ const UNKNOWN_TAG_VALUE: TagValue = null;

export class Recorder {
static addMeasurement(
aggregationData: AggregationData,
measurement: Measurement): AggregationData {
aggregationData: AggregationData, measurement: Measurement,
attachments?: {[key: string]: string}): AggregationData {
aggregationData.timestamp = Date.now();
const value = measurement.measure.type === MeasureType.DOUBLE ?
measurement.value :
Math.trunc(measurement.value);

switch (aggregationData.type) {
case AggregationType.DISTRIBUTION:
return this.addToDistribution(aggregationData, value);
return this.addToDistribution(aggregationData, value, attachments);

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

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

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

// No implicit recording for exemplars - if there are no attachments
// (contextual information), don't record exemplars.
if (attachments) {
distributionData.exemplars[bucketIndex] = {
value,
timestamp: distributionData.timestamp,
attachments
};
}
return distributionData;
}

Expand Down
13 changes: 9 additions & 4 deletions packages/opencensus-core/src/stats/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,15 @@ export class BaseStats implements Stats {
* Updates all views with the new measurements.
* @param measurements A list of measurements to record
* @param tags optional The tags to which the value is applied.
* tags could either be explicitly passed to the method, or implicitly
* read from current execution context.
* tags could either be explicitly passed to the method, or implicitly
* read from current execution context.
* @param attachments optional The contextual information associated with an
* example value. THe contextual information is represented as key - value
* string pairs.
*/
record(measurements: Measurement[], tags?: TagMap): void {
record(
measurements: Measurement[], tags?: TagMap,
attachments?: {[key: string]: string}): void {
if (this.hasNegativeValue(measurements)) {
this.logger.warn(`Dropping measurments ${measurements}, value to record
must be non-negative.`);
Expand All @@ -194,7 +199,7 @@ export class BaseStats implements Stats {
}
// Updates all views
for (const view of views) {
view.recordMeasurement(measurement, tags);
view.recordMeasurement(measurement, tags, attachments);
}

// Notifies all exporters
Expand Down
37 changes: 33 additions & 4 deletions packages/opencensus-core/src/stats/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,15 @@ export interface Stats {
* Updates all views with the new measurements.
* @param measurements A list of measurements to record
* @param tags optional The tags to which the value is applied.
* tags could either be explicitly passed to the method, or implicitly
* read from current execution context.
* tags could either be explicitly passed to the method, or implicitly
* read from current execution context.
* @param attachments optional The contextual information associated with an
* example value. The contextual information is represented as key - value
* string pairs.
*/
record(measurements: Measurement[], tags?: TagMap): void;
record(
measurements: Measurement[], tags?: TagMap,
attachments?: {[key: string]: string}): void;

/**
* Remove all registered Views and exporters from the stats.
Expand Down Expand Up @@ -181,8 +186,13 @@ export interface View {
* Measurements with measurement type INT64 will have its value truncated.
* @param measurement The measurement to record
* @param tags The tags to which the value is applied
* @param attachments optional The contextual information associated with an
* example value. THe contextual information is represented as key - value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit typo The not THe

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks.

* string pairs.
*/
recordMeasurement(measurement: Measurement, tags: TagMap): void;
recordMeasurement(
measurement: Measurement, tags: TagMap,
attachments?: {[key: string]: string}): void;
/**
* Returns a snapshot of an AggregationData for that tags/labels values.
* @param tagValues The desired data's tag values.
Expand Down Expand Up @@ -269,6 +279,25 @@ export interface DistributionData extends AggregationMetadata {
buckets: Bucket[];
/** Buckets count */
bucketCounts?: number[];
/** If the distribution does not have a histogram, then omit this field. */
exemplars?: StatsExemplar[];
}

/**
* Exemplars are example points that may be used to annotate aggregated
* Distribution values. They are metadata that gives information about a
* particular value added to a Distribution bucket.
*/
export interface StatsExemplar {
/**
* Value of the exemplar point. It determines which bucket the exemplar
* belongs to.
*/
readonly value: number;
/** The observation (sampling) time of the above value. */
readonly timestamp: number;
/** Contextual information about the example value. */
readonly attachments: {[key: string]: string};
}

export type Bucket = number;
Expand Down
58 changes: 44 additions & 14 deletions packages/opencensus-core/src/stats/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
import * as defaultLogger from '../common/console-logger';
import {getTimestampWithProcessHRTime, timestampFromMillis} from '../common/time-util';
import * as loggerTypes from '../common/types';
import {DistributionValue, LabelValue, Metric, MetricDescriptor, MetricDescriptorType, Point, TimeSeries, Timestamp} from '../metrics/export/types';
import {Bucket as metricBucket, DistributionValue, LabelValue, Metric, MetricDescriptor, MetricDescriptorType, Point, TimeSeries, Timestamp} from '../metrics/export/types';
import {TagMap} from '../tags/tag-map';
import {TagKey, TagValue} from '../tags/types';
import {isValidTagKey} from '../tags/validation';

import {BucketBoundaries} from './bucket-boundaries';
import {MetricUtils} from './metric-utils';
import {Recorder} from './recorder';
import {AggregationData, AggregationType, Measure, Measurement, View} from './types';
import {AggregationData, AggregationType, Measure, Measurement, StatsExemplar, View} from './types';

const RECORD_SEPARATOR = String.fromCharCode(30);

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

Recorder.addMeasurement(
this.tagValueAggregationMap[encodedTags], measurement);
this.tagValueAggregationMap[encodedTags], measurement, attachments);
}

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

switch (this.aggregation) {
case AggregationType.DISTRIBUTION:
const {buckets, bucketCounts} = this.bucketBoundaries;
const bucketsCopy = Object.assign([], buckets);
const bucketCountsCopy = Object.assign([], bucketCounts);
const exemplars = new Array(bucketCounts.length);

return {
...aggregationMetadata,
type: AggregationType.DISTRIBUTION,
Expand All @@ -159,7 +167,8 @@ export class BaseView implements View {
stdDeviation: null as number,
sumOfSquaredDeviation: null as number,
buckets: bucketsCopy,
bucketCounts: bucketCountsCopy
bucketCounts: bucketCountsCopy,
exemplars
};
case AggregationType.SUM:
return {...aggregationMetadata, type: AggregationType.SUM, value: 0};
Expand Down Expand Up @@ -221,18 +230,21 @@ export class BaseView implements View {
*/
private toPoint(timestamp: Timestamp, data: AggregationData): Point {
let value;

if (data.type === AggregationType.DISTRIBUTION) {
// TODO: Add examplar transition
const {count, sum, sumOfSquaredDeviation} = data;
const {count, sum, sumOfSquaredDeviation, exemplars} = data;
const buckets = [];
for (let bucket = 0; bucket < data.bucketCounts.length; bucket++) {
const bucketCount = data.bucketCounts[bucket];
const statsExemplar = exemplars ? exemplars[bucket] : undefined;
buckets.push(this.getMetricBucket(statsExemplar, bucketCount));
}

value = {
count,
sum,
sumOfSquaredDeviation,
buckets,
bucketOptions: {explicit: {bounds: data.buckets}},
// Bucket without an Exemplar.
buckets:
data.bucketCounts.map(bucketCount => ({count: bucketCount}))
} as DistributionValue;
} else {
value = data.value as number;
Expand All @@ -249,6 +261,24 @@ export class BaseView implements View {
return this.tagValueAggregationMap[this.encodeTagValues(tagValues)];
}

/** Returns a Bucket with count and examplar (if present) */
private getMetricBucket(statsExemplar: StatsExemplar, bucketCount: number):
metricBucket {
if (statsExemplar) {
// Bucket with an Exemplar.
return {
count: bucketCount,
exemplar: {
value: statsExemplar.value,
timestamp: timestampFromMillis(statsExemplar.timestamp),
attachments: statsExemplar.attachments
}
};
}
// Bucket with no Exemplar.
return {count: bucketCount};
}

/** Determines whether the given TagKeys are valid. */
private validateTagKeys(tagKeys: TagKey[]): TagKey[] {
const tagKeysCopy = Object.assign([], tagKeys);
Expand Down
35 changes: 35 additions & 0 deletions packages/opencensus-core/test/test-recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,41 @@ describe('Recorder', () => {
}
});

describe('for distribution aggregation data with attachments', () => {
const attachments = {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'};
it('should record measurements and attachments correctly', () => {
const distributionData: DistributionData = {
type: AggregationType.DISTRIBUTION,
tagValues,
timestamp: Date.now(),
startTime: Date.now(),
count: 0,
sum: 0,
mean: 0,
stdDeviation: 0,
sumOfSquaredDeviation: 0,
buckets: [2, 4, 6],
bucketCounts: [0, 0, 0, 0],
exemplars: [undefined, undefined, undefined, undefined]
};
const value = 5;
const measurement: Measurement = {measure, value};
const aggregationData =
Recorder.addMeasurement(
distributionData, measurement, attachments) as DistributionData;

assert.equal(aggregationData.sum, 5);
assert.equal(aggregationData.mean, 5);
assert.deepStrictEqual(aggregationData.buckets, [2, 4, 6]);
assert.deepStrictEqual(aggregationData.bucketCounts, [0, 0, 1, 0]);
assert.deepStrictEqual(aggregationData.exemplars, [
undefined, undefined,
{value: 5, timestamp: aggregationData.timestamp, attachments},
undefined
]);
});
});

describe('getTagValues()', () => {
const CALLER = {name: 'caller'};
const METHOD = {name: 'method'};
Expand Down
Loading