diff --git a/exporters/common/build.gradle.kts b/exporters/common/build.gradle.kts index a589816d2a2..48f610282a3 100644 --- a/exporters/common/build.gradle.kts +++ b/exporters/common/build.gradle.kts @@ -7,7 +7,7 @@ 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. @@ -69,9 +69,6 @@ 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") 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..9a3c0169cb9 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,26 +5,17 @@ package io.opentelemetry.exporter.internal.marshal; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; 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 generator; - private final JsonGenerator generator; - - JsonSerializer(OutputStream output) throws IOException { - this(JSON_FACTORY.createGenerator(output)); - } - - JsonSerializer(JsonGenerator generator) { - this.generator = generator; + JsonSerializer(OutputStream output) { + this.generator = new JsonWriter(output); } @Override @@ -105,13 +96,10 @@ protected void writeDoubleValue(double value) throws IOException { @Override public void writeString(ProtoFieldInfo field, byte[] utf8Bytes) throws IOException { generator.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)); + // Marshalers already encoded the String to UTF-8 bytes (binary serialization needs them for + // both size computation and writing), so write them directly rather than decoding and + // re-encoding. + generator.writeUtf8String(utf8Bytes); } @Override @@ -126,14 +114,8 @@ public void writeString( public void writeRepeatedString(ProtoFieldInfo field, byte[][] utf8Bytes) throws IOException { generator.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 - // 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(value, StandardCharsets.UTF_8)); + // See writeString(ProtoFieldInfo, byte[]): the bytes are already UTF-8, so write directly. + generator.writeUtf8String(value); } generator.writeEndArray(); } diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonWriter.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonWriter.java new file mode 100644 index 00000000000..3d7596aa00f --- /dev/null +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonWriter.java @@ -0,0 +1,308 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.marshal; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +/** + * A minimal JSON writer that serializes directly to an {@link OutputStream} as UTF-8, implementing + * only the subset of JSON generation {@link JsonSerializer} needs so OTLP JSON serialization has no + * third party dependency. Method names mirror the Jackson {@code JsonGenerator} that previously + * backed {@link JsonSerializer}. + * + *

The caller is responsible for structural validity (matching braces); this class only inserts + * separators between members. {@link #writeRaw(String)} writes verbatim without touching separator + * state, which {@link MarshalerUtil#preserializeJsonFields(Marshaler)} relies on. + */ +final class JsonWriter { + + private static final byte[] TRUE = {'t', 'r', 'u', 'e'}; + private static final byte[] FALSE = {'f', 'a', 'l', 's', 'e'}; + private static final byte[] HEX = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private final OutputStream out; + private final byte[] buffer = new byte[4096]; + private int pos; + + // Number of members already written at each nesting depth; depth 0 is the implicit root. + private int[] counts = new int[16]; + private int depth; + // True immediately after a field name has been written, meaning the next value is that field's + // value and must not be preceded by a comma. + private boolean afterKey; + + JsonWriter(OutputStream out) { + this.out = out; + } + + void writeStartObject() throws IOException { + beforeValue(); + writeByte((byte) '{'); + push(); + } + + void writeEndObject() throws IOException { + pop(); + writeByte((byte) '}'); + } + + void writeObjectFieldStart(String name) throws IOException { + writeFieldName(name); + writeStartObject(); + } + + void writeArrayFieldStart(String name) throws IOException { + writeFieldName(name); + beforeValue(); + writeByte((byte) '['); + push(); + } + + void writeEndArray() throws IOException { + pop(); + writeByte((byte) ']'); + } + + void writeFieldName(String name) throws IOException { + if (counts[depth] > 0) { + writeByte((byte) ','); + } + counts[depth]++; + writeQuoted(name); + writeByte((byte) ':'); + afterKey = true; + } + + void writeStringField(String name, String value) throws IOException { + writeFieldName(name); + writeString(value); + } + + void writeBooleanField(String name, boolean value) throws IOException { + writeFieldName(name); + beforeValue(); + writeRawBytes(value ? TRUE : FALSE); + } + + void writeNumberField(String name, int value) throws IOException { + writeFieldName(name); + beforeValue(); + writeAscii(Integer.toString(value)); + } + + void writeNumberField(String name, double value) throws IOException { + writeFieldName(name); + writeNumber(value); + } + + void writeBinaryField(String name, byte[] value) throws IOException { + writeFieldName(name); + beforeValue(); + writeByte((byte) '"'); + writeRawBytes(Base64.getEncoder().encode(value)); + writeByte((byte) '"'); + } + + void writeString(String value) throws IOException { + beforeValue(); + writeQuoted(value); + } + + /** + * Writes a JSON string value from already UTF-8 encoded bytes, avoiding a decode-then-re-encode + * round trip. ASCII bytes are escaped as needed; multi-byte UTF-8 sequences pass through + * verbatim. + */ + void writeUtf8String(byte[] utf8Bytes) throws IOException { + beforeValue(); + writeByte((byte) '"'); + for (byte b : utf8Bytes) { + if (b >= 0) { + writeEscapedAscii(b); // ASCII, may need escaping + } else { + writeByte(b); // multi-byte UTF-8, never escapable + } + } + writeByte((byte) '"'); + } + + void writeNumber(double value) throws IOException { + beforeValue(); + // proto3 JSON encodes the non-finite values as quoted strings; a bare NaN/Infinity is not valid + // JSON. This matches io.opentelemetry.api.common.JsonEncoding. + if (Double.isNaN(value)) { + writeAscii("\"NaN\""); + } else if (Double.isInfinite(value)) { + writeAscii(value > 0 ? "\"Infinity\"" : "\"-Infinity\""); + } else { + writeAscii(Double.toString(value)); + } + } + + /** Writes pre-serialized JSON verbatim, without updating separator state. */ + void writeRaw(String raw) throws IOException { + writeRawBytes(raw.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Drains buffered bytes to the underlying stream. Like {@code ProtoSerializer}, it neither + * flushes nor closes the underlying stream; the caller owns its lifecycle. + */ + void close() throws IOException { + try { + if (pos > 0) { + out.write(buffer, 0, pos); + pos = 0; + } + } catch (IOException e) { + // In try-with-resources, draining may rethrow the same exception that failed the body; wrap + // it so re-throwing doesn't trigger an IllegalArgumentException from illegal + // self-suppression. + throw new IOException(e); + } + } + + private void beforeValue() throws IOException { + if (afterKey) { + afterKey = false; + return; + } + if (counts[depth] > 0) { + writeByte((byte) ','); + } + counts[depth]++; + } + + private void push() { + depth++; + if (depth == counts.length) { + counts = Arrays.copyOf(counts, counts.length * 2); + } + counts[depth] = 0; + } + + private void pop() { + depth--; + } + + /** Writes a quoted, escaped JSON string, encoding {@code value} as UTF-8. */ + private void writeQuoted(String value) throws IOException { + writeByte((byte) '"'); + int length = value.length(); + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + if (c < 0x80) { + writeEscapedAscii((byte) c); + } else if (c < 0x800) { + writeByte((byte) (0xC0 | (c >> 6))); + writeByte((byte) (0x80 | (c & 0x3F))); + } else if (Character.isHighSurrogate(c) && i + 1 < length) { + char low = value.charAt(i + 1); + if (Character.isLowSurrogate(low)) { + int codePoint = Character.toCodePoint(c, low); + writeByte((byte) (0xF0 | (codePoint >> 18))); + writeByte((byte) (0x80 | ((codePoint >> 12) & 0x3F))); + writeByte((byte) (0x80 | ((codePoint >> 6) & 0x3F))); + writeByte((byte) (0x80 | (codePoint & 0x3F))); + i++; + } else { + writeByte((byte) '?'); + } + } else if (Character.isSurrogate(c)) { + // Unpaired surrogate; emit a replacement to keep the output valid UTF-8. + writeByte((byte) '?'); + } else { + writeByte((byte) (0xE0 | (c >> 12))); + writeByte((byte) (0x80 | ((c >> 6) & 0x3F))); + writeByte((byte) (0x80 | (c & 0x3F))); + } + } + writeByte((byte) '"'); + } + + /** Writes a single ASCII byte (0x00-0x7F), escaping it if required by JSON. */ + private void writeEscapedAscii(byte b) throws IOException { + switch (b) { + case '"': + writeByte((byte) '\\'); + writeByte((byte) '"'); + return; + case '\\': + writeByte((byte) '\\'); + writeByte((byte) '\\'); + return; + case '\b': + writeByte((byte) '\\'); + writeByte((byte) 'b'); + return; + case '\f': + writeByte((byte) '\\'); + writeByte((byte) 'f'); + return; + case '\n': + writeByte((byte) '\\'); + writeByte((byte) 'n'); + return; + case '\r': + writeByte((byte) '\\'); + writeByte((byte) 'r'); + return; + case '\t': + writeByte((byte) '\\'); + writeByte((byte) 't'); + return; + default: + if (b < 0x20) { + writeByte((byte) '\\'); + writeByte((byte) 'u'); + writeByte((byte) '0'); + writeByte((byte) '0'); + writeByte(HEX[(b >> 4) & 0xF]); + writeByte(HEX[b & 0xF]); + } else { + writeByte(b); + } + } + } + + /** Writes the bytes of an ASCII-only string such as a formatted number. */ + private void writeAscii(String value) throws IOException { + int length = value.length(); + for (int i = 0; i < length; i++) { + writeByte((byte) value.charAt(i)); + } + } + + private void writeRawBytes(byte[] bytes) throws IOException { + int offset = 0; + int remaining = bytes.length; + while (remaining > 0) { + if (pos == buffer.length) { + out.write(buffer, 0, pos); + pos = 0; + } + int chunk = Math.min(remaining, buffer.length - pos); + System.arraycopy(bytes, offset, buffer, pos, chunk); + pos += chunk; + offset += chunk; + remaining -= chunk; + } + } + + private void writeByte(byte b) throws IOException { + if (pos == buffer.length) { + out.write(buffer, 0, pos); + pos = 0; + } + buffer[pos++] = b; + } +} 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..1c65e3303ce 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,7 +5,6 @@ package io.opentelemetry.exporter.internal.marshal; -import com.fasterxml.jackson.core.JsonGenerator; import io.opentelemetry.sdk.common.export.MessageWriter; import java.io.IOException; import java.io.OutputStream; @@ -32,24 +31,6 @@ public final void writeJsonTo(OutputStream output) throws IOException { } } - /** 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 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'); - } - } - /** Returns the number of bytes this Marshaler will write in proto binary format. */ public abstract int getBinarySerializedSize(); 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..9d7a10471f6 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 @@ -35,19 +35,6 @@ public final class MarshalerUtil { private static final int SPAN_ID_VALUE_SIZE = CodedOutputStream.computeLengthDelimitedFieldSize(SpanId.getLength() / 2); - private static final boolean JSON_AVAILABLE; - - static { - boolean jsonAvailable = false; - try { - Class.forName("com.fasterxml.jackson.core.JsonFactory"); - jsonAvailable = true; - } catch (ClassNotFoundException e) { - // Not available - } - JSON_AVAILABLE = jsonAvailable; - } - private static final byte[] EMPTY_BYTES = new byte[0]; /** Groups SDK items by resource and instrumentation scope. */ @@ -73,10 +60,6 @@ Map>> groupByResourceAndScope( /** Preserialize into JSON format. */ public static String preserializeJsonFields(Marshaler marshaler) { - if (!MarshalerUtil.JSON_AVAILABLE) { - return ""; - } - ByteArrayOutputStream jsonBos = new ByteArrayOutputStream(); try { marshaler.writeJsonTo(jsonBos); @@ -85,11 +68,8 @@ public static String preserializeJsonFields(Marshaler marshaler) { "Serialization error, this is likely a bug in OpenTelemetry.", e); } - // We effectively cache `writeTo`, however Jackson would not allow us to only write out - // fields - // which is what writeTo does. So we need to write to an object but skip the object start - // / - // end. + // Strip the surrounding { } so the cached value can be embedded as the fields of an enclosing + // object. byte[] jsonBytes = jsonBos.toByteArray(); return new String(jsonBytes, 1, jsonBytes.length - 2, StandardCharsets.UTF_8); } diff --git a/exporters/common/src/test/java/io/opentelemetry/exporter/internal/marshal/JsonWriterTest.java b/exporters/common/src/test/java/io/opentelemetry/exporter/internal/marshal/JsonWriterTest.java new file mode 100644 index 00000000000..32c9b5a4803 --- /dev/null +++ b/exporters/common/src/test/java/io/opentelemetry/exporter/internal/marshal/JsonWriterTest.java @@ -0,0 +1,199 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.marshal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class JsonWriterTest { + + @FunctionalInterface + private interface Body { + void accept(JsonWriter writer) throws IOException; + } + + @ParameterizedTest + @MethodSource("cases") + void writesExpectedJson(Body body, String expected) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + JsonWriter writer = new JsonWriter(bos); + try { + body.accept(writer); + writer.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + assertThat(new String(bos.toByteArray(), StandardCharsets.UTF_8)).isEqualTo(expected); + } + + static Stream cases() { + // Built with concatenation to keep the source ASCII: quote, backslash, slash, control-char + // shortcuts, NUL, U+001F, 'a', and the 2-/3-/4-byte UTF-8 chars U+00E9 (e-acute), U+4E16 (CJK), + // and U+1F600 (grinning face). Forward slash is not escaped, control characters use lowercase + // \\u00xx, and multi-byte UTF-8 passes through verbatim. + String escapeInput = + "\"" + + '\\' + + '/' + + '\b' + + '\f' + + '\n' + + '\r' + + '\t' + + (char) 0x00 + + (char) 0x1f + + 'a' + + (char) 0xe9 + + (char) 0x4e16 + + new String(Character.toChars(0x1f600)); + String escapeExpected = + "\"\\\"\\\\/\\b\\f\\n\\r\\t\\u0000\\u001fa" + + (char) 0xe9 + + (char) 0x4e16 + + new String(Character.toChars(0x1f600)) + + "\""; + + // 'a', TAB, then U+00E9 (e-acute) as its two UTF-8 bytes 0xC3 0xA9. + byte[] utf8 = {'a', '\t', (byte) 0xc3, (byte) 0xa9}; + String utf8Expected = "\"a\\t" + (char) 0xe9 + "\""; + + return Stream.of( + Arguments.argumentSet( + "empty object", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeEndObject(); + }, + "{}"), + Arguments.argumentSet( + "object inserts separators between fields", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeStringField("a", "b"); + writer.writeNumberField("n", 1); + writer.writeBooleanField("t", true); + writer.writeBooleanField("f", false); + writer.writeEndObject(); + }, + "{\"a\":\"b\",\"n\":1,\"t\":true,\"f\":false}"), + Arguments.argumentSet( + "nested objects and arrays", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeObjectFieldStart("o"); + writer.writeStringField("k", "v"); + writer.writeEndObject(); + writer.writeArrayFieldStart("arr"); + writer.writeString("x"); + writer.writeNumber(2.5); + writer.writeEndArray(); + writer.writeEndObject(); + }, + "{\"o\":{\"k\":\"v\"},\"arr\":[\"x\",2.5]}"), + Arguments.argumentSet( + "string escaping", (Body) writer -> writer.writeString(escapeInput), escapeExpected), + Arguments.argumentSet( + "writeUtf8String escapes ascii and passes multi-byte through", + (Body) writer -> writer.writeUtf8String(utf8), + utf8Expected), + Arguments.argumentSet( + "integer boundaries", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeNumberField("min", Integer.MIN_VALUE); + writer.writeNumberField("max", Integer.MAX_VALUE); + writer.writeEndObject(); + }, + "{\"min\":-2147483648,\"max\":2147483647}"), + Arguments.argumentSet( + "exponent and signed-zero doubles", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeArrayFieldStart("d"); + writer.writeNumber(1.0e7); + writer.writeNumber(1.0e-4); + writer.writeNumber(-0.0); + writer.writeNumber(0.0); + writer.writeEndArray(); + writer.writeEndObject(); + }, + "{\"d\":[1.0E7,1.0E-4,-0.0,0.0]}"), + Arguments.argumentSet( + "empty base64", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeBinaryField("b", new byte[0]); + writer.writeEndObject(); + }, + "{\"b\":\"\"}"), + Arguments.argumentSet( + "objects in array", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeArrayFieldStart("a"); + writer.writeStartObject(); + writer.writeNumberField("x", 1); + writer.writeEndObject(); + writer.writeStartObject(); + writer.writeNumberField("y", 2); + writer.writeEndObject(); + writer.writeEndArray(); + writer.writeEndObject(); + }, + "{\"a\":[{\"x\":1},{\"y\":2}]}"), + Arguments.argumentSet( + "non-finite doubles are quoted", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeArrayFieldStart("d"); + writer.writeNumber(Double.NaN); + writer.writeNumber(Double.POSITIVE_INFINITY); + writer.writeNumber(Double.NEGATIVE_INFINITY); + writer.writeNumber(1.5); + writer.writeEndArray(); + writer.writeEndObject(); + }, + "{\"d\":[\"NaN\",\"Infinity\",\"-Infinity\",1.5]}"), + Arguments.argumentSet( + "binary field is base64", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeBinaryField("b", new byte[] {1, 2, 3}); + writer.writeEndObject(); + }, + "{\"b\":\"AQID\"}"), + // Mirrors MarshalerUtil#preserializeJsonFields: pre-serialized fields written raw into an + // otherwise empty object, without disturbing separator state. + Arguments.argumentSet( + "writeRaw embeds verbatim", + (Body) + writer -> { + writer.writeStartObject(); + writer.writeObjectFieldStart("resource"); + writer.writeRaw("\"key\":\"val\""); + writer.writeEndObject(); + writer.writeEndObject(); + }, + "{\"resource\":{\"key\":\"val\"}}")); + } +} diff --git a/exporters/logging-otlp/build.gradle.kts b/exporters/logging-otlp/build.gradle.kts index bdec906e21c..55219c9daee 100644 --- a/exporters/logging-otlp/build.gradle.kts +++ b/exporters/logging-otlp/build.gradle.kts @@ -24,8 +24,6 @@ dependencies { compileOnly(project(":api:incubator")) compileOnly(project(":sdk-extensions:autoconfigure-spi")) - implementation("com.fasterxml.jackson.core:jackson-core") - testImplementation(project(":api:incubator")) 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/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..a071dec98e3 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,13 +5,11 @@ package io.opentelemetry.exporter.logging.otlp.internal.writer; -import static io.opentelemetry.exporter.logging.otlp.internal.writer.JsonUtil.JSON_FACTORY; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.io.SegmentedStringWriter; import io.opentelemetry.exporter.internal.marshal.Marshaler; import io.opentelemetry.sdk.common.CompletableResultCode; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; @@ -29,23 +27,25 @@ public LoggerJsonWriter(Logger logger, String type) { this.type = type; } + // toString(String) decodes straight from the internal buffer, avoiding the array copy that + // new String(bos.toByteArray(), UTF_8) makes. The toString(Charset) overload errorprone wants is + // Java 10+, but this module targets Java 8. + @SuppressWarnings("JdkObsolete") @Override public CompletableResultCode write(Marshaler exportRequest) { - SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); - try (JsonGenerator gen = JsonUtil.create(sw)) { - exportRequest.writeJsonToGenerator(gen); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); - return CompletableResultCode.ofFailure(); - } - + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + String json; try { - logger.log(Level.INFO, sw.getAndClear()); - return CompletableResultCode.ofSuccess(); + exportRequest.writeJsonTo(bos); + // UTF-8 is always available; the declared UnsupportedEncodingException cannot occur. + json = bos.toString(StandardCharsets.UTF_8.name()); } catch (IOException e) { logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); return CompletableResultCode.ofFailure(); } + + logger.log(Level.INFO, json); + return CompletableResultCode.ofSuccess(); } @Override 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..7e17363b58a 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); @@ -38,10 +34,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)); + exportRequest.writeJsonTo(outputStream); + outputStream.write('\n'); + outputStream.flush(); return CompletableResultCode.ofSuccess(); } catch (IOException e) { logger.log(Level.WARNING, "Unable to write OTLP JSON " + type, e); 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..566000443af 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,11 +9,11 @@ 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; import java.io.IOException; +import java.io.OutputStream; import java.util.logging.Logger; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -34,9 +34,7 @@ void testToString() { @Test void error() throws IOException { Marshaler marshaler = mock(Marshaler.class); - Mockito.doThrow(new IOException("test")) - .when(marshaler) - .writeJsonToGenerator(any(JsonGenerator.class)); + Mockito.doThrow(new IOException("test")).when(marshaler).writeJsonTo(any(OutputStream.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..de88db42dec 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; @@ -48,9 +47,7 @@ void testToString() throws IOException { @Test void errorWriting() throws IOException { Marshaler marshaler = mock(Marshaler.class); - Mockito.doThrow(new IOException("test")) - .when(marshaler) - .writeJsonWithNewline(any(JsonGenerator.class)); + Mockito.doThrow(new IOException("test")).when(marshaler).writeJsonTo(any(OutputStream.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 9acc0125e7f..937f30785a3 100644 --- a/exporters/otlp/common/build.gradle.kts +++ b/exporters/otlp/common/build.gradle.kts @@ -30,14 +30,12 @@ dependencies { testImplementation(project(":sdk:logs")) testImplementation(project(":sdk:testing")) - testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.google.protobuf:protobuf-java-util") testImplementation("com.google.guava:guava") testImplementation("io.opentelemetry.proto:opentelemetry-proto") jmhImplementation(project(":api:incubator")) jmhImplementation(project(":sdk:testing")) - jmhImplementation("com.fasterxml.jackson.core:jackson-core") jmhImplementation("io.opentelemetry.proto:opentelemetry-proto") jmhImplementation("io.grpc:grpc-netty") } @@ -49,7 +47,6 @@ testing { implementation(project(":api:incubator")) implementation(project(":sdk:testing")) - implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.google.protobuf:protobuf-java-util") implementation("com.google.guava:guava") implementation("io.opentelemetry.proto:opentelemetry-proto") diff --git a/exporters/otlp/profiles/build.gradle.kts b/exporters/otlp/profiles/build.gradle.kts index bbed9ee6837..ccd965fba5f 100644 --- a/exporters/otlp/profiles/build.gradle.kts +++ b/exporters/otlp/profiles/build.gradle.kts @@ -20,7 +20,6 @@ dependencies { compileOnly("io.grpc:grpc-stub") testCompileOnly("com.google.guava:guava") - testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.google.protobuf:protobuf-java-util") testImplementation("io.opentelemetry.proto:opentelemetry-proto") testImplementation(project(":exporters:otlp:testing-internal")) diff --git a/exporters/sender/jdk/build.gradle.kts b/exporters/sender/jdk/build.gradle.kts index d7545fab832..26cd80a9b9c 100644 --- a/exporters/sender/jdk/build.gradle.kts +++ b/exporters/sender/jdk/build.gradle.kts @@ -15,6 +15,4 @@ dependencies { implementation(project(":exporters:common")) implementation(project(":sdk:common")) - - testImplementation("com.fasterxml.jackson.core:jackson-core") } diff --git a/exporters/sender/okhttp/build.gradle.kts b/exporters/sender/okhttp/build.gradle.kts index 29f1badf131..85aa30f3125 100644 --- a/exporters/sender/okhttp/build.gradle.kts +++ b/exporters/sender/okhttp/build.gradle.kts @@ -24,7 +24,6 @@ dependencies { implementation("com.squareup.okhttp3:okhttp") compileOnly("io.grpc:grpc-stub") - compileOnly("com.fasterxml.jackson.core:jackson-core") testImplementation("com.linecorp.armeria:armeria-junit5") }