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:
0xallam
2026-04-25 23:35:01 -07:00
co-authored by Claude Opus 4.7
parent 9d7f754b59
commit 46ff025209
22 changed files with 415 additions and 34 deletions
+10
View File
@@ -1,5 +1,6 @@
import atexit
import contextlib
import logging
import os
import signal
import sys
@@ -24,6 +25,9 @@ from .utils import (
)
logger = logging.getLogger(__name__)
def _resolve_sandbox_image() -> str:
image = load_settings().runtime.image
if not image:
@@ -182,6 +186,12 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
update_thread.start()
try:
logger.info(
"CLI launching scan: run_name=%s targets=%d interactive=%s",
args.run_name,
len(scan_config.get("targets") or []),
bool(getattr(args, "interactive", False)),
)
await run_strix_scan(
scan_config=scan_config,
scan_id=args.run_name,
+5 -2
View File
@@ -5,7 +5,6 @@ Strix Agent Interface
import argparse
import asyncio
import logging
import shutil
import sys
from pathlib import Path
@@ -43,7 +42,11 @@ from strix.telemetry.tracer import get_global_tracer
HOST_GATEWAY_HOSTNAME = "host.docker.internal"
logging.getLogger().setLevel(logging.ERROR)
# Per-scan logging is set up by ``setup_scan_logging`` from inside
# ``orchestration.scan.run_strix_scan`` once the scan ``run_dir`` is
# known — that's where ``strix.*`` levels and handlers are owned. Pre-scan
# work (``main()``, env validation, image pull) emits at WARNING+ to
# stderr via the SDK / stdlib defaults.
def validate_environment() -> None:
+13 -13
View File
@@ -959,9 +959,7 @@ class StrixTUIApp(App): # type: ignore[misc]
return True
except (KeyError, AttributeError, ValueError) as e:
import logging
logging.warning(f"Failed to update agent node label: {e}")
logger.warning(f"Failed to update agent node label: {e}")
return False
@@ -1422,7 +1420,7 @@ class StrixTUIApp(App): # type: ignore[misc]
)
except (KeyboardInterrupt, asyncio.CancelledError):
logging.info("Scan interrupted by user")
logger.info("Scan interrupted by user")
except (ConnectionError, TimeoutError):
logging.exception("Network error during scan")
except RuntimeError:
@@ -1503,9 +1501,7 @@ class StrixTUIApp(App): # type: ignore[misc]
self._reorganize_orphaned_agents(agent_id)
except (AttributeError, ValueError, RuntimeError) as e:
import logging
logging.warning(f"Failed to add agent node {agent_id}: {e}")
logger.warning(f"Failed to add agent node {agent_id}: {e}")
def _expand_new_agent_nodes(self) -> None:
if len(self.screen_stack) > 1 or self.show_splash:
@@ -1525,7 +1521,7 @@ class StrixTUIApp(App): # type: ignore[misc]
agents_tree = self.query_one("#agents_tree", Tree)
self._expand_node_recursively(agents_tree.root)
except (ValueError, Exception):
logging.debug("Tree not ready for expanding nodes")
logger.debug("Tree not ready for expanding nodes")
def _expand_node_recursively(self, node: TreeNode) -> None:
if not node.is_expanded:
@@ -1717,6 +1713,11 @@ class StrixTUIApp(App): # type: ignore[misc]
if not self.selected_agent_id:
return
logger.info(
"TUI: user message -> %s (len=%d)",
self.selected_agent_id,
len(message),
)
if self.tracer:
self.tracer.log_chat_message(
content=message,
@@ -1758,7 +1759,7 @@ class StrixTUIApp(App): # type: ignore[misc]
if isinstance(agent_name, str):
return agent_name
except (KeyError, AttributeError) as e:
logging.warning(f"Could not retrieve agent name for {agent_id}: {e}")
logger.warning(f"Could not retrieve agent name for {agent_id}: {e}")
return "Unknown Agent"
def action_toggle_help(self) -> None:
@@ -1836,9 +1837,7 @@ class StrixTUIApp(App): # type: ignore[misc]
return agent_name, True
except (KeyError, AttributeError, ValueError) as e:
import logging
logging.warning(f"Failed to gather agent events: {e}")
logger.warning(f"Failed to gather agent events: {e}")
return agent_name, False
@@ -1849,8 +1848,9 @@ class StrixTUIApp(App): # type: ignore[misc]
# The hard ``cancel_descendants`` path remains for KeyboardInterrupt
# in entry.py where graceful isn't possible.
if self._scan_loop is None or self._scan_loop.is_closed():
logging.warning("No active scan loop; cannot stop agent %s", agent_id)
logger.warning("No active scan loop; cannot stop agent %s", agent_id)
return
logger.info("TUI: graceful stop requested for %s (cascade)", agent_id)
asyncio.run_coroutine_threadsafe(
self.bus.cancel_descendants_graceful(agent_id),
self._scan_loop,