diff --git a/packages/opentelemetry-metrics/src/Handle.ts b/packages/opentelemetry-metrics/src/Handle.ts index 2a79ba8ff6..b6b8095800 100644 --- a/packages/opentelemetry-metrics/src/Handle.ts +++ b/packages/opentelemetry-metrics/src/Handle.ts @@ -51,7 +51,8 @@ export class CounterHandle extends BaseHandle implements types.CounterHandle { private readonly _monotonic: boolean, private readonly _valueType: types.ValueType, private readonly _labelValues: string[], - private readonly _logger: types.Logger + private readonly _logger: types.Logger, + private readonly _onUpdate: Function ) { super(_labelValues); } @@ -72,6 +73,7 @@ export class CounterHandle extends BaseHandle implements types.CounterHandle { value = Math.trunc(value); } this._data = this._data + value; + this._onUpdate(); } } @@ -85,7 +87,8 @@ export class GaugeHandle extends BaseHandle implements types.GaugeHandle { private readonly _monotonic: boolean, private readonly _valueType: types.ValueType, private readonly _labelValues: string[], - private readonly _logger: types.Logger + private readonly _logger: types.Logger, + private readonly _onUpdate: Function ) { super(_labelValues); } @@ -107,6 +110,7 @@ export class GaugeHandle extends BaseHandle implements types.GaugeHandle { value = Math.trunc(value); } this._data = value; + this._onUpdate(); } } diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index e66dd58ddd..bbcd2b97dd 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -29,14 +29,17 @@ import { DEFAULT_CONFIG, MeterConfig, } from './types'; -import { ReadableMetric } from './export/types'; +import { ReadableMetric, MetricExporter } from './export/types'; +import { notNull } from './Utils'; +import { ExportResult } from '@opentelemetry/base'; /** * Meter is an implementation of the {@link Meter} interface. */ export class Meter implements types.Meter { private readonly _logger: types.Logger; - private readonly _metrics = new Map(); + private readonly _metrics = new Map>(); + private readonly _exporters: MetricExporter[] = []; /** * Constructs a new Meter instance. @@ -88,7 +91,9 @@ export class Meter implements types.Meter { ...DEFAULT_METRIC_OPTIONS, ...options, }; - const counter = new CounterMetric(name, opt); + const counter = new CounterMetric(name, opt, () => { + this._exportOneMetric(name); + }); this._registerMetric(name, counter); return counter; } @@ -118,19 +123,49 @@ export class Meter implements types.Meter { ...DEFAULT_METRIC_OPTIONS, ...options, }; - const gauge = new GaugeMetric(name, opt); + const gauge = new GaugeMetric(name, opt, () => { + this._exportOneMetric(name); + }); this._registerMetric(name, gauge); return gauge; } /** - * Gets a collection of Metric`s to be exported. + * Gets a collection of Metrics to be exported. * @returns The list of metrics. */ getMetrics(): ReadableMetric[] { return Array.from(this._metrics.values()) .map(metric => metric.get()) - .filter(metric => !!metric); + .filter(notNull); + } + + /** + * Add an exporter to the list of registered exporters + * + * @param exporter {@Link MetricExporter} to add to the list of registered exporters + */ + addExporter(exporter: MetricExporter) { + this._exporters.push(exporter); + } + + /** + * Send a single metric by name to all registered exporters + */ + private _exportOneMetric(name: string) { + const metric = this._metrics.get(name); + if (!metric) return; + + const readableMetric = metric.get(); + if (!readableMetric) return; + + for (const exporter of this._exporters) { + exporter.export([readableMetric], result => { + if (result !== ExportResult.SUCCESS) { + this._logger.error(`Failed to export ${name}`); + } + }); + } } /** diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index 02bccdfadd..9ef8eab083 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -57,6 +57,7 @@ export abstract class Metric implements types.Metric { if (this._handles.has(hash)) return this._handles.get(hash)!; const handle = this._makeHandle(labelValues); + this._handles.set(hash, handle); return handle; } @@ -123,7 +124,11 @@ export abstract class Metric implements types.Metric { /** This is a SDK implementation of Counter Metric. */ export class CounterMetric extends Metric { - constructor(name: string, options: MetricOptions) { + constructor( + name: string, + options: MetricOptions, + private readonly _onUpdate: Function + ) { super( name, options, @@ -138,14 +143,19 @@ export class CounterMetric extends Metric { this._monotonic, this._valueType, labelValues, - this._logger + this._logger, + this._onUpdate ); } } /** This is a SDK implementation of Gauge Metric. */ export class GaugeMetric extends Metric { - constructor(name: string, options: MetricOptions) { + constructor( + name: string, + options: MetricOptions, + private readonly _onUpdate: Function + ) { super( name, options, @@ -160,7 +170,8 @@ export class GaugeMetric extends Metric { this._monotonic, this._valueType, labelValues, - this._logger + this._logger, + this._onUpdate ); } } diff --git a/packages/opentelemetry-metrics/src/Utils.ts b/packages/opentelemetry-metrics/src/Utils.ts index 4fa4242cb7..70a338fcaa 100644 --- a/packages/opentelemetry-metrics/src/Utils.ts +++ b/packages/opentelemetry-metrics/src/Utils.ts @@ -25,3 +25,12 @@ const COMMA_SEPARATOR = ','; export function hashLabelValues(labelValues: string[]): string { return labelValues.sort().join(COMMA_SEPARATOR); } + +/** + * Type guard to remove nulls from arrays + * + * @param value value to be checked for null equality + */ +export function notNull(value: T | null): value is T { + return value !== null; +} diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index c3e0f96f92..1d70f165a0 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -29,6 +29,7 @@ import { hrTime, hrTimeToMilliseconds, } from '@opentelemetry/core'; +import { NoopExporter } from './mocks/Exporter'; const performanceTimeOrigin = hrTime(); @@ -481,4 +482,56 @@ describe('Meter', () => { ); }); }); + + describe('Exporters', () => { + it('should register an exporter', () => { + const exporter = new NoopExporter(); + meter.addExporter(exporter); + assert.equal(meter['_exporters'].length, 1); + }); + + it('should export a gauge when it is updated', done => { + const exporter = new NoopExporter(); + exporter.on('export', metrics => { + assert.equal(metrics[0].descriptor.name, 'name'); + assert.equal(metrics[0].timeseries[0].points[0].value, 20); + assert.deepEqual(metrics[0].timeseries[0].labelValues, [ + { + value: 'value1', + }, + { + value: 'value2', + }, + ]); + done(); + }); + + meter.addExporter(exporter); + const gauge = meter.createGauge('name') as GaugeMetric; + const handle = gauge.getHandle(['value1', 'value2']); + handle.set(20); + }); + + it('should export a counter when it is updated', done => { + const counter = meter.createCounter('name') as CounterMetric; + const exporter = new NoopExporter(); + exporter.on('export', metrics => { + assert.equal(metrics[0].descriptor.name, 'name'); + assert.equal(metrics[0].timeseries[0].points[0].value, 20); + assert.deepEqual(metrics[0].timeseries[0].labelValues, [ + { + value: 'value1', + }, + { + value: 'value2', + }, + ]); + done(); + }); + + meter.addExporter(exporter); + const handle = counter.getHandle(['value1', 'value2']); + handle.add(20); + }); + }); }); diff --git a/packages/opentelemetry-metrics/test/mocks/Exporter.ts b/packages/opentelemetry-metrics/test/mocks/Exporter.ts new file mode 100644 index 0000000000..9ff760cdaf --- /dev/null +++ b/packages/opentelemetry-metrics/test/mocks/Exporter.ts @@ -0,0 +1,32 @@ +/*! + * 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 { MetricExporter, ReadableMetric } from '../../src/export/types'; +import { ExportResult } from '@opentelemetry/base'; +import { EventEmitter } from 'events'; + +export class NoopExporter extends EventEmitter implements MetricExporter { + export( + metrics: ReadableMetric[], + resultCallback: (result: ExportResult) => void + ): void { + this.emit('export', metrics, resultCallback); + } + + shutdown(): void { + this.emit('shutdown'); + } +}