Ctrl K
ComunidadGuías…

app/metrics.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Prometheus metrics — about the machine, never about the money.

The only endpoint that reads the database without a session, so *what* is
published matters more than the format. Every series describes the app's own
health: when prices were last fetched, whether that worked, whether the
database answers. `tests/test_metrics.py` pins the list closed.

Off by default, and no `prometheus_client` dependency: decisions.md #85.
"""

from __future__ import annotations

import datetime as dt
import time

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.models import FxRate, Price

# The version matters: Prometheus content-negotiates, and a bare `text/plain`
# is accepted but leaves the parser guessing. 0.0.4 is the text exposition
# format everything speaks.
CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"

# Process-lifetime counters. Prometheus expects counters to reset when a process
# restarts and handles it (`increase()`/`rate()` detect the drop), so there is
# nothing to persist here — and persisting them would mean a write on every feed
# run for a number nobody reads except a graph.
_runs = 0
_failures = 0
_last_success: float | None = None


def reset() -> None:
    """Back to a fresh process's state. For tests."""
    global _runs, _failures, _last_success
    _runs = 0
    _failures = 0
    _last_success = None


def record_feed_run(*, ok: bool) -> None:
    """Called once per price-feed run, whatever the outcome.

    `_last_success` is only stamped on success on purpose: the question the
    alert asks is "how long since this last *worked*", and refreshing the
    timestamp on a failure would answer "just now" forever while every fetch
    was failing.
    """
    global _runs, _failures, _last_success
    _runs += 1
    if ok:
        _last_success = time.time()
    else:
        _failures += 1


def _midnight_utc(day: dt.date) -> float:
    """A date as unix seconds.

    Prices are dated, not timestamped — a close belongs to a trading day, not a
    moment. UTC midnight is the conventional way to say that in a gauge, and it
    means an alert's arithmetic is the same everywhere regardless of the
    install's timezone.
    """
    return dt.datetime(day.year, day.month, day.day, tzinfo=dt.timezone.utc).timestamp()


def _render(name: str, help_text: str, kind: str, value: float) -> str:
    # `:.0f` rather than repr: these are whole seconds, and the default float
    # formatting turns them into 1.7543e+09, which is legal and unreadable at
    # 3am when an alert has just fired.
    return (f"# HELP {name} {help_text}\n"
            f"# TYPE {name} {kind}\n"
            f"{name} {value:.0f}\n")


def render(session: Session | None) -> str:
    """The exposition body.

    `session` is None when the app has no database yet (the first-run wizard has
    not been finished). That is a legitimate state, not an error: report
    `stocktake_database_ready 0` and skip the series that would need a query.
    """
    ready = session is not None
    out = [_render(
        "stocktake_database_ready",
        "Whether the app can reach its database (1) or not (0).",
        "gauge", 1 if ready else 0,
    )]

    if session is not None:
        # Market data is shared across every portfolio, so these read unscoped
        # by nature — there is no tenancy question to answer here, which is
        # also why they are safe to publish.
        last_price = session.execute(select(func.max(Price.date))).scalar()
        last_fx = session.execute(select(func.max(FxRate.date))).scalar()
        # Absent, not zero, when there is nothing. A zero would claim prices
        # were last updated in 1970 and fire the staleness alert on every fresh
        # install; absent lets a rule use `absent()` if it wants to say
        # something about that case, and say nothing if it does not.
        if last_price is not None:
            out.append(_render(
                "stocktake_last_price_date_seconds",
                "Trading day of the most recent close held, as unix seconds (UTC midnight).",
                "gauge", _midnight_utc(last_price),
            ))
        if last_fx is not None:
            out.append(_render(
                "stocktake_last_fx_date_seconds",
                "Date of the most recent FX rate held, as unix seconds (UTC midnight).",
                "gauge", _midnight_utc(last_fx),
            ))

    out.append(_render(
        "stocktake_feed_runs_total",
        "Price feed runs since this process started.",
        "counter", _runs,
    ))
    out.append(_render(
        "stocktake_feed_failures_total",
        "Price feed runs that raised, since this process started.",
        "counter", _failures,
    ))
    if _last_success is not None:
        out.append(_render(
            "stocktake_feed_last_success_seconds",
            "When the price feed last completed without raising, as unix seconds.",
            "gauge", _last_success,
        ))

    return "".join(out)