hud-python 0.4.50__py3-none-any.whl → 0.4.52__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 +13 -1
- hud/agents/base.py +5 -1
- hud/agents/lite_llm.py +1 -1
- hud/agents/tests/test_base.py +8 -16
- hud/cli/__init__.py +12 -22
- hud/cli/eval.py +53 -84
- hud/cli/tests/test_build.py +2 -1
- hud/cli/tests/test_eval.py +4 -0
- hud/cli/tests/test_mcp_server.py +1 -1
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/version_check.py +257 -0
- hud/clients/base.py +1 -1
- hud/clients/mcp_use.py +3 -1
- hud/datasets/parallel.py +2 -2
- hud/datasets/runner.py +85 -24
- hud/otel/config.py +8 -6
- hud/otel/context.py +4 -4
- hud/otel/exporters.py +231 -57
- hud/rl/learner.py +1 -1
- hud/server/router.py +1 -1
- hud/shared/exceptions.py +0 -5
- hud/shared/tests/test_exceptions.py +17 -16
- hud/telemetry/__init__.py +30 -6
- hud/telemetry/async_context.py +331 -0
- hud/telemetry/job.py +51 -12
- hud/telemetry/tests/test_trace.py +4 -4
- hud/telemetry/trace.py +16 -17
- hud/tools/computer/qwen.py +4 -1
- hud/tools/executors/base.py +4 -2
- hud/utils/task_tracking.py +223 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.50.dist-info → hud_python-0.4.52.dist-info}/METADATA +2 -1
- {hud_python-0.4.50.dist-info → hud_python-0.4.52.dist-info}/RECORD +37 -34
- {hud_python-0.4.50.dist-info → hud_python-0.4.52.dist-info}/WHEEL +0 -0
- {hud_python-0.4.50.dist-info → hud_python-0.4.52.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.50.dist-info → hud_python-0.4.52.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Version checking utilities for HUD CLI.
|
|
2
|
+
|
|
3
|
+
This module handles checking for updates to the hud-python package
|
|
4
|
+
and prompting users to upgrade when a new version is available.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Checks PyPI for the latest version of hud-python
|
|
8
|
+
- Caches results for 6 hours to avoid excessive API calls
|
|
9
|
+
- Displays a friendly prompt when an update is available
|
|
10
|
+
- Can be disabled with HUD_SKIP_VERSION_CHECK=1 environment variable
|
|
11
|
+
|
|
12
|
+
The version check runs automatically at the start of most CLI commands,
|
|
13
|
+
but is skipped for help and version commands to keep them fast.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import contextlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import NamedTuple
|
|
24
|
+
|
|
25
|
+
import httpx
|
|
26
|
+
from packaging import version
|
|
27
|
+
|
|
28
|
+
from hud.utils.hud_console import HUDConsole
|
|
29
|
+
|
|
30
|
+
# Cache location for version check data
|
|
31
|
+
CACHE_DIR = Path.home() / ".hud" / ".cache"
|
|
32
|
+
VERSION_CACHE_FILE = CACHE_DIR / "version_check.json"
|
|
33
|
+
|
|
34
|
+
# Cache duration in seconds (6 hours)
|
|
35
|
+
CACHE_DURATION = 6 * 60 * 60
|
|
36
|
+
|
|
37
|
+
# PyPI API URL for package info
|
|
38
|
+
PYPI_URL = "https://pypi.org/pypi/hud-python/json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VersionInfo(NamedTuple):
|
|
42
|
+
"""Version information from PyPI."""
|
|
43
|
+
|
|
44
|
+
latest: str
|
|
45
|
+
current: str
|
|
46
|
+
is_outdated: bool
|
|
47
|
+
checked_at: float
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_current_version() -> str:
|
|
51
|
+
"""Get the currently installed version of hud-python."""
|
|
52
|
+
try:
|
|
53
|
+
from hud import __version__
|
|
54
|
+
|
|
55
|
+
return __version__
|
|
56
|
+
except ImportError:
|
|
57
|
+
return "unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _fetch_latest_version() -> str | None:
|
|
61
|
+
"""Fetch the latest version from PyPI.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The latest version string, or None if the request fails.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
with httpx.Client(timeout=3.0) as client:
|
|
68
|
+
response = client.get(PYPI_URL)
|
|
69
|
+
if response.status_code == 200:
|
|
70
|
+
data = response.json()
|
|
71
|
+
return data["info"]["version"]
|
|
72
|
+
except Exception: # noqa: S110
|
|
73
|
+
# Silently fail - we don't want to disrupt the user's workflow
|
|
74
|
+
# if PyPI is down or there's a network issue
|
|
75
|
+
pass
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load_cache() -> VersionInfo | None:
|
|
80
|
+
"""Load cached version information.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Cached VersionInfo if valid, None otherwise.
|
|
84
|
+
"""
|
|
85
|
+
if not VERSION_CACHE_FILE.exists():
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
with open(VERSION_CACHE_FILE) as f:
|
|
90
|
+
data = json.load(f)
|
|
91
|
+
|
|
92
|
+
# Check if cache is still valid
|
|
93
|
+
if time.time() - data["checked_at"] > CACHE_DURATION:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
return VersionInfo(
|
|
97
|
+
latest=data["latest"],
|
|
98
|
+
current=data["current"],
|
|
99
|
+
is_outdated=data["is_outdated"],
|
|
100
|
+
checked_at=data["checked_at"],
|
|
101
|
+
)
|
|
102
|
+
except Exception:
|
|
103
|
+
# If cache is corrupted, return None
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _save_cache(info: VersionInfo) -> None:
|
|
108
|
+
"""Save version information to cache.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
info: Version information to cache.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
# Create cache directory if it doesn't exist
|
|
115
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
with open(VERSION_CACHE_FILE, "w") as f:
|
|
118
|
+
json.dump(
|
|
119
|
+
{
|
|
120
|
+
"latest": info.latest,
|
|
121
|
+
"current": info.current,
|
|
122
|
+
"is_outdated": info.is_outdated,
|
|
123
|
+
"checked_at": info.checked_at,
|
|
124
|
+
},
|
|
125
|
+
f,
|
|
126
|
+
)
|
|
127
|
+
except Exception: # noqa: S110
|
|
128
|
+
# Silently fail if we can't write cache
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _compare_versions(current: str, latest: str) -> bool:
|
|
133
|
+
"""Compare versions to determine if an update is available.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
current: Current version string
|
|
137
|
+
latest: Latest version string
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if latest is newer than current, False otherwise.
|
|
141
|
+
"""
|
|
142
|
+
if current == "unknown":
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
current_v = version.parse(current)
|
|
147
|
+
latest_v = version.parse(latest)
|
|
148
|
+
return latest_v > current_v
|
|
149
|
+
except Exception:
|
|
150
|
+
# If we can't parse versions, assume no update needed
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def check_for_updates() -> VersionInfo | None:
|
|
155
|
+
"""Check for updates to hud-python.
|
|
156
|
+
|
|
157
|
+
This function checks PyPI for the latest version and caches the result
|
|
158
|
+
for 6 hours to avoid excessive API calls.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
VersionInfo if check succeeds, None otherwise.
|
|
162
|
+
"""
|
|
163
|
+
# Check if we're in CI/testing environment
|
|
164
|
+
if os.environ.get("CI") or os.environ.get("HUD_SKIP_VERSION_CHECK"):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
# Get current version first
|
|
168
|
+
current = _get_current_version()
|
|
169
|
+
if current == "unknown":
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Try to load from cache
|
|
173
|
+
cached_info = _load_cache()
|
|
174
|
+
|
|
175
|
+
# If cache exists but current version has changed (user upgraded), invalidate cache
|
|
176
|
+
if cached_info and cached_info.current != current:
|
|
177
|
+
cached_info = None # Force fresh check
|
|
178
|
+
|
|
179
|
+
if cached_info:
|
|
180
|
+
# Update the current version in the cached info to reflect reality
|
|
181
|
+
# but keep the cached latest version and timestamp
|
|
182
|
+
return VersionInfo(
|
|
183
|
+
latest=cached_info.latest,
|
|
184
|
+
current=current, # Use actual current version, not cached
|
|
185
|
+
is_outdated=_compare_versions(current, cached_info.latest),
|
|
186
|
+
checked_at=cached_info.checked_at,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Fetch latest version from PyPI
|
|
190
|
+
latest = _fetch_latest_version()
|
|
191
|
+
if not latest:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
# Compare versions
|
|
195
|
+
is_outdated = _compare_versions(current, latest)
|
|
196
|
+
|
|
197
|
+
# Create version info
|
|
198
|
+
info = VersionInfo(
|
|
199
|
+
latest=latest,
|
|
200
|
+
current=current,
|
|
201
|
+
is_outdated=is_outdated,
|
|
202
|
+
checked_at=time.time(),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Save to cache
|
|
206
|
+
_save_cache(info)
|
|
207
|
+
|
|
208
|
+
return info
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def display_update_prompt(console: HUDConsole | None = None) -> None:
|
|
212
|
+
"""Display update prompt if a new version is available.
|
|
213
|
+
|
|
214
|
+
This function checks for updates and displays a prompt to the user
|
|
215
|
+
if their version is outdated.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
console: HUDConsole instance for output. If None, creates a new one.
|
|
219
|
+
"""
|
|
220
|
+
if console is None:
|
|
221
|
+
console = HUDConsole()
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
info = check_for_updates()
|
|
225
|
+
if info and info.is_outdated:
|
|
226
|
+
# Create update message
|
|
227
|
+
update_msg = (
|
|
228
|
+
f"🆕 A new version of hud-python is available: "
|
|
229
|
+
f"[bold cyan]{info.latest}[/bold cyan] "
|
|
230
|
+
f"(current: [dim]{info.current}[/dim])\n"
|
|
231
|
+
f" Run: [bold yellow]uv tool upgrade hud-python[/bold yellow] to update"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Display as a subtle but noticeable panel
|
|
235
|
+
console._stdout_console.print(
|
|
236
|
+
f"\n[yellow]{update_msg}[/yellow]\n",
|
|
237
|
+
highlight=False,
|
|
238
|
+
)
|
|
239
|
+
except Exception: # noqa: S110
|
|
240
|
+
# Never let version checking disrupt the user's workflow
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def force_version_check() -> VersionInfo | None:
|
|
245
|
+
"""Force a version check, bypassing the cache.
|
|
246
|
+
|
|
247
|
+
This is useful for explicit version checks or testing.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
VersionInfo if check succeeds, None otherwise.
|
|
251
|
+
"""
|
|
252
|
+
# Clear the cache to force a fresh check
|
|
253
|
+
if VERSION_CACHE_FILE.exists():
|
|
254
|
+
with contextlib.suppress(Exception):
|
|
255
|
+
VERSION_CACHE_FILE.unlink()
|
|
256
|
+
|
|
257
|
+
return check_for_updates()
|
hud/clients/base.py
CHANGED
|
@@ -170,7 +170,7 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
170
170
|
if self._initialized:
|
|
171
171
|
await self._disconnect()
|
|
172
172
|
self._initialized = False
|
|
173
|
-
hud_console.info("Shutdown completed")
|
|
173
|
+
hud_console.info("Environment Shutdown completed")
|
|
174
174
|
else:
|
|
175
175
|
hud_console.debug("Client was not initialized, skipping disconnect")
|
|
176
176
|
|
hud/clients/mcp_use.py
CHANGED
|
@@ -92,7 +92,9 @@ class MCPUseHUDClient(BaseHUDClient):
|
|
|
92
92
|
try:
|
|
93
93
|
assert self._client is not None # noqa: S101
|
|
94
94
|
self._sessions = await self._client.create_all_sessions()
|
|
95
|
-
|
|
95
|
+
session_count = len(self._sessions)
|
|
96
|
+
session_text = "session" if session_count == 1 else "sessions"
|
|
97
|
+
hud_console.info(f"Created {session_count} MCP {session_text}")
|
|
96
98
|
|
|
97
99
|
# Configure validation for all sessions based on client setting
|
|
98
100
|
try:
|
hud/datasets/parallel.py
CHANGED
|
@@ -111,13 +111,13 @@ def _process_worker(
|
|
|
111
111
|
"""Process a single task with telemetry tracking."""
|
|
112
112
|
async with sem:
|
|
113
113
|
try:
|
|
114
|
-
# Create trace for this task (linked to the job)
|
|
114
|
+
# Create trace for this task (linked to the job)
|
|
115
115
|
task_name = task_dict.get("prompt") or f"Task {index}"
|
|
116
116
|
|
|
117
117
|
# Use the job_id to group all tasks under the same job
|
|
118
118
|
raw_task_id = task_dict.get("id")
|
|
119
119
|
safe_task_id = str(raw_task_id) if raw_task_id is not None else None
|
|
120
|
-
with hud.
|
|
120
|
+
async with hud.async_trace(task_name, job_id=job_id, task_id=safe_task_id):
|
|
121
121
|
# Convert dict to Task
|
|
122
122
|
task = Task(**task_dict)
|
|
123
123
|
|
hud/datasets/runner.py
CHANGED
|
@@ -28,8 +28,11 @@ async def run_dataset(
|
|
|
28
28
|
split: str = "train",
|
|
29
29
|
auto_respond: bool = False,
|
|
30
30
|
) -> list[Any]:
|
|
31
|
-
"""
|
|
32
|
-
|
|
31
|
+
"""Run all tasks in a dataset with automatic job and telemetry tracking.
|
|
32
|
+
|
|
33
|
+
This function handles concurrent task execution with proper telemetry collection.
|
|
34
|
+
All tasks are executed in parallel up to `max_concurrent`, with full telemetry
|
|
35
|
+
automatically uploaded to the HUD platform.
|
|
33
36
|
|
|
34
37
|
Args:
|
|
35
38
|
name: Name for the job
|
|
@@ -37,23 +40,27 @@ async def run_dataset(
|
|
|
37
40
|
Dataset object, OR list of Task objects
|
|
38
41
|
agent_class: Agent class to instantiate (e.g., ClaudeAgent)
|
|
39
42
|
agent_config: Configuration/kwargs for agent (model, etc.)
|
|
40
|
-
max_concurrent: Maximum parallel task execution
|
|
43
|
+
max_concurrent: Maximum parallel task execution. Higher values improve throughput
|
|
44
|
+
but may increase memory usage. Recommended: 30-200 depending on
|
|
45
|
+
task complexity and available resources.
|
|
41
46
|
metadata: Optional metadata for the job
|
|
42
47
|
max_steps: Maximum steps per task
|
|
43
48
|
split: Dataset split to use when loading from string (default: "train")
|
|
44
49
|
auto_respond: Whether to use auto-response agent
|
|
45
50
|
|
|
46
51
|
Returns:
|
|
47
|
-
List of results from agent.run() in dataset order
|
|
52
|
+
List of results from agent.run() in dataset order. Telemetry is automatically
|
|
53
|
+
collected and uploaded for all tasks.
|
|
48
54
|
|
|
49
55
|
Example:
|
|
50
56
|
>>> from hud.agents import ClaudeAgent
|
|
51
|
-
>>> #
|
|
57
|
+
>>> # Basic usage with dataset identifier
|
|
52
58
|
>>> results = await run_dataset(
|
|
53
59
|
... "SheetBench Eval",
|
|
54
60
|
... "hud-evals/SheetBench-50",
|
|
55
61
|
... ClaudeAgent,
|
|
56
62
|
... {"model": "claude-3-5-sonnet-20241022"},
|
|
63
|
+
... max_concurrent=100, # Adjust based on your needs
|
|
57
64
|
... )
|
|
58
65
|
>>> # Option 2: From HuggingFace dataset object
|
|
59
66
|
>>> from datasets import load_dataset
|
|
@@ -62,9 +69,12 @@ async def run_dataset(
|
|
|
62
69
|
>>> # Option 3: From list of dicts
|
|
63
70
|
>>> tasks = [{"prompt": "...", "mcp_config": {...}, ...}, ...]
|
|
64
71
|
>>> results = await run_dataset("browser_eval", tasks, ClaudeAgent)
|
|
72
|
+
|
|
73
|
+
Note:
|
|
74
|
+
Telemetry collection and upload is handled automatically. The function ensures
|
|
75
|
+
all telemetry is flushed before returning, even at high concurrency levels.
|
|
65
76
|
"""
|
|
66
|
-
# Import here to avoid circular imports
|
|
67
|
-
import hud
|
|
77
|
+
import hud # Import here to avoid circular imports
|
|
68
78
|
|
|
69
79
|
dataset_link = None
|
|
70
80
|
|
|
@@ -91,33 +101,84 @@ async def run_dataset(
|
|
|
91
101
|
except Exception:
|
|
92
102
|
logger.warning("Failed to extract dataset verification info")
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
# Use async job context manager for high-concurrency telemetry
|
|
105
|
+
async with hud.async_job(name, metadata=job_metadata, dataset_link=dataset_link) as job_obj:
|
|
95
106
|
# Run tasks with semaphore for concurrency control
|
|
96
107
|
sem = asyncio.Semaphore(max_concurrent)
|
|
97
108
|
results: list[Any | None] = [None] * len(dataset)
|
|
98
109
|
|
|
99
110
|
async def _worker(index: int, task_dict: Any, max_steps: int = 10) -> None:
|
|
100
111
|
async with sem:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
try:
|
|
113
|
+
# Create trace for this task
|
|
114
|
+
task_name = task_dict.get("prompt") or f"Task {index}"
|
|
115
|
+
|
|
116
|
+
# Ensure task_id is a string for baggage propagation
|
|
117
|
+
raw_task_id = task_dict.get("id")
|
|
118
|
+
safe_task_id = str(raw_task_id) if raw_task_id is not None else None
|
|
119
|
+
async with hud.async_trace(task_name, job_id=job_obj.id, task_id=safe_task_id):
|
|
120
|
+
# with hud.trace(task_name, job_id=job_obj.id, task_id=safe_task_id):
|
|
121
|
+
# Convert dict to Task here, at trace level
|
|
122
|
+
task = Task(**task_dict)
|
|
123
|
+
|
|
124
|
+
agent = agent_class(**(agent_config or {}))
|
|
125
|
+
|
|
126
|
+
if auto_respond:
|
|
127
|
+
agent.response_agent = ResponseAgent()
|
|
128
|
+
results[index] = await agent.run(task, max_steps=max_steps)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.exception("Task %s failed: %s", index, e)
|
|
131
|
+
results[index] = None
|
|
116
132
|
|
|
117
133
|
# Execute all tasks
|
|
118
|
-
await asyncio.gather(
|
|
134
|
+
worker_results = await asyncio.gather(
|
|
119
135
|
*[_worker(i, task, max_steps=max_steps) for i, task in enumerate(dataset)],
|
|
120
136
|
return_exceptions=True, # Don't fail entire batch on one error
|
|
121
137
|
)
|
|
122
138
|
|
|
139
|
+
# Log any exceptions that occurred
|
|
140
|
+
for i, result in enumerate(worker_results):
|
|
141
|
+
if isinstance(result, Exception):
|
|
142
|
+
logger.error("Worker %s failed with exception: %s", i, result, exc_info=result)
|
|
143
|
+
|
|
144
|
+
# Ensure all telemetry is uploaded before returning
|
|
145
|
+
await _flush_telemetry()
|
|
146
|
+
|
|
123
147
|
return results
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def _flush_telemetry() -> None:
|
|
151
|
+
"""Flush all pending telemetry operations.
|
|
152
|
+
|
|
153
|
+
Ensures complete telemetry upload by:
|
|
154
|
+
1. Waiting for all async status updates to complete
|
|
155
|
+
2. Forcing OpenTelemetry span processor to export remaining spans
|
|
156
|
+
|
|
157
|
+
This prevents telemetry loss at high concurrency (200+ tasks) by ensuring
|
|
158
|
+
all operations complete before process exit.
|
|
159
|
+
"""
|
|
160
|
+
from hud.otel.config import is_telemetry_configured
|
|
161
|
+
from hud.utils import hud_console
|
|
162
|
+
from hud.utils.task_tracking import wait_all_tasks
|
|
163
|
+
|
|
164
|
+
hud_console.info("Uploading telemetry...")
|
|
165
|
+
|
|
166
|
+
# Step 1: Wait for async status updates (job/trace status)
|
|
167
|
+
completed_tasks = await wait_all_tasks(timeout_seconds=20.0)
|
|
168
|
+
if completed_tasks > 0:
|
|
169
|
+
hud_console.info(f"Completed {completed_tasks} pending telemetry tasks")
|
|
170
|
+
|
|
171
|
+
# Step 2: Flush OpenTelemetry span exports
|
|
172
|
+
if is_telemetry_configured():
|
|
173
|
+
try:
|
|
174
|
+
from opentelemetry import trace
|
|
175
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
176
|
+
|
|
177
|
+
provider = trace.get_tracer_provider()
|
|
178
|
+
if isinstance(provider, TracerProvider):
|
|
179
|
+
provider.force_flush(timeout_millis=20000)
|
|
180
|
+
logger.debug("OpenTelemetry spans flushed successfully")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.warning("Failed to flush OpenTelemetry: %s", e)
|
|
183
|
+
|
|
184
|
+
hud_console.info("Telemetry uploaded successfully")
|
hud/otel/config.py
CHANGED
|
@@ -94,16 +94,18 @@ def configure_telemetry(
|
|
|
94
94
|
|
|
95
95
|
# HUD exporter (only if enabled and API key is available)
|
|
96
96
|
if settings.telemetry_enabled and settings.api_key:
|
|
97
|
+
# Use the HudSpanExporter directly (it now handles async context internally)
|
|
97
98
|
exporter = HudSpanExporter(
|
|
98
99
|
telemetry_url=settings.hud_telemetry_url, api_key=settings.api_key
|
|
99
100
|
)
|
|
100
|
-
|
|
101
|
+
|
|
102
|
+
# Batch exports for efficiency while maintaining reasonable real-time visibility
|
|
101
103
|
provider.add_span_processor(
|
|
102
104
|
BatchSpanProcessor(
|
|
103
105
|
exporter,
|
|
104
|
-
schedule_delay_millis=1000,
|
|
105
|
-
max_queue_size=
|
|
106
|
-
max_export_batch_size=
|
|
106
|
+
schedule_delay_millis=1000, # Export every 5 seconds (less frequent)
|
|
107
|
+
max_queue_size=16384, # Larger queue for high-volume scenarios
|
|
108
|
+
max_export_batch_size=512, # Larger batches (fewer uploads)
|
|
107
109
|
export_timeout_millis=30000,
|
|
108
110
|
)
|
|
109
111
|
)
|
|
@@ -140,8 +142,8 @@ def configure_telemetry(
|
|
|
140
142
|
BatchSpanProcessor(
|
|
141
143
|
otlp_exporter,
|
|
142
144
|
schedule_delay_millis=1000,
|
|
143
|
-
max_queue_size=
|
|
144
|
-
max_export_batch_size=
|
|
145
|
+
max_queue_size=16384,
|
|
146
|
+
max_export_batch_size=512,
|
|
145
147
|
export_timeout_millis=30000,
|
|
146
148
|
)
|
|
147
149
|
)
|
hud/otel/context.py
CHANGED
|
@@ -520,8 +520,8 @@ class trace:
|
|
|
520
520
|
# Update task status if root (only for HUD backend)
|
|
521
521
|
if self.is_root and settings.telemetry_enabled and settings.api_key:
|
|
522
522
|
if exc_type is not None:
|
|
523
|
-
# Use
|
|
524
|
-
|
|
523
|
+
# Use fire-and-forget to avoid blocking the event loop
|
|
524
|
+
_fire_and_forget_status_update(
|
|
525
525
|
self.task_run_id,
|
|
526
526
|
"error",
|
|
527
527
|
job_id=self.job_id,
|
|
@@ -533,8 +533,8 @@ class trace:
|
|
|
533
533
|
if not self.job_id:
|
|
534
534
|
_print_trace_complete_url(self.task_run_id, error_occurred=True)
|
|
535
535
|
else:
|
|
536
|
-
# Use
|
|
537
|
-
|
|
536
|
+
# Use fire-and-forget to avoid blocking the event loop
|
|
537
|
+
_fire_and_forget_status_update(
|
|
538
538
|
self.task_run_id,
|
|
539
539
|
"completed",
|
|
540
540
|
job_id=self.job_id,
|