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

Commit a6f44e0

Browse files
eduardoemerykjin
authored andcommitted
feat: add stats recorder implementation (#85)
* feat: add stats recorder implementation * refactor: change distribution data attributes - Change bucket.max to bucket.highBoundary - Change bucket.min to bucket.lowBoundary - Remove bucketBoundaries * refactor: removes unecessary depencency * refactor(fix): changes to address review comments * fix: recorder truncates int64 values * refactor(fix): changes to address review comments
1 parent ce628e4 commit a6f44e0

File tree

5 files changed

+289
-24
lines changed

5 files changed

+289
-24
lines changed

packages/opencensus-core/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
},
5555
"dependencies": {
5656
"continuation-local-storage": "^3.2.1",
57-
"hdr-histogram-js": "^1.1.4",
5857
"log-driver": "^1.2.7",
5958
"semver": "^5.5.0",
6059
"shimmer": "^1.2.0",

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

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

17-
import {AggregationData, Measurement} from './types';
17+
import {AggregationData, AggregationType, CountData, DistributionData, LastValueData, Measurement, MeasureType, SumData} from './types';
1818

1919
export class Recorder {
2020
static addMeasurement(
2121
aggregationData: AggregationData,
2222
measurement: Measurement): AggregationData {
23-
throw new Error('Not Implemented');
23+
aggregationData.timestamp = Date.now();
24+
const value = measurement.measure.type === MeasureType.DOUBLE ?
25+
measurement.value :
26+
Math.trunc(measurement.value);
27+
28+
switch (aggregationData.type) {
29+
case AggregationType.DISTRIBUTION:
30+
return this.addToDistribution(aggregationData, value);
31+
32+
case AggregationType.SUM:
33+
return this.addToSum(aggregationData, value);
34+
35+
case AggregationType.COUNT:
36+
return this.addToCount(aggregationData, value);
37+
38+
default:
39+
return this.addToLastValue(aggregationData, value);
40+
}
41+
}
42+
43+
private static addToDistribution(
44+
distributionData: DistributionData, value: number): DistributionData {
45+
distributionData.count += 1;
46+
47+
const inletBucket = distributionData.buckets.find((bucket) => {
48+
return bucket.lowBoundary <= value && value < bucket.highBoundary;
49+
});
50+
inletBucket.count += 1;
51+
52+
if (value > distributionData.max) {
53+
distributionData.max = value;
54+
}
55+
56+
if (value < distributionData.min) {
57+
distributionData.min = value;
58+
}
59+
60+
if (distributionData.count === 1) {
61+
distributionData.mean = value;
62+
}
63+
64+
distributionData.sum += value;
65+
66+
const oldMean = distributionData.mean;
67+
distributionData.mean = distributionData.mean +
68+
(value - distributionData.mean) / distributionData.count;
69+
distributionData.sumSquaredDeviations =
70+
distributionData.sumSquaredDeviations +
71+
(value - oldMean) * (value - distributionData.mean);
72+
distributionData.stdDeviation = Math.sqrt(
73+
distributionData.sumSquaredDeviations / distributionData.count);
74+
75+
return distributionData;
76+
}
77+
78+
private static addToSum(sumData: SumData, value: number): SumData {
79+
sumData.value += value;
80+
return sumData;
81+
}
82+
83+
private static addToCount(countData: CountData, value: number): CountData {
84+
countData.value += 1;
85+
return countData;
86+
}
87+
88+
private static addToLastValue(lastValueData: LastValueData, value: number):
89+
LastValueData {
90+
lastValueData.value = value;
91+
return lastValueData;
2492
}
2593
}

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

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,6 @@ export interface View {
100100
registered: boolean;
101101
/** Returns a snapshot of an AggregationData for that tags/labels values */
102102
getSnapshot(tags: Tags): AggregationData;
103-
/** Returns a list of all AggregationData in the view */
104-
getSnapshots(): AggregationData[];
105103
}
106104

107105
/**
@@ -116,7 +114,7 @@ export const enum AggregationType {
116114
}
117115

118116
/** Defines how data is collected and aggregated */
119-
export interface AggregationData {
117+
export interface AggregationMetadata {
120118
/** The aggregation type of the aggregation data */
121119
readonly type: AggregationType;
122120
/** The tags/labels that this AggregationData collects and aggregates */
@@ -126,32 +124,37 @@ export interface AggregationData {
126124
}
127125

128126
/**
129-
* Data collected and aggregated with this AggregationData will be summed up.
127+
* Data collected and aggregated with this AggregationData will be summed
128+
* up.
130129
*/
131-
export interface SumData extends AggregationData {
130+
export interface SumData extends AggregationMetadata {
131+
type: AggregationType.SUM;
132132
/** The current accumulated value */
133133
value: number;
134134
}
135135

136136
/**
137137
* This AggregationData counts the number of measurements recorded.
138138
*/
139-
export interface CountData extends AggregationData {
139+
export interface CountData extends AggregationMetadata {
140+
type: AggregationType.COUNT;
140141
/** The current counted value */
141142
value: number;
142143
}
143144

144145
/**
145-
* This AggregationData represents the last recorded value. This is useful when
146-
* giving support to Gauges.
146+
* This AggregationData represents the last recorded value. This is useful
147+
* when giving support to Gauges.
147148
*/
148-
export interface LastValueData extends AggregationData {
149+
export interface LastValueData extends AggregationMetadata {
150+
type: AggregationType.LAST_VALUE;
149151
/** The last recorded value */
150152
value: number;
151153
}
152154

153155
/** This AggregationData contains a histogram of the collected values. */
154-
export interface DistributionData extends AggregationData {
156+
export interface DistributionData extends AggregationMetadata {
157+
type: AggregationType.DISTRIBUTION;
155158
/** The first timestamp a datapoint was added */
156159
readonly startTime: number;
157160
/** Get the total count of all recorded values in the histogram */
@@ -176,16 +179,16 @@ export interface DistributionData extends AggregationData {
176179
sumSquaredDeviations: number;
177180
/** Bucket distribution of the histogram */
178181
buckets: Bucket[];
179-
/** The bucket boundaries for a histogram */
180-
readonly bucketsBoundaries: number[];
181182
}
182183

183184
/** A simple histogram bucket interface. */
184185
export interface Bucket {
185186
/** Number of occurrences in the domain */
186187
count: number;
187-
/** The maximum bucket limit in domain */
188-
readonly max: number;
189-
/** The minimum bucket limit in domain */
190-
readonly min: number;
188+
/** The maximum possible value for a data point to fall in this bucket */
189+
readonly highBoundary: number;
190+
/** The minimum possible value for a data point to fall in this bucket */
191+
readonly lowBoundary: number;
191192
}
193+
194+
export type AggregationData = SumData|CountData|LastValueData|DistributionData;

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,4 @@ export class BaseView implements View {
6060
getSnapshot(tags: Tags): AggregationData {
6161
throw new Error('Not Implemented');
6262
}
63-
64-
/** Returns a list of all AggregationData in the view */
65-
getSnapshots(): AggregationData[] {
66-
throw new Error('Not Implemented');
67-
}
6863
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* Copyright 2018, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import * as mocha from 'mocha';
19+
20+
import {Recorder} from '../src';
21+
import {AggregationType, CountData, DistributionData, LastValueData, Measure, Measurement, MeasureType, MeasureUnit, SumData, Tags} from '../src/stats/types';
22+
23+
/** The order of how close values must be to be considerated almost equal */
24+
const EPSILON = 6;
25+
26+
interface RecorderTestCase {
27+
values: number[];
28+
description: string;
29+
}
30+
31+
function isAlmostEqual(
32+
actual: number, expected: number, epsilon: number): boolean {
33+
return Math.abs(actual - expected) < Math.pow(10, -epsilon);
34+
}
35+
36+
function assertDistributionData(
37+
distributionData: DistributionData, values: number[]) {
38+
const valuesSum = values.reduce((acc, cur) => acc + cur);
39+
40+
assert.strictEqual(distributionData.max, Math.max(...values));
41+
assert.strictEqual(distributionData.min, Math.min(...values));
42+
assert.strictEqual(distributionData.count, values.length);
43+
assert.strictEqual(distributionData.sum, valuesSum);
44+
45+
for (const bucket of distributionData.buckets) {
46+
const expectedBucketCount = values
47+
.filter(
48+
value => bucket.lowBoundary <= value &&
49+
value < bucket.highBoundary)
50+
.length;
51+
assert.strictEqual(bucket.count, expectedBucketCount);
52+
}
53+
54+
const expectedMean = valuesSum / values.length;
55+
assert.ok(isAlmostEqual(distributionData.mean, expectedMean, EPSILON));
56+
57+
const expectedSumSquaredDeviations =
58+
values.map(value => Math.pow(value - expectedMean, 2))
59+
.reduce((acc, curr) => acc + curr);
60+
assert.ok(isAlmostEqual(
61+
distributionData.sumSquaredDeviations, expectedSumSquaredDeviations,
62+
EPSILON));
63+
64+
const expectedStdDeviation =
65+
Math.sqrt(expectedSumSquaredDeviations / values.length);
66+
assert.ok(isAlmostEqual(
67+
distributionData.stdDeviation, expectedStdDeviation, EPSILON));
68+
}
69+
70+
describe('Recorder', () => {
71+
const measures: Measure[] = [
72+
{name: 'Test Measure 1', type: MeasureType.DOUBLE, unit: MeasureUnit.UNIT},
73+
{name: 'Test Measure 2', type: MeasureType.INT64, unit: MeasureUnit.UNIT}
74+
];
75+
const tags: Tags = {testKey: 'testValue'};
76+
const testCases: RecorderTestCase[] = [
77+
{values: [1.1, 2.5, 3.2, 4.7, 5.2], description: 'with positive values'}, {
78+
values: [-1.5, -2.3, -3.7, -4.3, -5.9],
79+
description: 'with negative values'
80+
},
81+
{values: [0, 0, 0, 0], description: 'with zeros'},
82+
{values: [1.1, -2.3, 3.2, -4.3, 5.2], description: 'with mixed values'}
83+
];
84+
85+
for (const measure of measures) {
86+
describe(`for count aggregation data of ${measure.type} values`, () => {
87+
for (const testCase of testCases) {
88+
it(`should record measurements ${testCase.description} correctly`,
89+
() => {
90+
const countData: CountData = {
91+
type: AggregationType.COUNT,
92+
tags,
93+
timestamp: Date.now(),
94+
value: 0
95+
};
96+
let count = 0;
97+
for (const value of testCase.values) {
98+
count++;
99+
const measurement: Measurement = {measure, tags, value};
100+
const updatedAggregationData =
101+
Recorder.addMeasurement(countData, measurement) as CountData;
102+
103+
assert.strictEqual(updatedAggregationData.value, count);
104+
}
105+
});
106+
}
107+
});
108+
109+
describe(
110+
`for last value aggregation data of ${measure.type} values`, () => {
111+
for (const testCase of testCases) {
112+
it(`should record measurements ${testCase.description} correctly`,
113+
() => {
114+
const lastValueData: LastValueData = {
115+
type: AggregationType.LAST_VALUE,
116+
tags,
117+
timestamp: Date.now(),
118+
value: undefined
119+
};
120+
for (const value of testCase.values) {
121+
const measurement: Measurement = {measure, tags, value};
122+
const lastValue = measure.type === MeasureType.DOUBLE ?
123+
value :
124+
Math.trunc(value);
125+
126+
const updatedAggregationData =
127+
Recorder.addMeasurement(lastValueData, measurement) as
128+
LastValueData;
129+
assert.strictEqual(updatedAggregationData.value, lastValue);
130+
}
131+
});
132+
}
133+
});
134+
135+
describe(`for sum aggregation data of ${measure.type} values`, () => {
136+
for (const testCase of testCases) {
137+
it(`should record measurements ${testCase.description} correctly`,
138+
() => {
139+
const sumData: SumData = {
140+
type: AggregationType.SUM,
141+
tags,
142+
timestamp: Date.now(),
143+
value: 0
144+
};
145+
let acc = 0;
146+
for (const value of testCase.values) {
147+
acc += measure.type === MeasureType.DOUBLE ? value :
148+
Math.trunc(value);
149+
const measurement: Measurement = {measure, tags, value};
150+
const updatedAggregationData =
151+
Recorder.addMeasurement(sumData, measurement) as SumData;
152+
153+
assert.strictEqual(updatedAggregationData.value, acc);
154+
}
155+
});
156+
}
157+
});
158+
159+
describe(
160+
`for distribution aggregation data of ${measure.type} values`, () => {
161+
for (const testCase of testCases) {
162+
it(`should record measurements ${testCase.description} correctly`,
163+
() => {
164+
const distributionData: DistributionData = {
165+
type: AggregationType.DISTRIBUTION,
166+
tags,
167+
timestamp: Date.now(),
168+
startTime: Date.now(),
169+
count: 0,
170+
sum: 0,
171+
max: Number.MIN_SAFE_INTEGER,
172+
min: Number.MAX_SAFE_INTEGER,
173+
mean: 0,
174+
stdDeviation: 0,
175+
sumSquaredDeviations: 0,
176+
buckets: [
177+
{highBoundary: 0, lowBoundary: -Infinity, count: 0},
178+
{highBoundary: 2, lowBoundary: 0, count: 0},
179+
{highBoundary: 4, lowBoundary: 2, count: 0},
180+
{highBoundary: 6, lowBoundary: 4, count: 0},
181+
{highBoundary: Infinity, lowBoundary: 6, count: 0}
182+
]
183+
};
184+
const sentValues = [];
185+
for (const value of testCase.values) {
186+
sentValues.push(
187+
measure.type === MeasureType.DOUBLE ? value :
188+
Math.trunc(value));
189+
const measurement: Measurement = {measure, tags, value};
190+
const updatedAggregationData =
191+
Recorder.addMeasurement(distributionData, measurement) as
192+
DistributionData;
193+
194+
assertDistributionData(distributionData, sentValues);
195+
}
196+
});
197+
}
198+
});
199+
}
200+
});

0 commit comments

Comments
 (0)