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 :telemetry.execute([:aprsme, :repo, :pool], %{size: pool_size}, %{}) 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 ( SELECT SUM(n_live_tup) AS n_live_tup, SUM(n_dead_tup) AS n_dead_tup, SUM(n_tup_ins) AS n_tup_ins, SUM(n_tup_upd) AS n_tup_upd, SUM(n_tup_del) AS n_tup_del FROM pg_stat_user_tables WHERE relid = 'packets'::regclass OR relid IN ( SELECT inhrelid FROM pg_inherits WHERE inhparent = 'packets'::regclass ) ) packet_stats """) 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