From 7e5978ba6357ec3360c65a328d2753faf79d78cf Mon Sep 17 00:00:00 2001 From: brunobat Date: Fri, 26 Jun 2026 10:40:14 +0100 Subject: [PATCH] Jackson 3 support --- dependencyManagement/build.gradle.kts | 1 + exporters/common/build.gradle.kts | 9 +- .../exporter/internal/JsonProviderUtil.java | 86 +++++++++++ .../internal/marshal/JsonSerializer.java | 108 +++++++------- .../exporter/internal/marshal/Marshaler.java | 48 ++++-- .../internal/marshal/MarshalerUtil.java | 10 +- exporters/logging-otlp/build.gradle.kts | 3 +- .../internal/writer/JsonProviderHolder.java | 38 +++++ .../otlp/internal/writer/JsonUtil.java | 30 ---- .../internal/writer/LoggerJsonWriter.java | 20 ++- .../internal/writer/StreamJsonWriter.java | 14 +- .../internal/writer/LoggerJsonWriterTest.java | 3 +- .../internal/writer/StreamJsonWriterTest.java | 3 +- exporters/otlp/common/build.gradle.kts | 3 + exporters/otlp/profiles/build.gradle.kts | 1 + json/jackson-2/build.gradle.kts | 18 +++ json/jackson-2/gradle.properties | 1 + .../internal/Jackson2JsonProvider.java | 71 +++++++++ .../jackson2/internal/Jackson2JsonWriter.java | 140 +++++++++++++++++ ...entelemetry.sdk.common.export.JsonProvider | 1 + .../internal/Jackson2JsonProviderTest.java | 108 ++++++++++++++ json/jackson-3/build.gradle.kts | 19 +++ json/jackson-3/gradle.properties | 1 + .../internal/Jackson3JsonProvider.java | 75 ++++++++++ .../jackson3/internal/Jackson3JsonWriter.java | 141 ++++++++++++++++++ ...entelemetry.sdk.common.export.JsonProvider | 1 + .../internal/Jackson3JsonProviderTest.java | 108 ++++++++++++++ .../sdk/common/export/JsonProvider.java | 36 +++++ .../sdk/common/export/JsonStringWriter.java | 23 +++ .../sdk/common/export/JsonWriter.java | 63 ++++++++ settings.gradle.kts | 2 + 31 files changed, 1049 insertions(+), 136 deletions(-) create mode 100644 exporters/common/src/main/java/io/opentelemetry/exporter/internal/JsonProviderUtil.java create mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonProviderHolder.java delete mode 100644 exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonUtil.java create mode 100644 json/jackson-2/build.gradle.kts create mode 100644 json/jackson-2/gradle.properties create mode 100644 json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProvider.java create mode 100644 json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonWriter.java create mode 100644 json/jackson-2/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider create mode 100644 json/jackson-2/src/test/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProviderTest.java create mode 100644 json/jackson-3/build.gradle.kts create mode 100644 json/jackson-3/gradle.properties create mode 100644 json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProvider.java create mode 100644 json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonWriter.java create mode 100644 json/jackson-3/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider create mode 100644 json/jackson-3/src/test/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProviderTest.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonProvider.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonStringWriter.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonWriter.java diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 4b205859618..e0749fe24cd 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -28,6 +28,7 @@ val DEPENDENCY_BOMS = listOf( // by FOSSA for containing EPL-licensed) "com.fasterxml.jackson:jackson-bom:2.22.0", + "tools.jackson:jackson-bom:3.2.0", "com.google.guava:guava-bom:33.6.0-jre", "com.google.protobuf:protobuf-bom:4.35.1", "com.squareup.okhttp3:okhttp-bom:$okhttpVersion", diff --git a/exporters/common/build.gradle.kts b/exporters/common/build.gradle.kts index c12a945698a..797a73b7461 100644 --- a/exporters/common/build.gradle.kts +++ b/exporters/common/build.gradle.kts @@ -7,13 +7,14 @@ plugins { description = "OpenTelemetry Exporter Common" otelJava.moduleName.set("io.opentelemetry.exporter.internal") -otelJava.osgiOptionalPackages.set(listOf("com.fasterxml.jackson.core", "com.google.common.io", "io.opentelemetry.api.incubator.config", "io.opentelemetry.sdk.autoconfigure.spi")) +otelJava.osgiOptionalPackages.set(listOf("com.google.common.io", "io.opentelemetry.api.incubator.config", "io.opentelemetry.sdk.autoconfigure.spi")) // sun.misc, io.grpc, and org.jspecify are not OSGi bundles and have no package versioning; must use unversioned optional. otelJava.osgiUnversionedOptionalPackages.set(listOf("sun.misc", "io.grpc", "org.jspecify.annotations")) // This bundle's exporters load sender implementations via SPI. otelJava.osgiServiceLoaderRequires.set(listOf( "io.opentelemetry.sdk.common.export.GrpcSenderProvider", - "io.opentelemetry.sdk.common.export.HttpSenderProvider" + "io.opentelemetry.sdk.common.export.HttpSenderProvider", + "io.opentelemetry.sdk.common.export.JsonProvider" )) java { @@ -69,13 +70,11 @@ dependencies { annotationProcessor("com.google.auto.value:auto-value") - // We include helpers shared by gRPC exporters but do not want to impose these - // dependency on all of our consumers. - compileOnly("com.fasterxml.jackson.core:jackson-core") // sun.misc.Unsafe from the JDK isn't found by the compiler, we provide our own trimmed down // version that we can compile against. compileOnly("io.grpc:grpc-stub") + testImplementation(project(":json:jackson-2")) testImplementation(project(":sdk:common")) testImplementation(project(":sdk:testing")) diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/JsonProviderUtil.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/JsonProviderUtil.java new file mode 100644 index 00000000000..3e9046e797a --- /dev/null +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/JsonProviderUtil.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal; + +import io.opentelemetry.api.internal.ConfigUtil; +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.sdk.common.export.JsonProvider; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utilities for loading {@link JsonProvider} implementations. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class JsonProviderUtil { + + private static final Logger LOGGER = Logger.getLogger(JsonProviderUtil.class.getName()); + + private static final String JSON_SPI_PROPERTY = "io.opentelemetry.sdk.common.export.JsonProvider"; + + private JsonProviderUtil() {} + + /** + * Resolve the {@link JsonProvider}. + * + *

If no {@link JsonProvider} is available, throw {@link IllegalStateException}. + * + *

If only one {@link JsonProvider} is available, use it. + * + *

If multiple are available and.. + * + *

+ */ + public static JsonProvider resolveJsonProvider(ComponentLoader componentLoader) { + Map jsonProviders = new HashMap<>(); + for (JsonProvider spi : componentLoader.load(JsonProvider.class)) { + jsonProviders.put(spi.getClass().getName(), spi); + } + + // No provider on classpath, throw + if (jsonProviders.isEmpty()) { + throw new IllegalStateException( + "No JsonProvider found on classpath. Please add dependency on " + + "opentelemetry-json-jackson-2 or opentelemetry-json-jackson-3"); + } + + // Exactly one provider on classpath, use it + if (jsonProviders.size() == 1) { + return jsonProviders.values().stream().findFirst().get(); + } + + // If we've reached here, there are multiple JsonProviders + String configuredProvider = ConfigUtil.getString(JSON_SPI_PROPERTY, ""); + + // Multiple providers but none configured, use first we find and log a warning + if (configuredProvider.isEmpty()) { + LOGGER.log( + Level.WARNING, + "Multiple JsonProvider found. Please include only one, " + + "or specify preference setting " + + JSON_SPI_PROPERTY + + " to the FQCN of the preferred provider."); + return jsonProviders.values().stream().findFirst().get(); + } + + // Multiple providers with configuration match, use configuration match + if (jsonProviders.containsKey(configuredProvider)) { + return jsonProviders.get(configuredProvider); + } + + // Multiple providers, configured does not match, throw + throw new IllegalStateException( + "No JsonProvider matched configured " + JSON_SPI_PROPERTY + ": " + configuredProvider); + } +} diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java index 62b391c8905..cf3aef006d8 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java @@ -5,126 +5,118 @@ package io.opentelemetry.exporter.internal.marshal; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; +import io.opentelemetry.sdk.common.export.JsonWriter; import java.io.IOException; -import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; final class JsonSerializer extends Serializer { - private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private final JsonWriter writer; - private final JsonGenerator generator; - - JsonSerializer(OutputStream output) throws IOException { - this(JSON_FACTORY.createGenerator(output)); - } - - JsonSerializer(JsonGenerator generator) { - this.generator = generator; + JsonSerializer(JsonWriter writer) { + this.writer = writer; } @Override protected void writeTraceId(ProtoFieldInfo field, String traceId) throws IOException { - generator.writeStringField(field.getJsonName(), traceId); + writer.writeStringField(field.getJsonName(), traceId); } @Override protected void writeSpanId(ProtoFieldInfo field, String spanId) throws IOException { - generator.writeStringField(field.getJsonName(), spanId); + writer.writeStringField(field.getJsonName(), spanId); } @Override public void writeBool(ProtoFieldInfo field, boolean value) throws IOException { - generator.writeBooleanField(field.getJsonName(), value); + writer.writeBooleanField(field.getJsonName(), value); } @Override protected void writeEnum(ProtoFieldInfo field, ProtoEnumInfo enumValue) throws IOException { - generator.writeNumberField(field.getJsonName(), enumValue.getEnumNumber()); + writer.writeNumberField(field.getJsonName(), enumValue.getEnumNumber()); } @Override protected void writeUint32(ProtoFieldInfo field, int value) throws IOException { - generator.writeNumberField(field.getJsonName(), value); + writer.writeNumberField(field.getJsonName(), value); } @Override protected void writeSInt32(ProtoFieldInfo field, int value) throws IOException { - generator.writeNumberField(field.getJsonName(), value); + writer.writeNumberField(field.getJsonName(), value); } @Override protected void writeint32(ProtoFieldInfo field, int value) throws IOException { - generator.writeNumberField(field.getJsonName(), value); + writer.writeNumberField(field.getJsonName(), value); } @Override public void writeInt64(ProtoFieldInfo field, long value) throws IOException { - generator.writeStringField(field.getJsonName(), Long.toString(value)); + writer.writeStringField(field.getJsonName(), Long.toString(value)); } @Override protected void writeFixed64(ProtoFieldInfo field, long value) throws IOException { - generator.writeStringField(field.getJsonName(), Long.toString(value)); + writer.writeStringField(field.getJsonName(), Long.toString(value)); } @Override protected void writeFixed64Value(long value) throws IOException { - generator.writeString(Long.toString(value)); + writer.writeString(Long.toString(value)); } @Override protected void writeUInt64Value(long value) throws IOException { - generator.writeString(Long.toString(value)); + writer.writeString(Long.toString(value)); } @Override public void writeUInt64(ProtoFieldInfo field, long value) throws IOException { - generator.writeStringField(field.getJsonName(), Long.toString(value)); + writer.writeStringField(field.getJsonName(), Long.toString(value)); } @Override protected void writeFixed32(ProtoFieldInfo field, int value) throws IOException { - generator.writeNumberField(field.getJsonName(), value); + writer.writeNumberField(field.getJsonName(), value); } @Override public void writeDouble(ProtoFieldInfo field, double value) throws IOException { - generator.writeNumberField(field.getJsonName(), value); + writer.writeNumberField(field.getJsonName(), value); } @Override protected void writeDoubleValue(double value) throws IOException { - generator.writeNumber(value); + writer.writeNumber(value); } @Override public void writeString(ProtoFieldInfo field, byte[] utf8Bytes) throws IOException { - generator.writeFieldName(field.getJsonName()); + writer.writeFieldName(field.getJsonName()); // Marshalers encoded String into UTF-8 bytes to optimize for binary serialization where // we are able to avoid the encoding process happening twice, one for size computation and one // for actual writing. JsonGenerator actually has a writeUTF8String that would be able to accept // this, but it only works when writing to an OutputStream, but not to a String like we do for // writing to logs. It's wasteful to take a String, convert it to bytes, and convert back to // the same String but we can see if this can be improved in the future. - generator.writeString(new String(utf8Bytes, StandardCharsets.UTF_8)); + writer.writeString(new String(utf8Bytes, StandardCharsets.UTF_8)); } @Override public void writeString( ProtoFieldInfo field, String string, int utf8Length, MarshalerContext context) throws IOException { - generator.writeFieldName(field.getJsonName()); - generator.writeString(string); + writer.writeFieldName(field.getJsonName()); + writer.writeString(string); } @Override public void writeRepeatedString(ProtoFieldInfo field, byte[][] utf8Bytes) throws IOException { - generator.writeArrayFieldStart(field.getJsonName()); + writer.writeArrayFieldStart(field.getJsonName()); for (byte[] value : utf8Bytes) { // Marshalers encoded String into UTF-8 bytes to optimize for binary serialization where // we are able to avoid the encoding process happening twice, one for size computation and one @@ -133,73 +125,73 @@ public void writeRepeatedString(ProtoFieldInfo field, byte[][] utf8Bytes) throws // this, but it only works when writing to an OutputStream, but not to a String like we do for // writing to logs. It's wasteful to take a String, convert it to bytes, and convert back to // the same String but we can see if this can be improved in the future. - generator.writeString(new String(value, StandardCharsets.UTF_8)); + writer.writeString(new String(value, StandardCharsets.UTF_8)); } - generator.writeEndArray(); + writer.writeEndArray(); } @Override public void writeBytes(ProtoFieldInfo field, byte[] value) throws IOException { - generator.writeBinaryField(field.getJsonName(), value); + writer.writeBinaryField(field.getJsonName(), value); } @Override public void writeByteBuffer(ProtoFieldInfo field, ByteBuffer value) throws IOException { byte[] data = new byte[value.capacity()]; ((ByteBuffer) value.duplicate().clear()).get(data); - generator.writeBinaryField(field.getJsonName(), data); + writer.writeBinaryField(field.getJsonName(), data); } @Override protected void writeStartMessage(ProtoFieldInfo field, int protoMessageSize) throws IOException { - generator.writeObjectFieldStart(field.getJsonName()); + writer.writeObjectFieldStart(field.getJsonName()); } @Override protected void writeEndMessage() throws IOException { - generator.writeEndObject(); + writer.writeEndObject(); } @Override protected void writeStartRepeatedPrimitive( ProtoFieldInfo field, int protoSizePerElement, int numElements) throws IOException { - generator.writeArrayFieldStart(field.getJsonName()); + writer.writeArrayFieldStart(field.getJsonName()); } @Override protected void writeEndRepeatedPrimitive() throws IOException { - generator.writeEndArray(); + writer.writeEndArray(); } @Override protected void writeStartRepeatedVarint(ProtoFieldInfo field, int payloadSize) throws IOException { - generator.writeArrayFieldStart(field.getJsonName()); + writer.writeArrayFieldStart(field.getJsonName()); } @Override protected void writeEndRepeatedVarint() throws IOException { - generator.writeEndArray(); + writer.writeEndArray(); } @Override public void serializeRepeatedMessage(ProtoFieldInfo field, Marshaler[] repeatedMessage) throws IOException { - generator.writeArrayFieldStart(field.getJsonName()); + writer.writeArrayFieldStart(field.getJsonName()); for (Marshaler marshaler : repeatedMessage) { writeMessageValue(marshaler); } - generator.writeEndArray(); + writer.writeEndArray(); } @Override public void serializeRepeatedMessage( ProtoFieldInfo field, List repeatedMessage) throws IOException { - generator.writeArrayFieldStart(field.getJsonName()); + writer.writeArrayFieldStart(field.getJsonName()); for (Marshaler marshaler : repeatedMessage) { writeMessageValue(marshaler); } - generator.writeEndArray(); + writer.writeEndArray(); } @Override @@ -209,52 +201,52 @@ public void serializeRepeatedMessageWithContext( StatelessMarshaler marshaler, MarshalerContext context) throws IOException { - generator.writeArrayFieldStart(field.getJsonName()); + writer.writeArrayFieldStart(field.getJsonName()); for (int i = 0; i < messages.size(); i++) { T message = messages.get(i); - generator.writeStartObject(); + writer.writeStartObject(); marshaler.writeTo(this, message, context); - generator.writeEndObject(); + writer.writeEndObject(); } - generator.writeEndArray(); + writer.writeEndArray(); } @Override public void writeStartRepeated(ProtoFieldInfo field) throws IOException { - generator.writeArrayFieldStart(field.getJsonName()); + writer.writeArrayFieldStart(field.getJsonName()); } @Override public void writeEndRepeated() throws IOException { - generator.writeEndArray(); + writer.writeEndArray(); } @Override public void writeStartRepeatedElement(ProtoFieldInfo field, int protoMessageSize) throws IOException { - generator.writeStartObject(); + writer.writeStartObject(); } @Override public void writeEndRepeatedElement() throws IOException { - generator.writeEndObject(); + writer.writeEndObject(); } // Not a field. void writeMessageValue(Marshaler message) throws IOException { - generator.writeStartObject(); + writer.writeStartObject(); message.writeTo(this); - generator.writeEndObject(); + writer.writeEndObject(); } @Override public void writeSerializedMessage(byte[] protoSerialized, String jsonSerialized) throws IOException { - generator.writeRaw(jsonSerialized); + writer.writeRaw(jsonSerialized); } @Override public void close() throws IOException { - generator.close(); + writer.close(); } } diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Marshaler.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Marshaler.java index d8ea5a9e5f4..f4722c6ac3f 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Marshaler.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Marshaler.java @@ -5,10 +5,14 @@ package io.opentelemetry.exporter.internal.marshal; -import com.fasterxml.jackson.core.JsonGenerator; +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.exporter.internal.JsonProviderUtil; +import io.opentelemetry.sdk.common.export.JsonProvider; +import io.opentelemetry.sdk.common.export.JsonWriter; import io.opentelemetry.sdk.common.export.MessageWriter; import java.io.IOException; import java.io.OutputStream; +import javax.annotation.Nullable; /** * Marshaler from an SDK structure to protobuf wire format. @@ -18,6 +22,9 @@ */ public abstract class Marshaler { + private static final Object lock = new Object(); + @Nullable private static volatile JsonProvider jsonProvider; + /** Marshals into the {@link OutputStream} in proto binary format. */ public final void writeBinaryTo(OutputStream output) throws IOException { try (Serializer serializer = new ProtoSerializer(output)) { @@ -27,27 +34,23 @@ public final void writeBinaryTo(OutputStream output) throws IOException { /** Marshals into the {@link OutputStream} in proto JSON format. */ public final void writeJsonTo(OutputStream output) throws IOException { - try (JsonSerializer serializer = new JsonSerializer(output)) { + try (JsonSerializer serializer = + new JsonSerializer(resolveJsonProvider().createJsonWriter(output))) { serializer.writeMessageValue(this); } } - /** Marshals into the {@link JsonGenerator} in proto JSON format. */ - // Intentionally not overloading writeJsonTo(OutputStream) in order to avoid compilation - // dependency on jackson when using writeJsonTo(OutputStream). See: - // https://github.com/open-telemetry/opentelemetry-java-contrib/pull/1551#discussion_r1849064365 - public final void writeJsonToGenerator(JsonGenerator output) throws IOException { - try (JsonSerializer serializer = new JsonSerializer(output)) { - serializer.writeMessageValue(this); - } + /** Marshals into the {@link JsonWriter} in proto JSON format. */ + public final void writeJsonToWriter(JsonWriter output) throws IOException { + JsonSerializer serializer = new JsonSerializer(output); + serializer.writeMessageValue(this); } - /** Marshals into the {@link JsonGenerator} in proto JSON format and adds a newline. */ - public final void writeJsonWithNewline(JsonGenerator output) throws IOException { - try (JsonSerializer serializer = new JsonSerializer(output)) { - serializer.writeMessageValue(this); - output.writeRaw('\n'); - } + /** Marshals into the {@link JsonWriter} in proto JSON format and adds a newline. */ + public final void writeJsonWithNewline(JsonWriter output) throws IOException { + JsonSerializer serializer = new JsonSerializer(output); + serializer.writeMessageValue(this); + output.writeRaw('\n'); } /** Returns the number of bytes this Marshaler will write in proto binary format. */ @@ -82,4 +85,17 @@ public int getContentLength() { } }; } + + private static JsonProvider resolveJsonProvider() { + if (jsonProvider == null) { + synchronized (lock) { + if (jsonProvider == null) { + jsonProvider = + JsonProviderUtil.resolveJsonProvider( + ComponentLoader.forClassLoader(Marshaler.class.getClassLoader())); + } + } + } + return jsonProvider; + } } diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/MarshalerUtil.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/MarshalerUtil.java index 494853723dc..ec73a2aa16f 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/MarshalerUtil.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/MarshalerUtil.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.trace.SpanId; import io.opentelemetry.api.trace.TraceId; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.common.export.JsonProvider; import io.opentelemetry.sdk.common.internal.DynamicPrimitiveLongList; import io.opentelemetry.sdk.resources.Resource; import java.io.ByteArrayOutputStream; @@ -20,6 +21,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.ServiceLoader; import java.util.function.Function; import javax.annotation.Nullable; @@ -40,9 +42,11 @@ public final class MarshalerUtil { static { boolean jsonAvailable = false; try { - Class.forName("com.fasterxml.jackson.core.JsonFactory"); - jsonAvailable = true; - } catch (ClassNotFoundException e) { + jsonAvailable = + ServiceLoader.load(JsonProvider.class, MarshalerUtil.class.getClassLoader()) + .iterator() + .hasNext(); + } catch (RuntimeException e) { // Not available } JSON_AVAILABLE = jsonAvailable; diff --git a/exporters/logging-otlp/build.gradle.kts b/exporters/logging-otlp/build.gradle.kts index bdec906e21c..a2a22b4119f 100644 --- a/exporters/logging-otlp/build.gradle.kts +++ b/exporters/logging-otlp/build.gradle.kts @@ -24,9 +24,8 @@ dependencies { compileOnly(project(":api:incubator")) compileOnly(project(":sdk-extensions:autoconfigure-spi")) - implementation("com.fasterxml.jackson.core:jackson-core") - testImplementation(project(":api:incubator")) + testImplementation(project(":json:jackson-2")) testImplementation(project(":sdk:testing")) testImplementation(project(":sdk-extensions:autoconfigure-spi")) diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonProviderHolder.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonProviderHolder.java new file mode 100644 index 00000000000..8b8a8b73df0 --- /dev/null +++ b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonProviderHolder.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp.internal.writer; + +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.exporter.internal.JsonProviderUtil; +import io.opentelemetry.sdk.common.export.JsonProvider; +import javax.annotation.Nullable; + +/** + * Lazy holder for the resolved {@link JsonProvider}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +final class JsonProviderHolder { + + private static final Object lock = new Object(); + @Nullable private static volatile JsonProvider jsonProvider; + + static JsonProvider get() { + if (jsonProvider == null) { + synchronized (lock) { + if (jsonProvider == null) { + jsonProvider = + JsonProviderUtil.resolveJsonProvider( + ComponentLoader.forClassLoader(JsonProviderHolder.class.getClassLoader())); + } + } + } + return jsonProvider; + } + + private JsonProviderHolder() {} +} diff --git a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonUtil.java b/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonUtil.java deleted file mode 100644 index 0b74ed8a478..00000000000 --- a/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/internal/writer/JsonUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 com.fasterxml.jackson.core.io.SegmentedStringWriter; -import java.io.IOException; - -/** - * 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 { - - public static final JsonFactory JSON_FACTORY = new JsonFactory(); - - public static JsonGenerator create(SegmentedStringWriter stringWriter) { - try { - return JSON_FACTORY.createGenerator(stringWriter); - } catch (IOException e) { - throw new IllegalStateException("Unable to create in-memory JsonGenerator, can't happen.", e); - } - } - - private JsonUtil() {} -} 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 index a95c5e0d2c1..14222a0d406 100644 --- 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 @@ -5,12 +5,9 @@ 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 io.opentelemetry.sdk.common.export.JsonStringWriter; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -31,16 +28,23 @@ public LoggerJsonWriter(Logger logger, String type) { @Override public CompletableResultCode write(Marshaler exportRequest) { - SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); - try (JsonGenerator gen = JsonUtil.create(sw)) { - exportRequest.writeJsonToGenerator(gen); + JsonStringWriter stringWriter; + try { + stringWriter = JsonProviderHolder.get().createJsonStringWriter(); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to create JSON writer for " + type, e); + return CompletableResultCode.ofFailure(); + } + + try (io.opentelemetry.sdk.common.export.JsonWriter writer = stringWriter.writer()) { + exportRequest.writeJsonToWriter(writer); } catch (IOException e) { logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); return CompletableResultCode.ofFailure(); } try { - logger.log(Level.INFO, sw.getAndClear()); + logger.log(Level.INFO, stringWriter.getAndClear()); return CompletableResultCode.ofSuccess(); } catch (IOException e) { logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); 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 index a3e4e262a7e..721342eeb1a 100644 --- 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 @@ -5,8 +5,6 @@ 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.common.internal.ThrottlingLogger; @@ -21,8 +19,6 @@ */ 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); @@ -37,11 +33,9 @@ public StreamJsonWriter(OutputStream originalStream, String type) { @Override public CompletableResultCode write(Marshaler exportRequest) { - try { - exportRequest.writeJsonWithNewline( - JSON_FACTORY - .createGenerator(outputStream) - .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET)); + try (io.opentelemetry.sdk.common.export.JsonWriter writer = + JsonProviderHolder.get().createJsonWriter(outputStream, false)) { + exportRequest.writeJsonWithNewline(writer); return CompletableResultCode.ofSuccess(); } catch (IOException e) { logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); @@ -63,8 +57,8 @@ public CompletableResultCode flush() { @SuppressWarnings({"SystemOut", "ReferenceEquality"}) @Override public CompletableResultCode close() { + // closing System.out or System.err is not allowed - it breaks the output stream 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 { 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 index 680c7cc444d..daed256be4d 100644 --- 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 @@ -9,7 +9,6 @@ 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 io.opentelemetry.internal.testing.slf4j.SuppressLogger; @@ -36,7 +35,7 @@ void error() throws IOException { Marshaler marshaler = mock(Marshaler.class); Mockito.doThrow(new IOException("test")) .when(marshaler) - .writeJsonToGenerator(any(JsonGenerator.class)); + .writeJsonToWriter(any(io.opentelemetry.sdk.common.export.JsonWriter.class)); Logger logger = Logger.getLogger(LoggerJsonWriter.class.getName()); 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 index 048fcab1c17..2c7711413ff 100644 --- 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 @@ -9,7 +9,6 @@ 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 io.opentelemetry.internal.testing.slf4j.SuppressLogger; @@ -50,7 +49,7 @@ void errorWriting() throws IOException { Marshaler marshaler = mock(Marshaler.class); Mockito.doThrow(new IOException("test")) .when(marshaler) - .writeJsonWithNewline(any(JsonGenerator.class)); + .writeJsonWithNewline(any(io.opentelemetry.sdk.common.export.JsonWriter.class)); StreamJsonWriter writer = new StreamJsonWriter(System.out, "type"); writer.write(marshaler); diff --git a/exporters/otlp/common/build.gradle.kts b/exporters/otlp/common/build.gradle.kts index c1d9bf4815a..2aaead87413 100644 --- a/exporters/otlp/common/build.gradle.kts +++ b/exporters/otlp/common/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { compileOnly(project(":sdk:logs")) compileOnly(project(":api:incubator")) + testImplementation(project(":json:jackson-2")) testImplementation(project(":sdk:metrics")) testImplementation(project(":sdk:trace")) testImplementation(project(":sdk:logs")) @@ -36,6 +37,7 @@ dependencies { testImplementation("io.opentelemetry.proto:opentelemetry-proto") jmhImplementation(project(":api:incubator")) + jmhImplementation(project(":json:jackson-2")) jmhImplementation(project(":sdk:testing")) jmhImplementation("com.fasterxml.jackson.core:jackson-core") jmhImplementation("io.opentelemetry.proto:opentelemetry-proto") @@ -47,6 +49,7 @@ testing { register("testIncubating") { dependencies { implementation(project(":api:incubator")) + implementation(project(":json:jackson-2")) implementation(project(":sdk:testing")) implementation("com.fasterxml.jackson.core:jackson-databind") diff --git a/exporters/otlp/profiles/build.gradle.kts b/exporters/otlp/profiles/build.gradle.kts index c9a5f8937dc..439414a9753 100644 --- a/exporters/otlp/profiles/build.gradle.kts +++ b/exporters/otlp/profiles/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { compileOnly("io.grpc:grpc-stub") testCompileOnly("com.google.guava:guava") + testImplementation(project(":json:jackson-2")) testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.google.protobuf:protobuf-java-util") testImplementation("io.opentelemetry.proto:opentelemetry-proto") diff --git a/json/jackson-2/build.gradle.kts b/json/jackson-2/build.gradle.kts new file mode 100644 index 00000000000..8ed1832cd69 --- /dev/null +++ b/json/jackson-2/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") + + id("otel.animalsniffer-conventions") +} + +description = "OpenTelemetry JSON Jackson 2 Provider" +otelJava.moduleName.set("io.opentelemetry.json.jackson2") +otelJava.osgiServiceLoaderProvides.set(listOf("io.opentelemetry.sdk.common.export.JsonProvider")) + +base.archivesName.set("opentelemetry-json-jackson-2") + +dependencies { + api(project(":sdk:common")) + + implementation("com.fasterxml.jackson.core:jackson-core") +} diff --git a/json/jackson-2/gradle.properties b/json/jackson-2/gradle.properties new file mode 100644 index 00000000000..4476ae57e31 --- /dev/null +++ b/json/jackson-2/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProvider.java b/json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProvider.java new file mode 100644 index 00000000000..ce64dd1794f --- /dev/null +++ b/json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.json.jackson2.internal; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.SegmentedStringWriter; +import io.opentelemetry.sdk.common.export.JsonProvider; +import io.opentelemetry.sdk.common.export.JsonStringWriter; +import io.opentelemetry.sdk.common.export.JsonWriter; +import java.io.IOException; +import java.io.OutputStream; + +/** + * {@link JsonProvider} implementation backed by Jackson 2. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class Jackson2JsonProvider implements JsonProvider { + + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + + @Override + public JsonWriter createJsonWriter(OutputStream output) throws IOException { + return new Jackson2JsonWriter(JSON_FACTORY.createGenerator(output)); + } + + @Override + public JsonWriter createJsonWriter(OutputStream output, boolean autoCloseTarget) + throws IOException { + JsonGenerator generator = JSON_FACTORY.createGenerator(output); + if (!autoCloseTarget) { + generator.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + } + return new Jackson2JsonWriter(generator); + } + + @Override + public JsonStringWriter createJsonStringWriter() throws IOException { + @SuppressWarnings("deprecation") + SegmentedStringWriter stringWriter = + new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); + JsonWriter writer = new Jackson2JsonWriter(JSON_FACTORY.createGenerator(stringWriter)); + return new Jackson2StringWriter(writer, stringWriter); + } + + private static final class Jackson2StringWriter implements JsonStringWriter { + + private final JsonWriter writer; + private final SegmentedStringWriter stringWriter; + + Jackson2StringWriter(JsonWriter writer, SegmentedStringWriter stringWriter) { + this.writer = writer; + this.stringWriter = stringWriter; + } + + @Override + public JsonWriter writer() { + return writer; + } + + @Override + public String getAndClear() throws IOException { + return stringWriter.getAndClear(); + } + } +} diff --git a/json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonWriter.java b/json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonWriter.java new file mode 100644 index 00000000000..ee4c1caa0a5 --- /dev/null +++ b/json/jackson-2/src/main/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonWriter.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.json.jackson2.internal; + +import com.fasterxml.jackson.core.JsonGenerator; +import io.opentelemetry.sdk.common.export.JsonWriter; +import java.io.IOException; + +/** + * {@link JsonWriter} implementation backed by Jackson 2's {@link JsonGenerator}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +final class Jackson2JsonWriter implements JsonWriter { + + private final JsonGenerator generator; + + Jackson2JsonWriter(JsonGenerator generator) { + this.generator = generator; + } + + @Override + public void writeStartObject() throws IOException { + generator.writeStartObject(); + } + + @Override + public void writeEndObject() throws IOException { + generator.writeEndObject(); + } + + @Override + public void writeStartArray() throws IOException { + generator.writeStartArray(); + } + + @Override + public void writeEndArray() throws IOException { + generator.writeEndArray(); + } + + @Override + public void writeFieldName(String name) throws IOException { + generator.writeFieldName(name); + } + + @Override + public void writeString(String value) throws IOException { + generator.writeString(value); + } + + @Override + public void writeStringField(String fieldName, String value) throws IOException { + generator.writeStringField(fieldName, value); + } + + @Override + public void writeNumber(int value) throws IOException { + generator.writeNumber(value); + } + + @Override + public void writeNumber(long value) throws IOException { + generator.writeNumber(value); + } + + @Override + public void writeNumber(double value) throws IOException { + generator.writeNumber(value); + } + + @Override + public void writeNumberField(String fieldName, int value) throws IOException { + generator.writeNumberField(fieldName, value); + } + + @Override + public void writeNumberField(String fieldName, long value) throws IOException { + generator.writeNumberField(fieldName, value); + } + + @Override + public void writeNumberField(String fieldName, double value) throws IOException { + generator.writeNumberField(fieldName, value); + } + + @Override + public void writeBoolean(boolean value) throws IOException { + generator.writeBoolean(value); + } + + @Override + public void writeBooleanField(String fieldName, boolean value) throws IOException { + generator.writeBooleanField(fieldName, value); + } + + @Override + public void writeBinary(byte[] data) throws IOException { + generator.writeBinary(data); + } + + @Override + public void writeBinaryField(String fieldName, byte[] data) throws IOException { + generator.writeBinaryField(fieldName, data); + } + + @Override + public void writeObjectFieldStart(String fieldName) throws IOException { + generator.writeObjectFieldStart(fieldName); + } + + @Override + public void writeArrayFieldStart(String fieldName) throws IOException { + generator.writeArrayFieldStart(fieldName); + } + + @Override + public void writeRaw(String text) throws IOException { + generator.writeRaw(text); + } + + @Override + public void writeRaw(char c) throws IOException { + generator.writeRaw(c); + } + + @Override + public void flush() throws IOException { + generator.flush(); + } + + @Override + public void close() throws IOException { + generator.close(); + } +} diff --git a/json/jackson-2/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider b/json/jackson-2/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider new file mode 100644 index 00000000000..b69ad052dde --- /dev/null +++ b/json/jackson-2/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider @@ -0,0 +1 @@ +io.opentelemetry.json.jackson2.internal.Jackson2JsonProvider diff --git a/json/jackson-2/src/test/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProviderTest.java b/json/jackson-2/src/test/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProviderTest.java new file mode 100644 index 00000000000..e324dcb541d --- /dev/null +++ b/json/jackson-2/src/test/java/io/opentelemetry/json/jackson2/internal/Jackson2JsonProviderTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.json.jackson2.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.common.export.JsonProvider; +import io.opentelemetry.sdk.common.export.JsonStringWriter; +import io.opentelemetry.sdk.common.export.JsonWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; + +class Jackson2JsonProviderTest { + + private final Jackson2JsonProvider provider = new Jackson2JsonProvider(); + + @Test + void serviceLoaderLoads() { + assertThat(ServiceLoader.load(JsonProvider.class)) + .anyMatch(p -> p instanceof Jackson2JsonProvider); + } + + @Test + void writeToOutputStream() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonWriter writer = provider.createJsonWriter(baos)) { + writeTestObject(writer); + } + assertThat(new String(baos.toByteArray(), StandardCharsets.UTF_8)) + .isEqualTo("{\"name\":\"test\",\"value\":42,\"active\":true}"); + } + + @Test + void writeToString() throws IOException { + JsonStringWriter stringWriter = provider.createJsonStringWriter(); + try (JsonWriter writer = stringWriter.writer()) { + writeTestObject(writer); + } + assertThat(stringWriter.getAndClear()) + .isEqualTo("{\"name\":\"test\",\"value\":42,\"active\":true}"); + } + + @Test + void autoCloseTargetTrue() throws IOException { + TestOutputStream stream = new TestOutputStream(); + try (JsonWriter writer = provider.createJsonWriter(stream, true)) { + writer.writeStartObject(); + writer.writeEndObject(); + } + assertThat(stream.closed).isTrue(); + } + + @Test + void autoCloseTargetFalse() throws IOException { + TestOutputStream stream = new TestOutputStream(); + try (JsonWriter writer = provider.createJsonWriter(stream, false)) { + writer.writeStartObject(); + writer.writeEndObject(); + } + assertThat(stream.closed).isFalse(); + } + + @Test + void writeNestedStructure() throws IOException { + JsonStringWriter stringWriter = provider.createJsonStringWriter(); + try (JsonWriter writer = stringWriter.writer()) { + writer.writeStartObject(); + writer.writeArrayFieldStart("items"); + writer.writeStartObject(); + writer.writeNumberField("id", 1); + writer.writeEndObject(); + writer.writeEndArray(); + writer.writeObjectFieldStart("nested"); + writer.writeNumberField("pi", 3.14); + writer.writeEndObject(); + writer.writeBinaryField("data", new byte[] {1, 2, 3}); + writer.writeEndObject(); + } + String json = stringWriter.getAndClear(); + assertThat(json).contains("\"items\":[{\"id\":1}]"); + assertThat(json).contains("\"nested\":{\"pi\":3.14}"); + assertThat(json).contains("\"data\":"); + } + + private static void writeTestObject(JsonWriter writer) throws IOException { + writer.writeStartObject(); + writer.writeStringField("name", "test"); + writer.writeNumberField("value", 42); + writer.writeBooleanField("active", true); + writer.writeEndObject(); + } + + private static class TestOutputStream extends ByteArrayOutputStream { + boolean closed; + + @Override + public void close() throws IOException { + closed = true; + super.close(); + } + } +} diff --git a/json/jackson-3/build.gradle.kts b/json/jackson-3/build.gradle.kts new file mode 100644 index 00000000000..a5a9a76adc1 --- /dev/null +++ b/json/jackson-3/build.gradle.kts @@ -0,0 +1,19 @@ +import org.gradle.api.JavaVersion + +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry JSON Jackson 3 Provider" +otelJava.moduleName.set("io.opentelemetry.json.jackson3") +otelJava.minJavaVersionSupported.set(JavaVersion.VERSION_17) +otelJava.osgiServiceLoaderProvides.set(listOf("io.opentelemetry.sdk.common.export.JsonProvider")) + +base.archivesName.set("opentelemetry-json-jackson-3") + +dependencies { + api(project(":sdk:common")) + + implementation("tools.jackson.core:jackson-core") +} diff --git a/json/jackson-3/gradle.properties b/json/jackson-3/gradle.properties new file mode 100644 index 00000000000..4476ae57e31 --- /dev/null +++ b/json/jackson-3/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProvider.java b/json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProvider.java new file mode 100644 index 00000000000..0839db956ea --- /dev/null +++ b/json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.json.jackson3.internal; + +import io.opentelemetry.sdk.common.export.JsonProvider; +import io.opentelemetry.sdk.common.export.JsonStringWriter; +import io.opentelemetry.sdk.common.export.JsonWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.json.JsonFactory; + +/** + * {@link JsonProvider} implementation backed by Jackson 3. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("CheckedExceptionNotThrown") +public final class Jackson3JsonProvider implements JsonProvider { + + private static final JsonFactory JSON_FACTORY = JsonFactory.builder().build(); + + private static final JsonFactory JSON_FACTORY_NO_AUTO_CLOSE = + JsonFactory.builder().disable(StreamWriteFeature.AUTO_CLOSE_TARGET).build(); + + @Override + public JsonWriter createJsonWriter(OutputStream output) throws IOException { + return new Jackson3JsonWriter(JSON_FACTORY.createGenerator(ObjectWriteContext.empty(), output)); + } + + @Override + public JsonWriter createJsonWriter(OutputStream output, boolean autoCloseTarget) + throws IOException { + JsonFactory factory = autoCloseTarget ? JSON_FACTORY : JSON_FACTORY_NO_AUTO_CLOSE; + return new Jackson3JsonWriter(factory.createGenerator(ObjectWriteContext.empty(), output)); + } + + @Override + public JsonStringWriter createJsonStringWriter() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = + new Jackson3JsonWriter( + JSON_FACTORY.createGenerator(ObjectWriteContext.empty(), stringWriter)); + return new Jackson3StringWriter(writer, stringWriter); + } + + private static final class Jackson3StringWriter implements JsonStringWriter { + + private final JsonWriter writer; + private final StringWriter stringWriter; + + Jackson3StringWriter(JsonWriter writer, StringWriter stringWriter) { + this.writer = writer; + this.stringWriter = stringWriter; + } + + @Override + public JsonWriter writer() { + return writer; + } + + @Override + public String getAndClear() { + String result = stringWriter.toString(); + stringWriter.getBuffer().setLength(0); + return result; + } + } +} diff --git a/json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonWriter.java b/json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonWriter.java new file mode 100644 index 00000000000..fdd45da2417 --- /dev/null +++ b/json/jackson-3/src/main/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonWriter.java @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.json.jackson3.internal; + +import io.opentelemetry.sdk.common.export.JsonWriter; +import java.io.IOException; +import tools.jackson.core.JsonGenerator; + +/** + * {@link JsonWriter} implementation backed by Jackson 3's {@link JsonGenerator}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("CheckedExceptionNotThrown") +final class Jackson3JsonWriter implements JsonWriter { + + private final JsonGenerator generator; + + Jackson3JsonWriter(JsonGenerator generator) { + this.generator = generator; + } + + @Override + public void writeStartObject() throws IOException { + generator.writeStartObject(); + } + + @Override + public void writeEndObject() throws IOException { + generator.writeEndObject(); + } + + @Override + public void writeStartArray() throws IOException { + generator.writeStartArray(); + } + + @Override + public void writeEndArray() throws IOException { + generator.writeEndArray(); + } + + @Override + public void writeFieldName(String name) throws IOException { + generator.writeName(name); + } + + @Override + public void writeString(String value) throws IOException { + generator.writeString(value); + } + + @Override + public void writeStringField(String fieldName, String value) throws IOException { + generator.writeStringProperty(fieldName, value); + } + + @Override + public void writeNumber(int value) throws IOException { + generator.writeNumber(value); + } + + @Override + public void writeNumber(long value) throws IOException { + generator.writeNumber(value); + } + + @Override + public void writeNumber(double value) throws IOException { + generator.writeNumber(value); + } + + @Override + public void writeNumberField(String fieldName, int value) throws IOException { + generator.writeNumberProperty(fieldName, value); + } + + @Override + public void writeNumberField(String fieldName, long value) throws IOException { + generator.writeNumberProperty(fieldName, value); + } + + @Override + public void writeNumberField(String fieldName, double value) throws IOException { + generator.writeNumberProperty(fieldName, value); + } + + @Override + public void writeBoolean(boolean value) throws IOException { + generator.writeBoolean(value); + } + + @Override + public void writeBooleanField(String fieldName, boolean value) throws IOException { + generator.writeBooleanProperty(fieldName, value); + } + + @Override + public void writeBinary(byte[] data) throws IOException { + generator.writeBinary(data); + } + + @Override + public void writeBinaryField(String fieldName, byte[] data) throws IOException { + generator.writeBinaryProperty(fieldName, data); + } + + @Override + public void writeObjectFieldStart(String fieldName) throws IOException { + generator.writeObjectPropertyStart(fieldName); + } + + @Override + public void writeArrayFieldStart(String fieldName) throws IOException { + generator.writeArrayPropertyStart(fieldName); + } + + @Override + public void writeRaw(String text) throws IOException { + generator.writeRaw(text); + } + + @Override + public void writeRaw(char c) throws IOException { + generator.writeRaw(c); + } + + @Override + public void flush() throws IOException { + generator.flush(); + } + + @Override + public void close() throws IOException { + generator.close(); + } +} diff --git a/json/jackson-3/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider b/json/jackson-3/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider new file mode 100644 index 00000000000..5fec482303a --- /dev/null +++ b/json/jackson-3/src/main/resources/META-INF/services/io.opentelemetry.sdk.common.export.JsonProvider @@ -0,0 +1 @@ +io.opentelemetry.json.jackson3.internal.Jackson3JsonProvider diff --git a/json/jackson-3/src/test/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProviderTest.java b/json/jackson-3/src/test/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProviderTest.java new file mode 100644 index 00000000000..9841278a1fe --- /dev/null +++ b/json/jackson-3/src/test/java/io/opentelemetry/json/jackson3/internal/Jackson3JsonProviderTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.json.jackson3.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.common.export.JsonProvider; +import io.opentelemetry.sdk.common.export.JsonStringWriter; +import io.opentelemetry.sdk.common.export.JsonWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; + +class Jackson3JsonProviderTest { + + private final Jackson3JsonProvider provider = new Jackson3JsonProvider(); + + @Test + void serviceLoaderLoads() { + assertThat(ServiceLoader.load(JsonProvider.class)) + .anyMatch(p -> p instanceof Jackson3JsonProvider); + } + + @Test + void writeToOutputStream() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonWriter writer = provider.createJsonWriter(baos)) { + writeTestObject(writer); + } + assertThat(new String(baos.toByteArray(), StandardCharsets.UTF_8)) + .isEqualTo("{\"name\":\"test\",\"value\":42,\"active\":true}"); + } + + @Test + void writeToString() throws IOException { + JsonStringWriter stringWriter = provider.createJsonStringWriter(); + try (JsonWriter writer = stringWriter.writer()) { + writeTestObject(writer); + } + assertThat(stringWriter.getAndClear()) + .isEqualTo("{\"name\":\"test\",\"value\":42,\"active\":true}"); + } + + @Test + void autoCloseTargetTrue() throws IOException { + TestOutputStream stream = new TestOutputStream(); + try (JsonWriter writer = provider.createJsonWriter(stream, true)) { + writer.writeStartObject(); + writer.writeEndObject(); + } + assertThat(stream.closed).isTrue(); + } + + @Test + void autoCloseTargetFalse() throws IOException { + TestOutputStream stream = new TestOutputStream(); + try (JsonWriter writer = provider.createJsonWriter(stream, false)) { + writer.writeStartObject(); + writer.writeEndObject(); + } + assertThat(stream.closed).isFalse(); + } + + @Test + void writeNestedStructure() throws IOException { + JsonStringWriter stringWriter = provider.createJsonStringWriter(); + try (JsonWriter writer = stringWriter.writer()) { + writer.writeStartObject(); + writer.writeArrayFieldStart("items"); + writer.writeStartObject(); + writer.writeNumberField("id", 1); + writer.writeEndObject(); + writer.writeEndArray(); + writer.writeObjectFieldStart("nested"); + writer.writeNumberField("pi", 3.14); + writer.writeEndObject(); + writer.writeBinaryField("data", new byte[] {1, 2, 3}); + writer.writeEndObject(); + } + String json = stringWriter.getAndClear(); + assertThat(json).contains("\"items\":[{\"id\":1}]"); + assertThat(json).contains("\"nested\":{\"pi\":3.14}"); + assertThat(json).contains("\"data\":"); + } + + private static void writeTestObject(JsonWriter writer) throws IOException { + writer.writeStartObject(); + writer.writeStringField("name", "test"); + writer.writeNumberField("value", 42); + writer.writeBooleanField("active", true); + writer.writeEndObject(); + } + + private static class TestOutputStream extends ByteArrayOutputStream { + boolean closed; + + @Override + public void close() throws IOException { + closed = true; + super.close(); + } + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonProvider.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonProvider.java new file mode 100644 index 00000000000..0f22a02bec2 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common.export; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A service provider interface (SPI) for providing JSON streaming capabilities backed by different + * JSON libraries (e.g. Jackson 2, Jackson 3). + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface JsonProvider { + + /** Creates a {@link JsonWriter} that writes to the given {@link OutputStream}. */ + JsonWriter createJsonWriter(OutputStream output) throws IOException; + + /** + * Creates a {@link JsonWriter} that writes to the given {@link OutputStream}. + * + * @param autoCloseTarget if {@code false}, the underlying {@link OutputStream} will not be closed + * when the writer is closed + */ + JsonWriter createJsonWriter(OutputStream output, boolean autoCloseTarget) throws IOException; + + /** + * Creates a {@link JsonStringWriter} that writes JSON to an in-memory string buffer. Use {@link + * JsonStringWriter#getAndClear()} to retrieve the result after closing the writer. + */ + JsonStringWriter createJsonStringWriter() throws IOException; +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonStringWriter.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonStringWriter.java new file mode 100644 index 00000000000..100cac3b36b --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonStringWriter.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common.export; + +import java.io.IOException; + +/** + * A {@link JsonWriter} that accumulates output into a {@link String}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface JsonStringWriter { + + /** Returns the underlying {@link JsonWriter}. */ + JsonWriter writer(); + + /** Returns the accumulated JSON string. Call after closing the writer. */ + String getAndClear() throws IOException; +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonWriter.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonWriter.java new file mode 100644 index 00000000000..2283d2a185e --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/JsonWriter.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common.export; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Abstraction over JSON streaming writers, allowing different JSON libraries (e.g. Jackson 2, + * Jackson 3) to be used interchangeably. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface JsonWriter extends Closeable { + + void writeStartObject() throws IOException; + + void writeEndObject() throws IOException; + + void writeStartArray() throws IOException; + + void writeEndArray() throws IOException; + + void writeFieldName(String name) throws IOException; + + void writeString(String value) throws IOException; + + void writeStringField(String fieldName, String value) throws IOException; + + void writeNumber(int value) throws IOException; + + void writeNumber(long value) throws IOException; + + void writeNumber(double value) throws IOException; + + void writeNumberField(String fieldName, int value) throws IOException; + + void writeNumberField(String fieldName, long value) throws IOException; + + void writeNumberField(String fieldName, double value) throws IOException; + + void writeBoolean(boolean value) throws IOException; + + void writeBooleanField(String fieldName, boolean value) throws IOException; + + void writeBinary(byte[] data) throws IOException; + + void writeBinaryField(String fieldName, byte[] data) throws IOException; + + void writeObjectFieldStart(String fieldName) throws IOException; + + void writeArrayFieldStart(String fieldName) throws IOException; + + void writeRaw(String text) throws IOException; + + void writeRaw(char c) throws IOException; + + void flush() throws IOException; +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ecbfc83fdeb..8da29a74da0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -55,6 +55,8 @@ include(":integration-tests:tracecontext") include(":integration-tests:graal") include(":integration-tests:graal-incubating") include(":integration-tests:osgi") +include(":json:jackson-2") +include(":json:jackson-3") include(":javadoc-crawler") include(":opencensus-shim") include(":opentelemetry-jfr-profiles-shim")