Skip to content

Commit 6fff239

Browse files
123liuzimingclaude
andcommitted
feat(deer-flow): add loongsuite-instrumentation-deer-flow package
Implements the hybrid LangChain-callback-reuse + 7-wrapt-monkey-patch plan from llm-dev/deer-flow/investigate/execute.md. The package covers DeerFlow-specific Entry / Agent (subagent) / Task / Sandbox / Memory spans on top of the LLM / Tool / ReAct Step spans already produced by loongsuite-instrumentation-langchain. Seven wrapt patches on public DeerFlow APIs: * runtime.runs.worker.run_agent -> ENTRY span * subagents.executor.SubagentExecutor._aexecute -> AGENT span * tools.builtins.task_tool.task_tool -> TASK span * sandbox.sandbox.Sandbox.execute_command + 5 other abstract methods -> TOOL span * sandbox.sandbox_provider.SandboxProvider.acquire/acquire_async/release -> TASK span * agents.memory.storage.FileMemoryStorage.load/save -> TASK span * agents.memory.updater.MemoryUpdater.aupdate_memory -> TASK span ENTRY / AGENT / TOOL spans use ExtendedTelemetryHandler (auto-emits genai_* metrics). TASK spans are created manually with tracer.start_span because ExtendedTelemetryHandler has no start_task/stop_task (TASK is a LoongSuite extension span kind that predates the handler). Recursion guard mirrors crewai _ENTRY_DEPTH / _TASK_DEPTH ContextVar pattern. Subagent cross-loop context propagation is free: DeerFlow executor.py:749/845 already uses contextvars.copy_context() (per Review Agent's correction). PII handling: gen_ai.tool.call.arguments / result and Memory input.value / output.value are gated by OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (default false); Memory content has a second-level switch OTEL_DEER_FLOW_CAPTURE_MEMORY_CONTENT (default false). Version guard: _instruments = ("deer-flow >= 2.1, < 3.0",); import failures are silently skipped by DeerFlowInstrumentor._instrument. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 01bba83 commit 6fff239

15 files changed

Lines changed: 1577 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Changelog
2+
3+
## 0.1.0.dev
4+
5+
* Initial release: DeerFlow 2.1+ instrumentation following the hybrid
6+
LangChain-callback-reuse + 7-wrapt-monkey-patch plan from
7+
`llm-dev/deer-flow/investigate/execute.md`.
8+
* Covers Entry / Agent (subagent) / Task (task_tool, sandbox lifecycle,
9+
memory load/save/update) / Tool (Sandbox ABC) spans. LLM / Tool (LangChain)
10+
/ ReAct Step spans are delegated to `loongsuite-instrumentation-langchain`.
11+
* PII handling: Memory content gated by a two-level switch
12+
(`OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` +
13+
`OTEL_DEER_FLOW_CAPTURE_MEMORY_CONTENT`).
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# LoongSuite Instrumentation for DeerFlow
2+
3+
OpenTelemetry instrumentation for [ByteDance DeerFlow](https://github.com/bytedance/deer-flow)
4+
(`deer-flow >= 2.1, < 3.0`).
5+
6+
## Design
7+
8+
This package follows the hybrid plan documented in
9+
`llm-dev/deer-flow/investigate/execute.md`:
10+
11+
* **LLM / Tool / ReAct Step spans are delegated** to
12+
`loongsuite-instrumentation-langchain` — DeerFlow builds on
13+
`langchain.agents.create_agent`, so LangChain's `BaseCallbackManager` wrap
14+
already emits those spans. This package **does not** wrap
15+
`BaseCallbackManager`, avoiding duplicate spans.
16+
* **DeerFlow-specific Entry / Agent / Task / Sandbox / Memory spans** are
17+
produced by seven `wrapt` monkey patches on DeerFlow public APIs:
18+
19+
| # | Module / Symbol | Span kind | Operation name |
20+
| --- | --- | --- | --- |
21+
| 1 | `deerflow.runtime.runs.worker.run_agent` | ENTRY | `enter` |
22+
| 2 | `deerflow.subagents.executor.SubagentExecutor._aexecute` | AGENT | `invoke_agent` |
23+
| 3 | `deerflow.tools.builtins.task_tool.task_tool` | TASK | `run_task` |
24+
| 4 | `deerflow.sandbox.sandbox.Sandbox.execute_command` (+ `read_file` / `write_file` / `glob` / `grep` / `list_dir`) | TOOL | `execute_tool` |
25+
| 5 | `deerflow.sandbox.sandbox_provider.SandboxProvider.acquire` / `acquire_async` / `release` | TASK | `run_task` |
26+
| 6 | `deerflow.agents.memory.storage.FileMemoryStorage.load` / `save` | TASK | `run_task` |
27+
| 7 | `deerflow.agents.memory.updater.MemoryUpdater.aupdate_memory` | TASK | `run_task` |
28+
29+
Span creation for ENTRY / AGENT / TOOL uses `opentelemetry.util.genai.ExtendedTelemetryHandler`,
30+
which also auto-emits the `genai_*` metrics via its built-in
31+
`ExtendedInvocationMetricsRecorder`. TASK spans are created manually with
32+
`tracer.start_as_current_span` because `ExtendedTelemetryHandler` does not
33+
expose `start_task` / `stop_task` (TASK is a LoongSuite extension kind that
34+
predates the handler).
35+
36+
## Configuration
37+
38+
| Env | Default | Effect |
39+
| --- | --- | --- |
40+
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `false` | Standard gen-ai content capture switch. Controls `gen_ai.input.messages` / `gen_ai.output.messages` / `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result`. |
41+
| `OTEL_DEER_FLOW_CAPTURE_MEMORY_CONTENT` | `false` | Second-level switch: when true (and the standard switch is also true), Memory spans will populate `input.value` / `output.value`. Memory content contains PII, hence the double gate. |
42+
43+
## Context propagation
44+
45+
DeerFlow propagates `contextvars` into subagent isolated event loops via
46+
`contextvars.copy_context()` (executor.py:749/845). OTel Context follows
47+
`ContextVar` automatically, so the ENTRY span's parent/child relationship to
48+
the subagent AGENT span is preserved without any manual injection here.
49+
50+
## Usage
51+
52+
```python
53+
from opentelemetry.instrumentation.deer_flow import DeerFlowInstrumentor
54+
55+
DeerFlowInstrumentor().instrument()
56+
```
57+
58+
Or via the `opentelemetry-instrument` CLI entry point:
59+
60+
```
61+
opentelemetry-instrument --instrumentation_modules deer_flow
62+
```
63+
64+
## Limitations
65+
66+
* IM Channel entry points (Slack / Feishu / Discord) do not go through
67+
`run_agent`; they are not instrumented in v1.
68+
* `task_tool` produces both a TASK span (this package, parent) and a TOOL
69+
span (langchain instrumentation, child). This is intentional — the TASK
70+
span describes "dispatch to subagent" semantics while the TOOL span covers
71+
the LangChain `@tool` invocation mechanics.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "loongsuite-instrumentation-deer-flow"
7+
dynamic = ["version"]
8+
description = "LoongSuite DeerFlow (bytedance/deer-flow) instrumentation"
9+
readme = "README.md"
10+
license = "Apache-2.0"
11+
requires-python = ">=3.12"
12+
authors = [
13+
{ name = "Zhiyong Liu", email = "liuzhiyong.lzy@alibaba-inc.com" },
14+
{ name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
15+
]
16+
classifiers = [
17+
"Development Status :: 4 - Beta",
18+
"Intended Audience :: Developers",
19+
"License :: OSI Approved :: Apache Software License",
20+
"Programming Language :: Python",
21+
"Programming Language :: Python :: 3.12",
22+
"Programming Language :: Python :: 3.13",
23+
]
24+
dependencies = [
25+
"opentelemetry-api >= 1.37.0",
26+
"opentelemetry-instrumentation >= 0.58b0",
27+
"opentelemetry-semantic-conventions >= 0.58b0",
28+
"wrapt >= 1.0.0, < 2.0.0",
29+
"opentelemetry-util-genai",
30+
]
31+
32+
[project.optional-dependencies]
33+
instruments = [
34+
"deer-flow >= 2.1, < 3.0",
35+
]
36+
37+
[project.entry-points.opentelemetry_instrumentor]
38+
deer_flow = "opentelemetry.instrumentation.deer_flow:DeerFlowInstrumentor"
39+
40+
[project.urls]
41+
Homepage = "https://github.com/alibaba/loongsuite-python-agent/tree/main/instrumentation-loongsuite/loongsuite-instrumentation-deer-flow"
42+
Repository = "https://github.com/alibaba/loongsuite-python-agent"
43+
44+
[tool.hatch.version]
45+
path = "src/opentelemetry/instrumentation/deer_flow/version.py"
46+
47+
[tool.hatch.build.targets.sdist]
48+
include = [
49+
"/src",
50+
"/tests",
51+
]
52+
53+
[tool.hatch.build.targets.wheel]
54+
packages = ["src/opentelemetry"]
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""OpenTelemetry instrumentation for ByteDance DeerFlow.
16+
17+
Implements the hybrid plan documented in
18+
``llm-dev/deer-flow/investigate/execute.md``: LLM / Tool / ReAct Step spans are
19+
delegated to ``loongsuite-instrumentation-langchain`` (DeerFlow is built on
20+
``langchain.agents.create_agent``), while DeerFlow-specific Entry / Agent /
21+
Task / Sandbox / Memory spans are produced by this package via seven
22+
``wrapt`` monkey patches on DeerFlow public APIs:
23+
24+
* ``runtime.runs.worker.run_agent`` → ENTRY span
25+
* ``subagents.executor.SubagentExecutor._aexecute`` → AGENT span (subagent)
26+
* ``tools.builtins.task_tool.task_tool`` → TASK span (subagent dispatch)
27+
* ``sandbox.sandbox.Sandbox.execute_command`` (and the other abstract
28+
methods) → TOOL span (``bash`` / ``read_file`` / ...)
29+
* ``sandbox.sandbox_provider.SandboxProvider.acquire`` /
30+
``acquire_async`` / ``release`` → TASK span (sandbox lifecycle)
31+
* ``agents.memory.storage.FileMemoryStorage.load`` / ``save`` → TASK span
32+
(``memory.load`` / ``memory.save``)
33+
* ``agents.memory.updater.MemoryUpdater.aupdate_memory`` → TASK span
34+
(``memory.update``)
35+
36+
This package **does not** wrap ``BaseCallbackManager`` — that path is owned by
37+
``loongsuite-instrumentation-langchain`` and wrapping it again would create
38+
duplicate LLM / Tool spans.
39+
"""
40+
41+
from __future__ import annotations
42+
43+
import logging
44+
from importlib import import_module
45+
from typing import Any, Collection
46+
47+
from opentelemetry.instrumentation.deer_flow.package import _instruments
48+
from opentelemetry.instrumentation.deer_flow.version import __version__
49+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
50+
from opentelemetry.instrumentation.utils import unwrap
51+
from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler
52+
53+
logger = logging.getLogger(__name__)
54+
55+
# (module_name, attr_path) tuples registered by ``_instrument`` so that
56+
# ``_uninstrument`` can call ``unwrap`` on each target.
57+
_UNINSTRUMENT_TARGETS: list[tuple[str, str]] = []
58+
59+
60+
class DeerFlowInstrumentor(BaseInstrumentor):
61+
"""Instrument ByteDance DeerFlow 2.1+ with OpenTelemetry."""
62+
63+
def __init__(self) -> None:
64+
super().__init__()
65+
self._handler: ExtendedTelemetryHandler | None = None
66+
self._targets: list[tuple[str, str]] = []
67+
68+
def instrumentation_dependencies(self) -> Collection[str]:
69+
return _instruments
70+
71+
def _instrument(self, **kwargs: Any) -> None:
72+
try:
73+
import deerflow # noqa: F401 — exercise the dependency.
74+
except ImportError:
75+
logger.debug(
76+
"DeerFlow is not installed; skipping instrumentation."
77+
)
78+
return
79+
except Exception as exc:
80+
logger.warning(
81+
"DeerFlow import raised an unexpected error: %s", exc
82+
)
83+
return
84+
85+
tracer_provider = kwargs.get("tracer_provider")
86+
meter_provider = kwargs.get("meter_provider")
87+
logger_provider = kwargs.get("logger_provider")
88+
89+
self._handler = ExtendedTelemetryHandler(
90+
tracer_provider=tracer_provider,
91+
meter_provider=meter_provider,
92+
logger_provider=logger_provider,
93+
)
94+
95+
from opentelemetry.instrumentation.deer_flow.patches import (
96+
entry,
97+
memory,
98+
sandbox,
99+
subagent,
100+
task_tool,
101+
)
102+
103+
self._targets = []
104+
for module in (entry, subagent, task_tool, sandbox, memory):
105+
try:
106+
targets = module.instrument(self._handler)
107+
except Exception as exc:
108+
logger.warning(
109+
"DeerFlow patch module %s failed to instrument: %s",
110+
module.__name__,
111+
exc,
112+
exc_info=True,
113+
)
114+
continue
115+
self._targets.extend(targets)
116+
117+
_UNINSTRUMENT_TARGETS[:] = list(self._targets)
118+
119+
def _uninstrument(self, **kwargs: Any) -> None:
120+
del kwargs
121+
for module_name, attr in self._targets:
122+
try:
123+
module = import_module(module_name)
124+
if "." in attr:
125+
class_name, method_name = attr.split(".", 1)
126+
cls = getattr(module, class_name, None)
127+
if cls is None:
128+
continue
129+
unwrap(cls, method_name)
130+
else:
131+
unwrap(module, attr)
132+
except Exception as exc:
133+
logger.debug(
134+
"DeerFlow: could not unwrap %s.%s: %s",
135+
module_name,
136+
attr,
137+
exc,
138+
)
139+
self._targets = []
140+
_UNINSTRUMENT_TARGETS[:] = []
141+
self._handler = None
142+
143+
144+
__all__ = ["DeerFlowInstrumentor", "__version__"]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
_instruments = ("deer-flow >= 2.1, < 3.0",)
16+
17+
_supports_metrics = True
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""DeerFlow wrapt patch modules.
16+
17+
Each submodule wraps a DeerFlow public API with an ``ExtendedTelemetryHandler``
18+
-powered span. ``instrument(handler)`` returns a list of ``(module_name,
19+
attr_path)`` tuples that ``DeerFlowInstrumentor._uninstrument`` can pass to
20+
``unwrap``.
21+
"""
22+
23+
from __future__ import annotations
24+
25+
from . import entry, memory, sandbox, subagent, task_tool
26+
27+
__all__ = ["entry", "memory", "sandbox", "subagent", "task_tool"]

0 commit comments

Comments
 (0)