""" Telemetry helpers для Humanized Stepan v2.7.2. Забезпечує єдиний тег AGX_STEPAN_METRIC на всіх ключових лог-рядках і PII-safe анонімізацію ідентифікаторів. Grep у проді: grep "AGX_STEPAN_METRIC" /logs/gateway.log Формат рядка: AGX_STEPAN_METRIC key=value key2=value2 PII-safe: - Ключі з pii_keys (default: {"user_id","chat_id"}) автоматично анонімізуються: user_id=h:3f9a12b4c7 (sha256 перших 10 hex-символів) - Дає можливість корелювати події одного користувача без прямого витоку. - Не є криптографічним захистом проти таргетованого знання. Правила серіалізації: - bool → "true" / "false" - int/float → str - list → елементи через кому - dict → компактний JSON - None → "null" - Нічого з секретів/токенів не передавати у kv. """ from __future__ import annotations import hashlib import json import logging from typing import Any TELEMETRY_TAG = "AGX_STEPAN_METRIC" # Ключі, які автоматично анонімізуються у tlog() _DEFAULT_PII_KEYS: frozenset[str] = frozenset({"user_id", "chat_id"}) def anonymize_id(value: str | None) -> str | None: """ Повертає PII-safe псевдонім для ідентифікатора. Правила: - None → None - Пусте рядок → повернути як є (нема що хешувати) - Інакше: "h:" + sha256(value)[:10] Формат стабільний: завжди 12 символів ("h:" + 10 hex). Колізії теоретично можливі, але практично нереальні для user_id-просторів. Приклади: anonymize_id("123456789") → "h:3f9a12b4c7" anonymize_id(None) → None anonymize_id("") → "" """ if value is None: return None if not value: return value try: digest = hashlib.sha256(value.encode()).hexdigest() return f"h:{digest[:10]}" except Exception: return "h:error" def _fmt_value(v: Any) -> str: if isinstance(v, bool): return str(v).lower() if isinstance(v, (int, float)): return str(v) if v is None: return "null" if isinstance(v, list): return ",".join(str(i) for i in v) if isinstance(v, dict): return json.dumps(v, ensure_ascii=False, separators=(",", ":")) return str(v) def tlog( logger: logging.Logger, msg: str, level: int = logging.INFO, pii_keys: frozenset[str] | set[str] = _DEFAULT_PII_KEYS, **kv: Any, ) -> None: """ Логує рядок з уніфікованим тегом AGX_STEPAN_METRIC і PII-safe анонімізацією. Приклади: tlog(logger, "depth", depth="light", reason="greeting") → "AGX_STEPAN_METRIC depth depth=light reason=greeting" tlog(logger, "memory_save", user_id="123456789", ok=True) → "AGX_STEPAN_METRIC memory_save user_id=h:3f9a12b4c7 ok=true" Ключі в pii_keys автоматично анонімізуються через anonymize_id(). Безпечний: всі помилки форматування ігноруються — fallback без kv. """ try: parts: list[str] = [] for k, v in kv.items(): if k in pii_keys: anon = anonymize_id(str(v) if v is not None else None) parts.append(f"{k}={_fmt_value(anon)}") else: parts.append(f"{k}={_fmt_value(v)}") kv_str = " ".join(parts) line = f"{TELEMETRY_TAG} {msg}" if kv_str: line = f"{line} {kv_str}" except Exception: line = f"{TELEMETRY_TAG} {msg}" logger.log(level, line)