Python Logging — Complete Guide with Examples
In this tutorial, you will learn about Python Logging. We cover key concepts, practical examples, and best practices to help you master this topic.
Print statements work for debugging on your laptop. In production, you need logging — the ability to record events with severity levels, timestamps, and structured context, routed to different destinations (console, files, external services).
Python's logging module is a production-ready logging framework built into the standard library. Every serious Python application uses it. At DodaTech, logging is critical for security auditing — every authentication attempt, file access, and configuration change is logged with structured detail.
What You'll Learn
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
- Handlers: StreamHandler, FileHandler, RotatingFileHandler
- Formatters: timestamps, loggers, structured output
- Logger hierarchy and propagation
- Configuration with dictConfig
- Structured logging with JSON
Why Logging Matters
Without logging, when something breaks in production you have no idea what happened. With proper logging, you can trace the exact sequence of events leading to a failure. In security-sensitive applications like DodaTech's threat detection engine, every anomalous event is logged and correlated.
Basic Logging
import logging
# Simple configuration
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Create a logger
logger = logging.getLogger(__name__)
# Log at different levels
logger.debug("This won't show — level is INFO")
logger.info("Application started")
logger.warning("Disk space low: 500MB remaining")
logger.error("Failed to connect to database: Connection refused")
logger.critical("System shutting down!")
Expected output:
2026-06-29 12:00:00,000 - __main__ - INFO - Application started
2026-06-29 12:00:00,001 - __main__ - WARNING - Disk space low: 500MB remaining
2026-06-29 12:00:00,002 - __main__ - ERROR - Failed to connect to database: Connection refused
2026-06-29 12:00:00,003 - __main__ - CRITICAL - System shutting down!
Log Levels Explained
| Level | Value | When to Use |
|---|---|---|
| DEBUG | 10 | Detailed info for debugging (not in production) |
| INFO | 20 | Normal operation: "Server started", "User logged in" |
| WARNING | 30 | Something unexpected but not an error: "Disk at 80%" |
| ERROR | 40 | A failure that doesn't stop the app: "Failed to send email" |
| CRITICAL | 50 | The app can't continue: "Database connection lost" |
Handlers and Formatters
Handlers send log records to different destinations:
import logging
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# Console handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter(
"%(levelname)s: %(message)s"
))
# File handler with rotation
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
))
# Rotating file handler (keeps last 5 files, 1MB each)
from logging.handlers import RotatingFileHandler
rotating = RotatingFileHandler(
"app.log", maxBytes=1024*1024, backupCount=5
)
logger.addHandler(console)
logger.addHandler(file_handler)
logger.info("This goes to both console and file")
Logger Hierarchy
Loggers are organized by name with dot-separated hierarchy:
import logging
# Root logger
root = logging.getLogger()
root.setLevel(logging.WARNING)
# Child loggers
app_logger = logging.getLogger("myapp")
db_logger = logging.getLogger("myapp.database")
api_logger = logging.getLogger("myapp.api")
# Messages propagate to parent loggers
api_logger.error("API request failed") # Also logged by myapp, then root
Configuration with dictConfig
For production applications, use a dictionary-based configuration:
import logging.config
LOGGING_CONFIG = {
"version": 1,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
},
"detailed": {
"format": "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d: %(message)s"
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detailed",
"filename": "app.log",
"maxBytes": 10485760, # 10MB
"backupCount": 5,
},
"error_file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "detailed",
"filename": "errors.log",
"maxBytes": 10485760,
"backupCount": 3,
},
},
"loggers": {
"myapp": {
"level": "DEBUG",
"handlers": ["console", "file"],
"propagate": False,
},
"myapp.database": {
"level": "WARNING",
"handlers": ["error_file"],
},
},
"root": {
"level": "WARNING",
"handlers": ["console"],
},
}
logging.config.dictConfig(LOGGING_CONFIG)
Structured Logging (JSON)
For log aggregation systems like ELK or Loki, JSON-formatted logs are ideal:
import json
import logging
class JSONFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"line": record.lineno,
}
if hasattr(record, "extra_data"):
log_entry["extra"] = record.extra_data
if record.exc_info and record.exc_info[0]:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry)
logger = logging.getLogger("myapp")
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
# With extra context
logger.info("User logged in", extra={"extra_data": {
"user_id": 42,
"ip": "192.168.1.1",
"method": "oauth"
}})
Expected output (JSON):
{"timestamp": "2026-06-29 12:00:00,000", "level": "INFO", "logger": "myapp", "message": "User logged in", "module": "<module>", "line": 10, "extra": {"user_id": 42, "ip": "192.168.1.1", "method": "oauth"}}
Common Pitfalls
Pitfall 1: Multiple Handlers = Duplicate Messages
# If you call basicConfig() AND add handlers, you get duplicates
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler()) # BAD: now messages print twice!
Pitfall 2: Not Using name
# BAD: hardcoded logger name
logger = logging.getLogger("myapp")
# GOOD: uses module path — enables hierarchy
logger = logging.getLogger(__name__)
# For myapp/database.py, __name__ is "myapp.database"
Pitfall 3: Logging Sensitive Data
# NEVER log passwords, tokens, or PII
logger.info(f"User logged in: {username}:{password}") # SECURITY RISK!
logger.info(f"Credit card: {card_number}") # PCI violation!
# Log safely
logger.info(f"User logged in: {username}")
logger.info(f"Credit card: ****{card_number[-4:]}")
Practice Questions
Configure logging to write WARNING and above to stderr, and everything to a file.
Create a custom handler that sends ERROR and CRITICAL logs to a Webhook URL.
Write a decorator that logs function calls with arguments and return values at DEBUG level.
Implement log sampling: log only every 10th DEBUG message to reduce volume.
Create a logger Adapter that automatically adds user_id and request_id to every log entry.
Challenge: Log Monitor
Write a class that watches a log file (like tail -f) and alerts when:
- More than 5 ERROR messages appear in 60 seconds
- A CRITICAL message appears
- More than 100 WARNING messages appear in 5 minutes
Use rotating file handling and implement debouncing (don't alert more than once per minute per condition). DodaTech's monitoring system uses this exact pattern to detect security incidents in real-time.
Real-World Task: Audit Logging
DodaTech applications need audit logs for Compliance. Create a logging setup that:
- Logs all user actions (login, logout, file access, config changes) to a JSON audit log
- Audit log is in a separate file, never rotated (append-only)
- Regular application logs go to a rotating file
- Security events (failed logins, permission errors) go to a separate security log
- Each log entry includes: timestamp, user_id, action, resource, status, ip_address
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro