Logging & Monitoring
Reviewed & published by Brayan K
Logging records what your app is doing — requests, warnings, and errors — so you can understand its behavior and diagnose problems in production.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll configure Python logging with levels and formatters, log every request, emit structured JSON logs, and learn how errors reach a monitoring service like Sentry.
What You'll Learn in This Lesson
- Configuring loggers, handlers, levels, and formats
- Filtering noise with logging levels
- Logging every request with app.logger
- Emitting structured JSON logs
- Counting and aggregating log records
- Sending errors to a service like Sentry
1 Loggers, Levels & Formatters
A logger produces messages; a handler sends them somewhere (console, file, remote service); a formatter shapes how each line looks; and a level filters out anything below a chosen severity. Flask's app.logger is a plain Python logger, so the same setup applies everywhere.
The runnable example sends logs into a StringIO buffer so you can read exactly what was recorded. The level is INFO, so the debug line is filtered out and only INFO and above appear.
# Configure Python logging and capture output to a StringIO
import logging
from io import StringIO
buffer = StringIO()
handler = logging.StreamHandler(buffer)
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
log = logging.getLogger("myapp")
log.setLevel(logging.INFO) # INFO and above are recorded
log.addHandler(handler)
log.debug("this is filtered out (below INFO)")
log.info("server started")
log.warning("disk almost full")
log.error("database connection failed")
print(buffer.getvalue(), end="")
# INFO: server started
# WARNING: disk almost full
# ERROR: database connection failed
# ✅ Expected output:
# INFO: server started
# WARNING: disk almost full
# ERROR: database connection failedThe debug message never appears because it's below the INFO threshold — that's how you keep production logs focused on what matters.
2 Logging Every Request
A common need is an access log: one line per request showing the method, path, and status. You attach a before_request hook to log the incoming request and an after_request hook to log the response. The runnable example routes app.logger into a buffer and exercises two requests — one that succeeds and one 404.
# Logging requests with Flask's app.logger via before/after hooks
import logging
from io import StringIO
from flask import Flask, request
app = Flask(__name__)
# Route app.logger's output into a buffer we can read
buffer = StringIO()
handler = logging.StreamHandler(buffer)
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
app.logger.handlers = [handler]
app.logger.setLevel(logging.INFO)
@app.before_request
def log_request():
app.logger.info(f"--> {request.method} {request.path}")
@app.after_request
def log_response(response):
app.logger.info(f"<-- {response.status_code} {request.path}")
return response
@app.route("/hello")
def hello():
return "hi"
with app.test_client() as c:
c.get("/hello")
c.get("/missing") # 404
print(buffer.getvalue(), end="")
# INFO --> GET /hello
# INFO <-- 200 /hello
# INFO --> GET /missing
# INFO <-- 404 /missing
# ✅ Expected output:
# INFO --> GET /hello
# INFO <-- 200 /hello
# INFO --> GET /missing
# INFO <-- 404 /missingEach request produces an arrow-in and an arrow-out line, including the 404 — a complete trace of what hit your app and how it responded.
3 Structured Logs & Error Monitoring
Plain-text logs are easy to read but hard for machines to search. Structured logs emit each entry as JSON with named fields, so a log platform can filter by user_id or count level instantly. The runnable example below uses a custom JsonFormatter that serializes the level, message, and any extra fields you pass.
# Structured (JSON) logs: machine-readable for log aggregators
import logging, json
from io import StringIO
class JsonFormatter(logging.Formatter):
def format(self, record):
payload = {
"level": record.levelname,
"message": record.getMessage(),
"logger": record.name,
}
# include any extra fields attached to the log call
if hasattr(record, "user_id"):
payload["user_id"] = record.user_id
return json.dumps(payload, sort_keys=True)
buffer = StringIO()
handler = logging.StreamHandler(buffer)
handler.setFormatter(JsonFormatter())
log = logging.getLogger("structured")
log.setLevel(logging.INFO)
log.handlers = [handler]
log.info("user logged in", extra={"user_id": 42})
log.warning("rate limit near")
for line in buffer.getvalue().strip().splitlines():
print(line)
# {"level": "INFO", "logger": "structured", "message": "user logged in", "user_id": 42}
# {"level": "WARNING", "logger": "structured", "message": "rate limit near"}
# ✅ Expected output:
# {"level": "INFO", "logger": "structured", "message": "user logged in", "user_id": 42}
# {"level": "WARNING", "logger": "structured", "message": "rate limit near"}🔬 Logging Levels Cheat Sheet
| Level | Use it for |
|---|---|
| DEBUG | Detailed dev diagnostics |
| INFO | Normal events (request served, job done) |
| WARNING | Something odd but recoverable |
| ERROR | A request or operation failed |
| CRITICAL | The app may be unable to continue |
🎯 Your Turn: Fill in the Blanks
Complete the setup below. Replace each ___ so only WARNING and above are recorded.
# 🎯 YOUR TURN: replace each ___ so only WARNING+ is recorded
import logging
from io import StringIO
buffer = StringIO()
handler = logging.StreamHandler(buffer)
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
log = logging.getLogger("turn")
log.setLevel(logging.___) # threshold: WARNING and above
log.___(handler) # attach the handler to the logger
log.info("ignored")
log.warning("heads up")
log.error("boom")
print(buffer.getvalue(), end="")
# WARNING: heads up
# ERROR: boomCommon Errors & Quick Reference
❌ My log messages never show up
Your logger's level is too high, or it has no handler. Set setLevel(logging.INFO) and add a handler. Remember the level filters at both the logger and the handler.
❌ Every line is logged twice
You added the same handler more than once, or messages propagate to the root logger which also has a handler. Add handlers a single time, or set logger.propagate = False.
| Task | Code |
|---|---|
| Log info | app.logger.info("msg") |
| Log an exception | app.logger.exception(e) |
| Set level | logger.setLevel(logging.INFO) |
| Rotate files | RotatingFileHandler(...) |
🏆 Mini Challenge: Count Logs by Level
Build a custom handler that counts how many records arrive at each level.
- Subclass logging.Handler and override emit
- Increment a Counter keyed on record.levelname
- Print the totals after emitting several records
# 🏆 Mini Challenge: count log records by level
import logging
from collections import Counter
class CountingHandler(logging.Handler):
def __init__(self):
super().__init__()
self.counts = Counter()
def emit(self, record):
self.counts[record.levelname] += 1
counter = CountingHandler()
log = logging.getLogger("counter")
log.setLevel(logging.DEBUG)
log.addHandler(counter)
log.info("a"); log.info("b")
log.warning("c")
log.error("d"); log.error("e"); log.error("f")
print(dict(counter.counts)) # {'INFO': 2, 'WARNING': 1, 'ERROR': 3}❓ Frequently Asked Questions
Lesson complete — you can see inside your running app!
You configured loggers, levels, and formatters, logged every request, emitted structured JSON logs, counted records by level, and learned how errors reach a service like Sentry.
Practice quiz
In Python logging, what does a handler do?
- Filters out low-severity messages
- Sends log records somewhere, like the console or a file
- Formats the timestamp only
- Creates the logger object
Answer: Sends log records somewhere, like the console or a file. A handler routes log records to a destination such as a console, file, or remote service.
What is the role of a formatter?
- It chooses the destination
- It sets the level threshold
- It shapes how each log line looks
- It creates handlers
Answer: It shapes how each log line looks. A formatter controls the layout of each emitted log line.
What does a logging level control?
- The file path
- The color of output
- The handler type
- Which severities are recorded and which are filtered out
Answer: Which severities are recorded and which are filtered out. The level filters out anything below the chosen severity.
What is Flask's app.logger?
- A plain Python logging.Logger
- A database connection
- A Jinja filter
- A request hook
Answer: A plain Python logging.Logger. app.logger is an ordinary Python logger, so standard logging setup applies.
With the level set to INFO, which call is filtered out?
- log.error(...)
- log.debug(...)
- log.warning(...)
- log.info(...)
Answer: log.debug(...). DEBUG is below INFO, so debug messages are filtered out.
Which hook is ideal for logging each incoming request?
- @app.teardown
- @app.route
- @app.before_request
- @app.errorhandler
Answer: @app.before_request. @app.before_request runs before each request, perfect for an access log entry.
What handler keeps log files from growing forever?
- StreamHandler
- NullHandler
- SocketHandler
- RotatingFileHandler
Answer: RotatingFileHandler. RotatingFileHandler rotates files once they reach a size limit.
What are structured (JSON) logs good for?
- Being searched and filtered by machines and log platforms
- Looking prettier in a terminal
- Reducing CPU usage
- Encrypting messages
Answer: Being searched and filtered by machines and log platforms. JSON logs expose named fields so aggregators can filter and count them.
What does a service like Sentry capture automatically?
- Every database row
- All HTTP headers
- Static files
- Unhandled exceptions with full context
Answer: Unhandled exceptions with full context. Sentry captures unhandled exceptions with stack trace and request context.
Which level fits a normal event like 'request served'?
- CRITICAL
- INFO
- ERROR
- DEBUG
Answer: INFO. INFO is used for normal operational events.
Continue this course
- Previous: Sending Email (Flask-Mail)
- Next: Custom CLI Commands (flask CLI) — Add your own flask commands with click options and arguments
- Quick reference: Flask cheat sheet
Frequently asked questions
What is app.logger in Flask?
app.logger is a standard Python logging.Logger that Flask sets up for you. You call app.logger.info(...), app.logger.warning(...), or app.logger.error(...) anywhere in your app to record what's happening. Because it's a normal logger, you configure it with the same handlers, levels, and formatters as any Python logging setup.
What are logging levels and why do they matter?
Levels rank messages by severity: DEBUG, INFO, WARNING, ERROR, CRITICAL. You set a threshold so only messages at that level or above are recorded — for example, level=INFO drops DEBUG noise. This lets you run verbose logs in development and quieter, higher-signal logs in production.
What is a handler and a formatter?
A handler decides where log records go — the console, a file, a remote service. A formatter decides how each record looks — the timestamp, level, logger name, and message. You attach a formatter to a handler and a handler to a logger, so one logger can write nicely formatted lines to multiple destinations.
What are structured logs?
Structured logs emit each entry as machine-readable data (usually JSON) instead of a free-form string. A line like {"level":"INFO","user_id":42,"message":"login"} can be searched, filtered, and aggregated by tools like Elasticsearch, Datadog, or CloudWatch far more easily than plain text.
How do errors get to a service like Sentry?
You install an SDK (for example sentry-sdk with its Flask integration) and initialize it at startup. It hooks into Flask's error handling, so any unhandled exception is captured with its stack trace, request data, and context and sent to Sentry, where you get alerts and grouping — without writing logging code for every route.