diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json index b3ff4e8457..64ff561763 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json @@ -20,9 +20,6 @@ }, { "path": "../opentelemetry-exporter-trace-otlp-http" - }, - { - "path": "../opentelemetry-sdk-metrics-base" } ] } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.esm.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.esm.json index 484461f470..063825713f 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.esm.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.esm.json @@ -14,9 +14,6 @@ }, { "path": "../opentelemetry-exporter-trace-otlp-http/tsconfig.esm.json" - }, - { - "path": "../opentelemetry-sdk-metrics-base/tsconfig.esm.json" } ] } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json index 13dc2e7744..d9595b8a98 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json @@ -14,9 +14,6 @@ }, { "path": "../opentelemetry-exporter-trace-otlp-http" - }, - { - "path": "../opentelemetry-sdk-metrics-base" } ] } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json index 35c3f6d9c7..50c20b23e2 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json @@ -20,9 +20,6 @@ }, { "path": "../opentelemetry-exporter-trace-otlp-proto" - }, - { - "path": "../opentelemetry-sdk-metrics-base" } ] } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json b/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json index 3c062d3feb..948abef3ce 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json @@ -11,9 +11,6 @@ "references": [ { "path": "../opentelemetry-api-metrics" - }, - { - "path": "../opentelemetry-sdk-metrics-base" } ] } diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/package.json b/experimental/packages/opentelemetry-sdk-metrics-base/package.json index b03868fd5f..30ef6073e2 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/package.json +++ b/experimental/packages/opentelemetry-sdk-metrics-base/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-metrics-base-wip", - "version": "0.26.0", + "version": "0.27.0", "private": true, "description": "Work in progress OpenTelemetry metrics SDK", "main": "build/src/index.js", @@ -64,7 +64,7 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@opentelemetry/api-metrics": "0.26.0", + "@opentelemetry/api-metrics": "0.27.0", "@opentelemetry/core": "1.0.0", "@opentelemetry/resources": "1.0.0", "lodash.merge": "^4.6.2" diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts new file mode 100644 index 0000000000..fac07ef4c2 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/Aggregation.ts @@ -0,0 +1,25 @@ +/* + * 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 { Measurement } from './Measurement'; + +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#aggregation + +export interface Aggregator { + aggregate(measurement: Measurement): void; +} + +// TODO define actual aggregator classes diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts new file mode 100644 index 0000000000..8c5d1212d3 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/Instruments.ts @@ -0,0 +1,70 @@ +/* + * 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 api from '@opentelemetry/api'; +import * as metrics from '@opentelemetry/api-metrics'; +import { Meter } from './Meter'; + +// 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', +} + +export class SyncInstrument { + constructor(private _meter: Meter, private _name: string) { } + + getName(): string { + return this._name; + } + + + aggregate(value: number, attributes: metrics.Attributes = {}, ctx: api.Context = api.context.active()) { + this._meter.aggregate(this, { + value, + attributes, + context: ctx, + }); + } +} + +export class UpDownCounter extends SyncInstrument implements metrics.Counter { + 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); + } +} + +export class Histogram extends SyncInstrument implements metrics.Histogram { + 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/Measurement.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/Measurement.ts new file mode 100644 index 0000000000..215426f9d7 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/Measurement.ts @@ -0,0 +1,27 @@ +/* + * 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 api from '@opentelemetry/api' +import { Attributes } from '@opentelemetry/api-metrics' + +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#measurement + +export type Measurement = { + value: number; + // TODO use common attributes + attributes: Attributes + context?: api.Context; +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/Meter.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/Meter.ts new file mode 100644 index 0000000000..c7e16406ba --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/Meter.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 * 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'; + +// 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) { } + + /** this exists just to prevent ts errors from unused variables and may be removed */ + getSchemaUrl(): string | undefined { + return this._schemaUrl; + } + + /** 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); + } + + createUpDownCounter(_name: string, _options?: metrics.MetricOptions): metrics.UpDownCounter { + return new UpDownCounter(this, _name); + } + + 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); + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts new file mode 100644 index 0000000000..b52af2ec65 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/MeterProvider.ts @@ -0,0 +1,148 @@ +/* + * 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 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'; + +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#meterprovider + +export type MeterProviderOptions = { + 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(); + } + + /** + * **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; + } + + // 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; + } + + 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/MetricExporter.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/MetricExporter.ts new file mode 100644 index 0000000000..4046f04136 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/MetricExporter.ts @@ -0,0 +1,51 @@ +/* + * 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://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricexporter + +// TODO should this just be an interface and exporters can implement their own shutdown? +export abstract class MetricExporter { + protected _shutdown = false; + + // TODO: define the methods that actually export - must allow for push and pull exporters + + async shutdown(): Promise { + if (this._shutdown) { + return; + } + + // Setting _shutdown before flushing might prevent some exporters from flushing + // Waiting until flushing is complete might allow another flush to occur during shutdown + const flushPromise = this.forceFlush(); + this._shutdown = true; + await flushPromise; + } + + abstract forceFlush(): Promise; + + isShutdown() { + return this._shutdown; + } +} + +export class ConsoleMetricExporter extends MetricExporter { + async export() { + throw new Error('Method not implemented'); + } + + // nothing to do + async forceFlush() {} +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/MetricReader.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/MetricReader.ts new file mode 100644 index 0000000000..9f20299a4f --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/MetricReader.ts @@ -0,0 +1,44 @@ +/* + * 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 { MetricExporter } from '.'; + +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricreader + +export class MetricReader { + private _shutdown = false; + + constructor(private _exporter: MetricExporter) {} + + async shutdown(): Promise { + if (this._shutdown) { + return; + } + + this._shutdown = true; + // errors thrown to caller + await this._exporter.shutdown(); + } + + async forceFlush(): Promise { + if (this._shutdown) { + return; + } + + // errors thrown to caller + await this._exporter.forceFlush(); + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/View.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/View.ts new file mode 100644 index 0000000000..2abbf8760f --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/View.ts @@ -0,0 +1,113 @@ +/* + * 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/index.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts index f4fa9c6472..ef6f9b17d2 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/index.ts @@ -14,4 +14,6 @@ * limitations under the License. */ -export * from './version'; +export { MeterProvider, MeterProviderOptions } from './MeterProvider'; +export * from './MetricExporter'; +export * from './MetricReader'; diff --git a/experimental/packages/opentelemetry-sdk-node/tsconfig.json b/experimental/packages/opentelemetry-sdk-node/tsconfig.json index 36c71e90d8..b0e33797f4 100644 --- a/experimental/packages/opentelemetry-sdk-node/tsconfig.json +++ b/experimental/packages/opentelemetry-sdk-node/tsconfig.json @@ -14,9 +14,6 @@ }, { "path": "../opentelemetry-instrumentation" - }, - { - "path": "../opentelemetry-sdk-metrics-base" } ] }