Packages

Zero-config APM SDK for Elixir — Phoenix, Plug, Ecto. Add the dep and set METRIXWIRE_KEY; every request/query/HTTP call is traced automatically.

Current section

Files

Jump to
metrixwire lib metrixwire trace.ex
Raw

lib/metrixwire/trace.ex

defmodule MetrixWire.Trace do
@moduledoc """
One request / unit of work. Holds its spans and trace-level meta. Serialized to
the exact ingest wire shape. Not part of any public API — instrumentation opens a
trace on request start, accumulates spans, and closes it on request stop.
Structs are immutable, so the "active" trace lives in the process dictionary (a
Phoenix/Plug request runs in one process) and every span append replaces it.
"""
alias MetrixWire.Span
defstruct route: nil,
method: nil,
status: "success",
started_at: nil,
started_mono: 0,
duration_ms: nil,
spans: [],
meta: %{},
start_memory: nil
@type t :: %__MODULE__{}
@doc "Open a new trace. `route`/`method` may be refined later from telemetry."
def new(route, method) do
%__MODULE__{
route: route,
method: method,
started_at: MetrixWire.iso8601(DateTime.utc_now()),
started_mono: MetrixWire.monotonic_ms(),
status: "success",
spans: [],
meta: %{},
start_memory: MetrixWire.memory_bytes()
}
end
@doc "Append a span (kept in reverse; reversed on serialize)."
def add_span(%__MODULE__{} = trace, %Span{} = span) do
%{trace | spans: [span | trace.spans]}
end
@doc "Set a trace-level meta key."
def put_meta(%__MODULE__{} = trace, key, value) do
%{trace | meta: Map.put(trace.meta, key, value)}
end
@doc "Attach an exception + flag the trace as an error. Never throws."
def capture_exception(%__MODULE__{} = trace, error, stacktrace \\ nil) do
case MetrixWire.exception_meta(error, stacktrace) do
nil -> %{trace | status: "error"}
meta -> %{trace | status: "error", meta: Map.put(trace.meta, "exception", meta)}
end
rescue
_ -> %{trace | status: "error"}
end
@doc "Freeze the duration + memory delta when the trace closes."
def finalize(%__MODULE__{} = trace) do
duration = max(MetrixWire.monotonic_ms() - trace.started_mono, 0)
mem = MetrixWire.memory_delta_mb(trace.start_memory)
meta = if mem > 0, do: Map.put(trace.meta, "memoryMb", mem), else: trace.meta
%{trace | duration_ms: duration, meta: meta}
end
@doc "Serialize to the ingest wire shape (string keys, camelCase)."
def to_map(%__MODULE__{} = trace) do
duration = trace.duration_ms || max(MetrixWire.monotonic_ms() - trace.started_mono, 0)
base = %{
"route" => trace.route,
"method" => trace.method,
"startedAt" => trace.started_at,
"durationMs" => duration,
"status" => trace.status,
"spans" => trace.spans |> Enum.reverse() |> Enum.map(&Span.to_map/1)
}
if map_size(trace.meta) > 0, do: Map.put(base, "meta", trace.meta), else: base
end
end