Imagine you ship a feature on Friday. By Monday morning, users are reporting something is broken. You were not watching when it happened, and the user's description is "it just stopped working." Without logs, you are solving a mystery with no clues.
Logs are your application's flight recorder. Every important event gets written down with a timestamp, so you can travel back in time and see exactly what happened, in what order, and why. The better your logs, the faster you debug, and the calmer your Friday deploys become.
Why logs matter in practice
Logs serve four distinct purposes, and each one will save you time at some point in your career.
| Use case | What logs tell you |
|---|---|
| Debugging | Which line of code failed, and what the inputs were |
| Monitoring | Whether the server is behaving normally right now |
| Auditing | Who did what and exactly when they did it |
| Security | Whether someone is probing for vulnerabilities |
The key insight is that logs are written during normal operation, before anything goes wrong. By the time you need them, your chance to add more has already passed. Write logs now so that future-you has the evidence needed to fix things fast.
Reading logs to debug AI-generated backend code
AI-generated backend code has a specific problem: it almost never includes logging. When Copilot or ChatGPT generates an Express route or a database query, the code handles the happy path but silently swallows errors or fails without leaving any trace of what happened.
Here is a typical AI-generated route with no observability:
// AI-generated - no logging at all
app.get('/api/users/:id', async (req, res) => {
try {
const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
res.json(user);
} catch (e) {
res.status(500).json({ error: 'Something went wrong' });
}
});When this breaks, you get nothing, just a 500 error and silence. Here is what it should look like:
// With proper logging
app.get('/api/users/:id', async (req, res) => {
logger.info('Fetching user', { userId: req.params.id, requestId: req.id });
try {
const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
if (!user) {
logger.warn('User not found', { userId: req.params.id });
return res.status(404).json({ error: 'User not found' });
}
logger.debug('User fetched successfully', { userId: user.id });
res.json(user);
} catch (error) {
logger.error('Failed to fetch user', {
userId: req.params.id,
error: error.message,
stack: error.stack
});
res.status(500).json({ error: 'Internal server error' });
}
});The difference is night and day when debugging production issues.
The five log levels
Not every event deserves the same amount of attention. Log levels let you filter by importance so you can focus on what matters without drowning in noise. Think of them like a hospital triage system, from routine check-in to full emergency.
| Level | When to use | Volume | Production default |
|---|---|---|---|
| DEBUG | Step-by-step tracing during development | Very high | Off |
| INFO | Normal, significant events | Medium | On |
| WARN | Unusual but handled situations | Low | On |
| ERROR | Operations that failed | Low | On |
| FATAL | System cannot continue | Very rare | On (with alerts) |
DEBUG: Highly detailed tracing
[DEBUG] Entering getUser() with id=42
[DEBUG] SQL query: SELECT * FROM users WHERE id = 42
[DEBUG] Query returned 1 row in 3msDEBUG logs capture step-by-step details that are only useful when you are hunting a specific bug. They create a lot of volume, so you typically disable them in production and enable them temporarily when you need to trace a problem.
INFO: Normal operation highlights
[INFO] Server started on port 3000
[INFO] User alice@example.com logged in
[INFO] Order #12345 processed successfullyINFO marks events that are expected and significant. Reading INFO logs should give you a clear narrative of what the application did: started, accepted requests, processed data, finished cleanly.
WARN: Something unusual happened
[WARN] API response took 3.5s (threshold: 2s)
[WARN] Retrying failed request (attempt 2 of 3)
[WARN] Database connection pool at 80% capacityWarnings are not failures, but they are early signals that something might go wrong soon. A slow response, a retry, a resource running low, these are yellow flags. Pay attention to warnings; they often precede errors by minutes or hours.
ERROR: Something failed
[ERROR] Database connection timeout after 30s
[ERROR] Payment processing failed: card declined
[ERROR] Failed to send email: SMTP server unreachableAn error means a specific operation failed. The application caught it and handled it gracefully, maybe it returned a 500 response to the user, but the underlying problem needs investigation.
FATAL: The application cannot continue
[FATAL] Out of memory - shutting down
[FATAL] Cannot connect to required database on startup
[FATAL] Critical configuration missing: JWT_SECRET not setFATAL means the process is about to exit or is completely broken. This is your "wake someone up at 3 AM" level.
Anatomy of a log entry
A well-structured log line packs several pieces of information into a single record:
2024-01-24 14:32:15 [ERROR] src/api/users.js:42 - User lookup failed
Error: User not found in database
at getUser (src/api/users.js:42)
at handleRequest (src/server.js:18)
at processRequest (src/middleware.js:7)| Part | Example | Why it matters |
|---|---|---|
| Timestamp | 2024-01-24 14:32:15 | Tells you exactly when the event occurred |
| Level | [ERROR] | Lets you filter by severity |
| Location | src/api/users.js:42 | Takes you straight to the relevant code |
| Message | User lookup failed | Human-readable summary of what happened |
| Stack trace | The indented lines | Shows the path the code took to reach the error |
The stack traceWhat is stack trace?A list of function calls recorded at the moment an error occurs, showing exactly which functions were active and in what order. deserves special attention. Read it from top to bottom: the top line is where the error was thrown, and each line below shows the calling function. The bug is almost always at the top.
Plain text vs structured JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. logs
You have two main choices for how to structure your log output.
Plain text
2024-01-24 14:32:15 INFO Server started on port 3000
2024-01-24 14:32:16 WARN Response time: 3.2s for GET /api/users
2024-01-24 14:32:17 ERROR Connection to database failedPlain text is easy to read in a terminalWhat is terminal?A text-based interface where you type commands to interact with your computer. Also called the command line or shell. with tail -f. It is great for local development. The downside is that querying it programmatically is awkward, you need regexWhat is regex?A compact pattern language for matching, searching, and replacing text, built into nearly every programming language and code editor. or custom parsing.
JSON format (structured loggingWhat is structured logging?Writing log entries as machine-readable JSON objects with consistent fields instead of plain text, making them searchable by log analysis tools.)
{
"timestamp": "2024-01-24T14:32:17Z",
"level": "ERROR",
"message": "Connection to database failed",
"file": "src/db.js",
"line": 42,
"userId": 12345,
"requestId": "abc-123-def",
"durationMs": 30000
}JSON logs are machine-friendly. Log aggregation tools can indexWhat is index?A data structure the database maintains alongside a table so it can find rows by specific columns quickly instead of scanning everything. any field, which means you can search for all errors from a specific user, or all requests that took over 2 seconds, with a single query. In production environments, JSON is the professional standard.
What to log and what to skip
Good logging is not about logging everything. It is about logging the right things.
| Log this | Skip this |
|---|---|
| User IDs and request IDs | Passwords and tokens |
| Error messages and stack traces | Credit card numbers |
| Request method and URL | Personal data you do not need |
| Response status and duration | Every iteration of a tight loop |
| Retry attempts and their outcomes | Redundant "entering function X" noise |
The privacy rule is absolute: if a piece of data could harm a user if exposed in a breach, it has no business in a log file.
Quick reference
| Level | When to use | Example message |
|---|---|---|
| DEBUG | Detailed internal tracing | "SQL query: SELECT * FROM users WHERE id=42" |
| INFO | Normal, significant events | "User logged in", "Server started" |
| WARN | Unusual but handled | "Slow response 3.2s", "Retry attempt 2/3" |
| ERROR | Operation failed | "Database timeout", "Payment declined" |
| FATAL | System cannot continue | "Out of memory", "Required config missing" |
// Logging in Node.js with Winston
const winston = require('winston');
// Configure logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Add console logging in development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Usage
logger.info('Server started', { port: 3000 });
logger.warn('API response slow', {
endpoint: '/api/users',
duration: 3500,
threshold: 2000
});
logger.error('Database query failed', {
query: 'SELECT * FROM users',
error: err.message,
stack: err.stack
});
// In Express middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now(), start,
userId: req.user?.id
});
});
next();
});
// Error handling
app.use((err, req, res, next) => {
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
userId: req.user?.id
});
res.status(500).json({ error: 'Internal server error' });
});