Skip to content

Commit

Permalink
feat: add InMemoryMetricExporter (#3039)
Browse files Browse the repository at this point in the history
* 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 <weyert.deboer@tapico.io>
Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 26, 2022
1 parent 1d0eaef commit 5ae0a37
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 0 deletions.
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
return Promise.resolve();
}

reset() {
this._metrics = [];
}

selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality {
return this._aggregationTemporality;
}

shutdown(): Promise<void> {
this._shutdown = true;
return Promise.resolve();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ResourceMetrics[]> {
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();
});
});

0 comments on commit 5ae0a37

Please sign in to comment.