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

refactor(metrics): export pipeline #1017

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
af75f12
refactor(metrics): metrics provider implements controller
legendecas May 5, 2020
963fb8c
refactor: make PushController a variant of PullController
legendecas May 24, 2020
5ada775
Merge branch 'master' into pull-controller
legendecas May 24, 2020
e80e541
feat: add server start callback
legendecas May 24, 2020
e7afc5f
feat: prometheus export pipeline
legendecas May 24, 2020
4b22cfb
Merge branch 'master' into pull-controller
legendecas May 28, 2020
60af018
Merge branch 'master' into pull-controller
legendecas May 31, 2020
5dda52a
feat: take `ConsoleMetricExporter` as examples
legendecas Jun 3, 2020
bb9a2be
Merge branch 'master' into pull-controller
legendecas Jun 3, 2020
2194b8c
Merge branch 'master' into pull-controller
legendecas Jun 24, 2020
15b03b0
test: removing assertion on deprecated labelKeys and monotonic attrib…
legendecas Jun 24, 2020
902454c
test: replace file headers
legendecas Jun 24, 2020
3dd9ebd
test: lint markdown autofix
legendecas Jun 24, 2020
b3a5ddc
test: add test coverage for PullController
legendecas Jun 24, 2020
c88bcec
Merge branch 'master' into pull-controller
legendecas Jul 3, 2020
58eddf2
test: await asynchronous collect
legendecas Jul 6, 2020
afbf0b2
docs: controllers can be installed as global meter provider
legendecas Jul 6, 2020
d4a45bc
feat(prometheus): add support for async metric collection
legendecas Jul 6, 2020
b679073
Merge remote-tracking branch 'upstream/master' into pull-controller
legendecas Jul 15, 2020
8440c53
refactor: fix naming confusion issue
legendecas Jul 15, 2020
30e883b
style: autofix
legendecas Jul 15, 2020
65c2e46
feat: add export pipeline installer interface
legendecas Jul 16, 2020
87a5981
Merge remote-tracking branch 'upstream/master' into pull-controller
legendecas Jul 16, 2020
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
23 changes: 8 additions & 15 deletions getting-started/ts-example/monitoring.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import { MeterProvider } from '@opentelemetry/metrics';
import { Metric, BoundCounter } from '@opentelemetry/api';
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { metrics, Metric, BoundCounter } from '@opentelemetry/api';
import { installExportPipeline } from '@opentelemetry/exporter-prometheus';

const exporter = new PrometheusExporter(
{
startServer: true,
},
() => {
console.log('prometheus scrape endpoint: http://localhost:9464/metrics');
},
);
installExportPipeline({
startServer: true,
}, () => {
console.log('prometheus scrape endpoint: http://localhost:9464/metrics');
});

const meter = new MeterProvider({
exporter,
interval: 1000,
}).getMeter('example-ts');
const meter = metrics.getMeter('example-ts');

const requestCount: Metric<BoundCounter> = meter.createCounter("requests", {
monotonic: true,
Expand Down
16 changes: 6 additions & 10 deletions packages/opentelemetry-exporter-prometheus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,23 @@ The OpenTelemetry Prometheus Metrics Exporter allows the user to send collected
## Installation

```bash
npm install --save @opentelemetry/metrics
npm install --save @opentelemetry/exporter-prometheus
npm install --save @opentelemetry/exporter-prometheus @opentelemetry/api
```

## Usage

Create & register the exporter on your application.

```js
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
const { MeterProvider } = require('@opentelemetry/metrics');
const prometheus = require('@opentelemetry/exporter-prometheus');
const api = require('@opentelemetry/api');

// Add your port and startServer to the Prometheus options
const options = {port: 9464, startServer: true};
const exporter = new PrometheusExporter(options);
const options = { port: 9464, startServer: true };
prometheus.installExportPipeline(options);

// Register the exporter
const meter = new MeterProvider({
exporter,
interval: 1000,
}).getMeter('example-prometheus');
const meter = api.metrics.getMeter('example-prometheus');

// Now, start recording data
const counter = meter.createCounter('metric_name', {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*!
* Copyright 2020, 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 * as api from '@opentelemetry/api';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the installExportPipeline for me it looks kind of magic from outside, not sure what is happening inside and how the exporter is being bound with meter and what if I want to change it. If we want to have similar pattern for other exporters (imho we should have those things unified) I would move installExportPipeline to a core or metrics and allow setting exporter as dependency injection. This way it will look a bit more clear for me and I will still be able to use the same pattern for any other exporter that I want. WDYT ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key point for installExportPipeline is that the export policy mainly depends on how the exporter works. For example, the prometheus exporter (which starts a server awaiting incoming scrapping) would prefer a pulling strategy and the console exporter would prefer a periodically print of the metric records. For this reason, the installExportPipeline was implemented for each exporter and in the installExportPipeline there is nothing magic other than installing a metric exporting strategy (controller) as the global metric provider as the metric provider is the role who holds all meters/metrics.

If the installExportPipeline is implemented as a method in opentelemetry/metric or core, there is no significant difference with the current approach since the exporting strategy still needs to be determined by who installs the pipeline which can be probably the end-user who uses the exporter rather than the exporter themselves.

e.g.

metrics.installExportPipeline({ exporter: new PrometheusExporter({ startServer: true }), strategy: 'pull' });

The assumption is based on that the end-user doesn't have to care about the strategy the exporter used. They do care about which exporter they are going to work with.

import { PullController } from '@opentelemetry/metrics';
import { PrometheusExporter } from '../prometheus';
import { ExporterConfig } from './types';

/**
* Install Prometheus export pipeline to global metrics api.
* @param exporterConfig Exporter configuration
* @param callback Callback to be called after a server was started
* @param controllerConfig Default meter configuration
*/
export function installExportPipeline(
exporterConfig?: ExporterConfig,
callback?: () => void,
controllerConfig?: ConstructorParameters<typeof PullController>[0]
) {
const exporter = new PrometheusExporter(exporterConfig, callback);
const pullController = new PullController({ ...controllerConfig, exporter });
exporter.setPullCallback(() => pullController.collect());
api.metrics.setGlobalMeterProvider(pullController);
return { exporter, controller: pullController };
}
1 change: 1 addition & 0 deletions packages/opentelemetry-exporter-prometheus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@

export * from './prometheus';
export * from './export/types';
export * from './export/export-pipeline';
11 changes: 11 additions & 0 deletions packages/opentelemetry-exporter-prometheus/src/prometheus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class PrometheusExporter implements MetricExporter {
private readonly _server: Server;
private readonly _prefix?: string;
private readonly _invalidCharacterRegex = /[^a-z0-9_]/gi;
private _pullCallback: (() => void) | undefined;

// This will be required when histogram is implemented. Leaving here so it is not forgotten
// Histogram cannot have a label named 'le'
Expand Down Expand Up @@ -117,6 +118,15 @@ export class PrometheusExporter implements MetricExporter {
this.stopServer(cb);
}

/**
* Set the pulling callback will be called on request of metrics.
*
* @param cb The callback
*/
setPullCallback(cb?: () => void) {
legendecas marked this conversation as resolved.
Show resolved Hide resolved
this._pullCallback = cb;
}

/**
* Updates the value of a single metric in the registry
*
Expand Down Expand Up @@ -295,6 +305,7 @@ export class PrometheusExporter implements MetricExporter {
response: ServerResponse
) => {
if (url.parse(request.url!).pathname === this._endpoint) {
this._pullCallback?.();
this._exportMetrics(response);
} else {
this._notFound(response);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*!
* Copyright 2020, 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 * as api from '@opentelemetry/api';
import * as assert from 'assert';
import { installExportPipeline, PrometheusExporter } from '../../src';
import { get } from '../request-util';
import { mockedTimeMS } from '../sandbox';
import { PullController } from '@opentelemetry/metrics';

describe('Prometheus Export Pipeline', () => {
let exporter: PrometheusExporter;

afterEach(done => {
exporter?.shutdown(done);
});

it('should install to global metrics api', async () => {
let controller: PullController;
await new Promise(resolve => {
({ exporter, controller } = installExportPipeline(
{ startServer: true },
resolve
));
});

assert.strictEqual(api.metrics.getMeterProvider(), controller!);

const counter = api.metrics.getMeter('test').createCounter('counter', {
description: 'a test description',
labelKeys: ['key1'],
});
const boundCounter = counter.bind({ key1: 'labelValue1' });
boundCounter.add(10);

const res = await get('http://localhost:9464/metrics');
assert.strictEqual(res.statusCode, 200);
assert(res.body != null);
const lines = res.body!.split('\n');

assert.strictEqual(lines[0], '# HELP counter a test description');

assert.deepStrictEqual(lines, [
'# HELP counter a test description',
'# TYPE counter counter',
`counter{key1="labelValue1"} 10 ${mockedTimeMS}`,
'',
]);
});
});
26 changes: 5 additions & 21 deletions packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,19 @@
* limitations under the License.
*/

import { HrTime, ObserverResult } from '@opentelemetry/api';
import { ObserverResult } from '@opentelemetry/api';
import {
CounterMetric,
CounterSumAggregator,
Meter,
MeterProvider,
PushController,
ObserverMetric,
Point,
} from '@opentelemetry/metrics';
import * as assert from 'assert';
import * as http from 'http';
import { PrometheusExporter } from '../src';

const mockedHrTime: HrTime = [1586347902211, 0];
const mockedTimeMS = 1586347902211000;
import { mockedTimeMS } from './sandbox';

describe('PrometheusExporter', () => {
let toPoint: () => Point;
before(() => {
toPoint = CounterSumAggregator.prototype.toPoint;
CounterSumAggregator.prototype.toPoint = function (): Point {
const point = toPoint.apply(this);
point.timestamp = mockedHrTime;
return point;
};
});
after(() => {
CounterSumAggregator.prototype.toPoint = toPoint;
});
describe('constructor', () => {
it('should construct an exporter', () => {
const exporter = new PrometheusExporter();
Expand Down Expand Up @@ -189,7 +173,7 @@ describe('PrometheusExporter', () => {

beforeEach(done => {
exporter = new PrometheusExporter();
meter = new MeterProvider().getMeter('test-prometheus');
meter = new PushController().getMeter('test-prometheus');
exporter.startServer(done);
});

Expand Down Expand Up @@ -418,7 +402,7 @@ describe('PrometheusExporter', () => {
let exporter: PrometheusExporter | undefined;

beforeEach(() => {
meter = new MeterProvider().getMeter('test-prometheus');
meter = new PushController().getMeter('test-prometheus');
counter = meter.createCounter('counter') as CounterMetric;
counter.bind({ key1: 'labelValue1' }).add(10);
});
Expand Down
42 changes: 42 additions & 0 deletions packages/opentelemetry-exporter-prometheus/test/request-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*!
* Copyright 2020, 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 * as http from 'http';

export interface TestMessage {
statusCode?: number;
body?: string;
}

export function get(url: string): Promise<TestMessage> {
return new Promise((resolve, reject) => {
http
.get(url, (res: http.IncomingMessage) => {
res.on('error', reject);

res.setEncoding('utf8');

let body = '';
res.on('data', data => {
body += data;
});
res.on('end', () => {
resolve({ statusCode: res.statusCode, body });
});
})
.on('error', reject);
});
}
33 changes: 33 additions & 0 deletions packages/opentelemetry-exporter-prometheus/test/sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2020, 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 { Point, CounterSumAggregator } from '@opentelemetry/metrics';
import { HrTime } from '@opentelemetry/api';

export const mockedHrTime: HrTime = [1586347902211, 0];
export const mockedTimeMS = 1586347902211000;

let toPoint: () => Point;
before(() => {
toPoint = CounterSumAggregator.prototype.toPoint;
CounterSumAggregator.prototype.toPoint = function (): Point {
const point = toPoint.apply(this);
point.timestamp = mockedHrTime;
return point;
};
});
after(() => {
CounterSumAggregator.prototype.toPoint = toPoint;
});
Loading