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..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 @@ -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; @@ -28,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; @@ -94,10 +97,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) { + setReaderMeterProvider((PeriodicMetricReader) reader, this); + } } } @@ -195,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/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..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 @@ -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,15 @@ public void register(CollectionRegistration collectionRegistration) { start(); } + /** + * Sets the {@link MeterProvider} to export metrics about this {@link PeriodicMetricReader} to. + * Automatically called by the meter provider the reader is registered to. + */ + @SuppressWarnings("UnusedMethod") + private void setMeterProvider(MeterProvider meterProvider) { + this.scheduled.setMeterProvider(meterProvider); + } + @Override public String toString() { return "PeriodicMetricReader{" @@ -157,10 +175,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 +198,15 @@ 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(); + } 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(); 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..bab3effdfcf --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderMetricsTest.java @@ -0,0 +1,62 @@ +/* + * 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 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; +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) + .hasAttributesSatisfying( + equalTo( + SemConvAttributes.OTEL_COMPONENT_TYPE, + "periodic_metric_reader"), + equalTo( + SemConvAttributes.OTEL_COMPONENT_NAME, + "periodic_metric_reader/0")))); + }); + } + } +}