diff --git a/examples/metrics/README.md b/examples/metrics/README.md new file mode 100644 index 0000000000..b5b09885f3 --- /dev/null +++ b/examples/metrics/README.md @@ -0,0 +1,41 @@ +# Overview + +OpenTelemetry metrics allow a user to collect data and export it to a metrics backend like Prometheus. + +This is a simple example that demonstrates basic metrics collection and exports those metrics to a Prometheus compatible endpoint. + +## Installation + +```sh +$ # from this directory +$ npm install +``` + +How to setup [Prometheus](https://prometheus.io/docs/prometheus/latest/getting_started/) please check +[Setup Prometheus](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-prometheus) + +## Run the Application +- Run the example + +### Observer +```sh +$ npm run start:observer +``` + +### Prometheus +1. In prometheus search for "metric_observer" + +### Links +1. Prometheus Scrape Endpoint http://localhost:9464/metrics +2. Prometheus graph http://localhost:9090/graph + +### Example +

+ +## Useful links +- For more information on OpenTelemetry, visit: +- For more information on OpenTelemetry metrics, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/metrics/metrics/observer.js b/examples/metrics/metrics/observer.js new file mode 100644 index 0000000000..bfc28707a7 --- /dev/null +++ b/examples/metrics/metrics/observer.js @@ -0,0 +1,35 @@ +'use strict'; + +const { MeterProvider } = require('@opentelemetry/metrics'); +const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); + +const exporter = new PrometheusExporter( + { + startServer: true, + }, + () => { + console.log('prometheus scrape endpoint: http://localhost:9464/metrics'); + }, +); + +const meter = new MeterProvider({ + exporter, + interval: 1000, +}).getMeter('example-observer'); + +const otelCpuUsage = meter.createObserver('metric_observer', { + monotonic: false, + labelKeys: ['pid', 'core'], + description: 'Example of a observer', +}); + +function getCpuUsage() { + return Math.random(); +} + +otelCpuUsage.setCallback((observerResult) => { + observerResult.observe(getCpuUsage, meter.labels({ pid: process.pid, core: '1' })); + observerResult.observe(getCpuUsage, meter.labels({ pid: process.pid, core: '2' })); + observerResult.observe(getCpuUsage, meter.labels({ pid: process.pid, core: '3' })); + observerResult.observe(getCpuUsage, meter.labels({ pid: process.pid, core: '4' })); +}); diff --git a/examples/metrics/metrics/observer.png b/examples/metrics/metrics/observer.png new file mode 100644 index 0000000000..7a9184af13 Binary files /dev/null and b/examples/metrics/metrics/observer.png differ diff --git a/examples/metrics/package.json b/examples/metrics/package.json new file mode 100644 index 0000000000..60a61d0fcc --- /dev/null +++ b/examples/metrics/package.json @@ -0,0 +1,33 @@ +{ + "name": "example-metrics", + "private": true, + "version": "0.4.0", + "description": "Example of using @opentelemetry/metrics", + "main": "index.js", + "scripts": { + "start:observer": "node metrics/observer.js" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" + }, + "keywords": [ + "opentelemetry", + "http", + "tracing", + "metrics" + ], + "engines": { + "node": ">=8" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "dependencies": { + "@opentelemetry/exporter-prometheus": "^0.4.0", + "@opentelemetry/metrics": "^0.4.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme" +} diff --git a/packages/opentelemetry-api/package.json b/packages/opentelemetry-api/package.json index 93eb52e62a..d9a44de09a 100644 --- a/packages/opentelemetry-api/package.json +++ b/packages/opentelemetry-api/package.json @@ -18,7 +18,8 @@ "compile": "npm run version:update && tsc -p .", "docs-test": "linkinator docs/out --silent --skip david-dm.org", "docs": "typedoc --tsconfig tsconfig.json --exclude test/**/*.ts", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", diff --git a/packages/opentelemetry-api/src/index.ts b/packages/opentelemetry-api/src/index.ts index 04b0876e0a..2653d5af56 100644 --- a/packages/opentelemetry-api/src/index.ts +++ b/packages/opentelemetry-api/src/index.ts @@ -26,6 +26,7 @@ export * from './metrics/MeterProvider'; export * from './metrics/Metric'; export * from './metrics/NoopMeter'; export * from './metrics/NoopMeterProvider'; +export * from './metrics/ObserverResult'; export * from './trace/attributes'; export * from './trace/Event'; export * from './trace/instrumentation/Plugin'; diff --git a/packages/opentelemetry-api/src/metrics/BoundInstrument.ts b/packages/opentelemetry-api/src/metrics/BoundInstrument.ts index 07302bd22f..11204029d6 100644 --- a/packages/opentelemetry-api/src/metrics/BoundInstrument.ts +++ b/packages/opentelemetry-api/src/metrics/BoundInstrument.ts @@ -16,6 +16,7 @@ import { CorrelationContext } from '../correlation_context/CorrelationContext'; import { SpanContext } from '../trace/span_context'; +import { ObserverResult } from './ObserverResult'; /** An Instrument for Counter Metric. */ export interface BoundCounter { @@ -44,3 +45,14 @@ export interface BoundMeasure { spanContext: SpanContext ): void; } + +/** Base interface for the Observer metrics. */ +export interface BoundObserver { + /** + * Sets callback for the observer. The callback is called once and then it + * sets observers for values. The observers are called periodically to + * retrieve the value. + * @param callback + */ + setCallback(callback: (observerResult: ObserverResult) => {}): void; +} diff --git a/packages/opentelemetry-api/src/metrics/Meter.ts b/packages/opentelemetry-api/src/metrics/Meter.ts index 54f09118d2..4348370fd0 100644 --- a/packages/opentelemetry-api/src/metrics/Meter.ts +++ b/packages/opentelemetry-api/src/metrics/Meter.ts @@ -15,7 +15,7 @@ */ import { Metric, MetricOptions, Labels, LabelSet } from './Metric'; -import { BoundCounter, BoundMeasure } from './BoundInstrument'; +import { BoundCounter, BoundMeasure, BoundObserver } from './BoundInstrument'; /** * An interface to allow the recording metrics. @@ -33,7 +33,7 @@ export interface Meter { createMeasure(name: string, options?: MetricOptions): Metric; /** - * Creates a new `counter` metric. Generally, this kind of metric when the + * Creates a new `Counter` metric. Generally, 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. * @param name the name of the metric. @@ -41,6 +41,13 @@ export interface Meter { */ createCounter(name: string, options?: MetricOptions): Metric; + /** + * Creates a new `Observer` metric. + * @param name the name of the metric. + * @param [options] the metric options. + */ + createObserver(name: string, options?: MetricOptions): Metric; + /** * Provide a pre-computed re-useable LabelSet by * converting the unordered labels into a canonicalized diff --git a/packages/opentelemetry-api/src/metrics/Metric.ts b/packages/opentelemetry-api/src/metrics/Metric.ts index e6e711353f..23f19f6af2 100644 --- a/packages/opentelemetry-api/src/metrics/Metric.ts +++ b/packages/opentelemetry-api/src/metrics/Metric.ts @@ -16,6 +16,7 @@ import { CorrelationContext } from '../correlation_context/CorrelationContext'; import { SpanContext } from '../trace/span_context'; +import { ObserverResult } from './ObserverResult'; /** * Options needed for metric creation @@ -105,6 +106,13 @@ export interface MetricUtils { */ add(value: number, labelSet: LabelSet): void; + /** + * Sets a callback where user can observe value for certain labels + * @param callback a function that will be called once to set observers + * for values + */ + setCallback(callback: (observerResult: ObserverResult) => void): void; + /** * Sets the given value. Values can be negative. */ diff --git a/packages/opentelemetry-api/src/metrics/NoopMeter.ts b/packages/opentelemetry-api/src/metrics/NoopMeter.ts index b3ab749831..2a48c68c96 100644 --- a/packages/opentelemetry-api/src/metrics/NoopMeter.ts +++ b/packages/opentelemetry-api/src/metrics/NoopMeter.ts @@ -16,9 +16,10 @@ import { Meter } from './Meter'; import { MetricOptions, Metric, Labels, LabelSet, MetricUtils } from './Metric'; -import { BoundMeasure, BoundCounter } from './BoundInstrument'; +import { BoundMeasure, BoundCounter, BoundObserver } from './BoundInstrument'; import { CorrelationContext } from '../correlation_context/CorrelationContext'; import { SpanContext } from '../trace/span_context'; +import { ObserverResult } from './ObserverResult'; /** * NoopMeter is a noop implementation of the {@link Meter} interface. It reuses @@ -45,6 +46,15 @@ export class NoopMeter implements Meter { return NOOP_COUNTER_METRIC; } + /** + * Returns constant noop observer. + * @param name the name of the metric. + * @param [options] the metric options. + */ + createObserver(name: string, options?: MetricOptions): Metric { + return NOOP_OBSERVER_METRIC; + } + labels(labels: Labels): LabelSet { return NOOP_LABEL_SET; } @@ -120,6 +130,11 @@ export class NoopMeasureMetric extends NoopMetric } } +export class NoopObserverMetric extends NoopMetric + implements Pick { + setCallback(callback: (observerResult: ObserverResult) => void): void {} +} + export class NoopBoundCounter implements BoundCounter { add(value: number): void { return; @@ -136,6 +151,10 @@ export class NoopBoundMeasure implements BoundMeasure { } } +export class NoopBoundObserver implements BoundObserver { + setCallback(callback: (observerResult: ObserverResult) => {}): void {} +} + export const NOOP_METER = new NoopMeter(); export const NOOP_BOUND_COUNTER = new NoopBoundCounter(); export const NOOP_COUNTER_METRIC = new NoopCounterMetric(NOOP_BOUND_COUNTER); @@ -143,4 +162,7 @@ export const NOOP_COUNTER_METRIC = new NoopCounterMetric(NOOP_BOUND_COUNTER); export const NOOP_BOUND_MEASURE = new NoopBoundMeasure(); export const NOOP_MEASURE_METRIC = new NoopMeasureMetric(NOOP_BOUND_MEASURE); +export const NOOP_BOUND_OBSERVER = new NoopBoundObserver(); +export const NOOP_OBSERVER_METRIC = new NoopObserverMetric(NOOP_BOUND_OBSERVER); + export const NOOP_LABEL_SET = {} as LabelSet; diff --git a/packages/opentelemetry-api/src/metrics/ObserverResult.ts b/packages/opentelemetry-api/src/metrics/ObserverResult.ts new file mode 100644 index 0000000000..f3eb082fd4 --- /dev/null +++ b/packages/opentelemetry-api/src/metrics/ObserverResult.ts @@ -0,0 +1,25 @@ +/*! + * 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 { LabelSet } from './Metric'; + +/** + * Interface that is being used in function setCallback for Observer Metric + */ +export interface ObserverResult { + observers: Map; + observe(callback: Function, labelSet: LabelSet): void; +} diff --git a/packages/opentelemetry-exporter-prometheus/package.json b/packages/opentelemetry-exporter-prometheus/package.json index 6b4a8c40af..2f0462b96c 100644 --- a/packages/opentelemetry-exporter-prometheus/package.json +++ b/packages/opentelemetry-exporter-prometheus/package.json @@ -15,7 +15,8 @@ "precompile": "tsc --version", "version:update": "node ../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", diff --git a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts index 6217993672..27c4bcf7a1 100644 --- a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts +++ b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts @@ -17,10 +17,13 @@ import { ExportResult } from '@opentelemetry/base'; import { NoopLogger } from '@opentelemetry/core'; import { + CounterSumAggregator, + LastValue, MetricExporter, MetricRecord, MetricDescriptor, MetricKind, + ObserverAggregator, Sum, } from '@opentelemetry/metrics'; import * as types from '@opentelemetry/api'; @@ -28,7 +31,6 @@ import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; import { Counter, Gauge, labelValues, Metric, Registry } from 'prom-client'; import * as url from 'url'; import { ExporterConfig } from './export/types'; -import { CounterSumAggregator, LabelSet } from '@opentelemetry/metrics'; export class PrometheusExporter implements MetricExporter { static readonly DEFAULT_OPTIONS = { @@ -138,13 +140,19 @@ export class PrometheusExporter implements MetricExporter { this._getLabelValues(labelKeys, record.labels), value as Sum ); + } else if (record.aggregator instanceof ObserverAggregator) { + metric.set( + this._getLabelValues(labelKeys, record.labels), + value as LastValue, + new Date() + ); } } // TODO: only counter and gauge are implemented in metrics so far } - private _getLabelValues(keys: string[], values: LabelSet) { + private _getLabelValues(keys: string[], values: types.LabelSet) { const labelValues: labelValues = {}; const labels = values.labels; for (let i = 0; i < keys.length; i++) { @@ -191,7 +199,7 @@ export class PrometheusExporter implements MetricExporter { return record.descriptor.monotonic ? new Counter(metricObject) : new Gauge(metricObject); - case MetricKind.GAUGE: + case MetricKind.OBSERVER: return new Gauge(metricObject); default: // Other metric types are currently unimplemented diff --git a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts index 9a6cc2d9cf..263e624ade 100644 --- a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts +++ b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { CounterMetric, Meter, MeterProvider } from '@opentelemetry/metrics'; +import { ObserverResult } from '@opentelemetry/api'; +import { + CounterMetric, + Meter, + MeterProvider, + ObserverMetric, +} from '@opentelemetry/metrics'; import * as assert from 'assert'; import * as http from 'http'; import { PrometheusExporter } from '../src'; @@ -215,6 +221,54 @@ describe('PrometheusExporter', () => { }); }); + it('should export an observer aggregation', done => { + function getCpuUsage() { + return Math.random(); + } + + const observer = meter.createObserver('metric_observer', { + description: 'a test description', + labelKeys: ['pid'], + }) as ObserverMetric; + + observer.setCallback((observerResult: ObserverResult) => { + observerResult.observe( + getCpuUsage, + meter.labels({ pid: String(123), core: '1' }) + ); + }); + + meter.collect(); + exporter.export(meter.getBatcher().checkPointSet(), () => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.strictEqual( + lines[0], + '# HELP metric_observer a test description' + ); + + assert.strictEqual(lines[1], '# TYPE metric_observer gauge'); + + const line3 = lines[2].split(' '); + assert.strictEqual(line3[0], 'metric_observer{pid="123"}'); + assert.ok( + parseFloat(line3[1]) >= 0 && parseFloat(line3[1]) <= 1 + ); + assert.ok(parseInt(line3[2], 10) <= new Date().getTime()); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + }); + it('should export multiple aggregations', done => { const counter = meter.createCounter('counter', { description: 'a test description', diff --git a/packages/opentelemetry-metrics/package.json b/packages/opentelemetry-metrics/package.json index 57882be849..6a7543fe1b 100644 --- a/packages/opentelemetry-metrics/package.json +++ b/packages/opentelemetry-metrics/package.json @@ -15,7 +15,8 @@ "precompile": "tsc --version", "version:update": "node ../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", diff --git a/packages/opentelemetry-metrics/src/BoundInstrument.ts b/packages/opentelemetry-metrics/src/BoundInstrument.ts index 5d441910f5..7a98698dba 100644 --- a/packages/opentelemetry-metrics/src/BoundInstrument.ts +++ b/packages/opentelemetry-metrics/src/BoundInstrument.ts @@ -16,6 +16,7 @@ import * as types from '@opentelemetry/api'; import { Aggregator } from './export/types'; +import { ObserverResult } from './ObserverResult'; /** * This class represent the base to BoundInstrument, which is responsible for generating @@ -131,3 +132,25 @@ export class BoundMeasure extends BaseBoundInstrument this.update(value); } } + +/** + * BoundObserver is an implementation of the {@link BoundObserver} interface. + */ +export class BoundObserver extends BaseBoundInstrument + implements types.BoundObserver { + constructor( + labelSet: types.LabelSet, + disabled: boolean, + monotonic: boolean, + valueType: types.ValueType, + logger: types.Logger, + aggregator: Aggregator + ) { + super(labelSet, logger, monotonic, disabled, valueType, aggregator); + } + + setCallback(callback: (observerResult: types.ObserverResult) => {}): void { + const observerResult = new ObserverResult(); + callback(observerResult); + } +} diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index a0117ede00..693aa450af 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -17,7 +17,7 @@ import * as types from '@opentelemetry/api'; import { ConsoleLogger } from '@opentelemetry/core'; import { BaseBoundInstrument } from './BoundInstrument'; -import { Metric, CounterMetric, MeasureMetric } from './Metric'; +import { Metric, CounterMetric, MeasureMetric, ObserverMetric } from './Metric'; import { MetricOptions, DEFAULT_METRIC_OPTIONS, @@ -107,6 +107,33 @@ export class Meter implements types.Meter { return counter; } + /** + * Creates a new observer metric. + * @param name the name of the metric. + * @param [options] the metric options. + */ + createObserver( + name: string, + options?: types.MetricOptions + ): types.Metric { + if (!this._isValidName(name)) { + this._logger.warn( + `Invalid metric name ${name}. Defaulting to noop metric implementation.` + ); + return types.NOOP_OBSERVER_METRIC; + } + const opt: MetricOptions = { + monotonic: false, // Observers are defined as non-monotonic by default + absolute: false, // not applicable to observer, set to false + logger: this._logger, + ...DEFAULT_METRIC_OPTIONS, + ...options, + }; + const observer = new ObserverMetric(name, opt, this._batcher); + this._registerMetric(name, observer); + return observer; + } + /** * Collects all the metrics created with this `Meter` for export. * diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index 1e38d7410e..179a481828 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -19,7 +19,9 @@ import { BoundCounter, BaseBoundInstrument, BoundMeasure, + BoundObserver, } from './BoundInstrument'; +import { ObserverResult } from './ObserverResult'; import { MetricOptions } from './types'; import { MetricKind, MetricDescriptor, MetricRecord } from './export/types'; import { Batcher } from './export/Batcher'; @@ -161,3 +163,44 @@ export class MeasureMetric extends Metric this.bind(labelSet).record(value); } } + +/** This is a SDK implementation of Observer Metric. */ +export class ObserverMetric extends Metric + implements Pick { + private _observerResult: types.ObserverResult = new ObserverResult(); + + constructor( + name: string, + options: MetricOptions, + private readonly _batcher: Batcher + ) { + super(name, options, MetricKind.OBSERVER); + } + + protected _makeInstrument(labelSet: types.LabelSet): BoundObserver { + return new BoundObserver( + labelSet, + this._disabled, + this._monotonic, + this._valueType, + this._logger, + this._batcher.aggregatorFor(MetricKind.OBSERVER) + ); + } + + getMetricRecord(): MetricRecord[] { + this._observerResult.observers.forEach((callback, labelSet) => { + const instrument = this.bind(labelSet); + instrument.update(callback()); + }); + return super.getMetricRecord(); + } + + /** + * Sets a callback where user can observe value for certain labels + * @param callback + */ + setCallback(callback: (observerResult: types.ObserverResult) => void): void { + callback(this._observerResult); + } +} diff --git a/packages/opentelemetry-metrics/src/ObserverResult.ts b/packages/opentelemetry-metrics/src/ObserverResult.ts new file mode 100644 index 0000000000..6f7e629269 --- /dev/null +++ b/packages/opentelemetry-metrics/src/ObserverResult.ts @@ -0,0 +1,28 @@ +/*! + * Copyright 2019, 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 { ObserverResult as TypeObserverResult } from '@opentelemetry/api'; +import { LabelSet } from './LabelSet'; + +/** + * Implementation of {@link TypeObserverResult} + */ +export class ObserverResult implements TypeObserverResult { + observers = new Map(); + observe(callback: any, labelSet: LabelSet): void { + this.observers.set(labelSet, callback); + } +} diff --git a/packages/opentelemetry-metrics/src/export/Aggregator.ts b/packages/opentelemetry-metrics/src/export/Aggregator.ts index dbf1d08fac..f9edf8ef93 100644 --- a/packages/opentelemetry-metrics/src/export/Aggregator.ts +++ b/packages/opentelemetry-metrics/src/export/Aggregator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Distribution, Sum, Aggregator } from './types'; +import { Aggregator, Distribution, LastValue, Sum } from './types'; /** Basic aggregator which calculates a Sum from individual measurements. */ export class CounterSumAggregator implements Aggregator { @@ -29,6 +29,19 @@ export class CounterSumAggregator implements Aggregator { } } +/** Basic aggregator for Observer which keeps the last recorded value. */ +export class ObserverAggregator implements Aggregator { + private _current: number = 0; + + update(value: number): void { + this._current = value; + } + + value(): LastValue { + return this._current; + } +} + /** Basic aggregator keeping all raw values (events, sum, max and min). */ export class MeasureExactAggregator implements Aggregator { private _distribution: Distribution; diff --git a/packages/opentelemetry-metrics/src/export/Batcher.ts b/packages/opentelemetry-metrics/src/export/Batcher.ts index 911abfada3..85ecbfb7fe 100644 --- a/packages/opentelemetry-metrics/src/export/Batcher.ts +++ b/packages/opentelemetry-metrics/src/export/Batcher.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { CounterSumAggregator, MeasureExactAggregator } from './Aggregator'; +import { + CounterSumAggregator, + MeasureExactAggregator, + ObserverAggregator, +} from './Aggregator'; import { MetricRecord, MetricKind, Aggregator } from './types'; /** @@ -47,6 +51,8 @@ export class UngroupedBatcher extends Batcher { switch (metricKind) { case MetricKind.COUNTER: return new CounterSumAggregator(); + case MetricKind.OBSERVER: + return new ObserverAggregator(); default: return new MeasureExactAggregator(); } diff --git a/packages/opentelemetry-metrics/src/export/types.ts b/packages/opentelemetry-metrics/src/export/types.ts index 916e17b23a..b57ab562b7 100644 --- a/packages/opentelemetry-metrics/src/export/types.ts +++ b/packages/opentelemetry-metrics/src/export/types.ts @@ -21,13 +21,16 @@ import { LabelSet } from '../LabelSet'; /** The kind of metric. */ export enum MetricKind { COUNTER, - GAUGE, MEASURE, + OBSERVER, } /** Sum returns an aggregated sum. */ export type Sum = number; +/** LastValue returns last value. */ +export type LastValue = number; + export interface Distribution { min: number; max: number; diff --git a/packages/opentelemetry-metrics/src/index.ts b/packages/opentelemetry-metrics/src/index.ts index 2ecd8b3395..2ecbb7c751 100644 --- a/packages/opentelemetry-metrics/src/index.ts +++ b/packages/opentelemetry-metrics/src/index.ts @@ -19,6 +19,7 @@ export * from './BoundInstrument'; export * from './Meter'; export * from './Metric'; export * from './MeterProvider'; +export * from './export/Aggregator'; export * from './export/ConsoleMetricExporter'; export * from './export/types'; export * from './export/Aggregator'; diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index 7a85f16023..87ade69ce1 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -24,11 +24,16 @@ import { MeterProvider, MeasureMetric, Distribution, + ObserverMetric, + MetricRecord, } from '../src'; import * as types from '@opentelemetry/api'; import { LabelSet } from '../src/LabelSet'; import { NoopLogger } from '@opentelemetry/core'; -import { CounterSumAggregator } from '../src/export/Aggregator'; +import { + CounterSumAggregator, + ObserverAggregator, +} from '../src/export/Aggregator'; import { ValueType } from '@opentelemetry/api'; describe('Meter', () => { @@ -391,6 +396,69 @@ describe('Meter', () => { }); }); + describe('#observer', () => { + it('should create an observer', () => { + const measure = meter.createObserver('name') as ObserverMetric; + assert.ok(measure instanceof Metric); + }); + + it('should create observer with options', () => { + const measure = meter.createObserver('name', { + description: 'desc', + unit: '1', + disabled: false, + }) as ObserverMetric; + assert.ok(measure instanceof Metric); + }); + it('should set callback and observe value ', () => { + const measure = meter.createObserver('name', { + description: 'desc', + labelKeys: ['pid', 'core'], + }) as ObserverMetric; + + function getCpuUsage() { + return Math.random(); + } + + measure.setCallback((observerResult: types.ObserverResult) => { + observerResult.observe( + getCpuUsage, + meter.labels({ pid: '123', core: '1' }) + ); + observerResult.observe( + getCpuUsage, + meter.labels({ pid: '123', core: '2' }) + ); + observerResult.observe( + getCpuUsage, + meter.labels({ pid: '123', core: '3' }) + ); + observerResult.observe( + getCpuUsage, + meter.labels({ pid: '123', core: '4' }) + ); + }); + + const metricRecords: MetricRecord[] = measure.getMetricRecord(); + assert.strictEqual(metricRecords.length, 4); + + const metric1 = metricRecords[0]; + const metric2 = metricRecords[1]; + const metric3 = metricRecords[2]; + const metric4 = metricRecords[3]; + + assert.ok(metric1.labels.identifier.indexOf('|#core:1,pid:123') === 0); + assert.ok(metric2.labels.identifier.indexOf('|#core:2,pid:123') === 0); + assert.ok(metric3.labels.identifier.indexOf('|#core:3,pid:123') === 0); + assert.ok(metric4.labels.identifier.indexOf('|#core:4,pid:123') === 0); + + ensureMetric(metric1); + ensureMetric(metric2); + ensureMetric(metric3); + ensureMetric(metric4); + }); + }); + describe('#getMetrics', () => { it('should create a DOUBLE counter', () => { const key = 'key'; @@ -450,3 +518,16 @@ describe('Meter', () => { }); }); }); + +function ensureMetric(metric: MetricRecord) { + assert.ok(metric.aggregator instanceof ObserverAggregator); + assert.ok(metric.aggregator.value() >= 0 && metric.aggregator.value() <= 1); + assert.ok(metric.aggregator.value() >= 0 && metric.aggregator.value() <= 1); + const descriptor = metric.descriptor; + assert.strictEqual(descriptor.name, 'name'); + assert.strictEqual(descriptor.description, 'desc'); + assert.strictEqual(descriptor.unit, '1'); + assert.strictEqual(descriptor.metricKind, MetricKind.OBSERVER); + assert.strictEqual(descriptor.valueType, ValueType.DOUBLE); + assert.strictEqual(descriptor.monotonic, false); +}