Logging
Every TaskFlow view written so far has been easy to poke at with a stray print()
statement while developing — run the dev server, watch the terminal, see what a variable holds.
That habit quietly breaks the moment an app actually ships. A print() only ever goes
to whichever terminal happened to be running the process at that instant; on a production server
there usually is no terminal watching, so the output goes nowhere and is lost forever.
Worse, every print() is treated as equally important — there's no way to say "this one
only matters when I'm actively debugging" versus "this one means something is actually broken and
someone should be paged." Python's built-in logging module, and Django's
configuration around it, solves both problems properly.
Getting a Logger
The standard pattern is two lines at the top of any module that needs to log something — typically
a views.py:
# taskflow/views.py
import logging
logger = logging.getLogger(__name__)
__name__ is the dotted path of the current module — for this file, something like
taskflow.views. Passing it to getLogger() names the logger after the
module it came from, so when a message shows up later you know immediately which part of TaskFlow
produced it, without having to add that detail by hand every time.
Log Levels: How Serious Is This Message?
Where print() treats every line the same, a logger call always carries a
severity level baked into the method name itself. This is the whole point of
logging over printing: you can later decide, in one place, which levels actually get recorded and
which get ignored, without touching the code that produces them.
| Level | Use it for |
|---|---|
logger.debug() | Fine-grained detail useful only while actively developing — variable values, "reached this branch," etc. |
logger.info() | Routine, expected events worth a record — "task created," "user logged in" |
logger.warning() | Something unexpected happened but the app handled it and kept going |
logger.error() | Something actually broke — an operation failed and likely needs attention |
logger.critical() | The application itself is in serious trouble — rare, and usually paired with alerting |
A useful rule of thumb while writing TaskFlow's views: reach for warning when a user
did something a little off but the view can recover gracefully on its own, and reach for
error only when something genuinely failed — a database write that didn't go through,
an external call that raised an exception.
Django's LOGGING Setting
A logger call by itself doesn't decide where its output ends up — that's the job of Django's
LOGGING dictionary in settings.py. It wires three pieces together:
formatters (how a message is rendered as text), handlers (where a
message actually goes — console, a file, email), and loggers (which handlers a
given logger name should use, and at what minimum level).
# taskflow_project/settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '{levelname} {asctime} {name}: {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
},
'loggers': {
'taskflow': {
'handlers': ['console'],
'level': 'INFO',
},
},
}
Reading this from the inside out: the console handler sends anything it receives to
the terminal, formatted using simple. The taskflow logger entry says any
logger whose name starts with taskflow — which covers taskflow.views,
since it's a dotted child of that name — should send its messages to the console
handler, but only if the message is INFO level or more severe. A debug()
call from that same logger would simply be dropped, since DEBUG ranks below
INFO in severity.
A Worked Example: Warning on a Redundant Completion
Suppose a TaskFlow view lets someone mark a task complete, and it turns out the task was already marked complete before this request came in. That's not really an error — nothing crashed — but it's also not quite normal, and it's worth a record in case it points at a bug in the frontend that keeps re-submitting the same request:
# taskflow/views.py
import logging
from django.shortcuts import get_object_or_404, redirect
logger = logging.getLogger(__name__)
def complete_task(request, task_id):
task = get_object_or_404(Task, pk=task_id)
if task.completed:
logger.warning(
"Task %s marked complete again by user %s",
task.id, request.user,
)
else:
task.completed = True
task.save()
return redirect('task-detail', pk=task.id)
Notice the message uses %s placeholders and passes task.id and
request.user as separate arguments, rather than building the string with an f-string
first. This matters more than it looks: if this logger's level is set above
WARNING somewhere, the string is never actually formatted at all — the placeholder
substitution only happens when the message is going to be recorded, which is a small but real
performance win at scale.
Automatically Emailing Admins on Real Failures
Django ships a handler purpose-built for production error reporting:
django.utils.log.AdminEmailHandler. Wired to the root logger at the ERROR
level and paired with an ADMINS setting listing who to notify, it emails those admins
automatically whenever an unhandled exception produces a 500 response — no explicit
logger.error() call required anywhere in your view code. It's a conceptual detail worth
knowing about rather than something to configure line-by-line here: the short version is that
Django can tell you, by email, the moment production throws an error you didn't anticipate.
logger.warning()/logger.error() calls in your own view code — those catch
things that are worth recording but don't necessarily crash the request.DEBUG-Level Logging Is a Production Risk, Not Just Noise
It's tempting to leave a logger's level at DEBUG everywhere so nothing ever gets
missed, but this has a real cost in production: DEBUG messages are usually far more
verbose than anything else, drowning out the messages that actually matter, and they frequently
include raw request data, query parameters, or object internals that shouldn't end up sitting in a
log file indefinitely. This is the exact same security mindset introduced in
CSRF & Security: minimize what's exposed by default, and
only turn on more detail deliberately, in a controlled environment, when you're actively
investigating something.