Skip to main content

lib/parapet.ex

defmodule Parapet do
  @moduledoc """
  Parapet provides telemetry foundations and safety rails for Phoenix SaaS teams.

  This top-level API provides boundary constraints ensuring that metric collection
  bugs never crash the host process and high cardinality labels are explicitly rejected.

  > #### Stable {: .info}
  >
  > This module is **stable** as of v1.0.0. Its public API will not change without a
  > major-version bump and a full deprecation cycle. See
  > [Stability & Deprecation Policy](stability.html) for details.
  """
  require Logger

  alias Parapet.Internal.SafeHandler

  @doc since: "1.0.0"
  @doc """
  Attaches an exception-safe telemetry handler or activates ecosystem integration adapters.

  When a list with `:adapters` is provided, it iterates the adapter atoms, resolves the
  corresponding `Parapet.Integrations.*` module, and invokes `setup/0` on each loaded adapter
  module. All built-in integration adapters implement the `Parapet.Integration` behaviour, so
  `setup/0` is uniform across every adapter.

  When a map is provided, it delegates to Parapet.Internal.SafeHandler to ensure errors
  in the callback do not propagate back to the execution of the instrumented application code.
  """
  def attach(opts) when is_list(opts) do
    adapters = Keyword.get(opts, :adapters, [])

    Enum.each(adapters, fn adapter ->
      module_name =
        adapter
        |> to_string()
        |> Macro.camelize()

      module = Module.concat(Parapet.Integrations, module_name)

      if Code.ensure_loaded?(module) do
        apply(module, :setup, [])
      end
    end)

    {:ok, adapters}
  end

  def attach(
        %{
          handler_id: handler_id,
          event_name: event_name,
          handler_module: handler_module,
          function_name: function_name
        } = args
      ) do
    config = Map.get(args, :config, %{})

    unless Code.ensure_loaded?(:telemetry) do
      Logger.warning("Telemetry is missing, Parapet handler #{handler_id} cannot be attached.")
    end

    case SafeHandler.attach(
           handler_id,
           event_name,
           handler_module,
           function_name,
           config
         ) do
      :ok ->
        Logger.debug("Parapet attached telemetry handler #{handler_id}")
        {:ok, [handler_id]}

      {:error, _} = error ->
        error
    end
  end
end