Skip to content

Commit d952d3d

Browse files
authored
Merge branch 'main' into main
2 parents b76cb36 + 4675bc3 commit d952d3d

File tree

13 files changed

+146
-20
lines changed

13 files changed

+146
-20
lines changed

.github/workflows/system-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
persist-credentials: false
4343
repository: 'DataDog/system-tests'
4444
# Automatically managed, use scripts/update-system-tests-version to update
45-
ref: 'e317348e48d9e934bb0f743e1537126447199dc4'
45+
ref: 'dacdb8249761abc576efde661345c23ac8731beb'
4646

4747
- name: Checkout dd-trace-py
4848
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -96,7 +96,7 @@ jobs:
9696
persist-credentials: false
9797
repository: 'DataDog/system-tests'
9898
# Automatically managed, use scripts/update-system-tests-version to update
99-
ref: 'e317348e48d9e934bb0f743e1537126447199dc4'
99+
ref: 'dacdb8249761abc576efde661345c23ac8731beb'
100100

101101
- name: Build runner
102102
uses: ./.github/actions/install_runner
@@ -277,7 +277,7 @@ jobs:
277277
persist-credentials: false
278278
repository: 'DataDog/system-tests'
279279
# Automatically managed, use scripts/update-system-tests-version to update
280-
ref: 'e317348e48d9e934bb0f743e1537126447199dc4'
280+
ref: 'dacdb8249761abc576efde661345c23ac8731beb'
281281
- name: Checkout dd-trace-py
282282
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
283283
with:

.gitlab/templates/debugging/exploration.yml

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,52 @@
1-
"debugging/exploration/boto3":
1+
".debugging/exploration":
22
stage: debugging
33
extends: .cached_testrunner
44
timeout: 30m
55
variables:
6-
DD_DEBUGGER_EXPL_INCLUDE: "boto3"
76
DD_DEBUGGER_EXPL_OUTPUT_FILE: "${{CI_PROJECT_DIR}}/debugger-expl.txt"
87
DD_DEBUGGER_EXPL_STATUS_MESSAGES: 1
98
PYTEST_PLUGINS: "exploration"
109
PYTHONPATH: "${{CI_PROJECT_DIR}}/tests/debugging/exploration/pytest"
10+
before_script:
11+
- !reference [.cached_testrunner, before_script]
12+
- python${{PYTHON_VERSION}} -m pip install -e .
13+
after_script:
14+
- cat ${{DD_DEBUGGER_EXPL_OUTPUT_FILE}}
15+
- !reference [.cached_testrunner, after_script]
16+
needs: []
17+
artifacts:
18+
paths:
19+
- ${{DD_DEBUGGER_EXPL_OUTPUT_FILE}}
20+
21+
"debugging/exploration/boto3":
22+
extends: ".debugging/exploration"
23+
variables:
24+
DD_DEBUGGER_EXPL_INCLUDE: "boto3"
25+
parallel:
26+
matrix:
27+
- PYTHON_VERSION: ["3.9", "3.10", "3.11", "3.12", "3.13"]
28+
BOTO3_TAG: 1.38.44
29+
script: |
30+
git clone --depth 1 --branch ${{BOTO3_TAG}} https://github.com/boto/boto3.git
31+
cd boto3
32+
python${{PYTHON_VERSION}} scripts/ci/install
33+
python${{PYTHON_VERSION}} scripts/ci/run-tests --test-runner 'pytest -svv -W error -W "ignore::dateutil.parser._parser.UnknownTimezoneWarning" -W "ignore::DeprecationWarning"'
34+
35+
"debugging/exploration/botocore":
36+
extends: ".debugging/exploration"
37+
timeout: 60m
38+
variables:
39+
DD_DEBUGGER_EXPL_INCLUDE: "botocore"
40+
DD_DEBUGGER_EXPL_PROFILER_DELETE_FUNCTION_PROBES: 1
41+
DD_DEBUGGER_EXPL_PROFILER_INSTRUMENTATION_RATE: 0.05
42+
DD_DEBUGGER_EXPL_COVERAGE_DELETE_LINE_PROBES: 1
43+
DD_DEBUGGER_EXPL_COVERAGE_INSTRUMENTATION_RATE: 0.05
1144
parallel:
1245
matrix:
1346
- PYTHON_VERSION: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1447
BOTO3_TAG: 1.38.44
1548
script: |
16-
python${{PYTHON_VERSION}} -m pip install -e .
1749
git clone --depth 1 --branch ${{BOTO3_TAG}} https://github.com/boto/boto3.git
1850
cd boto3
1951
python${{PYTHON_VERSION}} scripts/ci/install
2052
python${{PYTHON_VERSION}} scripts/ci/run-tests --test-runner 'pytest -svv -W error -W "ignore::dateutil.parser._parser.UnknownTimezoneWarning" -W "ignore::DeprecationWarning"'
21-
cat ${{DD_DEBUGGER_EXPL_OUTPUT_FILE}}
22-
needs: []
23-
artifacts:
24-
paths:
25-
- ${{DD_DEBUGGER_EXPL_OUTPUT_FILE}}

ddtrace/debugging/_products/code_origin/span.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import enum
2+
13
from ddtrace.internal.products import manager as product_manager
24
from ddtrace.settings._core import ValueSource
35
from ddtrace.settings.code_origin import config
@@ -14,6 +16,12 @@ def post_preload():
1416
pass
1517

1618

19+
def _start():
20+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
21+
22+
SpanCodeOriginProcessorEntry.enable()
23+
24+
1725
def start():
1826
if config.span.enabled:
1927
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
@@ -33,6 +41,12 @@ def restart(join=False):
3341
pass
3442

3543

44+
def _stop():
45+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
46+
47+
SpanCodeOriginProcessorEntry.disable()
48+
49+
3650
def stop(join=False):
3751
if config.span.enabled:
3852
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
@@ -48,3 +62,13 @@ def stop(join=False):
4862

4963
def at_exit(join=False):
5064
stop(join=join)
65+
66+
67+
class APMCapabilities(enum.IntFlag):
68+
APM_TRACING_ENABLE_CODE_ORIGIN = 1 << 40
69+
70+
71+
def apm_tracing_rc(lib_config, _config):
72+
if (enabled := lib_config.get("code_origin_enabled")) is not None:
73+
should_start = (config.span.spec.enabled.full_name not in config.source or config.span.enabled) and enabled
74+
_start() if should_start else _stop()

ddtrace/llmobs/_integrations/langchain.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,8 @@ def _llmobs_set_tags_from_chat_model(
499499
# do not append to the count, just set it once
500500
if not is_workflow and not tokens_set_top_level:
501501
tokens, run_id = self.check_token_usage_ai_message(chat_completion_msg)
502+
if run_id is None:
503+
continue
502504
input_tokens, output_tokens, total_tokens = tokens
503505
tokens_per_choice_run_id[run_id]["input_tokens"] = input_tokens
504506
tokens_per_choice_run_id[run_id]["output_tokens"] = output_tokens
@@ -726,7 +728,7 @@ def check_token_usage_chat_or_llm_result(self, result):
726728

727729
return input_tokens, output_tokens, total_tokens
728730

729-
def check_token_usage_ai_message(self, ai_message):
731+
def check_token_usage_ai_message(self, ai_message) -> Tuple[Tuple[int, int, int], Optional[str]]:
730732
"""Checks for token usage on an AI message object"""
731733
# depending on the provider + langchain-core version, the usage metadata can be in different places
732734
# either chat_completion_msg.usage_metadata or chat_completion_msg.response_metadata.{token}_usage
@@ -735,10 +737,10 @@ def check_token_usage_ai_message(self, ai_message):
735737
run_id = getattr(ai_message, "id", None) or getattr(ai_message, "run_id", "")
736738
run_id_base = "-".join(run_id.split("-")[:-1]) if run_id else ""
737739

738-
response_metadata = getattr(ai_message, "response_metadata", {}) or {}
739-
usage = usage or response_metadata.get("usage", {}) or response_metadata.get("token_usage", {})
740+
response_metadata = getattr(ai_message, "response_metadata", {})
741+
usage = usage or response_metadata.get("usage") or response_metadata.get("token_usage")
740742
if usage is None or not isinstance(usage, dict): # in case it is explicitly set to None
741-
return 0, 0, 0
743+
return (0, 0, 0), run_id_base
742744

743745
# could either be "{prompt,completion}_tokens" or "{input,output}_tokens"
744746
input_tokens = usage.get("input_tokens", 0) or usage.get("prompt_tokens", 0)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
features:
3+
- |
4+
code origin: added support for in-product enablement.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
LLM Observability: Fixed an issue where grabbing token values for some providers through ``langchain`` libraries raised a ``ValueError``.

scripts/gen_gitlab_config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,19 @@ def gen_debugger_exploration() -> None:
317317
We need to generate this dynamically from a template because it depends
318318
on the cached testrunner job, which is also generated dynamically.
319319
"""
320+
from needs_testrun import pr_matches_patterns
321+
322+
if not pr_matches_patterns(
323+
{
324+
".gitlab/templates/debugging/exploration.yml",
325+
"ddtrace/debugging/*",
326+
"ddtrace/internal/bytecode_injection/__init__.py",
327+
"ddtrace/internal/wrapping/context.py",
328+
"tests/debugging/exploration/*",
329+
}
330+
):
331+
return
332+
320333
with TESTS_GEN.open("a") as f:
321334
f.write(template("debugging/exploration"))
322335

tests/contrib/langchain/conftest.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def _openai_completion_object(
250250
def _openai_chat_completion_object(
251251
n: int = 1,
252252
tools: bool = False,
253+
include_usage: bool = True,
253254
):
254255
from datetime import datetime
255256

@@ -274,11 +275,6 @@ def _openai_chat_completion_object(
274275
object="chat.completion",
275276
choices=choices,
276277
created=int(datetime.now().timestamp()),
277-
usage=CompletionUsage(
278-
prompt_tokens=5,
279-
completion_tokens=5,
280-
total_tokens=10,
281-
),
282278
)
283279

284280
if tools:
@@ -297,6 +293,13 @@ def _openai_chat_completion_object(
297293
for choice in completion.choices:
298294
choice.message.tool_calls = [tool_call]
299295

296+
if include_usage:
297+
completion.usage = CompletionUsage(
298+
prompt_tokens=5,
299+
completion_tokens=5,
300+
total_tokens=10,
301+
)
302+
300303
return completion
301304

302305

@@ -352,6 +355,18 @@ def openai_chat_completion(respx_mock):
352355
)
353356

354357

358+
@pytest.fixture
359+
@pytest.mark.respx()
360+
def openai_chat_completion_no_usage(respx_mock):
361+
import httpx
362+
363+
completion = _openai_chat_completion_object(n=2, include_usage=False)
364+
365+
respx_mock.post("/v1/chat/completions").mock(
366+
return_value=httpx.Response(200, json=completion.model_dump(mode="json"))
367+
)
368+
369+
355370
@pytest.fixture
356371
@pytest.mark.respx()
357372
def openai_chat_completion_multiple(respx_mock):

tests/contrib/langchain/test_langchain_llmobs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ def test_llmobs_openai_chat_model(langchain_openai, llmobs_events, tracer, opena
130130
)
131131

132132

133+
def test_llmobs_openai_chat_model_no_usage(langchain_openai, llmobs_events, tracer, openai_chat_completion_no_usage):
134+
chat_model = langchain_openai.ChatOpenAI(temperature=0, max_tokens=256)
135+
chat_model.invoke([HumanMessage(content="When do you use 'who' instead of 'whom'?")])
136+
137+
assert len(llmobs_events) == 1
138+
assert llmobs_events[0]["metrics"].get("input_tokens") is None
139+
assert llmobs_events[0]["metrics"].get("output_tokens") is None
140+
assert llmobs_events[0]["metrics"].get("total_tokens") is None
141+
142+
133143
@mock.patch("langchain_core.language_models.chat_models.BaseChatModel._generate_with_cache")
134144
def test_llmobs_openai_chat_model_proxy(mock_generate, langchain_openai, llmobs_events, tracer, openai_chat_completion):
135145
mock_generate.return_value = mock_langchain_chat_generate_response

tests/debugging/exploration/_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ class ProfilerConfig(DDConfig):
101101
help="Whether to delete function probes after they are triggered",
102102
)
103103

104+
instrumentation_rate = DDConfig.v(
105+
float,
106+
"instrumentation_rate",
107+
default=1.0,
108+
help="Rate at which to instrument functions for profiling",
109+
)
110+
104111
class CoverageConfig(DDConfig):
105112
__item__ = "coverage"
106113
__prefix__ = "dd.debugger.expl.coverage"
@@ -119,5 +126,12 @@ class CoverageConfig(DDConfig):
119126
help="Whether to delete line probes after they are triggered",
120127
)
121128

129+
instrumentation_rate = DDConfig.v(
130+
float,
131+
"instrumentation_rate",
132+
default=1.0,
133+
help="Rate at which to instrument lines for coverage",
134+
)
135+
122136

123137
config = ExplorationConfig()

0 commit comments

Comments
 (0)