Skip to content

Commit d35ac4f

Browse files
authored
feat: Metrics SDK - aggregator, batcher, controller (open-telemetry#738)
* add Batcher and Aggregator * JSDoc comments * final patch * Descriptor to MetricDescriptor * merge createMeasure PR and fix the tests * move Aggregator interface to types
1 parent 014b324 commit d35ac4f

File tree

15 files changed

+674
-737
lines changed

15 files changed

+674
-737
lines changed

packages/opentelemetry-exporter-prometheus/src/prometheus.ts

+43-46
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717
import { ExportResult } from '@opentelemetry/base';
1818
import { NoopLogger } from '@opentelemetry/core';
1919
import {
20-
LabelValue,
21-
MetricDescriptor,
22-
MetricDescriptorType,
2320
MetricExporter,
24-
ReadableMetric,
21+
MetricRecord,
22+
MetricDescriptor,
23+
LastValue,
24+
MetricKind,
25+
Sum,
2526
} from '@opentelemetry/metrics';
2627
import * as types from '@opentelemetry/api';
2728
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
2829
import { Counter, Gauge, labelValues, Metric, Registry } from 'prom-client';
2930
import * as url from 'url';
3031
import { ExporterConfig } from './export/types';
32+
import { LabelSet } from '@opentelemetry/metrics/build/src/LabelSet';
33+
import { CounterSumAggregator } from '@opentelemetry/metrics/build/src/export/Aggregator';
3134

3235
export class PrometheusExporter implements MetricExporter {
3336
static readonly DEFAULT_OPTIONS = {
@@ -81,13 +84,10 @@ export class PrometheusExporter implements MetricExporter {
8184
* be a no-op and the exporter should reach into the metrics when the export endpoint is
8285
* called. As there is currently no interface to do this, this is our only option.
8386
*
84-
* @param readableMetrics Metrics to be sent to the prometheus backend
87+
* @param records Metrics to be sent to the prometheus backend
8588
* @param cb result callback to be called on finish
8689
*/
87-
export(
88-
readableMetrics: ReadableMetric[],
89-
cb: (result: ExportResult) => void
90-
) {
90+
export(records: MetricRecord[], cb: (result: ExportResult) => void) {
9191
if (!this._server) {
9292
// It is conceivable that the _server may not be started as it is an async startup
9393
// However unlikely, if this happens the caller may retry the export
@@ -97,8 +97,8 @@ export class PrometheusExporter implements MetricExporter {
9797

9898
this._logger.debug('Prometheus exporter export');
9999

100-
for (const readableMetric of readableMetrics) {
101-
this._updateMetric(readableMetric);
100+
for (const record of records) {
101+
this._updateMetric(record);
102102
}
103103

104104
cb(ExportResult.SUCCESS);
@@ -117,51 +117,53 @@ export class PrometheusExporter implements MetricExporter {
117117
/**
118118
* Updates the value of a single metric in the registry
119119
*
120-
* @param readableMetric Metric value to be saved
120+
* @param record Metric value to be saved
121121
*/
122-
private _updateMetric(readableMetric: ReadableMetric) {
123-
const metric = this._registerMetric(readableMetric);
122+
private _updateMetric(record: MetricRecord) {
123+
const metric = this._registerMetric(record);
124124
if (!metric) return;
125125

126-
const labelKeys = readableMetric.descriptor.labelKeys;
126+
const labelKeys = record.descriptor.labelKeys;
127+
const value = record.aggregator.value();
127128

128129
if (metric instanceof Counter) {
129-
for (const ts of readableMetric.timeseries) {
130-
// Prometheus counter saves internal state and increments by given value.
131-
// ReadableMetric value is the current state, not the delta to be incremented by.
132-
// Currently, _registerMetric creates a new counter every time the value changes,
133-
// so the increment here behaves as a set value (increment from 0)
134-
metric.inc(
135-
this._getLabelValues(labelKeys, ts.labelValues),
136-
ts.points[0].value as number
137-
);
138-
}
130+
// Prometheus counter saves internal state and increments by given value.
131+
// ReadableMetric value is the current state, not the delta to be incremented by.
132+
// Currently, _registerMetric creates a new counter every time the value changes,
133+
// so the increment here behaves as a set value (increment from 0)
134+
metric.inc(this._getLabelValues(labelKeys, record.labels), value as Sum);
139135
}
140136

141137
if (metric instanceof Gauge) {
142-
for (const ts of readableMetric.timeseries) {
138+
if (record.aggregator instanceof CounterSumAggregator) {
139+
metric.set(
140+
this._getLabelValues(labelKeys, record.labels),
141+
value as Sum
142+
);
143+
} else {
143144
metric.set(
144-
this._getLabelValues(labelKeys, ts.labelValues),
145-
ts.points[0].value as number
145+
this._getLabelValues(labelKeys, record.labels),
146+
(value as LastValue).value
146147
);
147148
}
148149
}
149150

150151
// TODO: only counter and gauge are implemented in metrics so far
151152
}
152153

153-
private _getLabelValues(keys: string[], values: LabelValue[]) {
154+
private _getLabelValues(keys: string[], values: LabelSet) {
154155
const labelValues: labelValues = {};
156+
const labels = values.labels;
155157
for (let i = 0; i < keys.length; i++) {
156-
if (values[i].value !== null) {
157-
labelValues[keys[i]] = values[i].value!;
158+
if (labels[keys[i]] !== null) {
159+
labelValues[keys[i]] = labels[keys[i]];
158160
}
159161
}
160162
return labelValues;
161163
}
162164

163-
private _registerMetric(readableMetric: ReadableMetric): Metric | undefined {
164-
const metricName = this._getPrometheusMetricName(readableMetric.descriptor);
165+
private _registerMetric(record: MetricRecord): Metric | undefined {
166+
const metricName = this._getPrometheusMetricName(record.descriptor);
165167
const metric = this._registry.getSingleMetric(metricName);
166168

167169
/**
@@ -177,31 +179,26 @@ export class PrometheusExporter implements MetricExporter {
177179
this._registry.removeSingleMetric(metricName);
178180
} else if (metric) return metric;
179181

180-
return this._newMetric(readableMetric, metricName);
182+
return this._newMetric(record, metricName);
181183
}
182184

183-
private _newMetric(
184-
readableMetric: ReadableMetric,
185-
name: string
186-
): Metric | undefined {
185+
private _newMetric(record: MetricRecord, name: string): Metric | undefined {
187186
const metricObject = {
188187
name,
189188
// prom-client throws with empty description which is our default
190-
help: readableMetric.descriptor.description || 'description missing',
191-
labelNames: readableMetric.descriptor.labelKeys,
189+
help: record.descriptor.description || 'description missing',
190+
labelNames: record.descriptor.labelKeys,
192191
// list of registries to register the newly created metric
193192
registers: [this._registry],
194193
};
195194

196-
switch (readableMetric.descriptor.type) {
197-
case MetricDescriptorType.COUNTER_DOUBLE:
198-
case MetricDescriptorType.COUNTER_INT64:
195+
switch (record.descriptor.metricKind) {
196+
case MetricKind.COUNTER:
199197
// there is no such thing as a non-monotonic counter in prometheus
200-
return readableMetric.descriptor.monotonic
198+
return record.descriptor.monotonic
201199
? new Counter(metricObject)
202200
: new Gauge(metricObject);
203-
case MetricDescriptorType.GAUGE_DOUBLE:
204-
case MetricDescriptorType.GAUGE_INT64:
201+
case MetricKind.GAUGE:
205202
return new Gauge(metricObject);
206203
default:
207204
// Other metric types are currently unimplemented

packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts

+21-12
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,13 @@ describe('PrometheusExporter', () => {
187187

188188
const boundCounter = counter.bind(meter.labels({ key1: 'labelValue1' }));
189189
boundCounter.add(10);
190-
exporter.export(meter.getMetrics(), () => {
190+
meter.collect();
191+
exporter.export(meter.getBatcher().checkPointSet(), () => {
191192
// This is to test the special case where counters are destroyed
192193
// and recreated in the exporter in order to get around prom-client's
193194
// aggregation and use ours.
194195
boundCounter.add(10);
195-
exporter.export(meter.getMetrics(), () => {
196+
exporter.export(meter.getBatcher().checkPointSet(), () => {
196197
http
197198
.get('http://localhost:9464/metrics', res => {
198199
res.on('data', chunk => {
@@ -227,7 +228,8 @@ describe('PrometheusExporter', () => {
227228

228229
const boundGauge = gauge.bind(meter.labels({ key1: 'labelValue1' }));
229230
boundGauge.set(10);
230-
exporter.export([gauge.get()!], () => {
231+
meter.collect();
232+
exporter.export(meter.getBatcher().checkPointSet(), () => {
231233
http
232234
.get('http://localhost:9464/metrics', res => {
233235
res.on('data', chunk => {
@@ -259,9 +261,10 @@ describe('PrometheusExporter', () => {
259261
labelKeys: ['counterKey1'],
260262
}) as CounterMetric;
261263

262-
gauge.bind(meter.labels({ key1: 'labelValue1' })).set(10);
263-
counter.bind(meter.labels({ key1: 'labelValue1' })).add(10);
264-
exporter.export([gauge.get()!, counter.get()!], () => {
264+
gauge.bind(meter.labels({ gaugeKey1: 'labelValue1' })).set(10);
265+
counter.bind(meter.labels({ counterKey1: 'labelValue1' })).add(10);
266+
meter.collect();
267+
exporter.export(meter.getBatcher().checkPointSet(), () => {
265268
http
266269
.get('http://localhost:9464/metrics', res => {
267270
res.on('data', chunk => {
@@ -308,7 +311,8 @@ describe('PrometheusExporter', () => {
308311

309312
const boundGauge = gauge.bind(meter.labels({ key1: 'labelValue1' }));
310313
boundGauge.set(10);
311-
exporter.export([gauge.get()!], () => {
314+
meter.collect();
315+
exporter.export(meter.getBatcher().checkPointSet(), () => {
312316
http
313317
.get('http://localhost:9464/metrics', res => {
314318
res.on('data', chunk => {
@@ -333,7 +337,8 @@ describe('PrometheusExporter', () => {
333337
const gauge = meter.createGauge('gauge.bad-name') as GaugeMetric;
334338
const boundGauge = gauge.bind(meter.labels({ key1: 'labelValue1' }));
335339
boundGauge.set(10);
336-
exporter.export([gauge.get()!], () => {
340+
meter.collect();
341+
exporter.export(meter.getBatcher().checkPointSet(), () => {
337342
http
338343
.get('http://localhost:9464/metrics', res => {
339344
res.on('data', chunk => {
@@ -362,7 +367,8 @@ describe('PrometheusExporter', () => {
362367
});
363368

364369
counter.bind(meter.labels({ key1: 'labelValue1' })).add(20);
365-
exporter.export(meter.getMetrics(), () => {
370+
meter.collect();
371+
exporter.export(meter.getBatcher().checkPointSet(), () => {
366372
http
367373
.get('http://localhost:9464/metrics', res => {
368374
res.on('data', chunk => {
@@ -407,7 +413,8 @@ describe('PrometheusExporter', () => {
407413
});
408414

409415
exporter.startServer(() => {
410-
exporter!.export(meter.getMetrics(), () => {
416+
meter.collect();
417+
exporter!.export(meter.getBatcher().checkPointSet(), () => {
411418
http
412419
.get('http://localhost:9464/metrics', res => {
413420
res.on('data', chunk => {
@@ -435,7 +442,8 @@ describe('PrometheusExporter', () => {
435442
});
436443

437444
exporter.startServer(() => {
438-
exporter!.export(meter.getMetrics(), () => {
445+
meter.collect();
446+
exporter!.export(meter.getBatcher().checkPointSet(), () => {
439447
http
440448
.get('http://localhost:8080/metrics', res => {
441449
res.on('data', chunk => {
@@ -463,7 +471,8 @@ describe('PrometheusExporter', () => {
463471
});
464472

465473
exporter.startServer(() => {
466-
exporter!.export(meter.getMetrics(), () => {
474+
meter.collect();
475+
exporter!.export(meter.getBatcher().checkPointSet(), () => {
467476
http
468477
.get('http://localhost:9464/test', res => {
469478
res.on('data', chunk => {

0 commit comments

Comments
 (0)