Skip to main content

Table of Contents

Log the same way, everywhere

Introduction

Log9er is a simple, opinionated Python logging wrapper class.

You can use it to normalize logging footprints across multiple projects, reducing boilerplate in development, and making it easier to create a uniform experience for log reading and parsing.

Is Log9er For You?

Log9er is tiny, because it replaces the standard Python library approach logger.debug(m) with logger.log(m, level=logging.DEBUG).

That's really the only big difference: It's parameterized, not declarative. The smaller difference is that it's intended to be passed around between related classes in a project, to enforce uniformity in the logs.

Log9er is for devs who have written too many def log() methods that clash with the other ones they've written. Like me.

Just do this instead:

from log9er import Logger

class Foo:
    def __init__(self, logger:Logger=None):

        # Pass a `log9er.Logger` in, or make a new one?
        # If new, we use `logging.getLogger()` to make sure it really is new.

        self.logger = logger or Logger(self.__class__.__name__)

        # Attach the `Logger.log()` method to your class.

        self.log = self.logger.log

        # Then log something at default `INFO` log-level.

        self.log(f"Initialized {self.__class__.__name__} with logger: {self.logger}")

This approach isn't for every project. For example, legacy projects with a lot of logging won't find it worthwhile to refactor.

But for those who want to have a self.log() method instead of a globally namespaced logging.{LOGLEVEL}() function… Log9er might be for you.

Features

Python's standard logging library is extremely flexible. It's like Photoshop.

Log9er is really very inflexible. It just gives you a Logger class and Logger.log() method that wrap Python's logging library methods, and gives you some basic ways to override some parameters.

The main benefit is uniformity of usage in development, reading, and parsing of logging across projects in the same ecosystem, or written by the same developer.

  • Reduce boilerplate logging logic across projects.
  • Log to stderr, syslog, or size-based rotating log files.
  • Reuse the same name to use the same Python logging logger.
  • Exception formatting, with traceback if the logger is in debug mode.

That's about it. Read the code. This is stupid-simple logging. It's not fancy.

Maybe that makes it elegant.

Installation

Log9er is available on PyPi, so just use pip or its alternatives to do something like:

pip install log9er

Usage

Log9er is a library, so you have to write some Python.

See the bundled API Documentation for reference, but here are the broad strokes.

import log9er

# Make a basic logger that prints to stderr
logger = log9er.Logger("HelloWorld", log="stderr")

# Grab the log function
log = logger.log

# Log something
log("Hello, World!")

# Output:
# Mar 29 13:34:46 HelloWorld[472647]: [INFO] HelloWorld!

Initialize Logger

The Logger class provides a flexible interface for logging messages to various backends (such as syslog, file-based logging, or stderr) with customizable log levels.

If an existing Python built-in logging.Logger instance is provided, all other initialization parameters (except debug) are ignored.

Arguments

  • name (str): The name to assign to the logger instance, unless logger provided.

  • log (str): The log output destination. For example, syslog or a file path. If stderr, logs go to standard output.

    • If log is set to stderr, then error-level messages will be in output in red, if supported by your terminal emulator.
  • level (int | str): The default logging level. Can be a string (e.g., INFO ) or an integer constant from the logging module (e.g., logging.INFO ).

  • propagate (bool): Whether log records should propagate to the root logger. If True, the root logger also processes these messages (default: False).

  • debug (bool): If True, enable traceback logging when exception=True.

  • max_bytes (int | None): If provided, and if log is a file path, use as the maxBytes for the RotatingFileHandler (default: 10485760).

  • backup_count (int | None): If provided, and if log is a file path, use as the backup_count for the RotatingFileHandler (default: 5).

  • logger (logging.Logger | None): If provided, use this logger instance directly and ignore other arguments (except debug).

Raises

  • TypeError: If logger is provided but is not an instance of logging.Logger.

  • ValueError: If level is an invalid log level string.

  • FileNotFoundError: If log is a file path and its directory does not exist.

  • OSError: If log="syslog" but /dev/log is not available on this system.

Default System Log Example

By default, Logger will log to syslog, at log level INFO.

This makes it easier to initialize the most common use case.

import logging
from log9er import Logger

# The 'name' arg is required for a new Logger
logger = Logger("Example")

# The above is equivalent to the following
logger = Logger(
    name = "Example",             # 'name' is the 1st kwarg for simplicity
    log = "syslog",
    level = logging.INFO,
    propagate = False,
    debug = False,
    max_bytes = (10*1024*1024),
    backup_count = 5,
    logging = None
)

Stream Log Example

Another common use case would be to stream the logging to console. For convenience, we call this the stderr option, since by default the Python logging.StreamHandler outputs to stderr, and Log9er uses this default.

In addition, if the console supports it, then error-level messages will appear in red.

If your CLI application supports a debug mode, you can pass this through to get full traceback of exceptions.

from log9er import Logger

logger = Logger("Example", log="stderr", debug=True)

Rotating File Log Example

Here's an example of initializing Logger using all possible arguments and keyword arguments (except logger).

Here we override the default max_bytes and backup_count for rotating log files, and limit the logger to error-level items.

Note that name is a required argument unless logger is passed.

from log9er import Logger

logger = Logger(
    "Example",
    log = "/var/log/example.log",
    level = "ERROR",
    propagate = True,
    debug = True,
    max_bytes = (5*1024*1024),
    backup_count = 3
)

Inject Logger Example

In addition, you can pass an existing Python logging.Logger object into the log9er.Logger wrapper.

Log9er will not override anything in the passed-in logger. It will not change the name, formatter, or handler defined on the given logger.

All it will do is provide the log9er.Logger.log() method to call the underlying logging.log() method. This allows the user to implement a uniform and parameterized approach to logging in related applications.

A less common use case would be to normalize an external Python logging.Logger for implementation in code that was written for a log9er.Logger.

import logging
import log9er

# 1. Suppose we already have a logging.Logger...

logging.basicConfig(level=logging.INFO)       # default level is WARNING
logger1 = logging.getLogger()                 # default logger name is "root"
logger1.info("Hello from logging.Logger!")

# Output:
# INFO:root:Hello from logging.Logger!

# 2. Create a log9er.Logger from the above

logger2 = log9er.Logger(logger=logger1)       # name not required if logger is passed
logger2.log("Hello from log9er.Logger!")

# Output:
# INFO:root:Hello from log9er.Logger!

Log Method

The initialized Logger class provides the Logger.log() method, which may be used directly on the object, or attached to your custom class, as seen in the Is Log9er For You? section.

While log() is a simple wrapper on Python's logger, it does have a few differences worth mentioning:

  • Multiple Messages: You can provide multiple messages as positional arguments, and they will be concatenated with a newline and a tab.

  • Log Level: You can supply a string like level="DEBUG" or the usual constant like level=logging.DEBUG.

  • Exceptions: You can set exception=True to treat the messages as an exception. Messages will be prefixed with Exception: and in debug mode, this method will also dump the traceback.

  • Prefix: You can set prefix="myfunction" or any other string to prefix your message(s), emphasized by placing the prefix in brackets.

Arguments

  • *msgs (str): One or more strings to log, e.g., log("Hello", "World").

  • level (int | str): The log level, e.g., logging.INFO or INFO . Defaults to logging.INFO.

  • exception (bool): If True, prefix with "Exception:" and log a traceback if self.debug is also True.

  • prefix (str | None): Optional prefix label. If provided, the log message is prefixed with, e.g. [myfunction] message.

Multiple Messages

One of the bigger differences between Python's logging and log9er is that it accepts multiple messages, and will format them for you on multiple lines, with tabs to indent subsequent message parts.

logger.log("Message 1", "Message 2", "Message 3", level=logging.WARNING)

# Output:
# Mar 29 17:14:10 Dummy[491147]: [WARNING] Message 1
#	Message 2
#	Message 3

Log Level

As seen above, the level argument accepts logging constants.

It also accepts simple case-insensitive string values:

logger.log("DEBUG constant", level=logging.DEBUG)
logger.log("INFO is the default")
logger.log("WARNING in upper case", level="WARNING")
logger.log("ERROR in lower case", level="error")
logger.log("CRITICAL in sarcastic case", level="CriTiCaL")

It will throw an exception if the level is invalid:

logger.log("Bad log level 'foo'", level="foo")

# Output:
# ValueError: Invalid log level 'FOO'

Exceptions

In Log9er, you just tell log() that the message is an exception, and if it's in debug mode, then it will also dump traceback.

With debug mode disabled, an exception only logs a single line:

>>> logger = log9er.Logger("Logger", log="stderr")
>>> try:
...     raise Exception("NO debug mode!")
... except Exception as ex:
...     logger.log(ex, exception=True)
...
Mar 29 18:13:30 Logger[498590]: [ERROR] Exception: NO debug mode!

With debug mode enabled, we also get the traceback of the exception:

>>> logger = log9er.Logger("Logger", log="stderr", debug=True)
>>> try:
...     raise Exception("YES debug mode!")
... except Exception as ex:
...     logger.log(ex, exception=True)
...
Mar 29 18:16:13 Logger[498590]: [ERROR] Exception: YES debug mode!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: YES debug mode!

Log9er simply wraps Python's built-in logging.exception() function for this traceback feature. It doesn't do anything special, or surprising.

Prefix

Logging only helps if it tells us what we need to know. The prefix argument allows you to set an arbitrary identifier in brackets prior to the message.

For example:

>>> logger.log("Prefixed message", prefix="Example")
Mar 29 18:23:34 Logger[498590]: [INFO] [Example] Prefixed message

Authors

This is a dead-simple wrapper. It's practically a toy, except that it's just so handy.

I doubt anyone else will work on it.

Joseph Edwards VIII

Release files for log9er 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for log9er 0.1.1
File Size Uploaded
log9er-0.1.1.tar.gz 77.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for log9er 0.1.1
File Interpreter ABI Platform
log9er-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 87.5 kB

Release files / log9er-0.1.1.tar.gz

Download URL log9er-0.1.1.tar.gz
Size 77.6 kB
Tags Source
SHA-256 checksum
How to use checksums
5fb5de01a85a4f7e376d40a9591e36d4e0b418998ee6aa3f36f479cabfd0bdb9
BLAKE2b-256 checksum
How to use checksums
54368792d3413e611351a9dac14af0cc41b4fa7aae2802b307563b08bf9198a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via python-httpx/0.27.2

Release files / log9er-0.1.1-py3-none-any.whl

Download URL log9er-0.1.1-py3-none-any.whl
Size 10.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ce881f32d4aed84b11b7f71d40f0200e3e450ae88c7b8dc1960d009192039d99
BLAKE2b-256 checksum
How to use checksums
a9fb683f170ee85a40c453e88042b225711984fa26877e541575992fdb6d6d73
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via python-httpx/0.27.2

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page