diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/aggregation.py new file mode 100644 index 00000000000..7f63ef0cee7 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/aggregation.py @@ -0,0 +1,117 @@ +# 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 +# +# http://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. + +from abc import ABC, abstractmethod +from collections import OrderedDict +from math import inf + +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + AGGREGATION_TEMPORALITY_CUMULATIVE, + AGGREGATION_TEMPORALITY_DELTA +) + + +class Aggregation(ABC): + + def __init__(self): + self._value = 0 + + @property + def value(self): + return self._value + + @abstractmethod + def aggregate(self, value): + pass + + @abstractmethod + def collect(self): + pass + + +class NoneAggregation(Aggregation): + """ + This aggregation drops all instrument measurements. + """ + + def aggregate(self, value): + pass + + def collect(self): + return self._value + + +class SumAggregation(Aggregation): + """ + This aggregation collects data for the SDK sum metric point. + """ + + def __init__(self, temporality=AGGREGATION_TEMPORALITY_CUMULATIVE): + super().__init__() + self._temporality = temporality + + def aggregate(self, value): + self._value = self._value + value + + def collect(self): + value = self._value + if self._temporality == AGGREGATION_TEMPORALITY_DELTA: + self._value = 0 + + return value + + +class LastValueAggregation(Aggregation): + + """ + This aggregation collects data for the SDK sum metric point. + """ + + def aggregate(self, value): + self._value = value + + def collect(self): + return self._value + + +class ExplicitBucketHistogramAggregation(Aggregation): + + """ + This aggregation collects data for the SDK sum metric point. + """ + + def __init__( + self, + temporality=AGGREGATION_TEMPORALITY_CUMULATIVE, + boundaries=(0, 5, 10, 25, 50, 75, 100, 250, 500, 1000, inf) + ): + super().__init__() + self._temporality = temporality + self._boundaries = boundaries + self._value = OrderedDict([(key, 0) for key in boundaries]) + + def aggregate(self, value): + for key in self._value.keys(): + + if value < key: + self._value[key] = self._value[key] + value + + break + + def collect(self): + value = self._value + if self._temporality == AGGREGATION_TEMPORALITY_DELTA: + self._value = OrderedDict([(key, 0) for key in self._boundaries]) + + return value diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/instrument.py new file mode 100644 index 00000000000..1543dde0704 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/instrument.py @@ -0,0 +1,103 @@ +# 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 +# +# http://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. + +from opentelemetry.metrics.instrument import ( + Counter, + ObservableCounter, + UpDownCounter, + ObservableUpDownCounter, + Histogram, + ObservableGauge +) + +from opentelemetry.sdk.metrics.aggregation import ( + SumAggregation, + LastValueAggregation, + ExplicitBucketHistogramAggregation, +) + + +class Counter(Counter): + + def __init__( + self, + aggregation=SumAggregation, + aggregation_config={} + ): + self._aggregation = aggregation(**aggregation_config) + super().__init__() + + def add(self, amount, attributes=None): + pass + + +class UpDownCounter(UpDownCounter): + + def __init__( + self, + aggregation=SumAggregation, + aggregation_config={} + ): + self._aggregation = aggregation(**aggregation_config) + super().__init__() + + def add(self, amount, attributes=None): + pass + + +class ObservableCounter(ObservableCounter): + + def __init__( + self, + aggregation=SumAggregation, + aggregation_config={} + ): + self._aggregation = aggregation(**aggregation_config) + super().__init__() + + +class ObservableUpDownCounter(ObservableUpDownCounter): + + def __init__( + self, + aggregation=SumAggregation, + aggregation_config={} + ): + self._aggregation = aggregation(**aggregation_config) + super().__init__() + + +class Histogram(Histogram): + + def __init__( + self, + aggregation=ExplicitBucketHistogramAggregation, + aggregation_config={} + ): + self._aggregation = aggregation(**aggregation_config) + super().__init__() + + def add(self, amount, attributes=None): + pass + + +class ObservableGauge(ObservableGauge): + + def __init__( + self, + aggregation=LastValueAggregation, + aggregation_config={} + ): + self._aggregation = aggregation(**aggregation_config) + super().__init__() diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py new file mode 100644 index 00000000000..01c461556e6 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -0,0 +1,265 @@ +# 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 +# +# http://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. + + +from unittest import TestCase +from math import inf + +from opentelemetry.sdk.metrics.aggregation import ( + NoneAggregation, + SumAggregation, + LastValueAggregation, + ExplicitBucketHistogramAggregation +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + AGGREGATION_TEMPORALITY_CUMULATIVE, + AGGREGATION_TEMPORALITY_DELTA +) + + +class TestNoneAggregation(TestCase): + + def test_aggregate(self): + """ + `NoneAggregation` drops all measurements. + """ + + none_aggregation = NoneAggregation() + + none_aggregation.aggregate(1) + none_aggregation.aggregate(2) + none_aggregation.aggregate(3) + + self.assertIs(none_aggregation.value, 0) + + +class TestSumAggregation(TestCase): + + def test_default_temporality(self): + """ + `SumAggregation` default temporality is + `AGGREGATION_TEMPORALITY_CUMULATIVE`. + """ + + sum_aggregation = SumAggregation() + self.assertEqual( + sum_aggregation._temporality, + AGGREGATION_TEMPORALITY_CUMULATIVE + ) + sum_aggregation = SumAggregation( + temporality=AGGREGATION_TEMPORALITY_DELTA + ) + self.assertEqual( + sum_aggregation._temporality, + AGGREGATION_TEMPORALITY_DELTA + ) + + def test_aggregate(self): + """ + `SumAggregation` collects data for sum metric points + """ + + sum_aggregation = SumAggregation() + + sum_aggregation.aggregate(1) + sum_aggregation.aggregate(2) + sum_aggregation.aggregate(3) + + self.assertEqual(sum_aggregation.value, 6) + + def test_delta_temporality(self): + """ + `SumAggregation` supports delta temporality + """ + + sum_aggregation = SumAggregation(AGGREGATION_TEMPORALITY_DELTA) + + sum_aggregation.aggregate(1) + sum_aggregation.aggregate(2) + sum_aggregation.aggregate(3) + + self.assertEqual(sum_aggregation.value, 6) + self.assertEqual(sum_aggregation.collect(), 6) + self.assertEqual(sum_aggregation.value, 0) + + sum_aggregation.aggregate(1) + sum_aggregation.aggregate(2) + sum_aggregation.aggregate(3) + + self.assertEqual(sum_aggregation.value, 6) + self.assertEqual(sum_aggregation.collect(), 6) + self.assertEqual(sum_aggregation.value, 0) + + def test_cumulative_temporality(self): + """ + `SumAggregation` supports cumulative temporality + """ + + sum_aggregation = SumAggregation(AGGREGATION_TEMPORALITY_CUMULATIVE) + + sum_aggregation.aggregate(1) + sum_aggregation.aggregate(2) + sum_aggregation.aggregate(3) + + self.assertEqual(sum_aggregation.value, 6) + self.assertEqual(sum_aggregation.collect(), 6) + self.assertEqual(sum_aggregation.value, 6) + + sum_aggregation.aggregate(1) + sum_aggregation.aggregate(2) + sum_aggregation.aggregate(3) + + self.assertEqual(sum_aggregation.value, 12) + self.assertEqual(sum_aggregation.collect(), 12) + self.assertEqual(sum_aggregation.value, 12) + + +class TestLastValueAggregation(TestCase): + + def test_aggregate(self): + """ + `LastValueAggregation` collects data for gauge metric points with delta + temporality + """ + + last_value_aggregation = LastValueAggregation() + + last_value_aggregation.aggregate(1) + self.assertEqual(last_value_aggregation.value, 1) + + last_value_aggregation.aggregate(2) + self.assertEqual(last_value_aggregation.value, 2) + + last_value_aggregation.aggregate(3) + self.assertEqual(last_value_aggregation.value, 3) + + +class TestExplicitBucketHistogramAggregation(TestCase): + + def test_default_temporality(self): + """ + `ExplicitBucketHistogramAggregation` default temporality is + `AGGREGATION_TEMPORALITY_CUMULATIVE`. + """ + + explicit_bucket_histogram_aggregation = ExplicitBucketHistogramAggregation() + self.assertEqual( + explicit_bucket_histogram_aggregation._temporality, + AGGREGATION_TEMPORALITY_CUMULATIVE + ) + explicit_bucket_histogram_aggregation = ExplicitBucketHistogramAggregation( + temporality=AGGREGATION_TEMPORALITY_DELTA + ) + self.assertEqual( + explicit_bucket_histogram_aggregation._temporality, + AGGREGATION_TEMPORALITY_DELTA + ) + + def test_aggregate(self): + """ + `ExplicitBucketHistogramAggregation` collects data for explicit_bucket_histogram metric points + """ + + explicit_bucket_histogram_aggregation = ExplicitBucketHistogramAggregation() + + explicit_bucket_histogram_aggregation.aggregate(-1) + explicit_bucket_histogram_aggregation.aggregate(2) + explicit_bucket_histogram_aggregation.aggregate(7) + explicit_bucket_histogram_aggregation.aggregate(8) + explicit_bucket_histogram_aggregation.aggregate(9999) + + self.assertEqual(explicit_bucket_histogram_aggregation.value[0], -1) + self.assertEqual(explicit_bucket_histogram_aggregation.value[5], 2) + self.assertEqual(explicit_bucket_histogram_aggregation.value[10], 15) + self.assertEqual( + explicit_bucket_histogram_aggregation.value[inf], 9999 + ) + + def test_delta_temporality(self): + """ + `ExplicitBucketHistogramAggregation` supports delta temporality + """ + + explicit_bucket_histogram_aggregation = ExplicitBucketHistogramAggregation(AGGREGATION_TEMPORALITY_DELTA) + + explicit_bucket_histogram_aggregation.aggregate(-1) + explicit_bucket_histogram_aggregation.aggregate(2) + explicit_bucket_histogram_aggregation.aggregate(7) + explicit_bucket_histogram_aggregation.aggregate(8) + explicit_bucket_histogram_aggregation.aggregate(9999) + + result = explicit_bucket_histogram_aggregation.collect() + + self.assertEqual(result[0], -1) + self.assertEqual(result[5], 2) + self.assertEqual(result[10], 15) + self.assertEqual(result[inf], 9999) + + self.assertEqual(explicit_bucket_histogram_aggregation.value[0], 0) + self.assertEqual(explicit_bucket_histogram_aggregation.value[5], 0) + self.assertEqual(explicit_bucket_histogram_aggregation.value[10], 0) + self.assertEqual( + explicit_bucket_histogram_aggregation.value[inf], 0 + ) + + def test_cumulative_temporality(self): + """ + `ExplicitBucketHistogramAggregation` supports cumulative temporality + """ + + explicit_bucket_histogram_aggregation = ExplicitBucketHistogramAggregation(AGGREGATION_TEMPORALITY_CUMULATIVE) + + explicit_bucket_histogram_aggregation.aggregate(-1) + explicit_bucket_histogram_aggregation.aggregate(2) + explicit_bucket_histogram_aggregation.aggregate(7) + explicit_bucket_histogram_aggregation.aggregate(8) + explicit_bucket_histogram_aggregation.aggregate(9999) + + result = explicit_bucket_histogram_aggregation.collect() + + self.assertEqual(result[0], -1) + self.assertEqual(result[5], 2) + self.assertEqual(result[10], 15) + self.assertEqual(result[inf], 9999) + + self.assertEqual(explicit_bucket_histogram_aggregation.value[0], -1) + self.assertEqual(explicit_bucket_histogram_aggregation.value[5], 2) + self.assertEqual(explicit_bucket_histogram_aggregation.value[10], 15) + self.assertEqual( + explicit_bucket_histogram_aggregation.value[inf], 9999 + ) + + explicit_bucket_histogram_aggregation.aggregate(-1) + explicit_bucket_histogram_aggregation.aggregate(2) + explicit_bucket_histogram_aggregation.aggregate(7) + explicit_bucket_histogram_aggregation.aggregate(8) + explicit_bucket_histogram_aggregation.aggregate(9999) + + result = explicit_bucket_histogram_aggregation.collect() + + self.assertEqual(result[0], -1 * 2) + self.assertEqual(result[5], 2 * 2) + self.assertEqual(result[10], 15 * 2) + self.assertEqual(result[inf], 9999 * 2) + + self.assertEqual( + explicit_bucket_histogram_aggregation.value[0], -1 * 2 + ) + self.assertEqual(explicit_bucket_histogram_aggregation.value[5], 2 * 2) + self.assertEqual( + explicit_bucket_histogram_aggregation.value[10], 15 * 2 + ) + self.assertEqual( + explicit_bucket_histogram_aggregation.value[inf], 9999 * 2 + ) diff --git a/tox.ini b/tox.ini index 75772516d72..4ed69f43fc2 100644 --- a/tox.ini +++ b/tox.ini @@ -111,6 +111,7 @@ commands_pre = opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util protobuf: pip install {toxinidir}/opentelemetry-proto + sdk: pip install {toxinidir}/opentelemetry-proto getting-started: pip install requests==2.26.0 flask==2.0.1 getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http"