Python Logging
Logging is a crucial aspect of software development that helps developers track events, debug issues, and monitor application health. Python's built-in logging
module provides a flexible framework for emitting log messages from your applications. In this guide, we'll explore how to implement effective logging in your Python projects.
Introduction to Python Logging
When developing applications, especially those running in production environments, it's essential to have visibility into what's happening within your code. While print()
statements might work for simple debugging during development, a proper logging system offers several advantages:
- Different severity levels for messages (debug, info, warning, error, critical)
- Configurable output destinations (console, files, network services)
- Structured formatting of log messages
- Filter capabilities based on importance or module
- Thread safety for concurrent applications
Python's logging
module is part of the standard library and follows best practices for application logging.
Getting Started with Basic Logging
Let's start with the simplest example of using the logging module:
import logging
# Log a simple message
logging.warning("This is a warning message")
Output:
WARNING:root:This is a warning message
By default, the logging module displays messages with severity level WARNING and above. The five standard logging levels in order of increasing severity are:
- DEBUG
- INFO
- WARNING
- ERROR
- CRITICAL
To log at different levels:
import logging
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message")
logging.error("This is an error message")
logging.critical("This is a critical message")
Output:
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
Notice that DEBUG and INFO messages don't appear by default. To change this, you need to configure the logging level:
import logging
# Set the logging level to INFO
logging.basicConfig(level=logging.INFO)
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message")
Output:
INFO:root:This is an info message
WARNING:root:This is a warning message
Configuring Logging Format
The default log format may not provide all the information you need. You can customize the format using the basicConfig()
function:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logging.debug("Debug message with timestamp")
Output:
2023-07-14 15:30:42,123 - root - DEBUG - Debug message with timestamp
Common format specifiers include:
%(asctime)s
: Human-readable date/time%(name)s
: Logger name%(levelname)s
: Level (DEBUG, INFO, etc.)%(message)s
: The actual log message%(filename)s
: Source filename%(funcName)s
: Function name%(lineno)d
: Line number%(process)d
: Process ID%(threadName)s
: Thread name
Logging to Files
To save logs to a file instead of (or in addition to) displaying them in the console:
import logging
# Configure logging to write to a file
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='app.log',
filemode='w' # 'w' to overwrite, 'a' to append
)
logging.debug("This message will be written to the file")
logging.info("Application started")
This will create an app.log
file in your working directory with the log messages.
Creating and Using Logger Objects
For larger applications, it's better to create specific loggers rather than using the root logger:
import logging
# Create a custom logger
logger = logging.getLogger('my_app')
# Set level for this logger
logger.setLevel(logging.DEBUG)
# Create handlers
c_handler = logging.StreamHandler() # Console handler
f_handler = logging.FileHandler('app.log') # File handler
# Set levels for handlers
c_handler.setLevel(logging.WARNING)
f_handler.setLevel(logging.ERROR)
# Create formatters and add to handlers
c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)
# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)
# Use the logger
logger.warning('This is a warning')
logger.error('This is an error')
Output in console:
my_app - WARNING - This is a warning
my_app - ERROR - This is an error
Output in file (app.log):
2023-07-14 15:35:23,456 - my_app - ERROR - This is an error
Logging in Multiple Modules
In larger applications with multiple modules, you should create loggers based on the module name. This creates a hierarchical structure of loggers:
File: main.py
:
import logging
import helper
# Configure root logger
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Create logger for this module
logger = logging.getLogger(__name__)
logger.info("Main module started")
helper.do_something()
logger.info("Main module finished")
File: helper.py
:
import logging
# Create logger for this module
logger = logging.getLogger(__name__)
def do_something():
logger.info("Helper function called")
try:
result = 10 / 0
except Exception as e:
logger.error(f"Error in helper function: {e}")
Output:
2023-07-14 15:40:12,789 - __main__ - INFO - Main module started
2023-07-14 15:40:12,790 - helper - INFO - Helper function called
2023-07-14 15:40:12,791 - helper - ERROR - Error in helper function: division by zero
2023-07-14 15:40:12,792 - __main__ - INFO - Main module finished
Real-World Application: Web Server Logging
Let's create a more practical example with Flask (a popular Python web framework):
import logging
from flask import Flask, request
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='webapp.log',
filemode='a'
)
# Create logger
logger = logging.getLogger('webapp')
# Create Flask app
app = Flask(__name__)
@app.before_request
def log_request_info():
logger.info(f'Request: {request.method} {request.path} from {request.remote_addr}')
@app.route('/')
def home():
logger.info('Home page accessed')
return 'Welcome to the homepage!'
@app.route('/api/data')
def get_data():
try:
# Simulate error
if request.args.get('error') == 'true':
raise ValueError("Requested error triggered")
logger.info('Data retrieved successfully')
return {"status": "success", "data": [1, 2, 3]}
except Exception as e:
logger.error(f'Error retrieving data: {str(e)}')
return {"status": "error", "message": str(e)}, 500
if __name__ == '__main__':
logger.info('Starting web application')
app.run(debug=True)
logger.info('Web application stopped')
This example demonstrates how logging can be used in a web application to:
- Record when the application starts and stops
- Log incoming requests with details
- Track specific route access
- Record errors with proper context
Logging Configuration Using Dictionary
For more advanced applications, you might want to configure logging using a dictionary:
import logging
import logging.config
# Define the logging configuration
config = {
'version': 1,
'formatters': {
'standard': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
},
'simple': {
'format': '%(levelname)s - %(message)s'
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'simple',
'stream': 'ext://sys.stdout'
},
'file': {
'class': 'logging.FileHandler',
'level': 'DEBUG',
'formatter': 'standard',
'filename': 'app_detailed.log',
'mode': 'a',
}
},
'loggers': {
'': { # root logger
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': True
},
'api': {
'handlers': ['file'],
'level': 'DEBUG',
'propagate': False
}
}
}
# Apply the configuration
logging.config.dictConfig(config)
# Use the loggers
root_logger = logging.getLogger()
api_logger = logging.getLogger('api')
root_logger.info("This is the root logger")
api_logger.debug("This is the API logger")
This configuration approach allows for more flexibility and is often used in production applications.
Rotating Log Files
For long-running applications, your log files might become too large. The RotatingFileHandler
helps manage file size:
import logging
from logging.handlers import RotatingFileHandler
# Create logger
logger = logging.getLogger('app')
logger.setLevel(logging.INFO)
# Create handler
# Max size of 5MB, keep 3 backup files
handler = RotatingFileHandler(
'app.log',
maxBytes=5*1024*1024, # 5MB
backupCount=3
)
handler.setLevel(logging.INFO)
# Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
# Add handler to logger
logger.addHandler(handler)
# Log messages
for i in range(1000000):
logger.info(f"Log message {i}: This is a test message")
This will create app.log
and, when it reaches 5MB, rotate it to app.log.1
and create a new app.log
. If you already have backups, they'll be renamed (e.g., app.log.1
becomes app.log.2
).
Capturing Stack Traces
When logging exceptions, it's useful to capture the full stack trace:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def divide(x, y):
try:
result = x / y
logger.info(f"Division result: {result}")
return result
except Exception as e:
logger.error("Exception occurred", exc_info=True)
# Or alternatively:
# logger.exception("Exception occurred") # automatically adds stack trace
divide(10, 0)
Output:
2023-07-14 16:05:23,456 - __main__ - ERROR - Exception occurred
Traceback (most recent call last):
File "example.py", line 15, in divide
result = x / y
ZeroDivisionError: division by zero
Summary
Python's logging module provides a robust framework for adding logging to your applications. Key takeaways include:
- Use the appropriate log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) based on message importance
- Configure logging early in your application
- Create specific loggers for different modules
- Format log messages to include relevant context
- Use file handlers for persistent logs
- Consider rotating logs for long-running applications
- Always log exceptions with stack traces
Proper logging practices will help you debug issues, monitor application health, and understand user behavior in production environments.
Additional Resources
- Python Official Documentation - Logging Module
- Python Logging Cookbook
- Real Python - Logging in Python
Exercises
- Create a simple script that logs messages at different severity levels to both console and file.
- Modify the logging format to include the line number and function name.
- Implement a rotating log system that creates a new log file each day.
- Create a multi-module application with hierarchical loggers.
- Create a logging configuration that sends critical errors to your email (hint: look up
SMTPHandler
).
By mastering Python logging, you'll be better equipped to build robust, maintainable, and observable applications - a critical skill for any DevOps practitioner or Python developer.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)