Use Python Logging Like a PRO: Don't be a Rookie


Written by - Deepak Prasad

Python Logging Module: An Overview

The Python Logging module is a fundamental component for tracking events in Python applications, providing a systematic way to collect and process log messages. It's an essential tool for debugging and monitoring Python code.

Modular Design of Python Logging:

  • Loggers in Python Logging: These are the primary elements of the Python Logging system. They capture log messages and pass them on for further handling.
  • Handlers in Python Logging: Handlers are responsible for dispatching the log messages to various destinations, such as files or the console, as defined in the Python Logging setup.
  • Filters in Python Logging: Filters offer a finer level of control, allowing selective processing of log messages based on specific criteria within the Python Logging framework.
  • Formatters in Python Logging: They define the layout of log messages, ensuring that the output is readable and informative.

 

Step-by-Step Example to use Python Logging

Follow the step by step instructions to configure and use Python logging:

1. Import the Logging Module:

Begin by importing the Python logging module:

import logging

2. Configure Logging:

Set up basic configuration for the logging system. This can be done using the basicConfig() function, where you can specify the logging level, output file, and message format.

logging.basicConfig(filename='example.log', level=logging.DEBUG, format='%(asctime)s:%(levelname)s:%(message)s')

3. Create a Logger Object:

Create a logger object that you will use to log messages.

logger = logging.getLogger(__name__)

4. Log Messages:

Use the logger to log messages at various severity levels: DEBUG, INFO, WARNING, ERROR, and CRITICAL.

logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning")
logger.error("This is an error message")
logger.critical("This is a critical message")

 

Understanding Python Loggers

Loggers are the core of the Python logging system. They are used to track events within an application. Events are logged via messages with varying levels of importance, such as DEBUG, INFO, WARNING, ERROR, and CRITICAL.

To use a logger, you first need to import the logging module and then create a logger instance:

import logging
logger = logging.getLogger('my_logger')

Loggers have levels that determine the severity of messages they should capture. Setting a level filters out messages below that level:

logger.setLevel(logging.INFO)

Example-1: Single Module Usage

Suppose you have a simple Python script, simple_script.py:

# simple_script.py
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

logger.info("This is an informational message from simple_script")

When you run this script, it will output an info message tagged with the script's name (__name__ equals '__main__' in this case).

Example-2: Multiple Modules with Different Loggers

For better organization, especially in larger applications, you should use module-level loggers. Now, imagine you have a more complex application with two modules: module_a.py and module_b.py.

# module_a.py
import logging

logger = logging.getLogger(__name__)

def function_a():
    logger.info("Message from function_a in module_a")
# module_b.py
import logging
import module_a

logger = logging.getLogger(__name__)

def function_b():
    logger.info("Message from function_b in module_b")
    module_a.function_a()

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    function_b()

When you run module_b.py, it will output:

INFO:__main__:Message from function_b in module_b
INFO:module_a:Message from function_a in module_a

Here, you see the benefit of __name__. This is because when module_b.py is run as the main program, its __name__ attribute is set to '__main__' instead of 'module_b'. That's why the logger in module_b.py identifies itself as __main__ in the output. However, module_a.py is imported, so its __name__ attribute remains 'module_a', as reflected in the log message. This behavior underscores how Python's __name__ attribute works in differentiating between running a script directly versus importing it as a module.

 

Understanding Python Logging Handlers

Handlers in Python's logging framework are responsible for dispatching the log messages to various destinations like console, files, or even over the network. They work in conjunction with loggers to control where and how log messages are outputted.

Python offers several types of handlers, including:

1. StreamHandler

To send logging output to streams like the console (sys.stdout or sys.stderr).

import logging
import sys

logger = logging.getLogger('StreamLogger')
logger.setLevel(logging.DEBUG)

stream_handler = logging.StreamHandler(sys.stdout)
logger.addHandler(stream_handler)

logger.info("This message goes to the console")

2. FileHandler

To write log messages to a disk file.

logger = logging.getLogger('FileLogger')
logger.setLevel(logging.INFO)

file_handler = logging.FileHandler('logfile.log')
logger.addHandler(file_handler)

logger.info("This message is written to logfile.log")

3. RotatingFileHandler

To write log messages to a file, rotating the log file at a certain size.

from logging.handlers import RotatingFileHandler

logger = logging.getLogger('RotatingFileLogger')
logger.setLevel(logging.INFO)

rotating_handler = RotatingFileHandler('rotating.log', maxBytes=1024, backupCount=5)
logger.addHandler(rotating_handler)

logger.info("This message is written to a rotating log")

4. TimedRotatingFileHandler

To rotates log files at certain timed intervals.

from logging.handlers import TimedRotatingFileHandler

logger = logging.getLogger('TimedRotatingFileLogger')
logger.setLevel(logging.INFO)

timed_handler = TimedRotatingFileHandler('timed.log', when="midnight", interval=1, backupCount=3)
logger.addHandler(timed_handler)

logger.info("This message is written to a timed rotating log")

5. SocketHandler

To send log messages to a TCP/IP socket.

from logging.handlers import SocketHandler

logger = logging.getLogger('SocketLogger')
logger.setLevel(logging.INFO)

socket_handler = SocketHandler('localhost', 9090)
logger.addHandler(socket_handler)

logger.info("This message is sent over a TCP/IP socket")

6. HTTPHandler

Sends log messages to a web server using HTTP (GET or POST).

from logging.handlers import HTTPHandler

logger = logging.getLogger('HttpLogger')
logger.setLevel(logging.INFO)

http_handler = HTTPHandler('localhost:8000', '/log', method='POST')
logger.addHandler(http_handler)

logger.info("This message is sent to a web server via HTTP")

7. SMTPHandler

To email log messages.

from logging.handlers import SMTPHandler

logger = logging.getLogger('EmailLogger')
logger.setLevel(logging.ERROR)

smtp_handler = SMTPHandler(mailhost=("smtp.example.com", 587), fromaddr="me@example.com", toaddrs=["admin@example.com"], subject="Error Log")
smtp_handler.setLevel(logging.ERROR)
logger.addHandler(smtp_handler)

logger.error("This error message is sent via email")

8. NullHandler

A do-nothing handler, useful in libraries where loggers are defined but the configuration is left to the application using the library.

logger = logging.getLogger('NullLogger')
logger.addHandler(logging.NullHandler())

logger.info("This message goes nowhere")

 

Using Formatter to format log messages

Formatters are objects that configure the final order, structure, and contents of the log message. Unlike the basic string formatting used in basicConfig, formatters provide more control and can be used with different handlers to have distinct formats for different log destinations.

import logging

# Create logger
logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

# Create handler
file_handler = logging.FileHandler('myapp.log')

# Create formatter and set it to the handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# Add handler to the logger
logger.addHandler(file_handler)

# Log messages
logger.info("This is an info message")

In this setup, the formatter specifies that each log message in myapp.log will include a timestamp, the logger's name, the level of the message, and the message itself.

Formatters in Python's logging module allow you to specify the exact format of your log messages. They use a string-based format where you can include various attributes. Some common options you can use in a formatter include:

  • %(asctime)s: The human-readable time when the LogRecord was created.
  • %(name)s: The name of the logger that created the record.
  • %(levelname)s: The textual logging level ('DEBUG', 'INFO', etc.).
  • %(message)s: The logged message.
  • %(pathname)s: The full pathname of the source file where the logging call was made.
  • %(filename)s: The filename portion of pathname.
  • %(module)s: The module name of the source file where the logging call was made.
  • %(funcName)s: The name of the function containing the logging call.
  • %(lineno)d: The line number in the source file where the logging call was made.
  • %(thread)d: The thread ID.

Example with Various Formatter Options:

import logging

# Create logger
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)

# Create handler
stream_handler = logging.StreamHandler()

# Create formatter with multiple options
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s')

# Set formatter to handler
stream_handler.setFormatter(formatter)

# Add handler to logger
logger.addHandler(stream_handler)

# Log a message
logger.info("This is a test log message")

In this example, the formatter includes the timestamp, logger name, log level, source file name, line number, and the actual log message. This makes the log output detailed and informative, which is particularly useful for debugging and monitoring.

 

Using Filters in Python Logging

Filters in Python Logging provide a fine-grained mechanism for additional control over which log records are processed. They can be attached to loggers and handlers and are used to implement logging policies.

Example of Custom Filter:

import logging

# Custom filter class
class InfoFilter(logging.Filter):
    def filter(self, record):
        return record.levelno == logging.INFO

# Logger setup
logger = logging.getLogger('myLogger')
logger.setLevel(logging.DEBUG)

# Handler setup
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.DEBUG)

# Attach custom filter to handler
info_filter = InfoFilter()
stream_handler.addFilter(info_filter)

logger.addHandler(stream_handler)

# Test logging
logger.info("This is an info message.")
logger.debug("This debug message will not be printed.")

In this example, a custom filter InfoFilter is created to allow only INFO level messages. When attached to the stream_handler, it ensures that only messages with the INFO level are processed by that handler. The debug message in this case will not be displayed, demonstrating the filter's effect.

 

You might wonder, how Filters are different then defining LogLevels?

Filters in Python Logging offer more granular control than log levels. While log levels define a global threshold for logging severity, filters can impose additional, more complex criteria.

Using Log Levels

Log levels (DEBUG, INFO, etc.) are straightforward. A logger or handler with a set level will only process messages at that level or higher.

logger = logging.getLogger('myLogger')
logger.setLevel(logging.INFO)

# This message will be logged
logger.info("An info message")

# This message will NOT be logged
logger.debug("A debug message")

In this example, only messages at INFO level and above are logged.

Using Filters

Filters allow for custom rules, beyond the severity level.

class CustomFilter(logging.Filter):
    def filter(self, record):
        return "special" in record.getMessage()

logger.addFilter(CustomFilter())

# This message will NOT be logged, although it's INFO level
logger.info("An ordinary info message")

# This message will be logged because it meets the filter's condition
logger.info("A special info message")

Here, despite both messages being INFO level, only the one containing "special" is logged due to the filter's custom condition.

 

Configure Python Logging using basicConfig()

The basicConfig function in Python's logging module is used to configure the default settings of the logging system. It's a simple way to set up logging, especially useful for small scripts or quick setup.

1. Basic Console Logging:

import logging

logging.basicConfig(level=logging.INFO)
logging.info("This will get logged to the console")

2. Logging to a File:

logging.basicConfig(filename='example.log', level=logging.DEBUG)
logging.debug("This message will be logged to example.log")

3. Custom Log Format:

logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.warning("This is a warning with a custom format")

4. Including Timestamps:

logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO, datefmt='%d-%b-%y %H:%M:%S')
logging.info("This log entry will have a timestamp")

 

Configure Python Logging using fileConfig()

fileConfig() is a function in Python's logging module that allows you to configure logging using a configuration file. This approach is beneficial for complex setups because it separates the logging configuration from the application code.

Key Features of fileConfig():

  • Separation of Configuration from Code: Keeps logging configuration in a separate file.
  • Ease of Maintenance: Allows updating logging settings without modifying the application code.
  • Flexibility: Supports various handlers, formatters, filters, and loggers.

The configuration file is typically in INI format with sections like [loggers], [handlers], [formatters], and [logger_loggername].

Logging Configuration File (logging_config.ini):

[loggers]
keys=root

[handlers]
keys=fileHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=fileHandler

[handler_fileHandler]
class=FileHandler
level=DEBUG
formatter=simpleFormatter
args=('test.log', 'a')

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

Let us understand this file:

  • [loggers]: Lists the loggers to be configured. Typically, it includes the root logger and any additional custom loggers.
  • [handlers]: Specifies the handlers to be used. Handlers determine where the log messages are directed (e.g., console, file).
  • [formatters]: Defines the formatters, which determine the format of the log output.
  • [logger_*]: Each logger defined in [loggers] gets a section. Here, you specify the log level, handlers, etc., for that logger.
  • [handler_*]: Each handler defined in [handlers] gets a section. You define the class (e.g., StreamHandler, FileHandler), level, formatter, and other specific arguments here.
  • [formatter_*]: Each formatter defined in [formatters] gets a section. You specify the format of the log message here.

Here is another sample INI file:

[loggers]
keys=root,exampleLogger

[handlers]
keys=consoleHandler,fileHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_exampleLogger]
level=DEBUG
handlers=fileHandler
qualname=exampleLogger
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=simpleFormatter
args=(sys.stdout,)

[handler_fileHandler]
class=FileHandler
level=DEBUG
formatter=simpleFormatter
args=('example.log', 'a')

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

In this example, there are two loggers (root and exampleLogger), two handlers (consoleHandler and fileHandler), and one formatter (simpleFormatter). The exampleLogger logs at DEBUG level to example.log, while the root logger logs at INFO level to the console. The format for log messages is specified in simpleFormatter.

Python Example:

import logging
import logging.config

logging.config.fileConfig('logging_config.ini')
logger = logging.getLogger(__name__)

logger.debug('This is a debug message')

In this setup, fileConfig('logging_config.ini') reads the logging configuration from logging_config.ini, which defines a file handler, a simple formatter, and sets the root logger's level to DEBUG. The Python code then gets a logger and logs a debug message. The log message is formatted and written to 'test.log' as per the configuration file.

 

How to add custom log levels

Adding custom log levels in Python logging involves a few steps: defining the new level, adding a method to both the logger and the logging module to handle it, and potentially configuring a formatter to display it properly.

1. Define the New Log Level:

import logging

VERBOSE_LEVEL_NUM = 15
logging.addLevelName(VERBOSE_LEVEL_NUM, "VERBOSE")

Add a Log Method for the New Level:

def verbose(self, message, *args, **kws):
    if self.isEnabledFor(VERBOSE_LEVEL_NUM):
        self._log(VERBOSE_LEVEL_NUM, message, args, **kws)

logging.Logger.verbose = verbose

Use the New Log Level:

logger = logging.getLogger(__name__)
logger.setLevel(VERBOSE_LEVEL_NUM)

logger.verbose("This is a verbose message")

Configure Formatter (Optional): If you're using a custom formatter, ensure it's set up to display the new log level.

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)

In this setup, the custom verbose messages will be logged with the VERBOSE level, providing more granularity in your logging setup.

Here's a complete example of how to define and use a custom log level in Python:

import logging

# Define a new log level
VERBOSE_LEVEL_NUM = 15
logging.addLevelName(VERBOSE_LEVEL_NUM, "VERBOSE")

# Create a new logging method for the level
def verbose(self, message, *args, **kws):
    if self.isEnabledFor(VERBOSE_LEVEL_NUM):
        self._log(VERBOSE_LEVEL_NUM, message, args, **kws)

logging.Logger.verbose = verbose

# Set up basic configuration and a simple formatter
logging.basicConfig(level=VERBOSE_LEVEL_NUM, format='%(asctime)s - %(levelname)s - %(message)s')

# Create a logger and use the custom level
logger = logging.getLogger(__name__)
logger.verbose("This is a verbose-level log message")

Output:

2023-11-12 00:14:46,482 - VERBOSE - This is a verbose-level log message

In this example, a new log level named "VERBOSE" is created and set lower than the standard INFO level. A new method verbose is added to the logger to handle this level. Finally, the logger uses this new level to log a message, demonstrating the custom log level in action.

 

Logging Uncaught Exceptions

Logging uncaught exceptions in Python is crucial for debugging and monitoring applications. It involves capturing exceptions that are not explicitly caught by try-except blocks.

Key Topics on Logging Uncaught Exceptions:

  1. Using sys.excepthook: Override Python's default exception handling.
  2. Creating a Custom Exception Handler: Define a function to log uncaught exceptions.
  3. Integrating with the Logging System: Ensure that uncaught exceptions are logged with appropriate severity.
import logging
import sys

def handle_exception(exc_type, exc_value, exc_traceback):
    """
    Custom exception handler logging uncaught exceptions.
    """
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return

    logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))

logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger(__name__)

# Override default excepthook
sys.excepthook = handle_exception

# Code that raises an uncaught exception
raise ValueError("This is an uncaught exception")

In this setup, handle_exception is a custom function that logs uncaught exceptions. It's set as the new sys.excepthook, which Python calls for unhandled exceptions. This way, exceptions that bubble up without being caught are logged. The KeyboardInterrupt is a special case to allow graceful exit on user interruption.

 

Summary

Python logging is a versatile system that allows you to track events in your software, making it easier to understand what's happening, especially during debugging and monitoring. Key components include loggers, handlers, formatters, and filters. Loggers capture messages, handlers direct them to various destinations, formatters define message structure, and filters provide additional message control. Python also supports custom log levels and uncaught exception logging. The system's flexibility lies in its configurability for simple scripts to complex applications.

For comprehensive information and official documentation, visit:

 

Deepak Prasad

He is the founder of GoLinuxCloud and brings over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels in various domains, from development to DevOps, Networking, and Security, ensuring robust and efficient solutions for diverse projects. You can reach out to him on his LinkedIn profile or join on Facebook page.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

2 thoughts on “Use Python Logging Like a PRO: Don't be a Rookie”

  1. I worked through the examples, all OK until the one under “Configuration file format”. I get this error –

    mick@DeskMint21:~/Documents/Programs/test$ python3 log_test8.py
    Traceback (most recent call last):
      File "/home/mick/Documents/Programs/test/log_test8.py", line 7, in 
        logging.config.fileConfig('logging.conf', disable_existing_loggers=False)
      File "/usr/lib/python3.10/logging/config.py", line 81, in fileConfig
        _install_loggers(cp, handlers, disable_existing_loggers)
      File "/usr/lib/python3.10/logging/config.py", line 230, in _install_loggers
        propagate = section.getint("propagate", fallback=1)
      File "/usr/lib/python3.10/configparser.py", line 1307, in get
        return _impl(self._name, option, raw=raw, vars=vars,
      File "/usr/lib/python3.10/configparser.py", line 819, in getint
        return self._get_conv(section, option, int, raw=raw, vars=vars,
      File "/usr/lib/python3.10/configparser.py", line 809, in _get_conv
        return self._get(section, conv, option, raw=raw, vars=vars,
      File "/usr/lib/python3.10/configparser.py", line 804, in _get
        return conv(self.get(section, option, **kwargs))
    ValueError: invalid literal for int() with base 10: ''
    mick@DeskMint21:~/Documents/Programs/test$ 

    What could cause this??

    Reply
    • There seems to be some bug because of which propagate=0 is showing as propagate= where ‘0’ is missing. You can add that manually and it should fix the issue. I will check this BUG in the meanwhile.

      Reply

Leave a Comment