From 258b677dbc2c985f96a5cc8b62d3b34276576361 Mon Sep 17 00:00:00 2001 From: Marc Pichler Date: Thu, 10 Nov 2022 21:23:50 +0100 Subject: [PATCH] feat(console-metric-exporter): add temporality configuration (#3387) * feat(console-metric-exporter): add temporality configuration * feat(console-metric-exporter): use a temporality selector instead Co-authored-by: Daniel Dyla --- experimental/CHANGELOG.md | 1 + .../src/export/ConsoleMetricExporter.ts | 20 ++- .../test/export/ConsoleMetricExporter.test.ts | 165 +++++++++++------- packages/sdk-metrics/test/export/utils.ts | 11 +- 4 files changed, 130 insertions(+), 67 deletions(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 62ccda9e0a6..6c180c18d5c 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to experimental packages in this project will be documented * feat(detectors): add browser detector module [#3292](https://github.com/open-telemetry/opentelemetry-js/pull/3292) @abinet18 * deps: remove unused proto-loader dependencies and update grpc-js and proto-loader versions [#3337](https://github.com/open-telemetry/opentelemetry-js/pull/3337) @seemk * feat(metrics-exporters): configure temporality via environment variable [#3305](https://github.com/open-telemetry/opentelemetry-js/pull/3305) @pichlermarc +* feat(console-metric-exporter): add temporality configuration [#3387](https://github.com/open-telemetry/opentelemetry-js/pull/3387) @pichlermarc ### :bug: (Bug Fix) diff --git a/packages/sdk-metrics/src/export/ConsoleMetricExporter.ts b/packages/sdk-metrics/src/export/ConsoleMetricExporter.ts index 39b268f1930..0b990dfe5e7 100644 --- a/packages/sdk-metrics/src/export/ConsoleMetricExporter.ts +++ b/packages/sdk-metrics/src/export/ConsoleMetricExporter.ts @@ -13,15 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { + ExportResult, + ExportResultCode +} from '@opentelemetry/core'; import { InstrumentType } from '../InstrumentDescriptor'; import { AggregationTemporality } from './AggregationTemporality'; import { ResourceMetrics } from './MetricData'; import { PushMetricExporter } from './MetricExporter'; +import { + AggregationTemporalitySelector, + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR, +} from './AggregationSelector'; + +interface ConsoleMetricExporterOptions { + temporalitySelector?: AggregationTemporalitySelector +} /* eslint-disable no-console */ export class ConsoleMetricExporter implements PushMetricExporter { protected _shutdown = false; + protected _temporalitySelector: AggregationTemporalitySelector; + + constructor(options?: ConsoleMetricExporterOptions) { + this._temporalitySelector = options?.temporalitySelector ?? DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR; + } export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void { if (this._shutdown) { @@ -38,7 +54,7 @@ export class ConsoleMetricExporter implements PushMetricExporter { } selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality { - return AggregationTemporality.CUMULATIVE; + return this._temporalitySelector(_instrumentType); } shutdown(): Promise { diff --git a/packages/sdk-metrics/test/export/ConsoleMetricExporter.test.ts b/packages/sdk-metrics/test/export/ConsoleMetricExporter.test.ts index 6097aeb555e..7cd8048bf98 100644 --- a/packages/sdk-metrics/test/export/ConsoleMetricExporter.test.ts +++ b/packages/sdk-metrics/test/export/ConsoleMetricExporter.test.ts @@ -22,6 +22,14 @@ import { MeterProvider } from '../../src/MeterProvider'; import { defaultResource } from '../util'; import * as assert from 'assert'; import * as sinon from 'sinon'; +import { assertAggregationTemporalitySelector } from './utils'; +import { + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR +} from '../../src/export/AggregationSelector'; +import { + AggregationTemporality, + InstrumentType +} from '../../src'; async function waitForNumberOfExports(exporter: sinon.SinonSpy<[metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void], void>, numberOfExports: number): Promise { @@ -38,71 +46,108 @@ async function waitForNumberOfExports(exporter: sinon.SinonSpy<[metrics: Resourc /* eslint-disable no-console */ describe('ConsoleMetricExporter', () => { - let previousConsoleDir: any; - let exporter: ConsoleMetricExporter; - let meterProvider: MeterProvider; - let meterReader: PeriodicExportingMetricReader; - let meter: metrics.Meter; - - beforeEach(() => { - previousConsoleDir = console.dir; - console.dir = () => {}; - - exporter = new ConsoleMetricExporter(); - meterProvider = new MeterProvider({ resource: defaultResource }); - meter = meterProvider.getMeter('ConsoleMetricExporter', '1.0.0'); - meterReader = new PeriodicExportingMetricReader({ - exporter: exporter, - exportIntervalMillis: 100, - exportTimeoutMillis: 100 + describe('export', () => { + let previousConsoleDir: any; + let exporter: ConsoleMetricExporter; + let meterProvider: MeterProvider; + let meterReader: PeriodicExportingMetricReader; + let meter: metrics.Meter; + + beforeEach(() => { + previousConsoleDir = console.dir; + console.dir = () => {}; + + exporter = new ConsoleMetricExporter(); + meterProvider = new MeterProvider({ resource: defaultResource }); + meter = meterProvider.getMeter('ConsoleMetricExporter', '1.0.0'); + meterReader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100 + }); + meterProvider.addMetricReader(meterReader); }); - meterProvider.addMetricReader(meterReader); - }); - afterEach(async () => { - console.dir = previousConsoleDir; + afterEach(async () => { + console.dir = previousConsoleDir; + + await meterReader.shutdown(); + }); + + it('should export information about metric', async () => { + const counter = meter.createCounter('counter_total', { + description: 'a test description', + }); + const counterAttribute = { key1: 'attributeValue1' }; + counter.add(10, counterAttribute); + counter.add(10, counterAttribute); + + const histogram = meter.createHistogram('histogram', { description: 'a histogram' }); + histogram.record(10); + histogram.record(100); + histogram.record(1000); + + const spyConsole = sinon.spy(console, 'dir'); + const spyExport = sinon.spy(exporter, 'export'); + + await waitForNumberOfExports(spyExport, 1); + const resourceMetrics = spyExport.args[0]; + const firstResourceMetric = resourceMetrics[0]; + const consoleArgs = spyConsole.args[0]; + const consoleMetric = consoleArgs[0]; + const keys = Object.keys(consoleMetric).sort().join(','); - await meterReader.shutdown(); + const expectedKeys = [ + 'dataPointType', + 'dataPoints', + 'descriptor', + ].join(','); + + assert.ok(firstResourceMetric.resource.attributes.resourceKey === 'my-resource', 'resourceKey'); + assert.ok(keys === expectedKeys, 'expectedKeys'); + assert.ok(consoleMetric.descriptor.name === 'counter_total', 'name'); + assert.ok(consoleMetric.descriptor.description === 'a test description', 'description'); + assert.ok(consoleMetric.descriptor.type === 'COUNTER', 'type'); + assert.ok(consoleMetric.descriptor.unit === '', 'unit'); + assert.ok(consoleMetric.descriptor.valueType === 1, 'valueType'); + assert.ok(consoleMetric.dataPoints[0].attributes.key1 === 'attributeValue1', 'ensure metric attributes exists'); + + assert.ok(spyExport.calledOnce); + }); }); - it('should export information about metric', async () => { - const counter = meter.createCounter('counter_total', { - description: 'a test description', + describe('constructor', () => { + it('with no arguments should select cumulative temporality', () => { + const exporter = new ConsoleMetricExporter(); + assertAggregationTemporalitySelector(exporter, DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR); + }); + + it('with empty options should select cumulative temporality', () => { + const exporter = new ConsoleMetricExporter({}); + assertAggregationTemporalitySelector(exporter, DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR); + }); + + it('with cumulative preference should select cumulative temporality', () => { + const exporter = new ConsoleMetricExporter({ temporalitySelector: _ => AggregationTemporality.CUMULATIVE }); + assertAggregationTemporalitySelector(exporter, _ => AggregationTemporality.CUMULATIVE); + }); + + it('with mixed preference should select matching temporality', () => { + // use delta-ish example as a representation of a commonly used "mixed" preference. + const selector = (instrumentType: InstrumentType) => { + switch (instrumentType) { + case InstrumentType.COUNTER: + case InstrumentType.OBSERVABLE_COUNTER: + case InstrumentType.HISTOGRAM: + case InstrumentType.OBSERVABLE_GAUGE: + return AggregationTemporality.DELTA; + case InstrumentType.UP_DOWN_COUNTER: + case InstrumentType.OBSERVABLE_UP_DOWN_COUNTER: + return AggregationTemporality.CUMULATIVE; + } + }; + const exporter = new ConsoleMetricExporter({ temporalitySelector: selector }); + assertAggregationTemporalitySelector(exporter, selector); }); - const counterAttribute = { key1: 'attributeValue1' }; - counter.add(10, counterAttribute); - counter.add(10, counterAttribute); - - const histogram = meter.createHistogram('histogram', { description: 'a histogram' }); - histogram.record(10); - histogram.record(100); - histogram.record(1000); - - const spyConsole = sinon.spy(console, 'dir'); - const spyExport = sinon.spy(exporter, 'export'); - - await waitForNumberOfExports(spyExport, 1); - const resourceMetrics = spyExport.args[0]; - const firstResourceMetric = resourceMetrics[0]; - const consoleArgs = spyConsole.args[0]; - const consoleMetric = consoleArgs[0]; - const keys = Object.keys(consoleMetric).sort().join(','); - - const expectedKeys = [ - 'dataPointType', - 'dataPoints', - 'descriptor', - ].join(','); - - assert.ok(firstResourceMetric.resource.attributes.resourceKey === 'my-resource', 'resourceKey'); - assert.ok(keys === expectedKeys, 'expectedKeys'); - assert.ok(consoleMetric.descriptor.name === 'counter_total', 'name'); - assert.ok(consoleMetric.descriptor.description === 'a test description', 'description'); - assert.ok(consoleMetric.descriptor.type === 'COUNTER', 'type'); - assert.ok(consoleMetric.descriptor.unit === '', 'unit'); - assert.ok(consoleMetric.descriptor.valueType === 1, 'valueType'); - assert.ok(consoleMetric.dataPoints[0].attributes.key1 === 'attributeValue1', 'ensure metric attributes exists'); - - assert.ok(spyExport.calledOnce); }); }); diff --git a/packages/sdk-metrics/test/export/utils.ts b/packages/sdk-metrics/test/export/utils.ts index cd8a9cc7ae3..3ad8543be7a 100644 --- a/packages/sdk-metrics/test/export/utils.ts +++ b/packages/sdk-metrics/test/export/utils.ts @@ -18,7 +18,8 @@ import { AggregationSelector, AggregationTemporalitySelector, InstrumentType, - MetricReader + MetricReader, + PushMetricExporter } from '../../src'; import * as assert from 'assert'; @@ -36,9 +37,9 @@ const instrumentTypes = [ * @param reader * @param expectedSelector */ -export function assertAggregationSelector(reader: MetricReader, expectedSelector: AggregationSelector) { +export function assertAggregationSelector(reader: MetricReader | PushMetricExporter, expectedSelector: AggregationSelector) { for (const instrumentType of instrumentTypes) { - assert.strictEqual(reader.selectAggregation(instrumentType), + assert.strictEqual(reader.selectAggregation?.(instrumentType), expectedSelector(instrumentType), `incorrect aggregation selection for ${InstrumentType[instrumentType]}`); } @@ -49,9 +50,9 @@ export function assertAggregationSelector(reader: MetricReader, expectedSelector * @param reader * @param expectedSelector */ -export function assertAggregationTemporalitySelector(reader: MetricReader, expectedSelector: AggregationTemporalitySelector) { +export function assertAggregationTemporalitySelector(reader: MetricReader | PushMetricExporter, expectedSelector: AggregationTemporalitySelector) { for (const instrumentType of instrumentTypes) { - assert.strictEqual(reader.selectAggregationTemporality(instrumentType), + assert.strictEqual(reader.selectAggregationTemporality?.(instrumentType), expectedSelector(instrumentType), `incorrect aggregation temporality selection for ${InstrumentType[instrumentType]}`); }