defmodule Settings.Instrumentation do @moduledoc """ Capture and guard the approvals section on every write. """ require Logger @baseline_table :fnord_approvals_baseline @trace_table :fnord_approvals_traces @spec init_baseline(map) :: :ok def init_baseline(data) do initial = Map.get(data, "approvals", %{}) Application.put_env(:fnord, :initial_global_approvals_snapshot, initial) # Ensure baseline table exists, handling potential race conditions if :ets.whereis(@baseline_table) == :undefined do try do :ets.new(@baseline_table, [:named_table, :public, read_concurrency: true]) rescue ArgumentError -> :ok end end # Clear any existing baseline entries or new table is empty :ets.delete_all_objects(@baseline_table) :ets.insert(@baseline_table, {:baseline, initial}) # Ensure trace table exists, handling potential race conditions if :ets.whereis(@trace_table) == :undefined do try do :ets.new(@trace_table, [:named_table, :public, :bag, read_concurrency: true]) rescue ArgumentError -> :ok end end :ok end @spec record_trace(atom, any, map, map) :: :ok def record_trace(op, key, before, after_value) do # Ensure ETS table exists before inserting the trace case :ets.whereis(@trace_table) do :undefined -> init_baseline(before) _ -> :ok end ts = System.system_time(:millisecond) # compute counts for before and after, ensuring after_counts has zeros for cleared keys before_counts = kind_counts(before) raw_after = kind_counts(after_value) after_counts = before_counts |> Enum.map(fn {k, _v} -> {k, Map.get(raw_after, k, 0)} end) |> Enum.into(%{}) # build entry map for insertion entry = %{ ts: ts, pid: self(), op: op, key: key, before_counts: before_counts, after_counts: after_counts, stack: trimmed_stack(20) } # Insert trace entry; ETS bag allows multiple entries per key :ets.insert(@trace_table, {:trace, ts, entry}) :ok end @spec guard_or_heal(map, map, map) :: map def guard_or_heal(before, after_value, %{op: op, key: key}) do bc = kind_counts(before) ac = kind_counts(after_value) baseline = case :ets.lookup(@baseline_table, :baseline) do [{:baseline, b}] -> b _ -> %{} end if approvals_cleared?(before, after_value) and Enum.any?(bc, fn {_k, v} -> v > 0 end) do if debug?() do dump_debug(op, key, baseline, bc, ac) after_value else healed = merge_approvals(baseline, Map.get(after_value, "approvals", %{})) :ets.insert(@baseline_table, {:baseline, healed}) Map.put(after_value, "approvals", healed) end else after_value end end @spec recent_traces(pos_integer) :: [map] def recent_traces(n) do :ets.lookup(@trace_table, :trace) |> Enum.map(fn {:trace, _ts, e} -> e end) |> Enum.sort_by(& &1.ts, :desc) |> Enum.take(n) end defp kind_counts(data) do case Map.get(data, "approvals") do ap when is_map(ap) -> ap |> Enum.into(%{}, fn {k, v} -> {k, if(is_list(v), do: length(v), else: 0)} end) _ -> %{} end end defp approvals_cleared?(_before, after_data) do Map.get(after_data, "approvals", %{}) |> Enum.all?(fn {_k, v} -> v == [] end) end defp merge_approvals(baseline, curr) do Map.merge(baseline, curr, fn _k, o, n -> (o ++ n) |> Enum.uniq() |> Enum.sort() end) end @doc """ Returns true if the FNORD_DEBUG_SETTINGS environment variable is set to "1", "true", or "yes" (case-insensitive). """ def debug?() do case System.get_env("FNORD_DEBUG_SETTINGS") |> to_string() |> String.downcase() do v when v in ["1", "true", "yes"] -> true _ -> false end end defp dump_debug(op, key, baseline, bc, ac) do # Log warning about cleared approvals in debug mode Logger.warning( "Settings FNORD_DEBUG_SETTINGS: approvals would have been cleared by #{op} on #{inspect(key)}.\n baseline=#{inspect(baseline)} before=#{inspect(bc)} after=#{inspect(ac)}" ) {:current_stacktrace, st} = Process.info(self(), :current_stacktrace) Logger.error("stack:\n#{Exception.format_stacktrace(st)}") Logger.error("traces:\n#{inspect(recent_traces(10), limit: :infinity)}") end defp trimmed_stack(limit) do {:current_stacktrace, st} = Process.info(self(), :current_stacktrace) st |> Enum.drop(2) |> Enum.take(limit) end end