Malla includes a zero-overhead observability system called Malla.Tracer. It provides a technology-agnostic API for instrumenting your services with spans, logs, metrics, and events.
The key principle is that you can instrument your code while you write it, without committing to a specific telemetry backend (like OpenTelemetry or Datadog). That decision can be deferred and implemented later by adding a plugin, without changing any of your instrumented code.
Quick Start
To use the tracer, use Malla.Tracer in your service module.
defmodule MyService do
use Malla.Service
use Malla.Tracer # Import the tracer macros
def process_payment(user_id, amount) do
# 'span' creates a new trace span for this unit of work
span [:payment, :process] do
# 'info' creates a log entry associated with the span
info("Processing payment", user_id: user_id, amount: amount)
case charge_card(user_id, amount) do
{:ok, receipt} ->
# 'metric' records a numerical value
metric(:payments_successful, 1)
{:ok, receipt}
{:error, reason} = err ->
# 'error' logs an error message
error("Payment failed", reason: reason)
metric(:payments_failed, 1)
# 'error/1' can also mark the span as having an error
error(err)
end
end
end
endHow It Works: The Plugin System
- Default Behavior: Without any special plugins,
Malla.Tracerprovides minimal functionality. Spans emit basic:telemetryevents, and logs are sent to the standard ElixirLogger. - With Plugins: When you add a telemetry plugin (e.g., for OpenTelemetry), that plugin overrides the default tracer callbacks. Your
span,info, andmetriccalls are then routed to the full-featured telemetry backend, enabling distributed tracing, rich context, and custom metrics. We will release soon a powerful plugin capable of sending all traces to Jaeger.
This allows you to add comprehensive observability to your application at any time just by adding a plugin to your service configuration.
Tracer API
Spans
Spans represent a unit of work and can be nested to create a detailed trace.
-
span(name, opts, do: block): Executes the block of code within a new span. Thenameis typically a list of atoms. -
span_update(attributes): Updates the current span with new key-value attributes.
span [:http, :request] do
span_update(path: "/users", method: "GET")
# Nested span
span [:database, :query] do
# ...
end
endLogging
Log macros create structured log entries that are automatically associated with the current span. They also support compile-time log level filtering.
-
debug(message, metadata \\ []) -
info(message, metadata \\ []) -
notice(message, metadata \\ []) -
warning(message, metadata \\ []) -
error(message, metadata \\ []) -
error(error_term): Marks the current span with an error status.
Compile-Time Filtering
You can set a minimum log level in your configuration to completely remove log calls from your compiled code, ensuring zero performance impact in production.
# In config/prod.exs
config :malla, log_min_level: :info # This will remove all `debug` callsMetrics and Events
metric(name, value, metadata \\ []): Records a numerical metric.event(name, data \\ [], metadata \\ []): Records a structured event within a span, representing a point-in-time occurrence.
Context Propagation
The tracer system works seamlessly with Malla's remote calls, automatically propagating trace context across nodes to enable distributed tracing.
For custom context propagation (e.g., to manually spawned processes or for integrating with external systems), you can use get_base/0 to retrieve the current span context. A telemetry plugin would then provide a corresponding set_base/1 function to restore that context in another process.
Custom Tracing with Plugins
While Malla may provide official telemetry plugins in the future, you can create your own to integrate with any backend.
A tracing plugin is a standard Malla plugin that implements one or more of the malla_span_* callbacks defined in Malla.Plugins.Tracer.
Here is a simplified example for OpenTelemetry:
defmodule OpenTelemetryPlugin do
use Malla.Plugin
# Override the 'malla_span' callback
defcb malla_span(name, opts, fun) do
# Convert Malla's span name to a string for OpenTelemetry
span_name = Enum.join(List.wrap(name), ".")
# Use the OpenTelemetry API to start a span
OpenTelemetry.Tracer.with_span span_name do
try do
result = fun.()
OpenTelemetry.Span.set_status(:ok)
result
rescue
e ->
OpenTelemetry.Span.record_exception(e)
OpenTelemetry.Span.set_status(:error, Exception.message(e))
reraise e, __STACKTRACE__
end
end
end
# Override the 'malla_span_log' callback to add logs as span events
defcb malla_span_log(level, text_fun, meta) do
text_str = text_fun.()
OpenTelemetry.Span.add_event(text_str, Map.new(meta))
# Continue the chain to also send the log to the default Elixir Logger
:cont
end
endBy adding this plugin to your service, all span and log calls will be sent to OpenTelemetry without any changes to your business logic.