1
+ import json
1
2
import logging
2
3
import os
3
4
import re
9
10
from logging import FileHandler , StreamHandler , Handler
10
11
from logging .handlers import RotatingFileHandler
11
12
from string import Formatter
12
- from typing import Optional
13
+ from typing import Dict , Optional
13
14
14
15
import discord
15
16
from discord .ext import commands
@@ -74,6 +75,71 @@ def line(self, level="info"):
74
75
)
75
76
76
77
78
+ class JsonFormatter (logging .Formatter ):
79
+ """
80
+ Formatter that outputs JSON strings after parsing the LogRecord.
81
+
82
+ Parameters
83
+ ----------
84
+ fmt_dict : Optional[Dict[str, str]]
85
+ {key: logging format attribute} pairs. Defaults to {"message": "message"}.
86
+ time_format: str
87
+ time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S"
88
+ msec_format: str
89
+ Microsecond formatting. Appended at the end. Default: "%s.%03dZ"
90
+ """
91
+
92
+ def __init__ (
93
+ self ,
94
+ fmt_dict : Optional [Dict [str , str ]] = None ,
95
+ time_format : str = "%Y-%m-%dT%H:%M:%S" ,
96
+ msec_format : str = "%s.%03dZ" ,
97
+ ):
98
+ self .fmt_dict : Dict [str , str ] = fmt_dict if fmt_dict is not None else {"message" : "message" }
99
+ self .default_time_format : str = time_format
100
+ self .default_msec_format : str = msec_format
101
+ self .datefmt : Optional [str ] = None
102
+
103
+ def usesTime (self ) -> bool :
104
+ """
105
+ Overwritten to look for the attribute in the format dict values instead of the fmt string.
106
+ """
107
+ return "asctime" in self .fmt_dict .values ()
108
+
109
+ def formatMessage (self , record ) -> Dict [str , str ]:
110
+ """
111
+ Overwritten to return a dictionary of the relevant LogRecord attributes instead of a string.
112
+ KeyError is raised if an unknown attribute is provided in the fmt_dict.
113
+ """
114
+ return {fmt_key : record .__dict__ [fmt_val ] for fmt_key , fmt_val in self .fmt_dict .items ()}
115
+
116
+ def format (self , record ) -> str :
117
+ """
118
+ Mostly the same as the parent's class method, the difference being that a dict is manipulated and dumped as JSON
119
+ instead of a string.
120
+ """
121
+ record .message = record .getMessage ()
122
+
123
+ if self .usesTime ():
124
+ record .asctime = self .formatTime (record , self .datefmt )
125
+
126
+ message_dict = self .formatMessage (record )
127
+
128
+ if record .exc_info :
129
+ # Cache the traceback text to avoid converting it multiple times
130
+ # (it's constant anyway)
131
+ if not record .exc_text :
132
+ record .exc_text = self .formatException (record .exc_info )
133
+
134
+ if record .exc_text :
135
+ message_dict ["exc_info" ] = record .exc_text
136
+
137
+ if record .stack_info :
138
+ message_dict ["stack_info" ] = self .formatStack (record .stack_info )
139
+
140
+ return json .dumps (message_dict , default = str )
141
+
142
+
77
143
class FileFormatter (logging .Formatter ):
78
144
ansi_escape = re .compile (r"\x1B\[[0-?]*[ -/]*[@-~]" )
79
145
@@ -85,11 +151,25 @@ def format(self, record):
85
151
log_stream_formatter = logging .Formatter (
86
152
"%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s" , datefmt = "%m/%d/%y %H:%M:%S"
87
153
)
154
+
88
155
log_file_formatter = FileFormatter (
89
156
"%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s" ,
90
157
datefmt = "%Y-%m-%d %H:%M:%S" ,
91
158
)
92
159
160
+ json_formatter = JsonFormatter (
161
+ {
162
+ "level" : "levelname" ,
163
+ "message" : "message" ,
164
+ "loggerName" : "name" ,
165
+ "processName" : "processName" ,
166
+ "processID" : "process" ,
167
+ "threadName" : "threadName" ,
168
+ "threadID" : "thread" ,
169
+ "timestamp" : "asctime" ,
170
+ }
171
+ )
172
+
93
173
94
174
def create_log_handler (
95
175
filename : Optional [str ] = None ,
@@ -98,6 +178,7 @@ def create_log_handler(
98
178
level : int = logging .DEBUG ,
99
179
mode : str = "a+" ,
100
180
encoding : str = "utf-8" ,
181
+ format : str = "plain" ,
101
182
maxBytes : int = 28000000 ,
102
183
backupCount : int = 1 ,
103
184
** kwargs ,
@@ -124,6 +205,9 @@ def create_log_handler(
124
205
encoding : str
125
206
If this keyword argument is specified along with filename, its value is used when the `FileHandler` is created,
126
207
and thus used when opening the output file. Defaults to 'utf-8'.
208
+ format : str
209
+ The format to output with, can either be 'json' or 'plain'. Will apply to whichever handler is created,
210
+ based on other conditional logic.
127
211
maxBytes : int
128
212
The max file size before the rollover occurs. Defaults to 28000000 (28MB). Rollover occurs whenever the current
129
213
log file is nearly `maxBytes` in length; but if either of `maxBytes` or `backupCount` is zero,
@@ -141,23 +225,28 @@ def create_log_handler(
141
225
142
226
if filename is None :
143
227
handler = StreamHandler (stream = sys .stdout , ** kwargs )
144
- handler . setFormatter ( log_stream_formatter )
228
+ formatter = log_stream_formatter
145
229
elif not rotating :
146
230
handler = FileHandler (filename , mode = mode , encoding = encoding , ** kwargs )
147
- handler . setFormatter ( log_file_formatter )
231
+ formatter = log_file_formatter
148
232
else :
149
233
handler = RotatingFileHandler (
150
234
filename , mode = mode , encoding = encoding , maxBytes = maxBytes , backupCount = backupCount , ** kwargs
151
235
)
152
- handler .setFormatter (log_file_formatter )
236
+ formatter = log_file_formatter
237
+
238
+ if format == "json" :
239
+ formatter = json_formatter
153
240
154
241
handler .setLevel (level )
242
+ handler .setFormatter (formatter )
155
243
return handler
156
244
157
245
158
246
logging .setLoggerClass (ModmailLogger )
159
247
log_level = logging .INFO
160
248
loggers = set ()
249
+
161
250
ch = create_log_handler (level = log_level )
162
251
ch_debug : Optional [RotatingFileHandler ] = None
163
252
@@ -173,7 +262,11 @@ def getLogger(name=None) -> ModmailLogger:
173
262
174
263
175
264
def configure_logging (bot ) -> None :
176
- global ch_debug , log_level
265
+ global ch_debug , log_level , ch
266
+
267
+ stream_log_format , file_log_format = bot .config ["stream_log_format" ], bot .config ["file_log_format" ]
268
+ if stream_log_format == "json" :
269
+ ch .setFormatter (json_formatter )
177
270
178
271
logger = getLogger (__name__ )
179
272
level_text = bot .config ["log_level" ].upper ()
@@ -198,8 +291,15 @@ def configure_logging(bot) -> None:
198
291
199
292
logger .info ("Log file: %s" , bot .log_file_path )
200
293
ch_debug = create_log_handler (bot .log_file_path , rotating = True )
294
+
295
+ if file_log_format == "json" :
296
+ ch_debug .setFormatter (json_formatter )
297
+
201
298
ch .setLevel (log_level )
202
299
300
+ logger .info ("Stream log format: %s" , stream_log_format )
301
+ logger .info ("File log format: %s" , file_log_format )
302
+
203
303
for log in loggers :
204
304
log .setLevel (log_level )
205
305
log .addHandler (ch_debug )
0 commit comments