Add structured JSON logging, OTel business metrics, and Grafana dashboard
Structured logging via JsonFormatter replaces uvicorn's default format so Loki can parse timestamps and fields. 14 business metrics (scrape stats, throttle events, circuit breaker state, cache hit rate, OCR success rate, Celery task lifecycle) are defined in a shared metrics module and instrumented across the scraper pipeline, API, and workers. Celery workers expose a Prometheus HTTP endpoint on configurable ports.
This commit is contained in:
parent
a1829957c1
commit
d6edb747d2
12 changed files with 742 additions and 49 deletions
|
|
@ -135,3 +135,54 @@ class CircuitBreaker:
|
|||
def is_half_open(self) -> bool:
|
||||
"""Check if circuit is currently half-open."""
|
||||
return self.state == CircuitState.HALF_OPEN
|
||||
|
||||
@property
|
||||
def state_as_int(self) -> int:
|
||||
"""Return the current state as an integer for metrics.
|
||||
|
||||
0 = closed, 1 = half_open, 2 = open.
|
||||
"""
|
||||
return {
|
||||
CircuitState.CLOSED: 0,
|
||||
CircuitState.HALF_OPEN: 1,
|
||||
CircuitState.OPEN: 2,
|
||||
}[self.state]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Global circuit breaker instance used by the scraper
|
||||
# ---------------------------------------------------------------------------
|
||||
_global_circuit_breaker: CircuitBreaker | None = None
|
||||
|
||||
|
||||
def get_circuit_breaker() -> CircuitBreaker | None:
|
||||
"""Return the global circuit breaker, if one has been set."""
|
||||
return _global_circuit_breaker
|
||||
|
||||
|
||||
def set_global_circuit_breaker(cb: CircuitBreaker) -> None:
|
||||
"""Set the global circuit breaker instance (called during scraper init)."""
|
||||
global _global_circuit_breaker
|
||||
_global_circuit_breaker = cb
|
||||
|
||||
|
||||
def register_circuit_breaker_gauge() -> None:
|
||||
"""Register an ObservableGauge that reports the circuit breaker state."""
|
||||
try:
|
||||
from opentelemetry.metrics import get_meter
|
||||
|
||||
meter = get_meter(__name__)
|
||||
|
||||
def _observe_cb_state(options: object) -> list: # type: ignore[type-arg]
|
||||
from opentelemetry.sdk.metrics._internal.measurement import Measurement
|
||||
cb = get_circuit_breaker()
|
||||
value = cb.state_as_int if cb is not None else 0
|
||||
return [Measurement(value)]
|
||||
|
||||
meter.create_observable_gauge(
|
||||
"circuit_breaker_state",
|
||||
callbacks=[_observe_cb_state],
|
||||
description="Circuit breaker state: 0=closed, 1=half_open, 2=open",
|
||||
)
|
||||
except Exception:
|
||||
pass # Metrics not initialised
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue