Skip to content

Commit eeeafa8

Browse files
mz1119claude
andcommitted
Emit no gen_ai.* attributes when LLM Observability is disabled
The attributes are derived from the LLMObs tag set, which the openai decorators do not build with LLM Observability off. Drop the fallback that resolved operation, model and ml_app from APM tags instead, and collapse GenAiApmTags back to a single apply(span). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c4a4b4c commit eeeafa8

4 files changed

Lines changed: 23 additions & 107 deletions

File tree

‎dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/OpenAiDecorator.java‎

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package datadog.trace.instrumentation.openai_java;
22

3-
import static datadog.trace.api.llmobs.GenAiApmTags.stringTag;
43
import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext;
54

65
import com.openai.core.ClientOptions;
@@ -18,7 +17,6 @@
1817
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
1918
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
2019
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
21-
import datadog.trace.bootstrap.instrumentation.api.Tags;
2220
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
2321
import datadog.trace.bootstrap.instrumentation.decorator.ClientDecorator;
2422
import java.util.List;
@@ -40,8 +38,6 @@ public class OpenAiDecorator extends ClientDecorator {
4038
private static final String TOKENS_LIMIT_METRIC = METRIC_PREFIX + "tokens.limit";
4139
private static final String TOKENS_REMAINING_METRIC = METRIC_PREFIX + "tokens.remaining";
4240

43-
private static final String EMBEDDINGS_ENDPOINT = "/v1/embeddings";
44-
4541
private static final String HEADER_PREFIX = "x-ratelimit-";
4642
private static final String LIMIT_REQUESTS_HEADER = HEADER_PREFIX + "limit-requests";
4743
private static final String REMAINING_REQUESTS_HEADER = HEADER_PREFIX + "remaining-requests";
@@ -193,30 +189,10 @@ protected void doBeforeFinish(@Nonnull Context context) {
193189
LLMObsMetricCollector.get()
194190
.recordSpanFinished(INTEGRATION, spanKind, isRootSpan, true, span.isError(), false);
195191
}
196-
} else if (span != null) {
197-
// Tracing still runs with LLM Observability off, where these remain resolvable.
198-
GenAiApmTags.apply(
199-
span, operationName(span), requestedModel(span), Config.get().getLlmObsMlApp());
200192
}
201193
super.doBeforeFinish(context);
202194
}
203195

204-
private static String operationName(AgentSpan span) {
205-
String endpoint = stringTag(span, CommonTags.OPENAI_REQUEST_ENDPOINT);
206-
if (endpoint == null) {
207-
return null;
208-
}
209-
return EMBEDDINGS_ENDPOINT.equals(endpoint)
210-
? Tags.LLMOBS_EMBEDDING_SPAN_KIND
211-
: Tags.LLMOBS_LLM_SPAN_KIND;
212-
}
213-
214-
/** The response model resolves aliases the request used, so it wins. */
215-
private static String requestedModel(AgentSpan span) {
216-
String model = stringTag(span, CommonTags.OPENAI_RESPONSE_MODEL);
217-
return model != null ? model : stringTag(span, CommonTags.OPENAI_REQUEST_MODEL);
218-
}
219-
220196
public void withHttpResponse(AgentSpan span, Headers headers) {
221197
if (!llmObsEnabled) {
222198
return;

‎dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/java/datadog/trace/instrumentation/openai_java/LlmObsContextPropagationForkedTest.java‎

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertNotNull;
55
import static org.junit.jupiter.api.Assertions.assertNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
67

78
import com.openai.client.OpenAIClient;
89
import com.openai.client.okhttp.OpenAIOkHttpClient;
@@ -329,51 +330,42 @@ void parentlessOpenAiRequestSpanIsDroppedAtZeroSampleRate() throws Exception {
329330
}
330331

331332
/**
332-
* Verifies the gen_ai.* attributes an openai.request span carries with LLM Observability disabled:
333-
* operation, model, provider and application, but never token usage or conversation id.
333+
* Verifies that an openai.request span carries no gen_ai.* attributes at all with LLM Observability
334+
* disabled. The attributes are derived from the LLMObs tag set, which the decorator does not build
335+
* in that configuration.
334336
*/
335337
@WithConfig(key = "llmobs.enabled", value = "false")
336338
class LlmObsDisabledForkedTest extends AbstractLlmObsOpenAiForkedTest {
337339

338340
@Test
339-
void chatCompletionEmitsTheGenAiAttributesAvailableWithoutLlmObs() {
341+
void chatCompletionEmitsNoGenAiAttributes() {
340342
try {
341343
openAiClient.chat().completions().create(buildMinimalChatParams());
342344
} catch (Exception ignored) {
343345
// The mock server returns no body, so the SDK may throw while parsing the response.
344346
}
345347

346-
DDSpan openAiSpan = awaitOpenAiSpan("/v1/chat/completions");
347-
348-
assertEquals("llm", openAiSpan.getTag("gen_ai.operation.name"));
349-
// The mock returns no body, so the request model stands in for the absent response model.
350-
assertEquals(
351-
openAiSpan.getTag("openai.request.model"), openAiSpan.getTag("gen_ai.request.model"));
352-
assertEquals("openai", openAiSpan.getTag("gen_ai.provider.name"));
353-
assertNotNull(openAiSpan.getTag("gen_ai.application.name"));
354-
355-
assertNull(openAiSpan.getTag("gen_ai.conversation.id"));
356-
assertNull(openAiSpan.getTag("gen_ai.usage.input_tokens"));
357-
assertNull(openAiSpan.getTag("gen_ai.usage.output_tokens"));
358-
assertNull(openAiSpan.getTag("gen_ai.usage.total_tokens"));
359-
360-
assertNull(openAiSpan.getTag("_ml_obs_tag.span.kind"));
348+
assertNoGenAiTags(awaitOpenAiSpan("/v1/chat/completions"));
361349
}
362350

363351
@Test
364-
void embeddingMapsToTheEmbeddingOperation() {
352+
void embeddingEmitsNoGenAiAttributes() {
365353
try {
366354
openAiClient.embeddings().create(buildMinimalEmbeddingParams());
367355
} catch (Exception ignored) {
368356
// The mock server returns no body, so the SDK may throw while parsing the response.
369357
}
370358

371-
DDSpan openAiSpan = awaitOpenAiSpan("/v1/embeddings");
359+
assertNoGenAiTags(awaitOpenAiSpan("/v1/embeddings"));
360+
}
372361

373-
assertEquals("embedding", openAiSpan.getTag("gen_ai.operation.name"));
374-
assertEquals(
375-
openAiSpan.getTag("openai.request.model"), openAiSpan.getTag("gen_ai.request.model"));
376-
assertEquals("openai", openAiSpan.getTag("gen_ai.provider.name"));
362+
private static void assertNoGenAiTags(DDSpan span) {
363+
// The endpoint and model the span was selected by are APM tags of their own, still present.
364+
assertNotNull(span.getTag("openai.request.model"));
365+
assertTrue(
366+
span.getTags().keySet().stream().noneMatch(key -> key.startsWith("gen_ai.")),
367+
"openai.request span should carry no gen_ai.* tags");
368+
assertNull(span.getTag("_ml_obs_tag.span.kind"));
377369
}
378370

379371
// Both tests here produce an openai.request span, so match on the endpoint rather than take the

‎internal-api/src/main/java/datadog/trace/api/llmobs/GenAiApmTags.java‎

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,16 @@ public final class GenAiApmTags {
4646
{LLMOBS_METRIC_PREFIX + "reasoning_output_tokens", USAGE_REASONING_OUTPUT_TOKENS},
4747
};
4848

49-
public static void apply(AgentSpan span) {
50-
apply(span, null, null, null);
51-
}
52-
5349
/**
5450
* Writes the attributes onto a span that is not yet finished, reading them from its {@code
55-
* _ml_obs_tag.} / {@code _ml_obs_metric.} tags. The arguments take precedence over those tags and
56-
* cover instrumentation that traces with LLM Observability disabled, where they are not all set.
57-
* No-op for a span with no resolvable operation.
51+
* _ml_obs_tag.} / {@code _ml_obs_metric.} tags. No-op for a span with no LLM Observability span
52+
* kind.
5853
*/
59-
public static void apply(AgentSpan span, String operationName, String modelName, String mlApp) {
54+
public static void apply(AgentSpan span) {
6055
if (span == null) {
6156
return;
6257
}
63-
String operation = firstNonEmpty(operationName, stringTag(span, SPAN_KIND_TAG));
58+
String operation = stringTag(span, SPAN_KIND_TAG);
6459
if (operation == null) {
6560
return;
6661
}
@@ -70,7 +65,7 @@ public static void apply(AgentSpan span, String operationName, String modelName,
7065
Tags.LLMOBS_LLM_SPAN_KIND.equals(operation)
7166
|| Tags.LLMOBS_EMBEDDING_SPAN_KIND.equals(operation);
7267

73-
String model = firstNonEmpty(modelName, stringTag(span, MODEL_NAME_TAG));
68+
String model = stringTag(span, MODEL_NAME_TAG);
7469
if (model != null || modelBacked) {
7570
span.setTag(REQUEST_MODEL, model == null ? DEFAULT_MODEL : model);
7671
}
@@ -79,7 +74,7 @@ public static void apply(AgentSpan span, String operationName, String modelName,
7974
span.setTag(
8075
PROVIDER_NAME, (provider == null ? DEFAULT_MODEL : provider).toLowerCase(Locale.ROOT));
8176
}
82-
String application = firstNonEmpty(mlApp, stringTag(span, ML_APP_TAG));
77+
String application = stringTag(span, ML_APP_TAG);
8378
if (application != null) {
8479
span.setTag(APPLICATION_NAME, application);
8580
}
@@ -100,7 +95,7 @@ public static void apply(AgentSpan span, String operationName, String modelName,
10095
}
10196

10297
/** The value of {@code key} as a non-empty string, or null. */
103-
public static String stringTag(AgentSpan span, String key) {
98+
private static String stringTag(AgentSpan span, String key) {
10499
Object value = span.getTag(key);
105100
if (value == null) {
106101
return null;
@@ -109,9 +104,5 @@ public static String stringTag(AgentSpan span, String key) {
109104
return string.isEmpty() ? null : string;
110105
}
111106

112-
private static String firstNonEmpty(String preferred, String fallback) {
113-
return preferred == null || preferred.isEmpty() ? fallback : preferred;
114-
}
115-
116107
private GenAiApmTags() {}
117108
}

‎internal-api/src/test/java/datadog/trace/api/llmobs/GenAiApmTagsTest.java‎

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -152,49 +152,6 @@ void spanWithoutLlmObsKindEmitsNothing() {
152152
@Test
153153
void nullSpanIsANoOp() {
154154
GenAiApmTags.apply(null);
155-
GenAiApmTags.apply(null, Tags.LLMOBS_LLM_SPAN_KIND, "gpt-4", "app");
156-
}
157-
158-
@Test
159-
void argumentsStandInForMissingLlmObsTags() {
160-
llmObsTag(LLMObsTags.MODEL_PROVIDER, "OpenAI");
161-
162-
GenAiApmTags.apply(span, Tags.LLMOBS_LLM_SPAN_KIND, "gpt-4", "my-app");
163-
164-
assertEquals(Tags.LLMOBS_LLM_SPAN_KIND, tags.get(GenAiApmTags.OPERATION_NAME));
165-
assertEquals("gpt-4", tags.get(GenAiApmTags.REQUEST_MODEL));
166-
assertEquals("openai", tags.get(GenAiApmTags.PROVIDER_NAME));
167-
assertEquals("my-app", tags.get(GenAiApmTags.APPLICATION_NAME));
168-
assertFalse(tags.containsKey(GenAiApmTags.CONVERSATION_ID));
169-
assertFalse(tags.containsKey(GenAiApmTags.USAGE_INPUT_TOKENS));
170-
}
171-
172-
@Test
173-
void argumentsTakePrecedenceOverLlmObsTags() {
174-
llmObsTag(Tags.SPAN_KIND, Tags.LLMOBS_WORKFLOW_SPAN_KIND);
175-
llmObsTag(LLMObsTags.MODEL_NAME, "gpt-3.5");
176-
llmObsTag(LLMObsTags.ML_APP, "tagged-app");
177-
178-
GenAiApmTags.apply(span, Tags.LLMOBS_LLM_SPAN_KIND, "gpt-4", "my-app");
179-
180-
assertEquals(Tags.LLMOBS_LLM_SPAN_KIND, tags.get(GenAiApmTags.OPERATION_NAME));
181-
assertEquals("gpt-4", tags.get(GenAiApmTags.REQUEST_MODEL));
182-
assertEquals("my-app", tags.get(GenAiApmTags.APPLICATION_NAME));
183-
}
184-
185-
@Test
186-
void modelBackedArgumentsFallBackToCustom() {
187-
GenAiApmTags.apply(span, Tags.LLMOBS_EMBEDDING_SPAN_KIND, null, "app");
188-
189-
assertEquals("custom", tags.get(GenAiApmTags.REQUEST_MODEL));
190-
assertEquals("custom", tags.get(GenAiApmTags.PROVIDER_NAME));
191-
}
192-
193-
@Test
194-
void spanWithNoResolvableOperationEmitsNothing() {
195-
GenAiApmTags.apply(span, null, "gpt-4", "my-app");
196-
197-
assertTrue(tags.keySet().stream().noneMatch(key -> key.startsWith("gen_ai.")));
198155
}
199156

200157
private void llmObsTag(String key, String value) {

0 commit comments

Comments
 (0)