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