What to Log and What Not to Log: A Practical Guide
Most developers either log too much or too little. This practical guide covers what actually belongs in your application logs, how to structure log messages for debugging, and the common mistakes that make logs useless when you need them most.
There’s a moment that every developer experiences at least once. Something breaks in production, a customer reports a problem, and you open your logs expecting to find the answer. Instead, you find one of two things: either a wall of noise so dense that finding the relevant entry feels like searching for a specific grain of sand on a beach, or a vast emptiness where the one log message you needed should have been but wasn’t. Both situations are equally frustrating, and both are the result of never sitting down and thinking deliberately about what’s worth logging.
This isn’t a topic that gets much attention. Tutorials teach you how to set up logging libraries, and documentation explains log levels, but nobody really talks about the craft of deciding what information belongs in your logs and what doesn’t. It’s treated as obvious, but it isn’t. Getting logging right is a skill that makes the difference between five-second diagnoses and five-hour debugging sessions, and we think it deserves a proper guide.
So here’s our opinionated, practical take on what to log, what not to log, and how to structure your log messages so they’re actually useful when things go wrong.
The Purpose of Logging (It’s Not What You Think)
Before we get into the specifics, let’s align on why we log in the first place. Logging is not about creating a record of everything your application does. That’s a common misconception, and it leads to over-logging, which leads to noise, which leads to people ignoring logs entirely, which is worse than not having them at all.
The purpose of logging is to answer questions you haven’t thought of yet. When something unexpected happens — a bug, a performance issue, a confused user, an outage — your logs are the first tool you reach for. Good logs let you reconstruct what happened, understand why it happened, and figure out what to do about it. Everything you log should serve that goal. If a log message doesn’t help you answer a future question, it probably doesn’t need to exist.
With that framing in mind, let’s talk about what’s actually worth logging.
What to Log: The Essential Categories
Business Events
These are the most valuable logs in your entire application, and they’re the ones most developers forget to add. A business event is anything that represents a meaningful action in your product: a user signed up, an order was placed, a payment was processed, a subscription was upgraded, an invitation was sent, a report was generated.
loggy.info("User signed up", {
userId: user.id,
plan: "free",
source: "google-oauth",
referrer: request.headers.get("referer"),
});
loggy.info("Order placed", {
orderId: order.id,
userId: user.id,
total: order.total,
itemCount: order.items.length,
currency: "usd",
});
loggy.info("Subscription upgraded", {
userId: user.id,
fromPlan: "free",
toPlan: "pro",
billingCycle: "monthly",
});
Why are these so important? Because when your CEO asks “how many people signed up from that Product Hunt launch?” or when a customer emails saying “I placed an order but never got a confirmation,” these logs let you answer in seconds. They’re also invaluable for understanding user behavior patterns that your analytics tools might not capture, especially around edge cases and error scenarios.
Always include relevant IDs in business event logs. User IDs, order IDs, session IDs, plan names — anything that lets you correlate the event with a specific entity in your system. The metadata is what makes logs searchable, and searchable logs are the difference between “let me check” and “I have no idea.”
Integration Boundaries
Every time your application communicates with an external service, you’re crossing a trust boundary. The database might be slow. The payment provider might return an unexpected error. The email API might be rate-limiting you. The third-party data feed might have changed its format. These integration points are where the majority of production issues originate, and they deserve thorough logging.
// Before the call
loggy.info("Sending payment request to Stripe", {
userId: user.id,
amount: charge.amount,
currency: charge.currency,
});
// After success
loggy.info("Payment processed successfully", {
userId: user.id,
chargeId: result.id,
amount: charge.amount,
});
// After failure
loggy.error("Payment processing failed", {
userId: user.id,
amount: charge.amount,
stripeError: error.code,
stripeMessage: error.message,
declineCode: error.decline_code,
});
The pattern here is simple: log before you make the call (so you know the attempt happened even if the call hangs), and log after with the result (success or failure). Include enough context that you could understand the situation without looking at any other system. When Stripe returns a card_declined error at 2 AM, you want your log to tell you everything: which user, what amount, what error code, what decline reason. No additional investigation needed.
Authentication and Authorization Events
Security-relevant events should always be logged. Someone logging in, logging out, failing to log in, accessing something they shouldn’t be able to access, changing their password, enabling two-factor authentication — all of these matter for security auditing and for debugging access issues.
loggy.info("User login successful", {
userId: user.id,
method: "email-password",
ip: request.ip,
});
loggy.warn("Failed login attempt", {
email: maskEmail(email), // "k***[email protected]"
reason: "invalid-password",
attemptCount: attempts,
ip: request.ip,
});
loggy.warn("Unauthorized access attempt", {
userId: user.id,
resource: "/admin/users",
requiredRole: "admin",
actualRole: user.role,
});
Notice the use of warn level for failed attempts rather than error. A single failed login isn’t an error — people mistype passwords all the time. But it’s worth logging as a warning because a pattern of failed attempts could indicate a security issue, and alerting on warning spikes can catch brute-force attempts automatically.
State Changes
When important entities in your system change state, log the transition. This is especially important for anything with a lifecycle — orders, subscriptions, deployments, background jobs — where understanding the sequence of state changes is essential for debugging.
loggy.info("Order status changed", {
orderId: order.id,
fromStatus: "pending",
toStatus: "processing",
trigger: "payment-confirmed",
});
By logging both the fromStatus and toStatus, you create a timeline that you can reconstruct later. If an order gets stuck in “processing” forever, you can search for its order ID and see every state change (or lack thereof) in chronological order.
Errors (With Context)
This one seems obvious, but the key word is “with context.” The most common logging mistake we see is error messages that don’t include enough information to actually debug the problem.
// Bad - useless without context
loggy.error("Something went wrong");
loggy.error(error.message);
// Good - everything you need to debug
loggy.error("Failed to process webhook", {
webhookId: webhook.id,
webhookType: webhook.type,
error: error.message,
stack: error.stack,
payload: sanitize(webhook.payload),
});
An error log should answer three questions: what was the application trying to do, what went wrong, and what was the context? If your error log doesn’t answer all three, the next person debugging that issue (probably future you, at midnight) will have to go spelunking through code and other systems to figure out what happened.
What Not to Log
Knowing what to leave out is just as important as knowing what to include. Over-logging creates noise that drowns out the signal, wastes storage, and in some cases can even create security vulnerabilities.
Sensitive Data
This is the big one. Never log full credit card numbers, passwords (even hashed ones), API keys or secrets, social security numbers, full dates of birth, or any personal data that falls under regulations like GDPR, CCPA, or HIPAA. If you need to reference a credit card in a log, use the last four digits. If you need to reference a user, use their user ID, not their email address or name.
// Never do this
loggy.info("Processing payment", {
cardNumber: "4242424242424242", // NEVER
cvv: "123", // NEVER
password: user.password, // NEVER
apiKey: process.env.STRIPE_KEY, // NEVER
});
// Do this instead
loggy.info("Processing payment", {
cardLast4: "4242",
userId: user.id,
amount: charge.amount,
});
This isn’t just good practice — it’s often a legal requirement. And even when it’s not legally required, a data breach that exposes logs containing credit card numbers or passwords is catastrophically worse than one that exposes user IDs and order amounts.
High-Volume Repetitive Events
If you have a loop that processes 10,000 items, do not log inside the loop. This is one of the fastest ways to generate noise and, if you’re using a logging tool with ingestion-based pricing, run up a huge bill.
// Bad - 10,000 log entries
for (const item of items) {
loggy.debug("Processing item", { itemId: item.id });
await processItem(item);
}
// Good - 2 log entries with useful aggregates
loggy.info("Starting batch processing", { itemCount: items.length });
const results = await processBatch(items);
loggy.info("Batch processing complete", {
itemCount: items.length,
successes: results.successes,
failures: results.failures,
durationMs: results.duration,
});
Log before and after the batch with aggregate information. If individual item failures matter, collect them and log them as a single summary rather than 10,000 individual entries.
Raw Request/Response Bodies
It’s tempting to log the entire request body for every API call, but this is usually a bad idea. Request bodies often contain sensitive data (form submissions with passwords, file uploads), they can be enormous, and they create a lot of noise. Log the metadata about the request — the method, path, relevant IDs, status code — and reserve full body logging for specific debugging scenarios that you enable temporarily.
Debugging Logs That Should Be Temporary
We’ve all done it: added a console.log("HERE") or loggy.debug("value of x", { x }) while debugging a tricky issue, and then forgotten to remove it. These logs accumulate over time and degrade the signal-to-noise ratio. If you’re adding a debug log to figure out a specific issue, comment it with a note about when it should be removed, or better yet, use a debug flag that’s off by default in production.
How to Structure Your Log Messages
The difference between a useful log and a useless one often comes down to structure. Here’s how to write log messages that you’ll actually appreciate at 2 AM.
Use a Consistent Message Format
Your log message should be a short, human-readable description of what happened. Keep it in the present tense or past tense, be specific, and don’t include data that belongs in metadata.
// Good messages
loggy.info("User signed up");
loggy.info("Payment processed successfully");
loggy.error("Failed to send email notification");
loggy.warn("Rate limit approaching threshold");
// Bad messages
loggy.info("user_signup_event_v2"); // not human-readable
loggy.info(`User ${userId} signed up for ${plan}`); // data in message
loggy.info("Done"); // done what?
loggy.info("Error!"); // what error?
Put variable data in the metadata object, not in the message string. This makes your logs easier to search (you can filter by message text) and easier to aggregate (all “User signed up” events group together regardless of which user).
Use Log Levels Correctly
Log levels aren’t just decoration — they’re a filtering mechanism that lets you control how much detail you see.
Debug is for detailed technical information that’s useful during development but too noisy for production. Variable values, function entry/exit points, detailed algorithm steps.
Info is for business events and normal operational information. User actions, successful operations, state changes. This is your baseline in production — the story of what your application is doing.
Warn is for things that aren’t failures but aren’t normal either. A retry that succeeded, a deprecated feature being used, a rate limit being approached, a configuration that seems wrong but doesn’t prevent operation.
Error is for genuine failures that need attention. An operation that failed and couldn’t be recovered, a service that’s unreachable, data that’s invalid in a way that shouldn’t be possible. If your logs are integrated with an alerting system, error-level logs are typically what trigger notifications.
Include Correlation IDs
When you can, include IDs that let you connect related log entries. A request ID that threads through all logs generated by a single HTTP request. A user ID that lets you see everything a specific user did. A trace ID if you’re using distributed tracing.
loggy.info("Order creation started", {
requestId: req.id,
userId: user.id,
traceId: span?.traceId,
});
These correlation IDs are what turn a pile of individual log entries into a coherent story. When a user reports a bug, you search by their user ID and see every action they took, in order, with full context. That’s powerful.
A Practical Logging Checklist
When adding logging to a new feature or reviewing existing code, run through this mental checklist:
For every API endpoint, am I logging the request receipt and the response status? For every external service call, am I logging before and after? For every business event, am I logging it with relevant IDs? For every error, am I including enough context to debug without looking at code? Am I accidentally logging any sensitive data? Am I logging in any hot loops?
If you can answer those questions, your logging coverage is probably solid. You don’t need to log everything — you need to log the right things.
Getting Started
If you’re reading this and realizing your logging could use improvement, the good news is that it doesn’t take much work. Start by auditing your most critical user flows — signup, payment, and whatever your core feature is — and make sure each step is logged with structured metadata. Add error context to your catch blocks. Set up alerts for error spikes so you know when things go wrong without watching a dashboard.
If you need a logging tool that makes all of this easy, Loggy lets you get set up in minutes with SDKs for Node.js, Python, and Go. The free tier is generous enough for side projects, and the flat-rate pricing means you never have to worry about logging too much. You can sign up here — no credit card required.
But regardless of which tool you use, the principles in this guide will make your logs dramatically more useful. Start with business events and error context, avoid sensitive data and hot-loop logging, use structured metadata, and make sure your log messages answer the question: “What will future me need to know when something goes wrong?”