voltwire-fastapi-logging 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voltwire/fastapi/logging/__init__.py +35 -0
- voltwire/fastapi/logging/_constants.py +11 -0
- voltwire/fastapi/logging/base.py +81 -0
- voltwire/fastapi/logging/middleware.py +55 -0
- voltwire_fastapi_logging-0.0.1.dist-info/METADATA +40 -0
- voltwire_fastapi_logging-0.0.1.dist-info/RECORD +7 -0
- voltwire_fastapi_logging-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
__version__ = "0.0.0"
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from voltwire.fastapi.logging._constants import log_context
|
|
8
|
+
from voltwire.fastapi.logging.middleware import ContextEnricher
|
|
9
|
+
|
|
10
|
+
__all__ = ["ContextEnricher", "log_context", "integration_logger", "log_banner", "IntegrationLogContext", "AppRuntimeSettings"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IntegrationLogContext(BaseModel):
|
|
14
|
+
integration: str
|
|
15
|
+
message: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AppRuntimeSettings(Protocol):
|
|
19
|
+
app_name: str
|
|
20
|
+
version: str
|
|
21
|
+
environment: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def log_banner(settings: AppRuntimeSettings):
|
|
25
|
+
print(
|
|
26
|
+
f"Application: '{settings.app_name}' Version: '{settings.version}' Environment: '{settings.environment}'",
|
|
27
|
+
flush=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def integration_logger(context: IntegrationLogContext, **extra: Any):
|
|
32
|
+
from loguru import logger
|
|
33
|
+
|
|
34
|
+
context_dict = {**context.model_dump(), **extra}
|
|
35
|
+
return logger.contextualize(**context_dict)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
APP_LOG_FORMAT = (
|
|
6
|
+
"<level>{level}</level>: <green>{time:YYYY-MM-DDTHH:mm:s:SSS!UTC}</green> "
|
|
7
|
+
"| {process}:{thread} | {name}:{line} - {message} | {extra}"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
def log_context(**extra: Any):
|
|
11
|
+
return logger.contextualize(**extra)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from voltwire.fastapi.logging._constants import APP_LOG_FORMAT
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LogSettings(Protocol):
|
|
12
|
+
level: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DefaultLogSettings:
|
|
17
|
+
level: str = "INFO"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Loggers known to attach their own handlers directly (bypassing propagation
|
|
21
|
+
# to root) rather than relying on the standard logging tree.
|
|
22
|
+
_DIRECTLY_HANDLED_LOGGERS = (
|
|
23
|
+
"uvicorn",
|
|
24
|
+
"uvicorn.access",
|
|
25
|
+
"uvicorn.error",
|
|
26
|
+
"gunicorn",
|
|
27
|
+
"gunicorn.access",
|
|
28
|
+
"gunicorn.error",
|
|
29
|
+
"sqlalchemy",
|
|
30
|
+
"sqlalchemy.engine",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InterceptHandler(logging.Handler):
|
|
35
|
+
"""Forwards stdlib `logging` records into loguru.
|
|
36
|
+
|
|
37
|
+
Installed on the root logger so any code using plain
|
|
38
|
+
`logging.getLogger(__name__)` (this codebase, other voltwire
|
|
39
|
+
packages, or third-party dependencies) is routed through loguru
|
|
40
|
+
without those callers needing to know loguru exists.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
44
|
+
try:
|
|
45
|
+
level = logger.level(record.levelname).name
|
|
46
|
+
except ValueError:
|
|
47
|
+
level = record.levelno
|
|
48
|
+
|
|
49
|
+
frame, depth = logging.currentframe(), 0
|
|
50
|
+
while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
|
|
51
|
+
frame = frame.f_back
|
|
52
|
+
depth += 1
|
|
53
|
+
|
|
54
|
+
logger.opt(depth=depth, exception=record.exc_info).log(
|
|
55
|
+
level, record.getMessage()
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def setup_logging(settings: LogSettings):
|
|
60
|
+
|
|
61
|
+
# ensure all python and loguru loggers are clean
|
|
62
|
+
logger.remove()
|
|
63
|
+
root = logging.getLogger()
|
|
64
|
+
for handler in root.handlers[:]:
|
|
65
|
+
root.removeHandler(handler)
|
|
66
|
+
|
|
67
|
+
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
|
68
|
+
|
|
69
|
+
# some libraries attach handlers directly to their own named logger
|
|
70
|
+
# instead of propagating to root - strip those so they route through
|
|
71
|
+
# the intercept handler too.
|
|
72
|
+
for name in _DIRECTLY_HANDLED_LOGGERS:
|
|
73
|
+
named_logger = logging.getLogger(name)
|
|
74
|
+
named_logger.handlers = []
|
|
75
|
+
named_logger.propagate = True
|
|
76
|
+
|
|
77
|
+
logger.add(
|
|
78
|
+
sys.stdout,
|
|
79
|
+
format=APP_LOG_FORMAT,
|
|
80
|
+
level=settings.level,
|
|
81
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Protocol, Callable, Awaitable, Any
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
|
|
8
|
+
from voltwire.fastapi.logging._constants import log_context
|
|
9
|
+
|
|
10
|
+
ContextEnricher = Callable[[Request, dict[str, Any]], Awaitable[None]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LoggingMiddlewareSettings(Protocol):
|
|
14
|
+
context_enrichers: list[ContextEnricher]
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DefaultLoggingMiddlewareSettings:
|
|
18
|
+
context_enrichers: list[ContextEnricher] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
class LoggingContextMiddleware(BaseHTTPMiddleware):
|
|
21
|
+
"""
|
|
22
|
+
This middleware enriches every log entry with request context.
|
|
23
|
+
- Adds method and path to all logs within the request scope
|
|
24
|
+
- Executes custom context enrichers to add additional context
|
|
25
|
+
|
|
26
|
+
Custom context enrichers can be registered via settings:
|
|
27
|
+
async def custom_enricher(request: Request, context: dict[str, Any]) -> None:
|
|
28
|
+
context["custom_field"] = "custom_value"
|
|
29
|
+
|
|
30
|
+
settings = DefaultLoggingMiddlewareSettings(
|
|
31
|
+
context_enrichers=[custom_enricher]
|
|
32
|
+
)
|
|
33
|
+
app.add_middleware(LoggingContextMiddleware, settings=settings)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, app, settings: LoggingMiddlewareSettings):
|
|
37
|
+
super().__init__(app)
|
|
38
|
+
self.settings = settings
|
|
39
|
+
|
|
40
|
+
async def dispatch(self, request: Request, call_next):
|
|
41
|
+
request_context = {"method": request.method, "path": request.url.path}
|
|
42
|
+
|
|
43
|
+
# Execute custom context enrichers
|
|
44
|
+
for enricher in self.settings.context_enrichers:
|
|
45
|
+
try:
|
|
46
|
+
await enricher(request, request_context)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning(f"Context enricher {enricher.__name__} failed: {str(e)}")
|
|
49
|
+
|
|
50
|
+
# Process the request within loguru context
|
|
51
|
+
with log_context(**request_context):
|
|
52
|
+
logger.trace(f"Request started: {request.method} {request.url.path}")
|
|
53
|
+
response = await call_next(request)
|
|
54
|
+
logger.trace(f"Request completed: {request.method} {request.url.path} - {response.status_code}")
|
|
55
|
+
return response
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: voltwire-fastapi-logging
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Logging utilities for FastAPI apps
|
|
5
|
+
Author-email: Hermann Steidel <hsteidel.software@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: <4.0,>=3.13
|
|
8
|
+
Requires-Dist: loguru>=0.7.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
Requires-Dist: starlette>=0.27.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
<img src="https://raw.githubusercontent.com/hsteidel/voltwire/main/assets/icons/fastapi-logging.svg" alt="" width="56" height="56" align="left">
|
|
14
|
+
|
|
15
|
+
# voltwire-fastapi-logging
|
|
16
|
+
|
|
17
|
+
Logging utilities for FastAPI apps: request-context-enriching middleware.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install voltwire-fastapi-logging
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from voltwire.fastapi.logging import setup_logging, DefaultLogSettings
|
|
29
|
+
|
|
30
|
+
setup_logging(DefaultLogSettings(enable_json=False, level="INFO"))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Add `LoggingContextMiddleware` to enrich every log entry within a request with a correlation
|
|
34
|
+
ID, method, and path:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from voltwire.fastapi.logging import LoggingContextMiddleware, DefaultLoggingMiddlewareSettings
|
|
38
|
+
|
|
39
|
+
app.add_middleware(LoggingContextMiddleware, settings=DefaultLoggingMiddlewareSettings())
|
|
40
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
voltwire/fastapi/logging/__init__.py,sha256=gHFrqH3OIg4-YueYIWOfv4yGJcsUwEYUc5WPfn2AjZA,920
|
|
2
|
+
voltwire/fastapi/logging/_constants.py,sha256=Ntzvv3fLW9ZsYv_yB4uHJXGSOg0ivGbTcXBN5_Dg5SU,290
|
|
3
|
+
voltwire/fastapi/logging/base.py,sha256=cKqyGid75Wek7zRaPyKMvw_8KIc49gY62LcMFgSdB9I,2212
|
|
4
|
+
voltwire/fastapi/logging/middleware.py,sha256=7k1aT52aTtRuVkpyG-DJPtjh-0rOZZKvwgwLBlfNG0E,2163
|
|
5
|
+
voltwire_fastapi_logging-0.0.1.dist-info/METADATA,sha256=lrjFI89ITQcxZHMZfPAcfePOvZo_3AUYsWx3zmw6Dtw,1161
|
|
6
|
+
voltwire_fastapi_logging-0.0.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
7
|
+
voltwire_fastapi_logging-0.0.1.dist-info/RECORD,,
|