diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 95be6b4e3d5..ca329878e31 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feature(add-console-metrics-exporter): add ConsoleMetricExporter [#3120](https://github.com/open-telemetry/opentelemetry-js/pull/3120) @weyert * feature(prometheus-serialiser): export the unit block when unit is set in metric descriptor [#3066](https://github.com/open-telemetry/opentelemetry-js/pull/3041) @weyert ### :bug: (Bug Fix) diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/export/ConsoleMetricExporter.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/ConsoleMetricExporter.ts new file mode 100644 index 00000000000..39b268f1930 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/ConsoleMetricExporter.ts @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { InstrumentType } from '../InstrumentDescriptor'; +import { AggregationTemporality } from './AggregationTemporality'; +import { ResourceMetrics } from './MetricData'; +import { PushMetricExporter } from './MetricExporter'; + +/* eslint-disable no-console */ +export class ConsoleMetricExporter implements PushMetricExporter { + protected _shutdown = false; + + export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void { + if (this._shutdown) { + // If the exporter is shutting down, by spec, we need to return FAILED as export result + setImmediate(resultCallback, { code: ExportResultCode.FAILED }); + return; + } + + return ConsoleMetricExporter._sendMetrics(metrics, resultCallback); + } + + forceFlush(): Promise { + return Promise.resolve(); + } + + selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality { + return AggregationTemporality.CUMULATIVE; + } + + shutdown(): Promise { + this._shutdown = true; + return Promise.resolve(); + } + + private static _sendMetrics(metrics: ResourceMetrics, done: (result: ExportResult) => void): void { + for (const scopeMetrics of metrics.scopeMetrics) { + for (const metric of scopeMetrics.metrics) { + console.dir({ + descriptor: metric.descriptor, + dataPointType: metric.dataPointType, + dataPoints: metric.dataPoints + }); + } + } + + done({ code: ExportResultCode.SUCCESS }); + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricExporter.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricExporter.ts index 217068ae9c6..782a39ccb8a 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricExporter.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricExporter.ts @@ -18,7 +18,6 @@ import { AggregationTemporality } from './AggregationTemporality'; import { ResourceMetrics } from './MetricData'; import { ExportResult, - ExportResultCode, } from '@opentelemetry/core'; import { InstrumentType } from '../InstrumentDescriptor'; @@ -54,30 +53,3 @@ export interface PushMetricExporter { */ shutdown(): Promise; } - -export class ConsoleMetricExporter implements PushMetricExporter { - protected _shutdown = true; - private _aggregationTemporality: AggregationTemporality; - - constructor(aggregationTemporality?: AggregationTemporality) { - this._aggregationTemporality = aggregationTemporality ?? AggregationTemporality.CUMULATIVE; - } - - export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void) { - return resultCallback({ - code: ExportResultCode.FAILED, - error: new Error('Method not implemented') - }); - } - - selectAggregationTemporality(_instrumentType: InstrumentType) { - return this._aggregationTemporality; - } - - // nothing to do - async forceFlush() {} - - async shutdown() { - this._shutdown = true; - } -} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts index ce8f7b67c25..e9ad1ef4b94 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts @@ -22,6 +22,7 @@ export * from './export/MetricProducer'; export * from './export/MetricReader'; export * from './export/PeriodicExportingMetricReader'; export * from './export/InMemoryMetricExporter'; +export * from './export/ConsoleMetricExporter'; export { InstrumentDescriptor, InstrumentType } from './InstrumentDescriptor'; export * from './Meter'; export * from './MeterProvider'; diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/export/ConsoleMetricExporter.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/export/ConsoleMetricExporter.test.ts new file mode 100644 index 00000000000..39edda36f1a --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/export/ConsoleMetricExporter.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as metrics from '@opentelemetry/api-metrics'; +import { ExportResult } from '@opentelemetry/core'; +import { ConsoleMetricExporter } from '../../src/export/ConsoleMetricExporter'; +import { PeriodicExportingMetricReader } from '../../src/export/PeriodicExportingMetricReader'; +import { ResourceMetrics } from '../../src/export/MetricData'; +import { MeterProvider } from '../../src/MeterProvider'; +import { defaultResource } from '../util'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; + + +async function waitForNumberOfExports(exporter: sinon.SinonSpy<[metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void], void>, numberOfExports: number): Promise { + if (numberOfExports <= 0) { + throw new Error('numberOfExports must be greater than or equal to 0'); + } + + let totalExports = 0; + while (totalExports < numberOfExports) { + await new Promise(resolve => setTimeout(resolve, 20)); + totalExports = exporter.callCount; + } +} + +/* 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 + }); + meterProvider.addMetricReader(meterReader); + }); + + 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(','); + + 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); + }); +});