From 5ae0a37ddb793c97278b649e010480d387825e25 Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Tue, 26 Jul 2022 13:38:28 +0100 Subject: [PATCH] feat: add InMemoryMetricExporter (#3039) * feat: add InMemoryMetricExporter Introduces the `InMemoryMetricExporter`-class which collects metrics and stores it in memory * style: improve comments * style: improve comments * test: created test for the `InMemoryMetricExporter`-class * fix: remove the `clear`-method from `InMemoryMetricExporter`-class * fix: add missing `ResourceMetrics` import * style: ran `lint:fix` command on the code base * docs: add CHANGELOG.md entry for the new `InMemoryMetricExporter`-class * test: drop the `describe.only` * test: improve test coverage * Split reset and forceFlush * fix: remove meterprovider typo * Remove calls to missing setup method * Use api meter in test * Browser support * style: lint Co-authored-by: Weyert de Boer Co-authored-by: Daniel Dyla --- experimental/CHANGELOG.md | 1 + .../src/export/InMemoryMetricExporter.ts | 76 +++++++++ .../src/index.ts | 1 + .../export/InMemoryMetricExporter.test.ts | 147 ++++++++++++++++++ 4 files changed, 225 insertions(+) create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/export/InMemoryMetricExporter.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/test/export/InMemoryMetricExporter.test.ts diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 11fabd1165..b53d775435 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to experimental packages in this project will be documented * feat(metrics-api): use common attributes definitions #3038 @legendecas * feat(otlp-proto): pre-compile proto files [#3098](https://github.com/open-telemetry/opentelemetry-js/pull/3098) @legendecas +* feat(opentelemetry-sdk-metrics-base): added InMemoryMetricExporter [#3039](https://github.com/open-telemetry/opentelemetry-js/pull/3039) @weyert ### :bug: (Bug Fix) diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/export/InMemoryMetricExporter.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/InMemoryMetricExporter.ts new file mode 100644 index 0000000000..c0ec67e74a --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/InMemoryMetricExporter.ts @@ -0,0 +1,76 @@ +/* + * 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 { ExportResultCode } from '@opentelemetry/core'; +import { ExportResult } from '@opentelemetry/core'; +import { InstrumentType } from '../InstrumentDescriptor'; +import { AggregationTemporality } from './AggregationTemporality'; +import { ResourceMetrics } from './MetricData'; +import { PushMetricExporter } from './MetricExporter'; + +/** + * In-memory Metrics Exporter is a Push Metric Exporter + * which accumulates metrics data in the local memory and + * allows to inspect it (useful for e.g. unit tests). + */ +export class InMemoryMetricExporter implements PushMetricExporter { + protected _shutdown = false; + protected _aggregationTemporality: AggregationTemporality; + private _metrics: ResourceMetrics[] = []; + + constructor(aggregationTemporality: AggregationTemporality) { + this._aggregationTemporality = aggregationTemporality; + } + + /** + * @inheritedDoc + */ + export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void { + // Avoid storing metrics when exporter is shutdown + if (this. _shutdown) { + setTimeout(() => resultCallback({ code: ExportResultCode.FAILED }), 0); + return; + } + + this._metrics.push(metrics); + setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0); + } + + /** + * Returns all the collected resource metrics + * @returns ResourceMetrics[] + */ + public getMetrics(): ResourceMetrics[] { + return this._metrics; + } + + forceFlush(): Promise { + return Promise.resolve(); + } + + reset() { + this._metrics = []; + } + + selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality { + return this._aggregationTemporality; + } + + shutdown(): Promise { + this._shutdown = true; + return Promise.resolve(); + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts index e60bb31dae..ce8f7b67c2 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts @@ -21,6 +21,7 @@ export * from './export/MetricExporter'; export * from './export/MetricProducer'; export * from './export/MetricReader'; export * from './export/PeriodicExportingMetricReader'; +export * from './export/InMemoryMetricExporter'; export { InstrumentDescriptor, InstrumentType } from './InstrumentDescriptor'; export * from './Meter'; export * from './MeterProvider'; diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/export/InMemoryMetricExporter.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/export/InMemoryMetricExporter.test.ts new file mode 100644 index 0000000000..7afb3299a4 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/export/InMemoryMetricExporter.test.ts @@ -0,0 +1,147 @@ +/* + * 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 { ExportResultCode } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import * as metrics from '@opentelemetry/api-metrics'; +import assert = require('assert'); +import { AggregationTemporality } from '../../src/export/AggregationTemporality'; +import { InMemoryMetricExporter } from '../../src/export/InMemoryMetricExporter'; +import { ResourceMetrics } from '../../src/export/MetricData'; +import { PeriodicExportingMetricReader } from '../../src/export/PeriodicExportingMetricReader'; +import { MeterProvider } from '../../src/MeterProvider'; +import { defaultResource } from '../util'; + +async function waitForNumberOfExports(exporter: InMemoryMetricExporter , 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)); + const exportedMetrics = exporter.getMetrics(); + totalExports = exportedMetrics.length; + } + + return exporter.getMetrics(); +} + +describe('InMemoryMetricExporter', () => { + let exporter: InMemoryMetricExporter; + let meterProvider: MeterProvider; + let meterReader: PeriodicExportingMetricReader; + let meter: metrics.Meter; + + beforeEach(() => { + exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); + meterProvider = new MeterProvider({ resource: defaultResource }); + meter = meterProvider.getMeter('InMemoryMetricExporter', '1.0.0'); + meterReader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100 + }); + meterProvider.addMetricReader(meterReader); + }); + + afterEach(async () => { + await exporter.shutdown(); + await meterReader.shutdown(); + }); + + it('should return failed result code', done => { + exporter.shutdown().then(() => { + const resource = new Resource({ + 'resource-attribute': 'resource attribute value', + }); + const resourceMetrics: ResourceMetrics = { + resource: resource, + scopeMetrics: + [ + { + scope: { + name: 'mylib', + version: '0.1.0', + schemaUrl: 'http://url.to.schema' + }, + metrics: [], + } + ] + }; + exporter.export(resourceMetrics, result => { + assert.ok(result.code === ExportResultCode.FAILED); + meterReader.shutdown().then(() => { + done(); + }); + }); + }); + }); + + it('should reset metrics when reset is called', async () => { + const counter = meter.createCounter('counter_total', { + description: 'a test description', + }); + const counterAttribute = { key1: 'attributeValue1' }; + counter.add(10, counterAttribute); + + const exportedMetrics = await waitForNumberOfExports(exporter, 1); + assert.ok(exportedMetrics.length > 0); + + exporter.reset(); + + const otherMetrics = exporter.getMetrics(); + assert.ok(otherMetrics.length === 0); + + await exporter.shutdown(); + await meterReader.shutdown(); + }); + + it('should be able to access 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 exportedMetrics = await waitForNumberOfExports(exporter, 1); + assert.ok(exportedMetrics.length > 0); + + const resourceMetrics = exportedMetrics.shift(); + assert.ok(resourceMetrics); + const firstScopeMetric = resourceMetrics?.scopeMetrics.shift(); + assert.ok(firstScopeMetric); + assert.ok(firstScopeMetric.metrics.length > 0); + const [counterMetric, histogramMetric] = firstScopeMetric.metrics; + assert.ok(counterMetric.descriptor.name, 'counter_total'); + assert.ok(counterMetric.dataPoints.length > 0); + const counterDataPoint = counterMetric.dataPoints.shift(); + assert.ok(counterDataPoint); + assert.strictEqual(counterDataPoint.attributes, counterAttribute); + + assert.ok(histogramMetric.descriptor.name, 'histogram'); + assert.ok(histogramMetric.dataPoints.length > 0); + const histogramDataPoint = histogramMetric.dataPoints.shift(); + assert.ok(histogramDataPoint); + + await meterReader.shutdown(); + }); +});