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:
@@ -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()
|
||||
|
||||
@@ -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}'.",
|
||||
)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user