metrixwire
Zero-config APM SDK for Elixir — Phoenix, Plug & Ecto. In Phoenix it's fully automatic: add the dependency and set METRIXWIRE_KEY. Every request, database query and outbound HTTP call is instrumented automatically. There is no manual span API. Non-blocking: if the MetrixWire endpoint is down, your app keeps running normally.
Installation
# mix.exs
def deps do
[
{:metrixwire, "~> 0.1", hex: :metrixwire}
]
end
mix deps.get
Usage
Phoenix — fully automatic
Add the dep, set the key, done. The SDK ships its own OTP application that starts on boot, brings up the transport, and attaches every :telemetry handler — no code changes, no middleware to wire up:
export METRIXWIRE_KEY=mw_...
Or configure it in config/runtime.exs:
config :metrixwire, api_key: System.get_env("METRIXWIRE_KEY")
Phoenix emits [:phoenix, :endpoint, :start | :stop] and [:phoenix, :router_dispatch, :*] telemetry, which the SDK hooks to open one trace per request, refine the route to the matched pattern (GET /users/:id), and capture the HTTP status and any exception.
Bare Plug — one line
Non-Phoenix Plug apps add a single plug to their pipeline (analogous to a Rack middleware). Everything else — Ecto queries, outbound HTTP — is still captured automatically:
plug MetrixWire.Plug
How the automatic tracing works
The whole Elixir ecosystem emits :telemetry events, so one handler set covers every framework. The SDK attaches them at boot via :telemetry.attach_many/4. A Phoenix/Plug request runs in a single process, and Ecto/Finch telemetry is emitted in the caller process, so the active trace — held in the process dictionary — correctly correlates every span to its request.
| Source | Traced automatically | How |
|---|---|---|
| Phoenix | ✅ | [:phoenix, :endpoint, :*] opens/closes the trace; [:phoenix, :router_dispatch, :*] refines the route + captures exceptions. |
| bare Plug | ✅ | plug MetrixWire.Plug — one trace per request, closed on before_send. |
| Ecto | ✅ | [<repo>, :query] → db_query span (SQL as description, total_time converted to ms, meta.rowCount from num_rows). Repos are auto-discovered. |
| Finch | ✅ | [:finch, :request, :stop] → http_call span with meta.statusCode. |
| Tesla | ✅ | [:tesla, :request, :stop] → http_call span with meta.statusCode. |
Each handler is guarded and never throws into the emitting process. Schema/transaction-control SQL (BEGIN, COMMIT, SET, …) is filtered out.
Configuration
All options live under config :metrixwire, ... and fall back to environment variables:
config :metrixwire,
api_key: "mw_...", # required (falls back to METRIXWIRE_KEY)
endpoint: "http://localhost:3000/ingest", # default (a base URL is accepted; /ingest is appended)
flush_interval_ms: 5000, # how often batches are sent
enabled: true, # set false to disable (or METRIXWIRE_ENABLED=false)
timeout_ms: 3000, # send timeout (short, non-blocking)
max_batch: 20, # flush immediately once this many are queued
capture_source: true # capture the file:line a span originated from
Environment variables: METRIXWIRE_KEY, METRIXWIRE_ENDPOINT, METRIXWIRE_ENABLED. With no API key the SDK boots in disabled mode (logs a warning, attaches nothing) instead of raising.
Non-blocking behavior
- Traces are batched in a
GenServerand flushed off the request path on a timer + oncemax_batchis reached. The HTTP send runs in a detachedTask, so a slow endpoint never backs up the mailbox. - Delivery uses the built-in
:httpc/:inets— no external HTTP client dependency. The only runtime dependency is:telemetry. - All transport errors are swallowed — instrumentation never throws into or blocks your app.
- A final flush runs on application shutdown; you can also call
MetrixWire.flush/0before a short-lived process exits.
Error boundary (escape hatch)
For frameworks the SDK can't hook automatically, attach an exception to the active trace — this is not a manual span API; requests/queries are still captured automatically:
try do
do_work()
rescue
e ->
MetrixWire.capture_exception(e, __STACKTRACE__)
reraise e, __STACKTRACE__
end