lib/safe_inspect.ex

defmodule SafeInspect do
  @moduledoc """
  Use this instead of normal `inspect`. Redacts sensitive data that we don't want in logs.
  """

  @redacted_keys :safe_inspect
                 |> Application.compile_env!(:redacted_keys)
                 |> Enum.map(fn k -> k |> to_string() |> String.downcase() end)

  @all_redacted_keys @redacted_keys ++
                       Enum.map(@redacted_keys, &String.replace(&1, "_", "-")) ++
                       Enum.map(@redacted_keys, &String.replace(&1, "_", ""))

  @spec inspect!(any(), keyword()) :: String.t()
  def inspect!(value, opts \\ []) do
    inspect(clean(value), opts)
  end

  defp clean(struct) when is_struct(struct) do
    cleaned =
      struct
      |> Map.from_struct()
      |> clean()
      |> Map.put(:__struct__, struct.__struct__)

    if struct.__struct__ == Ecto.Changeset do
      Map.put(cleaned, :errors, struct.errors)
    else
      cleaned
    end
  end

  defp clean(map) when is_map(map) do
    map
    |> Enum.map(fn
      {k, nil} -> {k, nil}
      {k, v} -> if k_to_string(k) in @all_redacted_keys, do: {k, :redacted}, else: {k, clean(v)}
    end)
    |> Enum.into(%{})
  end

  defp clean({k, nil}) when is_atom(k) or is_binary(k), do: {k, nil}

  defp clean({k, v}) when is_atom(k) or is_binary(k) do
    if k_to_string(k) in @all_redacted_keys do
      {k, :redacted}
    else
      {k, clean(v)}
    end
  end

  defp clean(tuple) when is_tuple(tuple) do
    tuple
    |> Tuple.to_list()
    |> clean()
    |> List.to_tuple()
  end

  defp clean(list) when is_list(list) do
    Enum.map(list, &clean/1)
  end

  defp clean(other) do
    other
  end

  defp k_to_string(k), do: k |> to_string() |> String.downcase()
end