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 21 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
27 changes: 10 additions & 17 deletions getting-started/ts-example/monitoring.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
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, PrometheusExporter } from '@opentelemetry/exporter-prometheus';

const exporter = new PrometheusExporter(
{
startServer: true,
},
() => {
console.log(
`prometheus scrape endpoint: http://localhost:${PrometheusExporter.DEFAULT_OPTIONS.port}${PrometheusExporter.DEFAULT_OPTIONS.endpoint}`,
);
},
);
installExportPipeline({
startServer: true,
}, () => {
console.log(
`prometheus scrape endpoint: http://localhost:${PrometheusExporter.DEFAULT_OPTIONS.port}${PrometheusExporter.DEFAULT_OPTIONS.endpoint}`,
);
});

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

const requestCount: Metric<BoundCounter> = meter.createCounter("requests", {
description: "Count all incoming requests"
Expand Down
14 changes: 5 additions & 9 deletions packages/opentelemetry-exporter-prometheus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ 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
Expand All @@ -23,17 +22,14 @@ Create & register the exporter on your application.

```js
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
const { MeterProvider } = require('@opentelemetry/metrics');
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 };
PrometheusExporter.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
33 changes: 32 additions & 1 deletion packages/opentelemetry-exporter-prometheus/src/prometheus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
MetricKind,
MetricRecord,
Sum,
MeterProvider,
} from '@opentelemetry/metrics';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { Counter, Gauge, Metric, Registry } from 'prom-client';
Expand All @@ -42,13 +43,32 @@ export class PrometheusExporter implements MetricExporter {
prefix: '',
};

/**
* 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
*/
static installExportPipeline(
exporterConfig?: ExporterConfig,
callback?: () => void,
controllerConfig?: ConstructorParameters<typeof MeterProvider>[0]
) {
obecny marked this conversation as resolved.
Show resolved Hide resolved
const exporter = new PrometheusExporter(exporterConfig, callback);
const meterProvider = new MeterProvider({ ...controllerConfig, exporter });
exporter.setPullCallback(() => meterProvider.collect());
api.metrics.setGlobalMeterProvider(meterProvider);
return { exporter, meterProvider };
}

private readonly _registry = new Registry();
private readonly _logger: api.Logger;
private readonly _port: number;
private readonly _endpoint: string;
private readonly _server: Server;
private readonly _prefix?: string;
private readonly _invalidCharacterRegex = /[^a-z0-9_]/gi;
private _pullCallback: (() => Promise<unknown>) | 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 @@ -116,6 +136,16 @@ export class PrometheusExporter implements MetricExporter {
this.stopServer(cb);
}

/**
* Set the pulling callback will be called on request of metrics. The
* callback can return a promise for asynchronous metric collection.
*
* @param cb The callback
*/
setPullCallback(cb?: () => Promise<unknown>) {
this._pullCallback = cb;
}

/**
* Updates the value of a single metric in the registry
*
Expand Down Expand Up @@ -292,11 +322,12 @@ export class PrometheusExporter implements MetricExporter {
* @param request Incoming HTTP request to export server
* @param response HTTP response object used to respond to request
*/
private _requestHandler = (
private _requestHandler = async (
request: IncomingMessage,
response: ServerResponse
) => {
if (url.parse(request.url!).pathname === this._endpoint) {
await this._pullCallback?.();
this._exportMetrics(response);
} else {
this._notFound(response);
Expand Down
69 changes: 45 additions & 24 deletions packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,15 @@
* limitations under the License.
*/

import { HrTime, ObserverResult } from '@opentelemetry/api';
import {
CounterMetric,
SumAggregator,
Meter,
MeterProvider,
Point,
} from '@opentelemetry/metrics';
import * as api from '@opentelemetry/api';
import { CounterMetric, Meter, MeterProvider } 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';
import { get } from './request-util';

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

describe('export pipeline', () => {
let exporter: PrometheusExporter;

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

it('should install export pipeline to global metrics api', async () => {
let meterProvider: MeterProvider;
await new Promise(resolve => {
({ exporter, meterProvider } = PrometheusExporter.installExportPipeline(
{ startServer: true },
resolve
));
});

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

const counter = api.metrics.getMeter('test').createCounter('counter', {
description: 'a test description',
});
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}`,
'',
]);
});
});

describe('server', () => {
it('it should start on startServer() and call the callback', done => {
const exporter = new PrometheusExporter({
Expand Down Expand Up @@ -247,7 +268,7 @@ describe('PrometheusExporter', () => {
{
description: 'a test description',
},
(observerResult: ObserverResult) => {
(observerResult: api.ObserverResult) => {
observerResult.observe(getCpuUsage(), {
pid: String(123),
core: '1',
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 The 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 The 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, SumAggregator } from '@opentelemetry/metrics';
import { HrTime } from '@opentelemetry/api';

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

let toPoint: () => Point;
before(() => {
toPoint = SumAggregator.prototype.toPoint;
SumAggregator.prototype.toPoint = function (): Point {
const point = toPoint.apply(this);
point.timestamp = mockedHrTime;
return point;
};
});
after(() => {
SumAggregator.prototype.toPoint = toPoint;
});
26 changes: 20 additions & 6 deletions packages/opentelemetry-metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,26 @@ OpenTelemetry metrics allow a user to collect data and export it to a metrics ba
## Installation

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

## Usage

### Install Export Pipeline
dyladan marked this conversation as resolved.
Show resolved Hide resolved

It is essential to install export pipeline first before all subsequent meter initialization to get exporters work properly. Here we'll take `ConsoleMetricExporter` for an example.

```js
import { ConsoleMetricExporter } from '@opentelemetry/metrics'
import { metrics } from '@opentelemetry/api'

// Install the export pipeline before all subsequent call to metrics.
ConsoleMetricExporter.installExportPipeline();

const meter = metrics.getMeter('example-meter');
const counter = meter.createCounter('foo');
```

### Counter

Choose this kind of metric when the value is a quantity, the sum is of primary interest, and the event count and value distribution are not of primary interest. It is restricted to non-negative increments.
Expand All @@ -28,10 +43,10 @@ Example uses for Counter:
- count the number of 5xx errors.

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

// Initialize the Meter to capture measurements in various ways.
const meter = new MeterProvider().getMeter('your-meter-name');
const meter = metrics.getMeter('your-meter-name');

const counter = meter.createCounter('metric_name', {
description: 'Example of a counter'
Expand All @@ -42,7 +57,6 @@ const labels = { pid: process.pid };
// Create a BoundInstrument associated with specified label values.
const boundCounter = counter.bind(labels);
boundCounter.add(10);

```

### UpDownCounter
Expand All @@ -57,10 +71,10 @@ Example uses for UpDownCounter:
- count semaphore up and down operations

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

// Initialize the Meter to capture measurements in various ways.
const meter = new MeterProvider().getMeter('your-meter-name');
const meter = metrics.getMeter('your-meter-name');

const counter = meter.createUpDownCounter('metric_name', {
description: 'Example of a UpDownCounter'
Expand Down
6 changes: 0 additions & 6 deletions packages/opentelemetry-metrics/src/Meter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import { ValueObserverMetric } from './ValueObserverMetric';
import { SumObserverMetric } from './SumObserverMetric';
import { DEFAULT_METRIC_OPTIONS, DEFAULT_CONFIG, MeterConfig } from './types';
import { Batcher, UngroupedBatcher } from './export/Batcher';
import { PushController } from './export/Controller';
import { NoopExporter } from './export/NoopExporter';

/**
* Meter is an implementation of the {@link Meter} interface.
Expand All @@ -52,10 +50,6 @@ export class Meter implements api.Meter {
this._batcher = config.batcher ?? new UngroupedBatcher();
this._resource = config.resource || Resource.createTelemetrySDKResource();
this._instrumentationLibrary = instrumentationLibrary;
// start the push controller
const exporter = config.exporter || new NoopExporter();
const interval = config.interval;
new PushController(this, exporter, interval);
}

/**
Expand Down
Loading