1+ import json
12import logging
23import os
34import re
910from logging import FileHandler , StreamHandler , Handler
1011from logging .handlers import RotatingFileHandler
1112from string import Formatter
12- from typing import Optional
13+ from typing import Dict , Optional
1314
1415import discord
1516from discord .ext import commands
@@ -74,6 +75,71 @@ def line(self, level="info"):
7475 )
7576
7677
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+
77143class FileFormatter (logging .Formatter ):
78144 ansi_escape = re .compile (r"\x1B\[[0-?]*[ -/]*[@-~]" )
79145
@@ -85,11 +151,25 @@ def format(self, record):
85151log_stream_formatter = logging .Formatter (
86152 "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s" , datefmt = "%m/%d/%y %H:%M:%S"
87153)
154+
88155log_file_formatter = FileFormatter (
89156 "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s" ,
90157 datefmt = "%Y-%m-%d %H:%M:%S" ,
91158)
92159
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+
93173
94174def create_log_handler (
95175 filename : Optional [str ] = None ,
@@ -98,6 +178,7 @@ def create_log_handler(
98178 level : int = logging .DEBUG ,
99179 mode : str = "a+" ,
100180 encoding : str = "utf-8" ,
181+ format : str = "plain" ,
101182 maxBytes : int = 28000000 ,
102183 backupCount : int = 1 ,
103184 ** kwargs ,
@@ -124,6 +205,9 @@ def create_log_handler(
124205 encoding : str
125206 If this keyword argument is specified along with filename, its value is used when the `FileHandler` is created,
126207 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.
127211 maxBytes : int
128212 The max file size before the rollover occurs. Defaults to 28000000 (28MB). Rollover occurs whenever the current
129213 log file is nearly `maxBytes` in length; but if either of `maxBytes` or `backupCount` is zero,
@@ -141,23 +225,28 @@ def create_log_handler(
141225
142226 if filename is None :
143227 handler = StreamHandler (stream = sys .stdout , ** kwargs )
144- handler . setFormatter ( log_stream_formatter )
228+ formatter = log_stream_formatter
145229 elif not rotating :
146230 handler = FileHandler (filename , mode = mode , encoding = encoding , ** kwargs )
147- handler . setFormatter ( log_file_formatter )
231+ formatter = log_file_formatter
148232 else :
149233 handler = RotatingFileHandler (
150234 filename , mode = mode , encoding = encoding , maxBytes = maxBytes , backupCount = backupCount , ** kwargs
151235 )
152- handler .setFormatter (log_file_formatter )
236+ formatter = log_file_formatter
237+
238+ if format == "json" :
239+ formatter = json_formatter
153240
154241 handler .setLevel (level )
242+ handler .setFormatter (formatter )
155243 return handler
156244
157245
158246logging .setLoggerClass (ModmailLogger )
159247log_level = logging .INFO
160248loggers = set ()
249+
161250ch = create_log_handler (level = log_level )
162251ch_debug : Optional [RotatingFileHandler ] = None
163252
@@ -173,7 +262,11 @@ def getLogger(name=None) -> ModmailLogger:
173262
174263
175264def 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 )
177270
178271 logger = getLogger (__name__ )
179272 level_text = bot .config ["log_level" ].upper ()
@@ -198,8 +291,15 @@ def configure_logging(bot) -> None:
198291
199292 logger .info ("Log file: %s" , bot .log_file_path )
200293 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+
201298 ch .setLevel (log_level )
202299
300+ logger .info ("Stream log format: %s" , stream_log_format )
301+ logger .info ("File log format: %s" , file_log_format )
302+
203303 for log in loggers :
204304 log .setLevel (log_level )
205305 log .addHandler (ch_debug )
0 commit comments