478 lines
14 KiB
TypeScript
478 lines
14 KiB
TypeScript
/*
|
|
* 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 { PeriodicExportingMetricReader } from '../../src/export/PeriodicExportingMetricReader';
|
|
import { AggregationTemporality } from '../../src/export/AggregationTemporality';
|
|
import { Aggregation, InstrumentType, PushMetricExporter } from '../../src';
|
|
import { ResourceMetrics } from '../../src/export/MetricData';
|
|
import * as assert from 'assert';
|
|
import * as sinon from 'sinon';
|
|
import { TimeoutError } from '../../src/utils';
|
|
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
|
|
import { assertRejects } from '../test-utils';
|
|
import { emptyResourceMetrics, TestMetricProducer } from './TestMetricProducer';
|
|
import {
|
|
assertAggregationSelector,
|
|
assertAggregationTemporalitySelector,
|
|
} from './utils';
|
|
import {
|
|
DEFAULT_AGGREGATION_SELECTOR,
|
|
DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR,
|
|
} from '../../src/export/AggregationSelector';
|
|
|
|
const MAX_32_BIT_INT = 2 ** 31 - 1;
|
|
|
|
class TestMetricExporter implements PushMetricExporter {
|
|
public exportTime = 0;
|
|
public forceFlushTime = 0;
|
|
public throwExport = false;
|
|
public throwFlush = false;
|
|
public rejectExport = false;
|
|
private _batches: ResourceMetrics[] = [];
|
|
private _shutdown: boolean = false;
|
|
|
|
export(
|
|
metrics: ResourceMetrics,
|
|
resultCallback: (result: ExportResult) => void
|
|
): void {
|
|
this._batches.push(metrics);
|
|
|
|
if (this.throwExport) {
|
|
throw new Error('Error during export');
|
|
}
|
|
setTimeout(() => {
|
|
if (this.rejectExport) {
|
|
resultCallback({
|
|
code: ExportResultCode.FAILED,
|
|
error: new Error('some error'),
|
|
});
|
|
} else {
|
|
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
}
|
|
}, this.exportTime);
|
|
}
|
|
|
|
async shutdown(): Promise<void> {
|
|
if (this._shutdown) return;
|
|
const flushPromise = this.forceFlush();
|
|
this._shutdown = true;
|
|
await flushPromise;
|
|
}
|
|
|
|
async forceFlush(): Promise<void> {
|
|
if (this.throwFlush) {
|
|
throw new Error('Error during forceFlush');
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, this.forceFlushTime));
|
|
}
|
|
|
|
async waitForNumberOfExports(
|
|
numberOfExports: number
|
|
): Promise<ResourceMetrics[]> {
|
|
if (numberOfExports <= 0) {
|
|
throw new Error('numberOfExports must be greater than or equal to 0');
|
|
}
|
|
|
|
while (this._batches.length < numberOfExports) {
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
}
|
|
return this._batches.slice(0, numberOfExports);
|
|
}
|
|
|
|
getExports(): ResourceMetrics[] {
|
|
return this._batches.slice(0);
|
|
}
|
|
}
|
|
|
|
class TestDeltaMetricExporter extends TestMetricExporter {
|
|
selectAggregationTemporality(
|
|
_instrumentType: InstrumentType
|
|
): AggregationTemporality {
|
|
return AggregationTemporality.DELTA;
|
|
}
|
|
}
|
|
|
|
class TestDropMetricExporter extends TestMetricExporter {
|
|
selectAggregation(_instrumentType: InstrumentType): Aggregation {
|
|
return Aggregation.Drop();
|
|
}
|
|
}
|
|
|
|
describe('PeriodicExportingMetricReader', () => {
|
|
afterEach(() => {
|
|
sinon.restore();
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should construct PeriodicExportingMetricReader without exceptions', () => {
|
|
const exporter = new TestDeltaMetricExporter();
|
|
assert.doesNotThrow(
|
|
() =>
|
|
new PeriodicExportingMetricReader({
|
|
exporter,
|
|
exportIntervalMillis: 4000,
|
|
exportTimeoutMillis: 3000,
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should throw when interval less or equal to 0', () => {
|
|
const exporter = new TestDeltaMetricExporter();
|
|
assert.throws(
|
|
() =>
|
|
new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 0,
|
|
exportTimeoutMillis: 0,
|
|
}),
|
|
/exportIntervalMillis must be greater than 0/
|
|
);
|
|
});
|
|
|
|
it('should throw when timeout less or equal to 0', () => {
|
|
const exporter = new TestDeltaMetricExporter();
|
|
assert.throws(
|
|
() =>
|
|
new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 1,
|
|
exportTimeoutMillis: 0,
|
|
}),
|
|
/exportTimeoutMillis must be greater than 0/
|
|
);
|
|
});
|
|
|
|
it('should throw when timeout less or equal to interval', () => {
|
|
const exporter = new TestDeltaMetricExporter();
|
|
assert.throws(
|
|
() =>
|
|
new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 100,
|
|
exportTimeoutMillis: 200,
|
|
}),
|
|
/exportIntervalMillis must be greater than or equal to exportTimeoutMillis/
|
|
);
|
|
});
|
|
|
|
it('should not start exporting', async () => {
|
|
const exporter = new TestDeltaMetricExporter();
|
|
const exporterMock = sinon.mock(exporter);
|
|
exporterMock.expects('export').never();
|
|
|
|
new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 1,
|
|
exportTimeoutMillis: 1,
|
|
});
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
exporterMock.verify();
|
|
});
|
|
});
|
|
|
|
describe('setMetricProducer', () => {
|
|
it('should start exporting periodically', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 30,
|
|
exportTimeoutMillis: 20,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
const result = await exporter.waitForNumberOfExports(2);
|
|
|
|
assert.deepStrictEqual(result, [
|
|
emptyResourceMetrics,
|
|
emptyResourceMetrics,
|
|
]);
|
|
await reader.shutdown();
|
|
});
|
|
});
|
|
|
|
describe('periodic export', () => {
|
|
it('should keep running on export errors', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
exporter.throwExport = true;
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 30,
|
|
exportTimeoutMillis: 20,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
|
|
const result = await exporter.waitForNumberOfExports(2);
|
|
assert.deepStrictEqual(result, [
|
|
emptyResourceMetrics,
|
|
emptyResourceMetrics,
|
|
]);
|
|
|
|
exporter.throwExport = false;
|
|
await reader.shutdown();
|
|
});
|
|
|
|
it('should keep running on export failure', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
exporter.rejectExport = true;
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 30,
|
|
exportTimeoutMillis: 20,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
|
|
const result = await exporter.waitForNumberOfExports(2);
|
|
assert.deepStrictEqual(result, [
|
|
emptyResourceMetrics,
|
|
emptyResourceMetrics,
|
|
]);
|
|
|
|
exporter.rejectExport = false;
|
|
await reader.shutdown();
|
|
});
|
|
|
|
it('should keep exporting on export timeouts', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
// set time longer than timeout.
|
|
exporter.exportTime = 40;
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: 30,
|
|
exportTimeoutMillis: 20,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
|
|
const result = await exporter.waitForNumberOfExports(2);
|
|
assert.deepStrictEqual(result, [
|
|
emptyResourceMetrics,
|
|
emptyResourceMetrics,
|
|
]);
|
|
|
|
exporter.throwExport = false;
|
|
await reader.shutdown();
|
|
});
|
|
});
|
|
|
|
describe('forceFlush', () => {
|
|
afterEach(() => {
|
|
sinon.restore();
|
|
});
|
|
|
|
it('should collect and forceFlush exporter', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
const exporterMock = sinon.mock(exporter);
|
|
exporterMock.expects('forceFlush').calledOnceWithExactly();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
await reader.forceFlush();
|
|
exporterMock.verify();
|
|
|
|
const exports = exporter.getExports();
|
|
assert.strictEqual(exports.length, 1);
|
|
|
|
await reader.shutdown();
|
|
});
|
|
|
|
it('should throw TimeoutError when forceFlush takes too long', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
exporter.forceFlushTime = 60;
|
|
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
await assertRejects(
|
|
() => reader.forceFlush({ timeoutMillis: 20 }),
|
|
TimeoutError
|
|
);
|
|
await reader.shutdown();
|
|
});
|
|
|
|
it('should throw when exporter throws', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
exporter.throwFlush = true;
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
|
|
await assertRejects(() => reader.forceFlush(), /Error during forceFlush/);
|
|
});
|
|
|
|
it('should not forceFlush exporter after shutdown', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
const exporterMock = sinon.mock(exporter);
|
|
// expect once on shutdown.
|
|
exporterMock.expects('forceFlush').once();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
await reader.shutdown();
|
|
await reader.forceFlush();
|
|
|
|
exporterMock.verify();
|
|
});
|
|
});
|
|
|
|
describe('selectAggregationTemporality', () => {
|
|
it('should default to Cumulative with no exporter preference', () => {
|
|
// Adding exporter without preference.
|
|
const exporter = new TestMetricExporter();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
});
|
|
|
|
assertAggregationTemporalitySelector(
|
|
reader,
|
|
DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR
|
|
);
|
|
reader.shutdown();
|
|
});
|
|
|
|
it('should default to exporter preference', () => {
|
|
// Adding exporter with DELTA preference.
|
|
const exporter = new TestDeltaMetricExporter();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
});
|
|
|
|
assertAggregationTemporalitySelector(
|
|
reader,
|
|
exporter.selectAggregationTemporality
|
|
);
|
|
reader.shutdown();
|
|
});
|
|
});
|
|
|
|
describe('selectAggregation', () => {
|
|
it('should use default aggregation with no exporter preference', () => {
|
|
// Adding exporter without preference.
|
|
const exporter = new TestMetricExporter();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
});
|
|
|
|
// check if the default selector is used.
|
|
assertAggregationSelector(reader, DEFAULT_AGGREGATION_SELECTOR);
|
|
reader.shutdown();
|
|
});
|
|
|
|
it('should default to exporter preference', () => {
|
|
// Adding exporter with Drop Aggregation preference.
|
|
const exporter = new TestDropMetricExporter();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
});
|
|
|
|
// check if the exporter's selector is used.
|
|
assertAggregationSelector(reader, exporter.selectAggregation);
|
|
reader.shutdown();
|
|
});
|
|
});
|
|
|
|
describe('shutdown', () => {
|
|
afterEach(() => {
|
|
sinon.restore();
|
|
});
|
|
|
|
it('should forceFlush', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
const exporterMock = sinon.mock(exporter);
|
|
exporterMock.expects('forceFlush').calledOnceWithExactly();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
await reader.shutdown();
|
|
exporterMock.verify();
|
|
});
|
|
|
|
it('should throw TimeoutError when forceFlush takes too long', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
exporter.forceFlushTime = 1000;
|
|
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
await assertRejects(
|
|
() => reader.shutdown({ timeoutMillis: 20 }),
|
|
TimeoutError
|
|
);
|
|
});
|
|
|
|
it('called twice should call export shutdown only once', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
const exporterMock = sinon.mock(exporter);
|
|
exporterMock.expects('shutdown').calledOnceWithExactly();
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
|
|
reader.setMetricProducer(new TestMetricProducer());
|
|
|
|
// call twice, the exporter's shutdown must only be called once.
|
|
await reader.shutdown();
|
|
await reader.shutdown();
|
|
|
|
exporterMock.verify();
|
|
});
|
|
|
|
it('should throw on non-initialized instance.', async () => {
|
|
const exporter = new TestMetricExporter();
|
|
exporter.throwFlush = true;
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter: exporter,
|
|
exportIntervalMillis: MAX_32_BIT_INT,
|
|
exportTimeoutMillis: 80,
|
|
});
|
|
|
|
await assertRejects(() => reader.shutdown(), /Error during forceFlush/);
|
|
});
|
|
});
|
|
});
|