lib/prom_ex/plug.ex

defmodule PromEx.Plug do
  @moduledoc """
  Use this plug in your Endpoint file to expose your metrics. The following options are supported by this plug:

  * `:prom_ex_module` - The PromEx module whose metrics will be published through this particular plug
  * `:path` - The path through which your metrics can be accessed (default is "/metrics")

  If you need to have some sort of access control around your metrics endpoint, I would suggest looking at another
  library that I maintain called `Unplug` (https://hex.pm/packages/unplug). Using `Unplug`, you can skip over this plug
  if some sort of requirement is not fulfilled. For example, if you wanted to configure the metrics endpoint to
  only be accessible if the request has an Authorization header that matches a configured environment variable you
  could do something like so using `Unplug`:

  ```elixir
  defmodule MyApp.UnplugPredicates.SecureMetricsEndpoint do
    @behaviour Unplug.Predicate

    @impl true
    def call(conn, env_var) do
      expected_secret = System.fetch_env!(env_var)
      match?([^expected_secret], Plug.Conn.get_req_header(conn, "authorization"))
    end
  end
  ```

  Which can then be used in your `endpoint.ex` file like so:

  ```elixir
  plug Unplug,
    if: {MyApp.UnplugPredicates.SecureMetricsEndpoint, "PROMETHEUS_AUTH_SECRET"},
    do: {PromEx.Plug, prom_ex_module: MyApp.PromEx}
  ```

  The reason that this functionality is not part of PromEx itself is that how you chose to configure the visibility
  of the metrics route is entirely up to the user and so it felt as though this plug would be over complicated by
  having to support application config, environment variables, etc. And given that `Unplug` exists for this purpose,
  it is the recommended tool for the job.
  """

  @behaviour Plug

  require Logger

  import Plug.Conn

  alias Plug.Conn

  @impl true
  def init(opts) do
    %{
      prom_ex_module: Keyword.fetch!(opts, :prom_ex_module),
      metrics_path: Keyword.get(opts, :path, "/metrics")
    }
  end

  @impl true
  def call(%Conn{request_path: metrics_path} = conn, %{metrics_path: metrics_path, prom_ex_module: prom_ex_module}) do
    case PromEx.get_metrics(prom_ex_module) do
      :prom_ex_down ->
        Logger.warning("Attempted to fetch metrics from #{prom_ex_module}, but the module has not been initialized")

        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(503, "Service Unavailable")
        |> halt()

      metrics ->
        PromEx.ETSCronFlusher.defer_ets_flush(prom_ex_module.__ets_cron_flusher_name__())

        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(200, metrics)
        |> halt()
    end
  end

  def call(conn, _opts) do
    conn
  end
end