Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
137 lines
4.2 KiB
Elixir
137 lines
4.2 KiB
Elixir
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
|