- Fix leader election guard test for :undefined pid - Fix cpu_sup.util() pattern matching to handle numeric return - Fix encoding_utils to always return explicit nil when needed - Remove unused finite_float? function - Fix signal handler to match :ok return from :os.set_signal/2 - Remove redundant catch-all pattern in database metrics - Fix Gridsquare pattern matching in info_live template - Remove unnecessary nil check for calculate_course result - Fix get_packet_received_at to not check for nil (always returns DateTime) - Remove redundant catch-all pattern in weather format_weather_value - Fix query builder to use from(p in Packet) instead of bare Packet atom Reduced dialyzer errors from 49 to 6. Remaining warnings are mostly false positives from template compilation and overloaded function specs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
254 lines
7.4 KiB
Elixir
254 lines
7.4 KiB
Elixir
defmodule Aprsme.Telemetry.DatabaseMetrics do
|
|
@moduledoc """
|
|
Collects database metrics from PostgreSQL and PgBouncer
|
|
"""
|
|
require Logger
|
|
|
|
def collect_db_pool_metrics do
|
|
# Get pool configuration
|
|
pool_size = Application.get_env(:aprsme, Aprsme.Repo)[:pool_size] || 10
|
|
|
|
# Try to get pool metrics using Ecto's telemetry
|
|
# First, let's check if the repo is started
|
|
case Process.whereis(Aprsme.Repo) do
|
|
nil ->
|
|
# Repo not started, report zeros
|
|
:telemetry.execute(
|
|
[:aprsme, :repo, :pool],
|
|
%{
|
|
size: pool_size,
|
|
idle: 0,
|
|
busy: 0,
|
|
available: 0,
|
|
queue_length: 0,
|
|
total: 0
|
|
},
|
|
%{}
|
|
)
|
|
|
|
repo_pid when is_pid(repo_pid) ->
|
|
# Get the pool metrics from DBConnection
|
|
# The pool supervisor is typically a child of the repo
|
|
case Supervisor.which_children(repo_pid) do
|
|
children when is_list(children) ->
|
|
# Find the DBConnection child
|
|
pool_info =
|
|
Enum.find(children, fn
|
|
{DBConnection.ConnectionPool, _, _, _} -> true
|
|
# In test mode
|
|
{DBConnection.Ownership, _, _, _} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
case pool_info do
|
|
{_, pool_pid, _, _} when is_pid(pool_pid) ->
|
|
# Try to get pool telemetry
|
|
pool_telemetry =
|
|
try do
|
|
# DBConnection exposes pool metrics via telemetry
|
|
{:ok,
|
|
%{
|
|
pool_size: pool_size,
|
|
idle_time: 0,
|
|
queue_time: 0
|
|
}}
|
|
catch
|
|
_, _ -> {:error, :not_available}
|
|
end
|
|
|
|
case pool_telemetry do
|
|
{:ok, _metrics} ->
|
|
:telemetry.execute(
|
|
[:aprsme, :repo, :pool],
|
|
%{
|
|
size: pool_size,
|
|
# Estimate
|
|
idle: max(0, pool_size - 2),
|
|
# Estimate
|
|
busy: 2,
|
|
available: max(0, pool_size - 2),
|
|
queue_length: 0,
|
|
total: pool_size
|
|
},
|
|
%{}
|
|
)
|
|
|
|
_ ->
|
|
# Use defaults
|
|
:telemetry.execute(
|
|
[:aprsme, :repo, :pool],
|
|
%{
|
|
size: pool_size,
|
|
idle: pool_size,
|
|
busy: 0,
|
|
available: pool_size,
|
|
queue_length: 0,
|
|
total: pool_size
|
|
},
|
|
%{}
|
|
)
|
|
end
|
|
|
|
_ ->
|
|
# Pool not found, use defaults
|
|
:telemetry.execute(
|
|
[:aprsme, :repo, :pool],
|
|
%{
|
|
size: pool_size,
|
|
idle: pool_size,
|
|
busy: 0,
|
|
available: pool_size,
|
|
queue_length: 0,
|
|
total: pool_size
|
|
},
|
|
%{}
|
|
)
|
|
end
|
|
end
|
|
end
|
|
rescue
|
|
e ->
|
|
Logger.debug("Error collecting pool metrics: #{inspect(e)}")
|
|
# Emit zero metrics on error
|
|
:telemetry.execute(
|
|
[:aprsme, :repo, :pool],
|
|
%{
|
|
size: 0,
|
|
idle: 0,
|
|
busy: 0,
|
|
available: 0,
|
|
queue_length: 0,
|
|
total: 0
|
|
},
|
|
%{}
|
|
)
|
|
end
|
|
|
|
def collect_postgres_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
|