Add request metrics database
This commit is contained in:
+78
-10
@@ -4,12 +4,19 @@ import os
|
||||
import requests
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
try:
|
||||
from recipe_service.metrics import initialize, record, request_context, usage_json
|
||||
except ModuleNotFoundError:
|
||||
from metrics import initialize, record, request_context, usage_json
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
||||
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
|
||||
PROXY_URL = "http://127.0.0.1:8768/gemini"
|
||||
|
||||
initialize()
|
||||
|
||||
|
||||
def settings() -> dict:
|
||||
try:
|
||||
@@ -35,17 +42,58 @@ def health():
|
||||
@app.post("/recipe")
|
||||
@app.post("/recipe/")
|
||||
def recipe():
|
||||
request_id, started_at, started_monotonic = request_context()
|
||||
image_mime = None
|
||||
image_bytes = None
|
||||
prompt_chars = None
|
||||
status_code = 500
|
||||
response_bytes = None
|
||||
usage = {}
|
||||
error = None
|
||||
if not authorized():
|
||||
return jsonify(error="unauthorized"), 401
|
||||
status_code = 401
|
||||
error = "unauthorized"
|
||||
response = jsonify(error=error)
|
||||
record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr,
|
||||
user_agent=request.user_agent.string, method=request.method, path=request.path,
|
||||
image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars,
|
||||
status_code=status_code, duration_ms=int((__import__('time').monotonic() - started_monotonic) * 1000),
|
||||
response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error)
|
||||
return response, status_code
|
||||
image = request.files.get("image")
|
||||
prompt = request.form.get("prompt")
|
||||
if image is None or not prompt:
|
||||
return jsonify(error="image and prompt are required"), 400
|
||||
status_code = 400
|
||||
error = "image and prompt are required"
|
||||
response = jsonify(error=error)
|
||||
record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr,
|
||||
user_agent=request.user_agent.string, method=request.method, path=request.path,
|
||||
status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()),
|
||||
usage_json=usage_json(usage), error=error)
|
||||
return response, status_code
|
||||
if image.mimetype not in ALLOWED_TYPES:
|
||||
return jsonify(error="unsupported image type"), 415
|
||||
status_code = 415
|
||||
error = "unsupported image type"
|
||||
response = jsonify(error=error)
|
||||
record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr,
|
||||
user_agent=request.user_agent.string, method=request.method, path=request.path,
|
||||
image_mime=image.mimetype, prompt_chars=len(prompt), status_code=status_code,
|
||||
duration_ms=0, response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error)
|
||||
return response, status_code
|
||||
image_data = image.read(MAX_IMAGE_BYTES + 1)
|
||||
if len(image_data) > MAX_IMAGE_BYTES:
|
||||
return jsonify(error="image is too large"), 413
|
||||
status_code = 413
|
||||
error = "image is too large"
|
||||
response = jsonify(error=error)
|
||||
record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr,
|
||||
user_agent=request.user_agent.string, method=request.method, path=request.path,
|
||||
image_mime=image.mimetype, image_bytes=len(image_data), prompt_chars=len(prompt),
|
||||
status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()),
|
||||
usage_json=usage_json(usage), error=error)
|
||||
return response, status_code
|
||||
image_mime = image.mimetype
|
||||
image_bytes = len(image_data)
|
||||
prompt_chars = len(prompt)
|
||||
try:
|
||||
config = settings()
|
||||
response = requests.post(
|
||||
@@ -55,15 +103,35 @@ def recipe():
|
||||
timeout=180,
|
||||
)
|
||||
except (requests.RequestException, RuntimeError) as exc:
|
||||
return jsonify(error=str(exc) if isinstance(exc, RuntimeError) else "Gemini unavailable"), 503
|
||||
error = str(exc) if isinstance(exc, RuntimeError) else "Gemini unavailable"
|
||||
status_code = 503
|
||||
response = jsonify(error=error)
|
||||
record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr,
|
||||
user_agent=request.user_agent.string, method=request.method, path=request.path,
|
||||
image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars,
|
||||
status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()),
|
||||
usage_json=usage_json(usage), error=error)
|
||||
return response, status_code
|
||||
if response.status_code != 200:
|
||||
try:
|
||||
detail = response.json().get("error", {}).get("message", "Gemini request failed")
|
||||
except ValueError:
|
||||
detail = "Gemini request failed"
|
||||
return jsonify(error=detail), 502
|
||||
error = detail
|
||||
status_code = 502
|
||||
response = jsonify(error=error)
|
||||
record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr,
|
||||
user_agent=request.user_agent.string, method=request.method, path=request.path,
|
||||
image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars,
|
||||
status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()),
|
||||
usage_json=usage_json(usage), error=error)
|
||||
return response, status_code
|
||||
data = response.json()
|
||||
return jsonify(
|
||||
text=data.get("text"),
|
||||
usage=data.get("usage", {}),
|
||||
)
|
||||
usage = data.get("usage", {})
|
||||
result = jsonify(text=data.get("text"), usage=usage)
|
||||
record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr,
|
||||
user_agent=request.user_agent.string, method=request.method, path=request.path,
|
||||
image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars,
|
||||
status_code=200, duration_ms=0, response_bytes=len(result.get_data()),
|
||||
usage_json=usage_json(usage), error=None)
|
||||
return result
|
||||
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
DEFAULT_DB_PATH = "/var/lib/recipe/metrics.sqlite3"
|
||||
|
||||
|
||||
def db_path() -> str:
|
||||
return os.environ.get("RECIPE_METRICS_DB", DEFAULT_DB_PATH)
|
||||
|
||||
|
||||
def initialize() -> None:
|
||||
path = db_path()
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with sqlite3.connect(path) as connection:
|
||||
connection.execute("PRAGMA journal_mode=WAL")
|
||||
connection.execute("""
|
||||
CREATE TABLE IF NOT EXISTS requests (
|
||||
request_id TEXT PRIMARY KEY,
|
||||
started_at TEXT NOT NULL,
|
||||
client_ip TEXT,
|
||||
user_agent TEXT,
|
||||
method TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
image_mime TEXT,
|
||||
image_bytes INTEGER,
|
||||
prompt_chars INTEGER,
|
||||
status_code INTEGER NOT NULL,
|
||||
duration_ms INTEGER NOT NULL,
|
||||
response_bytes INTEGER,
|
||||
usage_json TEXT,
|
||||
error TEXT
|
||||
)
|
||||
""")
|
||||
connection.execute("CREATE INDEX IF NOT EXISTS idx_requests_started_at ON requests(started_at)")
|
||||
connection.execute("CREATE INDEX IF NOT EXISTS idx_requests_status_code ON requests(status_code)")
|
||||
|
||||
|
||||
def record(**values) -> None:
|
||||
initialize()
|
||||
columns = [
|
||||
"request_id", "started_at", "client_ip", "user_agent", "method",
|
||||
"path", "image_mime", "image_bytes", "prompt_chars", "status_code",
|
||||
"duration_ms", "response_bytes", "usage_json", "error",
|
||||
]
|
||||
payload = [values.get(column) for column in columns]
|
||||
with sqlite3.connect(db_path()) as connection:
|
||||
connection.execute(
|
||||
f"INSERT INTO requests ({','.join(columns)}) VALUES ({','.join('?' for _ in columns)})",
|
||||
payload,
|
||||
)
|
||||
|
||||
|
||||
def request_context() -> tuple[str, str, float]:
|
||||
return str(uuid.uuid4()), time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), time.monotonic()
|
||||
|
||||
|
||||
def usage_json(usage: dict) -> str:
|
||||
return json.dumps(usage, ensure_ascii=True, separators=(",", ":"))
|
||||
@@ -17,6 +17,8 @@ NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
StateDirectory=recipe
|
||||
StateDirectoryMode=0770
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user