Last updated: March 19, 2026
JSON Logging: A Quick Guide for Engineers
If you’ve ever had to ssh onto a production server and grep through gigabytes of unstructured, plain-text logs to debug a critical failure, you already know the pain.
The classic printf approach to logging is a relic from a simpler time. In today’s world of distributed microservices, ephemeral containers, and complex user interactions, it’s not just inefficient; it’s a liability.
The industry’s answer has been structured logging, and its most popular implementation is newline-delimited JSON logging.
In this guide, we’ll cover the benefits of logging in JSON, but also tackle the hard truths that are often glossed over: the performance tax, the very real financial costs, and the operational discipline required to do it right at scale.
Let’s get started!
The unavoidable case for structured logging
Before we dive into JSON specifically, let’s be crystal clear on why we’re doing this. The goal of modern logging is not to create a human-readable story; it’s to produce a machine-parsable stream of events that is a useful signal for observability.
An unstructured log entry like this is a nightmare to parse reliably:
1[2025-07-06 16:50:54,123] ERROR in PaymentService: Transaction failed for user 12345, amount: 99.99 USD - card declined.
To analyze this across thousands of instances, you’d write fragile regular expressions that break the moment someone changes the wording.
Now, consider its structured equivalent:
1234567891011121314151617{"timestamp": "2025-07-06T16:50:54.123Z","level": "error","service": {"name": "PaymentService","version": "1.4.2"},"message": "Transaction failed","user": {"id": 12345},"payment": {"amount": 99.99,"currency": "USD","reason": "card_declined"}}
You can now easily and reliably query for:
- All errors from
PaymentService, - The total failed transaction amount in the last hour,
- Or the failure rate for a specific service version.
This is only the baseline. Structured logging is not a “nice to have”; it’s a fundamental prerequisite for observing modern software in production.
Implementing JSON logging
Getting your logs into a structured JSON format generally involves one of the following three approaches. You’ll likely use a combination of all three in any reasonably complex environment.
1. Application-level: Using a JSON logging framework
For application code, you’ll need adopt a structured logging framework that supports JSON output. Most language ecosystems offer robust options such as:
- Serilog (C#/.NET).
- Pino or Winston (Node.js).
- Slog, Zap, or Zerolog (Go).
- Logback (Java).
- Monolog (PHP).
- Logging module, Loguru or Structlog (Python).
For example, in Go, you can use the now built-in log/slog package to output JSON logs natively:
12345678910111213141516171819202122232425262728package mainimport ("log/slog""net/http""os")func helloHandler(w http.ResponseWriter, r *http.Request) {slog.Info("incoming request to /hello",slog.String("path", r.URL.Path),slog.String("method", r.Method),slog.String("user_agent", r.UserAgent()),)w.Write([]byte("hello world!"))}func main() {// Set up a global loggerslog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))mux := http.NewServeMux()mux.HandleFunc("/hello", helloHandler)http.ListenAndServe(":8080", mux)}
output123456{"time": "2025-07-11T06:03:21.284934328+01:00","level": "INFO","msg": "Debug message","process_id": 45787}
With your application now configured, the next step is tackling the other major sources of log data: the infrastructure components your application relies on.
2. Infrastructure-level: Configuring your dependencies
Many modern infrastructure components (databases, web servers, proxies) can now emit JSON logs natively, which is far more efficient than collecting plain-text logs and parsing them later.
You often can flip a single switch (or two) in many popular services to have them emit ready-to-consume JSON instead of plaintext logs. For example AWS Lambda provides a simple toggle as shown below:
Other common infrastructure components like NGINX and PostgreSQL can also be configured to output structured JSON logs natively with minimal setup.
3. Agent-level: Parsing unstructured logs
For legacy applications or services that can't be configured to output JSON, your last resort is to parse and transformed the unstructured data as they’re being collected. A tool like the OpenTelemetry Collector can perform these transformations on the fly using components like Filelog receiver or the Transform processor before sending it to your backend.
The downside is that this approach is fragile, tends to break when upstream formats change, and introduces operational complexity that native structured output simply does not have.
The guiding principle is simple: emit structured logs as close to the source as possible. Every transformation you push to a downstream agent is another step that can fail, introduce latency, or silently drop records under load.
Enforcing a uniform schema with OpenTelemetry
Switching every service to emit JSON is a great first step, but it isn’t the finish line. You may find that your auth-service logs a user ID as user.id, your billing-service prefers userId, and your legacy monolith uses usr.
This is the problem of schema inconsistency, and it makes querying and correlation much more difficult. Consistent field names (and data types) are just as critical as structured output.
The solution is to adopt a logging schema across all your applications and infrastructure. While you could invent your own, the industry is rapidly standardizing on the OpenTelemetry Semantic Conventions.
It’s a standardized specification for the structure of logs, metrics, and traces that gets you a consistent field semantics across your entire stack.
Adopting these conventions provides several key benefits.
First, you get standard names like http.request.method, db.query.text, and service.name that have a specific semantic meaning. An OpenTelemetry-native backend will automatically recognize these fields for powerful grouping and correlation.
The standard also cleanly separates metadata about the entity producing the log (Resource) from metadata about the event itself (Attributes).
Finally, it ensures that logs from different services can be correlated together, even if those services are written in different languages or use different logging libraries.
The good news is, you don’t have to overhaul your existing logging instrumentation to get all these benefits. You only have to install the appropriate log bridge for your logging library to automatically translate the log records into the OpenTelemetry log data model, and then let your logging pipeline handle the rest.
Practical considerations for JSON logging at scale
JSON logging pays off in richer diagnostics and simpler parsing, but it does introduce trade-offs. Before you flip every service to structured output, its necessary to account for the impact on performance, cost, and data storage.
The performance overhead
Serializing to JSON does incur extra CPU cycles and a few heap allocations, but in most applications that cost is vastly dwarfed by network round-trips or database queries.
The picture changes for ultra-low-latency workloads where logging happens on the hottest path. In such cases, a chatty or allocation-heavy logger can become a bottleneck.
This is why you must not just pick the most popular logger. For any service where performance matters, you must choose a logger designed to be allocation-aware.
In Go, for instance, Zerolog and Zap consistently benchmark as much as 10× faster than the older Logrus library for identical workloads.
Your performance tuning doesn’t stop at the code itself. You need to configure your agent to batch logs aggressively before sending, and use compression to reduce network costs.
For extreme scenarios, consider logging in a binary format within your cluster and only converting to JSON at the edge collector before shipping to your backend. This is an advanced move, but it’s the kind of trade-off required for ultra-low-latency systems.
Understanding the financial implications
Many observability vendors bill by gigabytes ingested, meaning that a 50% increase in log size can directly translate to a 50% increase in your bill, and that’s before add-ons like extra seats, indexing, long-term retention, or egress.
This creates a fundamental conflict: JSON is perfect for adding rich, valuable context to log records, but doing so increases data volume and, therefore, your costs.
The instinctive response of stripping contextual fields is the wrong one because you're trading away the observability value that structured logging exists to provide.
The levers you should reach for first are the ones covered in the next section: disciplined log-level filtering and deduplication as these reduce volume without sacrificing signal quality.
Beyond that, your choice of observability backend matters more than most engineers realize when budgeting for structured logging at scale. Per-gigabyte pricing directly penalizes the richness of your log entries as every field you add costs money.
Per-event pricing models, like the one Dash0 uses, decouple cost from payload size, meaning you can keep the full structured context in every log entry without a financial incentive to hollow it out.
Managing your log volume
There are two levers that you can employ to keep both performance and spend under control: log-level filtering and deduplication.
1. Filtering with log levels
Every structured logging framework supports severity levels and using them effectively helps you control the verbosity of your logs in different situations.
Most importantly, your log levels should be configurable at runtime without a redeploy so that when an incident occurs, you have the ability to temporarily raise the level for a specific service to get more detailed information without flooding your system with unnecessary noise from healthy components.
2. Taming log volume with deduplication
Even with disciplined log levels, high traffic services tend to generate large volumes of near identical log entries. The same timeout may be retried hundreds of times, the same health check may succeed every few seconds, and the same cache miss pattern may repeat across instances. These entries increase storage and ingestion costs without adding meaningful signal.
Deduplication addresses this by collapsing repeated log entries into a single record that includes a count, rather than storing every occurrence individually. The OpenTelemetry Collector conveniently provides a log deduplication processor that can perform this aggregation directly in your pipeline, but some logging frameworks also provide native deduplication capabilities.
Best practices for high-impact JSON logs
With the groundwork laid, follow these guidelines to squeeze maximum value from every log line.
1. Ensure your logs are highly dimensional
Enrich your logs with all the context needed to understand the event being recorded. Think about all the information you need to understand the operation if it goes sideways and add it to the log.
This deep context is what separates a generic log statement from a useful piece of observability data. Just make sure to switch to an observability backend that doesn’t penalize you for adding those valuable details.
2. Structure your stack traces
The default behavior of logging an exception as a single, multi-line string can be improved by rendering them as an array of frames so that you can aggregate errors by file, method, or exception type:
1234567891011121314151617181920212223{"level": "error","stack": [{"func": "main","line": "19","source": "main.go"},{"func": "main","line": "250","source": "proc.go"},{"func": "goexit","line": "1594","source": "asm_amd64.s"}],"error": "file open failed!","time": "2025-07-11T12:00:12+01:00","message": "failed to execute query"}
If you can't do this, at a minimum, ensure at a minimum that they're serialized as a single escaped string rather than split across multiple lines.
A multi-line stack trace will be treated as separate log events by most collectors, breaking the association with the original error entry and rendering it useless for debugging.
3. Always include a correlation ID
In a distributed system, a single user action can traverse a dozen services. Without a shared identifier that is threaded through every log entry, reconstructing that journey from your logs quickly turns into guesswork.
The standard approach is to propagate trace context fields (like trace_id and span_id) from the moment a request enters your system, usually at the edge, and include it in every log entry produced during that request’s lifetime.
Most structured logging frameworks provide a context aware logger, so once the ID is injected at the boundary, it is attached automatically to every subsequent log call.
123456789{"timestamp": "2025-07-11T12:00:12Z","level": "info","trace_id": "4bf92f3577b34da6a3ce929d0e0e4736","span_id": "a3ce929d0e0e4736","service": { "name": "OrderService" },"message": "Order placed successfully","order": { "id": "ord_8821", "total_usd": 149.99 }}
If you've adopted OpenTelemetry, trace_id and span_id follow the standard semantic conventions and are populated automatically when you use a log bridge. This is another strong reason to standardize on OpenTelemetry rather than inventing your own correlation scheme.
4. Include units in field names
A field named duration: 200 is ambiguous. Is it seconds, milliseconds, or microseconds? Ensure to remove the ambiguity by including the unit in the field name:
duration_ms: 200response_size_bytes: 5120cache_ttl_secs: 3600
This simple convention prevents costly misinterpretations when the pressure is on.
5. Redact sensitive data before it reaches your logs
JSON’s flexibility makes it surprisingly easy to log more than you intend. Passwords, API keys, and personally identifiable information can slip into log entries unless you are deliberate about what you serialize. Once written, those records are difficult to purge and may put you at odds with regulations such as GDPR or HIPAA.
The best place to address this risk is at the source, not in a downstream pipeline. Most structured logging frameworks support middleware or hooks that let you define redaction rules in one central location, so you're not relying on every developer to remember to scrub sensitive fields at each call site.
123456{"level": "info","message": "User login attempt","user": { "id": 12345, "passport_number": "[REDACTED]" },"auth": { "token": "[REDACTED]" }}
As a second line of defense, the OpenTelemetry Collector can apply redaction rules centrally across all your services which helps anything that slips through at the source, or for enforcing redaction on third-party components you don't control. Source-level redaction and collector-level redaction are complementary, not alternatives.
Final thoughts
Structured logging with JSON is a massive leap forward from the printf days. But treating it as a silver bullet is a mistake.
Effective logging is an engineering discipline. It requires you to think critically about performance, cost, and usability. It demands a deliberate approach to schema management and a strategy for controlling volume.
By embracing standards like OpenTelemetry, understanding the trade-offs, and implementing best practices for context and structure, you can transform your logs from a messy, reactive troubleshooting tool into a proactive, powerful engine for true system observability. Stop just writing logs. Start engineering your observability data.




