chore: route uvicorn logs to structlog; disable default access logs
This commit is contained in:
@@ -31,4 +31,4 @@ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log", "--log-config", "app/uvicorn_log_config.json"]
|
||||
Binary file not shown.
@@ -9,6 +9,7 @@ included automatically in log records.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
from typing import Any, Dict
|
||||
|
||||
import structlog
|
||||
@@ -29,29 +30,93 @@ def _add_required_defaults(_: Any, __: str, event_dict: Dict[str, Any]) -> Dict[
|
||||
return event_dict
|
||||
|
||||
|
||||
def _build_foreign_pre_chain() -> list:
|
||||
"""Common processor chain used for stdlib log bridging."""
|
||||
return [
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.add_log_level,
|
||||
_add_required_defaults,
|
||||
structlog.processors.TimeStamper(fmt="iso", key="timestamp"),
|
||||
structlog.processors.dict_tracebacks,
|
||||
]
|
||||
|
||||
|
||||
def _ensure_stdlib_processor_logging(log_level: int) -> None:
|
||||
"""Route stdlib (and uvicorn) loggers through structlog's ProcessorFormatter.
|
||||
|
||||
This ensures uvicorn error logs and any stdlib logs are emitted as JSON,
|
||||
matching the structlog output used by the application code.
|
||||
"""
|
||||
formatter = structlog.stdlib.ProcessorFormatter(
|
||||
processor=structlog.processors.JSONRenderer(),
|
||||
foreign_pre_chain=_build_foreign_pre_chain(),
|
||||
)
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# Root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.handlers = [handler]
|
||||
root_logger.setLevel(log_level)
|
||||
|
||||
# Uvicorn loggers (error/access/parent)
|
||||
for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
|
||||
uv_logger = logging.getLogger(name)
|
||||
uv_logger.handlers = [handler]
|
||||
uv_logger.propagate = False
|
||||
uv_logger.setLevel(log_level)
|
||||
|
||||
|
||||
def build_uvicorn_structlog_formatter() -> logging.Formatter:
|
||||
"""
|
||||
Factory used by logging.config.dictConfig to create a ProcessorFormatter
|
||||
that renders JSON and merges contextvars. This is referenced by
|
||||
app/uvicorn_log_config.json.
|
||||
"""
|
||||
return structlog.stdlib.ProcessorFormatter(
|
||||
processor=structlog.processors.JSONRenderer(),
|
||||
foreign_pre_chain=_build_foreign_pre_chain(),
|
||||
)
|
||||
|
||||
|
||||
def setup_logging(log_level: int = logging.INFO) -> None:
|
||||
"""
|
||||
Configure structlog for JSON logging with contextvars support.
|
||||
Configure structlog for JSON logging with contextvars support and bridge
|
||||
stdlib/uvicorn loggers to JSON output.
|
||||
|
||||
Args:
|
||||
log_level: Minimum log level for application logs.
|
||||
"""
|
||||
# Configure stdlib logging basic config for third-party libs (uvicorn, etc.)
|
||||
# Do not clobber handlers if already configured by a log-config (e.g., uvicorn --log-config)
|
||||
# basicConfig is a no-op if handlers exist.
|
||||
logging.basicConfig(level=log_level)
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.add_log_level,
|
||||
_add_required_defaults,
|
||||
structlog.processors.TimeStamper(fmt="iso", key="timestamp"),
|
||||
structlog.processors.dict_tracebacks,
|
||||
structlog.processors.JSONRenderer(),
|
||||
# Defer rendering to logging's ProcessorFormatter to avoid double JSON
|
||||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||||
],
|
||||
context_class=dict,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
wrapper_class=structlog.make_filtering_bound_logger(log_level),
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
# If uvicorn/root handlers are not already using ProcessorFormatter, ensure they do.
|
||||
def _has_processor_formatter(logger: logging.Logger) -> bool:
|
||||
for h in logger.handlers:
|
||||
if isinstance(getattr(h, "formatter", None), structlog.stdlib.ProcessorFormatter):
|
||||
return True
|
||||
return False
|
||||
|
||||
if not (_has_processor_formatter(logging.getLogger()) or _has_processor_formatter(logging.getLogger("uvicorn"))):
|
||||
_ensure_stdlib_processor_logging(log_level)
|
||||
|
||||
|
||||
|
||||
38
app/uvicorn_log_config.json
Normal file
38
app/uvicorn_log_config.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"structlog": {
|
||||
"()": "app.logging_config.build_uvicorn_structlog_formatter"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "structlog"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"": {
|
||||
"handlers": ["default"],
|
||||
"level": "INFO"
|
||||
},
|
||||
"uvicorn": {
|
||||
"handlers": ["default"],
|
||||
"level": "INFO",
|
||||
"propagate": false
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"handlers": ["default"],
|
||||
"level": "INFO",
|
||||
"propagate": false
|
||||
},
|
||||
"uvicorn.access": {
|
||||
"handlers": ["default"],
|
||||
"level": "INFO",
|
||||
"propagate": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user