LatticeStripe emits :telemetry events for every API request and webhook
verification. This guide shows how to bridge those events to OpenTelemetry spans
so they appear in your distributed tracing backend — with complete examples for
Honeycomb and Datadog.
Prerequisites
These dependencies are NOT part of LatticeStripe — add them to your own
application's mix.exs:
# In your application's mix.exs
defp deps do
[
{:lattice_stripe, "~> 1.7"},
# OpenTelemetry — exporter must be listed BEFORE opentelemetry
{:opentelemetry_exporter, "~> 1.8"},
{:opentelemetry, "~> 1.5"},
{:opentelemetry_api, "~> 1.4"},
# ... other deps
]
endImportant:
opentelemetry_exportermust be listed beforeopentelemetryin the deps list for correct initialization order.
The Bridge Handler
The handler below bridges all LatticeStripe telemetry events to OpenTelemetry
spans. Call setup/0 from your Application.start/2 to activate it.
defmodule MyApp.StripeOtelHandler do
@moduledoc """
Bridges LatticeStripe telemetry events to OpenTelemetry spans.
Call `setup/0` in your Application.start/2 to attach the handler.
"""
require OpenTelemetry.Tracer, as: Tracer
@request_events [
[:lattice_stripe, :request, :start],
[:lattice_stripe, :request, :stop],
[:lattice_stripe, :request, :exception]
]
@webhook_events [
[:lattice_stripe, :webhook, :verify, :start],
[:lattice_stripe, :webhook, :verify, :stop]
]
def setup do
:telemetry.attach_many(
"myapp-stripe-otel",
@request_events ++ @webhook_events,
&__MODULE__.handle_event/4,
%{}
)
end
# --- Request events ---
def handle_event([:lattice_stripe, :request, :start], _measurements, metadata, _config) do
Tracer.start_span("stripe.request", %{
kind: :client,
attributes: %{
"http.request.method" => metadata.method |> to_string() |> String.upcase(),
"url.path" => metadata.path,
"stripe.resource" => to_string(metadata.resource),
"stripe.operation" => to_string(metadata.operation)
}
})
end
def handle_event([:lattice_stripe, :request, :stop], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
attrs = %{
"http.response.status_code" => metadata[:http_status],
"stripe.request_id" => metadata[:request_id],
"stripe.attempts" => metadata[:attempts],
"stripe.duration_ms" => duration_ms
}
# Remove nil values to avoid polluting span attributes
attrs = Map.reject(attrs, fn {_k, v} -> is_nil(v) end)
Tracer.set_attributes(attrs)
case metadata.status do
:ok -> Tracer.set_status(:ok, "")
:error -> Tracer.set_status(:error, "Stripe request failed")
end
Tracer.end_span()
end
def handle_event([:lattice_stripe, :request, :exception], _measurements, metadata, _config) do
Tracer.record_exception(metadata.reason, metadata.stacktrace)
Tracer.set_status(:error, "exception")
Tracer.end_span()
end
# --- Webhook events ---
def handle_event([:lattice_stripe, :webhook, :verify, :start], _measurements, metadata, _config) do
Tracer.start_span("stripe.webhook.verify", %{kind: :server})
if path = metadata[:path] do
Tracer.set_attribute("url.path", path)
end
end
def handle_event([:lattice_stripe, :webhook, :verify, :stop], _measurements, metadata, _config) do
case metadata.result do
:ok ->
Tracer.set_status(:ok, "")
:error ->
Tracer.set_attribute(
"stripe.webhook.error_reason",
to_string(metadata[:error_reason] || "unknown")
)
Tracer.set_status(:error, "webhook verification failed")
end
Tracer.end_span()
end
endSpan context threading:
OpenTelemetry.Tracer.start_span/2stores span context in the process dictionary. LatticeStripe uses:telemetry.span/3internally, which emits:startand:stopevents in the same process. This means the span started in the:starthandler is automatically available in the:stophandler.
Wiring the Handler
Call setup/0 in your Application.start/2 before starting supervised children:
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
# Attach the OTel bridge before starting workers
MyApp.StripeOtelHandler.setup()
children = [
{Finch, name: MyApp.Finch, pools: %{"https://api.stripe.com" => [size: 10]}},
MyAppWeb.Endpoint
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
endBackend Configuration
Honeycomb
# config/runtime.exs
config :opentelemetry_exporter,
otlp_protocol: :grpc,
otlp_compression: :gzip,
otlp_endpoint: "https://api.honeycomb.io:443",
otlp_headers: [
{"x-honeycomb-team", System.fetch_env!("HONEYCOMB_API_KEY")},
{"x-honeycomb-dataset", "myapp-stripe"}
]
config :opentelemetry,
resource: %{service: %{name: "myapp", version: "1.0.0"}}Important: Use
config/runtime.exswithSystem.fetch_env!/1for API keys. Never hardcode secrets inconfig/config.exs— they will be committed to git.
Datadog
Datadog ingests OTLP directly via the Datadog Agent (no API key in config):
# config/config.exs (no secrets needed — Agent runs on localhost)
config :opentelemetry_exporter,
otlp_protocol: :http_protobuf,
otlp_endpoint: "http://localhost:4318"
config :opentelemetry,
resource: %{service: %{name: "myapp"}}The Datadog Agent must have OTLP ingestion enabled. In your datadog.yaml:
otlp_config:
receiver:
protocols:
http:
endpoint: 0.0.0.0:4318What You Will See
Once the handler is wired up:
- Each Stripe API request produces a
stripe.requestspan with HTTP method, path, response status, Stripe request ID, and duration. - Webhook verifications produce
stripe.webhook.verifyspans with path and outcome. - Failed requests (non-2xx, connection errors) show error status with the Stripe error details attached.
- Multiple retry attempts appear as a single span — the telemetry span wraps the
entire retry loop, so
stripe.attemptstells you how many attempts were made.
Common Pitfalls
Forgetting require OpenTelemetry.Tracer. OTel Tracer functions are macros.
Without require, you get an undefined function error at compile time. The
require OpenTelemetry.Tracer, as: Tracer line at the top of the module handles
this.
Using deprecated HTTP attribute names. Use http.request.method and
http.response.status_code (stable OTel semantic conventions introduced in
semconv v1.20.0), not the old http.method / http.status_code which are
deprecated. Backends that enforce semconv validation will reject spans using the
old names.
Wrong dep declaration scope. If you are building a library (not an
application), declare OTel deps as only: :dev to avoid forcing them on your
users. If you are building an application, omit :only so the exporter runs in
production — that is where you need the traces.
See Telemetry for the complete list of events and metadata keys.
See Performance for Finch pool sizing and production tuning.
See Circuit Breaker for failure protection.