edgeone 1.5.8 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +26 -26
  2. package/edgeone-bin/edgeone.js +3 -3
  3. package/edgeone-dist/cli.js +86879 -2294
  4. package/edgeone-dist/libs-pages-agent-toolkit/README.md +8 -0
  5. package/edgeone-dist/libs-pages-agent-toolkit/pages_agent_toolkit-0.1.40-py3-none-any.whl +0 -0
  6. package/edgeone-dist/libs-pages-blob-python/README.md +38 -0
  7. package/edgeone-dist/libs-pages-blob-python/pages_blob_python-0.11.0-py3-none-any.whl +0 -0
  8. package/edgeone-dist/pages/dev/runner-worker.js +86519 -2075
  9. package/edgeone-dist/pages/observability-python/__init__.py +32 -0
  10. package/edgeone-dist/pages/observability-python/_compat.py +69 -0
  11. package/edgeone-dist/pages/observability-python/apm/__init__.py +13 -0
  12. package/edgeone-dist/pages/observability-python/apm/config.py +85 -0
  13. package/edgeone-dist/pages/observability-python/apm/llm_semconv.py +53 -0
  14. package/edgeone-dist/pages/observability-python/apm/metrics_bridge.py +226 -0
  15. package/edgeone-dist/pages/observability-python/apm/span_exporter.py +384 -0
  16. package/edgeone-dist/pages/observability-python/bootstrap.py +158 -0
  17. package/edgeone-dist/pages/observability-python/build.py +119 -0
  18. package/edgeone-dist/pages/observability-python/context_patches.py +167 -0
  19. package/edgeone-dist/pages/observability-python/context_propagator.py +78 -0
  20. package/edgeone-dist/pages/observability-python/registry.json +95 -0
  21. package/edgeone-dist/pages/observability-python/registry.py +141 -0
  22. package/edgeone-dist/pages/observability-python/telemetry.py +214 -0
  23. package/edgeone-dist/pages/observability-python/tracer.py +165 -0
  24. package/edgeone-dist/pages/templates/agent-python/__init__.py +11 -0
  25. package/edgeone-dist/pages/templates/agent-python/adapter.py +908 -0
  26. package/edgeone-dist/pages/templates/agent-python/context.py +689 -0
  27. package/edgeone-dist/pages/templates/agent-python/local_blob_store.py +172 -0
  28. package/edgeone-dist/pages/templates/agent-python/memory.py +2301 -0
  29. package/edgeone-dist/pages/templates/agent-python/runtime.py +839 -0
  30. package/edgeone-dist/pages/templates/agent-python/store.py +204 -0
  31. package/edgeone-dist/studio/ui/assets/agent-obs-Dvi4IpEy.js +4 -0
  32. package/edgeone-dist/studio/ui/assets/agent-obs-qDJCE0TQ.css +1 -0
  33. package/edgeone-dist/studio/ui/assets/highlight-ClXAL37H.js +3 -0
  34. package/edgeone-dist/studio/ui/assets/index-Cz5oQnXW.css +1 -0
  35. package/edgeone-dist/studio/ui/assets/index-DD3d108t.js +1 -0
  36. package/edgeone-dist/studio/ui/assets/moment-BYRO94Ou.js +10 -0
  37. package/edgeone-dist/studio/ui/assets/react-dom-ZzBHVjtL.js +24 -0
  38. package/edgeone-dist/studio/ui/assets/react-hnpCyKql.js +17 -0
  39. package/edgeone-dist/studio/ui/assets/tea-CADagUwM.css +1 -0
  40. package/edgeone-dist/studio/ui/assets/tea-Slf_ajmf.js +334 -0
  41. package/edgeone-dist/studio/ui/favicon.ico +0 -0
  42. package/edgeone-dist/studio/ui/index.html +31 -0
  43. package/libs-pages-agent-toolkit/README.md +8 -0
  44. package/libs-pages-agent-toolkit/pages_agent_toolkit-0.1.40-py3-none-any.whl +0 -0
  45. package/libs-pages-blob-python/README.md +38 -0
  46. package/libs-pages-blob-python/pages_blob_python-0.11.0-py3-none-any.whl +0 -0
  47. package/package.json +33 -7
@@ -0,0 +1,167 @@
1
+ """Context propagation patches for OpenInference instrumentors.
2
+
3
+ Fixes two issues when multiple instrumentors coexist:
4
+
5
+ 1. LangChain + OpenAI: OpenAI instrumentor's ChatCompletion span should be a
6
+ child of LangChain's ChatOpenAI span, but LangChain uses callbacks (not
7
+ ContextVar) for span management. Fix: patch OpenAI instrumentor to query
8
+ LangChain's get_current_span() before creating its own span.
9
+
10
+ 2. CrewAI trace isolation: CrewAI's event bus fires events in a background
11
+ ThreadPoolExecutor thread. That thread has no OTel context from the main
12
+ asyncio task. Fix: patch CrewAI's EventAssembler to remember the ambient
13
+ context at construction time and use it as fallback when the background
14
+ thread's context is empty.
15
+
16
+ These patches are applied AFTER instrumentors are activated (since we need to
17
+ patch the already-instantiated objects) and are idempotent.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from typing import Optional
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def apply_context_patches() -> None:
28
+ """Apply all context propagation patches. Safe to call multiple times."""
29
+ _patch_openai_langchain_bridge()
30
+ _patch_crewai_ambient_context()
31
+
32
+
33
+ def _patch_openai_langchain_bridge() -> None:
34
+ """Patch OpenAI instrumentor to use LangChain's active span as parent.
35
+
36
+ Without this, ChatCompletion spans from openai instrumentor land under
37
+ the root span instead of under LangChain's ChatOpenAI span.
38
+
39
+ Strategy: wrap _Request.__call__ and _AsyncRequest.__call__ to attach
40
+ the LangChain parent context before the original method runs (which
41
+ calls _start_as_current_span internally). This way the tracer.start_span()
42
+ inside naturally picks up the correct parent from the ambient context.
43
+ """
44
+ try:
45
+ from openinference.instrumentation.openai import _request as openai_req
46
+ except ImportError:
47
+ return # openai instrumentor not installed
48
+
49
+ # Check if already patched
50
+ if getattr(openai_req, "_eo_langchain_bridge_patched", False):
51
+ return
52
+
53
+ try:
54
+ from opentelemetry import trace as trace_api
55
+ from opentelemetry import context as context_api
56
+ from opentelemetry.trace import INVALID_SPAN
57
+
58
+ def _get_langchain_parent_context() -> Optional[context_api.Context]:
59
+ """Query LangChain instrumentor for the current active span."""
60
+ try:
61
+ from openinference.instrumentation.langchain import get_current_span
62
+ parent_span = get_current_span()
63
+ if parent_span is not None and parent_span is not INVALID_SPAN:
64
+ return trace_api.set_span_in_context(parent_span)
65
+ except ImportError:
66
+ pass
67
+ except Exception:
68
+ pass
69
+ return None
70
+
71
+ # Patch _Request.__call__: attach LangChain context before execution
72
+ _orig_request_call = openai_req._Request.__call__
73
+
74
+ def _patched_request_call(self, wrapped, instance, args, kwargs):
75
+ parent_ctx = _get_langchain_parent_context()
76
+ if parent_ctx is None:
77
+ return _orig_request_call(self, wrapped, instance, args, kwargs)
78
+ # Attach LangChain's span as ambient context so start_span picks it up
79
+ token = context_api.attach(parent_ctx)
80
+ try:
81
+ return _orig_request_call(self, wrapped, instance, args, kwargs)
82
+ finally:
83
+ context_api.detach(token)
84
+
85
+ # Patch _AsyncRequest.__call__
86
+ _orig_async_request_call = openai_req._AsyncRequest.__call__
87
+
88
+ async def _patched_async_request_call(self, wrapped, instance, args, kwargs):
89
+ parent_ctx = _get_langchain_parent_context()
90
+ if parent_ctx is None:
91
+ return await _orig_async_request_call(self, wrapped, instance, args, kwargs)
92
+ token = context_api.attach(parent_ctx)
93
+ try:
94
+ return await _orig_async_request_call(self, wrapped, instance, args, kwargs)
95
+ finally:
96
+ context_api.detach(token)
97
+
98
+ openai_req._Request.__call__ = _patched_request_call
99
+ openai_req._AsyncRequest.__call__ = _patched_async_request_call
100
+ openai_req._eo_langchain_bridge_patched = True
101
+ logger.debug("[observability] patched openai instrumentor with langchain bridge")
102
+ except Exception as e:
103
+ logger.debug(f"[observability] langchain bridge patch failed: {e}")
104
+
105
+
106
+ def _patch_crewai_ambient_context() -> None:
107
+ """Patch CrewAI's kickoff wrapper to inherit the request's OTel context.
108
+
109
+ Problem: CrewAI's crew.kickoff() runs in run_in_executor (thread pool).
110
+ Python's run_in_executor does NOT propagate OTel context to worker threads.
111
+ So the CrewAI instrumentor's wrapper calls start_as_current_span() but
112
+ gets an empty context — creating a new trace instead of joining the request's.
113
+
114
+ Fix: patch _CrewKickoffWrapper.__call__ to attach our stored request
115
+ context before the original logic runs.
116
+ """
117
+ try:
118
+ from openinference.instrumentation.crewai import _wrappers as wrappers_mod
119
+ except ImportError:
120
+ return # crewai instrumentor not installed
121
+
122
+ if getattr(wrappers_mod, "_eo_ambient_context_patched", False):
123
+ return
124
+
125
+ try:
126
+ from opentelemetry import context as context_api
127
+
128
+ _CrewKickoffWrapper = getattr(wrappers_mod, "_CrewKickoffWrapper", None)
129
+ if _CrewKickoffWrapper is None:
130
+ return
131
+
132
+ _orig_call = _CrewKickoffWrapper.__call__
133
+
134
+ def _patched_kickoff_call(self, wrapped, instance, args, kwargs):
135
+ from . import context_patches as _cp
136
+ captured = getattr(_cp, "_crewai_request_context", None)
137
+ if captured is not None:
138
+ token = context_api.attach(captured)
139
+ try:
140
+ return _orig_call(self, wrapped, instance, args, kwargs)
141
+ finally:
142
+ context_api.detach(token)
143
+ return _orig_call(self, wrapped, instance, args, kwargs)
144
+
145
+ _CrewKickoffWrapper.__call__ = _patched_kickoff_call
146
+ wrappers_mod._eo_ambient_context_patched = True
147
+ logger.debug("[observability] patched crewai kickoff wrapper with ambient context")
148
+ except Exception as e:
149
+ logger.debug(f"[observability] crewai ambient context patch failed: {e}")
150
+
151
+
152
+ # Module-level variable for adapter to set per-request context.
153
+ # The adapter sets this AFTER attaching the root span context,
154
+ # so CrewAI's background thread can read it.
155
+ _crewai_request_context: Optional["context_api.Context"] = None
156
+
157
+
158
+ def set_request_context(ctx) -> None:
159
+ """Called by adapter after attaching root span context."""
160
+ global _crewai_request_context
161
+ _crewai_request_context = ctx
162
+
163
+
164
+ def clear_request_context() -> None:
165
+ """Called by adapter after request completes."""
166
+ global _crewai_request_context
167
+ _crewai_request_context = None
@@ -0,0 +1,78 @@
1
+ """Agent context propagator — Python parallel of Node's AgentContextPropagator.
2
+
3
+ 为每个 span 在 onStart 阶段统一注入:
4
+ - ``agent.run_id`` = 该 span 所属 trace 的 ``traceId``(与 traceId 对齐,
5
+ 上报到 APM / langfuse 后体现为 ``langfuse.agent.run_id == traceId``)。
6
+ - ``agent.conversation_id`` = adapter 通过 ``set_agent_conversation_id`` 设置的会话 ID。
7
+
8
+ 注意:业务层 ``ctx.run_id`` 仍由 runtime.py 用 ``uuid4()`` 生成,不受影响;
9
+ 本 propagator 只影响上报到 OTel exporter 的 attribute。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from contextvars import ContextVar
14
+ from typing import Optional
15
+
16
+ from opentelemetry.context import Context
17
+ from opentelemetry.sdk.trace import ReadableSpan, Span
18
+ from opentelemetry.sdk.trace.export import SpanProcessor
19
+
20
+
21
+ # 由 adapter 在 attach 完 root span context 之后立刻 set,handler 完成后 reset。
22
+ # 用 ContextVar 而不是模块全局,自然适配 asyncio 的 Task 隔离。
23
+ _conversation_id_var: ContextVar[Optional[str]] = ContextVar(
24
+ "_eo_agent_conversation_id", default=None
25
+ )
26
+
27
+
28
+ def set_agent_conversation_id(conversation_id: Optional[str]):
29
+ """Adapter 调用:设置当前请求的 conversation_id,返回 token 供 reset 用。
30
+
31
+ Returns:
32
+ ContextVar token; 调用方应在请求结束时执行
33
+ ``_conversation_id_var.reset(token)``。也可直接调用
34
+ :func:`reset_agent_conversation_id` 传入该 token。
35
+ """
36
+ return _conversation_id_var.set(conversation_id)
37
+
38
+
39
+ def reset_agent_conversation_id(token) -> None:
40
+ try:
41
+ _conversation_id_var.reset(token)
42
+ except Exception:
43
+ # token 失效或跨 context 复位等异常一律忽略,best-effort 清理。
44
+ pass
45
+
46
+
47
+ class AgentContextPropagator(SpanProcessor):
48
+ """SpanProcessor that injects ``agent.run_id`` (= traceId) and
49
+ ``agent.conversation_id`` into every span on start.
50
+ """
51
+
52
+ def on_start(
53
+ self, span: Span, parent_context: Optional[Context] = None
54
+ ) -> None: # noqa: D401
55
+ try:
56
+ ctx = span.get_span_context()
57
+ # 16-byte trace id → 32 char hex,对齐 OTel/langfuse 显示格式。
58
+ trace_id_hex = format(ctx.trace_id, "032x")
59
+ if trace_id_hex and trace_id_hex != "0" * 32:
60
+ span.set_attribute("agent.run_id", trace_id_hex)
61
+ except Exception:
62
+ pass
63
+
64
+ conv_id = _conversation_id_var.get()
65
+ if conv_id:
66
+ try:
67
+ span.set_attribute("agent.conversation_id", conv_id)
68
+ except Exception:
69
+ pass
70
+
71
+ def on_end(self, span: ReadableSpan) -> None: # noqa: D401
72
+ return None
73
+
74
+ def shutdown(self) -> None: # noqa: D401
75
+ return None
76
+
77
+ def force_flush(self, timeout_millis: int = 30000) -> bool: # noqa: D401
78
+ return True
@@ -0,0 +1,95 @@
1
+ [
2
+ {
3
+ "name": "langchain",
4
+ "importPatterns": ["langchain", "langgraph", "langchain_openai", "langchain_core"],
5
+ "pipPackages": ["openinference-instrumentation-langchain>=0.1.30"]
6
+ },
7
+ {
8
+ "name": "crewai",
9
+ "importPatterns": ["crewai"],
10
+ "pipPackages": [
11
+ "openinference-instrumentation-crewai>=0.1.8",
12
+ "openinference-instrumentation-litellm>=0.1"
13
+ ]
14
+ },
15
+ {
16
+ "name": "llama-index",
17
+ "importPatterns": ["llama_index"],
18
+ "pipPackages": ["openinference-instrumentation-llama-index>=4.0"]
19
+ },
20
+ {
21
+ "name": "openai",
22
+ "importPatterns": ["openai"],
23
+ "pipPackages": ["openinference-instrumentation-openai>=0.1.15"]
24
+ },
25
+ {
26
+ "name": "openai-agents",
27
+ "importPatterns": ["agents"],
28
+ "pipPackages": ["openinference-instrumentation-openai-agents>=1.5.0"]
29
+ },
30
+ {
31
+ "name": "anthropic",
32
+ "importPatterns": ["anthropic"],
33
+ "pipPackages": ["openinference-instrumentation-anthropic>=1.0.5"]
34
+ },
35
+ {
36
+ "name": "beeai",
37
+ "importPatterns": ["beeai_framework", "beeai"],
38
+ "pipPackages": ["openinference-instrumentation-beeai>=0.1.19"]
39
+ },
40
+ {
41
+ "name": "agno",
42
+ "importPatterns": ["agno"],
43
+ "pipPackages": ["openinference-instrumentation-agno>=0.1.34"]
44
+ },
45
+ {
46
+ "name": "autogen-agentchat",
47
+ "importPatterns": ["autogen_agentchat"],
48
+ "pipPackages": ["openinference-instrumentation-autogen-agentchat>=0.1.9"]
49
+ },
50
+ {
51
+ "name": "autogen",
52
+ "importPatterns": ["autogen"],
53
+ "pipPackages": ["openinference-instrumentation-autogen>=0.1.14"]
54
+ },
55
+ {
56
+ "name": "bedrock",
57
+ "importPatterns": ["boto3", "botocore"],
58
+ "pipPackages": ["openinference-instrumentation-bedrock>=0.1.39"]
59
+ },
60
+ {
61
+ "name": "claude-agent-sdk",
62
+ "importPatterns": ["claude_agent_sdk"],
63
+ "pipPackages": ["openinference-instrumentation-claude-agent-sdk>=0.1.4"]
64
+ },
65
+ {
66
+ "name": "dspy",
67
+ "importPatterns": ["dspy"],
68
+ "pipPackages": ["openinference-instrumentation-dspy>=0.1.37"]
69
+ },
70
+ {
71
+ "name": "google-adk",
72
+ "importPatterns": ["google.adk", "google_adk"],
73
+ "pipPackages": ["openinference-instrumentation-google-adk>=0.1.14"]
74
+ },
75
+ {
76
+ "name": "google-genai",
77
+ "importPatterns": ["google.generativeai", "google_generativeai", "google.genai"],
78
+ "pipPackages": ["openinference-instrumentation-google-genai>=1.0.2"]
79
+ },
80
+ {
81
+ "name": "groq",
82
+ "importPatterns": ["groq"],
83
+ "pipPackages": ["openinference-instrumentation-groq>=0.1.16"]
84
+ },
85
+ {
86
+ "name": "guardrails",
87
+ "importPatterns": ["guardrails"],
88
+ "pipPackages": ["openinference-instrumentation-guardrails>=0.1.14"]
89
+ },
90
+ {
91
+ "name": "mistralai",
92
+ "importPatterns": ["mistralai"],
93
+ "pipPackages": ["openinference-instrumentation-mistralai>=2.0.4"]
94
+ }
95
+ ]
@@ -0,0 +1,141 @@
1
+ """Framework → OpenInference instrumentor mapping."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Callable
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Entry:
10
+ name: str
11
+ instrumentor_factories: tuple[Callable[[], object], ...]
12
+
13
+
14
+ def _langchain_instrumentor() -> object:
15
+ from openinference.instrumentation.langchain import LangChainInstrumentor
16
+ instrumentor = LangChainInstrumentor()
17
+ # Patch missing callback methods that newer LangChain versions call.
18
+ # openinference-instrumentation-langchain may not have implemented these yet.
19
+ _tracer_cls = None
20
+ try:
21
+ from openinference.instrumentation.langchain._tracer import OpenInferenceTracer
22
+ _tracer_cls = OpenInferenceTracer
23
+ except ImportError:
24
+ pass
25
+ if _tracer_cls is not None:
26
+ for method in ("on_resume", "on_interrupt"):
27
+ if not hasattr(_tracer_cls, method):
28
+ setattr(_tracer_cls, method, lambda self, *a, **kw: None)
29
+ return instrumentor
30
+
31
+
32
+ def _crewai_instrumentor() -> object:
33
+ from openinference.instrumentation.crewai import CrewAIInstrumentor
34
+ return CrewAIInstrumentor()
35
+
36
+
37
+ def _litellm_instrumentor() -> object:
38
+ from openinference.instrumentation.litellm import LiteLLMInstrumentor
39
+ return LiteLLMInstrumentor()
40
+
41
+
42
+ def _llama_index_instrumentor() -> object:
43
+ from openinference.instrumentation.llama_index import LlamaIndexInstrumentor
44
+ return LlamaIndexInstrumentor()
45
+
46
+
47
+ def _openai_instrumentor() -> object:
48
+ from openinference.instrumentation.openai import OpenAIInstrumentor
49
+ return OpenAIInstrumentor()
50
+
51
+
52
+ def _openai_agents_instrumentor() -> object:
53
+ from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
54
+ return OpenAIAgentsInstrumentor()
55
+
56
+
57
+ def _anthropic_instrumentor() -> object:
58
+ from openinference.instrumentation.anthropic import AnthropicInstrumentor
59
+ return AnthropicInstrumentor()
60
+
61
+
62
+ def _beeai_instrumentor() -> object:
63
+ from openinference.instrumentation.beeai import BeeAIInstrumentor
64
+ return BeeAIInstrumentor()
65
+
66
+
67
+ def _agno_instrumentor() -> object:
68
+ from openinference.instrumentation.agno import AgnoInstrumentor
69
+ return AgnoInstrumentor()
70
+
71
+
72
+ def _autogen_agentchat_instrumentor() -> object:
73
+ from openinference.instrumentation.autogen_agentchat import AutoGenAgentChatInstrumentor
74
+ return AutoGenAgentChatInstrumentor()
75
+
76
+
77
+ def _autogen_instrumentor() -> object:
78
+ from openinference.instrumentation.autogen import AutoGenInstrumentor
79
+ return AutoGenInstrumentor()
80
+
81
+
82
+ def _bedrock_instrumentor() -> object:
83
+ from openinference.instrumentation.bedrock import BedrockInstrumentor
84
+ return BedrockInstrumentor()
85
+
86
+
87
+ def _claude_agent_sdk_instrumentor() -> object:
88
+ from openinference.instrumentation.claude_agent_sdk import ClaudeAgentSDKInstrumentor
89
+ return ClaudeAgentSDKInstrumentor()
90
+
91
+
92
+ def _dspy_instrumentor() -> object:
93
+ from openinference.instrumentation.dspy import DSPyInstrumentor
94
+ return DSPyInstrumentor()
95
+
96
+
97
+ def _google_adk_instrumentor() -> object:
98
+ from openinference.instrumentation.google_adk import GoogleADKInstrumentor
99
+ return GoogleADKInstrumentor()
100
+
101
+
102
+ def _google_genai_instrumentor() -> object:
103
+ from openinference.instrumentation.google_genai import GoogleGenAIInstrumentor
104
+ return GoogleGenAIInstrumentor()
105
+
106
+
107
+ def _groq_instrumentor() -> object:
108
+ from openinference.instrumentation.groq import GroqInstrumentor
109
+ return GroqInstrumentor()
110
+
111
+
112
+ def _guardrails_instrumentor() -> object:
113
+ from openinference.instrumentation.guardrails import GuardrailsInstrumentor
114
+ return GuardrailsInstrumentor()
115
+
116
+
117
+ def _mistralai_instrumentor() -> object:
118
+ from openinference.instrumentation.mistralai import MistralAIInstrumentor
119
+ return MistralAIInstrumentor()
120
+
121
+
122
+ REGISTRY: tuple[Entry, ...] = (
123
+ Entry(name="langchain", instrumentor_factories=(_langchain_instrumentor,)),
124
+ Entry(name="crewai", instrumentor_factories=(_crewai_instrumentor, _litellm_instrumentor)),
125
+ Entry(name="llama-index", instrumentor_factories=(_llama_index_instrumentor,)),
126
+ Entry(name="openai", instrumentor_factories=(_openai_instrumentor,)),
127
+ Entry(name="openai-agents", instrumentor_factories=(_openai_agents_instrumentor,)),
128
+ Entry(name="anthropic", instrumentor_factories=(_anthropic_instrumentor,)),
129
+ Entry(name="beeai", instrumentor_factories=(_beeai_instrumentor,)),
130
+ Entry(name="agno", instrumentor_factories=(_agno_instrumentor,)),
131
+ Entry(name="autogen-agentchat", instrumentor_factories=(_autogen_agentchat_instrumentor,)),
132
+ Entry(name="autogen", instrumentor_factories=(_autogen_instrumentor,)),
133
+ Entry(name="bedrock", instrumentor_factories=(_bedrock_instrumentor,)),
134
+ Entry(name="claude-agent-sdk", instrumentor_factories=(_claude_agent_sdk_instrumentor,)),
135
+ Entry(name="dspy", instrumentor_factories=(_dspy_instrumentor,)),
136
+ Entry(name="google-adk", instrumentor_factories=(_google_adk_instrumentor,)),
137
+ Entry(name="google-genai", instrumentor_factories=(_google_genai_instrumentor,)),
138
+ Entry(name="groq", instrumentor_factories=(_groq_instrumentor,)),
139
+ Entry(name="guardrails", instrumentor_factories=(_guardrails_instrumentor,)),
140
+ Entry(name="mistralai", instrumentor_factories=(_mistralai_instrumentor,)),
141
+ )
@@ -0,0 +1,214 @@
1
+ """Telemetry initialization — TracerProvider + APM exporter setup."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Optional
7
+
8
+ from opentelemetry import trace as otel_trace
9
+ from opentelemetry.context import Context, get_current
10
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
11
+ from opentelemetry.sdk.resources import Resource
12
+ from opentelemetry.sdk.trace import TracerProvider
13
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
14
+ from opentelemetry.trace import Span, Tracer, set_span_in_context
15
+
16
+ from .apm.config import resolve_apm_endpoint, resolve_service_name
17
+ from .apm.metrics_bridge import ApmMetricsBridge
18
+ from .apm.span_exporter import ApmSpanExporter, TranslatorContext
19
+ from ._compat import apply_compat_patches
20
+ from .context_propagator import AgentContextPropagator
21
+
22
+
23
+ @dataclass
24
+ class Telemetry:
25
+ tracer_provider: TracerProvider
26
+ tracer: Tracer
27
+ metrics_bridge: ApmMetricsBridge
28
+ _root_span_ids: set = field(default_factory=set)
29
+ _root_spans_with_children: set = field(default_factory=set)
30
+
31
+ def start_request_span(
32
+ self, name: str, attributes: dict[str, Any] | None = None
33
+ ) -> tuple[Span, Context]:
34
+ span = self.tracer.start_span(name, attributes=attributes or {})
35
+ self._root_span_ids.add(format(span.get_span_context().span_id, "016x"))
36
+ ctx = set_span_in_context(span, get_current())
37
+ return span, ctx
38
+
39
+ def end_request_span(self, span: Span) -> None:
40
+ span_id = format(span.get_span_context().span_id, "016x")
41
+ span.end()
42
+ # Flush pending spans BEFORE removing the root span ID.
43
+ # BatchSpanProcessor exports asynchronously — child spans queued
44
+ # earlier need the root ID still present when _is_trace_entry runs
45
+ # inside the ApmSpanExporter so they get gen_ai.is_entry=true.
46
+ try:
47
+ self.tracer_provider.force_flush(timeout_millis=5000)
48
+ except Exception:
49
+ pass
50
+ self._root_span_ids.discard(span_id)
51
+ self._root_spans_with_children.discard(span_id)
52
+
53
+ def shutdown(self) -> None:
54
+ self._root_span_ids.clear()
55
+ self._root_spans_with_children.clear()
56
+ try:
57
+ self.metrics_bridge.shutdown()
58
+ except Exception:
59
+ pass
60
+ try:
61
+ self.tracer_provider.shutdown()
62
+ except Exception:
63
+ pass
64
+
65
+
66
+ _telemetry: Telemetry | None = None
67
+
68
+
69
+ def setup_telemetry(
70
+ *,
71
+ tracer_provider: TracerProvider | None = None,
72
+ endpoint: str | None = None,
73
+ service_name: str | None = None,
74
+ apm_token: str | None = None,
75
+ ) -> Telemetry:
76
+ """Initialize telemetry. Called once at process startup.
77
+
78
+ Args:
79
+ tracer_provider: Override for testing (InMemoryExporter).
80
+ If None, creates production provider with APM exporter.
81
+ """
82
+ global _telemetry
83
+ if _telemetry is not None:
84
+ return _telemetry
85
+
86
+ apply_compat_patches()
87
+
88
+ endpoint = endpoint or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") or resolve_apm_endpoint()
89
+ service_name = service_name or resolve_service_name()
90
+ environment = os.environ.get("NODE_ENV") or "development"
91
+ apm_token = apm_token or os.environ.get("SVC_TRACE_KEY_LB6V03", "")
92
+
93
+ # Dev dual-export: optionally send to remote APM alongside local Studio.
94
+ dev_apm_token = os.environ.get("EDGEONE_DEV_APM_TOKEN", "")
95
+ dev_apm_endpoint = os.environ.get("EDGEONE_DEV_APM_ENDPOINT") or resolve_apm_endpoint()
96
+
97
+ # 使用 Resource.create() 以保留 SDK 默认属性(telemetry.sdk.language 等),
98
+ # 否则 inferRuntime() 无法识别 Python runtime,会误判为 agent-node。
99
+ # Resource.create() 会自动合并 OTEL_RESOURCE_ATTRIBUTES 和 SDK 默认属性。
100
+ resource = Resource.create({
101
+ "service.name": service_name,
102
+ "deployment.environment": environment,
103
+ "token": dev_apm_token or apm_token,
104
+ })
105
+
106
+ root_span_ids: set[str] = set()
107
+ root_spans_with_children: set[str] = set()
108
+
109
+ if tracer_provider is None:
110
+ translator_ctx = TranslatorContext(
111
+ is_root_span_id=lambda sid: sid in root_span_ids,
112
+ root_span_has_children=lambda sid: sid in root_spans_with_children,
113
+ )
114
+ raw_exporter = OTLPSpanExporter(endpoint=f"{endpoint.rstrip('/')}/v1/traces")
115
+ wrapped_exporter = ApmSpanExporter(raw_exporter, translator_ctx)
116
+
117
+ tracer_provider = TracerProvider(resource=resource)
118
+ tracer_provider.add_span_processor(BatchSpanProcessor(wrapped_exporter))
119
+
120
+ # Dev APM dual-export processor (if EDGEONE_DEV_APM_TOKEN is set)
121
+ if dev_apm_token:
122
+ dev_apm_raw = OTLPSpanExporter(endpoint=f"{dev_apm_endpoint.rstrip('/')}/v1/traces")
123
+ dev_apm_wrapped = ApmSpanExporter(dev_apm_raw, translator_ctx)
124
+ tracer_provider.add_span_processor(BatchSpanProcessor(dev_apm_wrapped))
125
+ print(f"[observability] APM dual-export enabled → {dev_apm_endpoint}")
126
+
127
+ metrics_bridge = ApmMetricsBridge(
128
+ endpoint=dev_apm_endpoint if dev_apm_token else endpoint,
129
+ apm_token=dev_apm_token or apm_token,
130
+ resource=resource,
131
+ )
132
+ tracer_provider.add_span_processor(metrics_bridge.span_processor)
133
+
134
+ # Inject agent.run_id (= traceId) and agent.conversation_id into every span.
135
+ # Mirrors Node's AgentContextPropagator so langfuse.agent.run_id aligns with traceId.
136
+ tracer_provider.add_span_processor(AgentContextPropagator())
137
+
138
+ # Track which root spans have children (for gen_ai.is_entry on root span)
139
+ tracer_provider.add_span_processor(
140
+ _RootChildrenTracker(root_span_ids, root_spans_with_children)
141
+ )
142
+
143
+ otel_trace.set_tracer_provider(tracer_provider)
144
+ else:
145
+ # Test mode — no APM exporter, no metrics bridge, don't touch global state
146
+ metrics_bridge = _NoOpMetricsBridge()
147
+ # 测试模式下也注册 propagator,让 InMemoryExporter 能验证注入行为。
148
+ try:
149
+ tracer_provider.add_span_processor(AgentContextPropagator())
150
+ except Exception:
151
+ # 某些测试 provider 可能不支持重复添加,忽略即可。
152
+ pass
153
+
154
+ tracer = tracer_provider.get_tracer("edgeone-agent-observability", "0.1.0")
155
+
156
+ _telemetry = Telemetry(
157
+ tracer_provider=tracer_provider,
158
+ tracer=tracer,
159
+ metrics_bridge=metrics_bridge,
160
+ _root_span_ids=root_span_ids,
161
+ _root_spans_with_children=root_spans_with_children,
162
+ )
163
+ return _telemetry
164
+
165
+
166
+ def get_telemetry() -> Telemetry | None:
167
+ return _telemetry
168
+
169
+
170
+ def reset_telemetry() -> None:
171
+ """Reset global state. For testing only."""
172
+ global _telemetry
173
+ _telemetry = None
174
+
175
+
176
+ class _NoOpMetricsBridge:
177
+ """Stub for test mode."""
178
+ @property
179
+ def span_processor(self):
180
+ return None
181
+
182
+ def shutdown(self) -> None:
183
+ pass
184
+
185
+
186
+ class _RootChildrenTracker:
187
+ """SpanProcessor that marks root spans as having children when a child starts."""
188
+
189
+ def __init__(self, root_span_ids: set[str], root_spans_with_children: set[str]) -> None:
190
+ self._root_span_ids = root_span_ids
191
+ self._root_spans_with_children = root_spans_with_children
192
+
193
+ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
194
+ parent = span.parent if hasattr(span, "parent") else None
195
+ if parent is None:
196
+ return
197
+ try:
198
+ parent_id = format(parent.span_id, "016x")
199
+ except (AttributeError, TypeError):
200
+ return
201
+ if parent_id in self._root_span_ids:
202
+ self._root_spans_with_children.add(parent_id)
203
+
204
+ def _on_ending(self, span) -> None:
205
+ pass
206
+
207
+ def on_end(self, span) -> None:
208
+ pass
209
+
210
+ def shutdown(self) -> None:
211
+ pass
212
+
213
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
214
+ return True