I have been using a custom formatter for logging to the terminal in my code. Lately I have been changing stuff in the code an I can't find why now in some parts of the code the log is printed twice.
This is the code for the custom formatter:
import logging
class MyFormatter(logging.Formatter):
debug_format = "[%(levelname)s] (%(module)s::%(funcName)s::%(lineno)d) %(message)s"
normal_format = "[%(levelname)s] %(message)s"
blue = "\x1b[36;21m"
grey = "\x1b[38;21m"
yellow = "\x1b[33;21m"
red = "\x1b[31;21m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
def __init__(self):
super().__init__(fmt="%(levelno)d: %(msg)s", datefmt=None, style="%")
def format(self, record):
# Save the original format configured by the user
# when the logger formatter was instantiated
format_orig = self._style._fmt
# Replace the original format with one customized by logging level
if record.levelno == logging.DEBUG:
self._style._fmt = MyFormatter.debug_format
format = MyFormatter.debug_format
else:
self._style._fmt = MyFormatter.normal_format
format = MyFormatter.normal_format
self.FORMATS = {
logging.DEBUG: MyFormatter.grey + format + MyFormatter.reset,
logging.INFO: MyFormatter.blue + format + MyFormatter.reset,
logging.WARNING: MyFormatter.yellow + format + MyFormatter.reset,
logging.ERROR: MyFormatter.red + format + MyFormatter.reset,
logging.CRITICAL: MyFormatter.bold_red + format + MyFormatter.reset,
}
log_fmt = self.FORMATS.get(record.levelno)
# Restore the original format configured by the user
self._style._fmt = format_orig
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
This is how I create my logger:
from src.logs import set_logger, logging
logger = set_logger(__name__, logging.DEBUG)
This is set_logger function code:
import logging
from .custom_formatter import MyFormatter
def set_logger(module_name: str, level=logging.DEBUG) -> logging.Logger:
logger = logging.getLogger(module_name)
logger.setLevel(level)
stream_handler = logging.StreamHandler()
formatter = MyFormatter()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
return logger
Now when I call this logger from main for example or at the top of a module which is imported, there is no problem, and it logs perfectly only once. However when calling the logger from inside a function in the same module it is printed twice.
I have notice by debugging that what is doing is going to the end of the format method in MyFormatter class and then it returns again to this format method, I have no clue what is going on here. Do you have any ideas on what could be happening?
PD: Also if I also call a print when the logger prints twice I only get one print, so that code runs only once for sure.
Thanks for your time!
Andrés
In set_logger(), it calls addHandler() but the logger (or an ancestor logger) will already have a handler, which you're not removing, so you'll have multiple handlers.
Have a look at the docs for Logger.propagate: https://docs.python.org/3/library/logging.html#logging.Logger.propagate
I'm having a python script for accessing/manipulating network devices with netmiko. To speed up the processing, I'm using now tqdm and a process pool for multiprocessing.
This is working fine except the logging stuff. The logger works fine for the main, but not for the processes created by tqdm.
The important code snippets are as follows:
def threaded_function(params):
"""the custom function
:param kwargs params: device details packed as keyword arguments
:returns: dictionary containing the results of each step
"""
logging.debug('TEST')
logging.info('TEST2')
logging.error('TEST3')
def main_loop():
logging.getLogger("netmiko").propagate = False
logging.getLogger("netmiko").disabled = True
logging.getLogger("paramiko").propagate = False
logging.getLogger("paramiko").disabled = True
log_time = datetime.now().strftime("%Y%m%d-%H%M%S")
LOG_FILE = os.path.basename(input_file) + '.txt'
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
if args.log2file:
log_format_file = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
rfh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1048576, backupCount=10)
rfh.setLevel(args.filelevel)
rfh.setFormatter(log_format_file)
logger.addHandler(rfh)
if args.log2con:
log_format_con = logging.Formatter('%(message)s')
ch = logging.StreamHandler()
ch.setLevel(args.conlevel)
ch.setFormatter(log_format_con)
logger.addHandler(ch)
if not args.log2file and not args.log2con:
logger.disabled = True
logger.propagate = False
logging.info("JOB STARTED: " + str(log_time))
pool = multiprocessing.Pool(max_cpu_count)
with tqdm(total=len(input_list), ascii=True, unit="dev", desc='processing',
disable=(False if args.log2con else True)) as t:
for _ in pool.imap_unordered(threaded_function, input_list):
t.update(1)
results.append(_)
if __name__ == "__main__":
main_loop()
Does anyone has an idea, where I'am wrong? I tried to pass a queue according the 2nd example from this doc (https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes) but no luck..
any help is appreciated
regs nick
i try to use python logging module to create a timed rotating handler to some sort cache to a file.
eventually i need to know what is the cache file name.
i cannot find any property in logger to retrieve it.
so i create kind of monkey job to overload the logger for later retrieve.
anybody know a better solution?
def srvcacheconfig(**kwargs):
srv_name = kwargs['server_name']
# cache_path = CACHEPATH
# ans=dict()
cache_filename = srv_name+CACHE_EXT
cache_logger = logging.getLogger(srv_name)
cache_logger.propagate = False
#monkey job
cache_logger.cache_name = cache_filename
cache_format = logging.Formatter("%(message)s")
# cache_handlers = logging.handlers.TimedRotatingFileHandler(cache_filename, when = 'midnight', backupCount = 366)
cache_handlers = logging.handlers.TimedRotatingFileHandler(cache_filename, when = 'M', interval = 1, backupCount = 200)
cache_handlers.setFormatter(cache_format)
cache_logger.addHandler(cache_handlers)
# consoleHandler = logging.StreamHandler()
# cache_logger.addHandler(consoleHandler)
cache_logger.setLevel(logging.DEBUG)
return cache_logger
thank you very much
The TimedRotatingFileHandler has a property named baseFilename, which holds the name of the logfile. However, this is not documented and may therefore change without further notice.
From this property you can get the logfile's name:
import logging
from logging.handlers import TimedRotatingFileHandler
my_logger = logging.getLogger('my_logger')
my_handler = TimedRotatingFileHandler('logfile.log')
my_logger.addHandler(my_handler)
def get_filename(some_logger):
return [some_handler.baseFilename
for some_handler in some_logger.handlers
if isinstance(some_handler, TimedRotatingFileHandler)]
print(get_filename(my_logger)) # ['/home/finwood/temp/so/logfile.log']
My current format string is:
formatter = logging.Formatter('%(asctime)s : %(message)s')
and I want to add a new field called app_name which will have a different value in each script that contains this formatter.
import logging
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.addHandler(syslog)
But I'm not sure how to pass that app_name value to the logger to interpolate into the format string. I can obviously get it to appear in the log message by passing it each time but this is messy.
I've tried:
logging.info('Log message', app_name='myapp')
logging.info('Log message', {'app_name', 'myapp'})
logging.info('Log message', 'myapp')
but none work.
You could use a LoggerAdapter so you don't have to pass the extra info with every logging call:
import logging
extra = {'app_name':'Super App'}
logger = logging.getLogger(__name__)
syslog = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(syslog)
logger = logging.LoggerAdapter(logger, extra)
logger.info('The sky is so blue')
logs (something like)
2013-07-09 17:39:33,596 Super App : The sky is so blue
Filters can also be used to add contextual information.
import logging
class AppFilter(logging.Filter):
def filter(self, record):
record.app_name = 'Super App'
return True
logger = logging.getLogger(__name__)
logger.addFilter(AppFilter())
syslog = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(syslog)
logger.info('The sky is so blue')
produces a similar log record.
Python3
As of Python3.2 you can now use LogRecordFactory
import logging
logging.basicConfig(format="%(custom_attribute)s - %(message)s")
old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
record.custom_attribute = "my-attr"
return record
logging.setLogRecordFactory(record_factory)
>>> logging.info("hello")
my-attr - hello
Of course, record_factory can be customized to be any callable and the value of custom_attribute could be updated if you keep a reference to the factory callable.
Why is that better than using Adapters / Filters?
You do not need to pass your logger around the application
It actually works with 3rd party libraries that use their own logger (by just calling logger = logging.getLogger(..)) would now have the same log format. (this is not the case with Filters / Adapters where you need to be using the same logger object)
You can stack/chain multiple factories
You need to pass the dict as a parameter to extra to do it that way.
logging.info('Log message', extra={'app_name': 'myapp'})
Proof:
>>> import logging
>>> logging.basicConfig(format="%(foo)s - %(message)s")
>>> logging.warning('test', extra={'foo': 'bar'})
bar - test
Also, as a note, if you try to log a message without passing the dict, then it will fail.
>>> logging.warning('test')
Traceback (most recent call last):
File "/usr/lib/python2.7/logging/__init__.py", line 846, in emit
msg = self.format(record)
File "/usr/lib/python2.7/logging/__init__.py", line 723, in format
return fmt.format(record)
File "/usr/lib/python2.7/logging/__init__.py", line 467, in format
s = self._fmt % record.__dict__
KeyError: 'foo'
Logged from file <stdin>, line 1
Another way is to create a custom LoggerAdapter. This is particularly useful when you can't change the format OR if your format is shared with code that does not send the unique key (in your case app_name):
class LoggerAdapter(logging.LoggerAdapter):
def __init__(self, logger, prefix):
super(LoggerAdapter, self).__init__(logger, {})
self.prefix = prefix
def process(self, msg, kwargs):
return '[%s] %s' % (self.prefix, msg), kwargs
And in your code, you would create and initialize your logger as usual:
logger = logging.getLogger(__name__)
# Add any custom handlers, formatters for this logger
myHandler = logging.StreamHandler()
myFormatter = logging.Formatter('%(asctime)s %(message)s')
myHandler.setFormatter(myFormatter)
logger.addHandler(myHandler)
logger.setLevel(logging.INFO)
Finally, you would create the wrapper adapter to add a prefix as needed:
logger = LoggerAdapter(logger, 'myapp')
logger.info('The world bores you when you are cool.')
The output will look something like this:
2013-07-09 17:39:33,596 [myapp] The world bores you when you are cool.
I found this SO question after implementing it myself. Hope it helps someone. In the code below, I'm inducing an extra key called claim_id in the logger format. It will log the claim_id whenever there is a claim_id key present in the environment. In my use case, I needed to log this information for an AWS Lambda function.
import logging
import os
LOG_FORMAT = '%(asctime)s %(name)s %(levelname)s %(funcName)s %(lineno)s ClaimID: %(claim_id)s: %(message)s'
class AppLogger(logging.Logger):
# Override all levels similarly - only info overriden here
def info(self, msg, *args, **kwargs):
return super(AppLogger, self).info(msg, extra={"claim_id": os.getenv("claim_id", "")})
def get_logger(name):
""" This function sets log level and log format and then returns the instance of logger"""
logging.setLoggerClass(AppLogger)
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
return logger
LOGGER = get_logger(__name__)
LOGGER.info("Hey")
os.environ["claim_id"] = "12334"
LOGGER.info("Hey")
Gist: https://gist.github.com/ygivenx/306f2e4e1506f302504fb67abef50652
If you need a default extra mapping, and you want to customize it for ad-hoc log messages, this works in Python 2.7+ by creating a LoggerAdapter that merges a default extra dictionary with any extra from a given message.
import logging
import os
import sys
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)-8s Py%(python)-4s pid:%(pid)-5s %(message)s',
)
_logger = logging.getLogger("my-logger")
_logger.setLevel(logging.DEBUG)
class DefaultExtrasAdapter(logging.LoggerAdapter):
def __init__(self, logger, extra):
super(DefaultExtrasAdapter, self).__init__(logger, extra)
def process(self, msg, kwargs):
# Speed gain if no extras are present
if "extra" in kwargs:
copy = dict(self.extra).copy()
copy.update(kwargs["extra"])
kwargs["extra"] = copy
else:
kwargs["extra"] = self.extra
return msg, kwargs
LOG = DefaultExtrasAdapter(_logger, {"python": sys.version_info[0], "pid": os.getpid()})
if __name__ == "__main__":
LOG.info("<-- With defaults")
LOG.info("<-- With my version", extra={"python": 3.10})
LOG.info("<-- With my pid", extra={"pid": 0})
LOG.info("<-- With both", extra={"python": 2.7, "pid": -1})
Results:
2021-08-05 18:58:27,308 INFO Py2 pid:8435 <-- With defaults
2021-08-05 18:58:27,309 INFO Py3.1 pid:8435 <-- With my version
2021-08-05 18:58:27,309 INFO Py2 pid:0 <-- With my pid
2021-08-05 18:58:27,309 INFO Py2.7 pid:-1 <-- With both
The accepted answer did not log the format in logfile, whereas the format was reflected in sys output.
Alternatively I used a simpler approach and worked as;
logging.basicConfig(filename="mylogfile.test",
filemode="w+",
format='%(asctime)s: ' +app_name+': %(message)s ',
level=logging.DEBUG)
Using mr2ert's answer, I came up with this comfortable solution (Though I guess it's not recommended) - Override the built-in logging methods to accept the custom argument and create the extra dictionary inside the methods:
import logging
class CustomLogger(logging.Logger):
def debug(self, msg, foo, *args, **kwargs):
extra = {'foo': foo}
if self.isEnabledFor(logging.DEBUG):
self._log(logging.DEBUG, msg, args, extra=extra, **kwargs)
*repeat for info, warning, etc*
logger = CustomLogger('CustomLogger', logging.DEBUG)
formatter = logging.Formatter('%(asctime)s [%(foo)s] %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.debug('test', 'bar')
Output:
2019-03-02 20:06:51,998 [bar] test
This is the built in function for reference:
def debug(self, msg, *args, **kwargs):
"""
Log 'msg % args' with severity 'DEBUG'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
"""
if self.isEnabledFor(DEBUG):
self._log(DEBUG, msg, args, **kwargs)
import logging;
class LogFilter(logging.Filter):
def __init__(self, code):
self.code = code
def filter(self, record):
record.app_code = self.code
return True
logging.basicConfig(format='[%(asctime)s:%(levelname)s]::[%(module)s -> %(name)s] - APP_CODE:%(app_code)s - MSG:%(message)s');
class Logger:
def __init__(self, className):
self.logger = logging.getLogger(className)
self.logger.setLevel(logging.ERROR)
#staticmethod
def getLogger(className):
return Logger(className)
def logMessage(self, level, code, msg):
self.logger.addFilter(LogFilter(code))
if level == 'WARN':
self.logger.warning(msg)
elif level == 'ERROR':
self.logger.error(msg)
else:
self.logger.info(msg)
class Test:
logger = Logger.getLogger('Test')
if __name__=='__main__':
logger.logMessage('ERROR','123','This is an error')
I have been trying for some time to figure out how to create a custom function for the python logging module. My goal is, with the usual function such as logging.debug(...) a log message over several channels, such as Telegram or MQTT, to publishing. So my idea is to add extra arguments to the normal log methode. For example logging.debug ("a log", telegram=True, mqtt=False) and maybe other arguments. All I find is the inheritance of the class logging.StreamingHandler and then using the method emit, but this only passes the argument record. So how can I implement my problem in a meaningful way? Do I have a thinking error or the wrong approach?
I solved my problem by creating a interface for the logging module.
A small view on my code:
# ulogging.py
import logging
import telegram
def uloggingConfig(*args, **kwargs):
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# general logging config section
fmt = kwargs.pop("fmt", "%(asctime)s %(levelname)s %(message)s")
datefmt = kwargs.pop("datefmt", "%m.%d.%Y %I:%M:%S %p")
streamHandler = logging.StreamHandler()
streamHandler.setLevel(logging.DEBUG)
formater = logging.Formatter(fmt=fmt, datefmt=datefmt)
streamHandler.setFormatter(formater)
logger.addHandler(streamHandler)
# telegram config section
telegramBot = kwargs.pop("teleBot", None)
telegramChatID = kwargs.pop("teleChatID", None)
telegramLevel = kwargs.pop("teleLevel", logging.INFO)
telegramFmt = kwargs.pop("telefmt", "%(message)s")
telegramDatefmt = kwargs.pop("teledatefmt", None)
if telegramBot is not None and telegramChatID is not None:
telegramStream = TelegramStream(telegramBot, telegramChatID)
formater = logging.Formatter(fmt=telegramFmt, datefmt=telegramDatefmt)
telegramStream.setFormatter(formater)
telegramStream.setLevel(telegramLevel)
logger.addHandler(telegramStream)
elif (telegramBot is not None and telegramChatID is None) or (telegramBot is None and telegramChatID is not None):
raise KeyError("teleBot and teleChatID have to be both given")
if kwargs:
keys = ', '.join(kwargs.keys())
raise ValueError('Unrecognised argument(s): %s' % keys)
return logger
def getLogger():
return logging.getLogger(__name__)
class TelegramStream(logging.StreamHandler):
def __init__(self, telegramBot, telegramChatID):
logging.StreamHandler.__init__(self)
self._bot = telegramBot
self._chatID = telegramChatID
def emit(self, record):
if record.levelname == "DEBUG":
self._bot.send_message(self._chatID, record.levelname + ": " + record.msg)
else:
self._bot.send_message(self._chatID, record.msg)
With the uloggingConfig() method I can now pass all settings for the different handlers (at the moment only for telegram, further handlers should follow). The uloggingConfig() method then takes over the configuration and returns a logger with which log messages can be created as usual.
A simple example:
logger = ulogging.uloggingConfig(fmt="format, datefmt="dateformat, teleBot=telegramBot, teleChatID=telegramChatID, teleLevel=logging.DEBUG)
logger.debug("A log message")