Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Prom Poller

livebooks/prom_poller.livemd

Prom Poller

Deps

Mix.install([
  {:vega_lite, "~> 0.1.3"},
  {:kino, "~> 0.5.0"},
  {:tesla, "~> 1.4"},
  {:jason, "~> 1.3"},
  # {:prometheus_parser, "~> 0.1.6"},
  {:prometheus_parser,
   git: "https://github.com/Logflare/turnio-prometheus-parser", branch: "master"}
])

Input

project_ref = Kino.Input.text("Supabase Project Ref")
bearer = Kino.Input.text("Supabase Project Service Role Key")

Define Prometheus Endpoint Request

defmodule Prom do
  def do_request(project_ref, bearer) when is_binary(bearer) and is_binary(project_ref) do
    middleware = [
      {Tesla.Middleware.BaseUrl, "https://#{project_ref}.supabase.co"},
      {Tesla.Middleware.Headers, [{"authorization", "Bearer " <> bearer}, {"apiKey", bearer}]}
    ]

    {:ok, response} =
      middleware
      |> Tesla.client()
      |> Tesla.get("/admin/v1/privileged/project-metrics")

    String.split(response.body, "\n")
    |> Enum.map(&amp;PrometheusParser.parse(&amp;1))
    |> Enum.reject(fn
      {:error, _y} -> true
      {:ok, _y} -> false
    end)
    |> Enum.map(fn {_x, y} -> y end)
  end
end

Poll the Prometheus Endpoint

defmodule Poller do
  use GenServer

  def start_link(args) when is_list(args) do
    GenServer.start_link(__MODULE__, args)
  end

  def init(args) do
    defaults = %{project_ref: "", bearer: ""}
    opts = Enum.into(args, defaults)
    stack = %{data: []} |> Map.merge(opts)

    if opts.project_ref == "" || opts.bearer == "",
      do: raise(ArgumentError, message: "`project_ref` and `bearer` required")

    poll(0)

    {:ok, stack}
  end

  def get_all_metrics(pid) when is_pid(pid) do
    GenServer.call(pid, :get_all_metrics)
  end

  def get_random_metric(pid) when is_pid(pid) do
    GenServer.call(pid, :get_rand_metric)
  end

  def get_metric(pid, label) when is_pid(pid) and is_binary(label) do
    GenServer.call(pid, {:get_metric, label})
  end

  def get_metric(pid, label, {"", ""}) when is_pid(pid) and is_binary(label) do
    GenServer.call(pid, {:get_metric, label})
  end

  def get_metric(pid, label, tag) when is_pid(pid) and is_binary(label) and is_tuple(tag) do
    GenServer.call(pid, {:get_metric, label, tag})
  end

  def handle_call(:get_all_metrics, _from, %{data: data} = stack) do
    {:reply, {:ok, data}, stack}
  end

  def handle_call({:get_metric, label}, _from, %{data: data} = stack) do
    metric =
      data
      |> Enum.filter(&amp;(&amp;1.line_type == "ENTRY"))
      |> Enum.find(%{}, fn %PrometheusParser.Line{label: l} ->
        l == label
      end)

    response =
      case metric do
        %PrometheusParser.Line{} -> {:ok, metric}
        %{} -> {:error, :metric_not_found}
      end

    {:reply, response, stack}
  end

  def handle_call({:get_metric, label, tag}, _from, %{data: data} = stack) do
    metric =
      Enum.find(data, %{}, fn %PrometheusParser.Line{label: l, pairs: tags} ->
        label_match = l == label
        tag_match = Enum.any?(tags, fn x -> x == tag end)

        if label_match and tag_match, do: true, else: false
      end)

    response =
      case metric do
        %PrometheusParser.Line{} -> {:ok, metric}
        %{} -> {:error, :metric_not_found}
      end

    {:reply, response, stack}
  end

  def handle_call(:get_rand_metric, _from, %{data: data} = stack) do
    metric = Enum.random(data)

    {:reply, {:ok, metric}, stack}
  end

  def handle_info(:poll, %{project_ref: ref, bearer: bearer} = stack) do
    metrics = Prom.do_request(ref, bearer)
    stack = stack |> Map.put(:data, metrics)

    poll()

    {:noreply, stack}
  end

  defp poll(interval \\ 5_000) do
    Process.send_after(self(), :poll, interval)
  end
end

{:ok, pid} =
  Poller.start_link(
    project_ref: Kino.Input.read(project_ref),
    bearer: Kino.Input.read(bearer)
  )

Browse Metrics

filter = Kino.Input.text("Filter Metrics by Label")
{:ok, data} = Poller.get_all_metrics(pid)

data =
  case Kino.Input.read(filter) do
    "" -> data
    filter -> Enum.filter(data, &amp;String.contains?(inspect(&amp;1.label), filter))
  end

Kino.DataTable.new(data)

Graph a Metric

Interesting Metrics

  • node_scrape_collector_duration_seconds
  • replication_realtime_lag_bytes
  • node_cpu_seconds_total
    • mode:user
  • go_memstats_frees_total
  • gotrue_up
  • promhttp_metric_handler_requests_in_flight
  • auth_users_user_count
  • supabase_usage_metrics_user_queries_total
metric_label = Kino.Input.text("Metric Label")
tag_key = Kino.Input.text("Metric Tag Key")
tag_value = Kino.Input.text("Metric Tag Value")
label = Kino.Input.read(metric_label)
tag_key = Kino.Input.read(tag_key)
tag_value = Kino.Input.read(tag_value)

Poller.get_metric(pid, label, {tag_key, tag_value})
alias VegaLite, as: Vl

label = Kino.Input.read(metric_label)

widget =
  Vl.new(width: 750, height: 400)
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "x",
    time_unit: :monthdatehoursminutesseconds,
    type: :temporal,
    title: "Time"
  )
  |> Vl.encode_field(:y, "y", type: :quantitative, title: label <> " #{tag_key}:#{tag_value}")
  |> Kino.VegaLite.new()
  |> Kino.render()

Kino.animate(1_000, 0, fn i ->
  {:ok, metric} = Poller.get_metric(pid, label)

  now = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

  point = %{x: now, y: metric.value}

  chart = Kino.VegaLite.push(widget, point, window: 1_000)

  {:cont, chart, i + 1}
end)