Skip to content
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"
}
}
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
* @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),
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, [
...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