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.. + * + *
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 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")