Skip to content

logging: Improve the logging module. #507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 0 commits into from

Conversation

iabdalkader
Copy link
Contributor

@iabdalkader iabdalkader commented Jul 16, 2022

  • Add support for all format specifiers, support for datefmt using strftime, and support for Stream and File handlers.
  • Ports/boards that need to use FileHandlers should enable MICROPY_PY_SYS_ATEXIT, and enable MICROPY_PY_SYS_EXC_INFO if using logging.exception().
  • Uses time: Add time module to provide strftime. #508 if available.

@iabdalkader iabdalkader force-pushed the logging_update branch 2 times, most recently from b1112b6 to ce952a6 Compare July 16, 2022 16:05
@andrewleech
Copy link
Contributor

Thanks for this, a cleanup of the logging module has been on my shortlist of todo so I'm quite interested.

On an initial quick skim through the changelog this looks like a good broad cleanup, with sensible additions added without too much extra "code bulk".

I've had a couple of early planning discussions with colleagues about this module and they always end up swinging back and forth between which features will be good for most users, but also small enough to fit on most ports/boards.

At first glance I feel there's a pretty good balance here! the included handlers are fairly simple, but should also provide a good base (and example) to be extended upon by individual users to meet their project. I'm also inclined to think that more complex handlers (eg. size limited and timestamped rotating files) could happily go in separate packages.

I'm keen to review and test in more depth in then coming days (hopefully) but in the mean time, can you confirm whether the included classes confirm to (a sunset of) the cpython logging module? By memory they look good, but just want to check.

@iabdalkader
Copy link
Contributor Author

iabdalkader commented Jul 17, 2022

Yes, this conforms to CPython logging (I've implemented it following the docs), both the API and functionality are compatible (if you run it on CPython you get the exact same output in stream/file).

think that more complex handlers (eg. size limited and timestamped rotating files) could happily go in separate packages

That's a good idea, more handlers can be added in a separate module and imported in __init__.py something like this:

try
 from logging_handlers import *
except:
   pass

@iabdalkader
Copy link
Contributor Author

Updated.

@iabdalkader
Copy link
Contributor Author

Does this run every time logging is imported ?

if hasattr(sys, "atexit"):
    sys.atexit(shutdown)

@dpgeorge
Copy link
Member

Does this run every time logging is imported ?

No, a module is only ever executed once, the first time it is imported. So that atexit register will run only once.

@iabdalkader
Copy link
Contributor Author

No, a module is only ever executed once, the first time it is imported. So that atexit register will run only once.

Yeah just wanted to double check. I think it's ready now, unless there's something else ?

Would be nice if this is available too with the updated logging: #508

@stinos
Copy link

stinos commented Nov 17, 2022

While at it could you perhaps add

def addLevelName(level, name):
    _level_dict[level] = name

and

def log(level, msg, *args):
    getLogger().log(level, msg, *args)

? I'm using these for CPython compatibility.

@dpgeorge
Copy link
Member

Testing this, I see that the basic use where you just start logging doesn't work (at least it doesn't match CPython). Eg:

import logging
logging.warning('test')

The above use-case should match CPython (ie the default logger should print out with the default formatting).

@ThinkTransit
Copy link
Contributor

If handlers are being added can a RotatingFileHandler be considered?

Ported from cpython

class RotatingFileHandler(Handler):
    def __init__(self, filename, maxsize=128_000, backupCount=5):
        if maxsize < 256:
            raise ValueError("maxsize must be at least 256 B")
        self.filename = filename
        self.maxsize = maxsize
        self.backupCount = backupCount
        self.formatter = Formatter()

        try:
            # checks if file exists and prevents overwriting it
            self.pos = os.stat(self.filename)[6]
            self.file = open(filename, "r+")
        except OSError:
            self.pos = 0
            self.file = open(filename, "w+")

        self.file.seek(self.pos)

    def shouldRollover(self, record):
        """
        Determine if rollover should occur.
        Basically, see if the supplied record would cause the file to exceed
        the size limit we have.
        """
        message = self.format(record)
        if len(message) + self.pos > self.maxsize:
            return True
        else:
            return False


    def doRollover(self):
        """
        Do a rollover.
        """
        # Close current file first
        self.file.close()

        if self.backupCount > 0:
            for i in range(self.backupCount - 1, 0, -1):
                sfn = "%s.%d" % (self.filename, i)
                dfn = "%s.%d" % (self.filename, i + 1)
                try:
                    os.rename(sfn, dfn)
                except:
                    print(f"Cant move file {sfn} to {dfn}")

            dfn = self.filename + ".1"
            try:
                os.rename(self.filename, dfn)
            except:
                print(f"Couldnt move file {self.filename} to {dfn}")

        print("Rollover complete")
        self.pos = 0
        self.file = open(self.filename, "w+")
        self.file.seek(self.pos)

@iabdalkader
Copy link
Contributor Author

basic use where you just start logging doesn't work

@dpgeorge Fixed, thanks.

While at it could you perhaps add

@stinos Added.

If handlers are being added can a RotatingFileHandler be considered?

@ThinkTransit Handlers will be added, just not in this PR, and probably in a separate package.

@iabdalkader
Copy link
Contributor Author

iabdalkader commented Nov 17, 2022

Just a minor optimization, this will only init the root logger and only if it's not initialized.

def getLogger(name="root"):
    if name not in loggers:
        loggers[name] = Logger(name)
        if name == "root":
            basicConfig()
    return loggers[name]

So basic example will init root logger:

import logging
logging.warning('test')

Creating a logger wont:

logging.getLogger(__name__)

@iabdalkader
Copy link
Contributor Author

ping @dpgeorge

@dpgeorge
Copy link
Member

Creating a logger wont

So this means the following code (with this PR as it currently is) won't print anything:

>>> import logging
>>> logging.getLogger('log').warning('test')

Whereas the code before this PR did print a message, and CPython does as well. I think it should work like it did before (and hence be CPython compatible).

@iabdalkader iabdalkader force-pushed the logging_update branch 2 times, most recently from 06575c8 to f3ab1fb Compare November 25, 2022 07:48
@iabdalkader
Copy link
Contributor Author

I think it should work like it did before (and hence be CPython compatible).

It was Not CPython compatible because it printed a logger name, level etc.. where as CPython would just print the message, but now it is. There's no global _level though, but feel free to add it if you want.

Pushed an update to fix the other comments.

@iabdalkader
Copy link
Contributor Author

Would it be better if #508 is included in logging ?

@iabdalkader
Copy link
Contributor Author

Alright, removed strftime.py, moved examples to examples/ (I added the basic test case you mentioned), reverted the change in logging.py (it will print None if there's no strftime).

Maybe it's better if we merge #508... I'll will look into that.

Yes please do, log time is very important.

@dpgeorge
Copy link
Member

dpgeorge commented Dec 1, 2022

This is getting better, however it still doesn't match the existing behaviour of this module or CPython for the existing example.

The existing example with the existing library had this output:

INFO:test:Test message2: 100(foobar)
WARN:test:Test message3: %d(%s)
ERROR:test:Test message4
CRIT:test:Test message5
INFO:root:Test message6
ERROR:test:Some trouble (expected)
Traceback (most recent call last):
  File "x.py", line 13, in <module>
ZeroDivisionError: divide by zero
levelname=INFO name=root message=Test message7

Which is quite close to CPython's output run against that same example.

With the new library here, that example has the output:

Test message: %d(%s)
Test message2: %d(%s)
Test message3: %d(%s)
Test message4
Test message5
Some trouble (%s)

@iabdalkader please can you add back the existing example as a separate example file in the examples directory, and then make it so the new library run on that example gives similar output to before (which should be similar or very close to the output CPython gives on that example).

@iabdalkader iabdalkader force-pushed the logging_update branch 2 times, most recently from ef3305c to b2fa953 Compare December 1, 2022 08:11
@iabdalkader
Copy link
Contributor Author

please can you add back the existing example as a separate example

@dpgeorge I did, it's in examples/example_logging_1.py and I found some issues and fixed them. The log should be identical to CPython now (with the exception of the longer backtrace exception from CPython):

$ mpremote mount . run examples/example_logging_1.py
Local directory . is mounted at /remote
INFO:test:Test message2: 100(foobar)
WARNING:test:Test message3: %d(%s)
ERROR:test:Test message4
CRITICAL:test:Test message5
INFO:root:Test message6
ERROR:test:Some trouble (expected)
Traceback (most recent call last):
  File "<stdin>", line 13, in <module>
ZeroDivisionError: divide by zero
INFO:root:Test message7
levelname=INFO name=root message=Test message7

The existing example with the existing library had this output:

Note I'm aiming for max compatibility with CPython Not with the old logging library (which is missing basic things like logging.warning), so I can't use the old library for reference, for example the old library doesn't print "message7" CPython prints it and so does the updated module.

@stinos
Copy link

stinos commented Dec 1, 2022

Regarding CPython-compatibility: I only now ran this implementation against a bunch of code I have and it seems there are some things which might be worth making compatible since it's new code anyway. Sorry I'm somewhat late with this, and I probably didn't check everything, but here are some things I ran into:

  • default argument for getLogger in CPython is None (which means 'get root logger'), but in this implementation getLogger(None) returns a Logger with name None instead of 'root'.
  • same for basicConfig and Formatter and StreamHandler and so on, which mostly have None as default arguments (this one I didn't actually run into, but is also simpler to implement anyway: for example instead of repeating fmt=_default_fmt in a couple of places, you just use fmt=None everywhere to indicate you want the default format, and then there's only one single place, Formatter.__init__, where this gets interpreted as _default_fmt).
  • not officially documented but the default format in CPYthon is logging.BASIC_FORMAT and other implementations use that as well. So since we're adding constants anyway, why not use the same name?
  • Logger.__init__ has level as last argument
  • the basicConfig implementation doesn't do the same as CPython's. Didn't figure out what exactly but from the looks of it, force=True does not remove all handlers it just closes them, plus level and format are only applied to the new handler created instead of to all existing handlers.

@iabdalkader
Copy link
Contributor Author

default argument for getLogger in CPython is None
Logger.init has level as last argument
force=True does not remove all handlers it just closes them
same for basicConfig and Formatter and StreamHandler and so on, which mostly have None as default arguments

The above should be fixed now.

the default format in CPYthon is logging.BASIC_FORMAT and

Not changing this because we'll then prefix it with an underscore, and it will be different anyway so I don't see the point.

Note I'm reluctant to change anything else in this PR, unless it's a bug, I don't want to risk breaking something else which will result in another review/fix cycle, and I really need a usable logging in micropython-lib as soon as possible.

@dpgeorge
Copy link
Member

dpgeorge commented Dec 1, 2022

Note I'm reluctant to change anything else in this PR, unless it's a bug

Yes I agree, it would be good to finalise on this PR, and anything else can be an extension done as a follow up.

@stinos
Copy link

stinos commented Dec 1, 2022

I understand, but in my opinion the very basics should at least work.

import logging
import sys

root = logging.getLogger('root')
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
logging.info('hello upy')
logging.shutdown()

part deleted, didn't test with last version

So I really think the minimal basicConfig implementation should at least be somewhat sound.

@iabdalkader
Copy link
Contributor Author

iabdalkader commented Dec 1, 2022

So I really think the minimal basicConfig implementation should at least be somewhat sound.

When you first call getLogger("root") it sets up the root logger using the default log level WARNING (same default level in CPython), the following call to basicConfig() without force=True is ignored, as it should be according to the docs:

This function does nothing if the root logger already has handlers configured, unless the keyword argument force is set to True.

So logging.info(), which is lower than warning, shouldn't print anything, as expected, can you explain why it should be otherwise ?

Running your example on CPython does print something, not sure why, and more interesting if you change it to the following, it doesn't print anything.

root = logging.getLogger('root')
logging.info('hello upy')
logging.basicConfig(level=logging.DEBUG)
logging.info('hello upy')

part deleted, didn't test with last version

I think I forgot to push after the latest changes, using None etc... but it's updated now.

EDIT: Maybe by default CPython doesn't add any handlers to the root longer, in that case the above would print the log.

@stinos
Copy link

stinos commented Dec 1, 2022

can you explain why it should be otherwise

Only because I ran it in CPython and it printed output :) But actually the code isn't a real usecase, I only started typing it because I wanted to show that the previous version which didn't clear all handlers was an issue.

But with that fixed it's ok as far as I'm concerned.

By default CPython doesn't add any handlers to the root longer

It does but only upon the first logging statement issued. But I don't think that matters.

What might matter though: in CPython logging.getLogger(None) is the one and only root logger, which is the parent of all other loggers, and hence which is not the the same as getLogger('root') (it is the same as logging.root though but that's an implementation detail). So configuring formatting globally on the root logger (which I think is rather common, and in any case it seems to make sense to me - with some bias becuase I do have multiple projects doing this) like:

logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
for handler in logging.getLogger().handlers:
    handler.setFormatter(logging.Formatter("[%(levelname)s]:%(name)s:%(message)s"))
logging.info('hello upy')
logging.getLogger('child').info('hello 2')

outputs

[INFO]:root:hello upy
[INFO]:child:hello 2

in CPython but

[INFO]:root:hello upy
INFO:child:hello 2

in MicroPython, and that last line only appears because there's a print statement in Logger, it doesn't do propagation like CPython does. However since that was already the case before this shouldn't be changed in this PR.

@iabdalkader
Copy link
Contributor Author

iabdalkader commented Dec 1, 2022

there's a print statement in Logger, it doesn't do propagation like CPython does. However since that was already the case before this shouldn't be changed in this PR.

That's an interesting example that shows a shortcoming of both the old and new logger, yes the print statement outputs the second line instead of propagating the log to the higher loggers (and to the root logger which prints that second line on CPython). And this was indeed the old behavior which Damien wanted to maintain, but it also seemed to me like the hierarchy would be an overkill for MicroPython. Maybe a middle ground solution would be to call the root logger instead print ? Something like:

def log(self, level, msg, *args):
    if self.isEnabledFor(level):
        if args:
            if isinstance(args[0], dict):
                args = args[0]
            msg = msg % args
        self.record.set(self.name, level, msg)
        handlers = self.handlers
        if not handlers:
            handlers = getLogger().handlers
        for h in handlers:
            h.emit(self.record)

With the above now your example outputs:

mpremote mount . run test2.py 
Local directory . is mounted at /remote
[INFO]:root:hello upy
[INFO]:child:hello 2

But with that fixed it's ok as far as I'm concerned.

Yes I can confirm this and everything in my previous comment should be fixed, I just forgot to push the changes sorry about the confusion.

@stinos
Copy link

stinos commented Dec 2, 2022

Maybe a middle ground solution would be to call the root logger instead print?

Good dea, definitely an improvement over current behavior. For a separate PR perhaps?

@iabdalkader
Copy link
Contributor Author

Pushed the change since I retested all the examples anyway, and it seems fine. I guess this should be ready now @dpgeorge .

@dpgeorge
Copy link
Member

dpgeorge commented Dec 6, 2022

Thanks for the work on this, it looks really good now. The existing example still works (and works better, the last call in that now has the same behaviour as CPython), and the new examples also match CPython.

Very good!

@dpgeorge
Copy link
Member

dpgeorge commented Dec 6, 2022

Merged in d717b04

@iabdalkader iabdalkader deleted the logging_update branch December 6, 2022 07:12
@iabdalkader
Copy link
Contributor Author

@dpgeorge That's great thanks for merging this and the review(s). Can we please also have the strftime somehow ? Right now it's in #508 but I guess I can update logging again now, it would be easier to review, but I prefer not to clutter the logging module with strftime, so if it's added here it should be in a separate file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants