From 2cdfc4a715d06b0980b5f8d62cc6106f81a8c1db Mon Sep 17 00:00:00 2001 From: legendecas Date: Thu, 28 Oct 2021 16:54:14 +0800 Subject: [PATCH] feat: bootstrap views api --- .../src/Instruments.ts | 10 +- .../src/MeterProvider.ts | 211 +++++++++--------- .../src/View.ts | 113 ---------- .../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 | 66 ++++++ .../src/view/ViewRegistry.ts | 63 ++++++ .../view/AttributesProcessor.test.ts} | 21 +- .../test/view/Predicate.test.ts | 66 ++++++ .../test/view/View.test.ts | 32 +++ .../test/view/ViewRegistry.test.ts | 152 +++++++++++++ 14 files changed, 754 insertions(+), 225 deletions(-) delete mode 100644 experimental/packages/opentelemetry-sdk-metrics-base/src/View.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 rename experimental/packages/opentelemetry-sdk-metrics-base/{src/Aggregation.ts => test/view/AttributesProcessor.test.ts} (57%) 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/Instruments.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts index 8c5d1212d37..9ef0f616066 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts @@ -29,6 +29,14 @@ export enum InstrumentType { OBSERVABLE_UP_DOWN_COUNTER = 'OBSERVABLE_UP_DOWN_COUNTER', } +export interface InstrumentDescriptor { + readonly name: string; + readonly description: string; + readonly unit: string; + readonly type: InstrumentType; + readonly valueType: metrics.ValueType; +} + export class SyncInstrument { constructor(private _meter: Meter, private _name: string) { } @@ -36,7 +44,7 @@ export class SyncInstrument { return this._name; } - + aggregate(value: number, attributes: metrics.Attributes = {}, ctx: api.Context = api.context.active()) { this._meter.aggregate(this, { value, diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts index b52af2ec65d..531539d650b 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts @@ -21,128 +21,131 @@ import { Measurement } from './Measurement'; import { Meter } from './Meter'; import { MetricExporter } from './MetricExporter'; import { MetricReader } from './MetricReader'; -import { View } from './View'; +import { InstrumentSelector } from './view/InstrumentSelector'; +import { MeterSelector } from './view/MeterSelector'; +import { View } from './view/View'; +import { ViewRegistry } from './view/ViewRegistry'; // 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 _resource: Resource; + private _shutdown = false; + private _metricReaders: MetricReader[] = []; + private _metricExporters: MetricExporter[] = []; + private readonly _viewRegistry = new ViewRegistry(); + + constructor(options: MeterProviderOptions) { + this._resource = options.resource ?? Resource.empty(); + } + + /** + * **Unstable** + * + * This method is only here to prevent typescript from complaining and may be removed. + */ + getResource() { + return this._resource; + } + + 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, { name, version }, 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._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); + } + + /** + * 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; } - /** - * 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}`) - } - } + 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 /** - * 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? + * 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 */ - 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}`) - } - } - } - } - - 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 2abbf8760f3..00000000000 --- 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/view/Aggregation.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts new file mode 100644 index 00000000000..360d907f16f --- /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 "../Instruments"; + +// 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 00000000000..5cdfa2dda44 --- /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 00000000000..40aa4778a93 --- /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 00000000000..23947b345d1 --- /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 00000000000..1eb0ce9d6dd --- /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 00000000000..0b2233f4ea1 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts @@ -0,0 +1,66 @@ +/* + * 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; + 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 00000000000..54cbd55788a --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/ViewRegistry.ts @@ -0,0 +1,63 @@ +/* + * 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 '../Instruments'; +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(it => { + return this._matchInstrument(it.instrumentSelector, instrument) && + this._matchMeter(it.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/src/Aggregation.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/AttributesProcessor.test.ts similarity index 57% rename from experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts rename to experimental/packages/opentelemetry-sdk-metrics-base/test/view/AttributesProcessor.test.ts index fac07ef4c20..fd7ea105fb8 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/AttributesProcessor.test.ts @@ -14,12 +14,19 @@ * limitations under the License. */ -import { Measurement } from './Measurement'; +import * as assert from 'assert'; +import { context } from '@opentelemetry/api'; +import { NoopAttributesProcessor } from '../../src/view/AttributesProcessor'; -// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#aggregation +describe('NoopAttributesProcessor', () => { + const processor = new NoopAttributesProcessor(); -export interface Aggregator { - aggregate(measurement: Measurement): void; -} - -// TODO define actual aggregator classes + 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 00000000000..276e6f305f5 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/Predicate.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { 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('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('')); + }); + }); + + 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('')); + }); + }); + + 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'); + }); + }); +}); 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 00000000000..11e3658744e --- /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 00000000000..3e0189a3f28 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/ViewRegistry.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { InstrumentDescriptor, 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'; + +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'); + } + }); + }); + }); +});