prop/lib/microwaveprop_web/metrics_plug.ex
Graham McIntire 6c5d066121
fix(metrics): defer ETS cron flusher on /metrics scrape
Without the deferral the cron-driven ETS flush GenServer fires every
7.5s alongside Prometheus scrapes, calls PromEx.get_metrics in a Task,
and dies with `Task.await ... time out` (hardcoded 10s) under contention.
Mirrors upstream PromEx.Plug, which calls defer_ets_flush after a
successful render specifically to suppress the cron while scraping is
healthy.
2026-05-02 10:59:24 -05:00

62 lines
1.6 KiB
Elixir

defmodule MicrowavepropWeb.MetricsPlug do
@moduledoc """
Serves PromEx metrics at `/metrics`. Optionally gated by a bearer
token via `config :microwaveprop, :prometheus_auth_token`, sourced
from the `PROMETHEUS_AUTH_TOKEN` env var in `runtime.exs`.
"""
@behaviour Plug
import Plug.Conn
@impl true
def init(opts), do: opts
@impl true
def call(conn, _opts) do
case authorized?(conn) do
:ok ->
body = PromEx.get_metrics(Microwaveprop.PromEx)
# Defer the ETS cron flusher; otherwise it runs every
# `ets_flush_interval` (7.5s) concurrently with scrapes and its
# `Task.await` (hardcoded 10s) trips under contention, killing
# the GenServer. Upstream `PromEx.Plug` does the same.
PromEx.ETSCronFlusher.defer_ets_flush(Microwaveprop.PromEx.__ets_cron_flusher_name__())
conn
|> put_resp_content_type("text/plain; version=0.0.4")
|> send_resp(200, body)
|> halt()
:denied ->
conn
|> put_resp_header("www-authenticate", ~s(Bearer realm="metrics"))
|> send_resp(401, "unauthorized")
|> halt()
end
end
defp authorized?(conn) do
case Application.get_env(:microwaveprop, :prometheus_auth_token) do
token when is_binary(token) and token != "" ->
check_token(conn, token)
_ ->
:ok
end
end
defp check_token(conn, expected) do
conn
|> get_req_header("authorization")
|> List.first()
|> case do
"Bearer " <> token ->
if Plug.Crypto.secure_compare(token, expected), do: :ok, else: :denied
_ ->
:denied
end
end
end