hud-python 0.4.1__py3-none-any.whl → 0.4.3__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.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/otel/collector.py
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
"""Global span collector for building in-memory traces.
|
|
2
|
-
|
|
3
|
-
This module provides a way to collect spans during execution
|
|
4
|
-
and retrieve them as a Trace object, enabling replay functionality
|
|
5
|
-
without modifying agent code.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import logging
|
|
11
|
-
import threading
|
|
12
|
-
from contextvars import ContextVar
|
|
13
|
-
from typing import TYPE_CHECKING
|
|
14
|
-
|
|
15
|
-
from opentelemetry import trace
|
|
16
|
-
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
17
|
-
|
|
18
|
-
from hud.types import Trace
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from opentelemetry.sdk.trace import ReadableSpan
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
# Global storage for collected spans by task_run_id
|
|
26
|
-
_TRACE_STORAGE: dict[str, TraceCollector] = {}
|
|
27
|
-
_LOCK = threading.Lock()
|
|
28
|
-
|
|
29
|
-
# Context variable to track if collection is enabled
|
|
30
|
-
_collecting_enabled: ContextVar[bool] = ContextVar("collecting_enabled", default=False)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class TraceCollector:
|
|
34
|
-
"""Collects spans for a single task run."""
|
|
35
|
-
|
|
36
|
-
def __init__(self, task_run_id: str) -> None:
|
|
37
|
-
self.task_run_id = task_run_id
|
|
38
|
-
self.spans: list[ReadableSpan] = []
|
|
39
|
-
self._lock = threading.Lock()
|
|
40
|
-
|
|
41
|
-
def add_span(self, span: ReadableSpan) -> None:
|
|
42
|
-
"""Thread-safe span addition."""
|
|
43
|
-
with self._lock:
|
|
44
|
-
self.spans.append(span)
|
|
45
|
-
|
|
46
|
-
def to_trace(self) -> Trace:
|
|
47
|
-
"""Convert collected spans to a Trace object."""
|
|
48
|
-
from .exporters import HudSpan, _span_to_dict
|
|
49
|
-
|
|
50
|
-
trace = Trace()
|
|
51
|
-
|
|
52
|
-
# Convert spans to TraceSteps
|
|
53
|
-
for span in self.spans:
|
|
54
|
-
try:
|
|
55
|
-
# Use the same conversion logic as the exporter
|
|
56
|
-
span_dict = _span_to_dict(span)
|
|
57
|
-
hud_span = HudSpan.model_validate(span_dict)
|
|
58
|
-
|
|
59
|
-
# The attributes field is already a TraceStep
|
|
60
|
-
step = hud_span.attributes
|
|
61
|
-
# Add timing from the span itself
|
|
62
|
-
step.start_timestamp = hud_span.start_time
|
|
63
|
-
step.end_timestamp = hud_span.end_time
|
|
64
|
-
trace.append(step)
|
|
65
|
-
|
|
66
|
-
except Exception as e:
|
|
67
|
-
# Log but don't fail the whole trace
|
|
68
|
-
logger.debug("Failed to convert span: %s", e)
|
|
69
|
-
|
|
70
|
-
return trace
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class CollectingSpanExporter(SpanExporter):
|
|
74
|
-
"""A span exporter that collects spans in memory for replay."""
|
|
75
|
-
|
|
76
|
-
def export(self, spans: list[ReadableSpan]) -> SpanExportResult:
|
|
77
|
-
"""Collect spans if collection is enabled."""
|
|
78
|
-
if not _collecting_enabled.get():
|
|
79
|
-
return SpanExportResult.SUCCESS
|
|
80
|
-
|
|
81
|
-
for span in spans:
|
|
82
|
-
# Extract task_run_id from span
|
|
83
|
-
task_run_id = span.attributes.get("hud.task_run_id") if span.attributes else None
|
|
84
|
-
if not task_run_id or not isinstance(task_run_id, str):
|
|
85
|
-
continue
|
|
86
|
-
|
|
87
|
-
# Get or create collector
|
|
88
|
-
with _LOCK:
|
|
89
|
-
if task_run_id not in _TRACE_STORAGE:
|
|
90
|
-
_TRACE_STORAGE[task_run_id] = TraceCollector(task_run_id)
|
|
91
|
-
collector = _TRACE_STORAGE[task_run_id]
|
|
92
|
-
|
|
93
|
-
# Add span
|
|
94
|
-
collector.add_span(span)
|
|
95
|
-
|
|
96
|
-
return SpanExportResult.SUCCESS
|
|
97
|
-
|
|
98
|
-
def shutdown(self) -> None:
|
|
99
|
-
"""Clean up resources."""
|
|
100
|
-
with _LOCK:
|
|
101
|
-
_TRACE_STORAGE.clear()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def enable_trace_collection(enabled: bool = True) -> None:
|
|
105
|
-
"""Enable or disable in-memory trace collection."""
|
|
106
|
-
_collecting_enabled.set(enabled)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def get_trace(task_run_id: str) -> Trace | None:
|
|
110
|
-
"""Retrieve collected trace for a task run ID.
|
|
111
|
-
|
|
112
|
-
Returns None if no trace was collected or collection was disabled.
|
|
113
|
-
"""
|
|
114
|
-
with _LOCK:
|
|
115
|
-
collector = _TRACE_STORAGE.get(task_run_id)
|
|
116
|
-
if collector:
|
|
117
|
-
return collector.to_trace()
|
|
118
|
-
return None
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def clear_trace(task_run_id: str) -> None:
|
|
122
|
-
"""Clear collected trace for a task run ID."""
|
|
123
|
-
with _LOCK:
|
|
124
|
-
_TRACE_STORAGE.pop(task_run_id, None)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def install_collector() -> None:
|
|
128
|
-
"""Install the collecting span exporter.
|
|
129
|
-
|
|
130
|
-
This should be called after configure_telemetry().
|
|
131
|
-
"""
|
|
132
|
-
provider = trace.get_tracer_provider()
|
|
133
|
-
# Guard for SDK tracer providers only
|
|
134
|
-
if hasattr(provider, "add_span_processor"):
|
|
135
|
-
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
136
|
-
|
|
137
|
-
exporter = CollectingSpanExporter()
|
|
138
|
-
processor = SimpleSpanProcessor(exporter)
|
|
139
|
-
try:
|
|
140
|
-
provider.add_span_processor(processor) # type: ignore[attr-defined]
|
|
141
|
-
except Exception:
|
|
142
|
-
logger.warning("Failed to add span processor")
|
|
1
|
+
"""Global span collector for building in-memory traces.
|
|
2
|
+
|
|
3
|
+
This module provides a way to collect spans during execution
|
|
4
|
+
and retrieve them as a Trace object, enabling replay functionality
|
|
5
|
+
without modifying agent code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
from contextvars import ContextVar
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from opentelemetry import trace
|
|
16
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
17
|
+
|
|
18
|
+
from hud.types import Trace
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Global storage for collected spans by task_run_id
|
|
26
|
+
_TRACE_STORAGE: dict[str, TraceCollector] = {}
|
|
27
|
+
_LOCK = threading.Lock()
|
|
28
|
+
|
|
29
|
+
# Context variable to track if collection is enabled
|
|
30
|
+
_collecting_enabled: ContextVar[bool] = ContextVar("collecting_enabled", default=False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TraceCollector:
|
|
34
|
+
"""Collects spans for a single task run."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, task_run_id: str) -> None:
|
|
37
|
+
self.task_run_id = task_run_id
|
|
38
|
+
self.spans: list[ReadableSpan] = []
|
|
39
|
+
self._lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
def add_span(self, span: ReadableSpan) -> None:
|
|
42
|
+
"""Thread-safe span addition."""
|
|
43
|
+
with self._lock:
|
|
44
|
+
self.spans.append(span)
|
|
45
|
+
|
|
46
|
+
def to_trace(self) -> Trace:
|
|
47
|
+
"""Convert collected spans to a Trace object."""
|
|
48
|
+
from .exporters import HudSpan, _span_to_dict
|
|
49
|
+
|
|
50
|
+
trace = Trace()
|
|
51
|
+
|
|
52
|
+
# Convert spans to TraceSteps
|
|
53
|
+
for span in self.spans:
|
|
54
|
+
try:
|
|
55
|
+
# Use the same conversion logic as the exporter
|
|
56
|
+
span_dict = _span_to_dict(span)
|
|
57
|
+
hud_span = HudSpan.model_validate(span_dict)
|
|
58
|
+
|
|
59
|
+
# The attributes field is already a TraceStep
|
|
60
|
+
step = hud_span.attributes
|
|
61
|
+
# Add timing from the span itself
|
|
62
|
+
step.start_timestamp = hud_span.start_time
|
|
63
|
+
step.end_timestamp = hud_span.end_time
|
|
64
|
+
trace.append(step)
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
# Log but don't fail the whole trace
|
|
68
|
+
logger.debug("Failed to convert span: %s", e)
|
|
69
|
+
|
|
70
|
+
return trace
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CollectingSpanExporter(SpanExporter):
|
|
74
|
+
"""A span exporter that collects spans in memory for replay."""
|
|
75
|
+
|
|
76
|
+
def export(self, spans: list[ReadableSpan]) -> SpanExportResult:
|
|
77
|
+
"""Collect spans if collection is enabled."""
|
|
78
|
+
if not _collecting_enabled.get():
|
|
79
|
+
return SpanExportResult.SUCCESS
|
|
80
|
+
|
|
81
|
+
for span in spans:
|
|
82
|
+
# Extract task_run_id from span
|
|
83
|
+
task_run_id = span.attributes.get("hud.task_run_id") if span.attributes else None
|
|
84
|
+
if not task_run_id or not isinstance(task_run_id, str):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Get or create collector
|
|
88
|
+
with _LOCK:
|
|
89
|
+
if task_run_id not in _TRACE_STORAGE:
|
|
90
|
+
_TRACE_STORAGE[task_run_id] = TraceCollector(task_run_id)
|
|
91
|
+
collector = _TRACE_STORAGE[task_run_id]
|
|
92
|
+
|
|
93
|
+
# Add span
|
|
94
|
+
collector.add_span(span)
|
|
95
|
+
|
|
96
|
+
return SpanExportResult.SUCCESS
|
|
97
|
+
|
|
98
|
+
def shutdown(self) -> None:
|
|
99
|
+
"""Clean up resources."""
|
|
100
|
+
with _LOCK:
|
|
101
|
+
_TRACE_STORAGE.clear()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def enable_trace_collection(enabled: bool = True) -> None:
|
|
105
|
+
"""Enable or disable in-memory trace collection."""
|
|
106
|
+
_collecting_enabled.set(enabled)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_trace(task_run_id: str) -> Trace | None:
|
|
110
|
+
"""Retrieve collected trace for a task run ID.
|
|
111
|
+
|
|
112
|
+
Returns None if no trace was collected or collection was disabled.
|
|
113
|
+
"""
|
|
114
|
+
with _LOCK:
|
|
115
|
+
collector = _TRACE_STORAGE.get(task_run_id)
|
|
116
|
+
if collector:
|
|
117
|
+
return collector.to_trace()
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def clear_trace(task_run_id: str) -> None:
|
|
122
|
+
"""Clear collected trace for a task run ID."""
|
|
123
|
+
with _LOCK:
|
|
124
|
+
_TRACE_STORAGE.pop(task_run_id, None)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def install_collector() -> None:
|
|
128
|
+
"""Install the collecting span exporter.
|
|
129
|
+
|
|
130
|
+
This should be called after configure_telemetry().
|
|
131
|
+
"""
|
|
132
|
+
provider = trace.get_tracer_provider()
|
|
133
|
+
# Guard for SDK tracer providers only
|
|
134
|
+
if hasattr(provider, "add_span_processor"):
|
|
135
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
136
|
+
|
|
137
|
+
exporter = CollectingSpanExporter()
|
|
138
|
+
processor = SimpleSpanProcessor(exporter)
|
|
139
|
+
try:
|
|
140
|
+
provider.add_span_processor(processor) # type: ignore[attr-defined]
|
|
141
|
+
except Exception:
|
|
142
|
+
logger.warning("Failed to add span processor")
|
hud/otel/config.py
CHANGED
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
"""Central configuration for OpenTelemetry inside HUD SDK.
|
|
2
|
-
|
|
3
|
-
This file is responsible for
|
|
4
|
-
1. creating the global ``TracerProvider``
|
|
5
|
-
2. attaching span processors (HUD enrichment, batch + exporter)
|
|
6
|
-
3. activating the community MCP instrumentation so that *every* MCP
|
|
7
|
-
request/response/notification is traced automatically.
|
|
8
|
-
|
|
9
|
-
It is *idempotent*: calling :func:`configure_telemetry` more than once
|
|
10
|
-
returns the same provider and does nothing.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import logging
|
|
16
|
-
from typing import Any
|
|
17
|
-
|
|
18
|
-
from opentelemetry import trace
|
|
19
|
-
from opentelemetry.sdk.resources import Resource
|
|
20
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
21
|
-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
22
|
-
|
|
23
|
-
from hud.settings import settings
|
|
24
|
-
|
|
25
|
-
from .collector import enable_trace_collection, install_collector
|
|
26
|
-
from .exporters import HudSpanExporter
|
|
27
|
-
from .instrumentation import install_mcp_instrumentation
|
|
28
|
-
from .processors import HudEnrichmentProcessor
|
|
29
|
-
|
|
30
|
-
logger = logging.getLogger(__name__)
|
|
31
|
-
|
|
32
|
-
# Global singleton provider so multiple calls do not create duplicates
|
|
33
|
-
_TRACER_PROVIDER: TracerProvider | None = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def is_telemetry_configured() -> bool:
|
|
37
|
-
"""Check if telemetry has been configured."""
|
|
38
|
-
return _TRACER_PROVIDER is not None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# ---------------------------------------------------------------------------
|
|
42
|
-
# Public API
|
|
43
|
-
# ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def configure_telemetry(
|
|
47
|
-
*,
|
|
48
|
-
service_name: str = "hud-sdk",
|
|
49
|
-
service_version: str | None = None,
|
|
50
|
-
environment: str | None = None,
|
|
51
|
-
extra_resource_attributes: dict[str, Any] | None = None,
|
|
52
|
-
enable_otlp: bool = False,
|
|
53
|
-
otlp_endpoint: str | None = None,
|
|
54
|
-
otlp_headers: dict[str, str] | None = None,
|
|
55
|
-
enable_collection: bool = True,
|
|
56
|
-
) -> TracerProvider:
|
|
57
|
-
"""Initialise OpenTelemetry for the current Python process.
|
|
58
|
-
|
|
59
|
-
It is safe to call this in every entry-point; the provider will only
|
|
60
|
-
be created once.
|
|
61
|
-
"""
|
|
62
|
-
global _TRACER_PROVIDER
|
|
63
|
-
|
|
64
|
-
if _TRACER_PROVIDER is not None:
|
|
65
|
-
return _TRACER_PROVIDER
|
|
66
|
-
|
|
67
|
-
# ------------------------------------------------------------------
|
|
68
|
-
# 1. Resource (identity of this service)
|
|
69
|
-
# ------------------------------------------------------------------
|
|
70
|
-
res_attrs: dict[str, Any] = {
|
|
71
|
-
"service.name": service_name,
|
|
72
|
-
"telemetry.sdk.name": "hud-otel",
|
|
73
|
-
"telemetry.sdk.language": "python",
|
|
74
|
-
}
|
|
75
|
-
if service_version:
|
|
76
|
-
res_attrs["service.version"] = service_version
|
|
77
|
-
if environment:
|
|
78
|
-
res_attrs["deployment.environment"] = environment
|
|
79
|
-
if extra_resource_attributes:
|
|
80
|
-
res_attrs.update(extra_resource_attributes)
|
|
81
|
-
|
|
82
|
-
resource = Resource.create(res_attrs)
|
|
83
|
-
|
|
84
|
-
# ------------------------------------------------------------------
|
|
85
|
-
# 2. Provider
|
|
86
|
-
# ------------------------------------------------------------------
|
|
87
|
-
provider = TracerProvider(resource=resource)
|
|
88
|
-
_TRACER_PROVIDER = provider
|
|
89
|
-
|
|
90
|
-
# ------------------------------------------------------------------
|
|
91
|
-
# 3. Processors / exporters
|
|
92
|
-
# ------------------------------------------------------------------
|
|
93
|
-
provider.add_span_processor(HudEnrichmentProcessor())
|
|
94
|
-
|
|
95
|
-
# HUD exporter (only if enabled and API key is available)
|
|
96
|
-
if settings.telemetry_enabled and settings.api_key:
|
|
97
|
-
exporter = HudSpanExporter(
|
|
98
|
-
telemetry_url=settings.hud_telemetry_url, api_key=settings.api_key
|
|
99
|
-
)
|
|
100
|
-
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
101
|
-
elif settings.telemetry_enabled and not settings.api_key and not enable_otlp:
|
|
102
|
-
# Error if no exporters are configured
|
|
103
|
-
raise ValueError(
|
|
104
|
-
"No telemetry backend configured. Either:\n"
|
|
105
|
-
"1. Set HUD_API_KEY environment variable for HUD telemetry\n"
|
|
106
|
-
"2. Use enable_otlp=True with configure_telemetry() for alternative backends (e.g., Jaeger)\n" # noqa: E501
|
|
107
|
-
)
|
|
108
|
-
elif not settings.telemetry_enabled:
|
|
109
|
-
logger.info("HUD telemetry disabled via HUD_TELEMETRY_ENABLED=false")
|
|
110
|
-
|
|
111
|
-
# OTLP exporter (optional - for standard OTel viewers)
|
|
112
|
-
if enable_otlp:
|
|
113
|
-
try:
|
|
114
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
115
|
-
|
|
116
|
-
otlp_config = {}
|
|
117
|
-
if otlp_endpoint:
|
|
118
|
-
otlp_config["endpoint"] = otlp_endpoint
|
|
119
|
-
# Default to HTTP endpoint if not specified
|
|
120
|
-
if not otlp_endpoint.startswith(("http://", "https://")):
|
|
121
|
-
otlp_config["endpoint"] = f"http://{otlp_endpoint}/v1/traces"
|
|
122
|
-
else:
|
|
123
|
-
# Default HTTP endpoint
|
|
124
|
-
otlp_config["endpoint"] = "http://localhost:4318/v1/traces"
|
|
125
|
-
|
|
126
|
-
if otlp_headers:
|
|
127
|
-
otlp_config["headers"] = otlp_headers
|
|
128
|
-
|
|
129
|
-
otlp_exporter = OTLPSpanExporter(**otlp_config)
|
|
130
|
-
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
|
|
131
|
-
logger.info("OTLP HTTP exporter enabled - endpoint: %s", otlp_config["endpoint"])
|
|
132
|
-
except ImportError:
|
|
133
|
-
logger.warning(
|
|
134
|
-
"OTLP export requested but opentelemetry-exporter-otlp-proto-http not installed. "
|
|
135
|
-
"Install with: pip install 'hud-python[agent]'"
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
# ------------------------------------------------------------------
|
|
139
|
-
# 4. Activate provider and instrumentation
|
|
140
|
-
# ------------------------------------------------------------------
|
|
141
|
-
trace.set_tracer_provider(provider)
|
|
142
|
-
install_mcp_instrumentation(provider)
|
|
143
|
-
|
|
144
|
-
# Install in-memory collector if requested
|
|
145
|
-
if enable_collection:
|
|
146
|
-
install_collector()
|
|
147
|
-
enable_trace_collection(True)
|
|
148
|
-
logger.debug("In-memory trace collection enabled")
|
|
149
|
-
|
|
150
|
-
# Agent instrumentation now handled by @hud.instrument decorators
|
|
151
|
-
logger.debug("OpenTelemetry configuration completed")
|
|
152
|
-
|
|
153
|
-
logger.debug("OpenTelemetry configured (provider id=%s)", id(provider))
|
|
154
|
-
return provider
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def shutdown_telemetry() -> None:
|
|
158
|
-
"""Flush and shutdown the global provider (if configured)."""
|
|
159
|
-
global _TRACER_PROVIDER
|
|
160
|
-
if _TRACER_PROVIDER is None:
|
|
161
|
-
return
|
|
162
|
-
_TRACER_PROVIDER.shutdown() # type: ignore[arg-type]
|
|
163
|
-
_TRACER_PROVIDER = None
|
|
164
|
-
logger.debug("OpenTelemetry shutdown complete")
|
|
1
|
+
"""Central configuration for OpenTelemetry inside HUD SDK.
|
|
2
|
+
|
|
3
|
+
This file is responsible for
|
|
4
|
+
1. creating the global ``TracerProvider``
|
|
5
|
+
2. attaching span processors (HUD enrichment, batch + exporter)
|
|
6
|
+
3. activating the community MCP instrumentation so that *every* MCP
|
|
7
|
+
request/response/notification is traced automatically.
|
|
8
|
+
|
|
9
|
+
It is *idempotent*: calling :func:`configure_telemetry` more than once
|
|
10
|
+
returns the same provider and does nothing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from opentelemetry import trace
|
|
19
|
+
from opentelemetry.sdk.resources import Resource
|
|
20
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
21
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
22
|
+
|
|
23
|
+
from hud.settings import settings
|
|
24
|
+
|
|
25
|
+
from .collector import enable_trace_collection, install_collector
|
|
26
|
+
from .exporters import HudSpanExporter
|
|
27
|
+
from .instrumentation import install_mcp_instrumentation
|
|
28
|
+
from .processors import HudEnrichmentProcessor
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# Global singleton provider so multiple calls do not create duplicates
|
|
33
|
+
_TRACER_PROVIDER: TracerProvider | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_telemetry_configured() -> bool:
|
|
37
|
+
"""Check if telemetry has been configured."""
|
|
38
|
+
return _TRACER_PROVIDER is not None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Public API
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def configure_telemetry(
|
|
47
|
+
*,
|
|
48
|
+
service_name: str = "hud-sdk",
|
|
49
|
+
service_version: str | None = None,
|
|
50
|
+
environment: str | None = None,
|
|
51
|
+
extra_resource_attributes: dict[str, Any] | None = None,
|
|
52
|
+
enable_otlp: bool = False,
|
|
53
|
+
otlp_endpoint: str | None = None,
|
|
54
|
+
otlp_headers: dict[str, str] | None = None,
|
|
55
|
+
enable_collection: bool = True,
|
|
56
|
+
) -> TracerProvider:
|
|
57
|
+
"""Initialise OpenTelemetry for the current Python process.
|
|
58
|
+
|
|
59
|
+
It is safe to call this in every entry-point; the provider will only
|
|
60
|
+
be created once.
|
|
61
|
+
"""
|
|
62
|
+
global _TRACER_PROVIDER
|
|
63
|
+
|
|
64
|
+
if _TRACER_PROVIDER is not None:
|
|
65
|
+
return _TRACER_PROVIDER
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# 1. Resource (identity of this service)
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
res_attrs: dict[str, Any] = {
|
|
71
|
+
"service.name": service_name,
|
|
72
|
+
"telemetry.sdk.name": "hud-otel",
|
|
73
|
+
"telemetry.sdk.language": "python",
|
|
74
|
+
}
|
|
75
|
+
if service_version:
|
|
76
|
+
res_attrs["service.version"] = service_version
|
|
77
|
+
if environment:
|
|
78
|
+
res_attrs["deployment.environment"] = environment
|
|
79
|
+
if extra_resource_attributes:
|
|
80
|
+
res_attrs.update(extra_resource_attributes)
|
|
81
|
+
|
|
82
|
+
resource = Resource.create(res_attrs)
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# 2. Provider
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
provider = TracerProvider(resource=resource)
|
|
88
|
+
_TRACER_PROVIDER = provider
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# 3. Processors / exporters
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
provider.add_span_processor(HudEnrichmentProcessor())
|
|
94
|
+
|
|
95
|
+
# HUD exporter (only if enabled and API key is available)
|
|
96
|
+
if settings.telemetry_enabled and settings.api_key:
|
|
97
|
+
exporter = HudSpanExporter(
|
|
98
|
+
telemetry_url=settings.hud_telemetry_url, api_key=settings.api_key
|
|
99
|
+
)
|
|
100
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
101
|
+
elif settings.telemetry_enabled and not settings.api_key and not enable_otlp:
|
|
102
|
+
# Error if no exporters are configured
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"No telemetry backend configured. Either:\n"
|
|
105
|
+
"1. Set HUD_API_KEY environment variable for HUD telemetry\n"
|
|
106
|
+
"2. Use enable_otlp=True with configure_telemetry() for alternative backends (e.g., Jaeger)\n" # noqa: E501
|
|
107
|
+
)
|
|
108
|
+
elif not settings.telemetry_enabled:
|
|
109
|
+
logger.info("HUD telemetry disabled via HUD_TELEMETRY_ENABLED=false")
|
|
110
|
+
|
|
111
|
+
# OTLP exporter (optional - for standard OTel viewers)
|
|
112
|
+
if enable_otlp:
|
|
113
|
+
try:
|
|
114
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
115
|
+
|
|
116
|
+
otlp_config = {}
|
|
117
|
+
if otlp_endpoint:
|
|
118
|
+
otlp_config["endpoint"] = otlp_endpoint
|
|
119
|
+
# Default to HTTP endpoint if not specified
|
|
120
|
+
if not otlp_endpoint.startswith(("http://", "https://")):
|
|
121
|
+
otlp_config["endpoint"] = f"http://{otlp_endpoint}/v1/traces"
|
|
122
|
+
else:
|
|
123
|
+
# Default HTTP endpoint
|
|
124
|
+
otlp_config["endpoint"] = "http://localhost:4318/v1/traces"
|
|
125
|
+
|
|
126
|
+
if otlp_headers:
|
|
127
|
+
otlp_config["headers"] = otlp_headers
|
|
128
|
+
|
|
129
|
+
otlp_exporter = OTLPSpanExporter(**otlp_config)
|
|
130
|
+
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
|
|
131
|
+
logger.info("OTLP HTTP exporter enabled - endpoint: %s", otlp_config["endpoint"])
|
|
132
|
+
except ImportError:
|
|
133
|
+
logger.warning(
|
|
134
|
+
"OTLP export requested but opentelemetry-exporter-otlp-proto-http not installed. "
|
|
135
|
+
"Install with: pip install 'hud-python[agent]'"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
# 4. Activate provider and instrumentation
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
trace.set_tracer_provider(provider)
|
|
142
|
+
install_mcp_instrumentation(provider)
|
|
143
|
+
|
|
144
|
+
# Install in-memory collector if requested
|
|
145
|
+
if enable_collection:
|
|
146
|
+
install_collector()
|
|
147
|
+
enable_trace_collection(True)
|
|
148
|
+
logger.debug("In-memory trace collection enabled")
|
|
149
|
+
|
|
150
|
+
# Agent instrumentation now handled by @hud.instrument decorators
|
|
151
|
+
logger.debug("OpenTelemetry configuration completed")
|
|
152
|
+
|
|
153
|
+
logger.debug("OpenTelemetry configured (provider id=%s)", id(provider))
|
|
154
|
+
return provider
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def shutdown_telemetry() -> None:
|
|
158
|
+
"""Flush and shutdown the global provider (if configured)."""
|
|
159
|
+
global _TRACER_PROVIDER
|
|
160
|
+
if _TRACER_PROVIDER is None:
|
|
161
|
+
return
|
|
162
|
+
_TRACER_PROVIDER.shutdown() # type: ignore[arg-type]
|
|
163
|
+
_TRACER_PROVIDER = None
|
|
164
|
+
logger.debug("OpenTelemetry shutdown complete")
|