1
- import datetime
2
- import json
3
1
import logging
4
2
import sys
5
3
from enum import Enum
6
- from typing import Any , Optional
4
+ from typing import Optional
5
+
6
+ import structlog
7
7
8
8
9
9
class LogLevel (str , Enum ):
@@ -46,123 +46,6 @@ def _missing_(cls, value: str) -> Optional["LogFormat"]:
46
46
)
47
47
48
48
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
-
166
49
def setup_logging (
167
50
log_level : Optional [LogLevel ] = None , log_format : Optional [LogFormat ] = None
168
51
) -> logging .Logger :
@@ -181,10 +64,52 @@ def setup_logging(
181
64
if log_format is None :
182
65
log_format = LogFormat .JSON
183
66
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
+ structlog .processors .CallsiteParameterAdder (
76
+ [
77
+ structlog .processors .CallsiteParameter .MODULE ,
78
+ ]
79
+ ),
80
+ ]
81
+ # Not sure why this is needed. I think it is a wrapper for the standard logging module.
82
+ # Should allow to log both with structlog and the standard logging module:
83
+ # import logging
84
+ # import structlog
85
+ # logging.getLogger("stdlog").info("woo")
86
+ # structlog.get_logger("structlog").info("amazing", events="oh yes")
87
+ structlog .configure (
88
+ processors = shared_processors
89
+ + [
90
+ # Prepare event dict for `ProcessorFormatter`.
91
+ structlog .stdlib .ProcessorFormatter .wrap_for_formatter ,
92
+ ],
93
+ logger_factory = structlog .stdlib .LoggerFactory (),
94
+ cache_logger_on_first_use = True ,
95
+ )
96
+
97
+ # The config aboves adds the following keys to all log entries: _record & _from_structlog.
98
+ # remove_processors_meta removes them.
99
+ processors = shared_processors + [structlog .stdlib .ProcessorFormatter .remove_processors_meta ]
100
+ # Choose the processors based on the log format
101
+ if log_format == LogFormat .JSON :
102
+ processors = processors + [
103
+ structlog .processors .dict_tracebacks ,
104
+ structlog .processors .JSONRenderer (),
105
+ ]
106
+ else :
107
+ processors = processors + [structlog .dev .ConsoleRenderer ()]
108
+ formatter = structlog .stdlib .ProcessorFormatter (
109
+ # foreign_pre_chain run ONLY on `logging` entries that do NOT originate within structlog.
110
+ foreign_pre_chain = shared_processors ,
111
+ processors = processors ,
112
+ )
188
113
189
114
# Create handlers for stdout and stderr
190
115
stdout_handler = logging .StreamHandler (sys .stdout )
@@ -208,7 +133,7 @@ def setup_logging(
208
133
root_logger .addHandler (stderr_handler )
209
134
210
135
# Create a logger for our package
211
- logger = logging . getLogger ("codegate" )
136
+ logger = structlog . get_logger ("codegate" )
212
137
logger .debug (
213
138
"Logging initialized" ,
214
139
extra = {
@@ -217,5 +142,3 @@ def setup_logging(
217
142
"handlers" : ["stdout" , "stderr" ],
218
143
},
219
144
)
220
-
221
- return logger
0 commit comments