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.

LevelUse 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.

Where it fits: This complements, but doesn't replace, deliberate 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.

Note: All of the logging configuration and view code above is correct and runnable Django code — but this website's own project doesn't run a live Django server, so nothing here actually produces log output on this page.
Next up: Logging closes out TaskFlow's core architecture topics. From here, the track shifts to a genuinely new capability — exposing TaskFlow's data as a REST API, starting with DRF: Serializers.