diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index 9eb42f21444..c357dddc847 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -87,7 +87,6 @@ tasks { "-Xlint:-processing", // We suppress the "options" warning because it prevents compilation on modern JDKs "-Xlint:-options", - // Fail build on any warning "-Werror", ), diff --git a/exporters/logging-otlp/build.gradle.kts b/exporters/logging-otlp/build.gradle.kts index 07a2044b5f3..a65f52f8522 100644 --- a/exporters/logging-otlp/build.gradle.kts +++ b/exporters/logging-otlp/build.gradle.kts @@ -20,5 +20,6 @@ dependencies { testImplementation(project(":sdk:testing")) + testImplementation("com.google.guava:guava") testImplementation("org.skyscreamer:jsonassert") } diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporter.java index c5b7d75cbe5..0c00cca908e 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporter.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporter.java @@ -5,18 +5,12 @@ package io.opentelemetry.exporter.logging.otlp; -import static io.opentelemetry.exporter.logging.otlp.JsonUtil.JSON_FACTORY; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.io.SegmentedStringWriter; -import io.opentelemetry.exporter.internal.otlp.logs.ResourceLogsMarshaler; +import io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporter; +import io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporterBuilder; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import java.io.IOException; import java.util.Collection; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; import java.util.logging.Logger; /** @@ -30,49 +24,31 @@ public final class OtlpJsonLoggingLogRecordExporter implements LogRecordExporter private static final Logger logger = Logger.getLogger(OtlpJsonLoggingLogRecordExporter.class.getName()); - private final AtomicBoolean isShutdown = new AtomicBoolean(); + private final OtlpStdoutLogRecordExporter delegate; /** Returns a new {@link OtlpJsonLoggingLogRecordExporter}. */ public static LogRecordExporter create() { - return new OtlpJsonLoggingLogRecordExporter(); + OtlpStdoutLogRecordExporter delegate = + new OtlpStdoutLogRecordExporterBuilder(logger).setWrapperJsonObject(false).build(); + return new OtlpJsonLoggingLogRecordExporter(delegate); } - private OtlpJsonLoggingLogRecordExporter() {} + OtlpJsonLoggingLogRecordExporter(OtlpStdoutLogRecordExporter delegate) { + this.delegate = delegate; + } @Override public CompletableResultCode export(Collection logs) { - if (isShutdown.get()) { - return CompletableResultCode.ofFailure(); - } - - ResourceLogsMarshaler[] allResourceLogs = ResourceLogsMarshaler.create(logs); - for (ResourceLogsMarshaler resourceLogs : allResourceLogs) { - SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); - try (JsonGenerator gen = JsonUtil.create(sw)) { - resourceLogs.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 log records", e); - } - } - return CompletableResultCode.ofSuccess(); + 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/OtlpJsonLoggingMetricExporter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java index caacea6e7eb..0f66cc57c95 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,11 +5,12 @@ package io.opentelemetry.exporter.logging.otlp; -import static io.opentelemetry.exporter.logging.otlp.JsonUtil.JSON_FACTORY; +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.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; 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 c57c9004d56..cc944e9bab6 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,9 +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.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -43,8 +46,7 @@ public CompletableResultCode export(Collection spans) { ResourceSpansMarshaler[] allResourceSpans = ResourceSpansMarshaler.create(spans); for (ResourceSpansMarshaler resourceSpans : allResourceSpans) { - SegmentedStringWriter sw = - new SegmentedStringWriter(JsonUtil.JSON_FACTORY._getBufferRecycler()); + SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); try (JsonGenerator gen = JsonUtil.create(sw)) { resourceSpans.writeJsonTo(gen); } catch (IOException e) { diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingLogRecordExporterProvider.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/LoggingLogRecordExporterProvider.java similarity index 93% rename from exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingLogRecordExporterProvider.java rename to exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/LoggingLogRecordExporterProvider.java index ebb0d2d0865..a08aafee355 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingLogRecordExporterProvider.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/LoggingLogRecordExporterProvider.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.logs; import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingLogRecordExporter; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -17,6 +17,7 @@ * at any time. */ public class LoggingLogRecordExporterProvider implements ConfigurableLogRecordExporterProvider { + @Override public LogRecordExporter createExporter(ConfigProperties config) { return OtlpJsonLoggingLogRecordExporter.create(); 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 new file mode 100644 index 00000000000..6a7adb6f742 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporter.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.logs; + +import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; +import io.opentelemetry.exporter.internal.otlp.logs.ResourceLogsMarshaler; +import io.opentelemetry.exporter.logging.otlp.internal.writer.JsonWriter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +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 log records to stdout. + * + *

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 { + + private static final Logger LOGGER = + Logger.getLogger(OtlpStdoutLogRecordExporter.class.getName()); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + private final Logger logger; + private final JsonWriter jsonWriter; + private final boolean wrapperJsonObject; + + OtlpStdoutLogRecordExporter(Logger logger, JsonWriter jsonWriter, boolean wrapperJsonObject) { + this.logger = logger; + this.jsonWriter = jsonWriter; + this.wrapperJsonObject = wrapperJsonObject; + } + + /** Returns a new {@link OtlpStdoutLogRecordExporterBuilder}. */ + @SuppressWarnings("SystemOut") + public static OtlpStdoutLogRecordExporterBuilder builder() { + return new OtlpStdoutLogRecordExporterBuilder(LOGGER).setOutput(System.out); + } + + @Override + public CompletableResultCode export(Collection logs) { + if (isShutdown.get()) { + return CompletableResultCode.ofFailure(); + } + + if (wrapperJsonObject) { + LogsRequestMarshaler request = LogsRequestMarshaler.create(logs); + return jsonWriter.write(request); + } else { + for (ResourceLogsMarshaler resourceLogs : ResourceLogsMarshaler.create(logs)) { + CompletableResultCode resultCode = jsonWriter.write(resourceLogs); + 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(", ", "OtlpStdoutLogRecordExporter{", "}"); + 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/logs/OtlpStdoutLogRecordExporterBuilder.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterBuilder.java new file mode 100644 index 00000000000..ea3f5c14234 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterBuilder.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.logs; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingLogRecordExporter; +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 OtlpJsonLoggingLogRecordExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OtlpStdoutLogRecordExporterBuilder { + + private static final String TYPE = "log records"; + + private final Logger logger; + private JsonWriter jsonWriter; + private boolean wrapperJsonObject = true; + + public OtlpStdoutLogRecordExporterBuilder(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 "resourceLogs" + * object. + */ + public OtlpStdoutLogRecordExporterBuilder 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 OtlpStdoutLogRecordExporter#shutdown()} is + * called unless it's {@link System#out} or {@link System#err}. + * + * @param outputStream the output stream to use. + */ + public OtlpStdoutLogRecordExporterBuilder setOutput(OutputStream outputStream) { + requireNonNull(outputStream, "outputStream"); + this.jsonWriter = new StreamJsonWriter(outputStream, TYPE); + return this; + } + + /** Sets the exporter to use the specified logger. */ + public OtlpStdoutLogRecordExporterBuilder 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 OtlpStdoutLogRecordExporter build() { + return new OtlpStdoutLogRecordExporter(logger, jsonWriter, wrapperJsonObject); + } +} 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 new file mode 100644 index 00000000000..204f5673c08 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterComponentProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.logs; + +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; + +/** + * File configuration SPI implementation for {@link OtlpStdoutLogRecordExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class OtlpStdoutLogRecordExporterComponentProvider + implements ComponentProvider { + + @Override + public Class getType() { + return LogRecordExporter.class; + } + + @Override + public String getName() { + return "experimental-otlp/stdout"; + } + + @Override + public LogRecordExporter create(StructuredConfigProperties config) { + OtlpStdoutLogRecordExporterBuilder builder = OtlpStdoutLogRecordExporter.builder(); + return builder.build(); + } +} 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 new file mode 100644 index 00000000000..b73262cea39 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/logs/OtlpStdoutLogRecordExporterProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.logs; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; + +/** + * {@link LogRecordExporter} SPI implementation for {@link OtlpStdoutLogRecordExporter}. + * + *

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 { + @Override + public LogRecordExporter createExporter(ConfigProperties config) { + OtlpStdoutLogRecordExporterBuilder builder = OtlpStdoutLogRecordExporter.builder(); + 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/JsonUtil.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonUtil.java similarity index 58% rename from exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/JsonUtil.java rename to exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonUtil.java index b4b8cbc577c..0b74ed8a478 100644 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/JsonUtil.java +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonUtil.java @@ -3,18 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.exporter.logging.otlp; +package io.opentelemetry.exporter.logging.otlp.internal.writer; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.io.SegmentedStringWriter; import java.io.IOException; -final class JsonUtil { +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class JsonUtil { - static final JsonFactory JSON_FACTORY = new JsonFactory(); + public static final JsonFactory JSON_FACTORY = new JsonFactory(); - static JsonGenerator create(SegmentedStringWriter stringWriter) { + public static JsonGenerator create(SegmentedStringWriter stringWriter) { try { return JSON_FACTORY.createGenerator(stringWriter); } catch (IOException e) { diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonWriter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonWriter.java new file mode 100644 index 00000000000..bfee16cba65 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonWriter.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.writer; + +import io.opentelemetry.exporter.internal.marshal.Marshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface JsonWriter { + CompletableResultCode write(Marshaler exportRequest); + + CompletableResultCode flush(); + + CompletableResultCode close(); +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/LoggerJsonWriter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/LoggerJsonWriter.java new file mode 100644 index 00000000000..1286e116f03 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/LoggerJsonWriter.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.writer; + +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.marshal.Marshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class LoggerJsonWriter implements JsonWriter { + + private final Logger logger; + private final String type; + + public LoggerJsonWriter(Logger logger, String type) { + this.logger = logger; + this.type = type; + } + + @Override + public CompletableResultCode write(Marshaler exportRequest) { + SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); + try (JsonGenerator gen = JsonUtil.create(sw)) { + exportRequest.writeJsonTo(gen); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); + return CompletableResultCode.ofFailure(); + } + + try { + logger.log(Level.INFO, sw.getAndClear()); + return CompletableResultCode.ofSuccess(); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); + return CompletableResultCode.ofFailure(); + } + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode close() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public String toString() { + return "LoggerJsonWriter"; + } +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/StreamJsonWriter.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/StreamJsonWriter.java new file mode 100644 index 00000000000..0674810fa5c --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/StreamJsonWriter.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.writer; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import io.opentelemetry.exporter.internal.marshal.Marshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class StreamJsonWriter implements JsonWriter { + + public static final JsonFactory JSON_FACTORY = new JsonFactory(); + + private static final Logger internalLogger = Logger.getLogger(StreamJsonWriter.class.getName()); + + private final ThrottlingLogger logger = new ThrottlingLogger(internalLogger); + + private final String type; + private final OutputStream outputStream; + + public StreamJsonWriter(OutputStream originalStream, String type) { + this.outputStream = originalStream; + this.type = type; + } + + @Override + public CompletableResultCode write(Marshaler exportRequest) { + try { + exportRequest.writeJsonTo( + JSON_FACTORY + .createGenerator(outputStream) + .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET)); + return CompletableResultCode.ofSuccess(); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); + return CompletableResultCode.ofFailure(); + } + } + + @Override + public CompletableResultCode flush() { + try { + outputStream.flush(); + return CompletableResultCode.ofSuccess(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to flush items", e); + return CompletableResultCode.ofFailure(); + } + } + + @SuppressWarnings("SystemOut") + @Override + public CompletableResultCode close() { + if (outputStream == System.out || outputStream == System.err) { + // closing System.out or System.err is not allowed - it breaks the output stream + return CompletableResultCode.ofSuccess(); + } + try { + outputStream.close(); + return CompletableResultCode.ofSuccess(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to close stream", e); + return CompletableResultCode.ofFailure(); + } + } + + @Override + public String toString() { + return "StreamJsonWriter{" + "outputStream=" + getName(outputStream) + '}'; + } + + @SuppressWarnings("SystemOut") + private static String getName(OutputStream outputStream) { + if (outputStream == System.out) { + return "stdout"; + } + if (outputStream == System.err) { + return "stderr"; + } + return outputStream.toString(); + } +} 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 new file mode 100644 index 00000000000..76364a2dae4 --- /dev/null +++ b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -0,0 +1 @@ +io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporterComponentProvider diff --git a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider index 9119f54ef00..20763b2e4e5 100644 --- a/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider +++ b/exporters/logging-otlp/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider @@ -1 +1,2 @@ -io.opentelemetry.exporter.logging.otlp.internal.LoggingLogRecordExporterProvider +io.opentelemetry.exporter.logging.otlp.internal.logs.LoggingLogRecordExporterProvider +io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporterProvider diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporterTest.java index 27ce4c3ea99..75677620dbb 100644 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporterTest.java +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingLogRecordExporterTest.java @@ -5,25 +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.logs.Severity; -import io.opentelemetry.api.trace.SpanContext; -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.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.logs.TestLogRecordData; -import java.util.Arrays; -import java.util.Collections; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,49 +20,7 @@ @SuppressLogger(OtlpJsonLoggingLogRecordExporter.class) class OtlpJsonLoggingLogRecordExporterTest { - private static final Resource RESOURCE = - Resource.create(Attributes.builder().put("key", "value").build()); - - private static final LogRecordData LOG1 = - TestLogRecordData.builder() - .setResource(RESOURCE) - .setInstrumentationScopeInfo( - InstrumentationScopeInfo.builder("instrumentation") - .setVersion("1") - .setAttributes(Attributes.builder().put("key", "value").build()) - .build()) - .setBody("body1") - .setSeverity(Severity.INFO) - .setSeverityText("INFO") - .setTimestamp(100L, TimeUnit.NANOSECONDS) - .setObservedTimestamp(200L, TimeUnit.NANOSECONDS) - .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) - .setSpanContext( - SpanContext.create( - "12345678876543211234567887654322", - "8765432112345876", - TraceFlags.getDefault(), - TraceState.getDefault())) - .build(); - - private static final LogRecordData LOG2 = - TestLogRecordData.builder() - .setResource(RESOURCE) - .setInstrumentationScopeInfo( - InstrumentationScopeInfo.builder("instrumentation2").setVersion("2").build()) - .setBody("body2") - .setSeverity(Severity.INFO) - .setSeverityText("INFO") - .setTimestamp(100L, TimeUnit.NANOSECONDS) - .setObservedTimestamp(200L, TimeUnit.NANOSECONDS) - .setAttributes(Attributes.of(booleanKey("important"), true)) - .setSpanContext( - SpanContext.create( - "12345678876543211234567887654322", - "8765432112345875", - TraceFlags.getDefault(), - TraceState.getDefault())) - .build(); + private final TestDataExporter testDataExporter = TestDataExporter.forLogs(); @RegisterExtension LogCapturer logs = LogCapturer.create().captureForType(OtlpJsonLoggingLogRecordExporter.class); @@ -90,90 +34,21 @@ void setUp() { @Test void log() throws Exception { - exporter.export(Arrays.asList(LOG1, LOG2)); + 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\"" - + " }" - + " }]" - + " }," - + " \"scopeLogs\": [{" - + " \"scope\":{" - + " \"name\":\"instrumentation2\"," - + " \"version\":\"2\"" - + " }," - + " \"logRecords\": [{" - + " \"timeUnixNano\":\"100\"," - + " \"observedTimeUnixNano\":\"200\"," - + " \"severityNumber\":9," - + " \"severityText\":\"INFO\"," - + " \"body\": {" - + " \"stringValue\":\"body2\"" - + " }," - + " \"attributes\": [{" - + " \"key\":\"important\"," - + " \"value\": {" - + " \"boolValue\":true" - + " }" - + " }]," - + " \"traceId\":\"12345678876543211234567887654322\"," - + " \"spanId\":\"8765432112345875\"" - + " }]" - + " }, {" - + " \"scope\": {" - + " \"name\":\"instrumentation\"," - + " \"version\":\"1\"," - + " \"attributes\": [{" - + " \"key\":\"key\"," - + " \"value\": {" - + " \"stringValue\":\"value\"" - + " }" - + " }]" - + " }," - + " \"logRecords\": [{" - + " \"timeUnixNano\":\"100\"," - + " \"observedTimeUnixNano\":\"200\"," - + " \"severityNumber\":9," - + " \"severityText\":\"INFO\"," - + " \"body\": {" - + " \"stringValue\":\"body1\"" - + " }," - + " \"attributes\": [{" - + " \"key\":\"animal\"," - + " \"value\": {" - + " \"stringValue\":\"cat\"" - + " }" - + " }, {" - + " \"key\":\"lives\"," - + " \"value\":{" - + " \"intValue\":\"9\"" - + " }" - + " }]," - + " \"traceId\":\"12345678876543211234567887654322\"," - + " \"spanId\":\"8765432112345876\"" - + " }]" - + " }]" - + "}", - message, - /* strict= */ false); + String expectedJson = testDataExporter.getExpectedJson(false); + JSONAssert.assertEquals("Got \n" + message, expectedJson, message, /* strict= */ false); assertThat(message).doesNotContain("\n"); } @Test void shutdown() { assertThat(exporter.shutdown().isSuccess()).isTrue(); - assertThat( - exporter.export(Collections.singletonList(LOG1)).join(10, TimeUnit.SECONDS).isSuccess()) - .isFalse(); + assertThat(testDataExporter.export(exporter).join(10, TimeUnit.SECONDS).isSuccess()).isFalse(); assertThat(logs.getEvents()).isEmpty(); assertThat(exporter.shutdown().isSuccess()).isTrue(); logs.assertContains("Calling shutdown() multiple times."); 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 new file mode 100644 index 00000000000..f98b81b69af --- /dev/null +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpStdoutLogRecordExporterTest.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.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)); + } + + private static Stream loadSpi(Class type) { + return Streams.stream(ServiceLoader.load(type, type.getClassLoader()).iterator()); + } + + private static OtlpStdoutLogRecordExporter createDefaultExporter() { + return OtlpStdoutLogRecordExporter.builder().build(); + } + + private static 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())); + } + 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/TestDataExporter.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java new file mode 100644 index 00000000000..53b51629e1c --- /dev/null +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 com.google.common.io.Resources; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +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.resources.Resource; +import io.opentelemetry.sdk.testing.logs.TestLogRecordData; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +abstract class TestDataExporter { + + private final String expectedFileNoWrapper; + private final String expectedFileWrapper; + private static final Resource RESOURCE = + Resource.create(Attributes.builder().put("key", "value").build()); + + private static final LogRecordData LOG1 = + TestLogRecordData.builder() + .setResource(RESOURCE) + .setInstrumentationScopeInfo( + InstrumentationScopeInfo.builder("instrumentation") + .setVersion("1") + .setAttributes(Attributes.builder().put("key", "value").build()) + .build()) + .setBody("body1") + .setSeverity(Severity.INFO) + .setSeverityText("INFO") + .setTimestamp(100L, TimeUnit.NANOSECONDS) + .setObservedTimestamp(200L, TimeUnit.NANOSECONDS) + .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) + .setSpanContext( + SpanContext.create( + "12345678876543211234567887654322", + "8765432112345876", + TraceFlags.getDefault(), + TraceState.getDefault())) + .build(); + + private static final LogRecordData LOG2 = + TestLogRecordData.builder() + .setResource(RESOURCE) + .setInstrumentationScopeInfo( + InstrumentationScopeInfo.builder("instrumentation2").setVersion("2").build()) + .setBody("body2") + .setSeverity(Severity.INFO) + .setSeverityText("INFO") + .setTimestamp(100L, TimeUnit.NANOSECONDS) + .setObservedTimestamp(200L, TimeUnit.NANOSECONDS) + .setAttributes(Attributes.of(booleanKey("important"), true)) + .setSpanContext( + SpanContext.create( + "12345678876543211234567887654322", + "8765432112345875", + TraceFlags.getDefault(), + TraceState.getDefault())) + .build(); + + public TestDataExporter(String expectedFileNoWrapper, String expectedFileWrapper) { + this.expectedFileNoWrapper = expectedFileNoWrapper; + this.expectedFileWrapper = expectedFileWrapper; + } + + public String getExpectedJson(boolean withWrapper) throws IOException { + String file = withWrapper ? expectedFileWrapper : expectedFileNoWrapper; + return Resources.toString(Resources.getResource(file), StandardCharsets.UTF_8); + } + + abstract CompletableResultCode export(T exporter); + + abstract CompletableResultCode flush(T exporter); + + abstract CompletableResultCode shutdown(T exporter); + + static TestDataExporter forLogs() { + return new TestDataExporter( + "expected-logs.json", "expected-logs-wrapper.json") { + @Override + public CompletableResultCode export(LogRecordExporter exporter) { + return exporter.export(Arrays.asList(LOG1, LOG2)); + } + + @Override + public CompletableResultCode flush(LogRecordExporter exporter) { + return exporter.flush(); + } + + @Override + public CompletableResultCode shutdown(LogRecordExporter exporter) { + return exporter.shutdown(); + } + }; + } +} diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingExporterProviderTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingExporterProviderTest.java deleted file mode 100644 index decd00aec4b..00000000000 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/LoggingExporterProviderTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.logging.otlp.internal; - -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; - -import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingLogRecordExporter; -import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingMetricExporter; -import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingSpanExporter; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import java.util.Collections; -import org.junit.jupiter.api.Test; - -class LoggingExporterProviderTest { - - @Test - void logRecordExporterProvider() { - LoggingLogRecordExporterProvider provider = new LoggingLogRecordExporterProvider(); - assertThat(provider.getName()).isEqualTo("logging-otlp"); - assertThat( - provider.createExporter(DefaultConfigProperties.createFromMap(Collections.emptyMap()))) - .isInstanceOf(OtlpJsonLoggingLogRecordExporter.class); - } - - @Test - void metricExporterProvider() { - LoggingMetricExporterProvider provider = new LoggingMetricExporterProvider(); - assertThat(provider.getName()).isEqualTo("logging-otlp"); - assertThat( - provider.createExporter(DefaultConfigProperties.createFromMap(Collections.emptyMap()))) - .isInstanceOf(OtlpJsonLoggingMetricExporter.class); - } - - @Test - void spanExporterProvider() { - LoggingSpanExporterProvider provider = new LoggingSpanExporterProvider(); - assertThat(provider.getName()).isEqualTo("logging-otlp"); - assertThat( - provider.createExporter(DefaultConfigProperties.createFromMap(Collections.emptyMap()))) - .isInstanceOf(OtlpJsonLoggingSpanExporter.class); - } -} diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/writer/LoggerJsonWriterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/writer/LoggerJsonWriterTest.java new file mode 100644 index 00000000000..8cba4ef9707 --- /dev/null +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/writer/LoggerJsonWriterTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.writer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; + +import com.fasterxml.jackson.core.JsonGenerator; +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.exporter.internal.marshal.Marshaler; +import java.io.IOException; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +class LoggerJsonWriterTest { + + @RegisterExtension + static final LogCapturer logs = LogCapturer.create().captureForType(LoggerJsonWriter.class); + + @Test + void testToString() { + LoggerJsonWriter writer = new LoggerJsonWriter(null, "type"); + assertThat(writer.toString()).isEqualTo("LoggerJsonWriter"); + } + + @Test + void error() throws IOException { + Marshaler marshaler = mock(Marshaler.class); + Mockito.doThrow(new IOException("test")).when(marshaler).writeJsonTo(any(JsonGenerator.class)); + + Logger logger = Logger.getLogger(LoggerJsonWriter.class.getName()); + + LoggerJsonWriter writer = new LoggerJsonWriter(logger, "type"); + writer.write(marshaler); + + logs.assertContains("Unable to write OTLP JSON type"); + } +} diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/writer/StreamJsonWriterTest.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/writer/StreamJsonWriterTest.java new file mode 100644 index 00000000000..2245a15e0c0 --- /dev/null +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/internal/writer/StreamJsonWriterTest.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.writer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; + +import com.fasterxml.jackson.core.JsonGenerator; +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.exporter.internal.marshal.Marshaler; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +@SuppressWarnings("SystemOut") +class StreamJsonWriterTest { + + @RegisterExtension + static final LogCapturer logs = LogCapturer.create().captureForType(StreamJsonWriter.class); + + @TempDir Path tempDir; + + @Test + @SuppressWarnings("SystemOut") + void testToString() throws IOException { + assertThat( + new StreamJsonWriter(Files.newOutputStream(tempDir.resolve("foo")), "type").toString()) + .startsWith("StreamJsonWriter{outputStream=") + .contains("Channel"); + assertThat(new StreamJsonWriter(System.out, "type").toString()) + .isEqualTo("StreamJsonWriter{outputStream=stdout}"); + assertThat(new StreamJsonWriter(System.err, "type").toString()) + .isEqualTo("StreamJsonWriter{outputStream=stderr}"); + } + + @Test + void errorWriting() throws IOException { + Marshaler marshaler = mock(Marshaler.class); + Mockito.doThrow(new IOException("test")).when(marshaler).writeJsonTo(any(JsonGenerator.class)); + + StreamJsonWriter writer = new StreamJsonWriter(System.out, "type"); + writer.write(marshaler); + + logs.assertContains("Unable to write OTLP JSON type"); + } + + @Test + void errorFlushing() { + OutputStream outputStream = + new FilterOutputStream(System.out) { + @Override + public void flush() throws IOException { + throw new IOException("No flush"); + } + }; + + StreamJsonWriter writer = new StreamJsonWriter(outputStream, "type"); + writer.flush(); + + logs.assertContains("Failed to flush items"); + } +} diff --git a/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json b/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json new file mode 100644 index 00000000000..887fdb13d4b --- /dev/null +++ b/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json @@ -0,0 +1,89 @@ +{ + "resourceLogs": [ + { + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "scopeLogs": [ + { + "scope": { + "name": "instrumentation", + "version": "1", + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "logRecords": [ + { + "timeUnixNano": "100", + "observedTimeUnixNano": "200", + "severityNumber": 9, + "severityText": "INFO", + "body": { + "stringValue": "body1" + }, + "attributes": [ + { + "key": "animal", + "value": { + "stringValue": "cat" + } + }, + { + "key": "lives", + "value": { + "intValue": "9" + } + } + ], + "droppedAttributesCount": -2, + "traceId": "12345678876543211234567887654322", + "spanId": "8765432112345876" + } + ] + }, + { + "scope": { + "name": "instrumentation2", + "version": "2", + "attributes": [] + }, + "logRecords": [ + { + "timeUnixNano": "100", + "observedTimeUnixNano": "200", + "severityNumber": 9, + "severityText": "INFO", + "body": { + "stringValue": "body2" + }, + "attributes": [ + { + "key": "important", + "value": { + "boolValue": true + } + } + ], + "droppedAttributesCount": -1, + "traceId": "12345678876543211234567887654322", + "spanId": "8765432112345875" + } + ] + } + ] + } + ] +} diff --git a/exporters/logging-otlp/src/test/resources/expected-logs.json b/exporters/logging-otlp/src/test/resources/expected-logs.json new file mode 100644 index 00000000000..fe84d67c4ef --- /dev/null +++ b/exporters/logging-otlp/src/test/resources/expected-logs.json @@ -0,0 +1,86 @@ +{ + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "scopeLogs": [ + { + "scope": { + "name": "instrumentation", + "version": "1", + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "value" + } + } + ] + }, + "logRecords": [ + { + "timeUnixNano": "100", + "observedTimeUnixNano": "200", + "severityNumber": 9, + "severityText": "INFO", + "body": { + "stringValue": "body1" + }, + "attributes": [ + { + "key": "animal", + "value": { + "stringValue": "cat" + } + }, + { + "key": "lives", + "value": { + "intValue": "9" + } + } + ], + "droppedAttributesCount": -2, + "traceId": "12345678876543211234567887654322", + "spanId": "8765432112345876" + } + ] + }, + { + "scope": { + "name": "instrumentation2", + "version": "2", + "attributes": [] + }, + "logRecords": [ + { + "timeUnixNano": "100", + "observedTimeUnixNano": "200", + "severityNumber": 9, + "severityText": "INFO", + "body": { + "stringValue": "body2" + }, + "attributes": [ + { + "key": "important", + "value": { + "boolValue": true + } + } + ], + "droppedAttributesCount": -1, + "traceId": "12345678876543211234567887654322", + "spanId": "8765432112345875" + } + ] + } + ] +} + diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfiguration.java index eacdf9d7c9a..ff70fa4d114 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfiguration.java @@ -30,6 +30,8 @@ final class LogRecordExporterConfiguration { EXPORTER_ARTIFACT_ID_BY_NAME.put("console", "opentelemetry-exporter-logging"); EXPORTER_ARTIFACT_ID_BY_NAME.put("logging", "opentelemetry-exporter-logging"); EXPORTER_ARTIFACT_ID_BY_NAME.put("logging-otlp", "opentelemetry-exporter-logging-otlp"); + EXPORTER_ARTIFACT_ID_BY_NAME.put( + "experimental-otlp/stdout", "opentelemetry-exporter-logging-otlp"); EXPORTER_ARTIFACT_ID_BY_NAME.put("otlp", "opentelemetry-exporter-otlp"); } diff --git a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java index 637cb5e80fe..b8577cebf41 100644 --- a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java @@ -46,6 +46,11 @@ void configureExporter_KnownSpiExportersNotOnClasspath() { .hasMessage( "otel.logs.exporter set to \"logging-otlp\" but opentelemetry-exporter-logging-otlp" + " not found on classpath. Make sure to add it as a dependency."); + assertThatThrownBy(() -> configureExporter("experimental-otlp/stdout", spiExportersManager)) + .isInstanceOf(ConfigurationException.class) + .hasMessage( + "otel.logs.exporter set to \"experimental-otlp/stdout\" but opentelemetry-exporter-logging-otlp" + + " not found on classpath. Make sure to add it as a dependency."); assertThatThrownBy(() -> configureExporter("otlp", spiExportersManager)) .isInstanceOf(ConfigurationException.class) .hasMessage( diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java index 1d670b0e04e..9043a70cdc6 100644 --- a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/LogRecordExporterConfigurationTest.java @@ -11,6 +11,7 @@ import com.google.common.collect.ImmutableMap; import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingLogRecordExporter; +import io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; @@ -38,6 +39,10 @@ void configureExporter_KnownSpiExportersOnClasspath() { assertThat( LogRecordExporterConfiguration.configureExporter("logging-otlp", spiExportersManager)) .isInstanceOf(OtlpJsonLoggingLogRecordExporter.class); + assertThat( + LogRecordExporterConfiguration.configureExporter( + "experimental-otlp/stdout", spiExportersManager)) + .isInstanceOf(OtlpStdoutLogRecordExporter.class); assertThat(LogRecordExporterConfiguration.configureExporter("otlp", spiExportersManager)) .isInstanceOf(OtlpGrpcLogRecordExporter.class); }