From 7c6281a1a786e237773556ce9dbb5ee9c979cab7 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 6 Oct 2022 16:09:45 +0200 Subject: [PATCH 01/11] feat(prometheus): serialize resource as target_info gauge --- experimental/examples/prometheus/package.json | 7 +- .../examples/prometheus/prometheus.yml | 1 + .../src/PrometheusExporter.ts | 15 +- .../src/PrometheusSerializer.ts | 63 ++++- .../test/PrometheusExporter.test.ts | 42 +++- .../test/PrometheusSerializer.test.ts | 218 ++++++++++++++---- .../tsconfig.json | 3 + lerna.json | 1 + 8 files changed, 294 insertions(+), 56 deletions(-) diff --git a/experimental/examples/prometheus/package.json b/experimental/examples/prometheus/package.json index 9816279e53..99f4a2a899 100644 --- a/experimental/examples/prometheus/package.json +++ b/experimental/examples/prometheus/package.json @@ -1,6 +1,6 @@ { "name": "prometheus-example", - "version": "0.32.0", + "version": "0.33.0", "description": "Example of using @opentelemetry/sdk-metrics and @opentelemetry/exporter-prometheus", "main": "index.js", "scripts": { @@ -10,7 +10,8 @@ "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.0.2", - "@opentelemetry/exporter-prometheus": "0.32.0", - "@opentelemetry/sdk-metrics": "0.32.0" + "@opentelemetry/exporter-prometheus": "0.33.0", + "@opentelemetry/sdk-metrics": "0.33.0", + "@opentelemetry/resources": "1.7.0" } } diff --git a/experimental/examples/prometheus/prometheus.yml b/experimental/examples/prometheus/prometheus.yml index 40ade8631d..c72c8593b8 100644 --- a/experimental/examples/prometheus/prometheus.yml +++ b/experimental/examples/prometheus/prometheus.yml @@ -5,5 +5,6 @@ scrape_configs: - job_name: 'opentelemetry' # metrics_path defaults to '/metrics' # scheme defaults to 'http'. + honor_labels: true static_configs: - targets: ['localhost:9464'] diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts index 42b051e301..cbf20d0004 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts @@ -18,8 +18,17 @@ import { diag } from '@opentelemetry/api'; import { globalErrorHandler, } from '@opentelemetry/core'; -import { Aggregation, AggregationTemporality, MetricReader } from '@opentelemetry/sdk-metrics'; -import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; +import { + Aggregation, + AggregationTemporality, + MetricReader +} from '@opentelemetry/sdk-metrics'; +import { + createServer, + IncomingMessage, + Server, + ServerResponse +} from 'http'; import { ExporterConfig } from './export/types'; import { PrometheusSerializer } from './PrometheusSerializer'; /** Node.js v8.x compat */ @@ -154,7 +163,7 @@ export class PrometheusExporter extends MetricReader { /** * Request handler that responds with the current state of metrics - * @param request Incoming HTTP request of server instance + * @param _request Incoming HTTP request of server instance * @param response HTTP response objet used to response to request */ public getMetricsRequestHandler( diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index 94144732ac..8f3eab4873 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -24,11 +24,15 @@ import { DataPoint, Histogram, } from '@opentelemetry/sdk-metrics'; -import type { +import { hrTimeToMilliseconds } from '@opentelemetry/core'; +import { + Resource, +} from '@opentelemetry/resources'; + +import { MetricAttributes, MetricAttributeValue } from '@opentelemetry/api-metrics'; -import { hrTimeToMilliseconds } from '@opentelemetry/core'; type PrometheusDataTypeLiteral = | 'counter' @@ -167,6 +171,43 @@ function stringify( }\n`; } +export function filterReservedResourceAttributes(resource: Resource): MetricAttributes { + return Object.fromEntries( + Object.entries(resource.attributes) + .filter(([key]) => { + if (key === 'job') { + diag.warn('Cannot serialize reserved resource attribute "job".'); + return true; + } + if (key === 'instance') { + diag.warn('Cannot serialize reserved resource attribute "instance".'); + return true; + } + + // drop any entries that will be used to construct 'job' and 'instance' + return key !== 'service.name' && key !== 'service.namespace' && key !== 'service.instance.id'; + } + )); +} + +function extractJobLabel(resource: Resource): string | undefined { + const serviceName = resource.attributes['service.name']; + const serviceNamespace = resource.attributes['service.namespace']; + + if (serviceNamespace != null && serviceName != null) { + return `${serviceNamespace.toString()}/${serviceName.toString()}`; + } else if (serviceName != null) { + return serviceName.toString(); + } + + return undefined; +} + +function extractInstanceLabel(resource: Resource): string | undefined { + const instance = resource.attributes['service.instance.id']; + return instance?.toString(); +} + export class PrometheusSerializer { private _prefix: string | undefined; private _appendTimestamp: boolean; @@ -179,7 +220,7 @@ export class PrometheusSerializer { } serialize(resourceMetrics: ResourceMetrics): string { - let str = ''; + let str = this._serializeResource(resourceMetrics.resource); for (const scopeMetrics of resourceMetrics.scopeMetrics) { str += this._serializeScopeMetrics(scopeMetrics); } @@ -311,4 +352,20 @@ export class PrometheusSerializer { return results; } + + protected _serializeResource(resource: Resource): string { + const name = 'target_info'; + const help = `# HELP ${name} Target metadata`; + const type = `# TYPE ${name} gauge`; + + const resourceAttributes = filterReservedResourceAttributes(resource); + + const otherAttributes = { + job: extractJobLabel(resource), + instance: extractInstanceLabel(resource) + }; + + const results = stringify(name, resourceAttributes, 1, undefined, otherAttributes).trim(); + return `${help}\n${type}\n${results}\n`; + } } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts index 080ac599e3..52dac40d54 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts @@ -14,16 +14,28 @@ * limitations under the License. */ -import { Meter, ObservableResult } from '@opentelemetry/api-metrics'; +import { + Meter, + ObservableResult +} from '@opentelemetry/api-metrics'; import { MeterProvider } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import * as sinon from 'sinon'; import * as http from 'http'; import { PrometheusExporter } from '../src'; -import { mockedHrTimeMs, mockHrTime } from './util'; +import { + mockedHrTimeMs, + mockHrTime +} from './util'; import { SinonStubbedInstance } from 'sinon'; import { Counter } from '@opentelemetry/api-metrics'; +const serializedEmptyResourceLines = [ + '# HELP target_info Target metadata', + '# TYPE target_info gauge', + 'target_info{job="",instance=""} 1' +]; + describe('PrometheusExporter', () => { beforeEach(() => { mockHrTime(); @@ -249,11 +261,12 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.strictEqual( - lines[0], + lines[serializedEmptyResourceLines.length], '# HELP counter_total a test description' ); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter_total a test description', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -283,6 +296,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP metric_observable_gauge a test description', '# TYPE metric_observable_gauge gauge', `metric_observable_gauge{pid="123",core="1"} 0.999 ${mockedHrTimeMs}`, @@ -302,6 +316,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter_total a test description', '# TYPE counter_total counter', `counter_total{counterKey1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -331,11 +346,14 @@ describe('PrometheusExporter', () => { }); }); - it('should export a comment if no metrics are registered', async () => { + it('should export resource even if no metrics are registered', async () => { const body = await request('http://localhost:9464/metrics'); const lines = body.split('\n'); - assert.deepStrictEqual(lines, ['# no registered metrics']); + assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, + '' + ]); }); it('should add a description if missing', async () => { @@ -347,6 +365,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -363,6 +382,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter_bad_name_total description missing', '# TYPE counter_bad_name_total counter', `counter_bad_name_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -380,6 +400,7 @@ describe('PrometheusExporter', () => { const body = await request('http://localhost:9464/metrics'); const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter a test description', '# TYPE counter gauge', `counter{key1="attributeValue1"} 20 ${mockedHrTimeMs}`, @@ -387,7 +408,7 @@ describe('PrometheusExporter', () => { ]); }); - it('should export an ObservableCounter as a counter', async() => { + it('should export an ObservableCounter as a counter', async () => { function getValue() { return 20; } @@ -408,6 +429,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP metric_observable_counter a test description', '# TYPE metric_observable_counter counter', `metric_observable_counter{key1="attributeValue1"} 20 ${mockedHrTimeMs}`, @@ -436,6 +458,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP metric_observable_up_down_counter a test description', '# TYPE metric_observable_up_down_counter gauge', `metric_observable_up_down_counter{key1="attributeValue1"} 20 ${mockedHrTimeMs}`, @@ -443,7 +466,7 @@ describe('PrometheusExporter', () => { ]); }); - it('should export a Histogram as a summary', async() => { + it('should export a Histogram as a summary', async () => { const histogram = meter.createHistogram('test_histogram', { description: 'a test description', }); @@ -454,6 +477,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP test_histogram a test description', '# TYPE test_histogram histogram', `test_histogram_count{key1="attributeValue1"} 1 ${mockedHrTimeMs}`, @@ -507,6 +531,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP test_prefix_counter_total description missing', '# TYPE test_prefix_counter_total counter', `test_prefix_counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -535,6 +560,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -563,6 +589,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -591,6 +618,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedEmptyResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', 'counter_total{key1="attributeValue1"} 10', diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts index 1ae4ab8f53..69cc15c31d 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts @@ -15,7 +15,10 @@ */ import * as assert from 'assert'; -import { MetricAttributes, UpDownCounter } from '@opentelemetry/api-metrics'; +import { + MetricAttributes, + UpDownCounter +} from '@opentelemetry/api-metrics'; import { Aggregation, AggregationTemporality, @@ -31,13 +34,22 @@ import { } from '@opentelemetry/sdk-metrics'; import * as sinon from 'sinon'; import { PrometheusSerializer } from '../src'; -import { mockedHrTimeMs, mockHrTime } from './util'; +import { + mockedHrTimeMs, + mockHrTime +} from './util'; +import { Resource } from '@opentelemetry/resources'; const attributes = { foo1: 'bar1', foo2: 'bar2', }; +const serializedEmptyResource = + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{job="",instance=""} 1\n'; + class TestMetricReader extends MetricReader { constructor() { super({ @@ -147,11 +159,11 @@ describe('PrometheusSerializer', () => { 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` + `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` ); }); @@ -161,11 +173,11 @@ describe('PrometheusSerializer', () => { 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' + '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' ); }); }); @@ -203,9 +215,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total counter\n' + - `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + - `test_total{val="2"} 1 ${mockedHrTimeMs}\n` + '# TYPE test_total counter\n' + + `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + + `test_total{val="2"} 1 ${mockedHrTimeMs}\n` ); }); @@ -215,9 +227,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total counter\n' + - 'test_total{val="1"} 1\n' + - 'test_total{val="2"} 1\n' + '# TYPE test_total counter\n' + + 'test_total{val="1"} 1\n' + + 'test_total{val="2"} 1\n' ); }); }); @@ -278,7 +290,7 @@ describe('PrometheusSerializer', () => { const reader = new TestMetricReader(); const meterProvider = new MeterProvider({ views: [ - new View({aggregation: new LastValueAggregation(), instrumentName: '*' }) + new View({ aggregation: new LastValueAggregation(), instrumentName: '*' }) ] }); meterProvider.addMetricReader(reader); @@ -361,19 +373,19 @@ describe('PrometheusSerializer', () => { 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` + '# 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` ); }); @@ -465,6 +477,7 @@ describe('PrometheusSerializer', () => { const result = await getCounterResult('test', serializer, { unit: unitOfMetric, exportAll: true }); assert.strictEqual( result, + serializedEmptyResource + '# HELP test_total description missing\n' + `# UNIT test_total ${unitOfMetric}\n` + '# TYPE test_total counter\n' + @@ -478,6 +491,7 @@ describe('PrometheusSerializer', () => { const result = await getCounterResult('test', serializer, { exportAll: true }); assert.strictEqual( result, + serializedEmptyResource + '# HELP test_total description missing\n' + '# TYPE test_total counter\n' + `test_total 1 ${mockedHrTimeMs}\n` @@ -593,13 +607,13 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, 'test_total{' + - '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` + '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` ); }); @@ -607,7 +621,7 @@ describe('PrometheusSerializer', () => { const serializer = new PrometheusSerializer(); const result = await testSerializer(serializer, 'test_total', counter => { - // if you try to use a attribute name like account-id prometheus will complain + // if you try to use an attribute name like account-id prometheus will complain // with an error like: // error while linting: text format parsing error in line 282: expected '=' after label name, found '-' counter.add(1, ({ @@ -621,4 +635,128 @@ describe('PrometheusSerializer', () => { ); }); }); + + describe('_serializeResource', () => { + it('should serialize resource', () => { + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + env: 'prod', + hostname: 'myhost', + datacenter: 'sdc', + region: 'europe', + owner: 'frontend' + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend",job="",instance=""} 1\n' + ); + }); + + it('should drop service attributes', () => { + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + env: 'prod', + hostname: 'myhost', + datacenter: 'sdc', + region: 'europe', + owner: 'frontend', + 'service.name': 'name', + 'service.namespace': 'namespace', + 'service.instance.id': 'instance id' + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend",job="namespace/name",instance="instance id"} 1\n' + ); + }); + + it('with undefined service.instance.id should add empty instance id', () => { + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + foo: 'bar', + 'service.name': 'name', + 'service.namespace': 'namespace', + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{foo="bar",job="namespace/name",instance=""} 1\n' + ); + }); + + it('with undefined service.instance.id should add empty instance id', () => { + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + foo: 'bar', + 'service.name': 'name', + 'service.namespace': 'namespace', + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{foo="bar",job="namespace/name",instance=""} 1\n' + ); + }); + + it('with undefined service.namespace should only use service.name for "job"', () => { + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + foo: 'bar', + 'service.name': 'name', + 'service.instance.id': '123' + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{foo="bar",job="name",instance="123"} 1\n' + ); + }); + + it('with undefined service.namespace and service.name should add empty "job"', () => { + // service.name MUST exist. But PrometheusSerializer is exported and used by non-SDK packages. + // In case semantic conventions are not adhered to, add empty as a fallback. + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + foo: 'bar', + 'service.instance.id': '123' + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{foo="bar",job="",instance="123"} 1\n' + ); + }); + + it('with defined service.namespace and undefined service.name should add empty "job"', () => { + // service.name MUST exist. But PrometheusSerializer is exported and used by non-SDK packages. + // In case semantic conventions are not adhered to, add empty as a fallback. + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + foo: 'bar', + 'service.instance.id': '123', + 'service.namespace': 'namespace' + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{foo="bar",job="",instance="123"} 1\n' + ); + }); + }); }); diff --git a/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json b/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json index 138ffc3aad..1283f8e6d4 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json @@ -15,6 +15,9 @@ { "path": "../../../packages/opentelemetry-core" }, + { + "path": "../../../packages/opentelemetry-resources" + }, { "path": "../opentelemetry-api-metrics" }, diff --git a/lerna.json b/lerna.json index 5c0f76b9c2..d97ab02a62 100644 --- a/lerna.json +++ b/lerna.json @@ -5,6 +5,7 @@ "api", "packages/*", "experimental/packages/*", + "experimental/examples/*", "experimental/backwards-compatability/*", "integration-tests/*", "selenium-tests", From ccc189930f4170bdacebbeaea69ff70c7bb3bc9f Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 7 Oct 2022 09:00:20 +0200 Subject: [PATCH 02/11] feat(prometheus): do not export filterReservedResourceAttributes --- .../src/PrometheusSerializer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index 8f3eab4873..c7949a5d85 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -171,7 +171,7 @@ function stringify( }\n`; } -export function filterReservedResourceAttributes(resource: Resource): MetricAttributes { +function filterReservedResourceAttributes(resource: Resource): MetricAttributes { return Object.fromEntries( Object.entries(resource.attributes) .filter(([key]) => { From 32791dfc727724d997a4fb4cd55b2481f847bee1 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 7 Oct 2022 09:57:35 +0200 Subject: [PATCH 03/11] fix(changelog): add changelog entry --- experimental/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index f14d274c65..3c785831fd 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) * feat(sdk-node): configure trace exporter with environment variables [#3143](https://github.com/open-telemetry/opentelemetry-js/pull/3143) @svetlanabrennan +* feat(prometheus): serialize resource as target_info gauge [#3300](https://github.com/open-telemetry/opentelemetry-js/pull/3300) @pichlermarc ### :bug: (Bug Fix) From 2c839081a8047da2b4f76aa5fe5d6c2a848abe85 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 7 Oct 2022 10:06:50 +0200 Subject: [PATCH 04/11] feat(prometheus): add resources dependency --- .../packages/opentelemetry-exporter-prometheus/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/package.json b/experimental/packages/opentelemetry-exporter-prometheus/package.json index 0067320ed4..62243f78c4 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/package.json +++ b/experimental/packages/opentelemetry-exporter-prometheus/package.json @@ -61,7 +61,8 @@ "dependencies": { "@opentelemetry/api-metrics": "0.33.0", "@opentelemetry/core": "1.7.0", - "@opentelemetry/sdk-metrics": "0.33.0" + "@opentelemetry/sdk-metrics": "0.33.0", + "@opentelemetry/resources": "1.7.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus" } From 3993d860c0a0f064dd603862fbddfd5df0e83c16 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 10 Oct 2022 13:49:39 +0200 Subject: [PATCH 05/11] fix(prometheus-example): remove resources package. --- .../packages/opentelemetry-exporter-prometheus/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/package.json b/experimental/packages/opentelemetry-exporter-prometheus/package.json index 62243f78c4..0067320ed4 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/package.json +++ b/experimental/packages/opentelemetry-exporter-prometheus/package.json @@ -61,8 +61,7 @@ "dependencies": { "@opentelemetry/api-metrics": "0.33.0", "@opentelemetry/core": "1.7.0", - "@opentelemetry/sdk-metrics": "0.33.0", - "@opentelemetry/resources": "1.7.0" + "@opentelemetry/sdk-metrics": "0.33.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus" } From 7e338e6067c8f1a3d6677fd885fc1a387a59b282 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 10 Oct 2022 15:56:53 +0200 Subject: [PATCH 06/11] fix(prometheus-exporter): remove generation of job and instance labels --- .../src/PrometheusSerializer.ts | 46 +------- .../test/PrometheusExporter.test.ts | 2 +- .../test/PrometheusSerializer.test.ts | 108 +----------------- 3 files changed, 4 insertions(+), 152 deletions(-) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index c7949a5d85..124d59327c 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -171,43 +171,6 @@ function stringify( }\n`; } -function filterReservedResourceAttributes(resource: Resource): MetricAttributes { - return Object.fromEntries( - Object.entries(resource.attributes) - .filter(([key]) => { - if (key === 'job') { - diag.warn('Cannot serialize reserved resource attribute "job".'); - return true; - } - if (key === 'instance') { - diag.warn('Cannot serialize reserved resource attribute "instance".'); - return true; - } - - // drop any entries that will be used to construct 'job' and 'instance' - return key !== 'service.name' && key !== 'service.namespace' && key !== 'service.instance.id'; - } - )); -} - -function extractJobLabel(resource: Resource): string | undefined { - const serviceName = resource.attributes['service.name']; - const serviceNamespace = resource.attributes['service.namespace']; - - if (serviceNamespace != null && serviceName != null) { - return `${serviceNamespace.toString()}/${serviceName.toString()}`; - } else if (serviceName != null) { - return serviceName.toString(); - } - - return undefined; -} - -function extractInstanceLabel(resource: Resource): string | undefined { - const instance = resource.attributes['service.instance.id']; - return instance?.toString(); -} - export class PrometheusSerializer { private _prefix: string | undefined; private _appendTimestamp: boolean; @@ -358,14 +321,7 @@ export class PrometheusSerializer { const help = `# HELP ${name} Target metadata`; const type = `# TYPE ${name} gauge`; - const resourceAttributes = filterReservedResourceAttributes(resource); - - const otherAttributes = { - job: extractJobLabel(resource), - instance: extractInstanceLabel(resource) - }; - - const results = stringify(name, resourceAttributes, 1, undefined, otherAttributes).trim(); + const results = stringify(name, resource.attributes, 1).trim(); return `${help}\n${type}\n${results}\n`; } } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts index 52dac40d54..20a8d5b9ce 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts @@ -33,7 +33,7 @@ import { Counter } from '@opentelemetry/api-metrics'; const serializedEmptyResourceLines = [ '# HELP target_info Target metadata', '# TYPE target_info gauge', - 'target_info{job="",instance=""} 1' + 'target_info 1' ]; describe('PrometheusExporter', () => { diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts index 69cc15c31d..759ea85f0f 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts @@ -48,7 +48,7 @@ const attributes = { const serializedEmptyResource = '# HELP target_info Target metadata\n' + '# TYPE target_info gauge\n' + - 'target_info{job="",instance=""} 1\n'; + 'target_info 1\n'; class TestMetricReader extends MetricReader { constructor() { @@ -651,111 +651,7 @@ describe('PrometheusSerializer', () => { result, '# HELP target_info Target metadata\n' + '# TYPE target_info gauge\n' + - 'target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend",job="",instance=""} 1\n' - ); - }); - - it('should drop service attributes', () => { - const serializer = new PrometheusSerializer(undefined, true); - const result = serializer['_serializeResource'](new Resource({ - env: 'prod', - hostname: 'myhost', - datacenter: 'sdc', - region: 'europe', - owner: 'frontend', - 'service.name': 'name', - 'service.namespace': 'namespace', - 'service.instance.id': 'instance id' - })); - - assert.strictEqual( - result, - '# HELP target_info Target metadata\n' + - '# TYPE target_info gauge\n' + - 'target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend",job="namespace/name",instance="instance id"} 1\n' - ); - }); - - it('with undefined service.instance.id should add empty instance id', () => { - const serializer = new PrometheusSerializer(undefined, true); - const result = serializer['_serializeResource'](new Resource({ - foo: 'bar', - 'service.name': 'name', - 'service.namespace': 'namespace', - })); - - assert.strictEqual( - result, - '# HELP target_info Target metadata\n' + - '# TYPE target_info gauge\n' + - 'target_info{foo="bar",job="namespace/name",instance=""} 1\n' - ); - }); - - it('with undefined service.instance.id should add empty instance id', () => { - const serializer = new PrometheusSerializer(undefined, true); - const result = serializer['_serializeResource'](new Resource({ - foo: 'bar', - 'service.name': 'name', - 'service.namespace': 'namespace', - })); - - assert.strictEqual( - result, - '# HELP target_info Target metadata\n' + - '# TYPE target_info gauge\n' + - 'target_info{foo="bar",job="namespace/name",instance=""} 1\n' - ); - }); - - it('with undefined service.namespace should only use service.name for "job"', () => { - const serializer = new PrometheusSerializer(undefined, true); - const result = serializer['_serializeResource'](new Resource({ - foo: 'bar', - 'service.name': 'name', - 'service.instance.id': '123' - })); - - assert.strictEqual( - result, - '# HELP target_info Target metadata\n' + - '# TYPE target_info gauge\n' + - 'target_info{foo="bar",job="name",instance="123"} 1\n' - ); - }); - - it('with undefined service.namespace and service.name should add empty "job"', () => { - // service.name MUST exist. But PrometheusSerializer is exported and used by non-SDK packages. - // In case semantic conventions are not adhered to, add empty as a fallback. - const serializer = new PrometheusSerializer(undefined, true); - const result = serializer['_serializeResource'](new Resource({ - foo: 'bar', - 'service.instance.id': '123' - })); - - assert.strictEqual( - result, - '# HELP target_info Target metadata\n' + - '# TYPE target_info gauge\n' + - 'target_info{foo="bar",job="",instance="123"} 1\n' - ); - }); - - it('with defined service.namespace and undefined service.name should add empty "job"', () => { - // service.name MUST exist. But PrometheusSerializer is exported and used by non-SDK packages. - // In case semantic conventions are not adhered to, add empty as a fallback. - const serializer = new PrometheusSerializer(undefined, true); - const result = serializer['_serializeResource'](new Resource({ - foo: 'bar', - 'service.instance.id': '123', - 'service.namespace': 'namespace' - })); - - assert.strictEqual( - result, - '# HELP target_info Target metadata\n' + - '# TYPE target_info gauge\n' + - 'target_info{foo="bar",job="",instance="123"} 1\n' + 'target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend"} 1\n' ); }); }); From 993199c358f7dd578eb00b2ce6bc7f25f6db63cd Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 10 Oct 2022 16:04:04 +0200 Subject: [PATCH 07/11] fix(prometheus-example): remove resources package. --- experimental/examples/prometheus/package.json | 3 +-- .../packages/opentelemetry-exporter-prometheus/package.json | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/examples/prometheus/package.json b/experimental/examples/prometheus/package.json index 99f4a2a899..837fcf9b61 100644 --- a/experimental/examples/prometheus/package.json +++ b/experimental/examples/prometheus/package.json @@ -11,7 +11,6 @@ "dependencies": { "@opentelemetry/api": "^1.0.2", "@opentelemetry/exporter-prometheus": "0.33.0", - "@opentelemetry/sdk-metrics": "0.33.0", - "@opentelemetry/resources": "1.7.0" + "@opentelemetry/sdk-metrics": "0.33.0" } } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/package.json b/experimental/packages/opentelemetry-exporter-prometheus/package.json index e45cdd33c6..c8305d3c88 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/package.json +++ b/experimental/packages/opentelemetry-exporter-prometheus/package.json @@ -61,7 +61,8 @@ "dependencies": { "@opentelemetry/api-metrics": "0.33.0", "@opentelemetry/core": "1.7.0", - "@opentelemetry/sdk-metrics": "0.33.0" + "@opentelemetry/sdk-metrics": "0.33.0", + "@opentelemetry/resources": "1.7.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus" } From dfd8a9f78bb0b51211d843d9990717bd0b612319 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 10 Oct 2022 16:05:07 +0200 Subject: [PATCH 08/11] fix(prometheus-example): remove 'honor_lables' setting --- experimental/examples/prometheus/prometheus.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/examples/prometheus/prometheus.yml b/experimental/examples/prometheus/prometheus.yml index c72c8593b8..40ade8631d 100644 --- a/experimental/examples/prometheus/prometheus.yml +++ b/experimental/examples/prometheus/prometheus.yml @@ -5,6 +5,5 @@ scrape_configs: - job_name: 'opentelemetry' # metrics_path defaults to '/metrics' # scheme defaults to 'http'. - honor_labels: true static_configs: - targets: ['localhost:9464'] From e22b2d3ad1c56c9df7dcc21223f67f955e322fe6 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 10 Oct 2022 16:43:01 +0200 Subject: [PATCH 09/11] fix(exporter-prometheus): undo auto-formatting --- .../src/PrometheusSerializer.ts | 9 +-- .../test/PrometheusSerializer.test.ts | 70 +++++++++---------- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index 124d59327c..065244a089 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -24,15 +24,12 @@ import { DataPoint, Histogram, } from '@opentelemetry/sdk-metrics'; -import { hrTimeToMilliseconds } from '@opentelemetry/core'; -import { - Resource, -} from '@opentelemetry/resources'; - -import { +import type { MetricAttributes, MetricAttributeValue } from '@opentelemetry/api-metrics'; +import { hrTimeToMilliseconds } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; type PrometheusDataTypeLiteral = | 'counter' diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts index 759ea85f0f..627ddc7a9f 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts @@ -159,11 +159,11 @@ describe('PrometheusSerializer', () => { 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` + `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` ); }); @@ -173,11 +173,11 @@ describe('PrometheusSerializer', () => { 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' + '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' ); }); }); @@ -266,9 +266,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total gauge\n' + - `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + - `test_total{val="2"} 1 ${mockedHrTimeMs}\n` + '# TYPE test_total gauge\n' + + `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + + `test_total{val="2"} 1 ${mockedHrTimeMs}\n` ); }); @@ -278,9 +278,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total gauge\n' + - 'test_total{val="1"} 1\n' + - 'test_total{val="2"} 1\n' + '# TYPE test_total gauge\n' + + 'test_total{val="1"} 1\n' + + 'test_total{val="2"} 1\n' ); }); }); @@ -374,18 +374,18 @@ describe('PrometheusSerializer', () => { 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` + `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` ); }); @@ -607,13 +607,13 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, 'test_total{' + - '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` + '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` ); }); From 7a30fead8eb34f5ea0b47777fc9ce8f606cc08c4 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 10 Oct 2022 16:46:20 +0200 Subject: [PATCH 10/11] fix(exporter-prometheus): undo auto-formatting --- .../test/PrometheusSerializer.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts index 627ddc7a9f..852946f355 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts @@ -215,9 +215,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total counter\n' + - `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + - `test_total{val="2"} 1 ${mockedHrTimeMs}\n` + '# TYPE test_total counter\n' + + `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + + `test_total{val="2"} 1 ${mockedHrTimeMs}\n` ); }); @@ -227,9 +227,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total counter\n' + - 'test_total{val="1"} 1\n' + - 'test_total{val="2"} 1\n' + '# TYPE test_total counter\n' + + 'test_total{val="1"} 1\n' + + 'test_total{val="2"} 1\n' ); }); }); @@ -266,9 +266,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total gauge\n' + - `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + - `test_total{val="2"} 1 ${mockedHrTimeMs}\n` + '# TYPE test_total gauge\n' + + `test_total{val="1"} 1 ${mockedHrTimeMs}\n` + + `test_total{val="2"} 1 ${mockedHrTimeMs}\n` ); }); @@ -278,9 +278,9 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test_total foobar\n' + - '# TYPE test_total gauge\n' + - 'test_total{val="1"} 1\n' + - 'test_total{val="2"} 1\n' + '# TYPE test_total gauge\n' + + 'test_total{val="1"} 1\n' + + 'test_total{val="2"} 1\n' ); }); }); From b22e4cd075368ecda63b0130e7577545db2d8a90 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 10 Oct 2022 17:13:44 +0200 Subject: [PATCH 11/11] feat(prometheus-exporter): move 'no metrics' comment to serializer --- .../src/PrometheusExporter.ts | 8 +------- .../src/PrometheusSerializer.ts | 12 ++++++++++-- .../test/PrometheusExporter.test.ts | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts index cbf20d0004..4f333eec55 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts @@ -34,8 +34,6 @@ import { PrometheusSerializer } from './PrometheusSerializer'; /** Node.js v8.x compat */ import { URL } from 'url'; -const NO_REGISTERED_METRICS = '# no registered metrics'; - export class PrometheusExporter extends MetricReader { static readonly DEFAULT_OPTIONS = { host: undefined, @@ -204,11 +202,7 @@ export class PrometheusExporter extends MetricReader { if (errors.length) { diag.error('PrometheusExporter: metrics collection errors', ...errors); } - let result = this._serializer.serialize(resourceMetrics); - if (result === '') { - result = NO_REGISTERED_METRICS; - } - response.end(result); + response.end(this._serializer.serialize(resourceMetrics)); }, err => { response.end(`# failed to export metrics: ${err}`); diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index 065244a089..410cbfbf34 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -168,6 +168,8 @@ function stringify( }\n`; } +const NO_REGISTERED_METRICS = '# no registered metrics'; + export class PrometheusSerializer { private _prefix: string | undefined; private _appendTimestamp: boolean; @@ -180,11 +182,17 @@ export class PrometheusSerializer { } serialize(resourceMetrics: ResourceMetrics): string { - let str = this._serializeResource(resourceMetrics.resource); + let str = ''; + for (const scopeMetrics of resourceMetrics.scopeMetrics) { str += this._serializeScopeMetrics(scopeMetrics); } - return str; + + if (str === '') { + str += NO_REGISTERED_METRICS; + } + + return this._serializeResource(resourceMetrics.resource) + str; } private _serializeScopeMetrics(scopeMetrics: ScopeMetrics) { diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts index 20a8d5b9ce..6b60952670 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts @@ -352,7 +352,7 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ ...serializedEmptyResourceLines, - '' + '# no registered metrics' ]); });