diff --git a/exporters/otlp-http/metrics/src/test/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterTest.java b/exporters/otlp-http/metrics/src/test/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterTest.java index c51a1dee9b0..12fec0357a9 100644 --- a/exporters/otlp-http/metrics/src/test/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterTest.java +++ b/exporters/otlp-http/metrics/src/test/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterTest.java @@ -153,6 +153,16 @@ void testExportGzipCompressed() { assertThat(parseRequestBody(gzipDecompress(request.content().array()))).isEqualTo(payload); } + @Test + void testExport_flush() { + OtlpHttpMetricExporter exporter = OtlpHttpMetricExporter.builder().build(); + try { + assertThat(exporter.flush().isSuccess()).isTrue(); + } finally { + exporter.shutdown(); + } + } + private static void assertRequestCommon(AggregatedHttpRequest request) { assertThat(request.method()).isEqualTo(HttpMethod.POST); assertThat(request.path()).isEqualTo("/v1/metrics"); diff --git a/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/GrpcGzipBenchmark.java b/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/GrpcGzipBenchmark.java index c59e181649d..13dec001e14 100644 --- a/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/GrpcGzipBenchmark.java +++ b/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/GrpcGzipBenchmark.java @@ -19,6 +19,7 @@ import io.opentelemetry.proto.metrics.v1.ResourceMetrics; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -49,8 +50,10 @@ public class GrpcGzipBenchmark { private static final Codec IDENTITY_CODEC = Codec.Identity.NONE; static { + InMemoryMetricReader metricReader = new InMemoryMetricReader(); SdkMeterProvider meterProvider = SdkMeterProvider.builder() + .registerMetricReader(metricReader) .setResource( Resource.create( Attributes.builder() @@ -118,7 +121,7 @@ public class GrpcGzipBenchmark { histogram.record(3.0); histogram.record(4.0); histogram.record(5.0); - Collection metricData = meterProvider.collectAllMetrics(); + Collection metricData = metricReader.collectAllMetrics(); List resourceMetrics = Arrays.stream(ResourceMetricsMarshaler.create(metricData)) diff --git a/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/MetricsRequestMarshalerBenchmark.java b/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/MetricsRequestMarshalerBenchmark.java index dd58a11a1ac..5217b6b7f20 100644 --- a/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/MetricsRequestMarshalerBenchmark.java +++ b/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/MetricsRequestMarshalerBenchmark.java @@ -16,6 +16,7 @@ import io.opentelemetry.exporter.otlp.internal.metrics.MetricsRequestMarshaler; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -40,8 +41,10 @@ public class MetricsRequestMarshalerBenchmark { private static final Collection METRICS; static { + InMemoryMetricReader metricReader = new InMemoryMetricReader(); SdkMeterProvider meterProvider = SdkMeterProvider.builder() + .registerMetricReader(metricReader) .setResource( Resource.create( Attributes.builder() @@ -110,7 +113,7 @@ public class MetricsRequestMarshalerBenchmark { histogram.record(4.0); histogram.record(5.0); - METRICS = meterProvider.collectAllMetrics(); + METRICS = metricReader.collectAllMetrics(); } @Benchmark diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollector.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollector.java index a34cd12796d..24ce036cbfa 100644 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollector.java +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollector.java @@ -5,14 +5,24 @@ package io.opentelemetry.exporter.prometheus; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.export.MetricReaderFactory; import io.prometheus.client.Collector; +import io.prometheus.client.CollectorRegistry; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; -public final class PrometheusCollector extends Collector { +/** + * A reader of OpenTelemetry metrics that exports into Prometheus as a Collector. + * + *

Usage: sdkMeterProvider.registerMetricReader(PrometheusCollector.create()); + */ +public final class PrometheusCollector extends Collector implements MetricReader { private final MetricProducer metricProducer; PrometheusCollector(MetricProducer metricProducer) { @@ -26,15 +36,39 @@ public List collect() { for (MetricData metricData : allMetrics) { allSamples.add(MetricAdapter.toMetricFamilySamples(metricData)); } - return allSamples; + return Collections.unmodifiableList(allSamples); } /** - * Returns a new builder instance for this exporter. - * - * @return a new builder instance for this exporter. + * Returns a new collector to be registered with a {@link + * io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder}. */ - public static PrometheusCollectorBuilder builder() { - return new PrometheusCollectorBuilder(); + public static MetricReaderFactory create() { + return new Factory(); + } + + // Prometheus cannot flush. + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + CollectorRegistry.defaultRegistry.unregister(this); + return CompletableResultCode.ofSuccess(); + } + + /** Our implementation of the metric reader factory. */ + // NOTE: This should be updated to (optionally) start the simple Http server exposing the metrics + // path. + private static class Factory implements MetricReaderFactory { + @Override + public MetricReader apply(MetricProducer producer) { + PrometheusCollector collector = new PrometheusCollector(producer); + // When SdkMeterProvider constructs us, we register with prometheus. + collector.register(); + return collector; + } } } diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorBuilder.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorBuilder.java deleted file mode 100644 index cf4365afadd..00000000000 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorBuilder.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import io.opentelemetry.sdk.metrics.export.MetricProducer; -import io.prometheus.client.Collector; -import java.util.Objects; -import javax.annotation.Nullable; - -/** Builder for {@link PrometheusCollector}. */ -public class PrometheusCollectorBuilder { - @Nullable private MetricProducer metricProducer; - - PrometheusCollectorBuilder() {} - - /** - * Sets the metric producer for the collector. Required. - * - * @param metricProducer the {@link MetricProducer} to use. - * @return this builder's instance. - */ - public PrometheusCollectorBuilder setMetricProducer(MetricProducer metricProducer) { - this.metricProducer = metricProducer; - return this; - } - - /** - * Constructs a new instance of the {@link Collector} based on the builder's values. - * - * @return a new {@code Collector} based on the builder's values. - */ - public PrometheusCollector build() { - return new PrometheusCollector(Objects.requireNonNull(metricProducer, "metricProducer")); - } - - /** - * Constructs a new instance of the {@link Collector} based on the builder's values and registers - * it to Prometheus {@link io.prometheus.client.CollectorRegistry#defaultRegistry}. - * - * @return a new {@code Collector} based on the builder's values. - */ - public PrometheusCollector buildAndRegister() { - return build().register(); - } -} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorTest.java index 7b1e2775797..d69a40ea274 100644 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorTest.java +++ b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorTest.java @@ -38,8 +38,9 @@ class PrometheusCollectorTest { @BeforeEach void setUp() { - prometheusCollector = - PrometheusCollector.builder().setMetricProducer(metricProducer).buildAndRegister(); + // Apply the SDK metric producer registers with prometheus. + prometheusCollector = new PrometheusCollector(metricProducer); + prometheusCollector.register(); } @Test diff --git a/integration-tests/src/test/java/io/opentelemetry/OtlpExporterIntegrationTest.java b/integration-tests/src/test/java/io/opentelemetry/OtlpExporterIntegrationTest.java index adbce3ba69a..a01c9a57100 100644 --- a/integration-tests/src/test/java/io/opentelemetry/OtlpExporterIntegrationTest.java +++ b/integration-tests/src/test/java/io/opentelemetry/OtlpExporterIntegrationTest.java @@ -42,8 +42,9 @@ import io.opentelemetry.proto.trace.v1.ResourceSpans; import io.opentelemetry.proto.trace.v1.Span.Link; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.MetricReaderFactory; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.IdGenerator; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -131,9 +132,7 @@ void beforeEach() { } @AfterEach - void afterEach() { - IntervalMetricReader.resetGlobalForTest(); - } + void afterEach() {} @Test void testOtlpGrpcTraceExport() { @@ -263,23 +262,22 @@ void testOtlpHttpMetricExport() { } private static void testMetricExport(MetricExporter metricExporter) { - SdkMeterProvider meterProvider = SdkMeterProvider.builder().setResource(RESOURCE).build(); - IntervalMetricReader intervalMetricReader = - IntervalMetricReader.builder() - .setMetricExporter(metricExporter) - .setMetricProducers(Collections.singletonList(meterProvider)) - .setExportIntervalMillis(5000) - .build(); - intervalMetricReader.startAndRegisterGlobal(); + MetricReaderFactory reader = PeriodicMetricReader.create(metricExporter, Duration.ofSeconds(5)); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().setResource(RESOURCE).registerMetricReader(reader).build(); Meter meter = meterProvider.meterBuilder(OtlpExporterIntegrationTest.class.getName()).build(); LongCounter longCounter = meter.counterBuilder("my-counter").build(); longCounter.add(100, Attributes.builder().put("key", "value").build()); - Awaitility.await() - .atMost(Duration.ofSeconds(30)) - .until(() -> grpcServer.metricRequests.size() == 1); + try { + Awaitility.await() + .atMost(Duration.ofSeconds(30)) + .until(() -> grpcServer.metricRequests.size() == 1); + } finally { + meterProvider.close(); + } ExportMetricsServiceRequest request = grpcServer.metricRequests.get(0); assertThat(request.getResourceMetricsCount()).isEqualTo(1); diff --git a/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/FakeMetricExporter.java b/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/FakeMetricExporter.java index 18574721237..e6ea235699c 100644 --- a/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/FakeMetricExporter.java +++ b/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/FakeMetricExporter.java @@ -72,7 +72,7 @@ public CompletableResultCode export(Collection metrics) { @Override public CompletableResultCode flush() { - return null; + return CompletableResultCode.ofSuccess(); } @Override diff --git a/perf-harness/src/test/java/io/opentelemetry/perf/OtlpPipelineStressTest.java b/perf-harness/src/test/java/io/opentelemetry/perf/OtlpPipelineStressTest.java index bc8243fb34c..2b3a2bc5e77 100644 --- a/perf-harness/src/test/java/io/opentelemetry/perf/OtlpPipelineStressTest.java +++ b/perf-harness/src/test/java/io/opentelemetry/perf/OtlpPipelineStressTest.java @@ -21,14 +21,14 @@ import io.opentelemetry.sdk.metrics.data.LongPointData; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.data.PointData; -import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.metrics.testing.InMemoryMetricExporter; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import java.io.IOException; -import java.util.Collections; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -105,7 +105,7 @@ public class OtlpPipelineStressTest { private SdkTracerProvider sdkTracerProvider; private OpenTelemetry openTelemetry; - private IntervalMetricReader intervalMetricReader; + private SdkMeterProvider meterProvider; private Proxy collectorProxy; private ToxiproxyClient toxiproxyClient; @@ -131,7 +131,7 @@ void setUp() throws IOException { @AfterEach void tearDown() throws IOException { - intervalMetricReader.shutdown(); + meterProvider.close(); sdkTracerProvider.shutdown(); toxiproxyClient.reset(); @@ -185,7 +185,7 @@ void oltpExportWithFlakyCollector() throws IOException, InterruptedException { Thread.sleep(10000); List finishedMetricItems = metricExporter.getFinishedMetricItems(); - intervalMetricReader.shutdown(); + meterProvider.close(); Thread.sleep(1000); reportMetrics(finishedMetricItems); Thread.sleep(10000); @@ -247,15 +247,12 @@ private void setupSdk() { .build()); // set up the metric exporter and wire it into the SDK and a timed reader. - SdkMeterProvider meterProvider = - SdkMeterProvider.builder().setResource(resource).buildAndRegisterGlobal(); - - intervalMetricReader = - IntervalMetricReader.builder() - .setMetricExporter(metricExporter) - .setMetricProducers(Collections.singleton(meterProvider)) - .setExportIntervalMillis(1000) - .buildAndStart(); + meterProvider = + SdkMeterProvider.builder() + .setResource(resource) + .registerMetricReader( + PeriodicMetricReader.create(metricExporter, Duration.ofSeconds(1))) + .buildAndRegisterGlobal(); // set up the span exporter and wire it into the SDK OtlpGrpcSpanExporter spanExporter = diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfiguration.java index 3104de408bd..b958d3afcd1 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfiguration.java @@ -16,10 +16,9 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; -import io.opentelemetry.sdk.metrics.export.IntervalMetricReaderBuilder; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.prometheus.client.exporter.HTTPServer; import java.io.IOException; import java.time.Duration; @@ -29,20 +28,20 @@ final class MetricExporterConfiguration { static void configureExporter( - String name, ConfigProperties config, SdkMeterProvider meterProvider) { + String name, ConfigProperties config, SdkMeterProviderBuilder sdkMeterProviderBuilder) { switch (name) { case "otlp": - configureOtlpMetrics(config, meterProvider); + configureOtlpMetrics(config, sdkMeterProviderBuilder); return; case "prometheus": - configurePrometheusMetrics(config, meterProvider); + configurePrometheusMetrics(config, sdkMeterProviderBuilder); return; case "logging": ClasspathUtil.checkClassExists( "io.opentelemetry.exporter.logging.LoggingMetricExporter", "Logging Metrics Exporter", "opentelemetry-exporter-logging"); - configureLoggingMetrics(config, meterProvider); + configureLoggingMetrics(config, sdkMeterProviderBuilder); return; case "none": return; @@ -51,7 +50,7 @@ static void configureExporter( if (spiExporter == null) { throw new ConfigurationException("Unrecognized value for otel.metrics.exporter: " + name); } - configureIntervalMetricReader(config, meterProvider, spiExporter); + configurePeriodicMetricReader(config, sdkMeterProviderBuilder, spiExporter); return; } } @@ -70,15 +69,14 @@ static MetricExporter configureSpiExporter(String name, ConfigProperties config) } private static void configureLoggingMetrics( - ConfigProperties config, SdkMeterProvider meterProvider) { - MetricExporter exporter = new LoggingMetricExporter(); - configureIntervalMetricReader(config, meterProvider, exporter); + ConfigProperties config, SdkMeterProviderBuilder sdkMeterProviderBuilder) { + configurePeriodicMetricReader(config, sdkMeterProviderBuilder, new LoggingMetricExporter()); } // Visible for testing @Nullable static MetricExporter configureOtlpMetrics( - ConfigProperties config, SdkMeterProvider meterProvider) { + ConfigProperties config, SdkMeterProviderBuilder sdkMeterProviderBuilder) { String protocol = OtlpConfigUtil.getOtlpProtocol(DATA_TYPE_METRICS, config); MetricExporter exporter; @@ -132,32 +130,35 @@ static MetricExporter configureOtlpMetrics( throw new ConfigurationException("Unsupported OTLP metrics protocol: " + protocol); } - configureIntervalMetricReader(config, meterProvider, exporter); + configurePeriodicMetricReader(config, sdkMeterProviderBuilder, exporter); return exporter; } - private static void configureIntervalMetricReader( - ConfigProperties config, SdkMeterProvider meterProvider, MetricExporter exporter) { - IntervalMetricReaderBuilder readerBuilder = - IntervalMetricReader.builder() - .setMetricProducers(Collections.singleton(meterProvider)) - .setMetricExporter(exporter); + private static void configurePeriodicMetricReader( + ConfigProperties config, + SdkMeterProviderBuilder sdkMeterProviderBuilder, + MetricExporter exporter) { + Duration exportInterval = config.getDuration("otel.imr.export.interval"); - if (exportInterval != null) { - readerBuilder.setExportIntervalMillis(exportInterval.toMillis()); + if (exportInterval == null) { + exportInterval = Duration.ofMinutes(1); } - IntervalMetricReader reader = readerBuilder.build().startAndRegisterGlobal(); - Runtime.getRuntime().addShutdownHook(new Thread(reader::shutdown)); + // Register the reader (which will start when SDK is built). + // This will shutdown when the SDK is shutdown. + sdkMeterProviderBuilder.registerMetricReader( + PeriodicMetricReader.create(exporter, exportInterval)); } private static void configurePrometheusMetrics( - ConfigProperties config, SdkMeterProvider meterProvider) { + ConfigProperties config, SdkMeterProviderBuilder sdkMeterProviderBuilder) { ClasspathUtil.checkClassExists( "io.opentelemetry.exporter.prometheus.PrometheusCollector", "Prometheus Metrics Server", "opentelemetry-exporter-prometheus"); - PrometheusCollector.builder().setMetricProducer(meterProvider).buildAndRegister(); + sdkMeterProviderBuilder.registerMetricReader(PrometheusCollector.create()); + // TODO: Move this portion into the PrometheusCollector so shutdown on SdkMeterProvider + // will collapse prometheus too? Integer port = config.getInt("otel.exporter.prometheus.port"); if (port == null) { port = 9464; diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfiguration.java index a562e5e6c85..8499176dcfa 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfiguration.java @@ -77,18 +77,6 @@ public static OpenTelemetrySdk initialize(boolean setResultAsGlobal, ConfigPrope } private static void configureMeterProvider(Resource resource, ConfigProperties config) { - String exporterName = config.getString("otel.metrics.exporter"); - if (exporterName == null) { - exporterName = "none"; - } - - if (exporterName.equals("none")) { - // No possiblity of having any metrics exported so no need to have the SDK installed at all. - // NB: If a user wants to add an exporter programatically using SdkMeterProviderConfigurer, - // they will need to use ConfigurableMetricExporter instead. - return; - } - SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder().setResource(resource); // Configure default exemplar filters. @@ -114,9 +102,17 @@ private static void configureMeterProvider(Resource resource, ConfigProperties c configurer.configure(meterProviderBuilder, config); } + String exporterName = config.getString("otel.metrics.exporter"); + if (exporterName == null) { + exporterName = "none"; + } + MetricExporterConfiguration.configureExporter(exporterName, config, meterProviderBuilder); + + // In the event no exporters are configured, this returns a stubbed SdkMeterProvider. SdkMeterProvider meterProvider = meterProviderBuilder.buildAndRegisterGlobal(); - MetricExporterConfiguration.configureExporter(exporterName, config, meterProvider); + // Make sure metrics shut down when JVM shuts down. + Runtime.getRuntime().addShutdownHook(new Thread(meterProvider::close)); } private OpenTelemetrySdkAutoConfiguration() {} diff --git a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/NotOnClasspathTest.java b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/NotOnClasspathTest.java index 8ea760310a0..059a2781bde 100644 --- a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/NotOnClasspathTest.java +++ b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/NotOnClasspathTest.java @@ -84,7 +84,7 @@ void logging_metrics() { assertThatThrownBy( () -> MetricExporterConfiguration.configureExporter( - "logging", EMPTY, SdkMeterProvider.builder().build())) + "logging", EMPTY, SdkMeterProvider.builder())) .isInstanceOf(ConfigurationException.class) .hasMessageContaining( "Logging Metrics Exporter enabled but opentelemetry-exporter-logging not found on " @@ -96,7 +96,7 @@ void otlpGrpcMetrics() { assertThatCode( () -> MetricExporterConfiguration.configureExporter( - "otlp", EMPTY, SdkMeterProvider.builder().build())) + "otlp", EMPTY, SdkMeterProvider.builder())) .doesNotThrowAnyException(); } @@ -108,7 +108,7 @@ void otlpHttpMetrics() { assertThatCode( () -> MetricExporterConfiguration.configureExporter( - "otlp", config, SdkMeterProvider.builder().build())) + "otlp", config, SdkMeterProvider.builder())) .doesNotThrowAnyException(); } @@ -117,7 +117,7 @@ void prometheus() { assertThatThrownBy( () -> MetricExporterConfiguration.configureExporter( - "prometheus", EMPTY, SdkMeterProvider.builder().build())) + "prometheus", EMPTY, SdkMeterProvider.builder())) .isInstanceOf(ConfigurationException.class) .hasMessageContaining( "Prometheus Metrics Server enabled but opentelemetry-exporter-prometheus not found on " diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableMetricExporterTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableMetricExporterTest.java index de45c129ee2..417e8374928 100644 --- a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableMetricExporterTest.java +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableMetricExporterTest.java @@ -33,13 +33,12 @@ void configuration() { @Test void exporterNotFound() { - SdkMeterProvider provider = SdkMeterProvider.builder().build(); assertThatThrownBy( () -> MetricExporterConfiguration.configureExporter( "catExporter", DefaultConfigProperties.createForTest(Collections.emptyMap()), - provider)) + SdkMeterProvider.builder())) .isInstanceOf(ConfigurationException.class) .hasMessageContaining("catExporter"); } diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfigurationTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfigurationTest.java index cf920a4aaf8..81aa48bdf8f 100644 --- a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfigurationTest.java @@ -21,7 +21,7 @@ void configureOtlpMetricsUnsupportedProtocol() { MetricExporterConfiguration.configureOtlpMetrics( DefaultConfigProperties.createForTest( ImmutableMap.of("otel.exporter.otlp.protocol", "foo")), - SdkMeterProvider.builder().build())) + SdkMeterProvider.builder())) .isInstanceOf(ConfigurationException.class) .hasMessageContaining("Unsupported OTLP metrics protocol: foo"); } diff --git a/sdk-extensions/autoconfigure/src/testInitializeRegistersGlobal/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfigurationTest.java b/sdk-extensions/autoconfigure/src/testInitializeRegistersGlobal/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfigurationTest.java index a3cd048160e..d5262b28cae 100644 --- a/sdk-extensions/autoconfigure/src/testInitializeRegistersGlobal/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testInitializeRegistersGlobal/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfigurationTest.java @@ -9,8 +9,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.metrics.GlobalMeterProvider; -import io.opentelemetry.api.metrics.internal.NoopMeterProvider; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,6 +43,10 @@ void initializeAndGet_noGlobal() { @Test void noMetricsSdk() { // OTEL_METRICS_EXPORTER=none so the metrics SDK should be completely disabled. - assertThat(GlobalMeterProvider.get()).isSameAs(NoopMeterProvider.getInstance()); + // This is a bit of an odd test, so we just ensure that we don't have the same impl class as if + // we instantiated an SDK with a reader. + assertThat(GlobalMeterProvider.get()) + .doesNotHaveSameClassAs( + SdkMeterProvider.builder().registerMetricReader(new InMemoryMetricReader()).build()); } } diff --git a/sdk-extensions/autoconfigure/src/testOtlpGrpc/java/io/opentelemetry/sdk/autoconfigure/OtlpGrpcConfigTest.java b/sdk-extensions/autoconfigure/src/testOtlpGrpc/java/io/opentelemetry/sdk/autoconfigure/OtlpGrpcConfigTest.java index f748aa28a44..12d6fe85cb8 100644 --- a/sdk-extensions/autoconfigure/src/testOtlpGrpc/java/io/opentelemetry/sdk/autoconfigure/OtlpGrpcConfigTest.java +++ b/sdk-extensions/autoconfigure/src/testOtlpGrpc/java/io/opentelemetry/sdk/autoconfigure/OtlpGrpcConfigTest.java @@ -35,7 +35,6 @@ import io.opentelemetry.sdk.metrics.data.LongPointData; import io.opentelemetry.sdk.metrics.data.LongSumData; import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.trace.TestSpanData; @@ -120,13 +119,11 @@ void setUp() { metricRequests.clear(); requestHeaders.clear(); GlobalOpenTelemetry.resetForTest(); - IntervalMetricReader.resetGlobalForTest(); } @AfterEach public void tearDown() { GlobalOpenTelemetry.resetForTest(); - IntervalMetricReader.resetGlobalForTest(); } @Test @@ -141,8 +138,7 @@ void configureExportersGeneral() { SpanExporter spanExporter = SpanExporterConfiguration.configureExporter("otlp", properties, Collections.emptyMap()); MetricExporter metricExporter = - MetricExporterConfiguration.configureOtlpMetrics( - properties, SdkMeterProvider.builder().build()); + MetricExporterConfiguration.configureOtlpMetrics(properties, SdkMeterProvider.builder()); assertThat(spanExporter).extracting("timeoutNanos").isEqualTo(TimeUnit.SECONDS.toNanos(15)); assertThat( @@ -231,7 +227,7 @@ public void configureMetricExporter() { props.put("otel.exporter.otlp.metrics.timeout", "15s"); MetricExporter metricExporter = MetricExporterConfiguration.configureOtlpMetrics( - DefaultConfigProperties.createForTest(props), SdkMeterProvider.builder().build()); + DefaultConfigProperties.createForTest(props), SdkMeterProvider.builder()); assertThat(metricExporter).extracting("timeoutNanos").isEqualTo(TimeUnit.SECONDS.toNanos(15)); assertThat( @@ -266,7 +262,7 @@ void configureTlsInvalidCertificatePath() { assertThatThrownBy( () -> MetricExporterConfiguration.configureOtlpMetrics( - properties, SdkMeterProvider.builder().build())) + properties, SdkMeterProvider.builder())) .isInstanceOf(ConfigurationException.class) .hasMessageContaining("Invalid OTLP certificate path:"); } diff --git a/sdk-extensions/autoconfigure/src/testOtlpHttp/java/io/opentelemetry/sdk/autoconfigure/OtlpHttpConfigTest.java b/sdk-extensions/autoconfigure/src/testOtlpHttp/java/io/opentelemetry/sdk/autoconfigure/OtlpHttpConfigTest.java index 7879df460c7..97e6c5ee2d3 100644 --- a/sdk-extensions/autoconfigure/src/testOtlpHttp/java/io/opentelemetry/sdk/autoconfigure/OtlpHttpConfigTest.java +++ b/sdk-extensions/autoconfigure/src/testOtlpHttp/java/io/opentelemetry/sdk/autoconfigure/OtlpHttpConfigTest.java @@ -33,7 +33,6 @@ import io.opentelemetry.sdk.metrics.data.LongPointData; import io.opentelemetry.sdk.metrics.data.LongSumData; import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.trace.TestSpanData; @@ -151,13 +150,11 @@ void setUp() { metricRequests.clear(); requestHeaders.clear(); GlobalOpenTelemetry.resetForTest(); - IntervalMetricReader.resetGlobalForTest(); } @AfterEach public void tearDown() { GlobalOpenTelemetry.resetForTest(); - IntervalMetricReader.resetGlobalForTest(); } @Test @@ -174,8 +171,7 @@ void configureExportersGeneral() { SpanExporter spanExporter = SpanExporterConfiguration.configureExporter("otlp", properties, Collections.emptyMap()); MetricExporter metricExporter = - MetricExporterConfiguration.configureOtlpMetrics( - properties, SdkMeterProvider.builder().build()); + MetricExporterConfiguration.configureOtlpMetrics(properties, SdkMeterProvider.builder()); assertThat(spanExporter) .extracting("client", as(InstanceOfAssertFactories.type(OkHttpClient.class))) @@ -273,7 +269,7 @@ public void configureMetricExporter() { props.put("otel.exporter.otlp.metrics.timeout", "15s"); MetricExporter metricExporter = MetricExporterConfiguration.configureOtlpMetrics( - DefaultConfigProperties.createForTest(props), SdkMeterProvider.builder().build()); + DefaultConfigProperties.createForTest(props), SdkMeterProvider.builder()); assertThat(metricExporter) .extracting("client", as(InstanceOfAssertFactories.type(OkHttpClient.class))) @@ -310,7 +306,7 @@ void configureTlsInvalidCertificatePath() { assertThatThrownBy( () -> MetricExporterConfiguration.configureOtlpMetrics( - properties, SdkMeterProvider.builder().build())) + properties, SdkMeterProvider.builder())) .isInstanceOf(ConfigurationException.class) .hasMessageContaining("Invalid OTLP certificate path:"); } diff --git a/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorCpuBenchmark.java b/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorCpuBenchmark.java index 11170b62503..f423fb99e54 100644 --- a/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorCpuBenchmark.java +++ b/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorCpuBenchmark.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -38,7 +38,7 @@ public class ExecutorServiceSpanProcessorCpuBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { - private MetricProducer collector; + private InMemoryMetricReader metricReader; private ExecutorServiceSpanProcessor processor; private Tracer tracer; private int numThreads = 1; @@ -51,9 +51,8 @@ public static class BenchmarkState { @Setup(Level.Iteration) public final void setup() { - final SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); - // Note: these will (likely) no longer be the same in future SDK. - collector = sdkMeterProvider; + metricReader = new InMemoryMetricReader(); + SdkMeterProvider.builder().registerMetricReader(metricReader).buildAndRegisterGlobal(); SpanExporter exporter = new DelayingSpanExporter(delayMs); ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); @@ -64,7 +63,7 @@ public final void setup() { @TearDown(Level.Iteration) public final void recordMetrics() { BatchSpanProcessorMetrics metrics = - new BatchSpanProcessorMetrics(collector.collectAllMetrics(), numThreads); + new BatchSpanProcessorMetrics(metricReader.collectAllMetrics(), numThreads); exportedSpans = metrics.exportedSpans(); droppedSpans = metrics.droppedSpans(); } diff --git a/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorDroppedSpansBenchmark.java b/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorDroppedSpansBenchmark.java index eb6d760de4d..4fff08a5536 100644 --- a/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorDroppedSpansBenchmark.java +++ b/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorDroppedSpansBenchmark.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -31,7 +31,7 @@ public class ExecutorServiceSpanProcessorDroppedSpansBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { - private MetricProducer collector; + private InMemoryMetricReader metricReader; private ExecutorServiceSpanProcessor processor; private Tracer tracer; private double dropRatio; @@ -41,9 +41,8 @@ public static class BenchmarkState { @Setup(Level.Iteration) public final void setup() { - final SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); - // Note: these will (likely) no longer be the same in future SDK. - collector = sdkMeterProvider; + metricReader = new InMemoryMetricReader(); + SdkMeterProvider.builder().registerMetricReader(metricReader).buildAndRegisterGlobal(); SpanExporter exporter = new DelayingSpanExporter(0); ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); @@ -54,7 +53,7 @@ public final void setup() { @TearDown(Level.Iteration) public final void recordMetrics() { BatchSpanProcessorMetrics metrics = - new BatchSpanProcessorMetrics(collector.collectAllMetrics(), numThreads); + new BatchSpanProcessorMetrics(metricReader.collectAllMetrics(), numThreads); dropRatio = metrics.dropRatio(); exportedSpans = metrics.exportedSpans(); droppedSpans = metrics.droppedSpans(); diff --git a/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorMultiThreadBenchmark.java b/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorMultiThreadBenchmark.java index 9681a47d1f7..e64340c0359 100644 --- a/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorMultiThreadBenchmark.java +++ b/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorMultiThreadBenchmark.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -35,7 +35,7 @@ public class ExecutorServiceSpanProcessorMultiThreadBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { - private MetricProducer collector; + private InMemoryMetricReader metricReader; private ExecutorServiceSpanProcessor processor; private Tracer tracer; private int numThreads = 1; @@ -48,9 +48,8 @@ public static class BenchmarkState { @Setup(Level.Iteration) public final void setup() { - final SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); - // Note: these will (likely) no longer be the same in future SDK. - collector = sdkMeterProvider; + metricReader = new InMemoryMetricReader(); + SdkMeterProvider.builder().registerMetricReader(metricReader).buildAndRegisterGlobal(); SpanExporter exporter = new DelayingSpanExporter(delayMs); ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); @@ -61,7 +60,7 @@ public final void setup() { @TearDown(Level.Iteration) public final void recordMetrics() { BatchSpanProcessorMetrics metrics = - new BatchSpanProcessorMetrics(collector.collectAllMetrics(), numThreads); + new BatchSpanProcessorMetrics(metricReader.collectAllMetrics(), numThreads); exportedSpans = metrics.exportedSpans(); droppedSpans = metrics.droppedSpans(); } diff --git a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java index 35d540aec59..4ba9f9ca78f 100644 --- a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java +++ b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.metrics.exemplar.ExemplarFilter; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.samplers.Sampler; @@ -30,6 +31,8 @@ Meter build() { return SdkMeterProvider.builder() .setClock(Clock.getDefault()) .setResource(Resource.empty()) + // Must register reader for real SDK. + .registerMetricReader(new InMemoryMetricReader()) .setExemplarFilter(ExemplarFilter.neverSample()) .build() .get("io.opentelemetry.sdk.metrics"); @@ -42,6 +45,8 @@ Meter build() { return SdkMeterProvider.builder() .setClock(Clock.getDefault()) .setResource(Resource.empty()) + // Must register reader for real SDK. + .registerMetricReader(new InMemoryMetricReader()) .build() .get("io.opentelemetry.sdk.metrics"); } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DefaultSdkMeterProvider.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DefaultSdkMeterProvider.java new file mode 100644 index 00000000000..ba075e08246 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DefaultSdkMeterProvider.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.exemplar.ExemplarFilter; +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.export.MetricReaderFactory; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState; +import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Default implementation for {@link SdkMeterProvider}. + * + *

This class is not intended to be used in application code and it is used only by {@link + * OpenTelemetry}. + */ +final class DefaultSdkMeterProvider implements SdkMeterProvider { + + private static final Logger LOGGER = Logger.getLogger(DefaultSdkMeterProvider.class.getName()); + static final String DEFAULT_METER_NAME = "unknown"; + + private final ComponentRegistry registry; + private final MeterProviderSharedState sharedState; + private final Set collectors; + private final List readers; + private final AtomicBoolean isClosed = new AtomicBoolean(false); + + DefaultSdkMeterProvider( + List readerFactories, + Clock clock, + Resource resource, + ViewRegistry viewRegistry, + ExemplarFilter exemplarSampler) { + this.sharedState = + MeterProviderSharedState.create(clock, resource, viewRegistry, exemplarSampler); + this.registry = + new ComponentRegistry<>( + instrumentationLibraryInfo -> new SdkMeter(sharedState, instrumentationLibraryInfo)); + + // Here we construct our own unique handle ids for this SDK. + // These are guaranteed to be unique per-reader for this SDK, and only this SDK. + // These are *only* mutated in our constructor, and safe to use concurrently after construction. + collectors = CollectionHandle.mutableSet(); + readers = new ArrayList<>(); + Supplier handleSupplier = CollectionHandle.createSupplier(); + for (MetricReaderFactory readerFactory : readerFactories) { + CollectionHandle handle = handleSupplier.get(); + // TODO: handle failure in creation or just crash? + MetricReader reader = readerFactory.apply(new LeasedMetricProducer(handle)); + collectors.add(handle); + readers.add(reader); + } + } + + @Override + public MeterBuilder meterBuilder(@Nullable String instrumentationName) { + if (instrumentationName == null || instrumentationName.isEmpty()) { + LOGGER.fine("Meter requested without instrumentation name."); + instrumentationName = DEFAULT_METER_NAME; + } + return new SdkMeterBuilder(registry, instrumentationName); + } + + @Override + public CompletableResultCode forceFlush() { + List results = new ArrayList<>(); + for (MetricReader reader : readers) { + results.add(reader.flush()); + } + return CompletableResultCode.ofAll(results); + } + + @Override + public CompletableResultCode close() { + if (isClosed.compareAndSet(false, true)) { + LOGGER.info("Multiple close calls"); + return CompletableResultCode.ofSuccess(); + } + List results = new ArrayList<>(); + for (MetricReader reader : readers) { + results.add(reader.shutdown()); + } + return CompletableResultCode.ofAll(results); + } + + @Override + public CompletableResultCode shutdown() { + return close(); + } + + /** Helper class to expose registered metric exports. */ + private class LeasedMetricProducer implements MetricProducer { + private final CollectionHandle handle; + + LeasedMetricProducer(CollectionHandle handle) { + this.handle = handle; + } + + @Override + public Collection collectAllMetrics() { + Collection meters = registry.getComponents(); + // TODO: This can be made more efficient by passing the list through the collection and + // appending + // rather than allocating individual lists and concatenating. + List result = new ArrayList<>(meters.size()); + for (SdkMeter meter : meters) { + result.addAll(meter.collectAll(handle, collectors, sharedState.getClock().now())); + } + return Collections.unmodifiableCollection(result); + } + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/NoopSdkMeterProvider.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/NoopSdkMeterProvider.java new file mode 100644 index 00000000000..a35f05a42ab --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/NoopSdkMeterProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; + +/** Implementation of SdkMeterProvider which does not collect metrics. */ +final class NoopSdkMeterProvider implements SdkMeterProvider { + + @Override + public MeterBuilder meterBuilder(String instrumentationName) { + return MeterProvider.noop().meterBuilder(instrumentationName); + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode close() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return close(); + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java index 70ef1f05362..7bdeaa421e8 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java @@ -12,9 +12,11 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState; import io.opentelemetry.sdk.metrics.internal.state.MeterSharedState; import java.util.Collection; +import java.util.Set; /** {@link SdkMeter} is SDK implementation of {@link Meter}. */ final class SdkMeter implements Meter { @@ -34,8 +36,10 @@ InstrumentationLibraryInfo getInstrumentationLibraryInfo() { } /** Collects all the metric recordings that changed since the previous call. */ - Collection collectAll(long epochNanos) { - return meterSharedState.collectAll(meterProviderSharedState, epochNanos); + Collection collectAll( + CollectionHandle collector, Set allCollectors, long epochNanos) { + return meterSharedState.collectAll( + collector, allCollectors, meterProviderSharedState, epochNanos); } @Override 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 30391473a53..81524028567 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 @@ -5,76 +5,32 @@ package io.opentelemetry.sdk.metrics; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.metrics.MeterBuilder; import io.opentelemetry.api.metrics.MeterProvider; -import io.opentelemetry.sdk.common.Clock; -import io.opentelemetry.sdk.internal.ComponentRegistry; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.exemplar.ExemplarFilter; -import io.opentelemetry.sdk.metrics.export.MetricProducer; -import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState; -import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry; -import io.opentelemetry.sdk.resources.Resource; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.logging.Logger; -import javax.annotation.Nullable; +import io.opentelemetry.sdk.common.CompletableResultCode; /** - * {@code SdkMeterProvider} implementation for {@link MeterProvider}. + * {@code SdkMeterProvider} provides SDK extensions for {@link MeterProvider}. * *

This class is not intended to be used in application code and it is used only by {@link - * OpenTelemetry}. - * - *

WARNING: A MetricProducer is stateful. It will only return changes since the last time it was - * accessed. This means that if more than one {@link - * io.opentelemetry.sdk.metrics.export.MetricExporter} has a handle to this MetricProducer, the two - * exporters will not receive copies of the same metric data to export. + * io.opentelemetry.api.OpenTelemetry}. */ -public final class SdkMeterProvider implements MeterProvider, MetricProducer { +public interface SdkMeterProvider extends MeterProvider { - private static final Logger LOGGER = Logger.getLogger(SdkMeterProvider.class.getName()); - static final String DEFAULT_METER_NAME = "unknown"; - private final ComponentRegistry registry; - private final MeterProviderSharedState sharedState; + /** Forces metric readers to immediately read metrics, if able. */ + CompletableResultCode forceFlush(); - SdkMeterProvider( - Clock clock, Resource resource, ViewRegistry viewRegistry, ExemplarFilter exemplarFilter) { - this.sharedState = - MeterProviderSharedState.create(clock, resource, viewRegistry, exemplarFilter); - this.registry = - new ComponentRegistry<>( - instrumentationLibraryInfo -> new SdkMeter(sharedState, instrumentationLibraryInfo)); - } + /** Shuts down metric collection and all associated metric readers. */ + CompletableResultCode close(); - @Override - public MeterBuilder meterBuilder(@Nullable String instrumentationName) { - if (instrumentationName == null || instrumentationName.isEmpty()) { - LOGGER.fine("Meter requested without instrumentation name."); - instrumentationName = DEFAULT_METER_NAME; - } - return new SdkMeterBuilder(registry, instrumentationName); - } - - @Override - public Collection collectAllMetrics() { - Collection meters = registry.getComponents(); - List result = new ArrayList<>(meters.size()); - for (SdkMeter meter : meters) { - result.addAll(meter.collectAll(sharedState.getClock().now())); - } - return Collections.unmodifiableCollection(result); - } + /** Shuts down metric collection and all associated metric readers. */ + CompletableResultCode shutdown(); /** * Returns a new {@link SdkMeterProviderBuilder} for {@link SdkMeterProvider}. * * @return a new {@link SdkMeterProviderBuilder} for {@link SdkMeterProvider}. */ - public static SdkMeterProviderBuilder builder() { + static SdkMeterProviderBuilder builder() { return new SdkMeterProviderBuilder(); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java index 5bdf9c8dff9..20cb4ae3ab6 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java @@ -8,11 +8,15 @@ import io.opentelemetry.api.metrics.GlobalMeterProvider; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.metrics.exemplar.ExemplarFilter; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.export.MetricReaderFactory; import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry; import io.opentelemetry.sdk.metrics.internal.view.ViewRegistryBuilder; import io.opentelemetry.sdk.metrics.view.InstrumentSelector; import io.opentelemetry.sdk.metrics.view.View; import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -24,6 +28,7 @@ public final class SdkMeterProviderBuilder { private Clock clock = Clock.getDefault(); private Resource resource = Resource.getDefault(); private final ViewRegistryBuilder viewRegistryBuilder = ViewRegistry.builder(); + private final List metricReaders = new ArrayList<>(); // Default the sampling strategy. private ExemplarFilter exemplarFilter = ExemplarFilter.sampleWithTraces(); @@ -107,6 +112,17 @@ public SdkMeterProvider buildAndRegisterGlobal() { return meterProvider; } + /** + * Registers a {@link MetricReader} for this SDK. + * + * @param reader The factory for a reader of metrics. + * @return this + */ + public SdkMeterProviderBuilder registerMetricReader(MetricReaderFactory reader) { + metricReaders.add(reader); + return this; + } + /** * Returns a new {@link SdkMeterProvider} built with the configuration of this {@link * SdkMeterProviderBuilder}. This provider is not registered as the global {@link @@ -118,6 +134,11 @@ public SdkMeterProvider buildAndRegisterGlobal() { * @see GlobalMeterProvider */ public SdkMeterProvider build() { - return new SdkMeterProvider(clock, resource, viewRegistryBuilder.build(), exemplarFilter); + // If no exporters are configured, optimize by returning no-op implementation. + if (metricReaders.isEmpty()) { + return new NoopSdkMeterProvider(); + } + return new DefaultSdkMeterProvider( + metricReaders, clock, resource, viewRegistryBuilder.build(), exemplarFilter); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReader.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReader.java deleted file mode 100644 index 754a01a9e0d..00000000000 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReader.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.metrics.export; - -import com.google.auto.value.AutoValue; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.internal.DaemonThreadFactory; -import io.opentelemetry.sdk.metrics.data.MetricData; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; - -/** - * Wraps a list of {@link MetricProducer}s and automatically reads and exports the metrics every - * export interval. Metrics may also be dropped when it becomes time to export again, and there is - * an export in progress. - */ -public final class IntervalMetricReader { - private static final Logger logger = Logger.getLogger(IntervalMetricReader.class.getName()); - - private static final AtomicReference globalIntervalMetricReader = - new AtomicReference<>(); - - private final Exporter exporter; - private final ScheduledExecutorService scheduler; - - @Nullable private volatile ScheduledFuture scheduledFuture; - private final Object lock = new Object(); - - /** - * {@linkplain IntervalMetricReader#forceFlush() Force flushes} the globally registered {@link - * IntervalMetricReader} if available, or does nothing otherwise. - */ - public static CompletableResultCode forceFlushGlobal() { - IntervalMetricReader intervalMetricReader = globalIntervalMetricReader.get(); - if (intervalMetricReader != null) { - return intervalMetricReader.forceFlush(); - } - return CompletableResultCode.ofSuccess(); - } - - /** - * Resets the globally registered {@link IntervalMetricReader} if available, or does nothing - * otherwise. This is only meant to be used from tests which need to reconfigure {@link - * IntervalMetricReader}. - */ - public static void resetGlobalForTest() { - IntervalMetricReader intervalMetricReader = globalIntervalMetricReader.get(); - if (intervalMetricReader != null) { - intervalMetricReader.shutdown(); - } - globalIntervalMetricReader.set(null); - } - - /** Stops the scheduled task and calls export one more time. */ - public CompletableResultCode shutdown() { - final CompletableResultCode result = new CompletableResultCode(); - ScheduledFuture scheduledFuture = this.scheduledFuture; - if (scheduledFuture != null) { - scheduledFuture.cancel(false); - } - scheduler.shutdown(); - try { - scheduler.awaitTermination(5, TimeUnit.SECONDS); - final CompletableResultCode flushResult = exporter.doRun(); - flushResult.join(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - // force a shutdown if the export hasn't finished. - scheduler.shutdownNow(); - // reset the interrupted status - Thread.currentThread().interrupt(); - } finally { - final CompletableResultCode shutdownResult = exporter.shutdown(); - shutdownResult.whenComplete( - () -> { - if (!shutdownResult.isSuccess()) { - result.fail(); - } else { - result.succeed(); - } - }); - } - return result; - } - - /** - * Returns a new {@link IntervalMetricReaderBuilder} for {@link IntervalMetricReader}. - * - * @return a new {@link IntervalMetricReaderBuilder} for {@link IntervalMetricReader}. - */ - public static IntervalMetricReaderBuilder builder() { - return new IntervalMetricReaderBuilder(InternalState.builder()); - } - - /** - * Requests the {@link IntervalMetricReader} to export current metrics and returns a {@link - * CompletableResultCode} which is completed when the flush is finished. - */ - public CompletableResultCode forceFlush() { - return exporter.doRun(); - } - - IntervalMetricReader(InternalState internalState) { - this( - internalState, - Executors.newScheduledThreadPool(1, new DaemonThreadFactory("IntervalMetricReader"))); - } - - // visible for testing - IntervalMetricReader(InternalState internalState, ScheduledExecutorService intervalMetricReader) { - this.exporter = new Exporter(internalState); - this.scheduler = intervalMetricReader; - } - - /** Starts this {@link IntervalMetricReader} to report to the configured exporter. */ - public IntervalMetricReader start() { - synchronized (lock) { - if (scheduledFuture != null) { - return this; - } - scheduledFuture = - scheduler.scheduleAtFixedRate( - exporter, - exporter.internalState.getExportIntervalMillis(), - exporter.internalState.getExportIntervalMillis(), - TimeUnit.MILLISECONDS); - return this; - } - } - - /** - * Starts this {@link IntervalMetricReader} and registers it as the global {@link - * IntervalMetricReader}. - */ - public IntervalMetricReader startAndRegisterGlobal() { - start(); - if (!globalIntervalMetricReader.compareAndSet(null, this)) { - logger.log(Level.WARNING, "Global IntervalMetricReader already registered, ignoring."); - } - return this; - } - - private static final class Exporter implements Runnable { - - private final InternalState internalState; - private final AtomicBoolean exportAvailable = new AtomicBoolean(true); - - private Exporter(InternalState internalState) { - this.internalState = internalState; - } - - @Override - public void run() { - // Ignore the CompletableResultCode from doRun() in order to keep run() asynchronous - doRun(); - } - - CompletableResultCode doRun() { - final CompletableResultCode flushResult = new CompletableResultCode(); - if (exportAvailable.compareAndSet(true, false)) { - try { - List metricsList = new ArrayList<>(); - for (MetricProducer metricProducer : internalState.getMetricProducers()) { - metricsList.addAll(metricProducer.collectAllMetrics()); - } - final CompletableResultCode result = - internalState.getMetricExporter().export(Collections.unmodifiableList(metricsList)); - result.whenComplete( - () -> { - if (!result.isSuccess()) { - logger.log(Level.FINE, "Exporter failed"); - } - flushResult.succeed(); - exportAvailable.set(true); - }); - } catch (Throwable t) { - exportAvailable.set(true); - logger.log(Level.WARNING, "Exporter threw an Exception", t); - flushResult.fail(); - } - } else { - logger.log(Level.FINE, "Exporter busy. Dropping metrics."); - flushResult.fail(); - } - return flushResult; - } - - CompletableResultCode shutdown() { - return internalState.getMetricExporter().shutdown(); - } - } - - @AutoValue - @Immutable - abstract static class InternalState { - static final long DEFAULT_INTERVAL_MILLIS = 60_000; - - abstract MetricExporter getMetricExporter(); - - abstract long getExportIntervalMillis(); - - abstract Collection getMetricProducers(); - - static Builder builder() { - return new AutoValue_IntervalMetricReader_InternalState.Builder() - .setExportIntervalMillis(DEFAULT_INTERVAL_MILLIS); - } - - @AutoValue.Builder - abstract static class Builder { - - abstract Builder setExportIntervalMillis(long exportIntervalMillis); - - abstract Builder setMetricExporter(MetricExporter metricExporter); - - abstract Builder setMetricProducers(Collection metricProducers); - - abstract InternalState build(); - } - } -} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderBuilder.java deleted file mode 100644 index 7fd38332902..00000000000 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderBuilder.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.metrics.export; - -import io.opentelemetry.api.internal.Utils; -import java.util.Collection; - -/** Builder for {@link IntervalMetricReader}. */ -public final class IntervalMetricReaderBuilder { - private final IntervalMetricReader.InternalState.Builder optionsBuilder; - - IntervalMetricReaderBuilder(IntervalMetricReader.InternalState.Builder optionsBuilder) { - this.optionsBuilder = optionsBuilder; - } - - /** - * Sets the export interval. - * - * @param exportIntervalMillis the export interval between pushes to the exporter. - * @return this. - */ - public IntervalMetricReaderBuilder setExportIntervalMillis(long exportIntervalMillis) { - optionsBuilder.setExportIntervalMillis(exportIntervalMillis); - return this; - } - - /** - * Sets the exporter to be called when export metrics. - * - * @param metricExporter the {@link MetricExporter} to be called when export metrics. - * @return this. - */ - public IntervalMetricReaderBuilder setMetricExporter(MetricExporter metricExporter) { - optionsBuilder.setMetricExporter(metricExporter); - return this; - } - - /** - * Sets a collection of {@link MetricProducer} from where the metrics should be read. - * - * @param metricProducers a collection of {@link MetricProducer} from where the metrics should be - * read. - * @return this. - */ - public IntervalMetricReaderBuilder setMetricProducers( - Collection metricProducers) { - optionsBuilder.setMetricProducers(metricProducers); - return this; - } - - /** - * Builds a new {@link IntervalMetricReader} with current settings. Does not start the background - * thread. Please call {@link IntervalMetricReader#start()} to do that. - * - * @return a {@code IntervalMetricReader}. - */ - public IntervalMetricReader build() { - IntervalMetricReader.InternalState internalState = optionsBuilder.build(); - Utils.checkArgument( - internalState.getExportIntervalMillis() > 0, "Export interval must be positive"); - - return new IntervalMetricReader(internalState); - } - - /** - * Builds a new {@link IntervalMetricReader} with current settings and starts the background - * thread running. - * - * @return a {@code IntervalMetricReader}. - */ - public IntervalMetricReader buildAndStart() { - return build().start(); - } -} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricExporter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricExporter.java index baf401b397e..8060e25f8b6 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricExporter.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricExporter.java @@ -19,8 +19,8 @@ public interface MetricExporter { /** * Exports the collection of given {@link MetricData}. Note that export operations can be - * performed simultaneously depending on the type of metric reader being used. However, the {@link - * IntervalMetricReader} will ensure that only one export can occur at a time. + * performed simultaneously depending on the type of metric reader being used. However, the caller + * MUST ensure that only one export can occur at a time. * * @param metrics the collection of {@link MetricData} to be exported. * @return the result of the export, which is often an asynchronous operation. @@ -30,7 +30,7 @@ public interface MetricExporter { /** * Exports the collection of {@link MetricData} that have not yet been exported. Note that flush * operations can be performed simultaneously depending on the type of metric reader being used. - * However, the {@link IntervalMetricReader} will ensure that only one export can occur at a time. + * However, the caller MUST ensure that only one export can occur at a time. * * @return the result of the flush, which is often an asynchronous operation. */ diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReader.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReader.java new file mode 100644 index 00000000000..0d42597b43c --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReader.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; + +/** + * A registered reader of metrics. + * + *

This interface provides the {@link io.opentelemetry.sdk.metrics.SdkMeterProvider} a mechanism + * of global control over metrics during shutdown or memory pressure scenarios. + */ +public interface MetricReader { + + /** + * Flushes metrics read by this reader. + * + *

In all scenarios, the associated {@link MetricProducer} should have its {@link + * MetricProducer#collectAllMetrics()} method called. + * + *

For push endpoints, this should collect and report metrics as normal. + * + * @return the result of the shutdown. + */ + CompletableResultCode flush(); + + /** + * Shuts down the metric reader. + * + *

For pull endpoints, like prometheus, this should shut down the metric hosting endpoint or + * server doing such a job. + * + *

For push endpoints, this should shut down any scheduler threads. + * + * @return the result of the shutdown. + */ + CompletableResultCode shutdown(); +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReaderFactory.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReaderFactory.java new file mode 100644 index 00000000000..028d3937a5a --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricReaderFactory.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +/** A constructor of {@link MetricReader}s. */ +public interface MetricReaderFactory { + /** + * Construct a new MetricReader. + * + * @param producer the mechanism of reading SDK metrics. + * @return a controller for this metric reader. + */ + MetricReader apply(MetricProducer producer); +} 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 new file mode 100644 index 00000000000..bf9f35b7b28 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java @@ -0,0 +1,177 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import java.time.Duration; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Wraps a {@link MetricExporter} and automatically reads and exports the metrics every export + * interval. Metrics may also be dropped when it becomes time to export again, and there is an + * export in progress. + */ +public class PeriodicMetricReader implements MetricReader { + private static final Logger logger = Logger.getLogger(PeriodicMetricReader.class.getName()); + + private final MetricProducer producer; + private final MetricExporter exporter; + private final ScheduledExecutorService scheduler; + private final Scheduled scheduled; + private final Object lock = new Object(); + + @Nullable private volatile ScheduledFuture scheduledFuture; + + /** + * Builds a factory that will register and start a PeriodicMetricReader. + * + *

This will export once every 5 minutes. + * + *

This will spin up a new daemon thread to schedule the export on. + * + * @param exporter The exporter receiving metrics. + */ + public static MetricReaderFactory create(MetricExporter exporter) { + return create(exporter, Duration.ofMinutes(5)); + } + + /** + * Builds a factory that will register and start a PeriodicMetricReader. + * + *

This will spin up a new daemon thread to schedule the export on. + * + * @param exporter The exporter receiving metrics. + * @param duration The duration (interval) between metric export calls. + */ + public static MetricReaderFactory create(MetricExporter exporter, Duration duration) { + return create( + exporter, + duration, + Executors.newScheduledThreadPool(1, new DaemonThreadFactory("PeriodicMetricReader"))); + } + + /** + * Builds a factory that will register and start a PeriodicMetricReader. + * + * @param exporter The exporter receiving metrics. + * @param duration The duration (interval) between metric export calls. + * @param scheduler The service to schedule export work. + */ + public static MetricReaderFactory create( + MetricExporter exporter, Duration duration, ScheduledExecutorService scheduler) { + return new PeriodicMetricReaderFactory(exporter, duration, scheduler); + } + + PeriodicMetricReader( + MetricProducer producer, MetricExporter exporter, ScheduledExecutorService scheduler) { + this.producer = producer; + this.exporter = exporter; + this.scheduler = scheduler; + this.scheduled = new Scheduled(); + } + + @Override + public CompletableResultCode flush() { + return scheduled.doRun(); + } + + @Override + public CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + ScheduledFuture scheduledFuture = this.scheduledFuture; + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + } + scheduler.shutdown(); + try { + scheduler.awaitTermination(5, TimeUnit.SECONDS); + final CompletableResultCode flushResult = scheduled.doRun(); + flushResult.join(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // force a shutdown if the export hasn't finished. + scheduler.shutdownNow(); + // reset the interrupted status + Thread.currentThread().interrupt(); + } finally { + final CompletableResultCode shutdownResult = scheduled.shutdown(); + shutdownResult.whenComplete( + () -> { + if (!shutdownResult.isSuccess()) { + result.fail(); + } else { + result.succeed(); + } + }); + } + return result; + } + + void start(Duration duration) { + // Autoconfigure sends us null or 0 durations and expects us to just not start. + if (duration == null || duration.isZero()) { + return; + } + + synchronized (lock) { + if (scheduledFuture != null) { + return; + } + scheduledFuture = + scheduler.scheduleAtFixedRate( + scheduled, duration.toMillis(), duration.toMillis(), TimeUnit.MILLISECONDS); + } + } + + private final class Scheduled implements Runnable { + private final AtomicBoolean exportAvailable = new AtomicBoolean(true); + + private Scheduled() {} + + @Override + public void run() { + // Ignore the CompletableResultCode from doRun() in order to keep run() asynchronous + doRun(); + } + + // Runs a collect + export cycle. + CompletableResultCode doRun() { + final CompletableResultCode flushResult = new CompletableResultCode(); + if (exportAvailable.compareAndSet(true, false)) { + try { + final CompletableResultCode result = exporter.export(producer.collectAllMetrics()); + result.whenComplete( + () -> { + if (!result.isSuccess()) { + logger.log(Level.FINE, "Exporter failed"); + } + flushResult.succeed(); + exportAvailable.set(true); + }); + } catch (Throwable t) { + exportAvailable.set(true); + logger.log(Level.WARNING, "Exporter threw an Exception", t); + flushResult.fail(); + } + } else { + logger.log(Level.FINE, "Exporter busy. Dropping metrics."); + flushResult.fail(); + } + return flushResult; + } + + CompletableResultCode shutdown() { + return exporter.shutdown(); + } + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderFactory.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderFactory.java new file mode 100644 index 00000000000..2414676b9d6 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; + +/** Factory for {@link PeriodicMetricReader}. */ +public class PeriodicMetricReaderFactory implements MetricReaderFactory { + private final MetricExporter exporter; + private final Duration duration; + private final ScheduledExecutorService scheduler; + + /** + * Builds a factory that will register and start a PeriodicMetricReader. + * + * @param exporter The exporter receiving metrics. + * @param duration The duration (interval) between metric export calls. + * @param scheduler The service to schedule export work. + */ + PeriodicMetricReaderFactory( + MetricExporter exporter, Duration duration, ScheduledExecutorService scheduler) { + this.exporter = exporter; + this.duration = duration; + this.scheduler = scheduler; + } + + @Override + public MetricReader apply(MetricProducer producer) { + PeriodicMetricReader result = new PeriodicMetricReader(producer, exporter, scheduler); + // TODO - allow a different start delay. + result.start(duration); + return result; + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/package-info.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/package-info.java index 8e0a2efc426..d3c5c31880e 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/package-info.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/package-info.java @@ -9,26 +9,10 @@ *

Contents

* *
    - *
  • {@link io.opentelemetry.sdk.metrics.export.IntervalMetricReader} *
  • {@link io.opentelemetry.sdk.metrics.export.MetricExporter} *
  • {@link io.opentelemetry.sdk.metrics.export.MetricProducer} - *
- * - *

Configuration options for {@link io.opentelemetry.sdk.metrics.export.IntervalMetricReader} can - * be read from system properties, environment variables, or {@link java.util.Properties} objects. - * - *

For system properties and {@link java.util.Properties} objects, {@link - * io.opentelemetry.sdk.metrics.export.IntervalMetricReader} will look for the following names: - * - *

    - *
  • {@code otel.imr.export.interval}: sets the export interval between pushes to the exporter. - *
- * - *

For environment variables, {@link io.opentelemetry.sdk.metrics.export.IntervalMetricReader} - * will look for the following names: - * - *

    - *
  • {@code OTEL_IMR_EXPORT_INTERVAL}: sets the export interval between pushes to the exporter. + *
  • {@link io.opentelemetry.sdk.metrics.export.MetricReader} + *
  • {@link io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} *
*/ @ParametersAreNonnullByDefault diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleAccumulation.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleAccumulation.java index d603af380b0..53089ed48b9 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleAccumulation.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleAccumulation.java @@ -11,23 +11,30 @@ import java.util.List; import javax.annotation.concurrent.Immutable; -/** An accumulation representing {@code long} values and exemplars. */ +/** + * An accumulation representing {@code long} values and exemplars. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + * + *

Visible for testing. + */ @Immutable @AutoValue -abstract class DoubleAccumulation { +public abstract class DoubleAccumulation { static DoubleAccumulation create(double value, List exemplars) { return new AutoValue_DoubleAccumulation(value, exemplars); } - static DoubleAccumulation create(double value) { + public static DoubleAccumulation create(double value) { return create(value, Collections.emptyList()); } DoubleAccumulation() {} /** The current value. */ - abstract double getValue(); + public abstract double getValue(); /** Sampled measurements recorded during this accumulation. */ abstract List getExemplars(); diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/export/CollectionHandle.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/export/CollectionHandle.java new file mode 100644 index 00000000000..75930fa9670 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/export/CollectionHandle.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.export; + +import java.util.AbstractSet; +import java.util.BitSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * A handle for a collection-pipeline of metrics. + * + *

This class provides an efficient means of leasing and tracking exporters. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class CollectionHandle { + /** The index of this handle. */ + private final int index; + + private CollectionHandle(int index) { + this.index = index; + } + + /** Construct a new (efficient) mutable set for tracking collection handles. */ + public static Set mutableSet() { + return new CollectionHandleSet(); + } + + /** + * Construct a new (mutable) set consistenting of the passed in collection handles. + * + *

Used by tests. + */ + static Set of(CollectionHandle... handles) { + Set result = mutableSet(); + for (CollectionHandle handle : handles) { + result.add(handle); + } + return result; + } + + /** + * Construct a new supplier of collection handles. + * + *

Handles returned by this supplier should not be used with unique handles produced by any + * other supplier. + */ + public static Supplier createSupplier() { + return new Supplier() { + private final AtomicInteger nextIndex = new AtomicInteger(1); + + @Override + public CollectionHandle get() { + return new CollectionHandle(nextIndex.getAndIncrement()); + } + }; + } + + @Override + public int hashCode() { + return index; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!(other instanceof CollectionHandle)) { + return false; + } + return index == ((CollectionHandle) other).index; + } + + @Override + public String toString() { + return "CollectionHandle(" + index + ")"; + } + + /** An optimised bitset version of {@code Set}. */ + private static class CollectionHandleSet extends AbstractSet { + private final BitSet storage = new BitSet(); + + @Override + public Iterator iterator() { + return new MyIterator(); + } + + @Override + public boolean add(CollectionHandle handle) { + if (storage.get(handle.index)) { + return false; + } + storage.set(handle.index); + return true; + } + + @Override + public boolean contains(Object handle) { + if (handle instanceof CollectionHandle) { + return storage.get(((CollectionHandle) handle).index); + } + return false; + } + + @Override + public boolean containsAll(Collection other) { + if (other instanceof CollectionHandleSet) { + BitSet result = (BitSet) storage.clone(); + BitSet otherStorage = ((CollectionHandleSet) other).storage; + result.and(otherStorage); + return result.equals(otherStorage); + } + return super.containsAll(other); + } + + private class MyIterator implements Iterator { + private int currentIndex = 0; + + @Override + public boolean hasNext() { + return (currentIndex != -1) && storage.nextSetBit(currentIndex) != -1; + } + + @Override + public CollectionHandle next() { + int result = storage.nextSetBit(currentIndex); + if (result != -1) { + // Start checking next bit next time. + currentIndex = result + 1; + return new CollectionHandle(result); + } + throw new NoSuchElementException("Called `.next` on iterator with no remaining values."); + } + } + + @Override + public int size() { + return storage.cardinality(); + } + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorage.java index abe7d38b7dc..5cb6758adf5 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorage.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorage.java @@ -15,11 +15,16 @@ import io.opentelemetry.sdk.metrics.exemplar.ExemplarFilter; import io.opentelemetry.sdk.metrics.internal.aggregator.Aggregator; import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor; import io.opentelemetry.sdk.metrics.view.View; import io.opentelemetry.sdk.resources.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import java.util.logging.Logger; import javax.annotation.Nullable; /** @@ -28,12 +33,15 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public final class AsynchronousMetricStorage implements MetricStorage { +public final class AsynchronousMetricStorage implements MetricStorage { private final MetricDescriptor metricDescriptor; private final ReentrantLock collectLock = new ReentrantLock(); - private final InstrumentProcessor instrumentProcessor; + private final AsyncAccumulator asyncAccumulator; + private final TemporalMetricStorage storage; private final Runnable metricUpdater; + private static final Logger logger = Logger.getLogger(AsynchronousMetricStorage.class.getName()); + /** Constructs asynchronous metric storage which stores nothing. */ public static MetricStorage empty() { return EmptyMetricStorage.INSTANCE; @@ -46,7 +54,6 @@ public static MetricStorage doubleAsynchronousAccumulator( InstrumentDescriptor instrument, Resource resource, InstrumentationLibraryInfo instrumentationLibraryInfo, - long startEpochNanos, Consumer metricUpdater) { final MetricDescriptor metricDescriptor = MetricDescriptor.create(view, instrument); Aggregator aggregator = @@ -57,18 +64,18 @@ public static MetricStorage doubleAsynchronousAccumulator( instrument, metricDescriptor, ExemplarFilter.neverSample()); + + final AsyncAccumulator measurementAccumulator = new AsyncAccumulator<>(); if (Aggregator.empty() == aggregator) { return empty(); } - final InstrumentProcessor instrumentProcessor = - new InstrumentProcessor<>(aggregator, startEpochNanos); final AttributesProcessor attributesProcessor = view.getAttributesProcessor(); // TODO: Find a way to grab the measurement JUST ONCE for all async metrics. final ObservableDoubleMeasurement result = new ObservableDoubleMeasurement() { @Override public void observe(double value, Attributes attributes) { - instrumentProcessor.batch( + measurementAccumulator.record( attributesProcessor.process(attributes, Context.current()), aggregator.accumulateDouble(value)); } @@ -78,8 +85,8 @@ public void observe(double value) { observe(value, Attributes.empty()); } }; - return new AsynchronousMetricStorage( - metricDescriptor, instrumentProcessor, () -> metricUpdater.accept(result)); + return new AsynchronousMetricStorage<>( + metricDescriptor, aggregator, measurementAccumulator, () -> metricUpdater.accept(result)); } /** Constructs storage for {@code long} valued instruments. */ @@ -88,7 +95,6 @@ public static MetricStorage longAsynchronousAccumulator( InstrumentDescriptor instrument, Resource resource, InstrumentationLibraryInfo instrumentationLibraryInfo, - long startEpochNanos, Consumer metricUpdater) { final MetricDescriptor metricDescriptor = MetricDescriptor.create(view, instrument); Aggregator aggregator = @@ -99,11 +105,14 @@ public static MetricStorage longAsynchronousAccumulator( instrument, metricDescriptor, ExemplarFilter.neverSample()); - if (Aggregator.empty() == aggregator) { - return empty(); + if (aggregator.isStateful()) { + // The aggregator is expecting to diff SUMs for DELTA temporality. + logger.warning( + String.format( + "Unable to provide DELTA accumulation on %s for instrument: %s", + metricDescriptor, instrument)); } - final InstrumentProcessor instrumentProcessor = - new InstrumentProcessor<>(aggregator, startEpochNanos); + final AsyncAccumulator measurementAccumulator = new AsyncAccumulator<>(); final AttributesProcessor attributesProcessor = view.getAttributesProcessor(); // TODO: Find a way to grab the measurement JUST ONCE for all async metrics. final ObservableLongMeasurement result = @@ -111,7 +120,7 @@ public static MetricStorage longAsynchronousAccumulator( @Override public void observe(long value, Attributes attributes) { - instrumentProcessor.batch( + measurementAccumulator.record( attributesProcessor.process(attributes, Context.current()), aggregator.accumulateLong(value)); } @@ -121,26 +130,33 @@ public void observe(long value) { observe(value, Attributes.empty()); } }; - return new AsynchronousMetricStorage( - metricDescriptor, instrumentProcessor, () -> metricUpdater.accept(result)); + return new AsynchronousMetricStorage<>( + metricDescriptor, aggregator, measurementAccumulator, () -> metricUpdater.accept(result)); } private AsynchronousMetricStorage( MetricDescriptor metricDescriptor, - InstrumentProcessor instrumentProcessor, + Aggregator aggregator, + AsyncAccumulator asyncAccumulator, Runnable metricUpdater) { this.metricDescriptor = metricDescriptor; - this.instrumentProcessor = instrumentProcessor; + this.asyncAccumulator = asyncAccumulator; this.metricUpdater = metricUpdater; + this.storage = new TemporalMetricStorage<>(aggregator, /* isSynchronous= */ false); } @Override @Nullable - public MetricData collectAndReset(long startEpochNanos, long epochNanos) { + public MetricData collectAndReset( + CollectionHandle collector, + Set allCollectors, + long startEpochNanos, + long epochNanos) { collectLock.lock(); try { metricUpdater.run(); - return instrumentProcessor.completeCollectionCycle(epochNanos); + return storage.buildMetricFor( + collector, asyncAccumulator.collectAndReset(), startEpochNanos, epochNanos); } finally { collectLock.unlock(); } @@ -150,4 +166,20 @@ public MetricData collectAndReset(long startEpochNanos, long epochNanos) { public MetricDescriptor getMetricDescriptor() { return metricDescriptor; } + + /** Helper class to record async measurements on demand. */ + private static final class AsyncAccumulator { + private Map currentAccumulation = new HashMap<>(); + + public void record(Attributes attributes, T accumulation) { + // TODO: error on metric overwrites + currentAccumulation.put(attributes, accumulation); + } + + public Map collectAndReset() { + Map result = currentAccumulation; + currentAccumulation = new HashMap<>(); + return result; + } + } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java index 2e92200d206..18ce799f871 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java @@ -9,13 +9,12 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.internal.aggregator.Aggregator; -import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorHandle; import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; +import java.util.Set; import javax.annotation.Nullable; /** @@ -26,23 +25,18 @@ */ public final class DefaultSynchronousMetricStorage implements SynchronousMetricStorage { private final MetricDescriptor metricDescriptor; - private final ConcurrentHashMap> aggregatorLabels; - private final ReentrantLock collectLock; - private final Aggregator aggregator; - private final InstrumentProcessor instrumentProcessor; + private final DeltaMetricStorage deltaMetricStorage; + private final TemporalMetricStorage temporalMetricStorage; private final AttributesProcessor attributesProcessor; DefaultSynchronousMetricStorage( MetricDescriptor metricDescriptor, Aggregator aggregator, - InstrumentProcessor instrumentProcessor, AttributesProcessor attributesProcessor) { - this.metricDescriptor = metricDescriptor; - aggregatorLabels = new ConcurrentHashMap<>(); - collectLock = new ReentrantLock(); - this.aggregator = aggregator; - this.instrumentProcessor = instrumentProcessor; this.attributesProcessor = attributesProcessor; + this.metricDescriptor = metricDescriptor; + this.deltaMetricStorage = new DeltaMetricStorage<>(aggregator); + this.temporalMetricStorage = new TemporalMetricStorage<>(aggregator, /* isSynchronous= */ true); } // This is a storage handle to use when the attributes processor requires @@ -69,33 +63,7 @@ public BoundStorageHandle bind(Attributes attributes) { // We cannot pre-bind attributes because we need to pull attributes from context. return lateBoundStorageHandle; } - return doBind(attributesProcessor.process(attributes, Context.current())); - } - - private BoundStorageHandle doBind(Attributes attributes) { - AggregatorHandle aggregatorHandle = aggregatorLabels.get(attributes); - if (aggregatorHandle != null && aggregatorHandle.acquire()) { - // At this moment it is guaranteed that the Bound is in the map and will not be removed. - return aggregatorHandle; - } - - // Missing entry or no longer mapped, try to add a new entry. - aggregatorHandle = aggregator.createHandle(); - while (true) { - AggregatorHandle boundAggregatorHandle = - aggregatorLabels.putIfAbsent(attributes, aggregatorHandle); - if (boundAggregatorHandle != null) { - if (boundAggregatorHandle.acquire()) { - // At this moment it is guaranteed that the Bound is in the map and will not be removed. - return boundAggregatorHandle; - } - // Try to remove the boundAggregator. This will race with the collect method, but only one - // will succeed. - aggregatorLabels.remove(attributes, boundAggregatorHandle); - continue; - } - return aggregatorHandle; - } + return deltaMetricStorage.bind(attributesProcessor.process(attributes, Context.current())); } // Overridden to make sure attributes processor can pull baggage. @@ -103,7 +71,7 @@ private BoundStorageHandle doBind(Attributes attributes) { public void recordLong(long value, Attributes attributes, Context context) { Objects.requireNonNull(attributes, "attributes"); attributes = attributesProcessor.process(attributes, context); - BoundStorageHandle handle = doBind(attributes); + BoundStorageHandle handle = deltaMetricStorage.bind(attributes); try { handle.recordLong(value, attributes, context); } finally { @@ -116,7 +84,7 @@ public void recordLong(long value, Attributes attributes, Context context) { public void recordDouble(double value, Attributes attributes, Context context) { Objects.requireNonNull(attributes, "attributes"); attributes = attributesProcessor.process(attributes, context); - BoundStorageHandle handle = doBind(attributes); + BoundStorageHandle handle = deltaMetricStorage.bind(attributes); try { handle.recordDouble(value, attributes, context); } finally { @@ -126,26 +94,13 @@ public void recordDouble(double value, Attributes attributes, Context context) { @Override @Nullable - public MetricData collectAndReset(long startEpochNanos, long epochNanos) { - collectLock.lock(); - try { - for (Map.Entry> entry : aggregatorLabels.entrySet()) { - boolean unmappedEntry = entry.getValue().tryUnmap(); - if (unmappedEntry) { - // If able to unmap then remove the record from the current Map. This can race with the - // acquire but because we requested a specific value only one will succeed. - aggregatorLabels.remove(entry.getKey(), entry.getValue()); - } - T accumulation = entry.getValue().accumulateThenReset(entry.getKey()); - if (accumulation == null) { - continue; - } - instrumentProcessor.batch(entry.getKey(), accumulation); - } - return instrumentProcessor.completeCollectionCycle(epochNanos); - } finally { - collectLock.unlock(); - } + public MetricData collectAndReset( + CollectionHandle collector, + Set allCollectors, + long startEpochNanos, + long epochNanos) { + Map result = deltaMetricStorage.collectFor(collector, allCollectors); + return temporalMetricStorage.buildMetricFor(collector, result, startEpochNanos, epochNanos); } @Override diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaAccumulation.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaAccumulation.java new file mode 100644 index 00000000000..c6c5ad990d5 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaAccumulation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.state; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import java.util.Map; +import java.util.Set; + +/** + * Synchronous recording of delta-accumulated measurements. + * + *

This stores in-progress metric values that haven't been exported yet. + */ +class DeltaAccumulation { + private final Map recording; + private final Set readers; + + DeltaAccumulation(Map recording) { + this.recording = recording; + this.readers = CollectionHandle.mutableSet(); + } + + /** Returns true if this accumulation was read by the {@link CollectionHandle}. */ + boolean wasReadBy(CollectionHandle handle) { + return readers.contains(handle); + } + + /** Returns true if all readers in the given set have read this accumulation. */ + boolean wasReadByAll(Set handles) { + return readers.containsAll(handles); + } + + /** + * Reads the current delta accumulation. + * + * @param handle The reader of the accumulation. + * @return the accumulation. + */ + Map read(CollectionHandle handle) { + readers.add(handle); + return recording; + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaMetricStorage.java new file mode 100644 index 00000000000..b3602981314 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaMetricStorage.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.state; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.internal.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Allows synchronous collection of metrics and reports delta values isolated by collection handle. + * + *

This storage should allow allocation of new aggregation cells for metrics and unique reporting + * of delta accumulations per-collection-handle. + */ +@ThreadSafe +class DeltaMetricStorage { + private final Aggregator aggregator; + private final ConcurrentHashMap> activeCollectionStorage = + new ConcurrentHashMap<>(); + private final List> unreportedDeltas = new ArrayList<>(); + + DeltaMetricStorage(Aggregator aggregator) { + this.aggregator = aggregator; + } + + /** + * Allocates memory for a new metric stream, and returns a handle for synchronous recordings. + * + * @param attributes The identifying attributes for the metric stream. + * @return A handle that will (efficiently) record synchronous measurements. + */ + public BoundStorageHandle bind(Attributes attributes) { + AggregatorHandle aggregatorHandle = activeCollectionStorage.get(attributes); + if (aggregatorHandle != null && aggregatorHandle.acquire()) { + // At this moment it is guaranteed that the Bound is in the map and will not be removed. + return aggregatorHandle; + } + + // Missing entry or no longer mapped, try to add a new entry. + aggregatorHandle = aggregator.createHandle(); + while (true) { + AggregatorHandle boundAggregatorHandle = + activeCollectionStorage.putIfAbsent(attributes, aggregatorHandle); + if (boundAggregatorHandle != null) { + if (boundAggregatorHandle.acquire()) { + // At this moment it is guaranteed that the Bound is in the map and will not be removed. + return boundAggregatorHandle; + } + // Try to remove the boundAggregator. This will race with the collect method, but only one + // will succeed. + activeCollectionStorage.remove(attributes, boundAggregatorHandle); + continue; + } + return aggregatorHandle; + } + } + + /** + * Returns the latest delta accumulation for a specific collection handle. + * + * @param collector The current reader of metrics. + * @param collectors All possible readers of metrics. + * @return The delta accumulation of metrics since the last read of a the specified reader. + */ + public synchronized Map collectFor( + CollectionHandle collector, Set collectors) { + // First we force a collection + collectSynchronousDeltaAccumulationAndReset(); + // Now build a delta result. + Map result = new HashMap<>(); + for (DeltaAccumulation point : unreportedDeltas) { + if (!point.wasReadBy(collector)) { + mergeInPlace(result, point.read(collector), aggregator); + } + } + // Now run a quick cleanup of deltas before returning. + unreportedDeltas.removeIf(delta -> delta.wasReadByAll(collectors)); + return result; + } + + /** + * Collects the currently accumulated measurements from the concurrent-friendly synchronous + * storage. + * + *

All synchronous handles will be collected + reset during this method. Additionally cleanup + * related stale concurrent-map handles will occur. Any {@code null} measurements are ignored. + */ + private synchronized void collectSynchronousDeltaAccumulationAndReset() { + Map result = new HashMap<>(); + for (Map.Entry> entry : activeCollectionStorage.entrySet()) { + boolean unmappedEntry = entry.getValue().tryUnmap(); + if (unmappedEntry) { + // If able to unmap then remove the record from the current Map. This can race with the + // acquire but because we requested a specific value only one will succeed. + activeCollectionStorage.remove(entry.getKey(), entry.getValue()); + } + T accumulation = entry.getValue().accumulateThenReset(entry.getKey()); + if (accumulation == null) { + continue; + } + // Feed latest batch to the aggregator. + result.put(entry.getKey(), accumulation); + } + if (!result.isEmpty()) { + unreportedDeltas.add(new DeltaAccumulation<>(result)); + } + } + + /** + * Merges accumulations from {@code toMerge} into {@code result}. + * + *

Note: This mutates the result map. + */ + static final void mergeInPlace( + Map result, Map toMerge, Aggregator aggregator) { + toMerge.forEach( + (k, v) -> { + if (result.containsKey(k)) { + result.put(k, aggregator.merge(result.get(k), v)); + } else { + result.put(k, v); + } + }); + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java index 44a5e34fbd6..722fc991372 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java @@ -9,6 +9,9 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import java.util.Set; +import javax.annotation.Nullable; final class EmptyMetricStorage implements SynchronousMetricStorage { static final EmptyMetricStorage INSTANCE = new EmptyMetricStorage(); @@ -34,12 +37,17 @@ public MetricDescriptor getMetricDescriptor() { } @Override - public MetricData collectAndReset(long startEpochNanos, long epochNanos) { - return null; + public BoundStorageHandle bind(Attributes attributes) { + return emptyHandle; } + @Nullable @Override - public BoundStorageHandle bind(Attributes attributes) { - return emptyHandle; + public MetricData collectAndReset( + CollectionHandle collector, + Set allCollectors, + long startEpochNanos, + long epochNanos) { + return null; } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentProcessor.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentProcessor.java deleted file mode 100644 index 06cc35bff22..00000000000 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentProcessor.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.metrics.internal.state; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.internal.aggregator.Aggregator; -import java.util.HashMap; -import java.util.Map; -import javax.annotation.Nullable; - -/** - * An {@code InstrumentProcessor} represents an internal instance of an {@code Accumulator} for a - * specific {code Instrument}. It records individual measurements (via the {@code Aggregator}). It - * batches together {@code Aggregator}s for the similar sets of attributes. - * - *

An entire collection cycle must be protected by a lock. A collection cycle is defined by - * multiple calls to {@code #batch(...)} followed by one {@code #completeCollectionCycle(...)}; - * - *

This class is internal and is hence not for public use. Its APIs are unstable and can change - * at any time. - */ -final class InstrumentProcessor { - private final Aggregator aggregator; - private final long startEpochNanos; - private long lastEpochNanos; - private Map accumulationMap; - - InstrumentProcessor(Aggregator aggregator, long startEpochNanos) { - this.aggregator = aggregator; - this.startEpochNanos = startEpochNanos; - this.lastEpochNanos = startEpochNanos; - this.accumulationMap = new HashMap<>(); - } - - /** - * Batches multiple entries together that are part of the same metric. It may remove attributes - * from the {@link Attributes} and merge aggregations together. - * - * @param attributes the {@link Attributes} associated with this {@code Aggregator}. - * @param accumulation the accumulation produced by this instrument. - */ - void batch(Attributes attributes, T accumulation) { - T currentAccumulation = accumulationMap.putIfAbsent(attributes, accumulation); - if (currentAccumulation != null) { - accumulationMap.put(attributes, aggregator.merge(currentAccumulation, accumulation)); - } - } - - /** - * Ends the current collection cycle and returns the list of metrics batched in this Batcher. - * - *

There may be more than one MetricData in case a multi aggregator is configured. - * - *

Based on the configured options this method may reset the internal state to produce deltas, - * or keep the internal state to produce cumulative metrics. - * - * @return the metric batched or {@code null}. - */ - @Nullable - MetricData completeCollectionCycle(long epochNanos) { - if (accumulationMap.isEmpty()) { - return null; - } - - MetricData metricData = - aggregator.toMetricData(accumulationMap, startEpochNanos, lastEpochNanos, epochNanos); - - lastEpochNanos = epochNanos; - if (!aggregator.isStateful()) { - accumulationMap = new HashMap<>(); - } - - return metricData; - } -} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterSharedState.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterSharedState.java index 465ffd5530e..41775bcb49d 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterSharedState.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterSharedState.java @@ -11,10 +11,12 @@ import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; import io.opentelemetry.sdk.metrics.view.View; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,12 +47,16 @@ public static MeterSharedState create(InstrumentationLibraryInfo instrumentation /** Collects all accumulated metric stream points. */ public List collectAll( - MeterProviderSharedState meterProviderSharedState, long epochNanos) { + CollectionHandle collector, + Set allCollectors, + MeterProviderSharedState meterProviderSharedState, + long epochNanos) { Collection metrics = getMetricStorageRegistry().getMetrics(); List result = new ArrayList<>(metrics.size()); for (MetricStorage metric : metrics) { MetricData current = - metric.collectAndReset(meterProviderSharedState.getStartEpochNanos(), epochNanos); + metric.collectAndReset( + collector, allCollectors, meterProviderSharedState.getStartEpochNanos(), epochNanos); if (current != null) { result.add(current); } @@ -73,7 +79,6 @@ public final WriteableMetricStorage registerSynchronousMetricStorage( instrument, meterProviderSharedState.getResource(), getInstrumentationLibraryInfo(), - meterProviderSharedState.getStartEpochNanos(), meterProviderSharedState.getExemplarFilter()); // TODO - move this in a better location. if (SynchronousMetricStorage.empty().equals(currentStorage)) { @@ -109,7 +114,6 @@ public final void registerLongAsynchronousInstrument( instrument, meterProviderSharedState.getResource(), getInstrumentationLibraryInfo(), - meterProviderSharedState.getStartEpochNanos(), metricUpdater); // TODO - move this in a better location. if (AsynchronousMetricStorage.empty().equals(currentStorage)) { @@ -140,10 +144,9 @@ public final void registerDoubleAsynchronousInstrument( instrument, meterProviderSharedState.getResource(), getInstrumentationLibraryInfo(), - meterProviderSharedState.getStartEpochNanos(), metricUpdater); // TODO - move this in a better location. - if (AsynchronousMetricStorage.empty().equals(currentStorage)) { + if (AsynchronousMetricStorage.empty() == currentStorage) { continue; } try { diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorage.java index 53da3847096..7d5ce1d5805 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorage.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorage.java @@ -7,6 +7,8 @@ import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import java.util.Set; import javax.annotation.Nullable; /** @@ -22,10 +24,19 @@ public interface MetricStorage { /** * Collects the metrics from this storage and resets for the next collection period. * + *

Note: This is a stateful operation and will reset any interval-related state for the {@code + * collector}. + * + * @param collector The identity of the current reader of metrics. + * @param allCollectors The set of all registered readers for metrics. * @param startEpochNanos The start timestamp for this SDK. * @param epochNanos The timestamp for this collection. * @return The {@link MetricData} from this collection period, or {@code null}. */ @Nullable - MetricData collectAndReset(long startEpochNanos, long epochNanos); + MetricData collectAndReset( + CollectionHandle collector, + Set allCollectors, + long startEpochNanos, + long epochNanos); } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorage.java index 1a67fbd4e40..fdf2ba4892d 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorage.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorage.java @@ -37,7 +37,6 @@ static SynchronousMetricStorage create( InstrumentDescriptor instrumentDescriptor, Resource resource, InstrumentationLibraryInfo instrumentationLibraryInfo, - long startEpochNanos, ExemplarFilter exemplarFilter) { final MetricDescriptor metricDescriptor = MetricDescriptor.create(view, instrumentDescriptor); final Aggregator aggregator = @@ -53,9 +52,6 @@ static SynchronousMetricStorage create( return empty(); } return new DefaultSynchronousMetricStorage<>( - metricDescriptor, - aggregator, - new InstrumentProcessor<>(aggregator, startEpochNanos), - view.getAttributesProcessor()); + metricDescriptor, aggregator, view.getAttributesProcessor()); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/TemporalMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/TemporalMetricStorage.java new file mode 100644 index 00000000000..3245bacbfc2 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/TemporalMetricStorage.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.state; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** Stores last reported time and (optional) accumulation for metrics. */ +@ThreadSafe +class TemporalMetricStorage { + private final Aggregator aggregator; + private final boolean isSynchronous; + private final Map> reportHistory = new HashMap<>(); + + TemporalMetricStorage(Aggregator aggregator, boolean isSynchronous) { + this.aggregator = aggregator; + this.isSynchronous = isSynchronous; + } + + /** + * Builds the {@link MetricData} streams to report against a specific metric reader. + * + * @param collector The handle of the metric reader. + * @param currentAccumulation THe current accumulation of metric data from instruments. This might + * be delta (for synchronous) or cumulative (for asynchronous). + * @param startEpochNanos The timestamp when the metrics SDK started. + * @param epochNanos The current collection timestamp. + * @return The {@link MetricData} points or {@code null}. + */ + @Nullable + synchronized MetricData buildMetricFor( + CollectionHandle collector, + Map currentAccumulation, + long startEpochNanos, + long epochNanos) { + // In case it's our first collection, default to start timestmap. + long lastCollectionEpoch = startEpochNanos; + Map result = currentAccumulation; + // Check our last report time. + if (reportHistory.containsKey(collector)) { + LastReportedAccumulation last = reportHistory.get(collector); + lastCollectionEpoch = last.getEpochNanos(); + // We need to merge with previous accumulation. + if (aggregator.isStateful()) { + // We merge the current into last, and take over that memory. + DeltaMetricStorage.mergeInPlace(last.getAccumlation(), currentAccumulation, aggregator); + result = last.getAccumlation(); + } + } + // Update last reported accumulation + if (isSynchronous) { + // Sync instruments remember the full recording. + reportHistory.put(collector, new LastReportedAccumulation<>(result, epochNanos)); + } else { + // Async instruments record the raw measurement. + reportHistory.put(collector, new LastReportedAccumulation<>(currentAccumulation, epochNanos)); + } + if (result.isEmpty()) { + return null; + } + return aggregator.toMetricData(result, startEpochNanos, lastCollectionEpoch, epochNanos); + } + + /** Remembers what was presented to a specific exporter. */ + private static class LastReportedAccumulation { + @Nullable private final Map accumulation; + private final long epochNanos; + + /** + * Constructs a new reporting record. + * + * @param accumulation The last accumulation of metric data or {@code null} if the accumulator + * is not stateful. + * @param epochNanos The timestamp the data was reported. + */ + LastReportedAccumulation(@Nullable Map accumulation, long epochNanos) { + this.accumulation = accumulation; + this.epochNanos = epochNanos; + } + + long getEpochNanos() { + return epochNanos; + } + + @Nullable + Map getAccumlation() { + return accumulation; + } + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricReader.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricReader.java new file mode 100644 index 00000000000..582007d1754 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricReader.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.testing; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.export.MetricReaderFactory; +import java.util.Collection; +import java.util.Collections; + +/** + * A {@link MetricReader} implementation that can be used to test OpenTelemetry integration. + * + *

Can be created using {@code InMemoryMetricReader.create(sdkMeterProvider)} + * + *

Example usage: + * + *


+ * public class InMemoryMetricReaderExample {
+ *   private final InMemoryMetricReader reader = new InMemoryMetricReader();
+ *   private final SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().registerMetricReader(reader).build();
+ *   private final Meter meter = sdkMeterProvider.get("example");
+ *   private final LongCounter metricCallCount = meter.counterBuilder("num_collects");
+ *
+ *   public void printMetrics() {
+ *     metricCallCount.add(1);
+ *     System.out.println(reader.collectAllMetrics());
+ *   }
+ *
+ *   public static void main(String[] args) {
+ *     InMemoryMetricReaderExample example = new InMemoryMetricReaderExample();
+ *     example.printMetrics();
+ *   }
+ * }
+ * 
+ */ +public class InMemoryMetricReader implements MetricReader, MetricReaderFactory { + // Note: we expect the `apply` method of `MetricReaderFactory` to be called + // prior to registering this being shared with other threads. + // This means this field does not need to be volatile because it will + // be filled out (and no longer mutated) prior to being shared with other threads. + private MetricProducer metricProducer; + private volatile Collection latest = Collections.emptyList(); + + /** Returns all metrics accumulated since the last call. */ + public Collection collectAllMetrics() { + flush(); + return latest; + } + + @Override + public CompletableResultCode flush() { + if (metricProducer != null) { + latest = metricProducer.collectAllMetrics(); + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public MetricReader apply(MetricProducer producer) { + this.metricProducer = producer; + return this; + } +} diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleCounterTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleCounterTest.java index 4a25815f472..ed41e2feb66 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleCounterTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleCounterTest.java @@ -15,6 +15,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -28,8 +29,13 @@ class SdkDoubleCounterTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkDoubleCounterTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .registerMetricReader(sdkMeterReader) + .setResource(RESOURCE) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -52,7 +58,7 @@ void collectMetrics_NoRecords() { DoubleCounter doubleCounter = sdkMeter.counterBuilder("testCounter").ofDoubles().build(); BoundDoubleCounter bound = doubleCounter.bind(Attributes.builder().put("foo", "bar").build()); try { - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } finally { bound.unbind(); } @@ -71,7 +77,7 @@ void collectMetrics_WithEmptyAttributes() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); doubleCounter.add(12d, Attributes.empty()); doubleCounter.add(12d); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -108,7 +114,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(321.5d); doubleCounter.add(111.1d, Attributes.builder().put("K", "V").build()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -140,7 +146,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(222d); doubleCounter.add(11d, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -203,7 +209,7 @@ void stressTest() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -250,7 +256,7 @@ void stressTest_WithDifferentLabelSet() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleGaugeBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleGaugeBuilderTest.java index dc95f810949..184ed5d547d 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleGaugeBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleGaugeBuilderTest.java @@ -11,6 +11,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -23,8 +24,13 @@ class SdkDoubleGaugeBuilderTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkDoubleGaugeBuilderTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerMetricReader(sdkMeterReader) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -34,7 +40,7 @@ void collectMetrics_NoRecords() { .setDescription("My own DoubleValueObserver") .setUnit("ms") .buildWithCallback(result -> {}); - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } @Test @@ -47,7 +53,7 @@ void collectMetrics_WithOneRecord() { .buildWithCallback( result -> result.observe(12.1d, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofSeconds(1)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -66,7 +72,7 @@ void collectMetrics_WithOneRecord() { .hasAttributes(Attributes.builder().put("k", "v").build()) .hasValue(12.1d))); testClock.advance(Duration.ofSeconds(1)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java index a5fbbd054f8..caa385d04da 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java @@ -16,6 +16,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -29,8 +30,13 @@ class SdkDoubleHistogramTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkDoubleHistogramTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerMetricReader(sdkMeterReader) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -53,7 +59,7 @@ void collectMetrics_NoRecords() { BoundDoubleHistogram bound = doubleRecorder.bind(Attributes.builder().put("key", "value").build()); try { - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } finally { bound.unbind(); } @@ -70,7 +76,7 @@ void collectMetrics_WithEmptyAttributes() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); doubleRecorder.record(12d, Attributes.empty()); doubleRecorder.record(12d); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -111,7 +117,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.record(321.5d); doubleRecorder.record(-121.5d, Attributes.builder().put("K", "V").build()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -143,7 +149,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.record(222d); doubleRecorder.record(17d, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -199,7 +205,7 @@ void stressTest() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -246,7 +252,7 @@ void stressTest_WithDifferentLabelSet() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleSumObserverTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleSumObserverTest.java index 8fbde6f3bf9..56c61a8ffba 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleSumObserverTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleSumObserverTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.common.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.metrics.view.Aggregation; import io.opentelemetry.sdk.metrics.view.InstrumentSelector; import io.opentelemetry.sdk.metrics.view.View; @@ -33,7 +34,9 @@ class SdkDoubleSumObserverTest { @Test void collectMetrics_NoRecords() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .counterBuilder("testObserver") @@ -41,13 +44,15 @@ void collectMetrics_NoRecords() { .setDescription("My own DoubleSumObserver") .setUnit("ms") .buildWithCallback(result -> {}); - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } @Test @SuppressWarnings("unchecked") void collectMetrics_WithOneRecord() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .counterBuilder("testObserver") @@ -57,7 +62,7 @@ void collectMetrics_WithOneRecord() { .buildWithCallback( result -> result.observe(12.1d, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -80,7 +85,7 @@ void collectMetrics_WithOneRecord() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -105,8 +110,10 @@ void collectMetrics_WithOneRecord() { @Test @SuppressWarnings("unchecked") void collectMetrics_DeltaSumAggregator() { + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder + .registerMetricReader(sdkMeterReader) .registerView( InstrumentSelector.builder() .setInstrumentType(InstrumentType.OBSERVABLE_SUM) @@ -124,7 +131,7 @@ void collectMetrics_DeltaSumAggregator() { .buildWithCallback( result -> result.observe(12.1d, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -147,7 +154,7 @@ void collectMetrics_DeltaSumAggregator() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java index 96246970cf6..4fc9b9f58f2 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java @@ -15,6 +15,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -28,8 +29,13 @@ class SdkDoubleUpDownCounterTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkDoubleUpDownCounterTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerMetricReader(sdkMeterReader) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -60,7 +66,7 @@ void collectMetrics_NoRecords() { BoundDoubleUpDownCounter bound = doubleUpDownCounter.bind(Attributes.builder().put("foo", "bar").build()); try { - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } finally { bound.unbind(); } @@ -79,7 +85,7 @@ void collectMetrics_WithEmptyAttributes() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); doubleUpDownCounter.add(12d, Attributes.empty()); doubleUpDownCounter.add(12d); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -118,7 +124,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(321.5d); doubleUpDownCounter.add(111.1d, Attributes.builder().put("K", "V").build()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -146,7 +152,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(222d); doubleUpDownCounter.add(11d, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -198,7 +204,7 @@ void stressTest() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -247,7 +253,7 @@ void stressTest_WithDifferentLabelSet() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownSumObserverTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownSumObserverTest.java index b5a3593f8c1..b9d12826b2d 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownSumObserverTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownSumObserverTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.common.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.metrics.view.Aggregation; import io.opentelemetry.sdk.metrics.view.InstrumentSelector; import io.opentelemetry.sdk.metrics.view.View; @@ -33,7 +34,9 @@ class SdkDoubleUpDownSumObserverTest { @Test void collectMetrics_NoRecords() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .upDownCounterBuilder("testObserver") @@ -41,13 +44,15 @@ void collectMetrics_NoRecords() { .setDescription("My own DoubleUpDownSumObserver") .setUnit("ms") .buildWithCallback(result -> {}); - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } @Test @SuppressWarnings("unchecked") void collectMetrics_WithOneRecord() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .upDownCounterBuilder("testObserver") @@ -55,7 +60,7 @@ void collectMetrics_WithOneRecord() { .buildWithCallback( result -> result.observe(12.1d, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -76,7 +81,7 @@ void collectMetrics_WithOneRecord() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -101,8 +106,10 @@ void collectMetrics_WithOneRecord() { @Test @SuppressWarnings("unchecked") void collectMetrics_DeltaSumAggregator() { + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder + .registerMetricReader(sdkMeterReader) .registerView( InstrumentSelector.builder() .setInstrumentType(InstrumentType.OBSERVABLE_UP_DOWN_SUM) @@ -118,7 +125,7 @@ void collectMetrics_DeltaSumAggregator() { .buildWithCallback( result -> result.observe(12.1d, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -139,7 +146,7 @@ void collectMetrics_DeltaSumAggregator() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongCounterTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongCounterTest.java index ea3e081e1aa..ac8f38c65d4 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongCounterTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongCounterTest.java @@ -15,6 +15,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -28,8 +29,13 @@ class SdkLongCounterTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkLongCounterTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerMetricReader(sdkMeterReader) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -51,7 +57,7 @@ void collectMetrics_NoRecords() { LongCounter longCounter = sdkMeter.counterBuilder("Counter").build(); BoundLongCounter bound = longCounter.bind(Attributes.builder().put("foo", "bar").build()); try { - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } finally { bound.unbind(); } @@ -65,7 +71,7 @@ void collectMetrics_WithEmptyAttributes() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); longCounter.add(12, Attributes.empty()); longCounter.add(12); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -102,7 +108,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(321); longCounter.add(111, Attributes.builder().put("K", "V").build()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -129,7 +135,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(222); longCounter.add(11, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -195,7 +201,7 @@ void stressTest() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -243,7 +249,7 @@ void stressTest_WithDifferentLabelSet() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongGaugeBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongGaugeBuilderTest.java index 408a4b93fe2..fd52787abb8 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongGaugeBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongGaugeBuilderTest.java @@ -11,6 +11,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -23,8 +24,13 @@ class SdkLongGaugeBuilderTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkLongGaugeBuilderTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerMetricReader(sdkMeterReader) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -35,7 +41,7 @@ void collectMetrics_NoRecords() { .setDescription("My own LongValueObserver") .setUnit("ms") .buildWithCallback(result -> {}); - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } @Test @@ -47,7 +53,7 @@ void collectMetrics_WithOneRecord() { .buildWithCallback( result -> result.observe(12, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofSeconds(1)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -64,7 +70,7 @@ void collectMetrics_WithOneRecord() { .hasAttributes(Attributes.builder().put("k", "v").build()) .hasValue(12))); testClock.advance(Duration.ofSeconds(1)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java index 156ebe13796..c8092699943 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java @@ -16,6 +16,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -29,8 +30,13 @@ class SdkLongHistogramTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkLongHistogramTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerMetricReader(sdkMeterReader) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -53,7 +59,7 @@ void collectMetrics_NoRecords() { LongHistogram longRecorder = sdkMeter.histogramBuilder("testRecorder").ofLongs().build(); BoundLongHistogram bound = longRecorder.bind(Attributes.builder().put("key", "value").build()); try { - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } finally { bound.unbind(); } @@ -71,7 +77,7 @@ void collectMetrics_WithEmptyAttributes() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); longRecorder.record(12, Attributes.empty()); longRecorder.record(12); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -112,7 +118,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.record(321); longRecorder.record(-121, Attributes.builder().put("K", "V").build()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -144,7 +150,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.record(222); longRecorder.record(17, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -200,7 +206,7 @@ void stressTest() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -247,7 +253,7 @@ void stressTest_WithDifferentLabelSet() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongSumObserverTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongSumObserverTest.java index b45db967faf..50ef46355a3 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongSumObserverTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongSumObserverTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.common.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.metrics.view.Aggregation; import io.opentelemetry.sdk.metrics.view.InstrumentSelector; import io.opentelemetry.sdk.metrics.view.View; @@ -33,27 +34,31 @@ class SdkLongSumObserverTest { @Test void collectMetrics_NoRecords() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .counterBuilder("testObserver") .setDescription("My own LongSumObserver") .setUnit("ms") .buildWithCallback(result -> {}); - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } @Test @SuppressWarnings("unchecked") void collectMetrics_WithOneRecord() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .counterBuilder("testObserver") .buildWithCallback( result -> result.observe(12, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -74,7 +79,7 @@ void collectMetrics_WithOneRecord() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -99,8 +104,10 @@ void collectMetrics_WithOneRecord() { @Test @SuppressWarnings("unchecked") void collectMetrics_DeltaSumAggregator() { + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder + .registerMetricReader(sdkMeterReader) .registerView( InstrumentSelector.builder() .setInstrumentType(InstrumentType.OBSERVABLE_SUM) @@ -115,7 +122,7 @@ void collectMetrics_DeltaSumAggregator() { .buildWithCallback( result -> result.observe(12, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -136,7 +143,7 @@ void collectMetrics_DeltaSumAggregator() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownCounterTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownCounterTest.java index 438576a118e..1385cf2c8c8 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownCounterTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownCounterTest.java @@ -15,6 +15,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.time.Duration; @@ -28,8 +29,13 @@ class SdkLongUpDownCounterTest { private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = InstrumentationLibraryInfo.create(SdkLongUpDownCounterTest.class.getName(), null); private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerMetricReader(sdkMeterReader) + .build(); private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); @Test @@ -53,7 +59,7 @@ void collectMetrics_NoRecords() { BoundLongUpDownCounter bound = longUpDownCounter.bind(Attributes.builder().put("foo", "bar").build()); try { - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } finally { bound.unbind(); } @@ -71,7 +77,7 @@ void collectMetrics_WithEmptyAttributes() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); longUpDownCounter.add(12, Attributes.empty()); longUpDownCounter.add(12); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -110,7 +116,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(321); longUpDownCounter.add(111, Attributes.builder().put("K", "V").build()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -137,7 +143,7 @@ void collectMetrics_WithMultipleCollects() { testClock.advance(Duration.ofNanos(SECOND_NANOS)); bound.add(222); longUpDownCounter.add(11, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -188,7 +194,7 @@ void stressTest() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -237,7 +243,7 @@ void stressTest_WithDifferentLabelSet() { } stressTestBuilder.build().run(); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownSumObserverTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownSumObserverTest.java index 352b060c3cf..450562a7369 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownSumObserverTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongUpDownSumObserverTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.common.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.metrics.view.Aggregation; import io.opentelemetry.sdk.metrics.view.InstrumentSelector; import io.opentelemetry.sdk.metrics.view.View; @@ -33,27 +34,31 @@ class SdkLongUpDownSumObserverTest { @Test void collectMetrics_NoRecords() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .upDownCounterBuilder("testObserver") .setDescription("My own LongUpDownSumObserver") .setUnit("ms") .buildWithCallback(result -> {}); - assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + assertThat(sdkMeterReader.collectAllMetrics()).isEmpty(); } @Test @SuppressWarnings("unchecked") void collectMetrics_WithOneRecord() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); sdkMeterProvider .get(getClass().getName()) .upDownCounterBuilder("testObserver") .buildWithCallback( result -> result.observe(12, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -74,7 +79,7 @@ void collectMetrics_WithOneRecord() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -99,8 +104,10 @@ void collectMetrics_WithOneRecord() { @Test @SuppressWarnings("unchecked") void collectMetrics_DeltaSumAggregator() { + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder + .registerMetricReader(sdkMeterReader) .registerView( InstrumentSelector.builder() .setInstrumentType(InstrumentType.OBSERVABLE_UP_DOWN_SUM) @@ -115,7 +122,7 @@ void collectMetrics_DeltaSumAggregator() { .buildWithCallback( result -> result.observe(12, Attributes.builder().put("k", "v").build())); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -136,7 +143,7 @@ void collectMetrics_DeltaSumAggregator() { .hasSize(1) .containsEntry("k", "v"))); testClock.advance(Duration.ofNanos(SECOND_NANOS)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java index 5501e16c23a..d556bb9179b 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java @@ -8,6 +8,7 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import org.junit.jupiter.api.Test; @@ -25,10 +26,20 @@ void buildAndRegisterGlobal() { @Test void defaultResource() { - SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + // We need a reader to have a resource. + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(new InMemoryMetricReader()).build(); assertThat(meterProvider) .extracting("sharedState") .hasFieldOrPropertyWithValue("resource", Resource.getDefault()); } + + @Test + void stubsWithNoReaders() { + // We need a reader to have a resource. + SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + + assertThat(meterProvider).isInstanceOf(NoopSdkMeterProvider.class); + } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderTest.java index 764bfd38ecf..3b1e7df0424 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderTest.java @@ -22,6 +22,7 @@ import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.common.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.metrics.view.Aggregation; import io.opentelemetry.sdk.metrics.view.InstrumentSelector; import io.opentelemetry.sdk.metrics.view.View; @@ -49,7 +50,10 @@ void defaultMeterName() { @Test @SuppressWarnings("unchecked") void collectAllSyncInstruments() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); + Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); LongCounter longCounter = sdkMeter.counterBuilder("testLongCounter").build(); longCounter.add(10, Attributes.empty()); @@ -67,7 +71,7 @@ void collectAllSyncInstruments() { DoubleHistogram doubleValueRecorder = sdkMeter.histogramBuilder("testDoubleHistogram").build(); doubleValueRecorder.record(10.1, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .allSatisfy( metric -> assertThat(metric) @@ -172,14 +176,16 @@ void collectAllSyncInstruments_OverwriteTemporality() { Aggregation.explicitBucketHistogram( AggregationTemporality.DELTA, Collections.emptyList())) .build()); - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); LongCounter longCounter = sdkMeter.counterBuilder("testLongCounter").build(); longCounter.add(10, Attributes.empty()); testClock.advance(Duration.ofNanos(50)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -200,7 +206,7 @@ void collectAllSyncInstruments_OverwriteTemporality() { longCounter.add(10, Attributes.empty()); testClock.advance(Duration.ofNanos(50)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -224,7 +230,9 @@ void collectAllSyncInstruments_DeltaHistogram() { registerViewForAllTypes( sdkMeterProviderBuilder, Aggregation.explicitBucketHistogram(AggregationTemporality.DELTA, Collections.emptyList())); - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); LongCounter longCounter = sdkMeter.counterBuilder("testLongCounter").build(); longCounter.add(10, Attributes.empty()); @@ -245,7 +253,7 @@ void collectAllSyncInstruments_DeltaHistogram() { testClock.advance(Duration.ofNanos(50)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .allSatisfy( metric -> assertThat(metric) @@ -281,7 +289,7 @@ void collectAllSyncInstruments_DeltaHistogram() { doubleUpDownCounter.add(10, Attributes.empty()); doubleValueRecorder.record(10, Attributes.empty()); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .allSatisfy( metric -> assertThat(metric) @@ -312,7 +320,9 @@ void collectAllSyncInstruments_DeltaHistogram() { @Test @SuppressWarnings("unchecked") void collectAllAsyncInstruments() { - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); sdkMeter .counterBuilder("testLongSumObserver") @@ -337,7 +347,7 @@ void collectAllAsyncInstruments() { .gaugeBuilder("testDoubleValueObserver") .buildWithCallback(doubleResult -> doubleResult.observe(10.1, Attributes.empty())); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .allSatisfy( metric -> assertThat(metric) @@ -431,8 +441,10 @@ void collectAllAsyncInstruments() { @Test @SuppressWarnings("unchecked") void viewSdk_AllowRenames() { + InMemoryMetricReader reader = new InMemoryMetricReader(); SdkMeterProvider provider = sdkMeterProviderBuilder + .registerMetricReader(reader) .registerView( InstrumentSelector.builder() // TODO: Make instrument type optional. @@ -451,7 +463,7 @@ void viewSdk_AllowRenames() { .setDescription("desc") .setUnit("unit") .buildWithCallback(o -> o.observe(1)); - assertThat(provider.collectAllMetrics()) + assertThat(reader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -470,8 +482,10 @@ void viewSdk_AllowMulitpleViewsPerSynchronousInstrument() { .setInstrumentType(InstrumentType.HISTOGRAM) .setInstrumentName("test") .build(); + InMemoryMetricReader reader = new InMemoryMetricReader(); SdkMeterProvider provider = sdkMeterProviderBuilder + .registerMetricReader(reader) .registerView( selector, View.builder() @@ -491,7 +505,7 @@ void viewSdk_AllowMulitpleViewsPerSynchronousInstrument() { DoubleHistogram histogram = meter.histogramBuilder("test").setDescription("desc").setUnit("unit").build(); histogram.record(1.0); - assertThat(provider.collectAllMetrics()) + assertThat(reader.collectAllMetrics()) .satisfiesExactlyInAnyOrder( metric -> assertThat(metric) @@ -516,8 +530,10 @@ void viewSdk_AllowMulitpleViewsPerAsynchronousInstrument() { .setInstrumentType(InstrumentType.OBSERVABLE_GAUGE) .setInstrumentName("test") .build(); + InMemoryMetricReader reader = new InMemoryMetricReader(); SdkMeterProvider provider = sdkMeterProviderBuilder + .registerMetricReader(reader) .registerView( selector, View.builder() @@ -539,7 +555,7 @@ void viewSdk_AllowMulitpleViewsPerAsynchronousInstrument() { .setDescription("desc") .setUnit("unit") .buildWithCallback(obs -> obs.observe(1.0)); - assertThat(provider.collectAllMetrics()) + assertThat(reader.collectAllMetrics()) .satisfiesExactlyInAnyOrder( metric -> assertThat(metric) @@ -562,8 +578,10 @@ void viewSdk_capturesBaggageFromContext() { .setInstrumentType(InstrumentType.COUNTER) .setInstrumentName("test") .build(); + InMemoryMetricReader reader = new InMemoryMetricReader(); SdkMeterProvider provider = sdkMeterProviderBuilder + .registerMetricReader(reader) .registerView( selector, View.builder() @@ -586,7 +604,7 @@ void viewSdk_capturesBaggageFromContext() { } // Now make sure all metrics have baggage appended. // Implicitly we should have ONLY ONE metric data point that has baggage appended. - assertThat(provider.collectAllMetrics()) + assertThat(reader.collectAllMetrics()) .satisfiesExactly( metric -> assertThat(metric) @@ -608,7 +626,9 @@ void collectAllAsyncInstruments_CumulativeHistogram() { sdkMeterProviderBuilder, Aggregation.explicitBucketHistogram( AggregationTemporality.CUMULATIVE, Collections.emptyList())); - SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder.registerMetricReader(sdkMeterReader).build(); Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); sdkMeter .counterBuilder("testLongSumObserver") @@ -635,7 +655,7 @@ void collectAllAsyncInstruments_CumulativeHistogram() { testClock.advance(Duration.ofNanos(50)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .allSatisfy( metric -> assertThat(metric) @@ -664,7 +684,7 @@ void collectAllAsyncInstruments_CumulativeHistogram() { testClock.advance(Duration.ofNanos(50)); - assertThat(sdkMeterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .allSatisfy( metric -> assertThat(metric) @@ -692,6 +712,159 @@ void collectAllAsyncInstruments_CumulativeHistogram() { "testDoubleValueObserver"); } + @Test + @SuppressWarnings("unchecked") + void sdkMeterProvider_supportsMultipleCollectorsCumulative() { + InMemoryMetricReader collector1 = new InMemoryMetricReader(); + InMemoryMetricReader collector2 = new InMemoryMetricReader(); + SdkMeterProvider meterProvider = + sdkMeterProviderBuilder + .registerMetricReader(collector1) + .registerMetricReader(collector2) + .build(); + Meter sdkMeter = meterProvider.get(SdkMeterProviderTest.class.getName()); + final LongCounter counter = sdkMeter.counterBuilder("testSum").build(); + final long startTime = testClock.now(); + + counter.add(1L); + testClock.advance(Duration.ofSeconds(1)); + + assertThat(collector1.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasResource(RESOURCE) + .hasName("testSum") + .hasLongSum() + .isCumulative() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasStartEpochNanos(startTime) + .hasEpochNanos(testClock.now()) + .hasValue(1))); + + counter.add(1L); + testClock.advance(Duration.ofSeconds(1)); + + // Make sure collector 2 sees the value collector 1 saw + assertThat(collector2.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasResource(RESOURCE) + .hasName("testSum") + .hasLongSum() + .isCumulative() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasStartEpochNanos(startTime) + .hasEpochNanos(testClock.now()) + .hasValue(2))); + + // Make sure Collector 1 sees the same point as 2 + assertThat(collector1.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasResource(RESOURCE) + .hasName("testSum") + .hasLongSum() + .isCumulative() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasStartEpochNanos(startTime) + .hasEpochNanos(testClock.now()) + .hasValue(2))); + } + + @Test + @SuppressWarnings("unchecked") + void sdkMeterProvider_supportsMultipleCollectorsDelta() { + // Note: we use a view to do delta aggregation, but any view ALWAYS uses double-precision right + // now. + InMemoryMetricReader collector1 = new InMemoryMetricReader(); + InMemoryMetricReader collector2 = new InMemoryMetricReader(); + SdkMeterProvider meterProvider = + sdkMeterProviderBuilder + .registerMetricReader(collector1) + .registerMetricReader(collector2) + .registerView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.COUNTER) + .setInstrumentName("testSum") + .build(), + View.builder() + .setAggregation(Aggregation.sum(AggregationTemporality.DELTA)) + .build()) + .build(); + Meter sdkMeter = meterProvider.get(SdkMeterProviderTest.class.getName()); + final LongCounter counter = sdkMeter.counterBuilder("testSum").build(); + final long startTime = testClock.now(); + + counter.add(1L); + testClock.advance(Duration.ofSeconds(1)); + + assertThat(collector1.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasResource(RESOURCE) + .hasName("testSum") + .hasLongSum() + .isDelta() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasStartEpochNanos(startTime) + .hasEpochNanos(testClock.now()) + .hasValue(1))); + long collectorOneTimeOne = testClock.now(); + + counter.add(1L); + testClock.advance(Duration.ofSeconds(1)); + + // Make sure collector 2 sees the value collector 1 saw + assertThat(collector2.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasResource(RESOURCE) + .hasName("testSum") + .hasLongSum() + .isDelta() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasStartEpochNanos(startTime) + .hasEpochNanos(testClock.now()) + .hasValue(2))); + + // Make sure Collector 1 sees the same point as 2, when it collects. + assertThat(collector1.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasResource(RESOURCE) + .hasName("testSum") + .hasLongSum() + .isDelta() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasStartEpochNanos(collectorOneTimeOne) + .hasEpochNanos(testClock.now()) + .hasValue(1))); + } + private static void registerViewForAllTypes( SdkMeterProviderBuilder meterProviderBuilder, Aggregation aggregation) { for (InstrumentType instrumentType : InstrumentType.values()) { diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterRegistryTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterRegistryTest.java index ce77cea0b21..58975a77dc7 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterRegistryTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterRegistryTest.java @@ -15,6 +15,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import org.junit.jupiter.api.Test; @@ -22,8 +23,13 @@ /** Unit tests for {@link SdkMeterProvider}. */ class SdkMeterRegistryTest { private final TestClock testClock = TestClock.create(); + private final InMemoryMetricReader sdkMeterReader = new InMemoryMetricReader(); private final SdkMeterProvider meterProvider = - SdkMeterProvider.builder().setClock(testClock).setResource(Resource.empty()).build(); + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(Resource.empty()) + .registerMetricReader(sdkMeterReader) + .build(); @Test void builder_HappyPath() { @@ -106,7 +112,7 @@ void metricProducer_GetAllMetrics() { LongCounter longCounter2 = sdkMeter2.counterBuilder("testLongCounter").build(); longCounter2.add(10, Attributes.empty()); - assertThat(meterProvider.collectAllMetrics()) + assertThat(sdkMeterReader.collectAllMetrics()) .allSatisfy( metric -> assertThat(metric) @@ -131,21 +137,21 @@ void metricProducer_GetAllMetrics() { void suppliesDefaultMeterForNullName() { SdkMeter meter = (SdkMeter) meterProvider.get(null); assertThat(meter.getInstrumentationLibraryInfo().getName()) - .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + .isEqualTo(DefaultSdkMeterProvider.DEFAULT_METER_NAME); meter = (SdkMeter) meterProvider.meterBuilder(null).build(); assertThat(meter.getInstrumentationLibraryInfo().getName()) - .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + .isEqualTo(DefaultSdkMeterProvider.DEFAULT_METER_NAME); } @Test void suppliesDefaultMeterForEmptyName() { SdkMeter meter = (SdkMeter) meterProvider.get(""); assertThat(meter.getInstrumentationLibraryInfo().getName()) - .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + .isEqualTo(DefaultSdkMeterProvider.DEFAULT_METER_NAME); meter = (SdkMeter) meterProvider.meterBuilder("").build(); assertThat(meter.getInstrumentationLibraryInfo().getName()) - .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + .isEqualTo(DefaultSdkMeterProvider.DEFAULT_METER_NAME); } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterTest.java index ec3be5771f4..d2527c4dcc8 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterTest.java @@ -17,11 +17,14 @@ import io.opentelemetry.api.metrics.LongUpDownCounter; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.metrics.internal.state.MeterSharedState; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; class SdkMeterTest { - private final SdkMeterProvider testMeterProvider = SdkMeterProvider.builder().build(); + // Meter must have an exporter configured to actual run. + private final SdkMeterProvider testMeterProvider = + SdkMeterProvider.builder().registerMetricReader(new InMemoryMetricReader()).build(); private final Meter sdkMeter = testMeterProvider.get(getClass().getName()); @RegisterExtension LogCapturer logs = LogCapturer.create().captureForType(MeterSharedState.class); diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderTest.java similarity index 68% rename from sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderTest.java rename to sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderTest.java index 353c5beb663..e20c1782233 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReaderTest.java @@ -21,6 +21,7 @@ import io.opentelemetry.sdk.metrics.data.LongSumData; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.resources.Resource; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -43,7 +44,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class IntervalMetricReaderTest { +class PeriodicMetricReaderTest { private static final List LONG_POINT_LIST = Collections.singletonList(LongPointData.create(1000, 3000, Attributes.empty(), 1234567)); @@ -72,30 +73,23 @@ void startOnlyOnce() { ScheduledFuture mock = mock(ScheduledFuture.class); when(scheduler.scheduleAtFixedRate(any(), anyLong(), anyLong(), any())).thenReturn(mock); - IntervalMetricReader intervalMetricReader = - new IntervalMetricReader( - IntervalMetricReader.InternalState.builder() - .setMetricProducers(Collections.emptyList()) - .setMetricExporter(mock(MetricExporter.class)) - .build(), - scheduler); + PeriodicMetricReaderFactory factory = + new PeriodicMetricReaderFactory( + mock(MetricExporter.class), Duration.ofMillis(1), scheduler); - intervalMetricReader.start(); - intervalMetricReader.start(); + // Starts the interval reader. + factory.apply(metricProducer); verify(scheduler, times(1)).scheduleAtFixedRate(any(), anyLong(), anyLong(), any()); } @Test - void intervalExport() throws Exception { + void periodicExport() throws Exception { WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(); - IntervalMetricReader intervalMetricReader = - IntervalMetricReader.builder() - .setExportIntervalMillis(100) - .setMetricExporter(waitingMetricExporter) - .setMetricProducers(Collections.singletonList(metricProducer)) - .buildAndStart(); + MetricReaderFactory factory = + PeriodicMetricReader.create(waitingMetricExporter, Duration.ofMillis(100)); + MetricReader reader = factory.apply(metricProducer); try { assertThat(waitingMetricExporter.waitForNumberOfExports(1)) .containsExactly(Collections.singletonList(METRIC_DATA)); @@ -104,52 +98,24 @@ void intervalExport() throws Exception { .containsExactly( Collections.singletonList(METRIC_DATA), Collections.singletonList(METRIC_DATA)); } finally { - intervalMetricReader.shutdown(); + reader.shutdown(); } } @Test - void forceFlush() throws Exception { + void flush() throws Exception { WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(); - IntervalMetricReader intervalMetricReader = - IntervalMetricReader.builder() - // Will force flush. - .setExportIntervalMillis(Long.MAX_VALUE) - .setMetricExporter(waitingMetricExporter) - .setMetricProducers(Collections.singletonList(metricProducer)) - .buildAndStart(); + MetricReaderFactory factory = + PeriodicMetricReader.create(waitingMetricExporter, Duration.ofMillis(Long.MAX_VALUE)); - assertThat(intervalMetricReader.forceFlush().join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + MetricReader reader = factory.apply(metricProducer); + assertThat(reader.flush().join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); try { assertThat(waitingMetricExporter.waitForNumberOfExports(1)) .containsExactly(Collections.singletonList(METRIC_DATA)); } finally { - intervalMetricReader.shutdown(); - } - } - - @Test - void forceFlushGlobal() throws Exception { - WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(); - IntervalMetricReader intervalMetricReader = - IntervalMetricReader.builder() - // Will force flush. - .setExportIntervalMillis(Long.MAX_VALUE) - .setMetricExporter(waitingMetricExporter) - .setMetricProducers(Collections.singletonList(metricProducer)) - .build() - .startAndRegisterGlobal(); - - assertThat(IntervalMetricReader.forceFlushGlobal().join(10, TimeUnit.SECONDS).isSuccess()) - .isTrue(); - - try { - assertThat(waitingMetricExporter.waitForNumberOfExports(1)) - .containsExactly(Collections.singletonList(METRIC_DATA)); - } finally { - intervalMetricReader.shutdown(); - IntervalMetricReader.resetGlobalForTest(); + reader.shutdown(); } } @@ -157,34 +123,26 @@ void forceFlushGlobal() throws Exception { @Timeout(2) public void intervalExport_exporterThrowsException() throws Exception { WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(/* shouldThrow=*/ true); - IntervalMetricReader intervalMetricReader = - IntervalMetricReader.builder() - .setExportIntervalMillis(100) - .setMetricExporter(waitingMetricExporter) - .setMetricProducers(Collections.singletonList(metricProducer)) - .buildAndStart(); - + MetricReaderFactory factory = + PeriodicMetricReader.create(waitingMetricExporter, Duration.ofMillis(100)); + MetricReader reader = factory.apply(metricProducer); try { assertThat(waitingMetricExporter.waitForNumberOfExports(2)) .containsExactly( Collections.singletonList(METRIC_DATA), Collections.singletonList(METRIC_DATA)); } finally { - intervalMetricReader.shutdown(); + reader.shutdown(); } } @Test void oneLastExportAfterShutdown() throws Exception { WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(); - IntervalMetricReader intervalMetricReader = - IntervalMetricReader.builder() - .setExportIntervalMillis(100_000) - .setMetricExporter(waitingMetricExporter) - .setMetricProducers(Collections.singletonList(metricProducer)) - .buildAndStart(); - + MetricReaderFactory factory = + PeriodicMetricReader.create(waitingMetricExporter, Duration.ofSeconds(100)); + MetricReader reader = factory.apply(metricProducer); // Assume that this will be called in less than 100 seconds. - intervalMetricReader.shutdown(); + reader.shutdown(); // This export was called during shutdown. assertThat(waitingMetricExporter.waitForNumberOfExports(1)) diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/export/TestCollectionHandle.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/export/TestCollectionHandle.java new file mode 100644 index 00000000000..f3f15eae798 --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/export/TestCollectionHandle.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.export; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Iterator; +import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; + +public class TestCollectionHandle { + + @Test + public void created_haveUniqueIdentity() { + Supplier supplier = CollectionHandle.createSupplier(); + CollectionHandle one = supplier.get(); + CollectionHandle two = supplier.get(); + + assertThat(one).isEqualTo(one); + assertThat(one).isNotEqualTo(two); + } + + @Test + public void mutableSet_allowsAddAndContains() { + Supplier supplier = CollectionHandle.createSupplier(); + Set mutable = CollectionHandle.mutableSet(); + CollectionHandle one = supplier.get(); + assertThat(mutable).hasSize(0); + assertThat(mutable.contains(one)).isFalse(); + mutable.add(one); + assertThat(mutable).hasSize(1); + assertThat(mutable.contains(one)).isTrue(); + + CollectionHandle two = supplier.get(); + assertThat(mutable.contains(two)).isFalse(); + mutable.add(two); + assertThat(mutable).hasSize(2); + assertThat(mutable.contains(two)).isTrue(); + } + + @Test + public void mutableSet_allowsContainsAll() { + Supplier supplier = CollectionHandle.createSupplier(); + CollectionHandle one = supplier.get(); + CollectionHandle two = supplier.get(); + CollectionHandle three = supplier.get(); + Set mutable = CollectionHandle.mutableSet(); + mutable.add(one); + mutable.add(two); + Set mutableCopy = CollectionHandle.of(one, two); + Set mutablePlus = CollectionHandle.of(one, two, three); + + assertThat(mutable.containsAll(mutableCopy)).isTrue(); + assertThat(mutable.containsAll(mutablePlus)).isFalse(); + assertThat(mutablePlus.containsAll(mutable)).isTrue(); + } + + @Test + public void mutableSet_iteratingWorks() { + Supplier supplier = CollectionHandle.createSupplier(); + CollectionHandle one = supplier.get(); + CollectionHandle two = supplier.get(); + CollectionHandle three = supplier.get(); + Set set = CollectionHandle.of(one, two, three); + assertThat(set).hasSize(3); + Iterator iterator = set.iterator(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(one); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(two); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(three); + assertThat(iterator.hasNext()).isFalse(); + // TODO: Verify next throws. + } +} diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageTest.java index ecf67bbe53e..5b5eefccfb3 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.metrics.common.InstrumentType; import io.opentelemetry.sdk.metrics.common.InstrumentValueType; import io.opentelemetry.sdk.metrics.exemplar.ExemplarFilter; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor; import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry; import io.opentelemetry.sdk.metrics.view.Aggregation; @@ -19,6 +20,7 @@ import io.opentelemetry.sdk.metrics.view.View; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -30,6 +32,8 @@ public class AsynchronousMetricStorageTest { MeterSharedState.create(InstrumentationLibraryInfo.empty()); private AttributesProcessor spyAttributesProcessor; private View view; + private CollectionHandle handle; + private Set all; @BeforeEach void setup() { @@ -51,6 +55,10 @@ void setup() { meterProviderSharedState = MeterProviderSharedState.create( testClock, Resource.empty(), viewRegistry, ExemplarFilter.sampleWithTraces()); + + handle = CollectionHandle.createSupplier().get(); + all = CollectionHandle.mutableSet(); + all.add(handle); } @Test @@ -65,9 +73,8 @@ void doubleAsynchronousAccumulator_AttributesProcessor_used() { InstrumentValueType.DOUBLE), meterProviderSharedState.getResource(), meterSharedState.getInstrumentationLibraryInfo(), - meterProviderSharedState.getStartEpochNanos(), value -> value.observe(1.0, Attributes.empty())) - .collectAndReset(0, testClock.now()); + .collectAndReset(handle, all, 0, testClock.now()); Mockito.verify(spyAttributesProcessor).process(Attributes.empty(), Context.current()); } @@ -83,9 +90,8 @@ void longAsynchronousAccumulator_AttributesProcessor_used() { InstrumentValueType.LONG), meterProviderSharedState.getResource(), meterSharedState.getInstrumentationLibraryInfo(), - meterProviderSharedState.getStartEpochNanos(), value -> value.observe(1, Attributes.empty())) - .collectAndReset(0, testClock.nanoTime()); + .collectAndReset(handle, all, 0, testClock.nanoTime()); Mockito.verify(spyAttributesProcessor).process(Attributes.empty(), Context.current()); } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/DeltaAccumulationTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/DeltaAccumulationTest.java new file mode 100644 index 00000000000..018a1c2b3ef --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/DeltaAccumulationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.state; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DeltaAccumulationTest { + private CollectionHandle handle1; + private CollectionHandle handle2; + private Set all; + + @BeforeEach + void setup() { + Supplier supplier = CollectionHandle.createSupplier(); + handle1 = supplier.get(); + handle2 = supplier.get(); + all = CollectionHandle.mutableSet(); + all.add(handle1); + all.add(handle2); + } + + @Test + void wasReadBy_works() { + Map measurement = new HashMap<>(); + measurement.put(Attributes.empty(), 1L); + DeltaAccumulation accumlation = new DeltaAccumulation<>(measurement); + assertThat(accumlation.wasReadBy(handle1)).isFalse(); + assertThat(accumlation.wasReadBy(handle2)).isFalse(); + assertThat(accumlation.wasReadByAll(all)).isFalse(); + + // Read and check. + assertThat(accumlation.read(handle1)).isEqualTo(measurement); + assertThat(accumlation.wasReadBy(handle1)).isTrue(); + assertThat(accumlation.wasReadBy(handle2)).isFalse(); + assertThat(accumlation.wasReadByAll(all)).isFalse(); + + // Read and check. + assertThat(accumlation.read(handle2)).isEqualTo(measurement); + assertThat(accumlation.wasReadBy(handle1)).isTrue(); + assertThat(accumlation.wasReadBy(handle2)).isTrue(); + assertThat(accumlation.wasReadByAll(all)).isTrue(); + } +} diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/DeltaMetricStorageTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/DeltaMetricStorageTest.java new file mode 100644 index 00000000000..cf40929f6a7 --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/DeltaMetricStorageTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.state; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.metrics.MetricAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.exemplar.ExemplarReservoir; +import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.internal.aggregator.DoubleAccumulation; +import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DeltaMetricStorageTest { + private static final InstrumentDescriptor DESCRIPTOR = + InstrumentDescriptor.create( + "name", "description", "unit", InstrumentType.COUNTER, InstrumentValueType.DOUBLE); + private static final MetricDescriptor METRIC_DESCRIPTOR = + MetricDescriptor.create("name", "description", "unit"); + + private CollectionHandle collector1; + private CollectionHandle collector2; + private Set allCollectors; + private DeltaMetricStorage storage; + + @BeforeEach + void setup() { + Supplier supplier = CollectionHandle.createSupplier(); + collector1 = supplier.get(); + collector2 = supplier.get(); + allCollectors = CollectionHandle.mutableSet(); + allCollectors.add(collector1); + allCollectors.add(collector2); + storage = + new DeltaMetricStorage<>( + AggregatorFactory.sum(AggregationTemporality.CUMULATIVE) + .create( + Resource.empty(), + InstrumentationLibraryInfo.create("test", "1.0"), + DESCRIPTOR, + METRIC_DESCRIPTOR, + ExemplarReservoir::noSamples)); + } + + @Test + void collectionDeltaForMultiReader() { + BoundStorageHandle bound = storage.bind(Attributes.empty()); + bound.recordDouble(1, Attributes.empty(), Context.root()); + // First collector only sees first recording. + assertThat(storage.collectFor(collector1, allCollectors)) + .hasSize(1) + .hasEntrySatisfying(Attributes.empty(), value -> assertThat(value.getValue()).isEqualTo(1)); + + bound.recordDouble(2, Attributes.empty(), Context.root()); + // First collector only sees second recording. + assertThat(storage.collectFor(collector1, allCollectors)) + .hasSize(1) + .hasEntrySatisfying(Attributes.empty(), value -> assertThat(value.getValue()).isEqualTo(2)); + + // First collector no longer sees a recording. + assertThat(storage.collectFor(collector1, allCollectors)).isEmpty(); + + // Second collector gets merged recordings + assertThat(storage.collectFor(collector2, allCollectors)) + .hasSize(1) + .hasEntrySatisfying(Attributes.empty(), value -> assertThat(value.getValue()).isEqualTo(3)); + + // Second collector no longer sees a recording. + assertThat(storage.collectFor(collector2, allCollectors)).isEmpty(); + } +} diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java index 1cc8aa4eacb..6bb7fc83b09 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java @@ -12,6 +12,8 @@ import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import java.util.Set; import org.junit.jupiter.api.Test; /** Unit tests for {@link MetricStorageRegistry}. */ @@ -81,7 +83,11 @@ public MetricDescriptor getMetricDescriptor() { } @Override - public MetricData collectAndReset(long startEpochNanos, long epochNanos) { + public MetricData collectAndReset( + CollectionHandle collector, + Set all, + long startEpochNanos, + long epochNanos) { return null; } @@ -105,7 +111,11 @@ public MetricDescriptor getMetricDescriptor() { } @Override - public MetricData collectAndReset(long startEpochNanos, long epochNanos) { + public MetricData collectAndReset( + CollectionHandle collector, + Set allCollectors, + long startEpochNanos, + long epochNanos) { return null; } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorageTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorageTest.java index dc42f95aa8c..bc1ff56f40f 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorageTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/SynchronousMetricStorageTest.java @@ -20,9 +20,12 @@ import io.opentelemetry.sdk.metrics.internal.aggregator.Aggregator; import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorFactory; import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -40,19 +43,24 @@ public class SynchronousMetricStorageTest { InstrumentationLibraryInfo.create("test", "1.0"), DESCRIPTOR, METRIC_DESCRIPTOR, - // TODO: do we need to verify anything here? ExemplarReservoir::noSamples); private final AttributesProcessor attributesProcessor = AttributesProcessor.noop(); + private CollectionHandle collector; + private Set allCollectors; + + @BeforeEach + void setup() { + collector = CollectionHandle.createSupplier().get(); + allCollectors = CollectionHandle.mutableSet(); + allCollectors.add(collector); + } @Test void attributesProcessor_used() { AttributesProcessor spyAttributesProcessor = Mockito.spy(this.attributesProcessor); SynchronousMetricStorage accumulator = new DefaultSynchronousMetricStorage<>( - METRIC_DESCRIPTOR, - aggregator, - new InstrumentProcessor<>(aggregator, testClock.now()), - spyAttributesProcessor); + METRIC_DESCRIPTOR, aggregator, spyAttributesProcessor); accumulator.bind(Attributes.empty()); Mockito.verify(spyAttributesProcessor).process(Attributes.empty(), Context.current()); } @@ -64,14 +72,10 @@ void attributesProcessor_applied() { AttributesProcessor.append(Attributes.builder().put("modifiedK", "modifiedV").build()); AttributesProcessor spyLabelsProcessor = Mockito.spy(attributesProcessor); SynchronousMetricStorage accumulator = - new DefaultSynchronousMetricStorage<>( - METRIC_DESCRIPTOR, - aggregator, - new InstrumentProcessor<>(aggregator, testClock.now()), - spyLabelsProcessor); + new DefaultSynchronousMetricStorage<>(METRIC_DESCRIPTOR, aggregator, spyLabelsProcessor); BoundStorageHandle handle = accumulator.bind(labels); handle.recordDouble(1, labels, Context.root()); - MetricData md = accumulator.collectAndReset(0, testClock.now()); + MetricData md = accumulator.collectAndReset(collector, allCollectors, 0, testClock.now()); assertThat(md) .hasDoubleGauge() .points() @@ -87,17 +91,13 @@ void attributesProcessor_applied() { @Test void sameAggregator_ForSameAttributes() { SynchronousMetricStorage accumulator = - new DefaultSynchronousMetricStorage<>( - METRIC_DESCRIPTOR, - aggregator, - new InstrumentProcessor<>(aggregator, testClock.now()), - attributesProcessor); + new DefaultSynchronousMetricStorage<>(METRIC_DESCRIPTOR, aggregator, attributesProcessor); BoundStorageHandle handle = accumulator.bind(Attributes.builder().put("K", "V").build()); BoundStorageHandle duplicateHandle = accumulator.bind(Attributes.builder().put("K", "V").build()); try { assertThat(duplicateHandle).isSameAs(handle); - accumulator.collectAndReset(0, testClock.now()); + accumulator.collectAndReset(collector, allCollectors, 0, testClock.now()); BoundStorageHandle anotherDuplicateAggregatorHandle = accumulator.bind(Attributes.builder().put("K", "V").build()); try { @@ -112,6 +112,6 @@ void sameAggregator_ForSameAttributes() { // If we try to collect once all bound references are gone AND no recordings have occurred, we // should not see any labels (or metric). - assertThat(accumulator.collectAndReset(0, testClock.now())).isNull(); + assertThat(accumulator.collectAndReset(collector, allCollectors, 0, testClock.now())).isNull(); } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/TemporalMetricStorageTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/TemporalMetricStorageTest.java new file mode 100644 index 00000000000..fc13721ed98 --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/TemporalMetricStorageTest.java @@ -0,0 +1,245 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.state; + +import static io.opentelemetry.sdk.testing.assertj.metrics.MetricAssertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.exemplar.ExemplarReservoir; +import io.opentelemetry.sdk.metrics.internal.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.internal.aggregator.DoubleAccumulation; +import io.opentelemetry.sdk.metrics.internal.descriptor.MetricDescriptor; +import io.opentelemetry.sdk.metrics.internal.export.CollectionHandle; +import io.opentelemetry.sdk.resources.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TemporalMetricStorageTest { + private static final InstrumentDescriptor DESCRIPTOR = + InstrumentDescriptor.create( + "name", "description", "unit", InstrumentType.COUNTER, InstrumentValueType.DOUBLE); + private static final InstrumentDescriptor ASYNC_DESCRIPTOR = + InstrumentDescriptor.create( + "name", "description", "unit", InstrumentType.OBSERVABLE_SUM, InstrumentValueType.DOUBLE); + private static final MetricDescriptor METRIC_DESCRIPTOR = + MetricDescriptor.create("name", "description", "unit"); + private static final Aggregator CUMULATIVE_SUM = + AggregatorFactory.sum(AggregationTemporality.CUMULATIVE) + .create( + Resource.empty(), + InstrumentationLibraryInfo.create("test", "1.0"), + DESCRIPTOR, + METRIC_DESCRIPTOR, + ExemplarReservoir::noSamples); + private static final Aggregator DELTA_SUM = + AggregatorFactory.sum(AggregationTemporality.DELTA) + .create( + Resource.empty(), + InstrumentationLibraryInfo.create("test", "1.0"), + DESCRIPTOR, + METRIC_DESCRIPTOR, + ExemplarReservoir::noSamples); + + private static final Aggregator ASYNC_CUMULATIVE_SUM = + AggregatorFactory.sum(AggregationTemporality.CUMULATIVE) + .create( + Resource.empty(), + InstrumentationLibraryInfo.create("test", "1.0"), + ASYNC_DESCRIPTOR, + METRIC_DESCRIPTOR, + ExemplarReservoir::noSamples); + + private static final Aggregator ASYNC_DELTA_SUM = + AggregatorFactory.sum(AggregationTemporality.DELTA) + .create( + Resource.empty(), + InstrumentationLibraryInfo.create("test", "1.0"), + ASYNC_DESCRIPTOR, + METRIC_DESCRIPTOR, + ExemplarReservoir::noSamples); + + private CollectionHandle collector1; + private CollectionHandle collector2; + private Set allCollectors; + + @BeforeEach + void setup() { + Supplier supplier = CollectionHandle.createSupplier(); + collector1 = supplier.get(); + collector2 = supplier.get(); + allCollectors = CollectionHandle.mutableSet(); + allCollectors.add(collector1); + allCollectors.add(collector2); + } + + private static Map createMeasurement(double value) { + Map measurement = new HashMap<>(); + measurement.put(Attributes.empty(), DoubleAccumulation.create(value)); + return measurement; + } + + @Test + void synchronousCumulative_joinsWithLastMeasurementForCumulative() { + TemporalMetricStorage storage = + new TemporalMetricStorage<>(CUMULATIVE_SUM, /* isSynchronous= */ true); + // Send in new measurement at time 10 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 10)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(10).hasValue(3)); + // Send in new measurement at time 30 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 30)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(30).hasValue(6)); + // Send in new measurement at time 40 for collector 2 + assertThat(storage.buildMetricFor(collector2, createMeasurement(4), 0, 60)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(60).hasValue(4)); + // Send in new measurement at time 35 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(2), 0, 35)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(35).hasValue(8)); + } + + @Test + void synchronousDelta_useLastTimestamp() { + TemporalMetricStorage storage = + new TemporalMetricStorage<>(DELTA_SUM, /* isSynchronous= */ true); + // Send in new measurement at time 10 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 10)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(10).hasValue(3)); + // Send in new measurement at time 30 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 30)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(10).hasEpochNanos(30).hasValue(3)); + // Send in new measurement at time 40 for collector 2 + assertThat(storage.buildMetricFor(collector2, createMeasurement(4), 0, 60)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(60).hasValue(4)); + // Send in new measurement at time 35 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(2), 0, 35)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(30).hasEpochNanos(35).hasValue(2)); + } + + @Test + void asynchronousCumulative_doesNotJoin() { + TemporalMetricStorage storage = + new TemporalMetricStorage<>(ASYNC_CUMULATIVE_SUM, /* isSynchronous= */ false); + // Send in new measurement at time 10 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 10)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(10).hasValue(3)); + // Send in new measurement at time 30 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 30)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(30).hasValue(3)); + // Send in new measurement at time 40 for collector 2 + assertThat(storage.buildMetricFor(collector2, createMeasurement(4), 0, 60)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(60).hasValue(4)); + // Send in new measurement at time 35 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(2), 0, 35)) + .hasDoubleSum() + .isCumulative() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(35).hasValue(2)); + } + + @Test + void asynchronousDelta_diffsLastTimestamp() { + TemporalMetricStorage storage = + new TemporalMetricStorage<>(ASYNC_DELTA_SUM, /* isSynchronous= */ false); + // Send in new measurement at time 10 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 10)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(10).hasValue(3)); + // Send in new measurement at time 30 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(3), 0, 30)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(10).hasEpochNanos(30).hasValue(0)); + // Send in new measurement at time 40 for collector 2 + assertThat(storage.buildMetricFor(collector2, createMeasurement(4), 0, 60)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(0).hasEpochNanos(60).hasValue(4)); + // Send in new measurement at time 35 for collector 1 + assertThat(storage.buildMetricFor(collector1, createMeasurement(2), 0, 35)) + .hasDoubleSum() + .isDelta() + .points() + .isNotEmpty() + .satisfiesExactly( + point -> assertThat(point).hasStartEpochNanos(30).hasEpochNanos(35).hasValue(-1)); + } +} diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricReaderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricReaderTest.java new file mode 100644 index 00000000000..d33aadf4e36 --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricReaderTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.testing; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.view.Aggregation; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link InMemoryMetricExporter}. */ +class InMemoryMetricReaderTest { + + private SdkMeterProvider provider; + private InMemoryMetricReader reader; + + @BeforeEach + void setup() { + reader = new InMemoryMetricReader(); + provider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setInstrumentType(InstrumentType.COUNTER).build(), + View.builder() + .setAggregation(Aggregation.sum(AggregationTemporality.DELTA)) + .build()) + .build(); + } + + private void generateFakeMetric(int index) { + provider.get("test").counterBuilder("test" + index).build().add(1); + } + + @Test + void test_collectAllMetrics() { + generateFakeMetric(1); + generateFakeMetric(2); + generateFakeMetric(3); + + assertThat(reader.collectAllMetrics()).hasSize(3); + } + + @Test + void test_reset() { + generateFakeMetric(1); + generateFakeMetric(2); + generateFakeMetric(3); + + assertThat(reader.collectAllMetrics()).hasSize(3); + assertThat(reader.collectAllMetrics()).hasSize(0); + } + + @Test + void test_flush() { + generateFakeMetric(1); + generateFakeMetric(2); + generateFakeMetric(3); + // TODO: Better assertions for CompletableResultCode. + assertThat(reader.flush()).isNotNull(); + assertThat(reader.collectAllMetrics()).hasSize(0); + } + + @Test + void test_shutdown() { + generateFakeMetric(1); + assertThat(reader.collectAllMetrics()).hasSize(1); + assertThat(reader.shutdown()).isNotNull(); + assertThat(reader.collectAllMetrics()).hasSize(0); + } +} diff --git a/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorCpuBenchmark.java b/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorCpuBenchmark.java index bc4cb6e8277..1282e568025 100644 --- a/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorCpuBenchmark.java +++ b/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorCpuBenchmark.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import java.util.concurrent.TimeUnit; @@ -34,7 +34,7 @@ public class BatchSpanProcessorCpuBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { - private MetricProducer collector; + private InMemoryMetricReader metricReader; private BatchSpanProcessor processor; private Tracer tracer; private int numThreads = 1; @@ -47,9 +47,8 @@ public static class BenchmarkState { @Setup(Level.Iteration) public final void setup() { - final SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); - // Note: these will (likely) no longer be the same in future SDK. - collector = sdkMeterProvider; + metricReader = new InMemoryMetricReader(); + SdkMeterProvider.builder().registerMetricReader(metricReader).buildAndRegisterGlobal(); SpanExporter exporter = new DelayingSpanExporter(delayMs); processor = BatchSpanProcessor.builder(exporter).build(); tracer = @@ -59,7 +58,7 @@ public final void setup() { @TearDown(Level.Iteration) public final void recordMetrics() { BatchSpanProcessorMetrics metrics = - new BatchSpanProcessorMetrics(collector.collectAllMetrics(), numThreads); + new BatchSpanProcessorMetrics(metricReader.collectAllMetrics(), numThreads); exportedSpans = metrics.exportedSpans(); droppedSpans = metrics.droppedSpans(); } diff --git a/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorDroppedSpansBenchmark.java b/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorDroppedSpansBenchmark.java index b3ae91ab47a..bdf5af27c27 100644 --- a/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorDroppedSpansBenchmark.java +++ b/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorDroppedSpansBenchmark.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import org.openjdk.jmh.annotations.AuxCounters; @@ -28,7 +28,7 @@ public class BatchSpanProcessorDroppedSpansBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { - private MetricProducer collector; + private InMemoryMetricReader metricReader; private BatchSpanProcessor processor; private Tracer tracer; private double dropRatio; @@ -38,9 +38,8 @@ public static class BenchmarkState { @Setup(Level.Iteration) public final void setup() { - final SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); - // Note: these will (likely) no longer be the same in future SDK. - collector = sdkMeterProvider; + metricReader = new InMemoryMetricReader(); + SdkMeterProvider.builder().registerMetricReader(metricReader).buildAndRegisterGlobal(); SpanExporter exporter = new DelayingSpanExporter(0); processor = BatchSpanProcessor.builder(exporter).build(); @@ -50,7 +49,7 @@ public final void setup() { @TearDown(Level.Iteration) public final void recordMetrics() { BatchSpanProcessorMetrics metrics = - new BatchSpanProcessorMetrics(collector.collectAllMetrics(), numThreads); + new BatchSpanProcessorMetrics(metricReader.collectAllMetrics(), numThreads); dropRatio = metrics.dropRatio(); exportedSpans = metrics.exportedSpans(); droppedSpans = metrics.droppedSpans(); diff --git a/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMultiThreadBenchmark.java b/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMultiThreadBenchmark.java index b489bf4febe..c1f1131e7f8 100644 --- a/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMultiThreadBenchmark.java +++ b/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMultiThreadBenchmark.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricReader; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import java.util.concurrent.TimeUnit; @@ -32,7 +32,7 @@ public class BatchSpanProcessorMultiThreadBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { - private MetricProducer collector; + private InMemoryMetricReader collector; private BatchSpanProcessor processor; private Tracer tracer; private int numThreads = 1; @@ -45,9 +45,8 @@ public static class BenchmarkState { @Setup(Level.Iteration) public final void setup() { - final SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); - // Note: these will (likely) no longer be the same in future SDK. - collector = sdkMeterProvider; + collector = new InMemoryMetricReader(); + SdkMeterProvider.builder().registerMetricReader(collector).buildAndRegisterGlobal(); SpanExporter exporter = new DelayingSpanExporter(delayMs); processor = BatchSpanProcessor.builder(exporter).build(); tracer =