diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Drop.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Drop.ts new file mode 100644 index 00000000000..2ff35d8b866 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Drop.ts @@ -0,0 +1,57 @@ +/* + * 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 { HrTime } from '@opentelemetry/api'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { AggregationTemporality } from '../export/AggregationTemporality'; +import { MetricData } from '../export/MetricData'; +import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { Maybe } from '../utils'; +import { + AggregatorKind, + Aggregator, + AccumulationRecord, +} from './types'; + +/** Basic aggregator for None which keeps no recorded value. */ +export class DropAggregator implements Aggregator { + kind: AggregatorKind.DROP = AggregatorKind.DROP; + + createAccumulation() { + return undefined; + } + + merge(_previous: undefined, _delta: undefined) { + return undefined; + } + + diff(_previous: undefined, _current: undefined) { + return undefined; + } + + toMetricData( + _resource: Resource, + _instrumentationLibrary: InstrumentationLibrary, + _instrumentDescriptor: InstrumentDescriptor, + _accumulationByAttributes: AccumulationRecord[], + _temporality: AggregationTemporality, + _sdkStartTime: HrTime, + _lastCollectionTime: HrTime, + _collectionTime: HrTime): Maybe { + return undefined; + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Histogram.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Histogram.ts new file mode 100644 index 00000000000..820f0c48987 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Histogram.ts @@ -0,0 +1,165 @@ +/* + * 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 { + Accumulation, + AccumulationRecord, + Aggregator, + AggregatorKind, + Histogram, +} from './types'; +import { HistogramMetricData, PointDataType } from '../export/MetricData'; +import { Resource } from '@opentelemetry/resources'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { HrTime } from '@opentelemetry/api'; +import { AggregationTemporality } from '../export/AggregationTemporality'; +import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { Maybe } from '../utils'; + +function createNewEmptyCheckpoint(boundaries: number[]): Histogram { + return { + buckets: { + boundaries, + counts: boundaries.map(() => 0).concat([0]), + }, + sum: 0, + count: 0, + }; +} + +export class HistogramAccumulation implements Accumulation { + constructor( + private readonly _boundaries: number[], + private _current: Histogram = createNewEmptyCheckpoint(_boundaries) + ) {} + + record(value: number): void { + this._current.count += 1; + this._current.sum += value; + + for (let i = 0; i < this._boundaries.length; i++) { + if (value < this._boundaries[i]) { + this._current.buckets.counts[i] += 1; + return; + } + } + // value is above all observed boundaries + this._current.buckets.counts[this._boundaries.length] += 1; + } + + toPoint(): Histogram { + return this._current; + } +} + +/** + * Basic aggregator which observes events and counts them in pre-defined buckets + * and provides the total sum and count of all observations. + */ +export class HistogramAggregator implements Aggregator { + public kind: AggregatorKind.HISTOGRAM = AggregatorKind.HISTOGRAM; + private readonly _boundaries: number[]; + + constructor(boundaries: number[]) { + if (boundaries === undefined || boundaries.length === 0) { + throw new Error('HistogramAggregator should be created with boundaries.'); + } + // we need to an ordered set to be able to correctly compute count for each + // boundary since we'll iterate on each in order. + this._boundaries = boundaries.sort((a, b) => a - b); + } + + createAccumulation() { + return new HistogramAccumulation(this._boundaries); + } + + /** + * Return the result of the merge of two histogram accumulations. As long as one Aggregator + * instance produces all Accumulations with constant boundaries we don't need to worry about + * merging accumulations with different boundaries. + */ + merge(previous: HistogramAccumulation, delta: HistogramAccumulation): HistogramAccumulation { + const previousPoint = previous.toPoint(); + const deltaPoint = delta.toPoint(); + + const previousCounts = previousPoint.buckets.counts; + const deltaCounts = deltaPoint.buckets.counts; + + const mergedCounts = new Array(previousCounts.length); + for (let idx = 0; idx < previousCounts.length; idx++) { + mergedCounts[idx] = previousCounts[idx] + deltaCounts[idx]; + } + + return new HistogramAccumulation(previousPoint.buckets.boundaries, { + buckets: { + boundaries: previousPoint.buckets.boundaries, + counts: mergedCounts, + }, + count: previousPoint.count + deltaPoint.count, + sum: previousPoint.sum + deltaPoint.sum, + }); + } + + /** + * Returns a new DELTA aggregation by comparing two cumulative measurements. + */ + diff(previous: HistogramAccumulation, current: HistogramAccumulation): HistogramAccumulation { + const previousPoint = previous.toPoint(); + const currentPoint = current.toPoint(); + + const previousCounts = previousPoint.buckets.counts; + const currentCounts = currentPoint.buckets.counts; + + const diffedCounts = new Array(previousCounts.length); + for (let idx = 0; idx < previousCounts.length; idx++) { + diffedCounts[idx] = currentCounts[idx] - previousCounts[idx]; + } + + return new HistogramAccumulation(previousPoint.buckets.boundaries, { + buckets: { + boundaries: previousPoint.buckets.boundaries, + counts: diffedCounts, + }, + count: currentPoint.count - previousPoint.count, + sum: currentPoint.sum - previousPoint.sum, + }); + } + + toMetricData( + resource: Resource, + instrumentationLibrary: InstrumentationLibrary, + metricDescriptor: InstrumentDescriptor, + accumulationByAttributes: AccumulationRecord[], + temporality: AggregationTemporality, + sdkStartTime: HrTime, + lastCollectionTime: HrTime, + collectionTime: HrTime): Maybe { + return { + resource, + instrumentationLibrary, + instrumentDescriptor: metricDescriptor, + pointDataType: PointDataType.HISTOGRAM, + pointData: accumulationByAttributes.map(([attributes, accumulation]) => { + return { + attributes, + startTime: temporality === AggregationTemporality.CUMULATIVE ? sdkStartTime : lastCollectionTime, + endTime: collectionTime, + point: accumulation.toPoint(), + } + }) + } + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/LastValue.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/LastValue.ts new file mode 100644 index 00000000000..fb86c26d0b5 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/LastValue.ts @@ -0,0 +1,94 @@ +/* + * 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 { LastValue, AggregatorKind, Aggregator, Accumulation, AccumulationRecord } from './types'; +import { HrTime } from '@opentelemetry/api'; +import { hrTime, hrTimeToMicroseconds, InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { AggregationTemporality } from '../export/AggregationTemporality'; +import { PointDataType, SingularMetricData } from '../export/MetricData'; +import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { Maybe } from '../utils'; + +export class LastValueAccumulation implements Accumulation { + constructor(private _current: number = 0, public sampleTime: HrTime = [0, 0]) {} + + record(value: number): void { + this._current = value; + this.sampleTime = hrTime(); + } + + toPoint(): LastValue { + return this._current; + } +} + +/** Basic aggregator which calculates a LastValue from individual measurements. */ +export class LastValueAggregator implements Aggregator { + public kind: AggregatorKind.LAST_VALUE = AggregatorKind.LAST_VALUE; + + createAccumulation() { + return new LastValueAccumulation(); + } + + /** + * Returns the result of the merge of the given accumulations. + * + * Return the newly captured (delta) accumulation for LastValueAggregator. + */ + merge(previous: LastValueAccumulation, delta: LastValueAccumulation): LastValueAccumulation { + // nanoseconds may lose precisions. + const latestAccumulation = hrTimeToMicroseconds(delta.sampleTime) >= hrTimeToMicroseconds(previous.sampleTime) ? delta : previous; + return new LastValueAccumulation(latestAccumulation.toPoint(), latestAccumulation.sampleTime); + } + + /** + * Returns a new DELTA aggregation by comparing two cumulative measurements. + * + * A delta aggregation is not meaningful to LastValueAggregator, just return + * the newly captured (delta) accumulation for LastValueAggregator. + */ + diff(previous: LastValueAccumulation, current: LastValueAccumulation): LastValueAccumulation { + // nanoseconds may lose precisions. + const latestAccumulation = hrTimeToMicroseconds(current.sampleTime) >= hrTimeToMicroseconds(previous.sampleTime) ? current : previous; + return new LastValueAccumulation(latestAccumulation.toPoint(), latestAccumulation.sampleTime); + } + + toMetricData( + resource: Resource, + instrumentationLibrary: InstrumentationLibrary, + instrumentDescriptor: InstrumentDescriptor, + accumulationByAttributes: AccumulationRecord[], + temporality: AggregationTemporality, + sdkStartTime: HrTime, + lastCollectionTime: HrTime, + collectionTime: HrTime): Maybe { + return { + resource, + instrumentationLibrary, + instrumentDescriptor, + pointDataType: PointDataType.SINGULAR, + pointData: accumulationByAttributes.map(([attributes, accumulation]) => { + return { + attributes, + startTime: temporality === AggregationTemporality.CUMULATIVE ? sdkStartTime : lastCollectionTime, + endTime: collectionTime, + point: accumulation.toPoint(), + } + }) + } + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Sum.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Sum.ts new file mode 100644 index 00000000000..081ccf802e1 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/Sum.ts @@ -0,0 +1,84 @@ +/* + * 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 { Sum, AggregatorKind, Aggregator, Accumulation, AccumulationRecord } from './types'; +import { HrTime } from '@opentelemetry/api'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { PointDataType, SingularMetricData } from '../export/MetricData'; +import { AggregationTemporality } from '../export/AggregationTemporality'; +import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { Maybe } from '../utils'; + +export class SumAccumulation implements Accumulation { + constructor(private _current: number = 0) {} + + record(value: number): void { + this._current += value; + } + + toPoint(): Sum { + return this._current; + } +} + +/** Basic aggregator which calculates a Sum from individual measurements. */ +export class SumAggregator implements Aggregator { + public kind: AggregatorKind.SUM = AggregatorKind.SUM; + + createAccumulation() { + return new SumAccumulation(); + } + + /** + * Returns the result of the merge of the given accumulations. + */ + merge(previous: SumAccumulation, delta: SumAccumulation): SumAccumulation { + return new SumAccumulation(previous.toPoint() + delta.toPoint()); + } + + /** + * Returns a new DELTA aggregation by comparing two cumulative measurements. + */ + diff(previous: SumAccumulation, current: SumAccumulation): SumAccumulation { + return new SumAccumulation(current.toPoint() - previous.toPoint()); + } + + toMetricData( + resource: Resource, + instrumentationLibrary: InstrumentationLibrary, + instrumentDescriptor: InstrumentDescriptor, + accumulationByAttributes: AccumulationRecord[], + temporality: AggregationTemporality, + sdkStartTime: HrTime, + lastCollectionTime: HrTime, + collectionTime: HrTime): Maybe { + return { + resource, + instrumentationLibrary, + instrumentDescriptor, + pointDataType: PointDataType.SINGULAR, + pointData: accumulationByAttributes.map(([attributes, accumulation]) => { + return { + attributes, + startTime: temporality === AggregationTemporality.CUMULATIVE ? sdkStartTime : lastCollectionTime, + endTime: collectionTime, + point: accumulation.toPoint(), + } + }) + } + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/index.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/index.ts new file mode 100644 index 00000000000..8906c4563a1 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export * from './Drop'; +export * from './Histogram'; +export * from './LastValue'; +export * from './Sum'; +export { Aggregator } from './types'; diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/types.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/types.ts new file mode 100644 index 00000000000..8a8e6897923 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/aggregator/types.ts @@ -0,0 +1,130 @@ +/* + * 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 { HrTime } from '@opentelemetry/api'; +import { Attributes } from '@opentelemetry/api-metrics'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { AggregationTemporality } from '../export/AggregationTemporality'; +import { MetricData } from '../export/MetricData'; +import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { Maybe } from '../utils'; + +/** The kind of aggregator. */ +export enum AggregatorKind { + DROP, + SUM, + LAST_VALUE, + HISTOGRAM, +} + +/** Point type for SumAggregation. */ +export type Sum = number; + +/** Point type for LastValueAggregation. */ +export type LastValue = number; + +/** Point type for HistogramAggregation. */ +export interface Histogram { + /** + * Buckets are implemented using two different arrays: + * - boundaries: contains every finite bucket boundary, which are inclusive lower bounds + * - counts: contains event counts for each bucket + * + * Note that we'll always have n+1 buckets, where n is the number of boundaries. + * This is because we need to count events that are below the lowest boundary. + * + * Example: if we measure the values: [5, 30, 5, 40, 5, 15, 15, 15, 25] + * with the boundaries [ 10, 20, 30 ], we will have the following state: + * + * buckets: { + * boundaries: [10, 20, 30], + * counts: [3, 3, 1, 2], + * } + */ + buckets: { + boundaries: number[]; + counts: number[]; + }; + sum: number; + count: number; +} + +/** + * An Aggregator accumulation state. + */ +export interface Accumulation { + record(value: number): void; +} + +export type AccumulationRecord = [Attributes, T]; + +/** + * Base interface for aggregators. Aggregators are responsible for holding + * aggregated values and taking a snapshot of these values upon export. + */ +export interface Aggregator { + /** The kind of the aggregator. */ + kind: AggregatorKind; + + /** + * Create a clean state of accumulation. + */ + createAccumulation(): T; + + /** + * Returns the result of the merge of the given accumulations. + * + * This should always assume that the accumulations do not overlap and merge together for a new + * cumulative report. + * + * @param previous the previously captured accumulation + * @param delta the newly captured (delta) accumulation + * @returns the result of the merge of the given accumulations + */ + merge(previous: T, delta: T): T; + + /** + * Returns a new DELTA aggregation by comparing two cumulative measurements. + * + * @param previous the previously captured accumulation + * @param current the newly captured (cumulative) accumulation + * @returns The resulting delta accumulation + */ + diff(previous: T, current: T): T; + + /** + * Returns the {@link MetricData} that this {@link Aggregator} will produce. + * + * @param resource the resource producing the metric. + * @param instrumentationLibrary the library that instrumented the metric + * @param instrumentDescriptor the metric instrument descriptor. + * @param accumulationByAttributes the array of attributes and accumulation pairs. + * @param temporality the temporality of the accumulation. + * @param sdkStartTime the start time of the sdk. + * @param lastCollectionTime the last collection time of the instrument. + * @param collectionTime the active collection time of the instrument. + * @return the {@link MetricData} that this {@link Aggregator} will produce. + */ + toMetricData(resource: Resource, + instrumentationLibrary: InstrumentationLibrary, + instrumentDescriptor: InstrumentDescriptor, + accumulationByAttributes: AccumulationRecord[], + temporality: AggregationTemporality, + sdkStartTime: HrTime, + lastCollectionTime: HrTime, + collectionTime: HrTime): Maybe; +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/export/AggregationTemporality.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/AggregationTemporality.ts new file mode 100644 index 00000000000..744d936d012 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/AggregationTemporality.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. + */ + +/** + * AggregationTemporality indicates the way additive quantities are expressed. + * + * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#temporality + */ +export enum AggregationTemporality { + DELTA, + CUMULATIVE, +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricData.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricData.ts new file mode 100644 index 00000000000..d989baa0b7d --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/export/MetricData.ts @@ -0,0 +1,69 @@ +/* + * 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 { HrTime } from '@opentelemetry/api'; +import { Attributes } from '@opentelemetry/api-metrics'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { Histogram } from '../aggregator/types'; + +export interface BaseMetricData { + readonly resource: Resource; + readonly instrumentationLibrary: InstrumentationLibrary; + readonly instrumentDescriptor: InstrumentDescriptor; + readonly pointDataType: PointDataType, +} + +export interface SingularMetricData extends BaseMetricData { + readonly pointDataType: PointDataType.SINGULAR, + readonly pointData: PointData[], +} + +export interface HistogramMetricData extends BaseMetricData { + readonly pointDataType: PointDataType.HISTOGRAM, + readonly pointData: PointData[], +} + +export type MetricData = SingularMetricData | HistogramMetricData; + +export enum PointDataType { + SINGULAR, + HISTOGRAM, + EXPONENTIAL_HISTOGRAM, +} + +export interface PointData { + /** + * The start epoch timestamp of the PointData, usually the time when + * the metric was created when the preferred AggregationTemporality is + * CUMULATIVE, or last collection time otherwise. + */ + readonly startTime: HrTime; + /** + * The end epoch timestamp when data were collected, usually it represents + * the moment when `MetricReader.collect` was called. + */ + readonly endTime: HrTime; + /** + * The attributes associated with this PointData. + */ + readonly attributes: Attributes; + /** + * The data points for this metric. + */ + readonly point: T; +} diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/noop.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/utils.ts similarity index 90% rename from experimental/packages/opentelemetry-sdk-metrics-base/test/noop.test.ts rename to experimental/packages/opentelemetry-sdk-metrics-base/src/utils.ts index 76c007ebc06..9adff043d4f 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/test/noop.test.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/utils.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -describe('nothing', () => { - it.skip('tests nothing'); -}); +export type Maybe = T | undefined; diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts index 41bb6ec73c9..b2b7b6a32ce 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/Aggregation.ts @@ -14,7 +14,12 @@ * limitations under the License. */ +import * as api from '@opentelemetry/api'; +import { Aggregator, SumAggregator, DropAggregator, LastValueAggregator, HistogramAggregator } from '../aggregator'; +import { Accumulation } from '../aggregator/types'; import { InstrumentDescriptor } from '../InstrumentDescriptor'; +import { InstrumentType } from '../Instruments'; +import { Maybe } from '../utils'; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#aggregation @@ -24,19 +29,97 @@ import { InstrumentDescriptor } from '../InstrumentDescriptor'; * 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; + abstract createAggregator(instrument: InstrumentDescriptor): Aggregator>; - static None(): Aggregation { - return NONE_AGGREGATION; + static Drop(): Aggregation { + return DROP_AGGREGATION; + } + + static Sum(): Aggregation { + return SUM_AGGREGATION; + } + + static LastValue(): Aggregation { + return LAST_VALUE_AGGREGATION; + } + + static Histogram(): Aggregation { + return HISTOGRAM_AGGREGATION; + } + + static Default(): Aggregation { + return DEFAULT_AGGREGATION; + } +} + +export class DropAggregation extends Aggregation { + private static DEFAULT_INSTANCE = new DropAggregator(); + createAggregator(_instrument: InstrumentDescriptor) { + return DropAggregation.DEFAULT_INSTANCE; + } +} + +export class SumAggregation extends Aggregation { + private static DEFAULT_INSTANCE = new SumAggregator(); + createAggregator(_instrument: InstrumentDescriptor) { + return SumAggregation.DEFAULT_INSTANCE; } } -export class NoneAggregation extends Aggregation { +export class LastValueAggregation extends Aggregation { + private static DEFAULT_INSTANCE = new LastValueAggregator(); createAggregator(_instrument: InstrumentDescriptor) { - // TODO: define aggregator type - return; + return LastValueAggregation.DEFAULT_INSTANCE; + } +} + +export class HistogramAggregation extends Aggregation { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#histogram-aggregation + private static DEFAULT_INSTANCE = new HistogramAggregator([0, 5, 10, 25, 50, 75, 100, 250, 500, 1000]); + createAggregator(_instrument: InstrumentDescriptor) { + return HistogramAggregation.DEFAULT_INSTANCE; + } +} + +export class ExplicitBucketHistogramAggregation extends Aggregation { + constructor(private _boundaries: number[]) { + super(); + } + + createAggregator(_instrument: InstrumentDescriptor) { + return new HistogramAggregator(this._boundaries); + } +} + +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#default-aggregation +export class DefaultAggregation extends Aggregation { + private _resolve(instrument: InstrumentDescriptor): Aggregation { + // cast to unknown to disable complaints on the (unreachable) fallback. + switch (instrument.type as unknown) { + case InstrumentType.COUNTER: + case InstrumentType.UP_DOWN_COUNTER: + case InstrumentType.OBSERVABLE_COUNTER: + case InstrumentType.OBSERVABLE_UP_DOWN_COUNTER: { + return SUM_AGGREGATION; + } + case InstrumentType.OBSERVABLE_GAUGE: { + return LAST_VALUE_AGGREGATION; + } + case InstrumentType.HISTOGRAM: { + return HISTOGRAM_AGGREGATION; + } + } + api.diag.warn(`Unable to recognize instrument type: ${instrument.type}`); + return DROP_AGGREGATION; + } + + createAggregator(instrument: InstrumentDescriptor): Aggregator> { + return this._resolve(instrument).createAggregator(instrument); } } -const NONE_AGGREGATION = new NoneAggregation(); +const DROP_AGGREGATION = new DropAggregation(); +const SUM_AGGREGATION = new SumAggregation(); +const LAST_VALUE_AGGREGATION = new LastValueAggregation(); +const HISTOGRAM_AGGREGATION = new HistogramAggregation(); +const DEFAULT_AGGREGATION = new DefaultAggregation(); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts index 9dfbcb07ed9..179b5807efd 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/src/view/View.ts @@ -60,9 +60,7 @@ export class View { 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.aggregation = config?.aggregation ?? Aggregation.Default(); this.attributesProcessor = config?.attributesProcessor ?? AttributesProcessor.Noop(); } } diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Drop.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Drop.test.ts new file mode 100644 index 00000000000..bc264cc8f81 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Drop.test.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 { HrTime } from '@opentelemetry/api'; +import * as assert from 'assert'; +import { DropAggregator } from '../../src/aggregator'; +import { AggregationTemporality } from '../../src/export/AggregationTemporality'; +import { defaultInstrumentationLibrary, defaultInstrumentDescriptor, defaultResource } from '../util'; + +describe('DropAggregator', () => { + describe('createAccumulation', () => { + it('no exceptions', () => { + const aggregator = new DropAggregator(); + const accumulation = aggregator.createAccumulation(); + assert.strictEqual(accumulation, undefined); + }); + }); + + describe('merge', () => { + it('no exceptions', () => { + const aggregator = new DropAggregator(); + const prev = aggregator.createAccumulation(); + const delta = aggregator.createAccumulation(); + assert.strictEqual(aggregator.merge(prev, delta), undefined); + }); + }); + + describe('diff', () => { + it('no exceptions', () => { + const aggregator = new DropAggregator(); + const prev = aggregator.createAccumulation(); + const curr = aggregator.createAccumulation(); + assert.strictEqual(aggregator.diff(prev, curr), undefined); + }); + }); + + describe('toMetricData', () => { + it('no exceptions', () => { + const aggregator = new DropAggregator(); + + const sdkStartTime: HrTime = [0, 0]; + const lastCollectionTime: HrTime = [1, 1]; + const collectionTime: HrTime = [2, 2]; + + assert.strictEqual(aggregator.toMetricData( + defaultResource, + defaultInstrumentationLibrary, + defaultInstrumentDescriptor, + [[{}, undefined]], + AggregationTemporality.DELTA, + sdkStartTime, + lastCollectionTime, + collectionTime, + ), undefined); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Histogram.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Histogram.test.ts new file mode 100644 index 00000000000..21ba31b848b --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Histogram.test.ts @@ -0,0 +1,185 @@ +/* + * 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 { HrTime } from '@opentelemetry/api'; +import * as assert from 'assert'; +import { HistogramAccumulation, HistogramAggregator } from '../../src/aggregator'; +import { AggregationTemporality } from '../../src/export/AggregationTemporality'; +import { MetricData, PointDataType } from '../../src/export/MetricData'; +import { commonValues, defaultInstrumentationLibrary, defaultInstrumentDescriptor, defaultResource } from '../util'; + +describe('HistogramAggregator', () => { + describe('createAccumulation', () => { + it('no exceptions on createAccumulation', () => { + const aggregator = new HistogramAggregator([1, 10, 100]); + const accumulation = aggregator.createAccumulation(); + assert(accumulation instanceof HistogramAccumulation); + }); + }); + + describe('merge', () => { + it('no exceptions', () => { + const aggregator = new HistogramAggregator([1, 10, 100]); + const prev = aggregator.createAccumulation(); + prev.record(0); + prev.record(1); + + const delta = aggregator.createAccumulation(); + delta.record(2); + delta.record(11); + + const expected = aggregator.createAccumulation(); + // replay actions on prev + expected.record(0); + expected.record(1); + // replay actions on delta + expected.record(2); + expected.record(11); + + assert.deepStrictEqual(aggregator.merge(prev, delta), expected); + }); + }); + + describe('diff', () => { + it('no exceptions', () => { + const aggregator = new HistogramAggregator([1, 10, 100]); + const prev = aggregator.createAccumulation(); + prev.record(0); + prev.record(1); + + const curr = aggregator.createAccumulation(); + // replay actions on prev + curr.record(0); + curr.record(1); + // perform new actions + curr.record(2); + curr.record(11); + + const expected = new HistogramAccumulation([1, 10, 100], { + buckets: { + boundaries: [1, 10, 100], + counts: [0, 1, 1, 0], + }, + count: 2, + sum: 13, + }); + + assert.deepStrictEqual(aggregator.diff(prev, curr), expected); + }); + }); + + describe('toMetricData', () => { + it('transform with AggregationTemporality.DELTA', () => { + const aggregator = new HistogramAggregator([1, 10, 100]); + + const accumulation = aggregator.createAccumulation(); + accumulation.record(0); + accumulation.record(1); + + const sdkStartTime: HrTime = [0, 0]; + const lastCollectionTime: HrTime = [1, 1]; + const collectionTime: HrTime = [2, 2]; + + const expected: MetricData = { + resource: defaultResource, + instrumentationLibrary: defaultInstrumentationLibrary, + instrumentDescriptor: defaultInstrumentDescriptor, + pointDataType: PointDataType.HISTOGRAM, + pointData: [ + { + attributes: {}, + startTime: lastCollectionTime, + endTime: collectionTime, + point: { + buckets: { + boundaries: [1, 10, 100], + counts: [1, 1, 0, 0], + }, + count: 2, + sum: 1, + }, + }, + ], + }; + assert.deepStrictEqual(aggregator.toMetricData( + defaultResource, + defaultInstrumentationLibrary, + defaultInstrumentDescriptor, + [[{}, accumulation]], + AggregationTemporality.DELTA, + sdkStartTime, + lastCollectionTime, + collectionTime, + ), expected); + }); + + it('transform with AggregationTemporality.CUMULATIVE', () => { + const aggregator = new HistogramAggregator([1, 10, 100]); + + const accumulation = aggregator.createAccumulation(); + accumulation.record(0); + accumulation.record(1); + + const sdkStartTime: HrTime = [0, 0]; + const lastCollectionTime: HrTime = [1, 1]; + const collectionTime: HrTime = [2, 2]; + + const expected: MetricData = { + resource: defaultResource, + instrumentationLibrary: defaultInstrumentationLibrary, + instrumentDescriptor: defaultInstrumentDescriptor, + pointDataType: PointDataType.HISTOGRAM, + pointData: [ + { + attributes: {}, + startTime: sdkStartTime, + endTime: collectionTime, + point: { + buckets: { + boundaries: [1, 10, 100], + counts: [1, 1, 0, 0], + }, + count: 2, + sum: 1, + }, + }, + ], + }; + assert.deepStrictEqual(aggregator.toMetricData( + defaultResource, + defaultInstrumentationLibrary, + defaultInstrumentDescriptor, + [[{}, accumulation]], + AggregationTemporality.CUMULATIVE, + sdkStartTime, + lastCollectionTime, + collectionTime, + ), expected); + }); + }); +}); + +describe('HistogramAccumulation', () => { + describe('record', () => { + it('no exceptions on record', () => { + const accumulation = new HistogramAccumulation([1, 10, 100]); + + for (const value of commonValues) { + accumulation.record(value); + } + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/LastValue.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/LastValue.test.ts new file mode 100644 index 00000000000..1f6017f3af8 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/LastValue.test.ts @@ -0,0 +1,180 @@ +/* + * 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 { HrTime } from '@opentelemetry/api'; +import * as assert from 'assert'; +import { LastValueAccumulation, LastValueAggregator } from '../../src/aggregator'; +import { AggregationTemporality } from '../../src/export/AggregationTemporality'; +import { MetricData, PointDataType } from '../../src/export/MetricData'; +import { commonValues, defaultInstrumentationLibrary, defaultInstrumentDescriptor, defaultResource, sleep } from '../util'; + +describe('LastValueAggregator', () => { + describe('createAccumulation', () => { + it('no exceptions on createAccumulation', () => { + const aggregator = new LastValueAggregator(); + const accumulation = aggregator.createAccumulation(); + assert(accumulation instanceof LastValueAccumulation); + }); + }); + + describe('merge', () => { + it('no exceptions', () => { + const aggregator = new LastValueAggregator(); + const prev = aggregator.createAccumulation(); + const delta = aggregator.createAccumulation(); + + prev.record(2); + delta.record(3); + + assert.deepStrictEqual(aggregator.merge(prev, delta), delta); + }); + + it('return the newly sampled accumulation', async () => { + const aggregator = new LastValueAggregator(); + const accumulation1 = aggregator.createAccumulation(); + const accumulation2 = aggregator.createAccumulation(); + + accumulation1.record(2); + await sleep(1); + accumulation2.record(3); + // refresh the accumulation1 + await sleep(1); + accumulation1.record(4); + + assert.deepStrictEqual(aggregator.merge(accumulation1, accumulation2), accumulation1); + assert.deepStrictEqual(aggregator.merge(accumulation2, accumulation1), accumulation1); + }); + }); + + describe('diff', () => { + it('no exceptions', () => { + const aggregator = new LastValueAggregator(); + const prev = aggregator.createAccumulation(); + const curr = aggregator.createAccumulation(); + + prev.record(2); + curr.record(3); + + assert.deepStrictEqual(aggregator.diff(prev, curr), curr); + }); + + it('return the newly sampled accumulation', async () => { + const aggregator = new LastValueAggregator(); + const accumulation1 = aggregator.createAccumulation(); + const accumulation2 = aggregator.createAccumulation(); + + accumulation1.record(2); + accumulation2.record(3); + // refresh the accumulation1 + await sleep(1); + accumulation1.record(4); + + assert.deepStrictEqual(aggregator.diff(accumulation1, accumulation2), accumulation1); + assert.deepStrictEqual(aggregator.diff(accumulation2, accumulation1), accumulation1); + }); + }); + + describe('toMetricData', () => { + it('transform with AggregationTemporality.DELTA', () => { + const aggregator = new LastValueAggregator(); + + const accumulation = aggregator.createAccumulation(); + accumulation.record(1); + accumulation.record(2); + accumulation.record(1); + accumulation.record(4); + + const sdkStartTime: HrTime = [0, 0]; + const lastCollectionTime: HrTime = [1, 1]; + const collectionTime: HrTime = [2, 2]; + + const expected: MetricData = { + resource: defaultResource, + instrumentationLibrary: defaultInstrumentationLibrary, + instrumentDescriptor: defaultInstrumentDescriptor, + pointDataType: PointDataType.SINGULAR, + pointData: [ + { + attributes: {}, + startTime: lastCollectionTime, + endTime: collectionTime, + point: 4, + }, + ], + }; + assert.deepStrictEqual(aggregator.toMetricData( + defaultResource, + defaultInstrumentationLibrary, + defaultInstrumentDescriptor, + [[{}, accumulation]], + AggregationTemporality.DELTA, + sdkStartTime, + lastCollectionTime, + collectionTime, + ), expected); + }); + + it('transform with AggregationTemporality.CUMULATIVE', () => { + const aggregator = new LastValueAggregator(); + + const accumulation = aggregator.createAccumulation(); + accumulation.record(1); + accumulation.record(2); + accumulation.record(1); + + const sdkStartTime: HrTime = [0, 0]; + const lastCollectionTime: HrTime = [1, 1]; + const collectionTime: HrTime = [2, 2]; + + const expected: MetricData = { + resource: defaultResource, + instrumentationLibrary: defaultInstrumentationLibrary, + instrumentDescriptor: defaultInstrumentDescriptor, + pointDataType: PointDataType.SINGULAR, + pointData: [ + { + attributes: {}, + startTime: sdkStartTime, + endTime: collectionTime, + point: 1, + }, + ], + }; + assert.deepStrictEqual(aggregator.toMetricData( + defaultResource, + defaultInstrumentationLibrary, + defaultInstrumentDescriptor, + [[{}, accumulation]], + AggregationTemporality.CUMULATIVE, + sdkStartTime, + lastCollectionTime, + collectionTime, + ), expected); + }); + }); +}); + +describe('LastValueAccumulation', () => { + describe('record', () => { + it('no exceptions on record', () => { + const accumulation = new LastValueAccumulation(); + + for (const value of commonValues) { + accumulation.record(value); + } + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Sum.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Sum.test.ts new file mode 100644 index 00000000000..99db9b27127 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/aggregator/Sum.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { HrTime } from '@opentelemetry/api'; +import * as assert from 'assert'; +import { SumAccumulation, SumAggregator } from '../../src/aggregator'; +import { AggregationTemporality } from '../../src/export/AggregationTemporality'; +import { MetricData, PointDataType } from '../../src/export/MetricData'; +import { commonValues, defaultInstrumentationLibrary, defaultInstrumentDescriptor, defaultResource } from '../util'; + +describe('SumAggregator', () => { + describe('createAccumulation', () => { + it('no exceptions on createAccumulation', () => { + const aggregator = new SumAggregator(); + const accumulation = aggregator.createAccumulation(); + assert(accumulation instanceof SumAccumulation); + }); + }); + + describe('merge', () => { + it('no exceptions', () => { + const aggregator = new SumAggregator(); + const prev = aggregator.createAccumulation(); + prev.record(1); + prev.record(2); + + const delta = aggregator.createAccumulation(); + delta.record(3); + delta.record(4); + + const expected = aggregator.createAccumulation(); + expected.record(1 + 2 + 3 + 4); + assert.deepStrictEqual(aggregator.merge(prev, delta), expected); + }); + }); + + describe('diff', () => { + it('no exceptions', () => { + const aggregator = new SumAggregator(); + const prev = aggregator.createAccumulation(); + prev.record(1); + prev.record(2); + + const curr = aggregator.createAccumulation(); + // replay actions performed on prev + curr.record(1); + curr.record(2); + // perform new actions + curr.record(3); + curr.record(4); + + const expected = aggregator.createAccumulation(); + expected.record(3 + 4); + assert.deepStrictEqual(aggregator.diff(prev, curr), expected); + }); + }); + + describe('toMetricData', () => { + it('transform with AggregationTemporality.DELTA', () => { + const aggregator = new SumAggregator(); + const accumulation = aggregator.createAccumulation(); + accumulation.record(1); + accumulation.record(2); + + const sdkStartTime: HrTime = [0, 0]; + const lastCollectionTime: HrTime = [1, 1]; + const collectionTime: HrTime = [2, 2]; + + const expected: MetricData = { + resource: defaultResource, + instrumentationLibrary: defaultInstrumentationLibrary, + instrumentDescriptor: defaultInstrumentDescriptor, + pointDataType: PointDataType.SINGULAR, + pointData: [ + { + attributes: {}, + startTime: lastCollectionTime, + endTime: collectionTime, + point: 3, + }, + ], + }; + assert.deepStrictEqual(aggregator.toMetricData( + defaultResource, + defaultInstrumentationLibrary, + defaultInstrumentDescriptor, + [[{}, accumulation]], + AggregationTemporality.DELTA, + sdkStartTime, + lastCollectionTime, + collectionTime, + ), expected); + }); + + it('transform with AggregationTemporality.CUMULATIVE', () => { + const aggregator = new SumAggregator(); + const accumulation = aggregator.createAccumulation(); + accumulation.record(1); + accumulation.record(2); + + const sdkStartTime: HrTime = [0, 0]; + const lastCollectionTime: HrTime = [1, 1]; + const collectionTime: HrTime = [2, 2]; + + const expected: MetricData = { + resource: defaultResource, + instrumentationLibrary: defaultInstrumentationLibrary, + instrumentDescriptor: defaultInstrumentDescriptor, + pointDataType: PointDataType.SINGULAR, + pointData: [ + { + attributes: {}, + startTime: sdkStartTime, + endTime: collectionTime, + point: 3, + }, + ], + }; + assert.deepStrictEqual(aggregator.toMetricData( + defaultResource, + defaultInstrumentationLibrary, + defaultInstrumentDescriptor, + [[{}, accumulation]], + AggregationTemporality.CUMULATIVE, + sdkStartTime, + lastCollectionTime, + collectionTime, + ), expected); + }); + }); +}); + +describe('SumAccumulation', () => { + describe('record', () => { + it('no exceptions on record', () => { + const accumulation = new SumAccumulation(); + + for (const value of commonValues) { + accumulation.record(value); + } + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/util.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/util.ts new file mode 100644 index 00000000000..bf56a9d50f7 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/util.ts @@ -0,0 +1,49 @@ +/* + * 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 { Attributes, ValueType } from '@opentelemetry/api-metrics'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { InstrumentDescriptor } from '../src/InstrumentDescriptor'; +import { InstrumentType } from '../src/Instruments'; + +export const defaultResource = new Resource({ + resourceKey: 'my-resource', +}); + +export const defaultInstrumentDescriptor: InstrumentDescriptor = { + name: 'default_metric', + description: 'a simple instrument', + type: InstrumentType.COUNTER, + unit: '1', + valueType: ValueType.DOUBLE, +}; + +export const defaultInstrumentationLibrary: InstrumentationLibrary = { + name: 'default', + version: '1.0.0', + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0' +}; + +export const commonValues: number[] = [1, -1, 1.0, Infinity, -Infinity, NaN]; +export const commonAttributes: Attributes[] = [{}, {1: '1'}, {a: '2'}, new (class Foo{ +a = '1' +})]; + +export const sleep = (time: number) => + new Promise(resolve => { + return setTimeout(resolve, time); + }); diff --git a/experimental/packages/opentelemetry-sdk-metrics-base/test/view/Aggregation.test.ts b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/Aggregation.test.ts new file mode 100644 index 00000000000..717c74a1ad0 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/Aggregation.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { + Aggregator, + DropAggregator, + HistogramAggregator, + LastValueAggregator, + SumAggregator, +} from '../../src/aggregator'; +import { InstrumentDescriptor } from '../../src/InstrumentDescriptor'; +import { InstrumentType } from '../../src/Instruments'; +import { + Aggregation, + DefaultAggregation, + DropAggregation, + ExplicitBucketHistogramAggregation, + HistogramAggregation, + LastValueAggregation, + SumAggregation, +} from '../../src/view/Aggregation'; +import { defaultInstrumentDescriptor } from '../util'; + +interface AggregationConstructor { + new(...args: any[]): Aggregation; +} + +interface AggregatorConstructor { + new(...args: any[]): Aggregator; +} + +describe('Aggregation', () => { + it('static aggregations', () => { + const staticMembers: [keyof typeof Aggregation, AggregationConstructor][] = [ + ['Drop', DropAggregation], + ['Sum', SumAggregation], + ['LastValue', LastValueAggregation], + ['Histogram', HistogramAggregation], + ['Default', DefaultAggregation], + ]; + + for (const [key, type] of staticMembers) { + const aggregation = (Aggregation[key] as () => Aggregation)(); + assert(aggregation instanceof type); + assert(aggregation.createAggregator(defaultInstrumentDescriptor)); + } + }); +}); + +describe('DefaultAggregation', () => { + describe('createAggregator', () => { + it('should create aggregators for instrument descriptors', () => { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#default-aggregation + const expectations: [InstrumentDescriptor, AggregatorConstructor][] = [ + [{ ...defaultInstrumentDescriptor, type: InstrumentType.COUNTER }, SumAggregator], + [{ ...defaultInstrumentDescriptor, type: InstrumentType.OBSERVABLE_COUNTER }, SumAggregator], + [{ ...defaultInstrumentDescriptor, type: InstrumentType.UP_DOWN_COUNTER }, SumAggregator], + [{ ...defaultInstrumentDescriptor, type: InstrumentType.OBSERVABLE_UP_DOWN_COUNTER }, SumAggregator], + [{ ...defaultInstrumentDescriptor, type: InstrumentType.OBSERVABLE_GAUGE }, LastValueAggregator], + [{ ...defaultInstrumentDescriptor, type: InstrumentType.HISTOGRAM }, HistogramAggregator], + // unknown instrument type + [{ ...defaultInstrumentDescriptor, type: -1 as unknown as InstrumentType }, DropAggregator], + ]; + + const aggregation = new DefaultAggregation(); + for (const [instrumentDescriptor, type] of expectations) { + assert(aggregation.createAggregator(instrumentDescriptor) instanceof type, `${InstrumentType[instrumentDescriptor.type]}`); + } + }); + }); +}); + +describe('ExplicitBucketHistogramAggregation', () => { + it('construct without exceptions', () => { + const aggregation = new ExplicitBucketHistogramAggregation([1, 10, 100]); + assert(aggregation instanceof ExplicitBucketHistogramAggregation); + }); + + describe('createAggregator', () => { + it('should create histogram aggregators with boundaries', () => { + const aggregator1 = new ExplicitBucketHistogramAggregation([1, 10, 100]).createAggregator(defaultInstrumentDescriptor); + assert(aggregator1 instanceof HistogramAggregator); + assert.deepStrictEqual(aggregator1['_boundaries'], [1, 10, 100]); + + const aggregator2 = new ExplicitBucketHistogramAggregation([10, 100, 1000]).createAggregator(defaultInstrumentDescriptor); + assert(aggregator2 instanceof HistogramAggregator); + assert.deepStrictEqual(aggregator2['_boundaries'], [10, 100, 1000]); + }); + }); +}); 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 index 11e3658744e..99520d9933f 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/test/view/View.test.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/View.test.ts @@ -25,7 +25,7 @@ describe('View', () => { const view = new View(); assert.strictEqual(view.name, undefined); assert.strictEqual(view.description, undefined); - assert.strictEqual(view.aggregation, Aggregation.None()); + assert.strictEqual(view.aggregation, Aggregation.Default()); 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 index e3c21a3b66f..e7185a18887 100644 --- a/experimental/packages/opentelemetry-sdk-metrics-base/test/view/ViewRegistry.test.ts +++ b/experimental/packages/opentelemetry-sdk-metrics-base/test/view/ViewRegistry.test.ts @@ -15,28 +15,13 @@ */ import * as assert from 'assert'; -import { ValueType } from '@opentelemetry/api-metrics-wip'; -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' -}; +import { defaultInstrumentationLibrary, defaultInstrumentDescriptor } from '../util'; + describe('ViewRegistry', () => { describe('findViews', () => {