Skip to content

Commit b15ea59

Browse files
committed
Implement OpenTelemetry Logs API
1 parent e6cac64 commit b15ea59

30 files changed

Lines changed: 1186 additions & 69 deletions

File tree

dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/OtelInstrumentationScope.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import javax.annotation.Nullable;
66

77
/** Instrumentation scopes have a mandatory name, optional version, and optional schema URL. */
8-
public final class OtelInstrumentationScope {
8+
public final class OtelInstrumentationScope implements Comparable<OtelInstrumentationScope> {
99

1010
private final UTF8BytesString scopeName;
1111
@Nullable private final UTF8BytesString scopeVersion;
@@ -32,6 +32,34 @@ public UTF8BytesString getSchemaUrl() {
3232
return schemaUrl;
3333
}
3434

35+
@Override
36+
public int compareTo(OtelInstrumentationScope that) {
37+
int cmp = scopeName.toString().compareTo(that.scopeName.toString());
38+
if (cmp != 0) {
39+
return cmp;
40+
}
41+
if (scopeVersion != that.scopeVersion) {
42+
if (scopeVersion == null) {
43+
return -1;
44+
} else if (that.scopeVersion == null) {
45+
return 1;
46+
}
47+
cmp = scopeVersion.toString().compareTo(that.scopeVersion.toString());
48+
if (cmp != 0) {
49+
return cmp;
50+
}
51+
}
52+
if (schemaUrl != that.schemaUrl) {
53+
if (schemaUrl == null) {
54+
return -1;
55+
} else if (that.schemaUrl == null) {
56+
return 1;
57+
}
58+
return schemaUrl.toString().compareTo(that.schemaUrl.toString());
59+
}
60+
return 0;
61+
}
62+
3563
@Override
3664
public boolean equals(Object o) {
3765
if (!(o instanceof OtelInstrumentationScope)) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package datadog.trace.bootstrap.otel.logs.data;
2+
3+
import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope;
4+
import datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor;
5+
import datadog.trace.bootstrap.otlp.logs.OtlpLogRecord;
6+
import datadog.trace.bootstrap.otlp.logs.OtlpLogsVisitor;
7+
import datadog.trace.bootstrap.otlp.logs.OtlpScopedLogsVisitor;
8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.Comparator;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Objects;
14+
import java.util.Queue;
15+
import java.util.WeakHashMap;
16+
import java.util.concurrent.ArrayBlockingQueue;
17+
import java.util.function.BiConsumer;
18+
19+
/** Processes log records, grouping them by instrumentation scope. */
20+
public final class OtelLogRecordProcessor {
21+
public static final OtelLogRecordProcessor INSTANCE = new OtelLogRecordProcessor();
22+
23+
private static final Comparator<OtlpLogRecord> BY_SCOPE =
24+
Comparator.comparing(o -> o.instrumentationScope);
25+
26+
private static final Map<ClassLoader, BiConsumer<Map<?, ?>, OtlpAttributeVisitor>>
27+
ATTRIBUTE_READERS = Collections.synchronizedMap(new WeakHashMap<>());
28+
29+
private final Queue<OtlpLogRecord> queue = new ArrayBlockingQueue<>(2048);
30+
31+
public void addLog(OtlpLogRecord logRecord) {
32+
queue.offer(logRecord);
33+
}
34+
35+
public void collectLogs(OtlpLogsVisitor visitor) {
36+
OtlpScopedLogsVisitor scopedVisitor = null;
37+
OtelInstrumentationScope currentScope = null;
38+
BiConsumer<Map<?, ?>, OtlpAttributeVisitor> attributesReader = null;
39+
ClassLoader attributesClassLoader = null;
40+
for (OtlpLogRecord logRecord : batchByScope()) {
41+
if (logRecord.instrumentationScope != currentScope) {
42+
currentScope = logRecord.instrumentationScope;
43+
scopedVisitor = visitor.visitScopedLogs(currentScope);
44+
}
45+
Map<?, ?> attributes = logRecord.attributes;
46+
if (attributes != null && !attributes.isEmpty()) {
47+
ClassLoader cl = getAttributesClassLoader(attributes);
48+
// avoid repeated lookups when attribute class-loader is same for all records
49+
if (attributesReader == null || !Objects.equals(cl, attributesClassLoader)) {
50+
attributesReader = ATTRIBUTE_READERS.get(cl);
51+
attributesClassLoader = cl;
52+
}
53+
if (attributesReader != null) {
54+
attributesReader.accept(attributes, scopedVisitor);
55+
}
56+
}
57+
scopedVisitor.visitLogRecord(logRecord);
58+
}
59+
}
60+
61+
private static ClassLoader getAttributesClassLoader(Map<?, ?> attributes) {
62+
// need to peek at the first key, as the map will be a JDK collection type
63+
return attributes.keySet().iterator().next().getClass().getClassLoader();
64+
}
65+
66+
public static void registerAttributeReader(
67+
ClassLoader cl, BiConsumer<Map<?, ?>, OtlpAttributeVisitor> reader) {
68+
ATTRIBUTE_READERS.put(cl, reader);
69+
}
70+
71+
private List<OtlpLogRecord> batchByScope() {
72+
int batchSize = queue.size();
73+
List<OtlpLogRecord> batch = new ArrayList<>(batchSize);
74+
for (int i = 0; i < batchSize; i++) {
75+
OtlpLogRecord logRecord = queue.poll();
76+
if (logRecord != null) {
77+
batch.add(logRecord);
78+
} else {
79+
break;
80+
}
81+
}
82+
batch.sort(BY_SCOPE);
83+
return batch;
84+
}
85+
}

dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/common/OtlpAttributeVisitor.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
/** A visitor to visit OpenTelemetry attributes. */
44
public interface OtlpAttributeVisitor {
55

6-
int STRING = 0; // AttributeType.STRING
7-
int BOOLEAN = 1; // AttributeType.BOOLEAN
8-
int LONG = 2; // AttributeType.LONG
9-
int DOUBLE = 3; // AttributeType.DOUBLE
10-
int STRING_ARRAY = 4; // AttributeType.STRING_ARRAY
11-
int BOOLEAN_ARRAY = 5; // AttributeType.BOOLEAN_ARRAY
12-
int LONG_ARRAY = 6; // AttributeType.LONG_ARRAY
13-
int DOUBLE_ARRAY = 7; // AttributeType.DOUBLE_ARRAY
6+
int STRING_ATTRIBUTE = 0; // AttributeType.STRING
7+
int BOOLEAN_ATTRIBUTE = 1; // AttributeType.BOOLEAN
8+
int LONG_ATTRIBUTE = 2; // AttributeType.LONG
9+
int DOUBLE_ATTRIBUTE = 3; // AttributeType.DOUBLE
10+
int STRING_ARRAY_ATTRIBUTE = 4; // AttributeType.STRING_ARRAY
11+
int BOOLEAN_ARRAY_ATTRIBUTE = 5; // AttributeType.BOOLEAN_ARRAY
12+
int LONG_ARRAY_ATTRIBUTE = 6; // AttributeType.LONG_ARRAY
13+
int DOUBLE_ARRAY_ATTRIBUTE = 7; // AttributeType.DOUBLE_ARRAY
1414

1515
/**
1616
* Visits an attribute.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package datadog.trace.bootstrap.otlp.logs;
2+
3+
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
4+
import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope;
5+
import java.util.Map;
6+
import javax.annotation.Nullable;
7+
8+
public final class OtlpLogRecord {
9+
10+
public static final int STRING_BODY = 0; // ValueType.STRING
11+
public static final int BOOLEAN_BODY = 1; // ValueType.BOOLEAN
12+
public static final int LONG_BODY = 2; // ValueType.LONG
13+
public static final int DOUBLE_BODY = 3; // ValueType.DOUBLE
14+
public static final int ARRAY_BODY = 4; // ValueType.ARRAY
15+
public static final int KEY_VALUE_LIST_BODY = 5; // ValueType.KEY_VALUE_LIST
16+
public static final int BYTES_BODY = 6; // ValueType.BYTES
17+
18+
public final OtelInstrumentationScope instrumentationScope;
19+
20+
public final long timestampNanos;
21+
public final long observedNanos;
22+
public final int severityNumber;
23+
@Nullable public final String severityText;
24+
public final int bodyType;
25+
@Nullable public final Object bodyValue;
26+
@Nullable public final String eventName;
27+
@Nullable public final Map<?, ?> attributes;
28+
@Nullable public final AgentSpanContext spanContext;
29+
30+
public OtlpLogRecord(
31+
OtelInstrumentationScope instrumentationScope,
32+
long timestampNanos,
33+
long observedNanos,
34+
int severityNumber,
35+
@Nullable String severityText,
36+
int bodyType,
37+
@Nullable Object bodyValue,
38+
@Nullable String eventName,
39+
@Nullable Map<?, ?> attributes,
40+
@Nullable AgentSpanContext spanContext) {
41+
this.instrumentationScope = instrumentationScope;
42+
this.timestampNanos = timestampNanos;
43+
this.observedNanos = observedNanos;
44+
this.severityNumber = severityNumber;
45+
this.severityText = severityText;
46+
this.bodyType = bodyType;
47+
this.bodyValue = bodyValue;
48+
this.eventName = eventName;
49+
this.attributes = attributes;
50+
this.spanContext = spanContext;
51+
}
52+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package datadog.trace.bootstrap.otlp.logs;
2+
3+
import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope;
4+
5+
/** A visitor to visit OpenTelemetry logs. */
6+
public interface OtlpLogsVisitor {
7+
/** Visits logs produced by an instrumentation scope. */
8+
OtlpScopedLogsVisitor visitScopedLogs(OtelInstrumentationScope scope);
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package datadog.trace.bootstrap.otlp.logs;
2+
3+
import datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor;
4+
5+
/** A visitor to visit log records produced by an instrumentation scope. */
6+
public interface OtlpScopedLogsVisitor extends OtlpAttributeVisitor {
7+
8+
/** Visits an attribute of the upcoming log record. */
9+
void visitAttribute(int type, String key, Object value);
10+
11+
/** Visits a log record. */
12+
void visitLogRecord(OtlpLogRecord logRecord);
13+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package datadog.opentelemetry.shim.logs;
2+
3+
import static datadog.opentelemetry.shim.trace.OtelExtractedContext.extract;
4+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
5+
6+
import datadog.trace.api.time.SystemTimeSource;
7+
import datadog.trace.api.time.TimeSource;
8+
import datadog.trace.bootstrap.otel.logs.data.OtelLogRecordProcessor;
9+
import datadog.trace.bootstrap.otlp.logs.OtlpLogRecord;
10+
import io.opentelemetry.api.common.AttributeKey;
11+
import io.opentelemetry.api.common.Value;
12+
import io.opentelemetry.api.logs.LogRecordBuilder;
13+
import io.opentelemetry.api.logs.Severity;
14+
import io.opentelemetry.context.Context;
15+
import java.time.Instant;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import java.util.concurrent.TimeUnit;
19+
import javax.annotation.Nullable;
20+
import javax.annotation.ParametersAreNonnullByDefault;
21+
22+
@ParametersAreNonnullByDefault
23+
final class OtelLogRecordBuilder implements LogRecordBuilder {
24+
// package-visible for testing
25+
static TimeSource TIME_SOURCE = SystemTimeSource.INSTANCE;
26+
27+
private static final AttributeKey<String> EXCEPTION_TYPE_KEY = stringKey("exception.type");
28+
private static final AttributeKey<String> EXCEPTION_MESSAGE_KEY = stringKey("exception.message");
29+
30+
private final OtelLogger logger;
31+
32+
private long timestampNanos;
33+
private long observedNanos;
34+
private Severity severity = Severity.UNDEFINED_SEVERITY_NUMBER;
35+
@Nullable private String severityText;
36+
private int bodyType;
37+
@Nullable private Object bodyValue;
38+
@Nullable private String eventName;
39+
@Nullable private Map<AttributeKey<?>, Object> attributes;
40+
@Nullable private Context context;
41+
42+
OtelLogRecordBuilder(OtelLogger logger) {
43+
this.logger = logger;
44+
}
45+
46+
@Override
47+
public LogRecordBuilder setTimestamp(long timestamp, TimeUnit unit) {
48+
this.timestampNanos = unit.toNanos(timestamp);
49+
return this;
50+
}
51+
52+
@Override
53+
public LogRecordBuilder setTimestamp(Instant instant) {
54+
this.timestampNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano();
55+
return this;
56+
}
57+
58+
@Override
59+
public LogRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) {
60+
this.observedNanos = unit.toNanos(timestamp);
61+
return this;
62+
}
63+
64+
@Override
65+
public LogRecordBuilder setObservedTimestamp(Instant instant) {
66+
this.observedNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano();
67+
return this;
68+
}
69+
70+
@Override
71+
public LogRecordBuilder setSeverity(Severity severity) {
72+
this.severity = severity;
73+
return this;
74+
}
75+
76+
@Override
77+
public LogRecordBuilder setSeverityText(String severityText) {
78+
this.severityText = severityText;
79+
return this;
80+
}
81+
82+
@Override
83+
public LogRecordBuilder setBody(String value) {
84+
this.bodyType = 1;
85+
this.bodyValue = value;
86+
return this;
87+
}
88+
89+
@Override
90+
public LogRecordBuilder setBody(Value<?> body) {
91+
this.bodyType = body.getType().ordinal();
92+
this.bodyValue = body.getValue();
93+
return this;
94+
}
95+
96+
@Override
97+
public <T> LogRecordBuilder setAttribute(@Nullable AttributeKey<T> key, @Nullable T value) {
98+
if (key == null || key.getKey().isEmpty()) {
99+
return this;
100+
}
101+
if (value != null) {
102+
if (attributes == null) {
103+
attributes = new HashMap<>();
104+
}
105+
attributes.put(key, value);
106+
} else if (attributes != null) {
107+
attributes.remove(key);
108+
}
109+
return this;
110+
}
111+
112+
@Override
113+
public LogRecordBuilder setContext(Context context) {
114+
this.context = context;
115+
return this;
116+
}
117+
118+
public LogRecordBuilder setEventName(String eventName) {
119+
this.eventName = eventName;
120+
return this;
121+
}
122+
123+
public LogRecordBuilder setException(@Nullable Throwable throwable) {
124+
if (throwable != null) {
125+
setExceptionAttribute(EXCEPTION_TYPE_KEY, throwable.getClass().getName());
126+
setExceptionAttribute(EXCEPTION_MESSAGE_KEY, throwable.getMessage());
127+
}
128+
return this;
129+
}
130+
131+
private void setExceptionAttribute(AttributeKey<String> key, @Nullable String value) {
132+
// avoid overwriting/removing existing exception details
133+
if (value != null && (attributes == null || !attributes.containsKey(key))) {
134+
setAttribute(key, value);
135+
}
136+
}
137+
138+
@Override
139+
public void emit() {
140+
Context context = this.context != null ? this.context : Context.current();
141+
if (logger.isEnabled(severity, context)) {
142+
OtelLogRecordProcessor.INSTANCE.addLog(
143+
new OtlpLogRecord(
144+
logger.instrumentationScope,
145+
timestampNanos,
146+
observedNanos != 0 ? observedNanos : TIME_SOURCE.getCurrentTimeNanos(),
147+
severity.getSeverityNumber(),
148+
severityText,
149+
bodyType,
150+
bodyValue,
151+
eventName,
152+
attributes != null ? new HashMap<>(attributes) : null,
153+
extract(context)));
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)