I'd like to have loglevel TRACE (5) for my application, as I don't think that debug() is sufficient. Additionally log(5, msg) isn't what I want. How can I add a custom loglevel to a Python logger?
I've a mylogger.py with the following content:
import logging
#property
def log(obj):
myLogger = logging.getLogger(obj.__class__.__name__)
return myLogger
In my code I use it in the following way:
class ExampleClass(object):
from mylogger import log
def __init__(self):
'''The constructor with the logger'''
self.log.debug("Init runs")
Now I'd like to call self.log.trace("foo bar")
Edit (Dec 8th 2016): I changed the accepted answer to pfa's which is, IMHO, an excellent solution based on the very good proposal from Eric S.
To people reading in 2022 and beyond: you should probably check out the currently next-highest-rated answer here: https://stackoverflow.com/a/35804945/1691778
My original answer is below.
--
#Eric S.
Eric S.'s answer is excellent, but I learned by experimentation that this will always cause messages logged at the new debug level to be printed -- regardless of what the log level is set to. So if you make a new level number of 9, if you call setLevel(50), the lower level messages will erroneously be printed.
To prevent that from happening, you need another line inside the "debugv" function to check if the logging level in question is actually enabled.
Fixed example that checks if the logging level is enabled:
import logging
DEBUG_LEVELV_NUM = 9
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
if self.isEnabledFor(DEBUG_LEVELV_NUM):
# Yes, logger takes its '*args' as 'args'.
self._log(DEBUG_LEVELV_NUM, message, args, **kws)
logging.Logger.debugv = debugv
If you look at the code for class Logger in logging.__init__.py for Python 2.7, this is what all the standard log functions do (.critical, .debug, etc.).
I apparently can't post replies to others' answers for lack of reputation... hopefully Eric will update his post if he sees this. =)
Combining all of the existing answers with a bunch of usage experience, I think that I have come up with a list of all the things that need to be done to ensure completely seamless usage of the new level. The steps below assume that you are adding a new level TRACE with value logging.DEBUG - 5 == 5:
logging.addLevelName(logging.DEBUG - 5, 'TRACE') needs to be invoked to get the new level registered internally so that it can be referenced by name.
The new level needs to be added as an attribute to logging itself for consistency: logging.TRACE = logging.DEBUG - 5.
A method called trace needs to be added to the logging module. It should behave just like debug, info, etc.
A method called trace needs to be added to the currently configured logger class. Since this is not 100% guaranteed to be logging.Logger, use logging.getLoggerClass() instead.
All the steps are illustrated in the method below:
def addLoggingLevel(levelName, levelNum, methodName=None):
"""
Comprehensively adds a new logging level to the `logging` module and the
currently configured logging class.
`levelName` becomes an attribute of the `logging` module with the value
`levelNum`. `methodName` becomes a convenience method for both `logging`
itself and the class returned by `logging.getLoggerClass()` (usually just
`logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
used.
To avoid accidental clobberings of existing attributes, this method will
raise an `AttributeError` if the level name is already an attribute of the
`logging` module or if the method name is already present
Example
-------
>>> addLoggingLevel('TRACE', logging.DEBUG - 5)
>>> logging.getLogger(__name__).setLevel("TRACE")
>>> logging.getLogger(__name__).trace('that worked')
>>> logging.trace('so did this')
>>> logging.TRACE
5
"""
if not methodName:
methodName = levelName.lower()
if hasattr(logging, levelName):
raise AttributeError('{} already defined in logging module'.format(levelName))
if hasattr(logging, methodName):
raise AttributeError('{} already defined in logging module'.format(methodName))
if hasattr(logging.getLoggerClass(), methodName):
raise AttributeError('{} already defined in logger class'.format(methodName))
# This method was inspired by the answers to Stack Overflow post
# http://stackoverflow.com/q/2183233/2988730, especially
# http://stackoverflow.com/a/13638084/2988730
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(levelNum):
self._log(levelNum, message, args, **kwargs)
def logToRoot(message, *args, **kwargs):
logging.log(levelNum, message, *args, **kwargs)
logging.addLevelName(levelNum, levelName)
setattr(logging, levelName, levelNum)
setattr(logging.getLoggerClass(), methodName, logForLevel)
setattr(logging, methodName, logToRoot)
You can find an even more detailed implementation in the utility library I maintain, haggis. The function haggis.logs.add_logging_level is a more production-ready implementation of this answer.
I took the avoid seeing "lambda" answer and had to modify where the log_at_my_log_level was being added. I too saw the problem that Paul did – I don't think this works. Don't you need logger as the first arg in log_at_my_log_level? This worked for me
import logging
DEBUG_LEVELV_NUM = 9
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
# Yes, logger takes its '*args' as 'args'.
self._log(DEBUG_LEVELV_NUM, message, args, **kws)
logging.Logger.debugv = debugv
This question is rather old, but I just dealt with the same topic and found a way similiar to those already mentioned which appears a little cleaner to me. This was tested on 3.4, so I'm not sure whether the methods used exist in older versions:
from logging import getLoggerClass, addLevelName, setLoggerClass, NOTSET
VERBOSE = 5
class MyLogger(getLoggerClass()):
def __init__(self, name, level=NOTSET):
super().__init__(name, level)
addLevelName(VERBOSE, "VERBOSE")
def verbose(self, msg, *args, **kwargs):
if self.isEnabledFor(VERBOSE):
self._log(VERBOSE, msg, args, **kwargs)
setLoggerClass(MyLogger)
While we have already plenty of correct answers, the following is in my opinion more pythonic:
import logging
from functools import partial, partialmethod
logging.TRACE = 5
logging.addLevelName(logging.TRACE, 'TRACE')
logging.Logger.trace = partialmethod(logging.Logger.log, logging.TRACE)
logging.trace = partial(logging.log, logging.TRACE)
If you want to use mypy on your code, it is recommended to add # type: ignore to suppress warnings from adding attribute.
Who started the bad practice of using internal methods (self._log) and why is each answer based on that?! The pythonic solution would be to use self.log instead so you don't have to mess with any internal stuff:
import logging
SUBDEBUG = 5
logging.addLevelName(SUBDEBUG, 'SUBDEBUG')
def subdebug(self, message, *args, **kws):
self.log(SUBDEBUG, message, *args, **kws)
logging.Logger.subdebug = subdebug
logging.basicConfig()
l = logging.getLogger()
l.setLevel(SUBDEBUG)
l.subdebug('test')
l.setLevel(logging.DEBUG)
l.subdebug('test')
I think you'll have to subclass the Logger class and add a method called trace which basically calls Logger.log with a level lower than DEBUG. I haven't tried this but this is what the docs indicate.
I find it easier to create a new attribute for the logger object that passes the log() function. I think the logger module provides the addLevelName() and the log() for this very reason. Thus no subclasses or new method needed.
import logging
#property
def log(obj):
logging.addLevelName(5, 'TRACE')
myLogger = logging.getLogger(obj.__class__.__name__)
setattr(myLogger, 'trace', lambda *args: myLogger.log(5, *args))
return myLogger
now
mylogger.trace('This is a trace message')
should work as expected.
Tips for creating a custom logger:
Do not use _log, use log (you don't have to check isEnabledFor)
the logging module should be the one creating instance of the custom logger since it does some magic in getLogger, so you will need to set the class via setLoggerClass
You do not need to define __init__ for the logger, class if you are not storing anything
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
def trace(self, msg, *args, **kwargs):
self.log(TRACE, msg, *args, **kwargs)
When calling this logger use setLoggerClass(MyLogger) to make this the default logger from getLogger
logging.setLoggerClass(MyLogger)
log = logging.getLogger(__name__)
# ...
log.trace("something specific")
You will need to setFormatter, setHandler, and setLevel(TRACE) on the handler and on the log itself to actually se this low level trace
This worked for me:
import logging
logging.basicConfig(
format=' %(levelname)-8.8s %(funcName)s: %(message)s',
)
logging.NOTE = 32 # positive yet important
logging.addLevelName(logging.NOTE, 'NOTE') # new level
logging.addLevelName(logging.CRITICAL, 'FATAL') # rename existing
log = logging.getLogger(__name__)
log.note = lambda msg, *args: log._log(logging.NOTE, msg, args)
log.note('school\'s out for summer! %s', 'dude')
log.fatal('file not found.')
The lambda/funcName issue is fixed with logger._log as #marqueed pointed out. I think using lambda looks a bit cleaner, but the drawback is that it can't take keyword arguments. I've never used that myself, so no biggie.
NOTE setup: school's out for summer! dude
FATAL setup: file not found.
As alternative to adding an extra method to the Logger class I would recommend using the Logger.log(level, msg) method.
import logging
TRACE = 5
logging.addLevelName(TRACE, 'TRACE')
FORMAT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
logging.basicConfig(format=FORMAT)
l = logging.getLogger()
l.setLevel(TRACE)
l.log(TRACE, 'trace message')
l.setLevel(logging.DEBUG)
l.log(TRACE, 'disabled trace message')
In my experience, this is the full solution the the op's problem... to avoid seeing "lambda" as the function in which the message is emitted, go deeper:
MY_LEVEL_NUM = 25
logging.addLevelName(MY_LEVEL_NUM, "MY_LEVEL_NAME")
def log_at_my_log_level(self, message, *args, **kws):
# Yes, logger takes its '*args' as 'args'.
self._log(MY_LEVEL_NUM, message, args, **kws)
logger.log_at_my_log_level = log_at_my_log_level
I've never tried working with a standalone logger class, but I think the basic idea is the same (use _log).
Addition to Mad Physicists example to get file name and line number correct:
def logToRoot(message, *args, **kwargs):
if logging.root.isEnabledFor(levelNum):
logging.root._log(levelNum, message, args, **kwargs)
based on pinned answer,
i wrote a little method which automaticaly create new logging levels
def set_custom_logging_levels(config={}):
"""
Assign custom levels for logging
config: is a dict, like
{
'EVENT_NAME': EVENT_LEVEL_NUM,
}
EVENT_LEVEL_NUM can't be like already has logging module
logging.DEBUG = 10
logging.INFO = 20
logging.WARNING = 30
logging.ERROR = 40
logging.CRITICAL = 50
"""
assert isinstance(config, dict), "Configuration must be a dict"
def get_level_func(level_name, level_num):
def _blank(self, message, *args, **kws):
if self.isEnabledFor(level_num):
# Yes, logger takes its '*args' as 'args'.
self._log(level_num, message, args, **kws)
_blank.__name__ = level_name.lower()
return _blank
for level_name, level_num in config.items():
logging.addLevelName(level_num, level_name.upper())
setattr(logging.Logger, level_name.lower(), get_level_func(level_name, level_num))
config may smth like that:
new_log_levels = {
# level_num is in logging.INFO section, that's why it 21, 22, etc..
"FOO": 21,
"BAR": 22,
}
Someone might wanna do, a root level custom logging; and avoid the usage of logging.get_logger(''):
import logging
from datetime import datetime
c_now=datetime.now()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] :: %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler("../logs/log_file_{}-{}-{}-{}.log".format(c_now.year,c_now.month,c_now.day,c_now.hour))
]
)
DEBUG_LEVELV_NUM = 99
logging.addLevelName(DEBUG_LEVELV_NUM, "CUSTOM")
def custom_level(message, *args, **kws):
logging.Logger._log(logging.root,DEBUG_LEVELV_NUM, message, args, **kws)
logging.custom_level = custom_level
# --- --- --- ---
logging.custom_level("Waka")
I'm confused; with python 3.5, at least, it just works:
import logging
TRACE = 5
"""more detail than debug"""
logging.basicConfig()
logging.addLevelName(TRACE,"TRACE")
logger = logging.getLogger('')
logger.debug("n")
logger.setLevel(logging.DEBUG)
logger.debug("y1")
logger.log(TRACE,"n")
logger.setLevel(TRACE)
logger.log(TRACE,"y2")
output:
DEBUG:root:y1
TRACE:root:y2
Following up on the top-rated answers by Eric S. and Mad Physicist:
Fixed example that checks if the logging level is enabled:
import logging
DEBUG_LEVELV_NUM = 9
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
if self.isEnabledFor(DEBUG_LEVELV_NUM):
# Yes, logger takes its '*args' as 'args'.
self._log(DEBUG_LEVELV_NUM, message, args, **kws)
logging.Logger.debugv = debugv
This code-snippet
adds a new log-level "DEBUGV" and assigns a number 9
defines a debugv-method, which logs a message with level "DEBUGV" unless the log-level is set to a value higher than 9 (e.g. log-level "DEBUG")
monkey-patches the logging.Logger-class, so that you can call logger.debugv
The suggested implementation worked well for me, but
code completion doesn't recognise the logger.debugv-method
pyright, which is part of our CI-pipeline, fails, because it cannot trace the debugv-member method of the Logger-class (see https://github.com/microsoft/pylance-release/issues/2335 about dynamically adding member-methods)
I ended up using inheritance as was suggested in Noufal Ibrahim's answer in this thread:
I think you'll have to subclass the Logger class and add a method called trace which basically calls Logger.log with a level lower than DEBUG. I haven't tried this but this is what the docs indicate.
Implementing Noufal Ibrahim's suggestion worked and pyright is happy:
import logging
# add log-level DEBUGV
DEBUGV = 9 # slightly lower than DEBUG (10)
logging.addLevelName(DEBUGV, "DEBUGV")
class MyLogger(logging.Logger):
"""Inherit from standard Logger and add level DEBUGV."""
def debugv(self, msg, *args, **kwargs):
"""Log 'msg % args' with severity 'DEBUGV'."""
if self.isEnabledFor(DEBUGV):
self._log(DEBUGV, msg, args, **kwargs)
logging.setLoggerClass(MyLogger)
Next you can initialise an instance of the extended logger using the logger-manager:
logger = logging.getLogger("whatever_logger_name")
Edit: In order to make pyright recognise the debugv-method, you may need to cast the logger returned by logging.getLogger, which would like this:
import logging
from typing import cast
logger = cast(MyLogger, logging.getLogger("whatever_logger_name"))
In case anyone wants an automated way to add a new logging level to the logging module (or a copy of it) dynamically, I have created this function, expanding #pfa's answer:
def add_level(log_name,custom_log_module=None,log_num=None,
log_call=None,
lower_than=None, higher_than=None, same_as=None,
verbose=True):
'''
Function to dynamically add a new log level to a given custom logging module.
<custom_log_module>: the logging module. If not provided, then a copy of
<logging> module is used
<log_name>: the logging level name
<log_num>: the logging level num. If not provided, then function checks
<lower_than>,<higher_than> and <same_as>, at the order mentioned.
One of those three parameters must hold a string of an already existent
logging level name.
In case a level is overwritten and <verbose> is True, then a message in WARNING
level of the custom logging module is established.
'''
if custom_log_module is None:
import imp
custom_log_module = imp.load_module('custom_log_module',
*imp.find_module('logging'))
log_name = log_name.upper()
def cust_log(par, message, *args, **kws):
# Yes, logger takes its '*args' as 'args'.
if par.isEnabledFor(log_num):
par._log(log_num, message, args, **kws)
available_level_nums = [key for key in custom_log_module._levelNames
if isinstance(key,int)]
available_levels = {key:custom_log_module._levelNames[key]
for key in custom_log_module._levelNames
if isinstance(key,str)}
if log_num is None:
try:
if lower_than is not None:
log_num = available_levels[lower_than]-1
elif higher_than is not None:
log_num = available_levels[higher_than]+1
elif same_as is not None:
log_num = available_levels[higher_than]
else:
raise Exception('Infomation about the '+
'log_num should be provided')
except KeyError:
raise Exception('Non existent logging level name')
if log_num in available_level_nums and verbose:
custom_log_module.warn('Changing ' +
custom_log_module._levelNames[log_num] +
' to '+log_name)
custom_log_module.addLevelName(log_num, log_name)
if log_call is None:
log_call = log_name.lower()
setattr(custom_log_module.Logger, log_call, cust_log)
return custom_log_module
I have a formatter that expects special attribute in the record, "user_id", that not always there(sometimes I add it to records using special logging.Filter).
I tried to override the makeRecord method of logging.Logger like so:
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)-15s user_id=%(user_id)s %(filename)s:%(lineno)-15s: %(message)s')
class OneTestLogger(logging.Logger):
def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
rv = logging.Logger.makeRecord(self, name, level, fn, lno,
msg, args, exc_info,
func, extra)
rv.__dict__.setdefault('user_id', 'master')
return rv
if __name__ == '__main__':
logger = OneTestLogger('main')
print logger
logger.info('Starting test')
But that doesn't seem to work and I keep getting:
<main.MyLogger instance at 0x7f31a6a5b638>
No handlers could be found for logger "main"
What am I doing wrong?
Thanks.
Following the guideline provided in Logging Cookbook. Just the first part, I did not implement Filter (which does not appear in the below quote either).
This has usually meant that if you need to do anything special with a LogRecord, you’ve had to do one of the following.
Create your own Logger subclass, which overrides Logger.makeRecord(), and set it using setLoggerClass() before any loggers that you care about are instantiated.
I have simplifed your exampled just to add the 'hostname':
import logging
from socket import gethostname
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(hostname)s - %(message)s')
class NewLogger(logging.Logger):
def makeRecord(self, *args, **kwargs):
rv = super(NewLogger, self).makeRecord(*args, **kwargs)
# updating the rv value of the original makeRecord
# my idea is to use the same logic than a decorator by
# intercepting the value return by the original makeRecord
# and expanded with what I need
rv.__dict__['hostname'] = gethostname()
# by curiosity I am checking what is in this dictionary
# print(rv.__dict__)
return rv
logging.setLoggerClass(NewLogger)
logger = logging.getLogger(__name__)
logger.info('Hello World!')
Note that this code worked on python 2.7
Very often when writing framework code, I would prefer the caller's line number and file name to be logged. For example, if I detect improper use of a framerwork level API call, I would like to log that.... not as an in-framework error but as "caller error".
This only comes into play when writing low level libraries and systems that use introspectionn.
Is there any way to get the logger to log "one level up"? Can I create a custom LogRecord and then modify it and use it within the installed loggers somehow? Trying to figure this out.
For example something like this:
def set(self, x):
if x not in self._cols:
log.error("Invalid attribute for set", stack_level=-1)
This is easy to find on google now since it was added to python 3.8, and it was mentioned in the other answer but here is a better explanation. You were close with the stack_level argument, but it should be stacklevel and the value should be 2. For example you can do:
import logging
def fun():
logging.basicConfig(format='line %(lineno)s : %(message)s')
log = logging.getLogger(__name__)
log.error('Something is wrong', stacklevel=2)
if __name__ == '__main__':
fun()
This will output:
line 9 : Something is wrong
If the stacklevel was set to the default of 1, it would output:
line 6 : Something is wrong
From the documentation: https://docs.python.org/3/library/logging.html#logging.Logger.debug
The third optional keyword argument is stacklevel, which defaults to 1. If greater than 1, the corresponding number of stack frames are skipped when computing the line number and function name set in the LogRecord created for the logging event. This can be used in logging helpers so that the function name, filename and line number recorded are not the information for the helper function/method, but rather its caller. The name of this parameter mirrors the equivalent one in the warnings module.
OK, I figured this out for python prior to 3.8:
First, you need to use inspect to get the frame. Then modify the extra= parameter in log with the info. But you also have to override makerecord to prevent the inappropriate guards that prevent log from working filename and linenumber overrides.
def myMakeRecord(self, name, level, fn, lno, msg, args, exc_info, func, extra, sinfo):
rv = logging.LogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo)
if extra is not None:
rv.__dict__.update(extra)
return rv
def mylog(logger, msg, level, *args)
logging.Logger.makeRecord = myMakeRecord
frame = inspect.currentframe()
caller = frame.f_back
override = {
"lineno":caller.f_lineno,
"filename":os.path.basename(caller.f_code.co_filename),
}
logger.log(level, msg, extra=override, *args)
Oddly, I couldn't get this to work when extra and sinfo had default values of none (like they do in the original definition). Maybe myMakeRecord should use *args.... but that would require grabbing extra = args[9] ... which is odd.... but maybe less bad (more likely to be future proof).
I did the same as Erik, but with some changes:
import logging
import inspect
from os import path
loggers_dict = {}
def myMakeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
if extra and 'pathname' in extra:
fn = extra.pop('pathname')
rv = logging.LogRecord(name, level, fn, lno, msg, args, exc_info, func)
if extra is not None:
rv.__dict__.update(extra)
return rv
logging.Logger.makeRecord = myMakeRecord
def getLogger():
fn, lno, func = base_logger.findCaller()
extras = {'pathname': fn, 'lineno': lno, 'funcName': func}
fnn = path.normcase(fn)
caller_name = inspect.modulesbyfile.get(fnn, inspect.getmodule(None, fn).__name__)
if caller_name not in loggers_dict:
loggers_dict[caller_name] = logging.getLogger(caller_name)
return loggers_dict[caller_name], extras
def myLogDebug(*msg):
log, extras = getLogger()
if len(msg) == 1:
log.debug(msg[0], extra=extras)
else:
log.debug(' '.join(map(str, msg)), extra=extras)
The main thing here is the legacy, everywhere people has put the call myLogDebug on the code, then would be a messy if I change everything. Other problem was the python 2.7 version, would be nice if I could use the param stacklevel from this thread.
Then I modified some stuff to get the right caller stack frame level, and doesn't change anything from original method.
EDIT - 1
caller_name = inspect.modulesbyfile.get(fnn, inspect.getmodule(None, fn).__name__)
This part is an little hack, just to don't run all the time getmodule from inspect module. There is an dict (an inside cache modulesbyfile) who gets direct access to module names after first getmodule.
Sometimes debug an track the source helps, it's not documented.
I use a LoggerAdapter to let my python logging output Linux TIDs instead of the long unique IDs. But this way I don't modify an existing logger but I create a new object:
new_logger = logging.LoggerAdapter(
logger=logging.getLogger('mylogger'),
extra=my_tid_extractor())
Now I want this LoggerAdapter be used by certain modules. As long as I know a global variable being used as logger I can do something like this:
somemodule.logger = new_logger
But this is not nice - it works only in a couple of cases and you need to know the logger variables used by the modules.
Do you know a way to make a LoggerAdapter available globally e.g. by calling s.th. like
logging.setLogger('mylogger', new_logger)
Or alternatively: is there some other way to let Python logging output Linux thread IDs like printed by ps?
Alternatively, you can to implement custom logger, and make it default in logging module.
Here is example:
import logging
import ctypes
SYS_gettid = 186
libc = ctypes.cdll.LoadLibrary('libc.so.6')
FORMAT = '%(asctime)-15s [thread=%(tid)s] %(message)s'
logging.basicConfig(level=logging.DEBUG, format=FORMAT)
def my_tid_extractor():
tid = libc.syscall(SYS_gettid)
return {'tid': tid}
class CustomLogger(logging.Logger):
def _log(self, level, msg, args, exc_info=None, extra=None):
if extra is None:
extra = my_tid_extractor()
super(CustomLogger, self)._log(level, msg, args, exc_info, extra)
logging.setLoggerClass(CustomLogger)
logger = logging.getLogger('test')
logger.debug('test')
Output sample:
2015-01-20 19:24:09,782 [thread=5017] test
I think you need override LoggerAdapter.process() method
Because the default LoggerAdapter.process method will do nothing, Here is example:
import logging
import random
L=logging.getLogger('name')
class myLogger(logging.LoggerAdapter):
def process(self,msg,kwargs):
return '(%d),%s' % (self.extra['name1'](1,1000),msg) ,kwargs
#put the randint function object
LA=myLogger(L,{'name1':random.randint})
#now,do some logging
LA.debug('some_loging_messsage')
out>>DEBUG:name:(167),some_loging_messsage
I had a similar problem. My solution might be a bit more generic than the accepted one.
I’ve also used a custom logger class, but I did a generic extension that allows me to register adapters after it’s instantiated.
class AdaptedLogger(logging.Logger):
"""A logger that allows you to register adapters on a instance."""
def __init__(self, name):
"""Create a new logger instance."""
super().__init__(name)
self.adapters = []
def _log(self, level, msg, *args, **kwargs):
"""Let adapters modify the message and keyword arguments."""
for adapter in self.adapters:
msg, kwargs = adapter.process(msg, kwargs)
return super()._log(level, msg, *args, **kwargs)
To make you logger use the class you have to instantiate it before it is used elsewhere. For example using:
original_class = logging.getLoggerClass()
logging.setLoggerClass(AdaptedLogger)
logcrm_logger = logging.getLogger("test")
logging.setLoggerClass(original_class)
Then you can register adapters on the instance at any time later on.
logger = logging.getLogger("test")
adapter = logging.LoggerAdapter(logger, extra=my_tid_extractor())
logger.adapters.append(adapter)
Actually the “adapters” can be any object now as long as they have a process-method with a signature compatible with logging.LoggingAdapter.process().