From 5641ec5fce8839f08d8c3a70b9ce99da360da684 Mon Sep 17 00:00:00 2001 From: Eddie Dunn <45917906+eddiedunn@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:57:28 -0400 Subject: [PATCH] feat: add CLI monitoring commands --- docs/router_api.md | 14 +++++++++ router/cli.py | 41 +++++++++++++++++++++++++++ tests/router/test_cli_logs_metrics.py | 26 +++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 tests/router/test_cli_logs_metrics.py diff --git a/docs/router_api.md b/docs/router_api.md index f92c303..98ae691 100644 --- a/docs/router_api.md +++ b/docs/router_api.md @@ -222,3 +222,17 @@ The router exposes Prometheus metrics at `/metrics`. Basic counters track request volume, latency and cache hits. Logs are written to `logs/router.log` and rotated daily. Configure the log level via the `LOG_LEVEL` environment variable. + +Tail the log file using the CLI: + +```bash +python -m router.cli show-logs --no-follow +``` + +Use `--follow` to stream updates or pass a custom path. + +Export metrics in Prometheus format: + +```bash +python -m router.cli export-metrics +``` diff --git a/router/cli.py b/router/cli.py index c3e7233..60bda18 100644 --- a/router/cli.py +++ b/router/cli.py @@ -5,6 +5,7 @@ import json import os from pathlib import Path +import time import httpx import typer @@ -103,5 +104,45 @@ def refresh_openai() -> None: typer.echo(f"Inserted {len(data)} models") +@app.command("show-logs") +def show_logs( + path: Path | None = typer.Argument(None), + follow: bool = True, +) -> None: + """Tail the router log file.""" + + file_path = ( + path if path is not None else Path(os.getenv("LOG_PATH", "logs/router.log")) + ) + if not file_path.exists(): + typer.echo(f"Log file '{file_path}' not found", err=True) + raise typer.Exit(1) + + with file_path.open() as fh: + if follow: + fh.seek(0, os.SEEK_END) + try: + while True: + line = fh.readline() + if not line: + time.sleep(0.5) + continue + typer.echo(line, nl=False) + except KeyboardInterrupt: + pass + else: + for line in fh: + typer.echo(line, nl=False) + + +@app.command("export-metrics") +def export_metrics(url: str = "http://localhost:8000/metrics") -> None: + """Fetch metrics from the router and print them.""" + + resp = httpx.get(url, timeout=10) + resp.raise_for_status() + typer.echo(resp.text) + + if __name__ == "__main__": app() diff --git a/tests/router/test_cli_logs_metrics.py b/tests/router/test_cli_logs_metrics.py new file mode 100644 index 0000000..f66b608 --- /dev/null +++ b/tests/router/test_cli_logs_metrics.py @@ -0,0 +1,26 @@ +from typer.testing import CliRunner +import router.cli as cli + + +def test_show_logs(tmp_path): + log_file = tmp_path / "router.log" + log_file.write_text("line1\nline2\n") + runner = CliRunner() + result = runner.invoke(cli.app, ["show-logs", str(log_file), "--no-follow"]) + assert result.exit_code == 0 + assert "line1" in result.stdout + assert "line2" in result.stdout + + +def test_export_metrics(monkeypatch): + class Resp: + text = "router_requests_total 1" + + def raise_for_status(self): + pass + + monkeypatch.setattr(cli.httpx, "get", lambda url, timeout=10: Resp()) + runner = CliRunner() + result = runner.invoke(cli.app, ["export-metrics"]) + assert result.exit_code == 0 + assert "router_requests_total" in result.stdout