from __future__ import annotations import json import logging import os from datetime import UTC, datetime from logging.handlers import RotatingFileHandler from pathlib import Path from typing import Final SCANNER_LOGGER_NAME: Final = "k1link.device_plugins.xgrids_k1" SCANNER_DIAGNOSTIC_FILE: Final = "scanner-diagnostics.jsonl" _HANDLER_MARKER: Final = "_mission_core_scanner_diagnostics_path" _EXTRA_FIELDS: Final = ( "event_code", "reason_code", "failed_phase", "dialogue_stage", "transport_state", "publish_attempts", "modeling_command_attempted", "outcome_unknown", "safe_to_retry", "status_reconciliation", "network_change_admissible", "network_change_reconciliation", "mqtt_loop_result_code", "mqtt_loop_result_name", "mqtt_loop_phase", "automatic_retry", "camera_source_id", "evidence_session_id", "activation_trigger", "lease_generation", "lease_state", "recovery_strategy", "endpoint_reachable", "host_route_class", "address_changed", "device_write_performed", "failure_stage", "start_checkpoint_released", "stale_session_retired", "stream_id", "backend_activity_sequence", "viewer_range_max_ns", "stalled_for_ms", "recovery_attempt", "preferred_port", "selected_port", ) class ScannerDiagnosticJsonFormatter(logging.Formatter): """Serialize a bounded, secret-free scanner diagnostic record.""" def format(self, record: logging.LogRecord) -> str: document: dict[str, object] = { "timestamp_utc": datetime.fromtimestamp(record.created, UTC) .isoformat() .replace("+00:00", "Z"), "level": record.levelname, "logger": record.name, "message": record.getMessage()[:1000], } for field in _EXTRA_FIELDS: value = getattr(record, field, None) if value is not None and isinstance(value, (str, int, bool)): document[field] = value return json.dumps(document, ensure_ascii=False, separators=(",", ":")) def configure_scanner_diagnostics(logs_root: Path) -> Path: """Install one private rotating JSONL handler for K1 field diagnostics.""" logs_root.mkdir(mode=0o700, parents=True, exist_ok=True) os.chmod(logs_root, 0o700) target = (logs_root / SCANNER_DIAGNOSTIC_FILE).resolve() logger = logging.getLogger(SCANNER_LOGGER_NAME) for handler in logger.handlers: if getattr(handler, _HANDLER_MARKER, None) == str(target): return target handler = RotatingFileHandler( target, maxBytes=5 * 1024 * 1024, backupCount=5, encoding="utf-8", delay=False, ) os.chmod(target, 0o600) handler.setLevel(logging.INFO) handler.setFormatter(ScannerDiagnosticJsonFormatter()) setattr(handler, _HANDLER_MARKER, str(target)) logger.addHandler(handler) logger.setLevel(logging.INFO) return target