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
|
CMD curl -f http://localhost:8000/health || exit 1
|
||||||
|
|
||||||
# Run the application
|
# 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
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import logging.config
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -29,29 +30,93 @@ def _add_required_defaults(_: Any, __: str, event_dict: Dict[str, Any]) -> Dict[
|
|||||||
return event_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:
|
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:
|
Args:
|
||||||
log_level: Minimum log level for application logs.
|
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)
|
logging.basicConfig(level=log_level)
|
||||||
|
|
||||||
structlog.configure(
|
structlog.configure(
|
||||||
processors=[
|
processors=[
|
||||||
|
structlog.stdlib.filter_by_level,
|
||||||
structlog.contextvars.merge_contextvars,
|
structlog.contextvars.merge_contextvars,
|
||||||
structlog.stdlib.add_log_level,
|
structlog.stdlib.add_log_level,
|
||||||
_add_required_defaults,
|
_add_required_defaults,
|
||||||
structlog.processors.TimeStamper(fmt="iso", key="timestamp"),
|
structlog.processors.TimeStamper(fmt="iso", key="timestamp"),
|
||||||
structlog.processors.dict_tracebacks,
|
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,
|
context_class=dict,
|
||||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
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,
|
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