diff --git a/test-requirements.txt b/test-requirements.txt index d118997..a41a729 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ flask connexion[flask,swagger-ui] quart sanic +sanic-testing fastapi uvicorn requests diff --git a/tests/test_quart.py b/tests/test_quart.py new file mode 100644 index 0000000..16cd959 --- /dev/null +++ b/tests/test_quart.py @@ -0,0 +1,108 @@ +"""Test suite for the quart backend (issue #137).""" +import asyncio +import json +import logging + +import pytest +import quart +from helpers.handler import FormattedMessageCollectorHandler +from helpers.imports import undo_imports_from_package + +LOGGER_NAME = "quart-test" +REQUEST_LOGGER_NAME = "quart-request-logger" + + +@pytest.fixture +def app_and_handlers(): + import json_logging + from json_logging.formatters import JSONRequestLogFormatter + + app = quart.Quart(__name__) + + logger = logging.getLogger(LOGGER_NAME) + logger.setLevel(logging.DEBUG) + log_handler = FormattedMessageCollectorHandler() + logger.addHandler(log_handler) + + request_logger = logging.getLogger(REQUEST_LOGGER_NAME) + request_handler = FormattedMessageCollectorHandler() + request_handler.setFormatter(JSONRequestLogFormatter()) + + json_logging.init_quart(enable_json=True) + json_logging.init_request_instrument(app, exclude_url_patterns=["^/no-request-instrumentation"]) + request_logger.addHandler(request_handler) + + @app.route("/log") + async def log(): + logger.info("info message") + return {} + + @app.route("/get-correlation-id") + async def get_correlation_id(): + return {"correlation_id": json_logging.get_correlation_id()} + + @app.route("/no-request-instrumentation") + async def excluded(): + return {} + + yield app, log_handler, request_handler + + logger.removeHandler(log_handler) + request_logger.removeHandler(request_handler) + undo_imports_from_package("json_logging") + + +def _get(app, path, **kwargs): + async def run(): + return await app.test_client().get(path, **kwargs) + + return asyncio.run(run()) + + +def test_log_inside_request_has_given_correlation_id(app_and_handlers): + app, log_handler, _ = app_and_handlers + + response = _get(app, "/log", headers={"X-Correlation-Id": "abc-def"}) + + assert response.status_code == 200 + msg = json.loads(log_handler.messages[0]) + assert msg["msg"] == "info message" + assert msg["correlation_id"] == "abc-def" + + +def test_get_correlation_id_returns_given_header(app_and_handlers): + app, _, _ = app_and_handlers + + response = _get(app, "/get-correlation-id", headers={"X-Correlation-Id": "abc-def"}) + + assert asyncio.run(response.get_json()) == {"correlation_id": "abc-def"} + + +def test_request_instrumentation_writes_request_log(app_and_handlers): + app, _, request_handler = app_and_handlers + + response = _get(app, "/log") + + assert response.status_code == 200 + assert len(request_handler.messages) == 1 + msg = json.loads(request_handler.messages[0]) + assert msg["method"] == "GET" + assert msg["request"] == "/log" + assert msg["response_status"] == 200 + assert msg["type"] == "request" + + +def test_excluded_url_is_not_request_logged(app_and_handlers): + app, _, request_handler = app_and_handlers + + response = _get(app, "/no-request-instrumentation") + + assert response.status_code == 200 + assert request_handler.messages == [] + + +def test_init_request_instrument_rejects_non_quart_app(app_and_handlers): + import json_logging + + with pytest.raises(RuntimeError, match="not a valid quart"): + json_logging.init_request_instrument(object()) diff --git a/tests/test_sanic.py b/tests/test_sanic.py new file mode 100644 index 0000000..185e4e0 --- /dev/null +++ b/tests/test_sanic.py @@ -0,0 +1,99 @@ +"""Test suite for the sanic backend (issue #137).""" +import json +import logging + +import pytest +import sanic +import sanic.response +from helpers.handler import FormattedMessageCollectorHandler +from helpers.imports import undo_imports_from_package + +LOGGER_NAME = "sanic-test" +REQUEST_LOGGER_NAME = "sanic-request" + + +@pytest.fixture +def app_and_handlers(): + import json_logging + from json_logging.formatters import JSONRequestLogFormatter + + app = sanic.Sanic("json-logging-sanic-test") + + logger = logging.getLogger(LOGGER_NAME) + logger.setLevel(logging.DEBUG) + log_handler = FormattedMessageCollectorHandler() + logger.addHandler(log_handler) + + request_logger = logging.getLogger(REQUEST_LOGGER_NAME) + request_handler = FormattedMessageCollectorHandler() + request_handler.setFormatter(JSONRequestLogFormatter()) + + json_logging.init_sanic(enable_json=True) + json_logging.init_request_instrument(app, exclude_url_patterns=["^/no-request-instrumentation"]) + request_logger.addHandler(request_handler) + + @app.route("/log") + async def log(request): + logger.info("info message") + return sanic.response.json({}) + + @app.route("/get-correlation-id") + async def get_correlation_id(request): + return sanic.response.json({"correlation_id": json_logging.get_correlation_id(request)}) + + @app.route("/no-request-instrumentation") + async def excluded(request): + return sanic.response.json({}) + + yield app, log_handler, request_handler + + logger.removeHandler(log_handler) + request_logger.removeHandler(request_handler) + undo_imports_from_package("json_logging") + sanic.Sanic._app_registry.pop("json-logging-sanic-test", None) + + +def test_get_correlation_id_returns_given_header(app_and_handlers): + app, _, _ = app_and_handlers + + _, response = app.test_client.get("/get-correlation-id", headers={"X-Correlation-Id": "abc-def"}) + + assert response.json == {"correlation_id": "abc-def"} + + +def test_get_correlation_id_is_generated_when_missing(app_and_handlers): + app, _, _ = app_and_handlers + + _, response = app.test_client.get("/get-correlation-id") + + assert response.json["correlation_id"] not in (None, "-", "") + + +def test_request_instrumentation_writes_request_log(app_and_handlers): + app, _, request_handler = app_and_handlers + + _, response = app.test_client.get("/log") + + assert response.status == 200 + assert len(request_handler.messages) == 1 + msg = json.loads(request_handler.messages[0]) + assert msg["method"] == "GET" + assert msg["request"] == "/log" + assert msg["response_status"] == 200 + assert msg["type"] == "request" + + +def test_excluded_url_is_not_request_logged(app_and_handlers): + app, _, request_handler = app_and_handlers + + _, response = app.test_client.get("/no-request-instrumentation") + + assert response.status == 200 + assert request_handler.messages == [] + + +def test_init_request_instrument_rejects_non_sanic_app(app_and_handlers): + import json_logging + + with pytest.raises(RuntimeError, match="not a valid Sanic"): + json_logging.init_request_instrument(object())