Skip to content

Commit 7086d5a

Browse files
feat(sdk-metrics-base): shutdown and forceflush on MeterProvider (#2890)
Co-authored-by: Valentin Marchaud <[email protected]>
1 parent 322dabe commit 7086d5a

File tree

7 files changed

+138
-52
lines changed

7 files changed

+138
-52
lines changed

experimental/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ All notable changes to experimental packages in this project will be documented
3838
* feat(prometheus): update prometheus exporter with wip metrics sdk #2824 @legendecas
3939
* feat(instrumentation-xhr): add applyCustomAttributesOnSpan hook #2134 @mhennoch
4040
* feat(proto): add @opentelemetry/otlp-transformer package with hand-rolled transformation #2746 @dyladan
41+
* feat(sdk-metrics-base): shutdown and forceflush on MeterProvider #2890 @legendecas
4142

4243
### :bug: (Bug Fix)
4344

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

+12-34
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Aggregation } from './view/Aggregation';
2828
import { FilteringAttributesProcessor } from './view/AttributesProcessor';
2929
import { InstrumentType } from './InstrumentDescriptor';
3030
import { PatternPredicate } from './view/Predicate';
31+
import { ForceFlushOptions, ShutdownOptions } from './types';
3132

3233
/**
3334
* MeterProviderOptions provides an interface for configuring a MeterProvider.
@@ -163,59 +164,36 @@ export class MeterProvider implements metrics.MeterProvider {
163164
/**
164165
* Flush all buffered data and shut down the MeterProvider and all registered
165166
* MetricReaders.
166-
* Returns a promise which is resolved when all flushes are complete.
167167
*
168-
* TODO: return errors to caller somehow?
168+
* Returns a promise which is resolved when all flushes are complete.
169169
*/
170-
async shutdown(): Promise<void> {
171-
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#shutdown
172-
170+
async shutdown(options?: ShutdownOptions): Promise<void> {
173171
if (this._shutdown) {
174172
api.diag.warn('shutdown may only be called once per MeterProvider');
175173
return;
176174
}
177175

178-
// TODO add a timeout - spec leaves it up the the SDK if this is configurable
179176
this._shutdown = true;
180177

181-
for (const collector of this._sharedState.metricCollectors) {
182-
try {
183-
await collector.shutdown();
184-
} catch (e) {
185-
// Log all Errors.
186-
if (e instanceof Error) {
187-
api.diag.error(`Error shutting down: ${e.message}`);
188-
}
189-
}
190-
}
178+
await Promise.all(this._sharedState.metricCollectors.map(collector => {
179+
return collector.shutdown(options);
180+
}));
191181
}
192182

193183
/**
194184
* Notifies all registered MetricReaders to flush any buffered data.
195-
* Returns a promise which is resolved when all flushes are complete.
196185
*
197-
* TODO: return errors to caller somehow?
186+
* Returns a promise which is resolved when all flushes are complete.
198187
*/
199-
async forceFlush(): Promise<void> {
200-
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#forceflush
201-
202-
// TODO add a timeout - spec leaves it up the the SDK if this is configurable
203-
188+
async forceFlush(options?: ForceFlushOptions): Promise<void> {
204189
// do not flush after shutdown
205190
if (this._shutdown) {
206-
api.diag.warn('invalid attempt to force flush after shutdown');
191+
api.diag.warn('invalid attempt to force flush after MeterProvider shutdown');
207192
return;
208193
}
209194

210-
for (const collector of this._sharedState.metricCollectors) {
211-
try {
212-
await collector.forceFlush();
213-
} catch (e) {
214-
// Log all Errors.
215-
if (e instanceof Error) {
216-
api.diag.error(`Error flushing: ${e.message}`);
217-
}
218-
}
219-
}
195+
await Promise.all(this._sharedState.metricCollectors.map(collector => {
196+
return collector.forceFlush(options);
197+
}));
220198
}
221199
}

experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricReader.ts

+7-14
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,7 @@ import { AggregationTemporality } from './AggregationTemporality';
1919
import { MetricProducer } from './MetricProducer';
2020
import { ResourceMetrics } from './MetricData';
2121
import { callWithTimeout, Maybe } from '../utils';
22-
23-
24-
export type ReaderOptions = {
25-
timeoutMillis?: number
26-
};
27-
28-
export type ReaderCollectionOptions = ReaderOptions;
29-
30-
export type ReaderShutdownOptions = ReaderOptions;
31-
32-
export type ReaderForceFlushOptions = ReaderOptions;
22+
import { CollectionOptions, ForceFlushOptions, ShutdownOptions } from '../types';
3323

3424
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricreader
3525

@@ -53,6 +43,9 @@ export abstract class MetricReader {
5343
* @param metricProducer
5444
*/
5545
setMetricProducer(metricProducer: MetricProducer) {
46+
if (this._metricProducer) {
47+
throw new Error('MetricReader can not be bound to a MeterProvider again.');
48+
}
5649
this._metricProducer = metricProducer;
5750
this.onInitialized();
5851
}
@@ -92,7 +85,7 @@ export abstract class MetricReader {
9285
/**
9386
* Collect all metrics from the associated {@link MetricProducer}
9487
*/
95-
async collect(options?: ReaderCollectionOptions): Promise<Maybe<ResourceMetrics>> {
88+
async collect(options?: CollectionOptions): Promise<Maybe<ResourceMetrics>> {
9689
if (this._metricProducer === undefined) {
9790
throw new Error('MetricReader is not bound to a MetricProducer');
9891
}
@@ -117,7 +110,7 @@ export abstract class MetricReader {
117110
* <p> NOTE: this operation will continue even after the promise rejects due to a timeout.
118111
* @param options options with timeout.
119112
*/
120-
async shutdown(options?: ReaderShutdownOptions): Promise<void> {
113+
async shutdown(options?: ShutdownOptions): Promise<void> {
121114
// Do not call shutdown again if it has already been called.
122115
if (this._shutdown) {
123116
api.diag.error('Cannot call shutdown twice.');
@@ -140,7 +133,7 @@ export abstract class MetricReader {
140133
* <p> NOTE: this operation will continue even after the promise rejects due to a timeout.
141134
* @param options options with timeout.
142135
*/
143-
async forceFlush(options?: ReaderForceFlushOptions): Promise<void> {
136+
async forceFlush(options?: ForceFlushOptions): Promise<void> {
144137
if (this._shutdown) {
145138
api.diag.warn('Cannot forceFlush on already shutdown MetricReader.');
146139
return;

experimental/packages/opentelemetry-sdk-metrics-base/src/state/MetricCollector.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { AggregationTemporality } from '../export/AggregationTemporality';
1919
import { ResourceMetrics } from '../export/MetricData';
2020
import { MetricProducer } from '../export/MetricProducer';
2121
import { MetricReader } from '../export/MetricReader';
22+
import { ForceFlushOptions, ShutdownOptions } from '../types';
2223
import { MeterProviderSharedState } from './MeterProviderSharedState';
2324

2425
/**
@@ -46,15 +47,15 @@ export class MetricCollector implements MetricProducer {
4647
/**
4748
* Delegates for MetricReader.forceFlush.
4849
*/
49-
async forceFlush(): Promise<void> {
50-
await this._metricReader.forceFlush();
50+
async forceFlush(options?: ForceFlushOptions): Promise<void> {
51+
await this._metricReader.forceFlush(options);
5152
}
5253

5354
/**
5455
* Delegates for MetricReader.shutdown.
5556
*/
56-
async shutdown(): Promise<void> {
57-
await this._metricReader.shutdown();
57+
async shutdown(options?: ShutdownOptions): Promise<void> {
58+
await this._metricReader.shutdown(options);
5859
}
5960
}
6061

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
export type CommonReaderOptions = {
18+
timeoutMillis?: number
19+
};
20+
21+
export type CollectionOptions = CommonReaderOptions;
22+
23+
export type ShutdownOptions = CommonReaderOptions;
24+
25+
export type ForceFlushOptions = CommonReaderOptions;

experimental/packages/opentelemetry-sdk-metrics-base/test/MeterProvider.test.ts

+54
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ import {
2424
defaultResource
2525
} from './util';
2626
import { TestMetricReader } from './export/TestMetricReader';
27+
import * as sinon from 'sinon';
2728

2829
describe('MeterProvider', () => {
30+
afterEach(() => {
31+
sinon.restore();
32+
});
33+
2934
describe('constructor', () => {
3035
it('should construct without exceptions', () => {
3136
const meterProvider = new MeterProvider();
@@ -422,4 +427,53 @@ describe('MeterProvider', () => {
422427
});
423428
});
424429
});
430+
431+
describe('shutdown', () => {
432+
it('should shutdown all registered metric readers', async () => {
433+
const meterProvider = new MeterProvider({ resource: defaultResource });
434+
const reader1 = new TestMetricReader();
435+
const reader2 = new TestMetricReader();
436+
const reader1ShutdownSpy = sinon.spy(reader1, 'shutdown');
437+
const reader2ShutdownSpy = sinon.spy(reader2, 'shutdown');
438+
439+
meterProvider.addMetricReader(reader1);
440+
meterProvider.addMetricReader(reader2);
441+
442+
await meterProvider.shutdown({ timeoutMillis: 1234 });
443+
await meterProvider.shutdown();
444+
await meterProvider.shutdown();
445+
446+
assert.strictEqual(reader1ShutdownSpy.callCount, 1);
447+
assert.deepStrictEqual(reader1ShutdownSpy.args[0][0], { timeoutMillis: 1234 });
448+
assert.strictEqual(reader2ShutdownSpy.callCount, 1);
449+
assert.deepStrictEqual(reader2ShutdownSpy.args[0][0], { timeoutMillis: 1234 });
450+
});
451+
});
452+
453+
describe('forceFlush', () => {
454+
it('should forceFlush all registered metric readers', async () => {
455+
const meterProvider = new MeterProvider({ resource: defaultResource });
456+
const reader1 = new TestMetricReader();
457+
const reader2 = new TestMetricReader();
458+
const reader1ForceFlushSpy = sinon.spy(reader1, 'forceFlush');
459+
const reader2ForceFlushSpy = sinon.spy(reader2, 'forceFlush');
460+
461+
meterProvider.addMetricReader(reader1);
462+
meterProvider.addMetricReader(reader2);
463+
464+
await meterProvider.forceFlush({ timeoutMillis: 1234 });
465+
await meterProvider.forceFlush({ timeoutMillis: 5678 });
466+
assert.strictEqual(reader1ForceFlushSpy.callCount, 2);
467+
assert.deepStrictEqual(reader1ForceFlushSpy.args[0][0], { timeoutMillis: 1234 });
468+
assert.deepStrictEqual(reader1ForceFlushSpy.args[1][0], { timeoutMillis: 5678 });
469+
assert.strictEqual(reader2ForceFlushSpy.callCount, 2);
470+
assert.deepStrictEqual(reader2ForceFlushSpy.args[0][0], { timeoutMillis: 1234 });
471+
assert.deepStrictEqual(reader2ForceFlushSpy.args[1][0], { timeoutMillis: 5678 });
472+
473+
await meterProvider.shutdown();
474+
await meterProvider.forceFlush();
475+
assert.strictEqual(reader1ForceFlushSpy.callCount, 2);
476+
assert.strictEqual(reader2ForceFlushSpy.callCount, 2);
477+
});
478+
});
425479
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 * as assert from 'assert';
18+
import { MeterProvider } from '../../src/MeterProvider';
19+
import { TestMetricReader } from './TestMetricReader';
20+
21+
22+
describe('MetricReader', () => {
23+
describe('setMetricProducer', () => {
24+
it('The SDK MUST NOT allow a MetricReader instance to be registered on more than one MeterProvider instance', () => {
25+
const reader = new TestMetricReader();
26+
const meterProvider1 = new MeterProvider();
27+
const meterProvider2 = new MeterProvider();
28+
29+
meterProvider1.addMetricReader(reader);
30+
assert.throws(() => meterProvider1.addMetricReader(reader), /MetricReader can not be bound to a MeterProvider again/);
31+
assert.throws(() => meterProvider2.addMetricReader(reader), /MetricReader can not be bound to a MeterProvider again/);
32+
});
33+
});
34+
});

0 commit comments

Comments
 (0)