Packages

Official Elixir SDK for Tripswitch — a circuit breaker management service

Current section

Files

Jump to
tripswitch_ex lib tripswitch.ex
Raw

lib/tripswitch.ex

defmodule Tripswitch do
@moduledoc """
Official Elixir SDK for Tripswitch — a circuit breaker management service.
## Quick start
{:ok, _pid} =
Tripswitch.Client.start_link(
project_id: "proj_...",
api_key: "eb_pk_...",
ingest_secret: "...",
name: MyApp.Tripswitch
)
result =
Tripswitch.execute(MyApp.Tripswitch, fn -> call_downstream() end,
breakers: ["payment-service"],
router: "router-id",
metrics: %{"latency_ms" => :latency},
tags: %{"region" => "us-east-1"}
)
## Options for `execute/3`
- `:breakers` — list of breaker names to check before running the task
- `:router` — router ID to tag samples with
- `:metrics``%{"name" => :latency | number | (result -> number)}`; one sample per entry
- `:deferred_metrics``(result -> %{"name" => number})`; merged with `:metrics`
- `:tags``%{String.t() => String.t()}` merged into every sample
- `:error_evaluator``(result -> boolean)`; returns `true` when result should be counted as a
failure. Defaults to matching `{:error, _}` tuples.
- `:trace_id` — string propagated on every sample
- `:breaker_selector``([breaker_meta] -> [name])` — dynamic breaker selection from metadata
- `:router_selector``([router_meta] -> router_id)` — dynamic router selection from metadata
"""
@contract_version "0.2"
alias Tripswitch.{Flusher, StateServer}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
@doc """
Returns the SDK contract version this library implements.
"""
@spec contract_version() :: String.t()
def contract_version, do: @contract_version
@doc """
Executes `task`, checking the configured breakers before running.
Returns the task's return value on success, or `{:error, :breaker_open}` if a
breaker is open (or probabilistically blocks a half-open call). Exceptions are
caught, counted as failures, and reraised after samples are flushed.
See module docs for full option reference.
"""
@spec execute(client_name :: atom(), task :: (-> result), opts :: keyword()) ::
result | {:error, :breaker_open}
when result: term()
def execute(client, task, opts \\ []) when is_function(task, 0) do
names = resolve_breaker_names(client, opts)
case check_gate(client, names) do
:blocked ->
{:error, :breaker_open}
:allowed ->
run_and_report(client, task, opts)
end
end
@doc """
Enqueues a single sample for the given client.
Useful when you want to report metrics outside of `execute/3`.
## Fields
- `:router_id` — (required) router the sample belongs to
- `:metric` — metric name string
- `:value` — numeric value
- `:ok``true` if the call succeeded
- `:trace_id` — optional trace propagation string
- `:tags` — optional `%{String.t() => String.t()}`
"""
@spec report(atom(), map()) :: :ok
def report(client, sample) when is_map(sample) do
Flusher.enqueue(client, sample)
end
@doc """
Returns the current state for a single breaker, or `nil` if not yet known.
## Return shape
%{name: "breaker-name", state: "open" | "closed" | "half_open", allow_rate: float()}
"""
@spec get_state(atom(), String.t()) :: map() | nil
def get_state(client, breaker_name) do
StateServer.get_state(client, breaker_name)
end
@doc """
Returns all known breaker states as a map keyed by breaker name.
"""
@spec get_all_states(atom()) :: %{String.t() => map()}
def get_all_states(client) do
StateServer.get_all_states(client)
end
@doc """
Returns a merged diagnostics map combining stats from the SSE listener, flusher,
and in-memory state.
Useful for health checks and observability.
"""
@spec stats(atom()) :: map()
def stats(client) do
Map.merge(StateServer.stats(client), Flusher.stats(client))
end
@doc """
Returns cached breaker metadata fetched by the background sync process.
Each entry is a map with `:id`, `:name`, and `:metadata` keys.
Returns an empty list until the first metadata sync completes.
"""
@spec get_breakers_metadata(atom()) :: list(map())
def get_breakers_metadata(client) do
StateServer.get_breakers_meta(client)
end
@doc """
Returns cached router metadata fetched by the background sync process.
Each entry is a map with `:id`, `:name`, and `:metadata` keys.
Returns an empty list until the first metadata sync completes.
"""
@spec get_routers_metadata(atom()) :: list(map())
def get_routers_metadata(client) do
StateServer.get_routers_meta(client)
end
@doc """
Returns `true` if the named breaker is currently open, `false` otherwise.
A half-open breaker returns `false` — use `execute/3` for probabilistic
half-open enforcement.
"""
@spec breaker_open?(atom(), String.t()) :: boolean()
def breaker_open?(client, breaker_name) do
StateServer.check_breakers(client, [breaker_name]) == :open
end
# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------
defp resolve_breaker_names(client, opts) do
cond do
names = opts[:breakers] ->
names
selector = opts[:breaker_selector] ->
client |> StateServer.get_breakers_meta() |> selector.()
true ->
[]
end
end
defp resolve_router_id(client, opts) do
cond do
router = opts[:router] -> router
selector = opts[:router_selector] -> client |> StateServer.get_routers_meta() |> selector.()
true -> nil
end
end
defp check_gate(_client, []), do: :allowed
defp check_gate(client, names) do
case StateServer.check_breakers(client, names) do
:open -> :blocked
{:half_open, rate} -> if :rand.uniform() < rate, do: :allowed, else: :blocked
:closed -> :allowed
end
end
defp run_and_report(client, task, opts) do
start_ms = System.monotonic_time(:millisecond)
{result, ok?, maybe_exception} = execute_task(task, opts[:error_evaluator])
elapsed_ms = System.monotonic_time(:millisecond) - start_ms
metrics = build_metrics(opts, result, elapsed_ms)
if metrics != [] do
router_id = resolve_router_id(client, opts)
trace_id = opts[:trace_id]
tags = opts[:tags] || %{}
Enum.each(metrics, fn {name, value} ->
Flusher.enqueue(client, %{
router_id: router_id,
metric: name,
value: value * 1.0,
ok: ok?,
trace_id: trace_id,
tags: tags
})
end)
end
case maybe_exception do
nil -> result
{exception, stacktrace} -> reraise exception, stacktrace
end
end
defp execute_task(task, error_evaluator) do
evaluator = error_evaluator || (&default_error?/1)
try do
result = task.()
{result, not evaluator.(result), nil}
rescue
e -> {nil, false, {e, __STACKTRACE__}}
end
end
defp build_metrics(opts, result, elapsed_ms) do
base =
(opts[:metrics] || %{})
|> Enum.flat_map(fn
{name, :latency} -> [{name, elapsed_ms}]
{name, f} when is_function(f, 1) -> [{name, f.(result)}]
{name, v} when is_number(v) -> [{name, v}]
_ -> []
end)
deferred =
case opts[:deferred_metrics] do
nil -> []
f when is_function(f, 1) -> f.(result) |> Enum.to_list()
end
base ++ deferred
end
defp default_error?({:error, _}), do: true
defp default_error?(_), do: false
end