Last updated: October 5, 2026
12 Logging Best Practices You Should Know
Most logging statements are added reactively. You add one when something breaks, another when you need more visibility into a code path, and over time the codebase accumulates logs with different levels of detail, naming, and quality.
That inconsistency becomes expensive in production because you end up storing records that nobody uses, missing the context you need during incidents, and manually piecing together events across services because the logs weren't designed to work together.
Logs aren't the whole observability story, but they remain one of the most useful sources of event-level detail during an investigation. This guide covers the practices that make them easier to search, safer to operate, cheaper to retain, and more useful alongside metrics and traces.
We'll start with what to log and how to structure it, then move into event context, OpenTelemetry conventions, performance, volume control, alerting, and retention.
1. Decide what's worth logging
You won't know every event worth logging before building a service, and you'll still make many logging decisions as you go, but having a few clear criteria helps you make those decisions consistently and with future investigations in mind.
A useful baseline is to log events that help you answer questions like:
- What happened?
- Which request, job, user action, or background task did it affect?
- Why did the operation fail or behave unexpectedly?
- What changed in the system?
- What context would you need to understand this event later?
Security-sensitive events deserve particular attention. The OWASP Logging Cheat Sheet recommends recording events such as authentication failures, authorization failures, suspicious workflow activity, and high-risk administrative actions.
OWASP also provides useful guidance on what should stay out of logs, including passwords, access tokens, encryption keys, payment card data, and other sensitive information. Practice 6 covers how to handle that safely.
2. Structure your logs
Unstructured, string-formatted logs are an anti-pattern in modern systems. Logs
designed to be read by someone running grep on a server become very hard to
query once you have more than a handful of services.
Logs are data and must be treated as such from the moment of creation. This means every log entry must be a structured, machine-parsable object (JSON is the lingua-franca here). Every piece of information becomes a distinct key-value pair, ready to be indexed, queried, and aggregated.
Instead of emitting a blob of text like this:
1[2025-07-24 07:45:10] INFO: Payment processed for user 12345 in 54ms. Request ID: abc-xyz-789
You'd output a queryable data record:
123456789{"level": "info","timestamp": "2025-07-24T06:45:10.123Z","message": "payment processed","service": "billing-api","duration_ms": 54,"user_id": "12345","request_id": "abc-xyz-789"}
This makes it trivial to answer questions like: "How many payment operations
failed in the last hour?" or "Show me all logs tied to request abc-xyz-789",
or even "Which logs are associated with user 12345 today?" with a simple fast
query.
Most modern logging frameworks now support or default to emitting structured output out of the box, and languages are starting to treat it as a core feature rather than an optional add-on (such as Go's log/slog package).
3. Write log messages you can search and group
Structured logging gives you fields, but the message still matters. It's often the first thing you scan, and a stable message makes repeated events much easier to group and count.
A common mistake is interpolating changing values directly into the message:
1logger.error(`Payment of $${amount} failed for user ${userId}`);
Each failed payment now produces a different message, so if you want to count payment failures or find all records for the same kind of event, you first have to account for those changing values.
It's better to keep the message constant and put the variable data in distinct fields:
1logger.error({ amount, "user.id": userId }, "payment failed");
Now every occurrence has the same msg, so you can group and count those
records directly. The amount and user ID remain available as separate fields for
filtering and analysis.
4. Use log levels as an actionable signal
Few topics in logging generate as much debate as the proper use of severity levels.
Some argue for
just using INFO and ERROR
to express that your system is either doing what it's supposed to or it isn't.
That keeps the model simple, but it also removes useful distinctions because not every anomaly is a failure, and not every failure requires the same response.
More granular levels let you preserve those distinctions. For example, WARN
can flag an unusual condition that deserves attention without indicating that an
operation failed, while ERROR records an actual failure.
FATAL is useful for conditions that force the process to terminate, and
DEBUG provides additional detail for investigations without making that
verbosity the production default.
Keep in mind that log levels should reflect both severity and actionability. If an event doesn't require attention, it shouldn't be logged at a level that triggers an alert.
You should also keep production verbosity restrained, and make it adjustable at runtime where possible. That lets you surface extra detail when required during an investigation without redeploying or leaving verbose logging on permanently.
Log levels aren't the main value of a log, but they're a powerful signal when used consistently. They help you separate the routine from the exceptional and surface low-level details only when it's needed most.
5. Enrich your logs with sufficient context
Once your logs are structured, make sure every record carries enough context to be useful on its own. A message tells you what happened, but the surrounding fields should tell you which specific event you're looking at and what made it different from every other occurrence.
Useful event context often includes:
- The entity involved, such as an order, payment, account, or job ID.
- The operation that was attempted.
- The outcome or reason for failure.
- Relevant domain values, such as payment method, subscription tier, or order status.
For example, when logging a payment failure, you'll want to include the details that distinguish each specific occurrence:
12345678910logger.error({"payment.id": paymentId,"order.id": orderId,"user.id": userId,"payment.method": paymentMethod,reason,},"payment failed",);
The message remains stable, while the fields give you something useful to filter, compare, and investigate.
The OWASP logging cheat sheet also provides a solid reference for useful event attributes, grouped by when the event happened, where (service and code location), who (source address and user identity), and what (event type, severity, and description).
6. Keep sensitive data out of your logs
Logs can easily capture passwords, tokens, personal data, or other sensitive values that were never meant to leave the application. The safest approach is to prevent sensitive data at several layers.
At the application level, avoid logging whole objects that may contain private fields. Instead, prefer logging-safe representations that explicitly allow only the fields you need:
12345678910111213type User struct {ID stringEmail stringPassword stringCreatedAt time.Time}func (u User) LogValue() slog.Value {return slog.GroupValue(slog.String("id", u.ID),slog.Time("created_at", u.CreatedAt),)}
Some logging frameworks can also mask known sensitive fields before records are written, so that values such as passwords, authorization tokens, or email addresses are redacted automatically.
Your telemetry pipeline should be the final safeguard across services, as centralized scrubbing gives you one place to catch sensitive values that application-level controls miss.
For example, if you're using the OpenTelemetry Collector, our guide to scrubbing sensitive data shows how to apply redaction in the Collector before such data leaves your environment.
7. Adopt the OpenTelemetry logs data model
Structured logging gives you machine-readable fields, but OpenTelemetry goes a step further by defining a common data model for logs (and other telemetry signals) across languages, frameworks, and services.
An OpenTelemetry log record has well-defined fields for timestamps, severity, the message body, event attributes, resource metadata, instrumentation scope, and optional trace and span IDs. This gives your logs a consistent shape even when they originate from different logging libraries.
That consistency gives you several useful properties:
- Severity can be normalized across logging frameworks.
- Resource attributes identify the service and environment that produced the record.
TraceIdandSpanIdcan connect a log directly to the request that produced it.Attributescarry event-specific context without being mixed into the message.EventNamecan identify well-known event types with a predictable schema.- Semantic conventions provide standard names and meanings for common attributes, helping logs from different services describe the same concepts consistently.
You don't need to replace your existing logging library to get these benefits. OpenTelemetry is designed to map logs from diverse libraries into the common model before export through a log bridge or the OpenTelemetry Collector.
Our guide to OpenTelemetry logging explains the data model, log bridges, trace correlation, and how you can bring your application logs into this unified model in more detail.
8. Treat logging performance as a first-class concern
Logging isn't free. Every record consumes CPU, memory, and I/O, so high-volume logging can become a noticeable source of overhead.
Before you start optimizing anything, profile representative workloads and look at how much time, allocation, and I/O your logging setup adds. If logging is a meaningful contributor, start with the choices that have the largest effect.
One of those choices is the logging library itself. Implementations can vary considerably, so benchmark results are worth checking for your language and workload. In Go, for example, performance benchmarks show large differences between libraries when recording structured fields.
Where appropriate, write logs asynchronously so request handling doesn't wait on slower disk or network I/O. But keep in mind that buffering introduces a durability trade-off since records queued in-memory may be lost if the process crashes before being flushed to disk.
Also avoid excessive logging inside your hot paths, and don't perform expensive work for a log statement that may never be emitted. If computing a log value is costly, check whether the relevant log level is enabled before doing the work.
123if logger.isEnabledFor(logging.DEBUG):details = build_expensive_debug_context()logger.debug("Debug context: %s", details)
9. Manage log volume and cost intelligently
High log volume increases ingestion and storage costs, puts more pressure on your logging pipeline, and makes useful records harder to find.
The answer isn't to stop logging, but to log smarter, and the best place to start is by cutting high-volume, low-value logs. Successful health checks are a classic example and are often better represented as metrics than as a continuous stream of log records.
You can also reduce volume by favoring richer events over many small ones. A wide event records the important context for an operation in a single structured record, which can eliminate several lower-value log lines that would otherwise describe the same activity piecemeal.
For even more control, some logging frameworks allow you to log continuously to an in-memory ring buffer. Under normal conditions the buffer just overwrites itself, so nothing ever leaves memory.
If the system hits an error, the buffer is dumped along with the error log to ensure that the context from the moments leading up to the failure is preserved. It keeps volume low in the happy path while still capturing rich detail when it matters most.
Sampling gives you another way to control volume, since you usually don't need every routine event to understand what's happening. For example, you might keep every failure while retaining only a small percentage of successful or otherwise repetitive events.
Log deduplication is also useful when the problem is repetition rather than overall traffic. During a cascading failure, a service may emit the same error thousands of times. Instead of storing every copy, you can collapse repeated records and retain a count of how often they occurred to avoid paying unnecessarily for superfluous records.
10. Centralize logs and connect them to other telemetry
Logs are much more useful once they leave the machine or container that produced them and enter a shared telemetry pipeline. Centralizing them gives you a consistent place to retain, process, search, and route records across services.
The exact collection method depends on your environment. You might collect
stdout and stderr
from containers,
tail application log files,
or export logs directly from the application. What matters is that the records
eventually flow through a common pipeline rather than remaining isolated on
individual hosts.
That pipeline can also apply shared controls across services, such as filtering noisy records, redacting sensitive fields, normalizing attributes, and routing different classes of logs to different destinations.
More importantly, logs shouldn't operate as an isolated telemetry signal. When logs, metrics, and traces share consistent service and request context, you can move between them seamlessly during an investigation. A metric can reveal that something changed, a trace can show where the problem occurred, and the related logs can provide the event-level detail you need to understand why.
OpenTelemetry provides a vendor-neutral framework for building this kind of interconnected multi-signal pipeline. It gives your telemetry data a common set of conventions and lets you collect, process, enrich, and route them together across your services through the OpenTelemetry Collector.
11. Alert on symptoms, not individual log lines
Alerting on every ERROR record is a fast route to alert fatigue. A log entry
tells you that something happened, while an alert should tell you that something
needs attention.
In practice, that usually means alerting on rates, trends, or sustained conditions rather than individual records. A single failed payment may be worth logging, while a payment failure rate above 5% for ten minutes is much more likely to indicate an incident.
When you need to alert on a count or rate, derive a metric from the logs and
alert on that instead. This also reinforces why log levels need to stay
meaningful, because the rate of ERROR records is only useful if ERROR
consistently represents a genuine failure.
Google's SRE guidance makes the same distinction by recommending that you page on symptoms, or what's broken, rather than every underlying event that might explain why.
There are exceptions, though. Some individual events are inherently actionable, such as access to a security honeytoken that legitimate activity should never touch. In those cases, a single record can justify an alert.
Whatever triggers the alert, make sure it points to a clear response so the person receiving it knows what to do next.
12. Set retention, access, and rotation policies deliberately
Keeping logs forever increases storage costs and the impact of a breach, but deleting them too soon can leave you without the evidence needed for an incident or audit.
Therefore, set retention according to what each class of log is used for:
- Debugging and operational logs may only be useful for days or weeks.
- Authentication, authorization, and administrative events may need to remain available for much longer investigations.
- Regulatory requirements can impose minimums. PCI DSS, for example, requires at least 12 months of audit log history, with the most recent three months immediately available.
Older logs also don't need to remain in expensive, high-performance storage. Keep recent data searchable and move older records to cheaper archival storage when appropriate.
Retention should go hand in hand with access control. Since logs often contain account identifiers, internal system details, and other operational context that shouldn't be broadly accessible, restrict access to the people and systems that need it, record administrative access where appropriate, and avoid giving broad read permissions simply because the data is centralized.
Finally, control local disk usage with rotation. On Linux,
logrotate can
rotate, compress, and expire files based on age or size, although its
copytruncate mode can lose a small amount of data. In cloud-native
environments, the platform usually handles container log rotation for you.
Bring your logging practices together with Dash0
The practices in this guide become more useful when your observability backend preserves the structure, context, and relationships you worked to add to your logs.
Dash0 is OpenTelemetry-native (not OpenTelemetry-compatible), so structured attributes, resource context, semantic conventions, and trace correlation remain useful after ingestion. Rather than treating logs as a separate data source, Dash0 lets you move between logs, metrics, and traces during an investigation while preserving the context that connects them.
And because Dash0 understands OpenTelemetry semantic conventions, standardized attributes can also drive richer filtering, analysis, and out-of-the-box views instead of being treated as arbitrary key-value pairs.
If you'd like to see how these practices work in a backend built natively around OpenTelemetry, start a free 14-day Dash0 trial today. You can send your existing telemetry, preserve the context you've added to your logs, and investigate them alongside related metrics and traces from the start.
Final thoughts
Good logging is less about producing more records and more about making each one useful. That means deciding what deserves a log, keeping messages stable, preserving the right context, protecting sensitive data, and controlling volume before it becomes expensive or noisy.
The biggest payoff comes when you stop treating logs as an isolated signal. Adopting OpenTelemetry gives logs, metrics, and traces a shared model, consistent context, and common conventions, so they can work together during an investigation instead of forcing you to reconstruct the story manually.
And that's what takes logging from a collection of isolated records to a connected observability signal you can navigate alongside the rest of your telemetry.






