Logging in Python is a crucial aspect of software development, particularly for debugging, monitoring, and understanding the flow of your program. Python provides a built-in logging module that makes it easy to incorporate logging into your applications.

Why Logging Matters

Logging is essential in software development for several reasons:

  1. Debugging: Logs provide valuable information about what happened leading up to an error, helping developers diagnose and fix issues.
  2. Monitoring: Logs help monitor the health and performance of an application, allowing developers and system administrators to identify trends, spot anomalies, and take proactive measures.
  3. Auditing: Logs serve as a record of events within a system, important for compliance, security audits, and forensic investigations.
  4. Performance Analysis: Logs contain performance data like response times and resource usage, which can help optimize an application’s performance.
  5. Troubleshooting: Logs track events across distributed systems, making it easier to diagnose and resolve issues.

Levels of Logging

Python’s logging module supports different levels of logging, each serving a specific purpose:

  1. DEBUG: Detailed information, used for debugging. Typically too verbose for production.
  2. INFO: General information about normal program operation.
  3. WARNING: Indicates a potential issue that doesn’t prevent program execution but might need attention.
  4. ERROR: Signals a serious problem that the program can recover from but may require intervention.
  5. CRITICAL: Indicates a critical error that may cause the program to crash or stop functioning.

Practical Examples

Basic Logger Configuration

Let’s start with a simple logger setup:

"""
logging module
"""

import logging

logger = logging.getLogger(__name__)

handler = logging.StreamHandler()

FMT = "%(levelname)s %(asctime)s [%(filename)s : %(funcName)s : %(lineno)d] %(message)s"
formatter = logging.Formatter(FMT)
handler.setFormatter(formatter)

logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

Here, the stream handler sends logs to the console, and we define a custom format for log messages.

We can now use this logger in another module:

"""
division function module
"""

from logger import logger

try:
num1 = int(input("Enter first number: "))
num2 = int(input("Enter second number: "))
result = num1 / num2
print(result)
except ZeroDivisionError as e:
logger.error(e)

When a division by zero occurs, the output looks like this:

$ python3 division.py
Enter first number: 2
Enter second number: 0
ERROR 2024-04-16 10:13:48,341 [division.py : <module> : 13] division by zero

Logger with File and Stream Handlers

We can extend our logger to write logs to both the console and a file:

"""
logger module with file and shell handler
"""

import logging

logger = logging.getLogger(__name__)

shell_handler = logging.StreamHandler()
file_handler = logging.FileHandler("debug.log")

logger.setLevel(logging.DEBUG)
shell_handler.setLevel(logging.WARNING)
file_handler.setLevel(logging.DEBUG)

FMT_SHELL = "%(levelname)s %(asctime)s %(message)s"
FMT_FILE = (
"%(levelname)s %(asctime)s [%(filename)s:%(funcName)s:%(lineno)d] %(message)s"
)

shell_formatter = logging.Formatter(FMT_SHELL)
file_formatter = logging.Formatter(FMT_FILE)

shell_handler.setFormatter(shell_formatter)
file_handler.setFormatter(file_formatter)

logger.addHandler(shell_handler)
logger.addHandler(file_handler)logger = logging.getLogger(__name__)
shell_handler = logging.StreamHandler()
file_handler = logging.FileHandler("debug.log")

In this setup, the shell only shows warnings and above, while the file captures detailed debug information.

Rich Logger for Better Readability

Using the rich library, we can make our logs more readable:

"""
rich logger module
"""

import logging

from rich.logging import RichHandler

logger = logging.getLogger(__name__)

# the handler determines where the logs go: stdout/file
shell_handler = RichHandler()
file_handler = logging.FileHandler("debug.log")

logger.setLevel(logging.DEBUG)
shell_handler.setLevel(logging.DEBUG)
file_handler.setLevel(logging.DEBUG)

# the formatter determines what our logs will look like
FMT_SHELL = "%(message)s"
FMT_FILE = (
"%(levelname)s %(asctime)s [%(filename)s:%(funcName)s:%(lineno)d] %(message)s"
)

shell_formatter = logging.Formatter(FMT_SHELL)
file_formatter = logging.Formatter(FMT_FILE)

shell_handler.setFormatter(shell_formatter)
file_handler.setFormatter(file_formatter)

logger.addHandler(shell_handler)

The rich library enhances console output with better formatting and color.

Conclusion

Python’s logging module is a powerful tool that helps developers track, debug, and monitor applications efficiently. Whether you’re using basic logging or integrating advanced features like the rich library, well-structured logs can save time and effort when diagnosing issues and analyzing performance.

Happy logging!