feat(logging): per-scan `{run_dir}/strix.log` with scan/agent context tagging
Every scan now writes a complete log file at ``{run_dir}/strix.log``
captured from the moment ``run_dir`` is resolved through teardown.
Stdlib ``logging`` only — no parallel framework.
New ``strix/telemetry/logging.py``:
* ``setup_scan_logging(run_dir, debug=)`` attaches a ``FileHandler``
(DEBUG, all ``strix.*``) plus a ``StreamHandler`` (ERROR by
default; DEBUG via ``STRIX_DEBUG=1``).
* ``ContextVar``-backed ``scan_id`` and ``agent_id`` injected by a
``Filter`` so every line is auto-tagged across asyncio tasks
without callers passing them explicitly.
* Third-party noise (``httpx``, ``litellm``, ``openai``,
``anthropic``, ``urllib3``, ``httpcore``) capped at WARNING.
* Returns a teardown handle for ``finally`` cleanup.
Wiring:
* ``orchestration/scan.py`` calls ``setup_scan_logging`` once per
scan after ``run_dir`` resolves; sets scan_id; tears down in
``finally``. Adds INFO logs for sandbox bring-up + scan
start/end.
* ``orchestration/hooks.py`` sets/clears ``agent_id`` ContextVar in
``on_agent_start`` / ``on_agent_end`` and emits INFO for agent
lifecycle, DEBUG for every tool start/end and LLM call.
* ``interface/main.py`` drops the ``setLevel(ERROR)`` silencer.
Coverage expanded across ~20 files (orchestration, agents, runtime,
llm, tools, interface, config, skills) with INFO for lifecycle and
DEBUG for verbose detail. Per the system instructions in
``logger.warning(f"…{e}")`` were converted to module logger calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -19,6 +20,9 @@ if TYPE_CHECKING:
|
||||
from agents.result import RunResultStreaming
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentMessageBus:
|
||||
"""Shared state for multi-agent orchestration.
|
||||
@@ -78,6 +82,7 @@ class AgentMessageBus:
|
||||
"warned_85": False,
|
||||
"warned_final": False,
|
||||
}
|
||||
logger.info("bus.register %s (%s) parent=%s", agent_id, name, parent_id or "-")
|
||||
|
||||
async def send(self, target: str, msg: dict[str, Any]) -> None:
|
||||
"""Append a message to ``target``'s inbox.
|
||||
@@ -87,13 +92,30 @@ class AgentMessageBus:
|
||||
"""
|
||||
async with self._lock:
|
||||
if target not in self.statuses:
|
||||
logger.debug("bus.send dropped (unknown target=%s)", target)
|
||||
return
|
||||
if self.statuses[target] in ("completed", "crashed", "stopped"):
|
||||
logger.debug(
|
||||
"bus.send dropped (target=%s status=%s)",
|
||||
target,
|
||||
self.statuses[target],
|
||||
)
|
||||
return
|
||||
self.inboxes.setdefault(target, []).append(msg)
|
||||
event = self._events.get(target)
|
||||
if event is not None:
|
||||
event.set()
|
||||
sender = msg.get("from", "?")
|
||||
msg_type = msg.get("type", "message")
|
||||
content = str(msg.get("content", ""))
|
||||
logger.debug(
|
||||
"bus.send %s -> %s (type=%s len=%d): %s",
|
||||
sender,
|
||||
target,
|
||||
msg_type,
|
||||
len(content),
|
||||
content[:200],
|
||||
)
|
||||
|
||||
async def wait_for_message(self, agent_id: str) -> None:
|
||||
"""Block until ``agent_id``'s inbox has at least one pending message.
|
||||
@@ -137,7 +159,9 @@ class AgentMessageBus:
|
||||
async with self._lock:
|
||||
msgs = self.inboxes.get(agent_id, [])
|
||||
self.inboxes[agent_id] = []
|
||||
return msgs
|
||||
if msgs:
|
||||
logger.debug("bus.drain %s -> %d message(s)", agent_id, len(msgs))
|
||||
return msgs
|
||||
|
||||
async def record_usage(self, agent_id: str, usage: Any) -> None:
|
||||
"""Accumulate per-call usage from RunHooks.on_llm_end.
|
||||
@@ -183,6 +207,7 @@ class AgentMessageBus:
|
||||
self.streams.pop(agent_id, None)
|
||||
self.stopping.discard(agent_id)
|
||||
self._events.pop(agent_id, None)
|
||||
logger.info("bus.finalize %s status=%s", agent_id, status)
|
||||
|
||||
async def park(self, agent_id: str) -> None:
|
||||
"""Mark an agent as ``waiting`` without finalizing.
|
||||
@@ -196,6 +221,7 @@ class AgentMessageBus:
|
||||
async with self._lock:
|
||||
if agent_id in self.statuses:
|
||||
self.statuses[agent_id] = "waiting"
|
||||
logger.debug("bus.park %s", agent_id)
|
||||
|
||||
async def mark_llm_failed(self, agent_id: str) -> None:
|
||||
"""Mark an agent as ``llm_failed`` after retries exhausted.
|
||||
@@ -209,6 +235,7 @@ class AgentMessageBus:
|
||||
async with self._lock:
|
||||
if agent_id in self.statuses:
|
||||
self.statuses[agent_id] = "llm_failed"
|
||||
logger.warning("bus.mark_llm_failed %s — awaiting user resume", agent_id)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def attach_stream(
|
||||
@@ -242,8 +269,10 @@ class AgentMessageBus:
|
||||
async with self._lock:
|
||||
streamed = self.streams.get(agent_id)
|
||||
if streamed is None:
|
||||
logger.debug("bus.request_interrupt %s — no active stream", agent_id)
|
||||
return False
|
||||
streamed.cancel(mode=mode) # type: ignore[arg-type] # mode is a Literal
|
||||
logger.info("bus.request_interrupt %s mode=%s", agent_id, mode)
|
||||
return True
|
||||
|
||||
async def total_stats(self) -> dict[str, Any]:
|
||||
@@ -274,6 +303,11 @@ class AgentMessageBus:
|
||||
order.append(aid)
|
||||
queue.extend(child for child, parent in self.parent_of.items() if parent == aid)
|
||||
tasks_to_cancel = [self.tasks[a] for a in reversed(order) if a in self.tasks]
|
||||
logger.info(
|
||||
"bus.cancel_descendants %s (hard, %d task(s))",
|
||||
root_agent_id,
|
||||
len(tasks_to_cancel),
|
||||
)
|
||||
for task in tasks_to_cancel:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
@@ -303,5 +337,11 @@ class AgentMessageBus:
|
||||
streams_to_cancel = [
|
||||
(aid, self.streams[aid]) for aid in reversed(order) if aid in self.streams
|
||||
]
|
||||
logger.info(
|
||||
"bus.cancel_descendants_graceful %s (%d active stream(s), %d total)",
|
||||
root_agent_id,
|
||||
len(streams_to_cancel),
|
||||
len(order),
|
||||
)
|
||||
for _aid, streamed in streams_to_cancel:
|
||||
streamed.cancel(mode="after_turn")
|
||||
|
||||
Reference in New Issue
Block a user