"""Debug Oracle CLI - main entry point."""
import argparse
import logging
import sys
from datetime import datetime, timezone
from .config import OracleConfig
from .ingest import DockerIngestor, SystemdIngestor, GitIngestor, SystemMetricsIngestor
from .reason import ReasoningEngine
from .output import OutputFormatter
from .models import Severity
from .watchdog import Watchdog
def setup_logging(verbose: int = 0):
"""Configure logging based on verbosity."""
level = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}.get(verbose, logging.INFO)
logging.basicConfig(
level=level,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
datefmt="%H:%M:%S",
)
def cmd_investigate(args, config: OracleConfig):
"""Investigate a problem."""
logger = logging.getLogger(__name__)
# Collect data
logger.info("Collecting data...")
docker = DockerIngestor()
systemd = SystemdIngestor()
git = GitIngestor()
metrics = SystemMetricsIngestor()
# Docker logs
containers = args.containers if args.containers else config.ingest.containers
logs = docker.get_all_logs(
containers=containers or None,
since_minutes=config.ingest.lookback_minutes,
tail=config.ingest.max_log_lines
)
# Systemd logs
system_logs = systemd.get_logs(
since_minutes=config.ingest.lookback_minutes,
priority=args.priority or "warning"
)
logs.extend(system_logs)
# Metrics
metric_snapshots = []
if config.ingest.include_metrics:
metric_snapshots.append(metrics.get_snapshot())
# Git changes
git_changes = []
if args.repos:
for repo in args.repos:
changes = git.get_recent_commits(
repo,
since_minutes=config.ingest.lookback_minutes * 24 # Git looks further back
)
git_changes.extend(changes)
logger.info(
"Collected: %d logs, %d metrics, %d git changes",
len(logs), len(metric_snapshots), len(git_changes)
)
# Show severity summary
formatter = OutputFormatter(config.output_format)
formatter.print_severity_summary(logs)
# Run analysis
query = args.query
if not query:
# Auto-generate query from errors
errors = [l for l in logs if l.severity in (Severity.ERROR, Severity.CRITICAL)]
if errors:
recent_errors = [e.message[:100] for e in errors[:5]]
query = f"System failure detected. Recent errors: {'; '.join(recent_errors)}"
else:
query = "Investigate recent system activity and identify potential issues."
try:
engine = ReasoningEngine(config.llm)
investigation = engine.analyze(query, logs, metric_snapshots, git_changes)
formatter.print_investigation(investigation)
engine.close()
except (RuntimeError, TimeoutError) as e:
print(f"\n[Error] Analysis failed: {e}", file=sys.stderr)
sys.exit(1)
def cmd_triage(args, config: OracleConfig):
"""Quick triage - assess situation before deep dive."""
logger = logging.getLogger(__name__)
docker = DockerIngestor()
logs = docker.get_all_logs(
containers=args.containers or None,
since_minutes=30,
tail=200
)
query = args.query or "Triage recent system issues"
try:
engine = ReasoningEngine(config.llm)
result = engine.triage(query, logs)
formatter = OutputFormatter(config.output_format)
if formatter.format == "rich":
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
console = Console()
console.print("\n[bold cyan]⚙ Quick Triage[/bold cyan]\n")
console.print(Panel(Markdown(result), title="Assessment", border_style="yellow"))
console.print()
else:
print(f"\nQuick Triage: {query}\n")
print(result)
engine.close()
except (RuntimeError, TimeoutError) as e:
print(f"\n[Error] Triage failed: {e}", file=sys.stderr)
sys.exit(1)
def cmd_watch(args, config: OracleConfig):
"""Start watchdog monitoring."""
import signal
import sys
import threading
# Import watchdog here to avoid circular imports
from .watchdog import Watchdog
# Determine alert target
alert_target = args.alert or config.alert_target
watchdog = Watchdog(
check_interval=args.interval,
containers=args.containers or [],
metrics_thresholds={
"cpu_critical": args.cpu_threshold or 90.0,
"memory_critical": args.mem_threshold or 90.0,
"disk_critical": args.disk_threshold or 90.0,
},
alert_target=alert_target,
llm_config=config.llm,
state_dir=config.state_dir,
)
# Handle shutdown gracefully
def shutdown_handler(signum, frame):
print("\nShutting down watchdog...")
watchdog.stop()
sys.exit(0)
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)
print(f"Debug Oracle Watchdog started")
print(f" Check interval: {args.interval}s")
print(f" Alert target: {alert_target}")
print(f" State: {config.state_dir}")
print(f" Press Ctrl+C to stop\n")
# Run in main thread (blocking)
watchdog.run()
def cmd_status(args, config: OracleConfig):
"""Show system status overview."""
docker = DockerIngestor()
metrics = SystemMetricsIngestor()
systemd = SystemdIngestor()
snapshot = metrics.get_snapshot()
containers = docker.get_containers()
failed = systemd.get_failed_units()
formatter = OutputFormatter(config.output_format)
if formatter.format == "rich":
from rich.console import Console
console = Console()
console.print("\n[bold cyan]⚙ System Status[/bold cyan]\n")
# Metrics
console.print("[bold]System Metrics:[/bold]")
console.print(f" CPU: {snapshot.cpu_percent:.1f}%")
console.print(f" Memory: {snapshot.memory_percent:.1f}%")
console.print(f" Disk: {snapshot.disk_percent:.1f}%")
console.print(f" Load: {snapshot.load_avg}")
console.print()
# Containers
if containers:
console.print(f"[bold]Running Containers ({len(containers)}):[/bold]")
for c in containers:
health = docker.get_health(c)
status = health.get("status", "unknown")
color = "green" if "running" in status else "red"
console.print(f" [{color}]{c}[/]: {status}")
console.print()
# Failed units
if failed:
console.print(f"[bold red]Failed Units ({len(failed)}):[/bold red]")
for unit in failed:
console.print(f" - {unit['unit']} ({unit['active']})")
console.print()
else:
print("\nSystem Status:")
print(f" CPU: {snapshot.cpu_percent:.1f}%")
print(f" Memory: {snapshot.memory_percent:.1f}%")
print(f" Disk: {snapshot.disk_percent:.1f}%")
print(f" Load: {snapshot.load_avg}")
print(f"\nContainers: {', '.join(containers)}")
if failed:
print(f"\nFailed: {', '.join(u['unit'] for u in failed)}")
print()
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(
prog="oracle",
description="Debug Oracle - causal debugging that explains why things broke",
)
parser.add_argument("-v", "--verbose", action="count", default=0, help="Verbosity level")
parser.add_argument("--llm-url", default=None, help="LLM API base URL")
parser.add_argument("--format", choices=["rich", "text", "json"], default="rich", help="Output format")
subparsers = parser.add_subparsers(dest="command", help="Command")
# Investigate
inv_parser = subparsers.add_parser("investigate", help="Investigate a problem")
inv_parser.add_argument("query", nargs="?", default=None, help="What to investigate")
inv_parser.add_argument("-c", "--containers", nargs="*", help="Docker containers to check")
inv_parser.add_argument("-r", "--repos", nargs="*", help="Git repos to check")
inv_parser.add_argument("-p", "--priority", choices=["info", "warning", "error"], help="Log priority filter")
inv_parser.set_defaults(func=cmd_investigate)
# Triage
triage_parser = subparsers.add_parser("triage", help="Quick triage assessment")
triage_parser.add_argument("query", nargs="?", default=None, help="What to triage")
triage_parser.add_argument("-c", "--containers", nargs="*", help="Docker containers to check")
triage_parser.set_defaults(func=cmd_triage)
# Status
status_parser = subparsers.add_parser("status", help="System status overview")
status_parser.set_defaults(func=cmd_status)
# Watch
watch_parser = subparsers.add_parser("watch", help="Start watchdog monitoring")
watch_parser.add_argument("-i", "--interval", type=int, default=300, help="Check interval in seconds (default: 300)")
watch_parser.add_argument("-c", "--containers", nargs="*", help="Docker containers to monitor")
watch_parser.add_argument("-a", "--alert", default="telegram", help="Alert target (default: telegram)")
watch_parser.add_argument("--cpu-threshold", type=float, default=90.0, help="CPU critical threshold %")
watch_parser.add_argument("--mem-threshold", type=float, default=90.0, help="Memory critical threshold %")
watch_parser.add_argument("--disk-threshold", type=float, default=90.0, help="Disk critical threshold %")
watch_parser.set_defaults(func=cmd_watch)
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(0)
# Setup
setup_logging(args.verbose)
config = OracleConfig(output_format=args.format)
if args.llm_url:
config.llm.base_url = args.llm_url
# Run command
args.func(args, config)
if __name__ == "__main__":
main()