feat(R7): metrics + JSON logs + security headers
deps-checksum / checksum lockfile (push) Canceled after 0s
CI / pytest (3.12) (push) Canceled after 0s
CI / Docker smoke test (push) Canceled after 0s

This commit is contained in:
truetype committed 2026-08-31 16:24:51 +02:00
1 parent 12d2018ac5
commit 3681fd95eb
9 files changed
+276 -3

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
"""Test per R7 (osservabilità): endpoint /metrics e logging strutturato."""
import importlib
import json
import pytest
import config
from bookish import create_app
@pytest.fixture()
def app(monkeypatch):
monkeypatch.setenv("BOOKISH_DEBUG", "true")
monkeypatch.setenv("BOOKISH_SECRET", "test-secret")
monkeypatch.setenv("BOOKISH_RATE_LIMIT_UPLOAD", "0")
importlib.reload(config)
app = create_app()
app.config["TESTING"] = True
yield app
def test_metrics_endpoint_returns_prometheus_text(app):
client = app.test_client()
# qualche richiesta per popolare i contatori
client.get("/")
client.get("/healthz")
r = client.get("/metrics")
assert r.status_code == 200
body = r.get_data(as_text=True)
assert "text/plain" in r.headers.get("Content-Type", "")
# formato Prometheus
assert body.startswith("# HELP")
assert "bookish_uptime_seconds" in body
assert "bookish_active_tasks " in body
# la richiesta a / metrics stessa non deve generare loop, ma dev'esserci
# almeno la richiesta HTTP totale registrata (route /)
assert 'bookish_http_requests_total{method="GET",route="/"}' in body
assert "bookish_http_request_duration_seconds_count" in body
def test_http_requests_are_counted_by_route_and_status(app):
from bookish.services import observability as obs
obs._http.clear()
client = app.test_client()
client.get("/") # 200
client.get("/nope") # 404
r = client.get("/metrics")
body = r.get_data(as_text=True)
# 404 sulla route <unknown> (nessun url_rule) e 200 su /
assert 'status="200"' in body
assert 'status="404"' in body
def test_conversion_observe_and_active_tasks(app):
from bookish.services import observability as obs
obs._conversions.clear()
obs.track_task(1)
obs.conversion_observe("completed", 1.5)
obs.conversion_observe("failed", 0.2)
obs.track_task(-1)
client = app.test_client()
r = client.get("/metrics")
body = r.get_data(as_text=True)
assert 'bookish_conversions_total{result="completed"} 1' in body
assert 'bookish_conversions_total{result="failed"} 1' in body
assert 'bookish_conversion_duration_seconds_sum{result="completed"} 1.500000' in body
assert "bookish_active_tasks 0" in body
def test_json_log_formatter(monkeypatch, capsys):
monkeypatch.setenv("LOG_FORMAT", "json")
from bookish.services import observability as obs
obs.configure_logging(json_output=True)
import logging
log = logging.getLogger("test.obs")
log.info("prova %s", "oggetto", extra={"task_id": "abc123"})
# il root handler scrive su stderr
out = capsys.readouterr().err
line = [l for l in out.splitlines() if "prova" in l][-1]
payload = json.loads(line)
assert payload["message"] == 'prova oggetto'
assert payload["level"] == "INFO"
assert payload["task_id"] == "abc123"
assert "logger" in payload and payload["logger"] == "test.obs"
def test_plain_log_format(monkeypatch, capsys):
# default: formato testo
import importlib as il
import config
monkeypatch.delenv("LOG_FORMAT", raising=False)
il.reload(config)
from bookish.services import observability as obs
obs.configure_logging(json_output=False)
import logging
logging.getLogger("my.obs2").info("plain-line")
out = capsys.readouterr().err
assert "plain-line" in out