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

Commit 4c4ce0f

Browse files
eduardoemerykjin
authored andcommitted
feat: add stats view implementation (#89)
* feat: add stats view implementation * refactor(fix): changes to address review comments * feat: add method to access view's columns * feat: add tag format verification
1 parent 6ec40aa commit 4c4ce0f

File tree

3 files changed

+370
-8
lines changed

3 files changed

+370
-8
lines changed

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export interface View {
8282
*/
8383
readonly name: string;
8484
/** Describes the view, e.g. "RPC latency distribution" */
85-
readonly description?: string;
85+
readonly description: string;
8686
/** The Measure to which this view is applied. */
8787
readonly measure: Measure;
8888
/**
@@ -98,7 +98,18 @@ export interface View {
9898
endTime: number;
9999
/** true if the view was registered */
100100
registered: boolean;
101-
/** Returns a snapshot of an AggregationData for that tags/labels values */
101+
/**
102+
* Records a measurement in the proper view's row. This method is used by
103+
* Stats. User should prefer using Stats.record() instead.
104+
*
105+
* Measurements with measurement type INT64 will have its value truncated.
106+
* @param measurement The measurement to record
107+
*/
108+
recordMeasurement(measurement: Measurement): void;
109+
/**
110+
* Returns a snapshot of an AggregationData for that tags/labels values.
111+
* @param tags The desired data's tags
112+
*/
102113
getSnapshot(tags: Tags): AggregationData;
103114
}
104115

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

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

17-
import {AggregationData, AggregationType, Measure, Tags, View} from './types';
17+
import {Recorder} from './recorder';
18+
import {AggregationData, AggregationMetadata, AggregationType, Bucket, CountData, DistributionData, LastValueData, Measure, Measurement, MeasureType, SumData, Tags, View} from './types';
19+
20+
const RECORD_SEPARATOR = String.fromCharCode(30);
21+
const UNIT_SEPARATOR = String.fromCharCode(31);
22+
23+
// String that has only printable characters
24+
const invalidString = /[^\u0020-\u007e]/;
1825

1926
export class BaseView implements View {
2027
/**
@@ -32,32 +39,164 @@ export class BaseView implements View {
3239
* If no Tags are provided, then, all data is recorded in a single
3340
* aggregation.
3441
*/
35-
private rows: {[key: string]: AggregationData};
42+
private rows: {[key: string]: AggregationData} = {};
43+
/**
44+
* A list of tag keys that represents the possible column labels
45+
*/
46+
private columns: string[];
3647
/**
3748
* An Aggregation describes how data collected is aggregated.
3849
* There are four aggregation types: count, sum, lastValue and distirbution.
3950
*/
4051
readonly aggregation: AggregationType;
4152
/** The start time for this view */
4253
readonly startTime: number;
54+
/** The bucket boundaries in a Distribution Aggregation */
55+
private bucketBoundaries?: number[];
4356
/**
4457
* The end time for this view - represents the last time a value was recorded
4558
*/
4659
endTime: number;
4760
/** true if the view was registered */
48-
registered: boolean;
61+
registered = false;
4962

63+
/**
64+
* Creates a new View instance. This constructor is used by Stats. User should
65+
* prefer using Stats.createView() instead.
66+
* @param name The view name
67+
* @param measure The view measure
68+
* @param aggregation The view aggregation type
69+
* @param tagsKeys The Tags' keys that view will have
70+
* @param description The view description
71+
* @param bucketBoundaries The view bucket boundaries for a distribution
72+
* aggregation type
73+
*/
5074
constructor(
5175
name: string, measure: Measure, aggregation: AggregationType,
52-
tagKeys: string[], description?: string) {
53-
throw new Error('Not Implemented');
76+
tagsKeys: string[], description: string, bucketBoundaries?: number[]) {
77+
if (aggregation === AggregationType.DISTRIBUTION && !bucketBoundaries) {
78+
throw new Error('No bucketBoundaries specified');
79+
}
80+
this.name = name;
81+
this.description = description;
82+
this.measure = measure;
83+
this.columns = tagsKeys;
84+
this.aggregation = aggregation;
85+
this.startTime = Date.now();
86+
this.bucketBoundaries = bucketBoundaries;
87+
}
88+
89+
/** Gets the view's tag keys */
90+
getColumns(): string[] {
91+
return this.columns;
92+
}
93+
94+
/**
95+
* Records a measurement in the proper view's row. This method is used by
96+
* Stats. User should prefer using Stats.record() instead.
97+
*
98+
* Measurements with measurement type INT64 will have its value truncated.
99+
* @param measurement The measurement to record
100+
*/
101+
recordMeasurement(measurement: Measurement) {
102+
// Checks if measurement has valid tags
103+
if (this.invalidTags(measurement.tags)) {
104+
return;
105+
}
106+
107+
// Checks if measurement has all tags in views
108+
for (const tagKey of this.columns) {
109+
if (!Object.keys(measurement.tags).some((key) => key === tagKey)) {
110+
return;
111+
}
112+
}
113+
114+
const encodedTags = this.encodeTags(measurement.tags);
115+
if (!this.rows[encodedTags]) {
116+
this.rows[encodedTags] = this.createAggregationData(measurement.tags);
117+
}
118+
Recorder.addMeasurement(this.rows[encodedTags], measurement);
119+
}
120+
121+
/**
122+
* Encodes a Tags object into a key sorted string.
123+
* @param tags The tags to encode
124+
*/
125+
private encodeTags(tags: Tags): string {
126+
return Object.keys(tags)
127+
.sort()
128+
.map(tagKey => {
129+
return tagKey + UNIT_SEPARATOR + tags[tagKey];
130+
})
131+
.join(RECORD_SEPARATOR);
132+
}
133+
134+
/**
135+
* Checks if tag keys and values have only printable characters.
136+
* @param tags The tags to be checked
137+
*/
138+
private invalidTags(tags: Tags): boolean {
139+
return Object.keys(tags).some(tagKey => {
140+
return invalidString.test(tagKey) || invalidString.test(tags[tagKey]);
141+
});
142+
}
143+
144+
/**
145+
* Creates an empty aggregation data for a given tags.
146+
* @param tags The tags for that aggregation data
147+
*/
148+
private createAggregationData(tags: Tags): AggregationData {
149+
const aggregationMetadata = {tags, timestamp: Date.now()};
150+
151+
switch (this.aggregation) {
152+
case AggregationType.DISTRIBUTION:
153+
return {
154+
...aggregationMetadata,
155+
type: AggregationType.DISTRIBUTION,
156+
startTime: this.startTime,
157+
count: 0,
158+
sum: 0,
159+
max: Number.MIN_SAFE_INTEGER,
160+
min: Number.MAX_SAFE_INTEGER,
161+
mean: null as number,
162+
stdDeviation: null as number,
163+
sumSquaredDeviations: null as number,
164+
buckets: this.createBuckets(this.bucketBoundaries)
165+
};
166+
case AggregationType.SUM:
167+
return {...aggregationMetadata, type: AggregationType.SUM, value: 0};
168+
case AggregationType.COUNT:
169+
return {...aggregationMetadata, type: AggregationType.COUNT, value: 0};
170+
default:
171+
return {
172+
...aggregationMetadata,
173+
type: AggregationType.LAST_VALUE,
174+
value: undefined
175+
};
176+
}
177+
}
178+
179+
/**
180+
* Creates empty Buckets, given a list of bucket boundaries.
181+
* @param bucketBoundaries a list with the bucket boundaries
182+
*/
183+
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+
});
54193
}
55194

56195
/**
57196
* Returns a snapshot of an AggregationData for that tags/labels values.
58197
* @param tags The desired data's tags
59198
*/
60199
getSnapshot(tags: Tags): AggregationData {
61-
throw new Error('Not Implemented');
200+
return this.rows[this.encodeTags(tags)];
62201
}
63202
}

0 commit comments

Comments
 (0)