OpenTelemetry Logs for Erlang/Elixir
This document covers OpenTelemetry Logs for Erlang/Elixir, focusing on integrating with the Elixir Logger and Erlang logger modules. To learn how to install and configure OpenTelemetry Erlang/Elixir SDK, see Getting started with OpenTelemetry Erlang/Elixir.
Prerequisites
If you are not familiar with logs terminology like structured logging or log-trace correlation, read the introduction to OpenTelemetry Logs first.
Overview
OpenTelemetry provides log integration for Erlang/Elixir through handler modules that bridge your existing logging infrastructure with OpenTelemetry:
- Elixir Logger backend: Integrates with Elixir's built-in Logger module
- Erlang logger handler: Integrates with Erlang's logger module (OTP 21+)
These integrations automatically capture logs and correlate them with traces when a span is active.
Elixir Logger integration
The OpenTelemetry Elixir SDK provides integration with Elixir's Logger module through the opentelemetry_logger_metadata package.
Installation
Add the dependency to your mix.exs:
defp deps do
[
{:opentelemetry, "~> 1.3"},
{:opentelemetry_api, "~> 1.2"},
{:opentelemetry_exporter, "~> 1.6"}
]
end
Basic configuration
Configure Logger to include OpenTelemetry metadata:
# config/config.exs
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:trace_id, :span_id, :trace_flags]
Adding trace context to logs
Use OpenTelemetry.Ctx to add trace context to your Logger metadata:
defmodule MyApp.TracedLogger do
require Logger
require OpenTelemetry.Tracer
def log_with_trace(level, message, metadata \\ []) do
# Get current span context
span_ctx = OpenTelemetry.Tracer.current_span_ctx()
trace_metadata = if OpenTelemetry.Span.is_valid(span_ctx) do
[
trace_id: OpenTelemetry.Span.trace_id(span_ctx) |> format_id(),
span_id: OpenTelemetry.Span.span_id(span_ctx) |> format_id(),
trace_flags: OpenTelemetry.Span.trace_flags(span_ctx)
]
else
[]
end
Logger.log(level, message, Keyword.merge(metadata, trace_metadata))
end
defp format_id(id) when is_integer(id) do
Integer.to_string(id, 16) |> String.downcase()
end
defp format_id(id), do: id
end
Complete example
defmodule MyApp.OrderProcessor do
require Logger
require OpenTelemetry.Tracer
def process_order(order_id) do
OpenTelemetry.Tracer.with_span "process_order" do
span_ctx = OpenTelemetry.Tracer.current_span_ctx()
trace_id = OpenTelemetry.Span.trace_id(span_ctx)
# Log with trace context
Logger.metadata(trace_id: trace_id)
Logger.info("Starting order processing", order_id: order_id)
case validate_order(order_id) do
:ok ->
Logger.info("Order validated successfully", order_id: order_id)
complete_order(order_id)
{:error, reason} ->
Logger.error("Order validation failed",
order_id: order_id,
reason: reason
)
OpenTelemetry.Tracer.set_status(:error, "Validation failed: #{reason}")
{:error, reason}
end
end
end
defp validate_order(_order_id), do: :ok
defp complete_order(order_id) do
OpenTelemetry.Tracer.with_span "complete_order" do
Logger.info("Completing order", order_id: order_id)
# Order completion logic
{:ok, order_id}
end
end
end
Erlang logger integration
For Erlang applications using OTP 21+ logger, you can create a custom handler to add trace context:
Custom logger handler
-module(otel_logger_handler).
-behaviour(logger_handler).
-export([log/2, adding_handler/1, removing_handler/1]).
adding_handler(Config) ->
{ok, Config}.
removing_handler(_Config) ->
ok.
log(#{msg := Msg, level := Level, meta := Meta} = LogEvent, Config) ->
%% Get current span context
SpanCtx = otel_tracer:current_span_ctx(),
%% Add trace context if valid span exists
EnrichedMeta = case otel_span:is_valid(SpanCtx) of
true ->
TraceId = otel_span:trace_id(SpanCtx),
SpanId = otel_span:span_id(SpanCtx),
TraceFlags = otel_span:trace_flags(SpanCtx),
Meta#{
trace_id => format_id(TraceId),
span_id => format_id(SpanId),
trace_flags => TraceFlags
};
false ->
Meta
end,
%% Forward to default handler or your preferred output
logger_std_h:log(LogEvent#{meta => EnrichedMeta}, Config).
format_id(Id) when is_integer(Id) ->
string:lowercase(integer_to_list(Id, 16));
format_id(Id) ->
Id.
Configuration
Add the handler in your sys.config:
[
{kernel, [
{logger, [
{handler, otel_handler, otel_logger_handler, #{
level => info,
formatter => {logger_formatter, #{
template => [time, " ", level, " ", msg, " ",
{trace_id, ["trace_id=", trace_id], []}, " ",
{span_id, ["span_id=", span_id], []}, "\n"]
}}
}}
]}
]}
].
Usage example
-module(order_processor).
-include_lib("opentelemetry_api/include/otel_tracer.hrl").
-export([process_order/1]).
process_order(OrderId) ->
?with_span(<<"process_order">>, #{}, fun() ->
logger:info("Starting order processing", #{order_id => OrderId}),
case validate_order(OrderId) of
ok ->
logger:info("Order validated successfully", #{order_id => OrderId}),
complete_order(OrderId);
{error, Reason} ->
logger:error("Order validation failed", #{
order_id => OrderId,
reason => Reason
}),
?set_status(?OTEL_STATUS_ERROR, <<"Validation failed">>),
{error, Reason}
end
end).
validate_order(_OrderId) -> ok.
complete_order(OrderId) ->
?with_span(<<"complete_order">>, #{}, fun() ->
logger:info("Completing order", #{order_id => OrderId}),
{ok, OrderId}
end).
Log-trace correlation
When you emit a log within an active trace span, you can include:
- trace_id: Links log to the entire distributed trace
- span_id: Links log to the specific operation
- trace_flags: Indicates if the trace is sampled
This enables bidirectional navigation between logs and traces in Uptrace.
Elixir correlation helper
defmodule MyApp.LogHelper do
require Logger
@doc """
Logs a message with automatic trace context injection.
"""
def info(message, metadata \\ []) do
Logger.info(message, add_trace_context(metadata))
end
def error(message, metadata \\ []) do
Logger.error(message, add_trace_context(metadata))
end
def warning(message, metadata \\ []) do
Logger.warning(message, add_trace_context(metadata))
end
def debug(message, metadata \\ []) do
Logger.debug(message, add_trace_context(metadata))
end
defp add_trace_context(metadata) do
span_ctx = OpenTelemetry.Tracer.current_span_ctx()
if OpenTelemetry.Span.is_valid(span_ctx) do
trace_id = OpenTelemetry.Span.trace_id(span_ctx)
span_id = OpenTelemetry.Span.span_id(span_ctx)
Keyword.merge(metadata, [
trace_id: format_hex(trace_id),
span_id: format_hex(span_id)
])
else
metadata
end
end
defp format_hex(id) when is_integer(id) do
id |> Integer.to_string(16) |> String.downcase() |> String.pad_leading(32, "0")
end
defp format_hex(id), do: to_string(id)
end
Phoenix integration
For Phoenix applications, you can use a custom Logger backend or Plug to automatically add trace context:
defmodule MyAppWeb.Plugs.TraceLogger do
@behaviour Plug
require Logger
def init(opts), do: opts
def call(conn, _opts) do
span_ctx = OpenTelemetry.Tracer.current_span_ctx()
if OpenTelemetry.Span.is_valid(span_ctx) do
trace_id = OpenTelemetry.Span.trace_id(span_ctx)
span_id = OpenTelemetry.Span.span_id(span_ctx)
Logger.metadata(
trace_id: format_hex(trace_id),
span_id: format_hex(span_id)
)
end
conn
end
defp format_hex(id) when is_integer(id) do
id |> Integer.to_string(16) |> String.downcase()
end
defp format_hex(id), do: to_string(id)
end
Add the plug to your endpoint or router:
# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
# Add after OpenTelemetry instrumentation
plug MyAppWeb.Plugs.TraceLogger
# ... other plugs
end
Best practices
Use structured logging
Use key-value pairs for structured logging to enable filtering and analysis:
# Good: Structured logging
Logger.info("Database query executed",
query_type: "SELECT",
table: "users",
duration_ms: 45,
rows_affected: 1
)
# Bad: Unstructured message
Logger.info("SELECT query on users table took 45ms and affected 1 row")
Log at appropriate levels
# Debug: Detailed diagnostic information
Logger.debug("Cache lookup", key: cache_key, hit: true)
# Info: Normal operational messages
Logger.info("Request processed", status: 200, duration_ms: 150)
# Warning: Unusual but recoverable situations
Logger.warning("Retry attempt", attempt: 3, max_attempts: 5)
# Error: Failures requiring attention
Logger.error("Payment processing failed", error: reason, order_id: order_id)
Avoid logging sensitive data
Never log passwords, tokens, API keys, or personally identifiable information (PII):
# Bad: Logging sensitive data
Logger.info("User login", password: password, token: auth_token)
# Good: Log only safe identifiers
Logger.info("User login", user_id: user_id, ip_address: client_ip)
Include context in error logs
When logging errors, include enough context to diagnose the issue:
try do
process_payment(order)
rescue
error ->
Logger.error("Payment processing failed",
error_type: error.__struct__,
error_message: Exception.message(error),
order_id: order.id,
amount: order.total,
stacktrace: Exception.format_stacktrace(__STACKTRACE__)
)
reraise error, __STACKTRACE__
end