Files
strix/strix/telemetry/utils.py
T
0xallamandClaude Opus 4.7 12baf2d792 refactor: lean on SDK for tracing + native session resume; nuke OTEL/Traceloop
The SDK ships its own tracing pipeline (``agents.tracing``) plus
``SQLiteSession`` for native conversation persistence. Strix's custom
OTEL bootstrap + Traceloop integration was dead weight — the SDK does
not bridge to OpenTelemetry, so all of our adapter code was solving a
problem we didn't actually need solved.

Telemetry purge:
- Drop the ``traceloop-sdk`` and
  ``opentelemetry-exporter-otlp-proto-http`` runtime deps. ``uv sync``
  uninstalls ~30 transitive packages (the OTEL family,
  ``traceloop-sdk``, ``protobuf``, ``opentelemetry-exporter-otlp-*``,
  ``deprecated``, ``wrapt``, ``backoff``, etc.) — about 1000 lines off
  ``uv.lock``.
- Delete ``bootstrap_otel`` and ``JsonlSpanExporter`` from
  ``telemetry/utils.py``; strip the OTEL pruning helpers,
  ``parse_traceloop_headers``, ``default_resource_attributes``,
  ``format_trace_id`` / ``format_span_id`` / ``iso_from_unix_ns``.
  Keep only the sanitizer + JSONL writer + write-lock registry.
- Strip ``Tracer._setup_telemetry``, ``_otel_tracer``,
  ``_remote_export_enabled``, ``_active_events_file_path``,
  ``_active_run_metadata``, ``_get_events_write_lock``,
  ``_set_association_properties``. ``_emit_event`` now generates
  trace/span ids from ``uuid4`` directly.
- Drop the ``traceloop_base_url`` / ``traceloop_api_key`` /
  ``traceloop_headers`` / ``strix_otel_telemetry`` config knobs.
- Rename ``is_otel_enabled`` → ``is_telemetry_enabled`` (the gate now
  controls JSONL emission only).

Native session resume:
- ``entry.py`` now constructs an ``agents.memory.SQLiteSession`` keyed
  by ``scan_id`` and persists conversation history at
  ``strix_runs/<scan_id>/session.db``. A second call to
  ``run_strix_scan`` with the same ``scan_id`` resumes from where the
  prior run left off — no manual state plumbing needed.

Tracer.agents fix (TUI agent tree was silently empty):
- ``StrixOrchestrationHooks.on_agent_start`` now mirrors bus state
  into ``tracer.agents`` (id / name / parent_id / status), and
  ``on_agent_end`` flips the entry to ``completed`` / ``crashed``.
  The TUI now actually shows the agent tree during scans.

Tooling:
- Drop ``pylint`` from dev deps; ``ruff`` covers everything we used
  it for. Strip the ``make lint`` pylint step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:18:21 -07:00

103 lines
3.2 KiB
Python

import json
import logging
import re
import threading
from pathlib import Path
from typing import Any
from scrubadub import Scrubber
from scrubadub.detectors import RegexDetector
from scrubadub.filth import Filth
logger = logging.getLogger(__name__)
_REDACTED = "[REDACTED]"
_SCREENSHOT_OMITTED = "[SCREENSHOT_OMITTED]"
_SCREENSHOT_KEY_PATTERN = re.compile(r"screenshot", re.IGNORECASE)
_SENSITIVE_KEY_PATTERN = re.compile(
r"(api[_-]?key|token|secret|password|authorization|cookie|session|credential|private[_-]?key)",
re.IGNORECASE,
)
_SENSITIVE_TOKEN_PATTERN = re.compile(
r"(?i)\b("
r"bearer\s+[a-z0-9._-]+|"
r"sk-[a-z0-9_-]{8,}|"
r"gh[pousr]_[a-z0-9_-]{12,}|"
r"xox[baprs]-[a-z0-9-]{12,}"
r")\b"
)
_SCRUBADUB_PLACEHOLDER_PATTERN = re.compile(r"\{\{[^}]+\}\}")
_EVENTS_FILE_LOCKS_LOCK = threading.Lock()
_EVENTS_FILE_LOCKS: dict[str, threading.Lock] = {}
class _SecretFilth(Filth): # type: ignore[misc]
type = "secret"
class _SecretTokenDetector(RegexDetector): # type: ignore[misc]
name = "strix_secret_token_detector"
filth_cls = _SecretFilth
regex = _SENSITIVE_TOKEN_PATTERN
class TelemetrySanitizer:
def __init__(self) -> None:
self._scrubber = Scrubber(detector_list=[_SecretTokenDetector])
def sanitize(self, data: Any, key_hint: str | None = None) -> Any: # noqa: PLR0911
if data is None:
return None
if isinstance(data, dict):
sanitized: dict[str, Any] = {}
for key, value in data.items():
key_str = str(key)
if _SCREENSHOT_KEY_PATTERN.search(key_str):
sanitized[key_str] = _SCREENSHOT_OMITTED
elif _SENSITIVE_KEY_PATTERN.search(key_str):
sanitized[key_str] = _REDACTED
else:
sanitized[key_str] = self.sanitize(value, key_hint=key_str)
return sanitized
if isinstance(data, list):
return [self.sanitize(item, key_hint=key_hint) for item in data]
if isinstance(data, tuple):
return [self.sanitize(item, key_hint=key_hint) for item in data]
if isinstance(data, str):
if key_hint and _SENSITIVE_KEY_PATTERN.search(key_hint):
return _REDACTED
cleaned = self._scrubber.clean(data)
return _SCRUBADUB_PLACEHOLDER_PATTERN.sub(_REDACTED, cleaned)
if isinstance(data, int | float | bool):
return data
return str(data)
def get_events_write_lock(output_path: Path) -> threading.Lock:
path_key = str(output_path.resolve(strict=False))
with _EVENTS_FILE_LOCKS_LOCK:
lock = _EVENTS_FILE_LOCKS.get(path_key)
if lock is None:
lock = threading.Lock()
_EVENTS_FILE_LOCKS[path_key] = lock
return lock
def reset_events_write_locks() -> None:
with _EVENTS_FILE_LOCKS_LOCK:
_EVENTS_FILE_LOCKS.clear()
def append_jsonl_record(output_path: Path, record: dict[str, Any]) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
with get_events_write_lock(output_path), output_path.open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")