Historical working copy retained for audit before consolidation into main. The canonicalized plugin architecture and later fixes already live in main; this snapshot is not a release or a request to restore obsolete source layout.
168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
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",
|
|
"operation_id",
|
|
"operation_stage",
|
|
"connection_mode",
|
|
"error_category",
|
|
"error_code",
|
|
"transport_error_code",
|
|
"reason_code",
|
|
"failed_phase",
|
|
"dialogue_stage",
|
|
"transport_state",
|
|
"publish_attempts",
|
|
"modeling_command_attempted",
|
|
"outcome_unknown",
|
|
"safe_to_retry",
|
|
"side_effect_status",
|
|
"network_change_attempted",
|
|
"device_write_attempted",
|
|
"device_write_confirmed",
|
|
"ble_att_error_code",
|
|
"ble_att_error_name",
|
|
"resolved_write_mode",
|
|
"max_write_without_response_size",
|
|
"mtu_size",
|
|
"frame_length",
|
|
"bridge_frame_length",
|
|
"quick_connect_frame_length",
|
|
"scan_elapsed_ms",
|
|
"scanner_start_ms",
|
|
"initial_window_ms",
|
|
"first_candidate_ms",
|
|
"scan_extended",
|
|
"candidate_count",
|
|
"likely_k1_candidate_count",
|
|
"discovery_generation",
|
|
"helper_stage",
|
|
"helper_elapsed_ms",
|
|
"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",
|
|
"ui_build_id",
|
|
"expected_ui_build_id",
|
|
"document_instance_id",
|
|
"viewer_instance_id",
|
|
"lifecycle_generation",
|
|
"backend_activity_sequence",
|
|
"viewer_range_max_ns",
|
|
"stalled_for_ms",
|
|
"recovery_attempt",
|
|
"camera_queue_bytes",
|
|
"camera_queue_segments",
|
|
"camera_queue_oldest_age_ms",
|
|
"camera_queue_rejection_reason",
|
|
"camera_retry_count",
|
|
"camera_generation",
|
|
"camera_pcl_admission_ms",
|
|
"camera_authority_wait_ms",
|
|
"camera_ffmpeg_prepare_ms",
|
|
"camera_post_spawn_commit_ms",
|
|
"camera_activation_total_ms",
|
|
"device_command_sent",
|
|
"websocket_close_code",
|
|
"transport_epoch",
|
|
"camera_append_error_name",
|
|
"camera_media_source_state",
|
|
"camera_video_error_code",
|
|
"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
|
|
if record.exc_info is not None:
|
|
exception_type, _exception, traceback = record.exc_info
|
|
if exception_type is not None:
|
|
document["exception_type"] = exception_type.__name__[:128]
|
|
if traceback is not None:
|
|
while traceback.tb_next is not None:
|
|
traceback = traceback.tb_next
|
|
code = traceback.tb_frame.f_code
|
|
# Preserve the failure location, never exception text, locals,
|
|
# source lines or absolute paths that may contain private data.
|
|
document["exception_site"] = (
|
|
f"{Path(code.co_filename).name}:{traceback.tb_lineno}:{code.co_name}"
|
|
)[:256]
|
|
properties = getattr(record, "write_characteristic_properties", None)
|
|
if isinstance(properties, (list, tuple)) and all(
|
|
isinstance(value, str)
|
|
and value in {"read", "write", "write-without-response", "notify", "indicate"}
|
|
for value in properties
|
|
):
|
|
document["write_characteristic_properties"] = sorted(set(properties))
|
|
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
|