From b927d9daf4de0211411c522aae081edf9f39cfa6 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Wed, 9 Oct 2024 20:17:14 +0200 Subject: [PATCH] Stdout exporter for span and metrics (#6750) --- .../internal/ExporterBuilderUtil.java | 92 ++++++ .../otlp/OtlpJsonLoggingMetricExporter.java | 77 ++--- .../otlp/OtlpJsonLoggingSpanExporter.java | 51 +--- .../LoggingLogRecordExporterProvider.java | 3 +- .../logs/OtlpStdoutLogRecordExporter.java | 2 +- ...outLogRecordExporterComponentProvider.java | 2 +- .../OtlpStdoutLogRecordExporterProvider.java | 3 +- .../LoggingMetricExporterProvider.java | 5 +- .../metrics/OtlpStdoutMetricExporter.java | 115 +++++++ .../OtlpStdoutMetricExporterBuilder.java | 124 ++++++++ ...StdoutMetricExporterComponentProvider.java | 41 +++ .../OtlpStdoutMetricExporterProvider.java | 34 +++ .../LoggingSpanExporterProvider.java | 5 +- .../traces/OtlpStdoutSpanExporter.java | 91 ++++++ .../traces/OtlpStdoutSpanExporterBuilder.java | 76 +++++ ...lpStdoutSpanExporterComponentProvider.java | 36 +++ .../OtlpStdoutSpanExporterProvider.java | 29 ++ ...toconfigure.spi.internal.ComponentProvider | 2 + ...metrics.ConfigurableMetricExporterProvider | 4 +- ...pi.traces.ConfigurableSpanExporterProvider | 3 +- .../otlp/AbstractOtlpStdoutExporterTest.java | 287 ++++++++++++++++++ .../OtlpJsonLoggingMetricExporterTest.java | 118 +------ .../otlp/OtlpJsonLoggingSpanExporterTest.java | 148 +-------- .../otlp/OtlpStdoutLogRecordExporterTest.java | 270 +--------------- .../otlp/OtlpStdoutMetricExporterTest.java | 124 ++++++++ .../otlp/OtlpStdoutSpanExporterTest.java | 44 +++ .../logging/otlp/TestDataExporter.java | 135 ++++++++ .../resources/expected-metrics-wrapper.json | 93 ++++++ .../src/test/resources/expected-metrics.json | 89 ++++++ .../resources/expected-spans-wrapper.json | 99 ++++++ .../src/test/resources/expected-spans.json | 95 ++++++ .../otlp/internal/OtlpConfigUtil.java | 99 ------ .../OtlpMetricExporterComponentProvider.java | 9 +- .../internal/OtlpMetricExporterProvider.java | 9 +- .../otlp/internal/OtlpConfigUtilTest.java | 10 +- 35 files changed, 1704 insertions(+), 720 deletions(-) rename exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/{ => metrics}/LoggingMetricExporterProvider.java (83%) create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporter.java create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterBuilder.java create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterComponentProvider.java create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterProvider.java rename exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/{ => traces}/LoggingSpanExporterProvider.java (83%) create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporter.java create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterBuilder.java create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterComponentProvider.java create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterProvider.java create mode 100644 exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/AbstractOtlpStdoutExporterTest.java create mode 100644 exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutMetricExporterTest.java create mode 100644 exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutSpanExporterTest.java create mode 100644 exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json create mode 100644 exporters/logging-otlp/src/test/resources/expected-metrics.json create mode 100644 exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json create mode 100644 exporters/logging-otlp/src/test/resources/expected-spans.json diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/ExporterBuilderUtil.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/ExporterBuilderUtil.java index 81caace0178..4e05183bb1a 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/ExporterBuilderUtil.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/ExporterBuilderUtil.java @@ -13,6 +13,8 @@ import io.opentelemetry.sdk.common.export.MemoryMode; import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; import java.net.URI; @@ -96,5 +98,95 @@ public static void configureHistogramDefaultAggregation( } } + /** + * Invoke the {@code aggregationTemporalitySelectorConsumer} with the configured {@link + * AggregationTemporality}. + */ + public static void configureOtlpAggregationTemporality( + ConfigProperties config, + Consumer aggregationTemporalitySelectorConsumer) { + String temporalityStr = config.getString("otel.exporter.otlp.metrics.temporality.preference"); + if (temporalityStr == null) { + return; + } + AggregationTemporalitySelector temporalitySelector; + switch (temporalityStr.toLowerCase(Locale.ROOT)) { + case "cumulative": + temporalitySelector = AggregationTemporalitySelector.alwaysCumulative(); + break; + case "delta": + temporalitySelector = AggregationTemporalitySelector.deltaPreferred(); + break; + case "lowmemory": + temporalitySelector = AggregationTemporalitySelector.lowMemory(); + break; + default: + throw new ConfigurationException("Unrecognized aggregation temporality: " + temporalityStr); + } + aggregationTemporalitySelectorConsumer.accept(temporalitySelector); + } + + public static void configureOtlpAggregationTemporality( + StructuredConfigProperties config, + Consumer aggregationTemporalitySelectorConsumer) { + String temporalityStr = config.getString("temporality_preference"); + if (temporalityStr == null) { + return; + } + AggregationTemporalitySelector temporalitySelector; + switch (temporalityStr.toLowerCase(Locale.ROOT)) { + case "cumulative": + temporalitySelector = AggregationTemporalitySelector.alwaysCumulative(); + break; + case "delta": + temporalitySelector = AggregationTemporalitySelector.deltaPreferred(); + break; + case "lowmemory": + temporalitySelector = AggregationTemporalitySelector.lowMemory(); + break; + default: + throw new ConfigurationException("Unrecognized temporality_preference: " + temporalityStr); + } + aggregationTemporalitySelectorConsumer.accept(temporalitySelector); + } + + /** + * Invoke the {@code defaultAggregationSelectorConsumer} with the configured {@link + * DefaultAggregationSelector}. + */ + public static void configureOtlpHistogramDefaultAggregation( + ConfigProperties config, + Consumer defaultAggregationSelectorConsumer) { + String defaultHistogramAggregation = + config.getString("otel.exporter.otlp.metrics.default.histogram.aggregation"); + if (defaultHistogramAggregation != null) { + configureHistogramDefaultAggregation( + defaultHistogramAggregation, defaultAggregationSelectorConsumer); + } + } + + /** + * Invoke the {@code defaultAggregationSelectorConsumer} with the configured {@link + * DefaultAggregationSelector}. + */ + public static void configureOtlpHistogramDefaultAggregation( + StructuredConfigProperties config, + Consumer defaultAggregationSelectorConsumer) { + String defaultHistogramAggregation = config.getString("default_histogram_aggregation"); + if (defaultHistogramAggregation == null) { + return; + } + if (AggregationUtil.aggregationName(Aggregation.base2ExponentialBucketHistogram()) + .equalsIgnoreCase(defaultHistogramAggregation)) { + defaultAggregationSelectorConsumer.accept( + DefaultAggregationSelector.getDefault() + .with(InstrumentType.HISTOGRAM, Aggregation.base2ExponentialBucketHistogram())); + } else if (!AggregationUtil.aggregationName(explicitBucketHistogram()) + .equalsIgnoreCase(defaultHistogramAggregation)) { + throw new ConfigurationException( + "Unrecognized default_histogram_aggregation: " + defaultHistogramAggregation); + } + } + private ExporterBuilderUtil() {} } diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java index 0f66cc57c95..b42ef4acab7 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java @@ -5,21 +5,14 @@ package io.opentelemetry.exporter.logging.otlp; -import static io.opentelemetry.exporter.logging.otlp.internal.writer.JsonUtil.JSON_FACTORY; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.io.SegmentedStringWriter; -import io.opentelemetry.exporter.internal.otlp.metrics.ResourceMetricsMarshaler; -import io.opentelemetry.exporter.logging.otlp.internal.writer.JsonUtil; +import io.opentelemetry.exporter.logging.otlp.internal.metrics.OtlpStdoutMetricExporter; +import io.opentelemetry.exporter.logging.otlp.internal.metrics.OtlpStdoutMetricExporterBuilder; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.MetricExporter; -import java.io.IOException; import java.util.Collection; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; import java.util.logging.Logger; /** @@ -31,16 +24,16 @@ public final class OtlpJsonLoggingMetricExporter implements MetricExporter { private static final Logger logger = Logger.getLogger(OtlpJsonLoggingMetricExporter.class.getName()); - private final AtomicBoolean isShutdown = new AtomicBoolean(); - private final AggregationTemporality aggregationTemporality; + private final OtlpStdoutMetricExporter delegate; + /** * Returns a new {@link OtlpJsonLoggingMetricExporter} with a aggregation temporality of {@link * AggregationTemporality#CUMULATIVE}. */ public static MetricExporter create() { - return new OtlpJsonLoggingMetricExporter(AggregationTemporality.CUMULATIVE); + return create(AggregationTemporality.CUMULATIVE); } /** @@ -48,13 +41,32 @@ public static MetricExporter create() { * aggregationTemporality}. */ public static MetricExporter create(AggregationTemporality aggregationTemporality) { - return new OtlpJsonLoggingMetricExporter(aggregationTemporality); + OtlpStdoutMetricExporter delegate = + new OtlpStdoutMetricExporterBuilder(logger).setWrapperJsonObject(false).build(); + return new OtlpJsonLoggingMetricExporter(delegate, aggregationTemporality); } - private OtlpJsonLoggingMetricExporter(AggregationTemporality aggregationTemporality) { + OtlpJsonLoggingMetricExporter( + OtlpStdoutMetricExporter delegate, AggregationTemporality aggregationTemporality) { + this.delegate = delegate; this.aggregationTemporality = aggregationTemporality; } + @Override + public CompletableResultCode export(Collection logs) { + return delegate.export(logs); + } + + @Override + public CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + /** * Return the aggregation temporality. * @@ -69,41 +81,4 @@ public AggregationTemporality getPreferredTemporality() { public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { return aggregationTemporality; } - - @Override - public CompletableResultCode export(Collection metrics) { - if (isShutdown.get()) { - return CompletableResultCode.ofFailure(); - } - - ResourceMetricsMarshaler[] allResourceMetrics = ResourceMetricsMarshaler.create(metrics); - for (ResourceMetricsMarshaler resourceMetrics : allResourceMetrics) { - SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); - try (JsonGenerator gen = JsonUtil.create(sw)) { - resourceMetrics.writeJsonTo(gen); - } catch (IOException e) { - // Shouldn't happen in practice, just skip it. - continue; - } - try { - logger.log(Level.INFO, sw.getAndClear()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to read OTLP JSON metrics", e); - } - } - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode flush() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode shutdown() { - if (!isShutdown.compareAndSet(false, true)) { - logger.log(Level.INFO, "Calling shutdown() multiple times."); - } - return CompletableResultCode.ofSuccess(); - } } diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporter.java index cc944e9bab6..63901351326 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporter.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporter.java @@ -5,19 +5,12 @@ package io.opentelemetry.exporter.logging.otlp; -import static io.opentelemetry.exporter.logging.otlp.internal.writer.JsonUtil.JSON_FACTORY; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.io.SegmentedStringWriter; -import io.opentelemetry.exporter.internal.otlp.traces.ResourceSpansMarshaler; -import io.opentelemetry.exporter.logging.otlp.internal.writer.JsonUtil; +import io.opentelemetry.exporter.logging.otlp.internal.traces.OtlpStdoutSpanExporter; +import io.opentelemetry.exporter.logging.otlp.internal.traces.OtlpStdoutSpanExporterBuilder; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.io.IOException; import java.util.Collection; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; import java.util.logging.Logger; /** @@ -29,49 +22,31 @@ public final class OtlpJsonLoggingSpanExporter implements SpanExporter { private static final Logger logger = Logger.getLogger(OtlpJsonLoggingSpanExporter.class.getName()); - private final AtomicBoolean isShutdown = new AtomicBoolean(); + private final OtlpStdoutSpanExporter delegate; /** Returns a new {@link OtlpJsonLoggingSpanExporter}. */ public static SpanExporter create() { - return new OtlpJsonLoggingSpanExporter(); + OtlpStdoutSpanExporter delegate = + new OtlpStdoutSpanExporterBuilder(logger).setWrapperJsonObject(false).build(); + return new OtlpJsonLoggingSpanExporter(delegate); } - private OtlpJsonLoggingSpanExporter() {} + OtlpJsonLoggingSpanExporter(OtlpStdoutSpanExporter delegate) { + this.delegate = delegate; + } @Override - public CompletableResultCode export(Collection spans) { - if (isShutdown.get()) { - return CompletableResultCode.ofFailure(); - } - - ResourceSpansMarshaler[] allResourceSpans = ResourceSpansMarshaler.create(spans); - for (ResourceSpansMarshaler resourceSpans : allResourceSpans) { - SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); - try (JsonGenerator gen = JsonUtil.create(sw)) { - resourceSpans.writeJsonTo(gen); - } catch (IOException e) { - // Shouldn't happen in practice, just skip it. - continue; - } - try { - logger.log(Level.INFO, sw.getAndClear()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to read OTLP JSON spans", e); - } - } - return CompletableResultCode.ofSuccess(); + public CompletableResultCode export(Collection logs) { + return delegate.export(logs); } @Override public CompletableResultCode flush() { - return CompletableResultCode.ofSuccess(); + return delegate.flush(); } @Override public CompletableResultCode shutdown() { - if (!isShutdown.compareAndSet(false, true)) { - logger.log(Level.INFO, "Calling shutdown() multiple times."); - } - return CompletableResultCode.ofSuccess(); + return delegate.shutdown(); } } diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/LoggingLogRecordExporterProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/LoggingLogRecordExporterProvider.java index a08aafee355..5e038f3c892 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/LoggingLogRecordExporterProvider.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/LoggingLogRecordExporterProvider.java @@ -16,7 +16,8 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public class LoggingLogRecordExporterProvider implements ConfigurableLogRecordExporterProvider { +public final class LoggingLogRecordExporterProvider + implements ConfigurableLogRecordExporterProvider { @Override public LogRecordExporter createExporter(ConfigProperties config) { diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporter.java index 6a7adb6f742..96331e56d23 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporter.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporter.java @@ -23,7 +23,7 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public class OtlpStdoutLogRecordExporter implements LogRecordExporter { +public final class OtlpStdoutLogRecordExporter implements LogRecordExporter { private static final Logger LOGGER = Logger.getLogger(OtlpStdoutLogRecordExporter.class.getName()); diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterComponentProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterComponentProvider.java index 204f5673c08..0806b7f0b40 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterComponentProvider.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterComponentProvider.java @@ -15,7 +15,7 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public class OtlpStdoutLogRecordExporterComponentProvider +public final class OtlpStdoutLogRecordExporterComponentProvider implements ComponentProvider { @Override diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterProvider.java index b73262cea39..23ba0079295 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterProvider.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterProvider.java @@ -15,7 +15,8 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public class OtlpStdoutLogRecordExporterProvider implements ConfigurableLogRecordExporterProvider { +public final class OtlpStdoutLogRecordExporterProvider + implements ConfigurableLogRecordExporterProvider { @Override public LogRecordExporter createExporter(ConfigProperties config) { OtlpStdoutLogRecordExporterBuilder builder = OtlpStdoutLogRecordExporter.builder(); diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingMetricExporterProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/LoggingMetricExporterProvider.java similarity index 83% rename from exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingMetricExporterProvider.java rename to exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/LoggingMetricExporterProvider.java index b5669b5426a..6748bbd99d1 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingMetricExporterProvider.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/LoggingMetricExporterProvider.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.exporter.logging.otlp.internal; +package io.opentelemetry.exporter.logging.otlp.internal.metrics; import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingMetricExporter; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -16,7 +16,8 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public class LoggingMetricExporterProvider implements ConfigurableMetricExporterProvider { +public final class LoggingMetricExporterProvider implements ConfigurableMetricExporterProvider { + @Override public MetricExporter createExporter(ConfigProperties config) { return OtlpJsonLoggingMetricExporter.create(); diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporter.java new file mode 100644 index 00000000000..81e9bef105c --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporter.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.metrics; + +import io.opentelemetry.exporter.internal.otlp.metrics.MetricsRequestMarshaler; +import io.opentelemetry.exporter.internal.otlp.metrics.ResourceMetricsMarshaler; +import io.opentelemetry.exporter.logging.otlp.internal.writer.JsonWriter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.util.Collection; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Exporter for sending OTLP metrics to stdout. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutMetricExporter implements MetricExporter { + + private static final Logger LOGGER = Logger.getLogger(OtlpStdoutMetricExporter.class.getName()); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + private final Logger logger; + private final JsonWriter jsonWriter; + private final boolean wrapperJsonObject; + private final AggregationTemporalitySelector aggregationTemporalitySelector; + private final DefaultAggregationSelector defaultAggregationSelector; + + OtlpStdoutMetricExporter( + Logger logger, + JsonWriter jsonWriter, + boolean wrapperJsonObject, + AggregationTemporalitySelector aggregationTemporalitySelector, + DefaultAggregationSelector defaultAggregationSelector) { + this.logger = logger; + this.jsonWriter = jsonWriter; + this.wrapperJsonObject = wrapperJsonObject; + this.aggregationTemporalitySelector = aggregationTemporalitySelector; + this.defaultAggregationSelector = defaultAggregationSelector; + } + + /** Returns a new {@link OtlpStdoutMetricExporterBuilder}. */ + @SuppressWarnings("SystemOut") + public static OtlpStdoutMetricExporterBuilder builder() { + return new OtlpStdoutMetricExporterBuilder(LOGGER).setOutput(System.out); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return aggregationTemporalitySelector.getAggregationTemporality(instrumentType); + } + + @Override + public Aggregation getDefaultAggregation(InstrumentType instrumentType) { + return defaultAggregationSelector.getDefaultAggregation(instrumentType); + } + + @Override + public CompletableResultCode export(Collection metrics) { + if (isShutdown.get()) { + return CompletableResultCode.ofFailure(); + } + + if (wrapperJsonObject) { + MetricsRequestMarshaler request = MetricsRequestMarshaler.create(metrics); + return jsonWriter.write(request); + } else { + for (ResourceMetricsMarshaler resourceMetrics : ResourceMetricsMarshaler.create(metrics)) { + CompletableResultCode resultCode = jsonWriter.write(resourceMetrics); + if (!resultCode.isSuccess()) { + // already logged + return resultCode; + } + } + return CompletableResultCode.ofSuccess(); + } + } + + @Override + public CompletableResultCode flush() { + return jsonWriter.flush(); + } + + @Override + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + logger.log(Level.INFO, "Calling shutdown() multiple times."); + } else { + jsonWriter.close(); + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public String toString() { + StringJoiner joiner = new StringJoiner(", ", "OtlpStdoutMetricExporter{", "}"); + joiner.add("jsonWriter=" + jsonWriter); + joiner.add("wrapperJsonObject=" + wrapperJsonObject); + return joiner.toString(); + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterBuilder.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterBuilder.java new file mode 100644 index 00000000000..63f16c09060 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterBuilder.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.metrics; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingMetricExporter; +import io.opentelemetry.exporter.logging.otlp.internal.writer.JsonWriter; +import io.opentelemetry.exporter.logging.otlp.internal.writer.LoggerJsonWriter; +import io.opentelemetry.exporter.logging.otlp.internal.writer.StreamJsonWriter; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.io.OutputStream; +import java.util.logging.Logger; + +/** + * Builder for {@link OtlpJsonLoggingMetricExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutMetricExporterBuilder { + + private static final String TYPE = "metrics"; + + private static final AggregationTemporalitySelector DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR = + AggregationTemporalitySelector.alwaysCumulative(); + + private AggregationTemporalitySelector aggregationTemporalitySelector = + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR; + + private DefaultAggregationSelector defaultAggregationSelector = + DefaultAggregationSelector.getDefault(); + + private final Logger logger; + private JsonWriter jsonWriter; + private boolean wrapperJsonObject = true; + + public OtlpStdoutMetricExporterBuilder(Logger logger) { + this.logger = logger; + this.jsonWriter = new LoggerJsonWriter(logger, TYPE); + } + + /** + * Sets the exporter to use the specified JSON object wrapper. + * + * @param wrapperJsonObject whether to wrap the JSON object in an outer JSON "resourceMetrics" + * object. + */ + public OtlpStdoutMetricExporterBuilder setWrapperJsonObject(boolean wrapperJsonObject) { + this.wrapperJsonObject = wrapperJsonObject; + return this; + } + + /** + * Sets the exporter to use the specified output stream. + * + *

The output stream will be closed when {@link OtlpStdoutMetricExporter#shutdown()} is called + * unless it's {@link System#out} or {@link System#err}. + * + * @param outputStream the output stream to use. + */ + public OtlpStdoutMetricExporterBuilder setOutput(OutputStream outputStream) { + requireNonNull(outputStream, "outputStream"); + this.jsonWriter = new StreamJsonWriter(outputStream, TYPE); + return this; + } + + /** Sets the exporter to use the specified logger. */ + public OtlpStdoutMetricExporterBuilder setOutput(Logger logger) { + requireNonNull(logger, "logger"); + this.jsonWriter = new LoggerJsonWriter(logger, TYPE); + return this; + } + + /** + * Set the {@link AggregationTemporalitySelector} used for {@link + * MetricExporter#getAggregationTemporality(InstrumentType)}. + * + *

If unset, defaults to {@link AggregationTemporalitySelector#alwaysCumulative()}. + * + *

{@link AggregationTemporalitySelector#deltaPreferred()} is a common configuration for delta + * backends. + */ + public OtlpStdoutMetricExporterBuilder setAggregationTemporalitySelector( + AggregationTemporalitySelector aggregationTemporalitySelector) { + requireNonNull(aggregationTemporalitySelector, "aggregationTemporalitySelector"); + this.aggregationTemporalitySelector = aggregationTemporalitySelector; + return this; + } + + /** + * Set the {@link DefaultAggregationSelector} used for {@link + * MetricExporter#getDefaultAggregation(InstrumentType)}. + * + *

If unset, defaults to {@link DefaultAggregationSelector#getDefault()}. + */ + public OtlpStdoutMetricExporterBuilder setDefaultAggregationSelector( + DefaultAggregationSelector defaultAggregationSelector) { + requireNonNull(defaultAggregationSelector, "defaultAggregationSelector"); + this.defaultAggregationSelector = defaultAggregationSelector; + + return this; + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance + */ + public OtlpStdoutMetricExporter build() { + return new OtlpStdoutMetricExporter( + logger, + jsonWriter, + wrapperJsonObject, + aggregationTemporalitySelector, + defaultAggregationSelector); + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterComponentProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterComponentProvider.java new file mode 100644 index 00000000000..dd8b3f643fa --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterComponentProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.metrics; + +import io.opentelemetry.exporter.internal.ExporterBuilderUtil; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +/** + * File configuration SPI implementation for {@link OtlpStdoutMetricExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutMetricExporterComponentProvider + implements ComponentProvider { + + @Override + public Class getType() { + return MetricExporter.class; + } + + @Override + public String getName() { + return "experimental-otlp/stdout"; + } + + @Override + public MetricExporter create(StructuredConfigProperties config) { + OtlpStdoutMetricExporterBuilder builder = OtlpStdoutMetricExporter.builder(); + ExporterBuilderUtil.configureOtlpAggregationTemporality( + config, builder::setAggregationTemporalitySelector); + ExporterBuilderUtil.configureOtlpHistogramDefaultAggregation( + config, builder::setDefaultAggregationSelector); + return builder.build(); + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterProvider.java new file mode 100644 index 00000000000..9eace851190 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/metrics/OtlpStdoutMetricExporterProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.metrics; + +import io.opentelemetry.exporter.internal.ExporterBuilderUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +/** + * {@link MetricExporter} SPI implementation for {@link OtlpStdoutMetricExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutMetricExporterProvider implements ConfigurableMetricExporterProvider { + @Override + public MetricExporter createExporter(ConfigProperties config) { + OtlpStdoutMetricExporterBuilder builder = OtlpStdoutMetricExporter.builder(); + ExporterBuilderUtil.configureOtlpAggregationTemporality( + config, builder::setAggregationTemporalitySelector); + ExporterBuilderUtil.configureOtlpHistogramDefaultAggregation( + config, builder::setDefaultAggregationSelector); + return builder.build(); + } + + @Override + public String getName() { + return "experimental-otlp/stdout"; + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingSpanExporterProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/LoggingSpanExporterProvider.java similarity index 83% rename from exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingSpanExporterProvider.java rename to exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/LoggingSpanExporterProvider.java index 6ce1856a894..6394acf78aa 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingSpanExporterProvider.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/LoggingSpanExporterProvider.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.exporter.logging.otlp.internal; +package io.opentelemetry.exporter.logging.otlp.internal.traces; import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingSpanExporter; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -16,7 +16,8 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public class LoggingSpanExporterProvider implements ConfigurableSpanExporterProvider { +public final class LoggingSpanExporterProvider implements ConfigurableSpanExporterProvider { + @Override public SpanExporter createExporter(ConfigProperties config) { return OtlpJsonLoggingSpanExporter.create(); diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporter.java new file mode 100644 index 00000000000..39c8829ef9b --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporter.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.traces; + +import io.opentelemetry.exporter.internal.otlp.traces.ResourceSpansMarshaler; +import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler; +import io.opentelemetry.exporter.logging.otlp.internal.writer.JsonWriter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Exporter for sending OTLP spans to stdout. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutSpanExporter implements SpanExporter { + + private static final Logger LOGGER = Logger.getLogger(OtlpStdoutSpanExporter.class.getName()); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + private final Logger logger; + private final JsonWriter jsonWriter; + private final boolean wrapperJsonObject; + + OtlpStdoutSpanExporter(Logger logger, JsonWriter jsonWriter, boolean wrapperJsonObject) { + this.logger = logger; + this.jsonWriter = jsonWriter; + this.wrapperJsonObject = wrapperJsonObject; + } + + /** Returns a new {@link OtlpStdoutSpanExporterBuilder}. */ + @SuppressWarnings("SystemOut") + public static OtlpStdoutSpanExporterBuilder builder() { + return new OtlpStdoutSpanExporterBuilder(LOGGER).setOutput(System.out); + } + + @Override + public CompletableResultCode export(Collection spans) { + if (isShutdown.get()) { + return CompletableResultCode.ofFailure(); + } + + if (wrapperJsonObject) { + TraceRequestMarshaler request = TraceRequestMarshaler.create(spans); + return jsonWriter.write(request); + } else { + for (ResourceSpansMarshaler resourceSpans : ResourceSpansMarshaler.create(spans)) { + CompletableResultCode resultCode = jsonWriter.write(resourceSpans); + if (!resultCode.isSuccess()) { + // already logged + return resultCode; + } + } + return CompletableResultCode.ofSuccess(); + } + } + + @Override + public CompletableResultCode flush() { + return jsonWriter.flush(); + } + + @Override + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + logger.log(Level.INFO, "Calling shutdown() multiple times."); + } else { + jsonWriter.close(); + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public String toString() { + StringJoiner joiner = new StringJoiner(", ", "OtlpStdoutSpanExporter{", "}"); + joiner.add("jsonWriter=" + jsonWriter); + joiner.add("wrapperJsonObject=" + wrapperJsonObject); + return joiner.toString(); + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterBuilder.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterBuilder.java new file mode 100644 index 00000000000..2ca9e5a97b3 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterBuilder.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.traces; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingSpanExporter; +import io.opentelemetry.exporter.logging.otlp.internal.writer.JsonWriter; +import io.opentelemetry.exporter.logging.otlp.internal.writer.LoggerJsonWriter; +import io.opentelemetry.exporter.logging.otlp.internal.writer.StreamJsonWriter; +import java.io.OutputStream; +import java.util.logging.Logger; + +/** + * Builder for {@link OtlpJsonLoggingSpanExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutSpanExporterBuilder { + + private static final String TYPE = "spans"; + + private final Logger logger; + private JsonWriter jsonWriter; + private boolean wrapperJsonObject = true; + + public OtlpStdoutSpanExporterBuilder(Logger logger) { + this.logger = logger; + this.jsonWriter = new LoggerJsonWriter(logger, TYPE); + } + + /** + * Sets the exporter to use the specified JSON object wrapper. + * + * @param wrapperJsonObject whether to wrap the JSON object in an outer JSON "resourceSpans" + * object. + */ + public OtlpStdoutSpanExporterBuilder setWrapperJsonObject(boolean wrapperJsonObject) { + this.wrapperJsonObject = wrapperJsonObject; + return this; + } + + /** + * Sets the exporter to use the specified output stream. + * + *

The output stream will be closed when {@link OtlpStdoutSpanExporter#shutdown()} is called + * unless it's {@link System#out} or {@link System#err}. + * + * @param outputStream the output stream to use. + */ + public OtlpStdoutSpanExporterBuilder setOutput(OutputStream outputStream) { + requireNonNull(outputStream, "outputStream"); + this.jsonWriter = new StreamJsonWriter(outputStream, TYPE); + return this; + } + + /** Sets the exporter to use the specified logger. */ + public OtlpStdoutSpanExporterBuilder setOutput(Logger logger) { + requireNonNull(logger, "logger"); + this.jsonWriter = new LoggerJsonWriter(logger, TYPE); + return this; + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance + */ + public OtlpStdoutSpanExporter build() { + return new OtlpStdoutSpanExporter(logger, jsonWriter, wrapperJsonObject); + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterComponentProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterComponentProvider.java new file mode 100644 index 00000000000..1d60e1a37b8 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterComponentProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.traces; + +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +/** + * File configuration SPI implementation for {@link OtlpStdoutSpanExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutSpanExporterComponentProvider + implements ComponentProvider { + + @Override + public Class getType() { + return SpanExporter.class; + } + + @Override + public String getName() { + return "experimental-otlp/stdout"; + } + + @Override + public SpanExporter create(StructuredConfigProperties config) { + OtlpStdoutSpanExporterBuilder builder = OtlpStdoutSpanExporter.builder(); + return builder.build(); + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterProvider.java new file mode 100644 index 00000000000..e5d2f008315 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/traces/OtlpStdoutSpanExporterProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.traces; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +/** + * {@link SpanExporter} SPI implementation for {@link OtlpStdoutSpanExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutSpanExporterProvider implements ConfigurableSpanExporterProvider { + @Override + public SpanExporter createExporter(ConfigProperties config) { + OtlpStdoutSpanExporterBuilder builder = OtlpStdoutSpanExporter.builder(); + return builder.build(); + } + + @Override + public String getName() { + return "experimental-otlp/stdout"; + } +} diff --git a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider index 76364a2dae4..b90ec18cc20 100644 --- a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider +++ b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -1 +1,3 @@ io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporterComponentProvider +io.opentelemetry.exporter.logging.otlp.internal.metrics.OtlpStdoutMetricExporterComponentProvider +io.opentelemetry.exporter.logging.otlp.internal.traces.OtlpStdoutSpanExporterComponentProvider diff --git a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider index 2b532ca9a38..b1641bbe303 100644 --- a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider +++ b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider @@ -1 +1,3 @@ -io.opentelemetry.exporter.logging.otlp.internal.LoggingMetricExporterProvider +io.opentelemetry.exporter.logging.otlp.internal.metrics.LoggingMetricExporterProvider +io.opentelemetry.exporter.logging.otlp.internal.metrics.OtlpStdoutMetricExporterProvider + diff --git a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider index fe444f4acf3..d1a8bfa347a 100644 --- a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider +++ b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider @@ -1 +1,2 @@ -io.opentelemetry.exporter.logging.otlp.internal.LoggingSpanExporterProvider +io.opentelemetry.exporter.logging.otlp.internal.traces.LoggingSpanExporterProvider +io.opentelemetry.exporter.logging.otlp.internal.traces.OtlpStdoutSpanExporterProvider diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/AbstractOtlpStdoutExporterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/AbstractOtlpStdoutExporterTest.java new file mode 100644 index 00000000000..b18f432a4de --- /dev/null +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/AbstractOtlpStdoutExporterTest.java @@ -0,0 +1,287 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ServiceLoader; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.json.JSONException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.event.LoggingEvent; + +abstract class AbstractOtlpStdoutExporterTest { + + private static PrintStream systemOut; + + private static final String TYPE = "experimental-otlp/stdout"; + + private static final ByteArrayOutputStream SYSTEM_OUT_STREAM = new ByteArrayOutputStream(); + private static final PrintStream SYSTEM_OUT_PRINT_STREAM = new PrintStream(SYSTEM_OUT_STREAM); + + @RegisterExtension LogCapturer logs; + private final String defaultConfigString; + private final TestDataExporter testDataExporter; + protected final Class exporterClass; + private final Class providerClass; + private final Class componentProviderType; + + @TempDir Path tempDir; + + public AbstractOtlpStdoutExporterTest( + TestDataExporter testDataExporter, + Class exporterClass, + Class providerClass, + Class componentProviderType, + String defaultConfigString) { + this.testDataExporter = testDataExporter; + this.exporterClass = exporterClass; + this.providerClass = providerClass; + logs = LogCapturer.create().captureForType(exporterClass); + this.defaultConfigString = defaultConfigString; + this.componentProviderType = componentProviderType; + } + + protected abstract T createExporter( + @Nullable OutputStream outputStream, boolean wrapperJsonObject); + + protected abstract T createDefaultExporter(); + + private String output(@Nullable OutputStream outputStream, @Nullable Path file) { + if (outputStream == null) { + return logs.getEvents().stream() + .map(LoggingEvent::getMessage) + .reduce("", (a, b) -> a + b + "\n") + .trim(); + } + + if (file != null) { + try { + return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + try { + return SYSTEM_OUT_STREAM.toString(StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @BeforeAll + @SuppressWarnings("SystemOut") + static void setUpStatic() { + systemOut = System.out; + System.setOut(SYSTEM_OUT_PRINT_STREAM); + } + + @AfterAll + @SuppressWarnings("SystemOut") + static void tearDownStatic() { + System.setOut(systemOut); + } + + @BeforeEach + void setUp() { + SYSTEM_OUT_STREAM.reset(); + } + + enum OutputType { + LOGGER, + SYSTEM_OUT, + FILE, + FILE_AND_BUFFERED_WRITER + } + + public static class TestCase { + + private final boolean wrapperJsonObject; + private final OutputType outputType; + + public TestCase(OutputType outputType, boolean wrapperJsonObject) { + this.outputType = outputType; + this.wrapperJsonObject = wrapperJsonObject; + } + + public OutputType getOutputType() { + return outputType; + } + + public boolean isWrapperJsonObject() { + return wrapperJsonObject; + } + } + + static Stream exportTestCases() { + return ImmutableList.of( + testCase(OutputType.SYSTEM_OUT, /* wrapperJsonObject= */ true), + testCase(OutputType.SYSTEM_OUT, /* wrapperJsonObject= */ false), + testCase(OutputType.FILE, /* wrapperJsonObject= */ true), + testCase(OutputType.FILE, /* wrapperJsonObject= */ false), + testCase(OutputType.FILE_AND_BUFFERED_WRITER, /* wrapperJsonObject= */ true), + testCase(OutputType.FILE_AND_BUFFERED_WRITER, /* wrapperJsonObject= */ false), + testCase(OutputType.LOGGER, /* wrapperJsonObject= */ true), + testCase(OutputType.LOGGER, /* wrapperJsonObject= */ false)) + .stream(); + } + + private static Arguments testCase(OutputType type, boolean wrapperJsonObject) { + return Arguments.of( + "output=" + type + ", wrapperJsonObject=" + wrapperJsonObject, + new TestCase(type, wrapperJsonObject)); + } + + @SuppressWarnings("SystemOut") + @ParameterizedTest(name = "{0}") + @MethodSource("exportTestCases") + void exportWithProgrammaticConfig(String name, TestCase testCase) + throws JSONException, IOException { + OutputStream outputStream; + Path file = null; + switch (testCase.getOutputType()) { + case LOGGER: + outputStream = null; + break; + case SYSTEM_OUT: + outputStream = System.out; + break; + case FILE: + file = tempDir.resolve("test.log"); + outputStream = Files.newOutputStream(file); + break; + case FILE_AND_BUFFERED_WRITER: + file = tempDir.resolve("test.log"); + outputStream = new BufferedOutputStream(Files.newOutputStream(file)); + break; + default: + throw new IllegalStateException("Unexpected value: " + testCase.getOutputType()); + } + T exporter = createExporter(outputStream, testCase.isWrapperJsonObject()); + testDataExporter.export(exporter); + + String output = output(outputStream, file); + String expectedJson = testDataExporter.getExpectedJson(testCase.isWrapperJsonObject()); + JSONAssert.assertEquals("Got \n" + output, expectedJson, output, false); + + if (testCase.isWrapperJsonObject()) { + assertThat(output).doesNotContain("\n"); + } + } + + @Test + void testShutdown() { + T exporter = createDefaultExporter(); + assertThat(testDataExporter.shutdown(exporter).isSuccess()).isTrue(); + assertThat(testDataExporter.export(exporter).join(10, TimeUnit.SECONDS).isSuccess()).isFalse(); + assertThat(testDataExporter.flush(exporter).join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + assertThat(output(null, null)).isEmpty(); + assertThat(testDataExporter.shutdown(exporter).isSuccess()).isTrue(); + logs.assertContains("Calling shutdown() multiple times."); + } + + @Test + void defaultToString() { + assertFullToString(createDefaultExporter(), defaultConfigString); + + assertFullToString( + loadExporter(DefaultConfigProperties.createFromMap(emptyMap())), defaultConfigString); + } + + protected Object exporterFromComponentProvider(StructuredConfigProperties properties) { + return ((ComponentProvider) + loadSpi(ComponentProvider.class) + .filter( + p -> { + ComponentProvider c = (ComponentProvider) p; + return "experimental-otlp/stdout".equals(c.getName()) + && c.getType().equals(componentProviderType); + }) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No provider found"))) + .create(properties); + } + + @Test + void componentProviderConfig() { + StructuredConfigProperties properties = mock(StructuredConfigProperties.class); + Object exporter = exporterFromComponentProvider(properties); + + assertThat(exporter).extracting("wrapperJsonObject").isEqualTo(true); + assertThat(exporter) + .extracting("jsonWriter") + .extracting(Object::toString) + .isEqualTo("StreamJsonWriter{outputStream=stdout}"); + } + + @SuppressWarnings("unchecked") + protected T loadExporter(ConfigProperties config) { + Object provider = loadProvider(); + + try { + return (T) + provider + .getClass() + .getDeclaredMethod("createExporter", ConfigProperties.class) + .invoke(provider, config); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Object loadProvider() { + return loadSpi(providerClass) + .filter( + p -> { + try { + return AbstractOtlpStdoutExporterTest.TYPE.equals( + p.getClass().getDeclaredMethod("getName").invoke(p)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No provider found")); + } + + protected static Stream loadSpi(Class type) { + return Streams.stream(ServiceLoader.load(type, type.getClassLoader()).iterator()); + } + + private void assertFullToString(T exporter, String expected) { + assertThat(exporter.toString()).isEqualTo(expected); + } +} diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporterTest.java index 287ec7b74a1..ccbfdb26dce 100644 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporterTest.java +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporterTest.java @@ -5,22 +5,13 @@ package io.opentelemetry.exporter.logging.otlp; -import static io.opentelemetry.api.common.AttributeKey.stringKey; import static org.assertj.core.api.Assertions.assertThat; import io.github.netmikey.logunit.api.LogCapturer; -import io.opentelemetry.api.common.Attributes; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.MetricExporter; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoublePointData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableSumData; -import io.opentelemetry.sdk.resources.Resource; -import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; @@ -32,39 +23,7 @@ @SuppressLogger(OtlpJsonLoggingMetricExporter.class) class OtlpJsonLoggingMetricExporterTest { - private static final Resource RESOURCE = - Resource.create(Attributes.builder().put("key", "value").build()); - - private static final MetricData METRIC1 = - ImmutableMetricData.createDoubleSum( - RESOURCE, - InstrumentationScopeInfo.builder("instrumentation") - .setVersion("1") - .setAttributes(Attributes.builder().put("key", "value").build()) - .build(), - "metric1", - "metric1 description", - "m", - ImmutableSumData.create( - true, - AggregationTemporality.CUMULATIVE, - Arrays.asList( - ImmutableDoublePointData.create( - 1, 2, Attributes.of(stringKey("cat"), "meow"), 4)))); - - private static final MetricData METRIC2 = - ImmutableMetricData.createDoubleSum( - RESOURCE, - InstrumentationScopeInfo.builder("instrumentation2").setVersion("2").build(), - "metric2", - "metric2 description", - "s", - ImmutableSumData.create( - true, - AggregationTemporality.CUMULATIVE, - Arrays.asList( - ImmutableDoublePointData.create( - 1, 2, Attributes.of(stringKey("cat"), "meow"), 4)))); + private final TestDataExporter testDataExporter = TestDataExporter.forMetrics(); @RegisterExtension LogCapturer logs = LogCapturer.create().captureForType(OtlpJsonLoggingMetricExporter.class); @@ -90,78 +49,15 @@ void getAggregationTemporality() { @Test void log() throws Exception { - exporter.export(Arrays.asList(METRIC1, METRIC2)); + testDataExporter.export(exporter); assertThat(logs.getEvents()) .hasSize(1) .allSatisfy(log -> assertThat(log.getLevel()).isEqualTo(Level.INFO)); - JSONAssert.assertEquals( - "{" - + " \"resource\": {" - + " \"attributes\": [{" - + " \"key\": \"key\"," - + " \"value\": {" - + " \"stringValue\": \"value\"" - + " }" - + " }]" - + " }," - + " \"scopeMetrics\": [{" - + " \"scope\": {" - + " \"name\": \"instrumentation2\"," - + " \"version\": \"2\"" - + " }," - + " \"metrics\": [{" - + " \"name\": \"metric2\"," - + " \"description\": \"metric2 description\"," - + " \"unit\": \"s\"," - + " \"sum\": {" - + " \"dataPoints\": [{" - + " \"attributes\": [{" - + " \"key\": \"cat\"," - + " \"value\": {\"stringValue\": \"meow\"}" - + " }]," - + " \"startTimeUnixNano\": \"1\"," - + " \"timeUnixNano\": \"2\"," - + " \"asDouble\": 4.0" - + " }]," - + " \"aggregationTemporality\": 2," - + " \"isMonotonic\": true" - + " }" - + " }]" - + " }, {" - + " \"scope\": {" - + " \"name\": \"instrumentation\"," - + " \"version\": \"1\"," - + " \"attributes\":[{" - + " \"key\":\"key\"," - + " \"value\":{" - + " \"stringValue\":\"value\"" - + " }" - + " }]" - + " }," - + " \"metrics\": [{" - + " \"name\": \"metric1\"," - + " \"description\": \"metric1 description\"," - + " \"unit\": \"m\"," - + " \"sum\": {" - + " \"dataPoints\": [{" - + " \"attributes\": [{" - + " \"key\": \"cat\"," - + " \"value\": {\"stringValue\": \"meow\"}" - + " }]," - + " \"startTimeUnixNano\": \"1\"," - + " \"timeUnixNano\": \"2\"," - + " \"asDouble\": 4.0" - + " }]," - + " \"aggregationTemporality\": 2," - + " \"isMonotonic\": true" - + " }" - + " }]" - + " }]" - + "}", - logs.getEvents().get(0).getMessage(), - /* strict= */ false); - assertThat(logs.getEvents().get(0).getMessage()).doesNotContain("\n"); + String message = logs.getEvents().get(0).getMessage(); + String expectedJson = testDataExporter.getExpectedJson(false); + JSONAssert.assertEquals("Got \n" + message, expectedJson, message, /* strict= */ false); + assertThat(message).doesNotContain("\n"); } @Test @@ -174,7 +70,7 @@ void shutdown() { assertThat(exporter.shutdown().isSuccess()).isTrue(); assertThat( exporter - .export(Collections.singletonList(METRIC1)) + .export(Collections.singletonList(TestDataExporter.METRIC1)) .join(10, TimeUnit.SECONDS) .isSuccess()) .isFalse(); diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporterTest.java index 309cf7afaf0..ac3129f4a1c 100644 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporterTest.java +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporterTest.java @@ -5,26 +5,11 @@ package io.opentelemetry.exporter.logging.otlp; -import static io.opentelemetry.api.common.AttributeKey.booleanKey; -import static io.opentelemetry.api.common.AttributeKey.longKey; -import static io.opentelemetry.api.common.AttributeKey.stringKey; import static org.assertj.core.api.Assertions.assertThat; import io.github.netmikey.logunit.api.LogCapturer; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.TraceFlags; -import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.trace.TestSpanData; -import io.opentelemetry.sdk.trace.data.EventData; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.data.StatusData; import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; @@ -36,59 +21,7 @@ @SuppressLogger(OtlpJsonLoggingSpanExporter.class) class OtlpJsonLoggingSpanExporterTest { - private static final Resource RESOURCE = - Resource.create(Attributes.builder().put("key", "value").build()); - - private static final SpanData SPAN1 = - TestSpanData.builder() - .setHasEnded(true) - .setSpanContext( - SpanContext.create( - "12345678876543211234567887654321", - "8765432112345678", - TraceFlags.getSampled(), - TraceState.getDefault())) - .setStartEpochNanos(100) - .setEndEpochNanos(100 + 1000) - .setStatus(StatusData.ok()) - .setName("testSpan1") - .setKind(SpanKind.INTERNAL) - .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) - .setEvents( - Collections.singletonList( - EventData.create( - 100 + 500, - "somethingHappenedHere", - Attributes.of(booleanKey("important"), true)))) - .setTotalAttributeCount(2) - .setTotalRecordedEvents(1) - .setTotalRecordedLinks(0) - .setInstrumentationScopeInfo( - InstrumentationScopeInfo.builder("instrumentation") - .setVersion("1") - .setAttributes(Attributes.builder().put("key", "value").build()) - .build()) - .setResource(RESOURCE) - .build(); - - private static final SpanData SPAN2 = - TestSpanData.builder() - .setHasEnded(false) - .setSpanContext( - SpanContext.create( - "12340000000043211234000000004321", - "8765000000005678", - TraceFlags.getSampled(), - TraceState.getDefault())) - .setStartEpochNanos(500) - .setEndEpochNanos(500 + 1001) - .setStatus(StatusData.error()) - .setName("testSpan2") - .setKind(SpanKind.CLIENT) - .setResource(RESOURCE) - .setInstrumentationScopeInfo( - InstrumentationScopeInfo.builder("instrumentation2").setVersion("2").build()) - .build(); + private final TestDataExporter testDataExporter = TestDataExporter.forSpans(); @RegisterExtension LogCapturer logs = LogCapturer.create().captureForType(OtlpJsonLoggingSpanExporter.class); @@ -102,85 +35,14 @@ void setUp() { @Test void log() throws Exception { - exporter.export(Arrays.asList(SPAN1, SPAN2)); + testDataExporter.export(exporter); assertThat(logs.getEvents()) .hasSize(1) .allSatisfy(log -> assertThat(log.getLevel()).isEqualTo(Level.INFO)); String message = logs.getEvents().get(0).getMessage(); - JSONAssert.assertEquals( - "{" - + " \"resource\": {" - + " \"attributes\": [{" - + " \"key\": \"key\"," - + " \"value\": {" - + " \"stringValue\": \"value\"" - + " }" - + " }]" - + " }," - + " \"scopeSpans\": [{" - + " \"scope\": {" - + " \"name\": \"instrumentation2\"," - + " \"version\": \"2\"" - + " }," - + " \"spans\": [{" - + " \"traceId\": \"12340000000043211234000000004321\"," - + " \"spanId\": \"8765000000005678\"," - + " \"name\": \"testSpan2\"," - + " \"kind\": 3," - + " \"startTimeUnixNano\": \"500\"," - + " \"endTimeUnixNano\": \"1501\"," - + " \"status\": {" - + " \"code\": 2" - + " }" - + " }]" - + " }, {" - + " \"scope\": {" - + " \"name\": \"instrumentation\"," - + " \"version\": \"1\"," - + " \"attributes\":[{" - + " \"key\":\"key\"," - + " \"value\":{" - + " \"stringValue\":\"value\"" - + " }" - + " }]" - + " }," - + " \"spans\": [{" - + " \"traceId\": \"12345678876543211234567887654321\"," - + " \"spanId\": \"8765432112345678\"," - + " \"name\": \"testSpan1\"," - + " \"kind\": 1," - + " \"startTimeUnixNano\": \"100\"," - + " \"endTimeUnixNano\": \"1100\"," - + " \"attributes\": [{" - + " \"key\": \"animal\"," - + " \"value\": {" - + " \"stringValue\": \"cat\"" - + " }" - + " }, {" - + " \"key\": \"lives\"," - + " \"value\": {" - + " \"intValue\": \"9\"" - + " }" - + " }]," - + " \"events\": [{" - + " \"timeUnixNano\": \"600\"," - + " \"name\": \"somethingHappenedHere\"," - + " \"attributes\": [{" - + " \"key\": \"important\"," - + " \"value\": {" - + " \"boolValue\": true" - + " }" - + " }]" - + " }]," - + " \"status\": {" - + " \"code\": 1" - + " }" - + " }]" - + " }]" - + "}", - message, - /* strict= */ false); + String expectedJson = testDataExporter.getExpectedJson(false); + JSONAssert.assertEquals("Got \n" + message, expectedJson, message, /* strict= */ false); assertThat(message).doesNotContain("\n"); } @@ -194,7 +56,7 @@ void shutdown() { assertThat(exporter.shutdown().isSuccess()).isTrue(); assertThat( exporter - .export(Collections.singletonList(SPAN1)) + .export(Collections.singletonList(TestDataExporter.SPAN1)) .join(10, TimeUnit.SECONDS) .isSuccess()) .isFalse(); diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutLogRecordExporterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutLogRecordExporterTest.java index f98b81b69af..e234b9745dc 100644 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutLogRecordExporterTest.java +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutLogRecordExporterTest.java @@ -5,283 +5,41 @@ package io.opentelemetry.exporter.logging.otlp; -import static java.util.Collections.emptyMap; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Streams; -import io.github.netmikey.logunit.api.LogCapturer; import io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporter; import io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporterBuilder; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider; import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.OutputStream; -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ServiceLoader; -import java.util.concurrent.TimeUnit; import java.util.logging.Logger; -import java.util.stream.Stream; import javax.annotation.Nullable; -import org.json.JSONException; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.skyscreamer.jsonassert.JSONAssert; -import org.slf4j.event.LoggingEvent; - -class OtlpStdoutLogRecordExporterTest { - - private static final ByteArrayOutputStream SYSTEM_OUT_STREAM = new ByteArrayOutputStream(); - private static final PrintStream SYSTEM_OUT_PRINT_STREAM = new PrintStream(SYSTEM_OUT_STREAM); - private static PrintStream systemOut; - private static final Class EXPORTER_CLASS = OtlpStdoutLogRecordExporter.class; - private static final String DEFAULT_CONFIG_STRING = - "OtlpStdoutLogRecordExporter{jsonWriter=StreamJsonWriter{outputStream=stdout}, wrapperJsonObject=true}"; - private static final String TYPE = "experimental-otlp/stdout"; - private static final TestDataExporter TEST_DATA_EXPORTER = - TestDataExporter.forLogs(); - private static final Class PROVIDER_CLASS = ConfigurableLogRecordExporterProvider.class; - private static final Class COMPONENT_PROVIDER_TYPE = LogRecordExporter.class; - - @RegisterExtension - LogCapturer logs = LogCapturer.create().captureForType(OtlpStdoutLogRecordExporter.class); - - @TempDir Path tempDir; - - @BeforeAll - @SuppressWarnings("SystemOut") - static void setUpStatic() { - systemOut = System.out; - System.setOut(SYSTEM_OUT_PRINT_STREAM); - } - @AfterAll - @SuppressWarnings("SystemOut") - static void tearDownStatic() { - System.setOut(systemOut); - } - - @BeforeEach - void setUp() { - SYSTEM_OUT_STREAM.reset(); - } - - static Stream exportTestCases() { - return ImmutableList.of( - testCase(OutputType.SYSTEM_OUT, /* wrapperJsonObject= */ true), - testCase(OutputType.SYSTEM_OUT, /* wrapperJsonObject= */ false), - testCase(OutputType.FILE, /* wrapperJsonObject= */ true), - testCase(OutputType.FILE, /* wrapperJsonObject= */ false), - testCase(OutputType.FILE_AND_BUFFERED_WRITER, /* wrapperJsonObject= */ true), - testCase(OutputType.FILE_AND_BUFFERED_WRITER, /* wrapperJsonObject= */ false), - testCase(OutputType.LOGGER, /* wrapperJsonObject= */ true), - testCase(OutputType.LOGGER, /* wrapperJsonObject= */ false)) - .stream(); - } - - private static Arguments testCase(OutputType type, boolean wrapperJsonObject) { - return Arguments.of( - "output=" + type + ", wrapperJsonObject=" + wrapperJsonObject, - new TestCase(type, wrapperJsonObject)); - } +class OtlpStdoutLogRecordExporterTest + extends AbstractOtlpStdoutExporterTest { - private static Stream loadSpi(Class type) { - return Streams.stream(ServiceLoader.load(type, type.getClassLoader()).iterator()); + public OtlpStdoutLogRecordExporterTest() { + super( + TestDataExporter.forLogs(), + OtlpStdoutLogRecordExporter.class, + ConfigurableLogRecordExporterProvider.class, + LogRecordExporter.class, + "OtlpStdoutLogRecordExporter{jsonWriter=StreamJsonWriter{outputStream=stdout}, wrapperJsonObject=true}"); } - private static OtlpStdoutLogRecordExporter createDefaultExporter() { + @Override + protected OtlpStdoutLogRecordExporter createDefaultExporter() { return OtlpStdoutLogRecordExporter.builder().build(); } - private static OtlpStdoutLogRecordExporter createExporter( + @Override + protected OtlpStdoutLogRecordExporter createExporter( @Nullable OutputStream outputStream, boolean wrapperJsonObject) { OtlpStdoutLogRecordExporterBuilder builder = OtlpStdoutLogRecordExporter.builder().setWrapperJsonObject(wrapperJsonObject); if (outputStream != null) { builder.setOutput(outputStream); } else { - builder.setOutput(Logger.getLogger(EXPORTER_CLASS.getName())); + builder.setOutput(Logger.getLogger(exporterClass.getName())); } return builder.build(); } - - private String output(@Nullable OutputStream outputStream, @Nullable Path file) { - if (outputStream == null) { - return logs.getEvents().stream() - .map(LoggingEvent::getMessage) - .reduce("", (a, b) -> a + b + "\n") - .trim(); - } - - if (file != null) { - try { - return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - return SYSTEM_OUT_STREAM.toString(StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings("SystemOut") - @ParameterizedTest(name = "{0}") - @MethodSource("exportTestCases") - void exportWithProgrammaticConfig(String name, TestCase testCase) - throws JSONException, IOException { - OutputStream outputStream; - Path file = null; - switch (testCase.getOutputType()) { - case LOGGER: - outputStream = null; - break; - case SYSTEM_OUT: - outputStream = System.out; - break; - case FILE: - file = tempDir.resolve("test.log"); - outputStream = Files.newOutputStream(file); - break; - case FILE_AND_BUFFERED_WRITER: - file = tempDir.resolve("test.log"); - outputStream = new BufferedOutputStream(Files.newOutputStream(file)); - break; - default: - throw new IllegalStateException("Unexpected value: " + testCase.getOutputType()); - } - OtlpStdoutLogRecordExporter exporter = - createExporter(outputStream, testCase.isWrapperJsonObject()); - TEST_DATA_EXPORTER.export(exporter); - - String output = output(outputStream, file); - String expectedJson = TEST_DATA_EXPORTER.getExpectedJson(testCase.isWrapperJsonObject()); - JSONAssert.assertEquals("Got \n" + output, expectedJson, output, false); - - if (testCase.isWrapperJsonObject()) { - assertThat(output).doesNotContain("\n"); - } - } - - @Test - void testShutdown() { - OtlpStdoutLogRecordExporter exporter = createDefaultExporter(); - assertThat(TEST_DATA_EXPORTER.shutdown(exporter).isSuccess()).isTrue(); - assertThat(TEST_DATA_EXPORTER.export(exporter).join(10, TimeUnit.SECONDS).isSuccess()) - .isFalse(); - assertThat(TEST_DATA_EXPORTER.flush(exporter).join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); - assertThat(output(null, null)).isEmpty(); - assertThat(TEST_DATA_EXPORTER.shutdown(exporter).isSuccess()).isTrue(); - logs.assertContains("Calling shutdown() multiple times."); - } - - @Test - void defaultToString() { - assertThat(createDefaultExporter().toString()).isEqualTo(DEFAULT_CONFIG_STRING); - - assertThat(loadExporter(DefaultConfigProperties.createFromMap(emptyMap()), TYPE).toString()) - .isEqualTo(DEFAULT_CONFIG_STRING); - } - - private OtlpStdoutLogRecordExporter exporterFromComponentProvider( - StructuredConfigProperties properties) { - return (OtlpStdoutLogRecordExporter) - ((ComponentProvider) - loadSpi(ComponentProvider.class) - .filter( - p -> { - ComponentProvider c = (ComponentProvider) p; - return "experimental-otlp/stdout".equals(c.getName()) - && c.getType().equals(COMPONENT_PROVIDER_TYPE); - }) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No provider found"))) - .create(properties); - } - - @Test - void componentProviderConfig() { - StructuredConfigProperties properties = mock(StructuredConfigProperties.class); - OtlpStdoutLogRecordExporter exporter = exporterFromComponentProvider(properties); - - assertThat(exporter).extracting("wrapperJsonObject").isEqualTo(true); - assertThat(exporter) - .extracting("jsonWriter") - .extracting(Object::toString) - .isEqualTo("StreamJsonWriter{outputStream=stdout}"); - } - - private OtlpStdoutLogRecordExporter loadExporter(ConfigProperties config, String name) { - Object provider = loadProvider(name); - - try { - return (OtlpStdoutLogRecordExporter) - provider - .getClass() - .getDeclaredMethod("createExporter", ConfigProperties.class) - .invoke(provider, config); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private Object loadProvider(String want) { - return loadSpi(PROVIDER_CLASS) - .filter( - p -> { - try { - return want.equals(p.getClass().getDeclaredMethod("getName").invoke(p)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No provider found")); - } - - enum OutputType { - LOGGER, - SYSTEM_OUT, - FILE, - FILE_AND_BUFFERED_WRITER - } - - static class TestCase { - private final boolean wrapperJsonObject; - private final OutputType outputType; - - public TestCase(OutputType outputType, boolean wrapperJsonObject) { - this.outputType = outputType; - this.wrapperJsonObject = wrapperJsonObject; - } - - public OutputType getOutputType() { - return outputType; - } - - public boolean isWrapperJsonObject() { - return wrapperJsonObject; - } - } } diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutMetricExporterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutMetricExporterTest.java new file mode 100644 index 00000000000..fdc6d945fc4 --- /dev/null +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutMetricExporterTest.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.exporter.logging.otlp.internal.metrics.OtlpStdoutMetricExporter; +import io.opentelemetry.exporter.logging.otlp.internal.metrics.OtlpStdoutMetricExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.io.OutputStream; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class OtlpStdoutMetricExporterTest + extends AbstractOtlpStdoutExporterTest { + + public OtlpStdoutMetricExporterTest() { + super( + TestDataExporter.forMetrics(), + OtlpStdoutMetricExporter.class, + ConfigurableMetricExporterProvider.class, + MetricExporter.class, + "OtlpStdoutMetricExporter{jsonWriter=StreamJsonWriter{outputStream=stdout}, wrapperJsonObject=true}"); + } + + @Override + protected OtlpStdoutMetricExporter createDefaultExporter() { + return OtlpStdoutMetricExporter.builder().build(); + } + + @Override + protected OtlpStdoutMetricExporter createExporter( + @Nullable OutputStream outputStream, boolean wrapperJsonObject) { + OtlpStdoutMetricExporterBuilder builder = + OtlpStdoutMetricExporter.builder().setWrapperJsonObject(wrapperJsonObject); + if (outputStream != null) { + builder.setOutput(outputStream); + } else { + builder.setOutput(Logger.getLogger(exporterClass.getName())); + } + return builder.build(); + } + + /** Test configuration specific to metric exporter. */ + @Test + void providerMetricConfig() { + OtlpStdoutMetricExporter exporter = + loadExporter( + DefaultConfigProperties.createFromMap( + ImmutableMap.of( + "otel.exporter.otlp.metrics.temporality.preference", + "DELTA", + "otel.exporter.otlp.metrics.default.histogram.aggregation", + "BASE2_EXPONENTIAL_BUCKET_HISTOGRAM"))); + + assertThat(exporter.getAggregationTemporality(InstrumentType.COUNTER)) + .isEqualTo(AggregationTemporality.DELTA); + + assertThat(exporter.getDefaultAggregation(InstrumentType.HISTOGRAM)) + .isEqualTo(Aggregation.base2ExponentialBucketHistogram()); + } + + @Test + void componentProviderMetricConfig() { + StructuredConfigProperties properties = mock(StructuredConfigProperties.class); + when(properties.getString("temporality_preference")).thenReturn("DELTA"); + when(properties.getString("default_histogram_aggregation")) + .thenReturn("BASE2_EXPONENTIAL_BUCKET_HISTOGRAM"); + + OtlpStdoutMetricExporter exporter = + (OtlpStdoutMetricExporter) exporterFromComponentProvider(properties); + assertThat(exporter.getAggregationTemporality(InstrumentType.COUNTER)) + .isEqualTo(AggregationTemporality.DELTA); + + assertThat(exporter.getDefaultAggregation(InstrumentType.HISTOGRAM)) + .isEqualTo(Aggregation.base2ExponentialBucketHistogram()); + } + + @Test + void validMetricConfig() { + assertThatCode( + () -> + OtlpStdoutMetricExporter.builder() + .setAggregationTemporalitySelector( + AggregationTemporalitySelector.deltaPreferred())) + .doesNotThrowAnyException(); + assertThat( + OtlpStdoutMetricExporter.builder() + .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred()) + .build() + .getAggregationTemporality(InstrumentType.COUNTER)) + .isEqualTo(AggregationTemporality.DELTA); + assertThat( + OtlpStdoutMetricExporter.builder() + .build() + .getAggregationTemporality(InstrumentType.COUNTER)) + .isEqualTo(AggregationTemporality.CUMULATIVE); + + assertThat( + OtlpStdoutMetricExporter.builder() + .setDefaultAggregationSelector( + DefaultAggregationSelector.getDefault() + .with(InstrumentType.HISTOGRAM, Aggregation.drop())) + .build() + .getDefaultAggregation(InstrumentType.HISTOGRAM)) + .isEqualTo(Aggregation.drop()); + } +} diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutSpanExporterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutSpanExporterTest.java new file mode 100644 index 00000000000..472ff4cd731 --- /dev/null +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutSpanExporterTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import io.opentelemetry.exporter.logging.otlp.internal.traces.OtlpStdoutSpanExporter; +import io.opentelemetry.exporter.logging.otlp.internal.traces.OtlpStdoutSpanExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.OutputStream; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +class OtlpStdoutSpanExporterTest extends AbstractOtlpStdoutExporterTest { + + public OtlpStdoutSpanExporterTest() { + super( + TestDataExporter.forSpans(), + OtlpStdoutSpanExporter.class, + ConfigurableSpanExporterProvider.class, + SpanExporter.class, + "OtlpStdoutSpanExporter{jsonWriter=StreamJsonWriter{outputStream=stdout}, wrapperJsonObject=true}"); + } + + @Override + protected OtlpStdoutSpanExporter createDefaultExporter() { + return OtlpStdoutSpanExporter.builder().build(); + } + + @Override + protected OtlpStdoutSpanExporter createExporter( + @Nullable OutputStream outputStream, boolean wrapperJsonObject) { + OtlpStdoutSpanExporterBuilder builder = + OtlpStdoutSpanExporter.builder().setWrapperJsonObject(wrapperJsonObject); + if (outputStream != null) { + builder.setOutput(outputStream); + } else { + builder.setOutput(Logger.getLogger(exporterClass.getName())); + } + return builder.build(); + } +} diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java index 53b51629e1c..54032fca3e0 100644 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java @@ -13,17 +13,30 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoublePointData; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableSumData; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.logs.TestLogRecordData; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.TimeUnit; abstract class TestDataExporter { @@ -74,6 +87,88 @@ abstract class TestDataExporter { TraceState.getDefault())) .build(); + static final SpanData SPAN1 = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + "12345678876543211234567887654321", + "8765432112345678", + TraceFlags.getSampled(), + TraceState.getDefault())) + .setStartEpochNanos(100) + .setEndEpochNanos(100 + 1000) + .setStatus(StatusData.ok()) + .setName("testSpan1") + .setKind(SpanKind.INTERNAL) + .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) + .setEvents( + Collections.singletonList( + EventData.create( + 100 + 500, + "somethingHappenedHere", + Attributes.of(booleanKey("important"), true)))) + .setTotalAttributeCount(2) + .setTotalRecordedEvents(1) + .setTotalRecordedLinks(0) + .setInstrumentationScopeInfo( + InstrumentationScopeInfo.builder("instrumentation") + .setVersion("1") + .setAttributes(Attributes.builder().put("key", "value").build()) + .build()) + .setResource(RESOURCE) + .build(); + + private static final SpanData SPAN2 = + TestSpanData.builder() + .setHasEnded(false) + .setSpanContext( + SpanContext.create( + "12340000000043211234000000004321", + "8765000000005678", + TraceFlags.getSampled(), + TraceState.getDefault())) + .setStartEpochNanos(500) + .setEndEpochNanos(500 + 1001) + .setStatus(StatusData.error()) + .setName("testSpan2") + .setKind(SpanKind.CLIENT) + .setResource(RESOURCE) + .setInstrumentationScopeInfo( + InstrumentationScopeInfo.builder("instrumentation2").setVersion("2").build()) + .build(); + + static final MetricData METRIC1 = + ImmutableMetricData.createDoubleSum( + RESOURCE, + InstrumentationScopeInfo.builder("instrumentation") + .setVersion("1") + .setAttributes(Attributes.builder().put("key", "value").build()) + .build(), + "metric1", + "metric1 description", + "m", + ImmutableSumData.create( + true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + ImmutableDoublePointData.create( + 1, 2, Attributes.of(stringKey("cat"), "meow"), 4)))); + + private static final MetricData METRIC2 = + ImmutableMetricData.createDoubleSum( + RESOURCE, + InstrumentationScopeInfo.builder("instrumentation2").setVersion("2").build(), + "metric2", + "metric2 description", + "s", + ImmutableSumData.create( + true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + ImmutableDoublePointData.create( + 1, 2, Attributes.of(stringKey("cat"), "meow"), 4)))); + public TestDataExporter(String expectedFileNoWrapper, String expectedFileWrapper) { this.expectedFileNoWrapper = expectedFileNoWrapper; this.expectedFileWrapper = expectedFileWrapper; @@ -109,4 +204,44 @@ public CompletableResultCode shutdown(LogRecordExporter exporter) { } }; } + + static TestDataExporter forSpans() { + return new TestDataExporter( + "expected-spans.json", "expected-spans-wrapper.json") { + @Override + public CompletableResultCode export(SpanExporter exporter) { + return exporter.export(Arrays.asList(SPAN1, SPAN2)); + } + + @Override + public CompletableResultCode flush(SpanExporter exporter) { + return exporter.flush(); + } + + @Override + public CompletableResultCode shutdown(SpanExporter exporter) { + return exporter.shutdown(); + } + }; + } + + static TestDataExporter forMetrics() { + return new TestDataExporter( + "expected-metrics.json", "expected-metrics-wrapper.json") { + @Override + public CompletableResultCode export(MetricExporter exporter) { + return exporter.export(Arrays.asList(METRIC1, METRIC2)); + } + + @Override + public CompletableResultCode flush(MetricExporter exporter) { + return exporter.flush(); + } + + @Override + public CompletableResultCode shutdown(MetricExporter exporter) { + return exporter.shutdown(); + } + }; + } } diff --git a/exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json b/exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json new file mode 100644 index 00000000000..9c1255a6279 --- /dev/null +++ b/exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json @@ -0,0 +1,93 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "instrumentation", + "version": "1", + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "metrics": [ + { + "name": "metric1", + "description": "metric1 description", + "unit": "m", + "sum": { + "dataPoints": [ + { + "startTimeUnixNano": "1", + "timeUnixNano": "2", + "asDouble": 4.0, + "exemplars": [], + "attributes": [ + { + "key": "cat", + "value": { + "stringValue": "meow" + } + } + ] + } + ], + "aggregationTemporality": 2, + "isMonotonic": true + } + } + ] + }, + { + "scope": { + "name": "instrumentation2", + "version": "2", + "attributes": [] + }, + "metrics": [ + { + "name": "metric2", + "description": "metric2 description", + "unit": "s", + "sum": { + "dataPoints": [ + { + "startTimeUnixNano": "1", + "timeUnixNano": "2", + "asDouble": 4.0, + "exemplars": [], + "attributes": [ + { + "key": "cat", + "value": { + "stringValue": "meow" + } + } + ] + } + ], + "aggregationTemporality": 2, + "isMonotonic": true + } + } + ] + } + ] + } + ] +} diff --git a/exporters/logging-otlp/src/test/resources/expected-metrics.json b/exporters/logging-otlp/src/test/resources/expected-metrics.json new file mode 100644 index 00000000000..1a05a682e56 --- /dev/null +++ b/exporters/logging-otlp/src/test/resources/expected-metrics.json @@ -0,0 +1,89 @@ +{ + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "instrumentation", + "version": "1", + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "metrics": [ + { + "name": "metric1", + "description": "metric1 description", + "unit": "m", + "sum": { + "dataPoints": [ + { + "startTimeUnixNano": "1", + "timeUnixNano": "2", + "asDouble": 4.0, + "exemplars": [], + "attributes": [ + { + "key": "cat", + "value": { + "stringValue": "meow" + } + } + ] + } + ], + "aggregationTemporality": 2, + "isMonotonic": true + } + } + ] + }, + { + "scope": { + "name": "instrumentation2", + "version": "2", + "attributes": [] + }, + "metrics": [ + { + "name": "metric2", + "description": "metric2 description", + "unit": "s", + "sum": { + "dataPoints": [ + { + "startTimeUnixNano": "1", + "timeUnixNano": "2", + "asDouble": 4.0, + "exemplars": [], + "attributes": [ + { + "key": "cat", + "value": { + "stringValue": "meow" + } + } + ] + } + ], + "aggregationTemporality": 2, + "isMonotonic": true + } + } + ] + } + ] +} diff --git a/exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json b/exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json new file mode 100644 index 00000000000..f1dd80ed9e8 --- /dev/null +++ b/exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json @@ -0,0 +1,99 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "instrumentation", + "version": "1", + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "spans": [ + { + "traceId": "12345678876543211234567887654321", + "spanId": "8765432112345678", + "name": "testSpan1", + "kind": 1, + "startTimeUnixNano": "100", + "endTimeUnixNano": "1100", + "attributes": [ + { + "key": "animal", + "value": { + "stringValue": "cat" + } + }, + { + "key": "lives", + "value": { + "intValue": "9" + } + } + ], + "events": [ + { + "timeUnixNano": "600", + "name": "somethingHappenedHere", + "attributes": [ + { + "key": "important", + "value": { + "boolValue": true + } + } + ] + } + ], + "links": [], + "status": { + "code": 1 + }, + "flags": 257 + } + ] + }, + { + "scope": { + "name": "instrumentation2", + "version": "2", + "attributes": [] + }, + "spans": [ + { + "traceId": "12340000000043211234000000004321", + "spanId": "8765000000005678", + "name": "testSpan2", + "kind": 3, + "startTimeUnixNano": "500", + "endTimeUnixNano": "1501", + "attributes": [], + "events": [], + "links": [], + "status": { + "code": 2 + }, + "flags": 257 + } + ] + } + ] + } + ] +} diff --git a/exporters/logging-otlp/src/test/resources/expected-spans.json b/exporters/logging-otlp/src/test/resources/expected-spans.json new file mode 100644 index 00000000000..22e57949d90 --- /dev/null +++ b/exporters/logging-otlp/src/test/resources/expected-spans.json @@ -0,0 +1,95 @@ +{ + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "instrumentation", + "version": "1", + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "spans": [ + { + "traceId": "12345678876543211234567887654321", + "spanId": "8765432112345678", + "name": "testSpan1", + "kind": 1, + "startTimeUnixNano": "100", + "endTimeUnixNano": "1100", + "attributes": [ + { + "key": "animal", + "value": { + "stringValue": "cat" + } + }, + { + "key": "lives", + "value": { + "intValue": "9" + } + } + ], + "events": [ + { + "timeUnixNano": "600", + "name": "somethingHappenedHere", + "attributes": [ + { + "key": "important", + "value": { + "boolValue": true + } + } + ] + } + ], + "links": [], + "status": { + "code": 1 + }, + "flags": 257 + } + ] + }, + { + "scope": { + "name": "instrumentation2", + "version": "2", + "attributes": [] + }, + "spans": [ + { + "traceId": "12340000000043211234000000004321", + "spanId": "8765000000005678", + "name": "testSpan2", + "kind": 3, + "startTimeUnixNano": "500", + "endTimeUnixNano": "1501", + "attributes": [], + "events": [], + "links": [], + "status": { + "code": 2 + }, + "flags": 257 + } + ] + } + ] +} diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtil.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtil.java index 1ca885bf5a0..c49430d4468 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtil.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtil.java @@ -5,8 +5,6 @@ package io.opentelemetry.exporter.otlp.internal; -import static io.opentelemetry.sdk.metrics.Aggregation.explicitBucketHistogram; - import io.opentelemetry.exporter.internal.ExporterBuilderUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; @@ -14,12 +12,6 @@ import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; import io.opentelemetry.sdk.common.export.MemoryMode; import io.opentelemetry.sdk.common.export.RetryPolicy; -import io.opentelemetry.sdk.metrics.Aggregation; -import io.opentelemetry.sdk.metrics.InstrumentType; -import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; -import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; -import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; @@ -30,7 +22,6 @@ import java.time.Duration; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -256,96 +247,6 @@ private static void configureOtlpHeaders( } } - /** - * Invoke the {@code aggregationTemporalitySelectorConsumer} with the configured {@link - * AggregationTemporality}. - */ - public static void configureOtlpAggregationTemporality( - ConfigProperties config, - Consumer aggregationTemporalitySelectorConsumer) { - String temporalityStr = config.getString("otel.exporter.otlp.metrics.temporality.preference"); - if (temporalityStr == null) { - return; - } - AggregationTemporalitySelector temporalitySelector; - switch (temporalityStr.toLowerCase(Locale.ROOT)) { - case "cumulative": - temporalitySelector = AggregationTemporalitySelector.alwaysCumulative(); - break; - case "delta": - temporalitySelector = AggregationTemporalitySelector.deltaPreferred(); - break; - case "lowmemory": - temporalitySelector = AggregationTemporalitySelector.lowMemory(); - break; - default: - throw new ConfigurationException("Unrecognized aggregation temporality: " + temporalityStr); - } - aggregationTemporalitySelectorConsumer.accept(temporalitySelector); - } - - public static void configureOtlpAggregationTemporality( - StructuredConfigProperties config, - Consumer aggregationTemporalitySelectorConsumer) { - String temporalityStr = config.getString("temporality_preference"); - if (temporalityStr == null) { - return; - } - AggregationTemporalitySelector temporalitySelector; - switch (temporalityStr.toLowerCase(Locale.ROOT)) { - case "cumulative": - temporalitySelector = AggregationTemporalitySelector.alwaysCumulative(); - break; - case "delta": - temporalitySelector = AggregationTemporalitySelector.deltaPreferred(); - break; - case "lowmemory": - temporalitySelector = AggregationTemporalitySelector.lowMemory(); - break; - default: - throw new ConfigurationException("Unrecognized temporality_preference: " + temporalityStr); - } - aggregationTemporalitySelectorConsumer.accept(temporalitySelector); - } - - /** - * Invoke the {@code defaultAggregationSelectorConsumer} with the configured {@link - * DefaultAggregationSelector}. - */ - public static void configureOtlpHistogramDefaultAggregation( - ConfigProperties config, - Consumer defaultAggregationSelectorConsumer) { - String defaultHistogramAggregation = - config.getString("otel.exporter.otlp.metrics.default.histogram.aggregation"); - if (defaultHistogramAggregation != null) { - ExporterBuilderUtil.configureHistogramDefaultAggregation( - defaultHistogramAggregation, defaultAggregationSelectorConsumer); - } - } - - /** - * Invoke the {@code defaultAggregationSelectorConsumer} with the configured {@link - * DefaultAggregationSelector}. - */ - public static void configureOtlpHistogramDefaultAggregation( - StructuredConfigProperties config, - Consumer defaultAggregationSelectorConsumer) { - String defaultHistogramAggregation = config.getString("default_histogram_aggregation"); - if (defaultHistogramAggregation == null) { - return; - } - if (AggregationUtil.aggregationName(Aggregation.base2ExponentialBucketHistogram()) - .equalsIgnoreCase(defaultHistogramAggregation)) { - defaultAggregationSelectorConsumer.accept( - DefaultAggregationSelector.getDefault() - .with(InstrumentType.HISTOGRAM, Aggregation.base2ExponentialBucketHistogram())); - } else if (!AggregationUtil.aggregationName(explicitBucketHistogram()) - .equalsIgnoreCase(defaultHistogramAggregation)) { - throw new ConfigurationException( - "Unrecognized default_histogram_aggregation: " + defaultHistogramAggregation); - } - } - private static URL createUrl(URL context, String spec) { try { return new URL(context, spec); diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterComponentProvider.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterComponentProvider.java index bac3b205835..2bbec38c533 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterComponentProvider.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterComponentProvider.java @@ -9,6 +9,7 @@ import static io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil.PROTOCOL_GRPC; import static io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil.PROTOCOL_HTTP_PROTOBUF; +import io.opentelemetry.exporter.internal.ExporterBuilderUtil; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; @@ -55,9 +56,9 @@ public MetricExporter create(StructuredConfigProperties config) { builder::setClientTls, builder::setRetryPolicy, builder::setMemoryMode); - OtlpConfigUtil.configureOtlpAggregationTemporality( + ExporterBuilderUtil.configureOtlpAggregationTemporality( config, builder::setAggregationTemporalitySelector); - OtlpConfigUtil.configureOtlpHistogramDefaultAggregation( + ExporterBuilderUtil.configureOtlpHistogramDefaultAggregation( config, builder::setDefaultAggregationSelector); return builder.build(); @@ -75,9 +76,9 @@ public MetricExporter create(StructuredConfigProperties config) { builder::setClientTls, builder::setRetryPolicy, builder::setMemoryMode); - OtlpConfigUtil.configureOtlpAggregationTemporality( + ExporterBuilderUtil.configureOtlpAggregationTemporality( config, builder::setAggregationTemporalitySelector); - OtlpConfigUtil.configureOtlpHistogramDefaultAggregation( + ExporterBuilderUtil.configureOtlpHistogramDefaultAggregation( config, builder::setDefaultAggregationSelector); return builder.build(); diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterProvider.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterProvider.java index 8103e9426cf..a60f57a250c 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterProvider.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpMetricExporterProvider.java @@ -9,6 +9,7 @@ import static io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil.PROTOCOL_GRPC; import static io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil.PROTOCOL_HTTP_PROTOBUF; +import io.opentelemetry.exporter.internal.ExporterBuilderUtil; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; @@ -45,9 +46,9 @@ public MetricExporter createExporter(ConfigProperties config) { builder::setClientTls, builder::setRetryPolicy, builder::setMemoryMode); - OtlpConfigUtil.configureOtlpAggregationTemporality( + ExporterBuilderUtil.configureOtlpAggregationTemporality( config, builder::setAggregationTemporalitySelector); - OtlpConfigUtil.configureOtlpHistogramDefaultAggregation( + ExporterBuilderUtil.configureOtlpHistogramDefaultAggregation( config, builder::setDefaultAggregationSelector); return builder.build(); @@ -65,9 +66,9 @@ public MetricExporter createExporter(ConfigProperties config) { builder::setClientTls, builder::setRetryPolicy, builder::setMemoryMode); - OtlpConfigUtil.configureOtlpAggregationTemporality( + ExporterBuilderUtil.configureOtlpAggregationTemporality( config, builder::setAggregationTemporalitySelector); - OtlpConfigUtil.configureOtlpHistogramDefaultAggregation( + ExporterBuilderUtil.configureOtlpHistogramDefaultAggregation( config, builder::setDefaultAggregationSelector); return builder.build(); diff --git a/exporters/otlp/all/src/test/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtilTest.java b/exporters/otlp/all/src/test/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtilTest.java index 5c678670519..25a0d5cf7ee 100644 --- a/exporters/otlp/all/src/test/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtilTest.java +++ b/exporters/otlp/all/src/test/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtilTest.java @@ -15,6 +15,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.google.common.collect.ImmutableMap; +import io.opentelemetry.exporter.internal.ExporterBuilderUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.metrics.Aggregation; @@ -362,8 +364,8 @@ void configureOtlpAggregationTemporality() { private static AggregationTemporality configureAggregationTemporality( Map properties) { AtomicReference temporalityRef = new AtomicReference<>(); - OtlpConfigUtil.configureOtlpAggregationTemporality( - DefaultConfigProperties.createFromMap(properties), temporalityRef::set); + ConfigProperties config = DefaultConfigProperties.createFromMap(properties); + ExporterBuilderUtil.configureOtlpAggregationTemporality(config, temporalityRef::set); // We apply the temporality selector to a HISTOGRAM instrument to simplify assertions return temporalityRef.get().getAggregationTemporality(InstrumentType.HISTOGRAM); } @@ -409,8 +411,8 @@ void configureOtlpHistogramDefaultAggregation() { private static DefaultAggregationSelector configureHistogramDefaultAggregation( Map properties) { AtomicReference aggregationRef = new AtomicReference<>(); - OtlpConfigUtil.configureOtlpHistogramDefaultAggregation( - DefaultConfigProperties.createFromMap(properties), aggregationRef::set); + ConfigProperties config = DefaultConfigProperties.createFromMap(properties); + ExporterBuilderUtil.configureOtlpHistogramDefaultAggregation(config, aggregationRef::set); // We apply the temporality selector to a HISTOGRAM instrument to simplify assertions return aggregationRef.get(); }