opentelemetry-js/packages/opentelemetry-metrics/test/Meter.test.ts

1397 lines
47 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 * as assert from 'assert';
import * as sinon from 'sinon';
import {
Meter,
Metric,
CounterMetric,
MetricKind,
Sum,
MeterProvider,
ValueRecorderMetric,
ValueObserverMetric,
MetricRecord,
Aggregator,
MetricDescriptor,
UpDownCounterMetric,
LastValueAggregator,
LastValue,
Histogram,
} from '../src';
import * as api from '@opentelemetry/api';
import { NoopLogger, hrTime, hrTimeToNanoseconds } from '@opentelemetry/core';
import { BatchObserverResult } from '../src/BatchObserverResult';
import { SumAggregator } from '../src/export/aggregators';
import { SumObserverMetric } from '../src/SumObserverMetric';
import { Resource } from '@opentelemetry/resources';
import { UpDownSumObserverMetric } from '../src/UpDownSumObserverMetric';
import { hashLabels } from '../src/Utils';
import { Processor } from '../src/export/Processor';
import { ValueType } from '@opentelemetry/api';
const nonNumberValues = [
// type undefined
undefined,
// type null
null,
// type function
function () {},
// type boolean
true,
false,
// type string
'1',
// type object
{},
// type symbol
// symbols cannot be cast to number, early errors will be thrown.
];
if (Number(process.versions.node.match(/^\d+/)) >= 10) {
nonNumberValues.push(
// type bigint
// Preferring BigInt builtin object instead of bigint literal to keep Node.js v8.x working.
// TODO: should metric instruments support bigint?
// @ts-ignore
BigInt(1) // eslint-disable-line node/no-unsupported-features/es-builtins
);
}
describe('Meter', () => {
let meter: Meter;
const keya = 'keya';
const keyb = 'keyb';
const labels: api.Labels = { [keyb]: 'value2', [keya]: 'value1' };
beforeEach(() => {
meter = new MeterProvider({
logger: new NoopLogger(),
}).getMeter('test-meter');
});
describe('#counter', () => {
const performanceTimeOrigin = hrTime();
it('should create a counter', () => {
const counter = meter.createCounter('name');
assert.ok(counter instanceof Metric);
});
it('should create a counter with options', () => {
const counter = meter.createCounter('name', {
description: 'desc',
unit: '1',
disabled: false,
});
assert.ok(counter instanceof Metric);
});
it('should be able to call add() directly on counter', async () => {
const counter = meter.createCounter('name') as CounterMetric;
counter.add(10, labels);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 10);
const lastTimestamp = record1.aggregator.toPoint().timestamp;
assert.ok(
hrTimeToNanoseconds(lastTimestamp) >
hrTimeToNanoseconds(performanceTimeOrigin)
);
counter.add(10, labels);
assert.strictEqual(record1.aggregator.toPoint().value, 20);
assert.ok(
hrTimeToNanoseconds(record1.aggregator.toPoint().timestamp) >
hrTimeToNanoseconds(lastTimestamp)
);
});
it('should be able to call add with no labels', async () => {
const counter = meter.createCounter('name', {
description: 'desc',
unit: '1',
disabled: false,
});
counter.add(1);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 1);
});
it('should pipe through resource', async () => {
const counter = meter.createCounter('name') as CounterMetric;
assert.ok(counter.resource instanceof Resource);
counter.add(1, { foo: 'bar' });
const [record] = await counter.getMetricRecord();
assert.ok(record.resource instanceof Resource);
});
it('should pipe through instrumentation library', async () => {
const counter = meter.createCounter('name') as CounterMetric;
assert.ok(counter.instrumentationLibrary);
counter.add(1, { foo: 'bar' });
const [record] = await counter.getMetricRecord();
const { name, version } = record.instrumentationLibrary;
assert.strictEqual(name, 'test-meter');
assert.strictEqual(version, '*');
});
describe('.bind()', () => {
it('should create a counter instrument', async () => {
const counter = meter.createCounter('name') as CounterMetric;
const boundCounter = counter.bind(labels);
boundCounter.add(10);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 10);
boundCounter.add(10);
assert.strictEqual(record1.aggregator.toPoint().value, 20);
});
it('should return the aggregator', () => {
const counter = meter.createCounter('name') as CounterMetric;
const boundCounter = counter.bind(labels);
boundCounter.add(20);
assert.ok(boundCounter.getAggregator() instanceof SumAggregator);
assert.strictEqual(boundCounter.getLabels(), labels);
});
it('should add positive values only', async () => {
const counter = meter.createCounter('name') as CounterMetric;
const boundCounter = counter.bind(labels);
boundCounter.add(10);
assert.strictEqual(meter.getProcessor().checkPointSet().length, 0);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 10);
boundCounter.add(-100);
assert.strictEqual(record1.aggregator.toPoint().value, 10);
});
it('should not add the instrument data when disabled', async () => {
const counter = meter.createCounter('name', {
disabled: true,
}) as CounterMetric;
const boundCounter = counter.bind(labels);
boundCounter.add(10);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 0);
});
it('should return same instrument on same label values', async () => {
const counter = meter.createCounter('name') as CounterMetric;
const boundCounter = counter.bind(labels);
boundCounter.add(10);
const boundCounter1 = counter.bind(labels);
boundCounter1.add(10);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 20);
assert.strictEqual(boundCounter, boundCounter1);
});
});
describe('.unbind()', () => {
it('should remove a counter instrument', () => {
const counter = meter.createCounter('name') as CounterMetric;
const boundCounter = counter.bind(labels);
assert.strictEqual(counter['_instruments'].size, 1);
counter.unbind(labels);
assert.strictEqual(counter['_instruments'].size, 0);
const boundCounter1 = counter.bind(labels);
assert.strictEqual(counter['_instruments'].size, 1);
assert.notStrictEqual(boundCounter, boundCounter1);
});
it('should not fail when removing non existing instrument', () => {
const counter = meter.createCounter('name');
counter.unbind({});
});
it('should clear all instruments', () => {
const counter = meter.createCounter('name') as CounterMetric;
counter.bind(labels);
assert.strictEqual(counter['_instruments'].size, 1);
counter.clear();
assert.strictEqual(counter['_instruments'].size, 0);
});
});
describe('.registerMetric()', () => {
it('skip already registered Metric', async () => {
const counter1 = meter.createCounter('name1') as CounterMetric;
counter1.bind(labels).add(10);
// should skip below metric
const counter2 = meter.createCounter('name1', {
valueType: api.ValueType.INT,
}) as CounterMetric;
counter2.bind(labels).add(500);
await meter.collect();
const record = meter.getProcessor().checkPointSet();
assert.strictEqual(record.length, 1);
assert.deepStrictEqual(record[0].descriptor, {
description: '',
metricKind: MetricKind.COUNTER,
name: 'name1',
unit: '1',
valueType: api.ValueType.DOUBLE,
});
assert.strictEqual(record[0].aggregator.toPoint().value, 10);
});
});
describe('names', () => {
it('should create counter with valid names', () => {
const counter1 = meter.createCounter('name1');
const counter2 = meter.createCounter(
'Name_with-all.valid_CharacterClasses'
);
assert.ok(counter1 instanceof CounterMetric);
assert.ok(counter2 instanceof CounterMetric);
});
it('should return no op metric if name is an empty string', () => {
const counter = meter.createCounter('');
assert.ok(counter instanceof api.NoopMetric);
});
it('should return no op metric if name does not start with a letter', () => {
const counter1 = meter.createCounter('1name');
const counter_ = meter.createCounter('_name');
assert.ok(counter1 instanceof api.NoopMetric);
assert.ok(counter_ instanceof api.NoopMetric);
});
it('should return no op metric if name is an empty string contain only letters, numbers, ".", "_", and "-"', () => {
const counter = meter.createCounter('name with invalid characters^&*(');
assert.ok(counter instanceof api.NoopMetric);
});
});
});
describe('#UpDownCounter', () => {
const performanceTimeOrigin = hrTime();
it('should create a UpDownCounter', () => {
const upDownCounter = meter.createUpDownCounter('name');
assert.ok(upDownCounter instanceof Metric);
});
it('should create a UpDownCounter with options', () => {
const upDownCounter = meter.createUpDownCounter('name', {
description: 'desc',
unit: '1',
disabled: false,
});
assert.ok(upDownCounter instanceof Metric);
});
it('should be able to call add() directly on UpDownCounter', async () => {
const upDownCounter = meter.createUpDownCounter('name');
upDownCounter.add(10, labels);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 10);
const lastTimestamp = record1.aggregator.toPoint().timestamp;
assert.ok(
hrTimeToNanoseconds(lastTimestamp) >
hrTimeToNanoseconds(performanceTimeOrigin)
);
upDownCounter.add(10, labels);
assert.strictEqual(record1.aggregator.toPoint().value, 20);
assert.ok(
hrTimeToNanoseconds(record1.aggregator.toPoint().timestamp) >
hrTimeToNanoseconds(lastTimestamp)
);
});
it('should be able to call add with no labels', async () => {
const upDownCounter = meter.createUpDownCounter('name', {
description: 'desc',
unit: '1',
disabled: false,
});
upDownCounter.add(1);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 1);
});
it('should pipe through resource', async () => {
const upDownCounter = meter.createUpDownCounter(
'name'
) as UpDownCounterMetric;
assert.ok(upDownCounter.resource instanceof Resource);
upDownCounter.add(1, { foo: 'bar' });
const [record] = await upDownCounter.getMetricRecord();
assert.ok(record.resource instanceof Resource);
});
describe('.bind()', () => {
it('should create a UpDownCounter instrument', async () => {
const upDownCounter = meter.createUpDownCounter('name');
const boundCounter = upDownCounter.bind(labels);
boundCounter.add(10);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 10);
boundCounter.add(-200);
assert.strictEqual(record1.aggregator.toPoint().value, -190);
});
it('should return the aggregator', () => {
const upDownCounter = meter.createUpDownCounter(
'name'
) as UpDownCounterMetric;
const boundCounter = upDownCounter.bind(labels);
boundCounter.add(20);
assert.ok(boundCounter.getAggregator() instanceof SumAggregator);
assert.strictEqual(boundCounter.getLabels(), labels);
});
it('should not add the instrument data when disabled', async () => {
const upDownCounter = meter.createUpDownCounter('name', {
disabled: true,
});
const boundCounter = upDownCounter.bind(labels);
boundCounter.add(10);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 0);
});
it('should return same instrument on same label values', async () => {
const upDownCounter = meter.createUpDownCounter('name');
const boundCounter = upDownCounter.bind(labels);
boundCounter.add(10);
const boundCounter1 = upDownCounter.bind(labels);
boundCounter1.add(10);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 20);
assert.strictEqual(boundCounter, boundCounter1);
});
it('should truncate non-integer values for INT valueType', async () => {
const upDownCounter = meter.createUpDownCounter('name', {
valueType: ValueType.INT,
});
const boundCounter = upDownCounter.bind(labels);
[-1.1, 2.2].forEach(val => {
boundCounter.add(val);
});
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 1);
});
it('should ignore non-number values for INT valueType', async () => {
const upDownCounter = meter.createUpDownCounter('name', {
valueType: ValueType.DOUBLE,
});
const boundCounter = upDownCounter.bind(labels);
await Promise.all(
nonNumberValues.map(async val => {
// @ts-expect-error
boundCounter.add(val);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 0);
})
);
});
it('should ignore non-number values for DOUBLE valueType', async () => {
const upDownCounter = meter.createUpDownCounter('name', {
valueType: ValueType.DOUBLE,
});
const boundCounter = upDownCounter.bind(labels);
await Promise.all(
nonNumberValues.map(async val => {
// @ts-expect-error
boundCounter.add(val);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.strictEqual(record1.aggregator.toPoint().value, 0);
})
);
});
});
describe('.unbind()', () => {
it('should remove a UpDownCounter instrument', () => {
const upDownCounter = meter.createUpDownCounter(
'name'
) as UpDownCounterMetric;
const boundCounter = upDownCounter.bind(labels);
assert.strictEqual(upDownCounter['_instruments'].size, 1);
upDownCounter.unbind(labels);
assert.strictEqual(upDownCounter['_instruments'].size, 0);
const boundCounter1 = upDownCounter.bind(labels);
assert.strictEqual(upDownCounter['_instruments'].size, 1);
assert.notStrictEqual(boundCounter, boundCounter1);
});
it('should not fail when removing non existing instrument', () => {
const upDownCounter = meter.createUpDownCounter('name');
upDownCounter.unbind({});
});
it('should clear all instruments', () => {
const upDownCounter = meter.createUpDownCounter(
'name'
) as CounterMetric;
upDownCounter.bind(labels);
assert.strictEqual(upDownCounter['_instruments'].size, 1);
upDownCounter.clear();
assert.strictEqual(upDownCounter['_instruments'].size, 0);
});
});
describe('.registerMetric()', () => {
it('skip already registered Metric', async () => {
const counter1 = meter.createCounter('name1') as CounterMetric;
counter1.bind(labels).add(10);
// should skip below metric
const counter2 = meter.createCounter('name1', {
valueType: api.ValueType.INT,
}) as CounterMetric;
counter2.bind(labels).add(500);
await meter.collect();
const record = meter.getProcessor().checkPointSet();
assert.strictEqual(record.length, 1);
assert.deepStrictEqual(record[0].descriptor, {
description: '',
metricKind: MetricKind.COUNTER,
name: 'name1',
unit: '1',
valueType: api.ValueType.DOUBLE,
});
assert.strictEqual(record[0].aggregator.toPoint().value, 10);
});
});
describe('names', () => {
it('should create counter with valid names', () => {
const counter1 = meter.createCounter('name1');
const counter2 = meter.createCounter(
'Name_with-all.valid_CharacterClasses'
);
assert.ok(counter1 instanceof CounterMetric);
assert.ok(counter2 instanceof CounterMetric);
});
it('should return no op metric if name is an empty string', () => {
const counter = meter.createCounter('');
assert.ok(counter instanceof api.NoopMetric);
});
it('should return no op metric if name does not start with a letter', () => {
const counter1 = meter.createCounter('1name');
const counter_ = meter.createCounter('_name');
assert.ok(counter1 instanceof api.NoopMetric);
assert.ok(counter_ instanceof api.NoopMetric);
});
it('should return no op metric if name is an empty string contain only letters, numbers, ".", "_", and "-"', () => {
const counter = meter.createCounter('name with invalid characters^&*(');
assert.ok(counter instanceof api.NoopMetric);
});
});
});
describe('#ValueRecorder', () => {
it('should create a valueRecorder', () => {
const valueRecorder = meter.createValueRecorder('name');
assert.ok(valueRecorder instanceof Metric);
});
it('should create a valueRecorder with options', () => {
const valueRecorder = meter.createValueRecorder('name', {
description: 'desc',
unit: '1',
disabled: false,
});
assert.ok(valueRecorder instanceof Metric);
});
it('should set histogram boundaries for value recorder', async () => {
const valueRecorder = meter.createValueRecorder('name', {
description: 'desc',
unit: '1',
disabled: false,
boundaries: [10, 20, 30, 100],
}) as ValueRecorderMetric;
valueRecorder.record(10);
valueRecorder.record(30);
valueRecorder.record(50);
valueRecorder.record(200);
await meter.collect();
const [record] = meter.getProcessor().checkPointSet();
assert.deepStrictEqual(record.aggregator.toPoint().value as Histogram, {
buckets: {
boundaries: [10, 20, 30, 100],
counts: [0, 1, 0, 2, 1],
},
count: 4,
sum: 290,
});
assert.ok(valueRecorder instanceof Metric);
});
it('should pipe through resource', async () => {
const valueRecorder = meter.createValueRecorder(
'name'
) as ValueRecorderMetric;
assert.ok(valueRecorder.resource instanceof Resource);
valueRecorder.record(1, { foo: 'bar' });
const [record] = await valueRecorder.getMetricRecord();
assert.ok(record.resource instanceof Resource);
});
it('should pipe through instrumentation library', async () => {
const valueRecorder = meter.createValueRecorder(
'name'
) as ValueRecorderMetric;
assert.ok(valueRecorder.instrumentationLibrary);
valueRecorder.record(1, { foo: 'bar' });
const [record] = await valueRecorder.getMetricRecord();
const { name, version } = record.instrumentationLibrary;
assert.strictEqual(name, 'test-meter');
assert.strictEqual(version, '*');
});
describe('names', () => {
it('should return no op metric if name is an empty string', () => {
const valueRecorder = meter.createValueRecorder('');
assert.ok(valueRecorder instanceof api.NoopMetric);
});
it('should return no op metric if name does not start with a letter', () => {
const valueRecorder1 = meter.createValueRecorder('1name');
const valueRecorder_ = meter.createValueRecorder('_name');
assert.ok(valueRecorder1 instanceof api.NoopMetric);
assert.ok(valueRecorder_ instanceof api.NoopMetric);
});
it('should return no op metric if name is an empty string contain only letters, numbers, ".", "_", and "-"', () => {
const valueRecorder = meter.createValueRecorder(
'name with invalid characters^&*('
);
assert.ok(valueRecorder instanceof api.NoopMetric);
});
});
describe('.bind()', () => {
const performanceTimeOrigin = hrTime();
it('should create a valueRecorder instrument', () => {
const valueRecorder = meter.createValueRecorder(
'name'
) as ValueRecorderMetric;
const boundValueRecorder = valueRecorder.bind(labels);
assert.doesNotThrow(() => boundValueRecorder.record(10));
});
it('should not set the instrument data when disabled', async () => {
const valueRecorder = meter.createValueRecorder('name', {
disabled: true,
}) as ValueRecorderMetric;
const boundValueRecorder = valueRecorder.bind(labels);
boundValueRecorder.record(10);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.deepStrictEqual(
record1.aggregator.toPoint().value as Histogram,
{
buckets: {
boundaries: [Infinity],
counts: [0, 0],
},
count: 0,
sum: 0,
}
);
});
it('should accept negative (and positive) values', async () => {
const valueRecorder = meter.createValueRecorder('name');
const boundValueRecorder = valueRecorder.bind(labels);
boundValueRecorder.record(-10);
boundValueRecorder.record(50);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.deepStrictEqual(
record1.aggregator.toPoint().value as Histogram,
{
buckets: {
boundaries: [Infinity],
counts: [2, 0],
},
count: 2,
sum: 40,
}
);
assert.ok(
hrTimeToNanoseconds(record1.aggregator.toPoint().timestamp) >
hrTimeToNanoseconds(performanceTimeOrigin)
);
});
it('should return same instrument on same label values', async () => {
const valueRecorder = meter.createValueRecorder(
'name'
) as ValueRecorderMetric;
const boundValueRecorder1 = valueRecorder.bind(labels);
boundValueRecorder1.record(10);
const boundValueRecorder2 = valueRecorder.bind(labels);
boundValueRecorder2.record(100);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.deepStrictEqual(
record1.aggregator.toPoint().value as Histogram,
{
buckets: {
boundaries: [Infinity],
counts: [2, 0],
},
count: 2,
sum: 110,
}
);
assert.strictEqual(boundValueRecorder1, boundValueRecorder2);
});
it('should ignore non-number values', async () => {
const valueRecorder = meter.createValueRecorder(
'name'
) as ValueRecorderMetric;
const boundValueRecorder = valueRecorder.bind(labels);
await Promise.all(
nonNumberValues.map(async val => {
// @ts-expect-error
boundValueRecorder.record(val);
await meter.collect();
const [record1] = meter.getProcessor().checkPointSet();
assert.deepStrictEqual(
record1.aggregator.toPoint().value as Histogram,
{
buckets: {
boundaries: [Infinity],
counts: [0, 0],
},
count: 0,
sum: 0,
}
);
})
);
});
});
describe('.unbind()', () => {
it('should remove the valueRecorder instrument', () => {
const valueRecorder = meter.createValueRecorder(
'name'
) as ValueRecorderMetric;
const boundValueRecorder = valueRecorder.bind(labels);
assert.strictEqual(valueRecorder['_instruments'].size, 1);
valueRecorder.unbind(labels);
assert.strictEqual(valueRecorder['_instruments'].size, 0);
const boundValueRecorder2 = valueRecorder.bind(labels);
assert.strictEqual(valueRecorder['_instruments'].size, 1);
assert.notStrictEqual(boundValueRecorder, boundValueRecorder2);
});
it('should not fail when removing non existing instrument', () => {
const valueRecorder = meter.createValueRecorder('name');
valueRecorder.unbind({});
});
it('should clear all instruments', () => {
const valueRecorder = meter.createValueRecorder(
'name'
) as ValueRecorderMetric;
valueRecorder.bind(labels);
assert.strictEqual(valueRecorder['_instruments'].size, 1);
valueRecorder.clear();
assert.strictEqual(valueRecorder['_instruments'].size, 0);
});
});
});
describe('#SumObserverMetric', () => {
it('should create an Sum observer', () => {
const sumObserver = meter.createSumObserver('name') as SumObserverMetric;
assert.ok(sumObserver instanceof Metric);
});
it('should return noop observer when name is invalid', () => {
const spy = sinon.stub(meter['_logger'], 'warn');
const sumObserver = meter.createSumObserver('na me');
assert.ok(sumObserver === api.NOOP_SUM_OBSERVER_METRIC);
const args = spy.args[0];
assert.ok(
args[0],
'Invalid metric name na me. Defaulting to noop metric implementation.'
);
});
it('should create observer with options', () => {
const sumObserver = meter.createSumObserver('name', {
description: 'desc',
unit: '1',
disabled: false,
}) as SumObserverMetric;
assert.ok(sumObserver instanceof Metric);
});
it('should set callback and observe value ', async () => {
let counter = 0;
function getValue() {
console.log('getting value, counter:', counter);
if (++counter % 2 == 0) {
return 3;
}
return -1;
}
const sumObserver = meter.createSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
// simulate async
return new Promise(resolve => {
setTimeout(() => {
observerResult.observe(getValue(), { pid: '123', core: '1' });
resolve();
}, 1);
});
}
) as SumObserverMetric;
let metricRecords = await sumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
let point = metricRecords[0].aggregator.toPoint();
assert.strictEqual(point.value, -1);
assert.strictEqual(
hashLabels(metricRecords[0].labels),
'|#core:1,pid:123'
);
metricRecords = await sumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
point = metricRecords[0].aggregator.toPoint();
assert.strictEqual(point.value, 3);
metricRecords = await sumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
point = metricRecords[0].aggregator.toPoint();
assert.strictEqual(point.value, 3);
});
it('should set callback and observe value when callback returns nothing', async () => {
const sumObserver = meter.createSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
observerResult.observe(1, { pid: '123', core: '1' });
}
) as SumObserverMetric;
const metricRecords = await sumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
});
it(
'should set callback and observe value when callback returns anything' +
' but Promise',
async () => {
const sumObserver = meter.createSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
observerResult.observe(1, { pid: '123', core: '1' });
return '1';
}
) as SumObserverMetric;
const metricRecords = await sumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
}
);
it('should reject getMetricRecord when callback throws an error', async () => {
const sumObserver = meter.createSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
observerResult.observe(1, { pid: '123', core: '1' });
throw new Error('Boom');
}
) as SumObserverMetric;
await sumObserver
.getMetricRecord()
.then()
.catch(e => {
assert.strictEqual(e.message, 'Boom');
});
});
it('should pipe through resource', async () => {
const sumObserver = meter.createSumObserver('name', {}, result => {
result.observe(42, { foo: 'bar' });
return Promise.resolve();
}) as SumObserverMetric;
assert.ok(sumObserver.resource instanceof Resource);
const [record] = await sumObserver.getMetricRecord();
assert.ok(record.resource instanceof Resource);
});
});
describe('#ValueObserver', () => {
it('should create a value observer', () => {
const valueObserver = meter.createValueObserver(
'name'
) as ValueObserverMetric;
assert.ok(valueObserver instanceof Metric);
});
it('should return noop observer when name is invalid', () => {
const spy = sinon.stub(meter['_logger'], 'warn');
const valueObserver = meter.createValueObserver('na me');
assert.ok(valueObserver === api.NOOP_VALUE_OBSERVER_METRIC);
const args = spy.args[0];
assert.ok(
args[0],
'Invalid metric name na me. Defaulting to noop metric implementation.'
);
});
it('should create observer with options', () => {
const valueObserver = meter.createValueObserver('name', {
description: 'desc',
unit: '1',
disabled: false,
}) as ValueObserverMetric;
assert.ok(valueObserver instanceof Metric);
});
it('should set callback and observe value ', async () => {
const valueObserver = meter.createValueObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
// simulate async
return new Promise(resolve => {
setTimeout(() => {
observerResult.observe(getCpuUsage(), { pid: '123', core: '1' });
observerResult.observe(getCpuUsage(), { pid: '123', core: '2' });
observerResult.observe(getCpuUsage(), { pid: '123', core: '3' });
observerResult.observe(getCpuUsage(), { pid: '123', core: '4' });
resolve();
}, 1);
});
}
) as ValueObserverMetric;
function getCpuUsage() {
return Math.random();
}
const metricRecords: MetricRecord[] = await valueObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 4);
const metric1 = metricRecords[0];
const metric2 = metricRecords[1];
const metric3 = metricRecords[2];
const metric4 = metricRecords[3];
assert.strictEqual(hashLabels(metric1.labels), '|#core:1,pid:123');
assert.strictEqual(hashLabels(metric2.labels), '|#core:2,pid:123');
assert.strictEqual(hashLabels(metric3.labels), '|#core:3,pid:123');
assert.strictEqual(hashLabels(metric4.labels), '|#core:4,pid:123');
ensureMetric(metric1);
ensureMetric(metric2);
ensureMetric(metric3);
ensureMetric(metric4);
});
it('should pipe through resource', async () => {
const valueObserver = meter.createValueObserver('name', {}, result => {
result.observe(42, { foo: 'bar' });
}) as ValueObserverMetric;
assert.ok(valueObserver.resource instanceof Resource);
const [record] = await valueObserver.getMetricRecord();
assert.ok(record.resource instanceof Resource);
});
});
describe('#UpDownSumObserverMetric', () => {
it('should create an UpDownSum observer', () => {
const upDownSumObserver = meter.createUpDownSumObserver(
'name'
) as UpDownSumObserverMetric;
assert.ok(upDownSumObserver instanceof Metric);
});
it('should return noop observer when name is invalid', () => {
const spy = sinon.stub(meter['_logger'], 'warn');
const upDownSumObserver = meter.createUpDownSumObserver('na me');
assert.ok(upDownSumObserver === api.NOOP_UP_DOWN_SUM_OBSERVER_METRIC);
const args = spy.args[0];
assert.ok(
args[0],
'Invalid metric name na me. Defaulting to noop metric implementation.'
);
});
it('should create observer with options', () => {
const upDownSumObserver = meter.createUpDownSumObserver('name', {
description: 'desc',
unit: '1',
disabled: false,
}) as UpDownSumObserverMetric;
assert.ok(upDownSumObserver instanceof Metric);
});
it('should set callback and observe value ', async () => {
let counter = 0;
function getValue() {
counter++;
if (counter % 2 === 0) {
return 2;
}
return 3;
}
const upDownSumObserver = meter.createUpDownSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
// simulate async
return new Promise(resolve => {
setTimeout(() => {
observerResult.observe(getValue(), { pid: '123', core: '1' });
resolve();
}, 1);
});
}
) as UpDownSumObserverMetric;
let metricRecords = await upDownSumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
let point = metricRecords[0].aggregator.toPoint();
assert.strictEqual(point.value, 3);
assert.strictEqual(
hashLabels(metricRecords[0].labels),
'|#core:1,pid:123'
);
metricRecords = await upDownSumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
point = metricRecords[0].aggregator.toPoint();
assert.strictEqual(point.value, 2);
metricRecords = await upDownSumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
point = metricRecords[0].aggregator.toPoint();
assert.strictEqual(point.value, 3);
});
it('should set callback and observe value when callback returns nothing', async () => {
const upDownSumObserver = meter.createUpDownSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
observerResult.observe(1, { pid: '123', core: '1' });
}
) as UpDownSumObserverMetric;
const metricRecords = await upDownSumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
});
it(
'should set callback and observe value when callback returns anything' +
' but Promise',
async () => {
const upDownSumObserver = meter.createUpDownSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
observerResult.observe(1, { pid: '123', core: '1' });
return '1';
}
) as UpDownSumObserverMetric;
const metricRecords = await upDownSumObserver.getMetricRecord();
assert.strictEqual(metricRecords.length, 1);
}
);
it('should reject getMetricRecord when callback throws an error', async () => {
const upDownSumObserver = meter.createUpDownSumObserver(
'name',
{
description: 'desc',
},
(observerResult: api.ObserverResult) => {
observerResult.observe(1, { pid: '123', core: '1' });
throw new Error('Boom');
}
) as UpDownSumObserverMetric;
await upDownSumObserver
.getMetricRecord()
.then()
.catch(e => {
assert.strictEqual(e.message, 'Boom');
});
});
it('should pipe through resource', async () => {
const upDownSumObserver = meter.createUpDownSumObserver(
'name',
{},
result => {
result.observe(42, { foo: 'bar' });
return Promise.resolve();
}
) as UpDownSumObserverMetric;
assert.ok(upDownSumObserver.resource instanceof Resource);
const [record] = await upDownSumObserver.getMetricRecord();
assert.ok(record.resource instanceof Resource);
});
});
describe('#batchObserver', () => {
it('should create a batch observer', () => {
const measure = meter.createBatchObserver('name', () => {});
assert.ok(measure instanceof Metric);
});
it('should create batch observer with options', () => {
const measure = meter.createBatchObserver('name', () => {}, {
description: 'desc',
unit: '1',
disabled: false,
maxTimeoutUpdateMS: 100,
});
assert.ok(measure instanceof Metric);
});
it('should use callback to observe values ', async () => {
const tempMetric = meter.createValueObserver('cpu_temp_per_app', {
description: 'desc',
}) as ValueObserverMetric;
const cpuUsageMetric = meter.createValueObserver('cpu_usage_per_app', {
description: 'desc',
}) as ValueObserverMetric;
meter.createBatchObserver(
'metric_batch_observer',
observerBatchResult => {
interface StatItem {
usage: number;
temp: number;
}
interface Stat {
name: string;
core1: StatItem;
core2: StatItem;
}
function someAsyncMetrics() {
return new Promise(resolve => {
const stats: Stat[] = [
{
name: 'app1',
core1: { usage: 2.1, temp: 67 },
core2: { usage: 3.1, temp: 69 },
},
{
name: 'app2',
core1: { usage: 1.2, temp: 67 },
core2: { usage: 4.5, temp: 69 },
},
];
resolve(stats);
});
}
Promise.all([
someAsyncMetrics(),
// simulate waiting
new Promise((resolve, reject) => {
setTimeout(resolve, 1);
}),
]).then((stats: unknown[]) => {
const apps = (stats[0] as unknown) as Stat[];
apps.forEach(app => {
observerBatchResult.observe({ app: app.name, core: '1' }, [
tempMetric.observation(app.core1.temp),
cpuUsageMetric.observation(app.core1.usage),
]);
observerBatchResult.observe({ app: app.name, core: '2' }, [
tempMetric.observation(app.core2.temp),
cpuUsageMetric.observation(app.core2.usage),
]);
});
});
}
);
await meter.collect();
const records = meter.getProcessor().checkPointSet();
assert.strictEqual(records.length, 8);
const metric1 = records[0];
const metric2 = records[1];
const metric3 = records[2];
const metric4 = records[3];
assert.strictEqual(hashLabels(metric1.labels), '|#app:app1,core:1');
assert.strictEqual(hashLabels(metric2.labels), '|#app:app1,core:2');
assert.strictEqual(hashLabels(metric3.labels), '|#app:app2,core:1');
assert.strictEqual(hashLabels(metric4.labels), '|#app:app2,core:2');
ensureMetric(metric1, 'cpu_temp_per_app', 67);
ensureMetric(metric2, 'cpu_temp_per_app', 69);
ensureMetric(metric3, 'cpu_temp_per_app', 67);
ensureMetric(metric4, 'cpu_temp_per_app', 69);
const metric5 = records[4];
const metric6 = records[5];
const metric7 = records[6];
const metric8 = records[7];
assert.strictEqual(hashLabels(metric5.labels), '|#app:app1,core:1');
assert.strictEqual(hashLabels(metric6.labels), '|#app:app1,core:2');
assert.strictEqual(hashLabels(metric7.labels), '|#app:app2,core:1');
assert.strictEqual(hashLabels(metric8.labels), '|#app:app2,core:2');
ensureMetric(metric5, 'cpu_usage_per_app', 2.1);
ensureMetric(metric6, 'cpu_usage_per_app', 3.1);
ensureMetric(metric7, 'cpu_usage_per_app', 1.2);
ensureMetric(metric8, 'cpu_usage_per_app', 4.5);
});
it('should not observe values when timeout', done => {
const cpuUsageMetric = meter.createValueObserver('cpu_usage_per_app', {
description: 'desc',
}) as ValueObserverMetric;
meter.createBatchObserver(
'metric_batch_observer',
observerBatchResult => {
Promise.all([
// simulate waiting 11ms
new Promise((resolve, reject) => {
setTimeout(resolve, 11);
}),
]).then(async () => {
// try to hack to be able to update
(observerBatchResult as BatchObserverResult).cancelled = false;
observerBatchResult.observe({ foo: 'bar' }, [
cpuUsageMetric.observation(123),
]);
// simulate some waiting
await setTimeout(() => {}, 5);
const cpuUsageMetricRecords: MetricRecord[] = await cpuUsageMetric.getMetricRecord();
const value = cpuUsageMetric
.bind({ foo: 'bar' })
.getAggregator()
.toPoint().value;
assert.deepStrictEqual(value, 0);
assert.strictEqual(cpuUsageMetricRecords.length, 0);
done();
});
},
{
maxTimeoutUpdateMS: 10, // timeout after 10ms
}
);
meter.collect();
});
it('should pipe through instrumentation library', async () => {
const observer = meter.createValueObserver(
'name',
{},
(observerResult: api.ObserverResult) => {
observerResult.observe(42, { foo: 'bar' });
}
) as ValueObserverMetric;
assert.ok(observer.instrumentationLibrary);
const [record] = await observer.getMetricRecord();
const { name, version } = record.instrumentationLibrary;
assert.strictEqual(name, 'test-meter');
assert.strictEqual(version, '*');
});
});
describe('#getMetrics', () => {
it('should create a DOUBLE counter', async () => {
const key = 'key';
const counter = meter.createCounter('counter', {
description: 'test',
});
const labels = { [key]: 'counter-value' };
const boundCounter = counter.bind(labels);
boundCounter.add(10.45);
await meter.collect();
const record = meter.getProcessor().checkPointSet();
assert.strictEqual(record.length, 1);
assert.deepStrictEqual(record[0].descriptor, {
name: 'counter',
description: 'test',
metricKind: MetricKind.COUNTER,
unit: '1',
valueType: api.ValueType.DOUBLE,
});
assert.strictEqual(record[0].labels, labels);
const value = record[0].aggregator.toPoint().value as Sum;
assert.strictEqual(value, 10.45);
});
it('should create an INT counter', async () => {
const key = 'key';
const counter = meter.createCounter('counter', {
description: 'test',
valueType: api.ValueType.INT,
});
const labels = { [key]: 'counter-value' };
const boundCounter = counter.bind(labels);
boundCounter.add(10.45);
await meter.collect();
const record = meter.getProcessor().checkPointSet();
assert.strictEqual(record.length, 1);
assert.deepStrictEqual(record[0].descriptor, {
name: 'counter',
description: 'test',
metricKind: MetricKind.COUNTER,
unit: '1',
valueType: api.ValueType.INT,
});
assert.strictEqual(record[0].labels, labels);
const value = record[0].aggregator.toPoint().value as Sum;
assert.strictEqual(value, 10);
});
});
it('should allow custom processor', () => {
const customMeter = new MeterProvider().getMeter('custom-processor', '*', {
processor: new CustomProcessor(),
});
assert.throws(() => {
const valueRecorder = customMeter.createValueRecorder('myValueRecorder');
valueRecorder.bind({}).record(1);
}, /aggregatorFor method not implemented/);
});
});
class CustomProcessor extends Processor {
process(record: MetricRecord): void {
throw new Error('process method not implemented.');
}
aggregatorFor(metricKind: MetricDescriptor): Aggregator {
throw new Error('aggregatorFor method not implemented.');
}
}
function ensureMetric(metric: MetricRecord, name?: string, value?: LastValue) {
assert.ok(metric.aggregator instanceof LastValueAggregator);
const lastValue = metric.aggregator.toPoint().value;
if (value) {
assert.deepStrictEqual(lastValue, value);
}
const descriptor = metric.descriptor;
assert.strictEqual(descriptor.name, name || 'name');
assert.strictEqual(descriptor.description, 'desc');
assert.strictEqual(descriptor.unit, '1');
assert.strictEqual(descriptor.metricKind, MetricKind.VALUE_OBSERVER);
assert.strictEqual(descriptor.valueType, api.ValueType.DOUBLE);
}