Skip to main content

Overview

Flask uses Python’s standard logging 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 a StreamHandler 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)

Build docs developers (and LLMs) love