0.2.6: Prometheus metrics endpoint for daemon observability

GET /metrics exposes sync counters (total, errors), duration
histogram, and file upload/delete counters — all labeled by source.
Includes oikb_info with build version. Adds prometheus-client dep.
This commit is contained in:
Timothy Jaeryang Baek 2026-05-21 12:43:19 +04:00
parent dae21fa4de
commit 6efcee9af8
6 changed files with 112 additions and 5 deletions

View file

@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [0.2.6] - 2025-05-21
### Added
- Prometheus metrics endpoint (`GET /metrics`) on the daemon — exports `oikb_sync_total`, `oikb_sync_duration_seconds`, `oikb_files_uploaded_total`, `oikb_files_deleted_total`, `oikb_sync_errors_total`, and `oikb_info` build metadata. All counters labeled by source.
- `prometheus-client>=0.20` added as a core dependency.
## [0.2.5] - 2025-05-21
### Added

View file

@ -47,6 +47,7 @@ Features:
- **Scheduled sync** — configurable per-source intervals (`30m`, `1h`, `6h`)
- **Webhooks** — instant sync on push via `/webhooks/github`, `/webhooks/gitlab`, `/webhooks/slack`, `/webhooks/confluence`
- **Health checks**`GET /health` for Docker/K8s readiness probes
- **Prometheus metrics**`GET /metrics` exports sync counters, duration histograms, and error rates
- **Sync history**`GET /history` queryable log of all syncs
- **On-demand sync**`POST /sync/{identifier}` trigger by `name` or `kb-id`
- **API key auth** — set `OIKB_API_KEY` to secure endpoints (Docker secrets `_FILE` supported)

View file

@ -1,6 +1,6 @@
[project]
name = "oikb"
version = "0.2.5"
version = "0.2.6"
description = "Sync anything to Open WebUI Knowledge Bases"
readme = "README.md"
authors = [
@ -11,6 +11,7 @@ license = {file = "LICENSE"}
dependencies = [
"click>=8.1",
"httpx>=0.27",
"prometheus-client>=0.20",
"pyyaml>=6.0",
"rich>=13.0",
"watchdog>=4.0",

View file

@ -1,3 +1,3 @@
"""oikb — Open WebUI Knowledge Base CLI."""
__version__ = "0.2.5"
__version__ = "0.2.6"

View file

@ -16,8 +16,10 @@ import uvicorn
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from oikb import __version__
from oikb.env import API_KEY
from oikb.history import SyncHistory
from oikb.metrics import record_sync, set_build_info
log = logging.getLogger(__name__)
@ -38,7 +40,7 @@ async def verify_api_key(
app = FastAPI(
title="oikb",
description="Sync engine for Open WebUI Knowledge Bases. Trigger syncs, check status, and query history.",
version="0.2.5",
version="0.2.6",
)
# Runtime state populated by start_daemon().
@ -164,10 +166,12 @@ async def _run_entry(entry: dict) -> None:
)
client.close()
duration_ms = int((time.time() - started_at) * 1000)
duration_s = time.time() - started_at
duration_ms = int(duration_s * 1000)
status = "success" if not result.errors else "partial"
_scheduler_state[source] = {
"status": "success" if not result.errors else "partial",
"status": status,
"last_sync": time.time(),
"duration_ms": duration_ms,
"files_added": result.added,
@ -177,6 +181,15 @@ async def _run_entry(entry: dict) -> None:
"errors": result.errors or [],
}
record_sync(
source=source,
status=status,
duration_seconds=duration_s,
files_added=result.added,
files_modified=result.modified,
files_deleted=result.deleted,
)
if _history:
await asyncio.to_thread(
_history.log,
@ -200,6 +213,11 @@ async def _run_entry(entry: dict) -> None:
"last_sync": time.time(),
"error": str(e),
}
record_sync(
source=source,
status="error",
duration_seconds=time.time() - started_at,
)
if _history:
await asyncio.to_thread(
_history.log,
@ -260,6 +278,12 @@ def start_daemon(
global _history, _entries
_entries = entries
_history = SyncHistory()
set_build_info(__version__)
# Mount Prometheus metrics endpoint.
from prometheus_client import make_asgi_app
metrics_app = make_asgi_app()
app.mount("/metrics", metrics_app)
# Mount webhook routes.
from oikb.webhooks import router as webhook_router, configure as configure_webhooks
@ -287,6 +311,7 @@ def start_daemon(
if webhook_entries:
click.echo(f" {len(webhook_entries)} webhook-enabled source(s)")
click.echo(f" GET http://localhost:{port}/health")
click.echo(f" GET http://localhost:{port}/metrics")
click.echo(f" GET http://localhost:{port}/history")
click.echo(f" POST http://localhost:{port}/sync/{{kb_id}}")
if webhook_entries:

73
src/oikb/metrics.py Normal file
View file

@ -0,0 +1,73 @@
"""Prometheus metrics for the oikb daemon."""
from __future__ import annotations
from prometheus_client import Counter, Histogram, Info
# ── Counters ─────────────────────────────────────────────────────
SYNC_TOTAL = Counter(
"oikb_sync_total",
"Total sync operations",
["source", "status"],
)
FILES_UPLOADED = Counter(
"oikb_files_uploaded_total",
"Total files uploaded (added + modified)",
["source"],
)
FILES_DELETED = Counter(
"oikb_files_deleted_total",
"Total files deleted during sync",
["source"],
)
SYNC_ERRORS = Counter(
"oikb_sync_errors_total",
"Total failed sync operations",
["source"],
)
# ── Histograms ───────────────────────────────────────────────────
SYNC_DURATION = Histogram(
"oikb_sync_duration_seconds",
"Sync duration in seconds",
["source"],
buckets=(1, 5, 10, 30, 60, 120, 300, 600, 1800),
)
# ── Info ─────────────────────────────────────────────────────────
BUILD_INFO = Info(
"oikb",
"oikb build information",
)
def set_build_info(version: str) -> None:
"""Set build info labels (called once on startup)."""
BUILD_INFO.info({"version": version})
def record_sync(
source: str,
status: str,
duration_seconds: float,
files_added: int = 0,
files_modified: int = 0,
files_deleted: int = 0,
) -> None:
"""Record metrics for a completed sync operation."""
SYNC_TOTAL.labels(source=source, status=status).inc()
SYNC_DURATION.labels(source=source).observe(duration_seconds)
uploaded = files_added + files_modified
if uploaded:
FILES_UPLOADED.labels(source=source).inc(uploaded)
if files_deleted:
FILES_DELETED.labels(source=source).inc(files_deleted)
if status == "error":
SYNC_ERRORS.labels(source=source).inc()