Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions tests/test_fastapi_quart_extractors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Request info extraction for the fastapi and quart backends (issue #137)."""
from types import SimpleNamespace

import pytest
from helpers.imports import undo_imports_from_package


@pytest.fixture
def fresh_json_logging():
import json_logging

yield json_logging
undo_imports_from_package("json_logging")


class _BrokenHeaders:
"""Headers whose membership test fails, like a request that is already closed."""

def __contains__(self, item):
raise RuntimeError("headers unavailable")


def test_fastapi_init_request_instrument_rejects_non_fastapi_app(fresh_json_logging):
fresh_json_logging.init_fastapi()

with pytest.raises(RuntimeError, match="not a valid fastapi"):
fresh_json_logging.init_request_instrument(object())


def test_fastapi_protocol_is_built_from_scope(fresh_json_logging):
from json_logging.framework.fastapi.implementation import FastAPIRequestInfoExtractor

extractor = FastAPIRequestInfoExtractor()
http = SimpleNamespace(scope={"type": "http", "http_version": "1.1"})

assert extractor.get_protocol(http) == "HTTP/1.1"


@pytest.mark.parametrize("scope", [{"type": "http"}, {"type": "websocket", "http_version": "1.1"}, {}])
def test_fastapi_protocol_falls_back_to_empty_value(fresh_json_logging, scope):
from json_logging.framework.fastapi.implementation import FastAPIRequestInfoExtractor

extractor = FastAPIRequestInfoExtractor()

assert extractor.get_protocol(SimpleNamespace(scope=scope)) == fresh_json_logging.EMPTY_VALUE


def test_fastapi_header_lookup_falls_back_to_default(fresh_json_logging):
from json_logging.framework.fastapi.implementation import FastAPIRequestInfoExtractor

extractor = FastAPIRequestInfoExtractor()

assert extractor.get_http_header(SimpleNamespace(headers={"X-Present": "yes"}), "X-Present") == "yes"
assert extractor.get_http_header(SimpleNamespace(headers={}), "X-Missing", "fallback") == "fallback"
assert extractor.get_http_header(SimpleNamespace(headers=_BrokenHeaders()), "X", "fallback") == "fallback"


def test_quart_remote_user(fresh_json_logging):
from json_logging.framework.quart import QuartRequestInfoExtractor

extractor = QuartRequestInfoExtractor()

assert extractor.get_remote_user(SimpleNamespace(authorization=None)) == fresh_json_logging.EMPTY_VALUE
assert extractor.get_remote_user(SimpleNamespace(authorization=SimpleNamespace(username="alice"))) == "alice"


def test_quart_header_lookup_falls_back_to_default(fresh_json_logging):
from json_logging.framework.quart import QuartRequestInfoExtractor

extractor = QuartRequestInfoExtractor()

assert extractor.get_http_header(SimpleNamespace(headers={"X-Present": "yes"}), "X-Present") == "yes"
assert extractor.get_http_header(SimpleNamespace(headers={}), "X-Missing", "fallback") == "fallback"
assert extractor.get_http_header(SimpleNamespace(headers=_BrokenHeaders()), "X", "fallback") == "fallback"


def test_quart_correlation_id_outside_request_context_is_none(fresh_json_logging):
from json_logging.framework.quart import QuartRequestInfoExtractor

extractor = QuartRequestInfoExtractor()

extractor.set_correlation_id(None, "ignored") # no app context: must not raise
assert extractor.get_correlation_id_in_request_context(None) is None


def test_quart_request_fields_are_read_from_the_request(fresh_json_logging):
from json_logging.framework.quart import QuartRequestInfoExtractor

extractor = QuartRequestInfoExtractor()
request = SimpleNamespace(
scheme="https", path="/hello", content_length=12, method="POST", remote_addr="10.0.0.1", host="example.com:8443")

assert extractor.get_protocol(request) == "https"
assert extractor.get_path(request) == "/hello"
assert extractor.get_content_length(request) == 12
assert extractor.get_method(request) == "POST"
assert extractor.get_remote_ip(request) == "10.0.0.1"
assert extractor.get_remote_port(request) == "8443"
assert extractor.get_remote_port(SimpleNamespace(host="example.com")) is None
11 changes: 11 additions & 0 deletions tests/test_init_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,14 @@ def fail(*args, **kwargs):

assert len(errors) == 1
assert "No logging handlers found" in errors[0]


def test_config_root_logger_switches_root_handlers_to_json(fresh_json_logging, monkeypatch):
handler = logging.StreamHandler()
monkeypatch.setattr(logging.root, "handlers", [handler])
fresh_json_logging.init_non_web(enable_json=True)
handler.setFormatter(logging.Formatter()) # undo what init_non_web already did to the handler

fresh_json_logging.config_root_logger()

assert isinstance(handler.formatter, fresh_json_logging.JSONLogFormatter)
75 changes: 75 additions & 0 deletions tests/test_request_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Call-stack request discovery and cached exception text (issue #137)."""
import logging

import pytest
from helpers.imports import undo_imports_from_package


class FakeRequest:
"""Stand-in for a framework request class."""


@pytest.fixture
def request_util():
from json_logging import util
from json_logging.framework_base import BaseRequestInfoExtractor, BaseResponseInfoExtractor

class FakeRequestInfoExtractor(BaseRequestInfoExtractor):
@staticmethod
def get_request_class_type():
return FakeRequest

@staticmethod
def support_global_request_object():
return False

yield util.RequestUtil(
request_info_extractor_class=FakeRequestInfoExtractor,
response_info_extractor_class=BaseResponseInfoExtractor,
)
undo_imports_from_package("json_logging")


def test_request_is_found_in_a_local_named_request(request_util):
request = FakeRequest()

assert request_util.get_request_from_call_stack() is request


def test_request_is_found_in_a_local_named_req(request_util):
req = FakeRequest()

assert request_util.get_request_from_call_stack() is req


def test_request_is_found_in_a_local_with_any_name(request_util):
something_else = FakeRequest()

assert request_util.get_request_from_call_stack() is something_else


def test_request_is_found_in_a_calling_frame(request_util):
def inner():
return request_util.get_request_from_call_stack()

request = FakeRequest()

assert inner() is request


def test_request_of_the_wrong_type_is_ignored(request_util):
request = object() # noqa: F841 - a "request" local that is not a request

assert request_util.get_request_from_call_stack() is None


def test_exception_text_is_used_when_there_is_no_exc_info():
from json_logging.formatters import JSONLogFormatter

record = logging.LogRecord("exc-text-test", logging.ERROR, __file__, 1, "boom", None, None)
record.exc_text = "cached traceback"

fields = JSONLogFormatter().get_exc_fields(record)

assert fields["exc_info"] == "cached traceback"
assert fields["filename"] == record.filename
Loading