feat: support request-id header

This commit is contained in:
2026-03-08 02:12:46 +02:00
parent 437ae2c16c
commit a809a4b67f
18 changed files with 424 additions and 68 deletions

View File

@@ -14,6 +14,7 @@ from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.database.migrations import Migrations
from ahriman.core.log.log_loader import LogLoader
from ahriman.core.repository import Repository
from ahriman.core.spawn import Spawn
from ahriman.core.status import Client
@@ -124,6 +125,14 @@ def import_error(package: str, components: list[str], mocker: MockerFixture) ->
# generic fixtures
@pytest.fixture(autouse=True)
def _register_log_context() -> None:
"""
register log context variables and factory
"""
LogLoader.register_context()
@pytest.fixture
def aur_package_ahriman() -> AURPackage:
"""

View File

@@ -30,6 +30,13 @@ def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
assert ahriman_client._login_url().endswith("/api/v1/login")
def test_headers(ahriman_client: SyncAhrimanClient) -> None:
"""
must inject request id header
"""
assert "X-Request-ID" in ahriman_client.headers()
def test_on_session_creation(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must log in user on start

View File

@@ -94,6 +94,13 @@ def test_adapters() -> None:
assert all(adapter.max_retries == client.retry for adapter in adapters.values())
def test_headers() -> None:
"""
must return empty additional headers
"""
assert SyncHttpClient().headers() == {}
def test_make_request(mocker: MockerFixture) -> None:
"""
must make HTTP request
@@ -194,6 +201,20 @@ def test_make_request_session() -> None:
stream=None, auth=None, timeout=client.timeout)
def test_make_request_with_additional_headers(mocker: MockerFixture) -> None:
"""
must merge additional headers into request
"""
request_mock = mocker.patch("requests.Session.request")
mocker.patch("ahriman.core.http.sync_http_client.SyncHttpClient.headers", return_value={"X-Custom": "value"})
client = SyncHttpClient()
client.make_request("GET", "url")
request_mock.assert_called_once_with(
"GET", "url", params=None, data=None, headers={"X-Custom": "value"}, files=None, json=None,
stream=None, auth=None, timeout=client.timeout)
def test_on_session_creation() -> None:
"""
must do nothing on start

View File

@@ -1,8 +1,6 @@
import logging
import pytest
from pytest_mock import MockerFixture
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task
from ahriman.core.database import SQLite
@@ -30,59 +28,46 @@ def test_logger_name(database: SQLite, repo: Repo, task_ahriman: Task) -> None:
assert task_ahriman.logger_name == "ahriman.core.build_tools.task.Task"
def test_package_logger_set_reset(database: SQLite) -> None:
def test_in_context(database: SQLite) -> None:
"""
must set and reset package base attribute
must set and reset generic log context
"""
log_record_id = LogRecordId("base", "version")
with database.in_context("package_id", "42"):
record = logging.makeLogRecord({})
assert record.package_id == "42"
database._package_logger_set(log_record_id.package_base, log_record_id.version)
record = logging.makeLogRecord({})
assert record.package_id == log_record_id
assert not hasattr(record, "package_id")
def test_in_context_failed(database: SQLite) -> None:
"""
must reset context even if exception occurs
"""
with pytest.raises(ValueError):
with database.in_context("package_id", "42"):
raise ValueError()
database._package_logger_reset()
record = logging.makeLogRecord({})
with pytest.raises(AttributeError):
assert record.package_id
assert not hasattr(record, "package_id")
def test_in_package_context(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_in_package_context(database: SQLite, package_ahriman: Package) -> None:
"""
must set package log context
"""
set_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_set")
reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset")
with database.in_package_context(package_ahriman.base, package_ahriman.version):
pass
record = logging.makeLogRecord({})
assert record.package_id == LogRecordId(package_ahriman.base, package_ahriman.version)
set_mock.assert_called_once_with(package_ahriman.base, package_ahriman.version)
reset_mock.assert_called_once_with()
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_in_package_context_empty_version(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_in_package_context_empty_version(database: SQLite, package_ahriman: Package) -> None:
"""
must set package log context with empty version
"""
set_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_set")
reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset")
with database.in_package_context(package_ahriman.base, None):
pass
set_mock.assert_called_once_with(package_ahriman.base, None)
reset_mock.assert_called_once_with()
def test_in_package_context_failed(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must reset package context even if exception occurs
"""
mocker.patch("ahriman.core.log.LazyLogging._package_logger_set")
reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset")
with pytest.raises(ValueError):
with database.in_package_context(package_ahriman.base, ""):
raise ValueError()
reset_mock.assert_called_once_with()
record = logging.makeLogRecord({})
assert record.package_id == LogRecordId(package_ahriman.base, "<unknown>")

View File

@@ -0,0 +1,75 @@
import logging
from ahriman.core.log.log_context import LogContext
def test_get() -> None:
"""
must get context variable value
"""
token = LogContext.set("package_id", "value")
assert LogContext.get("package_id") == "value"
LogContext.reset("package_id", token)
def test_get_empty() -> None:
"""
must return None when context variable is unknown or not set
"""
assert LogContext.get("package_id") is None
assert LogContext.get("random") is None
def test_log_record_factory() -> None:
"""
must inject all registered context variables into log records
"""
package_token = LogContext.set("package_id", "package")
record = logging.makeLogRecord({})
assert record.package_id == "package"
LogContext.reset("package_id", package_token)
def test_log_record_factory_empty() -> None:
"""
must not inject context variable when value is None
"""
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_register() -> None:
"""
must register a context variable
"""
variable = LogContext.register("random")
assert "random" in LogContext._context
assert LogContext._context["random"] is variable
del LogContext._context["random"]
def test_reset() -> None:
"""
must reset context variable so it is no longer injected
"""
token = LogContext.set("package_id", "value")
LogContext.reset("package_id", token)
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_set() -> None:
"""
must set context variable and inject it into log records
"""
token = LogContext.set("package_id", "value")
record = logging.makeLogRecord({})
assert record.package_id == "value"
LogContext.reset("package_id", token)

View File

@@ -7,6 +7,7 @@ from pytest_mock import MockerFixture
from systemd.journal import JournalHandler
from ahriman.core.configuration import Configuration
from ahriman.core.log.log_context import LogContext
from ahriman.core.log.log_loader import LogLoader
from ahriman.models.log_handler import LogHandler
@@ -75,3 +76,13 @@ def test_load_quiet(configuration: Configuration, mocker: MockerFixture) -> None
_, repository_id = configuration.check_loaded()
LogLoader.load(repository_id, configuration, LogHandler.Journald, quiet=True, report=False)
disable_mock.assert_called_once_with(logging.WARNING)
def test_register_context() -> None:
"""
must register predefined context variables and install log record factory
"""
LogLoader.register_context()
assert "package_id" in LogContext._context
assert "request_id" in LogContext._context
assert logging.getLogRecordFactory().__func__ is LogContext.log_record_factory.__func__

View File

@@ -0,0 +1,43 @@
import logging
import pytest
from unittest.mock import AsyncMock, MagicMock
from typing import Any
from ahriman.web.middlewares.request_id_handler import request_id_handler
async def test_request_id_handler() -> None:
"""
must use request id from request if available
"""
request = pytest.helpers.request("", "", "")
request.headers = MagicMock()
request.headers.getone.return_value = "request_id"
response = MagicMock()
response.headers = {}
async def check_handler(_: Any) -> MagicMock:
record = logging.makeLogRecord({})
assert record.request_id == "request_id"
return response
handler = request_id_handler()
await handler(request, check_handler)
assert response.headers["X-Request-ID"] == "request_id"
async def test_request_id_handler_generate() -> None:
"""
must generate request id and set it in response header
"""
request = pytest.helpers.request("", "", "")
response = MagicMock()
response.headers = {}
request_handler = AsyncMock(return_value=response)
handler = request_id_handler()
await handler(request, request_handler)
assert "X-Request-ID" in response.headers