20.1. Application Logging
Table of Contents
Why Application Logging Matters
Application logging is how your backend “remembers” what happened. Every important event, error, and decision can be written as a log entry.
Without logs:
- Debugging production problems is almost impossible.
- You cannot understand real user behavior.
- You cannot prove what happened when something goes wrong.
With good logs you can:
- Reconstruct a user’s path through your system.
- Find the cause of errors and performance issues.
- Detect security incidents and abuse.
- Measure important business events, like “order created.”
Key idea: Logging is not optional in backend systems. A production backend without logs is effectively blind.
In this chapter you will learn what to log, how to structure logs, and how to use Python’s logging effectively in backend applications.
Logs vs Other Observability Data
Modern systems use three main types of data:
| Type | Example questions | Examples |
|---|---|---|
| Logs | “What exactly happened at 12:01:05?” | Error stack traces, info messages |
| Metrics | “Is error rate up today?” | Request count, latency histograms |
| Traces | “How did this single request flow through services?” | Distributed tracing spans |
Logs are the most detailed. Metrics and traces are covered in other chapters; here we focus on logs as text/structured records of events.
Basic Logging Concepts
Log entries
A log entry is one record of something that happened at a specific time. For example:
2026-08-28T12:01:05Z INFO user_id=42 path=/login "Login successful"Most log entries contain:
- Timestamp: When it happened.
- Level: Severity, such as INFO or ERROR.
- Message: Human readable description.
- Context: Extra fields like user_id, request_id.
Logging levels (overview)
Details of log levels are in the next chapter, but you must understand that every log has a severity. Typical levels:
| Level | Use for |
|---|---|
| DEBUG | Detailed dev-only information |
| INFO | Normal events, like “user registered” |
| WARNING | Something odd that still works |
| ERROR | Something failed for this request or operation |
| CRITICAL | Application or key part is unusable |
The level lets you filter and control how noisy logs are.
What to Log in a Backend
Request related events
You should log the key lifecycle events of an HTTP request.
Typical examples:
- At the start of a request:
- Method, path, request_id, user_id (if authenticated), client IP.
- At the end of a request:
- Status code, response time, request_id.
- On important domain actions:
- “User created,” “Order paid,” “Email sent.”
Example (concept, not language specific):
2026-08-28T12:01:05Z INFO request_id=abc123 method=POST path=/api/orders user_id=42 "Create order"
2026-08-28T12:01:05Z INFO request_id=abc123 status=201 duration_ms=134 "Request completed"
Notice how request_id appears in both lines. That lets you search all logs related to a single request.
Business events
Some logs are about business events, not just technical ones:
- “Cart abandoned after 2 hours.”
- “Password reset requested.”
- “Inventory reserved for order 1043.”
These help with debugging and also with analytics. Later you might turn them into metrics or events in a data pipeline, but logging them is the first step.
Errors and exceptions
Every error that affects a user should produce a log. For uncaught exceptions you should log:
- Error message,
- Stack trace,
- Context like user_id, path, request_id.
In Python, this usually means using logger.exception in exception handlers so that the stack trace is recorded.
Background jobs and tasks
Background workers are often hidden from direct user interaction, so logs are your only view of what they are doing.
Log for each job:
- When the job starts and ends.
- Job parameters (sanitized).
- Retry attempts.
- Failure reasons.
Example:
2026-08-28T12:10:00Z INFO job=email_send job_id=xyz user_id=42 "Sending verification email"
2026-08-28T12:10:01Z ERROR job=email_send job_id=xyz user_id=42 "SMTP connection failed"
2026-08-28T12:10:10Z INFO job=email_send job_id=xyz user_id=42 attempt=2 "Sending verification email"What Not to Log
Logging too much is harmful. It can:
- Fill disks and log dashboards.
- Leak sensitive information.
- Make real problems harder to find.
Sensitive data
Never log secrets, even at DEBUG:
- Passwords.
- Password reset tokens.
- Full credit card numbers.
- Private keys or secrets.
- Session or JWT tokens.
- Internal access tokens.
Obfuscate or remove confidential fields.
Rule: Treat logs as if any employee and possibly attackers could read them. Do not log secrets or personal data unless absolutely necessary and allowed by law.
Example, bad:
INFO "User login" email=john@example.com password=supersecretBetter:
INFO "User login" email_hash=fd5a... ip=203.0.113.10You can hash or partially mask values (for example show last 4 digits) when you need to correlate but not reveal.
Large payloads
Avoid logging:
- Whole file contents.
- Full request or response bodies for large JSON.
- Huge arrays or binary data.
Instead log:
- Sizes (for example
body_size=12345). - Counts (for example
items_count=37). - Sample IDs (for example first few item IDs).
Bad:
DEBUG "Received payload" payload={very_large_json_here}Better:
DEBUG "Received payload" body_size=8453 items_count=37Text Logs vs Structured Logs
Plain text logs
Plain text logs are simple strings. They are easy to read but harder for machines to parse.
Example:
2026-08-28 12:01:05 [INFO] User 42 created order 1001 in 134msProblems:
- “User 42” is not a field. If you want to filter by
user_id=42, you must run complex text searches. - Parsing is fragile if message formats change.
Structured logs (JSON)
Structured logs store fields in a machine readable format, usually JSON.
Example:
{
"timestamp": "2026-08-28T12:01:05Z",
"level": "INFO",
"message": "Order created",
"user_id": 42,
"order_id": 1001,
"duration_ms": 134,
"request_id": "abc123"
}Benefits:
- You can filter in your log system:
user_id = 42 AND level = "ERROR". - It is easy to aggregate by
order_id,service, etc. - Format is consistent and less fragile.
Most modern backend teams prefer structured logging with JSON, especially once logs are sent to tools like Elasticsearch, Loki, or cloud logging services.
For local development you can still pretty print JSON or use a human friendly formatter.
Python Logging Basics
You will use Python for backend development in this course, so you need a basic mental model of the logging system.
The logger, handler, formatter pipeline
Python’s logging has three main parts:
| Part | What it does | Example |
|---|---|---|
| Logger | Where your code writes messages | logger.info("Order created") |
| Handler | Where logs are sent | Console, file, HTTP, syslog |
| Formatter | How logs are turned into text or JSON | '%(asctime)s %(levelname)s %(message)s' |
When you call logger.info(...):
- The logger receives your log record.
- It passes it to one or more handlers.
- Each handler uses a formatter to produce the final string or JSON.
- The handler writes to a destination like stdout or a file.
A minimal setup
Very simple example that logs to the console:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
logger = logging.getLogger(__name__)
logger.info("Application started")
logger.warning("Low disk space")Output:
2026-08-28 12:00:00,123 INFO __main__ Application started
2026-08-28 12:00:01,456 WARNING __main__ Low disk spaceIn real backends you will use more advanced configuration, but the pattern is always:
- Configure logging once at startup.
- Get a
loggerin each module. - Use
logger.debug/info/warning/error/exception/criticalto write logs.
Adding Context to Logs
Logs become very powerful when you include context fields.
Common context fields in a backend:
| Field | Description |
|---|---|
request_id | Unique ID for each HTTP request |
user_id | Authenticated user ID or anonymous |
path | Request path, for example /api/orders |
method | HTTP method, such as GET or POST |
status | HTTP status code |
duration_ms | Time spent handling the request |
service | Service name in a microservice architecture |
Example: logging with context in Python
Imagine you have request_id and user_id available. One simple pattern:
logger.info(
"Order created",
extra={"user_id": user_id, "order_id": order_id, "request_id": request_id},
)A JSON formatter could turn this into:
{
"timestamp": "2026-08-28T12:01:05Z",
"level": "INFO",
"message": "Order created",
"user_id": 42,
"order_id": 1001,
"request_id": "abc123"
}
Your FastAPI middleware can automatically attach request_id, path, and method to all logs created during a request, so you do not have to pass them manually every time. That pattern is covered in later chapters, but you should already understand why it is important.
Logging in a Web Application
Where to log in request handling
In a typical FastAPI application you might log in:
- Startup and shutdown:
- “Application started,” “Connected to database.”
- Middleware:
- Incoming request details.
- Outgoing response status and time.
- Endpoints (controllers):
- Key domain actions, for example “Created task,” “User updated profile.”
- Services:
- Non trivial operations, for example “Charging card,” “Inventory reserved.”
- Error handlers:
- Unexpected exceptions with stack traces.
Example flow:
[Middleware] Received request GET /api/tasks user_id=42 request_id=abc123
[Service] Listing tasks user_id=42 request_id=abc123
[Middleware] Completed request GET /api/tasks 200 duration_ms=14 request_id=abc123
Notice how the same request_id appears in each log. That lets you:
- Reconstruct the full path of that single call across layers.
- Quickly spot where it slowed down or failed.
Logging Strategy and Best Practices
Choose consistent formats
Decide early:
- Will logs be JSON or plain text?
- What fields must be included in every entry?
- How will timestamps look, for example ISO 8601.
Example standard fields:
| Field | Required? | Example value |
|---|---|---|
timestamp | Yes | 2026-08-28T12:01:05Z |
level | Yes | INFO |
message | Yes | Order created |
service | Yes | orders-api |
request_id | Recommended | abc123 |
user_id | Recommended | 42 or anonymous |
Having a standard makes it easy to search across all your services.
Log at the right level
Although detailed log level usage is covered in the next chapter, some core guidelines:
- Use INFO for business events that you might care about in production.
- Use DEBUG for low level technical details that you only enable when debugging.
- Use WARNING when something looks suspicious, but the request still succeeds.
- Use ERROR when a user facing operation failed.
- Use CRITICAL when the system is unusable.
Rule: An application with everything at INFO or DEBUG is as useless as an application with no logs. Too much noise hides real problems.
Avoid duplication
Do not log the same error in 5 places. Typical pattern:
- Log the error at the boundary of a layer.
- Let inner functions raise exceptions but not log them again.
Example:
def service_get_user(user_id: int):
user = repo_get_user(user_id) # If this raises, we do not log here
if not user:
raise UserNotFoundError(user_id)
return user
@app.get("/users/{user_id}")
def get_user_endpoint(user_id: int):
try:
user = service_get_user(user_id)
return user
except UserNotFoundError:
logger.info("User not found", extra={"user_id": user_id})
raise HTTPException(status_code=404, detail="User not found")
except Exception:
logger.exception("Unexpected error in get_user_endpoint", extra={"user_id": user_id})
raise HTTPException(status_code=500, detail="Internal server error")Only the endpoint logs, not every inner function. This keeps logs clean and readable.
Think about retention and volume
Every log line costs:
- Disk or storage,
- Network bandwidth,
- Money, if you use a hosted logging service.
You should:
- Avoid logging repetitive data at INFO.
- Use DEBUG level for very detailed information and keep it disabled in production most of the time.
- Configure retention policies, for example:
- Keep INFO and above for 30 days.
- Keep DEBUG and TRACE only for a few hours if enabled.
Example: Simple Logging Plan for a New API
Imagine you are building a simple Task Management API.
Here is a minimal logging plan:
- Startup logs:
- “Starting Task API, version X.Y.”
- “Connected to PostgreSQL” or error.
- Request logs (middleware):
- On start: method, path, request_id, user_id.
- On complete: status, duration_ms, request_id.
- Domain events (INFO):
task_created,task_completed,task_deletedwithtask_id,user_id.- Warnings:
- When a user hits a soft limit, for example “Too many tasks, consider upgrading.”
- Errors:
- Unhandled exceptions with stack traces, status 500.
- External service failures, for example “Email service timeout.”
Sample log lines:
2026-08-28T12:00:00Z INFO service=tasks-api "Starting Task API" version=1.0.0
2026-08-28T12:00:00Z INFO service=tasks-api "Database connection established"
2026-08-28T12:01:05Z INFO service=tasks-api request_id=abc123 method=POST path=/api/tasks user_id=42 "Request started"
2026-08-28T12:01:05Z INFO service=tasks-api request_id=abc123 user_id=42 task_id=1001 "Task created"
2026-08-28T12:01:05Z INFO service=tasks-api request_id=abc123 status=201 duration_ms=12 "Request completed"
2026-08-28T12:02:10Z ERROR service=tasks-api request_id=def456 user_id=42 "Unexpected error in /api/tasks" error="TimeoutError"From this you can:
- Track performance problems using
duration_ms. - See which actions users perform.
- Investigate specific broken requests using
request_id.
Summary
- Application logging records what happens inside your backend over time.
- Logs are essential for debugging, monitoring, security, and understanding user behavior.
- Each log entry should at least include a timestamp, level, message, and useful context.
- Prefer structured logging such as JSON for production systems.
- Add request and business context like
request_id,user_id, andorder_id. - Avoid logging sensitive information and huge payloads.
- Use levels wisely so that important information stands out.
Later chapters will build on this and show how to define log levels, structure logs across services, and integrate them with monitoring tools.
Views: 10
KAHIBARO