diff --git a/CHANGELOG.md b/CHANGELOG.md index db8b1b26d96..32bf66fb902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- Refactor InMemoryMetricExporter and add tests ([#3519](https://github.com/open-telemetry/opentelemetry-python/pull/3519)) ## Version 1.21.0/0.42b0 (2023-11-01) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 0568270ae6b..db7178bbda9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -21,7 +21,7 @@ from sys import stdout from threading import Event, Lock, RLock, Thread from time import time_ns -from typing import IO, Callable, Dict, Iterable, Optional +from typing import IO, Callable, Dict, Iterable, Optional, Sequence from typing_extensions import final @@ -56,7 +56,7 @@ _ObservableUpDownCounter, _UpDownCounter, ) -from opentelemetry.sdk.metrics._internal.point import MetricsData +from opentelemetry.sdk.metrics._internal.point import Metric, MetricsData from opentelemetry.util._once import Once _logger = getLogger(__name__) @@ -171,6 +171,39 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: return True +class InMemoryMetricExporter(MetricExporter): + """Implementation of :class:`.MetricExporter` that stores metrics in memory. + + This class can be used for testing purposes. It stores the exported metrics + in a dictionary in memory, indexed by an auto-incrementing counter that can + be retrieved using the `metrics` attribute. + + The `export` method adds the metrics to the in-memory store and increments + the counter. Each set of metrics can be accessed by its unique index. + """ + + def __init__(self): + super().__init__() + self.metrics = {} + self._counter = 0 + + def export( + self, + metrics_data: Sequence[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + self.metrics[self._counter] = metrics_data + self._counter += 1 + return MetricExportResult.SUCCESS + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + pass + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + class MetricReader(ABC): # pylint: disable=too-many-branches """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 97c31b97ec7..4b6d47dc7e1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -16,6 +16,7 @@ from opentelemetry.sdk.metrics._internal.export import ( AggregationTemporality, ConsoleMetricExporter, + InMemoryMetricExporter, InMemoryMetricReader, MetricExporter, MetricExportResult, @@ -44,6 +45,7 @@ __all__ = [ "AggregationTemporality", "ConsoleMetricExporter", + "InMemoryMetricExporter", "InMemoryMetricReader", "MetricExporter", "MetricExportResult", diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_exporter.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_exporter.py new file mode 100644 index 00000000000..2754a45a762 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_exporter.py @@ -0,0 +1,56 @@ +# 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 typing import Sequence +from unittest import TestCase + +from opentelemetry.sdk.metrics.export import ( + InMemoryMetricExporter, + MetricExportResult, +) + + +class MockMetric: + # A placeholder class for mock metrics + pass + + +class TestInMemoryMetricExporter(TestCase): + def setUp(self): + self.exporter = InMemoryMetricExporter() + + def test_initialization(self): + self.assertIsInstance(self.exporter.metrics, dict) + self.assertEqual(len(self.exporter.metrics), 0) + self.assertEqual(self.exporter._counter, 0) + + def test_export(self): + mock_metrics: Sequence[MockMetric] = [MockMetric(), MockMetric()] + + # Test the first export + result = self.exporter.export(mock_metrics) + self.assertEqual(result, MetricExportResult.SUCCESS) + self.assertIn(0, self.exporter.metrics) + self.assertEqual(self.exporter.metrics[0], mock_metrics) + self.assertEqual(self.exporter._counter, 1) + + # Test the second export + result = self.exporter.export(mock_metrics) + self.assertEqual(result, MetricExportResult.SUCCESS) + self.assertIn(1, self.exporter.metrics) + self.assertEqual(self.exporter.metrics[1], mock_metrics) + self.assertEqual(self.exporter._counter, 2) + + def test_force_flush(self): + self.assertTrue(self.exporter.force_flush()) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 8373d3dfe0d..557beaf1f04 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -15,7 +15,7 @@ from logging import WARNING from time import sleep -from typing import Iterable, Sequence +from typing import Iterable from unittest import TestCase from unittest.mock import MagicMock, Mock, patch @@ -32,9 +32,8 @@ ) from opentelemetry.sdk.metrics._internal import SynchronousMeasurementConsumer from opentelemetry.sdk.metrics.export import ( + InMemoryMetricExporter, Metric, - MetricExporter, - MetricExportResult, MetricReader, PeriodicExportingMetricReader, ) @@ -447,29 +446,6 @@ def test_create_observable_up_down_counter(self): self.assertEqual(observable_up_down_counter.name, "name") -class InMemoryMetricExporter(MetricExporter): - def __init__(self): - super().__init__() - self.metrics = {} - self._counter = 0 - - def export( - self, - metrics: Sequence[Metric], - timeout_millis: float = 10_000, - **kwargs, - ) -> MetricExportResult: - self.metrics[self._counter] = metrics - self._counter += 1 - return MetricExportResult.SUCCESS - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - pass - - def force_flush(self, timeout_millis: float = 10_000) -> bool: - return True - - class TestDuplicateInstrumentAggregateData(TestCase): def test_duplicate_instrument_aggregate_data(self):