Skip to content

Commit 27286d6

Browse files
mccullsclaude
andcommitted
Add OTLP/JSON export support for traces, metrics, and logs
Adds JSON siblings of existing hand-rolled protobuf OTLP encoders (OtlpCommonJson, OtlpResourceJson, OtlpTraceJson/Collector, OtlpMetricsJson/Collector, OtlpLogsJson/Collector) and wires the previously unused/silently-disabled HTTP_JSON protocol into OtlpWriter, OtlpMetricsService, and OtlpLogsService. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent a996fee commit 27286d6

42 files changed

Lines changed: 2761 additions & 210 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

components/json/src/main/java/datadog/json/JsonWriter.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package datadog.json;
22

33
import static java.nio.charset.StandardCharsets.UTF_8;
4-
import static java.util.Locale.ROOT;
54

65
import java.io.ByteArrayOutputStream;
76
import java.io.Flushable;
@@ -14,6 +13,7 @@
1413
*/
1514
public final class JsonWriter implements Flushable, AutoCloseable {
1615
private static final int INITIAL_CAPACITY = 256;
16+
private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
1717
private final ByteArrayOutputStream outputStream;
1818
private final OutputStreamWriter writer;
1919
private final JsonStructure structure;
@@ -282,14 +282,10 @@ private void writeStringLiteral(String str) {
282282
if (c > 127) {
283283
this.writer.write('\\');
284284
this.writer.write('u');
285-
String hexCharacter = Integer.toHexString(c).toUpperCase(ROOT);
286-
if (c < 4096) {
287-
this.writer.write('0');
288-
if (c < 256) {
289-
this.writer.write('0');
290-
}
291-
}
292-
this.writer.append(hexCharacter);
285+
this.writer.write(HEX_DIGITS[(c >>> 12) & 0xF]);
286+
this.writer.write(HEX_DIGITS[(c >>> 8) & 0xF]);
287+
this.writer.write(HEX_DIGITS[(c >>> 4) & 0xF]);
288+
this.writer.write(HEX_DIGITS[c & 0xF]);
293289
} else {
294290
switch (c) {
295291
case '"': // Quotation mark
@@ -319,7 +315,16 @@ private void writeStringLiteral(String str) {
319315
this.writer.write('t');
320316
break;
321317
default:
322-
this.writer.write(c);
318+
if (c < 0x20) {
319+
this.writer.write('\\');
320+
this.writer.write('u');
321+
this.writer.write('0');
322+
this.writer.write('0');
323+
this.writer.write(HEX_DIGITS[(c >>> 4) & 0xF]);
324+
this.writer.write(HEX_DIGITS[c & 0xF]);
325+
} else {
326+
this.writer.write(c);
327+
}
323328
break;
324329
}
325330
}

components/json/src/test/java/datadog/json/JsonWriterTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ void testStringEscaping() {
100100
}
101101
}
102102

103+
@Test
104+
void testControlCharacterEscaping() {
105+
try (JsonWriter writer = new JsonWriter()) {
106+
writer.beginArray().value("\u0001").value("\u001F").endArray();
107+
108+
assertEquals(
109+
"[\"\\u0001\",\"\\u001F\"]", writer.toString(), "Check control character escaping");
110+
}
111+
}
112+
103113
@Test
104114
void testArrayObjectNesting() {
105115
try (JsonWriter writer = new JsonWriter()) {

dd-trace-core/src/main/java/datadog/trace/common/writer/OtlpPayloadDispatcher.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import datadog.trace.core.otlp.common.OtlpPayload;
55
import datadog.trace.core.otlp.common.OtlpSender;
66
import datadog.trace.core.otlp.trace.OtlpTraceCollector;
7-
import datadog.trace.core.otlp.trace.OtlpTraceProtoCollector;
87
import java.util.Collection;
98
import java.util.Collections;
109
import java.util.List;
@@ -13,10 +12,6 @@ final class OtlpPayloadDispatcher implements PayloadDispatcher {
1312
private final OtlpTraceCollector collector;
1413
private final OtlpSender sender;
1514

16-
OtlpPayloadDispatcher(OtlpSender sender) {
17-
this(sender, new OtlpTraceProtoCollector());
18-
}
19-
2015
OtlpPayloadDispatcher(OtlpSender sender, OtlpTraceCollector collector) {
2116
this.sender = sender;
2217
this.collector = collector;

dd-trace-core/src/main/java/datadog/trace/common/writer/OtlpWriter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
import datadog.trace.core.otlp.common.OtlpGrpcSender;
1414
import datadog.trace.core.otlp.common.OtlpHttpSender;
1515
import datadog.trace.core.otlp.common.OtlpSender;
16+
import datadog.trace.core.otlp.trace.OtlpTraceCollector;
17+
import datadog.trace.core.otlp.trace.OtlpTraceJsonCollector;
18+
import datadog.trace.core.otlp.trace.OtlpTraceProtoCollector;
1619
import java.util.Collections;
1720
import java.util.Map;
1821
import java.util.concurrent.TimeUnit;
@@ -140,7 +143,11 @@ public OtlpWriter build() {
140143
endpoint, HTTP_TRACES_SIGNAL_PATH, headers, timeoutMillis, compression);
141144
}
142145

143-
final OtlpPayloadDispatcher dispatcher = new OtlpPayloadDispatcher(sender);
146+
final OtlpTraceCollector collector =
147+
protocol == OtlpConfig.Protocol.HTTP_JSON
148+
? new OtlpTraceJsonCollector()
149+
: new OtlpTraceProtoCollector();
150+
final OtlpPayloadDispatcher dispatcher = new OtlpPayloadDispatcher(sender, collector);
144151
final TraceProcessingWorker worker =
145152
new TraceProcessingWorker(
146153
traceBufferSize,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package datadog.trace.core.otlp.common;
2+
3+
import datadog.json.JsonWriter;
4+
5+
/** Tracks a JSON array that's opened lazily, on its first element, and closed once done. */
6+
public final class LazyJsonArray {
7+
private boolean open;
8+
9+
/** Opens the named array if it isn't already open. */
10+
public void ensureOpen(JsonWriter writer, String name) {
11+
if (!open) {
12+
writer.name(name).beginArray();
13+
open = true;
14+
}
15+
}
16+
17+
/** Closes the array if it's currently open. */
18+
public void closeIfOpen(JsonWriter writer) {
19+
if (open) {
20+
writer.endArray();
21+
open = false;
22+
}
23+
}
24+
25+
/** Resets the tracked state without touching the writer. */
26+
public void reset() {
27+
open = false;
28+
}
29+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package datadog.trace.core.otlp.common;
2+
3+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ARRAY_ATTRIBUTE;
4+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ATTRIBUTE;
5+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ARRAY_ATTRIBUTE;
6+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ATTRIBUTE;
7+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ARRAY_ATTRIBUTE;
8+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ATTRIBUTE;
9+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE;
10+
import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE;
11+
12+
import datadog.json.JsonWriter;
13+
import datadog.trace.api.DDSpanId;
14+
import datadog.trace.api.DDTraceId;
15+
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
16+
import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope;
17+
import java.util.List;
18+
19+
/** Provides writers for OpenTelemetry's "common.proto" JSON encoding. */
20+
public final class OtlpCommonJson {
21+
private OtlpCommonJson() {}
22+
23+
/** Hex-encodes a 128-bit trace id, per the OTLP JSON encoding spec. */
24+
public static String hexTraceId(DDTraceId traceId) {
25+
return traceId.toHexString();
26+
}
27+
28+
/** Hex-encodes a 64-bit span/parent id, per the OTLP JSON encoding spec. */
29+
public static String hexSpanId(long spanId) {
30+
return DDSpanId.toHexStringPadded(spanId);
31+
}
32+
33+
public static void writeInstrumentationScope(JsonWriter writer, OtelInstrumentationScope scope) {
34+
writer.beginObject();
35+
writer.name("name").value(scope.getName().toString());
36+
if (scope.getVersion() != null) {
37+
writer.name("version").value(scope.getVersion().toString());
38+
}
39+
writer.endObject();
40+
}
41+
42+
/** Writes a scope's {@code "scope"} and optional sibling {@code "schemaUrl"} fields. */
43+
public static void writeScopeAndSchema(JsonWriter writer, OtelInstrumentationScope scope) {
44+
writer.name("scope");
45+
writeInstrumentationScope(writer, scope);
46+
if (scope.getSchemaUrl() != null) {
47+
writer.name("schemaUrl").value(scope.getSchemaUrl().toString());
48+
}
49+
}
50+
51+
/** Writes one {@code KeyValue} JSON object: {@code {"key":...,"value":{...}}}. */
52+
@SuppressWarnings("unchecked")
53+
public static void writeAttribute(JsonWriter writer, int type, CharSequence key, Object value) {
54+
writeAttributeKey(writer, key);
55+
switch (type) {
56+
case STRING_ATTRIBUTE:
57+
writeStringValue(writer, (String) value);
58+
break;
59+
case BOOLEAN_ATTRIBUTE:
60+
writeBooleanValue(writer, (boolean) value);
61+
break;
62+
case LONG_ATTRIBUTE:
63+
writeIntValue(writer, ((Number) value).longValue());
64+
break;
65+
case DOUBLE_ATTRIBUTE:
66+
writeDoubleValue(writer, ((Number) value).doubleValue());
67+
break;
68+
case STRING_ARRAY_ATTRIBUTE:
69+
writeArrayValue(writer, STRING_ATTRIBUTE, (List<String>) value);
70+
break;
71+
case BOOLEAN_ARRAY_ATTRIBUTE:
72+
writeArrayValue(writer, BOOLEAN_ATTRIBUTE, (List<Boolean>) value);
73+
break;
74+
case LONG_ARRAY_ATTRIBUTE:
75+
writeArrayValue(writer, LONG_ATTRIBUTE, (List<? extends Number>) value);
76+
break;
77+
case DOUBLE_ARRAY_ATTRIBUTE:
78+
writeArrayValue(writer, DOUBLE_ATTRIBUTE, (List<? extends Number>) value);
79+
break;
80+
default:
81+
throw new IllegalArgumentException("Unknown attribute type: " + type);
82+
}
83+
writer.endObject();
84+
}
85+
86+
public static void writeAttribute(JsonWriter writer, UTF8BytesString key, String value) {
87+
writeAttribute(writer, STRING_ATTRIBUTE, key.toString(), value);
88+
}
89+
90+
public static void writeAttribute(JsonWriter writer, UTF8BytesString key, long value) {
91+
writeAttributeKey(writer, key);
92+
writeIntValue(writer, value);
93+
writer.endObject();
94+
}
95+
96+
private static void writeAttributeKey(JsonWriter writer, CharSequence key) {
97+
writer.beginObject();
98+
writer.name("key").value(key.toString());
99+
writer.name("value");
100+
}
101+
102+
private static void writeArrayValue(JsonWriter writer, int elementType, List<?> values) {
103+
writer.beginObject();
104+
writer.name("arrayValue").beginObject();
105+
writer.name("values").beginArray();
106+
for (Object value : values) {
107+
writeAnyValue(writer, elementType, value);
108+
}
109+
writer.endArray();
110+
writer.endObject();
111+
writer.endObject();
112+
}
113+
114+
private static void writeAnyValue(JsonWriter writer, int type, Object value) {
115+
switch (type) {
116+
case STRING_ATTRIBUTE:
117+
writeStringValue(writer, (String) value);
118+
break;
119+
case BOOLEAN_ATTRIBUTE:
120+
writeBooleanValue(writer, (boolean) value);
121+
break;
122+
case LONG_ATTRIBUTE:
123+
writeIntValue(writer, ((Number) value).longValue());
124+
break;
125+
case DOUBLE_ATTRIBUTE:
126+
writeDoubleValue(writer, ((Number) value).doubleValue());
127+
break;
128+
default:
129+
throw new IllegalArgumentException("Unknown attribute type: " + type);
130+
}
131+
}
132+
133+
private static void writeStringValue(JsonWriter writer, String value) {
134+
writer.beginObject().name("stringValue").value(value).endObject();
135+
}
136+
137+
private static void writeBooleanValue(JsonWriter writer, boolean value) {
138+
writer.beginObject().name("boolValue").value(value).endObject();
139+
}
140+
141+
private static void writeIntValue(JsonWriter writer, long value) {
142+
// int64 fields are encoded as decimal strings, per the OTLP JSON encoding spec
143+
writer.beginObject().name("intValue").value(Long.toString(value)).endObject();
144+
}
145+
146+
private static void writeDoubleValue(JsonWriter writer, double value) {
147+
writer.beginObject().name("doubleValue");
148+
writeDouble(writer, value);
149+
writer.endObject();
150+
}
151+
152+
/** Writes a double per the OTLP/ProtoJSON encoding spec: NaN/Infinity as JSON strings. */
153+
public static void writeDouble(JsonWriter writer, double value) {
154+
if (Double.isNaN(value)) {
155+
writer.value("NaN");
156+
} else if (Double.isInfinite(value)) {
157+
writer.value(value > 0 ? "Infinity" : "-Infinity");
158+
} else {
159+
writer.value(value);
160+
}
161+
}
162+
}

dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpHttpSender.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ public OtlpHttpSender(
5454
this.client = buildHttpClient(isPlainHttp(url), unixDomainSocketPath, null, timeoutMillis);
5555
}
5656

57+
public HttpUrl url() {
58+
return url;
59+
}
60+
5761
@Override
5862
public void send(OtlpPayload payload) {
5963
Request request = makeRequest(payload);

dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpPayload.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import java.nio.ByteBuffer;
44

55
public final class OtlpPayload {
6+
public static final String PROTOBUF_CONTENT_TYPE = "application/x-protobuf";
7+
public static final String JSON_CONTENT_TYPE = "application/json";
8+
69
public static final OtlpPayload EMPTY = new OtlpPayload(ByteBuffer.allocate(0), "");
710

811
private final ByteBuffer content;

dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpProtoBuffer.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static datadog.trace.core.otlp.common.OtlpCommonProto.LEN_WIRE_TYPE;
44
import static datadog.trace.core.otlp.common.OtlpCommonProto.sizeVarInt;
55
import static datadog.trace.core.otlp.common.OtlpCommonProto.writeVarInt;
6+
import static datadog.trace.core.otlp.common.OtlpPayload.PROTOBUF_CONTENT_TYPE;
67
import static datadog.trace.util.BitUtils.nextPowerOfTwo;
78

89
import datadog.communication.serialization.GrowableBuffer;
@@ -17,8 +18,6 @@
1718
* @see GrowableBuffer
1819
*/
1920
public final class OtlpProtoBuffer {
20-
private static final String PROTOBUF_CONTENT_TYPE = "application/x-protobuf";
21-
2221
private final int initialCapacity;
2322
private ByteBuffer buffer;
2423
private int remaining;

0 commit comments

Comments
 (0)