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

Commit bf1f90c

Browse files
authored
Drops negative bucket bounds and negative values in record API (#175)
#172 * Ignores negative bucket bounds * Drops negative values in record API
1 parent f1685dc commit bf1f90c

File tree

4 files changed

+133
-35
lines changed

4 files changed

+133
-35
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import * as defaultLogger from '../common/console-logger';
18+
import * as loggerTypes from '../common/types';
1719
import {StatsEventListener} from '../exporters/types';
1820

1921
import {AggregationType, Measure, Measurement, MeasureType, MeasureUnit, View} from './types';
@@ -24,8 +26,16 @@ export class Stats {
2426
private statsEventListeners: StatsEventListener[] = [];
2527
/** A map of Measures (name) to their corresponding Views */
2628
private registeredViews: {[key: string]: View[]} = {};
29+
/** An object to log information to */
30+
private logger: loggerTypes.Logger;
2731

28-
constructor() {}
32+
/**
33+
* Creates stats
34+
* @param logger
35+
*/
36+
constructor(logger = defaultLogger) {
37+
this.logger = logger.logger();
38+
}
2939

3040
/**
3141
* Registers a view to listen to new measurements in its measure. Prefer using
@@ -110,6 +120,12 @@ export class Stats {
110120
*/
111121
record(...measurements: Measurement[]) {
112122
for (const measurement of measurements) {
123+
if (measurement.value < 0) {
124+
this.logger.warn(`Dropping value ${
125+
measurement.value}, value to record must be non-negative.`);
126+
break;
127+
}
128+
113129
const views = this.registeredViews[measurement.measure.name];
114130
if (!views) {
115131
break;

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

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17+
import * as defaultLogger from '../common/console-logger';
18+
import * as loggerTypes from '../common/types';
19+
1720
import {Recorder} from './recorder';
1821
import {AggregationData, AggregationMetadata, AggregationType, Bucket, CountData, DistributionData, LastValueData, Measure, Measurement, MeasureType, SumData, Tags, View} from './types';
1922

@@ -59,6 +62,8 @@ export class BaseView implements View {
5962
endTime: number;
6063
/** true if the view was registered */
6164
registered = false;
65+
/** An object to log information to */
66+
private logger: loggerTypes.Logger;
6267

6368
/**
6469
* Creates a new View instance. This constructor is used by Stats. User should
@@ -70,13 +75,16 @@ export class BaseView implements View {
7075
* @param description The view description
7176
* @param bucketBoundaries The view bucket boundaries for a distribution
7277
* aggregation type
78+
* @param logger
7379
*/
7480
constructor(
7581
name: string, measure: Measure, aggregation: AggregationType,
76-
tagsKeys: string[], description: string, bucketBoundaries?: number[]) {
82+
tagsKeys: string[], description: string, bucketBoundaries?: number[],
83+
logger = defaultLogger) {
7784
if (aggregation === AggregationType.DISTRIBUTION && !bucketBoundaries) {
7885
throw new Error('No bucketBoundaries specified');
7986
}
87+
this.logger = logger.logger();
8088
this.name = name;
8189
this.description = description;
8290
this.measure = measure;
@@ -115,6 +123,7 @@ export class BaseView implements View {
115123
if (!this.rows[encodedTags]) {
116124
this.rows[encodedTags] = this.createAggregationData(measurement.tags);
117125
}
126+
118127
Recorder.addMeasurement(this.rows[encodedTags], measurement);
119128
}
120129

@@ -181,15 +190,57 @@ export class BaseView implements View {
181190
* @param bucketBoundaries a list with the bucket boundaries
182191
*/
183192
private createBuckets(bucketBoundaries: number[]): Bucket[] {
184-
return bucketBoundaries.map((boundary, boundaryIndex) => {
185-
return {
186-
count: 0,
187-
lowBoundary: boundaryIndex ? boundary : -Infinity,
188-
highBoundary: (boundaryIndex === bucketBoundaries.length - 1) ?
189-
Infinity :
190-
bucketBoundaries[boundaryIndex + 1]
191-
};
192-
});
193+
let negative = 0;
194+
const result = bucketBoundaries.reduce((accumulator, boundary, index) => {
195+
if (boundary >= 0) {
196+
const nextBoundary = bucketBoundaries[index + 1];
197+
this.validateBoundary(boundary, nextBoundary);
198+
const len = bucketBoundaries.length - negative;
199+
const position = index - negative;
200+
const bucket = this.createBucket(boundary, nextBoundary, position, len);
201+
accumulator.push(bucket);
202+
} else {
203+
negative++;
204+
}
205+
return accumulator;
206+
}, []);
207+
if (negative) {
208+
this.logger.warn(`Dropping ${
209+
negative} negative bucket boundaries, the values must be strictly > 0.`);
210+
}
211+
return result;
212+
}
213+
214+
/**
215+
* Checks boundaries order and duplicates
216+
* @param current Boundary
217+
* @param next Next boundary
218+
*/
219+
private validateBoundary(current: number, next: number) {
220+
if (next) {
221+
if (current > next) {
222+
this.logger.error('Bucket boundaries not sorted.');
223+
}
224+
if (current === next) {
225+
this.logger.error('Bucket boundaries not unique.');
226+
}
227+
}
228+
}
229+
230+
/**
231+
* Creates empty bucket boundary.
232+
* @param current Current boundary
233+
* @param next Next boundary
234+
* @param position Index of boundary
235+
* @param max Maximum length of boundaries
236+
*/
237+
private createBucket(
238+
current: number, next: number, position: number, max: number): Bucket {
239+
return {
240+
count: 0,
241+
lowBoundary: position ? current : -Infinity,
242+
highBoundary: (position === max - 1) ? Infinity : next
243+
};
193244
}
194245

195246
/**

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

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -135,49 +135,57 @@ describe('Stats', () => {
135135
describe('record()', () => {
136136
let measure: Measure;
137137
const testExporter = new TestExporter();
138-
138+
let view;
139+
let aggregationData: LastValueData;
139140
before(() => {
140141
measure = stats.createMeasureInt64(measureName, measureUnit);
141142
});
142143

143144
beforeEach(() => {
144145
testExporter.clean();
146+
stats.registerExporter(testExporter);
147+
view = stats.createView(
148+
viewName, measure, AggregationType.LAST_VALUE, tagKeys, description);
145149
});
146150

147151
it('should record a single measurement', () => {
148-
stats.registerExporter(testExporter);
149-
const view = stats.createView(
150-
viewName, measure, AggregationType.LAST_VALUE, tagKeys, description);
151152
const measurement = {measure, tags, value: 1};
152-
153153
assert.strictEqual(testExporter.recordedMeasurements.length, 0);
154-
155154
stats.record(measurement);
156-
const aggregationData =
157-
testExporter.registeredViews[0].getSnapshot(tags) as LastValueData;
158-
159155
assert.strictEqual(testExporter.recordedMeasurements.length, 1);
160156
assert.deepEqual(testExporter.recordedMeasurements[0], measurement);
157+
aggregationData =
158+
testExporter.registeredViews[0].getSnapshot(tags) as LastValueData;
161159
assert.strictEqual(aggregationData.value, measurement.value);
162160
});
163161

164-
it('should record multiple measurements', () => {
162+
it('should not record a single negative measurement', () => {
165163
stats.registerExporter(testExporter);
166-
const view = stats.createView(
167-
viewName, measure, AggregationType.LAST_VALUE, tagKeys, description);
164+
const measurement = {measure, tags, value: -1};
165+
stats.record(measurement);
166+
assert.strictEqual(testExporter.recordedMeasurements.length, 0);
167+
});
168+
169+
it('should record multiple measurements', () => {
168170
const measurement1 = {measure, tags, value: 1};
169171
const measurement2 = {measure, tags, value: 1};
170-
171172
assert.strictEqual(testExporter.recordedMeasurements.length, 0);
172-
173173
stats.record(measurement1, measurement2);
174-
const aggregationData =
175-
testExporter.registeredViews[0].getSnapshot(tags) as LastValueData;
176-
177174
assert.strictEqual(testExporter.recordedMeasurements.length, 2);
178175
assert.deepEqual(testExporter.recordedMeasurements[0], measurement1);
179176
assert.deepEqual(testExporter.recordedMeasurements[1], measurement2);
177+
aggregationData =
178+
testExporter.registeredViews[0].getSnapshot(tags) as LastValueData;
180179
assert.strictEqual(aggregationData.value, measurement2.value);
181180
});
181+
182+
it('should stop record on negative value of multiple measurement', () => {
183+
const measurments = [
184+
{measure, tags, value: 1}, {measure, tags, value: -1},
185+
{measure, tags, value: 1}
186+
];
187+
stats.record(...measurments);
188+
assert.equal(testExporter.recordedMeasurements.length, 1);
189+
});
182190
});
183191
});

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ function assertView(
7878
aggregationType: AggregationType) {
7979
assert.strictEqual(view.aggregation, aggregationType);
8080
const aggregationData = view.getSnapshot(measurement.tags);
81-
8281
switch (aggregationData.type) {
8382
case AggregationType.SUM:
8483
const acc = recordedValues.reduce((acc, cur) => acc + cur);
@@ -125,19 +124,18 @@ describe('BaseView', () => {
125124
});
126125

127126
describe('recordMeasurement()', () => {
128-
const measurementValues = [1.1, -2.3, 3.2, -4.3, 5.2];
127+
const measurementValues = [1.1, 2.3, 3.2, 4.3, 5.2];
129128
const bucketBoundaries = [0, 2, 4, 6];
130129
const emptyAggregation = {};
130+
const tags: Tags = {testKey1: 'testValue', testKey2: 'testValue'};
131131

132132
for (const aggregationTestCase of aggregationTestCases) {
133-
const tags: Tags = {testKey1: 'testValue', testKey2: 'testValue'};
134-
const view = new BaseView(
135-
'test/view/name', measure, aggregationTestCase.aggregationType,
136-
['testKey1', 'testKey2'], 'description test', bucketBoundaries);
137-
138133
it(`should record measurements on a View with ${
139134
aggregationTestCase.description} Aggregation Data type`,
140135
() => {
136+
const view = new BaseView(
137+
'test/view/name', measure, aggregationTestCase.aggregationType,
138+
['testKey1', 'testKey2'], 'description test', bucketBoundaries);
141139
const recordedValues = [];
142140
for (const value of measurementValues) {
143141
recordedValues.push(value);
@@ -150,6 +148,31 @@ describe('BaseView', () => {
150148
});
151149
}
152150

151+
it('should ignore negative bucket bounds', () => {
152+
const negativeBucketBoundaries = [-Infinity, -4, -2, 0, 2, 4, 6];
153+
const view = new BaseView(
154+
'test/view/name', measure, AggregationType.DISTRIBUTION,
155+
['testKey1', 'testKey2'], 'description test',
156+
negativeBucketBoundaries);
157+
const recordedValues = [];
158+
for (const value of measurementValues) {
159+
recordedValues.push(value);
160+
const measurement = {measure, tags, value};
161+
view.recordMeasurement(measurement);
162+
}
163+
const data = view.getSnapshot(tags) as DistributionData;
164+
const expectedBuckets = [
165+
{count: 1, lowBoundary: -Infinity, highBoundary: 2},
166+
{count: 2, lowBoundary: 2, highBoundary: 4},
167+
{count: 2, lowBoundary: 4, highBoundary: 6},
168+
{count: 0, lowBoundary: 6, highBoundary: Infinity}
169+
];
170+
assert.equal(data.buckets.length, expectedBuckets.length);
171+
expectedBuckets.forEach((bucket, index) => {
172+
assert.deepStrictEqual(data.buckets[index], bucket);
173+
});
174+
});
175+
153176
const view = new BaseView(
154177
'test/view/name', measure, AggregationType.LAST_VALUE,
155178
['testKey1', 'testKey2'], 'description test');

0 commit comments

Comments
 (0)