Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions ddtrace/internal/settings/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,16 @@ def _default_config() -> dict[str, _ConfigItem]:
envs=["DD_APPSEC_SCA_ENABLED"],
modifier=asbool,
),
"_llmobs_enabled": _ConfigItem(
default=False,
envs=["DD_LLMOBS_ENABLED"],
modifier=asbool,
),
"_llmobs_ml_app": _ConfigItem(
default=None,
envs=["DD_LLMOBS_ML_APP"],
modifier=lambda x: x,
),
}


Expand Down Expand Up @@ -663,9 +673,7 @@ def __init__(self) -> None:
self._dd_app_key = _get_config("DD_APP_KEY", report_telemetry=False)
self._dd_site = _get_config("DD_SITE", "datadoghq.com")

self._llmobs_enabled = _get_config("DD_LLMOBS_ENABLED", False, asbool)
self._llmobs_sample_rate = _get_config("DD_LLMOBS_SAMPLE_RATE", 1.0, float)
self._llmobs_ml_app = _get_config("DD_LLMOBS_ML_APP")
self._llmobs_agentless_enabled = _get_config("DD_LLMOBS_AGENTLESS_ENABLED", None, asbool)
self._llmobs_instrumented_proxy_urls = _get_config(
"DD_LLMOBS_INSTRUMENTED_PROXY_URLS", None, lambda x: set(x.strip().split(","))
Expand Down
21 changes: 15 additions & 6 deletions ddtrace/llmobs/_llmobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@
log = get_logger(__name__)


SUPPORTED_LLMOBS_INTEGRATIONS = {
SUPPORTED_LLMOBS_INTEGRATIONS: dict[str, str] = {
"anthropic": "anthropic",
"bedrock": "botocore",
"openai": "openai",
Expand All @@ -194,7 +194,10 @@
"mcp": "mcp",
"pydantic_ai": "pydantic_ai",
"claude_agent_sdk": "claude_agent_sdk",
# requests/concurrent frameworks for distributed injection/extraction
}

# requests/concurrent frameworks for distributed injection/extraction
_INTEGRATIONS_W_PROPAGATION_SUPPORT: dict[str, str] = {
"requests": "requests",
"httpx": "httpx",
"urllib3": "urllib3",
Expand Down Expand Up @@ -1725,9 +1728,12 @@ def register_processor(cls, processor: Optional[Callable[[LLMObsSpan], Optional[

@classmethod
def _integration_is_enabled(cls, integration: str) -> bool:
if integration not in SUPPORTED_LLMOBS_INTEGRATIONS:
module_name = SUPPORTED_LLMOBS_INTEGRATIONS.get(
integration, _INTEGRATIONS_W_PROPAGATION_SUPPORT.get(integration)
)
if module_name is None:
return False
return SUPPORTED_LLMOBS_INTEGRATIONS[integration] in ddtrace._monkey._get_patched_modules()
return module_name in ddtrace._monkey._get_patched_modules()

@classmethod
def disable(cls) -> None:
Expand Down Expand Up @@ -1997,9 +2003,12 @@ def _patch_integrations() -> None:
"""
Patch LLM integrations. Ensure that we do not ignore DD_TRACE_<MODULE>_ENABLED or DD_PATCH_MODULES settings.
"""
llm_integrations: list[str] = list(SUPPORTED_LLMOBS_INTEGRATIONS.values()) + list(
_INTEGRATIONS_W_PROPAGATION_SUPPORT.values()
)
integrations_to_patch: dict[str, Union[list[str], bool]] = {
integration: ["bedrock-runtime", "bedrock-agent-runtime"] if integration == "botocore" else True
for integration in SUPPORTED_LLMOBS_INTEGRATIONS.values()
for integration in llm_integrations
}
for module, _ in integrations_to_patch.items():
env_var = "DD_TRACE_%s_ENABLED" % module.upper()
Expand All @@ -2008,7 +2017,7 @@ def _patch_integrations() -> None:
dd_patch_modules = os.getenv("DD_PATCH_MODULES")
dd_patch_modules_to_str = parse_tags_str(dd_patch_modules)
integrations_to_patch.update(
{k: asbool(v) for k, v in dd_patch_modules_to_str.items() if k in SUPPORTED_LLMOBS_INTEGRATIONS.values()}
{k: asbool(v) for k, v in dd_patch_modules_to_str.items() if k in llm_integrations}
)
patch(raise_errors=True, **integrations_to_patch)
llm_patched_modules = [k for k, v in integrations_to_patch.items() if v]
Expand Down
107 changes: 107 additions & 0 deletions ddtrace/llmobs/product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""LLMObs product — Remote Config support for `llmobs_enabled` / `llmobs_ml_app`.

Wires into the APM_TRACING RC product via the "apm-tracing.rc" event hub so
that operators can enable/disable LLMObs and set the ml_app name at runtime
without restarting the service.

AIDEV-NOTE: Capability bit assignments (1<<50, 1<<51) are POC placeholders
and must be confirmed with the dd-go RC backend before GA.
"""

import enum
import typing as t

from ddtrace.internal.logger import get_logger


log = get_logger(__name__)

# AIDEV-NOTE: This product depends on "apm-tracing-rc" so that the
# "apm-tracing.rc" event channel exists before apm_tracing_rc() is registered.
requires = ["remote-configuration", "apm-tracing-rc"]


class APMCapabilities(enum.IntFlag):
APM_TRACING_LLMOBS = 1 << 48


def post_preload() -> None:
pass


def start() -> None:
_track_enabled_llm_integrations()


def restart(join: bool = False) -> None:
pass


def stop(join: bool = False) -> None:
pass


def _track_enabled_llm_integrations() -> None:
"""Append detected (patched) LLM framework integrations as a RC client_tracer tag.

Only LLM framework integrations are reported so the backend can identify
this as an LLM-enabled service for 1-click enablement.
"""
try:
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller
from ddtrace.llmobs._llmobs import SUPPORTED_LLMOBS_INTEGRATIONS
from ddtrace.llmobs._llmobs import LLMObs

client = remoteconfig_poller._client
if client is None:
return

llm_integrations_enabled: list[str] = sorted(
f"llm_integrations:{LLMObs._integration_is_enabled(integration_name)}"
for integration_name in SUPPORTED_LLMOBS_INTEGRATIONS
)

existing_tags: list = list(client._client_tracer.get("tags", []))
existing_tags = [t for t in existing_tags if not t.startswith("llm_integrations:")]
existing_tags.append("llm_integrations:" + ",".join(llm_integrations_enabled))
client._client_tracer["tags"] = existing_tags
log.debug(
"Registered LLM integration tags with RC client: %s. Detected: %s", existing_tags, llm_integrations_enabled
)

except Exception:
log.debug("Failed to update RC client with LLM integration tags", exc_info=True)


def apm_tracing_rc(lib_config: dict, dd_config: t.Any) -> None:
"""Handle APM_TRACING RC payloads for LLMObs fields.

Expected lib_config shape:
llmobs.enabled (bool) — enable or disable LLMObs at runtime.
llmobs.ml_app_name (str) — ML application name; applied alongside enablement.

Config values are written via the standard remote_config source so they
participate in the normal config precedence chain.
"""
llmobs_config = lib_config.get("llmobs")
if llmobs_config is None:
return

from ddtrace.llmobs import LLMObs

rc_llmobs_enabled = llmobs_config.get("enabled")
llmobs_enabled_config = dd_config._config["_llmobs_enabled"]
llmobs_enabled_config.set_value(rc_llmobs_enabled, "remote_config")

rc_llmobs_ml_app = llmobs_config.get("ml_app_name")
llmobs_ml_app_config = dd_config._config["_llmobs_ml_app"]
llmobs_ml_app_config.set_value(rc_llmobs_ml_app, "remote_config")

if dd_config._llmobs_enabled and not LLMObs.enabled:
log.debug("Enabling LLMObs via Remote Config (ml_app=%r)", dd_config._llmobs_ml_app)
LLMObs.enable(ml_app=dd_config._llmobs_ml_app, _auto=True)
elif dd_config._llmobs_enabled is False and LLMObs.enabled:
log.debug("Disabling LLMObs via Remote Config: %r", llmobs_config)
LLMObs.disable()

_track_enabled_llm_integrations()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ datadog = "ddtrace.contrib.internal.mlflow.auth_plugin:DatadogHeaderProvider"

[project.entry-points.'ddtrace.products']
"apm-tracing-rc" = "ddtrace.internal.remoteconfig.products.apm_tracing"
"llmobs" = "ddtrace.llmobs.product"
"code-origin-for-spans" = "ddtrace.debugging._products.code_origin.span"
"dynamic-instrumentation" = "ddtrace.debugging._products.dynamic_instrumentation"
"exception-replay" = "ddtrace.debugging._products.exception_replay"
Expand Down
Loading