Overview
Flask uses Python’s standardlogging module to provide flexible logging capabilities. The framework automatically configures a logger for your application and provides utilities for common logging scenarios.
Basic Logging
Using the App Logger
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
app.logger.debug('Debug message')
app.logger.info('Info message')
app.logger.warning('Warning message')
app.logger.error('Error message')
app.logger.critical('Critical message')
return 'Check logs'
Log Levels
import logging
# Set log level
app.logger.setLevel(logging.DEBUG)
# Log levels (lowest to highest)
logging.DEBUG # Detailed information for debugging
logging.INFO # General informational messages
logging.WARNING # Warning messages (default level)
logging.ERROR # Error messages
logging.CRITICAL # Critical errors
Configuration
Default Handler
Flask automatically adds aStreamHandler when needed:
from flask.logging import default_handler
# Default format: [%(asctime)s] %(levelname)s in %(module)s: %(message)s
app.logger.debug('This uses the default handler')
Debug Mode Logging
app.config['DEBUG'] = True
# In debug mode:
# - Logger level is set to DEBUG
# - More verbose output
# - Stack traces shown in browser
Custom Handlers
File Handler
import logging
from logging.handlers import RotatingFileHandler
if not app.debug:
# Create file handler
file_handler = RotatingFileHandler(
'app.log',
maxBytes=10240000, # 10MB
backupCount=10
)
# Set format
file_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
))
# Set level
file_handler.setLevel(logging.INFO)
# Add to app logger
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Application startup')
Email Handler
from logging.handlers import SMTPHandler
if not app.debug:
mail_handler = SMTPHandler(
mailhost=('smtp.example.com', 587),
fromaddr='[email protected]',
toaddrs=['[email protected]'],
subject='Application Error',
credentials=(app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']),
secure=()
)
mail_handler.setLevel(logging.ERROR)
mail_handler.setFormatter(logging.Formatter(
'''
Message type: %(levelname)s
Location: %(pathname)s:%(lineno)d
Module: %(module)s
Function: %(funcName)s
Time: %(asctime)s
Message:
%(message)s
'''
))
app.logger.addHandler(mail_handler)
Syslog Handler
from logging.handlers import SysLogHandler
if not app.debug:
syslog_handler = SysLogHandler()
syslog_handler.setLevel(logging.WARNING)
app.logger.addHandler(syslog_handler)
Advanced Configuration
Using dictConfig
import logging.config
LOGGING_CONFIG = {
'version': 1,
'formatters': {
'default': {
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
},
'detailed': {
'format': '%(asctime)s %(levelname)s [%(name)s:%(lineno)d] %(message)s',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'default',
'stream': 'ext://sys.stdout',
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'INFO',
'formatter': 'detailed',
'filename': 'app.log',
'maxBytes': 10485760, # 10MB
'backupCount': 10,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'file']
}
}
logging.config.dictConfig(LOGGING_CONFIG)
Using fileConfig
# logging.conf
[loggers]
keys=root
[handlers]
keys=consoleHandler,fileHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler,fileHandler
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[handler_fileHandler]
class=handlers.RotatingFileHandler
level=INFO
formatter=simpleFormatter
args=('app.log', 'a', 10485760, 10)
[formatter_simpleFormatter]
format=[%(asctime)s] %(levelname)s in %(module)s: %(message)s
import logging.config
logging.config.fileConfig('logging.conf')
app.logger = logging.getLogger('root')
Structured Logging
JSON Logging
import json
import logging
class JSONFormatter(logging.Formatter):
def format(self, record):
log_data = {
'timestamp': self.formatTime(record),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno,
}
if record.exc_info:
log_data['exception'] = self.formatException(record.exc_info)
return json.dumps(log_data)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
app.logger.addHandler(handler)
Contextual Logging
from flask import g, request
import logging
class RequestFormatter(logging.Formatter):
def format(self, record):
record.url = request.url if request else 'N/A'
record.remote_addr = request.remote_addr if request else 'N/A'
record.method = request.method if request else 'N/A'
return super().format(record)
formatter = RequestFormatter(
'[%(asctime)s] %(remote_addr)s %(method)s %(url)s '
'%(levelname)s: %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
app.logger.addHandler(handler)
Common Patterns
Request Logging
from flask import request
import time
@app.before_request
def log_request_info():
"""Log details about each request."""
app.logger.info(
f'Request: {request.method} {request.path} '
f'from {request.remote_addr}'
)
g.start_time = time.time()
@app.after_request
def log_response_info(response):
"""Log response details."""
duration = time.time() - g.start_time
app.logger.info(
f'Response: {response.status_code} '
f'in {duration:.3f}s'
)
return response
Error Logging
@app.errorhandler(Exception)
def log_exception(error):
"""Log all unhandled exceptions."""
app.logger.error(
'Unhandled exception',
exc_info=error,
extra={
'url': request.url,
'method': request.method,
'ip': request.remote_addr,
}
)
return 'Internal Server Error', 500
Database Query Logging
from flask_sqlalchemy import get_debug_queries
@app.after_request
def log_slow_queries(response):
"""Log slow database queries."""
for query in get_debug_queries():
if query.duration >= 0.5: # 500ms
app.logger.warning(
f'Slow query: {query.statement} '
f'took {query.duration:.3f}s'
)
return response
Security Event Logging
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
app.logger.info(
f'Successful login for user {username} '
f'from {request.remote_addr}'
)
return redirect(url_for('dashboard'))
else:
app.logger.warning(
f'Failed login attempt for user {username} '
f'from {request.remote_addr}'
)
flash('Invalid credentials')
return redirect(url_for('login'))
wsgi_errors_stream
Flask provides a special stream that routes to the WSGI server’s error stream:from flask.logging import wsgi_errors_stream
import logging
handler = logging.StreamHandler(wsgi_errors_stream)
app.logger.addHandler(handler)
Third-Party Integration
Sentry
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
dsn=app.config['SENTRY_DSN'],
integrations=[FlaskIntegration()],
traces_sample_rate=1.0
)
# Errors are automatically captured
Loguru
from loguru import logger
# Configure loguru
logger.add(
'app.log',
rotation='10 MB',
retention='1 month',
compression='zip'
)
# Use loguru instead of app.logger
@app.route('/')
def index():
logger.info('Request received')
return 'Hello'
Python-JSON-Logger
from pythonjsonlogger import jsonlogger
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
app.logger.addHandler(logHandler)
Best Practices
1. Use Appropriate Log Levels
# DEBUG: Detailed diagnostic info
app.logger.debug(f'Processing user {user_id} with options {options}')
# INFO: General informational messages
app.logger.info(f'User {username} logged in')
# WARNING: Something unexpected but handled
app.logger.warning(f'Deprecated API endpoint used: {request.path}')
# ERROR: Error occurred but app continues
app.logger.error(f'Failed to send email to {email}: {error}')
# CRITICAL: Critical error, app may not continue
app.logger.critical('Database connection lost')
2. Include Contextual Information
app.logger.error(
'Payment processing failed',
extra={
'user_id': user.id,
'order_id': order.id,
'amount': order.total,
'error_code': error.code
}
)
3. Don’t Log Sensitive Data
# Bad: Logging sensitive information
app.logger.info(f'User logged in with password {password}')
# Good: Log without sensitive data
app.logger.info(f'User {username} logged in from {request.remote_addr}')
4. Use Rotating File Handlers
from logging.handlers import RotatingFileHandler
# Prevent logs from filling disk
handler = RotatingFileHandler(
'app.log',
maxBytes=10485760, # 10MB
backupCount=10 # Keep 10 backup files
)
5. Configure Different Levels for Different Environments
if app.config['ENV'] == 'production':
app.logger.setLevel(logging.WARNING)
else:
app.logger.setLevel(logging.DEBUG)
Testing Logging
import logging
def test_logging(caplog):
"""Test that logging works correctly."""
with caplog.at_level(logging.INFO):
app.logger.info('Test message')
assert 'Test message' in caplog.text
def test_error_logged(client, caplog):
"""Test that errors are logged."""
with caplog.at_level(logging.ERROR):
response = client.get('/error')
assert 'Error occurred' in caplog.text
Performance Considerations
Lazy Logging
# Bad: String formatting always happens
app.logger.debug('Processing ' + str(large_object))
# Good: Formatting only happens if message is logged
app.logger.debug('Processing %s', large_object)
Conditional Logging
if app.logger.isEnabledFor(logging.DEBUG):
# Only compute expensive debug info if debugging
debug_info = compute_expensive_debug_info()
app.logger.debug('Debug info: %s', debug_info)
