
Flask Logging Best Practices
Flask is a fantastic framework for building web applications, but without proper logging, debugging and monitoring can become a nightmare. You need to know what’s going on inside your app when it’s running, especially in production. Let’s explore the best practices for setting up logging in your Flask applications so you can keep track of errors, user activities, and system behavior efficiently.
Setting Up Basic Logging
By default, Flask uses Python’s built-in logging module. However, the default setup might not be sufficient for a production application. Let’s start with a basic configuration.
You can easily configure logging directly in your Flask app. Here's a simple example:
import logging
from flask import Flask
app = Flask(__name__)
if not app.debug:
# Set up logging for production
import logging
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler('app.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Flask application startup')
This code sets up a rotating file handler that writes logs to a file called app.log
, rotating when the file reaches 10KB and keeping 10 backup files. The log format includes the timestamp, log level, message, and the file and line number where the log was generated.
Why rotating files? They prevent your disk from filling up with log data. Without rotation, a single log file could grow indefinitely.
Another useful practice is to set different log levels for development and production. In development, you might want DEBUG
level logs, while in production, INFO
or WARNING
might be more appropriate to avoid noise.
Environment | Recommended Level | Purpose |
---|---|---|
Development | DEBUG | Detailed output for debugging |
Staging | INFO | General operational information |
Production | WARNING | Only important messages and errors |
Remember, never run your production app with DEBUG=True
. It’s a security risk and can leak sensitive information.
Structured Logging for Better Analysis
While plain text logs are readable, structured logging (like JSON) makes it easier to parse and analyze logs, especially when using log management systems.
Here’s how you can set up JSON logging:
import json
import logging
from pythonjsonlogger import jsonlogger
class JsonFormatter(jsonlogger.JsonFormatter):
def parse(self):
return [
'timestamp',
'level',
'message',
'module',
'funcName',
'lineno',
]
formatter = JsonFormatter('%(timestamp)s %(level)s %(message)s %(module)s %(funcName)s %(lineno)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
This requires the python-json-logger
package, which you can install via pip. Structured logs are extremely useful when you’re using tools like ELK Stack, Splunk, or cloud services like AWS CloudWatch or Google Stackdriver.
Benefits of structured logging: - Easier to query and filter - Better integration with monitoring tools - Consistent format across services
Using Application and Request Contexts
In Flask, you often want to log information specific to a request, such as the user ID or request ID. You can use Flask’s application context and request context to enrich your logs.
Consider adding a request ID to correlate all logs for a single request:
from flask import request, g
import uuid
@app.before_request
def before_request():
g.request_id = uuid.uuid4().hex
@app.after_request
def after_request(response):
app.logger.info(
'Request completed',
extra={
'request_id': g.request_id,
'path': request.path,
'method': request.method,
'status': response.status_code
}
)
return response
Now, every log message can include this request_id
. You might need a custom logging filter to automatically add it to every log record.
class RequestFilter(logging.Filter):
def filter(self, record):
if hasattr(g, 'request_id'):
record.request_id = g.request_id
else:
record.request_id = 'N/A'
return True
app.logger.addFilter(RequestFilter())
Then update your formatter to include request_id
:
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - [%(request_id)s] - %(message)s')
This approach greatly improves traceability. When an error occurs, you can easily find all logs related to that specific request.
Handling External Loggers
Your Flask app might use other libraries that also log messages (e.g., SQLAlchemy, requests). It’s important to configure these loggers appropriately to avoid missing critical information or getting overwhelmed by verbose output.
You can configure the root logger to capture logs from all modules:
import logging
from logging.handlers import RotatingFileHandler
# Set up root logger
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
# Create a file handler for the root logger
file_handler = RotatingFileHandler('all_logs.log', maxBytes=10240, backupCount=10)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Optionally, set levels for specific noisy loggers
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
This ensures that logs from all parts of your application are captured. However, be cautious: some libraries can be very verbose. You might want to set higher log levels for them to reduce noise.
Common loggers to adjust:
- sqlalchemy.engine
(set to WARNING unless debugging DB issues)
- urllib3
(can be very verbose)
- requests
(similar to urllib3)
Logger Name | Recommended Level | Reason |
---|---|---|
sqlalchemy.engine | WARNING | Avoid logging every SQL query |
urllib3 | WARNING | Reduce HTTP request noise |
requests | WARNING | Same as above |
Logging Errors and Exceptions
Unhandled exceptions can crash your app. It’s crucial to log these errors with as much context as possible.
Flask provides a way to log exceptions via @app.errorhandler
:
@app.errorhandler(500)
def internal_server_error(e):
app.logger.error('Unhandled exception', exc_info=e)
return "Internal server error", 500
But for more control, you can use a custom error handler that logs additional context:
@app.errorhandler(Exception)
def handle_exception(e):
app.logger.error(
'Unhandled exception',
exc_info=e,
extra={
'request_id': g.get('request_id', 'N/A'),
'path': request.path,
'method': request.method,
'user_agent': request.headers.get('User-Agent')
}
)
return "Internal server error", 500
Always use exc_info
when logging exceptions to capture the stack trace. This is invaluable for debugging.
For expected errors (like validation failures), use app.logger.warning
instead of error to avoid alerting on non-critical issues.
Centralized Logging for Distributed Apps
If your application runs on multiple servers (e.g., in a Kubernetes cluster), writing logs to local files isn’t sufficient. You need a way to aggregate logs from all instances.
Common solutions: - Use a logging agent (e.g., Fluentd, Filebeat) to ship logs to a central system. - Send logs directly to a cloud service (e.g., AWS CloudWatch Logs, Google Stackdriver). - Use a dedicated log management service (e.g., Loggly, Papertrail).
Here’s an example of sending logs to Syslog, which can then be collected centrally:
import logging
from logging.handlers import SysLogHandler
syslog_handler = SysLogHandler(address=('logs.example.com', 514))
formatter = logging.Formatter('%(name)s: %(levelname)s %(message)s')
syslog_handler.setFormatter(formatter)
app.logger.addHandler(syslog_handler)
Alternatively, for cloud services, you might use a library specific to that service. For example, for Google Cloud:
import google.cloud.logging
from google.cloud.logging.handlers import CloudLoggingHandler
client = google.cloud.logging.Client()
handler = CloudLoggingHandler(client)
app.logger.addHandler(handler)
Key takeaway: In distributed environments, never rely solely on local log files. Always have a mechanism to centralize logs for full visibility.
Performance Considerations
Logging is I/O-intensive and can impact your application’s performance if not done carefully. Here are some tips to minimize the impact:
- Use asynchronous logging: The
logging
module by default blocks until the log write is complete. Consider usingQueueHandler
andQueueListener
for non-blocking logging.
from logging.handlers import QueueHandler, QueueListener
import queue
log_queue = queue.Queue(-1) # No limit on queue size
queue_handler = QueueHandler(log_queue)
file_handler = RotatingFileHandler('app.log', maxBytes=10240, backupCount=10)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
listener = QueueListener(log_queue, file_handler)
listener.start()
app.logger.addHandler(queue_handler)
-
Be mindful of log volume: Logging too much information can slow down your app and fill up disks. Use appropriate log levels and avoid logging large objects (like full request bodies) unless necessary.
-
Sample debug logs in production: If you need debug logging in production for troubleshooting, consider sampling—only log a fraction of requests to reduce volume.
import random
if random.random() < 0.01: # Sample 1% of requests
app.logger.debug('Detailed debug info: %s', some_data)
Security and Compliance
Logs often contain sensitive information. It’s crucial to ensure that you’re not logging data that could compromise security or violate regulations like GDPR.
Avoid logging: - Passwords - API keys - Personal identifiable information (PII) - Credit card numbers
You can use filters to redact sensitive information:
class RedactingFilter(logging.Filter):
def __init__(self, patterns):
super().__init__()
self.patterns = patterns
def filter(self, record):
if hasattr(record, 'msg'):
for pattern in self.patterns:
record.msg = re.sub(pattern, '[REDACTED]', record.msg)
return True
redactor = RedactingFilter([r'password=[^&]*', r'api_key=[^&]*'])
app.logger.addFilter(redactor)
This simple filter redacts any password
or api_key
query parameters from log messages.
Regularly audit your logs to ensure no sensitive data is being captured accidentally.
Testing Your Logging Setup
Don’t wait for production to find out your logging isn’t working. Write tests to verify your logging configuration.
Use Python’s unittest
module to test log messages:
import unittest
from unittest.mock import patch
from io import StringIO
import logging
class TestLogging(unittest.TestCase):
def test_log_output(self):
with patch('sys.stdout', new=StringIO()) as fake_out:
app.logger.info('Test message')
self.assertIn('Test message', fake_out.getvalue())
You can also test that certain events trigger the expected log messages.
Integration with monitoring: Ensure your logs are being picked up by your monitoring system. Set up alerts for critical errors—but avoid alerting fatigue by only alerting on issues that require immediate attention.
Final Thoughts
Effective logging is a cornerstone of maintainable and reliable Flask applications. By following these best practices—setting up proper log rotation, enriching logs with context, handling external loggers, centralizing logs in distributed environments, considering performance and security, and testing your setup—you’ll be well-equipped to troubleshoot issues and understand your application’s behavior in production.
Remember, the goal of logging is to provide insight without overwhelming noise. Start with a simple setup and refine it as your application grows. Happy logging