logging enhancements

This commit is contained in:
Piotr Dec 2025-10-22 22:53:49 +02:00
parent 2dec6d5384
commit 569aefeccb
Signed by: stawros
GPG key ID: 74B18A3F0F1E99C0
8 changed files with 88 additions and 20 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ uv.lock
__pycache__/ __pycache__/
**/dist/ **/dist/
**/*.log

View file

@ -6,6 +6,11 @@ from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
class LoggingConfig(BaseModel):
level: str = "INFO"
path: Path = Path("logs/karl.log")
class AppConfig(BaseModel): class AppConfig(BaseModel):
host: str = "127.0.0.1" host: str = "127.0.0.1"
port: int = 8081 port: int = 8081
@ -26,6 +31,7 @@ class KeePassConfig(BaseModel):
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="KARL_", env_nested_delimiter="__") model_config = SettingsConfigDict(env_prefix="KARL_", env_nested_delimiter="__")
logging: LoggingConfig = LoggingConfig()
app: AppConfig = AppConfig() app: AppConfig = AppConfig()
git: GitConfig = GitConfig() git: GitConfig = GitConfig()
kp: KeePassConfig = KeePassConfig() kp: KeePassConfig = KeePassConfig()

View file

@ -4,8 +4,7 @@ from fastapi import FastAPI
from injectable import load_injection_container from injectable import load_injection_container
from app.config import get_settings from app.config import get_settings
from app.core.core import WebhookProcessor from app.util.logging import HandlerFactory
from app.util.logging import LoggingHandler, ExternalLoggingHandler
class KarlApplication: class KarlApplication:
@ -13,6 +12,7 @@ class KarlApplication:
def __init__(self) -> None: def __init__(self) -> None:
self._set_logging() self._set_logging()
_app = FastAPI(title="Karl", version="0.1.0") _app = FastAPI(title="Karl", version="0.1.0")
self._set_middlewares(_app)
self._set_routes(_app) self._set_routes(_app)
self._set_events(_app) self._set_events(_app)
self._init_services() self._init_services()
@ -23,7 +23,12 @@ class KarlApplication:
await self._app.__call__(scope, receive, send) await self._app.__call__(scope, receive, send)
def _set_logging(self): def _set_logging(self):
logging.basicConfig(level=logging.INFO, handlers=[LoggingHandler()]) settings = get_settings()
logging.addLevelName(5, "TRACE")
logging.Logger.trace = lambda s, msg, *args, **kwargs: s.log(5, msg, *args, **kwargs)
logging.basicConfig(level=settings.logging.level,
handlers=HandlerFactory.create(HandlerFactory.Target.ALL, handler_prefix='karl.',
file_path=settings.logging.path))
loggers = ( loggers = (
"uvicorn", "uvicorn",
@ -33,12 +38,16 @@ class KarlApplication:
"asyncio", "asyncio",
"starlette", "starlette",
) )
external_handler = ExternalLoggingHandler() external_handlers = HandlerFactory.create(HandlerFactory.Target.ALL, file_path=settings.logging.path)
for logger_name in loggers: for logger_name in loggers:
logging_logger = logging.getLogger(logger_name) logging_logger = logging.getLogger(logger_name)
logging_logger.handlers = [external_handler] logging_logger.handlers = external_handlers
logging_logger.propagate = False logging_logger.propagate = False
def _set_middlewares(self, app: FastAPI):
from app.web.middlewares import LoggingMiddleware
app.add_middleware(LoggingMiddleware)
def _set_routes(self, app: FastAPI): def _set_routes(self, app: FastAPI):
from app.core.router import router as core_router from app.core.router import router as core_router
app.include_router(core_router) app.include_router(core_router)
@ -52,11 +61,9 @@ class KarlApplication:
def _init_services(self): def _init_services(self):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
load_injection_container() load_injection_container()
webhook_service = WebhookProcessor()
logger.info(webhook_service.health)
def app(): def run():
return KarlApplication() return KarlApplication()
@ -65,7 +72,7 @@ if __name__ == "__main__":
settings = get_settings() settings = get_settings()
uvicorn.run( uvicorn.run(
"app.main:app", "app.main:run",
factory=True, factory=True,
host=settings.app.host, host=settings.app.host,
port=settings.app.port, port=settings.app.port,

View file

@ -1,4 +1,8 @@
from logging import Formatter, StreamHandler from enum import Enum, auto
from logging import Formatter, StreamHandler, Handler
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
from typing import List
class NamingCache: class NamingCache:
@ -48,12 +52,35 @@ class ApplicationFormatter(Formatter):
return formatted return formatted
class LoggingHandler(StreamHandler): class HandlerFactory:
def __init__(self): class Target(Enum):
super().__init__() CONSOLE = auto()
self.setFormatter(ApplicationFormatter(handler_prefix='karl.')) FILE = auto()
ALL = auto()
class ExternalLoggingHandler(StreamHandler): @staticmethod
def __init__(self): def create(target: Target, handler_prefix: str = '', file_path: Path = None) -> List[Handler]:
super().__init__() def console_handler(prefix: str = ''):
self.setFormatter(ApplicationFormatter()) handler = StreamHandler()
handler.setFormatter(ApplicationFormatter(prefix))
handler.setLevel('INFO')
return handler
def file_handler(prefix: str = ''):
handler = TimedRotatingFileHandler(file_path, when='midnight', backupCount=30)
handler.setFormatter(ApplicationFormatter(prefix))
handler.setLevel('TRACE')
return handler
handlers = []
match target:
case HandlerFactory.Target.CONSOLE:
handlers.append(console_handler(handler_prefix))
case HandlerFactory.Target.FILE:
handlers.append(file_handler(handler_prefix))
case HandlerFactory.Target.ALL:
handlers.append(file_handler(handler_prefix))
handlers.append(console_handler(handler_prefix))
case _:
raise ValueError(f"Unknown target: {target}")
return handlers

0
app/web/__init__.py Normal file
View file

24
app/web/middlewares.py Normal file
View file

@ -0,0 +1,24 @@
import logging
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
logger = logging.getLogger(__name__)
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
client = f"{request.client.host}:{request.client.port}"
match request.method:
case "POST" | "PUT" | "DELETE" if request.headers.get("Content-Type") == "application/json":
body = await request.body()
logger.trace(f"Request from {client}: {body.decode()}")
case "GET":
logger.trace(f"Request from {client}")
case _:
logger.trace(f"Request from {client} (content-type:{request.headers.get("Content-Type")})")
response = await call_next(request)
logger.trace(f"Respone: {response.status_code} {type(response)}")
return response

View file

@ -1,3 +1,6 @@
logging:
level: "TRACE"
path: "logs/karl.log"
app: app:
host: "127.0.0.1" host: "127.0.0.1"
port: 8081 port: 8081

2
run.sh
View file

@ -1 +1 @@
uvicorn app.main:app --factory --reload uvicorn app.main:run --factory --reload