diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java index eb3a28be885..294f2661139 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java @@ -275,6 +275,12 @@ CompletableResultCode doRun() { Collection metricData; try { metricData = collectionRegistration.collectAllMetrics(); + } catch (Throwable t) { + // Record the failure on the self-observability sample before it propagates to the + // outer handler; otherwise error.type is never set and collection failures are + // reported as successful collections. + error = t.getClass().getName(); + throw t; } finally { long durationNanos = CLOCK.nanoTime() - startNanoTime; instrumentation.recordCollection(durationNanos / 1_000_000_000.0, error); diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderMetricsTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderMetricsTest.java index bab3effdfcf..09ddf0af9e5 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderMetricsTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderMetricsTest.java @@ -12,10 +12,13 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.internal.SemConvAttributes; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricProducer; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; class SdkMeterProviderMetricsTest { @@ -59,4 +62,44 @@ void simple() { }); } } + + @Test + void collectionFailureIsRecordedWithErrorType() { + InMemoryMetricExporter metricExporter = InMemoryMetricExporter.create(); + AtomicBoolean shouldFail = new AtomicBoolean(true); + // Fail the first collection, then succeed so the recorded self-observability sample can be + // exported on the following flush. + MetricProducer failingProducer = + resource -> { + if (shouldFail.getAndSet(false)) { + throw new IllegalStateException("boom"); + } + return Collections.emptyList(); + }; + try (SdkMeterProvider meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.create(metricExporter)) + .registerMetricProducer(failingProducer) + .build()) { + + meterProvider.forceFlush().join(10, TimeUnit.SECONDS); + metricExporter.reset(); + // Export again to export the metric reader's self-observability metric. + meterProvider.forceFlush().join(10, TimeUnit.SECONDS); + + assertThat(metricExporter.getFinishedMetricItems()) + .anySatisfy( + m -> + assertThat(m) + .hasName("otel.sdk.metric_reader.collection.duration") + .hasHistogramSatisfying( + h -> + h.hasPointsSatisfying( + p -> + p.hasAttributesSatisfying( + equalTo( + SemConvAttributes.ERROR_TYPE, + "java.lang.IllegalStateException"))))); + } + } }