diff --git a/packages/opencensus-core/src/exporters/console-exporter.ts b/packages/opencensus-core/src/exporters/console-exporter.ts index 94ef45fd6..d1b33bba1 100644 --- a/packages/opencensus-core/src/exporters/console-exporter.ts +++ b/packages/opencensus-core/src/exporters/console-exporter.ts @@ -92,7 +92,7 @@ export class ConsoleStatsExporter implements types.StatsEventListener { * @param view recorded view from measurement * @param measurement recorded measurement */ - onRecord(view: View, measurement: Measurement) { - console.log(`Measurement recorded: ${view.measure.name}`); + onRecord(views: View[], measurement: Measurement) { + console.log(`Measurement recorded: ${measurement.measure.name}`); } } diff --git a/packages/opencensus-core/src/exporters/types.ts b/packages/opencensus-core/src/exporters/types.ts index 8bfe9272c..c7f0f511f 100644 --- a/packages/opencensus-core/src/exporters/types.ts +++ b/packages/opencensus-core/src/exporters/types.ts @@ -39,10 +39,10 @@ export interface StatsEventListener { onRegisterView(view: View): void; /** * Is called whenever a new measurement is recorded. - * @param view The view related to the measurement + * @param views The views related to the measurement * @param measurement The recorded measurement */ - onRecord(view: View, measurement: Measurement): void; + onRecord(views: View[], measurement: Measurement): void; } export type ExporterConfig = configTypes.BufferConfig; diff --git a/packages/opencensus-core/src/stats/stats.ts b/packages/opencensus-core/src/stats/stats.ts index 40ed336be..5e5ecf12f 100644 --- a/packages/opencensus-core/src/stats/stats.ts +++ b/packages/opencensus-core/src/stats/stats.ts @@ -16,13 +16,14 @@ import {StatsEventListener} from '../exporters/types'; -import {AggregationType, Measure, Measurement, MeasureUnit, View} from './types'; +import {AggregationType, Measure, Measurement, MeasureType, MeasureUnit, View} from './types'; +import {BaseView} from './view'; export class Stats { /** A list of Stats exporters */ private statsEventListeners: StatsEventListener[] = []; /** A map of Measures (name) to their corresponding Views */ - private registeredViews: {[key: string]: View[]}; + private registeredViews: {[key: string]: View[]} = {}; constructor() {} @@ -32,7 +33,18 @@ export class Stats { * @param view The view to be registered */ registerView(view: View) { - throw new Error('Not Implemented'); + if (this.registeredViews[view.measure.name]) { + this.registeredViews[view.measure.name].push(view); + } else { + this.registeredViews[view.measure.name] = [view]; + } + + view.registered = true; + + // Notifies all exporters + for (const exporter of this.statsEventListeners) { + exporter.onRegisterView(view); + } } /** @@ -42,11 +54,17 @@ export class Stats { * @param aggregation The view aggregation type * @param tagKeys The view columns (tag keys) * @param description The view description + * @param bucketBoundaries The view bucket boundaries for a distribution + * aggregation type */ createView( name: string, measure: Measure, aggregation: AggregationType, - tagKeys: string[], description?: string): View { - throw new Error('Not Implemented'); + tagKeys: string[], description: string, + bucketBoundaries?: number[]): View { + const view = new BaseView( + name, measure, aggregation, tagKeys, description, bucketBoundaries); + this.registerView(view); + return view; } /** @@ -54,7 +72,13 @@ export class Stats { * @param exporter An stats exporter */ registerExporter(exporter: StatsEventListener) { - throw new Error('Not Implemented'); + this.statsEventListeners.push(exporter); + + for (const measureName of Object.keys(this.registeredViews)) { + for (const view of this.registeredViews[measureName]) { + exporter.onRegisterView(view); + } + } } /** @@ -65,7 +89,7 @@ export class Stats { */ createMeasureDouble(name: string, unit: MeasureUnit, description?: string): Measure { - throw new Error('Not Implemented'); + return {name, unit, type: MeasureType.DOUBLE, description}; } /** @@ -77,14 +101,28 @@ export class Stats { */ createMeasureInt64(name: string, unit: MeasureUnit, description?: string): Measure { - throw new Error('Not Implemented'); + return {name, unit, type: MeasureType.INT64, description}; } /** * Updates all views with the new measurements. * @param measurements A list of measurements to record */ - record(measurements: Measurement[]) { - throw new Error('Not Implemented'); + record(...measurements: Measurement[]) { + for (const measurement of measurements) { + const views = this.registeredViews[measurement.measure.name]; + if (!views) { + break; + } + // Updates all views + for (const view of views) { + view.recordMeasurement(measurement); + } + + // Notifies all exporters + for (const exporter of this.statsEventListeners) { + exporter.onRecord(views, measurement); + } + } } } diff --git a/packages/opencensus-core/src/stats/types.ts b/packages/opencensus-core/src/stats/types.ts index b22187d70..f3f27a21c 100644 --- a/packages/opencensus-core/src/stats/types.ts +++ b/packages/opencensus-core/src/stats/types.ts @@ -111,6 +111,8 @@ export interface View { * @param tags The desired data's tags */ getSnapshot(tags: Tags): AggregationData; + /** Gets the view's tag keys */ + getColumns(): string[]; } /** diff --git a/packages/opencensus-core/test/test-stats.ts b/packages/opencensus-core/test/test-stats.ts new file mode 100644 index 000000000..3507622e5 --- /dev/null +++ b/packages/opencensus-core/test/test-stats.ts @@ -0,0 +1,183 @@ +/** + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import * as mocha from 'mocha'; + +import {BaseView, Stats, StatsEventListener} from '../src'; +import {AggregationType, LastValueData, Measure, Measurement, MeasureType, MeasureUnit, View} from '../src/stats/types'; + +class TestExporter implements StatsEventListener { + registeredViews: View[] = []; + recordedMeasurements: Measurement[] = []; + + onRegisterView(view: View) { + this.registeredViews.push(view); + } + + onRecord(views: View[], measurement: Measurement) { + this.recordedMeasurements.push(measurement); + } + + clean() { + this.registeredViews = []; + this.recordedMeasurements = []; + } +} + +describe('Stats', () => { + let stats: Stats; + + beforeEach(() => { + stats = new Stats(); + }); + + const viewName = 'testViewName'; + const tags = {tagKey1: 'tagValue1', tagKey2: 'tagValue2'}; + const tagKeys = Object.keys(tags); + const measureName = 'testMeasureDouble'; + const measureUnit = MeasureUnit.UNIT; + const description = 'test description'; + + describe('createMeasureDouble()', () => { + it('should create a measure of type double', () => { + const measureDouble = + stats.createMeasureDouble(measureName, measureUnit, description); + assert.strictEqual(measureDouble.type, MeasureType.DOUBLE); + assert.strictEqual(measureDouble.name, measureName); + assert.strictEqual(measureDouble.unit, measureUnit); + assert.strictEqual(measureDouble.description, description); + }); + }); + + describe('createMeasureInt64()', () => { + it('should create a measure of type int64', () => { + const measureDouble = + stats.createMeasureInt64(measureName, measureUnit, description); + assert.strictEqual(measureDouble.type, MeasureType.INT64); + assert.strictEqual(measureDouble.name, measureName); + assert.strictEqual(measureDouble.unit, measureUnit); + assert.strictEqual(measureDouble.description, description); + }); + }); + + describe('createView()', () => { + const aggregationTypes = [ + AggregationType.COUNT, AggregationType.SUM, AggregationType.LAST_VALUE, + AggregationType.DISTRIBUTION + ]; + let measure: Measure; + + before(() => { + measure = stats.createMeasureInt64(measureName, measureUnit); + }); + + for (const aggregationType of aggregationTypes) { + it(`should create a view with ${aggregationType} aggregation`, () => { + const bucketBoundaries = + AggregationType.DISTRIBUTION ? [1, 2, 3] : null; + const view = stats.createView( + viewName, measure, aggregationType, tagKeys, description, + bucketBoundaries); + + assert.strictEqual(view.name, viewName); + assert.strictEqual(view.measure, measure); + assert.strictEqual(view.description, description); + assert.deepEqual(view.measure, measure); + assert.strictEqual(view.aggregation, aggregationType); + assert.ok(view.registered); + }); + } + + it('should not create a view with distribution aggregation when no bucket boundaries were given', + () => { + assert.throws(stats.createView, 'No bucketBoundaries specified'); + }); + }); + + describe('registerView()', () => { + let measure: Measure; + const testExporter = new TestExporter(); + + before(() => { + measure = stats.createMeasureInt64(measureName, measureUnit); + }); + + it('should register a view', () => { + stats.registerExporter(testExporter); + const view = new BaseView( + viewName, measure, AggregationType.LAST_VALUE, tagKeys, description); + + assert.ok(!view.registered); + assert.strictEqual(testExporter.registeredViews.length, 0); + + stats.registerView(view); + + assert.ok(view.registered); + assert.strictEqual(testExporter.registeredViews.length, 1); + assert.deepEqual(testExporter.registeredViews[0], view); + }); + }); + + describe('record()', () => { + let measure: Measure; + const testExporter = new TestExporter(); + + before(() => { + measure = stats.createMeasureInt64(measureName, measureUnit); + }); + + beforeEach(() => { + testExporter.clean(); + }); + + it('should record a single measurement', () => { + stats.registerExporter(testExporter); + const view = stats.createView( + viewName, measure, AggregationType.LAST_VALUE, tagKeys, description); + const measurement = {measure, tags, value: 1}; + + assert.strictEqual(testExporter.recordedMeasurements.length, 0); + + stats.record(measurement); + const aggregationData = + testExporter.registeredViews[0].getSnapshot(tags) as LastValueData; + + assert.strictEqual(testExporter.recordedMeasurements.length, 1); + assert.deepEqual(testExporter.recordedMeasurements[0], measurement); + assert.strictEqual(aggregationData.value, measurement.value); + }); + + it('should record multiple measurements', () => { + stats.registerExporter(testExporter); + const view = stats.createView( + viewName, measure, AggregationType.LAST_VALUE, tagKeys, description); + const measurement1 = {measure, tags, value: 1}; + const measurement2 = {measure, tags, value: 1}; + + assert.strictEqual(testExporter.recordedMeasurements.length, 0); + + stats.record(measurement1, measurement2); + const aggregationData = + testExporter.registeredViews[0].getSnapshot(tags) as LastValueData; + + assert.strictEqual(testExporter.recordedMeasurements.length, 2); + assert.deepEqual(testExporter.recordedMeasurements[0], measurement1); + assert.deepEqual(testExporter.recordedMeasurements[1], measurement2); + assert.strictEqual(aggregationData.value, measurement2.value); + }); + }); +});