feat: live app log console, cross-filter, JSONL export
Backend:
- /ws/app-logs — real-time app/cot log stream (raw JSONL)
- /api/logs/recent — REST snapshot of ring buffer
- GET /tasks/{id}/logs/export — streaming JSONL export with CoT parse + redaction
- Thread-safe ring buffer (seq-based polling) replaces unsafe asyncio.Queue
- GIL-friendly multi-row Core insert for log persistence
- task_id ContextVar propagates into CotJsonFormatter for CoT correlation
- Hot-apply logging level on settings update (FR-005)
- Buffer trim under DEBUG floods; drop DEBUG first, preserve ERROR/WARNING
- List projection (include_result=False) keeps reports list slim
- Security event consolidated to single REASON atom
Frontend:
- ReportsLogModel + ReportsLogPanel — full live JSONL console
- Cross-filter pinning: Tasks → Logs tab with task chip badges
- LogEntryRow — CoT-aware rendering (marker icons, expandable payload)
- TaskFilterChip, taskChipMeta — scannable type/id/env chips
- Global drawer push via CSS variable (lg+ padding, not overlay)
- i18n en/ru for all log console strings
- Ctrl+A in log panel selects only log lines (window-level handler)
QA fixes:
- Svelte 5 reactivity: SvelteDate/SvelteSet/SvelteURLSearchParams
- Fix seed_trace_id shadowing (F823) in lifecycle.py
- Remove dead code (selectedEnvironment, goToReportsPage)
- Add @BRIEF to C2 test functions, missing {#each} keys
- Remove unused import json as _json from app.py
All 3200+ frontend tests pass; backend lint clean.
This commit is contained in:
@@ -31,6 +31,7 @@ import uuid
|
||||
# These are the SINGLE source of truth — both backend and agent read them.
|
||||
_trace_id: ContextVar[str] = ContextVar("_trace_id", default="")
|
||||
_span_id: ContextVar[str] = ContextVar("_span_id", default="")
|
||||
_task_id: ContextVar[str] = ContextVar("_task_id", default="")
|
||||
# #endregion Shared.TraceContext
|
||||
|
||||
# ── Timing tracking ─────────────────────────────────────────────────────────
|
||||
@@ -230,6 +231,24 @@ def get_trace_id() -> str:
|
||||
return _trace_id.get()
|
||||
# #endregion Shared.get_trace_id
|
||||
|
||||
|
||||
# #region Shared.task_id_context [C:1] [TYPE Block] [SEMANTICS task_id,contextvar]
|
||||
# @BRIEF Task-scoped identity for correlating app CoT lines with task_logs.
|
||||
def set_task_id(task_id: str) -> None:
|
||||
"""Bind the current background task id into ContextVar (empty string clears)."""
|
||||
_task_id.set(task_id or "")
|
||||
|
||||
|
||||
def get_task_id() -> str:
|
||||
"""Return the task_id bound for this async context, or empty string."""
|
||||
return _task_id.get() or ""
|
||||
|
||||
|
||||
def clear_task_id() -> None:
|
||||
"""Clear task_id binding after plugin execute finishes."""
|
||||
_task_id.set("")
|
||||
# #endregion Shared.task_id_context
|
||||
|
||||
# ── Span ID API ─────────────────────────────────────────────────────────────
|
||||
|
||||
# #region Shared.push_span [C:1] [TYPE Function] [SEMANTICS span_id,contextvar,stack]
|
||||
@@ -432,14 +451,17 @@ def _summarise_args(args, kwargs) -> dict:
|
||||
# Use shared/logger.py's logger.reason/reflect/explore or cot_logger.log() directly.
|
||||
|
||||
__all__ = [
|
||||
"clear_task_id",
|
||||
"cot_logger",
|
||||
"cot_span",
|
||||
"derive_src",
|
||||
"get_task_id",
|
||||
"get_trace_id",
|
||||
"log",
|
||||
"pop_span",
|
||||
"push_span",
|
||||
"seed_trace_id",
|
||||
"set_task_id",
|
||||
"set_trace_id",
|
||||
"is_routine_infra",
|
||||
"set_routine_suppression",
|
||||
|
||||
@@ -18,6 +18,7 @@ from datetime import UTC, datetime
|
||||
|
||||
from .cot_logger import (
|
||||
_span_id,
|
||||
_task_id,
|
||||
_trace_id,
|
||||
derive_src,
|
||||
is_routine_infra,
|
||||
@@ -86,8 +87,17 @@ class CotJsonFormatter(logging.Formatter):
|
||||
span_id = _span_id.get()
|
||||
if span_id:
|
||||
log_obj["span_id"] = span_id
|
||||
# Correlate app CoT lines with task_logs / Reports cross-filter
|
||||
task_id = _task_id.get() or None
|
||||
if task_id:
|
||||
log_obj["task_id"] = task_id
|
||||
if payload is not None:
|
||||
# Ensure payload.task_id is present when context is bound
|
||||
if isinstance(payload, dict) and task_id and "task_id" not in payload:
|
||||
payload = {**payload, "task_id": task_id}
|
||||
log_obj["payload"] = payload
|
||||
elif task_id:
|
||||
log_obj["payload"] = {"task_id": task_id}
|
||||
if error is not None:
|
||||
log_obj["error"] = error
|
||||
if elapsed_ms is not None:
|
||||
|
||||
Reference in New Issue
Block a user