Python3 add logging level
Asked Answered
U

1

7

I have this code which works just fine for me.

import logging
import logging.handlers

logger = None


def create_logger():
    global logger
    logger = logging.getLogger('Logger')
    logger.setLevel(logging.DEBUG)
    handler = logging.handlers.RotatingFileHandler("C:/Users/user/Desktop/info.log", maxBytes=1000000, backupCount=20)
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)


create_logger()
logger.info("Text info")
logger.debug("Text debug")
logger.warning("Text warning")
logger.error("Text error")
logger.critical("Text critical")

And the output looks great:

2017-12-19 15:06:43,021 - Logger - INFO - Text info
2017-12-19 15:06:43,021 - Logger - DEBUG - Text debug
2017-12-19 15:06:43,022 - Logger - WARNING - Text warning
2017-12-19 15:06:43,022 - Logger - ERROR - Text error
2017-12-19 15:06:43,022 - Logger - CRITICAL - Text critical

Well, I want to add a new logging level like this:

logger.message("Text message")  

And the output should be like this

2017-12-19 15:06:43,022 - Logger - MESSAGE - Text message

Uticas answered 19/12, 2017 at 14:12 Comment(2)
How is "message" different than "info" or any of the other levels?Diamine
see also here: #2183733 for a detailed discussion how to add new logging levels.Foretopsail
A
25

From Logging documentation (emphasis added):

Defining your own levels is possible, but should not be necessary, as the existing levels have been chosen on the basis of practical experience. However, if you are convinced that you need custom levels, great care should be exercised when doing this, and it is possibly a very bad idea to define custom levels if you are developing a library. That’s because if multiple library authors all define their own custom levels, there is a chance that the logging output from such multiple libraries used together will be difficult for the using developer to control and/or interpret, because a given numeric value might mean different things for different libraries.

An overview of default logging levels:

enter image description here

But if you still want to, you can make your own log level:

In the logging-module, _levelToName and _nameToLevel are mappings between logging names and levels. Instead of manually adding to them, the addLevelName() function does this for you.

Here, a new logging level called MESSAGE is added with log level 25:

import logging

# Define MESSAGE log level
MESSAGE = 25

# "Register" new loggin level
logging.addLevelName(MESSAGE, 'MESSAGE')  # addLevelName(25, 'MESSAGE')

# Verify
assert logging.getLevelName(MESSAGE) == 'MESSAGE'

If you don't want to make your own logger class but still wants to log other log levels, then you can use the Logger.log(level, msg)-method on the traditional loggers:

logging.log(MESSAGE, 'This is a message')

EDIT: Add message directly

 def message(self, msg, *args, **kwargs):
    if self.isEnabledFor(MESSAGE):
        self._log(MESSAGE, msg, args, **kwargs) 

Make the message()-function available in logging:

 logging.message = message
 # or setattr(logging, 'message', message)

Make the message()-function available in the logger:

 logging.Logger.message = message
 # or setattr(logging.Logger, 'message', message)

Make custom Logger class

You could make your own logger class to make a message(msg)-method, to be used similarily as the others (e.g. info(msg), warning(msg), etc.)

In the following example, a new logger is made with a message(msg)-method to log MESSAGE:

class MyLogger(logging.Logger):
    def message(self, msg, *args, **kwargs):
        if self.isEnabledFor(MESSAGE):
            self._log(MESSAGE, msg, args, **kwargs)

Get logger

I'm not sure of what's the best way to make it work with logging.getLogger(name), but below are two approaches. Ref. comments, I believe the first approach is better:

Either make the new logger the default logging class, which means new logger instances will be of the MyLogger class instead of the default logging.Logger class:

logging.setLoggerClass(MyLogger)
logger = logging.getLogger('A new logger name')
logger.message('This seems to work')
assert isInstance(logger, MyLogger)

Or just make an instance of the logger and add it to the loggerDict in the active logging.Manager instance (EDIT: not recommended, see comments):

my_logger = MyLogger('Foo')
logging.Logger.manager.loggerDict['Foo'] = my_logger
logger = logging.getLogger('Foo')
logger.message('This is the same instance as my_logger')
assert logger is my_logger

Use the new log level

# Use the new logger class
logger.warning('Custom log levels might be a bad idea')
logger.message('Here is a message')
# Log with custom log level:
logger.log(MESSAGE, 'This is a message')

This assumes that MESSAGE is predefined as an integer numerical value representing the log level. (E.g. 25 as previously mentioned)

Ajar answered 20/12, 2017 at 18:13 Comment(3)
Generally your answer is good, but some of the things you suggest involve use of logging internals, which might not be a good idea. For example, instantiating a logger instance (MyLogger) directly, or manually updating the manager's loggerDict.Signally
Thanks for the feedback. I've updated my answer with the intentions of addressing your comment, but I'd love any additional feedback,.Ajar
Great explanation! Covers everything I was looking for. Plus one for that.Lavadalavage

© 2022 - 2024 — McMap. All rights reserved.