|
13 | 13 | import time |
14 | 14 | from typing import Any, Dict, List, Optional |
15 | 15 |
|
16 | | -from openspace.observability.metrics import metrics as _metrics |
17 | | -from openspace.observability.tracing import tracer as _tracer |
| 16 | +try: |
| 17 | + from openspace.observability.metrics import metrics as _metrics |
| 18 | + from openspace.observability.tracing import tracer as _tracer |
| 19 | +except ImportError: |
| 20 | + _metrics = None # type: ignore[assignment] |
| 21 | + _tracer = None # type: ignore[assignment] |
18 | 22 | from openspace.prompts import GroundingAgentPrompts |
19 | 23 | from openspace.utils.logging import Logger |
20 | 24 |
|
@@ -105,16 +109,21 @@ async def process(agent, context: Dict[str, Any]) -> Dict[str, Any]: |
105 | 109 | tools=tools, |
106 | 110 | ) |
107 | 111 |
|
| 112 | + # ── Observability setup (isolated — must not kill execution) ────── |
| 113 | + _gauge_incremented = False |
108 | 114 | try: |
109 | | - # ── Observability: trace + metrics inside try to prevent leaks ── |
110 | 115 | _instr_hash = hashlib.sha256(instruction.encode()).hexdigest()[:12] |
111 | | - trace = _tracer.start_trace( |
| 116 | + _tracer.start_trace( |
112 | 117 | "grounding.process", |
113 | 118 | instruction_hash=_instr_hash, |
114 | 119 | instruction_length=len(instruction), |
115 | 120 | ) |
116 | 121 | _metrics.execution_in_flight.labels(agent=_agent_label).inc() |
| 122 | + _gauge_incremented = True |
| 123 | + except Exception: |
| 124 | + logger.debug("Observability setup failed", exc_info=True) |
117 | 125 |
|
| 126 | + try: |
118 | 127 | while current_iteration < max_iterations: |
119 | 128 | current_iteration += 1 |
120 | 129 | logger.info( |
@@ -293,33 +302,42 @@ async def process(agent, context: Dict[str, Any]) -> Dict[str, Any]: |
293 | 302 | f"{result.get('status')}" |
294 | 303 | ) |
295 | 304 |
|
296 | | - # ── Observability: record success metrics ──────────────────── |
297 | | - _elapsed = time.monotonic() - _exec_start |
298 | | - _metrics.execution_latency.labels(agent=_agent_label, status="success").observe(_elapsed) |
299 | | - _metrics.execution_iterations.labels(agent=_agent_label).observe(current_iteration) |
300 | | - _metrics.execution_total.labels(agent=_agent_label, status="success").inc() |
301 | | - _metrics.execution_in_flight.labels(agent=_agent_label).dec() |
302 | | - root_span = _tracer.current_span() |
303 | | - if root_span: |
304 | | - root_span.attributes["iterations"] = current_iteration |
305 | | - root_span.attributes["status"] = result.get("status", "unknown") |
306 | | - _tracer.finish_trace() |
| 305 | + # ── Observability: record success (isolated — must not discard result) |
| 306 | + try: |
| 307 | + _elapsed = time.monotonic() - _exec_start |
| 308 | + _metrics.execution_latency.labels(agent=_agent_label, status="success").observe(_elapsed) |
| 309 | + _metrics.execution_iterations.labels(agent=_agent_label).observe(current_iteration) |
| 310 | + _metrics.execution_total.labels(agent=_agent_label, status="success").inc() |
| 311 | + if _gauge_incremented: |
| 312 | + _metrics.execution_in_flight.labels(agent=_agent_label).dec() |
| 313 | + _gauge_incremented = False |
| 314 | + root_span = _tracer.current_span() |
| 315 | + if root_span: |
| 316 | + root_span.attributes["iterations"] = current_iteration |
| 317 | + root_span.attributes["status"] = result.get("status", "unknown") |
| 318 | + _tracer.finish_trace() |
| 319 | + except Exception: |
| 320 | + logger.debug("Observability recording failed", exc_info=True) |
307 | 321 |
|
308 | 322 | return result |
309 | 323 |
|
310 | 324 | except Exception as e: |
311 | 325 | logger.error(f"Grounding Agent: Execution failed: {e}") |
312 | 326 |
|
313 | | - # ── Observability: record error metrics ────────────────────── |
314 | | - _elapsed = time.monotonic() - _exec_start |
315 | | - _metrics.execution_latency.labels(agent=_agent_label, status="error").observe(_elapsed) |
316 | | - _metrics.execution_total.labels(agent=_agent_label, status="error").inc() |
317 | | - _metrics.execution_in_flight.labels(agent=_agent_label).dec() |
318 | | - root_span = _tracer.current_span() |
319 | | - if root_span: |
320 | | - root_span.add_event("error", error_type=type(e).__name__) |
321 | | - root_span.status = "error" |
322 | | - _tracer.finish_trace() |
| 327 | + # ── Observability: record error (isolated — must not mask result) |
| 328 | + try: |
| 329 | + _elapsed = time.monotonic() - _exec_start |
| 330 | + _metrics.execution_latency.labels(agent=_agent_label, status="error").observe(_elapsed) |
| 331 | + _metrics.execution_total.labels(agent=_agent_label, status="error").inc() |
| 332 | + if _gauge_incremented: |
| 333 | + _metrics.execution_in_flight.labels(agent=_agent_label).dec() |
| 334 | + root_span = _tracer.current_span() |
| 335 | + if root_span: |
| 336 | + root_span.add_event("error", error_type=type(e).__name__) |
| 337 | + root_span.status = "error" |
| 338 | + _tracer.finish_trace() |
| 339 | + except Exception: |
| 340 | + logger.debug("Observability recording failed", exc_info=True) |
323 | 341 |
|
324 | 342 | result = { |
325 | 343 | "error": type(e).__name__, |
@@ -367,16 +385,19 @@ def _build_retrieved_tools_list( |
367 | 385 | return retrieved_tools_list |
368 | 386 |
|
369 | 387 |
|
| 388 | +import re |
| 389 | + |
370 | 390 | # ── Label sanitization ─────────────────────────────────────────────── |
371 | 391 |
|
372 | | -# Prometheus label values should be bounded. We keep a short allowlist |
373 | | -# and map everything else to "unknown" to prevent cardinality bombs. |
374 | | -_LABEL_MAX_LEN = 64 |
| 392 | +# Prometheus label values must be bounded and safe. Strict allowlist |
| 393 | +# prevents cardinality bombs and scraper injection. |
| 394 | +_LABEL_MAX_LEN = 32 |
| 395 | +_LABEL_SAFE_RE = re.compile(r"[^a-zA-Z0-9_\-]") |
375 | 396 |
|
376 | 397 |
|
377 | 398 | def _sanitize_label(value: str) -> str: |
378 | | - """Clamp a label value to a safe length and replace spaces.""" |
| 399 | + """Sanitize a string for use as a Prometheus label value.""" |
379 | 400 | if not value: |
380 | 401 | return "unknown" |
381 | | - clean = value[:_LABEL_MAX_LEN].replace(" ", "_") |
382 | | - return clean |
| 402 | + clean = _LABEL_SAFE_RE.sub("_", value[:_LABEL_MAX_LEN]) |
| 403 | + return clean or "unknown" |
0 commit comments