opencode-config/.opencode/scripts/observability.py
Sergey a8f9aa0bc6
feat: migrate .opencode/ config from opencode (#23)
* feat: migrate .opencode/ config from opencode

* refactor: rename repo refs and sanitize for public

* docs(handoff): add pr-7 handoff + ADR-002

* docs(handoff): fix PR number

* docs: update project map with .opencode/ structure

---------

Co-authored-by: opencode-agent <agent@slaid098.dev>
2026-07-23 22:57:39 +03:00

94 lines
3.3 KiB
Python
Executable file

#!/usr/bin/env python3
"""Parse opencode log for permission denials and tool errors."""
import re
import sys
from collections import defaultdict
from pathlib import Path
LOG_PATH = Path.home() / ".local" / "share" / "opencode" / "log" / "opencode.log"
def parse_line(line):
"""Extract fields from a log line."""
ts_match = re.search(r"timestamp=(\S+)", line)
run_match = re.search(r"run=(\S+)", line)
ts = ts_match.group(1) if ts_match else "?"
run = run_match.group(1) if run_match else "?"
return ts, run
def _process_line(line, sessions, denials, errors):
"""Classify a single log line into sessions/denials/errors."""
if "message=created" in line and "agent=" in line:
ts, run = parse_line(line)
agent_match = re.search(r"agent=(\S+)", line)
session_match = re.search(r"id=(\S+)", line)
title_match = re.search(r'title="([^"]*)"', line)
agent = agent_match.group(1) if agent_match else "unknown"
session = session_match.group(1) if session_match else "?"
title = title_match.group(1) if title_match else ""
sessions[run] = {"agent": agent, "session": session, "title": title}
elif "action.action=deny" in line:
ts, run = parse_line(line)
pattern_match = re.search(r'pattern="([^"]*)"', line)
perm_match = re.search(r"permission=(\S+)", line)
pattern = pattern_match.group(1) if pattern_match else "?"
perm = perm_match.group(1) if perm_match else "?"
denials.append({"ts": ts, "run": run, "pattern": pattern, "perm": perm})
elif "message=process" in line and "level=ERROR" in line:
ts, run = parse_line(line)
error_match = re.search(r"error=(\S+)", line)
session_match = re.search(r"session\.id=(\S+)", line)
error = error_match.group(1) if error_match else "unknown"
session = session_match.group(1) if session_match else "?"
errors.append({"ts": ts, "run": run, "error": error, "session": session})
def main():
if not LOG_PATH.exists():
print("Log file not found: {LOG_PATH}")
sys.exit(1)
sessions = {}
denials = []
errors = []
with open(LOG_PATH) as f:
for line in f:
_process_line(line, sessions, denials, errors)
if not denials and not errors:
print("No denials or errors found in log.")
return
print("# Observability Report")
print(f"# Log: {LOG_PATH}")
print(f"# Denials: {len(denials)} | Errors: {len(errors)}")
print()
if denials:
print("## Permission Denials\n")
by_agent = defaultdict(list)
for d in denials:
info = sessions.get(d["run"], {"agent": "unknown", "session": "?", "title": ""})
by_agent[info["agent"]].append(d)
for agent, items in sorted(by_agent.items()):
print(f"### {agent} ({len(items)} denials)\n")
for d in items:
info = sessions.get(d["run"], {"session": "?"})
print(f"- {d['ts']} | `{d['pattern']}` | {d['perm']} | {info['session']}")
print()
if errors:
print("## Process Errors\n")
for e in errors[-20:]:
info = sessions.get(e["run"], {"agent": "unknown"})
print(f"- {e['ts']} | {info['agent']} | {e['error']} | {e['session']}")
if __name__ == "__main__":
main()