77
88import os
99import sys
10+ import json
1011import logging
11- from typing import Optional
12+ from datetime import datetime , timezone
13+ from typing import Optional , List
1214
1315import uvicorn
1416
1719DEFAULT_HOST = "127.0.0.1"
1820ENV_PORT = "API_PORT"
1921ENV_HOST = "API_HOST"
22+ ENV_LOG_PATH = "API_LOG_PATH"
2023
2124
2225def _parse_port (value : str ) -> int :
@@ -37,6 +40,51 @@ def _resolve_host(value: Optional[str]) -> str:
3740 return "127.0.0.1" if host == "localhost" else host
3841
3942
43+ class _JsonLogFormatter (logging .Formatter ):
44+ """Format log records as compact JSON lines for stdout/stderr capture."""
45+ def format (self , record : logging .LogRecord ) -> str :
46+ payload = {
47+ "ts" : datetime .fromtimestamp (record .created , tz = timezone .utc ).isoformat (),
48+ "level" : record .levelname ,
49+ "logger" : record .name ,
50+ "message" : record .getMessage (),
51+ }
52+ return json .dumps (payload , separators = ("," , ":" ))
53+
54+
55+ def _configure_logging (log_path : Optional [str ]) -> List [logging .Handler ]:
56+ """
57+ Configure structured logging to stdout and optionally to a log file.
58+ """
59+ formatter = _JsonLogFormatter ()
60+ handlers : List [logging .Handler ] = []
61+
62+ stream_handler = logging .StreamHandler (stream = sys .stdout )
63+ stream_handler .setFormatter (formatter )
64+ handlers .append (stream_handler )
65+
66+ if log_path :
67+ try :
68+ file_handler = logging .FileHandler (log_path )
69+ except OSError as exc :
70+ raise ValueError (f"{ ENV_LOG_PATH } is not writable: { log_path } " ) from exc
71+ file_handler .setFormatter (formatter )
72+ handlers .append (file_handler )
73+
74+ root_logger = logging .getLogger ()
75+ root_logger .setLevel (logging .INFO )
76+ root_logger .handlers = handlers
77+
78+ # Ensure uvicorn loggers use the same handlers/format.
79+ for logger_name in ("uvicorn.error" , "uvicorn.access" ):
80+ logger = logging .getLogger (logger_name )
81+ logger .handlers = handlers
82+ logger .setLevel (logging .INFO )
83+ logger .propagate = False
84+
85+ return handlers
86+
87+
4088def main () -> None :
4189 """
4290 Start the FastAPI sidecar using environment configuration.
@@ -46,7 +94,12 @@ def main() -> None:
4694 Optional:
4795 API_HOST: host to bind, defaults to 127.0.0.1.
4896 """
49- logging .basicConfig (level = logging .INFO , format = "%(message)s" )
97+ log_path = os .getenv (ENV_LOG_PATH )
98+ try :
99+ _configure_logging (log_path )
100+ except ValueError as exc :
101+ logging .error (str (exc ))
102+ sys .exit (2 )
50103
51104 port_raw = os .getenv (ENV_PORT )
52105 if not port_raw :
0 commit comments