Packages

Production-grade Elixir client for the Setu API platform (UPI, BBPS, WhatsApp, Account Aggregator, KYC, eSign)

Current section

Files

Jump to
setu_client lib setu_client telemetry.ex
Raw

lib/setu_client/telemetry.ex

defmodule SetuClient.Telemetry do
@moduledoc """
Telemetry integration for the Setu SDK.
## Events emitted
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:setu, :request, :start]` | `system_time` | `method, url, attempt` |
| `[:setu, :request, :stop]` | `duration` (native) | `method, url, status, attempt` |
| `[:setu, :request, :exception]` | `duration` | `method, url, kind, reason, attempt` |
| `[:setu, :token, :refresh]` | `duration` | `environment, success` |
| `[:setu, :rate_limit, :wait]` | `wait_ms` | `%{}` |
## Attaching a handler
:telemetry.attach_many(
"my-handler",
[[:setu, :request, :stop], [:setu, :token, :refresh]],
&MyApp.Metrics.handle/4,
nil
)
"""
use GenServer
require Logger
# ── Public API ─────────────────────────────────────────────────────────────
@doc false
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "Wraps `fun` in a telemetry span under `event_prefix`."
@spec span([atom()], map(), (-> result)) :: result when result: term()
def span(event_prefix, meta, fun) do
start = System.monotonic_time()
:telemetry.execute(event_prefix ++ [:start], %{system_time: System.system_time()}, meta)
try do
result = fun.()
duration = System.monotonic_time() - start
stop_meta =
case result do
{:ok, %{status: status}} -> Map.put(meta, :status, status)
_ -> meta
end
:telemetry.execute(event_prefix ++ [:stop], %{duration: duration}, stop_meta)
result
rescue
e ->
duration = System.monotonic_time() - start
:telemetry.execute(
event_prefix ++ [:exception],
%{duration: duration},
Map.merge(meta, %{kind: :error, reason: e})
)
reraise e, __STACKTRACE__
end
end
@doc false
@spec emit_token_refresh(atom(), boolean(), integer()) :: :ok
def emit_token_refresh(env, success, duration) do
:telemetry.execute(
[:setu, :token, :refresh],
%{duration: duration},
%{environment: env, success: success}
)
end
@doc false
@spec emit_rate_limit_wait(non_neg_integer()) :: :ok
def emit_rate_limit_wait(wait_ms) do
:telemetry.execute([:setu, :rate_limit, :wait], %{wait_ms: wait_ms}, %{})
end
# ── GenServer callbacks ────────────────────────────────────────────────────
@impl GenServer
@spec init(keyword()) :: {:ok, :ok}
def init(_opts) do
attach_default_handlers()
{:ok, :ok}
end
# ── Private ────────────────────────────────────────────────────────────────
@spec attach_default_handlers() :: :ok
defp attach_default_handlers do
events = [[:setu, :request, :exception], [:setu, :token, :refresh]]
:telemetry.attach_many(
"setu-default-logger",
events,
&__MODULE__.handle_event/4,
:ok
)
rescue
_ -> :ok
end
@doc false
@spec handle_event([atom()], map(), map(), term()) :: :ok
def handle_event([:setu, :request, :exception], _m, meta, _cfg) do
Logger.warning("[Setu] HTTP exception url=#{meta.url} reason=#{inspect(meta.reason)}")
end
def handle_event([:setu, :token, :refresh], %{duration: d}, meta, _cfg) do
ms = System.convert_time_unit(d, :native, :millisecond)
status = if meta.success, do: "ok", else: "failed"
Logger.debug("[Setu] Token refresh env=#{meta.environment} status=#{status} #{ms}ms")
end
def handle_event(_event, _m, _meta, _cfg), do: :ok
end