From 36ac4b22714cdede9b9f831f4c74a37ac481f73b Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 21 Jan 2019 12:06:12 -0800 Subject: [PATCH 1/2] Use Tags API --- CHANGELOG.md | 69 ++++- .../src/exporters/console-exporter.ts | 4 +- .../opencensus-core/src/exporters/types.ts | 5 +- .../opencensus-core/src/stats/metric-utils.ts | 19 +- .../opencensus-core/src/stats/recorder.ts | 15 ++ packages/opencensus-core/src/stats/stats.ts | 18 +- packages/opencensus-core/src/stats/types.ts | 30 ++- packages/opencensus-core/src/stats/view.ts | 127 ++++------ packages/opencensus-core/src/tags/tag-map.ts | 4 +- .../opencensus-core/src/tags/validation.ts | 4 +- .../test/test-metric-producer.ts | 37 +-- .../opencensus-core/test/test-metric-utils.ts | 11 +- .../opencensus-core/test/test-recorder.ts | 66 ++++- packages/opencensus-core/test/test-stats.ts | 51 ++-- packages/opencensus-core/test/test-view.ts | 236 ++++++++++-------- .../src/prometheus-stats.ts | 64 +++-- .../test/test-prometheus-stats.ts | 64 ++--- .../src/stackdriver-monitoring.ts | 10 +- .../src/stackdriver-stats-utils.ts | 3 +- .../test/test-stackdriver-stats-utils.ts | 12 + .../page-handlers/rpcz.page-handler.ts | 48 ++-- .../page-handlers/statsz.page-handler.ts | 22 +- .../opencensus-exporter-zpages/src/zpages.ts | 11 +- .../templates/statsz-view-count.ejs | 10 +- .../templates/statsz-view-distribution.ejs | 12 +- .../templates/statsz-view-lastvalue.ejs | 10 +- .../templates/statsz-view-sum.ejs | 10 +- .../test/test-zpages.ts | 153 ++++++------ 28 files changed, 661 insertions(+), 464 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6a28e24..b0bd689c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ## Unreleased - Add Metrics API. - Add Resource API. +- Add Tags API. - Add Gauges (`DoubleGauge`, `LongGauge`, `DerivedDoubleGauge`, `DerivedLongGauge`) APIs. - Add support for supplying instrumentation configuration via tracing option. Option argument added to instrumentation interface. - Add ignoreIncomingPaths and ignoreOutgoingUrls support to the http and https tracing instrumentations. @@ -14,26 +15,82 @@ All notable changes to this project will be documented in this file. - Modify `Logger` interface: `level` made optional, `silly` removed. - The ```new Stats()``` has been deprecated on Stats class. The global singleton ```globalStats``` object should be used instead. Also, ```registerView()``` is separated out from ```createView()```. +- Use ```TagKey```, ```TagValue``` and ```TagMap``` to create the tag keys, tag values. ##### Old code ```js const { Stats } = require("@opencensus/core"); const stats = new Stats(); +// Counts/groups the lengths of lines read in. +const mLineLengths = stats.createMeasureInt64( + "demo/line_lengths", + MeasureUnit.BYTE, + "The distribution of line lengths" +); + +// Create tag keys +const tagKeys = ["method", "status"]; + // Create and register the view -stats.createView(...); +stats.createView( + "demo/lines_in", + mLineLengths, + AggregationType.COUNT, + tagKeys, + "The number of lines from standard input" +); + +// Records measurements +stats.record({ + measure: mLineLengths, + tags, + value: 2 +}); + ``` ##### New code ```js -// Get the global singleton stats object +// Gets the global stats instance const { globalStats } = require("@opencensus/core"); -// Create the view -const view = globalStats.createView(...); - -// register the view +// Counts/groups the lengths of lines read in. +const mLineLengths = globalStats.createMeasureInt64( + "demo/line_lengths", + MeasureUnit.BYTE, + "The distribution of line lengths" +); + +// Creates the method and status key +const methodKey = {name: "method"}; +const statusKey = {name: "status"}; + +// Creates the view +const view = globalStats.createView( + "demo/lines_in", + mLineLengths, + AggregationType.COUNT, + [methodKey, statusKey], + "The number of lines from standard input" +); + +// Registers the view globalStats.registerView(view); + +// Creates tags map -> key/value pair +const tagMap = new TagMap(); +tagMap.set(methodKey, {value: 'REPL'}); +tagMap.set(statusKey, {value: 'OK'}); + +// Creates measurements (measure + value) +const measurements = [{ + measure: mLineLengths, + value: 2 +}]; + +// Records measurement with tagMap +globalStats.record(measurements, tagMap); ``` ## 0.0.8 - 2018-12-14 diff --git a/packages/opencensus-core/src/exporters/console-exporter.ts b/packages/opencensus-core/src/exporters/console-exporter.ts index d01a31bc4..42825f406 100644 --- a/packages/opencensus-core/src/exporters/console-exporter.ts +++ b/packages/opencensus-core/src/exporters/console-exporter.ts @@ -16,6 +16,7 @@ import * as loggerTypes from '../common/types'; import {Measurement, View} from '../stats/types'; +import {TagKey, TagValue} from '../tags/types'; import * as modelTypes from '../trace/model/types'; import {ExporterBuffer} from './exporter-buffer'; @@ -93,7 +94,8 @@ export class ConsoleStatsExporter implements types.StatsEventListener { * @param view recorded view from measurement * @param measurement recorded measurement */ - onRecord(views: View[], measurement: Measurement) { + onRecord( + views: View[], measurement: Measurement, tags: Map) { 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 ff08dc74d..ef2543e4e 100644 --- a/packages/opencensus-core/src/exporters/types.ts +++ b/packages/opencensus-core/src/exporters/types.ts @@ -15,6 +15,7 @@ */ import {Measurement, View} from '../stats/types'; +import {TagKey, TagValue} from '../tags/types'; import * as configTypes from '../trace/config/types'; import * as modelTypes from '../trace/model/types'; @@ -44,7 +45,9 @@ export interface StatsEventListener { * @param views The views related to the measurement * @param measurement The recorded measurement */ - onRecord(views: View[], measurement: Measurement): void; + onRecord( + views: View[], measurement: Measurement, + tags: Map): void; /** * Starts the exporter that polls Metric from Metrics library and send diff --git a/packages/opencensus-core/src/stats/metric-utils.ts b/packages/opencensus-core/src/stats/metric-utils.ts index 69d2c4c6a..65e95243b 100644 --- a/packages/opencensus-core/src/stats/metric-utils.ts +++ b/packages/opencensus-core/src/stats/metric-utils.ts @@ -15,9 +15,10 @@ */ -import {LabelKey, LabelValue, MetricDescriptor, MetricDescriptorType} from '../metrics/export/types'; +import {LabelValue, MetricDescriptor, MetricDescriptorType} from '../metrics/export/types'; +import {TagValue} from '../tags/types'; -import {AggregationType, Measure, MeasureType, Tags, View} from './types'; +import {AggregationType, Measure, MeasureType, View} from './types'; /** Utils to convert Stats data models to Metric data models */ export class MetricUtils { @@ -61,25 +62,25 @@ export class MetricUtils { * @returns {MetricDescriptor} */ static viewToMetricDescriptor(view: View): MetricDescriptor { - // TODO(mayurkale): add description return { name: view.name, description: view.description, unit: view.measure.unit, type: MetricUtils.getType(view.measure, view.aggregation), labelKeys: view.getColumns().map( - tag => ({key: tag, description: ''} as LabelKey)) + // TODO(mayurkale): add description + tagKey => ({key: tagKey.name, description: ''})) }; } /** - * Converts tags to label values. - * @param tags + * Converts tag values to label values. + * @param tagValues * @returns {LabelValue[]} List of label values */ - static tagsToLabelValues(tags: Tags): LabelValue[] { - return Object.keys(tags).map(key => { - return {value: tags[key]} as LabelValue; + static tagValuesToLabelValues(tagValues: TagValue[]): LabelValue[] { + return tagValues.map((tagValue) => { + return {value: tagValue.value}; }); } } diff --git a/packages/opencensus-core/src/stats/recorder.ts b/packages/opencensus-core/src/stats/recorder.ts index fe03b2dcb..7fdb1edeb 100644 --- a/packages/opencensus-core/src/stats/recorder.ts +++ b/packages/opencensus-core/src/stats/recorder.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {TagKey, TagValue} from '../tags/types'; import {AggregationData, AggregationType, CountData, DistributionData, LastValueData, Measurement, MeasureType, SumData} from './types'; export class Recorder { @@ -40,6 +41,20 @@ export class Recorder { } } + /** Gets the tag values from tags and columns */ + static getTagValues(tags: Map, columns: TagKey[]): + TagValue[] { + const tagValues: TagValue[] = []; + + // ignore not found key values. + columns.forEach((tagKey) => { + if (tags.has(tagKey)) { + tagValues.push(tags.get(tagKey)); + } + }); + return tagValues; + } + private static addToDistribution( distributionData: DistributionData, value: number): DistributionData { distributionData.count += 1; diff --git a/packages/opencensus-core/src/stats/stats.ts b/packages/opencensus-core/src/stats/stats.ts index bd5d8b6d7..b72f7cedc 100644 --- a/packages/opencensus-core/src/stats/stats.ts +++ b/packages/opencensus-core/src/stats/stats.ts @@ -19,6 +19,8 @@ import * as loggerTypes from '../common/types'; import {StatsEventListener} from '../exporters/types'; import {Metric} from '../metrics/export/types'; import {Metrics} from '../metrics/metrics'; +import {TagMap} from '../tags/tag-map'; +import {TagKey} from '../tags/types'; import {MetricProducerForStats} from './metric-producer'; import {AggregationType, Measure, Measurement, MeasureType, MeasureUnit, Stats, View} from './types'; @@ -83,7 +85,7 @@ export class BaseStats implements Stats { */ createView( name: string, measure: Measure, aggregation: AggregationType, - tagKeys: string[], description: string, + tagKeys: TagKey[], description: string, bucketBoundaries?: number[]): View { const view = new BaseView( name, measure, aggregation, tagKeys, description, bucketBoundaries); @@ -156,14 +158,22 @@ 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. */ - record(...measurements: Measurement[]): void { + record(measurements: Measurement[], tags?: TagMap): void { if (this.hasNegativeValue(measurements)) { this.logger.warn(`Dropping measurments ${measurements}, value to record must be non-negative.`); return; } + if (!tags) { + // TODO(mayurkale): read tags current execution context + tags = new TagMap(); + } + for (const measurement of measurements) { const views = this.registeredViews[measurement.measure.name]; if (!views) { @@ -171,12 +181,12 @@ export class BaseStats implements Stats { } // Updates all views for (const view of views) { - view.recordMeasurement(measurement); + view.recordMeasurement(measurement, tags); } // Notifies all exporters for (const exporter of this.statsEventListeners) { - exporter.onRecord(views, measurement); + exporter.onRecord(views, measurement, tags.tags); } } } diff --git a/packages/opencensus-core/src/stats/types.ts b/packages/opencensus-core/src/stats/types.ts index 2a994fb35..eda7814c6 100644 --- a/packages/opencensus-core/src/stats/types.ts +++ b/packages/opencensus-core/src/stats/types.ts @@ -16,6 +16,8 @@ import {StatsEventListener} from '../exporters/types'; import {Metric} from '../metrics/export/types'; +import {TagMap} from '../tags/tag-map'; +import {TagKey, TagValue} from '../tags/types'; /** Main interface for stats. */ export interface Stats { @@ -31,7 +33,7 @@ export interface Stats { */ createView( name: string, measure: Measure, aggregation: AggregationType, - tagKeys: string[], description: string, + tagKeys: TagKey[], description: string, bucketBoundaries?: number[]): View; /** @@ -62,8 +64,11 @@ 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. */ - record(...measurements: Measurement[]): void; + record(measurements: Measurement[], tags?: TagMap): void; /** * Remove all registered Views and exporters from the stats. @@ -83,12 +88,6 @@ export interface Stats { registerExporter(exporter: StatsEventListener): void; } - -/** Tags are maps of names -> values */ -export interface Tags { - [key: string]: string; -} - /** * Describes the type of the individual values/measurements recorded by an * application. It includes information such as the type of measurement, the @@ -140,8 +139,6 @@ export interface Measurement { * up to Number.MAX_SAFE_INTERGER. */ readonly value: number; - /** The tags to which the value is applied */ - readonly tags: Tags; } /** @@ -176,15 +173,16 @@ 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 */ - recordMeasurement(measurement: Measurement): void; + recordMeasurement(measurement: Measurement, tags: TagMap): void; /** * Returns a snapshot of an AggregationData for that tags/labels values. - * @param tags The desired data's tags + * @param tagValues The desired data's tag values. */ - getSnapshot(tags: Tags): AggregationData; + getSnapshot(tagValues: TagValue[]): AggregationData; /** Gets the view's tag keys */ - getColumns(): string[]; + getColumns(): TagKey[]; /** Gets view`s metric */ getMetric(start: number): Metric; } @@ -204,8 +202,8 @@ export enum AggregationType { export interface AggregationMetadata { /** The aggregation type of the aggregation data */ readonly type: AggregationType; - /** The tags/labels that this AggregationData collects and aggregates */ - readonly tags: Tags; + /** The tagValues that this AggregationData collects and aggregates */ + readonly tagValues: TagValue[]; /** The latest timestamp a new data point was recorded */ timestamp: number; } diff --git a/packages/opencensus-core/src/stats/view.ts b/packages/opencensus-core/src/stats/view.ts index 60696f202..8cf62e2fe 100644 --- a/packages/opencensus-core/src/stats/view.ts +++ b/packages/opencensus-core/src/stats/view.ts @@ -18,17 +18,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 {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, Tags, View} from './types'; +import {AggregationData, AggregationType, Measure, Measurement, View} from './types'; const RECORD_SEPARATOR = String.fromCharCode(30); -const UNIT_SEPARATOR = String.fromCharCode(31); - -// String that has only printable characters -const invalidString = /[^\u0020-\u007e]/; export class BaseView implements View { /** @@ -46,11 +44,11 @@ export class BaseView implements View { * If no Tags are provided, then, all data is recorded in a single * aggregation. */ - private rows: {[key: string]: AggregationData} = {}; + private tagValueAggregationMap: {[key: string]: AggregationData} = {}; /** * A list of tag keys that represents the possible column labels */ - private columns: string[]; + private columns: TagKey[]; /** * An Aggregation describes how data collected is aggregated. * There are four aggregation types: count, sum, lastValue and distirbution. @@ -74,10 +72,6 @@ export class BaseView implements View { /** An object to log information to */ // @ts-ignore private logger: loggerTypes.Logger; - /** - * Max Length of a TagKey - */ - private readonly MAX_LENGTH: number = 256; /** * Creates a new View instance. This constructor is used by Stats. User should * prefer using Stats.createView() instead. @@ -92,7 +86,7 @@ export class BaseView implements View { */ constructor( name: string, measure: Measure, aggregation: AggregationType, - tagsKeys: string[], description: string, bucketBoundaries?: number[], + tagsKeys: TagKey[], description: string, bucketBoundaries?: number[], logger = defaultLogger) { if (aggregation === AggregationType.DISTRIBUTION && !bucketBoundaries) { throw new Error('No bucketBoundaries specified'); @@ -101,7 +95,7 @@ export class BaseView implements View { this.name = name; this.description = description; this.measure = measure; - this.columns = tagsKeys; + this.columns = this.validateTagKeys(tagsKeys); this.aggregation = aggregation; this.startTime = Date.now(); this.bucketBoundaries = new BucketBoundaries(bucketBoundaries); @@ -109,7 +103,7 @@ export class BaseView implements View { } /** Gets the view's tag keys */ - getColumns(): string[] { + getColumns(): TagKey[] { return this.columns; } @@ -119,80 +113,37 @@ 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 */ - recordMeasurement(measurement: Measurement) { - // Checks if measurement has valid tags - if (this.invalidTags(measurement.tags)) { - return; - } - - // Checks if measurement has all tags in views - for (const tagKey of this.columns) { - if (!Object.keys(measurement.tags).some((key) => key === tagKey)) { - return; - } - } + recordMeasurement(measurement: Measurement, tags: TagMap) { + const tagValues = Recorder.getTagValues(tags.tags, this.columns); + const encodedTags = this.encodeTagValues(tagValues); - const encodedTags = this.encodeTags(measurement.tags); - if (!this.rows[encodedTags]) { - this.rows[encodedTags] = this.createAggregationData(measurement.tags); + if (!this.tagValueAggregationMap[encodedTags]) { + this.tagValueAggregationMap[encodedTags] = + this.createAggregationData(tagValues); } - Recorder.addMeasurement(this.rows[encodedTags], measurement); + Recorder.addMeasurement( + this.tagValueAggregationMap[encodedTags], measurement); } /** - * Encodes a Tags object into a key sorted string. - * @param tags The tags to encode + * Encodes a TagValue object into a value sorted string. + * @param tagValues The tagValues to encode */ - private encodeTags(tags: Tags): string { - return Object.keys(tags) + private encodeTagValues(tagValues: TagValue[]): string { + return tagValues.map(tagValue => tagValue.value) .sort() - .map(tagKey => { - return tagKey + UNIT_SEPARATOR + tags[tagKey]; - }) .join(RECORD_SEPARATOR); } - /** - * Checks if tag keys and values are valid. - * @param tags The tags to be checked - */ - private invalidTags(tags: Tags): boolean { - const result: boolean = - this.invalidPrintableCharacters(tags) || this.invalidLength(tags); - if (result) { - this.logger.warn( - 'Unable to create tagkey/tagvalue with the specified tags.'); - } - return result; - } - - /** - * Checks if tag keys and values have only printable characters. - * @param tags The tags to be checked - */ - private invalidPrintableCharacters(tags: Tags): boolean { - return Object.keys(tags).some(tagKey => { - return invalidString.test(tagKey) || invalidString.test(tags[tagKey]); - }); - } - - /** - * Checks if length of tagkey is greater than 0 & less than 256. - * @param tags The tags to be checked - */ - private invalidLength(tags: Tags): boolean { - return Object.keys(tags).some(tagKey => { - return tagKey.length <= 0 || tagKey.length >= this.MAX_LENGTH; - }); - } /** * Creates an empty aggregation data for a given tags. - * @param tags The tags for that aggregation data + * @param tagValues The tags for that aggregation data */ - private createAggregationData(tags: Tags): AggregationData { - const aggregationMetadata = {tags, timestamp: Date.now()}; + 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); @@ -247,10 +198,11 @@ export class BaseView implements View { const timeseries: TimeSeries[] = []; - Object.keys(this.rows).forEach(key => { - const {tags} = this.rows[key]; - const labelValues: LabelValue[] = MetricUtils.tagsToLabelValues(tags); - const point: Point = this.toPoint(now, this.getSnapshot(tags)); + Object.keys(this.tagValueAggregationMap).forEach(key => { + const {tagValues} = this.tagValueAggregationMap[key]; + const labelValues: LabelValue[] = + MetricUtils.tagValuesToLabelValues(tagValues); + const point: Point = this.toPoint(now, this.getSnapshot(tagValues)); if (startTimestamp) { timeseries.push({startTimestamp, labelValues, points: [point]}); @@ -294,7 +246,22 @@ export class BaseView implements View { * @param tags The desired data's tags * @returns {AggregationData} */ - getSnapshot(tags: Tags): AggregationData { - return this.rows[this.encodeTags(tags)]; + getSnapshot(tagValues: TagValue[]): AggregationData { + return this.tagValueAggregationMap[this.encodeTagValues(tagValues)]; + } + + /** Determines whether the given TagKeys are valid. */ + private validateTagKeys(tagKeys: TagKey[]): TagKey[] { + const tagKeysCopy = Object.assign([], tagKeys); + tagKeysCopy.forEach((tagKey) => { + if (!isValidTagKey(tagKey)) { + throw new Error(`Invalid TagKey name: ${tagKey}`); + } + }); + const tagKeysSet = new Set(tagKeysCopy.map(tagKey => tagKey.name)); + if (tagKeysSet.size !== tagKeysCopy.length) { + throw new Error('Columns have duplicate'); + } + return tagKeysCopy; } } diff --git a/packages/opencensus-core/src/tags/tag-map.ts b/packages/opencensus-core/src/tags/tag-map.ts index bd0c0eef3..932ed517a 100644 --- a/packages/opencensus-core/src/tags/tag-map.ts +++ b/packages/opencensus-core/src/tags/tag-map.ts @@ -25,11 +25,11 @@ export class TagMap { /** Adds the key/value pair regardless of whether the key is present. */ set(tagKey: TagKey, tagValue: TagValue): void { if (!isValidTagKey(tagKey)) { - throw Error(`Invalid TagKey name: ${tagKey.name}`); + throw new Error(`Invalid TagKey name: ${tagKey.name}`); } if (!isValidTagValue(tagValue)) { - throw Error(`Invalid TagValue: ${tagValue.value}`); + throw new Error(`Invalid TagValue: ${tagValue.value}`); } this.registeredTags.set(tagKey, tagValue); } diff --git a/packages/opencensus-core/src/tags/validation.ts b/packages/opencensus-core/src/tags/validation.ts index 408113cf6..1e4ca782c 100644 --- a/packages/opencensus-core/src/tags/validation.ts +++ b/packages/opencensus-core/src/tags/validation.ts @@ -21,7 +21,7 @@ const TAG_KEY_MAX_LENGTH = 255; /** Determines whether the given String is a valid tag key. */ export function isValidTagKey(tagKey: TagKey): boolean { - if (!tagKey) { + if (!tagKey || !tagKey.name) { return false; } return isPrintableString(tagKey.name) && tagKey.name.length > 0 && @@ -30,7 +30,7 @@ export function isValidTagKey(tagKey: TagKey): boolean { /** Determines whether the given String is a valid tag value. */ export function isValidTagValue(tagValue: TagValue): boolean { - if (!tagValue) { + if (!tagValue || tagValue.value === null || tagValue.value === undefined) { return false; } return isPrintableString(tagValue.value) && diff --git a/packages/opencensus-core/test/test-metric-producer.ts b/packages/opencensus-core/test/test-metric-producer.ts index 27c9d56c6..a0f1fe4e4 100644 --- a/packages/opencensus-core/test/test-metric-producer.ts +++ b/packages/opencensus-core/test/test-metric-producer.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; -import {AggregationType, globalStats, Measurement, MeasureUnit, Tags, View} from '../src'; +import {AggregationType, globalStats, Measurement, MeasureUnit, TagMap, View} from '../src'; import {LabelKey, LabelValue, MetricDescriptorType} from '../src/metrics/export/types'; import {MetricProducerForStats} from '../src/stats/metric-producer'; @@ -31,15 +31,18 @@ describe('Metric producer for stats', () => { const measureDouble = globalStats.createMeasureDouble( 'opencensus.io/test/double', MeasureUnit.UNIT, 'Measure Double'); - const tags: Tags = {testKey1: 'testValue1', testKey2: 'testValue2'}; + const tagKeys = [{name: 'testKey1'}, {name: 'testKey2'}]; + const tagMap = new TagMap(); + tagMap.set(tagKeys[0], {value: 'testValue1'}); + tagMap.set(tagKeys[1], {value: 'testValue2'}); const labelKeys: LabelKey[] = [ {'key': 'testKey1', 'description': ''}, {'key': 'testKey2', 'description': ''} ]; const labelValues: LabelValue[] = [{'value': 'testValue1'}, {'value': 'testValue2'}]; - const measurement1: Measurement = {measure: measureDouble, value: 25, tags}; - const measurement2: Measurement = {measure: measureDouble, value: 300, tags}; + const measurement1: Measurement = {measure: measureDouble, value: 25}; + const measurement2: Measurement = {measure: measureDouble, value: 300}; // expected constants const expectedMetricDescriptor1 = { @@ -73,10 +76,9 @@ describe('Metric producer for stats', () => { it('should add sum stats', () => { const view: View = globalStats.createView( - viewName1, measureDouble, AggregationType.SUM, Object.keys(tags), - description); + viewName1, measureDouble, AggregationType.SUM, tagKeys, description); globalStats.registerView(view); - view.recordMeasurement(measurement1); + view.recordMeasurement(measurement1, tagMap); const metrics = metricProducerForStats.getMetrics(); @@ -94,10 +96,10 @@ describe('Metric producer for stats', () => { it('should add count stats', () => { const view: View = globalStats.createView( - viewName2, measureDouble, AggregationType.COUNT, Object.keys(tags), + viewName2, measureDouble, AggregationType.COUNT, tagKeys, description); globalStats.registerView(view); - view.recordMeasurement(measurement1); + view.recordMeasurement(measurement1, tagMap); let metrics = metricProducerForStats.getMetrics(); @@ -118,18 +120,18 @@ describe('Metric producer for stats', () => { assert.equal(actualTimeSeries2[0].points[0].value, 1); // update count view - view.recordMeasurement(measurement2); + view.recordMeasurement(measurement2, tagMap); metrics = metricProducerForStats.getMetrics(); assert.deepStrictEqual(metrics[1].timeseries[0].points[0].value, 2); }); it('should add lastValue stats', () => { const view: View = globalStats.createView( - viewName3, measureDouble, AggregationType.LAST_VALUE, Object.keys(tags), + viewName3, measureDouble, AggregationType.LAST_VALUE, tagKeys, description); globalStats.registerView(view); - view.recordMeasurement(measurement1); - view.recordMeasurement(measurement2); + view.recordMeasurement(measurement1, tagMap); + view.recordMeasurement(measurement2, tagMap); const metrics = metricProducerForStats.getMetrics(); @@ -155,14 +157,13 @@ describe('Metric producer for stats', () => { it('should add distribution stats', () => { const measurementValues = [1.1, 2.3, 3.2, 4.3, 5.2]; const buckets = [2, 4, 6]; - const view: View = globalStats.createView( - viewName3, measureDouble, AggregationType.DISTRIBUTION, - Object.keys(tags), description, buckets); + viewName3, measureDouble, AggregationType.DISTRIBUTION, tagKeys, + description, buckets); globalStats.registerView(view); for (const value of measurementValues) { - const measurement: Measurement = {measure: measureDouble, value, tags}; - view.recordMeasurement(measurement); + const measurement: Measurement = {measure: measureDouble, value}; + view.recordMeasurement(measurement, tagMap); } const metrics = metricProducerForStats.getMetrics(); diff --git a/packages/opencensus-core/test/test-metric-utils.ts b/packages/opencensus-core/test/test-metric-utils.ts index b2f47517e..4b26e36a8 100644 --- a/packages/opencensus-core/test/test-metric-utils.ts +++ b/packages/opencensus-core/test/test-metric-utils.ts @@ -18,7 +18,7 @@ import * as assert from 'assert'; import {BaseView, MetricUtils} from '../src'; import {MetricDescriptorType} from '../src/metrics/export/types'; -import {AggregationType, Measure, MeasureType, MeasureUnit, Tags} from '../src/stats/types'; +import {AggregationType, Measure, MeasureType, MeasureUnit} from '../src/stats/types'; describe('MetricUtil', () => { @@ -29,7 +29,7 @@ describe('MetricUtil', () => { type: MeasureType.DOUBLE, unit: MeasureUnit.UNIT }; - const tagKeys = ['testKey1', 'testKey2']; + const tagKeys = [{name: 'testKey1'}, {name: 'testKey2'}]; const view = new BaseView( 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, VIEW_DESCRIPTION); @@ -47,9 +47,10 @@ describe('MetricUtil', () => { }); it('should convert tag values to label values', () => { - const tags: Tags = {test: 'test1', tag: 'test2', empty: '', fake: null}; + const tags = + [{value: 'value1'}, {value: 'value2'}, {value: ''}, {value: null}]; assert.deepStrictEqual( - MetricUtils.tagsToLabelValues(tags), - [{value: 'test1'}, {value: 'test2'}, {value: ''}, {value: null}]); + MetricUtils.tagValuesToLabelValues(tags), + [{value: 'value1'}, {value: 'value2'}, {value: ''}, {value: null}]); }); }); diff --git a/packages/opencensus-core/test/test-recorder.ts b/packages/opencensus-core/test/test-recorder.ts index 92ba0137a..9d6338add 100644 --- a/packages/opencensus-core/test/test-recorder.ts +++ b/packages/opencensus-core/test/test-recorder.ts @@ -15,8 +15,8 @@ */ import * as assert from 'assert'; -import {Recorder} from '../src'; -import {AggregationType, CountData, DistributionData, LastValueData, Measure, Measurement, MeasureType, MeasureUnit, SumData, Tags} from '../src/stats/types'; +import {Recorder, TagMap} from '../src'; +import {AggregationType, CountData, DistributionData, LastValueData, Measure, Measurement, MeasureType, MeasureUnit, SumData} from '../src/stats/types'; /** The order of how close values must be to be considerated almost equal */ const EPSILON = 6; @@ -59,7 +59,7 @@ describe('Recorder', () => { {name: 'Test Measure 1', type: MeasureType.DOUBLE, unit: MeasureUnit.UNIT}, {name: 'Test Measure 2', type: MeasureType.INT64, unit: MeasureUnit.UNIT} ]; - const tags: Tags = {testKey: 'testValue'}; + const tagValues = [{value: 'testValue'}]; const testCases: RecorderTestCase[] = [ {values: [1.1, 2.5, 3.2, 4.7, 5.2], description: 'with positive values'}, { values: [-1.5, -2.3, -3.7, -4.3, -5.9], @@ -76,14 +76,14 @@ describe('Recorder', () => { () => { const countData: CountData = { type: AggregationType.COUNT, - tags, + tagValues, timestamp: Date.now(), value: 0 }; let count = 0; for (const value of testCase.values) { count++; - const measurement: Measurement = {measure, tags, value}; + const measurement: Measurement = {measure, value}; const updatedAggregationData = Recorder.addMeasurement(countData, measurement) as CountData; @@ -100,12 +100,12 @@ describe('Recorder', () => { () => { const lastValueData: LastValueData = { type: AggregationType.LAST_VALUE, - tags, + tagValues, timestamp: Date.now(), value: undefined }; for (const value of testCase.values) { - const measurement: Measurement = {measure, tags, value}; + const measurement: Measurement = {measure, value}; const lastValue = measure.type === MeasureType.DOUBLE ? value : Math.trunc(value); @@ -125,7 +125,7 @@ describe('Recorder', () => { () => { const sumData: SumData = { type: AggregationType.SUM, - tags, + tagValues, timestamp: Date.now(), value: 0 }; @@ -133,7 +133,7 @@ describe('Recorder', () => { for (const value of testCase.values) { acc += measure.type === MeasureType.DOUBLE ? value : Math.trunc(value); - const measurement: Measurement = {measure, tags, value}; + const measurement: Measurement = {measure, value}; const updatedAggregationData = Recorder.addMeasurement(sumData, measurement) as SumData; @@ -150,7 +150,7 @@ describe('Recorder', () => { () => { const distributionData: DistributionData = { type: AggregationType.DISTRIBUTION, - tags, + tagValues, timestamp: Date.now(), startTime: Date.now(), count: 0, @@ -166,7 +166,7 @@ describe('Recorder', () => { sentValues.push( measure.type === MeasureType.DOUBLE ? value : Math.trunc(value)); - const measurement: Measurement = {measure, tags, value}; + const measurement: Measurement = {measure, value}; const updatedAggregationData = Recorder.addMeasurement(distributionData, measurement) as DistributionData; @@ -175,5 +175,49 @@ describe('Recorder', () => { }); } }); + + describe('getTagValues()', () => { + const CALLER = {name: 'caller'}; + const METHOD = {name: 'method'}; + const ORIGINATOR = {name: 'originator'}; + const CALLER_V = {value: 'some caller'}; + const METHOD_V = {value: 'some method'}; + const ORIGINATOR_V = {value: 'some originator'}; + let tagMap: TagMap; + + beforeEach(() => { + tagMap = new TagMap(); + }); + + it('should return tag values from tags and columns', () => { + const columns = [CALLER, METHOD]; + tagMap.set(CALLER, CALLER_V); + tagMap.set(METHOD, METHOD_V); + const tagValues = Recorder.getTagValues(tagMap.tags, columns); + assert.equal(tagValues.length, 2); + assert.deepStrictEqual(tagValues, [CALLER_V, METHOD_V]); + }); + + it('should return tag values from tags and columns with extra keys', + () => { + const columns = [CALLER, METHOD, ORIGINATOR]; + tagMap.set(CALLER, CALLER_V); + tagMap.set(METHOD, METHOD_V); + const tagValues = Recorder.getTagValues(tagMap.tags, columns); + assert.equal(tagValues.length, 2); + assert.deepStrictEqual(tagValues, [CALLER_V, METHOD_V]); + }); + + it('should return tag values from tags and columns with extra tags', + () => { + const columns = [CALLER, METHOD]; + tagMap.set(CALLER, CALLER_V); + tagMap.set(METHOD, METHOD_V); + tagMap.set(ORIGINATOR, ORIGINATOR_V); + const tagValues = Recorder.getTagValues(tagMap.tags, columns); + assert.equal(tagValues.length, 2); + assert.deepStrictEqual(tagValues, [CALLER_V, METHOD_V]); + }); + }); } }); diff --git a/packages/opencensus-core/test/test-stats.ts b/packages/opencensus-core/test/test-stats.ts index d9c563d56..433c82d43 100644 --- a/packages/opencensus-core/test/test-stats.ts +++ b/packages/opencensus-core/test/test-stats.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert'; -import {BaseView, globalStats, StatsEventListener} from '../src'; +import {BaseView, globalStats, StatsEventListener, TagKey, TagMap, TagValue} from '../src'; import {AggregationType, LastValueData, Measure, Measurement, MeasureType, MeasureUnit, View} from '../src/stats/types'; class TestExporter implements StatsEventListener { @@ -26,7 +26,8 @@ class TestExporter implements StatsEventListener { this.registeredViews.push(view); } - onRecord(views: View[], measurement: Measurement) { + onRecord( + views: View[], measurement: Measurement, tagMap: Map) { this.recordedMeasurements.push(measurement); } @@ -46,8 +47,12 @@ describe('Stats', () => { }); const viewName = 'testViewName'; - const tags = {tagKey1: 'tagValue1', tagKey2: 'tagValue2'}; - const tagKeys = Object.keys(tags); + const tagKeys = [{name: 'testKey1'}, {name: 'testKey2'}]; + const tagValues = [{value: 'testValue1'}, {value: 'testValue2'}]; + const tagMap = new TagMap(); + tagMap.set(tagKeys[0], tagValues[0]); + tagMap.set(tagKeys[1], tagValues[1]); + const measureName = 'testMeasureDouble'; const measureUnit = MeasureUnit.UNIT; const description = 'test description'; @@ -159,43 +164,49 @@ describe('Stats', () => { }); it('should record a single measurement', () => { - const measurement = {measure, tags, value: 1}; + const measurement = {measure, value: 1}; assert.strictEqual(testExporter.recordedMeasurements.length, 0); - globalStats.record(measurement); + globalStats.record([measurement], tagMap); assert.strictEqual(testExporter.recordedMeasurements.length, 1); assert.deepEqual(testExporter.recordedMeasurements[0], measurement); - aggregationData = - testExporter.registeredViews[0].getSnapshot(tags) as LastValueData; + aggregationData = testExporter.registeredViews[0].getSnapshot( + tagValues) as LastValueData; assert.strictEqual(aggregationData.value, measurement.value); }); it('should not record a single negative measurement', () => { globalStats.registerExporter(testExporter); - const measurement = {measure, tags, value: -1}; - globalStats.record(measurement); + const measurement = {measure, value: -1}; + globalStats.record([measurement], tagMap); assert.strictEqual(testExporter.recordedMeasurements.length, 0); }); + it('should record when tagMap is not passed', () => { + globalStats.registerExporter(testExporter); + const measurement = {measure, value: 10}; + globalStats.record([measurement]); + console.log(JSON.stringify(testExporter.recordedMeasurements)); + assert.strictEqual(testExporter.recordedMeasurements.length, 2); + }); + it('should record multiple measurements', () => { - const measurement1 = {measure, tags, value: 1}; - const measurement2 = {measure, tags, value: 1}; + const measurement1 = {measure, value: 1}; + const measurement2 = {measure, value: 1}; assert.strictEqual(testExporter.recordedMeasurements.length, 0); - globalStats.record(measurement1, measurement2); + globalStats.record([measurement1, measurement2], tagMap); assert.strictEqual(testExporter.recordedMeasurements.length, 2); assert.deepEqual(testExporter.recordedMeasurements[0], measurement1); assert.deepEqual(testExporter.recordedMeasurements[1], measurement2); - aggregationData = - testExporter.registeredViews[0].getSnapshot(tags) as LastValueData; + aggregationData = testExporter.registeredViews[0].getSnapshot( + tagValues) as LastValueData; assert.strictEqual(aggregationData.value, measurement2.value); }); it('should skip whole multiple measurment if one of value is negative', () => { - const measurments = [ - {measure, tags, value: 1}, {measure, tags, value: -1}, - {measure, tags, value: 1} - ]; - globalStats.record(...measurments); + const measurments = + [{measure, value: 1}, {measure, value: -1}, {measure, value: 1}]; + globalStats.record(measurments, tagMap); assert.equal(testExporter.recordedMeasurements.length, 0); }); }); diff --git a/packages/opencensus-core/test/test-view.ts b/packages/opencensus-core/test/test-view.ts index 7d7638d12..3ebb1181a 100644 --- a/packages/opencensus-core/test/test-view.ts +++ b/packages/opencensus-core/test/test-view.ts @@ -16,10 +16,10 @@ import * as assert from 'assert'; -import {BaseView} from '../src'; +import {BaseView, TagMap, TagValue} from '../src'; import {TEST_ONLY} from '../src/common/time-util'; import {DistributionValue, MetricDescriptorType, Timestamp} from '../src/metrics/export/types'; -import {AggregationType, DistributionData, Measure, Measurement, MeasureType, MeasureUnit, Tags, View} from '../src/stats/types'; +import {AggregationType, DistributionData, Measure, MeasureType, MeasureUnit, View} from '../src/stats/types'; /** The order of how close values must be to be considerated almost equal */ const EPSILON = 6; @@ -59,10 +59,10 @@ function assertDistributionData( } function assertView( - view: View, measurement: Measurement, recordedValues: number[], - aggregationType: AggregationType) { + view: View, recordedValues: number[], aggregationType: AggregationType, + tagValues: TagValue[]) { assert.strictEqual(view.aggregation, aggregationType); - const aggregationData = view.getSnapshot(measurement.tags); + const aggregationData = view.getSnapshot(tagValues); switch (aggregationData.type) { case AggregationType.SUM: const acc = recordedValues.reduce((acc, cur) => acc + cur); @@ -113,19 +113,62 @@ describe('BaseView', () => { describe('getColumns()', () => { it('should access the given tag keys', () => { - const tagKeys = ['testKey1', 'testKey2']; + const tagKeys = [{name: 'testKey1'}, {name: 'testKey2'}]; const view = new BaseView( 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, 'description test'); - assert.strictEqual(view.getColumns(), tagKeys); + assert.deepStrictEqual(view.getColumns(), tagKeys); + }); + }); + + describe('createView()', () => { + it('should throw error when keys are duplicate', () => { + const tagKeys = + [{name: 'testKey1'}, {name: 'testKey1'}, {name: 'testKey2'}]; + assert.throws(() => { + const view = new BaseView( + 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, + 'description test'); + assert.deepStrictEqual(view.getColumns(), tagKeys); + }, /^Error: Columns have duplicate$/); + }); + + it('should throw error when keys are undefined', () => { + const tagKeys = [{name: 'testKey1'}, undefined, {name: 'testKey2'}]; + assert.throws(() => { + const view = new BaseView( + 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, + 'description test'); + assert.deepStrictEqual(view.getColumns(), tagKeys); + }, /^Error: Invalid TagKey name: undefined$/); + }); + + it('should throw error when keys contains null', () => { + const tagKeys = [{name: 'testKey1'}, null, {name: 'testKey2'}]; + assert.throws(() => { + const view = new BaseView( + 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, + 'description test'); + assert.deepStrictEqual(view.getColumns(), tagKeys); + }, /^Error: Invalid TagKey name: null$/); }); }); describe('recordMeasurement()', () => { const measurementValues = [1.1, 2.3, 3.2, 4.3, 5.2]; const bucketBoundaries = [2, 4, 6]; - const tags: Tags = {testKey1: 'testValue', testKey2: 'testValue'}; + const tagKey1 = {name: 'testKey1'}; + const tagKey2 = {name: 'testKey2'}; + const tagKeys = [tagKey1, tagKey2]; + const tagKey3 = {name: 'testKey3'}; + const tagValue1 = {value: 'testValue1'}; + const tagValue2 = {value: 'testValue2'}; + const tagValue3 = {value: 'testValue3'}; + + const tags = new TagMap(); + tags.set(tagKey1, tagValue1); + tags.set(tagKey2, tagValue2); for (const aggregationTestCase of aggregationTestCases) { it(`should record measurements on a View with ${ @@ -133,15 +176,15 @@ describe('BaseView', () => { () => { const view = new BaseView( 'test/view/name', measure, aggregationTestCase.aggregationType, - ['testKey1', 'testKey2'], 'description test', bucketBoundaries); + tagKeys, 'description test', bucketBoundaries); const recordedValues = []; for (const value of measurementValues) { recordedValues.push(value); - const measurement = {measure, tags, value}; - view.recordMeasurement(measurement); + const measurement = {measure, value}; + view.recordMeasurement(measurement, tags); assertView( - view, measurement, recordedValues, - aggregationTestCase.aggregationType); + view, recordedValues, aggregationTestCase.aggregationType, + [tagValue1, tagValue2]); } }); } @@ -149,74 +192,30 @@ describe('BaseView', () => { it('should ignore negative bucket bounds', () => { const negativeBucketBoundaries = [-Infinity, -4, -2, 0, 2, 4, 6]; const view = new BaseView( - 'test/view/name', measure, AggregationType.DISTRIBUTION, - ['testKey1', 'testKey2'], 'description test', - negativeBucketBoundaries); + 'test/view/name', measure, AggregationType.DISTRIBUTION, tagKeys, + 'description test', negativeBucketBoundaries); const recordedValues = []; for (const value of measurementValues) { recordedValues.push(value); - const measurement = {measure, tags, value}; - view.recordMeasurement(measurement); + const measurement = {measure, value}; + view.recordMeasurement(measurement, tags); } - const data = view.getSnapshot(tags) as DistributionData; + const data = view.getSnapshot([tagValue1, tagValue2]) as DistributionData; assert.deepStrictEqual(data.buckets, [2, 4, 6]); assert.deepStrictEqual(data.bucketCounts, [1, 2, 2, 0]); }); const view = new BaseView( - 'test/view/name', measure, AggregationType.LAST_VALUE, - ['testKey1', 'testKey2'], 'description test'); + 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, + 'description test'); it('should not record a measurement when it have wrong tag keys', () => { - const measurement = {measure, tags: {testKey3: 'testValue'}, value: 10}; - view.recordMeasurement(measurement); - assert.ok(!view.getSnapshot(measurement.tags)); - }); - - it('should not record a measurement when tags are not valid', () => { - const measurement = { - measure, - tags: {testKey3: String.fromCharCode(30) + 'testValue'}, - value: 10 - }; - view.recordMeasurement(measurement); - assert.ok(!view.getSnapshot(measurement.tags)); + const tagMap = new TagMap(); + tagMap.set(tagKey3, tagValue3); + const measurement = {measure, value: 10}; + view.recordMeasurement(measurement, tagMap); + assert.ok(!view.getSnapshot([tagValue3])); }); - - it('should not record a measurement when it have not enough tag keys', - () => { - const measurement = { - measure, - tags: {testKey1: 'testValue'}, - value: 10 - }; - view.recordMeasurement(measurement); - assert.ok(!view.getSnapshot(measurement.tags)); - }); - - it('should not record a measurement when a tag key is longer than 255 characters', - () => { - const tagkey = 'a'.repeat(256); - const measurement = { - measure, - tags: {[tagkey]: 'testValue'}, - value: 10 - }; - view.recordMeasurement(measurement); - assert.ok(!view.getSnapshot(measurement.tags)); - }); - - it('should not record a measurement when tag key is 0 character long', - () => { - const tagkey = ''; - const measurement = { - measure, - tags: {[tagkey]: 'testValue'}, - value: 10 - }; - view.recordMeasurement(measurement); - assert.ok(!view.getSnapshot(measurement.tags)); - }); }); describe('getMetric()', () => { @@ -227,16 +226,29 @@ describe('BaseView', () => { const mockStartTimestamp: Timestamp = {seconds: 1546540757, nanos: 282e6}; const measurementValues = [1.1, 2.3, 3.2, 4.3, 5.2]; const buckets = [2, 4, 6]; - const tags: Tags = {testKey1: 'testValue', testKey2: 'testValue'}; - const tags1: Tags = {testKey1: 'testValue1', testKey2: 'testValue1'}; + const tagKey1 = {name: 'testKey1'}; + const tagKey2 = {name: 'testKey2'}; + const tagKeys = [tagKey1, tagKey2]; + const tagValue1 = {value: 'testValue1'}; + const tagValue2 = {value: 'testValue2'}; + const tagValue3 = {value: 'testValue3'}; + const tagValue4 = {value: 'testValue4'}; + + const tags = new TagMap(); + tags.set(tagKey1, tagValue1); + tags.set(tagKey2, tagValue2); + + const tagMap = new TagMap(); + tagMap.set(tagKey1, tagValue3); + tagMap.set(tagKey2, tagValue4); for (const aggregationTestCase of aggregationTestCases) { const view: View = new BaseView( 'test/view/name', measure, aggregationTestCase.aggregationType, - ['testKey1', 'testKey2'], 'description test', buckets); + tagKeys, 'description test', buckets); for (const value of measurementValues) { - const measurement = {measure, tags, value}; - view.recordMeasurement(measurement); + const measurement = {measure, value}; + view.recordMeasurement(measurement, tags); } beforeEach(() => { @@ -296,20 +308,20 @@ describe('BaseView', () => { it('should have labelValues', () => { assert.ok(labelValues); assert.deepStrictEqual( - labelValues, [{value: 'testValue'}, {value: 'testValue'}]); + labelValues, [{value: 'testValue1'}, {value: 'testValue2'}]); }); }); } describe('DISTRIBUTION aggregation type', () => { const view: View = new BaseView( - 'test/view/name', measure, AggregationType.DISTRIBUTION, - ['testKey1', 'testKey2'], 'description test', buckets); + 'test/view/name', measure, AggregationType.DISTRIBUTION, tagKeys, + 'description test', buckets); let total = 0; for (const value of measurementValues) { total += value; - const measurement = {measure, tags, value}; - view.recordMeasurement(measurement); + const measurement = {measure, value}; + view.recordMeasurement(measurement, tags); } it('should have point', () => { @@ -340,15 +352,15 @@ describe('BaseView', () => { 'DISTRIBUTION aggregation type: record with measurements in succession from a single view and single measure', () => { const view: View = new BaseView( - 'test/view/name', measure, AggregationType.DISTRIBUTION, - ['testKey1', 'testKey2'], 'description test', buckets); + 'test/view/name', measure, AggregationType.DISTRIBUTION, tagKeys, + 'description test', buckets); let total = 0; for (const value of measurementValues) { total += value; - const measurement = {measure, tags, value}; - const measurement1 = {measure, tags: tags1, value}; - view.recordMeasurement(measurement); - view.recordMeasurement(measurement1); + const measurement = {measure, value}; + const measurement1 = {measure, value}; + view.recordMeasurement(measurement, tags); + view.recordMeasurement(measurement1, tagMap); } it('should have points', () => { @@ -376,8 +388,8 @@ describe('BaseView', () => { sumOfSquaredDeviation: 10.427999999999997 }); assert.deepEqual( - labelValues1, [{'value': 'testValue'}, {'value': 'testValue'}]); - + labelValues1, + [{'value': 'testValue1'}, {'value': 'testValue2'}]); assert.ok(points2); [point] = points2; ({timestamp, value} = point); @@ -396,17 +408,17 @@ describe('BaseView', () => { }); assert.deepEqual( labelValues2, - [{'value': 'testValue1'}, {'value': 'testValue1'}]); + [{'value': 'testValue3'}, {'value': 'testValue4'}]); }); }); describe('COUNT aggregation type', () => { const view: View = new BaseView( - 'test/view/name', measure, AggregationType.COUNT, - ['testKey1', 'testKey2'], 'description test', buckets); + 'test/view/name', measure, AggregationType.COUNT, tagKeys, + 'description test', buckets); for (const value of measurementValues) { - const measurement = {measure, tags, value}; - view.recordMeasurement(measurement); + const measurement = {measure, value}; + view.recordMeasurement(measurement, tags); } it('should have point', () => { @@ -428,13 +440,13 @@ describe('BaseView', () => { describe('SUM aggregation type', () => { const view: View = new BaseView( - 'test/view/name', measure, AggregationType.SUM, - ['testKey1', 'testKey2'], 'description test', buckets); + 'test/view/name', measure, AggregationType.SUM, tagKeys, + 'description test', buckets); let total = 0; for (const value of measurementValues) { total += value; - const measurement = {measure, tags, value}; - view.recordMeasurement(measurement); + const measurement = {measure, value}; + view.recordMeasurement(measurement, tags); } it('should have point', () => { @@ -456,11 +468,11 @@ describe('BaseView', () => { describe('LAST_VALUE aggregation type', () => { const view: View = new BaseView( - 'test/view/name', measure, AggregationType.LAST_VALUE, - ['testKey1', 'testKey2'], 'description test', buckets); + 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, + 'description test', buckets); for (const value of measurementValues) { - const measurement = {measure, tags, value}; - view.recordMeasurement(measurement); + const measurement = {measure, value}; + view.recordMeasurement(measurement, tags); } it('should have point', () => { @@ -480,30 +492,38 @@ describe('BaseView', () => { }); describe('getSnapshots()', () => { - const tags: Tags = {testKey1: 'testValue', testKey2: 'testValue'}; let view: View; + const tagKey1 = {name: 'testKey1'}; + const tagKey2 = {name: 'testKey2'}; + const tagKeys = [tagKey1, tagKey2]; + const tagValue1 = {value: 'testValue1'}; + const tagValue2 = {value: 'testValue2'}; + + const tags = new TagMap(); + tags.set(tagKey1, tagValue1); + tags.set(tagKey2, tagValue2); before(() => { view = new BaseView( - 'test/view/name', measure, AggregationType.LAST_VALUE, - ['testKey1', 'testKey2'], 'description test'); + 'test/view/name', measure, AggregationType.LAST_VALUE, tagKeys, + 'description test'); - const measurement = {measure, tags, value: 10}; - view.recordMeasurement(measurement); + const measurement = {measure, value: 10}; + view.recordMeasurement(measurement, tags); }); it('should not get aggregation data when wrong tags values are given', () => { assert.ok(!view.getSnapshot( - {testKey1: 'wrongTagValue', testKey2: 'wrongTagValue'})); + [{value: 'wrongTagValue'}, {value: 'wrongTagValue'}])); }); it('should not get aggregation data when not enough tags are given', () => { - assert.ok(!view.getSnapshot({testKey1: 'testValue'})); + assert.ok(!view.getSnapshot([tagValue1])); }); it('should get aggregation data when tags are correct', () => { - assert.ok(view.getSnapshot(tags)); + assert.ok(view.getSnapshot([tagValue1, tagValue2])); }); }); }); diff --git a/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts b/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts index d3d6b353f..18eed4848 100644 --- a/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts +++ b/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import {AggregationType, DistributionData, ExporterConfig, logger, Logger, Measurement, MeasureUnit, StatsEventListener, Tags, View} from '@opencensus/core'; +import {AggregationType, DistributionData, ExporterConfig, logger, Logger, Measurement, StatsEventListener, TagKey, TagValue, View} from '@opencensus/core'; import * as express from 'express'; import * as http from 'http'; -import {Counter, Gauge, Histogram, Metric, Registry} from 'prom-client'; +import {Counter, Gauge, Histogram, labelValues, Metric, Registry} from 'prom-client'; export interface PrometheusExporterOptions extends ExporterConfig { /** App prefix for metrics, if needed - default opencensus */ @@ -75,12 +75,15 @@ export class PrometheusStatsExporter implements StatsEventListener { /** * Method called every new stats' record - * @param views - * @param measurement + * @param views The views related to the measurement + * @param measurement The recorded measurement + * @param tags The tags to which the value is applied */ - onRecord(views: View[], measurement: Measurement) { + onRecord( + views: View[], measurement: Measurement, tags: Map) { for (const view of views) { - this.updateMetric(view, measurement); + const labelValues = this.getLabelValues(view.getColumns(), tags); + this.updateMetric(view, measurement, labelValues); } } @@ -93,12 +96,23 @@ export class PrometheusStatsExporter implements StatsEventListener { // // it to backend (dependency with PR#253). } + private getLabelValues(columns: TagKey[], tags: Map): + labelValues { + const labels: labelValues = {}; + columns.forEach((tagKey) => { + if (tags.has(tagKey)) { + labels[tagKey.name] = tags.get(tagKey).value; + } + }); + return labels; + } + /** * Register or get a metric in Prometheus * @param view View will be used to register the metric - * @param tags Optional, used only for histogram metric + * @param labels Object with label keys and values */ - private registerMetric(view: View, tags?: Tags): Metric { + private registerMetric(view: View, labels: labelValues): Metric { const metricName = this.getPrometheusMetricName(view); /** Get metric if already registered */ let metric = this.registry.getSingleMetric(metricName); @@ -107,14 +121,10 @@ export class PrometheusStatsExporter implements StatsEventListener { return metric; } - const labels = view.getColumns(); + const labelNames = view.getColumns().map((tagKey) => tagKey.name); // Create a new metric if there is no one - const metricObj = { - name: metricName, - help: view.description, - labelNames: labels - }; + const metricObj = {name: metricName, help: view.description, labelNames}; // Creating the metric based on aggregation type switch (view.aggregation) { @@ -126,12 +136,12 @@ export class PrometheusStatsExporter implements StatsEventListener { metric = new Gauge(metricObj); break; case AggregationType.DISTRIBUTION: - this.validateDisallowedLeLabelForHistogram(labels); + this.validateDisallowedLeLabelForHistogram(labelNames); const distribution = { name: metricName, help: view.description, - labelNames: labels, - buckets: this.getBoundaries(view, tags) + labelNames, + buckets: this.getBoundaries(view, labels) }; metric = new Histogram(distribution); break; @@ -149,20 +159,21 @@ export class PrometheusStatsExporter implements StatsEventListener { * @param view View will be used to update the metric * @param measurement Measurement with the new value to update the metric */ - private updateMetric(view: View, measurement: Measurement) { - const metric = this.registerMetric(view, measurement.tags); + private updateMetric( + view: View, measurement: Measurement, labels: labelValues) { + const metric = this.registerMetric(view, labels); // Updating the metric based on metric instance type and aggregation type if (metric instanceof Counter) { - metric.inc(measurement.tags); + metric.inc(labels); } else if ( view.aggregation === AggregationType.SUM && metric instanceof Gauge) { - metric.inc(measurement.tags, measurement.value); + metric.inc(labels, measurement.value); } else if ( view.aggregation === AggregationType.LAST_VALUE && metric instanceof Gauge) { - metric.set(measurement.tags, measurement.value); + metric.set(labels, measurement.value); } else if (metric instanceof Histogram) { - metric.observe(measurement.tags, measurement.value); + metric.observe(labels, measurement.value); } else { this.logger.error('Metric not supported'); } @@ -209,8 +220,11 @@ export class PrometheusStatsExporter implements StatsEventListener { * @param view View used to get the DistributionData * @param tags Tags used to get the DistributionData */ - private getBoundaries(view: View, tags: Tags): number[] { - const data = view.getSnapshot(tags) as DistributionData; + private getBoundaries(view: View, labels: labelValues): number[] { + const tagValues = Object.keys(labels).map((label) => { + return {value: String(labels[label])}; + }); + const data = view.getSnapshot(tagValues) as DistributionData; return data.buckets; } diff --git a/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts b/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts index 8ca1fa010..eb3735bab 100644 --- a/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts +++ b/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {AggregationType, globalStats, Measure, MeasureUnit} from '@opencensus/core'; +import {AggregationType, globalStats, Measure, MeasureUnit, TagMap} from '@opencensus/core'; import * as assert from 'assert'; import * as http from 'http'; @@ -23,8 +23,12 @@ import {PrometheusStatsExporter} from '../src/'; describe('Prometheus Stats Exporter', () => { const options = {port: 9464, startServer: false}; const prometheusServerUrl = `http://localhost:${options.port}/metrics`; - const tags = {tagKey1: 'tagValue1'}; - const tagKeys = Object.keys(tags); + const tagKey1 = {name: 'tagKey1'}; + const tagValue1 = {value: 'tagValue1'}; + const tagKeys = [tagKey1]; + const tagMap = new TagMap(); + tagMap.set(tagKey1, tagValue1); + let exporter: PrometheusStatsExporter; let measure: Measure; @@ -45,10 +49,10 @@ describe('Prometheus Stats Exporter', () => { const view = globalStats.createView( 'ocnodemetrics/countview', measure, AggregationType.COUNT, tagKeys, 'A count aggregation example', null); - const measurement = {measure, tags, value: 2}; - const measurement2 = {measure, tags, value: 3}; + const measurement = {measure, value: 2}; + const measurement2 = {measure, value: 3}; globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); http.get(prometheusServerUrl, (res) => { res.on('data', (chunk) => { @@ -69,15 +73,13 @@ describe('Prometheus Stats Exporter', () => { it('should create a sum aggregation', (done) => { const measure = globalStats.createMeasureDouble('testMeasureDouble', MeasureUnit.UNIT); - const tags = {tagKey1: 'tagValue1'}; - const tagKeys = Object.keys(tags); const view = globalStats.createView( 'ocnodemetrics/sumview', measure, AggregationType.SUM, tagKeys, 'A sum aggregation example', null); - const measurement = {measure, tags, value: 2}; - const measurement2 = {measure, tags, value: 3}; + const measurement = {measure, value: 2}; + const measurement2 = {measure, value: 3}; globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); http.get(prometheusServerUrl, (res) => { res.on('data', (chunk) => { @@ -98,15 +100,13 @@ describe('Prometheus Stats Exporter', () => { it('should create a last value aggregation', (done) => { const measure = globalStats.createMeasureDouble('testMeasureDouble', MeasureUnit.UNIT); - const tags = {tagKey1: 'tagValue1'}; - const tagKeys = Object.keys(tags); const view = globalStats.createView( 'ocnodemetrics/lastvalueview', measure, AggregationType.LAST_VALUE, tagKeys, 'A last value aggregation example', null); - const measurement = {measure, tags, value: 2}; - const measurement2 = {measure, tags, value: 3}; + const measurement = {measure, value: 2}; + const measurement2 = {measure, value: 3}; globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); http.get(prometheusServerUrl, (res) => { res.on('data', (chunk) => { @@ -127,16 +127,14 @@ describe('Prometheus Stats Exporter', () => { it('should create a distribution aggregation', (done) => { const measure = globalStats.createMeasureDouble('testMeasureDouble', MeasureUnit.UNIT); - const tags = {tagKey1: 'tagValue1'}; - const tagKeys = Object.keys(tags); const boundaries = [10, 20, 30, 40]; const view = globalStats.createView( 'ocnodemetrics/distributionview', measure, AggregationType.DISTRIBUTION, tagKeys, 'A distribution aggregation example', boundaries); - const measurement = {measure, tags, value: 12}; - const measurement2 = {measure, tags, value: 31}; + const measurement = {measure, value: 12}; + const measurement2 = {measure, value: 31}; globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); http.get(prometheusServerUrl, (res) => { res.on('data', (chunk) => { @@ -177,18 +175,21 @@ describe('Prometheus Stats Exporter', () => { () => { const measure = globalStats.createMeasureDouble( 'testMeasureDouble', MeasureUnit.UNIT); - const tags = {le: 'tagValue1'}; - const tagKeys = Object.keys(tags); + const tagKeyWithLe = {name: 'le'}; + const tagKeys = [tagKeyWithLe]; + const tagMap = new TagMap(); + tagMap.set(tagKeyWithLe, tagValue1); + const boundaries = [10, 20, 30, 40]; const view = globalStats.createView( 'ocnodemetrics/distributionview1', measure, AggregationType.DISTRIBUTION, tagKeys, 'A distribution aggregation example', boundaries); globalStats.registerView(view); - const measurement = {measure, tags, value: 2}; + const measurement = {measure, value: 2}; assert.throws(() => { - globalStats.record(measurement); + globalStats.record([measurement], tagMap); }, /^Error: le is a reserved label keyword$/); }); }); @@ -196,8 +197,11 @@ describe('Prometheus Stats Exporter', () => { describe('Prometheus Stats Exporter with prefix option', () => { const options = {port: 9464, startServer: false, prefix: 'opencensus'}; const prometheusServerUrl = `http://localhost:${options.port}/metrics`; - const tags = {le: 'tagValue1'}; - const tagKeys = Object.keys(tags); + const tagKeyWithLe = {name: 'le'}; + const tagValue1 = {value: 'tagValue1'}; + const tagKeys = [tagKeyWithLe]; + const tagMap = new TagMap(); + tagMap.set(tagKeyWithLe, tagValue1); let exporter: PrometheusStatsExporter; let measure: Measure; @@ -218,10 +222,10 @@ describe('Prometheus Stats Exporter with prefix option', () => { const view = globalStats.createView( 'test/key-1', measure, AggregationType.COUNT, tagKeys, 'A count aggregation example', null); - const measurement = {measure, tags, value: 2}; - const measurement2 = {measure, tags, value: 3}; + const measurement = {measure, value: 2}; + const measurement2 = {measure, value: 3}; globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); http.get(prometheusServerUrl, (res) => { res.on('data', (chunk) => { diff --git a/packages/opencensus-exporter-stackdriver/src/stackdriver-monitoring.ts b/packages/opencensus-exporter-stackdriver/src/stackdriver-monitoring.ts index 371a885b9..5df1f0716 100644 --- a/packages/opencensus-exporter-stackdriver/src/stackdriver-monitoring.ts +++ b/packages/opencensus-exporter-stackdriver/src/stackdriver-monitoring.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {logger, Logger, Measurement, Metric, MetricDescriptor as OCMetricDescriptor, MetricProducerManager, Metrics, StatsEventListener, View} from '@opencensus/core'; +import {logger, Logger, Measurement, Metric, MetricDescriptor as OCMetricDescriptor, MetricProducerManager, Metrics, StatsEventListener, TagKey, TagValue, View} from '@opencensus/core'; import {auth, JWT} from 'google-auth-library'; import {google} from 'googleapis'; @@ -223,8 +223,10 @@ export class StackdriverStatsExporter implements StatsEventListener { /** * Is called whenever a measure is recorded. - * @param views The views associated with the measure - * @param measurement The measurement recorded + * @param views The views related to the measurement + * @param measurement The recorded measurement + * @param tags The tags to which the value is applied */ - onRecord(views: View[], measurement: Measurement) {} + onRecord( + views: View[], measurement: Measurement, tags: Map) {} } diff --git a/packages/opencensus-exporter-stackdriver/src/stackdriver-stats-utils.ts b/packages/opencensus-exporter-stackdriver/src/stackdriver-stats-utils.ts index d7fbaff3e..ef5b41434 100644 --- a/packages/opencensus-exporter-stackdriver/src/stackdriver-stats-utils.ts +++ b/packages/opencensus-exporter-stackdriver/src/stackdriver-stats-utils.ts @@ -138,7 +138,8 @@ function createMetric( const labels: {[key: string]: string} = {}; for (let i = 0; i < labelValues.length; i++) { const value = labelValues[i].value; - if (value && metricDescriptor.labelKeys[i]) { + if (value !== null && value !== undefined && + metricDescriptor.labelKeys[i]) { labels[metricDescriptor.labelKeys[i].key] = value; } else { // TODO(mayurkale) : consider to throw an error when LabelValue and diff --git a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-stats-utils.ts b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-stats-utils.ts index 71084535b..b7444046f 100644 --- a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-stats-utils.ts +++ b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-stats-utils.ts @@ -104,6 +104,7 @@ describe('Stackdriver Stats Exporter Utils', () => { describe('createMetric()', () => { const labelKeys: LabelKey[] = [{'key': 'key1', 'description': 'desc'}]; const labelValues: LabelValue[] = [{'value': 'value1'}]; + const emptyLabelValues: LabelValue[] = [{'value': ''}]; const metricDescriptor: OCMetricDescriptor = { name: METRIC_NAME, description: METRIC_DESCRIPTION, @@ -141,6 +142,17 @@ describe('Stackdriver Stats Exporter Utils', () => { assert.deepStrictEqual( metric.labels, {'opencensus_task': OPENCENSUS_TASK_VALUE_DEFAULT}); }); + + it('should return a Stackdriver Metric With Empty label value', () => { + const metric = TEST_ONLY.createMetric( + metricDescriptor, emptyLabelValues, + StackdriverStatsExporter.CUSTOM_OPENCENSUS_DOMAIN); + assert.strictEqual( + metric.type, `custom.googleapis.com/opencensus/${METRIC_NAME}`); + assert.deepStrictEqual( + metric.labels, + {'key1': '', 'opencensus_task': OPENCENSUS_TASK_VALUE_DEFAULT}); + }); }); describe('createDistribution()', () => { diff --git a/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/rpcz.page-handler.ts b/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/rpcz.page-handler.ts index 17203bc73..88a718978 100644 --- a/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/rpcz.page-handler.ts +++ b/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/rpcz.page-handler.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import {AggregationType, CountData, DistributionData} from '@opencensus/core'; - +import {AggregationType} from '@opencensus/core'; import {StatsParams} from '../../zpages'; const ejs = require('ejs'); @@ -102,21 +101,38 @@ export class RpczPageHandler { for (const view of rpcViews) { const recordedData = this.statsParams.recordedData[view.name]; - for (const snapshot of recordedData) { - let method = snapshot.tags['grpc_client_method']; - let zMeasures = rpczData.measuresSent; - // Switches to received data if it's a server - if (!method) { - method = snapshot.tags['grpc_server_method']; + let clientMethodIndex = -1; + let clientStatusIndex = -1; + let serverMethodIndex = -1; + let serverStatusIndex = -1; + const columns = view.getColumns(); + for (let i = 0; i < columns.length; i++) { + if (columns[i].name === 'grpc_client_method') { + clientMethodIndex = i; + } else if (columns[i].name === 'grpc_client_status') { + clientStatusIndex = i; + } else if (columns[i].name === 'grpc_server_method') { + serverMethodIndex = i; + } else if (columns[i].name === 'grpc_server_status') { + serverStatusIndex = i; + } + } + for (const snapshot of recordedData) { + let method = ''; + let zMeasures; + if (clientMethodIndex !== -1) { + method = snapshot.tagValues[clientMethodIndex].value; + zMeasures = rpczData.measuresSent; + } else if (serverMethodIndex !== -1) { + // Switches to received data if it's a server + method = snapshot.tagValues[serverMethodIndex].value; zMeasures = rpczData.measuresReceived; } - if (method) { if (!zMeasures[method]) { zMeasures[method] = this.newEmptyZMeasure(); } - if (snapshot.type === AggregationType.DISTRIBUTION) { // Fills the output columns for that method if (view.name === DefaultViews.CLIENT_SENT_BYTES_PER_RPC || @@ -159,10 +175,14 @@ export class RpczPageHandler { zMeasures[method].rate.hr = zMeasures[method].rate.min * 60; // Fills the error columns for that method - const error = (snapshot.tags['grpc_client_status'] || - snapshot.tags['grpc_server_status']) && - (snapshot.tags['grpc_client_status'] !== 'OK' || - snapshot.tags['grpc_server_status'] !== 'OK'); + const error = + (clientStatusIndex !== -1 && + snapshot.tagValues[clientStatusIndex] && + snapshot.tagValues[clientStatusIndex].value !== 'OK') || + (serverStatusIndex !== -1 && + snapshot.tagValues[serverStatusIndex] && + snapshot.tagValues[serverStatusIndex].value !== 'OK'); + if (error) { zMeasures[method].errors.tot += snapshot.value; zMeasures[method].errors.min = diff --git a/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/statsz.page-handler.ts b/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/statsz.page-handler.ts index 0aa0e378b..58632dcfa 100644 --- a/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/statsz.page-handler.ts +++ b/packages/opencensus-exporter-zpages/src/zpages-frontend/page-handlers/statsz.page-handler.ts @@ -14,14 +14,12 @@ * limitations under the License. */ -import {AggregationData, AggregationType, Tags, View} from '@opencensus/core'; - +import {AggregationData, AggregationType, TagKey, TagValue, View} from '@opencensus/core'; import {StatsParams} from '../../zpages'; const ejs = require('ejs'); import * as pkgDir from 'pkg-dir'; -import {fileLoader} from 'ejs'; export interface StatszParams { path: string; @@ -33,8 +31,8 @@ type FolderType = { // AggregationData in zPages format export type ZPagesStatsData = { - tagKeys: string[], - tagValues?: string[], + tagKeys: TagKey[], + tagValues?: TagValue[], snapshot?: AggregationData }; @@ -191,23 +189,13 @@ export class StatszPageHandler { } return recordedData.map(snapshot => { return { - tagKeys: Object.keys(snapshot.tags), - tagValues: this.getTagValues(snapshot.tags), + tagKeys: selectedView.getColumns(), + tagValues: snapshot.tagValues, snapshot }; }); } - /** - * Extracts the tag's values from given tags. - * @param tags The list of tags - */ - private getTagValues(tags: Tags): string[] { - return Object.keys(tags).map((tagKey) => { - return tags[tagKey]; - }); - } - /** * Loader an file from templates folder * @param fileName name of the file will be to load with extension diff --git a/packages/opencensus-exporter-zpages/src/zpages.ts b/packages/opencensus-exporter-zpages/src/zpages.ts index 772d80426..2d7454126 100644 --- a/packages/opencensus-exporter-zpages/src/zpages.ts +++ b/packages/opencensus-exporter-zpages/src/zpages.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import {AggregationData, Exporter, ExporterConfig, Measure, Measurement, RootSpan, Span, StatsEventListener, Tags, View} from '@opencensus/core'; +import {AggregationData, Exporter, ExporterConfig, Measure, Measurement, RootSpan, Span, StatsEventListener, TagKey, TagValue, View} from '@opencensus/core'; import {logger, Logger} from '@opencensus/core'; import * as express from 'express'; import * as http from 'http'; - import {createRoutes} from './zpages-frontend/routes'; /** Interface to Zpages options */ @@ -112,10 +111,14 @@ export class ZpagesExporter implements Exporter, StatsEventListener { * Called whenever a measurement is recorded * @param views the view list where the measurement was recorded * @param measurement the recorded measurement + * @param tags The tags to which the value is applied */ - onRecord(views: View[], measurement: Measurement): void { + onRecord( + views: View[], measurement: Measurement, + tags: Map): void { + const tagValues = [...tags.values()]; views.map(view => { - const snapshot = view.getSnapshot(measurement.tags); + const snapshot = view.getSnapshot(tagValues); // Check if there is no data for the current view if (!this.statsParams.recordedData[view.name]) { this.statsParams.recordedData[view.name] = [snapshot]; diff --git a/packages/opencensus-exporter-zpages/templates/statsz-view-count.ejs b/packages/opencensus-exporter-zpages/templates/statsz-view-count.ejs index 904fa7152..862933db5 100644 --- a/packages/opencensus-exporter-zpages/templates/statsz-view-count.ejs +++ b/packages/opencensus-exporter-zpages/templates/statsz-view-count.ejs @@ -2,7 +2,7 @@ - TagKey: + TagKey: Count @@ -12,8 +12,12 @@ - - + + + + + + diff --git a/packages/opencensus-exporter-zpages/templates/statsz-view-distribution.ejs b/packages/opencensus-exporter-zpages/templates/statsz-view-distribution.ejs index 53774af46..0265245c4 100644 --- a/packages/opencensus-exporter-zpages/templates/statsz-view-distribution.ejs +++ b/packages/opencensus-exporter-zpages/templates/statsz-view-distribution.ejs @@ -2,7 +2,7 @@ - TagKey: + TagKey: Mean, By Count @@ -17,8 +17,12 @@ - - + + + + + + @@ -32,7 +36,7 @@ - + [...) diff --git a/packages/opencensus-exporter-zpages/templates/statsz-view-lastvalue.ejs b/packages/opencensus-exporter-zpages/templates/statsz-view-lastvalue.ejs index 0345f5785..ab37802ad 100644 --- a/packages/opencensus-exporter-zpages/templates/statsz-view-lastvalue.ejs +++ b/packages/opencensus-exporter-zpages/templates/statsz-view-lastvalue.ejs @@ -2,7 +2,7 @@ - TagKey: + TagKey: Last Value, @@ -12,8 +12,12 @@ - - + + + + + + diff --git a/packages/opencensus-exporter-zpages/templates/statsz-view-sum.ejs b/packages/opencensus-exporter-zpages/templates/statsz-view-sum.ejs index a18a5978d..67da621b9 100644 --- a/packages/opencensus-exporter-zpages/templates/statsz-view-sum.ejs +++ b/packages/opencensus-exporter-zpages/templates/statsz-view-sum.ejs @@ -2,7 +2,7 @@ - TagKey: + TagKey: Sum, @@ -12,8 +12,12 @@ - - + + + + + + diff --git a/packages/opencensus-exporter-zpages/test/test-zpages.ts b/packages/opencensus-exporter-zpages/test/test-zpages.ts index ecc36b890..c8ba3a594 100644 --- a/packages/opencensus-exporter-zpages/test/test-zpages.ts +++ b/packages/opencensus-exporter-zpages/test/test-zpages.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import {AggregationType, CountData, DistributionData, globalStats, Measure, Measurement, MeasureUnit, RootSpan, SumData, Tags, TracerConfig} from '@opencensus/core'; +import {AggregationType, CountData, DistributionData, globalStats, Measure, Measurement, MeasureUnit, RootSpan, SumData, TagMap, TracerConfig} from '@opencensus/core'; import * as assert from 'assert'; import axios from 'axios'; import * as http from 'http'; import * as qs from 'querystring'; - import {ZpagesExporter, ZpagesExporterOptions} from '../src/zpages'; import {RpczData} from '../src/zpages-frontend/page-handlers/rpcz.page-handler'; import {StatsViewData, StatszParams} from '../src/zpages-frontend/page-handlers/statsz.page-handler'; @@ -237,11 +236,12 @@ describe('Zpages Exporter', () => { // STATS TESTS describe('when a view is accessed in statsz page', () => { - const tags: Tags = {tagKey1: 'tagValue1', tagKey2: 'tagValue1'}; - const tagKeys = Object.keys(tags); - const tagValues = Object.keys(tags).map((tagKey) => { - return tags[tagKey]; - }); + const tagKeys = [{name: 'testKey1'}, {name: 'testKey2'}]; + const tagValues = [{value: 'testValue1'}, {value: 'testValue2'}]; + const tagMap = new TagMap(); + tagMap.set(tagKeys[0], tagValues[0]); + tagMap.set(tagKeys[1], tagValues[1]); + let zpages: ZpagesExporter; let measure: Measure; let zpagesData: StatsViewData; @@ -253,8 +253,8 @@ describe('Zpages Exporter', () => { globalStats.registerExporter(zpages); measure = globalStats.createMeasureDouble( 'testMeasureDouble', MeasureUnit.UNIT, 'A test measure'); - measurement = {measure, tags, value: 22}; - measurement2 = {measure, tags, value: 11}; + measurement = {measure, value: 22}; + measurement2 = {measure, value: 11}; zpages.startServer(done); }); @@ -269,7 +269,7 @@ describe('Zpages Exporter', () => { 'test/CountView', measure, AggregationType.COUNT, tagKeys, 'A count test', null); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/CountView'}); @@ -284,16 +284,14 @@ describe('Zpages Exporter', () => { 'test/CountView', measure, AggregationType.COUNT, tagKeys, 'A count test', null); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/CountView'}); const data = zpagesData.statsData[0]; const snapshot = data.snapshot as CountData; - assert.equal(data.tagKeys[0], tagKeys[0]); - assert.equal(data.tagKeys[1], tagKeys[1]); - assert.equal(data.tagValues[0], tagValues[0]); - assert.equal(data.tagValues[1], tagValues[1]); + assert.deepEqual(data.tagKeys, tagKeys); + assert.deepEqual(data.tagValues, tagValues); assert.equal(snapshot.value, 2); }); }); @@ -305,7 +303,7 @@ describe('Zpages Exporter', () => { 'test/SumView', measure, AggregationType.SUM, tagKeys, 'A sum test', null); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/SumView'}); @@ -321,16 +319,14 @@ describe('Zpages Exporter', () => { 'test/SumView', measure, AggregationType.SUM, tagKeys, 'A sum test', null); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/SumView'}); const data = zpagesData.statsData[0]; const snapshot = data.snapshot as SumData; - assert.equal(data.tagKeys[0], tagKeys[0]); - assert.equal(data.tagKeys[1], tagKeys[1]); - assert.equal(data.tagValues[0], tagValues[0]); - assert.equal(data.tagValues[1], tagValues[1]); + assert.deepEqual(data.tagKeys, tagKeys); + assert.deepEqual(data.tagValues, tagValues); assert.equal(snapshot.value, 33); }); }); @@ -342,7 +338,7 @@ describe('Zpages Exporter', () => { 'test/LastValueView', measure, AggregationType.LAST_VALUE, tagKeys, 'A last value test', null); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/LastValueView'}); @@ -358,16 +354,14 @@ describe('Zpages Exporter', () => { 'test/LastValueView', measure, AggregationType.LAST_VALUE, tagKeys, 'A last value test', null); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/LastValueView'}); const data = zpagesData.statsData[0]; const snapshot = data.snapshot as SumData; - assert.equal(data.tagKeys[0], tagKeys[0]); - assert.equal(data.tagKeys[1], tagKeys[1]); - assert.equal(data.tagValues[0], tagValues[0]); - assert.equal(data.tagValues[1], tagValues[1]); + assert.deepEqual(data.tagKeys, tagKeys); + assert.deepEqual(data.tagValues, tagValues); assert.equal(snapshot.value, 11); }); }); @@ -380,7 +374,7 @@ describe('Zpages Exporter', () => { 'test/DistributionView', measure, AggregationType.DISTRIBUTION, tagKeys, 'A distribution test', boundaries); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/DistributionView'}); @@ -398,17 +392,15 @@ describe('Zpages Exporter', () => { 'test/DistributionView', measure, AggregationType.DISTRIBUTION, tagKeys, 'A distribution test', boundaries); globalStats.registerView(view); - globalStats.record(measurement, measurement2); + globalStats.record([measurement, measurement2], tagMap); zpagesData = await zpagesClient.getStatsz({path: 'test/DistributionView'}); const data = zpagesData.statsData[0]; const snapshot = data.snapshot as DistributionData; - assert.equal(data.tagKeys[0], tagKeys[0]); - assert.equal(data.tagKeys[1], tagKeys[1]); - assert.equal(data.tagValues[0], tagValues[0]); - assert.equal(data.tagValues[1], tagValues[1]); + assert.deepEqual(data.tagKeys, tagKeys); + assert.deepEqual(data.tagValues, tagValues); assert.equal(snapshot.count, 2); assert.equal(snapshot.mean, 16.5); assert.equal(snapshot.sumOfSquaredDeviation, 60.5); @@ -433,12 +425,13 @@ describe('Zpages Exporter', () => { }); it('should get the sent stats', async () => { - const tags = {grpc_client_method: 'ExampleMethod'}; - const tags2 = { - grpc_client_method: 'ExampleMethod', - grpc_client_status: 'DEADLINE_EXCEEDED' - }; - const tagKeys = Object.keys(tags); + const GRPC_CLIENT_METHOD = {name: 'grpc_client_method'}; + const GRPC_CLIENT_STATUS = {name: 'grpc_client_status'}; + const GRPC_CLIENT_METHOD_V = {value: 'ExampleMethod'}; + const GRPC_CLIENT_STATUS_V = {value: 'DEADLINE_EXCEEDED'}; + const tagMap = new TagMap(); + tagMap.set(GRPC_CLIENT_METHOD, GRPC_CLIENT_METHOD_V); + tagMap.set(GRPC_CLIENT_STATUS, GRPC_CLIENT_STATUS_V); const measure = globalStats.createMeasureDouble( 'grpc.io/client/sent_bytes_per_rpc', MeasureUnit.BYTE, @@ -458,58 +451,64 @@ describe('Zpages Exporter', () => { const view1 = globalStats.createView( 'grpc.io/client/sent_bytes_per_rpc', measure, - AggregationType.DISTRIBUTION, tagKeys, 'Sent bytes per RPC', + AggregationType.DISTRIBUTION, + [GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS], 'Sent bytes per RPC', boundaries); globalStats.registerView(view1); const view2 = globalStats.createView( 'grpc.io/client/received_bytes_per_rpc', measure2, - AggregationType.DISTRIBUTION, tagKeys, 'Sent bytes per RPC', + AggregationType.DISTRIBUTION, + [GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS], 'Sent bytes per RPC', boundaries); globalStats.registerView(view2); const view3 = globalStats.createView( 'grpc.io/client/roundtrip_latency', measure3, - AggregationType.DISTRIBUTION, tagKeys, 'Latency in msecs', + AggregationType.DISTRIBUTION, + [GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS], 'Latency in msecs', boundaries); globalStats.registerView(view3); const view4 = globalStats.createView( 'grpc.io/client/completed_rpcs', measure3, AggregationType.COUNT, - tagKeys, 'Number of completed client RPCs', null); + [GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS], + 'Number of completed client RPCs', null); globalStats.registerView(view4); const view5 = globalStats.createView( 'grpc.io/client/started_rpcs', measure4, AggregationType.COUNT, - tagKeys, 'Number of started client RPCs', null); + [GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS], + 'Number of started client RPCs', null); globalStats.registerView(view5); - const measurement = {measure, tags, value: 22000}; - const measurement2 = {measure: measure2, tags, value: 1100}; - const measurement3 = {measure: measure3, tags, value: 1}; - const measurement4 = {measure: measure3, tags: tags2, value: 2}; - const measurement5 = {measure: measure4, tags, value: 2}; + const measurement = {measure, value: 22000}; + const measurement2 = {measure: measure2, value: 1100}; + const measurement3 = {measure: measure3, value: 1}; + const measurement4 = {measure: measure3, value: 2}; + const measurement5 = {measure: measure4, value: 2}; globalStats.record( - measurement, measurement2, measurement3, measurement4, measurement5); + [measurement, measurement2, measurement3, measurement4, measurement5], + tagMap); rpczData = await zpagesClient.getRpcz(); const measures = rpczData.measuresSent['ExampleMethod']; - assert.equal(measures.count.tot, 2); - assert.equal(measures.avgLatency.tot.toFixed(3), 2.000); + assert.equal(measures.avgLatency.tot.toFixed(3), 1.500); assert.equal(measures.input.tot.toFixed(3), 1.074); assert.equal(measures.output.tot.toFixed(3), 21.484); - assert.equal(measures.errors.tot, 1); + assert.equal(measures.errors.tot, 2); }); it('should get the received stats', async () => { - const tags3 = {grpc_server_method: 'ExampleMethod'}; - const tags4 = { - grpc_server_method: 'ExampleMethod', - grpc_server_status: 'DEADLINE_EXCEEDED' - }; - const tagKeys2 = Object.keys(tags3); + const GRPC_SERVER_METHOD = {name: 'grpc_server_method'}; + const GRPC_SERVER_STATUS = {name: 'grpc_server_status'}; + const GRPC_SERVER_METHOD_V = {value: 'ExampleMethod'}; + const GRPC_SERVER_STATUS_V = {value: 'DEADLINE_EXCEEDED'}; + const tagMap = new TagMap(); + tagMap.set(GRPC_SERVER_METHOD, GRPC_SERVER_METHOD_V); + tagMap.set(GRPC_SERVER_STATUS, GRPC_SERVER_STATUS_V); const boundaries = [10, 20, 30, 40]; const measure5 = globalStats.createMeasureDouble( @@ -530,50 +529,58 @@ describe('Zpages Exporter', () => { const view1 = globalStats.createView( 'grpc.io/server/received_bytes_per_rpc', measure5, - AggregationType.DISTRIBUTION, tagKeys2, 'Sent bytes per RPC', + AggregationType.DISTRIBUTION, + [GRPC_SERVER_METHOD, GRPC_SERVER_STATUS], 'Sent bytes per RPC', boundaries); globalStats.registerView(view1); const view2 = globalStats.createView( 'grpc.io/server/sent_bytes_per_rpc', measure6, - AggregationType.DISTRIBUTION, tagKeys2, 'Sent bytes per RPC', + AggregationType.DISTRIBUTION, + [GRPC_SERVER_METHOD, GRPC_SERVER_STATUS], 'Sent bytes per RPC', boundaries); globalStats.registerView(view2); const view3 = globalStats.createView( 'grpc.io/server/server_latency', measure7, - AggregationType.DISTRIBUTION, tagKeys2, 'Latency in msecs', + AggregationType.DISTRIBUTION, + [GRPC_SERVER_METHOD, GRPC_SERVER_STATUS], 'Latency in msecs', boundaries); globalStats.registerView(view3); const view4 = globalStats.createView( 'grpc.io/server/completed_rpcs', measure7, AggregationType.COUNT, - tagKeys2, 'Number of completed client RPCs', null); + [GRPC_SERVER_METHOD, GRPC_SERVER_STATUS], + 'Number of completed client RPCs', null); globalStats.registerView(view4); const view5 = globalStats.createView( 'grpc.io/server/started_rpcs', measure8, AggregationType.COUNT, - tagKeys2, 'Number of started client RPCs', null); + [GRPC_SERVER_METHOD, GRPC_SERVER_STATUS], + 'Number of started client RPCs', null); globalStats.registerView(view5); - const measurement6 = {measure: measure5, tags: tags3, value: 2200}; - const measurement7 = {measure: measure6, tags: tags3, value: 1100}; - const measurement8 = {measure: measure7, tags: tags3, value: 1}; - const measurement9 = {measure: measure7, tags: tags4, value: 2}; - const measurement10 = {measure: measure8, tags: tags3, value: 2}; + const measurement6 = {measure: measure5, value: 2200}; + const measurement7 = {measure: measure6, value: 1100}; + const measurement8 = {measure: measure7, value: 1}; + const measurement9 = {measure: measure7, value: 2}; + const measurement10 = {measure: measure8, value: 2}; globalStats.record( - measurement6, measurement7, measurement8, measurement9, - measurement10); + [ + measurement6, measurement7, measurement8, measurement9, + measurement10 + ], + tagMap); rpczData = await zpagesClient.getRpcz(); const measures = rpczData.measuresReceived['ExampleMethod']; assert.equal(measures.count.tot, 2); - assert.equal(measures.avgLatency.tot.toFixed(3), 2.000); + assert.equal(measures.avgLatency.tot.toFixed(3), 1.500); assert.equal(measures.input.tot.toFixed(3), 2.148); assert.equal(measures.output.tot.toFixed(3), 1.074); - assert.equal(measures.errors.tot, 1); + assert.equal(measures.errors.tot, 2); }); }); }); From 0310a04fe20693b37a4383438ce33d934cca8151 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Thu, 24 Jan 2019 22:27:35 -0800 Subject: [PATCH 2/2] replace not found key values by null and fix nit --- .../opencensus-core/src/stats/metric-utils.ts | 5 ++-- .../opencensus-core/src/stats/recorder.ts | 15 ++++------ packages/opencensus-core/src/stats/view.ts | 3 +- .../opencensus-core/test/test-metric-utils.ts | 7 +++++ .../opencensus-core/test/test-recorder.ts | 4 +-- .../src/prometheus-stats.ts | 28 +++++++++---------- .../test/test-prometheus-stats.ts | 26 +++++++++++++++++ 7 files changed, 57 insertions(+), 31 deletions(-) diff --git a/packages/opencensus-core/src/stats/metric-utils.ts b/packages/opencensus-core/src/stats/metric-utils.ts index 65e95243b..9b9ed7df3 100644 --- a/packages/opencensus-core/src/stats/metric-utils.ts +++ b/packages/opencensus-core/src/stats/metric-utils.ts @@ -79,8 +79,7 @@ export class MetricUtils { * @returns {LabelValue[]} List of label values */ static tagValuesToLabelValues(tagValues: TagValue[]): LabelValue[] { - return tagValues.map((tagValue) => { - return {value: tagValue.value}; - }); + return tagValues.map( + (tagValue) => ({value: tagValue ? tagValue.value : null})); } } diff --git a/packages/opencensus-core/src/stats/recorder.ts b/packages/opencensus-core/src/stats/recorder.ts index 7fdb1edeb..15cd6fa23 100644 --- a/packages/opencensus-core/src/stats/recorder.ts +++ b/packages/opencensus-core/src/stats/recorder.ts @@ -17,6 +17,8 @@ import {TagKey, TagValue} from '../tags/types'; import {AggregationData, AggregationType, CountData, DistributionData, LastValueData, Measurement, MeasureType, SumData} from './types'; +const UNKNOWN_TAG_VALUE: TagValue = null; + export class Recorder { static addMeasurement( aggregationData: AggregationData, @@ -44,15 +46,10 @@ export class Recorder { /** Gets the tag values from tags and columns */ static getTagValues(tags: Map, columns: TagKey[]): TagValue[] { - const tagValues: TagValue[] = []; - - // ignore not found key values. - columns.forEach((tagKey) => { - if (tags.has(tagKey)) { - tagValues.push(tags.get(tagKey)); - } - }); - return tagValues; + return columns.map( + (tagKey) => + (tags.get(tagKey) || + /** replace not found key values by null. */ UNKNOWN_TAG_VALUE)); } private static addToDistribution( diff --git a/packages/opencensus-core/src/stats/view.ts b/packages/opencensus-core/src/stats/view.ts index 8cf62e2fe..038eb0317 100644 --- a/packages/opencensus-core/src/stats/view.ts +++ b/packages/opencensus-core/src/stats/view.ts @@ -118,7 +118,6 @@ export class BaseView implements View { recordMeasurement(measurement: Measurement, tags: TagMap) { const tagValues = Recorder.getTagValues(tags.tags, this.columns); const encodedTags = this.encodeTagValues(tagValues); - if (!this.tagValueAggregationMap[encodedTags]) { this.tagValueAggregationMap[encodedTags] = this.createAggregationData(tagValues); @@ -133,7 +132,7 @@ export class BaseView implements View { * @param tagValues The tagValues to encode */ private encodeTagValues(tagValues: TagValue[]): string { - return tagValues.map(tagValue => tagValue.value) + return tagValues.map((tagValue) => tagValue ? tagValue.value : null) .sort() .join(RECORD_SEPARATOR); } diff --git a/packages/opencensus-core/test/test-metric-utils.ts b/packages/opencensus-core/test/test-metric-utils.ts index 4b26e36a8..c7b1694b9 100644 --- a/packages/opencensus-core/test/test-metric-utils.ts +++ b/packages/opencensus-core/test/test-metric-utils.ts @@ -53,4 +53,11 @@ describe('MetricUtil', () => { MetricUtils.tagValuesToLabelValues(tags), [{value: 'value1'}, {value: 'value2'}, {value: ''}, {value: null}]); }); + + it('should convert tag values to label values with null tag value', () => { + const tags = [{value: 'value1'}, null, null, {value: null}]; + assert.deepStrictEqual( + MetricUtils.tagValuesToLabelValues(tags), + [{value: 'value1'}, {value: null}, {value: null}, {value: null}]); + }); }); diff --git a/packages/opencensus-core/test/test-recorder.ts b/packages/opencensus-core/test/test-recorder.ts index 9d6338add..563c89397 100644 --- a/packages/opencensus-core/test/test-recorder.ts +++ b/packages/opencensus-core/test/test-recorder.ts @@ -204,8 +204,8 @@ describe('Recorder', () => { tagMap.set(CALLER, CALLER_V); tagMap.set(METHOD, METHOD_V); const tagValues = Recorder.getTagValues(tagMap.tags, columns); - assert.equal(tagValues.length, 2); - assert.deepStrictEqual(tagValues, [CALLER_V, METHOD_V]); + assert.equal(tagValues.length, 3); + assert.deepStrictEqual(tagValues, [CALLER_V, METHOD_V, null]); }); it('should return tag values from tags and columns with extra tags', diff --git a/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts b/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts index 18eed4848..f75b3e78a 100644 --- a/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts +++ b/packages/opencensus-exporter-prometheus/src/prometheus-stats.ts @@ -82,8 +82,7 @@ export class PrometheusStatsExporter implements StatsEventListener { onRecord( views: View[], measurement: Measurement, tags: Map) { for (const view of views) { - const labelValues = this.getLabelValues(view.getColumns(), tags); - this.updateMetric(view, measurement, labelValues); + this.updateMetric(view, measurement, tags); } } @@ -112,7 +111,7 @@ export class PrometheusStatsExporter implements StatsEventListener { * @param view View will be used to register the metric * @param labels Object with label keys and values */ - private registerMetric(view: View, labels: labelValues): Metric { + private registerMetric(view: View, tags: Map): Metric { const metricName = this.getPrometheusMetricName(view); /** Get metric if already registered */ let metric = this.registry.getSingleMetric(metricName); @@ -122,7 +121,6 @@ export class PrometheusStatsExporter implements StatsEventListener { } const labelNames = view.getColumns().map((tagKey) => tagKey.name); - // Create a new metric if there is no one const metricObj = {name: metricName, help: view.description, labelNames}; @@ -141,7 +139,7 @@ export class PrometheusStatsExporter implements StatsEventListener { name: metricName, help: view.description, labelNames, - buckets: this.getBoundaries(view, labels) + buckets: this.getBoundaries(view, tags) }; metric = new Histogram(distribution); break; @@ -160,20 +158,21 @@ export class PrometheusStatsExporter implements StatsEventListener { * @param measurement Measurement with the new value to update the metric */ private updateMetric( - view: View, measurement: Measurement, labels: labelValues) { - const metric = this.registerMetric(view, labels); + view: View, measurement: Measurement, tags: Map) { + const metric = this.registerMetric(view, tags); // Updating the metric based on metric instance type and aggregation type + const labelValues = this.getLabelValues(view.getColumns(), tags); if (metric instanceof Counter) { - metric.inc(labels); + metric.inc(labelValues); } else if ( view.aggregation === AggregationType.SUM && metric instanceof Gauge) { - metric.inc(labels, measurement.value); + metric.inc(labelValues, measurement.value); } else if ( view.aggregation === AggregationType.LAST_VALUE && metric instanceof Gauge) { - metric.set(labels, measurement.value); + metric.set(labelValues, measurement.value); } else if (metric instanceof Histogram) { - metric.observe(labels, measurement.value); + metric.observe(labelValues, measurement.value); } else { this.logger.error('Metric not supported'); } @@ -220,10 +219,9 @@ export class PrometheusStatsExporter implements StatsEventListener { * @param view View used to get the DistributionData * @param tags Tags used to get the DistributionData */ - private getBoundaries(view: View, labels: labelValues): number[] { - const tagValues = Object.keys(labels).map((label) => { - return {value: String(labels[label])}; - }); + private getBoundaries(view: View, tags: Map): number[] { + const tagValues = + view.getColumns().map((tagKey) => (tags.get(tagKey) || null)); const data = view.getSnapshot(tagValues) as DistributionData; return data.buckets; } diff --git a/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts b/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts index eb3735bab..ebea1d84b 100644 --- a/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts +++ b/packages/opencensus-exporter-prometheus/test/test-prometheus-stats.ts @@ -97,6 +97,32 @@ describe('Prometheus Stats Exporter', () => { }).on('error', done); }); + it('should create a sum aggregation with empty tagMap', (done) => { + const measure = + globalStats.createMeasureDouble('testMeasureDouble', MeasureUnit.UNIT); + const view = globalStats.createView( + 'ocnodemetrics/sumview1', measure, AggregationType.SUM, tagKeys, + 'A sum aggregation example', null); + const measurement = {measure, value: 2}; + const measurement2 = {measure, value: 3}; + globalStats.registerView(view); + globalStats.record([measurement, measurement2]); + + http.get(prometheusServerUrl, (res) => { + res.on('data', (chunk) => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.equal( + lines[0], + '# HELP ocnodemetrics_sumview1 A sum aggregation example'); + assert.equal(lines[1], '# TYPE ocnodemetrics_sumview1 gauge'); + assert.equal(lines[2], 'ocnodemetrics_sumview1 5'); + done(); + }); + }).on('error', done); + }); + it('should create a last value aggregation', (done) => { const measure = globalStats.createMeasureDouble('testMeasureDouble', MeasureUnit.UNIT);