diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java index 1c741ed99ae..2b2ca408969 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java @@ -26,6 +26,8 @@ public final class OtelConventions { static final String SPAN_KIND_INTERNAL = "internal"; static final String OPERATION_NAME_SPECIFIC_ATTRIBUTE = "operation.name"; static final String ANALYTICS_EVENT_SPECIFIC_ATTRIBUTES = "analytics.event"; + static final String HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = "http.response.status_code"; + private static final Logger LOGGER = LoggerFactory.getLogger(OtelConventions.class); private OtelConventions() {} @@ -104,6 +106,11 @@ public static boolean applyReservedAttribute(AgentSpan span, AttributeKey span.setMetric(ANALYTICS_SAMPLE_RATE, ((Boolean) value) ? 1 : 0); return true; } + case LONG: + if (HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE.equals(name) && value instanceof Number) { + span.setHttpStatusCode(((Number) value).intValue()); + return true; + } } return false; } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java index c83e7f55018..64f22653e5e 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java @@ -1,6 +1,7 @@ package datadog.opentelemetry.shim.trace; import static datadog.opentelemetry.shim.trace.OtelConventions.ANALYTICS_EVENT_SPECIFIC_ATTRIBUTES; +import static datadog.opentelemetry.shim.trace.OtelConventions.HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE; import static datadog.opentelemetry.shim.trace.OtelConventions.OPERATION_NAME_SPECIFIC_ATTRIBUTE; import static datadog.opentelemetry.shim.trace.OtelConventions.toSpanKindTagValue; import static datadog.opentelemetry.shim.trace.OtelExtractedContext.extract; @@ -40,12 +41,19 @@ public class OtelSpanBuilder implements SpanBuilder { * set). */ private int overriddenAnalyticsSampleRate; + /** + * HTTP status code overridden value by {@link + * OtelConventions#HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE} reserved attribute ({@code -1} if not + * set). + */ + private int overriddenHttpStatusCode; public OtelSpanBuilder(AgentTracer.SpanBuilder delegate) { this.delegate = delegate; this.spanKindSet = false; this.overriddenOperationName = null; this.overriddenAnalyticsSampleRate = -1; + this.overriddenHttpStatusCode = -1; } @Override @@ -98,6 +106,11 @@ public SpanBuilder setAttribute(String key, String value) { @Override public SpanBuilder setAttribute(String key, long value) { + // Check reserved attributes + if (HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE.equals(key)) { + this.overriddenHttpStatusCode = (int) value; + return this; + } this.delegate.withTag(key, value); return this; } @@ -121,7 +134,28 @@ public SpanBuilder setAttribute(String key, boolean value) { @Override public SpanBuilder setAttribute(AttributeKey key, T value) { + String name = key.getKey(); switch (key.getType()) { + case STRING: + if (value instanceof String) { + setAttribute(name, (String) value); + break; + } + case BOOLEAN: + if (value instanceof Boolean) { + setAttribute(name, ((Boolean) value).booleanValue()); + break; + } + case LONG: + if (value instanceof Number) { + setAttribute(name, ((Number) value).longValue()); + break; + } + case DOUBLE: + if (value instanceof Number) { + setAttribute(name, ((Number) value).doubleValue()); + break; + } case STRING_ARRAY: case BOOLEAN_ARRAY: case LONG_ARRAY: @@ -130,16 +164,16 @@ public SpanBuilder setAttribute(AttributeKey key, T value) { List valueList = (List) value; if (valueList.isEmpty()) { // Store as object to prevent delegate to remove tag when value is empty - this.delegate.withTag(key.getKey(), (Object) ""); + this.delegate.withTag(name, (Object) ""); } else { for (int index = 0; index < valueList.size(); index++) { - this.delegate.withTag(key.getKey() + "." + index, valueList.get(index)); + this.delegate.withTag(name + "." + index, valueList.get(index)); } } } break; default: - this.delegate.withTag(key.getKey(), value); + this.delegate.withTag(name, value); break; } return this; @@ -181,6 +215,9 @@ public Span startSpan() { if (this.overriddenAnalyticsSampleRate != -1) { delegate.setMetric(ANALYTICS_SAMPLE_RATE, this.overriddenAnalyticsSampleRate); } + if (this.overriddenHttpStatusCode != -1) { + delegate.setHttpStatusCode(this.overriddenHttpStatusCode); + } return new OtelSpan(delegate); } } diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ConventionsTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ConventionsTest.groovy index d7d73f80726..6668a440665 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ConventionsTest.groovy +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ConventionsTest.groovy @@ -1,5 +1,6 @@ import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.DDTags +import datadog.trace.bootstrap.instrumentation.api.Tags import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.context.Context import io.opentelemetry.context.ThreadLocalContextStorage @@ -9,6 +10,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND import static datadog.opentelemetry.shim.trace.OtelConventions.OPERATION_NAME_SPECIFIC_ATTRIBUTE import static datadog.opentelemetry.shim.trace.OtelConventions.SPAN_KIND_INTERNAL import static datadog.opentelemetry.shim.trace.OtelConventions.toSpanKindTagValue +import static io.opentelemetry.api.common.AttributeKey.longKey import static io.opentelemetry.api.trace.SpanKind.CLIENT import static io.opentelemetry.api.trace.SpanKind.CONSUMER import static io.opentelemetry.api.trace.SpanKind.INTERNAL @@ -191,6 +193,65 @@ class OpenTelemetry14ConventionsTest extends AgentTestRunner { false | "" | 0 } + def "test span http.response.status_code specific tag"() { + setup: + def builder = tracer.spanBuilder("some-name") + + when: + if (setInBuilder) { + if (attributeKey) { + builder.setAttribute(longKey("http.response.status_code"), value) + } else { + builder.setAttribute("http.response.status_code", value) + } + } + def result = builder.startSpan() + if (!setInBuilder) { + if (attributeKey) { + result.setAttribute(longKey("http.response.status_code"), value) + } else { + result.setAttribute("http.response.status_code", value) + } + } + result.end() + + then: + assertTraces(1) { + trace(1) { + span { + parent() + operationName "internal" + tags { + defaultTags() + "$SPAN_KIND" "$SPAN_KIND_INTERNAL" + if (value != null) { + "$Tags.HTTP_STATUS" expectedStatus + } + } + } + } + } + + where: + setInBuilder | attributeKey | value | expectedStatus + true | false | null | 0 // Not used + true | false | 200 | 200 + true | false | 404L | 404 + true | false | 500 as Long | 500 + false | false | null | 0 // Not used + false | false | 200 | 200 + false | false | 404L | 404 + false | false | 500 as Long | 500 + true | true | null | 0 // Not used + true | true | 200 | 200 + true | true | 404L | 404 + true | true | 500 as Long | 500 + false | true | null | 0 // Not used + false | true | 200 | 200 + false | true | 404L | 404 + false | true | 500 as Long | 500 + } + @Override void cleanup() { // Test for context leak