1- import datetime
2- import json
31import logging
42import sys
53from enum import Enum
6- from typing import Any , Optional
4+ from typing import Optional
5+
6+ import structlog
77
88
99class LogLevel (str , Enum ):
@@ -46,123 +46,6 @@ def _missing_(cls, value: str) -> Optional["LogFormat"]:
4646 )
4747
4848
49- class JSONFormatter (logging .Formatter ):
50- """Custom formatter that outputs log records as JSON."""
51-
52- def __init__ (self ) -> None :
53- """Initialize the JSON formatter."""
54- super ().__init__ ()
55- self .default_time_format = "%Y-%m-%dT%H:%M:%S"
56- self .default_msec_format = "%s.%03dZ"
57-
58- def format (self , record : logging .LogRecord ) -> str :
59- """Format the log record as a JSON string.
60-
61- Args:
62- record: The log record to format
63-
64- Returns:
65- str: JSON formatted log entry
66- """
67- # Create the base log entry
68- log_entry : dict [str , Any ] = {
69- "timestamp" : self .formatTime (record , self .default_time_format ),
70- "level" : record .levelname ,
71- "module" : record .module ,
72- "message" : record .getMessage (),
73- "extra" : {},
74- }
75-
76- # Add extra fields from the record
77- extra_attrs = {}
78- for key , value in record .__dict__ .items ():
79- if key not in {
80- "args" ,
81- "asctime" ,
82- "created" ,
83- "exc_info" ,
84- "exc_text" ,
85- "filename" ,
86- "funcName" ,
87- "levelname" ,
88- "levelno" ,
89- "lineno" ,
90- "module" ,
91- "msecs" ,
92- "msg" ,
93- "name" ,
94- "pathname" ,
95- "process" ,
96- "processName" ,
97- "relativeCreated" ,
98- "stack_info" ,
99- "thread" ,
100- "threadName" ,
101- "extra" ,
102- }:
103- extra_attrs [key ] = value
104-
105- # Handle the explicit extra parameter if present
106- if hasattr (record , "extra" ):
107- try :
108- if isinstance (record .extra , dict ):
109- extra_attrs .update (record .extra )
110- except Exception :
111- extra_attrs ["unserializable_extra" ] = str (record .extra )
112-
113- # Add all extra attributes to the log entry
114- if extra_attrs :
115- try :
116- json .dumps (extra_attrs ) # Test if serializable
117- log_entry ["extra" ] = extra_attrs
118- except (TypeError , ValueError ):
119- # If serialization fails, convert values to strings
120- serializable_extra = {}
121- for key , value in extra_attrs .items ():
122- try :
123- json .dumps ({key : value }) # Test individual value
124- serializable_extra [key ] = value
125- except (TypeError , ValueError ):
126- serializable_extra [key ] = str (value )
127- log_entry ["extra" ] = serializable_extra
128-
129- # Handle exception info if present
130- if record .exc_info :
131- log_entry ["extra" ]["exception" ] = self .formatException (record .exc_info )
132-
133- # Handle stack info if present
134- if record .stack_info :
135- log_entry ["extra" ]["stack_info" ] = self .formatStack (record .stack_info )
136-
137- return json .dumps (log_entry )
138-
139-
140- class TextFormatter (logging .Formatter ):
141- """Standard text formatter with consistent timestamp format."""
142-
143- def __init__ (self ) -> None :
144- """Initialize the text formatter."""
145- super ().__init__ (
146- fmt = "%(asctime)s - %(levelname)s - %(name)s - %(message)s" ,
147- datefmt = "%Y-%m-%dT%H:%M:%S.%03dZ" ,
148- )
149-
150- def formatTime ( # noqa: N802
151- self , record : logging .LogRecord , datefmt : Optional [str ] = None
152- ) -> str :
153- """Format the time with millisecond precision.
154-
155- Args:
156- record: The log record
157- datefmt: The date format string (ignored as we use a fixed format)
158-
159- Returns:
160- str: Formatted timestamp
161- """
162- ct = datetime .datetime .fromtimestamp (record .created , datetime .UTC )
163- return ct .strftime (self .datefmt )
164-
165-
16649def setup_logging (
16750 log_level : Optional [LogLevel ] = None , log_format : Optional [LogFormat ] = None
16851) -> logging .Logger :
@@ -181,10 +64,47 @@ def setup_logging(
18164 if log_format is None :
18265 log_format = LogFormat .JSON
18366
184- # Create formatters
185- json_formatter = JSONFormatter ()
186- text_formatter = TextFormatter ()
187- formatter = json_formatter if log_format == LogFormat .JSON else text_formatter
67+ # The configuration was taken from structlog documentation
68+ # https://www.structlog.org/en/stable/standard-library.html
69+ # Specifically the section "Rendering Using structlog-based Formatters Within logging"
70+
71+ # Adds log level and timestamp to log entries
72+ shared_processors = [
73+ structlog .processors .add_log_level ,
74+ structlog .processors .TimeStamper (fmt = "%Y-%m-%dT%H:%M:%S.%03dZ" , utc = True ),
75+ ]
76+ # Not sure why this is needed. I think it is a wrapper for the standard logging module.
77+ # Should allow to log both with structlog and the standard logging module:
78+ # import logging
79+ # import structlog
80+ # logging.getLogger("stdlog").info("woo")
81+ # structlog.get_logger("structlog").info("amazing", events="oh yes")
82+ structlog .configure (
83+ processors = shared_processors
84+ + [
85+ # Prepare event dict for `ProcessorFormatter`.
86+ structlog .stdlib .ProcessorFormatter .wrap_for_formatter ,
87+ ],
88+ logger_factory = structlog .stdlib .LoggerFactory (),
89+ cache_logger_on_first_use = True ,
90+ )
91+
92+ # The config aboves adds the following keys to all log entries: _record & _from_structlog.
93+ # remove_processors_meta removes them.
94+ processors = shared_processors + [structlog .stdlib .ProcessorFormatter .remove_processors_meta ]
95+ # Choose the processors based on the log format
96+ if log_format == LogFormat .JSON :
97+ processors = processors + [
98+ structlog .processors .dict_tracebacks ,
99+ structlog .processors .JSONRenderer (),
100+ ]
101+ else :
102+ processors = processors + [structlog .dev .ConsoleRenderer ()]
103+ formatter = structlog .stdlib .ProcessorFormatter (
104+ # foreign_pre_chain run ONLY on `logging` entries that do NOT originate within structlog.
105+ foreign_pre_chain = shared_processors ,
106+ processors = processors ,
107+ )
188108
189109 # Create handlers for stdout and stderr
190110 stdout_handler = logging .StreamHandler (sys .stdout )
@@ -208,7 +128,7 @@ def setup_logging(
208128 root_logger .addHandler (stderr_handler )
209129
210130 # Create a logger for our package
211- logger = logging . getLogger ("codegate" )
131+ logger = structlog . get_logger ("codegate" )
212132 logger .debug (
213133 "Logging initialized" ,
214134 extra = {
@@ -217,5 +137,3 @@ def setup_logging(
217137 "handlers" : ["stdout" , "stderr" ],
218138 },
219139 )
220-
221- return logger
0 commit comments