fixed version prometheus-client

added env variable SPOOLMAN_METRICS_ENABLED for enabled database collector

set metrics path as /metrics

checked Null values at vendors, init weight and price
removed some logs
This commit is contained in:
Dmitry Belousov
2024-04-15 00:17:36 +03:00
parent fc8df5346d
commit 91d922d0b8
7 changed files with 109 additions and 63 deletions

View File

@@ -1,11 +1,9 @@
"""SQLAlchemy database setup."""
import asyncio
import datetime
import logging
import shutil
import sqlite3
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from os import PathLike
from pathlib import Path
from typing import Optional, Union
@@ -15,7 +13,6 @@ from sqlalchemy import URL
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from spoolman import env
from spoolman.prometheus.metrics import filament_metrics, spool_metrics
logger = logging.getLogger(__name__)
@@ -187,12 +184,12 @@ async def _backup_task() -> Optional[Path]:
async def _metrics() -> None:
"""Create some useful prometheus metrics"""
logger.info("Start metrics collection")
async with get_session() as session:
"""Create some useful prometheus metrics."""
logger.debug("Start metrics collection")
async for session in get_db_session():
await filament_metrics(session)
await spool_metrics(session)
logger.info("End metrics collection")
logger.debug("End metrics collection")
def schedule_tasks(scheduler: Scheduler) -> None:
@@ -203,15 +200,16 @@ def schedule_tasks(scheduler: Scheduler) -> None:
"""
if __db is None:
raise RuntimeError("DB is not setup.")
if env.is_metrics_enabled():
logger.info("Scheduling automatic metric collection.")
# Run every minute, may be needs specify timer
scheduler.minutely(datetime.time(second=0), _metrics)
if not env.is_automatic_backup_enabled():
return
if "sqlite" in __db.connection_url.drivername:
logger.info("Scheduling automatic database backup for midnight.")
# Schedule for midnight
scheduler.daily(datetime.time(hour=0, minute=0, second=0), _backup_task) # type: ignore[arg-type]
logger.info("Scheduling automatic metric collection.")
logger.info("%s", datetime.time(minute=1))
scheduler.minutely(datetime.time(second=0), _metrics)
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
@@ -231,18 +229,3 @@ async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
raise exc
finally:
await session.close()
@asynccontextmanager
async def get_session() -> AsyncSession:
if __db is None or __db.session_maker is None:
raise RuntimeError("DB is not setup.")
async with __db.session_maker() as session:
try:
yield session
await session.commit()
except Exception as exc:
await session.rollback()
raise exc
finally:
await session.close()

View File

@@ -380,3 +380,21 @@ def is_data_dir_mounted() -> bool:
mounts = subprocess.run("mount", check=True, stdout=subprocess.PIPE, text=True) # noqa: S603, S607
data_dir = str(get_data_dir().resolve())
return any(data_dir in line for line in mounts.stdout.splitlines())
def is_metrics_enabled() -> bool:
"""Get whether collect prometheus metrics at database is enabled.
Returns False if no environment variable was set for collect metrics.
Returns:
bool: Whether collect metrics is enabled.
"""
metrics_enabled = os.getenv("SPOOLMAN_METRICS_ENABLED", "FALSE").upper()
if metrics_enabled in {"FALSE", "0"}:
return False
if metrics_enabled in {"TRUE", "1"}:
return True
raise ValueError(
f"Failed to parse SPOOLMAN_METRICS_ENABLED variable: Unknown metrics enabled '{metrics_enabled}'.",
)

View File

@@ -1,5 +1,4 @@
"""Main entrypoint to the server."""
import logging
import subprocess
from logging.handlers import TimedRotatingFileHandler
@@ -9,14 +8,15 @@ import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import PlainTextResponse
from prometheus_client import generate_latest
from scheduler.asyncio.scheduler import Scheduler
from spoolman import env
from spoolman.api.v1.router import app as v1_app
from spoolman.client import SinglePageApplication
from spoolman.database import database
from spoolman.prometheus.metrics import metrics_app
from spoolman.prometheus.metrics import registry
# Define a console logger
console_handler = logging.StreamHandler()
@@ -39,8 +39,24 @@ app = FastAPI(
)
app.add_middleware(GZipMiddleware)
app.mount("/api/v1", v1_app)
app.mount("/metrics", metrics_app)
app.mount("/", app=SinglePageApplication(directory="client/dist"), name="client")
# WA for prometheus /metrics bind with SinglePageApp at root
@app.get(
"/metrics",
response_class=PlainTextResponse,
name="Get metrics for prometheus",
description=(
"Get app metrics for prometheusIf enabled SPOOLMAN_METRICS_ENABLED returned metrics by Spools and Filaments"
),
)
def get_metrics() -> bytes:
"""Return prometheus metrics."""
return generate_latest(registry)
app.mount("", app=SinglePageApplication(directory="client/dist"))
# Allow all origins if in debug mode
if env.is_debug_mode():

View File

@@ -1,23 +1,25 @@
import sqlalchemy
"""Prometheus metrics collectors."""
import logging
from typing import Callable
import sqlalchemy
from prometheus_client import REGISTRY, Gauge, make_asgi_app
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import contains_eager
from spoolman.database import models
from prometheus_client import REGISTRY, make_asgi_app, Gauge
registry = REGISTRY
PREFIX = "spoolman"
SPOOL_PRICE = Gauge("%s_spool_price" % PREFIX, "Total Spool price", ['spool_id', 'filament_id'])
SPOOL_USED_WEIGHT = Gauge("%s_spool_weight_used" % PREFIX, "Spool Used Weight", ['spool_id', 'filament_id'])
FILAMENT_INFO = Gauge("%s_filament_info" % PREFIX, "Filament information", [
'filament_id',
'vendor',
'name',
'material',
'color'])
SPOOL_PRICE = Gauge("%s_spool_price" % PREFIX, "Total Spool price", ["spool_id", "filament_id"])
SPOOL_USED_WEIGHT = Gauge("%s_spool_weight_used" % PREFIX, "Spool Used Weight", ["spool_id", "filament_id"])
FILAMENT_INFO = Gauge(
"%s_filament_info" % PREFIX,
"Filament information",
["filament_id", "vendor", "name", "material", "color"],
)
FILAMENT_DENSITY = Gauge("%s_filament_density" % PREFIX, "Density of filament", ["filament_id"])
FILAMENT_DIAMETER = Gauge("%s_filament_diameter" % PREFIX, "Diameter of filament", ["filament_id"])
FILAMENT_WEIGHT = Gauge("%s_filament_weight" % PREFIX, "Net weight of filament", ["filament_id"])
@@ -25,47 +27,60 @@ FILAMENT_WEIGHT = Gauge("%s_filament_weight" % PREFIX, "Net weight of filament",
logger = logging.getLogger(__name__)
def make_metrics_app():
registry = REGISTRY
def make_metrics_app() -> Callable:
"""Start ASGI prometheus app with global registry."""
logger.info("Start metrics app")
return make_asgi_app(registry=registry)
metrics_app = make_metrics_app()
metrics_app = make_asgi_app()
async def spool_metrics(db: AsyncSession) -> None:
stmt = (
sqlalchemy.select(models.Spool)
.where(
sqlalchemy.or_(
models.Spool.archived.is_(False),
models.Spool.archived.is_(None),
)
)
"""Get metrics by Spools from DB and write to prometheus.
Args:
db: async db session
"""
stmt = sqlalchemy.select(models.Spool).where(
sqlalchemy.or_(
models.Spool.archived.is_(False),
models.Spool.archived.is_(None),
),
)
rows = await db.execute(stmt)
result = list(rows.unique().scalars().all())
for row in result:
SPOOL_PRICE.labels(str(row.id), str(row.filament_id)).set(row.price)
if row.price is not None:
SPOOL_PRICE.labels(str(row.id), str(row.filament_id)).set(row.price)
SPOOL_USED_WEIGHT.labels(str(row.id), str(row.filament_id)).set(row.used_weight)
async def filament_metrics(db: AsyncSession) -> None:
"""Get metrics and info by Filaments from DB and write to prometheus.
Args:
db: async db session
"""
stmt = (
sqlalchemy.select(models.Filament)
.options(contains_eager(models.Filament.vendor))
.join(models.Filament.vendor, isouter=True)
.options(contains_eager(models.Filament.vendor))
.join(models.Filament.vendor, isouter=True)
)
rows = await db.execute(stmt)
result = list(rows.unique().scalars().all())
for row in result:
FILAMENT_INFO.labels(str(row.id),
row.vendor.name,
row.name,
row.material,
row.color_hex).set(1)
vendor_name = "-"
if row.vendor is not None:
vendor_name = row.vendor.name
FILAMENT_INFO.labels(
str(row.id),
vendor_name,
row.name,
row.material,
row.color_hex,
).set(1)
FILAMENT_DENSITY.labels(str(row.id)).set(row.density)
FILAMENT_DIAMETER.labels(str(row.id)).set(row.diameter)
FILAMENT_WEIGHT.labels(str(row.id)).set(row.weight)
if row.weight is not None:
FILAMENT_WEIGHT.labels(str(row.id)).set(row.weight)