From 2be1b73b0e7d71e4843db933b01234e0b16a0453 Mon Sep 17 00:00:00 2001 From: Gary Huang Date: Wed, 7 May 2025 11:08:22 -0400 Subject: [PATCH 1/2] add APIs for llm obs sdk (#8135) * add APIs for llm obs * add llm message class to support llm spans * follow java convention of naming Id instead of ID * add codeowners --- .github/CODEOWNERS | 6 + dd-trace-api/build.gradle | 6 + .../java/datadog/trace/api/llmobs/LLMObs.java | 136 ++++++++++++++++ .../datadog/trace/api/llmobs/LLMObsSpan.java | 145 ++++++++++++++++++ .../trace/api/llmobs/noop/NoOpLLMObsSpan.java | 61 ++++++++ .../llmobs/noop/NoOpLLMObsSpanFactory.java | 38 +++++ 6 files changed, 392 insertions(+) create mode 100644 dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java create mode 100644 dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsSpan.java create mode 100644 dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpan.java create mode 100644 dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpanFactory.java diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5983b5d0bd2..791377393d2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -74,3 +74,9 @@ dd-trace-core/src/main/java/datadog/trace/core/datastreams @Dat dd-trace-core/src/test/groovy/datadog/trace/core/datastreams @DataDog/data-streams-monitoring internal-api/src/main/java/datadog/trace/api/datastreams @DataDog/data-streams-monitoring internal-api/src/test/groovy/datadog/trace/api/datastreams @DataDog/data-streams-monitoring + +# @DataDog/ml-observability +dd-trace-api/src/main/java/datadog/trace/api/llmobs/ @DataDog/ml-observability +dd-java-agent/agent-llmobs/ @DataDog/ml-observability +dd-trace-core/src/main/java/datadog/trace/llmobs/ @DataDog/ml-observability +dd-trace-core/src/test/groovy/datadog/trace/llmobs/ @DataDog/ml-observability diff --git a/dd-trace-api/build.gradle b/dd-trace-api/build.gradle index c4b00313208..4175700c6ce 100644 --- a/dd-trace-api/build.gradle +++ b/dd-trace-api/build.gradle @@ -32,6 +32,12 @@ excludedClassesCoverage += [ 'datadog.trace.api.profiling.ProfilingScope', 'datadog.trace.api.profiling.ProfilingContext', 'datadog.trace.api.profiling.ProfilingContextAttribute.NoOp', + 'datadog.trace.api.llmobs.LLMObs', + 'datadog.trace.api.llmobs.LLMObs.LLMMessage', + 'datadog.trace.api.llmobs.LLMObs.ToolCall', + 'datadog.trace.api.llmobs.LLMObsSpan', + 'datadog.trace.api.llmobs.noop.NoOpLLMObsSpan', + 'datadog.trace.api.llmobs.noop.NoOpLLMObsSpanFactory', 'datadog.trace.api.experimental.DataStreamsCheckpointer', 'datadog.trace.api.experimental.DataStreamsCheckpointer.NoOp', 'datadog.trace.api.experimental.DataStreamsContextCarrier', diff --git a/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java new file mode 100644 index 00000000000..fd3e1f0a952 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java @@ -0,0 +1,136 @@ +package datadog.trace.api.llmobs; + +import datadog.trace.api.llmobs.noop.NoOpLLMObsSpanFactory; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +public class LLMObs { + protected LLMObs() {} + + protected static LLMObsSpanFactory SPAN_FACTORY = NoOpLLMObsSpanFactory.INSTANCE; + + public static LLMObsSpan startLLMSpan( + String spanName, + String modelName, + String modelProvider, + @Nullable String mlApp, + @Nullable String sessionId) { + + return SPAN_FACTORY.startLLMSpan(spanName, modelName, modelProvider, mlApp, sessionId); + } + + public static LLMObsSpan startAgentSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + + return SPAN_FACTORY.startAgentSpan(spanName, mlApp, sessionId); + } + + public static LLMObsSpan startToolSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + + return SPAN_FACTORY.startToolSpan(spanName, mlApp, sessionId); + } + + public static LLMObsSpan startTaskSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + + return SPAN_FACTORY.startTaskSpan(spanName, mlApp, sessionId); + } + + public static LLMObsSpan startWorkflowSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + + return SPAN_FACTORY.startWorkflowSpan(spanName, mlApp, sessionId); + } + + public interface LLMObsSpanFactory { + LLMObsSpan startLLMSpan( + String spanName, + String modelName, + String modelProvider, + @Nullable String mlApp, + @Nullable String sessionId); + + LLMObsSpan startAgentSpan(String spanName, @Nullable String mlApp, @Nullable String sessionId); + + LLMObsSpan startToolSpan(String spanName, @Nullable String mlApp, @Nullable String sessionId); + + LLMObsSpan startTaskSpan(String spanName, @Nullable String mlApp, @Nullable String sessionId); + + LLMObsSpan startWorkflowSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId); + } + + public static class ToolCall { + private String name; + private String type; + private String toolId; + private Map arguments; + + public static ToolCall from( + String name, String type, String toolId, Map arguments) { + return new ToolCall(name, type, toolId, arguments); + } + + private ToolCall(String name, String type, String toolId, Map arguments) { + this.name = name; + this.type = type; + this.toolId = toolId; + this.arguments = arguments; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getToolId() { + return toolId; + } + + public Map getArguments() { + return arguments; + } + } + + public static class LLMMessage { + private String role; + private String content; + private List toolCalls; + + public static LLMMessage from(String role, String content, List toolCalls) { + return new LLMMessage(role, content, toolCalls); + } + + public static LLMMessage from(String role, String content) { + return new LLMMessage(role, content); + } + + private LLMMessage(String role, String content, List toolCalls) { + this.role = role; + this.content = content; + this.toolCalls = toolCalls; + } + + private LLMMessage(String role, String content) { + this.role = role; + this.content = content; + } + + public String getRole() { + return role; + } + + public String getContent() { + return content; + } + + public List getToolCalls() { + return toolCalls; + } + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsSpan.java b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsSpan.java new file mode 100644 index 00000000000..80668eabd57 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsSpan.java @@ -0,0 +1,145 @@ +package datadog.trace.api.llmobs; + +import java.util.List; +import java.util.Map; + +/** This interface represent an individual LLM Obs span. */ +public interface LLMObsSpan { + + /** + * Annotate the span with inputs and outputs for LLM spans + * + * @param inputMessages The input messages of the span in the form of a list + * @param outputMessages The output messages of the span in the form of a list + */ + void annotateIO(List inputMessages, List outputMessages); + + /** + * Annotate the span with inputs and outputs + * + * @param inputData The input data of the span in the form of a string + * @param outputData The output data of the span in the form of a string + */ + void annotateIO(String inputData, String outputData); + + /** + * Annotate the span with metadata + * + * @param metadata A map of JSON serializable key-value pairs that contains metadata information + * relevant to the input or output operation described by the span + */ + void setMetadata(Map metadata); + + /** + * Annotate the span with metrics + * + * @param metrics A map of JSON serializable keys and numeric values that users can add as metrics + * relevant to the operation described by the span (input_tokens, output_tokens, total_tokens, + * etc.). + */ + void setMetrics(Map metrics); + + /** + * Annotate the span with a single metric key value pair for the span’s context (number of tokens + * document length, etc). + * + * @param key the name of the metric + * @param value the value of the metric + */ + void setMetric(CharSequence key, int value); + + /** + * Annotate the span with a single metric key value pair for the span’s context (number of tokens + * document length, etc). + * + * @param key the name of the metric + * @param value the value of the metric + */ + void setMetric(CharSequence key, long value); + + /** + * Annotate the span with a single metric key value pair for the span’s context (number of tokens + * document length, etc). + * + * @param key the name of the metric + * @param value the value of the metric + */ + void setMetric(CharSequence key, double value); + + /** + * Annotate the span with tags + * + * @param tags An map of JSON serializable key-value pairs that users can add as tags regarding + * the span’s context (session, environment, system, versioning, etc.). + */ + void setTags(Map tags); + + /** + * Annotate the span with a single tag key value pair as a tag regarding the span’s context + * (session, environment, system, versioning, etc.). + * + * @param key the key of the tag + * @param value the value of the tag + */ + void setTag(String key, String value); + + /** + * Annotate the span with a single tag key value pair as a tag regarding the span’s context + * (session, environment, system, versioning, etc.). + * + * @param key the key of the tag + * @param value the value of the tag + */ + void setTag(String key, boolean value); + + /** + * Annotate the span with a single tag key value pair as a tag regarding the span’s context + * (session, environment, system, versioning, etc.). + * + * @param key the key of the tag + * @param value the value of the tag + */ + void setTag(String key, int value); + + /** + * Annotate the span with a single tag key value pair as a tag regarding the span’s context + * (session, environment, system, versioning, etc.). + * + * @param key the key of the tag + * @param value the value of the tag + */ + void setTag(String key, long value); + + /** + * Annotate the span with a single tag key value pair as a tag regarding the span’s context + * (session, environment, system, versioning, etc.). + * + * @param key the key of the tag + * @param value the value of the tag + */ + void setTag(String key, double value); + + /** + * Annotate the span to indicate that an error occurred + * + * @param error whether an error occurred + */ + void setError(boolean error); + + /** + * Annotate the span with an error message + * + * @param errorMessage the message of the error + */ + void setErrorMessage(String errorMessage); + + /** + * Annotate the span with a throwable + * + * @param throwable the errored throwable + */ + void addThrowable(Throwable throwable); + + /** Finishes (closes) a span */ + void finish(); +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpan.java b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpan.java new file mode 100644 index 00000000000..a1b160616e7 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpan.java @@ -0,0 +1,61 @@ +package datadog.trace.api.llmobs.noop; + +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.api.llmobs.LLMObsSpan; +import java.util.List; +import java.util.Map; + +public class NoOpLLMObsSpan implements LLMObsSpan { + public static final LLMObsSpan INSTANCE = new NoOpLLMObsSpan(); + + @Override + public void annotateIO(List inputData, List outputData) {} + + @Override + public void annotateIO(String inputData, String outputData) {} + + @Override + public void setMetadata(Map metadata) {} + + @Override + public void setMetrics(Map metrics) {} + + @Override + public void setMetric(CharSequence key, int value) {} + + @Override + public void setMetric(CharSequence key, long value) {} + + @Override + public void setMetric(CharSequence key, double value) {} + + @Override + public void setTags(Map tags) {} + + @Override + public void setTag(String key, String value) {} + + @Override + public void setTag(String key, boolean value) {} + + @Override + public void setTag(String key, int value) {} + + @Override + public void setTag(String key, long value) {} + + @Override + public void setTag(String key, double value) {} + + @Override + public void setError(boolean error) {} + + @Override + public void setErrorMessage(String errorMessage) {} + + @Override + public void addThrowable(Throwable throwable) {} + + @Override + public void finish() {} +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpanFactory.java b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpanFactory.java new file mode 100644 index 00000000000..080aa41bd82 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/noop/NoOpLLMObsSpanFactory.java @@ -0,0 +1,38 @@ +package datadog.trace.api.llmobs.noop; + +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.api.llmobs.LLMObsSpan; +import javax.annotation.Nullable; + +public class NoOpLLMObsSpanFactory implements LLMObs.LLMObsSpanFactory { + public static final NoOpLLMObsSpanFactory INSTANCE = new NoOpLLMObsSpanFactory(); + + public LLMObsSpan startLLMSpan( + String spanName, + String modelName, + String modelProvider, + @Nullable String mlApp, + @Nullable String sessionId) { + return NoOpLLMObsSpan.INSTANCE; + } + + public LLMObsSpan startAgentSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return NoOpLLMObsSpan.INSTANCE; + } + + public LLMObsSpan startToolSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return NoOpLLMObsSpan.INSTANCE; + } + + public LLMObsSpan startTaskSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return NoOpLLMObsSpan.INSTANCE; + } + + public LLMObsSpan startWorkflowSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return NoOpLLMObsSpan.INSTANCE; + } +} From 77291d8b1a074a0462a64df585526eadbd514ab5 Mon Sep 17 00:00:00 2001 From: Gary Huang Date: Wed, 4 Jun 2025 15:35:47 -0400 Subject: [PATCH 2/2] implement LLM Obs SDK spans APIs (#8390) * add APIs for llm obs * add llm message class to support llm spans * add llm message class to support llm spans * impl llmobs agent and llmobs apis * support llm messages with tool calls * handle default model name and provider * rm unneeded file * spotless * add APIs for llm obs sdk (#8135) * add APIs for llm obs * add llm message class to support llm spans * follow java convention of naming Id instead of ID * add codeowners * rename ID to Id according to java naming conventions * Undo change to integrations-core submodule * fix build gradle * rm empty line * fix test --- .../communication/BackendApiFactory.java | 1 + .../java/datadog/trace/bootstrap/Agent.java | 46 ++- dd-java-agent/agent-llmobs/build.gradle | 42 +++ .../datadog/trace/llmobs/LLMObsSystem.java | 104 +++++++ .../trace/llmobs/domain/DDLLMObsSpan.java | 276 ++++++++++++++++++ .../trace/llmobs/domain/LLMObsInternal.java | 10 + .../llmobs/domain/DDLLMObsSpanTest.groovy | 267 +++++++++++++++++ dd-java-agent/build.gradle | 1 + .../java/datadog/trace/api/DDSpanTypes.java | 2 + .../datadog/trace/api/llmobs/LLMObsTags.java | 15 + .../main/java/datadog/trace/api/Config.java | 28 ++ .../bootstrap/instrumentation/api/Tags.java | 6 + settings.gradle | 3 + 13 files changed, 800 insertions(+), 1 deletion(-) create mode 100644 dd-java-agent/agent-llmobs/build.gradle create mode 100644 dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/LLMObsSystem.java create mode 100644 dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java create mode 100644 dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/LLMObsInternal.java create mode 100644 dd-java-agent/agent-llmobs/src/test/groovy/datadog/trace/llmobs/domain/DDLLMObsSpanTest.groovy create mode 100644 dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsTags.java diff --git a/communication/src/main/java/datadog/communication/BackendApiFactory.java b/communication/src/main/java/datadog/communication/BackendApiFactory.java index bebb7b42828..f3382792baa 100644 --- a/communication/src/main/java/datadog/communication/BackendApiFactory.java +++ b/communication/src/main/java/datadog/communication/BackendApiFactory.java @@ -72,6 +72,7 @@ private HttpUrl getAgentlessUrl(Intake intake) { public enum Intake { API("api", "v2", Config::isCiVisibilityAgentlessEnabled, Config::getCiVisibilityAgentlessUrl), + LLMOBS_API("api", "v2", Config::isLlmObsAgentlessEnabled, Config::getLlMObsAgentlessUrl), LOGS( "http-intake.logs", "v2", diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 00b54848832..c1246ff6f7c 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -25,6 +25,7 @@ import datadog.trace.api.config.GeneralConfig; import datadog.trace.api.config.IastConfig; import datadog.trace.api.config.JmxFetchConfig; +import datadog.trace.api.config.LlmObsConfig; import datadog.trace.api.config.ProfilingConfig; import datadog.trace.api.config.RemoteConfigConfig; import datadog.trace.api.config.TraceInstrumentationConfig; @@ -41,6 +42,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI; import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; +import datadog.trace.bootstrap.instrumentation.api.WriterConstants; import datadog.trace.bootstrap.instrumentation.jfr.InstrumentationBasedProfiling; import datadog.trace.util.AgentTaskScheduler; import datadog.trace.util.AgentThreadFactory.AgentThread; @@ -109,7 +111,9 @@ private enum AgentFeature { EXCEPTION_REPLAY(DebuggerConfig.EXCEPTION_REPLAY_ENABLED, false), CODE_ORIGIN(TraceInstrumentationConfig.CODE_ORIGIN_FOR_SPANS_ENABLED, false), DATA_JOBS(GeneralConfig.DATA_JOBS_ENABLED, false), - AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false); + AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false), + LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false), + LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false); private final String configKey; private final String systemProp; @@ -156,6 +160,8 @@ public boolean isEnabledByDefault() { private static boolean iastFullyDisabled; private static boolean cwsEnabled = false; private static boolean ciVisibilityEnabled = false; + private static boolean llmObsEnabled = false; + private static boolean llmObsAgentlessEnabled = false; private static boolean usmEnabled = false; private static boolean telemetryEnabled = true; private static boolean dynamicInstrumentationEnabled = false; @@ -290,6 +296,25 @@ public static void start( exceptionReplayEnabled = isFeatureEnabled(AgentFeature.EXCEPTION_REPLAY); codeOriginEnabled = isFeatureEnabled(AgentFeature.CODE_ORIGIN); agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION); + llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS); + + // setup writers when llmobs is enabled to accomodate apm and llmobs + if (llmObsEnabled) { + // for llm obs spans, use agent proxy by default, apm spans will use agent writer + setSystemPropertyDefault( + propertyNameToSystemPropertyName(TracerConfig.WRITER_TYPE), + WriterConstants.MULTI_WRITER_TYPE + + ":" + + WriterConstants.DD_INTAKE_WRITER_TYPE + + "," + + WriterConstants.DD_AGENT_WRITER_TYPE); + if (llmObsAgentlessEnabled) { + // use API writer only + setSystemPropertyDefault( + propertyNameToSystemPropertyName(TracerConfig.WRITER_TYPE), + WriterConstants.DD_INTAKE_WRITER_TYPE); + } + } patchJPSAccess(inst); @@ -597,6 +622,7 @@ public void execute() { maybeStartAppSec(scoClass, sco); maybeStartCiVisibility(instrumentation, scoClass, sco); + maybeStartLLMObs(instrumentation, scoClass, sco); // start debugger before remote config to subscribe to it before starting to poll maybeStartDebugger(instrumentation, scoClass, sco); maybeStartRemoteConfig(scoClass, sco); @@ -952,6 +978,24 @@ private static void maybeStartCiVisibility(Instrumentation inst, Class scoCla } } + private static void maybeStartLLMObs(Instrumentation inst, Class scoClass, Object sco) { + if (llmObsEnabled) { + StaticEventLogger.begin("LLM Observability"); + + try { + final Class llmObsSysClass = + AGENT_CLASSLOADER.loadClass("datadog.trace.llmobs.LLMObsSystem"); + final Method llmObsInstallerMethod = + llmObsSysClass.getMethod("start", Instrumentation.class, scoClass); + llmObsInstallerMethod.invoke(null, inst, sco); + } catch (final Throwable e) { + log.warn("Not starting LLM Observability subsystem", e); + } + + StaticEventLogger.end("LLM Observability"); + } + } + private static void maybeInstallLogsIntake(Class scoClass, Object sco) { if (agentlessLogSubmissionEnabled) { StaticEventLogger.begin("Logs Intake"); diff --git a/dd-java-agent/agent-llmobs/build.gradle b/dd-java-agent/agent-llmobs/build.gradle new file mode 100644 index 00000000000..5e4d7cd6a78 --- /dev/null +++ b/dd-java-agent/agent-llmobs/build.gradle @@ -0,0 +1,42 @@ +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath group: 'org.jetbrains.kotlin', name: 'kotlin-gradle-plugin', version: libs.versions.kotlin.get() + } +} + +plugins { + id 'com.gradleup.shadow' + id 'java-test-fixtures' +} + +apply from: "$rootDir/gradle/java.gradle" +apply from: "$rootDir/gradle/version.gradle" +apply from: "$rootDir/gradle/test-with-kotlin.gradle" + +minimumBranchCoverage = 0.0 +minimumInstructionCoverage = 0.0 + +dependencies { + api libs.slf4j + + implementation project(':communication') + implementation project(':components:json') + implementation project(':internal-api') + + testImplementation project(":utils:test-utils") + + testFixturesApi project(':dd-java-agent:testing') + testFixturesApi project(':utils:test-utils') +} + +shadowJar { + dependencies deps.excludeShared +} + +jar { + archiveClassifier = 'unbundled' +} diff --git a/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/LLMObsSystem.java b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/LLMObsSystem.java new file mode 100644 index 00000000000..fbfef5c771f --- /dev/null +++ b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/LLMObsSystem.java @@ -0,0 +1,104 @@ +package datadog.trace.llmobs; + +import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.trace.api.Config; +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.api.llmobs.LLMObsSpan; +import datadog.trace.api.llmobs.LLMObsTags; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.llmobs.domain.DDLLMObsSpan; +import datadog.trace.llmobs.domain.LLMObsInternal; +import java.lang.instrument.Instrumentation; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LLMObsSystem { + + private static final Logger LOGGER = LoggerFactory.getLogger(LLMObsSystem.class); + + private static final String CUSTOM_MODEL_VAL = "custom"; + + public static void start(Instrumentation inst, SharedCommunicationObjects sco) { + Config config = Config.get(); + if (!config.isLlmObsEnabled()) { + LOGGER.debug("LLM Observability is disabled"); + return; + } + + sco.createRemaining(config); + + LLMObsInternal.setLLMObsSpanFactory( + new LLMObsManualSpanFactory(config.getLlmObsMlApp(), config.getServiceName())); + } + + private static class LLMObsManualSpanFactory implements LLMObs.LLMObsSpanFactory { + + private final String serviceName; + private final String defaultMLApp; + + public LLMObsManualSpanFactory(String defaultMLApp, String serviceName) { + this.defaultMLApp = defaultMLApp; + this.serviceName = serviceName; + } + + @Override + public LLMObsSpan startLLMSpan( + String spanName, + String modelName, + String modelProvider, + @Nullable String mlApp, + @Nullable String sessionId) { + + DDLLMObsSpan span = + new DDLLMObsSpan( + Tags.LLMOBS_LLM_SPAN_KIND, spanName, getMLApp(mlApp), sessionId, serviceName); + + if (modelName == null || modelName.isEmpty()) { + modelName = CUSTOM_MODEL_VAL; + } + span.setTag(LLMObsTags.MODEL_NAME, modelName); + + if (modelProvider == null || modelProvider.isEmpty()) { + modelProvider = CUSTOM_MODEL_VAL; + } + span.setTag(LLMObsTags.MODEL_PROVIDER, modelProvider); + return span; + } + + @Override + public LLMObsSpan startAgentSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return new DDLLMObsSpan( + Tags.LLMOBS_AGENT_SPAN_KIND, spanName, getMLApp(mlApp), sessionId, serviceName); + } + + @Override + public LLMObsSpan startToolSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return new DDLLMObsSpan( + Tags.LLMOBS_TOOL_SPAN_KIND, spanName, getMLApp(mlApp), sessionId, serviceName); + } + + @Override + public LLMObsSpan startTaskSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return new DDLLMObsSpan( + Tags.LLMOBS_TASK_SPAN_KIND, spanName, getMLApp(mlApp), sessionId, serviceName); + } + + @Override + public LLMObsSpan startWorkflowSpan( + String spanName, @Nullable String mlApp, @Nullable String sessionId) { + return new DDLLMObsSpan( + Tags.LLMOBS_WORKFLOW_SPAN_KIND, spanName, getMLApp(mlApp), sessionId, serviceName); + } + + private String getMLApp(String mlApp) { + if (mlApp == null || mlApp.isEmpty()) { + return defaultMLApp; + } + return mlApp; + } + } +} diff --git a/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java new file mode 100644 index 00000000000..734bf1df526 --- /dev/null +++ b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java @@ -0,0 +1,276 @@ +package datadog.trace.llmobs.domain; + +import datadog.trace.api.DDSpanTypes; +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.api.llmobs.LLMObsSpan; +import datadog.trace.api.llmobs.LLMObsTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DDLLMObsSpan implements LLMObsSpan { + private static final String LLM_MESSAGE_UNKNOWN_ROLE = "unknown"; + + // Well known tags for LLM obs will be prefixed with _ml_obs_(tags|metrics). + // Prefix for tags + private static final String LLMOBS_TAG_PREFIX = "_ml_obs_tag."; + // Prefix for metrics + private static final String LLMOBS_METRIC_PREFIX = "_ml_obs_metric."; + + // internal tags to be prefixed + private static final String INPUT = LLMOBS_TAG_PREFIX + "input"; + private static final String OUTPUT = LLMOBS_TAG_PREFIX + "output"; + private static final String SPAN_KIND = LLMOBS_TAG_PREFIX + Tags.SPAN_KIND; + private static final String METADATA = LLMOBS_TAG_PREFIX + LLMObsTags.METADATA; + + private static final String LLM_OBS_INSTRUMENTATION_NAME = "llmobs"; + + private static final Logger LOGGER = LoggerFactory.getLogger(DDLLMObsSpan.class); + + private final AgentSpan span; + private final String spanKind; + + private boolean finished = false; + + public DDLLMObsSpan( + @Nonnull String kind, + String spanName, + @Nonnull String mlApp, + String sessionId, + @Nonnull String serviceName) { + + if (null == spanName || spanName.isEmpty()) { + spanName = kind; + } + + AgentTracer.SpanBuilder spanBuilder = + AgentTracer.get() + .buildSpan(LLM_OBS_INSTRUMENTATION_NAME, spanName) + .withServiceName(serviceName) + .withSpanType(DDSpanTypes.LLMOBS); + + this.span = spanBuilder.start(); + this.span.setTag(SPAN_KIND, kind); + this.spanKind = kind; + this.span.setTag(LLMOBS_TAG_PREFIX + LLMObsTags.ML_APP, mlApp); + if (sessionId != null && !sessionId.isEmpty()) { + this.span.setTag(LLMOBS_TAG_PREFIX + LLMObsTags.SESSION_ID, sessionId); + } + } + + @Override + public String toString() { + return super.toString() + + ", trace_id=" + + this.span.context().getTraceId() + + ", span_id=" + + this.span.context().getSpanId() + + ", ml_app=" + + this.span.getTag(LLMObsTags.ML_APP) + + ", service=" + + this.span.getServiceName() + + ", span_kind=" + + this.span.getTag(SPAN_KIND); + } + + @Override + public void annotateIO(List inputData, List outputData) { + if (finished) { + return; + } + if (inputData != null && !inputData.isEmpty()) { + this.span.setTag(INPUT, inputData); + } + if (outputData != null && !outputData.isEmpty()) { + this.span.setTag(OUTPUT, outputData); + } + } + + @Override + public void annotateIO(String inputData, String outputData) { + if (finished) { + return; + } + boolean wrongSpanKind = false; + if (inputData != null && !inputData.isEmpty()) { + if (Tags.LLMOBS_LLM_SPAN_KIND.equals(this.spanKind)) { + wrongSpanKind = true; + annotateIO( + Collections.singletonList(LLMObs.LLMMessage.from(LLM_MESSAGE_UNKNOWN_ROLE, inputData)), + null); + } else { + this.span.setTag(INPUT, inputData); + } + } + if (outputData != null && !outputData.isEmpty()) { + if (Tags.LLMOBS_LLM_SPAN_KIND.equals(this.spanKind)) { + wrongSpanKind = true; + annotateIO( + null, + Collections.singletonList( + LLMObs.LLMMessage.from(LLM_MESSAGE_UNKNOWN_ROLE, outputData))); + } else { + this.span.setTag(OUTPUT, outputData); + } + } + if (wrongSpanKind) { + LOGGER.warn( + "the span being annotated is an LLM span, it is recommended to use the overload with List as arguments"); + } + } + + @Override + public void setMetadata(Map metadata) { + if (finished) { + return; + } + Object value = span.getTag(METADATA); + if (value == null) { + this.span.setTag(METADATA, new HashMap<>(metadata)); + return; + } + + if (value instanceof Map) { + ((Map) value).putAll(metadata); + } else { + LOGGER.debug( + "unexpected instance type for metadata {}, overwriting for now", + value.getClass().getName()); + this.span.setTag(METADATA, new HashMap<>(metadata)); + } + } + + @Override + public void setMetrics(Map metrics) { + if (finished) { + return; + } + for (Map.Entry entry : metrics.entrySet()) { + this.span.setMetric(LLMOBS_METRIC_PREFIX + entry.getKey(), entry.getValue().doubleValue()); + } + } + + @Override + public void setMetric(CharSequence key, int value) { + if (finished) { + return; + } + this.span.setMetric(LLMOBS_METRIC_PREFIX + key, value); + } + + @Override + public void setMetric(CharSequence key, long value) { + if (finished) { + return; + } + this.span.setMetric(LLMOBS_METRIC_PREFIX + key, value); + } + + @Override + public void setMetric(CharSequence key, double value) { + if (finished) { + return; + } + this.span.setMetric(LLMOBS_METRIC_PREFIX + key, value); + } + + @Override + public void setTags(Map tags) { + if (finished) { + return; + } + if (tags != null && !tags.isEmpty()) { + for (Map.Entry entry : tags.entrySet()) { + this.span.setTag(LLMOBS_TAG_PREFIX + entry.getKey(), entry.getValue()); + } + } + } + + @Override + public void setTag(String key, String value) { + if (finished) { + return; + } + this.span.setTag(LLMOBS_TAG_PREFIX + key, value); + } + + @Override + public void setTag(String key, boolean value) { + if (finished) { + return; + } + this.span.setTag(LLMOBS_TAG_PREFIX + key, value); + } + + @Override + public void setTag(String key, int value) { + if (finished) { + return; + } + this.span.setTag(LLMOBS_TAG_PREFIX + key, value); + } + + @Override + public void setTag(String key, long value) { + if (finished) { + return; + } + this.span.setTag(LLMOBS_TAG_PREFIX + key, value); + } + + @Override + public void setTag(String key, double value) { + if (finished) { + return; + } + this.span.setTag(LLMOBS_TAG_PREFIX + key, value); + } + + @Override + public void setError(boolean error) { + if (finished) { + return; + } + this.span.setError(error); + } + + @Override + public void setErrorMessage(String errorMessage) { + if (finished) { + return; + } + if (errorMessage == null || errorMessage.isEmpty()) { + return; + } + this.span.setError(true); + this.span.setErrorMessage(errorMessage); + } + + @Override + public void addThrowable(Throwable throwable) { + if (finished) { + return; + } + if (throwable == null) { + return; + } + this.span.setError(true); + this.span.addThrowable(throwable); + } + + @Override + public void finish() { + if (finished) { + return; + } + this.span.finish(); + this.finished = true; + } +} diff --git a/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/LLMObsInternal.java b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/LLMObsInternal.java new file mode 100644 index 00000000000..42b0c097e48 --- /dev/null +++ b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/LLMObsInternal.java @@ -0,0 +1,10 @@ +package datadog.trace.llmobs.domain; + +import datadog.trace.api.llmobs.LLMObs; + +public class LLMObsInternal extends LLMObs { + + public static void setLLMObsSpanFactory(final LLMObsSpanFactory factory) { + LLMObs.SPAN_FACTORY = factory; + } +} diff --git a/dd-java-agent/agent-llmobs/src/test/groovy/datadog/trace/llmobs/domain/DDLLMObsSpanTest.groovy b/dd-java-agent/agent-llmobs/src/test/groovy/datadog/trace/llmobs/domain/DDLLMObsSpanTest.groovy new file mode 100644 index 00000000000..a6c8534be2b --- /dev/null +++ b/dd-java-agent/agent-llmobs/src/test/groovy/datadog/trace/llmobs/domain/DDLLMObsSpanTest.groovy @@ -0,0 +1,267 @@ +package datadog.trace.llmobs.domain + +import datadog.trace.agent.tooling.TracerInstaller +import datadog.trace.api.DDTags +import datadog.trace.api.IdGenerationStrategy +import datadog.trace.api.llmobs.LLMObs +import datadog.trace.api.llmobs.LLMObsSpan +import datadog.trace.api.llmobs.LLMObsTags +import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.AgentTracer +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.core.CoreTracer +import datadog.trace.test.util.DDSpecification +import org.apache.groovy.util.Maps +import spock.lang.Shared + +class DDLLMObsSpanTest extends DDSpecification{ + @SuppressWarnings('PropertyName') + @Shared + AgentTracer.TracerAPI TEST_TRACER + + void setupSpec() { + TEST_TRACER = + Spy( + CoreTracer.builder() + .idGenerationStrategy(IdGenerationStrategy.fromName("SEQUENTIAL")) + .build()) + TracerInstaller.forceInstallGlobalTracer(TEST_TRACER) + + TEST_TRACER.startSpan(*_) >> { + def agentSpan = callRealMethod() + agentSpan + } + } + + void cleanupSpec() { + TEST_TRACER?.close() + } + + void setup() { + assert TEST_TRACER.activeSpan() == null: "Span is active before test has started: " + TEST_TRACER.activeSpan() + TEST_TRACER.flush() + } + + void cleanup() { + TEST_TRACER.flush() + } + + // Prefix for tags + private static final String LLMOBS_TAG_PREFIX = "_ml_obs_tag." + // Prefix for metrics + private static final String LLMOBS_METRIC_PREFIX = "_ml_obs_metric." + + // internal tags to be prefixed + private static final String INPUT = LLMOBS_TAG_PREFIX + "input" + private static final String OUTPUT = LLMOBS_TAG_PREFIX + "output" + private static final String METADATA = LLMOBS_TAG_PREFIX + LLMObsTags.METADATA + + + def "test span simple"() { + setup: + def test = givenALLMObsSpan(Tags.LLMOBS_WORKFLOW_SPAN_KIND, "test-span") + + when: + def input = "test input" + def output = "test output" + // initial set + test.annotateIO(input, output) + test.setMetadata(Maps.of("sport", "baseball", "price_data", Maps.of("gpt4", 100))) + test.setMetrics(Maps.of("rank", 1)) + test.setMetric("likelihood", 0.1) + test.setTag("DOMAIN", "north-america") + test.setTags(Maps.of("bulk1", 1, "bulk2", "2")) + def errMsg = "mr brady" + test.setErrorMessage(errMsg) + + then: + def innerSpan = (AgentSpan)test.span + assert Tags.LLMOBS_WORKFLOW_SPAN_KIND.equals(innerSpan.getTag(LLMOBS_TAG_PREFIX + "span.kind")) + + assert null == innerSpan.getTag("input") + assert input.equals(innerSpan.getTag(INPUT)) + assert null == innerSpan.getTag("output") + assert output.equals(innerSpan.getTag(OUTPUT)) + + assert null == innerSpan.getTag("metadata") + def expectedMetadata = Maps.of("sport", "baseball", "price_data", Maps.of("gpt4", 100)) + assert expectedMetadata.equals(innerSpan.getTag(METADATA)) + + assert null == innerSpan.getTag("rank") + def rankMetric = innerSpan.getTag(LLMOBS_METRIC_PREFIX + "rank") + assert rankMetric instanceof Number && 1 == (int)rankMetric + + assert null == innerSpan.getTag("likelihood") + def likelihoodMetric = innerSpan.getTag(LLMOBS_METRIC_PREFIX + "likelihood") + assert likelihoodMetric instanceof Number + assert 0.1 == (double)likelihoodMetric + + assert null == innerSpan.getTag("DOMAIN") + def domain = innerSpan.getTag(LLMOBS_TAG_PREFIX + "DOMAIN") + assert domain instanceof String + assert "north-america".equals((String)domain) + + assert null == innerSpan.getTag("bulk1") + def tagBulk1 = innerSpan.getTag(LLMOBS_TAG_PREFIX + "bulk1") + assert tagBulk1 instanceof Number + assert 1 == ((int)tagBulk1) + + assert null == innerSpan.getTag("bulk2") + def tagBulk2 = innerSpan.getTag(LLMOBS_TAG_PREFIX + "bulk2") + assert tagBulk2 instanceof String + assert "2".equals((String)tagBulk2) + + assert innerSpan.isError() + assert innerSpan.getTag(DDTags.ERROR_MSG) instanceof String + assert errMsg.equals(innerSpan.getTag(DDTags.ERROR_MSG)) + } + + def "test span with overwrites"() { + setup: + def test = givenALLMObsSpan(Tags.LLMOBS_AGENT_SPAN_KIND, "test-span") + + when: + def input = "test input" + // initial set + test.annotateIO(input, "test output") + // this should be a no-op + test.annotateIO("", "") + // this should replace the initial output + def expectedOutput = Arrays.asList(Maps.of("role", "user", "content", "how much is gas")) + test.annotateIO(null, expectedOutput) + + // initial set + test.setMetadata(Maps.of("sport", "baseball", "price_data", Maps.of("gpt4", 100))) + // this should replace baseball with hockey + test.setMetadata(Maps.of("sport", "hockey")) + // this should add a new key + test.setMetadata(Maps.of("temperature", 30)) + + // initial set + test.setMetrics(Maps.of("rank", 1)) + // this should replace the metric + test.setMetric("rank", 10) + + // initial set + test.setTag("DOMAIN", "north-america") + // add and replace + test.setTags(Maps.of("bulk1", 1, "DOMAIN", "europe")) + + def throwableMsg = "false positive" + test.addThrowable(new Throwable(throwableMsg)) + test.setError(false) + + then: + def innerSpan = (AgentSpan)test.span + assert Tags.LLMOBS_AGENT_SPAN_KIND.equals(innerSpan.getTag(LLMOBS_TAG_PREFIX + "span.kind")) + + assert null == innerSpan.getTag("input") + assert input.equals(innerSpan.getTag(INPUT)) + assert null == innerSpan.getTag("output") + assert expectedOutput.equals(innerSpan.getTag(OUTPUT)) + + assert null == innerSpan.getTag("metadata") + def expectedMetadata = Maps.of("sport", "hockey", "price_data", Maps.of("gpt4", 100), "temperature", 30) + assert expectedMetadata.equals(innerSpan.getTag(METADATA)) + + assert null == innerSpan.getTag("rank") + def rankMetric = innerSpan.getTag(LLMOBS_METRIC_PREFIX + "rank") + assert rankMetric instanceof Number && 10 == (int)rankMetric + + assert null == innerSpan.getTag("DOMAIN") + def domain = innerSpan.getTag(LLMOBS_TAG_PREFIX + "DOMAIN") + assert domain instanceof String + assert "europe".equals((String)domain) + + assert null == innerSpan.getTag("bulk1") + def tagBulk1 = innerSpan.getTag(LLMOBS_TAG_PREFIX + "bulk1") + assert tagBulk1 instanceof Number + assert 1 == ((int)tagBulk1) + + assert !innerSpan.isError() + assert innerSpan.getTag(DDTags.ERROR_MSG) instanceof String + assert throwableMsg.equals(innerSpan.getTag(DDTags.ERROR_MSG)) + assert innerSpan.getTag(DDTags.ERROR_STACK) instanceof String + assert ((String)innerSpan.getTag(DDTags.ERROR_STACK)).contains(throwableMsg) + } + + def "test llm span string input formatted to messages"() { + setup: + def test = givenALLMObsSpan(Tags.LLMOBS_LLM_SPAN_KIND, "test-span") + + when: + def input = "test input" + def output = "test output" + // initial set + test.annotateIO(input, output) + + then: + def innerSpan = (AgentSpan)test.span + assert Tags.LLMOBS_LLM_SPAN_KIND.equals(innerSpan.getTag(LLMOBS_TAG_PREFIX + "span.kind")) + + assert null == innerSpan.getTag("input") + def spanInput = innerSpan.getTag(INPUT) + assert spanInput instanceof List + assert ((List)spanInput).size() == 1 + assert spanInput.get(0) instanceof LLMObs.LLMMessage + def expectedInputMsg = LLMObs.LLMMessage.from("unknown", input) + assert expectedInputMsg.getContent().equals(input) + assert expectedInputMsg.getRole().equals("unknown") + assert expectedInputMsg.getToolCalls().equals(null) + + assert null == innerSpan.getTag("output") + def spanOutput = innerSpan.getTag(OUTPUT) + assert spanOutput instanceof List + assert ((List)spanOutput).size() == 1 + assert spanOutput.get(0) instanceof LLMObs.LLMMessage + def expectedOutputMsg = LLMObs.LLMMessage.from("unknown", output) + assert expectedOutputMsg.getContent().equals(output) + assert expectedOutputMsg.getRole().equals("unknown") + assert expectedOutputMsg.getToolCalls().equals(null) + } + + def "test llm span with messages"() { + setup: + def test = givenALLMObsSpan(Tags.LLMOBS_LLM_SPAN_KIND, "test-span") + + when: + def inputMsg = LLMObs.LLMMessage.from("user", "input") + def outputMsg = LLMObs.LLMMessage.from("assistant", "output", Arrays.asList(LLMObs.ToolCall.from("weather-tool", "function", "6176241000", Maps.of("location", "paris")))) + // initial set + test.annotateIO(Arrays.asList(inputMsg), Arrays.asList(outputMsg)) + + then: + def innerSpan = (AgentSpan)test.span + assert Tags.LLMOBS_LLM_SPAN_KIND.equals(innerSpan.getTag(LLMOBS_TAG_PREFIX + "span.kind")) + + assert null == innerSpan.getTag("input") + def spanInput = innerSpan.getTag(INPUT) + assert spanInput instanceof List + assert ((List)spanInput).size() == 1 + def spanInputMsg = spanInput.get(0) + assert spanInputMsg instanceof LLMObs.LLMMessage + assert spanInputMsg.getContent().equals(inputMsg.getContent()) + assert spanInputMsg.getRole().equals("user") + assert spanInputMsg.getToolCalls().equals(null) + + assert null == innerSpan.getTag("output") + def spanOutput = innerSpan.getTag(OUTPUT) + assert spanOutput instanceof List + assert ((List)spanOutput).size() == 1 + def spanOutputMsg = spanOutput.get(0) + assert spanOutputMsg instanceof LLMObs.LLMMessage + assert spanOutputMsg.getContent().equals(outputMsg.getContent()) + assert spanOutputMsg.getRole().equals("assistant") + assert spanOutputMsg.getToolCalls().size() == 1 + def toolCall = spanOutputMsg.getToolCalls().get(0) + assert toolCall.getName().equals("weather-tool") + assert toolCall.getType().equals("function") + assert toolCall.getToolId().equals("6176241000") + def expectedToolArgs = Maps.of("location", "paris") + assert toolCall.getArguments().equals(expectedToolArgs) + } + + private LLMObsSpan givenALLMObsSpan(String kind, name){ + new DDLLMObsSpan(kind, name, "test-ml-app", null, "test-svc") + } +} diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle index 1b2c3804acb..b75c6030059 100644 --- a/dd-java-agent/build.gradle +++ b/dd-java-agent/build.gradle @@ -135,6 +135,7 @@ includeSubprojShadowJar ':dd-java-agent:appsec', 'appsec' includeSubprojShadowJar ':dd-java-agent:agent-iast', 'iast' includeSubprojShadowJar ':dd-java-agent:agent-debugger', 'debugger' includeSubprojShadowJar ':dd-java-agent:agent-ci-visibility', 'ci-visibility' +includeSubprojShadowJar ':dd-java-agent:agent-llmobs', 'llm-obs' includeSubprojShadowJar ':dd-java-agent:agent-logs-intake', 'logs-intake' includeSubprojShadowJar ':dd-java-agent:cws-tls', 'cws-tls' diff --git a/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java b/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java index 335dde0effe..04f9738c643 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java @@ -40,4 +40,6 @@ public class DDSpanTypes { public static final String WEBSOCKET = "websocket"; public static final String SERVERLESS = "serverless"; + + public static final String LLMOBS = "llm"; } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsTags.java b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsTags.java new file mode 100644 index 00000000000..afa4f2b241e --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObsTags.java @@ -0,0 +1,15 @@ +package datadog.trace.api.llmobs; + +// Well known tags for llm obs +public class LLMObsTags { + public static final String ML_APP = "ml_app"; + public static final String SESSION_ID = "session_id"; + + // meta + public static final String METADATA = "metadata"; + + // LLM spans related + public static final String MODEL_NAME = "model_name"; + public static final String MODEL_VERSION = "model_version"; + public static final String MODEL_PROVIDER = "model_provider"; +} 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 6f3041ca7d8..07ff2d148e8 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -320,6 +320,7 @@ public static String getHostName() { private final int iastDbRowsToTaint; private final boolean llmObsAgentlessEnabled; + private final String llmObsAgentlessUrl; private final String llmObsMlApp; private final boolean ciVisibilityTraceSanitationEnabled; @@ -1460,6 +1461,22 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) configProvider.getBoolean(LLMOBS_AGENTLESS_ENABLED, DEFAULT_LLM_OBS_AGENTLESS_ENABLED); llmObsMlApp = configProvider.getString(LLMOBS_ML_APP); + final String llmObsAgentlessUrlStr = getFinalLLMObsUrl(); + URI parsedLLMObsUri = null; + if (llmObsAgentlessUrlStr != null && !llmObsAgentlessUrlStr.isEmpty()) { + try { + parsedLLMObsUri = new URL(llmObsAgentlessUrlStr).toURI(); + } catch (MalformedURLException | URISyntaxException ex) { + log.error( + "Cannot parse LLM Observability agentless URL '{}', skipping", llmObsAgentlessUrlStr); + } + } + if (parsedLLMObsUri != null) { + llmObsAgentlessUrl = llmObsAgentlessUrlStr; + } else { + llmObsAgentlessUrl = null; + } + ciVisibilityTraceSanitationEnabled = configProvider.getBoolean(CIVISIBILITY_TRACE_SANITATION_ENABLED, true); @@ -2891,6 +2908,10 @@ public boolean isLlmObsAgentlessEnabled() { return llmObsAgentlessEnabled; } + public String getLlMObsAgentlessUrl() { + return llmObsAgentlessUrl; + } + public String getLlmObsMlApp() { return llmObsMlApp; } @@ -3968,6 +3989,13 @@ public String getFinalProfilingUrl() { } } + public String getFinalLLMObsUrl() { + if (llmObsAgentlessEnabled) { + return "https://llmobs-intake." + site + "/api/v2/llmobs"; + } + return null; + } + public String getFinalCrashTrackingTelemetryUrl() { if (crashTrackingAgentless) { // when agentless crashTracking is turned on we send directly to our intake diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java index 78c90b312a8..fd11b2bf565 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java @@ -152,4 +152,10 @@ public class Tags { public static final String PROPAGATED_TRACE_SOURCE = "_dd.p.ts"; public static final String PROPAGATED_DEBUG = "_dd.p.debug"; + + public static final String LLMOBS_LLM_SPAN_KIND = "llm"; + public static final String LLMOBS_WORKFLOW_SPAN_KIND = "workflow"; + public static final String LLMOBS_TASK_SPAN_KIND = "task"; + public static final String LLMOBS_AGENT_SPAN_KIND = "agent"; + public static final String LLMOBS_TOOL_SPAN_KIND = "tool"; } diff --git a/settings.gradle b/settings.gradle index db584c29b13..3218b1cdb50 100644 --- a/settings.gradle +++ b/settings.gradle @@ -99,6 +99,9 @@ include ':dd-java-agent:appsec' // ci-visibility include ':dd-java-agent:agent-ci-visibility' +// llm-observability +include ':dd-java-agent:agent-llmobs' + // iast include ':dd-java-agent:agent-iast'