
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Debugging a distributed system at 3 AM requires more than scattered console output; it demands queryable, structured data that correlates across services. Effective production logging for Elixir applications hinges on configuring the standard Logger library to emit machine-readable JSON asynchronously, preventing I/O bottlenecks from degrading your BEAM runtime performance. This guide covers the exact configuration patterns, backend choices, and metadata strategies needed to turn raw logs into actionable observability signals without sacrificing throughput or compliance.
:logger with an asynchronous JSON formatter (like logger_json), setting environment-specific log levels via runtime config, and enriching entries with trace context. Always use non-blocking backends to prevent I/O latency from stalling OTP processes during high-load incidents.How do you configure structured production logging for Elixir applications?
The default Elixir logger outputs human-readable text suitable for development but disastrous for production parsing. In 2026, every serious deployment must emit structured JSON. This allows log aggregators like Grafana Loki or Elasticsearch to index fields automatically, enabling queries like {app="billing", user_id="123"} instead of fragile regex matching against unstructured strings. The community standard for this is logger_json, which formats logs as single-line JSON objects compatible with most cloud-native stacks.
Installing and configuring logger_json
Add the dependency to your mix.exs file and configure the formatter in your runtime configuration. Avoid placing production logging configuration in config/dev.exs or config/prod.exs; use runtime.exs to read environment variables at boot time, ensuring the same release artifact works across staging and production.
# mix.exs
defp deps do
[
{:logger_json, "~> 6.0"},
{:jason, "~> 1.4"}
]
end In your config/runtime.exs, replace the default formatter conditionally based on the environment. This pattern ensures local development remains readable while production emits structured data:
# config/runtime.exs
if config_env() == :prod do
config :logger, :default_formatter,
format: {LoggerJSON.Formatters.Basic, :format},
metadata: [:request_id, :trace_id, :span_id, :user_id],
json_encoder: Jason
# Set level via env var for dynamic adjustment
level = System.get_env("LOG_LEVEL", "info") |> String.to_existing_atom()
config :logger, level: level
end A common mistake is forgetting to add :jason explicitly. While logger_json depends on it, some edge cases in umbrella apps fail to resolve the encoder at runtime without the direct dependency. Always declare it. For teams following broader standards, aligning this setup with structured logging best practices ensures consistency across polyglot microservices.
Why must Elixir logging be asynchronous in production?
The BEAM VM schedules lightweight processes across fixed CPU cores. If a logging call blocks waiting for disk I/O or network acknowledgment, it stalls that scheduler, potentially cascading into latency spikes across unrelated requests. Synchronous logging is acceptable for low-traffic internal tools but is a liability for any customer-facing Phoenix application handling concurrent connections.
Configuring async mode safely
Elixir’s Logger supports an asynchronous mode where log messages are sent to a dedicated handler process via message passing. The calling process returns immediately, resuming business logic while the handler batches and writes logs in the background.
# config/runtime.exs
config :logger, :default_handler,
level: :info,
module: :logger_std_h,
config: [
type: :standard_io,
max_size: 10_000, # Drop oldest if queue exceeds this
burst_limit: 5000, # Max messages per second
sync_mode_qlen: 200, # Switch to sync only if queue < 200
drop_mode_qlen: 9000 # Start dropping at this threshold
] The parameters above define a safety valve. Under normal load, logging is fully async. If the queue grows beyond sync_mode_qlen, Logger temporarily switches to synchronous mode to apply backpressure rather than losing logs. Only when the queue approaches memory limits does it drop entries. This hybrid approach balances reliability with availability—a critical trade-off in incident response scenarios where partial data beats total silence.
How do you integrate OpenTelemetry with Elixir Logger?
Logs without trace context are orphaned anecdotes. Modern observability requires correlating log entries with specific traces and spans. The opentelemetry_logger_metadata_bridge package automatically injects trace_id and span_id into every log entry emitted within an active span. This is non-negotiable for debugging distributed transactions in Phoenix or Broadway pipelines.
Bridge configuration and metadata propagation
After installing opentelemetry and the bridge package, configure the logger to include trace fields in its metadata whitelist. Without this explicit allowlist, the bridge attaches the data internally but the formatter ignores it.
# config/runtime.exs
config :logger, :default_formatter,
metadata: [:request_id, :trace_id, :span_id, :otel_trace_flags]
# Ensure the bridge starts before your app supervision tree
config :opentelemetry_logger_metadata_bridge,
enabled: true When a request enters your Phoenix endpoint, the OpenTelemetry instrumentation creates a root span. Every subsequent Logger.info/2 call within that request cycle automatically carries the trace ID. When viewing logs in Jaeger or Tempo, you can jump directly from a slow trace to its associated log lines. For teams adopting broader telemetry standards, see OpenTelemetry: the observability standard for cross-language alignment strategies.
What metadata should you include in Elixir production logs?
Metadata transforms generic messages into queryable events. However, over-enrichment inflates storage costs and risks leaking PII. A disciplined metadata strategy includes three tiers: correlation identifiers, operational context, and domain markers.
- Correlation:
request_id,trace_id,span_id,correlation_id(for cross-service workflows). - Operational:
node,pid,mfa(module/function/arity),file,line. Include these in dev/staging; consider strippingfileandlinein high-volume production to reduce payload size. - Domain:
user_id,tenant_id,order_id,feature_flag. These enable business-level debugging like "show all logs for tenant X during the outage window."
Never log sensitive fields: passwords, tokens, full credit card numbers, or PHI. Use metadata filters or redaction libraries to scrub known keys before formatting. In regulated environments, this isn't optional—it's audit evidence. Teams managing compliance should reference automating SOC 2 compliance evidence to embed log sanitization checks directly into CI pipelines.
How do Elixir logging backends compare for production workloads?
Choosing the right backend depends on your infrastructure, volume, and compliance requirements. The table below compares the most common options used in 2026 production deployments.
| Backend | Best For | Async Support | Structured Output | Trade-offs |
|---|---|---|---|---|
| logger_json + stdio | Kubernetes / Cloud Native | Native | JSON (native) | Requires external aggregator; no built-in buffering |
| Sentry Logger | Error tracking & alerts | Via handler | Sentry protocol | Only for errors/crashes; not general-purpose logging |
| LoggerFileBackend | On-prem / air-gapped | Yes | Configurable | Disk I/O risk; manual rotation needed; no central search |
| Logflare / HTTP | Serverless / Edge | Buffered | JSON | Network dependency; potential data loss on outage |
| Broadway + Kafka | High-volume audit trails | Fully async | Any | Complexity overhead; requires Kafka cluster |
For most Phoenix applications deployed on Kubernetes or ECS, logger_json writing to stdout is the correct default. Container orchestrators capture stdout natively and forward it to your chosen aggregator. Custom HTTP or file backends introduce failure modes that compound during outages—exactly when you need logs most. Reserve specialized backends for specific compliance or legacy constraints.
Implementing Production Logging for Elixir Applications Safely
Reliable observability is a product of deliberate engineering, not accidental configuration. Start with logger_json on stdout, enforce asynchronous delivery with bounded queues, and enrich every entry with trace context via OpenTelemetry. Define your metadata schema early and treat it as a contract between your application and your ops team. Review log volume weekly—what seems useful in staging often becomes noise at scale. If your current setup lacks structure or async safety, prioritize fixing those two gaps before adding new integrations. For architecture reviews or help migrating legacy Elixir systems to modern observability standards, reach out to discuss your specific deployment.