Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ exporter.
([#8609](https://github.com/open-telemetry/opentelemetry-java/pull/8609))
* Logging: Include aggregation temporality in `LoggingMetricExporter` `toString`
([#8623](https://github.com/open-telemetry/opentelemetry-java/pull/8623))
* Logging: Support the `output_stream` option when declaratively configuring `otlp_file/development`
([#8676](https://github.com/open-telemetry/opentelemetry-java/pull/8676))

#### Extensions

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.
*
* <p>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.
*
* <p>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<OutputStream> outputStreamConsumer) {
String outputStream = config.getString("output_stream");
if (outputStream == null) {
return;
}
if (STDOUT.equalsIgnoreCase(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 output_stream: " + outputStream, e);
}
if (!FILE_SCHEME.equalsIgnoreCase(uri.getScheme())) {
throw new ConfigurationException("Unrecognized output_stream: " + outputStream);
}
try {
return Paths.get(uri);
} catch (IllegalArgumentException | FileSystemNotFoundException e) {
throw new ConfigurationException("Unrecognized output_stream: " + outputStream, e);
}
}

private OutputStreamConfigUtil() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,7 @@
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -302,6 +304,92 @@ void componentProviderConfig() {
.isEqualTo(MemoryMode.REUSABLE_DATA);
}

static Stream<Arguments> outputStreamStdoutTestCases() {
return Stream.of(
Arguments.argumentSet("stdout", "stdout"), Arguments.argumentSet("upper case", "STDOUT"));
}

@ParameterizedTest
@MethodSource("outputStreamStdoutTestCases")
void componentProviderConfigOutputStreamStdout(String value) {
DeclarativeConfigProperties properties = spy(DeclarativeConfigProperties.empty());
when(properties.getString("output_stream")).thenReturn(value);

assertThat(exporterFromComponentProvider(properties))
.extracting("jsonWriter")
.extracting(Object::toString)
.isEqualTo("StreamJsonWriter{outputStream=stdout}");
}

static Stream<Arguments> outputStreamFileTestCases() {
return Stream.of(
Arguments.argumentSet("file uri", "test.jsonl", (UnaryOperator<String>) uri -> uri),
Arguments.argumentSet(
"authority-less file uri with upper case scheme",
"test.jsonl",
(UnaryOperator<String>) uri -> uri.replaceFirst("^file://", "FILE:")),
Arguments.argumentSet(
"missing parent directory", "missing/test.jsonl", (UnaryOperator<String>) uri -> uri));
}

@ParameterizedTest
@MethodSource("outputStreamFileTestCases")
void componentProviderConfigOutputStreamFile(String relativePath, UnaryOperator<String> uriForm)
throws Exception {
Path file = tempDir.resolve(relativePath);
DeclarativeConfigProperties properties = spy(DeclarativeConfigProperties.empty());
when(properties.getString("output_stream")).thenReturn(uriForm.apply(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<Arguments> outputStreamUnrecognizedTestCases() {
return Stream.of(
Arguments.argumentSet("no scheme", "not-a-stream"),
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);
}

@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")
protected T exporterFromComponentProvider(DeclarativeConfigProperties properties) {
return (T)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading