defmodule Aprsme.Telemetry.DatabaseMetrics do @moduledoc """ Collects database metrics from PostgreSQL and PgBouncer """ require Logger def collect_db_pool_metrics do pool_size = Application.get_env(:aprsme, Aprsme.Repo)[:pool_size] || 10 case Process.whereis(Aprsme.Repo) do nil -> report_pool_metrics_unavailable(pool_size) repo_pid when is_pid(repo_pid) -> collect_from_repo(repo_pid, pool_size) end rescue e -> Logger.debug("Error collecting pool metrics: #{inspect(e)}") report_pool_metrics_error() end defp collect_from_repo(repo_pid, pool_size) do children = Supervisor.which_children(repo_pid) pool_info = find_pool_child(children) collect_from_pool(pool_info, pool_size) end defp find_pool_child(children) do Enum.find(children, fn {DBConnection.ConnectionPool, _, _, _} -> true {DBConnection.Ownership, _, _, _} -> true _ -> false end) end defp collect_from_pool({_, pool_pid, _, _}, pool_size) when is_pid(pool_pid) do # Try to get pool telemetry case try_get_pool_telemetry(pool_size) do {:ok, _metrics} -> report_pool_metrics_estimated(pool_size) _ -> report_pool_metrics_idle(pool_size) end end defp collect_from_pool(_, pool_size), do: report_pool_metrics_idle(pool_size) defp try_get_pool_telemetry(pool_size) do {:ok, %{pool_size: pool_size, idle_time: 0, queue_time: 0}} catch _, _ -> {:error, :not_available} end defp report_pool_metrics_unavailable(pool_size) do emit_pool_metrics(%{ size: pool_size, idle: 0, busy: 0, available: 0, queue_length: 0, total: 0 }) end defp report_pool_metrics_idle(pool_size) do emit_pool_metrics(%{ size: pool_size, idle: pool_size, busy: 0, available: pool_size, queue_length: 0, total: pool_size }) end defp report_pool_metrics_estimated(pool_size) do # Cannot reliably introspect DBConnection pool state, so report # pool_size as total without guessing busy/idle breakdown. report_pool_metrics_idle(pool_size) end defp report_pool_metrics_error do emit_pool_metrics(%{ size: 0, idle: 0, busy: 0, available: 0, queue_length: 0, total: 0 }) end defp emit_pool_metrics(metrics) do :telemetry.execute([:aprsme, :repo, :pool], metrics, %{}) end def collect_postgres_metrics do do_collect_postgres_metrics(Application.get_env(:aprsme, :env)) end # Skip database metrics collection in the test environment — the extra # pg_stat_* queries add test-suite noise and sometimes fail in CI sandboxes. defp do_collect_postgres_metrics(:test), do: :ok defp do_collect_postgres_metrics(_env) do case Process.whereis(Aprsme.Repo) do # Repo not started yet, skip metrics collection silently. nil -> :ok _pid -> collect_database_metrics() end end defp collect_database_metrics do collect_connection_stats() collect_packets_table_stats() collect_replication_lag() rescue e -> Logger.debug("Error collecting PostgreSQL metrics: #{inspect(e)}") end defp collect_connection_stats do case Aprsme.Repo.query(""" SELECT count(*) as total, count(*) FILTER (WHERE state = 'active') as active, count(*) FILTER (WHERE state = 'idle') as idle, count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction, count(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting FROM pg_stat_activity WHERE datname = current_database() """) do {:ok, %{rows: [[total, active, idle, idle_in_tx, waiting]]}} -> :telemetry.execute( [:aprsme, :postgres, :connections], %{ total: to_number(total), active: to_number(active), idle: to_number(idle), idle_in_transaction: to_number(idle_in_tx), waiting: to_number(waiting) }, %{} ) _ -> :ok end end defp collect_packets_table_stats do case Aprsme.Repo.query(""" SELECT n_live_tup as live_tuples, n_dead_tup as dead_tuples, n_tup_ins as inserts, n_tup_upd as updates, n_tup_del as deletes FROM pg_stat_user_tables WHERE relname = 'packets' """) do {:ok, %{rows: [[live, dead, ins, upd, del]]}} -> emit_packets_table_telemetry(live, dead, ins, upd, del) _ -> :ok end end defp collect_replication_lag do case Aprsme.Repo.query(""" SELECT extract(epoch from (now() - pg_last_xact_replay_timestamp()))::int as lag_seconds WHERE pg_is_in_recovery() """) do {:ok, %{rows: [[lag]]}} when not is_nil(lag) -> :telemetry.execute([:aprsme, :postgres, :replication], %{lag_seconds: lag}, %{}) _ -> :ok end end def collect_pgbouncer_metrics do # PgBouncer metrics would require a separate connection to PgBouncer's admin interface # For now, we'll skip these as they require additional setup :ok end defp emit_packets_table_telemetry(live, dead, ins, upd, del) do :telemetry.execute( [:aprsme, :postgres, :packets_table], %{ live_tuples: to_number(live), dead_tuples: to_number(dead), total_inserts: to_number(ins), total_updates: to_number(upd), total_deletes: to_number(del) }, %{} ) end # Coerce Decimal/nil values to native Erlang numbers for :telemetry.execute defp to_number(nil), do: 0 defp to_number(%Decimal{} = d), do: Decimal.to_float(d) defp to_number(n) when is_number(n), do: n defp to_number(_), do: 0 end