mirror of
https://github.com/usestrix/strix.git
synced 2026-08-18 09:49:17 +02:00
Five rounds of sweep across the tree. Net ~544 lines removed. Removed: - Section-divider banners and one-line section labels (# Display utilities, # ----- list_requests -----, # CVSS breakdown, etc.). - Module-level prose docstrings on internal modules. Kept one-line summaries; trimmed multi-paragraph narration about SDK/Strix responsibility splits, cache strategies, three-source precedence. - Internal-helper docstrings that just restate the function name — caido_api helpers (caido_url, get_client, view_request, etc.), settings-class one-liners (LLMSettings, RuntimeSettings, ...), UI helper docstrings. - Args/Returns blocks on non-LLM-facing internal helpers (build_strix_agent, render_system_prompt, create_or_reuse, bootstrap_caido) — kept only the genuinely non-obvious params. - Internal-history phrasing — "Mirrors main-branch shape", "pre-SDK harness", "previous lookup matched no attribute". - Narrative comments inside function bodies that explained what the next line does, design rationale obvious from the surrounding code, or "we used to..." asides. - Trailing periods on every error-string literal across the tool tree. - Duplicated roundtripTime quirk comment (kept the LLM-facing copy in tools/proxy/tools.py). Kept (every one names an upstream bug, vendored-code provenance, or non-obvious data quirk): - core/runner.py: SDK replay-with-empty-initial-input + on_agent_end lifecycle gap. - runtime/docker_client.py: VERBATIM COPY block of the upstream _create_container body, pinned to SDK v0.14.6. - runtime/session_manager.py: NO_PROXY for agent-browser CDP loopback. - tools/proxy/caido_api.py: generated-pydantic Request.raw quirk, replay double-history pitfall. - tools/proxy/tools.py: Caido roundtripTime=0 quirk for proxy captures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
"""Per-scan logging setup."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import logging
|
|
import os
|
|
import warnings
|
|
from contextvars import ContextVar
|
|
from pathlib import Path # noqa: TC003 used at runtime by ``setup_scan_logging``
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
|
|
_SCAN_ID: ContextVar[str | None] = ContextVar("strix_scan_id", default=None)
|
|
_AGENT_ID: ContextVar[str | None] = ContextVar("strix_agent_id", default=None)
|
|
|
|
|
|
def set_scan_id(scan_id: str) -> None:
|
|
"""Set the scan_id seen on every log record from this point in the task tree."""
|
|
_SCAN_ID.set(scan_id)
|
|
|
|
|
|
def set_agent_id(agent_id: str | None) -> None:
|
|
"""Set or clear the agent_id seen on every log record from this point.
|
|
|
|
``None`` clears (renders as ``-`` in the log line). Mutations are
|
|
isolated to the current asyncio task and tasks created from it after
|
|
the call.
|
|
"""
|
|
_AGENT_ID.set(agent_id)
|
|
|
|
|
|
class _StrixContextFilter(logging.Filter):
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
record.scan_id = _SCAN_ID.get() or "-"
|
|
record.agent_id = _AGENT_ID.get() or "-"
|
|
return True
|
|
|
|
|
|
_FORMAT = "%(asctime)s.%(msecs)03d %(levelname)-7s %(scan_id)s %(agent_id)s %(name)s: %(message)s"
|
|
_DATEFMT = "%Y-%m-%d %H:%M:%S"
|
|
|
|
|
|
# Third-party loggers that get noisy at DEBUG. Capped so the file isn't
|
|
# drowned in their internals when STRIX_DEBUG=1.
|
|
_NOISY_LIBS: tuple[str, ...] = (
|
|
"httpx",
|
|
"httpcore",
|
|
"urllib3",
|
|
"litellm",
|
|
"openai",
|
|
"anthropic",
|
|
)
|
|
|
|
|
|
_HANDLER_TAG = "_strix_scan_handler"
|
|
|
|
|
|
# ``openai.agents`` is the openai-agents SDK's canonical logger root.
|
|
_TRACKED_ROOTS: tuple[str, ...] = ("strix", "openai.agents")
|
|
|
|
|
|
def configure_dependency_logging() -> None:
|
|
"""Quiet dependency logging/warnings that obscure Strix scan logs."""
|
|
with contextlib.suppress(Exception):
|
|
import litellm
|
|
|
|
litellm_logging = litellm._logging
|
|
litellm_logging._disable_debugging() # type: ignore[no-untyped-call]
|
|
|
|
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
|
|
logging.getLogger("asyncio").propagate = False
|
|
warnings.filterwarnings("ignore", category=RuntimeWarning, module="asyncio")
|
|
|
|
|
|
def setup_scan_logging(run_dir: Path, *, debug: bool | None = None) -> Callable[[], None]:
|
|
"""Attach scan-scoped handlers; return a teardown callable.
|
|
|
|
Args:
|
|
run_dir: Per-scan output directory. ``{run_dir}/strix.log`` is
|
|
created if missing and opened append-mode (so re-runs of the
|
|
same scan_id concatenate cleanly).
|
|
debug: When ``True``, stderr handler runs at DEBUG instead of
|
|
ERROR. ``None`` (default) reads ``STRIX_DEBUG`` env: ``1`` /
|
|
``true`` / ``yes`` / ``on`` enables debug.
|
|
|
|
Returns:
|
|
A no-arg callable that flushes/closes/removes the handlers this
|
|
call attached. Idempotent — calling twice is a no-op the second
|
|
time. Safe to call from a ``finally`` block.
|
|
"""
|
|
configure_dependency_logging()
|
|
|
|
if debug is None:
|
|
debug = (os.environ.get("STRIX_DEBUG") or "").strip().lower() in {
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
"on",
|
|
}
|
|
|
|
run_dir.mkdir(parents=True, exist_ok=True)
|
|
log_path = run_dir / "strix.log"
|
|
|
|
formatter = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
|
context_filter = _StrixContextFilter()
|
|
|
|
file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
|
|
file_handler.setLevel(logging.DEBUG)
|
|
file_handler.setFormatter(formatter)
|
|
file_handler.addFilter(context_filter)
|
|
setattr(file_handler, _HANDLER_TAG, True)
|
|
|
|
stream_handler = logging.StreamHandler()
|
|
stream_handler.setLevel(logging.DEBUG if debug else logging.ERROR)
|
|
stream_handler.setFormatter(formatter)
|
|
stream_handler.addFilter(context_filter)
|
|
setattr(stream_handler, _HANDLER_TAG, True)
|
|
|
|
tracked_loggers = [logging.getLogger(name) for name in _TRACKED_ROOTS]
|
|
for tracked in tracked_loggers:
|
|
tracked.setLevel(logging.DEBUG)
|
|
tracked.addHandler(file_handler)
|
|
tracked.addHandler(stream_handler)
|
|
tracked.propagate = False
|
|
|
|
for name in _NOISY_LIBS:
|
|
logging.getLogger(name).setLevel(logging.WARNING)
|
|
|
|
def _teardown() -> None:
|
|
for tracked in tracked_loggers:
|
|
for handler in list(tracked.handlers):
|
|
if getattr(handler, _HANDLER_TAG, False):
|
|
tracked.removeHandler(handler)
|
|
with contextlib.suppress(Exception):
|
|
handler.flush()
|
|
handler.close()
|
|
|
|
return _teardown
|