Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(prometheus): serialize resource as target_info gauge #3300

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 4 additions & 3 deletions experimental/examples/prometheus/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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"
pichlermarc marked this conversation as resolved.
Show resolved Hide resolved
}
}
1 change: 1 addition & 0 deletions experimental/examples/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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
dyladan marked this conversation as resolved.
Show resolved Hide resolved
* @param response HTTP response objet used to response to request
*/
public getMetricsRequestHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -167,6 +171,43 @@ 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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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),
legendecas marked this conversation as resolved.
Show resolved Hide resolved
instance: extractInstanceLabel(resource)
};

const results = stringify(name, resourceAttributes, 1, undefined, otherAttributes).trim();
return `${help}\n${type}\n${results}\n`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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}`,
Expand All @@ -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}`,
Expand Down Expand Up @@ -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, [
legendecas marked this conversation as resolved.
Show resolved Hide resolved
...serializedEmptyResourceLines,
''
]);
});

it('should add a description if missing', async () => {
Expand All @@ -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}`,
Expand All @@ -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}`,
Expand All @@ -380,14 +400,15 @@ 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}`,
'',
]);
});

it('should export an ObservableCounter as a counter', async() => {
it('should export an ObservableCounter as a counter', async () => {
function getValue() {
return 20;
}
Expand All @@ -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}`,
Expand Down Expand Up @@ -436,14 +458,15 @@ 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}`,
'',
]);
});

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',
});
Expand All @@ -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}`,
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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',
Expand Down
Loading