Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5778.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: set `error.type` on failed metric reader collections instead of recording only the duration
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,14 @@ def collect(self, timeout_millis: float = 10_000) -> None:
return

start_time = perf_counter()
collection_error_type = None
try:
metrics = self._collect(self, timeout_millis=timeout_millis)
except Exception as error: # pylint: disable=broad-exception-caught
collection_error_type = type(error).__qualname__
raise
finally:
self._metrics.record_collection(perf_counter() - start_time)
self._metrics.record_collection(perf_counter() - start_time, collection_error_type)

if metrics is not None:
self._receive_metrics(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
from opentelemetry.semconv._incubating.metrics.otel_metrics import (
create_otel_sdk_metric_reader_collection_duration,
)
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE

_component_counter = Counter()


class MetricReaderMetricsT(Protocol):
def record_collection(self, duration: float) -> None: ...
def record_collection(self, duration: float, error_type: str | None = None) -> None: ...


class NoOpMetricReaderMetrics:
def record_collection(self, duration: float) -> None:
def record_collection(self, duration: float, error_type: str | None = None) -> None:
pass


Expand All @@ -39,8 +40,11 @@ def __init__(self, component_type: str, meter_provider: MeterProvider) -> None:

self._collection_duration = create_otel_sdk_metric_reader_collection_duration(meter)

def record_collection(self, duration: float) -> None:
self._collection_duration.record(duration, self._standard_attrs)
def record_collection(self, duration: float, error_type: str | None = None) -> None:
attributes = self._standard_attrs
if error_type is not None:
attributes = {**self._standard_attrs, ERROR_TYPE: error_type}
self._collection_duration.record(duration, attributes)


def create_metric_reader_metrics(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,47 @@ def test_metric_reader_metrics(self):
self.assertTrue(name.startswith("periodic_metric_reader/"))

mp.shutdown()

@patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"})
def test_metric_reader_metrics_failed_collection_records_error_type(self):
exporter = FakeMetricsExporter()
pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=100000)
mp = MeterProvider(metric_readers=[pmr])

# The first two collections succeed. The second one exports the data
# point recorded by the first one, so the exporter is cleared after it.
mp.force_flush()
mp.force_flush()
exporter.metrics.clear()

successful_collect = pmr._collect

def failing_collect(*args, **kwargs):
raise MetricsTimeoutError("simulated collection failure")

pmr._set_collect_callback(failing_collect)
with self.assertRaises(MetricsTimeoutError):
pmr.collect()
pmr._set_collect_callback(successful_collect)

# This collection exports the data point recorded by the failed one.
mp.force_flush()

collection_points = []
for metrics_data in exporter.metrics:
for resource_metrics in metrics_data.resource_metrics:
for scope_metrics in resource_metrics.scope_metrics:
if scope_metrics.scope.name != "opentelemetry-sdk":
continue
for metric in scope_metrics.metrics:
if metric.name == "otel.sdk.metric_reader.collection.duration":
collection_points.extend(metric.data.data_points)

self.assertTrue(collection_points)
error_types = [point.attributes.get("error.type") for point in collection_points]
# A failed collection must be reported with its failure cause, while a
# successful one must not set error.type at all.
self.assertIn("MetricsTimeoutError", error_types)
self.assertIn(None, error_types)

mp.shutdown()
Loading