From b884eeca2e4cd6ff544ea44b8f7e4bd4027ceed0 Mon Sep 17 00:00:00 2001 From: Jonah Rosenblum Date: Mon, 17 Aug 2020 23:01:47 +0000 Subject: [PATCH] Add Graceful Shutdown Support (#1321) * feat: graceful shutdown for tracing and metrics * fix: wording in test case * fix: typo * fix meterprovider config to use bracket notation Co-authored-by: Daniel Dyla * fix meterprovider config to use bracket notation Co-authored-by: Daniel Dyla * fix: add callbacks to shutdown methods * fix: merge conflict * simplify meter shutdown code Co-authored-by: Daniel Dyla * fix: fix one-liner * private function name style fix Co-authored-by: Daniel Dyla * fix: naming of private member variables * fix: graceful shutdown now works in browser * fix: window event listener will trigger once * fix: modify global shutdown helper functions * fix: remove callback from remove listener args * fix: change global shutdown function names and simplify functionality * fix: add rest of function refactoring and simplification * fix: remove unintended code snippet * fix: refactor naming of listener cleanup function and fix sandbox issue * fix: make global shutdown cleanup local * fix: change interval of MeterProvider collection to ensure it does not trigger through clock * chore: removing _cleanupGlobalShutdownListeners * fix: remove unnecesary trace provider member function * Removing default span attributes (#1342) * refactor(opentelemetry-tracing): removing default span attributes Signed-off-by: Aravin Sivakumar * refactor(opentelemetry-tracing): removing default span attributed from tracer object Signed-off-by: Aravin Sivakumar * refactor(opentelemetry-tracing): removing accidental add to package.json Signed-off-by: Aravin Sivakumar * refactor(opentelemetry-tracing): removing redundant test and fixing suggestions by Shawn and Daniel Signed-off-by: Aravin Sivakumar * feat: add baggage support to the opentracing shim (#918) Co-authored-by: Mayur Kale * Add nodejs sdk package (#1187) Co-authored-by: Naseem Co-authored-by: legendecas Co-authored-by: Mark Wolff Co-authored-by: Matthew Wear * feat: add OTEL_LOG_LEVEL env var (#974) * Proto update to latest to support arrays and maps (#1339) * chore: 0.10.0 release proposal (#1345) * fix: add missing grpc-js index (#1358) * chore: 0.10.1 release proposal (#1359) * feat(api/context-base): change compile target to es5 (#1368) * Feat: Make ID generator configurable (#1331) Co-authored-by: Daniel Dyla * fix: require grpc-js instead of grpc in grpc-js example (#1364) Co-authored-by: Bartlomiej Obecny * chore(deps): update all non-major dependencies (#1371) * chore: bump metapackage dependencies (#1383) * chore: 0.10.2 proposal (#1382) * fix: remove unnecesary trace provider member function * refactor(metrics): distinguish different aggregator types (#1325) Co-authored-by: Daniel Dyla * Propagate b3 parentspanid and debug flag (#1346) * feat: Export MinMaxLastSumCountAggregator metrics to the collector as Summary (#1320) Co-authored-by: Daniel Dyla * feat: Collector Metric Exporter for the Web (#1308) Co-authored-by: Daniel Dyla * Fix issues in TypeScript getting started example code (#1374) Co-authored-by: Daniel Dyla * chore: deploy canary releases (#1384) * fix: protos pull * fix: address marius' feedback * chore: deleting removeAllListeners from prometheus, fixing tests, cleanu of events when using shutdown notifier * fix: add documentation and cleanup code * fix: remove async label from shutdown and cleanup test case * fix: update controller collect to return promise * fix: make downsides of disabling graceful shutdown more apparent Co-authored-by: Daniel Dyla Co-authored-by: Bartlomiej Obecny Co-authored-by: Aravin <34178459+aravinsiva@users.noreply.github.com> Co-authored-by: Ruben Vargas Palma Co-authored-by: Mayur Kale Co-authored-by: Naseem Co-authored-by: legendecas Co-authored-by: Mark Wolff Co-authored-by: Matthew Wear Co-authored-by: Naseem Co-authored-by: Mark Wolff Co-authored-by: Cong Zou <32532612+EdZou@users.noreply.github.com> Co-authored-by: Reginald McDonald <40721169+reggiemcdonald@users.noreply.github.com> Co-authored-by: WhiteSource Renovate Co-authored-by: srjames90 Co-authored-by: David W Co-authored-by: Mick Dekkers --- .../src/platform/browser/ShutdownNotifier.ts | 32 ++++++++ .../src/platform/browser/index.ts | 1 + .../src/platform/node/ShutdownNotifier.ts | 32 ++++++++ .../src/platform/node/index.ts | 1 + .../test/prometheus.test.ts | 82 ++++++++++++++++++- packages/opentelemetry-metrics/src/Meter.ts | 7 +- .../src/MeterProvider.ts | 27 +++++- .../src/export/Controller.ts | 24 ++++-- packages/opentelemetry-metrics/src/types.ts | 4 + .../test/MeterProvider.test.ts | 78 +++++++++++++++++- .../src/BasicTracerProvider.ts | 19 +++++ packages/opentelemetry-tracing/src/config.ts | 1 + packages/opentelemetry-tracing/src/types.ts | 3 + .../test/BasicTracerProvider.test.ts | 54 ++++++++++++ .../test/MultiSpanProcessor.test.ts | 57 +++++++++++++ .../test/export/BatchSpanProcessor.test.ts | 1 - .../test/export/InMemorySpanExporter.test.ts | 12 +-- 17 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 packages/opentelemetry-core/src/platform/browser/ShutdownNotifier.ts create mode 100644 packages/opentelemetry-core/src/platform/node/ShutdownNotifier.ts diff --git a/packages/opentelemetry-core/src/platform/browser/ShutdownNotifier.ts b/packages/opentelemetry-core/src/platform/browser/ShutdownNotifier.ts new file mode 100644 index 00000000000..05ccc38e011 --- /dev/null +++ b/packages/opentelemetry-core/src/platform/browser/ShutdownNotifier.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/** + * Adds an event listener to trigger a callback when an unload event in the window is detected + */ +export function notifyOnGlobalShutdown(cb: () => void): () => void { + window.addEventListener('unload', cb, { once: true }); + return function removeCallbackFromGlobalShutdown() { + window.removeEventListener('unload', cb, false); + }; +} + +/** + * Warning: meant for internal use only! Closes the current window, triggering the unload event + */ +export function _invokeGlobalShutdown() { + window.close(); +} diff --git a/packages/opentelemetry-core/src/platform/browser/index.ts b/packages/opentelemetry-core/src/platform/browser/index.ts index 85023842c4d..e14dac0d3bb 100644 --- a/packages/opentelemetry-core/src/platform/browser/index.ts +++ b/packages/opentelemetry-core/src/platform/browser/index.ts @@ -21,3 +21,4 @@ export * from './RandomIdGenerator'; export * from './performance'; export * from './sdk-info'; export * from './timer-util'; +export * from './ShutdownNotifier'; diff --git a/packages/opentelemetry-core/src/platform/node/ShutdownNotifier.ts b/packages/opentelemetry-core/src/platform/node/ShutdownNotifier.ts new file mode 100644 index 00000000000..f9868105aff --- /dev/null +++ b/packages/opentelemetry-core/src/platform/node/ShutdownNotifier.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/** + * Adds an event listener to trigger a callback when a SIGTERM is detected in the process + */ +export function notifyOnGlobalShutdown(cb: () => void): () => void { + process.once('SIGTERM', cb); + return function removeCallbackFromGlobalShutdown() { + process.removeListener('SIGTERM', cb); + }; +} + +/** + * Warning: meant for internal use only! Sends a SIGTERM to the current process + */ +export function _invokeGlobalShutdown() { + process.kill(process.pid, 'SIGTERM'); +} diff --git a/packages/opentelemetry-core/src/platform/node/index.ts b/packages/opentelemetry-core/src/platform/node/index.ts index 85023842c4d..e14dac0d3bb 100644 --- a/packages/opentelemetry-core/src/platform/node/index.ts +++ b/packages/opentelemetry-core/src/platform/node/index.ts @@ -21,3 +21,4 @@ export * from './RandomIdGenerator'; export * from './performance'; export * from './sdk-info'; export * from './timer-util'; +export * from './ShutdownNotifier'; diff --git a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts index 1091ac0482c..e642aba620a 100644 --- a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts +++ b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts @@ -15,6 +15,10 @@ */ import { HrTime, ObserverResult } from '@opentelemetry/api'; +import { + notifyOnGlobalShutdown, + _invokeGlobalShutdown, +} from '@opentelemetry/core'; import { CounterMetric, SumAggregator, @@ -32,6 +36,7 @@ const mockedTimeMS = 1586347902211000; describe('PrometheusExporter', () => { let toPoint: () => Point; + let removeEvent: Function | undefined; before(() => { toPoint = SumAggregator.prototype.toPoint; SumAggregator.prototype.toPoint = function (): Point { @@ -185,16 +190,27 @@ describe('PrometheusExporter', () => { describe('export', () => { let exporter: PrometheusExporter; + let meterProvider: MeterProvider; let meter: Meter; beforeEach(done => { exporter = new PrometheusExporter(); - meter = new MeterProvider().getMeter('test-prometheus'); + meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: true, + }); + meter = meterProvider.getMeter('test-prometheus', '1', { + exporter: exporter, + }); exporter.startServer(done); }); afterEach(done => { exporter.shutdown(done); + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } }); it('should export a count aggregation', done => { @@ -320,6 +336,70 @@ describe('PrometheusExporter', () => { }); }); + it('should export multiple labels on graceful shutdown', done => { + const counter = meter.createCounter('counter', { + description: 'a test description', + }) as CounterMetric; + + counter.bind({ counterKey1: 'labelValue1' }).add(10); + counter.bind({ counterKey1: 'labelValue2' }).add(20); + counter.bind({ counterKey1: 'labelValue3' }).add(30); + + removeEvent = notifyOnGlobalShutdown(() => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepStrictEqual(lines, [ + '# HELP counter a test description', + '# TYPE counter counter', + `counter{counterKey1="labelValue1"} 10 ${mockedTimeMS}`, + `counter{counterKey1="labelValue2"} 20 ${mockedTimeMS}`, + `counter{counterKey1="labelValue3"} 30 ${mockedTimeMS}`, + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + _invokeGlobalShutdown(); + }); + + it('should export multiple labels on manual shutdown', done => { + const counter = meter.createCounter('counter', { + description: 'a test description', + }) as CounterMetric; + + counter.bind({ counterKey1: 'labelValue1' }).add(10); + counter.bind({ counterKey1: 'labelValue2' }).add(20); + counter.bind({ counterKey1: 'labelValue3' }).add(30); + meterProvider.shutdown(() => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepStrictEqual(lines, [ + '# HELP counter a test description', + '# TYPE counter counter', + `counter{counterKey1="labelValue1"} 10 ${mockedTimeMS}`, + `counter{counterKey1="labelValue2"} 20 ${mockedTimeMS}`, + `counter{counterKey1="labelValue3"} 30 ${mockedTimeMS}`, + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + it('should export a comment if no metrics are registered', done => { exporter.export([], () => { http diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index e6c33621137..61261fea014 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -40,6 +40,7 @@ export class Meter implements api.Meter { private readonly _batcher: Batcher; private readonly _resource: Resource; private readonly _instrumentationLibrary: InstrumentationLibrary; + private readonly _controller: PushController; /** * Constructs a new Meter instance. @@ -55,7 +56,7 @@ export class Meter implements api.Meter { // start the push controller const exporter = config.exporter || new NoopExporter(); const interval = config.interval; - new PushController(this, exporter, interval); + this._controller = new PushController(this, exporter, interval); } /** @@ -309,6 +310,10 @@ export class Meter implements api.Meter { return this._batcher; } + async shutdown(): Promise { + await this._controller.shutdown(); + } + /** * Registers metric to register. * @param name The name of the metric. diff --git a/packages/opentelemetry-metrics/src/MeterProvider.ts b/packages/opentelemetry-metrics/src/MeterProvider.ts index b178593dfa4..349d3514d57 100644 --- a/packages/opentelemetry-metrics/src/MeterProvider.ts +++ b/packages/opentelemetry-metrics/src/MeterProvider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ConsoleLogger } from '@opentelemetry/core'; +import { ConsoleLogger, notifyOnGlobalShutdown } from '@opentelemetry/core'; import * as api from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { Meter } from '.'; @@ -26,6 +26,7 @@ import { DEFAULT_CONFIG, MeterConfig } from './types'; export class MeterProvider implements api.MeterProvider { private readonly _config: MeterConfig; private readonly _meters: Map = new Map(); + private _cleanNotifyOnGlobalShutdown: Function | undefined; readonly resource: Resource; readonly logger: api.Logger; @@ -36,6 +37,11 @@ export class MeterProvider implements api.MeterProvider { logger: this.logger, resource: this.resource, }); + if (this._config.gracefulShutdown) { + this._cleanNotifyOnGlobalShutdown = notifyOnGlobalShutdown( + this._shutdownAllMeters.bind(this) + ); + } } /** @@ -54,4 +60,23 @@ export class MeterProvider implements api.MeterProvider { return this._meters.get(key)!; } + + shutdown(cb: () => void = () => {}): void { + this._shutdownAllMeters().then(() => { + setTimeout(cb, 0); + }); + if (this._cleanNotifyOnGlobalShutdown) { + this._cleanNotifyOnGlobalShutdown(); + this._cleanNotifyOnGlobalShutdown = undefined; + } + } + + private _shutdownAllMeters() { + if (this._config.exporter) { + this._config.exporter.shutdown(); + } + return Promise.all( + Array.from(this._meters, ([_, meter]) => meter.shutdown()) + ); + } } diff --git a/packages/opentelemetry-metrics/src/export/Controller.ts b/packages/opentelemetry-metrics/src/export/Controller.ts index 0b63ba12cf0..2c48b73820f 100644 --- a/packages/opentelemetry-metrics/src/export/Controller.ts +++ b/packages/opentelemetry-metrics/src/export/Controller.ts @@ -38,12 +38,24 @@ export class PushController extends Controller { unrefTimer(this._timer); } - private _collect() { - this._meter.collect(); - this._exporter.export(this._meter.getBatcher().checkPointSet(), result => { - if (result !== ExportResult.SUCCESS) { - // @todo: log error - } + async shutdown(): Promise { + await this._collect(); + } + + private async _collect(): Promise { + await this._meter.collect(); + return new Promise((resolve, reject) => { + this._exporter.export( + this._meter.getBatcher().checkPointSet(), + result => { + if (result === ExportResult.SUCCESS) { + resolve(); + } else { + // @todo log error + reject(); + } + } + ); }); } } diff --git a/packages/opentelemetry-metrics/src/types.ts b/packages/opentelemetry-metrics/src/types.ts index cc01af9f8d9..87e07df10f3 100644 --- a/packages/opentelemetry-metrics/src/types.ts +++ b/packages/opentelemetry-metrics/src/types.ts @@ -39,11 +39,15 @@ export interface MeterConfig { /** Metric batcher. */ batcher?: Batcher; + + /** Bool for whether or not graceful shutdown is enabled. If disabled metrics will not be exported when SIGTERM is recieved */ + gracefulShutdown?: boolean; } /** Default Meter configuration. */ export const DEFAULT_CONFIG = { logLevel: getEnv().OTEL_LOG_LEVEL, + gracefulShutdown: true, }; /** The default metric creation options value. */ diff --git a/packages/opentelemetry-metrics/test/MeterProvider.test.ts b/packages/opentelemetry-metrics/test/MeterProvider.test.ts index 7156e12e7cc..55cdafd66be 100644 --- a/packages/opentelemetry-metrics/test/MeterProvider.test.ts +++ b/packages/opentelemetry-metrics/test/MeterProvider.test.ts @@ -15,10 +15,29 @@ */ import * as assert from 'assert'; +import * as sinon from 'sinon'; import { MeterProvider, Meter, CounterMetric } from '../src'; -import { NoopLogger } from '@opentelemetry/core'; +import { + NoopLogger, + notifyOnGlobalShutdown, + _invokeGlobalShutdown, +} from '@opentelemetry/core'; describe('MeterProvider', () => { + let removeEvent: Function | undefined; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } + }); + describe('constructor', () => { it('should construct an instance without any options', () => { const provider = new MeterProvider(); @@ -73,4 +92,61 @@ describe('MeterProvider', () => { assert.notEqual(meter3, meter4); }); }); + + describe('shutdown()', () => { + it('should call shutdown when SIGTERM is received', () => { + const meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: true, + }); + const shutdownStub1 = sandbox.stub( + meterProvider.getMeter('meter1'), + 'shutdown' + ); + const shutdownStub2 = sandbox.stub( + meterProvider.getMeter('meter2'), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.calledOnce(shutdownStub1); + sinon.assert.calledOnce(shutdownStub2); + }); + _invokeGlobalShutdown(); + }); + + it('should call shutdown when manually invoked', () => { + const meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: true, + }); + const sandbox = sinon.createSandbox(); + const shutdownStub1 = sandbox.stub( + meterProvider.getMeter('meter1'), + 'shutdown' + ); + const shutdownStub2 = sandbox.stub( + meterProvider.getMeter('meter2'), + 'shutdown' + ); + meterProvider.shutdown(() => { + sinon.assert.calledOnce(shutdownStub1); + sinon.assert.calledOnce(shutdownStub2); + }); + }); + + it('should not trigger shutdown if graceful shutdown is turned off', () => { + const meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: false, + }); + const shutdownStub = sandbox.stub( + meterProvider.getMeter('meter1'), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.notCalled(shutdownStub); + }); + _invokeGlobalShutdown(); + }); + }); }); diff --git a/packages/opentelemetry-tracing/src/BasicTracerProvider.ts b/packages/opentelemetry-tracing/src/BasicTracerProvider.ts index cd19857f43d..87299e30fc2 100644 --- a/packages/opentelemetry-tracing/src/BasicTracerProvider.ts +++ b/packages/opentelemetry-tracing/src/BasicTracerProvider.ts @@ -20,6 +20,7 @@ import { HttpTraceContext, HttpCorrelationContext, CompositePropagator, + notifyOnGlobalShutdown, } from '@opentelemetry/core'; import { SpanProcessor, Tracer } from '.'; import { DEFAULT_CONFIG } from './config'; @@ -35,6 +36,7 @@ export class BasicTracerProvider implements api.TracerProvider { private readonly _config: TracerConfig; private readonly _registeredSpanProcessors: SpanProcessor[] = []; private readonly _tracers: Map = new Map(); + private _cleanNotifyOnGlobalShutdown: Function | undefined; activeSpanProcessor = new NoopSpanProcessor(); readonly logger: api.Logger; @@ -47,6 +49,11 @@ export class BasicTracerProvider implements api.TracerProvider { logger: this.logger, resource: this.resource, }); + if (this._config.gracefulShutdown) { + this._cleanNotifyOnGlobalShutdown = notifyOnGlobalShutdown( + this._shutdownActiveProcessor.bind(this) + ); + } } getTracer(name: string, version = '*', config?: TracerConfig): Tracer { @@ -99,4 +106,16 @@ export class BasicTracerProvider implements api.TracerProvider { api.propagation.setGlobalPropagator(config.propagator); } } + + shutdown(cb: () => void = () => {}) { + this.activeSpanProcessor.shutdown(cb); + if (this._cleanNotifyOnGlobalShutdown) { + this._cleanNotifyOnGlobalShutdown(); + this._cleanNotifyOnGlobalShutdown = undefined; + } + } + + private _shutdownActiveProcessor() { + this.activeSpanProcessor.shutdown(); + } } diff --git a/packages/opentelemetry-tracing/src/config.ts b/packages/opentelemetry-tracing/src/config.ts index 180dda3d771..4bbb6e11cfc 100644 --- a/packages/opentelemetry-tracing/src/config.ts +++ b/packages/opentelemetry-tracing/src/config.ts @@ -37,4 +37,5 @@ export const DEFAULT_CONFIG = { numberOfLinksPerSpan: DEFAULT_MAX_LINKS_PER_SPAN, numberOfEventsPerSpan: DEFAULT_MAX_EVENTS_PER_SPAN, }, + gracefulShutdown: true, }; diff --git a/packages/opentelemetry-tracing/src/types.ts b/packages/opentelemetry-tracing/src/types.ts index 832b3b48988..4e5059cea3f 100644 --- a/packages/opentelemetry-tracing/src/types.ts +++ b/packages/opentelemetry-tracing/src/types.ts @@ -43,6 +43,9 @@ export interface TracerConfig { /** Resource associated with trace telemetry */ resource?: Resource; + /** Bool for whether or not graceful shutdown is enabled. If disabled spans will not be exported when SIGTERM is recieved */ + gracefulShutdown?: boolean; + /** * Generator of trace and span IDs * The default idGenerator generates random ids diff --git a/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts b/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts index 5933d04fd42..2e95fce0203 100644 --- a/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts +++ b/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts @@ -24,14 +24,29 @@ import { setActiveSpan, setExtractedSpanContext, TraceState, + notifyOnGlobalShutdown, + _invokeGlobalShutdown, } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; +import * as sinon from 'sinon'; import { BasicTracerProvider, Span } from '../src'; describe('BasicTracerProvider', () => { + let sandbox: sinon.SinonSandbox; + let removeEvent: Function | undefined; + beforeEach(() => { context.disable(); + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } }); describe('constructor', () => { @@ -352,4 +367,43 @@ describe('BasicTracerProvider', () => { assert.ok(tracerProvider.resource instanceof Resource); }); }); + + describe('.shutdown()', () => { + it('should trigger shutdown when SIGTERM is recieved', () => { + const tracerProvider = new BasicTracerProvider(); + const shutdownStub = sandbox.stub( + tracerProvider.getActiveSpanProcessor(), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.calledOnce(shutdownStub); + }); + _invokeGlobalShutdown(); + }); + + it('should trigger shutdown when manually invoked', () => { + const tracerProvider = new BasicTracerProvider(); + const shutdownStub = sandbox.stub( + tracerProvider.getActiveSpanProcessor(), + 'shutdown' + ); + tracerProvider.shutdown(); + sinon.assert.calledOnce(shutdownStub); + }); + + it('should not trigger shutdown if graceful shutdown is turned off', () => { + const tracerProvider = new BasicTracerProvider({ + gracefulShutdown: false, + }); + const sandbox = sinon.createSandbox(); + const shutdownStub = sandbox.stub( + tracerProvider.getActiveSpanProcessor(), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.notCalled(shutdownStub); + }); + _invokeGlobalShutdown(); + }); + }); }); diff --git a/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts b/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts index c4e4aa35fd9..7db10cd26e4 100644 --- a/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts +++ b/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts @@ -23,6 +23,10 @@ import { Span, SpanProcessor, } from '../src'; +import { + notifyOnGlobalShutdown, + _invokeGlobalShutdown, +} from '@opentelemetry/core'; import { MultiSpanProcessor } from '../src/MultiSpanProcessor'; class TestProcessor implements SpanProcessor { @@ -38,6 +42,14 @@ class TestProcessor implements SpanProcessor { } describe('MultiSpanProcessor', () => { + let removeEvent: Function | undefined; + afterEach(() => { + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } + }); + it('should handle empty span processor', () => { const multiSpanProcessor = new MultiSpanProcessor([]); @@ -84,6 +96,51 @@ describe('MultiSpanProcessor', () => { assert.strictEqual(processor1.spans.length, processor2.spans.length); }); + it('should export spans on graceful shutdown from two span processor', () => { + const processor1 = new TestProcessor(); + const processor2 = new TestProcessor(); + const multiSpanProcessor = new MultiSpanProcessor([processor1, processor2]); + + const tracerProvider = new BasicTracerProvider(); + tracerProvider.addSpanProcessor(multiSpanProcessor); + const tracer = tracerProvider.getTracer('default'); + const span = tracer.startSpan('one'); + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + span.end(); + assert.strictEqual(processor1.spans.length, 1); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + removeEvent = notifyOnGlobalShutdown(() => { + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + }); + _invokeGlobalShutdown(); + }); + + it('should export spans on manual shutdown from two span processor', () => { + const processor1 = new TestProcessor(); + const processor2 = new TestProcessor(); + const multiSpanProcessor = new MultiSpanProcessor([processor1, processor2]); + + const tracerProvider = new BasicTracerProvider(); + tracerProvider.addSpanProcessor(multiSpanProcessor); + const tracer = tracerProvider.getTracer('default'); + const span = tracer.startSpan('one'); + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + span.end(); + assert.strictEqual(processor1.spans.length, 1); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + tracerProvider.shutdown(() => { + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + }); + }); + it('should force span processors to flush', () => { let flushed = false; const processor: SpanProcessor = { diff --git a/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts b/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts index 73ac0a00538..29a9d4117ff 100644 --- a/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts +++ b/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts @@ -214,7 +214,6 @@ describe('BatchSpanProcessor', () => { it('should call an async callback when shutdown is complete', done => { let exportedSpans = 0; sinon.stub(exporter, 'export').callsFake((spans, callback) => { - console.log('uh, export?'); setTimeout(() => { exportedSpans = exportedSpans + spans.length; callback(ExportResult.SUCCESS); diff --git a/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts b/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts index 47af46580e2..b8c05e4a0fe 100644 --- a/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts +++ b/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts @@ -24,13 +24,13 @@ import { context } from '@opentelemetry/api'; import { ExportResult, setActiveSpan } from '@opentelemetry/core'; describe('InMemorySpanExporter', () => { - const memoryExporter = new InMemorySpanExporter(); - const provider = new BasicTracerProvider(); - provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + let memoryExporter: InMemorySpanExporter; + let provider: BasicTracerProvider; - afterEach(() => { - // reset spans in memory. - memoryExporter.reset(); + beforeEach(() => { + memoryExporter = new InMemorySpanExporter(); + provider = new BasicTracerProvider(); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); }); it('should get finished spans', () => {