From 9b5feb2f21f43758fd9b4e711319739db4d63ec0 Mon Sep 17 00:00:00 2001 From: legendecas Date: Mon, 22 Nov 2021 04:28:49 +0800 Subject: [PATCH] feat(metrics-sdk): bootstrap views api (#2625) Co-authored-by: Valentin Marchaud --- .../src/InstrumentDescriptor.ts | 36 ++++ .../src/Instruments.ts | 60 +++--- .../src/Meter.ts | 88 ++++---- .../src/MeterProvider.ts | 193 ++++++++---------- .../src/View.ts | 113 ---------- .../MeterProviderSharedState.ts} | 14 +- .../src/state/MultiWritableMetricStorage.ts | 29 +++ .../src/state/WritableMetricStorage.ts | 26 +++ .../src/view/Aggregation.ts | 42 ++++ .../src/view/AttributesProcessor.ts | 39 ++++ .../src/view/InstrumentSelector.ts | 41 ++++ .../src/view/MeterSelector.ts | 50 +++++ .../src/view/Predicate.ts | 73 +++++++ .../src/view/View.ts | 68 ++++++ .../src/view/ViewRegistry.ts | 65 ++++++ .../test/view/AttributesProcessor.test.ts | 32 +++ .../test/view/Predicate.test.ts | 86 ++++++++ .../test/view/View.test.ts | 32 +++ .../test/view/ViewRegistry.test.ts | 153 ++++++++++++++ 19 files changed, 946 insertions(+), 294 deletions(-) create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/InstrumentDescriptor.ts delete mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/View.ts rename experimental/packages/opentelemetry-sdk-metrics-base/src/{Aggregation.ts => state/MeterProviderSharedState.ts} (67%) create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/state/MultiWritableMetricStorage.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/state/WritableMetricStorage.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/view/AttributesProcessor.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/view/InstrumentSelector.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/view/MeterSelector.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/view/Predicate.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/view/ViewRegistry.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/test/view/AttributesProcessor.test.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/test/view/Predicate.test.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/test/view/View.test.ts create mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/test/view/ViewRegistry.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/InstrumentDescriptor.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/InstrumentDescriptor.ts new file mode 100644 index 0000000000..452356e3cc --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/InstrumentDescriptor.ts @@ -0,0 +1,36 @@ +/* + * 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 { MetricOptions, ValueType } from '@opentelemetry/api-metrics'; +import { InstrumentType } from './Instruments'; + +export interface InstrumentDescriptor { + readonly name: string; + readonly description: string; + readonly unit: string; + readonly type: InstrumentType; + readonly valueType: ValueType; +} + +export function createInstrumentDescriptor(name: string, type: InstrumentType, options?: MetricOptions): InstrumentDescriptor { + return { + name, + type, + description: options?.description ?? '', + unit: options?.unit ?? '1', + valueType: options?.valueType ?? ValueType.DOUBLE, + }; +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts index 8c5d1212d3..3953c9ecff 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts @@ -16,55 +16,51 @@ import * as api from '@opentelemetry/api'; import * as metrics from '@opentelemetry/api-metrics'; -import { Meter } from './Meter'; +import { InstrumentDescriptor } from './InstrumentDescriptor'; +import { WritableMetricStorage } from './state/WritableMetricStorage'; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument export enum InstrumentType { - COUNTER = 'COUNTER', - HISTOGRAM = 'HISTOGRAM', - UP_DOWN_COUNTER = 'UP_DOWN_COUNTER', - OBSERVABLE_COUNTER = 'OBSERVABLE_COUNTER', - OBSERVABLE_GAUGE = 'OBSERVABLE_GAUGE', - OBSERVABLE_UP_DOWN_COUNTER = 'OBSERVABLE_UP_DOWN_COUNTER', + COUNTER = 'COUNTER', + HISTOGRAM = 'HISTOGRAM', + UP_DOWN_COUNTER = 'UP_DOWN_COUNTER', + OBSERVABLE_COUNTER = 'OBSERVABLE_COUNTER', + OBSERVABLE_GAUGE = 'OBSERVABLE_GAUGE', + OBSERVABLE_UP_DOWN_COUNTER = 'OBSERVABLE_UP_DOWN_COUNTER', } export class SyncInstrument { - constructor(private _meter: Meter, private _name: string) { } + constructor(private _writableMetricStorage: WritableMetricStorage, private _descriptor: InstrumentDescriptor) { } - getName(): string { - return this._name; - } + getName(): string { + return this._descriptor.name; + } - - aggregate(value: number, attributes: metrics.Attributes = {}, ctx: api.Context = api.context.active()) { - this._meter.aggregate(this, { - value, - attributes, - context: ctx, - }); - } + aggregate(value: number, attributes: metrics.Attributes = {}, context: api.Context = api.context.active()) { + this._writableMetricStorage.record(value, attributes, context); + } } export class UpDownCounter extends SyncInstrument implements metrics.Counter { - add(value: number, attributes?: metrics.Attributes, ctx?: api.Context): void { - this.aggregate(value, attributes, ctx); - } + add(value: number, attributes?: metrics.Attributes, ctx?: api.Context): void { + this.aggregate(value, attributes, ctx); + } } export class Counter extends SyncInstrument implements metrics.Counter { - add(value: number, attributes?: metrics.Attributes, ctx?: api.Context): void { - if (value < 0) { - api.diag.warn(`negative value provided to counter ${this.getName()}: ${value}`); - return; - } - - this.aggregate(value, attributes, ctx); + add(value: number, attributes?: metrics.Attributes, ctx?: api.Context): void { + if (value < 0) { + api.diag.warn(`negative value provided to counter ${this.getName()}: ${value}`); + return; } + + this.aggregate(value, attributes, ctx); + } } export class Histogram extends SyncInstrument implements metrics.Histogram { - record(value: number, attributes?: metrics.Attributes, ctx?: api.Context): void { - this.aggregate(value, attributes, ctx); - } + record(value: number, attributes?: metrics.Attributes, ctx?: api.Context): void { + this.aggregate(value, attributes, ctx); + } } diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/Meter.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/Meter.ts index c7e16406ba..1fa967e6a4 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/Meter.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/Meter.ts @@ -16,50 +16,68 @@ import * as metrics from '@opentelemetry/api-metrics'; import { InstrumentationLibrary } from '@opentelemetry/core'; -import { Counter, Histogram, UpDownCounter } from './Instruments'; -import { Measurement } from './Measurement'; -import { MeterProvider } from './MeterProvider'; +import { createInstrumentDescriptor, InstrumentDescriptor } from './InstrumentDescriptor'; +import { Counter, Histogram, InstrumentType, UpDownCounter } from './Instruments'; +import { MeterProviderSharedState } from './state/MeterProviderSharedState'; +import { MultiMetricStorage } from './state/MultiWritableMetricStorage'; +import { NoopWritableMetricStorage, WritableMetricStorage } from './state/WritableMetricStorage'; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meter export class Meter implements metrics.Meter { - // instrumentation library required by spec to be on meter - // spec requires provider config changes to apply to previously created meters, achieved by holding a reference to the provider - constructor(private _provider: MeterProvider, private _instrumentationLibrary: InstrumentationLibrary, private _schemaUrl?: string) { } + private _metricStorageRegistry = new Map(); - /** this exists just to prevent ts errors from unused variables and may be removed */ - getSchemaUrl(): string | undefined { - return this._schemaUrl; - } + // instrumentation library required by spec to be on meter + // spec requires provider config changes to apply to previously created meters, achieved by holding a reference to the provider + constructor(private _meterProviderSharedState: MeterProviderSharedState, private _instrumentationLibrary: InstrumentationLibrary) { } - /** this exists just to prevent ts errors from unused variables and may be removed */ - getInstrumentationLibrary(): InstrumentationLibrary { - return this._instrumentationLibrary; - } + /** this exists just to prevent ts errors from unused variables and may be removed */ + getInstrumentationLibrary(): InstrumentationLibrary { + return this._instrumentationLibrary; + } - createHistogram(_name: string, _options?: metrics.MetricOptions): Histogram { - return new Histogram(this, _name); - } - - createCounter(_name: string, _options?: metrics.MetricOptions): metrics.Counter { - return new Counter(this, _name); - } + createHistogram(name: string, options?: metrics.MetricOptions): Histogram { + const descriptor = createInstrumentDescriptor(name, InstrumentType.HISTOGRAM, options); + const storage = this._registerMetricStorage(descriptor); + return new Histogram(storage, descriptor); + } - createUpDownCounter(_name: string, _options?: metrics.MetricOptions): metrics.UpDownCounter { - return new UpDownCounter(this, _name); - } + createCounter(name: string, options?: metrics.MetricOptions): metrics.Counter { + const descriptor = createInstrumentDescriptor(name, InstrumentType.COUNTER, options); + const storage = this._registerMetricStorage(descriptor); + return new Counter(storage, descriptor); + } - createObservableGauge(_name: string, _options?: metrics.MetricOptions, _callback?: (observableResult: metrics.ObservableResult) => void): metrics.ObservableBase { - throw new Error('Method not implemented.'); - } - createObservableCounter(_name: string, _options?: metrics.MetricOptions, _callback?: (observableResult: metrics.ObservableResult) => void): metrics.ObservableBase { - throw new Error('Method not implemented.'); - } - createObservableUpDownCounter(_name: string, _options?: metrics.MetricOptions, _callback?: (observableResult: metrics.ObservableResult) => void): metrics.ObservableBase { - throw new Error('Method not implemented.'); - } + createUpDownCounter(name: string, options?: metrics.MetricOptions): metrics.UpDownCounter { + const descriptor = createInstrumentDescriptor(name, InstrumentType.UP_DOWN_COUNTER, options); + const storage = this._registerMetricStorage(descriptor); + return new UpDownCounter(storage, descriptor); + } + + createObservableGauge(_name: string, _options?: metrics.MetricOptions, _callback?: (observableResult: metrics.ObservableResult) => void): metrics.ObservableBase { + throw new Error('Method not implemented.'); + } + + createObservableCounter(_name: string, _options?: metrics.MetricOptions, _callback?: (observableResult: metrics.ObservableResult) => void): metrics.ObservableBase { + throw new Error('Method not implemented.'); + } + + createObservableUpDownCounter(_name: string, _options?: metrics.MetricOptions, _callback?: (observableResult: metrics.ObservableResult) => void): metrics.ObservableBase { + throw new Error('Method not implemented.'); + } - public aggregate(metric: unknown, measurement: Measurement) { - this._provider.aggregate(this, metric, measurement); + private _registerMetricStorage(descriptor: InstrumentDescriptor) { + const views = this._meterProviderSharedState.viewRegistry.findViews(descriptor, this._instrumentationLibrary); + const storages = views.map(_view => { + // TODO: create actual metric storages. + const storage = new NoopWritableMetricStorage(); + // TODO: handle conflicts + this._metricStorageRegistry.set(descriptor.name, storage); + return storage; + }); + if (storages.length === 1) { + return storages[0]; } + return new MultiMetricStorage(storages); + } } diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts index b52af2ec65..501c9bf239 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts @@ -17,132 +17,109 @@ import * as api from '@opentelemetry/api'; import * as metrics from '@opentelemetry/api-metrics'; import { Resource } from '@opentelemetry/resources'; -import { Measurement } from './Measurement'; import { Meter } from './Meter'; import { MetricExporter } from './MetricExporter'; import { MetricReader } from './MetricReader'; -import { View } from './View'; +import { MeterProviderSharedState } from './state/MeterProviderSharedState'; +import { InstrumentSelector } from './view/InstrumentSelector'; +import { MeterSelector } from './view/MeterSelector'; +import { View } from './view/View'; + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#meterprovider export type MeterProviderOptions = { - resource?: Resource; + resource?: Resource; } export class MeterProvider { - private _resource: Resource; - private _shutdown = false; - private _metricReaders: MetricReader[] = []; - private _metricExporters: MetricExporter[] = []; - private _views: View[] = []; - - constructor(options: MeterProviderOptions) { - this._resource = options.resource ?? Resource.empty(); + private _sharedState: MeterProviderSharedState; + private _shutdown = false; + private _metricReaders: MetricReader[] = []; + private _metricExporters: MetricExporter[] = []; + + constructor(options: MeterProviderOptions) { + this._sharedState = new MeterProviderSharedState(options.resource ?? Resource.empty()); + } + + getMeter(name: string, version = '', options: metrics.MeterOptions = {}): metrics.Meter { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#meter-creation + if (this._shutdown) { + api.diag.warn('A shutdown MeterProvider cannot provide a Meter') + return metrics.NOOP_METER; } - /** - * **Unstable** - * - * This method is only here to prevent typescript from complaining and may be removed. - */ - getResource() { - return this._resource; + // Spec leaves it unspecified if creating a meter with duplicate + // name/version returns the same meter. We create a new one here + // for simplicity. This may change in the future. + // TODO: consider returning the same meter if the same name/version is used + return new Meter(this._sharedState, { name, version, schemaUrl: options.schemaUrl }); + } + + addMetricReader(metricReader: MetricReader) { + this._metricReaders.push(metricReader); + } + + addView(view: View, instrumentSelector: InstrumentSelector, meterSelector: MeterSelector) { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view + this._sharedState.viewRegistry.addView(view, instrumentSelector, meterSelector); + } + + /** + * Flush all buffered data and shut down the MeterProvider and all exporters and metric readers. + * Returns a promise which is resolved when all flushes are complete. + * + * TODO: return errors to caller somehow? + */ + async shutdown(): Promise { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#shutdown + + if (this._shutdown) { + api.diag.warn('shutdown may only be called once per MeterProvider'); + return; } - getMeter(name: string, version = '', options: metrics.MeterOptions = {}): metrics.Meter { - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#meter-creation - if (this._shutdown) { - api.diag.warn('A shutdown MeterProvider cannot provide a Meter') - return metrics.NOOP_METER; + // TODO add a timeout - spec leaves it up the the SDK if this is configurable + this._shutdown = true; + + // Shut down all exporters and readers. + // Log all Errors. + for (const exporter of this._metricExporters) { + try { + await exporter.shutdown(); + } catch (e) { + if (e instanceof Error) { + api.diag.error(`Error shutting down: ${e.message}`) } - - // Spec leaves it unspecified if creating a meter with duplicate - // name/version returns the same meter. We create a new one here - // for simplicity. This may change in the future. - // TODO: consider returning the same meter if the same name/version is used - return new Meter(this, { name, version }, options.schemaUrl); - } - - addMetricReader(metricReader: MetricReader) { - this._metricReaders.push(metricReader); + } } - - addView(view: View) { - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view - this._views.push(view); - } - - /** - * Flush all buffered data and shut down the MeterProvider and all exporters and metric readers. - * Returns a promise which is resolved when all flushes are complete. - * - * TODO: return errors to caller somehow? - */ - async shutdown(): Promise { - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#shutdown - - if (this._shutdown) { - api.diag.warn('shutdown may only be called once per MeterProvider'); - return; - } - - // TODO add a timeout - spec leaves it up the the SDK if this is configurable - this._shutdown = true; - - // Shut down all exporters and readers. - // Log all Errors. - for (const exporter of this._metricExporters) { - try { - await exporter.shutdown(); - } catch (e) { - if (e instanceof Error) { - api.diag.error(`Error shutting down: ${e.message}`) - } - } - } + } + + /** + * Notifies all exporters and metric readers to flush any buffered data. + * Returns a promise which is resolved when all flushes are complete. + * + * TODO: return errors to caller somehow? + */ + async forceFlush(): Promise { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#forceflush + + // TODO add a timeout - spec leaves it up the the SDK if this is configurable + + // do not flush after shutdown + if (this._shutdown) { + api.diag.warn('invalid attempt to force flush after shutdown') + return; } - /** - * Notifies all exporters and metric readers to flush any buffered data. - * Returns a promise which is resolved when all flushes are complete. - * - * TODO: return errors to caller somehow? - */ - async forceFlush(): Promise { - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#forceflush - - // TODO add a timeout - spec leaves it up the the SDK if this is configurable - - // do not flush after shutdown - if (this._shutdown) { - api.diag.warn('invalid attempt to force flush after shutdown') - return; + for (const exporter of [...this._metricExporters, ...this._metricReaders]) { + try { + await exporter.forceFlush(); + } catch (e) { + if (e instanceof Error) { + api.diag.error(`Error flushing: ${e.message}`) } - - for (const exporter of [...this._metricExporters, ...this._metricReaders]) { - try { - await exporter.forceFlush(); - } catch (e) { - if (e instanceof Error) { - api.diag.error(`Error flushing: ${e.message}`) - } - } - } - } - - public aggregate(_meter: Meter, _metric: unknown, _measurement: Measurement) { - // TODO actually aggregate - - /** - * if there are no views: - * apply the default configuration - * else: - * for each view: - * if view matches: - * apply view configuration - * if no view matched: - * if user has not disabled default fallback: - * apply default configuration - */ + } } + } } diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/View.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/View.ts deleted file mode 100644 index 2abbf8760f..0000000000 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/View.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 { InstrumentType } from './Instruments'; - -// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view - -/** - * A metric view selects a stream of metrics from a MeterProvider and applies - * a configuration to that stream. If no configuration is provided, the default - * configuration is used. - */ -export class View { - private _selector: Partial; - - /** - * Construct a metric view - * - * @param options a required object which describes the view selector and configuration - */ - constructor(options: ViewOptions) { - if (typeof options.selector == null) { - throw new Error('Missing required view selector') - } - - if ( - options.selector.instrumentType == null && - options.selector.instrumentName == null && - options.selector.meterName == null && - options.selector.meterVersion == null && - options.selector.meterSchemaUrl == null - ) { - // It is recommended by the SDK specification to fail fast when invalid options are provided - throw new Error('Cannot create a view which selects no options'); - } - - this._selector = options.selector; - } - - /** - * Given a metric selector, determine if all of this view's metric selectors match. - * - * @param selector selector to match - * @returns boolean - */ - public match(selector: ViewMetricSelector) { - return this._matchSelectorProperty('instrumentType', selector.instrumentType) && - this._matchInstrumentName(selector.instrumentName) && - this._matchSelectorProperty('meterName', selector.meterName) && - this._matchSelectorProperty('meterVersion', selector.meterVersion) && - this._matchSelectorProperty('meterSchemaUrl', selector.meterSchemaUrl); - } - - /** - * Match instrument name against the configured selector metric name, which may include wildcards - */ - private _matchInstrumentName(name: string) { - if (this._selector.instrumentName == null) { - return true; - } - - // TODO wildcard support - return this._selector.instrumentName === name; - } - - private _matchSelectorProperty(property: Prop, metricProperty: ViewMetricSelector[Prop]): boolean { - if (this._selector[property] == null) { - return true; - } - - if (this._selector[property] === metricProperty) { - return true; - } - - return false; - } -} - -export type ViewMetricSelector = { - instrumentType: InstrumentType; - instrumentName: string; - meterName: string; - meterVersion?: string; - meterSchemaUrl?: string; -} - -export type ViewOptions = { - name?: string; - selector: Partial; - streamConfig?: ViewStreamConfig; -} - -export type ViewStreamConfig = { - description: string; - attributeKeys?: string[]; - - // TODO use these types when they are defined - aggregation?: unknown; - exemplarReservoir?: unknown; -} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/state/MeterProviderSharedState.ts similarity index 67% rename from experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts rename to experimental/packages/opentelemetry-sdk-metrics-base/src/state/MeterProviderSharedState.ts index fac07ef4c2..57a2b022b0 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/state/MeterProviderSharedState.ts @@ -14,12 +14,14 @@ * limitations under the License. */ -import { Measurement } from './Measurement'; +import { Resource } from '@opentelemetry/resources'; +import { ViewRegistry } from '../view/ViewRegistry'; -// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#aggregation +/** + * An internal record for shared meter provider states. + */ +export class MeterProviderSharedState { + viewRegistry = new ViewRegistry(); -export interface Aggregator { - aggregate(measurement: Measurement): void; + constructor(public resource: Resource) {} } - -// TODO define actual aggregator classes diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/state/MultiWritableMetricStorage.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/state/MultiWritableMetricStorage.ts new file mode 100644 index 0000000000..43c064828f --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/state/MultiWritableMetricStorage.ts @@ -0,0 +1,29 @@ +/* + * 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 { Context } from '@opentelemetry/api'; +import { Attributes } from '@opentelemetry/api-metrics'; +import { WritableMetricStorage } from './WritableMetricStorage'; + +export class MultiMetricStorage implements WritableMetricStorage { + constructor(private readonly _backingStorages: WritableMetricStorage[]) {} + + record(value: number, attributes: Attributes, context: Context) { + this._backingStorages.forEach(it => { + it.record(value, attributes, context); + }); + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/state/WritableMetricStorage.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/state/WritableMetricStorage.ts new file mode 100644 index 0000000000..147cc21654 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/state/WritableMetricStorage.ts @@ -0,0 +1,26 @@ +/* + * 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 { Context } from '@opentelemetry/api'; +import { Attributes } from '@opentelemetry/api-metrics'; + +export interface WritableMetricStorage { + record(value: number, attributes: Attributes, context: Context): void; +} + +export class NoopWritableMetricStorage implements WritableMetricStorage { + record(_value: number, _attributes: Attributes, _context: Context): void {} +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts new file mode 100644 index 0000000000..41bb6ec73c --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts @@ -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 { InstrumentDescriptor } from '../InstrumentDescriptor'; + +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#aggregation + +/** + * Configures how measurements are combined into metrics for {@link View}s. + * + * Aggregation provides a set of built-in aggregations via static methods. + */ +export abstract class Aggregation { + // TODO: define the actual aggregator classes + abstract createAggregator(instrument: InstrumentDescriptor): unknown; + + static None(): Aggregation { + return NONE_AGGREGATION; + } +} + +export class NoneAggregation extends Aggregation { + createAggregator(_instrument: InstrumentDescriptor) { + // TODO: define aggregator type + return; + } +} + +const NONE_AGGREGATION = new NoneAggregation(); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/AttributesProcessor.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/AttributesProcessor.ts new file mode 100644 index 0000000000..5cdfa2dda4 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/AttributesProcessor.ts @@ -0,0 +1,39 @@ +/* + * 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 { Context } from '@opentelemetry/api'; +import { Attributes } from '@opentelemetry/api-metrics'; + +/** + * The {@link AttributesProcessor} is responsible for customizing which + * attribute(s) are to be reported as metrics dimension(s) and adding + * additional dimension(s) from the {@link Context}. + */ +export abstract class AttributesProcessor { + abstract process(incoming: Attributes, context: Context): Attributes; + + static Noop() { + return NOOP; + } +} + +export class NoopAttributesProcessor extends AttributesProcessor { + process(incoming: Attributes, _context: Context) { + return incoming; + } +} + +const NOOP = new NoopAttributesProcessor; diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/InstrumentSelector.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/InstrumentSelector.ts new file mode 100644 index 0000000000..40aa4778a9 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/InstrumentSelector.ts @@ -0,0 +1,41 @@ +/* + * 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 { InstrumentType } from '../Instruments'; +import { PatternPredicate, Predicate } from './Predicate'; + +export interface InstrumentSelectorCriteria { + name?: string; + type?: InstrumentType; +} + +export class InstrumentSelector { + private _nameFilter: Predicate; + private _type?: InstrumentType; + + constructor(criteria?: InstrumentSelectorCriteria) { + this._nameFilter = new PatternPredicate(criteria?.name ?? '*'); + this._type = criteria?.type; + } + + getType() { + return this._type + } + + getNameFilter() { + return this._nameFilter; + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/MeterSelector.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/MeterSelector.ts new file mode 100644 index 0000000000..23947b345d --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/MeterSelector.ts @@ -0,0 +1,50 @@ +/* + * 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 { ExactPredicate, Predicate } from './Predicate'; + +export interface MeterSelectorCriteria { + name?: string; + version?: string; + schemaUrl?: string; +} + +export class MeterSelector { + private _nameFilter: Predicate; + private _versionFilter: Predicate; + private _schemaUrlFilter: Predicate; + + constructor(criteria?: MeterSelectorCriteria) { + this._nameFilter = new ExactPredicate(criteria?.name); + this._versionFilter = new ExactPredicate(criteria?.version); + this._schemaUrlFilter = new ExactPredicate(criteria?.schemaUrl); + } + + getNameFilter() { + return this._nameFilter; + } + + /** + * TODO: semver filter? no spec yet. + */ + getVersionFilter() { + return this._versionFilter; + } + + getSchemaUrlFilter() { + return this._schemaUrlFilter; + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Predicate.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Predicate.ts new file mode 100644 index 0000000000..dd86990da6 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Predicate.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +// https://tc39.es/proposal-regex-escaping +// escape ^ $ \ . + ? ( ) [ ] { } | +// do not need to escape * as we are interpret it as wildcard +const ESCAPE = /[\^$\\.+?()[\]{}|]/g; + +export interface Predicate { + match(str: string): boolean; +} + +/** + * Wildcard pattern predicate, support patterns like `*`, `foo*`, `*bar`. + */ +export class PatternPredicate implements Predicate { + private _matchAll: boolean; + private _regexp?: RegExp; + + constructor(pattern: string) { + if (pattern === '*') { + this._matchAll = true; + } else { + this._matchAll = false; + this._regexp = new RegExp(PatternPredicate.escapePattern(pattern)); + } + } + + match(str: string): boolean { + if (this._matchAll) { + return true; + } + + return this._regexp!.test(str); + } + + static escapePattern(pattern: string): string { + return `^${pattern.replace(ESCAPE, '\\$&').replace('*', '.*')}$`; + } +} + +export class ExactPredicate implements Predicate { + private _matchAll: boolean; + private _pattern?: string; + + constructor(pattern?: string) { + this._matchAll = pattern === undefined; + this._pattern = pattern; + } + + match(str: string): boolean { + if (this._matchAll) { + return true; + } + if (str === this._pattern) { + return true; + } + return false; + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts new file mode 100644 index 0000000000..9dfbcb07ed --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts @@ -0,0 +1,68 @@ +/* + * 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 { Aggregation } from './Aggregation'; +import { AttributesProcessor } from './AttributesProcessor'; + +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view + +export interface ViewStreamConfig { + /** + * the name of the resulting metric to generate, or null if the same as the instrument. + */ + name?: string; + /** + * the name of the resulting metric to generate, or null if the same as the instrument. + */ + description?: string; + /** + * the aggregation used for this view. + */ + aggregation?: Aggregation; + /** + * processor of attributes before performing aggregation. + */ + attributesProcessor?: AttributesProcessor; +} + +/** + * A View provides the flexibility to customize the metrics that are output by + * the SDK. For example, the view can + * - customize which Instruments are to be processed/ignored. + * - customize the aggregation. + * - customize which attribute(s) are to be reported as metrics dimension(s). + * - add additional dimension(s) from the {@link Context}. + */ +export class View { + readonly name?: string; + readonly description?: string; + readonly aggregation: Aggregation; + readonly attributesProcessor: AttributesProcessor; + + /** + * Construct a metric view + * + * @param config how the result metric streams were configured + */ + constructor(config?: ViewStreamConfig) { + this.name = config?.name; + this.description = config?.description; + // TODO: the default aggregation should be Aggregation.Default(). + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#default-aggregation + this.aggregation = config?.aggregation ?? Aggregation.None(); + this.attributesProcessor = config?.attributesProcessor ?? AttributesProcessor.Noop(); + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/ViewRegistry.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/ViewRegistry.ts new file mode 100644 index 0000000000..e792e574b2 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/ViewRegistry.ts @@ -0,0 +1,65 @@ +/* + * 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 { InstrumentationLibrary } from '@opentelemetry/core'; +import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { InstrumentSelector } from './InstrumentSelector'; +import { MeterSelector } from './MeterSelector'; +import { View } from './View'; + +interface RegisteredView { + instrumentSelector: InstrumentSelector; + meterSelector: MeterSelector; + view: View; +} + +export class ViewRegistry { + private static DEFAULT_VIEW = new View(); + private _registeredViews: RegisteredView[] = []; + + addView(view: View, instrumentSelector: InstrumentSelector = new InstrumentSelector(), meterSelector: MeterSelector = new MeterSelector()) { + this._registeredViews.push({ + instrumentSelector, + meterSelector, + view, + }); + } + + findViews(instrument: InstrumentDescriptor, meter: InstrumentationLibrary): View[] { + const views = this._registeredViews + .filter(registeredView => { + return this._matchInstrument(registeredView.instrumentSelector, instrument) && + this._matchMeter(registeredView.meterSelector, meter); + }) + .map(it => it.view); + + if (views.length === 0) { + return [ViewRegistry.DEFAULT_VIEW]; + } + return views; + } + + private _matchInstrument(selector: InstrumentSelector, instrument: InstrumentDescriptor): boolean { + return (selector.getType() === undefined || instrument.type === selector.getType()) && + selector.getNameFilter().match(instrument.name); + } + + private _matchMeter(selector: MeterSelector, meter: InstrumentationLibrary): boolean { + return selector.getNameFilter().match(meter.name) && + (meter.version === undefined || selector.getVersionFilter().match(meter.version)) && + (meter.schemaUrl === undefined || selector.getSchemaUrlFilter().match(meter.schemaUrl)); + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/view/AttributesProcessor.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/AttributesProcessor.test.ts new file mode 100644 index 0000000000..fd7ea105fb --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/AttributesProcessor.test.ts @@ -0,0 +1,32 @@ +/* + * 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 assert from 'assert'; +import { context } from '@opentelemetry/api'; +import { NoopAttributesProcessor } from '../../src/view/AttributesProcessor'; + +describe('NoopAttributesProcessor', () => { + const processor = new NoopAttributesProcessor(); + + it('should return identical attributes on process', () => { + assert.deepStrictEqual( + processor.process({ foo: 'bar' }, context.active()), + { + foo: 'bar', + } + ); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/view/Predicate.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/Predicate.test.ts new file mode 100644 index 0000000000..608ff20a17 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/Predicate.test.ts @@ -0,0 +1,86 @@ +/* + * 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 assert from 'assert'; +import { ExactPredicate, PatternPredicate } from '../../src/view/Predicate'; + +describe('PatternPredicate', () => { + describe('asterisk match', () => { + it('should match anything', () => { + const predicate = new PatternPredicate('*'); + assert.ok(predicate.match('foo')); + assert.ok(predicate.match('')); + }); + + it('should match trailing part', () => { + const predicate = new PatternPredicate('foo*'); + assert.ok(predicate.match('foo')); + assert.ok(predicate.match('foobar')); + + assert.ok(!predicate.match('_foo')); + assert.ok(!predicate.match('bar')); + assert.ok(!predicate.match('')); + }); + + it('should match leading part', () => { + const predicate = new PatternPredicate('*bar'); + assert.ok(predicate.match('foobar')); + assert.ok(predicate.match('bar')); + + assert.ok(!predicate.match('foo')); + assert.ok(!predicate.match('bar_')); + assert.ok(!predicate.match('')); + }); + }); + + describe('exact match', () => { + it('should match exactly', () => { + const predicate = new PatternPredicate('foobar'); + assert.ok(predicate.match('foobar')); + + assert.ok(!predicate.match('foo')); + assert.ok(!predicate.match('_foobar_')); + assert.ok(!predicate.match('')); + }); + }); + + describe('escapePattern', () => { + it('should escape regexp elements', () => { + assert.strictEqual(PatternPredicate.escapePattern('^$\\.+?()[]{}|'), '^\\^\\$\\\\\\.\\+\\?\\(\\)\\[\\]\\{\\}\\|$'); + assert.strictEqual(PatternPredicate.escapePattern('*'), '^.*$'); + assert.strictEqual(PatternPredicate.escapePattern('foobar'), '^foobar$'); + assert.strictEqual(PatternPredicate.escapePattern('foo*'), '^foo.*$'); + assert.strictEqual(PatternPredicate.escapePattern('*bar'), '^.*bar$'); + }); + }); +}); + +describe('ExactPredicate', () => { + it('should match all', () => { + const predicate = new ExactPredicate(); + assert.ok(predicate.match('foo')); + assert.ok(predicate.match('')); + }); + + it('should exact match', () => { + const predicate = new ExactPredicate('foobar'); + assert.ok(!predicate.match('foo')); + assert.ok(!predicate.match('bar')); + assert.ok(!predicate.match('')); + + assert.ok(predicate.match('foobar')); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/view/View.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/View.test.ts new file mode 100644 index 0000000000..11e3658744 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/View.test.ts @@ -0,0 +1,32 @@ +/* + * 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 assert from 'assert'; +import { Aggregation } from '../../src/view/Aggregation'; +import { AttributesProcessor } from '../../src/view/AttributesProcessor'; +import { View } from '../../src/view/View'; + +describe('View', () => { + describe('constructor', () => { + it('should construct view without arguments', () => { + const view = new View(); + assert.strictEqual(view.name, undefined); + assert.strictEqual(view.description, undefined); + assert.strictEqual(view.aggregation, Aggregation.None()); + assert.strictEqual(view.attributesProcessor, AttributesProcessor.Noop()); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/view/ViewRegistry.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/ViewRegistry.test.ts new file mode 100644 index 0000000000..9aae44dea2 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/ViewRegistry.test.ts @@ -0,0 +1,153 @@ +/* + * 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 assert from 'assert'; +import { ValueType } from '@opentelemetry/api-metrics'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { InstrumentType } from '../../src/Instruments'; +import { ViewRegistry } from '../../src/view/ViewRegistry'; +import { View } from '../../src/view/View'; +import { InstrumentSelector } from '../../src/view/InstrumentSelector'; +import { MeterSelector } from '../../src/view/MeterSelector'; +import { InstrumentDescriptor } from '../../src/InstrumentDescriptor'; + +const defaultInstrumentDescriptor: InstrumentDescriptor = { + name: '', + description: '', + type: InstrumentType.COUNTER, + unit: '', + valueType: ValueType.DOUBLE, +}; + +const defaultInstrumentationLibrary: InstrumentationLibrary = { + name: 'default', + version: '1.0.0', + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0' +}; + +describe('ViewRegistry', () => { + describe('findViews', () => { + it('should return default view if no view registered', () => { + const registry = new ViewRegistry(); + const views = registry.findViews(defaultInstrumentDescriptor, defaultInstrumentationLibrary); + assert.strictEqual(views.length, 1); + assert.strictEqual(views[0], ViewRegistry['DEFAULT_VIEW']); + }); + + describe('InstrumentSelector', () => { + it('should match view with instrument name', () => { + const registry = new ViewRegistry(); + registry.addView(new View({ name: 'no-filter' })); + registry.addView(new View({ name: 'foo' }), new InstrumentSelector({ + name: 'foo', + })); + registry.addView(new View({ name: 'bar' }), new InstrumentSelector({ + name: 'bar' + })); + + { + const views = registry.findViews({ + ...defaultInstrumentDescriptor, + name: 'foo' + }, defaultInstrumentationLibrary); + + assert.strictEqual(views.length, 2); + assert.strictEqual(views[0].name, 'no-filter') + assert.strictEqual(views[1].name, 'foo'); + } + + { + const views = registry.findViews({ + ...defaultInstrumentDescriptor, + name: 'bar' + }, defaultInstrumentationLibrary); + + assert.strictEqual(views.length, 2); + assert.strictEqual(views[0].name, 'no-filter'); + assert.strictEqual(views[1].name, 'bar'); + } + }); + + it('should match view with instrument type', () => { + const registry = new ViewRegistry(); + registry.addView(new View({ name: 'no-filter' })); + registry.addView(new View({ name: 'counter' }), new InstrumentSelector({ + type: InstrumentType.COUNTER, + })); + registry.addView(new View({ name: 'histogram' }), new InstrumentSelector({ + type: InstrumentType.HISTOGRAM, + })); + + { + const views = registry.findViews({ + ...defaultInstrumentDescriptor, + type: InstrumentType.COUNTER + }, defaultInstrumentationLibrary); + + assert.strictEqual(views.length, 2); + assert.strictEqual(views[0].name, 'no-filter') + assert.strictEqual(views[1].name, 'counter'); + } + + { + const views = registry.findViews({ + ...defaultInstrumentDescriptor, + type: InstrumentType.HISTOGRAM + }, defaultInstrumentationLibrary); + + assert.strictEqual(views.length, 2); + assert.strictEqual(views[0].name, 'no-filter'); + assert.strictEqual(views[1].name, 'histogram'); + } + }); + }); + + describe('MeterSelector', () => { + it('should match view with meter name', () => { + const registry = new ViewRegistry(); + registry.addView(new View({ name: 'no-filter' })); + registry.addView(new View({ name: 'foo' }), undefined, new MeterSelector({ + name: 'foo' + })); + registry.addView(new View({ name: 'bar' }), undefined, new MeterSelector({ + name: 'bar' + })); + + { + const views = registry.findViews(defaultInstrumentDescriptor, { + ...defaultInstrumentationLibrary, + name: 'foo', + }); + + assert.strictEqual(views.length, 2); + assert.strictEqual(views[0].name, 'no-filter') + assert.strictEqual(views[1].name, 'foo'); + } + + { + const views = registry.findViews(defaultInstrumentDescriptor, { + ...defaultInstrumentationLibrary, + name: 'bar' + }); + + assert.strictEqual(views.length, 2); + assert.strictEqual(views[0].name, 'no-filter'); + assert.strictEqual(views[1].name, 'bar'); + } + }); + }); + }); +});