feat(R7): metrics + JSON logs + security headers
This commit is contained in:
1 parent
12d2018ac5
commit
3681fd95eb
9 files changed
+276
-3
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user