Simplify complex functions: - aprs_symbol.ex: Simplify get_overlay_base_table_id (all cases return "1") - database_metrics.ex: Extract metric reporting helpers to reduce nesting Co-Authored-By: Graham <noreply@anthropic.com>
251 lines
6.7 KiB
Elixir
251 lines
6.7 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
|
|
|
|
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
|
|
case Supervisor.which_children(repo_pid) do
|
|
children when is_list(children) ->
|
|
pool_info = find_pool_child(children)
|
|
collect_from_pool(pool_info, pool_size)
|
|
|
|
_ ->
|
|
report_pool_metrics_idle(pool_size)
|
|
end
|
|
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
|
|
emit_pool_metrics(%{
|
|
size: pool_size,
|
|
idle: max(0, pool_size - 2),
|
|
busy: 2,
|
|
available: max(0, pool_size - 2),
|
|
queue_length: 0,
|
|
total: 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
|
|
# Skip database metrics collection in test environment
|
|
if Application.get_env(:aprsme, :env) == :test do
|
|
:ok
|
|
else
|
|
do_collect_postgres_metrics()
|
|
end
|
|
end
|
|
|
|
defp do_collect_postgres_metrics do
|
|
# Check if Repo is started before collecting metrics
|
|
case Process.whereis(Aprsme.Repo) do
|
|
nil ->
|
|
# Repo not started yet, skip metrics collection silently
|
|
:ok
|
|
|
|
_pid ->
|
|
collect_database_metrics()
|
|
end
|
|
end
|
|
|
|
defp collect_database_metrics do
|
|
# Database size
|
|
case Aprsme.Repo.query("SELECT pg_database_size(current_database()) as size") do
|
|
{:ok, %{rows: [[size]]}} ->
|
|
:telemetry.execute(
|
|
[:aprsme, :postgres, :database],
|
|
%{size_bytes: size},
|
|
%{}
|
|
)
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
# Connection stats
|
|
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: total || 0,
|
|
active: active || 0,
|
|
idle: idle || 0,
|
|
idle_in_transaction: idle_in_tx || 0,
|
|
waiting: waiting || 0
|
|
},
|
|
%{}
|
|
)
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
# Table stats for packets table
|
|
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,
|
|
pg_table_size(c.oid) as table_size,
|
|
pg_indexes_size(c.oid) as indexes_size
|
|
FROM pg_stat_user_tables s
|
|
JOIN pg_class c ON c.relname = s.relname
|
|
WHERE s.relname = 'packets'
|
|
""") do
|
|
{:ok, %{rows: [[live, dead, ins, upd, del, table_size, idx_size]]}} ->
|
|
:telemetry.execute(
|
|
[:aprsme, :postgres, :packets_table],
|
|
%{
|
|
live_tuples: live || 0,
|
|
dead_tuples: dead || 0,
|
|
total_inserts: ins || 0,
|
|
total_updates: upd || 0,
|
|
total_deletes: del || 0,
|
|
table_size_bytes: table_size || 0,
|
|
indexes_size_bytes: idx_size || 0
|
|
},
|
|
%{}
|
|
)
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
# Query performance stats
|
|
case Aprsme.Repo.query("""
|
|
SELECT
|
|
sum(calls) as total_calls,
|
|
sum(total_exec_time) as total_time,
|
|
avg(mean_exec_time) as avg_time,
|
|
max(max_exec_time) as max_time
|
|
FROM pg_stat_statements
|
|
WHERE query NOT LIKE '%pg_stat%'
|
|
""") do
|
|
{:ok, %{rows: [[calls, total_time, avg_time, max_time]]}} ->
|
|
:telemetry.execute(
|
|
[:aprsme, :postgres, :query_stats],
|
|
%{
|
|
total_calls: trunc(calls || 0),
|
|
total_time_ms: total_time || 0,
|
|
avg_time_ms: avg_time || 0,
|
|
max_time_ms: max_time || 0
|
|
},
|
|
%{}
|
|
)
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
# Replication lag (if applicable)
|
|
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
|
|
rescue
|
|
e ->
|
|
Logger.debug("Error collecting PostgreSQL metrics: #{inspect(e)}")
|
|
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
|
|
end
|