added prometheus exporter

This commit is contained in:
Jan Felix Wiebe 2025-07-11 14:56:30 +02:00
parent be5ce63add
commit 7017df5fa3
5 changed files with 139 additions and 2 deletions

View file

@ -3,6 +3,7 @@ import uuid
from datetime import datetime from datetime import datetime
from models import Order, Drink from models import Order, Drink
from websocket_manager import websocket_manager from websocket_manager import websocket_manager
from prometheus_metrics import drink_metrics
class OrderDatabase: class OrderDatabase:
@ -19,6 +20,9 @@ class OrderDatabase:
) )
self.orders[order_id] = order self.orders[order_id] = order
# Zeichne verkaufte Getränke für Prometheus-Statistiken auf
drink_metrics.record_drinks(drinks)
# WebSocket-Broadcast für neue Bestellung # WebSocket-Broadcast für neue Bestellung
await websocket_manager.broadcast_order_created(order.model_dump()) await websocket_manager.broadcast_order_created(order.model_dump())

View file

@ -1,13 +1,15 @@
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, APIRouter from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, APIRouter
from fastapi.responses import Response
from typing import List from typing import List
from models import CreateOrderRequest, Order, DrinkType, MateType from models import CreateOrderRequest, Order, DrinkType, MateType
from database import db from database import db
from websocket_manager import websocket_manager from websocket_manager import websocket_manager
from prometheus_metrics import drink_metrics, CONTENT_TYPE_LATEST
import json import json
app = FastAPI( app = FastAPI(
title="Tschunk Order API", title="Tschunk Order API",
description="Eine RESTful API für Tschunk-Bestellungen mit WebSocket-Unterstützung", description="Eine RESTful API für Tschunk-Bestellungen mit WebSocket-Unterstützung und Prometheus-Metriken",
version="1.0.0" version="1.0.0"
) )
@ -129,6 +131,28 @@ async def get_available_drinks():
} }
@api_router.get("/stats/drinks")
async def get_drink_statistics():
"""
Gibt die Statistiken der verkauften Getränke zurück.
"""
return drink_metrics.get_stats()
@app.get("/metrics")
async def get_prometheus_metrics():
"""
Prometheus-Metriken-Endpunkt.
Gibt die Metriken im Prometheus-Format zurück:
- tschunk_drinks_sold_total: Anzahl verkaufter Getränke nach Typ und Mate-Sorte
"""
return Response(
content=drink_metrics.get_metrics(),
media_type=CONTENT_TYPE_LATEST
)
# API Router zur App hinzufügen # API Router zur App hinzufügen
app.include_router(api_router) app.include_router(api_router)

View file

@ -0,0 +1,96 @@
from prometheus_client import Counter, generate_latest, CONTENT_TYPE_LATEST, CollectorRegistry
from typing import Dict, Tuple
import json
import os
from models import DrinkType, MateType
class DrinkSalesMetrics:
def __init__(self, stats_file: str = "drink_stats.json"):
self.stats_file = stats_file
self.stats: Dict[Tuple[str, str], int] = self._load_stats()
# Erstelle eine eigene Registry ohne Standard-Metriken
self.registry = CollectorRegistry()
# Prometheus Counter für verkaufte Getränke
self.drinks_sold_counter = Counter(
'tschunk_drinks_sold_total',
'Total number of drinks sold',
['drink_type', 'mate_type'],
registry=self.registry
)
# Initialisiere Counter mit gespeicherten Werten
self._initialize_counters()
def _load_stats(self) -> Dict[Tuple[str, str], int]:
"""Lädt die gespeicherten Statistiken aus der JSON-Datei."""
if os.path.exists(self.stats_file):
try:
with open(self.stats_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Konvertiere String-Tupel zurück zu Tuple
return {tuple(k.split('|')): v for k, v in data.items()}
except (json.JSONDecodeError, KeyError, ValueError) as e:
print(f"Fehler beim Laden der Statistiken: {e}")
return {}
return {}
def _save_stats(self):
"""Speichert die Statistiken in eine JSON-Datei."""
try:
# Konvertiere Tuple zu String für JSON-Serialisierung
data = {f"{k[0]}|{k[1]}": v for k, v in self.stats.items()}
with open(self.stats_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Fehler beim Speichern der Statistiken: {e}")
def _initialize_counters(self):
"""Initialisiert die Prometheus-Counter mit den gespeicherten Werten."""
for (drink_type, mate_type), count in self.stats.items():
# Setze den Counter auf den gespeicherten Wert
# Da Counter nur inkrementiert werden können, müssen wir das manuell machen
for _ in range(count):
self.drinks_sold_counter.labels(
drink_type=drink_type,
mate_type=mate_type
).inc()
def record_drinks(self, drinks):
"""Zeichnet verkaufte Getränke auf und aktualisiert die Statistiken."""
for drink in drinks:
drink_type = drink.drink_type.value
mate_type = drink.mate_type.value
quantity = drink.quantity
# Aktualisiere lokale Statistiken
key = (drink_type, mate_type)
self.stats[key] = self.stats.get(key, 0) + quantity
# Aktualisiere Prometheus-Counter
self.drinks_sold_counter.labels(
drink_type=drink_type,
mate_type=mate_type
).inc(quantity)
# Speichere Statistiken persistent
self._save_stats()
def get_stats(self) -> Dict[str, Dict[str, int]]:
"""Gibt die aktuellen Statistiken zurück."""
result = {}
for (drink_type, mate_type), count in self.stats.items():
if drink_type not in result:
result[drink_type] = {}
result[drink_type][mate_type] = count
return result
def get_metrics(self):
"""Gibt die Prometheus-Metriken zurück."""
return generate_latest(self.registry)
# Globale Instanz der Metriken
drink_metrics = DrinkSalesMetrics()

View file

@ -7,3 +7,4 @@ websockets>=12.0
pytest>=7.0.0 pytest>=7.0.0
pytest-asyncio>=0.23.0 pytest-asyncio>=0.23.0
httpx>=0.25.0 httpx>=0.25.0
prometheus-client>=0.19.0

View file

@ -17,6 +17,18 @@ server {
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Prometheus metrics endpoint - proxy to backend
location /metrics {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# API routes - proxy to backend # API routes - proxy to backend
location /api/ { location /api/ {
limit_req zone=api burst=20 nodelay; limit_req zone=api burst=20 nodelay;