ExternalService emits :telemetry events so
that calls to external services can be observed and instrumented. Attach a
handler to forward them to your metrics or logging backend — StatsD, Prometheus
(via TelemetryMetrics), structured logs, or anything else.
Every event carries a :service key in its metadata identifying the service it
relates to.
The events
[:external_service, :call, :start]
Emitted when a guarded call begins.
- Measurements:
:system_time,:monotonic_time - Metadata:
:service
[:external_service, :call, :stop]
Emitted when a guarded call completes — including when it completes with an error
value such as ExternalService.RetriesExhausted or
ExternalService.CircuitBreakerOpen.
- Measurements:
:duration,:monotonic_time - Metadata:
:service,:result(the value returned from the call)
[:external_service, :call, :exception]
Emitted when a guarded call raises — for example a non-retriable exception from
your function, or call!/3 raising on an open breaker or exhausted retries.
- Measurements:
:duration,:monotonic_time - Metadata:
:service,:kind,:reason,:stacktrace
[:external_service, :call, :retry]
Emitted each time a call's function fails in a way that melts the circuit breaker
(it returned :retry / {:retry, reason}, or it raised). Whether another
attempt is actually made depends on the retry options.
- Measurements:
:count(always1) - Metadata:
:service,:reason
[:external_service, :circuit_breaker, :blown]
Emitted when a call is rejected because the service's circuit breaker is open.
- Measurements:
:count(always1) - Metadata:
:service
[:external_service, :rate_limit, :sleep]
Emitted when a call is throttled and put to sleep to stay within the configured rate limit.
- Measurements:
:sleep_time(milliseconds) - Metadata:
:service
Event names are a stable contract
The event names use :circuit_breaker (not the underlying :fuse)
deliberately, so they remained stable through the 2.0 terminology changes.
Treat them as a public API you can build dashboards on.
The :call events form a :telemetry.span/3,
so :start is always paired with either :stop or :exception, and the
:duration measurement is directly usable as call latency.
Attaching a handler
A minimal handler that logs retries and breaker trips:
:telemetry.attach_many(
"external-service-logger",
[
[:external_service, :call, :retry],
[:external_service, :circuit_breaker, :blown],
[:external_service, :rate_limit, :sleep]
],
&MyApp.ServiceTelemetry.handle_event/4,
nil
)
defmodule MyApp.ServiceTelemetry do
require Logger
def handle_event([:external_service, :call, :retry], _measurements, %{service: svc, reason: reason}, _config) do
Logger.warning("Retrying #{inspect(svc)}: #{inspect(reason)}")
end
def handle_event([:external_service, :circuit_breaker, :blown], _measurements, %{service: svc}, _config) do
Logger.error("Circuit breaker open for #{inspect(svc)}")
end
def handle_event([:external_service, :rate_limit, :sleep], %{sleep_time: ms}, %{service: svc}, _config) do
Logger.info("Rate limited #{inspect(svc)}; slept #{ms}ms")
end
endAttach handlers once at application start (for example in your
Application.start/2).
With Telemetry.Metrics
If you use Telemetry.Metrics, the
events map cleanly onto metric definitions:
import Telemetry.Metrics
[
# Call latency, tagged by service.
summary("external_service.call.stop.duration",
unit: {:native, :millisecond},
tags: [:service]
),
# How often calls fail and trigger a retry.
counter("external_service.call.retry.count", tags: [:service]),
# How often the breaker trips.
counter("external_service.circuit_breaker.blown.count", tags: [:service]),
# Time lost to rate-limit throttling.
sum("external_service.rate_limit.sleep.sleep_time", tags: [:service])
]These four signals — latency, retry rate, breaker trips, and throttle time — give you a clear, per-service picture of the health of every external dependency your application relies on.