Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions fastloom/monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import os
import re
import shutil
from collections.abc import Callable, Sequence
from enum import Enum
from os import getenv
Expand All @@ -25,7 +26,11 @@
from fastloom.cache.settings import RedisSettings
from fastloom.db.settings import MongoSettings
from fastloom.launcher.utils import is_installed
from fastloom.observability.settings import ObservabilitySettings, OtelConfig
from fastloom.observability.settings import (
ObservabilitySettings,
OtelConfig,
PrometheusConfig,
)
from fastloom.settings.base import FastAPISettings
from fastloom.signals.settings import RabbitmqSettings
from fastloom.tenant.protocols import TenantMonitoringSchema
Expand Down Expand Up @@ -245,11 +250,36 @@ def instrument_pydantic_ai():
logfire.instrument_pydantic_ai()


def instrument_prometheus(
settings: ObservabilitySettings,
prefix: str = "",
app: FastAPI | None = None,
):
from prometheus_fastapi_instrumentator import Instrumentator

Instrumentator(
should_group_status_codes=settings.PROMETHEUS_GROUP_STATUS_CODES,
should_ignore_untemplated=settings.PROMETHEUS_IGNORE_UNTEMPLATED,
should_group_untemplated=settings.PROMETHEUS_GROUP_UNTEMPLATED,
should_round_latency_decimals=settings.PROMETHEUS_ROUND_LATENCY_DECIMALS,
should_instrument_requests_inprogress=settings.PROMETHEUS_INSTRUMENT_REQUESTS_INPROGRESS,
round_latency_decimals=settings.PROMETHEUS_LATENCY_DECIMALS,
inprogress_name=settings.PROMETHEUS_INPROGRESS_NAME,
inprogress_labels=settings.PROMETHEUS_INPROGRESS_LABELS,
).instrument(app).expose(
app,
endpoint=f"{prefix}{settings.PROMETHEUS_METRICS_ENDPOINT}",
include_in_schema=settings.PROMETHEUS_INCLUDE_IN_SCHEMA,
should_gzip=settings.PROMETHEUS_SHOULD_GZIP,
)


class Instruments(Enum):
REDIS = instrument_redis
CELERY = instrument_celery
RABBIT = instrument_rabbit
HTTPX = instrument_httpx
PROMETHEUS = instrument_prometheus
REQUESTS = instrument_requests
METRICS = instrument_metrics
MONGODB = instrument_mongodb
Expand Down Expand Up @@ -306,6 +336,8 @@ def infer_instruments[T: BaseModel](settings: T) -> list[Instruments]:
instruments.append(Instruments.MONGODB)
if isinstance(settings, ObservabilitySettings) and settings.METRICS:
instruments.append(Instruments.METRICS)
if isinstance(settings, Instruments.PROMETHEUS):
instruments.append(Instruments.PROMETHEUS)
if is_installed("pydantic_ai"):
instruments.append(Instruments.PYDANTIC_AI)
return instruments
Expand All @@ -320,6 +352,14 @@ def setup_otel_config(settings: ObservabilitySettings):
os.environ[field_name] = str(value)


def setup_prometheus_multiproc(settings: PrometheusConfig) -> None:
if not (path := settings.PROMETHEUS_MULTIPROC_DIR):
return
shutil.rmtree(path, ignore_errors=True)
os.makedirs(path)
os.environ["PROMETHEUS_MULTIPROC_DIR"] = path


class InitMonitoring:
def __init__(
self,
Expand All @@ -331,6 +371,7 @@ def __init__(
self.instruments = instruments
self.otel_sampling = otel_sampling
setup_otel_config(settings)
setup_prometheus_multiproc(settings)

def __enter__(self):
if int(self.settings.SENTRY_ENABLED):
Expand All @@ -350,5 +391,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): ...
def instrument(
self, app: FastAPI, settings: FastAPISettings | None = None
):
if app is not None and int(self.settings.OTEL_ENABLED):
if app is None:
return
if int(self.settings.OTEL_ENABLED):
instrument_fastapi(app, settings)
if int(self.settings.PROMETHEUS_ENABLED):
instrument_prometheus(
self.settings,
prefix=self.settings.API_PREFIX,
app=app,
)
47 changes: 47 additions & 0 deletions fastloom/observability/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import inspect
from collections.abc import Callable
from functools import wraps

_CACHE: dict[str, tuple] = {}


def _metric_name(func: Callable) -> str:
name = getattr(func, "__name__", "unknown")
parts = (getattr(func, "__module__", None) or "").split(".")
return f"{parts[1]}_{name}" if len(parts) > 1 else name


def prom_track(name: str | None = None):
from prometheus_client import Counter, Histogram

def decorator(func: Callable) -> Callable:
base = name or _metric_name(func)
counter, histogram = _CACHE.setdefault(
base,
(
Counter(f"{base}_calls_total", f"Total calls of {base}"),
Histogram(
f"{base}_duration_seconds",
f"Duration of {base} in seconds",
),
),
)

if inspect.iscoroutinefunction(func):

@wraps(func)
async def wrapper(*args, **kwargs): # pyright: ignore[reportRedeclaration]
counter.inc()
with histogram.time():
return await func(*args, **kwargs)
else:

@wraps(func)
def wrapper(*args, **kwargs):
counter.inc()
with histogram.time():
return func(*args, **kwargs)

return wrapper

return decorator
22 changes: 21 additions & 1 deletion fastloom/observability/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,28 @@ class OtelConfig(BaseModel):
] = EnvDefault(".*")


class ObservabilitySettings(MonitoringSettings, OtelConfig):
class PrometheusConfig(BaseModel):
PROMETHEUS_METRICS_ENDPOINT: EnvBackend[str] = EnvDefault("/metrics")
PROMETHEUS_INCLUDE_IN_SCHEMA: EnvBackend[bool] = EnvDefault(True)
PROMETHEUS_SHOULD_GZIP: EnvBackend[bool] = EnvDefault(False)
PROMETHEUS_GROUP_STATUS_CODES: EnvBackend[bool] = EnvDefault(True)
PROMETHEUS_IGNORE_UNTEMPLATED: EnvBackend[bool] = EnvDefault(False)
PROMETHEUS_GROUP_UNTEMPLATED: EnvBackend[bool] = EnvDefault(True)
PROMETHEUS_ROUND_LATENCY_DECIMALS: EnvBackend[bool] = EnvDefault(False)
PROMETHEUS_LATENCY_DECIMALS: EnvBackend[int] = EnvDefault(4)
PROMETHEUS_INSTRUMENT_REQUESTS_INPROGRESS: EnvBackend[bool] = EnvDefault(
False
)
PROMETHEUS_INPROGRESS_NAME: EnvBackend[str] = EnvDefault(
"http_requests_inprogress"
)
PROMETHEUS_INPROGRESS_LABELS: EnvBackend[bool] = EnvDefault(False)
PROMETHEUS_MULTIPROC_DIR: EnvBackend[str] = EnvDefault("")


class ObservabilitySettings(MonitoringSettings, OtelConfig, PrometheusConfig):
SENTRY_ENABLED: int = 0
OTEL_ENABLED: int = 0
PROMETHEUS_ENABLED: int = 0
SENTRY_DSN: AnyHttpUrl | None = None
METRICS: bool = False
44 changes: 40 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ kafka = [
]
redis = ["faststream[redis]", "redis-om>=1.0,<2.0"]
fastapi = ["fastapi>=0,<1", "uvicorn", "python-multipart"]
prometheus = ["prometheus-fastapi-instrumentator>=7.0.0,<8.0.0"]

mongo = ["beanie>=2.0.0,<3.0.0"]
celery = ["celery>=5.5.3,<6.0.0"]
Expand Down