feat(metrics-sdk): bootstrap views api (#2625)

Co-authored-by: Valentin Marchaud <contact@vmarchaud.fr>
This commit is contained in:
legendecas 2021-11-22 04:28:49 +08:00 committed by GitHub
parent 13acbd3675
commit 9b5feb2f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 945 additions and 293 deletions

View File

@ -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,
};
}

View File

@ -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);
}
}

View File

@ -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<string, WritableMetricStorage>();
/** 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);
}
public aggregate(metric: unknown, measurement: Measurement) {
this._provider.aggregate(this, metric, measurement);
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.');
}
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);
}
}

View File

@ -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[] = [];
private _sharedState: MeterProviderSharedState;
private _shutdown = false;
private _metricReaders: MetricReader[] = [];
private _metricExporters: MetricExporter[] = [];
constructor(options: MeterProviderOptions) {
this._resource = options.resource ?? Resource.empty();
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<void> {
// 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);
/**
* 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<void> {
// 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;
}
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<void> {
// 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}`)
}
}
}
/**
* 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<void> {
// 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
*/
}
}
}

View File

@ -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<ViewMetricSelector>;
/**
* 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<Prop extends keyof ViewMetricSelector>(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<ViewMetricSelector>;
streamConfig?: ViewStreamConfig;
}
export type ViewStreamConfig = {
description: string;
attributeKeys?: string[];
// TODO use these types when they are defined
aggregation?: unknown;
exemplarReservoir?: unknown;
}

View File

@ -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

View File

@ -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);
});
}
}

View File

@ -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 {}
}

View File

@ -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();

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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',
}
);
});
});

View File

@ -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'));
});
});

View File

@ -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());
});
});
});

View File

@ -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');
}
});
});
});
});