Spaces:
Running on Zero
Running on Zero
agharsallah commited on
Commit Β·
e1959d5
1
Parent(s): bd9568a
feat: Enhance telemetry store with revision tracking and optimize UI refresh logic
Browse files- src/observability/store.py +30 -3
- src/ui/fishbowl/app.py +59 -18
- src/ui/fishbowl/render/telemetry.py +78 -18
src/observability/store.py
CHANGED
|
@@ -50,20 +50,26 @@ class TelemetryStore:
|
|
| 50 |
self._spans: deque[SpanRecord] = deque(maxlen=capacity)
|
| 51 |
self._metrics: deque[MetricPoint] = deque(maxlen=capacity * 4)
|
| 52 |
self._counters: dict[tuple, float] = {}
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
# ββ ingest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
|
| 56 |
def add_log(self, record: dict) -> None:
|
| 57 |
with self._lock:
|
| 58 |
self._logs.append(record)
|
|
|
|
| 59 |
|
| 60 |
def add_span(self, span: SpanRecord) -> None:
|
| 61 |
with self._lock:
|
| 62 |
self._spans.append(span)
|
|
|
|
| 63 |
|
| 64 |
def add_metric(self, point: MetricPoint, *, counter: bool = False) -> None:
|
| 65 |
with self._lock:
|
| 66 |
self._metrics.append(point)
|
|
|
|
| 67 |
if counter:
|
| 68 |
key = (point.name, tuple(sorted(point.labels.items())))
|
| 69 |
self._counters[key] = self._counters.get(key, 0.0) + point.value
|
|
@@ -78,10 +84,30 @@ class TelemetryStore:
|
|
| 78 |
with self._lock:
|
| 79 |
return list(self._spans)[-n:]
|
| 80 |
|
| 81 |
-
def metric_points(self, name: str | None = None) -> list[MetricPoint]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
with self._lock:
|
| 83 |
-
|
| 84 |
-
return [p for p in points if name is None or p.name == name]
|
| 85 |
|
| 86 |
def counter_totals(self) -> dict[str, float]:
|
| 87 |
"""Cumulative total per metric name, summed across label sets."""
|
|
@@ -102,6 +128,7 @@ class TelemetryStore:
|
|
| 102 |
self._spans.clear()
|
| 103 |
self._metrics.clear()
|
| 104 |
self._counters.clear()
|
|
|
|
| 105 |
|
| 106 |
|
| 107 |
def now_ts() -> float:
|
|
|
|
| 50 |
self._spans: deque[SpanRecord] = deque(maxlen=capacity)
|
| 51 |
self._metrics: deque[MetricPoint] = deque(maxlen=capacity * 4)
|
| 52 |
self._counters: dict[tuple, float] = {}
|
| 53 |
+
# Monotonic ingest counter β a cheap "has anything changed since I last looked?"
|
| 54 |
+
# signal so the Telemetry tab can skip recomputing/repainting on an idle tick.
|
| 55 |
+
self._rev = 0
|
| 56 |
|
| 57 |
# ββ ingest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
|
| 59 |
def add_log(self, record: dict) -> None:
|
| 60 |
with self._lock:
|
| 61 |
self._logs.append(record)
|
| 62 |
+
self._rev += 1
|
| 63 |
|
| 64 |
def add_span(self, span: SpanRecord) -> None:
|
| 65 |
with self._lock:
|
| 66 |
self._spans.append(span)
|
| 67 |
+
self._rev += 1
|
| 68 |
|
| 69 |
def add_metric(self, point: MetricPoint, *, counter: bool = False) -> None:
|
| 70 |
with self._lock:
|
| 71 |
self._metrics.append(point)
|
| 72 |
+
self._rev += 1
|
| 73 |
if counter:
|
| 74 |
key = (point.name, tuple(sorted(point.labels.items())))
|
| 75 |
self._counters[key] = self._counters.get(key, 0.0) + point.value
|
|
|
|
| 84 |
with self._lock:
|
| 85 |
return list(self._spans)[-n:]
|
| 86 |
|
| 87 |
+
def metric_points(self, name: str | None = None, limit: int | None = None) -> list[MetricPoint]:
|
| 88 |
+
"""Recorded points, optionally filtered by ``name``.
|
| 89 |
+
|
| 90 |
+
With ``limit`` set, only the most recent ``limit`` matching points are returned
|
| 91 |
+
(still chronological) β this bounds both the scan and the chart payload for
|
| 92 |
+
high-frequency metrics like agent-turn latency, which would otherwise grow until
|
| 93 |
+
the buffer is full.
|
| 94 |
+
"""
|
| 95 |
+
with self._lock:
|
| 96 |
+
if limit is None:
|
| 97 |
+
return [p for p in self._metrics if name is None or p.name == name]
|
| 98 |
+
out: list[MetricPoint] = []
|
| 99 |
+
for p in reversed(self._metrics): # newest-first, stop once we have `limit`
|
| 100 |
+
if name is None or p.name == name:
|
| 101 |
+
out.append(p)
|
| 102 |
+
if len(out) >= limit:
|
| 103 |
+
break
|
| 104 |
+
out.reverse()
|
| 105 |
+
return out
|
| 106 |
+
|
| 107 |
+
def revision(self) -> int:
|
| 108 |
+
"""Monotonic ingest counter β bumped on every log/span/metric and on ``clear``."""
|
| 109 |
with self._lock:
|
| 110 |
+
return self._rev
|
|
|
|
| 111 |
|
| 112 |
def counter_totals(self) -> dict[str, float]:
|
| 113 |
"""Cumulative total per metric name, summed across label sets."""
|
|
|
|
| 128 |
self._spans.clear()
|
| 129 |
self._metrics.clear()
|
| 130 |
self._counters.clear()
|
| 131 |
+
self._rev += 1 # a clear is a change too β let the UI repaint to empty
|
| 132 |
|
| 133 |
|
| 134 |
def now_ts() -> float:
|
src/ui/fishbowl/app.py
CHANGED
|
@@ -276,9 +276,12 @@ except Exception: # pragma: no cover
|
|
| 276 |
def build_telemetry() -> None:
|
| 277 |
"""The Telemetry tab: structured log feed + metric charts + per-trace timeline.
|
| 278 |
|
| 279 |
-
Reads live from the in-memory telemetry store (ADR-0024)
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
| 282 |
"""
|
| 283 |
try:
|
| 284 |
from src.ui.fishbowl.render import telemetry as t
|
|
@@ -306,24 +309,62 @@ def build_telemetry() -> None:
|
|
| 306 |
row_count=(12, "dynamic"),
|
| 307 |
column_count=(5, "fixed"),
|
| 308 |
)
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
timer = gr.Timer(3.0)
|
| 311 |
|
| 312 |
-
|
| 313 |
-
return (
|
| 314 |
-
t.kpi_markdown(),
|
| 315 |
-
t.log_rows(level, layer),
|
| 316 |
-
t.calls_frame(),
|
| 317 |
-
t.tokens_frame(),
|
| 318 |
-
t.latency_frame(),
|
| 319 |
-
t.traces_html(),
|
| 320 |
-
)
|
| 321 |
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
|
| 329 |
# ββ scenario registry (assembled from config/, not hardcoded) βββββββββββββββββββ
|
|
|
|
| 276 |
def build_telemetry() -> None:
|
| 277 |
"""The Telemetry tab: structured log feed + metric charts + per-trace timeline.
|
| 278 |
|
| 279 |
+
Reads live from the in-memory telemetry store (ADR-0024). The 3s auto-tick is
|
| 280 |
+
*non-blocking and idle-cheap*: it gates on the store's revision and returns
|
| 281 |
+
``gr.skip()`` (no recompute, no repaint, ``show_progress='hidden'`` so no overlay)
|
| 282 |
+
whenever nothing was recorded since the last paint. The filter dropdowns and β³
|
| 283 |
+
Refresh force a repaint; "show more traces" pages older traces into the timeline.
|
| 284 |
+
Clicking a span reveals the prompt + memory that agent saw.
|
| 285 |
"""
|
| 286 |
try:
|
| 287 |
from src.ui.fishbowl.render import telemetry as t
|
|
|
|
| 309 |
row_count=(12, "dynamic"),
|
| 310 |
column_count=(5, "fixed"),
|
| 311 |
)
|
| 312 |
+
_traces0, _shown0, _total0 = t.render_traces(t.DEFAULT_TRACES)
|
| 313 |
+
traces = gr.HTML(_traces0)
|
| 314 |
+
load_more = gr.Button(t.more_label(_shown0, _total0), interactive=_total0 > _shown0, size="sm")
|
| 315 |
+
|
| 316 |
+
# Per-user view state: the signature of the last paint (so an idle tick can skip the
|
| 317 |
+
# work) and how many traces the timeline currently shows ("show more" grows it).
|
| 318 |
+
sig_state = gr.State(None)
|
| 319 |
+
trace_limit_state = gr.State(t.DEFAULT_TRACES)
|
| 320 |
timer = gr.Timer(3.0)
|
| 321 |
|
| 322 |
+
outs = [kpi, feed, calls_plot, tokens_plot, latency_plot, traces, load_more]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
+
def _values(level, layer, trace_limit):
|
| 325 |
+
"""Recompute all panel outputs in one store pass (single counter read)."""
|
| 326 |
+
snap = t.snapshot(level, layer, trace_limit)
|
| 327 |
+
more = gr.update(
|
| 328 |
+
value=t.more_label(snap["trace_shown"], snap["trace_total"]),
|
| 329 |
+
interactive=snap["trace_total"] > snap["trace_shown"],
|
| 330 |
+
)
|
| 331 |
+
return (snap["kpi"], snap["feed"], snap["calls"], snap["tokens"], snap["latency"], snap["traces"], more)
|
| 332 |
+
|
| 333 |
+
def _sig(level, layer, trace_limit):
|
| 334 |
+
# Read the revision BEFORE rendering: if data lands mid-render the stored sig
|
| 335 |
+
# stays behind, so the next tick repaints to catch up (never a missed update).
|
| 336 |
+
return (t.revision(), level, layer, int(trace_limit))
|
| 337 |
+
|
| 338 |
+
def _tick(level, layer, trace_limit, last_sig):
|
| 339 |
+
"""Auto-refresh: repaint only when the store advanced or a filter/limit changed."""
|
| 340 |
+
sig = _sig(level, layer, trace_limit)
|
| 341 |
+
if sig == last_sig:
|
| 342 |
+
return (gr.skip(),) * (len(outs) + 1) # idle β leave every output (and sig) as-is
|
| 343 |
+
return _values(level, layer, trace_limit) + (sig,)
|
| 344 |
+
|
| 345 |
+
def _force(level, layer, trace_limit):
|
| 346 |
+
"""Manual Refresh / filter change: always repaint with the current filters."""
|
| 347 |
+
sig = _sig(level, layer, trace_limit)
|
| 348 |
+
return _values(level, layer, trace_limit) + (sig,)
|
| 349 |
+
|
| 350 |
+
def _load_more(level, layer, trace_limit):
|
| 351 |
+
"""Grow the timeline by one page and repaint (carries the new limit back to state)."""
|
| 352 |
+
new_limit = int(trace_limit) + t.TRACE_PAGE
|
| 353 |
+
sig = _sig(level, layer, new_limit)
|
| 354 |
+
return _values(level, layer, new_limit) + (sig, new_limit)
|
| 355 |
+
|
| 356 |
+
# Auto-tick is hidden (no progress overlay β no 3s flicker); user-driven actions show
|
| 357 |
+
# a minimal indicator so the click registers.
|
| 358 |
+
timer.tick(_tick, [level_dd, layer_dd, trace_limit_state, sig_state], outs + [sig_state], show_progress="hidden")
|
| 359 |
+
refresh.click(_force, [level_dd, layer_dd, trace_limit_state], outs + [sig_state], show_progress="minimal")
|
| 360 |
+
level_dd.change(_force, [level_dd, layer_dd, trace_limit_state], outs + [sig_state], show_progress="minimal")
|
| 361 |
+
layer_dd.change(_force, [level_dd, layer_dd, trace_limit_state], outs + [sig_state], show_progress="minimal")
|
| 362 |
+
load_more.click(
|
| 363 |
+
_load_more,
|
| 364 |
+
[level_dd, layer_dd, trace_limit_state],
|
| 365 |
+
outs + [sig_state, trace_limit_state],
|
| 366 |
+
show_progress="minimal",
|
| 367 |
+
)
|
| 368 |
|
| 369 |
|
| 370 |
# ββ scenario registry (assembled from config/, not hardcoded) βββββββββββββββββββ
|
src/ui/fishbowl/render/telemetry.py
CHANGED
|
@@ -71,9 +71,13 @@ def log_rows(level: str = "all", layer: str = "all", limit: int = 250) -> list[l
|
|
| 71 |
# ββ metrics (chart data) ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
|
| 73 |
|
| 74 |
-
def kpi_markdown() -> str:
|
| 75 |
-
"""A one-line headline of the key counters.
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
calls = int(c.get("llm.calls", 0))
|
| 78 |
tin, tout = int(c.get("llm.tokens.input", 0)), int(c.get("llm.tokens.output", 0))
|
| 79 |
cost = c.get("llm.cost_usd", 0.0)
|
|
@@ -95,9 +99,9 @@ def _df(rows: list[dict], columns: list[str]):
|
|
| 95 |
return pd.DataFrame(rows or [{c: None for c in columns}][:0], columns=columns)
|
| 96 |
|
| 97 |
|
| 98 |
-
def calls_frame():
|
| 99 |
"""Counts by metric (calls / tool calls / events / trips) for a bar chart."""
|
| 100 |
-
c = obs.telemetry_store().counter_totals()
|
| 101 |
keep = {
|
| 102 |
"llm.calls": "llm calls",
|
| 103 |
"tool.calls": "tool calls",
|
|
@@ -108,9 +112,9 @@ def calls_frame():
|
|
| 108 |
return _df(rows, ["metric", "count"])
|
| 109 |
|
| 110 |
|
| 111 |
-
def tokens_frame():
|
| 112 |
"""Input vs output tokens for a bar chart."""
|
| 113 |
-
c = obs.telemetry_store().counter_totals()
|
| 114 |
rows = [
|
| 115 |
{"kind": "input", "tokens": float(c.get("llm.tokens.input", 0))},
|
| 116 |
{"kind": "output", "tokens": float(c.get("llm.tokens.output", 0))},
|
|
@@ -118,9 +122,14 @@ def tokens_frame():
|
|
| 118 |
return _df(rows, ["kind", "tokens"])
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
def latency_frame():
|
| 122 |
-
"""Agent-turn latency
|
| 123 |
-
points = obs.telemetry_store().metric_points("agent.turn.seconds")
|
| 124 |
rows = [
|
| 125 |
{"n": i, "seconds": round(p.value, 4), "agent": str(p.labels.get("agent", "?"))} for i, p in enumerate(points)
|
| 126 |
]
|
|
@@ -132,26 +141,36 @@ def latency_frame():
|
|
| 132 |
_PROMPT_KEYS = ("llm.prompt", "agent.prompt", "memory.query")
|
| 133 |
_OUTPUT_KEYS = ("llm.completion", "memory.visible_count")
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
def
|
| 137 |
-
"""Recent spans grouped by trace,
|
| 138 |
|
| 139 |
-
Each span shows name + duration + status; spans
|
| 140 |
-
(``llm.prompt`` / ``agent`` / ``memory.*`` attributes)
|
| 141 |
-
what the agent sent and saw β the heart of the 'what did
|
|
|
|
|
|
|
| 142 |
"""
|
| 143 |
-
spans = obs.telemetry_store().recent_spans(
|
| 144 |
if not spans:
|
| 145 |
-
return "<div class='tele-empty'>No traces yet β run the show to populate the timeline.</div>"
|
| 146 |
|
| 147 |
by_trace: dict[str, list] = {}
|
| 148 |
for sp in spans:
|
| 149 |
by_trace.setdefault(sp.trace_id, []).append(sp)
|
| 150 |
# Most recent traces first (by max end time within the trace).
|
| 151 |
ordered = sorted(by_trace.items(), key=lambda kv: max(s.end_ms for s in kv[1]), reverse=True)
|
|
|
|
|
|
|
| 152 |
|
| 153 |
out: list[str] = []
|
| 154 |
-
for trace_id, group in ordered[:
|
| 155 |
depth = _depth_map(group)
|
| 156 |
root_dur = max(s.end_ms for s in group) - min(s.start_ms for s in group)
|
| 157 |
out.append(
|
|
@@ -161,7 +180,12 @@ def traces_html(limit_traces: int = 8) -> str:
|
|
| 161 |
for sp in sorted(group, key=lambda s: s.start_ms):
|
| 162 |
out.append(_span_html(sp, depth.get(sp.span_id, 0)))
|
| 163 |
out.append("</div>")
|
| 164 |
-
return "\n".join(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
|
| 167 |
def _depth_map(group: list) -> dict[str, int]:
|
|
@@ -210,6 +234,42 @@ def _span_html(sp, depth: int) -> str:
|
|
| 210 |
return head + (f"<div class='tele-span-body'>{body}</div>" if body else "") + "</div>"
|
| 211 |
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
TELEMETRY_CSS = """
|
| 214 |
.tele-trace { border:1px solid #2e3d25; border-radius:8px; margin:8px 0; padding:8px; background:#0b0f0a; }
|
| 215 |
.tele-trace-hd { color:#5fd0d0; font-weight:700; margin-bottom:6px; }
|
|
|
|
| 71 |
# ββ metrics (chart data) ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
|
| 73 |
|
| 74 |
+
def kpi_markdown(counters: dict | None = None) -> str:
|
| 75 |
+
"""A one-line headline of the key counters.
|
| 76 |
+
|
| 77 |
+
Pass a pre-read ``counters`` snapshot (see :func:`snapshot`) to avoid re-locking the
|
| 78 |
+
store; omit it and one is fetched.
|
| 79 |
+
"""
|
| 80 |
+
c = counters if counters is not None else obs.telemetry_store().counter_totals()
|
| 81 |
calls = int(c.get("llm.calls", 0))
|
| 82 |
tin, tout = int(c.get("llm.tokens.input", 0)), int(c.get("llm.tokens.output", 0))
|
| 83 |
cost = c.get("llm.cost_usd", 0.0)
|
|
|
|
| 99 |
return pd.DataFrame(rows or [{c: None for c in columns}][:0], columns=columns)
|
| 100 |
|
| 101 |
|
| 102 |
+
def calls_frame(counters: dict | None = None):
|
| 103 |
"""Counts by metric (calls / tool calls / events / trips) for a bar chart."""
|
| 104 |
+
c = counters if counters is not None else obs.telemetry_store().counter_totals()
|
| 105 |
keep = {
|
| 106 |
"llm.calls": "llm calls",
|
| 107 |
"tool.calls": "tool calls",
|
|
|
|
| 112 |
return _df(rows, ["metric", "count"])
|
| 113 |
|
| 114 |
|
| 115 |
+
def tokens_frame(counters: dict | None = None):
|
| 116 |
"""Input vs output tokens for a bar chart."""
|
| 117 |
+
c = counters if counters is not None else obs.telemetry_store().counter_totals()
|
| 118 |
rows = [
|
| 119 |
{"kind": "input", "tokens": float(c.get("llm.tokens.input", 0))},
|
| 120 |
{"kind": "output", "tokens": float(c.get("llm.tokens.output", 0))},
|
|
|
|
| 122 |
return _df(rows, ["kind", "tokens"])
|
| 123 |
|
| 124 |
|
| 125 |
+
#: Cap on how many latency observations the line chart plots β a rolling window that
|
| 126 |
+
#: keeps both the store scan and the browser-side chart light on long runs.
|
| 127 |
+
_LATENCY_POINTS = 300
|
| 128 |
+
|
| 129 |
+
|
| 130 |
def latency_frame():
|
| 131 |
+
"""Agent-turn latency over time (rolling window: seq index β seconds, by agent)."""
|
| 132 |
+
points = obs.telemetry_store().metric_points("agent.turn.seconds", limit=_LATENCY_POINTS)
|
| 133 |
rows = [
|
| 134 |
{"n": i, "seconds": round(p.value, 4), "agent": str(p.labels.get("agent", "?"))} for i, p in enumerate(points)
|
| 135 |
]
|
|
|
|
| 141 |
_PROMPT_KEYS = ("llm.prompt", "agent.prompt", "memory.query")
|
| 142 |
_OUTPUT_KEYS = ("llm.completion", "memory.visible_count")
|
| 143 |
|
| 144 |
+
#: How many traces the timeline shows at first, and how many more each "show more" adds.
|
| 145 |
+
DEFAULT_TRACES = 8
|
| 146 |
+
TRACE_PAGE = 8
|
| 147 |
+
#: Span buffer slice scanned to assemble the timeline (β€ store capacity).
|
| 148 |
+
_SPAN_SCAN = 3000
|
| 149 |
+
|
| 150 |
|
| 151 |
+
def render_traces(limit_traces: int = DEFAULT_TRACES) -> tuple[str, int, int]:
|
| 152 |
+
"""Recent spans grouped by trace, newest first; render the first ``limit_traces``.
|
| 153 |
|
| 154 |
+
Returns ``(html, shown, total)``. Each span shows name + duration + status; spans
|
| 155 |
+
that carry a prompt or memory (``llm.prompt`` / ``agent`` / ``memory.*`` attributes)
|
| 156 |
+
expand to reveal exactly what the agent sent and saw β the heart of the 'what did
|
| 157 |
+
each agent do' view. Only ``limit_traces`` traces are turned into HTML, so the
|
| 158 |
+
payload stays bounded while ``total`` lets the caller offer a "show more" control.
|
| 159 |
"""
|
| 160 |
+
spans = obs.telemetry_store().recent_spans(_SPAN_SCAN)
|
| 161 |
if not spans:
|
| 162 |
+
return "<div class='tele-empty'>No traces yet β run the show to populate the timeline.</div>", 0, 0
|
| 163 |
|
| 164 |
by_trace: dict[str, list] = {}
|
| 165 |
for sp in spans:
|
| 166 |
by_trace.setdefault(sp.trace_id, []).append(sp)
|
| 167 |
# Most recent traces first (by max end time within the trace).
|
| 168 |
ordered = sorted(by_trace.items(), key=lambda kv: max(s.end_ms for s in kv[1]), reverse=True)
|
| 169 |
+
total = len(ordered)
|
| 170 |
+
limit = max(0, int(limit_traces))
|
| 171 |
|
| 172 |
out: list[str] = []
|
| 173 |
+
for trace_id, group in ordered[:limit]:
|
| 174 |
depth = _depth_map(group)
|
| 175 |
root_dur = max(s.end_ms for s in group) - min(s.start_ms for s in group)
|
| 176 |
out.append(
|
|
|
|
| 180 |
for sp in sorted(group, key=lambda s: s.start_ms):
|
| 181 |
out.append(_span_html(sp, depth.get(sp.span_id, 0)))
|
| 182 |
out.append("</div>")
|
| 183 |
+
return "\n".join(out), min(limit, total), total
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def traces_html(limit_traces: int = DEFAULT_TRACES) -> str:
|
| 187 |
+
"""The trace timeline HTML alone (no counts) β for callers that don't paginate."""
|
| 188 |
+
return render_traces(limit_traces)[0]
|
| 189 |
|
| 190 |
|
| 191 |
def _depth_map(group: list) -> dict[str, int]:
|
|
|
|
| 234 |
return head + (f"<div class='tele-span-body'>{body}</div>" if body else "") + "</div>"
|
| 235 |
|
| 236 |
|
| 237 |
+
# ββ panel orchestration βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 238 |
+
# One entry point the Telemetry tab drives: a single store read per repaint, plus a
|
| 239 |
+
# cheap change signal so an idle auto-tick can skip the work entirely (see app.py).
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def revision() -> int:
|
| 243 |
+
"""Cheap 'has anything been recorded since I last painted?' signal for the UI."""
|
| 244 |
+
return obs.telemetry_store().revision()
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def more_label(shown: int, total: int) -> str:
|
| 248 |
+
"""Label for the 'show more traces' button given how many are shown vs. available."""
|
| 249 |
+
return f"β Show more traces Β· {shown}/{total}" if total > shown else f"All {total} traces shown"
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def snapshot(level: str = "all", layer: str = "all", trace_limit: int = DEFAULT_TRACES) -> dict:
|
| 253 |
+
"""Every Telemetry output computed in one pass β one counter read, one span scan.
|
| 254 |
+
|
| 255 |
+
Centralising the reads here means the metric panels share a single
|
| 256 |
+
``counter_totals()`` snapshot (instead of three separate locks) and the auto-refresh
|
| 257 |
+
can recompute the whole panel only when :func:`revision` says the store advanced.
|
| 258 |
+
"""
|
| 259 |
+
counters = obs.telemetry_store().counter_totals()
|
| 260 |
+
traces, shown, total = render_traces(trace_limit)
|
| 261 |
+
return {
|
| 262 |
+
"kpi": kpi_markdown(counters),
|
| 263 |
+
"feed": log_rows(level, layer),
|
| 264 |
+
"calls": calls_frame(counters),
|
| 265 |
+
"tokens": tokens_frame(counters),
|
| 266 |
+
"latency": latency_frame(),
|
| 267 |
+
"traces": traces,
|
| 268 |
+
"trace_shown": shown,
|
| 269 |
+
"trace_total": total,
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
TELEMETRY_CSS = """
|
| 274 |
.tele-trace { border:1px solid #2e3d25; border-radius:8px; margin:8px 0; padding:8px; background:#0b0f0a; }
|
| 275 |
.tele-trace-hd { color:#5fd0d0; font-weight:700; margin-bottom:6px; }
|