TouchPortalAPI.logger
1__copyright__ = """ 2 This file is part of the TouchPortal-API project. 3 Copyright (c) TouchPortal-API Developers/Contributors 4 All rights reserved. 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU General Public License for more details. 15 16 You should have received a copy of the GNU General Public License 17 along with this program. If not, see <https://www.gnu.org/licenses/>. 18""" 19 20import os 21import sys 22from dataclasses import asdict, is_dataclass 23from datetime import datetime, date, time 24from json import JSONEncoder, dumps 25from logging import Formatter, getLogger, getLevelName, StreamHandler, NullHandler, Handler 26from logging.handlers import TimedRotatingFileHandler 27 28class Logger: 29 """ A helper class for common logging requirements, which can be configured via the constructor and provides some convenience 30 methods. 31 32 It uses an instance of Python's `logging.Logger()` class, either the one specified in the `logger` constructor parameter, 33 or, if `logger` is `None`, one obtained with `logging.getLogger(name=name)`. 34 Any logger interactions which are not directly supported by this helper class can be accessed directly via `Logger.logger` member. 35 36 Due to the how Python's logger works, the first ("root") instance of the logger will define the defaults for any named loggers 37 added later. These defaults can optionally be overridden per child instance by passing the desired parameters to the constructor 38 or via the `setLogLevel()`, `setStreamDestination()`, and `setFileDestination()` methods. 39 40 The class provides aliases for the `logging.Logger` log writing methodss like `debug()`, `info()`, 'log()', etc. 41 In addition, some shorter aliases are provided (`dbg()`, `inf()`, `wrn()/warn()`, `err()`, `crt()/fatal()`). 42 43 For further details on Python's built-in logging, see: https://docs.python.org/3/library/logging.html 44 """ 45 46 """ The default options to use for `TimedRotatingFileHandler`. """ 47 DEFAULT_FILE_HANDLER_OPTS:dict = {'when': 'D', 'backupCount': 7, 'delay': True} 48 """ The default log formatter for stream and file logger handlers. """ 49 DEFAULT_LOG_FORMATTER = Formatter( 50 fmt="{asctime:s}.{msecs:03.0f} [{levelname:.1s}] [{filename:s}:{lineno:d}] {message:s}", 51 datefmt="%H:%M:%S", style="{" 52 ) 53 54 def __init__(self, name=None, level=None, stream=None, filename=None, logger=None, 55 formatter=DEFAULT_LOG_FORMATTER, 56 fileHandlerOpts=DEFAULT_FILE_HANDLER_OPTS ): 57 """ 58 Creates an instance of the logger. 59 60 Args: 61 `name`: A name for this logger instance. Each named logger is a global instance, specifying an existing name will use that instance. 62 The "root" logger has no name. 63 `level`: Logging level for this logger. `None` will keep the default (root or existing) logger level. 64 `stream`: Add an instance of `logging.StreamHandler()` with specified stream (eg. `os.stderr`). 65 `filename`: Add an instance of `logging.handlers.TimedRotatingFileHandler()` with specified file name. 66 By default the logs are rotated daily and the last 7 files are preserved. This can be changed via the `fileHandlerOpts` argument. 67 `logger`: Use specified `logging.Logger()` instance. By default a logger instance is created/retreived using `logging.getLogger(name=name)`. 68 `formatter`: Use specified `logging.Formatter()` as the formatter for the added stream and/or file handlers. 69 By default the static `DEFAULT_LOG_FORMATTER` is used. 70 `fileHandlerOpts`: Additional parameters for `TimedRotatingFileHandler` logger. 71 """ 72 # Create/get logger instance unless specified 73 self.logger = logger if logger else getLogger(name=name) 74 # use default formatter unless specified 75 self.formatter = formatter if formatter else self.DEFAULT_LOG_FORMATTER 76 # store instance of file/stream handler for possible future access to them (eg. to set logging level) 77 self.fileHandler = None 78 self.streamHandler = None 79 self.nullHandler = None 80 # logging function aliases 81 self.log = self.logger.log 82 self.dbg = self.debug = self.logger.debug 83 self.inf = self.info = self.logger.info 84 self.wrn = self.warn = self.warning = self.logger.warning 85 self.err = self.error = self.logger.error 86 self.crt = self.fatal = self.critical = self.logger.critical 87 self.exception = self.logger.exception 88 # set logging level if specified 89 if level: 90 self.setLogLevel(level) 91 # add file log if a filename was specified 92 if filename: 93 self.setFileDestination(filename, fileHandlerOpts) 94 # add a stream handler if requested or as fallback for failed file logger above 95 if stream: 96 self.setStreamDestination(stream) 97 98 def setLogLevel(self, level, logger=None): 99 """ 100 Set the miniimum logging level, either globally for all log handlers (`logger=None`, the default), or a specific instance. 101 `level` can be one of: "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants), 102 or `None` to disable all logging. 103 """ 104 if logger: 105 if isinstance(level, Handler): 106 logger.setLevel(level) 107 return 108 109 currentLevel = None if self.nullHandler else self.logger.getEffectiveLevel() 110 if level and isinstance(level, str): 111 level = getLevelName(level) # actually gets the numeric value from a name 112 if level == currentLevel: 113 return 114 if level: 115 self.logger.setLevel(level) 116 # if switching from null logging, remove null handler and re-add stream/file handler(s) 117 if self.nullHandler: 118 self.logger.removeHandler(self.nullHandler) 119 self.nullHandler = None 120 if self.fileHandler: 121 self.logger.addHandler(self.fileHandler) 122 if self.streamHandler: 123 self.logger.addHandler(self.streamHandler) 124 else: 125 # switch to null logging, remove known file handlers if they exist and set null handler with critical level 126 if self.fileHandler: 127 self.logger.removeHandler(self.fileHandler) 128 if self.streamHandler: 129 self.logger.removeHandler(self.streamHandler) 130 self.nullHandler = NullHandler() 131 self.logger.addHandler(self.nullHandler) 132 self.logger.setLevel("CRITICAL") 133 134 def setStreamDestination(self, stream): 135 """ Set a destination for the StreamHandler logger. `stream` should be a file stream type (eg. os.stderr) or `None` to disable. """ 136 if self.streamHandler: 137 self.logger.removeHandler(self.streamHandler) 138 self.streamHandler = None 139 if stream: 140 try: 141 self.streamHandler = StreamHandler(stream) 142 self.streamHandler.setFormatter(self.formatter) 143 self.logger.addHandler(self.streamHandler) 144 except Exception as e: 145 print(f"Error while creating stream logger: \n{repr(e)}") 146 147 def setFileDestination(self, filename, handlerOpts = DEFAULT_FILE_HANDLER_OPTS): 148 """ Set a destination for the File logger. `filename` should be a file name (with or w/out a path) or `None` to disable the file logger. """ 149 if self.fileHandler: 150 self.logger.removeHandler(self.fileHandler) 151 self.fileHandler = None 152 if filename: 153 try: 154 if not os.path.splitext(filename)[1]: 155 filename += ".log" 156 self.fileHandler = TimedRotatingFileHandler(str(filename), **handlerOpts) 157 self.fileHandler.setFormatter(self.formatter) 158 self.logger.addHandler(self.fileHandler) 159 except Exception as e: 160 print(f"Error while creating file logger: \n{repr(e)}") 161 162 class JsonEncoder(JSONEncoder): 163 """ Custom JSON encoder for handling `dataclass` types and pretty-printing date/time types. Used for `format_json()` method. """ 164 def default(self, obj): 165 if is_dataclass(obj): 166 return asdict(obj) 167 if isinstance(obj, (datetime, date, time)): 168 return obj.isoformat() 169 return super(Logger.JsonEncoder, self).default(obj) 170 171 @staticmethod 172 def format_json(data, indent=2): 173 """ Returns a string representation of an object, serialized to JSON and formatted for human-readable output (such as logging). """ 174 return dumps(data, cls=Logger.JsonEncoder, indent=indent)
29class Logger: 30 """ A helper class for common logging requirements, which can be configured via the constructor and provides some convenience 31 methods. 32 33 It uses an instance of Python's `logging.Logger()` class, either the one specified in the `logger` constructor parameter, 34 or, if `logger` is `None`, one obtained with `logging.getLogger(name=name)`. 35 Any logger interactions which are not directly supported by this helper class can be accessed directly via `Logger.logger` member. 36 37 Due to the how Python's logger works, the first ("root") instance of the logger will define the defaults for any named loggers 38 added later. These defaults can optionally be overridden per child instance by passing the desired parameters to the constructor 39 or via the `setLogLevel()`, `setStreamDestination()`, and `setFileDestination()` methods. 40 41 The class provides aliases for the `logging.Logger` log writing methodss like `debug()`, `info()`, 'log()', etc. 42 In addition, some shorter aliases are provided (`dbg()`, `inf()`, `wrn()/warn()`, `err()`, `crt()/fatal()`). 43 44 For further details on Python's built-in logging, see: https://docs.python.org/3/library/logging.html 45 """ 46 47 """ The default options to use for `TimedRotatingFileHandler`. """ 48 DEFAULT_FILE_HANDLER_OPTS:dict = {'when': 'D', 'backupCount': 7, 'delay': True} 49 """ The default log formatter for stream and file logger handlers. """ 50 DEFAULT_LOG_FORMATTER = Formatter( 51 fmt="{asctime:s}.{msecs:03.0f} [{levelname:.1s}] [{filename:s}:{lineno:d}] {message:s}", 52 datefmt="%H:%M:%S", style="{" 53 ) 54 55 def __init__(self, name=None, level=None, stream=None, filename=None, logger=None, 56 formatter=DEFAULT_LOG_FORMATTER, 57 fileHandlerOpts=DEFAULT_FILE_HANDLER_OPTS ): 58 """ 59 Creates an instance of the logger. 60 61 Args: 62 `name`: A name for this logger instance. Each named logger is a global instance, specifying an existing name will use that instance. 63 The "root" logger has no name. 64 `level`: Logging level for this logger. `None` will keep the default (root or existing) logger level. 65 `stream`: Add an instance of `logging.StreamHandler()` with specified stream (eg. `os.stderr`). 66 `filename`: Add an instance of `logging.handlers.TimedRotatingFileHandler()` with specified file name. 67 By default the logs are rotated daily and the last 7 files are preserved. This can be changed via the `fileHandlerOpts` argument. 68 `logger`: Use specified `logging.Logger()` instance. By default a logger instance is created/retreived using `logging.getLogger(name=name)`. 69 `formatter`: Use specified `logging.Formatter()` as the formatter for the added stream and/or file handlers. 70 By default the static `DEFAULT_LOG_FORMATTER` is used. 71 `fileHandlerOpts`: Additional parameters for `TimedRotatingFileHandler` logger. 72 """ 73 # Create/get logger instance unless specified 74 self.logger = logger if logger else getLogger(name=name) 75 # use default formatter unless specified 76 self.formatter = formatter if formatter else self.DEFAULT_LOG_FORMATTER 77 # store instance of file/stream handler for possible future access to them (eg. to set logging level) 78 self.fileHandler = None 79 self.streamHandler = None 80 self.nullHandler = None 81 # logging function aliases 82 self.log = self.logger.log 83 self.dbg = self.debug = self.logger.debug 84 self.inf = self.info = self.logger.info 85 self.wrn = self.warn = self.warning = self.logger.warning 86 self.err = self.error = self.logger.error 87 self.crt = self.fatal = self.critical = self.logger.critical 88 self.exception = self.logger.exception 89 # set logging level if specified 90 if level: 91 self.setLogLevel(level) 92 # add file log if a filename was specified 93 if filename: 94 self.setFileDestination(filename, fileHandlerOpts) 95 # add a stream handler if requested or as fallback for failed file logger above 96 if stream: 97 self.setStreamDestination(stream) 98 99 def setLogLevel(self, level, logger=None): 100 """ 101 Set the miniimum logging level, either globally for all log handlers (`logger=None`, the default), or a specific instance. 102 `level` can be one of: "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants), 103 or `None` to disable all logging. 104 """ 105 if logger: 106 if isinstance(level, Handler): 107 logger.setLevel(level) 108 return 109 110 currentLevel = None if self.nullHandler else self.logger.getEffectiveLevel() 111 if level and isinstance(level, str): 112 level = getLevelName(level) # actually gets the numeric value from a name 113 if level == currentLevel: 114 return 115 if level: 116 self.logger.setLevel(level) 117 # if switching from null logging, remove null handler and re-add stream/file handler(s) 118 if self.nullHandler: 119 self.logger.removeHandler(self.nullHandler) 120 self.nullHandler = None 121 if self.fileHandler: 122 self.logger.addHandler(self.fileHandler) 123 if self.streamHandler: 124 self.logger.addHandler(self.streamHandler) 125 else: 126 # switch to null logging, remove known file handlers if they exist and set null handler with critical level 127 if self.fileHandler: 128 self.logger.removeHandler(self.fileHandler) 129 if self.streamHandler: 130 self.logger.removeHandler(self.streamHandler) 131 self.nullHandler = NullHandler() 132 self.logger.addHandler(self.nullHandler) 133 self.logger.setLevel("CRITICAL") 134 135 def setStreamDestination(self, stream): 136 """ Set a destination for the StreamHandler logger. `stream` should be a file stream type (eg. os.stderr) or `None` to disable. """ 137 if self.streamHandler: 138 self.logger.removeHandler(self.streamHandler) 139 self.streamHandler = None 140 if stream: 141 try: 142 self.streamHandler = StreamHandler(stream) 143 self.streamHandler.setFormatter(self.formatter) 144 self.logger.addHandler(self.streamHandler) 145 except Exception as e: 146 print(f"Error while creating stream logger: \n{repr(e)}") 147 148 def setFileDestination(self, filename, handlerOpts = DEFAULT_FILE_HANDLER_OPTS): 149 """ Set a destination for the File logger. `filename` should be a file name (with or w/out a path) or `None` to disable the file logger. """ 150 if self.fileHandler: 151 self.logger.removeHandler(self.fileHandler) 152 self.fileHandler = None 153 if filename: 154 try: 155 if not os.path.splitext(filename)[1]: 156 filename += ".log" 157 self.fileHandler = TimedRotatingFileHandler(str(filename), **handlerOpts) 158 self.fileHandler.setFormatter(self.formatter) 159 self.logger.addHandler(self.fileHandler) 160 except Exception as e: 161 print(f"Error while creating file logger: \n{repr(e)}") 162 163 class JsonEncoder(JSONEncoder): 164 """ Custom JSON encoder for handling `dataclass` types and pretty-printing date/time types. Used for `format_json()` method. """ 165 def default(self, obj): 166 if is_dataclass(obj): 167 return asdict(obj) 168 if isinstance(obj, (datetime, date, time)): 169 return obj.isoformat() 170 return super(Logger.JsonEncoder, self).default(obj) 171 172 @staticmethod 173 def format_json(data, indent=2): 174 """ Returns a string representation of an object, serialized to JSON and formatted for human-readable output (such as logging). """ 175 return dumps(data, cls=Logger.JsonEncoder, indent=indent)
A helper class for common logging requirements, which can be configured via the constructor and provides some convenience methods.
It uses an instance of Python's logging.Logger()
class, either the one specified in the logger
constructor parameter,
or, if logger
is None
, one obtained with logging.getLogger(name=name)
.
Any logger interactions which are not directly supported by this helper class can be accessed directly via Logger.logger
member.
Due to the how Python's logger works, the first ("root") instance of the logger will define the defaults for any named loggers
added later. These defaults can optionally be overridden per child instance by passing the desired parameters to the constructor
or via the setLogLevel()
, setStreamDestination()
, and setFileDestination()
methods.
The class provides aliases for the logging.Logger
log writing methodss like debug()
, info()
, 'log()', etc.
In addition, some shorter aliases are provided (dbg()
, inf()
, wrn()/warn()
, err()
, crt()/fatal()
).
For further details on Python's built-in logging, see: https://docs.python.org/3/library/logging.html
55 def __init__(self, name=None, level=None, stream=None, filename=None, logger=None, 56 formatter=DEFAULT_LOG_FORMATTER, 57 fileHandlerOpts=DEFAULT_FILE_HANDLER_OPTS ): 58 """ 59 Creates an instance of the logger. 60 61 Args: 62 `name`: A name for this logger instance. Each named logger is a global instance, specifying an existing name will use that instance. 63 The "root" logger has no name. 64 `level`: Logging level for this logger. `None` will keep the default (root or existing) logger level. 65 `stream`: Add an instance of `logging.StreamHandler()` with specified stream (eg. `os.stderr`). 66 `filename`: Add an instance of `logging.handlers.TimedRotatingFileHandler()` with specified file name. 67 By default the logs are rotated daily and the last 7 files are preserved. This can be changed via the `fileHandlerOpts` argument. 68 `logger`: Use specified `logging.Logger()` instance. By default a logger instance is created/retreived using `logging.getLogger(name=name)`. 69 `formatter`: Use specified `logging.Formatter()` as the formatter for the added stream and/or file handlers. 70 By default the static `DEFAULT_LOG_FORMATTER` is used. 71 `fileHandlerOpts`: Additional parameters for `TimedRotatingFileHandler` logger. 72 """ 73 # Create/get logger instance unless specified 74 self.logger = logger if logger else getLogger(name=name) 75 # use default formatter unless specified 76 self.formatter = formatter if formatter else self.DEFAULT_LOG_FORMATTER 77 # store instance of file/stream handler for possible future access to them (eg. to set logging level) 78 self.fileHandler = None 79 self.streamHandler = None 80 self.nullHandler = None 81 # logging function aliases 82 self.log = self.logger.log 83 self.dbg = self.debug = self.logger.debug 84 self.inf = self.info = self.logger.info 85 self.wrn = self.warn = self.warning = self.logger.warning 86 self.err = self.error = self.logger.error 87 self.crt = self.fatal = self.critical = self.logger.critical 88 self.exception = self.logger.exception 89 # set logging level if specified 90 if level: 91 self.setLogLevel(level) 92 # add file log if a filename was specified 93 if filename: 94 self.setFileDestination(filename, fileHandlerOpts) 95 # add a stream handler if requested or as fallback for failed file logger above 96 if stream: 97 self.setStreamDestination(stream)
Creates an instance of the logger.
Args
name
: A name for this logger instance. Each named logger is a global instance, specifying an existing name will use that instance. The "root" logger has no name.level
: Logging level for this logger.None
will keep the default (root or existing) logger level.stream
: Add an instance oflogging.StreamHandler()
with specified stream (eg.os.stderr
).filename
: Add an instance oflogging.handlers.TimedRotatingFileHandler()
with specified file name. By default the logs are rotated daily and the last 7 files are preserved. This can be changed via thefileHandlerOpts
argument.logger
: Use specifiedlogging.Logger()
instance. By default a logger instance is created/retreived usinglogging.getLogger(name=name)
.formatter
: Use specifiedlogging.Formatter()
as the formatter for the added stream and/or file handlers. By default the staticDEFAULT_LOG_FORMATTER
is used.fileHandlerOpts
: Additional parameters forTimedRotatingFileHandler
logger.
The default log formatter for stream and file logger handlers.
99 def setLogLevel(self, level, logger=None): 100 """ 101 Set the miniimum logging level, either globally for all log handlers (`logger=None`, the default), or a specific instance. 102 `level` can be one of: "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants), 103 or `None` to disable all logging. 104 """ 105 if logger: 106 if isinstance(level, Handler): 107 logger.setLevel(level) 108 return 109 110 currentLevel = None if self.nullHandler else self.logger.getEffectiveLevel() 111 if level and isinstance(level, str): 112 level = getLevelName(level) # actually gets the numeric value from a name 113 if level == currentLevel: 114 return 115 if level: 116 self.logger.setLevel(level) 117 # if switching from null logging, remove null handler and re-add stream/file handler(s) 118 if self.nullHandler: 119 self.logger.removeHandler(self.nullHandler) 120 self.nullHandler = None 121 if self.fileHandler: 122 self.logger.addHandler(self.fileHandler) 123 if self.streamHandler: 124 self.logger.addHandler(self.streamHandler) 125 else: 126 # switch to null logging, remove known file handlers if they exist and set null handler with critical level 127 if self.fileHandler: 128 self.logger.removeHandler(self.fileHandler) 129 if self.streamHandler: 130 self.logger.removeHandler(self.streamHandler) 131 self.nullHandler = NullHandler() 132 self.logger.addHandler(self.nullHandler) 133 self.logger.setLevel("CRITICAL")
Set the miniimum logging level, either globally for all log handlers (logger=None
, the default), or a specific instance.
level
can be one of: "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py logging
module Level constants),
or None
to disable all logging.
135 def setStreamDestination(self, stream): 136 """ Set a destination for the StreamHandler logger. `stream` should be a file stream type (eg. os.stderr) or `None` to disable. """ 137 if self.streamHandler: 138 self.logger.removeHandler(self.streamHandler) 139 self.streamHandler = None 140 if stream: 141 try: 142 self.streamHandler = StreamHandler(stream) 143 self.streamHandler.setFormatter(self.formatter) 144 self.logger.addHandler(self.streamHandler) 145 except Exception as e: 146 print(f"Error while creating stream logger: \n{repr(e)}")
Set a destination for the StreamHandler logger. stream
should be a file stream type (eg. os.stderr) or None
to disable.
148 def setFileDestination(self, filename, handlerOpts = DEFAULT_FILE_HANDLER_OPTS): 149 """ Set a destination for the File logger. `filename` should be a file name (with or w/out a path) or `None` to disable the file logger. """ 150 if self.fileHandler: 151 self.logger.removeHandler(self.fileHandler) 152 self.fileHandler = None 153 if filename: 154 try: 155 if not os.path.splitext(filename)[1]: 156 filename += ".log" 157 self.fileHandler = TimedRotatingFileHandler(str(filename), **handlerOpts) 158 self.fileHandler.setFormatter(self.formatter) 159 self.logger.addHandler(self.fileHandler) 160 except Exception as e: 161 print(f"Error while creating file logger: \n{repr(e)}")
Set a destination for the File logger. filename
should be a file name (with or w/out a path) or None
to disable the file logger.
172 @staticmethod 173 def format_json(data, indent=2): 174 """ Returns a string representation of an object, serialized to JSON and formatted for human-readable output (such as logging). """ 175 return dumps(data, cls=Logger.JsonEncoder, indent=indent)
Returns a string representation of an object, serialized to JSON and formatted for human-readable output (such as logging).
163 class JsonEncoder(JSONEncoder): 164 """ Custom JSON encoder for handling `dataclass` types and pretty-printing date/time types. Used for `format_json()` method. """ 165 def default(self, obj): 166 if is_dataclass(obj): 167 return asdict(obj) 168 if isinstance(obj, (datetime, date, time)): 169 return obj.isoformat() 170 return super(Logger.JsonEncoder, self).default(obj)
Custom JSON encoder for handling dataclass
types and pretty-printing date/time types. Used for format_json()
method.
165 def default(self, obj): 166 if is_dataclass(obj): 167 return asdict(obj) 168 if isinstance(obj, (datetime, date, time)): 169 return obj.isoformat() 170 return super(Logger.JsonEncoder, self).default(obj)
Implement this method in a subclass such that it returns
a serializable object for o
, or calls the base implementation
(to raise a TypeError
).
For example, to support arbitrary iterators, you could implement default like this::
def default(self, o):
try:
iterable = iter(o)
except TypeError:
pass
else:
return list(iterable)
# Let the base class default method raise the TypeError
return JSONEncoder.default(self, o)
Inherited Members
- json.encoder.JSONEncoder
- JSONEncoder
- item_separator
- key_separator
- encode
- iterencode