towerops/lib/towerops/monitoring.ex
Graham McIntire 3068f944fb
Add TimescaleDB aggregate tests, implement on-demand calculation
- Updated Monitoring module to calculate hourly/daily stats on-demand from monitoring_checks table
- Removed TimescaleDB continuous aggregate dependency (disabled due to licensing)
- Enabled all previously skipped TimescaleDB tests with proper test data
- Fixed UUID encoding for SQL queries using Ecto.UUID.dump!
- Fixed Decimal handling in uptime percentage calculation
- Added test for get_uptime_percentage with nil checks
- Monitoring module now at 100% coverage
2026-01-13 08:09:12 -06:00

125 lines
3.7 KiB
Elixir

defmodule Towerops.Monitoring do
@moduledoc """
The Monitoring context.
"""
import Ecto.Query
alias Ecto.Adapters.SQL
alias Towerops.Monitoring.Check
alias Towerops.Repo
@doc """
Creates a monitoring check.
"""
def create_check(attrs) do
%Check{}
|> Check.changeset(attrs)
|> Repo.insert()
end
@doc """
Returns the list of checks for an equipment.
"""
def list_equipment_checks(equipment_id, limit \\ 100) do
Repo.all(from(c in Check, where: c.equipment_id == ^equipment_id, order_by: [desc: c.checked_at], limit: ^limit))
end
@doc """
Returns the latest check for an equipment.
"""
def get_latest_check(equipment_id) do
Repo.one(from(c in Check, where: c.equipment_id == ^equipment_id, order_by: [desc: c.checked_at], limit: 1))
end
@doc """
Deletes old monitoring checks older than the given date.
"""
def delete_old_checks(older_than_date) do
Repo.delete_all(from(c in Check, where: c.checked_at < ^older_than_date))
end
@doc """
Gets hourly statistics for equipment over a time range.
Calculates aggregates on-demand from monitoring_checks.
"""
def get_hourly_stats(equipment_id, start_time, end_time) do
query = """
SELECT
date_trunc('hour', checked_at) as bucket,
COUNT(*) as total_checks,
COUNT(*) FILTER (WHERE status = 'success') as successful_checks,
COUNT(*) FILTER (WHERE status IN ('failure', 'timeout')) as failed_checks,
ROUND(AVG(response_time_ms)::numeric, 2) as avg_response_time_ms,
MIN(response_time_ms) as min_response_time_ms,
MAX(response_time_ms) as max_response_time_ms,
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / NULLIF(COUNT(*), 0), 2) as uptime_percentage
FROM monitoring_checks
WHERE equipment_id = $1::uuid
AND checked_at >= $2
AND checked_at <= $3
GROUP BY bucket
ORDER BY bucket ASC
"""
SQL.query!(
Repo,
query,
[Ecto.UUID.dump!(equipment_id), start_time, end_time]
)
end
@doc """
Gets daily statistics for equipment over a time range.
Calculates aggregates on-demand from monitoring_checks.
"""
def get_daily_stats(equipment_id, start_time, end_time) do
query = """
SELECT
date_trunc('day', checked_at) as bucket,
COUNT(*) as total_checks,
COUNT(*) FILTER (WHERE status = 'success') as successful_checks,
COUNT(*) FILTER (WHERE status IN ('failure', 'timeout')) as failed_checks,
ROUND(AVG(response_time_ms)::numeric, 2) as avg_response_time_ms,
MIN(response_time_ms) as min_response_time_ms,
MAX(response_time_ms) as max_response_time_ms,
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / NULLIF(COUNT(*), 0), 2) as uptime_percentage
FROM monitoring_checks
WHERE equipment_id = $1::uuid
AND checked_at >= $2
AND checked_at <= $3
GROUP BY bucket
ORDER BY bucket ASC
"""
SQL.query!(
Repo,
query,
[Ecto.UUID.dump!(equipment_id), start_time, end_time]
)
end
@doc """
Gets uptime percentage for equipment over the last N days.
Calculates on-demand from monitoring_checks.
"""
def get_uptime_percentage(equipment_id, days_ago \\ 30) do
start_time = DateTime.add(DateTime.utc_now(), -days_ago * 24 * 60 * 60, :second)
query = """
SELECT
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / NULLIF(COUNT(*), 0), 2) as uptime_percentage
FROM monitoring_checks
WHERE equipment_id = $1::uuid
AND checked_at >= $2
"""
case SQL.query!(Repo, query, [Ecto.UUID.dump!(equipment_id), start_time]) do
%{rows: [[%Decimal{} = uptime]]} ->
uptime |> Decimal.to_float() |> Float.round(2)
_ ->
nil
end
end
end