#673 offenders adopt prax append_jsonl: @backup (1 .jsonl site), @hooks (2 .jsonl sites), @trigger (11 raw .log appenders across 8 files -> .jsonl + downstream medic_state/medic/log_watcher readers + 5 tests). Zero raw open('a') log appenders remain fleet-wide. Verified: backup 18 / hooks 114 / trigger 189 tests green, no recursion regression, append_jsonl wired at every site.
This commit is contained in:
@@ -14,7 +14,7 @@ import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.prax import append_jsonl, logger
|
||||
|
||||
|
||||
def log_operation(operation: str, data: dict) -> None:
|
||||
@@ -24,12 +24,9 @@ def log_operation(operation: str, data: dict) -> None:
|
||||
"operation": operation,
|
||||
**data,
|
||||
}
|
||||
log_dir = Path(__file__).resolve().parents[3] / "logs"
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
log_file = log_dir / "operations.jsonl"
|
||||
log_file = Path(__file__).resolve().parents[3] / "logs" / "operations.jsonl"
|
||||
try:
|
||||
with open(log_file, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, default=str) + "\n")
|
||||
append_jsonl(log_file, entry)
|
||||
except OSError as e:
|
||||
logger.warning(f"Failed to write operation log: {e}")
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
"""JSONL diagnostic logging — appends structured entries for hook activity."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from aipass.prax import append_jsonl
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
BRANCH_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
||||
@@ -22,9 +22,7 @@ LOG_FILE = BRANCH_ROOT / "logs" / "engine.jsonl"
|
||||
def log_entry(entry: dict) -> None:
|
||||
"""Append a JSONL log entry for detailed diagnostics."""
|
||||
try:
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
append_jsonl(LOG_FILE, entry)
|
||||
except OSError as exc:
|
||||
logger.error("[HOOKS] log write failed: %s", exc)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from pathlib import Path
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from aipass.prax import append_jsonl
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
PENDING_DIR = Path.home() / ".aipass" / "telegram_pending"
|
||||
@@ -760,8 +761,6 @@ def _write_delivery_log(intended_text: str, chunks: list[str], chunk_results: li
|
||||
record["culprit"] = culprit
|
||||
|
||||
try:
|
||||
_DELIVERY_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(_DELIVERY_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record) + "\n")
|
||||
append_jsonl(_DELIVERY_LOG, record)
|
||||
except OSError as e:
|
||||
logger.warning("[HOOKS] telegram: delivery log write failed: %s", e)
|
||||
|
||||
@@ -19,23 +19,26 @@ import os
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
# Trigger package root: .../aipass/trigger/
|
||||
TRIGGER_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
_CONFIG_LOG = TRIGGER_ROOT / "logs" / "config.log"
|
||||
_CONFIG_LOG = TRIGGER_ROOT / "logs" / "config.jsonl"
|
||||
|
||||
|
||||
def _log_warning(message: str) -> None:
|
||||
"""Log warning to file (config cannot import prax logger — circular)."""
|
||||
"""Log warning to file (recursion-safe prax path)."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
_CONFIG_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(_CONFIG_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"{ts} | WARNING | {message}\n")
|
||||
_append_jsonl(_CONFIG_LOG, {"level": "WARNING", "msg": message})
|
||||
except Exception:
|
||||
pass # Meta-logging: cannot log a failure to log
|
||||
pass
|
||||
|
||||
|
||||
# AIPass package root: .../aipass/
|
||||
|
||||
@@ -35,24 +35,28 @@ Architecture (Medic v2):
|
||||
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from aipass.trigger.apps.config import TRIGGER_ROOT
|
||||
from aipass.trigger.apps.handlers.json import json_handler
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "error_detected_handler.log"
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "error_detected_handler.jsonl"
|
||||
|
||||
|
||||
def _log_warning(message: str) -> None:
|
||||
"""Log warning to file (event handlers cannot import Prax logger - causes recursion)."""
|
||||
"""Log warning to file (recursion-safe prax path)."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"{ts} | WARNING | {message}\n")
|
||||
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
|
||||
except Exception:
|
||||
pass
|
||||
pass # seedgo:bypass meta-logging
|
||||
|
||||
|
||||
def _find_repo_root() -> Path:
|
||||
@@ -348,22 +352,29 @@ REPORT TO @devpulse:
|
||||
|
||||
def _write_suppression_log(reason: str, branch: str, module: str, message: str) -> None:
|
||||
"""Write a line to the medic suppression log."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
suppressed_log = TRIGGER_ROOT / "logs" / "medic_suppressed.log"
|
||||
suppressed_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(suppressed_log, "a", encoding="utf-8") as f:
|
||||
f.write(f"{datetime.now().isoformat()} | {reason} - {branch}: {module} - {message[:100]}\n")
|
||||
suppressed_log = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl"
|
||||
entry = {
|
||||
"ts": datetime.now().isoformat(),
|
||||
"reason": reason,
|
||||
"branch": branch,
|
||||
"module": module,
|
||||
"msg": message[:100],
|
||||
}
|
||||
_append_jsonl(suppressed_log, entry)
|
||||
except Exception as exc:
|
||||
_log_warning(f"suppression log write failed ({reason}): {exc}")
|
||||
|
||||
|
||||
def _write_rate_log(reason: str, detail: str) -> None:
|
||||
"""Write a line to the rate-limited log."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
rate_log = TRIGGER_ROOT / "logs" / "rate_limited.log"
|
||||
rate_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(rate_log, "a", encoding="utf-8") as f:
|
||||
f.write(f"{datetime.now().isoformat()} | {reason}: {detail}\n")
|
||||
rate_log = TRIGGER_ROOT / "logs" / "rate_limited.jsonl"
|
||||
_append_jsonl(rate_log, {"ts": datetime.now().isoformat(), "reason": reason, "detail": detail})
|
||||
except Exception as exc:
|
||||
_log_warning(f"rate log write failed ({reason}): {exc}")
|
||||
|
||||
|
||||
@@ -24,24 +24,27 @@ Event data expected:
|
||||
- error: str | None — error message if success=False
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from aipass.trigger.apps.config import TRIGGER_ROOT
|
||||
from aipass.trigger.apps.handlers.json import json_handler
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "memory_pool_handler.log"
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "memory_pool_handler.jsonl"
|
||||
|
||||
|
||||
def _log_warning(message: str) -> None:
|
||||
"""Log warning to file (event handlers cannot import prax logger — causes recursion)."""
|
||||
"""Log warning to file (recursion-safe prax path)."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"{ts} | WARNING | {message}\n")
|
||||
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
|
||||
except Exception:
|
||||
pass # Meta-logging: cannot log a failure to log
|
||||
pass # seedgo:bypass meta-logging
|
||||
|
||||
|
||||
def handle_memory_pool_auto_processed(
|
||||
|
||||
@@ -29,6 +29,11 @@ from typing import Optional
|
||||
from aipass.trigger.apps.config import TRIGGER_ROOT, AIPASS_PKG_ROOT, atomic_write_json
|
||||
from aipass.trigger.apps.handlers.json import json_handler
|
||||
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
|
||||
def _find_repo_root() -> Path:
|
||||
"""Walk up from this file to find the repo root (contains AIPASS_REGISTRY.json)."""
|
||||
@@ -45,21 +50,19 @@ REPO_ROOT = _find_repo_root()
|
||||
FLOW_JSON_DIR = AIPASS_PKG_ROOT / "flow" / "flow_json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "PLAN_REGISTRY.json"
|
||||
|
||||
# Log file for handler errors (no Prax imports in handlers - causes recursion)
|
||||
HANDLER_LOG = TRIGGER_ROOT / "logs" / "plan_file_handler.log"
|
||||
HANDLER_LOG = TRIGGER_ROOT / "logs" / "plan_file_handler.jsonl"
|
||||
|
||||
MODULE_NAME = "trigger.plan_file"
|
||||
|
||||
|
||||
def _log_error(message: str) -> None:
|
||||
"""Log error to file (handlers cannot import Prax logger - causes recursion)"""
|
||||
"""Log error to file (recursion-safe prax path)."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
with open(HANDLER_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"[{timestamp}] [{MODULE_NAME}] {message}\n")
|
||||
_append_jsonl(HANDLER_LOG, {"level": "ERROR", "module": MODULE_NAME, "msg": message})
|
||||
except Exception:
|
||||
pass # Last resort - cannot fail on logging failure
|
||||
pass # seedgo:bypass meta-logging
|
||||
|
||||
|
||||
def _load_registry() -> dict:
|
||||
|
||||
@@ -20,24 +20,27 @@ Events:
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from aipass.trigger.apps.config import TRIGGER_ROOT
|
||||
from aipass.trigger.apps.handlers.json import json_handler
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "pr_status_sync_handler.log"
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "pr_status_sync_handler.jsonl"
|
||||
|
||||
|
||||
def _log_info(message: str) -> None:
|
||||
"""Log to file (event handlers cannot import prax logger — causes recursion)."""
|
||||
"""Log to file (recursion-safe prax path)."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"{ts} | INFO | {message}\n")
|
||||
_append_jsonl(_HANDLER_LOG, {"level": "INFO", "msg": message})
|
||||
except Exception:
|
||||
pass # Meta-logging: cannot log a failure to log
|
||||
pass # seedgo:bypass meta-logging
|
||||
|
||||
|
||||
def _run_status_sync(reason: str) -> None:
|
||||
|
||||
@@ -8,23 +8,25 @@
|
||||
|
||||
"""Event Handler Registry - Setup all event handlers on startup"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from aipass.trigger.apps.handlers.json import json_handler
|
||||
from aipass.trigger.apps.config import TRIGGER_ROOT
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "registry_handler.log"
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "registry_handler.jsonl"
|
||||
|
||||
|
||||
def _log_warning(message: str) -> None:
|
||||
"""Log warning to file (event handlers cannot import Prax logger — causes recursion)."""
|
||||
"""Log warning to file (recursion-safe prax path)."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"{ts} | WARNING | {message}\n")
|
||||
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
|
||||
except Exception:
|
||||
pass # Meta-logging: cannot log a failure to log
|
||||
pass # seedgo:bypass meta-logging
|
||||
|
||||
|
||||
def setup_handlers():
|
||||
|
||||
@@ -21,14 +21,19 @@ import json
|
||||
import hashlib
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Callable, Dict, List, Optional, Set
|
||||
from aipass.trigger.apps.config import TRIGGER_ROOT, atomic_write_json
|
||||
from aipass.trigger.apps.handlers.json import json_handler
|
||||
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
SYSTEM_LOGS_DIR = TRIGGER_ROOT.parent.parent.parent / "system_logs"
|
||||
TRIGGER_DATA_FILE = TRIGGER_ROOT / "trigger_json" / "trigger_data.json"
|
||||
SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.log"
|
||||
SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl"
|
||||
|
||||
MAX_HASHES = 500
|
||||
MAX_LOOKBACK_HOURS = 24
|
||||
@@ -38,18 +43,17 @@ MAX_ERRORS_PER_SCAN = 50 # Stop after this many new errors found
|
||||
MAX_FILE_SIZE_BYTES = 512_000 # Skip files larger than 500KB
|
||||
SCAN_TIME_BUDGET_SECONDS = 5.0 # Abort entire scan after this many seconds
|
||||
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "startup_handler.log"
|
||||
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "startup_handler.jsonl"
|
||||
|
||||
|
||||
def _log_warning(message: str) -> None:
|
||||
"""Log warning to file (event handlers cannot import Prax logger - causes recursion)."""
|
||||
"""Log warning to file (recursion-safe prax path)."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"{ts} | WARNING | {message}\n")
|
||||
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
|
||||
except Exception:
|
||||
pass
|
||||
pass # seedgo:bypass meta-logging
|
||||
|
||||
|
||||
def _load_trigger_data() -> Dict[str, Any]:
|
||||
@@ -96,18 +100,13 @@ def _save_trigger_data(data: Dict[str, Any]) -> None:
|
||||
|
||||
|
||||
def _log_suppression(reason: str) -> None:
|
||||
"""Log a catchup suppression event to medic_suppressed.log.
|
||||
|
||||
Args:
|
||||
reason: Description of why scanning was capped or skipped
|
||||
"""
|
||||
"""Log a catchup suppression event to medic_suppressed.jsonl."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
SUPPRESSED_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(SUPPRESSED_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(f"{datetime.now().isoformat()} | error_catchup: {reason}\n")
|
||||
_append_jsonl(SUPPRESSED_LOG, {"ts": datetime.now().isoformat(), "source": "error_catchup", "reason": reason})
|
||||
except Exception as exc:
|
||||
_log_warning(f"log suppression write failed: {exc}")
|
||||
return
|
||||
|
||||
|
||||
def _generate_error_hash(source_module: str, message: str) -> str:
|
||||
|
||||
@@ -12,12 +12,17 @@ import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
import inspect
|
||||
|
||||
from aipass.trigger.apps.config import atomic_write_json
|
||||
|
||||
try:
|
||||
from aipass.prax import append_jsonl as _append_jsonl
|
||||
except Exception:
|
||||
_append_jsonl = None
|
||||
|
||||
if sys.platform == "win32":
|
||||
os.environ.setdefault("PYTHONUTF8", "1")
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
@@ -28,19 +33,19 @@ if sys.platform == "win32":
|
||||
# Infrastructure — redirect to temp dir during tests
|
||||
_test_log_dir = os.environ.get("AIPASS_TEST_LOG_DIR")
|
||||
if _test_log_dir:
|
||||
_LOG_FILE = Path(_test_log_dir) / "trigger" / "json_handler.log"
|
||||
_LOG_FILE = Path(_test_log_dir) / "trigger" / "json_handler.jsonl"
|
||||
else:
|
||||
_LOG_FILE = Path(__file__).parent.parent.parent.parent / "logs" / "json_handler.log"
|
||||
_LOG_FILE = Path(__file__).parent.parent.parent.parent / "logs" / "json_handler.jsonl"
|
||||
|
||||
|
||||
def _log_warning(msg: str) -> None:
|
||||
"""File-based warning logger to avoid circular imports with prax."""
|
||||
"""Recursion-safe warning logger via prax append_jsonl."""
|
||||
if _append_jsonl is None:
|
||||
return
|
||||
try:
|
||||
_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(_LOG_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} | WARNING | {msg}\n")
|
||||
_append_jsonl(_LOG_FILE, {"level": "WARNING", "msg": msg})
|
||||
except Exception:
|
||||
pass # Meta-logging: cannot log a failure to log
|
||||
pass # seedgo:bypass meta-logging
|
||||
|
||||
|
||||
# Constants
|
||||
|
||||
@@ -48,8 +48,8 @@ STALE_ENTRY_THRESHOLD_SECONDS = 300 # 5 minutes
|
||||
# Compared case-insensitively against Path.name (see on_modified)
|
||||
EXCLUDED_LOG_FILES: Set[str] = {
|
||||
"dispatch.log",
|
||||
"medic_suppressed.log",
|
||||
"rate_limited.log",
|
||||
"medic_suppressed.jsonl",
|
||||
"rate_limited.jsonl",
|
||||
"error_monitor.log",
|
||||
"log_watcher.log",
|
||||
"log_watcher.log.1",
|
||||
|
||||
@@ -28,8 +28,8 @@ from aipass.trigger.apps.handlers.json import json_handler
|
||||
logger = get_direct_logger()
|
||||
|
||||
TRIGGER_CONFIG_FILE = TRIGGER_ROOT / "trigger_json" / "trigger_config.json"
|
||||
MEDIC_SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.log"
|
||||
RATE_LIMITED_LOG = TRIGGER_ROOT / "logs" / "rate_limited.log"
|
||||
MEDIC_SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl"
|
||||
RATE_LIMITED_LOG = TRIGGER_ROOT / "logs" / "rate_limited.jsonl"
|
||||
|
||||
|
||||
def read_config() -> dict:
|
||||
@@ -190,8 +190,8 @@ def get_suppression_stats() -> Dict[str, Any]:
|
||||
lines = MEDIC_SUPPRESSED_LOG.read_text(encoding="utf-8").strip().splitlines()
|
||||
suppressed_count = len(lines)
|
||||
if lines:
|
||||
last_line = lines[-1]
|
||||
last_suppressed = last_line.split(" | ")[0] if " | " in last_line else "unknown"
|
||||
entry = json.loads(lines[-1])
|
||||
last_suppressed = entry.get("ts", "unknown")
|
||||
except Exception as exc:
|
||||
logger.warning("get_suppression_stats failed: %s", exc)
|
||||
return {"suppressed_count": 0, "last_suppressed": "error reading log"}
|
||||
@@ -216,8 +216,8 @@ def get_rate_limit_stats() -> Dict[str, Any]:
|
||||
lines = RATE_LIMITED_LOG.read_text(encoding="utf-8").strip().splitlines()
|
||||
dispatch_count = len(lines)
|
||||
if lines:
|
||||
last_line = lines[-1]
|
||||
last_dispatch = last_line.split(" | ")[0] if " | " in last_line else "unknown"
|
||||
entry = json.loads(lines[-1])
|
||||
last_dispatch = entry.get("ts", "unknown")
|
||||
except Exception as exc:
|
||||
logger.warning("get_rate_limit_stats failed: %s", exc)
|
||||
return {"rate_limited_count": 0, "last_rate_limited": "error reading log"}
|
||||
|
||||
@@ -198,11 +198,11 @@ def print_help() -> None:
|
||||
console.print()
|
||||
console.print(" [yellow]off[/yellow] Global kill switch. ALL error dispatch stops. No branch")
|
||||
console.print(" receives auto-healing emails. Errors still logged to")
|
||||
console.print(" medic_suppressed.log for review.")
|
||||
console.print(" medic_suppressed.jsonl for review.")
|
||||
console.print()
|
||||
console.print(" [yellow]mute[/yellow] Per-branch suppress. Only the muted branch stops receiving")
|
||||
console.print(" dispatch. All other branches continue normally. Muted errors")
|
||||
console.print(" logged to medic_suppressed.log.")
|
||||
console.print(" logged to medic_suppressed.jsonl.")
|
||||
console.print()
|
||||
console.rule("EXAMPLES")
|
||||
console.print()
|
||||
@@ -227,7 +227,7 @@ def print_help() -> None:
|
||||
console.print(" -> handler checks medic_enabled -> checks branch mute list")
|
||||
console.print(" -> dispatches fix-it email to affected branch (or suppresses)")
|
||||
console.print()
|
||||
console.print(" Suppressed errors: trigger/logs/medic_suppressed.log")
|
||||
console.print(" Suppressed errors: trigger/logs/medic_suppressed.jsonl")
|
||||
console.print()
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ def _handle_status(console) -> None:
|
||||
console.print(f" Last rate limit: {rate_limits['last_rate_limited']}")
|
||||
console.print()
|
||||
if not enabled:
|
||||
console.print(" [dim]All error dispatch suppressed. Errors logged to medic_suppressed.log[/dim]")
|
||||
console.print(" [dim]All error dispatch suppressed. Errors logged to medic_suppressed.jsonl[/dim]")
|
||||
|
||||
|
||||
def _handle_on(console) -> None:
|
||||
@@ -342,7 +342,7 @@ def _handle_off(console) -> None:
|
||||
Panel(
|
||||
"[bold yellow]Medic DISABLED[/bold yellow]\n\n"
|
||||
"Error dispatch is [yellow]suppressed[/yellow]. Errors are still detected\n"
|
||||
"and logged to [dim]medic_suppressed.log[/dim] for review.\n"
|
||||
"and logged to [dim]medic_suppressed.jsonl[/dim] for review.\n"
|
||||
"Log watcher: [yellow]stopped[/yellow]",
|
||||
title="Medic",
|
||||
border_style="yellow",
|
||||
|
||||
@@ -1073,10 +1073,10 @@ class TestShouldProcessEdgeCases:
|
||||
assert watcher._should_process(path) is False
|
||||
|
||||
def test_excluded_medic_suppressed(self):
|
||||
"""medic_suppressed.log is excluded."""
|
||||
"""medic_suppressed.jsonl is excluded."""
|
||||
lw = _import_log_watcher()
|
||||
watcher = lw.BranchLogWatcher()
|
||||
path = str(Path("/src") / "aipass" / "flow" / "logs" / "medic_suppressed.log")
|
||||
path = str(Path("/src") / "aipass" / "flow" / "logs" / "medic_suppressed.jsonl")
|
||||
assert watcher._should_process(path) is False
|
||||
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ def test_handle_command_status_suppression_hint_when_disabled():
|
||||
|
||||
console = _get_console()
|
||||
printed = _get_print_str_args(console)
|
||||
hint = " [dim]All error dispatch suppressed. Errors logged to medic_suppressed.log[/dim]"
|
||||
hint = " [dim]All error dispatch suppressed. Errors logged to medic_suppressed.jsonl[/dim]"
|
||||
assert hint in printed, f"Expected suppression hint '{hint}' in printed args: {printed}"
|
||||
|
||||
|
||||
|
||||
@@ -63,8 +63,8 @@ def state_mod(tmp_path, monkeypatch):
|
||||
import aipass.trigger.apps.handlers.medic_state as mod
|
||||
|
||||
config_file = tmp_path / "trigger_json" / "trigger_config.json"
|
||||
suppressed_log = tmp_path / "logs" / "medic_suppressed.log"
|
||||
rate_limited_log = tmp_path / "logs" / "rate_limited.log"
|
||||
suppressed_log = tmp_path / "logs" / "medic_suppressed.jsonl"
|
||||
rate_limited_log = tmp_path / "logs" / "rate_limited.jsonl"
|
||||
|
||||
monkeypatch.setattr(mod, "TRIGGER_CONFIG_FILE", config_file)
|
||||
monkeypatch.setattr(mod, "MEDIC_SUPPRESSED_LOG", suppressed_log)
|
||||
@@ -424,26 +424,26 @@ class TestGetSuppressionStats:
|
||||
assert result["last_suppressed"] == "never"
|
||||
|
||||
def test_populated_log(self, state_mod):
|
||||
"""get_suppression_stats parses log lines and returns correct stats."""
|
||||
"""get_suppression_stats parses JSONL lines and returns correct stats."""
|
||||
log_file = state_mod.MEDIC_SUPPRESSED_LOG
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
"2026-04-01 10:00:00 | ImportError | FLOW",
|
||||
"2026-04-02 11:00:00 | TimeoutError | API",
|
||||
"2026-04-03 12:00:00 | ValueError | DRONE",
|
||||
'{"ts": "2026-04-01T10:00:00", "reason": "count<2", "branch": "FLOW"}',
|
||||
'{"ts": "2026-04-02T11:00:00", "reason": "count<2", "branch": "API"}',
|
||||
'{"ts": "2026-04-03T12:00:00", "reason": "count<2", "branch": "DRONE"}',
|
||||
]
|
||||
log_file.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
result = state_mod.get_suppression_stats()
|
||||
|
||||
assert result["suppressed_count"] == 3
|
||||
assert result["last_suppressed"] == "2026-04-03 12:00:00"
|
||||
assert result["last_suppressed"] == "2026-04-03T12:00:00"
|
||||
|
||||
def test_log_line_without_pipe_separator(self, state_mod):
|
||||
"""get_suppression_stats returns 'unknown' when last line has no pipe."""
|
||||
def test_log_line_without_ts_field(self, state_mod):
|
||||
"""get_suppression_stats returns 'unknown' when last entry has no ts."""
|
||||
log_file = state_mod.MEDIC_SUPPRESSED_LOG
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.write_text("malformed line without pipe", encoding="utf-8")
|
||||
log_file.write_text('{"reason": "no timestamp"}', encoding="utf-8")
|
||||
|
||||
result = state_mod.get_suppression_stats()
|
||||
|
||||
@@ -478,25 +478,25 @@ class TestGetRateLimitStats:
|
||||
assert result["last_rate_limited"] == "never"
|
||||
|
||||
def test_populated_log(self, state_mod):
|
||||
"""get_rate_limit_stats parses log lines and returns correct stats."""
|
||||
"""get_rate_limit_stats parses JSONL lines and returns correct stats."""
|
||||
log_file = state_mod.RATE_LIMITED_LOG
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
"2026-04-01 08:00:00 | ImportError | fp123",
|
||||
"2026-04-02 09:00:00 | TimeoutError | fp456",
|
||||
'{"ts": "2026-04-01T08:00:00", "reason": "backoff", "detail": "fp123"}',
|
||||
'{"ts": "2026-04-02T09:00:00", "reason": "backoff", "detail": "fp456"}',
|
||||
]
|
||||
log_file.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
result = state_mod.get_rate_limit_stats()
|
||||
|
||||
assert result["rate_limited_count"] == 2
|
||||
assert result["last_rate_limited"] == "2026-04-02 09:00:00"
|
||||
assert result["last_rate_limited"] == "2026-04-02T09:00:00"
|
||||
|
||||
def test_log_line_without_pipe_separator(self, state_mod):
|
||||
"""get_rate_limit_stats returns 'unknown' when last line has no pipe."""
|
||||
def test_log_line_without_ts_field(self, state_mod):
|
||||
"""get_rate_limit_stats returns 'unknown' when last entry has no ts."""
|
||||
log_file = state_mod.RATE_LIMITED_LOG
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.write_text("malformed line", encoding="utf-8")
|
||||
log_file.write_text('{"reason": "no timestamp"}', encoding="utf-8")
|
||||
|
||||
result = state_mod.get_rate_limit_stats()
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ class TestHandleMemoryPoolAutoProcessedFailure:
|
||||
error="pool write failed",
|
||||
)
|
||||
|
||||
log_file = tmp_path / "logs" / "memory_pool_handler.log"
|
||||
log_file = tmp_path / "logs" / "memory_pool_handler.jsonl"
|
||||
assert log_file.exists()
|
||||
content = log_file.read_text()
|
||||
assert "pool write failed" in content
|
||||
|
||||
Reference in New Issue
Block a user