Logging in Python like a pro
guicommits.com
guicommits.com
Things I really dislike about it is the lack of “context”. Usually log messages are about something - often a request. You can’t attach a request ID or something else a nested set of function calls that log.
The whole library is just really rubbish and has not aged well. There’s no standard way of outputting JSON logs, but there’s a built in way to spawn a socket server that allows the logging configuration to be updated remotely via a bespoke protocol? And everything is built mostly around logging to files.
It also hits that perfect sweet spot of being both overly engineered and totally inflexible. I wanted to funnel our log messages to s3 via the “s3fs” module, which supports a “file like” object that gets written to s3. I had to hack around the various handlers to support this because it assumes it’s a “real” file.
Hate it.
Overall, I prefer less magic and more explicitness.
Pytest is very opinionated about how and when it's going to be run. If you want to put a test in say, Jupyter or a Markdown block, unittest is much more amenable to just doing what you want without having to read through documentation to break a whole bunch of magic behavior.
By “Java written all over it” do you mean that like JUnit for Java, it's an implementation of the xUnit pattern derived from SUnit for Smalltalk?
So, it's maybe not the best evidence that the Python version is particularly a clone of the Java port.
It has a bit of an initial learning curve, but once you understand how fixtures work it's amazing how productive you can be with it.
Also with improvement of type hinting, tooling like PyCharm starts to understand pytest better and make it easier to read and manage and a lot of the magic can be dispellt by written out explicitly. Though this need some discipline from the dev team.
I might be in the minority but I also find python's mock implementation awful. Mockito in Java is way better where you explicitly state which calls you expect and if you get an unexpected call it errors out.
And regarding the sibling comment: Yes, I prefer unittest as well.
It's literally easier to just have an IOBuffer laying around.
https://docs.python.org/3/library/logging.html#logging.Logge...
For dictConfig, the documentation has examples
https://docs.python.org/3/library/logging.config.html#object...
Python's logging has its faults, but you're complaining about something I could find in 5 minutes of Google.
Each module should use `logger = logging.getLogger(__name__)` and the logger config can set in one place, conventionally in the `__main__` script.
https://docs.python.org/3/howto/logging.html#advanced-loggin...
When running a webserver though, an even simpler trick is to add middleware that sets the current request's info as the current thread's name, and then including threadName in your log format.
[1] https://docs.python.org/3/library/logging.html#logging.Logge...
I had mistakenly assumed ContextVars only worked for async code.
Sadly, my social media is subscribed to by pretty heavy hitters in the Python community, but when I asked about the best logging for python it was all crickets. Maybe that says something. :-)
But IMO it inherited most of loggers problems while not offering a better interface
It is basically a kwargs to json or 'key=value' converter
(Also json logs are overrated. There I said it. Especially one key per line, please don't)
You need good quality logs though. Because noisy useless JSON logs are even more noisy and useless.
Agreed on the one key per line bit.
print(f"log_func: {var_a} not found!")One has to remember that logging was never intended to be some infinitely generalizable metaprotocol for massively streaming parallel notifications to the cloud or whatever. It was meant as a replacement for "if (DEBUG > 4): print(...)". For which I think it does quite well, thank you.
The whole library is just really rubbish and has not aged well. Hate it.
So what have you written that has made people's (not just your bosses' or your clients') lives easier, for nearly 20 years now (if only incrementally)? Do share.
I see and grant your points, but I find this strong emotional rebuke of something that was never intended to be anything more than a simple convenience library to be well, strange.
So now everything needs to work with it, it’s internals and it’s way of doing things. And to top it off it’s near impossible to refactor or replace.
Also logging can be set up remotely, but it's easier to do like that from the container and then configure the hos to send everything to a single log machine.
Presumably these levels derive from syslog or older. I've no idea why they omitted NOTICE.
I just recently discovered that there's no documented way to dump the current configuration (https://stackoverflow.com/q/72624883) so you can see what loggers exist and where their output is going.
Those are just println() and eprintln()
That sounds like there's a Log4shell-like vulnerability waiting to be found. But I couldn't even find it, I found the SocketHandler but that's just a log destination.
EDIT: shockingly, I did find it: https://docs.python.org/3/howto/logging-cookbook.html#config...
https://github.com/Delgan/loguru
Also iirc s3's "file-like interface" does not actually obey the file protocol, which is obnoxious.
The irony is Java logging moved on and with newer libraries offers better supports for lazy evaluation, structured logging and multiple output formats, while Python logging seems stuck with an ancient copy of log4j 1.x
One of my favorite features of loguru is the ability to contextualize logging messages using a context manager.
with logger.contextualize(user_name=user_name):
handle_request(...)
Within the context manager, all loguru logging messages include the contextual information. This is especially nice when used in combination with loguru's native JSON formatting support and a JSON-compatible log archiving system.One downside of loguru is that it doesn't include messages from Python's standard logging system, but it can be integrated as a logging handler for the standard logging system. At the company where I work we've created a helper similar to Datadog's ddtrace-run executable to automatically set up these integrations and log message formatting with loguru
I'll never go back to that rancid pile of shit that is the builtin logger lib.
This is the standard approach to produce a log:
```from loguru import logger logger.info("hello world") ```
But betteid avise you to research more, because the library is nice to use and is well documented.
``` logger.exception("Failed to connect.") # Will print exception. ```
Ex:
logger.debug("A non-critical exception occurred", exc_info=True)The advice I give to people is "think about logging like a feature with customers, just like any other feature." If you think about logging this way, you ideally put yourself in the position of your logs' "customer"- a bleary-eyed teammate who just got paged out of bed at 2am, or a security guy who isn't intimately familiar with your code. Providing extra context for those users is really kind, as they don't have a "secret decoder ring".
Since the article discusses it, I also highly recommend AWS CloudWatch logs. It's super easy to log to CloudWatch from Lambda with the Python logger class. It's a little harder from EC2, but not really that difficult. The advantage is that you don't need to SSH somewhere and navigate to a file and then locate the interesting stuff in that file - You just keep the CloudWatch console open (or keep a link to it handy) and the latest stuff is on top with easy filtering.
You may well find yourself wishing you did this if you also output DEBUG logs in production.
I miss that I can't do that any longer
As a security guy, I hate SSH to production (the whole "cattle, not pets" thing). In my last company we had an internal tool to federate you to the AWS console. We had runbooks in a wiki, and had links literally to the logs for a particular component/service/region - the link would federate you to the right account and take you directly to the target log in cloudwatch logs in the appropriate region. Safer and easier than ssh-to-prod.
Or even just print(): You've got a unique log group for a lambda, CloudWatch logs autonatically timestamps entries, and stdout & stderr from a lambda both go to the current CloudWatch log stream in the lambda’s CloudWatch logs group.
ed: clarified that this relates to AWS Lambda only
The cost, however, can easily get out of hand and AWS does not have per resource billing enabled by default. So it might not be obvious which Log Group is responsible for for which charge.
I also think its console is way inferior than something like Grafana for searching and visualisation.
E.g. a Lambda serving API requests, and you just want 'the logs'.
Other than searching (for something specific) you can only do it (afaik?) with third-party CLI tools, or (found this as I write) v2 (as in, the separate v2 project/binary/distribution methods, not just being up to date :eyeroll:) of the awscli. Short of rearranging your group/stream naming anyway.
Whereas what I really want is just to see all of the logs in the group, with the stream they're from as a filterable property.
(And maybe keep the hyper-specific-ness as a new 'publisher' (what unique thing put the event in the stream) property or something?)
Shorter version: Use structured logging!
If using Python, use `structlog`: https://www.structlog.org/en/stable/
It can be tricky to forward all logs through it [0] but it's definitely worth it.
[0] https://www.structlog.org/en/stable/standard-library.html
I made this library to make global structured logging in Python super easy.
Here’s a logging format that I use and probably you should too. Among other things, it gets around most of the context issues by outputting the file and line number of the code that called the log.
Before I would try and make every log message unique or mostly unique so there weren’t double ups.
I don’t agree with the authors logging clutter though. If you’re manually eyeballing the logs looking for a message you’re already doing it wrong. Use search, have ids in the messages etc.
Remember folks, debug logging on because if it’s not logged, when it breaks you don’t have the logs…
Even storing logs in a format that you need to decode or download would be a red flag for me. Seconds of debugging all of a sudden turns into minutes or even hours.
You should be able to immediately tell from looking at any log entry:
* When it happened
* How serious it is (Informational, Warning, Error, Fatal - at a minimum)
* Type of issue (ideally a unique issue type from a list of known issue types)
* Who logged it (system / subsystem / module / line number)
* What happened (including any germane variable values)
For powerful log channeling and notifications to Telegram, Slack, etc. there is this Python package:
https://pypi.org/project/notifiers/
I also created a logging handler for Discord, where one can get notifications on errors to a private channel:
https://github.com/tradingstrategy-ai/python-logging-discord...
I've also come to find a few "wide" log entries significantly better than having many smaller log messages (especially as the system grows).
Ideally, one operation == one log message, and contains all context: userid, sessionid, url path, domain, error codes/messages, timing, etc. log error/warning if it fails, info if success. It's much easier to follow than one operation spread across a half dozen log messages. In dev, with one thing happening at a time, it's no big deal, but in production there's way more stuff happening and it is interspersed with hundreds of lines of logs of other operations as well as other threads running the same operation concurrently.
Everyone seems to just go,
logger.info('Doing that')
But logs really should be covering the context instead of being single dimensional by being placed at the execution point, which will also make the code far more readable. //Sorry, I don't know python well but trying to run a lambda as a parameter.
myLogger.info('Doing that', lambda : (
doThat()
whatNot()))
And then, you know exactly what the log is covering as well as take a benchmark of the said part and report how long it took to run.You don't need 'Done that' logging, as if you throw inside, the log doesn't happen.
You might want to know 'Doing that' was run even if it threw inside, but if logging is paired with an error reporting tools like Sentry, which is quite essential than depending on simple logging, you can know the stack trace easily to track which part of the code was run.
It seems no logging libraries do it the multidimensional way but it's an essential part of how I do logging.
Interesting capabilities. There's a rundown with log output examples seen in https://docs.python.org/3/library/logging.html#logging.Logge...
One of the main gripes I do have with it is its reliance on the old-school %-based formatting for log messages:
logger.debug("Invalid object '%s'", obj)
As there are real advantages to providing a template and object args separately, this is a bit of a shame since it pushes people to use pre-formatted strings without any args instead.I fixed this for myself by writing bracelogger[0]. If this is a pain point for you too, you might find it useful.
Ex:
logger.debug("Invalid object of type {0.__class__.__name__}: {0}", obj)
[0]: https://github.com/pR0Ps/bracelogger import pygogo as gogo
kwargs = {'contextual': True}
extra = {'additional': True}
logger = gogo.Gogo('basic').get_structured_logger('base', **kwargs)
logger.debug('message', extra=extra)
# Prints the following to `stdout`:
{"additional": true, "contextual": true, "message": "message"} import logging
logger = logging.getLogger(__name__)
But boy oh have I seen some funny business where people attach the logger to their objects like so class Wat:
def __init__(self):
self.logger = logging.getLogger(…)
All for what? For WAT?!I really wanted to have an app with a reasonable default level, then allow flags like --debug, --verbose plus --quiet and it never seemed to map like I wanted.
maybe it's obvious and I just don't see it.
One of the biggest logging mistakes I see is people misusing Error. What most developers consider an error -- external service call failed, user gave bad input, or a timeout happened - is often not really a problem. The service call is re-tried. Users are going to user (and provide garbage data and then fix it). None of these require operator intervention.
To me, Errors imply action needed, or highlight an unresolved situation. If the service call keeps failing after all retries, and now a feature is impacted (eg a user won't get a report, or there's a gap in data somewhere) then that can be an Error.
All the temporary faults are useful as Warn to highlight them and distinguish from other normal operations, but no action is needed. This is most useful when a user reports something like "it seems to be taking much longer than usual to get email notifications" and when an operator looks at the logs, they're full of warnings. It's also useful for monitoring: high number of warnings in the log can cause a non-critical notification to operators (during business hours; not a wake-me-up-at-2am page).