/* * 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 { SumAggregator, HistogramAggregator, LastValueAggregator, MeterProvider, CounterMetric, ValueRecorderMetric, UpDownCounterMetric, ValueObserverMetric, } from '@opentelemetry/metrics'; import * as assert from 'assert'; import { Labels } from '@opentelemetry/api'; import { PrometheusSerializer } from '../src/PrometheusSerializer'; import { PrometheusLabelsBatcher } from '../src/PrometheusLabelsBatcher'; import { ExactProcessor } from './ExactProcessor'; import { mockedHrTimeMs, mockAggregator } from './util'; const labels = { foo1: 'bar1', foo2: 'bar2', }; describe('PrometheusSerializer', () => { describe('constructor', () => { it('should construct a serializer', () => { const serializer = new PrometheusSerializer(); assert(serializer instanceof PrometheusSerializer); }); }); describe('serialize a metric record', () => { describe('with SumAggregator', () => { mockAggregator(SumAggregator); it('should serialize metric record with sum aggregator', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const counter = meter.createCounter('test') as CounterMetric; counter.bind(labels).add(1); const records = await counter.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, `test{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` ); }); it('serialize metric record with sum aggregator without timestamp', async () => { const serializer = new PrometheusSerializer(undefined, false); const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const counter = meter.createCounter('test') as CounterMetric; counter.bind(labels).add(1); const records = await counter.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual(result, 'test{foo1="bar1",foo2="bar2"} 1\n'); }); }); describe('with LastValueAggregator', () => { mockAggregator(LastValueAggregator); it('should serialize metric record with LastValue aggregator', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider({ processor: new ExactProcessor(LastValueAggregator), }).getMeter('test'); const observer = meter.createValueObserver( 'test', {}, observerResult => { observerResult.observe(1, labels); } ) as ValueObserverMetric; await meter.collect(); const records = await observer.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, `test{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` ); }); it('serialize metric record with sum aggregator without timestamp', async () => { const serializer = new PrometheusSerializer(undefined, false); const meter = new MeterProvider({ processor: new ExactProcessor(LastValueAggregator), }).getMeter('test'); const observer = meter.createValueObserver( 'test', {}, observerResult => { observerResult.observe(1, labels); } ) as ValueObserverMetric; await meter.collect(); const records = await observer.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual(result, 'test{foo1="bar1",foo2="bar2"} 1\n'); }); }); describe('with HistogramAggregator', () => { mockAggregator(HistogramAggregator); it('should serialize metric record with sum aggregator', async () => { const serializer = new PrometheusSerializer(); const processor = new ExactProcessor(HistogramAggregator, [1, 10, 100]); const meter = new MeterProvider({ processor }).getMeter('test'); const recorder = meter.createValueRecorder('test', { description: 'foobar', }) as ValueRecorderMetric; recorder.bind(labels).record(5); const records = await recorder.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, `test_count{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` + `test_sum{foo1="bar1",foo2="bar2"} 5 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="1"} 0 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="10"} 1 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="100"} 1 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="+Inf"} 1 ${mockedHrTimeMs}\n` ); }); it('should serialize metric record with sum aggregator, boundaries defined in constructor', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider().getMeter('test'); const recorder = meter.createValueRecorder('test', { description: 'foobar', boundaries: [1, 10, 100], }) as ValueRecorderMetric; recorder.bind(labels).record(5); const records = await recorder.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, `test_count{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` + `test_sum{foo1="bar1",foo2="bar2"} 5 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="1"} 0 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="10"} 1 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="100"} 1 ${mockedHrTimeMs}\n` + `test_bucket{foo1="bar1",foo2="bar2",le="+Inf"} 1 ${mockedHrTimeMs}\n` ); }); it('serialize metric record with sum aggregator without timestamp', async () => { const serializer = new PrometheusSerializer(undefined, false); const processor = new ExactProcessor(HistogramAggregator, [1, 10, 100]); const meter = new MeterProvider({ processor }).getMeter('test'); const recorder = meter.createValueRecorder('test', { description: 'foobar', }) as ValueRecorderMetric; recorder.bind(labels).record(5); const records = await recorder.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, 'test_count{foo1="bar1",foo2="bar2"} 1\n' + 'test_sum{foo1="bar1",foo2="bar2"} 5\n' + 'test_bucket{foo1="bar1",foo2="bar2",le="1"} 0\n' + 'test_bucket{foo1="bar1",foo2="bar2",le="10"} 1\n' + 'test_bucket{foo1="bar1",foo2="bar2",le="100"} 1\n' + 'test_bucket{foo1="bar1",foo2="bar2",le="+Inf"} 1\n' ); }); }); }); describe('serialize a checkpoint set', () => { describe('with SumAggregator', () => { mockAggregator(SumAggregator); it('should serialize metric record with sum aggregator', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const processor = new PrometheusLabelsBatcher(); const counter = meter.createCounter('test', { description: 'foobar', }) as CounterMetric; counter.bind({ val: '1' }).add(1); counter.bind({ val: '2' }).add(1); const records = await counter.getMetricRecord(); records.forEach(it => processor.process(it)); const checkPointSet = processor.checkPointSet(); const result = serializer.serialize(checkPointSet); assert.strictEqual( result, '# HELP test foobar\n' + '# TYPE test counter\n' + `test{val="1"} 1 ${mockedHrTimeMs}\n` + `test{val="2"} 1 ${mockedHrTimeMs}\n` ); }); it('serialize metric record with sum aggregator without timestamp', async () => { const serializer = new PrometheusSerializer(undefined, false); const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const processor = new PrometheusLabelsBatcher(); const counter = meter.createCounter('test', { description: 'foobar', }) as CounterMetric; counter.bind({ val: '1' }).add(1); counter.bind({ val: '2' }).add(1); const records = await counter.getMetricRecord(); records.forEach(it => processor.process(it)); const checkPointSet = processor.checkPointSet(); const result = serializer.serialize(checkPointSet); assert.strictEqual( result, '# HELP test foobar\n' + '# TYPE test counter\n' + 'test{val="1"} 1\n' + 'test{val="2"} 1\n' ); }); }); describe('with LastValueAggregator', () => { mockAggregator(LastValueAggregator); it('serialize metric record with LastValue aggregator', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider({ processor: new ExactProcessor(LastValueAggregator), }).getMeter('test'); const processor = new PrometheusLabelsBatcher(); const observer = meter.createValueObserver( 'test', { description: 'foobar', }, observerResult => { observerResult.observe(1, labels); } ) as ValueObserverMetric; await meter.collect(); const records = await observer.getMetricRecord(); records.forEach(it => processor.process(it)); const checkPointSet = processor.checkPointSet(); const result = serializer.serialize(checkPointSet); assert.strictEqual( result, '# HELP test foobar\n' + '# TYPE test gauge\n' + `test{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` ); }); }); describe('with HistogramAggregator', () => { mockAggregator(HistogramAggregator); it('serialize metric record with HistogramAggregator aggregator, cumulative', async () => { const serializer = new PrometheusSerializer(); const processor = new ExactProcessor(HistogramAggregator, [1, 10, 100]); const meter = new MeterProvider({ processor }).getMeter('test'); const recorder = meter.createValueRecorder('test', { description: 'foobar', }) as ValueRecorderMetric; recorder.bind({ val: '1' }).record(5); recorder.bind({ val: '1' }).record(50); recorder.bind({ val: '1' }).record(120); recorder.bind({ val: '2' }).record(5); const records = await recorder.getMetricRecord(); const labelBatcher = new PrometheusLabelsBatcher(); records.forEach(it => labelBatcher.process(it)); const checkPointSet = labelBatcher.checkPointSet(); const result = serializer.serialize(checkPointSet); assert.strictEqual( result, '# HELP test foobar\n' + '# TYPE test histogram\n' + `test_count{val="1"} 3 ${mockedHrTimeMs}\n` + `test_sum{val="1"} 175 ${mockedHrTimeMs}\n` + `test_bucket{val="1",le="1"} 0 ${mockedHrTimeMs}\n` + `test_bucket{val="1",le="10"} 1 ${mockedHrTimeMs}\n` + `test_bucket{val="1",le="100"} 2 ${mockedHrTimeMs}\n` + `test_bucket{val="1",le="+Inf"} 3 ${mockedHrTimeMs}\n` + `test_count{val="2"} 1 ${mockedHrTimeMs}\n` + `test_sum{val="2"} 5 ${mockedHrTimeMs}\n` + `test_bucket{val="2",le="1"} 0 ${mockedHrTimeMs}\n` + `test_bucket{val="2",le="10"} 1 ${mockedHrTimeMs}\n` + `test_bucket{val="2",le="100"} 1 ${mockedHrTimeMs}\n` + `test_bucket{val="2",le="+Inf"} 1 ${mockedHrTimeMs}\n` ); }); }); }); describe('serialize non-normalized values', () => { describe('with SumAggregator', () => { mockAggregator(SumAggregator); it('should serialize records without labels', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const counter = meter.createCounter('test') as CounterMetric; counter.bind({}).add(1); const records = await counter.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual(result, `test 1 ${mockedHrTimeMs}\n`); }); it('should serialize non-string label values', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const counter = meter.createCounter('test') as CounterMetric; counter .bind(({ object: {}, NaN: NaN, null: null, undefined: undefined, } as unknown) as Labels) .add(1); const records = await counter.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, `test{object="[object Object]",NaN="NaN",null="null",undefined="undefined"} 1 ${mockedHrTimeMs}\n` ); }); it('should serialize non-finite values', async () => { const serializer = new PrometheusSerializer(); const cases = [ [NaN, 'Nan'], [-Infinity, '-Inf'], [+Infinity, '+Inf'], ] as [number, string][]; for (const esac of cases) { const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const counter = meter.createUpDownCounter( 'test' ) as UpDownCounterMetric; counter.bind(labels).add(esac[0]); const records = await counter.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, `test{foo1="bar1",foo2="bar2"} ${esac[1]} ${mockedHrTimeMs}\n` ); } }); it('should escape backslash (\\), double-quote ("), and line feed (\\n) in label values', async () => { const serializer = new PrometheusSerializer(); const meter = new MeterProvider({ processor: new ExactProcessor(SumAggregator), }).getMeter('test'); const counter = meter.createCounter('test') as CounterMetric; counter .bind(({ backslash: '\u005c', // \ => \\ (\u005c\u005c) doubleQuote: '\u0022', // " => \" (\u005c\u0022) lineFeed: '\u000a', // ↵ => \n (\u005c\u006e) backslashN: '\u005c\u006e', // \n => \\n (\u005c\u005c\u006e) backslashDoubleQuote: '\u005c\u0022', // \" => \\\" (\u005c\u005c\u005c\u0022) backslashLineFeed: '\u005c\u000a', // \↵ => \\\n (\u005c\u005c\u005c\u006e) } as unknown) as Labels) .add(1); const records = await counter.getMetricRecord(); const record = records[0]; const result = serializer.serializeRecord( record.descriptor.name, record ); assert.strictEqual( result, 'test{' + 'backslash="\u005c\u005c",' + 'doubleQuote="\u005c\u0022",' + 'lineFeed="\u005c\u006e",' + 'backslashN="\u005c\u005c\u006e",' + 'backslashDoubleQuote="\u005c\u005c\u005c\u0022",' + 'backslashLineFeed="\u005c\u005c\u005c\u006e"' + `} 1 ${mockedHrTimeMs}\n` ); }); }); }); });