From b15ea597e47555394d79f62ef1cd03e9fea704df Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Mon, 27 Apr 2026 00:15:30 +0100 Subject: [PATCH 1/7] Implement OpenTelemetry Logs API --- .../otel/common/OtelInstrumentationScope.java | 30 ++- .../logs/data/OtelLogRecordProcessor.java | 85 ++++++++ .../otlp/common/OtlpAttributeVisitor.java | 16 +- .../bootstrap/otlp/logs/OtlpLogRecord.java | 52 +++++ .../bootstrap/otlp/logs/OtlpLogsVisitor.java | 9 + .../otlp/logs/OtlpScopedLogsVisitor.java | 13 ++ .../shim/logs/OtelLogRecordBuilder.java | 156 ++++++++++++++ .../opentelemetry/shim/logs/OtelLogger.java | 31 +++ .../shim/logs/OtelLoggerBuilder.java | 38 ++++ .../shim/logs/OtelLoggerProvider.java | 59 ++++++ .../opentelemetry-1.27/build.gradle | 26 +++ .../OpenTelemetryLogsInstrumentation.java | 88 ++++++++ ...vationByInstrumentationNameForkedTest.java | 13 ++ ...y127ActivationByOtelRfcNameForkedTest.java | 12 ++ .../java/OpenTelemetry127ActivationTest.java | 25 +++ ...elemetry127DisableByDefaultForkedTest.java | 10 + .../java/opentelemetry127/logs/LogsTest.java | 199 ++++++++++++++++++ .../datadog/trace/api/ConfigDefaults.java | 4 + .../datadog/trace/api/config/OtlpConfig.java | 9 + .../core/otlp/common/OtlpCommonProto.java | 32 +-- .../core/otlp/common/OtlpResourceProto.java | 4 +- .../trace/core/otlp/trace/OtlpTraceProto.java | 18 +- .../core/otlp/common/OtlpCommonProtoTest.java | 44 ++-- .../otlp/metrics/OtlpMetricsProtoTest.java | 16 +- .../main/java/datadog/trace/api/Config.java | 91 +++++++- .../datadog/trace/api/InstrumenterConfig.java | 10 + .../OtelEnvMetricCollectorImplTest.groovy | 4 +- metadata/supported-configurations.json | 112 ++++++++++ settings.gradle.kts | 1 + .../provider/OtelEnvironmentConfigSource.java | 48 ++++- 30 files changed, 1186 insertions(+), 69 deletions(-) create mode 100644 dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java create mode 100644 dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java create mode 100644 dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogsVisitor.java create mode 100644 dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpScopedLogsVisitor.java create mode 100644 dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java create mode 100644 dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogger.java create mode 100644 dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerBuilder.java create mode 100644 dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerProvider.java create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByInstrumentationNameForkedTest.java create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByOtelRfcNameForkedTest.java create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationTest.java create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127DisableByDefaultForkedTest.java create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/LogsTest.java diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/OtelInstrumentationScope.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/OtelInstrumentationScope.java index a1b5e238f22..28e912d8a54 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/OtelInstrumentationScope.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/OtelInstrumentationScope.java @@ -5,7 +5,7 @@ import javax.annotation.Nullable; /** Instrumentation scopes have a mandatory name, optional version, and optional schema URL. */ -public final class OtelInstrumentationScope { +public final class OtelInstrumentationScope implements Comparable { private final UTF8BytesString scopeName; @Nullable private final UTF8BytesString scopeVersion; @@ -32,6 +32,34 @@ public UTF8BytesString getSchemaUrl() { return schemaUrl; } + @Override + public int compareTo(OtelInstrumentationScope that) { + int cmp = scopeName.toString().compareTo(that.scopeName.toString()); + if (cmp != 0) { + return cmp; + } + if (scopeVersion != that.scopeVersion) { + if (scopeVersion == null) { + return -1; + } else if (that.scopeVersion == null) { + return 1; + } + cmp = scopeVersion.toString().compareTo(that.scopeVersion.toString()); + if (cmp != 0) { + return cmp; + } + } + if (schemaUrl != that.schemaUrl) { + if (schemaUrl == null) { + return -1; + } else if (that.schemaUrl == null) { + return 1; + } + return schemaUrl.toString().compareTo(that.schemaUrl.toString()); + } + return 0; + } + @Override public boolean equals(Object o) { if (!(o instanceof OtelInstrumentationScope)) { diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java new file mode 100644 index 00000000000..93ce59392c0 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java @@ -0,0 +1,85 @@ +package datadog.trace.bootstrap.otel.logs.data; + +import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; +import datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor; +import datadog.trace.bootstrap.otlp.logs.OtlpLogRecord; +import datadog.trace.bootstrap.otlp.logs.OtlpLogsVisitor; +import datadog.trace.bootstrap.otlp.logs.OtlpScopedLogsVisitor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.WeakHashMap; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.function.BiConsumer; + +/** Processes log records, grouping them by instrumentation scope. */ +public final class OtelLogRecordProcessor { + public static final OtelLogRecordProcessor INSTANCE = new OtelLogRecordProcessor(); + + private static final Comparator BY_SCOPE = + Comparator.comparing(o -> o.instrumentationScope); + + private static final Map, OtlpAttributeVisitor>> + ATTRIBUTE_READERS = Collections.synchronizedMap(new WeakHashMap<>()); + + private final Queue queue = new ArrayBlockingQueue<>(2048); + + public void addLog(OtlpLogRecord logRecord) { + queue.offer(logRecord); + } + + public void collectLogs(OtlpLogsVisitor visitor) { + OtlpScopedLogsVisitor scopedVisitor = null; + OtelInstrumentationScope currentScope = null; + BiConsumer, OtlpAttributeVisitor> attributesReader = null; + ClassLoader attributesClassLoader = null; + for (OtlpLogRecord logRecord : batchByScope()) { + if (logRecord.instrumentationScope != currentScope) { + currentScope = logRecord.instrumentationScope; + scopedVisitor = visitor.visitScopedLogs(currentScope); + } + Map attributes = logRecord.attributes; + if (attributes != null && !attributes.isEmpty()) { + ClassLoader cl = getAttributesClassLoader(attributes); + // avoid repeated lookups when attribute class-loader is same for all records + if (attributesReader == null || !Objects.equals(cl, attributesClassLoader)) { + attributesReader = ATTRIBUTE_READERS.get(cl); + attributesClassLoader = cl; + } + if (attributesReader != null) { + attributesReader.accept(attributes, scopedVisitor); + } + } + scopedVisitor.visitLogRecord(logRecord); + } + } + + private static ClassLoader getAttributesClassLoader(Map attributes) { + // need to peek at the first key, as the map will be a JDK collection type + return attributes.keySet().iterator().next().getClass().getClassLoader(); + } + + public static void registerAttributeReader( + ClassLoader cl, BiConsumer, OtlpAttributeVisitor> reader) { + ATTRIBUTE_READERS.put(cl, reader); + } + + private List batchByScope() { + int batchSize = queue.size(); + List batch = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + OtlpLogRecord logRecord = queue.poll(); + if (logRecord != null) { + batch.add(logRecord); + } else { + break; + } + } + batch.sort(BY_SCOPE); + return batch; + } +} diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/common/OtlpAttributeVisitor.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/common/OtlpAttributeVisitor.java index c1984784f20..886ad3ac122 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/common/OtlpAttributeVisitor.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/common/OtlpAttributeVisitor.java @@ -3,14 +3,14 @@ /** A visitor to visit OpenTelemetry attributes. */ public interface OtlpAttributeVisitor { - int STRING = 0; // AttributeType.STRING - int BOOLEAN = 1; // AttributeType.BOOLEAN - int LONG = 2; // AttributeType.LONG - int DOUBLE = 3; // AttributeType.DOUBLE - int STRING_ARRAY = 4; // AttributeType.STRING_ARRAY - int BOOLEAN_ARRAY = 5; // AttributeType.BOOLEAN_ARRAY - int LONG_ARRAY = 6; // AttributeType.LONG_ARRAY - int DOUBLE_ARRAY = 7; // AttributeType.DOUBLE_ARRAY + int STRING_ATTRIBUTE = 0; // AttributeType.STRING + int BOOLEAN_ATTRIBUTE = 1; // AttributeType.BOOLEAN + int LONG_ATTRIBUTE = 2; // AttributeType.LONG + int DOUBLE_ATTRIBUTE = 3; // AttributeType.DOUBLE + int STRING_ARRAY_ATTRIBUTE = 4; // AttributeType.STRING_ARRAY + int BOOLEAN_ARRAY_ATTRIBUTE = 5; // AttributeType.BOOLEAN_ARRAY + int LONG_ARRAY_ATTRIBUTE = 6; // AttributeType.LONG_ARRAY + int DOUBLE_ARRAY_ATTRIBUTE = 7; // AttributeType.DOUBLE_ARRAY /** * Visits an attribute. diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java new file mode 100644 index 00000000000..1711999f52a --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java @@ -0,0 +1,52 @@ +package datadog.trace.bootstrap.otlp.logs; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; +import java.util.Map; +import javax.annotation.Nullable; + +public final class OtlpLogRecord { + + public static final int STRING_BODY = 0; // ValueType.STRING + public static final int BOOLEAN_BODY = 1; // ValueType.BOOLEAN + public static final int LONG_BODY = 2; // ValueType.LONG + public static final int DOUBLE_BODY = 3; // ValueType.DOUBLE + public static final int ARRAY_BODY = 4; // ValueType.ARRAY + public static final int KEY_VALUE_LIST_BODY = 5; // ValueType.KEY_VALUE_LIST + public static final int BYTES_BODY = 6; // ValueType.BYTES + + public final OtelInstrumentationScope instrumentationScope; + + public final long timestampNanos; + public final long observedNanos; + public final int severityNumber; + @Nullable public final String severityText; + public final int bodyType; + @Nullable public final Object bodyValue; + @Nullable public final String eventName; + @Nullable public final Map attributes; + @Nullable public final AgentSpanContext spanContext; + + public OtlpLogRecord( + OtelInstrumentationScope instrumentationScope, + long timestampNanos, + long observedNanos, + int severityNumber, + @Nullable String severityText, + int bodyType, + @Nullable Object bodyValue, + @Nullable String eventName, + @Nullable Map attributes, + @Nullable AgentSpanContext spanContext) { + this.instrumentationScope = instrumentationScope; + this.timestampNanos = timestampNanos; + this.observedNanos = observedNanos; + this.severityNumber = severityNumber; + this.severityText = severityText; + this.bodyType = bodyType; + this.bodyValue = bodyValue; + this.eventName = eventName; + this.attributes = attributes; + this.spanContext = spanContext; + } +} diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogsVisitor.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogsVisitor.java new file mode 100644 index 00000000000..e77f05d936a --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogsVisitor.java @@ -0,0 +1,9 @@ +package datadog.trace.bootstrap.otlp.logs; + +import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; + +/** A visitor to visit OpenTelemetry logs. */ +public interface OtlpLogsVisitor { + /** Visits logs produced by an instrumentation scope. */ + OtlpScopedLogsVisitor visitScopedLogs(OtelInstrumentationScope scope); +} diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpScopedLogsVisitor.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpScopedLogsVisitor.java new file mode 100644 index 00000000000..d03a5e4e4ac --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpScopedLogsVisitor.java @@ -0,0 +1,13 @@ +package datadog.trace.bootstrap.otlp.logs; + +import datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor; + +/** A visitor to visit log records produced by an instrumentation scope. */ +public interface OtlpScopedLogsVisitor extends OtlpAttributeVisitor { + + /** Visits an attribute of the upcoming log record. */ + void visitAttribute(int type, String key, Object value); + + /** Visits a log record. */ + void visitLogRecord(OtlpLogRecord logRecord); +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java new file mode 100644 index 00000000000..cea11510084 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java @@ -0,0 +1,156 @@ +package datadog.opentelemetry.shim.logs; + +import static datadog.opentelemetry.shim.trace.OtelExtractedContext.extract; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import datadog.trace.api.time.SystemTimeSource; +import datadog.trace.api.time.TimeSource; +import datadog.trace.bootstrap.otel.logs.data.OtelLogRecordProcessor; +import datadog.trace.bootstrap.otlp.logs.OtlpLogRecord; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.context.Context; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +final class OtelLogRecordBuilder implements LogRecordBuilder { + // package-visible for testing + static TimeSource TIME_SOURCE = SystemTimeSource.INSTANCE; + + private static final AttributeKey EXCEPTION_TYPE_KEY = stringKey("exception.type"); + private static final AttributeKey EXCEPTION_MESSAGE_KEY = stringKey("exception.message"); + + private final OtelLogger logger; + + private long timestampNanos; + private long observedNanos; + private Severity severity = Severity.UNDEFINED_SEVERITY_NUMBER; + @Nullable private String severityText; + private int bodyType; + @Nullable private Object bodyValue; + @Nullable private String eventName; + @Nullable private Map, Object> attributes; + @Nullable private Context context; + + OtelLogRecordBuilder(OtelLogger logger) { + this.logger = logger; + } + + @Override + public LogRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { + this.timestampNanos = unit.toNanos(timestamp); + return this; + } + + @Override + public LogRecordBuilder setTimestamp(Instant instant) { + this.timestampNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano(); + return this; + } + + @Override + public LogRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { + this.observedNanos = unit.toNanos(timestamp); + return this; + } + + @Override + public LogRecordBuilder setObservedTimestamp(Instant instant) { + this.observedNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano(); + return this; + } + + @Override + public LogRecordBuilder setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + @Override + public LogRecordBuilder setSeverityText(String severityText) { + this.severityText = severityText; + return this; + } + + @Override + public LogRecordBuilder setBody(String value) { + this.bodyType = 1; + this.bodyValue = value; + return this; + } + + @Override + public LogRecordBuilder setBody(Value body) { + this.bodyType = body.getType().ordinal(); + this.bodyValue = body.getValue(); + return this; + } + + @Override + public LogRecordBuilder setAttribute(@Nullable AttributeKey key, @Nullable T value) { + if (key == null || key.getKey().isEmpty()) { + return this; + } + if (value != null) { + if (attributes == null) { + attributes = new HashMap<>(); + } + attributes.put(key, value); + } else if (attributes != null) { + attributes.remove(key); + } + return this; + } + + @Override + public LogRecordBuilder setContext(Context context) { + this.context = context; + return this; + } + + public LogRecordBuilder setEventName(String eventName) { + this.eventName = eventName; + return this; + } + + public LogRecordBuilder setException(@Nullable Throwable throwable) { + if (throwable != null) { + setExceptionAttribute(EXCEPTION_TYPE_KEY, throwable.getClass().getName()); + setExceptionAttribute(EXCEPTION_MESSAGE_KEY, throwable.getMessage()); + } + return this; + } + + private void setExceptionAttribute(AttributeKey key, @Nullable String value) { + // avoid overwriting/removing existing exception details + if (value != null && (attributes == null || !attributes.containsKey(key))) { + setAttribute(key, value); + } + } + + @Override + public void emit() { + Context context = this.context != null ? this.context : Context.current(); + if (logger.isEnabled(severity, context)) { + OtelLogRecordProcessor.INSTANCE.addLog( + new OtlpLogRecord( + logger.instrumentationScope, + timestampNanos, + observedNanos != 0 ? observedNanos : TIME_SOURCE.getCurrentTimeNanos(), + severity.getSeverityNumber(), + severityText, + bodyType, + bodyValue, + eventName, + attributes != null ? new HashMap<>(attributes) : null, + extract(context))); + } + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogger.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogger.java new file mode 100644 index 00000000000..bd11a07f9f0 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogger.java @@ -0,0 +1,31 @@ +package datadog.opentelemetry.shim.logs; + +import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; +import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.context.Context; +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +final class OtelLogger implements Logger { + final OtelInstrumentationScope instrumentationScope; + + OtelLogger(OtelInstrumentationScope instrumentationScope) { + this.instrumentationScope = instrumentationScope; + } + + @Override + public LogRecordBuilder logRecordBuilder() { + return new OtelLogRecordBuilder(this); + } + + public boolean isEnabled(Severity severity, Context context) { + return true; + } + + @Override + public String toString() { + return "OtelLogger{instrumentationScope=" + instrumentationScope + "}"; + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerBuilder.java new file mode 100644 index 00000000000..b0b9da1f90e --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerBuilder.java @@ -0,0 +1,38 @@ +package datadog.opentelemetry.shim.logs; + +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerBuilder; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +final class OtelLoggerBuilder implements LoggerBuilder { + private final OtelLoggerProvider loggerProvider; + + private final String instrumentationScopeName; + @Nullable private String instrumentationScopeVersion; + @Nullable private String schemaUrl; + + OtelLoggerBuilder(OtelLoggerProvider loggerProvider, String instrumentationScopeName) { + this.loggerProvider = loggerProvider; + this.instrumentationScopeName = instrumentationScopeName; + } + + @Override + public LoggerBuilder setInstrumentationVersion(String instrumentationScopeVersion) { + this.instrumentationScopeVersion = instrumentationScopeVersion; + return this; + } + + @Override + public LoggerBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public Logger build() { + return loggerProvider.getLoggerShim( + instrumentationScopeName, instrumentationScopeVersion, schemaUrl); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerProvider.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerProvider.java new file mode 100644 index 00000000000..45fc6924353 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLoggerProvider.java @@ -0,0 +1,59 @@ +package datadog.opentelemetry.shim.logs; + +import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; +import datadog.trace.bootstrap.otel.logs.data.OtelLogRecordProcessor; +import datadog.trace.util.Strings; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerBuilder; +import io.opentelemetry.api.logs.LoggerProvider; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import org.slf4j.LoggerFactory; + +@ParametersAreNonnullByDefault +public final class OtelLoggerProvider implements LoggerProvider { + private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(OtelLoggerProvider.class); + private static final String DEFAULT_LOGGER_NAME = "unknown"; + + public static final LoggerProvider INSTANCE = new OtelLoggerProvider(); + + /** Logger shims, indexed by instrumentation scope. */ + private final Map loggers = new ConcurrentHashMap<>(); + + @SuppressWarnings("unchecked") + private OtelLoggerProvider() { + // register attribute reader for class-loader where this provider is being used/injected + OtelLogRecordProcessor.registerAttributeReader( + AttributeKey.class.getClassLoader(), + (attributes, visitor) -> + ((Map, ?>) attributes) + .forEach((a, v) -> visitor.visitAttribute(a.getType().ordinal(), a.getKey(), v))); + } + + @Override + public Logger get(String instrumentationScopeName) { + return getLoggerShim(instrumentationScopeName, null, null); + } + + @Override + public LoggerBuilder loggerBuilder(String instrumentationScopeName) { + return new OtelLoggerBuilder(this, instrumentationScopeName); + } + + Logger getLoggerShim( + String instrumentationScopeName, + @Nullable String instrumentationScopeVersion, + @Nullable String schemaUrl) { + if (Strings.isBlank(instrumentationScopeName)) { + LOGGER.debug("Logger requested without instrumentation scope name."); + instrumentationScopeName = DEFAULT_LOGGER_NAME; + } + return loggers.computeIfAbsent( + new OtelInstrumentationScope( + instrumentationScopeName, instrumentationScopeVersion, schemaUrl), + OtelLogger::new); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle new file mode 100644 index 00000000000..2c6bfeebf33 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle @@ -0,0 +1,26 @@ +def openTelemetryVersion = '1.27.0' + +muzzle { + pass { + module = 'opentelemetry-api' + group = 'io.opentelemetry' + versions = "[$openTelemetryVersion,)" + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + compileOnly group: 'io.opentelemetry', name: 'opentelemetry-api', version: openTelemetryVersion + compileOnly group: 'com.google.auto.value', name: 'auto-value-annotations', version: '1.6.6' + + implementation project(':dd-java-agent:agent-otel:otel-shim') + + muzzleBootstrap project(path: ':dd-java-agent:agent-otel:otel-bootstrap', configuration: 'shadow') + testImplementation project(path: ':dd-java-agent:agent-otel:otel-bootstrap', configuration: 'shadow') + + testImplementation group: 'io.opentelemetry', name: 'opentelemetry-api', version: openTelemetryVersion + latestDepTestImplementation group: 'io.opentelemetry', name: 'opentelemetry-api', version: '1+' +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java new file mode 100644 index 00000000000..be21319cf51 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java @@ -0,0 +1,88 @@ +package datadog.trace.instrumentation.opentelemetry127; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +import com.google.auto.service.AutoService; +import datadog.opentelemetry.shim.logs.OtelLoggerProvider; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.InstrumenterConfig; +import io.opentelemetry.api.logs.LoggerProvider; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Provides our logs implementations to OpenTelemetry clients. + * + *

Note that the minimum version for Datadog support of the OpenTelemetry logs API is 1.27. + * Tracing support is handled by a separate instrumentation under the 'opentelemetry-1.4' module. + */ +@AutoService(InstrumenterModule.class) +public class OpenTelemetryLogsInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.CanShortcutTypeMatching, Instrumenter.HasMethodAdvice { + + public OpenTelemetryLogsInstrumentation() { + super("opentelemetry-logs", "opentelemetry-1.27"); + } + + @Override + protected boolean defaultEnabled() { + return InstrumenterConfig.get().isLogsOtelEnabled(); + } + + @Override + public String hierarchyMarkerType() { + return "io.opentelemetry.api.OpenTelemetry"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return named(hierarchyMarkerType()).or(implementsInterface(named(hierarchyMarkerType()))); + } + + @Override + public String[] knownMatchingTypes() { + return new String[] { + "io.opentelemetry.api.DefaultOpenTelemetry", + "io.opentelemetry.api.GlobalOpenTelemetry$ObfuscatedOpenTelemetry" + }; + } + + @Override + public boolean onlyMatchKnownTypes() { + return isShortcutMatchingEnabled(false); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.opentelemetry.shim.logs.OtelLogger", + "datadog.opentelemetry.shim.logs.OtelLoggerBuilder", + "datadog.opentelemetry.shim.logs.OtelLoggerProvider", + "datadog.opentelemetry.shim.logs.OtelLogRecordBuilder", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + // LoggerProvider OpenTelemetry.getLogsBridge() + transformer.applyAdvice( + isMethod() + .and(named("getLogsBridge")) + .and(takesNoArguments()) + .and(returns(named("io.opentelemetry.api.logs.LoggerProvider"))), + OpenTelemetryLogsInstrumentation.class.getName() + "$LoggerProviderAdvice"); + } + + public static class LoggerProviderAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void returnProvider(@Advice.Return(readOnly = false) LoggerProvider result) { + result = OtelLoggerProvider.INSTANCE; + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByInstrumentationNameForkedTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByInstrumentationNameForkedTest.java new file mode 100644 index 00000000000..bcc39764540 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByInstrumentationNameForkedTest.java @@ -0,0 +1,13 @@ +import datadog.trace.junit.utils.config.WithConfig; + +// Forked test: runs in an isolated JVM with opentelemetry-logs instrumentation enabled +// by integration name. GlobalOpenTelemetry holds static state that must reset between variants. +@WithConfig(key = "integration.opentelemetry-logs.enabled", value = "true") +class OpenTelemetry127ActivationByInstrumentationNameForkedTest + extends OpenTelemetry127ActivationTest { + + @Override + boolean shouldBeInjected() { + return true; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByOtelRfcNameForkedTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByOtelRfcNameForkedTest.java new file mode 100644 index 00000000000..3e475214a79 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByOtelRfcNameForkedTest.java @@ -0,0 +1,12 @@ +import datadog.trace.junit.utils.config.WithConfig; + +// Forked test: runs in an isolated JVM with opentelemetry-logs instrumentation enabled +// by OTel RFC config name. GlobalOpenTelemetry holds static state that must reset between variants. +@WithConfig(key = "logs.otel.enabled", value = "true") +class OpenTelemetry127ActivationByOtelRfcNameForkedTest extends OpenTelemetry127ActivationTest { + + @Override + boolean shouldBeInjected() { + return true; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationTest.java new file mode 100644 index 00000000000..9eb9f54380f --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationTest.java @@ -0,0 +1,25 @@ +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.logs.Logger; +import org.junit.jupiter.api.Test; + +abstract class OpenTelemetry127ActivationTest extends AbstractInstrumentationTest { + + abstract boolean shouldBeInjected(); + + @Test + void testInstrumentationInjection() { + Logger logger = GlobalOpenTelemetry.get().getLogsBridge().get("some-instrumentation"); + if (shouldBeInjected()) { + assertTrue( + logger.getClass().getName().endsWith(".OtelLogger"), + "Expected OtelLogger but got: " + logger.getClass().getName()); + } else { + assertTrue( + logger.getClass().getName().endsWith(".DefaultLogger"), + "Expected DefaultLogger but got: " + logger.getClass().getName()); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127DisableByDefaultForkedTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127DisableByDefaultForkedTest.java new file mode 100644 index 00000000000..ead0a1f166a --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127DisableByDefaultForkedTest.java @@ -0,0 +1,10 @@ +// Forked test: runs in an isolated JVM with no opentelemetry-logs config override, +// verifying that the instrumentation is disabled by default. +// GlobalOpenTelemetry holds static state that must reset between variants. +class OpenTelemetry127DisableByDefaultForkedTest extends OpenTelemetry127ActivationTest { + + @Override + boolean shouldBeInjected() { + return false; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/LogsTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/LogsTest.java new file mode 100644 index 00000000000..d57e4095cc8 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/LogsTest.java @@ -0,0 +1,199 @@ +package opentelemetry127.logs; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; +import datadog.trace.bootstrap.otel.logs.data.OtelLogRecordProcessor; +import datadog.trace.bootstrap.otlp.logs.OtlpLogRecord; +import datadog.trace.bootstrap.otlp.logs.OtlpLogsVisitor; +import datadog.trace.bootstrap.otlp.logs.OtlpScopedLogsVisitor; +import datadog.trace.junit.utils.config.WithConfig; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerProvider; +import io.opentelemetry.api.logs.Severity; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@WithConfig(key = "logs.otel.enabled", value = "true") +class LogsTest extends AbstractInstrumentationTest { + + private final LogsReader logsReader = new LogsReader(); + + @BeforeEach + void drainQueue() { + // drain any stale log records from the shared processor queue before each test + OtelLogRecordProcessor.INSTANCE.collectLogs(LogsDrainer.INSTANCE); + } + + @ParameterizedTest + @EnumSource( + value = Severity.class, + names = {"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"}) + void testSeverity(Severity severity) { + LoggerProvider loggerProvider = GlobalOpenTelemetry.get().getLogsBridge(); + Logger logger = loggerProvider.get("test-severity"); + + logger.logRecordBuilder().setBody("test message").setSeverity(severity).emit(); + + OtelLogRecordProcessor.INSTANCE.collectLogs(logsReader); + + assertEquals(1, logsReader.logs.size()); + CapturedLog log = logsReader.logs.get(0); + assertEquals("test-severity", log.scopeName); + assertEquals(severity.getSeverityNumber(), log.severityNumber); + assertEquals("test message", log.bodyValue); + } + + @Test + void testSeverityText() { + Logger logger = GlobalOpenTelemetry.get().getLogsBridge().get("test-severity-text"); + + logger + .logRecordBuilder() + .setBody("message") + .setSeverity(Severity.INFO) + .setSeverityText("custom-level") + .emit(); + + OtelLogRecordProcessor.INSTANCE.collectLogs(logsReader); + + assertEquals(1, logsReader.logs.size()); + CapturedLog log = logsReader.logs.get(0); + assertEquals(Severity.INFO.getSeverityNumber(), log.severityNumber); + assertEquals("custom-level", log.severityText); + } + + @Test + void testAttributes() { + Logger logger = GlobalOpenTelemetry.get().getLogsBridge().get("test-attributes"); + + logger + .logRecordBuilder() + .setBody("attributed message") + .setAttribute(stringKey("str.key"), "str-value") + .setAttribute(longKey("long.key"), 42L) + .setAttribute(booleanKey("bool.key"), true) + .setAttribute(doubleKey("double.key"), 1.5) + .emit(); + + OtelLogRecordProcessor.INSTANCE.collectLogs(logsReader); + + assertEquals(1, logsReader.logs.size()); + CapturedLog log = logsReader.logs.get(0); + assertEquals("test-attributes", log.scopeName); + assertEquals("attributed message", log.bodyValue); + assertEquals("str-value", log.attributes.get("str.key")); + assertEquals(42L, log.attributes.get("long.key")); + assertEquals(true, log.attributes.get("bool.key")); + assertEquals(1.5, log.attributes.get("double.key")); + } + + @Test + void testMultipleScopes() { + Logger loggerA = GlobalOpenTelemetry.get().getLogsBridge().get("scope-a"); + Logger loggerB = GlobalOpenTelemetry.get().getLogsBridge().get("scope-b"); + + loggerA.logRecordBuilder().setBody("a-1").setSeverity(Severity.INFO).emit(); + loggerB.logRecordBuilder().setBody("b-1").setSeverity(Severity.WARN).emit(); + loggerA.logRecordBuilder().setBody("a-2").setSeverity(Severity.DEBUG).emit(); + + OtelLogRecordProcessor.INSTANCE.collectLogs(logsReader); + + // logs are sorted by scope name, so all scope-a logs come before scope-b logs + assertEquals(3, logsReader.logs.size()); + + List scopeALogs = + logsReader.logs.stream() + .filter(l -> "scope-a".equals(l.scopeName)) + .collect(Collectors.toList()); + List scopeBLogs = + logsReader.logs.stream() + .filter(l -> "scope-b".equals(l.scopeName)) + .collect(Collectors.toList()); + + assertEquals(2, scopeALogs.size()); + assertEquals("a-1", scopeALogs.get(0).bodyValue); + assertEquals("a-2", scopeALogs.get(1).bodyValue); + + assertEquals(1, scopeBLogs.size()); + assertEquals("b-1", scopeBLogs.get(0).bodyValue); + } + + static class CapturedLog { + final String scopeName; + final int severityNumber; + final String severityText; + final Object bodyValue; + final Map attributes; + + CapturedLog( + String scopeName, + int severityNumber, + String severityText, + Object bodyValue, + Map attributes) { + this.scopeName = scopeName; + this.severityNumber = severityNumber; + this.severityText = severityText; + this.bodyValue = bodyValue; + this.attributes = attributes; + } + } + + static class LogsReader implements OtlpLogsVisitor, OtlpScopedLogsVisitor { + final List logs = new ArrayList<>(); + private String currentScopeName; + private final Map currentAttributes = new HashMap<>(); + + @Override + public OtlpScopedLogsVisitor visitScopedLogs(OtelInstrumentationScope scope) { + currentScopeName = scope.getName().toString(); + return this; + } + + @Override + public void visitAttribute(int type, String key, Object value) { + currentAttributes.put(key, value); + } + + @Override + public void visitLogRecord(OtlpLogRecord logRecord) { + logs.add( + new CapturedLog( + currentScopeName, + logRecord.severityNumber, + logRecord.severityText, + logRecord.bodyValue, + new HashMap<>(currentAttributes))); + currentAttributes.clear(); + } + } + + private static class LogsDrainer implements OtlpLogsVisitor { + public static final LogsDrainer INSTANCE = new LogsDrainer(); + + @Override + public OtlpScopedLogsVisitor visitScopedLogs(OtelInstrumentationScope scope) { + return new OtlpScopedLogsVisitor() { + @Override + public void visitAttribute(int type, String key, Object value) {} + + @Override + public void visitLogRecord(OtlpLogRecord record) {} + }; + } + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index fd6c384de21..199ef3bfcb3 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -101,6 +101,9 @@ public final class ConfigDefaults { static final boolean DEFAULT_JMX_FETCH_MULTIPLE_RUNTIME_SERVICES_ENABLED = false; static final int DEFAULT_JMX_FETCH_MULTIPLE_RUNTIME_SERVICES_LIMIT = 10; + public static final boolean DEFAULT_LOGS_OTEL_ENABLED = false; + static final int DEFAULT_OTLP_LOGS_TIMEOUT = 10_000; // ms + public static final boolean DEFAULT_METRICS_OTEL_ENABLED = false; // Default recommended by Datadog; it differs from Otel’s default of 60000 (60s) static final int DEFAULT_METRICS_OTEL_INTERVAL = 10_000; // ms @@ -110,6 +113,7 @@ public final class ConfigDefaults { static final int DEFAULT_OTLP_TRACES_TIMEOUT = 10_000; // ms + static final String DEFAULT_OTLP_HTTP_LOGS_ENDPOINT = "v1/logs"; static final String DEFAULT_OTLP_HTTP_METRICS_ENDPOINT = "v1/metrics"; static final String DEFAULT_OTLP_HTTP_TRACES_ENDPOINT = "v1/traces"; static final String DEFAULT_OTLP_HTTP_PORT = "4318"; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java index 4b24bfa5b1f..ae7a3fe9cc7 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java @@ -2,6 +2,15 @@ public final class OtlpConfig { + public static final String LOGS_OTEL_ENABLED = "logs.otel.enabled"; + public static final String LOGS_OTEL_EXPORTER = "logs.otel.exporter"; + + public static final String OTLP_LOGS_ENDPOINT = "otlp.logs.endpoint"; + public static final String OTLP_LOGS_HEADERS = "otlp.logs.headers"; + public static final String OTLP_LOGS_PROTOCOL = "otlp.logs.protocol"; + public static final String OTLP_LOGS_COMPRESSION = "otlp.logs.compression"; + public static final String OTLP_LOGS_TIMEOUT = "otlp.logs.timeout"; + public static final String METRICS_OTEL_ENABLED = "metrics.otel.enabled"; public static final String METRICS_OTEL_EXPORTER = "metrics.otel.exporter"; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java index 2aa6de29335..d771856d27f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java @@ -1,13 +1,13 @@ package datadog.trace.core.otlp.common; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ARRAY; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ARRAY; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ARRAY; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static java.nio.charset.StandardCharsets.UTF_8; import datadog.communication.serialization.GenerationalUtf8Cache; @@ -168,28 +168,28 @@ public static void writeAttribute(StreamingBuffer buf, int type, CharSequence ke keyUtf8 = keyUtf8(key.toString()); } switch (type) { - case STRING: + case STRING_ATTRIBUTE: writeStringAttribute(buf, keyUtf8, valueUtf8((String) value)); break; - case BOOLEAN: + case BOOLEAN_ATTRIBUTE: writeBooleanAttribute(buf, keyUtf8, (boolean) value); break; - case LONG: + case LONG_ATTRIBUTE: writeLongAttribute(buf, keyUtf8, ((Number) value).longValue()); break; - case DOUBLE: + case DOUBLE_ATTRIBUTE: writeDoubleAttribute(buf, keyUtf8, ((Number) value).doubleValue()); break; - case STRING_ARRAY: + case STRING_ARRAY_ATTRIBUTE: writeStringArrayAttribute(buf, keyUtf8, (List) value); break; - case BOOLEAN_ARRAY: + case BOOLEAN_ARRAY_ATTRIBUTE: writeBooleanArrayAttribute(buf, keyUtf8, (List) value); break; - case LONG_ARRAY: + case LONG_ARRAY_ATTRIBUTE: writeLongArrayAttribute(buf, keyUtf8, (List) value); break; - case DOUBLE_ARRAY: + case DOUBLE_ARRAY_ATTRIBUTE: writeDoubleArrayAttribute(buf, keyUtf8, (List) value); break; default: diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index 6dea535bf71..e45676d0721 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -1,7 +1,7 @@ package datadog.trace.core.otlp.common; import static datadog.communication.ddagent.TracerVersion.TRACER_VERSION; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static datadog.trace.core.otlp.common.OtlpCommonProto.LEN_WIRE_TYPE; import static datadog.trace.core.otlp.common.OtlpCommonProto.recordMessage; import static datadog.trace.core.otlp.common.OtlpCommonProto.writeAttribute; @@ -67,6 +67,6 @@ static byte[] buildResourceMessage(Config config) { private static void writeResourceAttribute(StreamingBuffer buf, String key, String value) { writeTag(buf, 1, LEN_WIRE_TYPE); - writeAttribute(buf, STRING, key, value); + writeAttribute(buf, STRING_ATTRIBUTE, key, value); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java index 67469ac1173..e6c12f1eb0a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java @@ -8,10 +8,10 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CONSUMER; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_PRODUCER; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static datadog.trace.common.writer.RemoteMapper.HTTP_STATUS; import static datadog.trace.common.writer.ddagent.TraceMapper.ORIGIN_KEY; import static datadog.trace.common.writer.ddagent.TraceMapper.PROCESS_TAGS_KEY; @@ -181,7 +181,7 @@ public static byte[] recordSpanLinkMessage(GrowableBuffer buf, AgentSpanLink spa .forEach( (key, value) -> { writeTag(buf, 4, LEN_WIRE_TYPE); - writeAttribute(buf, STRING, key, value); + writeAttribute(buf, STRING_ATTRIBUTE, key, value); }); writeTag(buf, 6, I32_WIRE_TYPE); @@ -205,18 +205,18 @@ private static void writeSpanTag(StreamingBuffer buf, TagMap.EntryReader tagEntr writeTag(buf, 9, LEN_WIRE_TYPE); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: - writeAttribute(buf, BOOLEAN, tagEntry.tag(), tagEntry.objectValue()); + writeAttribute(buf, BOOLEAN_ATTRIBUTE, tagEntry.tag(), tagEntry.objectValue()); break; case TagMap.EntryReader.INT: case TagMap.EntryReader.LONG: - writeAttribute(buf, LONG, tagEntry.tag(), tagEntry.objectValue()); + writeAttribute(buf, LONG_ATTRIBUTE, tagEntry.tag(), tagEntry.objectValue()); break; case TagMap.EntryReader.FLOAT: case TagMap.EntryReader.DOUBLE: - writeAttribute(buf, DOUBLE, tagEntry.tag(), tagEntry.objectValue()); + writeAttribute(buf, DOUBLE_ATTRIBUTE, tagEntry.tag(), tagEntry.objectValue()); break; default: - writeAttribute(buf, STRING, tagEntry.tag(), tagEntry.stringValue()); + writeAttribute(buf, STRING_ATTRIBUTE, tagEntry.tag(), tagEntry.stringValue()); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpCommonProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpCommonProtoTest.java index 071aafb7a29..21b7641dc1a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpCommonProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpCommonProtoTest.java @@ -1,13 +1,13 @@ package datadog.trace.core.otlp.common; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ARRAY; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ARRAY; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ARRAY; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -166,7 +166,7 @@ void testInstrumentationScopeWithNullVersion() throws IOException { @ParameterizedTest @ValueSource(strings = {"hello", "", "héllo", "日本語", "emoji 🎉"}) void testStringAttribute(String value) throws IOException { - byte[] bytes = encode(STRING, "str-key", value); + byte[] bytes = encode(STRING_ATTRIBUTE, "str-key", value); CodedInputStream kv = keyValueStream(bytes); assertEquals("str-key", readKeyField(kv)); @@ -181,7 +181,7 @@ void testStringAttribute(String value) throws IOException { @ParameterizedTest @ValueSource(booleans = {true, false}) void testBooleanAttribute(boolean value) throws IOException { - byte[] bytes = encode(BOOLEAN, "bool-key", value); + byte[] bytes = encode(BOOLEAN_ATTRIBUTE, "bool-key", value); CodedInputStream kv = keyValueStream(bytes); assertEquals("bool-key", readKeyField(kv)); @@ -196,7 +196,7 @@ void testBooleanAttribute(boolean value) throws IOException { @ParameterizedTest @ValueSource(longs = {0L, 1L, -1L, 42L, Long.MIN_VALUE, Long.MAX_VALUE}) void testLongAttribute(long value) throws IOException { - byte[] bytes = encode(LONG, "long-key", value); + byte[] bytes = encode(LONG_ATTRIBUTE, "long-key", value); CodedInputStream kv = keyValueStream(bytes); assertEquals("long-key", readKeyField(kv)); @@ -222,7 +222,7 @@ void testLongAttribute(long value) throws IOException { Double.NEGATIVE_INFINITY }) void testDoubleAttribute(double value) throws IOException { - byte[] bytes = encode(DOUBLE, "dbl-key", value); + byte[] bytes = encode(DOUBLE_ATTRIBUTE, "dbl-key", value); CodedInputStream kv = keyValueStream(bytes); assertEquals("dbl-key", readKeyField(kv)); @@ -239,7 +239,7 @@ void testDoubleAttribute(double value) throws IOException { @Test void testEmptyStringArrayAttribute() throws IOException { - byte[] bytes = encode(STRING_ARRAY, "arr-str", Collections.emptyList()); + byte[] bytes = encode(STRING_ARRAY_ATTRIBUTE, "arr-str", Collections.emptyList()); CodedInputStream kv = keyValueStream(bytes); assertEquals("arr-str", readKeyField(kv)); @@ -251,7 +251,8 @@ void testEmptyStringArrayAttribute() throws IOException { @Test void testStringArrayAttribute() throws IOException { - byte[] bytes = encode(STRING_ARRAY, "arr-str", Arrays.asList("alpha", "héllo", "日本語")); + byte[] bytes = + encode(STRING_ARRAY_ATTRIBUTE, "arr-str", Arrays.asList("alpha", "héllo", "日本語")); CodedInputStream kv = keyValueStream(bytes); assertEquals("arr-str", readKeyField(kv)); @@ -271,7 +272,7 @@ void testStringArrayAttribute() throws IOException { @Test void testEmptyBooleanArrayAttribute() throws IOException { - byte[] bytes = encode(BOOLEAN_ARRAY, "arr-bool", Collections.emptyList()); + byte[] bytes = encode(BOOLEAN_ARRAY_ATTRIBUTE, "arr-bool", Collections.emptyList()); CodedInputStream kv = keyValueStream(bytes); assertEquals("arr-bool", readKeyField(kv)); @@ -283,7 +284,7 @@ void testEmptyBooleanArrayAttribute() throws IOException { @Test void testBooleanArrayAttribute() throws IOException { - byte[] bytes = encode(BOOLEAN_ARRAY, "arr-bool", Arrays.asList(true, false, true)); + byte[] bytes = encode(BOOLEAN_ARRAY_ATTRIBUTE, "arr-bool", Arrays.asList(true, false, true)); CodedInputStream kv = keyValueStream(bytes); assertEquals("arr-bool", readKeyField(kv)); @@ -303,7 +304,7 @@ void testBooleanArrayAttribute() throws IOException { @Test void testEmptyLongArrayAttribute() throws IOException { - byte[] bytes = encode(LONG_ARRAY, "arr-long", Collections.emptyList()); + byte[] bytes = encode(LONG_ARRAY_ATTRIBUTE, "arr-long", Collections.emptyList()); CodedInputStream kv = keyValueStream(bytes); assertEquals("arr-long", readKeyField(kv)); @@ -316,7 +317,10 @@ void testEmptyLongArrayAttribute() throws IOException { @Test void testLongArrayAttribute() throws IOException { byte[] bytes = - encode(LONG_ARRAY, "arr-long", Arrays.asList(0L, -1L, Long.MIN_VALUE, Long.MAX_VALUE)); + encode( + LONG_ARRAY_ATTRIBUTE, + "arr-long", + Arrays.asList(0L, -1L, Long.MIN_VALUE, Long.MAX_VALUE)); CodedInputStream kv = keyValueStream(bytes); assertEquals("arr-long", readKeyField(kv)); @@ -336,7 +340,7 @@ void testLongArrayAttribute() throws IOException { @Test void testEmptyDoubleArrayAttribute() throws IOException { - byte[] bytes = encode(DOUBLE_ARRAY, "arr-dbl", Collections.emptyList()); + byte[] bytes = encode(DOUBLE_ARRAY_ATTRIBUTE, "arr-dbl", Collections.emptyList()); CodedInputStream kv = keyValueStream(bytes); assertEquals("arr-dbl", readKeyField(kv)); @@ -350,7 +354,7 @@ void testEmptyDoubleArrayAttribute() throws IOException { void testDoubleArrayAttribute() throws IOException { byte[] bytes = encode( - DOUBLE_ARRAY, + DOUBLE_ARRAY_ATTRIBUTE, "arr-dbl", Arrays.asList(0.0, -1.5, Double.NaN, Double.POSITIVE_INFINITY)); CodedInputStream kv = keyValueStream(bytes); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpMetricsProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpMetricsProtoTest.java index d29a2cac7e1..224f662c7d8 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpMetricsProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpMetricsProtoTest.java @@ -1,9 +1,9 @@ package datadog.trace.core.otlp.metrics; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.DOUBLE_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; @@ -257,19 +257,19 @@ private static MetricSpec histogram( } private static AttrSpec strAttr(String key, String value) { - return new AttrSpec(STRING, key, value); + return new AttrSpec(STRING_ATTRIBUTE, key, value); } private static AttrSpec longAttr(String key, long value) { - return new AttrSpec(LONG, key, value); + return new AttrSpec(LONG_ATTRIBUTE, key, value); } private static AttrSpec boolAttr(String key, boolean value) { - return new AttrSpec(BOOLEAN, key, value); + return new AttrSpec(BOOLEAN_ATTRIBUTE, key, value); } private static AttrSpec dblAttr(String key, double value) { - return new AttrSpec(DOUBLE, key, value); + return new AttrSpec(DOUBLE_ATTRIBUTE, key, value); } // ── test cases ───────────────────────────────────────────────────────────── diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 34de791eee7..06054109b1b 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -115,9 +115,11 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_METRICS_OTEL_INTERVAL; import static datadog.trace.api.ConfigDefaults.DEFAULT_METRICS_OTEL_TIMEOUT; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_GRPC_PORT; +import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_HTTP_LOGS_ENDPOINT; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_HTTP_METRICS_ENDPOINT; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_HTTP_PORT; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_HTTP_TRACES_ENDPOINT; +import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_LOGS_TIMEOUT; import static datadog.trace.api.ConfigDefaults.DEFAULT_OTLP_TRACES_TIMEOUT; import static datadog.trace.api.ConfigDefaults.DEFAULT_PARTIAL_FLUSH_MIN_SPANS; import static datadog.trace.api.ConfigDefaults.DEFAULT_PERF_METRICS_ENABLED; @@ -449,10 +451,16 @@ import static datadog.trace.api.config.JmxFetchConfig.JMX_TAGS; import static datadog.trace.api.config.LlmObsConfig.LLMOBS_AGENTLESS_ENABLED; import static datadog.trace.api.config.LlmObsConfig.LLMOBS_ML_APP; +import static datadog.trace.api.config.OtlpConfig.LOGS_OTEL_EXPORTER; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_CARDINALITY_LIMIT; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_EXPORTER; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_INTERVAL; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_TIMEOUT; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_COMPRESSION; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_ENDPOINT; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_HEADERS; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_PROTOCOL; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_TIMEOUT; import static datadog.trace.api.config.OtlpConfig.OTLP_METRICS_COMPRESSION; import static datadog.trace.api.config.OtlpConfig.OTLP_METRICS_ENDPOINT; import static datadog.trace.api.config.OtlpConfig.OTLP_METRICS_HEADERS; @@ -932,6 +940,13 @@ public static String getHostName() { private final boolean jmxFetchMultipleRuntimeServicesEnabled; private final int jmxFetchMultipleRuntimeServicesLimit; + private final String logsOtelExporter; + private final String otlpLogsEndpoint; + private final Map otlpLogsHeaders; + private final OtlpConfig.Protocol otlpLogsProtocol; + private final OtlpConfig.Compression otlpLogsCompression; + private final int otlpLogsTimeout; + private final String metricsOtelExporter; private final int metricsOtelInterval; private final int metricsOtelTimeout; @@ -1920,6 +1935,40 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins statsDClientSocketBuffer = configProvider.getInteger(STATSD_CLIENT_SOCKET_BUFFER); statsDClientSocketTimeout = configProvider.getInteger(STATSD_CLIENT_SOCKET_TIMEOUT); + logsOtelExporter = configProvider.getString(LOGS_OTEL_EXPORTER); + + // keep OTLP default timeout below the overall export timeout + int otlpTimeout = configProvider.getInteger(OTLP_LOGS_TIMEOUT, DEFAULT_OTLP_LOGS_TIMEOUT); + if (otlpTimeout < 0) { + log.warn("Invalid OTLP logs timeout: {}. The value must be positive", otlpTimeout); + otlpTimeout = DEFAULT_OTLP_LOGS_TIMEOUT; + } + otlpLogsTimeout = otlpTimeout; + + otlpLogsHeaders = configProvider.getMergedMap(OTLP_LOGS_HEADERS, '='); + otlpLogsProtocol = + configProvider.getEnum( + OTLP_LOGS_PROTOCOL, OtlpConfig.Protocol.class, OtlpConfig.Protocol.HTTP_PROTOBUF); + otlpLogsCompression = + configProvider.getEnum( + OTLP_LOGS_COMPRESSION, OtlpConfig.Compression.class, OtlpConfig.Compression.NONE); + + String otlpLogsEndpointFromEnvironment = configProvider.getString(OTLP_LOGS_ENDPOINT); + if (otlpLogsEndpointFromEnvironment == null) { + if (otlpLogsProtocol == OtlpConfig.Protocol.GRPC) { + otlpLogsEndpointFromEnvironment = "http://" + agentHost + ':' + DEFAULT_OTLP_GRPC_PORT; + } else { + otlpLogsEndpointFromEnvironment = + "http://" + + agentHost + + ':' + + DEFAULT_OTLP_HTTP_PORT + + '/' + + DEFAULT_OTLP_HTTP_LOGS_ENDPOINT; + } + } + otlpLogsEndpoint = otlpLogsEndpointFromEnvironment; + metricsOtelExporter = configProvider.getString(METRICS_OTEL_EXPORTER); int cardinalityLimit = @@ -1950,7 +1999,7 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins // keep OTLP default timeout below the overall export timeout int defaultOtlpMetricsTimeout = Math.min(metricsOtelTimeout, DEFAULT_METRICS_OTEL_TIMEOUT); - int otlpTimeout = configProvider.getInteger(OTLP_METRICS_TIMEOUT, defaultOtlpMetricsTimeout); + otlpTimeout = configProvider.getInteger(OTLP_METRICS_TIMEOUT, defaultOtlpMetricsTimeout); if (otlpTimeout < 0) { log.warn("Invalid OTLP metrics timeout: {}. The value must be positive", otlpTimeout); otlpTimeout = defaultOtlpMetricsTimeout; @@ -5307,6 +5356,34 @@ public boolean isJmxFetchIntegrationEnabled( return configProvider.isEnabled(integrationNames, "jmxfetch.", ".enabled", defaultEnabled); } + public boolean isLogsOtelEnabled() { + return instrumenterConfig.isLogsOtelEnabled(); + } + + public boolean isLogsOtlpExporterEnabled() { + return "otlp".equalsIgnoreCase(logsOtelExporter); + } + + public String getOtlpLogsEndpoint() { + return otlpLogsEndpoint; + } + + public Map getOtlpLogsHeaders() { + return otlpLogsHeaders; + } + + public OtlpConfig.Protocol getOtlpLogsProtocol() { + return otlpLogsProtocol; + } + + public OtlpConfig.Compression getOtlpLogsCompression() { + return otlpLogsCompression; + } + + public int getOtlpLogsTimeout() { + return otlpLogsTimeout; + } + public boolean isMetricsOtelEnabled() { return instrumenterConfig.isMetricsOtelEnabled(); } @@ -6397,6 +6474,18 @@ public String toString() { + aiGuardEnabled + ", aiGuardEndpoint=" + aiGuardEndpoint + + ", logsOtelExporter=" + + logsOtelExporter + + ", otlpLogsEndpoint=" + + otlpLogsEndpoint + + ", otlpLogsHeaders=" + + otlpLogsHeaders + + ", otlpLogsProtocol=" + + otlpLogsProtocol + + ", otlpLogsCompression=" + + otlpLogsCompression + + ", otlpLogsTimeout=" + + otlpLogsTimeout + ", metricsOtelExporter=" + metricsOtelExporter + ", metricsOtelInterval=" diff --git a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java index 1f5e3cddc30..e8627a5a852 100644 --- a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java @@ -10,6 +10,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_INTEGRATIONS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_LLM_OBS_ENABLED; +import static datadog.trace.api.ConfigDefaults.DEFAULT_LOGS_OTEL_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_MEASURE_METHODS; import static datadog.trace.api.ConfigDefaults.DEFAULT_MEASURE_NATIVE_METHODS; import static datadog.trace.api.ConfigDefaults.DEFAULT_METRICS_OTEL_ENABLED; @@ -41,6 +42,7 @@ import static datadog.trace.api.config.GeneralConfig.TRIAGE_REPORT_TRIGGER; import static datadog.trace.api.config.IastConfig.IAST_ENABLED; import static datadog.trace.api.config.LlmObsConfig.LLMOBS_ENABLED; +import static datadog.trace.api.config.OtlpConfig.LOGS_OTEL_ENABLED; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_ENABLED; import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_ENABLED; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DIRECT_ALLOCATION_ENABLED; @@ -149,6 +151,7 @@ public class InstrumenterConfig { private final boolean traceEnabled; private final boolean traceOtelEnabled; private final boolean metricsOtelEnabled; + private final boolean logsOtelEnabled; private final ProfilingEnablement profilingEnabled; private final boolean ciVisibilityEnabled; private final ProductActivation appSecActivation; @@ -258,6 +261,7 @@ private InstrumenterConfig() { traceOtelEnabled = configProvider.getBoolean(TRACE_OTEL_ENABLED, DEFAULT_TRACE_OTEL_ENABLED); metricsOtelEnabled = configProvider.getBoolean(METRICS_OTEL_ENABLED, DEFAULT_METRICS_OTEL_ENABLED); + logsOtelEnabled = configProvider.getBoolean(LOGS_OTEL_ENABLED, DEFAULT_LOGS_OTEL_ENABLED); profilingEnabled = ProfilingEnablement.of( @@ -453,6 +457,10 @@ public boolean isMetricsOtelEnabled() { return metricsOtelEnabled; } + public boolean isLogsOtelEnabled() { + return logsOtelEnabled; + } + public boolean isProfilingEnabled() { return profilingEnabled.isActive(); } @@ -755,6 +763,8 @@ public String toString() { + traceOtelEnabled + ", metricsOtelEnabled=" + metricsOtelEnabled + + ", logsOtelEnabled=" + + logsOtelEnabled + ", profilingEnabled=" + profilingEnabled + ", ciVisibilityEnabled=" diff --git a/internal-api/src/test/groovy/datadog/trace/api/telemetry/OtelEnvMetricCollectorImplTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/telemetry/OtelEnvMetricCollectorImplTest.groovy index df44fe16d02..ba893699615 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/telemetry/OtelEnvMetricCollectorImplTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/telemetry/OtelEnvMetricCollectorImplTest.groovy @@ -198,8 +198,8 @@ class OtelEnvMetricCollectorImplTest extends DDSpecification { where: otelEnvKey | otelEnvValue || metricType | metricValue | metricNamespace | metricName | tagsOtelValue - 'OTEL_METRICS_EXPORTER' | 'otlp' || 'count' | 1 | 'tracers' | 'otel.env.unsupported' | 'config_opentelemetry:otel_metrics_exporter' + 'OTEL_METRICS_EXPORTER' | 'zipkin' || 'count' | 1 | 'tracers' | 'otel.env.unsupported' | 'config_opentelemetry:otel_metrics_exporter' 'OTEL_TRACES_EXPORTER' | 'zipkin' || 'count' | 1 | 'tracers' | 'otel.env.unsupported' | 'config_opentelemetry:otel_traces_exporter' - 'OTEL_LOGS_EXPORTER' | 'otlp' || 'count' | 1 | 'tracers' | 'otel.env.unsupported' | 'config_opentelemetry:otel_logs_exporter' + 'OTEL_LOGS_EXPORTER' | 'zipkin' || 'count' | 1 | 'tracers' | 'otel.env.unsupported' | 'config_opentelemetry:otel_logs_exporter' } } diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index a1569256c66..93a198b3ac8 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2225,6 +2225,62 @@ "aliases": ["DD_LOGS_INJECTION"] } ], + "DD_LOGS_OTEL_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], + "DD_LOGS_OTEL_EXPORTER": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "DD_OTLP_LOGS_ENDPOINT": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "DD_OTLP_LOGS_HEADERS": [ + { + "version": "A", + "type": "map", + "default": null, + "aliases": [] + } + ], + "DD_OTLP_LOGS_PROTOCOL": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "DD_OTLP_LOGS_COMPRESSION": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "DD_OTLP_LOGS_TIMEOUT": [ + { + "version": "A", + "type": "int", + "default": null, + "aliases": [] + } + ], "DD_TRACE_LOG_LEVEL": [ { "version": "D", @@ -8321,6 +8377,14 @@ "aliases": ["DD_TRACE_INTEGRATION_OPENSEARCH_TRANSPORT_ENABLED", "DD_INTEGRATION_OPENSEARCH_TRANSPORT_ENABLED"] } ], + "DD_TRACE_OPENTELEMETRY_1_27_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": null, + "aliases": ["DD_TRACE_INTEGRATION_OPENTELEMETRY_1_27_ENABLED", "DD_INTEGRATION_OPENTELEMETRY_1_27_ENABLED"] + } + ], "DD_TRACE_OPENTELEMETRY_1_47_ENABLED": [ { "version": "A", @@ -8409,6 +8473,14 @@ "aliases": ["DD_TRACE_INTEGRATION_OPENTELEMETRY_EXPERIMENTAL_ENABLED", "DD_INTEGRATION_OPENTELEMETRY_EXPERIMENTAL_ENABLED"] } ], + "DD_TRACE_OPENTELEMETRY_LOGS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": null, + "aliases": ["DD_TRACE_INTEGRATION_OPENTELEMETRY_LOGS_ENABLED", "DD_INTEGRATION_OPENTELEMETRY_LOGS_ENABLED"] + } + ], "DD_TRACE_OPENTELEMETRY_METRICS_ENABLED": [ { "version": "A", @@ -11497,6 +11569,46 @@ "aliases": [] } ], + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "OTEL_EXPORTER_OTLP_LOGS_HEADERS": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": [ + { + "version": "A", + "type": "string", + "default": "SDK-dependent, but will typically be either http/protobuf or grpc.", + "aliases": [] + } + ], + "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], + "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": [ + { + "version": "A", + "type": "int", + "default": "10000", + "aliases": [] + } + ], "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": [ { "version": "A", diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d3d15d16f1..bd5aaceffaa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -496,6 +496,7 @@ include( ":dd-java-agent:instrumentation:opensearch:opensearch-common", ":dd-java-agent:instrumentation:opentelemetry:opentelemetry-0.3", ":dd-java-agent:instrumentation:opentelemetry:opentelemetry-1.4", + ":dd-java-agent:instrumentation:opentelemetry:opentelemetry-1.27", ":dd-java-agent:instrumentation:opentelemetry:opentelemetry-1.47", ":dd-java-agent:instrumentation:opentelemetry:opentelemetry-annotations-1.20", ":dd-java-agent:instrumentation:opentelemetry:opentelemetry-annotations-1.26", diff --git a/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java b/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java index 05434e10371..e015c1e2179 100644 --- a/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java +++ b/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java @@ -1,5 +1,6 @@ package datadog.trace.bootstrap.config.provider; +import static datadog.trace.api.ConfigDefaults.DEFAULT_LOGS_OTEL_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_METRICS_OTEL_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_OTEL_ENABLED; import static datadog.trace.api.config.GeneralConfig.ENV; @@ -8,11 +9,18 @@ import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; import static datadog.trace.api.config.GeneralConfig.TAGS; import static datadog.trace.api.config.GeneralConfig.VERSION; +import static datadog.trace.api.config.OtlpConfig.LOGS_OTEL_ENABLED; +import static datadog.trace.api.config.OtlpConfig.LOGS_OTEL_EXPORTER; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_CARDINALITY_LIMIT; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_ENABLED; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_EXPORTER; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_INTERVAL; import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_TIMEOUT; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_COMPRESSION; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_ENDPOINT; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_HEADERS; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_PROTOCOL; +import static datadog.trace.api.config.OtlpConfig.OTLP_LOGS_TIMEOUT; import static datadog.trace.api.config.OtlpConfig.OTLP_METRICS_COMPRESSION; import static datadog.trace.api.config.OtlpConfig.OTLP_METRICS_ENDPOINT; import static datadog.trace.api.config.OtlpConfig.OTLP_METRICS_HEADERS; @@ -90,7 +98,7 @@ public ConfigOrigin origin() { OtelEnvironmentConfigSource(Properties datadogConfigFile) { this.datadogConfigFile = datadogConfigFile; - this.enabled = traceOtelEnabled() || metricsOtelEnabled(); + this.enabled = traceOtelEnabled() || metricsOtelEnabled() || logsOtelEnabled(); if (enabled) { setupOtelEnvironment(); @@ -104,6 +112,7 @@ private void setupOtelEnvironment() { if ("true".equalsIgnoreCase(sdkDisabled)) { capture(TRACE_OTEL_ENABLED, "false"); capture(METRICS_OTEL_ENABLED, "false"); + capture(LOGS_OTEL_ENABLED, "false"); return; } String logLevel = getOtelProperty("otel.log.level", "dd." + LOG_LEVEL); @@ -129,7 +138,11 @@ private void setupOtelEnvironment() { capture(RUNTIME_METRICS_ENABLED, mapDataCollection("metrics")); } - mapDataCollection("logs"); // check setting, but no need to capture it + if (logsOtelEnabled()) { + setupLogsOtelEnvironment(); + } else { + mapDataCollection("logs"); + } } private void setupTraceOtelEnvironment() { @@ -207,6 +220,24 @@ private void setupMetricsOtelEnvironment() { } } + private void setupLogsOtelEnvironment() { + String exporter = getOtelProperty("otel.logs.exporter"); + if (exporter == null || "otlp".equalsIgnoreCase(exporter)) { // logs defaults to OTLP + capture(LOGS_OTEL_EXPORTER, "otlp"); + capture(OTLP_LOGS_HEADERS, getOtelOtlpProperty("logs", "headers", "dd." + OTLP_LOGS_HEADERS)); + capture( + OTLP_LOGS_PROTOCOL, getOtelOtlpProperty("logs", "protocol", "dd." + OTLP_LOGS_PROTOCOL)); + capture( + OTLP_LOGS_COMPRESSION, + getOtelOtlpProperty("logs", "compression", "dd." + OTLP_LOGS_COMPRESSION)); + capture(OTLP_LOGS_TIMEOUT, getOtelOtlpProperty("logs", "timeout", "dd." + OTLP_LOGS_TIMEOUT)); + capture( + OTLP_LOGS_ENDPOINT, getOtelOtlpProperty("logs", "endpoint", "dd." + OTLP_LOGS_ENDPOINT)); + } else { + mapDataCollection("logs"); + } + } + private boolean traceOtelEnabled() { String enabled = getDatadogProperty("dd." + TRACE_OTEL_ENABLED); if (null != enabled) { @@ -225,6 +256,15 @@ private boolean metricsOtelEnabled() { } } + private boolean logsOtelEnabled() { + String enabled = getDatadogProperty("dd." + LOGS_OTEL_ENABLED); + if (null != enabled) { + return Boolean.parseBoolean(enabled); + } else { + return DEFAULT_LOGS_OTEL_ENABLED; + } + } + /** * Gets an OpenTelemetry property. * @@ -275,6 +315,10 @@ private String getOtelOtlpProperty(String signal, String subkey, String ddSysPro otelValue = getOtelProperty(otelKey); // special case when using general endpoint as fallback: append appropriate suffix if ("endpoint".equals(subkey) && otelValue != null && !otelValue.startsWith("unix://")) { + if ("logs".equals(signal) + && !"grpc".equalsIgnoreCase(otelEnvironment.get(OTLP_LOGS_PROTOCOL))) { + otelValue = otelValue + (otelValue.endsWith("/") ? "v1/logs" : "/v1/logs"); + } if ("metrics".equals(signal) && !"grpc".equalsIgnoreCase(otelEnvironment.get(OTLP_METRICS_PROTOCOL))) { otelValue = otelValue + (otelValue.endsWith("/") ? "v1/metrics" : "/v1/metrics"); From e72e4f9a1c84bf29328a3c9a5dad2d64ca8d3f60 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 29 Apr 2026 08:07:50 +0100 Subject: [PATCH 2/7] Review feedback --- .../datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java index cea11510084..cc4e85512e5 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java @@ -1,6 +1,7 @@ package datadog.opentelemetry.shim.logs; import static datadog.opentelemetry.shim.trace.OtelExtractedContext.extract; +import static datadog.trace.bootstrap.otlp.logs.OtlpLogRecord.STRING_BODY; import static io.opentelemetry.api.common.AttributeKey.stringKey; import datadog.trace.api.time.SystemTimeSource; @@ -81,7 +82,7 @@ public LogRecordBuilder setSeverityText(String severityText) { @Override public LogRecordBuilder setBody(String value) { - this.bodyType = 1; + this.bodyType = STRING_BODY; this.bodyValue = value; return this; } From 57e55404ea1aab8e949453fd8f46f9a62c0ad24b Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 29 Apr 2026 10:46:17 +0100 Subject: [PATCH 3/7] Move defensive attributes copy to if builder is used after emit. Also make attributes non-null in OtlpLogRecord. --- .../otel/logs/data/OtelLogRecordProcessor.java | 2 +- .../trace/bootstrap/otlp/logs/OtlpLogRecord.java | 4 ++-- .../opentelemetry/shim/logs/OtelLogRecordBuilder.java | 11 ++++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java index 93ce59392c0..d0401336d11 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java @@ -43,7 +43,7 @@ public void collectLogs(OtlpLogsVisitor visitor) { scopedVisitor = visitor.visitScopedLogs(currentScope); } Map attributes = logRecord.attributes; - if (attributes != null && !attributes.isEmpty()) { + if (!attributes.isEmpty()) { ClassLoader cl = getAttributesClassLoader(attributes); // avoid repeated lookups when attribute class-loader is same for all records if (attributesReader == null || !Objects.equals(cl, attributesClassLoader)) { diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java index 1711999f52a..345844abe4c 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otlp/logs/OtlpLogRecord.java @@ -24,7 +24,7 @@ public final class OtlpLogRecord { public final int bodyType; @Nullable public final Object bodyValue; @Nullable public final String eventName; - @Nullable public final Map attributes; + public final Map attributes; @Nullable public final AgentSpanContext spanContext; public OtlpLogRecord( @@ -36,7 +36,7 @@ public OtlpLogRecord( int bodyType, @Nullable Object bodyValue, @Nullable String eventName, - @Nullable Map attributes, + Map attributes, @Nullable AgentSpanContext spanContext) { this.instrumentationScope = instrumentationScope; this.timestampNanos = timestampNanos; diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java index cc4e85512e5..8e78dd05658 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/logs/OtelLogRecordBuilder.java @@ -14,6 +14,7 @@ import io.opentelemetry.api.logs.Severity; import io.opentelemetry.context.Context; import java.time.Instant; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -40,6 +41,8 @@ final class OtelLogRecordBuilder implements LogRecordBuilder { @Nullable private Map, Object> attributes; @Nullable private Context context; + private boolean attributesEmitted; + OtelLogRecordBuilder(OtelLogger logger) { this.logger = logger; } @@ -99,6 +102,11 @@ public LogRecordBuilder setAttribute(@Nullable AttributeKey key, @Nullabl if (key == null || key.getKey().isEmpty()) { return this; } + if (attributesEmitted && attributes != null) { + // defensive copy if builder used after emit + attributes = new HashMap<>(attributes); + attributesEmitted = false; + } if (value != null) { if (attributes == null) { attributes = new HashMap<>(); @@ -138,6 +146,7 @@ private void setExceptionAttribute(AttributeKey key, @Nullable String va @Override public void emit() { + attributesEmitted = true; Context context = this.context != null ? this.context : Context.current(); if (logger.isEnabled(severity, context)) { OtelLogRecordProcessor.INSTANCE.addLog( @@ -150,7 +159,7 @@ public void emit() { bodyType, bodyValue, eventName, - attributes != null ? new HashMap<>(attributes) : null, + attributes != null ? attributes : Collections.emptyMap(), extract(context))); } } From ba09e0fce3b681c23dcabd55f0bda829e5e5fe14 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 29 Apr 2026 11:14:07 +0100 Subject: [PATCH 4/7] Add 'opentelemetry-1' as an alias for OTel logs instrumentation - to match OTel traces instrumentation --- .../opentelemetry/opentelemetry-1.27/build.gradle | 2 ++ .../opentelemetry127/OpenTelemetryLogsInstrumentation.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle index 2c6bfeebf33..d11887584d0 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/build.gradle @@ -5,6 +5,8 @@ muzzle { module = 'opentelemetry-api' group = 'io.opentelemetry' versions = "[$openTelemetryVersion,)" + assertInverse = true + skipVersions = ['0.13.0'] // bad release } } diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java index be21319cf51..ca9219cba91 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/main/java/datadog/trace/instrumentation/opentelemetry127/OpenTelemetryLogsInstrumentation.java @@ -27,7 +27,7 @@ public class OpenTelemetryLogsInstrumentation extends InstrumenterModule.Tracing implements Instrumenter.CanShortcutTypeMatching, Instrumenter.HasMethodAdvice { public OpenTelemetryLogsInstrumentation() { - super("opentelemetry-logs", "opentelemetry-1.27"); + super("opentelemetry-logs", "opentelemetry-1.27", "opentelemetry-1"); } @Override From 0ac061dd3f699075de7ecf4b62241054ae607e76 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 29 Apr 2026 11:21:03 +0100 Subject: [PATCH 5/7] Cleanup test package/class names --- ...metryLogsActivationByInstrumentationNameForkedTest.java} | 6 ++++-- ...OpenTelemetryLogsActivationByOtelRfcNameForkedTest.java} | 4 +++- .../logs/OpenTelemetryLogsActivationTest.java} | 4 +++- .../logs/OpenTelemetryLogsDisableByDefaultForkedTest.java} | 4 +++- .../logs/{LogsTest.java => OpenTelemetryLogsTest.java} | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) rename dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/{OpenTelemetry127ActivationByInstrumentationNameForkedTest.java => opentelemetry127/logs/OpenTelemetryLogsActivationByInstrumentationNameForkedTest.java} (72%) rename dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/{OpenTelemetry127ActivationByOtelRfcNameForkedTest.java => opentelemetry127/logs/OpenTelemetryLogsActivationByOtelRfcNameForkedTest.java} (73%) rename dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/{OpenTelemetry127ActivationTest.java => opentelemetry127/logs/OpenTelemetryLogsActivationTest.java} (87%) rename dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/{OpenTelemetry127DisableByDefaultForkedTest.java => opentelemetry127/logs/OpenTelemetryLogsDisableByDefaultForkedTest.java} (70%) rename dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/{LogsTest.java => OpenTelemetryLogsTest.java} (99%) diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByInstrumentationNameForkedTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationByInstrumentationNameForkedTest.java similarity index 72% rename from dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByInstrumentationNameForkedTest.java rename to dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationByInstrumentationNameForkedTest.java index bcc39764540..43545da3840 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByInstrumentationNameForkedTest.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationByInstrumentationNameForkedTest.java @@ -1,10 +1,12 @@ +package opentelemetry127.logs; + import datadog.trace.junit.utils.config.WithConfig; // Forked test: runs in an isolated JVM with opentelemetry-logs instrumentation enabled // by integration name. GlobalOpenTelemetry holds static state that must reset between variants. @WithConfig(key = "integration.opentelemetry-logs.enabled", value = "true") -class OpenTelemetry127ActivationByInstrumentationNameForkedTest - extends OpenTelemetry127ActivationTest { +class OpenTelemetryLogsActivationByInstrumentationNameForkedTest + extends OpenTelemetryLogsActivationTest { @Override boolean shouldBeInjected() { diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByOtelRfcNameForkedTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationByOtelRfcNameForkedTest.java similarity index 73% rename from dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByOtelRfcNameForkedTest.java rename to dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationByOtelRfcNameForkedTest.java index 3e475214a79..3968c82376a 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationByOtelRfcNameForkedTest.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationByOtelRfcNameForkedTest.java @@ -1,9 +1,11 @@ +package opentelemetry127.logs; + import datadog.trace.junit.utils.config.WithConfig; // Forked test: runs in an isolated JVM with opentelemetry-logs instrumentation enabled // by OTel RFC config name. GlobalOpenTelemetry holds static state that must reset between variants. @WithConfig(key = "logs.otel.enabled", value = "true") -class OpenTelemetry127ActivationByOtelRfcNameForkedTest extends OpenTelemetry127ActivationTest { +class OpenTelemetryLogsActivationByOtelRfcNameForkedTest extends OpenTelemetryLogsActivationTest { @Override boolean shouldBeInjected() { diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationTest.java similarity index 87% rename from dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationTest.java rename to dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationTest.java index 9eb9f54380f..aa820a45a9d 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127ActivationTest.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsActivationTest.java @@ -1,3 +1,5 @@ +package opentelemetry127.logs; + import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.trace.agent.test.AbstractInstrumentationTest; @@ -5,7 +7,7 @@ import io.opentelemetry.api.logs.Logger; import org.junit.jupiter.api.Test; -abstract class OpenTelemetry127ActivationTest extends AbstractInstrumentationTest { +abstract class OpenTelemetryLogsActivationTest extends AbstractInstrumentationTest { abstract boolean shouldBeInjected(); diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127DisableByDefaultForkedTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsDisableByDefaultForkedTest.java similarity index 70% rename from dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127DisableByDefaultForkedTest.java rename to dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsDisableByDefaultForkedTest.java index ead0a1f166a..328e7fd3961 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/OpenTelemetry127DisableByDefaultForkedTest.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsDisableByDefaultForkedTest.java @@ -1,7 +1,9 @@ +package opentelemetry127.logs; + // Forked test: runs in an isolated JVM with no opentelemetry-logs config override, // verifying that the instrumentation is disabled by default. // GlobalOpenTelemetry holds static state that must reset between variants. -class OpenTelemetry127DisableByDefaultForkedTest extends OpenTelemetry127ActivationTest { +class OpenTelemetryLogsDisableByDefaultForkedTest extends OpenTelemetryLogsActivationTest { @Override boolean shouldBeInjected() { diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/LogsTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsTest.java similarity index 99% rename from dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/LogsTest.java rename to dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsTest.java index d57e4095cc8..613d8af6017 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/LogsTest.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/src/test/java/opentelemetry127/logs/OpenTelemetryLogsTest.java @@ -28,7 +28,7 @@ import org.junit.jupiter.params.provider.EnumSource; @WithConfig(key = "logs.otel.enabled", value = "true") -class LogsTest extends AbstractInstrumentationTest { +class OpenTelemetryLogsTest extends AbstractInstrumentationTest { private final LogsReader logsReader = new LogsReader(); From 11dba5337687fbd02dc73729a0e7ae98174d6fad Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 29 Apr 2026 18:29:52 +0100 Subject: [PATCH 6/7] Review feedback --- .../trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java index d0401336d11..18fc06599bc 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/logs/data/OtelLogRecordProcessor.java @@ -69,6 +69,7 @@ public static void registerAttributeReader( } private List batchByScope() { + // capture expected batch size; records emitted after here go into next batch int batchSize = queue.size(); List batch = new ArrayList<>(batchSize); for (int i = 0; i < batchSize; i++) { @@ -76,7 +77,7 @@ private List batchByScope() { if (logRecord != null) { batch.add(logRecord); } else { - break; + break; // should not happen unless another thread is also batching records } } batch.sort(BY_SCOPE); From 29949353c8964de5883106fd1a93fb3425e0e606 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 29 Apr 2026 18:39:17 +0100 Subject: [PATCH 7/7] Add gradle.lockfile --- .../opentelemetry-1.27/gradle.lockfile | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/gradle.lockfile diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/gradle.lockfile b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/gradle.lockfile new file mode 100644 index 00000000000..39b762b75e0 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.27/gradle.lockfile @@ -0,0 +1,128 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +cafe.cryptography:curve25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +cafe.cryptography:ed25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +ch.qos.logback:logback-classic:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okhttp3:okhttp:3.12.15=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okio:okio:1.17.6=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-instrument-java:0.0.3=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:java-dogstatsd-client:4.4.5=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.datadoghq:sketches-java:0.8.3=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.javaparser:javaparser-core:3.25.6=codenarc +com.github.jnr:jffi:1.3.14=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-a64asm:1.0.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-constants:0.10.4=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-enxio:0.32.19=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-ffi:2.2.18=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-posix:3.1.21=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-unixsocket:0.38.24=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-x86asm:1.0.2=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs:4.9.8=spotbugs +com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs +com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,testAnnotationProcessor,testCompileClasspath +com.google.auto.service:auto-service:1.1.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.auto.value:auto-value-annotations:1.6.6=compileClasspath +com.google.auto:auto-common:1.2.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.13.2=spotbugs +com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.errorprone:error_prone_annotations:2.41.0=spotbugs +com.google.guava:failureaccess:1.0.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.guava:guava:20.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.0.1-jre=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.re2j:re2j:1.7=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.squareup.moshi:moshi:1.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:logging-interceptor:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:1.17.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.thoughtworks.qdox:qdox:1.12.1=codenarc +commons-fileupload:commons-fileupload:1.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.20.0=spotbugs +de.thetaphi:forbiddenapis:3.10=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.16=latestDepTestRuntimeClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-api:1.27.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-api:1.61.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath +io.opentelemetry:opentelemetry-common:1.61.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath +io.opentelemetry:opentelemetry-context:1.27.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-context:1.61.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath +io.sqreen:libsqreen:17.3.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:3.1.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +jaxen:jaxen:2.0.0=spotbugs +junit:junit:4.13.2=latestDepTestRuntimeClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.8=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.8=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +net.sf.saxon:Saxon-HE:12.9=spotbugs +org.apache.ant:ant-antlr:1.10.14=codenarc +org.apache.ant:ant-junit:1.10.14=codenarc +org.apache.bcel:bcel:6.11.0=spotbugs +org.apache.commons:commons-lang3:3.19.0=spotbugs +org.apache.commons:commons-text:1.14.0=spotbugs +org.apache.logging.log4j:log4j-api:2.25.2=spotbugs +org.apache.logging.log4j:log4j-core:2.25.2=spotbugs +org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,testCompileClasspath +org.checkerframework:checker-qual:3.33.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +org.codehaus.groovy:groovy-ant:3.0.23=codenarc +org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc +org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.groovy:groovy-templates:3.0.23=codenarc +org.codehaus.groovy:groovy-xml:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codenarc:CodeNarc:3.7.0=codenarc +org.dom4j:dom4j:2.2.0=spotbugs +org.gmetrics:GMetrics:2.1.0=codenarc +org.hamcrest:hamcrest-core:1.3=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jctools:jctools-core-jdk11:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jctools:jctools-core:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-runner:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-suite-api:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-suite-commons:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit:junit-bom:5.14.0=spotbugs +org.junit:junit-bom:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:4.4.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.9=spotbugs +org.ow2.asm:asm-commons:9.9=spotbugs +org.ow2.asm:asm-commons:9.9.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-tree:9.9=spotbugs +org.ow2.asm:asm-tree:9.9.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-util:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-util:9.9=spotbugs +org.ow2.asm:asm:9.9=spotbugs +org.ow2.asm:asm:9.9.1=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:jcl-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:log4j-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath +org.slf4j:slf4j-api:1.7.32=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j +org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j +org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.spockframework:spock-bom:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.spockframework:spock-core:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-junit:1.2.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-parser:1.2.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.xmlresolver:xmlresolver:5.3.3=spotbugs +empty=spotbugsPlugins