Header Image (C) Tai Kedzierski
This post is opinionated.
Python's default log setup is unhelpful; it works against the "batteries included" approach we have come to expect.
From a useful log message, I want to know when, what level, and what information. I may want it on console, I may want it in a file.
This should be simple - but in Python I end up every time having to look up how to create a full logging utility with custom file handling and string formatting.
It should be as simple as logger = getLogger()
, but the default behaviour for some unknown reason is to provide a completely useless formatting, and no shorthand for a sensible default.
That or I need to download some pip package of unknown provenance, trust that it hasn't been name-hijacked, or doing some obfuscated exfilration. The leftpad incident from 2016 comes to mind, as well as the Revival hijack attack from 2024 which was essentially the same problem in a different repo system.
In fact, any user-repo without namespacing is vulnerable to this: Node's npm, Python's pip, Arch's AUR, Canonical's snap ... to name a handful who just let users upload whatever. Even namespacing isn't a guarantee of trust - I've come across projects that distribute their software through these channels not through the project's name, but via some arbitrary dev's monicker, raising doubt as to the authenticity of the package. I gave my thought process on how to decide on whether to trust a source in a previous post on using syncthing in a work environment.
External dependencies in user-controlled repos are the devil, and should only be considered when the solution to a problem is complex. And in general, simple solutions should just exist directly in the code base - ideally self-written, but sometimes the problem just strafes into the "cumbersome enough" space to make a dependency feel both reasonable and icky.
The answer: write it once, stash it away in a Github gist or in a "useful snippets" repo of your own. Copy and paste.
Copy Paste? Ew!
"Copy and paste" of code probably sends alarm bells ringing for any seasoned coder. "Don't repeat yourself," "use a package manager," "write once, update everywhere." These are good instincts to have, but case-by-case, it is also good to know when copy-paste is preferable.
In this case, the requirement is to avoid unnecessary external dependencies for a simple solution to a simple need . In leftpad as with this mini-logger, the required code snippet is short and easy to understand ; it is no loss to reimplement if needed. It is also appropriately licensed (yes, it may be just a snippet; it remains however recommendable to ensure that what you are copying is indeed allowable. Be wary of copying random blobs of code.)
Mini Logger Snippet
I include below a code snippet for a mini-logger utility which allows for a single call with minimal configuration:
from minilog import SimpleLogger
LOG = SimpleLogger(name="mylog", level=SimpleLogger.INFO)
LOG.info("this is useful")
Which prints to console:
2024-11-20 10:43:44,567 | INFO | mylog : this is useful
The mini-logger code
Copy this into a minilogger.py
file in your project. Tada - no external dependency needed. Left untouched, it will remain the same forever. No name hijacking. No supply-chain injection.
# For completeness:
# (C) Tai Kedzierski - Provided under MIT license. Go wild.
import logging
class SimpleLogger(logging.Logger):
FORMAT_STRING = '%(asctime)s | %(levelname)s | %(name)s : %(message)s'
ERROR = logging.ERROR
WARN = logging.WARN
INFO = logging.INFO
DEBUG = logging.DEBUG
def __init__(self, name="main", fmt_string=FORMAT_STRING, level=logging.WARNING, console=True, files=None):
logging.Logger.__init__(self, name, level)
formatter_obj = logging.Formatter(fmt_string)
if files is None:
files = []
elif isinstance(files, str):
files = [files]
def _add_stream(handler:logging.Handler, **kwargs):
handler = handler(**kwargs)
handler.setLevel(level)
handler.setFormatter(formatter_obj)
self.addHandler(handler)
if console is True:
_add_stream(logging.StreamHandler, stream=sys.stdout)
for filepath in files:
_add_stream(logging.FileHandler, filename=filepath)
The MIT license essentially allows you to "do whatever you want with this." No strings attached.
There we are. A simple log 🪵
Top comments (0)