174

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.

vvvvv
  • 25,404
  • 19
  • 49
  • 81
tuergeist
  • 9,171
  • 3
  • 37
  • 58

19 Answers19

226

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. =)

pfa
  • 2,491
  • 1
  • 14
  • 6
  • 8
    This is the better answer because it correctly checks the log level. – Colonel Panic Aug 15 '14 at 18:54
  • 6
    @pfa What about adding `logging.DEBUG_LEVEL_NUM = 9` so you can access that debug level everywhere you import the logger in your code? – edgarstack Aug 31 '16 at 18:56
  • 8
    Definitely instead `DEBUG_LEVEL_NUM = 9` you should defined `logging.DEBUG_LEVEL_NUM = 9`. This way you will be able to use `log_instance.setLevel(logging.DEBUG_LEVEL_NUM)` the same way as you using right know `logging.DEBUG` or `logging.INFO` – maQ Jan 19 '17 at 10:17
  • 2
    This answer has been very helpful. Thank you pfa and EricS. I would like to suggest that for completeness two more statements be included: `logging.DEBUGV = DEBUG_LEVELV_NUM` and `logging.__all__ += ['DEBUGV']` The second is not terribly important but the first is necessary if you have any code which dynamically adjusts the logging level and you want to be able to do something like `if verbose: logger.setLevel(logging.DEBUGV)` ` – Keith Hanlan Dec 19 '18 at 18:45
  • 2
    as this is a runtime definition being added to existing class, do you know if there is a way to make Intellij / Pycharm discover that and allow autocomplete? – bodziec Mar 03 '20 at 10:02
  • 1
    If you employ a logging config file for default handlers, formats, any log level set there can not be overridden and expanded, i.e. set in conf to DEBUG and then add a TRACE. If using logging.config.fileConfig(logConfigFile) either don't set any level, or use level=NOTSET if you want to keep as a placeholder, since NOTSET=0. Then set your level elsewhere, logging.getLogger().setLevel(logging.DEBUG) – chars Sep 01 '22 at 00:52
153

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:

  1. 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.
  2. The new level needs to be added as an attribute to logging itself for consistency: logging.TRACE = logging.DEBUG - 5.
  3. A method called trace needs to be added to the logging module. It should behave just like debug, info, etc.
  4. 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.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Sort the answers by `Oldest`, and you will appreciate that this is the best answer of them all! – Serge Stroobandt Nov 30 '16 at 23:33
  • Thanks. I've done quite a bit of work cobbling something like this together and this QA was very helpful, so I tried to add something on. – Mad Physicist Dec 01 '16 at 14:19
  • 1
    @PeterDolan. Let me know if you have trouble with this. In my personal toolbox I have an extended version that lets you configure how to handle conflicting level definitions. That came up for me once because I like to add a TRACE level, and so does one of the components of sphinx. – Mad Physicist Mar 16 '18 at 18:26
  • @MadPhysicist thank you, will do. I'm likely just adding a couple fairly basic logging levels so it should be fairly straightforward, but if issues come up I'll ping you. Thanks!! – Peter Dolan Mar 16 '18 at 18:29
  • 1
    Is the lack of asterisk in front of `args` in the `logForLevel` implementation intentional / required? – Chris L. Barnes Aug 02 '19 at 14:54
  • 1
    @Tunisia. It's unintentional. Thanks for the catch. – Mad Physicist Aug 02 '19 at 18:12
  • LOL..... `@Tunisia. It's unintentional. Thanks for the catch` It's not unintentional, `_log()` does not accept `*args`, it requires `args` (as a tuple). Passing them as you edited above will not work, you need to remove the `*` from `*args` in your method named `logForLevel`. --- BTW, nice clean writeup :-) – Matt Conway Mar 10 '20 at 17:22
  • 1
    @MattConway. LOL indeed. It's autopilot, 3+ years after the fact. Thanks for the catch. :) I reverted to the correct version. – Mad Physicist Mar 10 '20 at 19:13
  • You should set `kwargs["stacklevel"] = 3` before calling `logging.log()` in order to make the line numbers and function names come out correct with custom formatters. – Tim Sweet Jun 01 '22 at 21:14
  • @TimSweet. In fact, that's a perfect textbook usecase for using `dict.setdefault`. – Mad Physicist Jun 01 '22 at 21:59
  • Nice, I wasn't aware of setdefault, thanks. I ended up using: `kwargs.setdefault("stacklevel",1)` `kwargs["stacklevel"] += 2` to respect passed-in values – Tim Sweet Jun 03 '22 at 01:10
  • 1
    @TimSweet. At that point, use either `kwargs.setdefault('stacklevel', 3)`, which respects the user's explicit choice, or `kwargs['stacklevel'] = kwargs.get('stacklevel', 1) + 2`. Either way, you get a legible one-liner. – Mad Physicist Jun 03 '22 at 01:57
  • I followed this approach and somehow ended up with duplicate log outputs. I was using streamlit, which already has some weird behaviour with `logging`, but I ended up solving my problem by using `logger.propagate = False` (https://stackoverflow.com/a/55877763/4434071) – CryoDrakon Nov 10 '22 at 15:50
  • @CryoDrakon. I'm wondering if you had multiple handlers set up somewhere. Did you try to use the code shown here, or the implementation in haggis? – Mad Physicist Nov 10 '22 at 17:44
72

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
Piotr Dobrogost
  • 41,292
  • 40
  • 236
  • 366
Eric S.
  • 737
  • 5
  • 2
  • 8
    +1 too. An elegant approach, and it worked perfectly. An important note: ***You only need to do this once, in a single module, and it will work for all modules***. You don't even have to import the "setup" module. So toss this in a package's `__init__.py` and be happy :D – MestreLion Sep 28 '12 at 18:01
  • 5
    @Eric S. You should take a look at this answer: http://stackoverflow.com/a/13638084/600110 – Sam Mussmann Nov 30 '12 at 02:20
  • 1
    I agree with @SamMussmann. I missed that answer because this was the top voted answer. – Colonel Panic Aug 15 '14 at 19:02
  • @Eric S. Why do you need args without *? If I do that, I get `TypeError: not all arguments converted during string formatting` but it works fine with *. (Python 3.4.3). Is it a python version issue, or something I'm missing? – Peter Apr 16 '17 at 20:10
  • 3
    This answer does not work for me. Trying to do a 'logging.debugv' gives an error `AttributeError: module 'logging' has no attribute 'debugv'` – Alex Aug 26 '18 at 09:55
49

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)
Wisperwind
  • 963
  • 7
  • 16
  • 1
    This is IMHO the best answer, since it avoids monkey patching. What `get` and `setLoggerClass` exactly do and why they are needed? – Marco Sulla Oct 15 '15 at 12:21
  • 3
    @MarcoSulla They are documented as part of Python's logging module. The dynamic sub-classing, I assume, is used in-case someone wanted their own llogger while using this library. This MyLogger would then become a sub-class of my class, combining the two. – CrackerJack9 Oct 24 '15 at 16:16
  • This is very similar to the solution presented in [this discussion](https://bugs.python.org/issue31732#msg307164) as to whether add a `TRACE` level to the default logging library. +1 – IMP1 Feb 26 '20 at 11:00
28

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.

DerWeh
  • 1,721
  • 1
  • 15
  • 26
  • 2
    It looks great, but the last line is confusing. Shouldn't it be `logging.trace = partial(logging.log, logging.TRACE) # type: ignore` ? – Sergey Nudnov Mar 25 '19 at 17:33
  • 1
    @SergeyNudnov thanks for pointing out, I fixed it. Was a mistake from my side, I just copied from my code and apparently messed up the cleaning. – DerWeh Mar 27 '19 at 08:40
19

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')
schlamar
  • 9,238
  • 3
  • 38
  • 76
  • 24
    Using _log() instead of log() is needed to avoid introducing an extra level in the call stack. If log() is used, the introduction of the extra stack frame causes several LogRecord attributes (funcName, lineno, filename, pathname, ...) to point at the debug function instead of the actual caller. This is likely not the desired result. – rivy Feb 16 '14 at 17:03
  • 7
    Since when is calling a class's own internal methods not permissible? Just because the function is defined outside of the class doesn't mean that it's an external method. – OozeMeister Oct 24 '15 at 19:40
  • 5
    This method not only alters the stack trace unnecessarily, but also does not check that the correct level is being logged. – Mad Physicist Feb 29 '16 at 15:36
  • I feel, what @schlamar says is right, but the counter reason got same number of votes. So what to use ? – Sumit Murari Nov 20 '17 at 04:15
  • 1
    Why wouldn't a method use an internal method? – Gringo Suave Sep 01 '18 at 20:36
9

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.

Noufal Ibrahim
  • 71,383
  • 13
  • 135
  • 169
  • 3
    And you'll probably want to replace `logging.getLogger` to return your subclass instead of the built-in class. – S.Lott Feb 02 '10 at 11:38
  • 6
    @S.Lott - Actually (at least with the present version of Python, maybe it was not the case back in 2010) you have to use [`setLoggerClass(MyClass)`](https://docs.python.org/3/library/logging.html?highlight=logging#logging.setLoggerClass) and then call `getLogger()` as normal... – mac Nov 21 '14 at 16:25
  • IMO, this is by far the best (and most Pythonic) answer, and if I could've given it multiple +1's, I would've. It's simple to execute, however sample code would've been nice. :-D – Deacon Jan 22 '18 at 20:28
  • @DougR.Thanks but like I said, I haven't tried it. :) – Noufal Ibrahim Jan 23 '18 at 04:41
8

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.

LtPinback
  • 506
  • 6
  • 12
  • Wouldn't this have a small performance hit versus subclassing? With this approach, each time some asks for a logger, they'll have to make the setattr call. You'd probably wrap these together in a custom class but nonetheless, that setattr has to be called on every logger created, right? – Matthew Lund Feb 14 '12 at 23:49
  • @Zbigniew below indicated this didn't work, which I think is because your logger needs to make its call to `_log`, not `log`. – marqueed Mar 03 '12 at 01:30
6

Tips for creating a custom logger:

  1. Do not use _log, use log (you don't have to check isEnabledFor)
  2. 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
  3. 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

Bryce Guinta
  • 3,456
  • 1
  • 35
  • 36
4

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.
Gringo Suave
  • 29,931
  • 6
  • 88
  • 75
3

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')
schlamar
  • 9,238
  • 3
  • 38
  • 76
2

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)
2

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).

marqueed
  • 1,083
  • 10
  • 12
  • I don't think this works. Don't you need `logger` as the first arg in `log_at_my_log_level`? – Paul Nov 09 '11 at 00:15
  • Yes, I think you probably would. This answer was adapted from code that solves a slightly different problem. – marqueed Nov 10 '11 at 01:33
1

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,
}
1

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")
0

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

Community
  • 1
  • 1
gerardw
  • 5,822
  • 46
  • 39
0

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"))
tilt
  • 11
  • 2
0

I recombined all the best answers (especially https://stackoverflow.com/a/35804945 and https://stackoverflow.com/a/55276759) from the thread and got IMO universal and most pythonic way below:

import logging
from functools import partial, partialmethod


def add_logging_level(level_name, level_num, method_name=None):
    """
    Comprehensively adds a new logging level to the `logging` module and the
    currently configured logging class.

    `level_name` becomes an attribute of the `logging` module with the value
    `level_num`.
    `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
    -------
    >>> add_logging_level('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 method_name:
        method_name = level_name.lower()

    if hasattr(logging, level_name):
        raise AttributeError(f'{level_name} already defined in logging module')
    if hasattr(logging, method_name):
        raise AttributeError(
            f'{method_name} already defined in logging module'
        )
    if hasattr(logging.getLoggerClass(), method_name):
        raise AttributeError(f'{method_name} already defined in logger class')

    # This method was inspired by the answers to Stack Overflow post
    # http://stackoverflow.com/q/2183233/2988730, especially
    # https://stackoverflow.com/a/35804945
    # https://stackoverflow.com/a/55276759
    logging.addLevelName(level_num, level_name)
    setattr(logging, level_name, level_num)
    setattr(
        logging.getLoggerClass(), method_name,
        partialmethod(logging.getLoggerClass().log, level_num)
    )
    setattr(logging, method_name, partial(logging.log, level_num))

Duiesel
  • 11
  • 2
-3

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
Vasilis Lemonidis
  • 636
  • 1
  • 7
  • 25