Packages

Feature flag library with runtime config for storage backends and caching.

Current section

Files

Jump to
bandera lib bandera audit.ex
Raw

lib/bandera/audit.ex

defmodule Bandera.Audit do
@moduledoc """
Opt-in audit hook. Turns Bandera's existing write telemetry
(`[:bandera, :enable|:disable|:clear]` span `:stop` events) into structured
`Bandera.Audit.Event` records and forwards them to a callback you provide.
Bandera.Audit.attach(:my_audit, fn event ->
MyApp.AuditLog.insert!(event)
end)
An exception raised by your callback is caught and logged rather than propagated,
so a transient failure can't make `:telemetry` silently detach the handler and
drop all later audit events.
"""
require Logger
defmodule Event do
@moduledoc "A single flag-change audit record."
@enforce_keys [:action, :flag_name, :at]
defstruct [:action, :flag_name, :options, :result, :actor, :at]
@type t :: %__MODULE__{
action: :enable | :disable | :clear,
flag_name: atom,
options: keyword,
result: term,
actor: term,
at: DateTime.t()
}
end
@actions [:enable, :disable, :clear]
@doc false
@spec from_telemetry([atom], map) :: Event.t() | :ignore
def from_telemetry([:bandera, action, :stop], metadata) when action in @actions do
options = Map.get(metadata, :options, [])
%Event{
action: action,
flag_name: Map.get(metadata, :flag_name),
options: options,
result: Map.get(metadata, :result),
actor: Keyword.get(options, :by),
at: DateTime.utc_now()
}
end
def from_telemetry(_event_name, _metadata), do: :ignore
@stop_events for action <- @actions, do: [:bandera, action, :stop]
@doc """
Attach a handler that invokes `callback` with a `Bandera.Audit.Event` for every
`enable`/`disable`/`clear`. `handler_id` is any term unique to this handler.
"""
@spec attach(term, (Event.t() -> any)) :: :ok | {:error, :already_exists}
def attach(handler_id, callback) when is_function(callback, 1) do
:telemetry.attach_many(
handler_id,
@stop_events,
fn event_name, _measurements, metadata, cb ->
case from_telemetry(event_name, metadata) do
:ignore ->
:ok
event ->
try do
cb.(event)
rescue
error ->
Logger.error(
"[Bandera.Audit] handler #{inspect(handler_id)} raised: " <>
Exception.message(error)
)
end
:ok
end
end,
callback
)
end
@doc "Detach a handler attached with `attach/2`."
@spec detach(term) :: :ok | {:error, :not_found}
def detach(handler_id), do: :telemetry.detach(handler_id)
end