23.8. Production Logging
Table of Contents
Why Production Logging Is Different
In development you log mainly for yourself. In production you log for future you, your teammates, and often for 3 a.m. you that has to fix a broken system fast.
Production logging must:
- Be reliable logs must not silently disappear.
- Be structured so tools can search and group them.
- Be centralized you cannot SSH into every server manually.
- Be actionable they should help you detect and fix problems quickly.
- Be safe they must not leak secrets or personal data.
Think of production logs as your system’s black box recorder.
In production, unstructured print() logs, local-only log files, and logs containing secrets or personal data are unacceptable.
We will focus on patterns that apply to any backend stack, with concrete examples in Python-like pseudocode where helpful.
Logging Goals in Production
Observability and Debugging
In production you often cannot reproduce issues locally. Logs must:
- Tell you what happened
- Show where it happened (service, file, function, request)
- Give enough context (user, request id, inputs, environment)
- Provide a timeline of events
Example of helpful context:
{
"level": "ERROR",
"timestamp": "2026-08-28T09:15:22.123Z",
"service": "orders-api",
"env": "prod",
"request_id": "a0a1f774-6dae-4d88-9b1b-0b0f86377c2b",
"user_id": 42,
"path": "/api/orders",
"method": "POST",
"message": "Failed to create order",
"error": "psycopg2.IntegrityError: duplicate key value violates unique constraint"
}Compare that to:
Something went wrongOnly the first one is useful in production.
Auditing and Compliance
For some systems you must be able to answer questions like:
- Who changed this user’s role?
- Who issued this refund?
- When did this configuration change?
That means keeping audit logs for sensitive operations, for example:
{
"level": "INFO",
"type": "audit",
"timestamp": "2026-08-28T10:02:11.011Z",
"service": "admin-api",
"action": "user_role_changed",
"performed_by": 1,
"target_user": 42,
"old_role": "user",
"new_role": "admin",
"ip": "203.0.113.7"
}Audit logs:
- Use INFO level (not DEBUG) so they are always recorded.
- Are often kept longer than regular application logs.
- May have special storage or access rules.
Performance and Capacity Planning
Logs can help you see:
- Slow endpoints
- Frequent database timeouts
- Queue build-ups
- Memory or CPU pressure
For example, logging slow requests:
{
"level": "WARNING",
"event": "slow_request",
"path": "/api/orders",
"method": "GET",
"duration_ms": 1200,
"user_id": 42
}You can then build dashboards on top of this information.
What to Log in Production
Request and Response Information
At a minimum, log for each incoming request:
- Timestamp
- HTTP method and path
- Status code
- Duration
- Correlation or request ID
- Client IP (careful behind proxies, use trusted headers)
- User identifier if authenticated
- Service / application name and environment
Example structure:
{
"timestamp": "2026-08-28T09:30:01.500Z",
"level": "INFO",
"event": "request_completed",
"service": "shop-api",
"env": "prod",
"request_id": "3a4e...",
"method": "GET",
"path": "/api/products",
"status_code": 200,
"duration_ms": 45,
"client_ip": "198.51.100.23",
"user_id": 123
}Do not log full request or response bodies by default in production, especially not:
- Passwords
- Tokens
- Credit card numbers
- Personal data (names, emails, addresses) unless anonymized or strictly required.
Business Events
Log important business events such as:
- User registered
- Order created
- Payment failed
- Password changed
Example:
{
"level": "INFO",
"event": "order_created",
"order_id": 987,
"user_id": 123,
"total": 149.99,
"currency": "USD",
"timestamp": "2026-08-28T11:00:21.044Z"
}These logs help with:
- Debugging user issues
- Generating reports
- Detecting suspicious behavior
Errors and Exceptions
When an error happens:
- Log a clear message
- Include the exception type and message
- Include a stack trace
- Add request context if possible
Example (Python-style):
logger.exception(
"Failed to process payment",
extra={
"event": "payment_error",
"user_id": user_id,
"order_id": order_id,
"payment_provider": "stripe",
}
)This should produce a log with:
level: ERRORevent: "payment_error"message: "Failed to process payment"- stack trace
Never log raw secrets such as:
- Passwords
- API keys
- Access tokens
- Private keys
- Full credit card numbers
Mask or omit them before logging.
Log Levels and How to Use Them in Production
Most logging systems have at least these levels:
| Level | Purpose in production | Example |
|---|---|---|
| DEBUG | Detailed internal information for troubleshooting | Variable values, low-level function calls |
| INFO | Normal operation events | Startup complete, request completed, user logged in |
| WARNING | Something unexpected, but system still works | Slow request, near limit, retry succeeded |
| ERROR | A request or action failed | Unhandled exception in endpoint, DB error, third-party fail |
| CRITICAL / FATAL | Application or subsystem is unusable | Service cannot start, configuration missing |
Production defaults:
- DEBUG logs are usually disabled or heavily limited in production.
- INFO logs describe expected behavior and business events.
- WARNING logs are early signals that something might be wrong.
- ERROR logs should be investigated. Too many means a problem.
- CRITICAL logs often trigger alerts.
Example usage in Python style:
logger.debug("Cache miss", extra={"key": cache_key})
logger.info("User logged in", extra={"user_id": user.id})
logger.warning("Slow query", extra={"duration_ms": 900, "query": "SELECT ..."})
logger.error("Failed to send email", exc_info=True)
logger.critical("Database unavailable, shutting down")Define clear rules for when to use each level and stick to them. Mixed or random use of log levels makes your logs noisy and almost useless.
Structured Logging in Production
Why Structured Logging
Plain text logs like:
[INFO] 2026-08-28 10:01:03 User 42 logged in from 203.0.113.7are hard for machines to parse reliably.
Structured logs such as JSON:
{
"timestamp": "2026-08-28T10:01:03.501Z",
"level": "INFO",
"event": "user_login",
"user_id": 42,
"ip": "203.0.113.7",
"service": "auth-api",
"env": "prod"
}are much easier to:
- Search (
event:user_login AND user_id:42) - Aggregate (count logins per hour, per IP)
- Visualize (dashboards, charts)
- Correlate with metrics and traces
Structured logging is the standard for modern production systems.
Common Fields to Include
You can define a common schema for all logs:
| Field | Description |
|---|---|
timestamp | ISO 8601 UTC time |
level | DEBUG / INFO / WARNING / ERROR / CRITICAL |
service | Service or application name |
env | Environment, for example prod, staging |
host | Hostname / container id |
request_id | Correlation id for the request |
user_id | Authenticated user, if any |
event | Short event name |
message | Human readable description |
Then each log can add its own custom fields.
Adding Structure in Code
Python-like example with a JSON logger:
import logging
import json
import sys
class JsonFormatter(logging.Formatter):
def format(self, record):
log = {
"timestamp": self.formatTime(record, datefmt="%Y-%m-%dT%H:%M:%S.%fZ"),
"level": record.levelname,
"message": record.getMessage(),
"service": "orders-api",
"env": "prod",
}
if hasattr(record, "request_id"):
log["request_id"] = record.request_id
if hasattr(record, "user_id"):
log["user_id"] = record.user_id
return json.dumps(log)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logger = logging.getLogger("orders-api")
logger.setLevel(logging.INFO)
logger.addHandler(handler)Then:
logger.info("Order created", extra={"user_id": 123, "request_id": "abc"})Outputs JSON with these fields.
Correlation IDs and Traceability
When a single user action touches multiple services or components, you want to connect all related logs. This is where correlation IDs (or trace IDs) help.
How It Works
- When a request enters your system, you:
- Look for an existing correlation ID header, for example
X-Request-ID. - If none, generate a new unique ID.
- You attach this ID to:
- The request context
- All logs produced while handling this request
- Any outgoing requests to other services (as a header).
- Downstream services also log this ID.
Now you can search the log system by that ID and follow the whole flow.
Example log fields:
{
"timestamp": "2026-08-28T10:20:00.111Z",
"level": "INFO",
"service": "auth-api",
"event": "request_completed",
"request_id": "a3f4...",
"path": "/login",
"status_code": 200
}{
"timestamp": "2026-08-28T10:20:00.210Z",
"level": "INFO",
"service": "orders-api",
"event": "request_completed",
"request_id": "a3f4...",
"path": "/orders",
"status_code": 201
}
Search for request_id:a3f4... and you see both logs.
Implementing in a Web Backend
In many frameworks, you do this with middleware:
- Generate or read a
request_idon each request. - Store it in a request-scoped context.
- Configure your logger to read from that context on each log call.
Even simple frameworks allow you to pass it manually in extra fields.
Log Storage and Centralization
In production you often have:
- Multiple instances of the same service
- Multiple services
- Containers that appear and disappear
You cannot SSH into each instance and read /var/log/app.log.
Centralized Logging Pattern
Typical pattern:
- Applications print logs to stdout/stderr as structured lines, often JSON.
- The container runtime or host collects logs.
- A log agent (Fluent Bit, Filebeat, Vector, etc.) ships logs to a central system.
- A log backend stores and indexes logs, for example:
- Elasticsearch / OpenSearch
- Loki
- Cloud logging (AWS CloudWatch, GCP Logging, etc.)
- Hosted log platforms
Then you use a UI or API to:
- Search logs
- Filter by service, level, request_id, user_id
- Build dashboards
In containerized production systems, log to stdout/stderr, not to random local files. Let the platform handle collection and rotation.
Log Rotation and Retention
Logs grow forever if you let them. In production you must define:
- Rotation when to split files or indices into smaller units.
- Retention how long to keep logs before deleting or archiving.
Choices often depend on:
- Legal requirements
- Storage costs
- How far back you need to investigate issues
Example policy:
- Keep full application logs for 14 days.
- Keep error logs for 90 days.
- Keep audit logs for 1 year.
Log Volume, Sampling, and Cost
In production, logs:
- Consume disk space
- Need to be indexed
- Cost money in cloud systems
You should avoid both extremes:
- Too few logs you cannot debug issues.
- Too many logs you pay a lot and cannot find the important ones.
Reducing Log Noise
Some strategies:
- Lower the log level for very frequent events.
- Avoid logging every internal step at INFO level.
- Remove verbose DEBUG logs from hot paths.
- Avoid logging huge payloads or responses.
Example bad pattern:
logger.info("User details: %s", user.to_dict())On a busy site this can generate millions of logs per hour.
Better:
logger.debug("Loaded user", extra={"user_id": user.id})Then DEBUG can be disabled in production or enabled only for specific components.
Sampling
For extremely high volume events, you can sample:
- Log only some percentage of identical events.
- Or log the first N occurrences per time period and then summarize.
Example: Log at most 1 out of 100 successful requests, but log all errors.
Pseudocode:
import random
if random.random() < 0.01: # 1% sampling
logger.info("Request completed", extra=fields)Errors and warnings should usually not be sampled, or sampled very lightly.
Security and Privacy in Production Logs
Logs can easily become a security and privacy risk.
Do Not Log Sensitive Data
Sensitive examples:
- Passwords
- Reset tokens
- Access and refresh tokens
- API keys
- Secrets
- Credit card numbers, CVV
- National ID numbers
- Personal addresses and phone numbers, if avoidable
Typical mistakes:
- Logging full HTTP headers, which may include
Authorization. - Logging full request bodies with password fields.
- Logging exceptions that include secrets in their messages.
Masking example:
safe_body = {**body}
if "password" in safe_body:
safe_body["password"] = "***redacted***"
logger.warning("Invalid registration data", extra={"body": safe_body})Everything you log might be seen by many people or systems. Never treat logs as private or safe for secrets.
Access Control and Encryption
- Log storage should require authentication and authorization.
- Use TLS when sending logs over the network.
- Limit who can:
- Access raw logs.
- Query logs for specific users.
- Export logs.
Data Retention and Anonymization
If your system handles personal data, you may need:
- To delete or anonymize logs after a certain period.
- To remove or pseudonymize user identifiers.
- To avoid logging more personal data than necessary.
For example, log user_id instead of email, and keep the mapping in your main database, which is already protected and has its own retention rules.
Designing a Production Logging Strategy
When preparing for production deployment, define and implement a clear logging strategy.
Define What You Log
For each service, write down:
- Minimal request log fields.
- Important business events to log.
- Which errors to log and at which level.
- What must never be logged.
Example checklist for an API service:
- [ ] Log request start and end with
request_id. - [ ] Log all unhandled exceptions with stack traces.
- [ ] Log user login/logout, registration, and password changes.
- [ ] Log payment attempts and failures.
- [ ] Do not log passwords, tokens, or credit card data.
Standardize Log Format
Across services, agree on:
- JSON lines as the output format.
- Same field names (
service,env,request_id,user_id). - Common log levels.
This makes cross-service debugging easier.
Integrate with Monitoring and Alerting
Use logs for:
- Alerting for example:
- Too many 5xx responses.
- Many authentication failures from the same IP.
- CRITICAL logs from any service.
- Dashboards:
- Requests per second by endpoint.
- Error rate per service.
- Distribution of response times.
This usually involves:
- Search queries on structured fields.
- Aggregations and visualizations.
Example: Minimal Production Logging Setup for a Simple API
Imagine a FastAPI or similar web service running in Docker.
- Application logging:
- Use the standard logging library.
- Configure a JSON formatter.
- Include
service,env, andrequest_idfields in all logs. - Log to stdout only.
- Middleware:
- Generate or read
X-Request-ID. - Log request completion with path, method, status code, duration, user_id.
- Attach
request_idto a per-request context used by the logger. - Log levels:
- Set global level to
INFO. - Use
WARNINGfor slow requests or retries. - Use
ERRORandCRITICALfor real failures. - Centralization:
- Use Docker logging driver or a small agent to send container logs to a central log system (for example, CloudWatch, Loki, or Elastic).
- Set a retention policy (for example, 30 days for general logs, 90 days for errors).
- Security:
- Filter out or mask sensitive fields from request/response logs.
- Restrict access to the logging system to the operations and backend team.
- Documentation:
- Write a short document that explains:
- Which events are logged.
- Where to find logs for each environment.
- How to search by
request_idoruser_id. - What each log level means in your system.
Summary
Production logging is not just “print some messages.” It is:
- Structured: JSON or similar, with consistent fields.
- Centralized: in a log system, not scattered across servers.
- Context-rich: correlation IDs, user ids, request info.
- Level-driven: INFO for normal events, WARNING for odd behavior, ERROR / CRITICAL for failures.
- Safe: no secrets or unnecessary personal data.
- Managed: with rotation, retention, and access control.
If you design production logging carefully before deployment, you will save hours or days every time something goes wrong in production.
Views: 6
KAHIBARO