otelzero 0.2.0__py3-none-any.whl

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.
otelzero/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ """otelzero: instrument agent frameworks via import hooks and send telemetry.
2
+
3
+ Install monitoring for your framework *before* importing it, then run as usual::
4
+
5
+ from otelzero import install_crewai_monitoring
6
+
7
+ install_crewai_monitoring(server_url="http://localhost:8000")
8
+ from crewai import Agent, Crew, Task # now auto-instrumented
9
+
10
+ Supports CrewAI, LangChain, LangGraph, LlamaIndex, the OpenAI Agents SDK,
11
+ Pydantic AI, smolagents, AWS Strands Agents, Google ADK, Databricks Agent Bricks,
12
+ the Claude Agent SDK, Microsoft Agent Framework and the Snowflake Cortex Agent
13
+ SDK, plus framework-agnostic manual instrumentation via ``install_monitoring``.
14
+
15
+ Pydantic AI, AWS Strands Agents, Google ADK and Microsoft Agent Framework already
16
+ trace themselves in the GenAI semantic conventions, and LangChain / LangGraph
17
+ trace themselves through LangSmith's OpenTelemetry export. Their install
18
+ functions switch that native OpenTelemetry on (exporting to the registry)
19
+ instead of wrapping anything, so for these the import order does not matter.
20
+
21
+ """
22
+
23
+ from otelzero.context import run_context
24
+ from otelzero.hooks import (
25
+ install_adk_monitoring,
26
+ install_claude_agent_monitoring,
27
+ install_crewai_monitoring,
28
+ install_databricks_monitoring,
29
+ install_langchain_monitoring,
30
+ install_langgraph_monitoring,
31
+ install_llama_index_monitoring,
32
+ install_microsoft_monitoring,
33
+ install_openai_agents_monitoring,
34
+ install_pydantic_ai_monitoring,
35
+ install_smolagents_monitoring,
36
+ install_snowflake_monitoring,
37
+ install_strands_monitoring,
38
+ )
39
+ from otelzero.telemetry import TelemetryClient, install_monitoring
40
+
41
+ # Kept in sync with [project].version by commitizen (`cz bump`).
42
+ __version__ = "0.2.0"
43
+
44
+ __all__ = [
45
+ "__version__",
46
+ "TelemetryClient",
47
+ "run_context",
48
+ "install_monitoring",
49
+ "install_adk_monitoring",
50
+ "install_claude_agent_monitoring",
51
+ "install_crewai_monitoring",
52
+ "install_databricks_monitoring",
53
+ "install_langchain_monitoring",
54
+ "install_langgraph_monitoring",
55
+ "install_llama_index_monitoring",
56
+ "install_microsoft_monitoring",
57
+ "install_openai_agents_monitoring",
58
+ "install_pydantic_ai_monitoring",
59
+ "install_smolagents_monitoring",
60
+ "install_snowflake_monitoring",
61
+ "install_strands_monitoring",
62
+ ]
otelzero/__main__.py ADDED
@@ -0,0 +1,140 @@
1
+ """
2
+ CrewAI Monitoring Demo
3
+ """
4
+
5
+ import sys
6
+ import time
7
+
8
+ from otelzero import TelemetryClient, install_crewai_monitoring
9
+ from otelzero.detection import _detect_package_version
10
+
11
+
12
+ def main():
13
+ print("=" * 60)
14
+ print("CrewAI Monitoring Demo")
15
+ print("=" * 60)
16
+ print()
17
+ print("Make sure the server is running:")
18
+ print(" uv run kiji-agent-registry (in the kiji-agent-registry repo)")
19
+ print()
20
+ print("Then open the dashboard:")
21
+ print(" http://localhost:8000")
22
+ print()
23
+ print("=" * 60)
24
+ print()
25
+
26
+ # Install monitoring BEFORE importing crewai
27
+ install_crewai_monitoring(server_url="http://localhost:8000")
28
+
29
+ # Now import crewai - it will be instrumented automatically
30
+ try:
31
+ from crewai import Agent, Crew, Task
32
+
33
+ print("CrewAI imported and instrumented")
34
+ print()
35
+ except ImportError:
36
+ print("CrewAI not installed. Install with: pip install crewai")
37
+ print(" For demo purposes, we'll simulate the workflow...")
38
+ print()
39
+
40
+ # Simulate some agent activity for demo
41
+ telemetry = TelemetryClient(
42
+ framework="CrewAI",
43
+ framework_version=_detect_package_version("crewai") or "0.102.0",
44
+ user="Sarah",
45
+ )
46
+
47
+ print("Simulating agent activities...")
48
+ for i in range(5):
49
+ agent_role = f"Agent-{i % 2 + 1}"
50
+
51
+ # Start
52
+ telemetry.send_log(
53
+ event_type="agent_start",
54
+ agent_role=agent_role,
55
+ details={"goal": f"Complete task {i}"},
56
+ )
57
+ time.sleep(0.5)
58
+
59
+ # End or Error
60
+ if i % 4 == 0: # Simulate occasional error
61
+ telemetry.send_log(
62
+ event_type="agent_error",
63
+ agent_role=agent_role,
64
+ error="Simulated error",
65
+ traceback="Sample traceback...",
66
+ )
67
+ else:
68
+ telemetry.send_log(
69
+ event_type="agent_end",
70
+ agent_role=agent_role,
71
+ details={"status": "success"},
72
+ )
73
+
74
+ print(f" {agent_role}: Run {i + 1} completed")
75
+ time.sleep(1)
76
+
77
+ print()
78
+ print("Simulation complete! Check the dashboard.")
79
+ sys.exit(0)
80
+
81
+ # Real CrewAI example
82
+ print("Creating CrewAI agents and tasks...")
83
+ print()
84
+
85
+ # Create multiple agents
86
+ researcher = Agent(
87
+ role="Research Analyst",
88
+ goal="Conduct comprehensive research on AI trends",
89
+ backstory="Expert data analyst with 10 years of experience in AI research",
90
+ verbose=True,
91
+ )
92
+
93
+ writer = Agent(
94
+ role="Content Writer",
95
+ goal="Write engaging content based on research",
96
+ backstory="Professional writer specializing in technology topics",
97
+ verbose=True,
98
+ )
99
+
100
+ # Create tasks
101
+ research_task = Task(
102
+ description="Research the latest developments in Large Language Models",
103
+ expected_output="A detailed research report",
104
+ agent=researcher,
105
+ )
106
+
107
+ writing_task = Task(
108
+ description="Write an article based on the research findings",
109
+ expected_output="A well-written article",
110
+ agent=writer,
111
+ )
112
+
113
+ # Create and run crew
114
+ crew = Crew(
115
+ agents=[researcher, writer], tasks=[research_task, writing_task], verbose=True
116
+ )
117
+
118
+ print("Starting crew execution...")
119
+ print("Watch the dashboard at http://localhost:8000")
120
+ print()
121
+
122
+ try:
123
+ result = crew.kickoff()
124
+ print()
125
+ print("=" * 60)
126
+ print("Crew execution completed!")
127
+ print("=" * 60)
128
+ print(f"Result: {result}")
129
+ except Exception as e:
130
+ print()
131
+ print("=" * 60)
132
+ print(f"Crew execution failed: {e}")
133
+ print("=" * 60)
134
+
135
+ print()
136
+ print("Check the monitoring dashboard for detailed logs!")
137
+ print("Dashboard: http://localhost:8000")
138
+
139
+
140
+ main()
otelzero/context.py ADDED
@@ -0,0 +1,86 @@
1
+ """Per-run identity for agents that serve more than one user.
2
+
3
+ :class:`~otelzero.telemetry.TelemetryClient` carries a process-wide user and
4
+ session, which is right for a script: one process, one person, one run. It is
5
+ wrong for a hosted agent, where a single process handles many callers -- every
6
+ run would report the same user and collapse into one flow.
7
+
8
+ Wrapping a run in :func:`run_context` gives it its own identity::
9
+
10
+ with otelzero.run_context(user="alice", conversation_id=thread_id):
11
+ crew.kickoff(...)
12
+
13
+ Because the scope lives in a :class:`~contextvars.ContextVar`, concurrent
14
+ ``asyncio`` tasks and threads each see their own without any hook signature
15
+ having to thread it through. Outside a scope nothing changes: the client's
16
+ defaults apply exactly as before.
17
+ """
18
+
19
+ import contextlib
20
+ import contextvars
21
+ import uuid
22
+ from typing import Iterator, Optional
23
+
24
+
25
+ class RunScope:
26
+ """The identity of one agent run: who invoked it, and which flow it is.
27
+
28
+ ``session_name`` is mutable because the title is discovered rather than
29
+ given -- the first framework hook to see the run's intent text fills it in
30
+ via :meth:`TelemetryClient.suggest_session_name`.
31
+ """
32
+
33
+ __slots__ = ("user", "conversation_id", "session_name")
34
+
35
+ def __init__(
36
+ self,
37
+ user: Optional[str] = None,
38
+ conversation_id: Optional[str] = None,
39
+ session_name: Optional[str] = None,
40
+ ):
41
+ self.user = user
42
+ self.conversation_id = conversation_id
43
+ self.session_name = session_name
44
+
45
+
46
+ _current_scope: contextvars.ContextVar[Optional[RunScope]] = contextvars.ContextVar(
47
+ "otelzero_run_scope", default=None
48
+ )
49
+
50
+
51
+ def current_scope() -> Optional[RunScope]:
52
+ """The :class:`RunScope` in effect, or ``None`` outside :func:`run_context`."""
53
+ return _current_scope.get()
54
+
55
+
56
+ @contextlib.contextmanager
57
+ def run_context(
58
+ user: Optional[str] = None,
59
+ conversation_id: Optional[str] = None,
60
+ name: Optional[str] = None,
61
+ ) -> Iterator[RunScope]:
62
+ """Scope the enclosed agent run to its own user and conversation.
63
+
64
+ Args:
65
+ user: Who this run is for. Falls back to the client's user when omitted.
66
+ conversation_id: Groups this run's events into one flow, and is exported
67
+ as ``gen_ai.conversation.id``. Pass your own thread/session id to
68
+ keep a multi-turn conversation together; a fresh one is generated
69
+ per scope otherwise.
70
+ name: A short human title for the flow. Left to the framework hooks to
71
+ derive from the run's intent text when omitted.
72
+
73
+ Yields:
74
+ The active :class:`RunScope`, mainly so callers can read back the
75
+ generated ``conversation_id``.
76
+ """
77
+ scope = RunScope(
78
+ user=user,
79
+ conversation_id=conversation_id or uuid.uuid4().hex[:8],
80
+ session_name=name.strip() if isinstance(name, str) and name.strip() else None,
81
+ )
82
+ token = _current_scope.set(scope)
83
+ try:
84
+ yield scope
85
+ finally:
86
+ _current_scope.reset(token)
otelzero/detection.py ADDED
@@ -0,0 +1,58 @@
1
+ """Best-effort detection of the executing user and installed package versions.
2
+
3
+ These helpers are intentionally defensive: every lookup degrades to ``None``
4
+ rather than raising, so instrumentation never breaks the host application just
5
+ because a name or version could not be resolved.
6
+ """
7
+
8
+ import getpass
9
+ from typing import Optional
10
+
11
+
12
+ def _detect_user() -> Optional[str]:
13
+ """Best-effort login name of the user running the agent.
14
+
15
+ Uses ``getpass.getuser()`` (env vars LOGNAME/USER/USERNAME, then the password
16
+ database) -- works without a controlling TTY, unlike ``os.getlogin()``.
17
+ """
18
+ try:
19
+ return getpass.getuser()
20
+ except Exception: # getpass can raise OSError when it cannot resolve a name
21
+ return None
22
+
23
+
24
+ def _detect_package_version(package: str) -> Optional[str]:
25
+ """Try to detect the installed version of a package."""
26
+ try:
27
+ from importlib.metadata import version
28
+
29
+ return version(package)
30
+ except Exception:
31
+ return None
32
+
33
+
34
+ # Framework display labels whose PyPI distribution name is not simply the label
35
+ # lower-cased with spaces hyphenated. Microsoft's framework is branded "Microsoft
36
+ # Agent Framework" but ships as the ``agent-framework`` distribution; Snowflake's
37
+ # is branded "Snowflake Cortex" but ships as ``cortex-code-agent-sdk``; LlamaIndex
38
+ # is one word but its core ships as ``llama-index-core``.
39
+ _FRAMEWORK_DIST_ALIASES = {
40
+ "microsoft agent framework": "agent-framework",
41
+ "snowflake cortex": "cortex-code-agent-sdk",
42
+ "llamaindex": "llama-index-core",
43
+ }
44
+
45
+
46
+ def _detect_framework_version(framework: Optional[str]) -> Optional[str]:
47
+ """Detect the installed version of a framework from its name.
48
+
49
+ Normalizes the framework label to a distribution name (e.g. ``"CrewAI"`` ->
50
+ ``"crewai"``, ``"LangChain"`` -> ``"langchain"``) and looks up its version.
51
+ A few labels whose distribution name differs from that convention are mapped
52
+ explicitly via :data:`_FRAMEWORK_DIST_ALIASES`.
53
+ """
54
+ if not framework:
55
+ return None
56
+ key = framework.lower()
57
+ dist = _FRAMEWORK_DIST_ALIASES.get(key, key.replace(" ", "-"))
58
+ return _detect_package_version(dist)
@@ -0,0 +1,96 @@
1
+ """Framework-specific import hooks and their ``install_*_monitoring`` helpers.
2
+
3
+ Most submodules instrument one agent framework by intercepting its import and
4
+ wrapping the run entry point(s), building on :mod:`otelzero.hooks.base`. The
5
+ frameworks that already trace themselves in the GenAI semantic conventions
6
+ (Pydantic AI, AWS Strands Agents, Google ADK, Microsoft Agent Framework) and
7
+ LangChain / LangGraph (through LangSmith's OTel export) are instead switched
8
+ onto their *native* OpenTelemetry and observed through
9
+ :mod:`otelzero.hooks._native`.
10
+ """
11
+
12
+ from otelzero.hooks.adk import install_adk_monitoring
13
+ from otelzero.hooks.base import (
14
+ _ImportInstrumenter,
15
+ _InstrumentingLoader,
16
+ install_hook,
17
+ nesting_guard,
18
+ start_agent_run,
19
+ trace_agent_run,
20
+ )
21
+ from otelzero.hooks.claude_sdk import (
22
+ ClaudeAgentSDKImportHook,
23
+ _claude_model_id,
24
+ install_claude_agent_monitoring,
25
+ )
26
+ from otelzero.hooks.crewai import (
27
+ CrewAIImportHook,
28
+ _crewai_model_id,
29
+ install_crewai_monitoring,
30
+ )
31
+ from otelzero.hooks.databricks import (
32
+ DatabricksImportHook,
33
+ install_databricks_monitoring,
34
+ )
35
+ from otelzero.hooks.langchain import install_langchain_monitoring
36
+ from otelzero.hooks.langgraph import install_langgraph_monitoring
37
+ from otelzero.hooks.llama_index import (
38
+ LlamaIndexImportHook,
39
+ _llama_index_model_id,
40
+ install_llama_index_monitoring,
41
+ wrap_workflow_module,
42
+ )
43
+ from otelzero.hooks.microsoft import install_microsoft_monitoring
44
+ from otelzero.hooks.openai_agents import (
45
+ OpenAIAgentsImportHook,
46
+ _agents_model_id,
47
+ install_openai_agents_monitoring,
48
+ )
49
+ from otelzero.hooks.pydantic_ai import install_pydantic_ai_monitoring
50
+ from otelzero.hooks.smolagents import (
51
+ SmolagentsImportHook,
52
+ _smolagents_model_id,
53
+ install_smolagents_monitoring,
54
+ )
55
+ from otelzero.hooks.snowflake import (
56
+ SnowflakeCortexImportHook,
57
+ _snowflake_model_id,
58
+ install_snowflake_monitoring,
59
+ )
60
+ from otelzero.hooks.strands import install_strands_monitoring
61
+
62
+ __all__ = [
63
+ "ClaudeAgentSDKImportHook",
64
+ "CrewAIImportHook",
65
+ "DatabricksImportHook",
66
+ "LlamaIndexImportHook",
67
+ "OpenAIAgentsImportHook",
68
+ "SmolagentsImportHook",
69
+ "SnowflakeCortexImportHook",
70
+ "install_adk_monitoring",
71
+ "install_claude_agent_monitoring",
72
+ "install_crewai_monitoring",
73
+ "install_databricks_monitoring",
74
+ "install_langchain_monitoring",
75
+ "install_langgraph_monitoring",
76
+ "install_llama_index_monitoring",
77
+ "install_microsoft_monitoring",
78
+ "install_openai_agents_monitoring",
79
+ "install_pydantic_ai_monitoring",
80
+ "install_smolagents_monitoring",
81
+ "install_snowflake_monitoring",
82
+ "install_strands_monitoring",
83
+ "install_hook",
84
+ "nesting_guard",
85
+ "start_agent_run",
86
+ "trace_agent_run",
87
+ "wrap_workflow_module",
88
+ "_ImportInstrumenter",
89
+ "_InstrumentingLoader",
90
+ "_agents_model_id",
91
+ "_claude_model_id",
92
+ "_crewai_model_id",
93
+ "_llama_index_model_id",
94
+ "_smolagents_model_id",
95
+ "_snowflake_model_id",
96
+ ]