Skip to content

Commit 5ae0a37

Browse files
weyerttapico-weyertdyladan
authored
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 <[email protected]> Co-authored-by: Daniel Dyla <[email protected]>
1 parent 1d0eaef commit 5ae0a37

File tree

4 files changed

+225
-0
lines changed

4 files changed

+225
-0
lines changed

experimental/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ All notable changes to experimental packages in this project will be documented
1919

2020
* feat(metrics-api): use common attributes definitions #3038 @legendecas
2121
* feat(otlp-proto): pre-compile proto files [#3098](https://github.com/open-telemetry/opentelemetry-js/pull/3098) @legendecas
22+
* feat(opentelemetry-sdk-metrics-base): added InMemoryMetricExporter [#3039](https://github.com/open-telemetry/opentelemetry-js/pull/3039) @weyert
2223

2324
### :bug: (Bug Fix)
2425

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ExportResultCode } from '@opentelemetry/core';
18+
import { ExportResult } from '@opentelemetry/core';
19+
import { InstrumentType } from '../InstrumentDescriptor';
20+
import { AggregationTemporality } from './AggregationTemporality';
21+
import { ResourceMetrics } from './MetricData';
22+
import { PushMetricExporter } from './MetricExporter';
23+
24+
/**
25+
* In-memory Metrics Exporter is a Push Metric Exporter
26+
* which accumulates metrics data in the local memory and
27+
* allows to inspect it (useful for e.g. unit tests).
28+
*/
29+
export class InMemoryMetricExporter implements PushMetricExporter {
30+
protected _shutdown = false;
31+
protected _aggregationTemporality: AggregationTemporality;
32+
private _metrics: ResourceMetrics[] = [];
33+
34+
constructor(aggregationTemporality: AggregationTemporality) {
35+
this._aggregationTemporality = aggregationTemporality;
36+
}
37+
38+
/**
39+
* @inheritedDoc
40+
*/
41+
export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void {
42+
// Avoid storing metrics when exporter is shutdown
43+
if (this. _shutdown) {
44+
setTimeout(() => resultCallback({ code: ExportResultCode.FAILED }), 0);
45+
return;
46+
}
47+
48+
this._metrics.push(metrics);
49+
setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0);
50+
}
51+
52+
/**
53+
* Returns all the collected resource metrics
54+
* @returns ResourceMetrics[]
55+
*/
56+
public getMetrics(): ResourceMetrics[] {
57+
return this._metrics;
58+
}
59+
60+
forceFlush(): Promise<void> {
61+
return Promise.resolve();
62+
}
63+
64+
reset() {
65+
this._metrics = [];
66+
}
67+
68+
selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality {
69+
return this._aggregationTemporality;
70+
}
71+
72+
shutdown(): Promise<void> {
73+
this._shutdown = true;
74+
return Promise.resolve();
75+
}
76+
}

experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './export/MetricExporter';
2121
export * from './export/MetricProducer';
2222
export * from './export/MetricReader';
2323
export * from './export/PeriodicExportingMetricReader';
24+
export * from './export/InMemoryMetricExporter';
2425
export { InstrumentDescriptor, InstrumentType } from './InstrumentDescriptor';
2526
export * from './Meter';
2627
export * from './MeterProvider';
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { ExportResultCode } from '@opentelemetry/core';
17+
import { Resource } from '@opentelemetry/resources';
18+
import * as metrics from '@opentelemetry/api-metrics';
19+
import assert = require('assert');
20+
import { AggregationTemporality } from '../../src/export/AggregationTemporality';
21+
import { InMemoryMetricExporter } from '../../src/export/InMemoryMetricExporter';
22+
import { ResourceMetrics } from '../../src/export/MetricData';
23+
import { PeriodicExportingMetricReader } from '../../src/export/PeriodicExportingMetricReader';
24+
import { MeterProvider } from '../../src/MeterProvider';
25+
import { defaultResource } from '../util';
26+
27+
async function waitForNumberOfExports(exporter: InMemoryMetricExporter , numberOfExports: number): Promise<ResourceMetrics[]> {
28+
if (numberOfExports <= 0) {
29+
throw new Error('numberOfExports must be greater than or equal to 0');
30+
}
31+
32+
let totalExports = 0;
33+
while (totalExports < numberOfExports) {
34+
await new Promise(resolve => setTimeout(resolve, 20));
35+
const exportedMetrics = exporter.getMetrics();
36+
totalExports = exportedMetrics.length;
37+
}
38+
39+
return exporter.getMetrics();
40+
}
41+
42+
describe('InMemoryMetricExporter', () => {
43+
let exporter: InMemoryMetricExporter;
44+
let meterProvider: MeterProvider;
45+
let meterReader: PeriodicExportingMetricReader;
46+
let meter: metrics.Meter;
47+
48+
beforeEach(() => {
49+
exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
50+
meterProvider = new MeterProvider({ resource: defaultResource });
51+
meter = meterProvider.getMeter('InMemoryMetricExporter', '1.0.0');
52+
meterReader = new PeriodicExportingMetricReader({
53+
exporter: exporter,
54+
exportIntervalMillis: 100,
55+
exportTimeoutMillis: 100
56+
});
57+
meterProvider.addMetricReader(meterReader);
58+
});
59+
60+
afterEach(async () => {
61+
await exporter.shutdown();
62+
await meterReader.shutdown();
63+
});
64+
65+
it('should return failed result code', done => {
66+
exporter.shutdown().then(() => {
67+
const resource = new Resource({
68+
'resource-attribute': 'resource attribute value',
69+
});
70+
const resourceMetrics: ResourceMetrics = {
71+
resource: resource,
72+
scopeMetrics:
73+
[
74+
{
75+
scope: {
76+
name: 'mylib',
77+
version: '0.1.0',
78+
schemaUrl: 'http://url.to.schema'
79+
},
80+
metrics: [],
81+
}
82+
]
83+
};
84+
exporter.export(resourceMetrics, result => {
85+
assert.ok(result.code === ExportResultCode.FAILED);
86+
meterReader.shutdown().then(() => {
87+
done();
88+
});
89+
});
90+
});
91+
});
92+
93+
it('should reset metrics when reset is called', async () => {
94+
const counter = meter.createCounter('counter_total', {
95+
description: 'a test description',
96+
});
97+
const counterAttribute = { key1: 'attributeValue1' };
98+
counter.add(10, counterAttribute);
99+
100+
const exportedMetrics = await waitForNumberOfExports(exporter, 1);
101+
assert.ok(exportedMetrics.length > 0);
102+
103+
exporter.reset();
104+
105+
const otherMetrics = exporter.getMetrics();
106+
assert.ok(otherMetrics.length === 0);
107+
108+
await exporter.shutdown();
109+
await meterReader.shutdown();
110+
});
111+
112+
it('should be able to access metric', async () => {
113+
const counter = meter.createCounter('counter_total', {
114+
description: 'a test description',
115+
});
116+
const counterAttribute = { key1: 'attributeValue1' };
117+
counter.add(10, counterAttribute);
118+
counter.add(10, counterAttribute);
119+
120+
const histogram = meter.createHistogram('histogram', { description: 'a histogram' });
121+
histogram.record(10);
122+
histogram.record(100);
123+
histogram.record(1000);
124+
125+
const exportedMetrics = await waitForNumberOfExports(exporter, 1);
126+
assert.ok(exportedMetrics.length > 0);
127+
128+
const resourceMetrics = exportedMetrics.shift();
129+
assert.ok(resourceMetrics);
130+
const firstScopeMetric = resourceMetrics?.scopeMetrics.shift();
131+
assert.ok(firstScopeMetric);
132+
assert.ok(firstScopeMetric.metrics.length > 0);
133+
const [counterMetric, histogramMetric] = firstScopeMetric.metrics;
134+
assert.ok(counterMetric.descriptor.name, 'counter_total');
135+
assert.ok(counterMetric.dataPoints.length > 0);
136+
const counterDataPoint = counterMetric.dataPoints.shift();
137+
assert.ok(counterDataPoint);
138+
assert.strictEqual(counterDataPoint.attributes, counterAttribute);
139+
140+
assert.ok(histogramMetric.descriptor.name, 'histogram');
141+
assert.ok(histogramMetric.dataPoints.length > 0);
142+
const histogramDataPoint = histogramMetric.dataPoints.shift();
143+
assert.ok(histogramDataPoint);
144+
145+
await meterReader.shutdown();
146+
});
147+
});

0 commit comments

Comments
 (0)