6

I have some Python code I need to add logging to.

I've always preferred nice big C macro looking statements like "DEBUG()", "ERROR()", etc for logging. I feel it makes the code easier to read (no objects) when trace-points are visually differentiable from the actual code.

I also would like to be able to set logging levels at a per-module level.

How could I make a module called "log" that is capable of doing this (while making use of the Python std library logging module)?

E.g.:

File: main.py

# This imports LOG_MODULE_NAME, DEBUG, WARN, etc
from log import *
import my_module

LOG_MODULE_NAME("main")

log.set_level("main", log.LVL_DEBUG)
log.set_level("my_module", log.LVL_WARN)

if __name__ == "__main__":
    foo = my_module.myFunc(2)

DEBUG("Exiting main.py")

File: my_module.py

from log import *

LOG_MODULE_NAME("my_module")


def myFunc(x):
    DEBUG("Entering function")
    if x != 1:
         WARN("I thought it would be 1")
    DEBUG("Exiting function")
    return x+1

I'd expect output to look something like:

[WARN:my_module - my_module.py:9] I thought it would be 1
[DEBUG:main - main.py:11] Exiting main.py
codeforester
  • 39,467
  • 16
  • 112
  • 140
BobIsNotMyName
  • 415
  • 4
  • 11
  • possible duplicate of [Elegant setup of Python logging in Django](http://stackoverflow.com/questions/1598823/elegant-setup-of-python-logging-in-django) – dnozay Jul 17 '13 at 21:17
  • 1
    @dnozay It is not. OP is trying to create functions which wrap around a `Logger`. – 2rs2ts Jul 17 '13 at 21:24
  • if you read the question I linked, we don't need an nth question about how to setup logging when different modules are involved. – dnozay Jul 17 '13 at 21:47
  • Please edit your question. As it stands, your current question asks how to write an entire module. Have you tried writing this module? What parts are you stuck on? Using the Python logging module? Getting the name of a module that a function is being called from? Setting up the formatter as you've specified? – Mark Hildreth Jul 17 '13 at 22:58
  • I was stuck on getting a function (DEBUG, WARN, etc) to have behavior dependent on the file/module from which it is called. Rethinking this, I guess I could have asked this in a bit more straight-forward of a way (that has nothing to do with logging). I can't just reference "\__name\__" in log.py, because that would refer to the log module. Also, I'd like to be able to "register" multiple files as a single "module." In any case, after reading 2rs2ts's answer, I think just making use of a global name is the correct way to do this. I'm not sure about impact of "from x import *" though. – BobIsNotMyName Jul 17 '13 at 23:38
  • @BobIsNotMyName `import` gives you access to the code inside a module. It doesn't mean that the namespaces will collide, apparently. At any rate, you don't need nor want to make a module which wraps around `logging`. – 2rs2ts Jul 17 '13 at 23:58

4 Answers4

6

These answers seem to skip the very simple idea that functions are first-class objects which allows for:

def a_function(n):
    pass

MY_HAPPY_NAME = a_function
MY_HAPPY_NAME(15) # which is equivalent to a_function(15)

Except I recommend that you don't do this. PEP8 coding conventions are widely used because life is too short to make me have to read identifiers in the COBOLy way that you like to write them.

Some other answers also use the global statement which is almost always not needed in Python. If you are making module-level logger instances then

import logging

log = logging.getLogger(__name__)

def some_method():
    ...
    log.debug(...)

is a perfectly workable, concise way of using the module-level variable log. You could even do

log = logging.getLogger(__name__)
DEBUG = log.debug

def some_method():
    ...
    DEBUG(...)

but I'd reserve the right to call that ugly.

msw
  • 42,753
  • 9
  • 87
  • 112
  • I totally forgot about how you can take advantage of how functions work in Python. Very, very good call. I agree that it's an awful idea to do what OP's doing as well, but hey, he asked. – 2rs2ts Jul 18 '13 at 05:13
  • Maybe it is a bad idea... But I find the code looks much more cluttered when there are all sorts of "log.debug", "log.warn", etc lines interwoven throughout. Visually, it's hard to discriminate between instrumentation/tracing vs actual useful code. OTOH, "DEBUG", "WARN", etc sometimes actually make the code easier to read (instrumentation begins to look like commenting). – BobIsNotMyName Jul 18 '13 at 15:26
  • @BobIsNotMyName Your approach eliminates the possibility of having multiple loggers with different configurations, though. Unless you add a logger as an argument to your "macro" functions, in which case it's less clear than a method call what you are doing. – 2rs2ts Jul 18 '13 at 16:31
  • @msw, to be fair the only difference between your answer and the "standard" way to do this is `DEBUG = log.debug`. – dnozay Jul 18 '13 at 17:31
4

To expand on the @2rs2ts notion, you can just log by module too as shown in the Python logging HOWTO.

A good convention to use when naming loggers is to use a module-level logger, in each module which uses logging, named as follows:

logger = logging.getLogger(__name__) 

This means that logger names track the package/module hierarchy, and it’s intuitively obvious where events are logged just from the logger name

Make the first 2 lines of all your files this:

import logging
logger = logging.getLogger(__name__) 

Then you have per-module logging that is easily customizable.

You can log to different levels via:

logger.debug("I just saw this %s" , var )
logger.info("I just saw this %s" , var )
logger.warn("I just saw this %s" , var )

Note that I passed the string templating variable as an *arg, and not a % to immediately template the string. If your logger is set to be higher than the level you log at, you save yourself the CPU cycles. There are a handful of tricks like that in the logging docs.

You can set the logging level and where to log on whatever app/script you run; the logging module will handle the rest.

Makoto
  • 104,088
  • 27
  • 192
  • 230
Jonathan Vanasco
  • 15,111
  • 10
  • 48
  • 72
3

If you want to have the logger's name indicate the module in which it was used, you can use the logger.getLogger([name]) module function, and pass __name__ as its (optional) argument, as explained here.

If you want to use names like DEBUG(), do something like this in each of your files...

LOG_MODULE_NAME = logging.getLogger(__name__)

def DEBUG(msg):
    global LOG_MODULE_NAME
    LOG_MODULE_NAME.debug(msg)

I am not clear on the way that global namespaces actually work in Python... this answer says

each module has its own "global" namespace.

So I guess you will be just fine like that, since LOG_MODULE_NAME won't collide between modules.

I believe that this approach will give you one logfile, where the lines will look like this:

DEBUG:my_module:Entering function
WARN:my_module:I thought it would be 1
DEBUG:my_module:Exiting function
DEBUG:root:Exiting main.py

Not instrospective enough? You want the inpsect module, which will give you a lot of information about the program while it's running. This will, for instance, get you the current line number.

Your note about "setting log levels at a per-module level" makes me think that you want something like getEffectiveLevel(). You could try to smush that in like this:

LOG_MODULE_NAME = logging.getLogger(__name__)
MODULE_LOG_LEVEL = log.LVL_WARN

def DEBUG(msg):
    if MODULE_LOG_LEVEL = log.LVL_DEBUG:
        global LOG_MODULE_NAME
        LOG_MODULE_NAME.debug(msg)

I'm not experienced enough with the logging module to be able to tell you how you might be able to make each module's log change levels dynamically, though.

Community
  • 1
  • 1
2rs2ts
  • 10,662
  • 10
  • 51
  • 95
  • I would add that the logging module gives you the ability to customize your logging output to match what you've requested above. – zzzirk Jul 17 '13 at 21:17
  • Originally I had posted a link to the `logging` tutorial, but then I realized OP had actually mentioned the module already so I removed that. – 2rs2ts Jul 17 '13 at 21:22
  • Thanks. Yes, I read a lot about the logging module before posting this. I just wanted to know how to get "DEBUG()" to have behavior based upon the file from which it was called (or more specifically, based upon the string passed to LOG_MODULE_NAME() in said file - as in my example code). Anyway, the way global variables are handled in Python still isn't 100% clear to me, but I think that's the way to go. – BobIsNotMyName Jul 17 '13 at 23:48
  • @BobIsNotMyName You could also use a preprocessor. ;) – 2rs2ts Jul 17 '13 at 23:50
  • 1
    @2rs2ts: Your use of `global` to reach module-scope bindings is bad enough, but `import` inside a conditional is just plain scary. Please see my answer for a more Pythonic approach. – msw Jul 18 '13 at 00:47
  • @msw I didn't write that anywhere, but, you have a point about `global`. – 2rs2ts Jul 18 '13 at 05:12
  • No, you didn't write an import in a conditional; my apologies. – msw Jul 18 '13 at 05:30
  • @2rs2ts, your solution seems to mess up the line numbers, are you able to produce a statement just like in the question: `[DEBUG:main - main.py:11] Exiting main.py` ? – dnozay Jul 19 '13 at 16:32
  • @dnozay What do you mean by "mess up the line numbers"? If you want the line numbers you have to make sure the `format` (e.g. in `logging.basicConfig()`) includes the [`"%(lineno)d"` attribute](http://docs.python.org/2/library/logging.html#logrecord-attributes). – 2rs2ts Jul 19 '13 at 16:35
  • @dnozay The line number is that of the location of the call to `Logger.debug()` or other such logging calls. Read [the documentation](http://docs.python.org/2/library/logging.html#logrecord-attributes): "lineno `%(lineno)d` Source line number where the logging call was issued (if available)." This is the expected behavior. – 2rs2ts Jul 19 '13 at 17:31
  • 1
    I know; which is exactly why this answer doesn't solve the question; @msw's answer is better in that respect where the line number is where the `DEBUG()` call is issued. – dnozay Jul 19 '13 at 17:37
  • @dnozay I can see how that was implicit in OP's question. I thought msw's answer was better too, personally. – 2rs2ts Jul 19 '13 at 18:04
1

The cleaner solution can be found on this other question. The cleaner way to configure logging for a library is to use these few lines in each of your module.

import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())

You main script (e.g. my script.py) is responsible for:


Take your example:

def myFunc(x):
    DEBUG("Entering function")
    if x != 1:
         WARN("I thought it would be 1")
    DEBUG("Exiting function")
    return x+1

and your expected output:

[WARN:my_module - my_module.py:9] I thought it would be 1
[DEBUG:main - main.py:11] Exiting main.py

How do we figure out that the DEBUG("Existing main.py") happened on main.py:11 ? The obvious answer is you cannot unless you inspect the frame and check who the caller is. If you want to define DEBUG() function in a log.py module, then you won't get the correct records. Functions are very different from macros, you have to define a function and there is no inline replacement, so you are effectively not in the same frame anymore.

Just for kicks, I wrote a solution using introspection with the inspect module (note: don't run this as production code), but the reality is that in order to answer the challenges "where was this function called and at what line?" some work needs to be done.

  • bar.py

    # file: bar.py
    # this is just a module to show DEBUG works when used in a different module
    from log import DEBUG
    
    def baz():
        DEBUG('oh hai from baz')
    
  • log.py

    # file: log.py
    import inspect
    import logging
    
    def DEBUG(msg, *args):
        curframe = inspect.currentframe()
        try:
            # http://docs.python.org/2/library/inspect.html#inspect.getouterframes
            _, filename, _, code, _, _ = inspect.getouterframes(curframe, 2)[1]
            logging.debug('[%s:%s] '+msg, filename, code, *args)
        finally:
            del curframe
    
  • main script script.py

    # file: script.py
    # responsible for the logging setup.
    import logging
    from log import DEBUG
    from bar import baz
    
    def foo():
        DEBUG('oh hai')
    
    if __name__ == '__main__':
        logging.basicConfig()
        logging.root.setLevel(logging.DEBUG)
        foo()
        baz()
    

The obvious problem with my solution is that the we only use the root logger:

$ python script.py 
DEBUG:root:[script.py:foo] oh hai
DEBUG:root:[/Users/dnozay/Desktop/bar.py:baz] oh hai from baz

Given all these elements, I would discourage re-implementing the functionality already available in logging.

Community
  • 1
  • 1
dnozay
  • 23,846
  • 6
  • 82
  • 104
  • 1
    @msw, answering this question in the way it was asked will promote bad practices. even @2rs2ts 's answer has changed to promote good practices; `logging.getLogger(__name__)` **is** the best practice. the obvious answer to this question is "don't do that! please follow the tutorial". – dnozay Jul 18 '13 at 17:18