Skip to content

Easy Logging

py_simple.easy_logging

easy_logging is meant to simplify logging code blocks and function calls. Built on top of the logging module, it handles start, success, and error messages without repeating try/except blocks.

Configure logging in your application before using these helpers, for example with logging.basicConfig(level=logging.INFO).

clear_log_file(file_path)

Empties a log file, leaving it in place but with no contents. Args: file_path (str): Path to the log file to clear. Returns: bool: True if the file was cleared successfully, False if the file could not be found (e.g., False when the path doesn't exist). Example: === "The Py_simple Way"

from py_simple import clear_log_file
clear_log_file("app.log")
=== "The Traditional Way"
import os
if os.path.exists("app.log"):
    open("app.log", "w").close()

count_log_levels(file_path)

Counts how many lines in a log file mention each standard level.

Looks for DEBUG, INFO, WARNING, ERROR, and CRITICAL as whole words, so a line is counted once for each level name it contains. A missing file returns zeros for every level.

Parameters:

Name Type Description Default
file_path str

Path to the log file to scan.

required

Returns:

Type Description
dict[str, int]

dict[str, int]: Counts keyed by level name, always including DEBUG, INFO, WARNING, ERROR, and CRITICAL.

Example
from py_simple import count_log_levels

totals = count_log_levels("app.log")
print(totals["ERROR"])
import os
import re

levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
totals = {level: 0 for level in levels}
if os.path.exists("app.log"):
    with open("app.log") as log_file:
        for line in log_file:
            for level in levels:
                if re.search(rf"{level}", line):
                    totals[level] += 1

find_log_lines(file_path, search_text)

Returns log file lines that contain the supplied search text.

Searches case sensitively and removes trailing newline characters from returned lines. Returns an empty list when the file does not exist or when no lines contain the search text.

Parameters:

Name Type Description Default
file_path str

Path to the log file to search.

required
search_text str

Text to look for in each log line.

required

Returns:

Type Description
list[str]

list[str]: Matching log lines without trailing newline characters.

Example
from py_simple.easy_logging import find_log_lines

errors = find_log_lines("app.log", "ERROR")
import os

if os.path.exists("app.log"):
    with open("app.log") as log_file:
        errors = [
            line.rstrip("\n")
            for line in log_file
            if "ERROR" in line
        ]
else:
    errors = []

log_function(message_template)

Creates a decorator that logs each call to a synchronous function and method.

Formats the message using the function's argument names and values, including default values for omitted arguments. Excludes self and cls from the formatting context. Uses log_step to log the start, successful completion, or exception with a traceback through the root logger. Preserves the function's metadata and return value.

Argument binding and message formatting happen before logging starts; errors in those steps propagate without being logged by this helper. For async functions or generators, this decorator only logs creation of the coroutine or generator, not its later execution.

Parameters:

Name Type Description Default
message_template str

Message with named str.format_map fields matching function parameters, such as "Add {a} and {b}". Format specifications such as "{price:.2f}" are supported.

required

Returns:

Type Description
Callable[[Callable[P, R]], Callable[P, R]]

Callable[[Callable[P, R]], Callable[P, R]]: Decorator that wraps a function with logging while preserving its parameters and return type.

Raises:

Type Description
KeyError

A message field is missing from the formatting context.

ValueError

The message template or a format specification is invalid.

TypeError

The supplied arguments cannot be bound to the wrapped function's signature.

Exception

Re-raises any Exception from the wrapped function after logging it.

Example
import logging
from py_simple.easy_logging import log_function

logging.basicConfig(level=logging.INFO)

@log_function("Add {a} and {b}")
def add(a: int, b: int = 10) -> int:
    return a + b

print(add(5))  # -> 15; logs "Add 5 and 10"
import logging

logging.basicConfig(level=logging.INFO)

def add(a: int, b: int = 10) -> int:
    message = f"Add {a} and {b}"
    try:
        logging.info(f"Starting: {message}")
        result = a + b
    except Exception:
        logging.exception(f"Error during: {message}")
        raise
    else:
        logging.info(f"Finished: {message}")
        return result

print(add(5))  # -> 15; logs "Add 5 and 10"

log_step(message)

Logs the start, successful completion, or failure of a code block.

Writes "Starting:" and "Finished:" messages at INFO level. If the block raises an Exception, logs "Error during:" at ERROR level with the traceback and re-raises the original exception instead of logging a successful completion. Uses the root logger and its configuration.

Parameters:

Name Type Description Default
message str

Description of the operation, such as "Calculate total".

required

Yields:

Name Type Description
None None

Runs the enclosed code block without providing an object for an optional as target.

Raises:

Type Description
Exception

Re-raises any Exception raised by the enclosed block after logging it.

Example
import logging
from py_simple.easy_logging import log_step

logging.basicConfig(level=logging.INFO)

with log_step("Calculate total"):
    total = sum([10, 20, 30])

print(total)  # -> 60
import logging

logging.basicConfig(level=logging.INFO)

try:
    logging.info("Starting: Calculate total")
    total = sum([10, 20, 30])
except Exception:
    logging.exception("Error during: Calculate total")
    raise
else:
    logging.info("Finished: Calculate total")

print(total)  # -> 60

log_to_file(file_path, message)

Appends a message to a log file.

Creates the file if it does not exist and writes the message as one line. Returns True after the message is written successfully.

Parameters:

Name Type Description Default
file_path str

Path to the log file to write to.

required
message str

Message to append to the log file.

required

Returns:

Name Type Description
bool bool

True when the message is written successfully.

Example
from py_simple import log_to_file

log_to_file("app.log", "Started import job")
with open("app.log", "a") as log_file:
    log_file.write("Started import job\n")

read_recent_log_lines(file_path, line_count=10)

Reads the most recent lines from a log file.

Parameters:

Name Type Description Default
file_path str

Path to the log file to read.

required
line_count int

Number of recent lines to return. Defaults to 10.

10

Returns:

Type Description
list[str]

list[str]: The last line_count lines without trailing newline characters. Returns an empty list if the file does not exist or if line_count is less than 1.

Example
from py_simple import read_recent_log_lines

recent_lines = read_recent_log_lines("app.log", line_count=5)
import os

if os.path.exists("app.log"):
    with open("app.log") as log_file:
        recent_lines = [line.rstrip("\n") for line in log_file.readlines()[-5:]]
else:
    recent_lines = []

setup_file_logger(log_file, level=logging.INFO, format_string='%(asctime)s - %(levelname)s - %(message)s', name=None)

Sets up and returns a logger configured to write to a file.

Creates parent directories for the log file if they do not exist. Adds a logging.FileHandler formatted with format_string at the specified logging level. If a handler for the same file path is already attached to the logger, duplicate handlers are not added.

Parameters:

Name Type Description Default
log_file str

Path to the log file to write to.

required
level int or str

Logging level such as logging.INFO, logging.DEBUG, or string equivalents like "INFO". Defaults to logging.INFO.

INFO
format_string str

Message format for the log handler. Defaults to "%(asctime)s - %(levelname)s - %(message)s".

'%(asctime)s - %(levelname)s - %(message)s'
name str

Name of the logger to configure. Defaults to None, which uses log_file as the logger name.

None

Returns:

Type Description
Logger

logging.Logger: The configured logger instance.

Example
from py_simple import setup_file_logger

logger = setup_file_logger("app.log")
logger.info("Application started")
import logging

logger = logging.getLogger("app.log")
logger.setLevel(logging.INFO)
handler = logging.FileHandler("app.log", encoding="utf-8")
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
    "%(asctime)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("Application started")

temporary_log_level(logger, level)

Changes a logger's level for a block and restores its previous level.

Restores the exact explicit level, including NOTSET, even if the block raises. Nested blocks restore the level that was active on entry. Handler levels/filters, logger filters, propagation and global logging settings still apply and are not changed. A shared logger's temporary level is visible to other users of that logger; this is not thread-local.

Parameters:

Name Type Description Default
logger Logger

Logger to adjust, normally obtained with logging.getLogger(). Its explicit level will change.

required
level int | str

Integer logging level or a registered level name, such as logging.DEBUG or "debug". String names are uppercased, matching setup_file_logger. Integers follow Logger.setLevel.

required

Yields:

Name Type Description
None None

Runs the block without providing an as target value.

Raises:

Type Description
TypeError

If logger is not a logging.Logger or level is neither an integer nor a string accepted by logging.

ValueError

If a string does not name a registered logging level.

BaseException

Propagates any exception from the enclosed block after restoring the previous level.

Example
import logging
from py_simple.easy_logging import temporary_log_level

logger = logging.getLogger("example")
logger.setLevel(logging.WARNING)
with temporary_log_level(logger, "debug"):
    print(logger.isEnabledFor(logging.DEBUG))  # True
print(logger.level == logging.WARNING)  # True
import logging

logger = logging.getLogger("example")
logger.setLevel(logging.WARNING)
previous_level = logger.level
logger.setLevel("DEBUG")
try:
    print(logger.isEnabledFor(logging.DEBUG))  # True
finally:
    logger.setLevel(previous_level)
print(logger.level == logging.WARNING)  # True