diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/OutputStreamConfigUtil.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/OutputStreamConfigUtil.java
new file mode 100644
index 00000000000..ed0b8a3d707
--- /dev/null
+++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/OutputStreamConfigUtil.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.exporter.logging.otlp.internal;
+
+import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.function.Consumer;
+
+/**
+ * Utilities for configuring the output stream of the OTLP file exporters.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public final class OutputStreamConfigUtil {
+
+ private static final String STDOUT = "stdout";
+ private static final String FILE_SCHEME = "file";
+
+ /**
+ * Invoke the {@code outputStreamConsumer} with the configured output stream.
+ *
+ *
Recognized values are {@code stdout} and a file URI such as {@code
+ * file:///path/to/file.jsonl}. Missing parent directories of the file are created, and the file
+ * is appended to if it already exists.
+ */
+ @SuppressWarnings("SystemOut")
+ public static void configureOutputStream(
+ DeclarativeConfigProperties config, Consumer outputStreamConsumer) {
+ String outputStream = config.getString("output_stream");
+ if (outputStream == null) {
+ return;
+ }
+ if (STDOUT.equals(outputStream)) {
+ outputStreamConsumer.accept(System.out);
+ return;
+ }
+ Path path = filePath(outputStream);
+ try {
+ Path parent = path.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ outputStreamConsumer.accept(
+ new BufferedOutputStream(
+ Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.APPEND)));
+ } catch (IOException e) {
+ throw new ConfigurationException("Unable to open output_stream: " + outputStream, e);
+ }
+ }
+
+ private static Path filePath(String outputStream) {
+ URI uri;
+ try {
+ uri = new URI(outputStream);
+ } catch (URISyntaxException e) {
+ throw new ConfigurationException(unrecognized(outputStream), e);
+ }
+ if (!FILE_SCHEME.equals(uri.getScheme())) {
+ throw new ConfigurationException(unrecognized(outputStream));
+ }
+ try {
+ return Paths.get(uri);
+ } catch (IllegalArgumentException | FileSystemNotFoundException e) {
+ throw new ConfigurationException(unrecognized(outputStream), e);
+ }
+ }
+
+ private static String unrecognized(String outputStream) {
+ return "Unrecognized output_stream: "
+ + outputStream
+ + ". Expected \"stdout\" or a file:// URI.";
+ }
+
+ private OutputStreamConfigUtil() {}
+}
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 3feebf33811..c2a943f2f17 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
@@ -7,6 +7,7 @@
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.exporter.internal.IncubatingExporterBuilderUtil;
+import io.opentelemetry.exporter.logging.otlp.internal.OutputStreamConfigUtil;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
@@ -32,6 +33,7 @@ public String getName() {
public LogRecordExporter create(DeclarativeConfigProperties config) {
OtlpStdoutLogRecordExporterBuilder builder = OtlpStdoutLogRecordExporter.builder();
IncubatingExporterBuilderUtil.configureExporterMemoryMode(config, builder::setMemoryMode);
+ OutputStreamConfigUtil.configureOutputStream(config, builder::setOutput);
return builder.build();
}
}
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
index bf8d8a73be9..2d62bf105f1 100644
--- 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
@@ -7,6 +7,7 @@
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.exporter.internal.IncubatingExporterBuilderUtil;
+import io.opentelemetry.exporter.logging.otlp.internal.OutputStreamConfigUtil;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.metrics.export.MetricExporter;
@@ -36,6 +37,7 @@ public MetricExporter create(DeclarativeConfigProperties config) {
config, builder::setAggregationTemporalitySelector);
IncubatingExporterBuilderUtil.configureOtlpHistogramDefaultAggregation(
config, builder::setDefaultAggregationSelector);
+ OutputStreamConfigUtil.configureOutputStream(config, builder::setOutput);
return builder.build();
}
}
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
index 969b804f863..9d51f2b9fac 100644
--- 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
@@ -7,6 +7,7 @@
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.exporter.internal.IncubatingExporterBuilderUtil;
+import io.opentelemetry.exporter.logging.otlp.internal.OutputStreamConfigUtil;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.trace.export.SpanExporter;
@@ -32,6 +33,7 @@ public String getName() {
public SpanExporter create(DeclarativeConfigProperties config) {
OtlpStdoutSpanExporterBuilder builder = OtlpStdoutSpanExporter.builder();
IncubatingExporterBuilderUtil.configureExporterMemoryMode(config, builder::setMemoryMode);
+ OutputStreamConfigUtil.configureOutputStream(config, builder::setOutput);
return builder.build();
}
}
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
index 406856fe5a8..b97558948d2 100644
--- 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
@@ -16,6 +16,7 @@
import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.common.export.MemoryMode;
@@ -302,6 +303,83 @@ void componentProviderConfig() {
.isEqualTo(MemoryMode.REUSABLE_DATA);
}
+ @Test
+ void componentProviderConfigOutputStreamStdout() {
+ DeclarativeConfigProperties properties = spy(DeclarativeConfigProperties.empty());
+ when(properties.getString("output_stream")).thenReturn("stdout");
+
+ assertThat(exporterFromComponentProvider(properties))
+ .extracting("jsonWriter")
+ .extracting(Object::toString)
+ .isEqualTo("StreamJsonWriter{outputStream=stdout}");
+ }
+
+ static Stream outputStreamFileTestCases() {
+ return Stream.of(
+ Arguments.argumentSet("file uri", "test.jsonl"),
+ Arguments.argumentSet("missing parent directory", "missing/test.jsonl"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("outputStreamFileTestCases")
+ void componentProviderConfigOutputStreamFile(String relativePath) throws Exception {
+ Path file = tempDir.resolve(relativePath);
+ DeclarativeConfigProperties properties = spy(DeclarativeConfigProperties.empty());
+ when(properties.getString("output_stream")).thenReturn(file.toUri().toString());
+
+ T exporter = exporterFromComponentProvider(properties);
+ testDataExporter.export(exporter);
+
+ String output = new String(Files.readAllBytes(file), StandardCharsets.UTF_8).trim();
+ JSONAssert.assertEquals(
+ "Got \n" + output,
+ testDataExporter.getExpectedJson(/* withWrapper= */ true),
+ output,
+ false);
+
+ // an exporter created later for the same path appends instead of truncating
+ testDataExporter.shutdown(exporter);
+ T secondExporter = exporterFromComponentProvider(properties);
+ testDataExporter.export(secondExporter);
+ testDataExporter.shutdown(secondExporter);
+ assertThat(new String(Files.readAllBytes(file), StandardCharsets.UTF_8).trim().split("\n"))
+ .hasSize(2);
+ }
+
+ static Stream outputStreamUnrecognizedTestCases() {
+ return Stream.of(
+ Arguments.argumentSet("no scheme", "not-a-stream"),
+ Arguments.argumentSet("upper case stdout", "STDOUT"),
+ Arguments.argumentSet("upper case file scheme", "FILE:///test.jsonl"),
+ Arguments.argumentSet("unsupported scheme", "http://example.com/traces.jsonl"),
+ Arguments.argumentSet("relative file uri", "file://traces.jsonl"),
+ Arguments.argumentSet("malformed file uri", "file:///with space.jsonl"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("outputStreamUnrecognizedTestCases")
+ void componentProviderConfigOutputStreamUnrecognized(String value) {
+ DeclarativeConfigProperties properties = spy(DeclarativeConfigProperties.empty());
+ when(properties.getString("output_stream")).thenReturn(value);
+
+ assertThatExceptionOfType(ConfigurationException.class)
+ .isThrownBy(() -> exporterFromComponentProvider(properties))
+ .withMessage(
+ "Unrecognized output_stream: " + value + ". Expected \"stdout\" or a file:// URI.");
+ }
+
+ @Test
+ void componentProviderConfigOutputStreamNotOpenable() {
+ DeclarativeConfigProperties properties = spy(DeclarativeConfigProperties.empty());
+ // the path exists but is a directory, so it cannot be opened for writing
+ when(properties.getString("output_stream")).thenReturn(tempDir.toUri().toString());
+
+ assertThatExceptionOfType(ConfigurationException.class)
+ .isThrownBy(() -> exporterFromComponentProvider(properties))
+ .withMessageStartingWith("Unable to open output_stream: ")
+ .withCauseInstanceOf(IOException.class);
+ }
+
@SuppressWarnings("unchecked") // Each test subclass selects a provider that creates T.
protected T exporterFromComponentProvider(DeclarativeConfigProperties properties) {
return (T)
diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java
index d0ab0fb0476..6c0ca5c8309 100644
--- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java
+++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java
@@ -96,6 +96,11 @@ void parseAndCreate_Examples(File example, @TempDir Path tempDir)
"cert_file: "
+ clientCertificatePath.replace("\\", "\\\\")
+ System.lineSeparator())
+ // Snippets write to file:///var/log/*.jsonl, which is not writable in tests. The
+ // value can be the last line of the file, so the line terminator is not matched.
+ .replaceAll(
+ "output_stream: file:.*",
+ "output_stream: " + tempDir.resolve("output.jsonl").toUri())
// A snippet references a custom id generator named my_custom_id_generator. Replace with
// one named test, which we provide via SPI
.replace("my_custom_id_generator", "test");