From 3ea72afb5162eb1c776fd105862d3f79baac2166 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 30 Jan 2026 15:03:23 +0900 Subject: [PATCH 1/6] Add metrics for PeriodicMetricReader --- .../sdk/metrics/SdkMeterProvider.java | 9 ++-- .../export/MetricReaderInstrumentation.java | 49 +++++++++++++++++++ .../metrics/export/PeriodicMetricReader.java | 39 ++++++++++++++- .../internal/SdkMeterProviderUtil.java | 15 ++++++ .../metrics/SdkMeterProviderMetricsTest.java | 49 +++++++++++++++++++ 5 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReaderInstrumentation.java create mode 100644 sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderMetricsTest.java diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java index f9998fc89be..8c1202402e3 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java @@ -19,6 +19,7 @@ import io.opentelemetry.sdk.metrics.export.CollectionRegistration; import io.opentelemetry.sdk.metrics.export.MetricProducer; import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.metrics.internal.MeterConfig; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilterInternal; @@ -94,10 +95,12 @@ public static SdkMeterProviderBuilder builder() { for (RegisteredReader registeredReader : registeredReaders) { List readerMetricProducers = new ArrayList<>(metricProducers); readerMetricProducers.add(new LeasedMetricProducer(registry, sharedState, registeredReader)); - registeredReader - .getReader() - .register(new SdkCollectionRegistration(readerMetricProducers, sharedState)); + MetricReader reader = registeredReader.getReader(); + reader.register(new SdkCollectionRegistration(readerMetricProducers, sharedState)); registeredReader.setLastCollectEpochNanos(startEpochNanos); + if (reader instanceof PeriodicMetricReader) { + SdkMeterProviderUtil.setMeterProvider((PeriodicMetricReader) reader, this); + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReaderInstrumentation.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReaderInstrumentation.java new file mode 100644 index 00000000000..80ef97d324f --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReaderInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.internal.ComponentId; +import io.opentelemetry.sdk.common.internal.SemConvAttributes; +import java.util.Collections; +import javax.annotation.Nullable; + +final class MetricReaderInstrumentation { + + private final DoubleHistogram collectionDuration; + private final Attributes standardAttrs; + + MetricReaderInstrumentation(ComponentId componentId, MeterProvider meterProvider) { + Meter meter = meterProvider.get("io.opentelemetry.sdk.metrics"); + + standardAttrs = + Attributes.of( + SemConvAttributes.OTEL_COMPONENT_TYPE, + componentId.getTypeName(), + SemConvAttributes.OTEL_COMPONENT_NAME, + componentId.getComponentName()); + + collectionDuration = + meter + .histogramBuilder("otel.sdk.metric_reader.collection.duration") + .setUnit("s") + .setDescription("The duration of the collect operation of the metric reader.") + .setExplicitBucketBoundariesAdvice(Collections.emptyList()) + .build(); + } + + void recordCollection(double seconds, @Nullable String error) { + Attributes attrs = standardAttrs; + if (error != null) { + attrs = attrs.toBuilder().put(SemConvAttributes.ERROR_TYPE, error).build(); + } + + collectionDuration.record(seconds, attrs); + } +} 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 6a07b2cbb1e..fec9a491c52 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 @@ -5,8 +5,11 @@ package io.opentelemetry.sdk.metrics.export; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.common.internal.ComponentId; import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.SdkMeterProvider; @@ -34,11 +37,17 @@ public final class PeriodicMetricReader implements MetricReader { private static final Logger logger = Logger.getLogger(PeriodicMetricReader.class.getName()); + private static final Clock CLOCK = Clock.getDefault(); + + private static final ComponentId COMPONENT_ID = + ComponentId.generateLazy("periodic_metric_reader"); + private final MetricExporter exporter; private final long intervalNanos; private final ScheduledExecutorService scheduler; private final Scheduled scheduled; private final Object lock = new Object(); + private volatile CollectionRegistration collectionRegistration = CollectionRegistration.noop(); @Nullable private volatile ScheduledFuture scheduledFuture; @@ -135,6 +144,14 @@ public void register(CollectionRegistration collectionRegistration) { start(); } + /** + * Sets the {@link MeterProvider} to export metrics about this {@link PeriodicMetricReader} to. + * Automatically called by the meter provide the reader is registered to. + */ + void setMeterProvider(MeterProvider meterProvider) { + this.scheduled.setMeterProvider(meterProvider); + } + @Override public String toString() { return "PeriodicMetricReader{" @@ -157,10 +174,18 @@ void start() { } private final class Scheduled implements Runnable { + private final AtomicBoolean exportAvailable = new AtomicBoolean(true); + private MetricReaderInstrumentation instrumentation = + new MetricReaderInstrumentation(COMPONENT_ID, MeterProvider.noop()); + private Scheduled() {} + void setMeterProvider(MeterProvider meterProvider) { + instrumentation = new MetricReaderInstrumentation(COMPONENT_ID, meterProvider); + } + @Override public void run() { // Ignore the CompletableResultCode from doRun() in order to keep run() asynchronous @@ -172,7 +197,18 @@ CompletableResultCode doRun() { CompletableResultCode flushResult = new CompletableResultCode(); if (exportAvailable.compareAndSet(true, false)) { try { - Collection metricData = collectionRegistration.collectAllMetrics(); + long startNanoTime = CLOCK.nanoTime(); + String error = null; + Collection metricData; + try { + metricData = collectionRegistration.collectAllMetrics(); + } catch (Throwable t) { + error = t.getClass().getName(); + throw t; + } finally { + long durationNanos = CLOCK.nanoTime() - startNanoTime; + instrumentation.recordCollection(durationNanos / 1_000_000_000.0, error); + } if (metricData.isEmpty()) { logger.log(Level.FINE, "No metric data to export - skipping export."); flushResult.succeed(); @@ -184,6 +220,7 @@ CompletableResultCode doRun() { if (!result.isSuccess()) { logger.log(Level.FINE, "Exporter failed"); } + flushResult.succeed(); exportAvailable.set(true); }); diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java index 5bec97a8603..e440cb23d97 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java @@ -5,11 +5,13 @@ package io.opentelemetry.sdk.metrics.internal; +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.ViewBuilder; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor; import io.opentelemetry.sdk.metrics.internal.view.StringPredicates; import java.lang.reflect.InvocationTargetException; @@ -76,6 +78,19 @@ public static SdkMeterProviderBuilder addMeterConfiguratorCondition( return sdkMeterProviderBuilder; } + /** Reflectively sets the meter provider for a PeriodicMetricReader to export metrics to. */ + public static void setMeterProvider( + PeriodicMetricReader metricReader, SdkMeterProvider meterProvider) { + try { + Method method = + PeriodicMetricReader.class.getDeclaredMethod("setMeterProvider", MeterProvider.class); + method.setAccessible(true); + method.invoke(metricReader, meterProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Error calling setMeterProvider on PeriodicMetricReader", e); + } + } + /** * Reflectively add an {@link AttributesProcessor} to the {@link ViewBuilder} which appends * key-values from baggage to all measurements. 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 new file mode 100644 index 00000000000..0c88d6a9740 --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderMetricsTest.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class SdkMeterProviderMetricsTest { + @Test + void simple() { + InMemoryMetricExporter metricExporter = InMemoryMetricExporter.create(); + try (SdkMeterProvider meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.create(metricExporter)) + .build()) { + Meter meter = meterProvider.get("test"); + + LongCounter counter = meter.counterBuilder("counter").build(); + + counter.add(1); + + meterProvider.forceFlush().join(10, TimeUnit.SECONDS); + metricExporter.reset(); + // Export again to export the metric reader's metric. + meterProvider.forceFlush().join(10, TimeUnit.SECONDS); + + List metrics = metricExporter.getFinishedMetricItems(); + assertThat(metrics) + .satisfiesExactlyInAnyOrder( + m -> assertThat(m).hasName("counter"), + m -> { + assertThat(m) + .hasName("otel.sdk.metric_reader.collection.duration") + .hasHistogramSatisfying(h -> h.hasPointsSatisfying(p -> p.hasCount(1))); + }); + } + } +} From 6ce0f06825aaad6c31ebff7b50989b31b43e66ea Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 30 Jan 2026 15:05:27 +0900 Subject: [PATCH 2/6] Assert attributes --- .../sdk/metrics/SdkMeterProviderMetricsTest.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 0c88d6a9740..bab3effdfcf 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 @@ -6,9 +6,11 @@ package io.opentelemetry.sdk.metrics; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import io.opentelemetry.api.metrics.LongCounter; 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.PeriodicMetricReader; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; @@ -42,7 +44,18 @@ void simple() { m -> { assertThat(m) .hasName("otel.sdk.metric_reader.collection.duration") - .hasHistogramSatisfying(h -> h.hasPointsSatisfying(p -> p.hasCount(1))); + .hasHistogramSatisfying( + h -> + h.hasPointsSatisfying( + p -> + p.hasCount(1) + .hasAttributesSatisfying( + equalTo( + SemConvAttributes.OTEL_COMPONENT_TYPE, + "periodic_metric_reader"), + equalTo( + SemConvAttributes.OTEL_COMPONENT_NAME, + "periodic_metric_reader/0")))); }); } } From 6ea119d668d28c1abda1c33b1a6990b49209e84d Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 30 Jan 2026 15:06:18 +0900 Subject: [PATCH 3/6] Drift --- .../opentelemetry/sdk/metrics/export/PeriodicMetricReader.java | 1 - 1 file changed, 1 deletion(-) 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 fec9a491c52..45e92927471 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 @@ -220,7 +220,6 @@ CompletableResultCode doRun() { if (!result.isSuccess()) { logger.log(Level.FINE, "Exporter failed"); } - flushResult.succeed(); exportAvailable.set(true); }); From b3915cf6f3b467540eee403d92e11e0abf8f3c71 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 30 Jan 2026 16:15:06 +0900 Subject: [PATCH 4/6] Typo --- .../opentelemetry/sdk/metrics/export/PeriodicMetricReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 45e92927471..2f4cb824023 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 @@ -146,7 +146,7 @@ public void register(CollectionRegistration collectionRegistration) { /** * Sets the {@link MeterProvider} to export metrics about this {@link PeriodicMetricReader} to. - * Automatically called by the meter provide the reader is registered to. + * Automatically called by the meter provider the reader is registered to. */ void setMeterProvider(MeterProvider meterProvider) { this.scheduled.setMeterProvider(meterProvider); From 27b9b9a4f0aff17aa113e5c06b2e770c980dedc9 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 6 Feb 2026 09:30:23 +0900 Subject: [PATCH 5/6] Cleanup --- .../sdk/metrics/export/PeriodicMetricReader.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 2f4cb824023..fe0d87bce94 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 @@ -148,7 +148,8 @@ public void register(CollectionRegistration collectionRegistration) { * Sets the {@link MeterProvider} to export metrics about this {@link PeriodicMetricReader} to. * Automatically called by the meter provider the reader is registered to. */ - void setMeterProvider(MeterProvider meterProvider) { + @SuppressWarnings("UnusedMethod") + private void setMeterProvider(MeterProvider meterProvider) { this.scheduled.setMeterProvider(meterProvider); } @@ -202,9 +203,6 @@ CompletableResultCode doRun() { Collection metricData; try { metricData = collectionRegistration.collectAllMetrics(); - } catch (Throwable t) { - error = t.getClass().getName(); - throw t; } finally { long durationNanos = CLOCK.nanoTime() - startNanoTime; instrumentation.recordCollection(durationNanos / 1_000_000_000.0, error); From bcf2b2ba9731c7cdbf65f54d0106d0601635a058 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 6 Feb 2026 09:40:38 +0900 Subject: [PATCH 6/6] Move method --- .../sdk/metrics/SdkMeterProvider.java | 16 +++++++++++++++- .../metrics/internal/SdkMeterProviderUtil.java | 15 --------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java index 8c1202402e3..a4aa68b5a23 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java @@ -29,6 +29,8 @@ import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry; import io.opentelemetry.sdk.resources.Resource; import java.io.Closeable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -99,7 +101,7 @@ public static SdkMeterProviderBuilder builder() { reader.register(new SdkCollectionRegistration(readerMetricProducers, sharedState)); registeredReader.setLastCollectEpochNanos(startEpochNanos); if (reader instanceof PeriodicMetricReader) { - SdkMeterProviderUtil.setMeterProvider((PeriodicMetricReader) reader, this); + setReaderMeterProvider((PeriodicMetricReader) reader, this); } } } @@ -198,6 +200,18 @@ public String toString() { + "}"; } + private static void setReaderMeterProvider( + PeriodicMetricReader metricReader, SdkMeterProvider meterProvider) { + try { + Method method = + PeriodicMetricReader.class.getDeclaredMethod("setMeterProvider", MeterProvider.class); + method.setAccessible(true); + method.invoke(metricReader, meterProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Error calling setMeterProvider on PeriodicMetricReader", e); + } + } + /** Helper class to expose registered metric exports. */ private static class LeasedMetricProducer implements MetricProducer { diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java index e440cb23d97..5bec97a8603 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java @@ -5,13 +5,11 @@ package io.opentelemetry.sdk.metrics.internal; -import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.ViewBuilder; -import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor; import io.opentelemetry.sdk.metrics.internal.view.StringPredicates; import java.lang.reflect.InvocationTargetException; @@ -78,19 +76,6 @@ public static SdkMeterProviderBuilder addMeterConfiguratorCondition( return sdkMeterProviderBuilder; } - /** Reflectively sets the meter provider for a PeriodicMetricReader to export metrics to. */ - public static void setMeterProvider( - PeriodicMetricReader metricReader, SdkMeterProvider meterProvider) { - try { - Method method = - PeriodicMetricReader.class.getDeclaredMethod("setMeterProvider", MeterProvider.class); - method.setAccessible(true); - method.invoke(metricReader, meterProvider); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException("Error calling setMeterProvider on PeriodicMetricReader", e); - } - } - /** * Reflectively add an {@link AttributesProcessor} to the {@link ViewBuilder} which appends * key-values from baggage to all measurements.