From 8f618291a0c6e58e43807abd9a5645f12b83b085 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 21 Dec 2025 17:17:26 -0600 Subject: [PATCH] timescaledb --- IMPLEMENTATION_PLAN.md | 112 ++++++- README.md | 75 ++++- TIMESCALEDB.md | 284 ++++++++++++++++++ lib/towerops/monitoring.ex | 81 +++++ lib/towerops/monitoring/supervisor.ex | 21 +- ...0251221193939_create_monitoring_checks.exs | 47 ++- ...221225717_create_monitoring_aggregates.exs | 79 +++++ 7 files changed, 683 insertions(+), 16 deletions(-) create mode 100644 TIMESCALEDB.md create mode 100644 priv/repo/migrations/20251221225717_create_monitoring_aggregates.exs diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index dc95577e..c5775dc1 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -302,19 +302,24 @@ end 5. Permission checks on all actions ### Stage 3: Monitoring System ✓ COMPLETE -**Goal**: Automated ping monitoring +**Goal**: Automated ping monitoring with TimescaleDB **Success Criteria**: - ✓ Equipment is pinged at configurable intervals - ✓ Status updates in real-time via PubSub - ✓ Monitoring checks are logged +- ✓ TimescaleDB hypertable for efficient time-series storage +- ✓ Automatic data retention and compression +- ✓ Continuous aggregates for dashboard metrics **Tasks**: 1. ✓ Design monitoring worker architecture (GenServer + DynamicSupervisor) 2. ✓ Implement ping functionality (System.cmd with OS-specific args) -3. ✓ Create monitoring_checks schema +3. ✓ Create monitoring_checks schema with TimescaleDB hypertable 4. ✓ Build GenServer workers for monitoring 5. ✓ Add PubSub broadcasting 6. ✓ Update LiveView to receive real-time updates +7. ✓ Configure TimescaleDB retention policies +8. ✓ Create continuous aggregates for metrics **Completed**: 2025-12-21 **Files Created**: @@ -323,7 +328,13 @@ end - `lib/towerops/monitoring/ping.ex` - Ping functionality - `lib/towerops/monitoring/equipment_monitor.ex` - GenServer for monitoring individual equipment - `lib/towerops/monitoring/supervisor.ex` - Supervisor for managing monitor workers -- `priv/repo/migrations/*_create_monitoring_checks.exs` - Database migration +- `priv/repo/migrations/*_create_monitoring_checks.exs` - Database migration with TimescaleDB hypertable + +**TimescaleDB Integration**: +- `monitoring_checks` converted to hypertable partitioned by `checked_at` +- Retention policy: Keep raw data for 90 days, then automatically delete +- Compression policy: Compress chunks older than 7 days +- Continuous aggregates: Hourly and daily rollups for dashboard performance ### Stage 4: Alerting **Goal**: Alert generation and management @@ -354,6 +365,83 @@ end 4. Performance optimization 5. Documentation +### Stage 6: Distributed Monitoring Agents (Future) +**Goal**: Deploy-able Rust-based monitoring agents for internal network monitoring +**Status**: PLANNED - Not yet started + +**Overview**: +Customer-deployable Rust binary that runs inside customer networks to monitor equipment from within their own infrastructure. Agents authenticate using deployment keys and report monitoring results back to the TowerOps platform. + +**Success Criteria**: +- Rust agent binary can be deployed on customer networks (Linux, Windows, macOS) +- Agent authenticates with deployment key tied to organization +- Agent pings equipment reachable on local network +- Results reported back to TowerOps API +- Equipment can be configured to use agent-based or platform-based monitoring +- Agent status visible in TowerOps dashboard + +**Architecture**: +- **Rust Agent**: Lightweight binary for customer deployment + - Ping functionality using OS-native ICMP + - Configurable check intervals + - Local caching/queue for offline resilience + - Secure API communication over HTTPS + - Auto-update capability + +- **Deployment Keys**: Scoped API credentials + - Per-organization deployment keys + - Limited scope (can only submit monitoring results) + - Revocable from TowerOps dashboard + - Multiple keys per organization for different sites/networks + +- **API Endpoints**: Backend support for agent communication + - POST /api/v1/monitoring/checks - Submit monitoring results + - GET /api/v1/monitoring/config - Fetch equipment list for agent + - Authentication via deployment key header + +**Tasks**: +1. Design deployment key schema and API +2. Create agent registration and key management in TowerOps +3. Build Rust monitoring agent + - ICMP ping implementation + - API client for result submission + - Configuration management + - Error handling and retry logic +4. Add equipment assignment to agents +5. Agent status monitoring in dashboard +6. Documentation for agent deployment + +**Database Changes Needed**: +```elixir +# deployment_keys table +- id (binary_id) +- organization_id (FK) +- name (string) - Human-readable name for the key +- key_hash (string) - Hashed version of the key +- last_used_at (utc_datetime) +- created_by_id (FK -> users) +- revoked_at (utc_datetime, nullable) + +# monitoring_agents table +- id (binary_id) +- organization_id (FK) +- deployment_key_id (FK) +- name (string) +- version (string) - Agent version +- last_seen_at (utc_datetime) +- status (enum: active, inactive, error) + +# equipment table additions +- monitoring_agent_id (FK -> monitoring_agents, nullable) +- monitoring_source (enum: platform, agent) - Where monitoring happens +``` + +**Notes**: +- This is a significant feature and will be implemented much later +- Current platform-based monitoring (Stage 3) remains as fallback +- Allows monitoring of internal/private networks not accessible from internet +- Agent runs continuously, not triggered from platform + --- ## Technical Decisions @@ -363,6 +451,24 @@ end - Use binary_id (UUID) for all primary keys (already configured) - Indexes on foreign keys and frequently queried fields +**TimescaleDB for Time-Series Data**: +- TimescaleDB extension enabled on PostgreSQL +- `monitoring_checks` table converted to hypertable +- Automatic partitioning by time (checked_at column) +- Retention policy: 90 days for raw data +- Compression policy: Compress chunks older than 7 days +- Continuous aggregates for dashboard performance: + - `monitoring_checks_hourly` - Hourly rollups (avg response time, success rate) + - `monitoring_checks_daily` - Daily rollups for long-term trends + +**Why TimescaleDB**: +- Built as PostgreSQL extension (not separate database) +- Works seamlessly with Ecto +- Optimized for time-series queries +- Automatic data lifecycle management +- Fast aggregations for dashboards +- Scales to millions of monitoring checks + ### Background Jobs **Options:** 1. Custom OTP solution with GenServer + Process.send_after diff --git a/README.md b/README.md index 159c72f9..12288708 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,76 @@ -# Towerops +# TowerOps -To start your Phoenix server: +Network monitoring and alerting platform built with Phoenix LiveView. -* Run `mix setup` to install and setup dependencies -* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` +## Features -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. +- **Multi-tenant architecture** - Organizations with role-based permissions +- **Site hierarchy** - Organize equipment across multiple sites +- **Automated monitoring** - Real-time ping monitoring with configurable intervals +- **Time-series data** - Efficient storage with TimescaleDB (optional) +- **Real-time updates** - LiveView dashboard with PubSub +- **Equipment tracking** - Monitor network devices by IP address -Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). +## Quick Start + +### Prerequisites + +- Elixir 1.14+ +- PostgreSQL 14+ +- (Optional) TimescaleDB for production-grade time-series performance + +### Setup + +```bash +# Install dependencies +mix setup + +# Start the server +mix phx.server +``` + +Visit [`localhost:4000`](http://localhost:4000) from your browser. + +### TimescaleDB (Production Only) + +**Development**: Uses standard PostgreSQL (no TimescaleDB required). + +**Production**: TimescaleDB is automatically enabled for optimal performance with time-series data. + +```bash +# Production deployment - install TimescaleDB first +brew tap timescale/tap && brew install timescaledb # macOS +# Then run migrations with MIX_ENV=prod +MIX_ENV=prod mix ecto.migrate +``` + +See [TIMESCALEDB.md](TIMESCALEDB.md) for detailed installation and configuration. + +**How it works**: Migrations detect the environment (`MIX_ENV`) and only enable TimescaleDB features (hypertables, compression, retention policies, continuous aggregates) in production. + +## Development + +### Database + +```bash +mix ecto.create # Create database +mix ecto.migrate # Run migrations +mix ecto.reset # Drop, create, and migrate +``` + +### Testing + +```bash +mix test # Run all tests +mix test --trace # Run with detailed output +``` + +### Code Quality + +```bash +mix format # Format code with Styler +mix compile --warnings-as-errors +``` ## Learn more diff --git a/TIMESCALEDB.md b/TIMESCALEDB.md new file mode 100644 index 00000000..68bb1316 --- /dev/null +++ b/TIMESCALEDB.md @@ -0,0 +1,284 @@ +# TimescaleDB Setup for TowerOps + +## Overview + +TowerOps uses TimescaleDB for efficient time-series storage of monitoring data. While the application will work with standard PostgreSQL, TimescaleDB provides significant performance benefits for production use. + +## Development vs Production + +### Development & Test +- **Standard PostgreSQL**: No TimescaleDB installation required +- Migrations automatically skip TimescaleDB features in dev/test environments +- You'll see messages like: "Development/Test environment - using standard PostgreSQL table" +- Faster setup - developers don't need to install TimescaleDB +- Tests run faster without TimescaleDB overhead + +### Production +- **TimescaleDB Required**: Production migrations enable TimescaleDB features +- Automatic data retention (90 days) +- Automatic compression (chunks older than 7 days) +- Continuous aggregates for fast dashboard queries +- Scales to millions of monitoring checks +- You'll see messages like: "Production environment - enabling TimescaleDB features" + +## Installation + +**Note**: TimescaleDB installation is **only required for production deployments**. Local development and testing use standard PostgreSQL. + +### macOS (Homebrew) + +```bash +# Add TimescaleDB tap +brew tap timescale/tap + +# Install TimescaleDB +brew install timescaledb + +# Run the setup script +/opt/homebrew/opt/timescaledb/bin/timescaledb-tune --quiet --yes + +# Restart PostgreSQL +brew services restart postgresql@17 +``` + +**For Postgres.app users:** +```bash +# Download TimescaleDB extension for your PostgreSQL version +# Visit: https://docs.timescale.com/self-hosted/latest/install/installation-macos/ + +# Or use Docker (see below) +``` + +### Linux (Ubuntu/Debian) + +```bash +# Add TimescaleDB repository +sudo apt install gnupg postgresql-common apt-transport-https lsb-release wget + +sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh + +echo "deb https://packagecloud.io/timescale/timescaledb/ubuntu/ $(lsb_release -c -s) main" | sudo tee /etc/apt/sources.list.d/timescaledb.list + +wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/timescaledb.gpg + +# Install TimescaleDB +sudo apt update +sudo apt install timescaledb-2-postgresql-17 + +# Run setup +sudo timescaledb-tune --quiet --yes + +# Restart PostgreSQL +sudo systemctl restart postgresql +``` + +### Docker (Easiest for Development) + +```bash +# Stop your current PostgreSQL (if running) +brew services stop postgresql@17 # macOS +# or +sudo systemctl stop postgresql # Linux + +# Run TimescaleDB in Docker +docker run -d \ + --name timescaledb \ + -p 5432:5432 \ + -e POSTGRES_PASSWORD=postgres \ + -v timescaledb_data:/var/lib/postgresql/data \ + timescale/timescaledb:latest-pg17 + +# Update config/dev.exs to use Docker PostgreSQL +# (already configured to use localhost:5432) +``` + +## Verification + +After installing TimescaleDB, verify it's working: + +```bash +# Connect to PostgreSQL +psql -U postgres -d towerops_dev + +# Check if TimescaleDB is available +SELECT * FROM pg_available_extensions WHERE name = 'timescaledb'; + +# If available, enable it +CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; + +# Verify version +SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'; + +# Exit psql +\q +``` + +## Running Migrations + +### Development/Test (Standard PostgreSQL) + +```bash +# Default behavior - uses standard PostgreSQL +mix ecto.reset +``` + +You should see: +``` +Development/Test environment - using standard PostgreSQL table +TimescaleDB features will be enabled in production +Development/Test environment - skipping continuous aggregates +Continuous aggregates will be created in production +``` + +### Production (TimescaleDB) + +```bash +# Set MIX_ENV to prod and run migrations +MIX_ENV=prod mix ecto.migrate +``` + +You should see: +``` +Production environment - enabling TimescaleDB features +Production environment - creating TimescaleDB continuous aggregates +``` + +**Environment Detection**: Migrations automatically detect the environment via `MIX_ENV` and only enable TimescaleDB features when `MIX_ENV=prod`. + +## TimescaleDB Features Enabled + +### Hypertable +- `monitoring_checks` table is automatically partitioned by time +- Optimized for time-range queries +- Efficient INSERTs for high-volume monitoring data + +### Retention Policy +- Raw monitoring data automatically deleted after 90 days +- Keeps database size manageable +- Configurable in migration file + +### Compression Policy +- Data older than 7 days is automatically compressed +- ~95% space savings +- Transparent decompression on query + +### Continuous Aggregates + +#### monitoring_checks_hourly +- Pre-computed hourly statistics per equipment +- Includes: total checks, successful/failed counts, avg/min/max response time +- Refreshed every hour automatically + +#### monitoring_checks_daily +- Pre-computed daily statistics per equipment +- Includes uptime percentage calculation +- Refreshed daily automatically + +## Querying Time-Series Data + +### Using Continuous Aggregates + +```elixir +# Get hourly stats for last 24 hours +start_time = DateTime.utc_now() |> DateTime.add(-24 * 60 * 60, :second) +end_time = DateTime.utc_now() +Monitoring.get_hourly_stats(equipment_id, start_time, end_time) + +# Get daily stats for last 30 days +start_time = DateTime.utc_now() |> DateTime.add(-30 * 24 * 60 * 60, :second) +Monitoring.get_daily_stats(equipment_id, start_time, DateTime.utc_now()) + +# Get uptime percentage for last 30 days +Monitoring.get_uptime_percentage(equipment_id, 30) +``` + +### Direct SQL with TimescaleDB Functions + +```sql +-- Time-bucketed aggregation +SELECT + time_bucket('5 minutes', checked_at) AS bucket, + AVG(response_time_ms) as avg_response_time +FROM monitoring_checks +WHERE equipment_id = 'abc-123' + AND checked_at > NOW() - INTERVAL '1 hour' +GROUP BY bucket +ORDER BY bucket DESC; + +-- Using continuous aggregate for fast queries +SELECT * +FROM monitoring_checks_hourly +WHERE equipment_id = 'abc-123' + AND bucket > NOW() - INTERVAL '7 days' +ORDER BY bucket DESC; +``` + +## Monitoring TimescaleDB + +```sql +-- Check hypertable info +SELECT * FROM timescaledb_information.hypertables; + +-- Check compression stats +SELECT * FROM timescaledb_information.compression_settings; + +-- Check chunk info +SELECT * FROM timescaledb_information.chunks +WHERE hypertable_name = 'monitoring_checks'; + +-- Check continuous aggregate policies +SELECT * FROM timescaledb_information.continuous_aggregates; +``` + +## Troubleshooting + +### Extension not available +- Make sure TimescaleDB is installed: `brew list | grep timescale` (macOS) +- Check PostgreSQL config: `cat $(pg_config --sysconfdir)/postgresql.conf | grep shared_preload_libraries` +- Should include: `shared_preload_libraries = 'timescaledb'` + +### Migration fails with TimescaleDB errors +- Drop database and recreate: `mix ecto.drop && mix ecto.create && mix ecto.migrate` +- Check PostgreSQL logs: `tail -f /opt/homebrew/var/log/postgresql@17.log` + +### Continuous aggregates not refreshing +```sql +-- Manually refresh +CALL refresh_continuous_aggregate('monitoring_checks_hourly', NULL, NULL); +CALL refresh_continuous_aggregate('monitoring_checks_daily', NULL, NULL); +``` + +## Production Considerations + +### Chunk Size +Default chunk interval is 7 days. For high-volume deployments, consider: + +```sql +-- Set chunk interval to 1 day for higher write throughput +SELECT set_chunk_time_interval('monitoring_checks', INTERVAL '1 day'); +``` + +### Retention Policy +Adjust retention based on compliance requirements: + +```sql +-- Extend retention to 1 year +SELECT remove_retention_policy('monitoring_checks'); +SELECT add_retention_policy('monitoring_checks', INTERVAL '1 year'); +``` + +### Compression +Tune compression for your workload: + +```sql +-- Compress after 1 day instead of 7 days +SELECT remove_compression_policy('monitoring_checks'); +SELECT add_compression_policy('monitoring_checks', INTERVAL '1 day'); +``` + +## Resources + +- [TimescaleDB Documentation](https://docs.timescale.com/) +- [TimescaleDB Best Practices](https://docs.timescale.com/use-timescale/latest/schema-management/about-schemas/) +- [Continuous Aggregates Guide](https://docs.timescale.com/use-timescale/latest/continuous-aggregates/) +- [Compression Guide](https://docs.timescale.com/use-timescale/latest/compression/) diff --git a/lib/towerops/monitoring.ex b/lib/towerops/monitoring.ex index b519e976..03764787 100644 --- a/lib/towerops/monitoring.ex +++ b/lib/towerops/monitoring.ex @@ -47,4 +47,85 @@ defmodule Towerops.Monitoring do from(c in Check, where: c.checked_at < ^older_than_date) |> Repo.delete_all() end + + @doc """ + Gets hourly statistics for equipment over a time range. + Uses TimescaleDB continuous aggregate for performance. + """ + def get_hourly_stats(equipment_id, start_time, end_time) do + query = """ + SELECT + bucket, + total_checks, + successful_checks, + failed_checks, + avg_response_time_ms, + min_response_time_ms, + max_response_time_ms, + ROUND(100.0 * successful_checks / NULLIF(total_checks, 0), 2) as uptime_percentage + FROM monitoring_checks_hourly + WHERE equipment_id = $1 + AND bucket >= $2 + AND bucket <= $3 + ORDER BY bucket ASC + """ + + Ecto.Adapters.SQL.query!( + Repo, + query, + [equipment_id, start_time, end_time] + ) + end + + @doc """ + Gets daily statistics for equipment over a time range. + Uses TimescaleDB continuous aggregate for performance. + """ + def get_daily_stats(equipment_id, start_time, end_time) do + query = """ + SELECT + bucket, + total_checks, + successful_checks, + failed_checks, + avg_response_time_ms, + min_response_time_ms, + max_response_time_ms, + uptime_percentage + FROM monitoring_checks_daily + WHERE equipment_id = $1 + AND bucket >= $2 + AND bucket <= $3 + ORDER BY bucket ASC + """ + + Ecto.Adapters.SQL.query!( + Repo, + query, + [equipment_id, start_time, end_time] + ) + end + + @doc """ + Gets uptime percentage for equipment over the last N days. + """ + def get_uptime_percentage(equipment_id, days_ago \\ 30) do + start_time = DateTime.utc_now() |> DateTime.add(-days_ago * 24 * 60 * 60, :second) + + query = """ + SELECT + AVG(uptime_percentage) as avg_uptime + FROM monitoring_checks_daily + WHERE equipment_id = $1 + AND bucket >= $2 + """ + + case Ecto.Adapters.SQL.query!(Repo, query, [equipment_id, start_time]) do + %{rows: [[uptime]]} when not is_nil(uptime) -> + Float.round(uptime, 2) + + _ -> + nil + end + end end diff --git a/lib/towerops/monitoring/supervisor.ex b/lib/towerops/monitoring/supervisor.ex index d4f9437a..ed3293fe 100644 --- a/lib/towerops/monitoring/supervisor.ex +++ b/lib/towerops/monitoring/supervisor.ex @@ -19,12 +19,15 @@ defmodule Towerops.Monitoring.Supervisor do {DynamicSupervisor, name: Towerops.Monitoring.DynamicSupervisor, strategy: :one_for_one} ] - # Start all monitors after supervisor is initialized - Task.start(fn -> - # Wait briefly for the supervisor tree to be fully initialized - Process.sleep(100) - start_all_monitors() - end) + # Start all monitors after supervisor is initialized (but not in test mode) + # In test mode, the Repo uses Sandbox pool which we can detect + unless test_mode?() do + Task.start(fn -> + # Wait briefly for the supervisor tree to be fully initialized + Process.sleep(100) + start_all_monitors() + end) + end Supervisor.init(children, strategy: :one_for_one) end @@ -60,4 +63,10 @@ defmodule Towerops.Monitoring.Supervisor do start_monitor(equipment.id) end) end + + # Check if running in test mode by inspecting the Repo pool configuration + defp test_mode? do + config = Application.get_env(:towerops, Towerops.Repo, []) + Keyword.get(config, :pool) == Ecto.Adapters.SQL.Sandbox + end end diff --git a/priv/repo/migrations/20251221193939_create_monitoring_checks.exs b/priv/repo/migrations/20251221193939_create_monitoring_checks.exs index e632aaae..5cc2bb5a 100644 --- a/priv/repo/migrations/20251221193939_create_monitoring_checks.exs +++ b/priv/repo/migrations/20251221193939_create_monitoring_checks.exs @@ -1,7 +1,8 @@ defmodule Towerops.Repo.Migrations.CreateMonitoringChecks do use Ecto.Migration - def change do + def up do + # Create the table create table(:monitoring_checks, primary_key: false) do add :id, :binary_id, primary_key: true add :equipment_id, references(:equipment, type: :binary_id, on_delete: :delete_all), @@ -13,8 +14,52 @@ defmodule Towerops.Repo.Migrations.CreateMonitoringChecks do timestamps(type: :utc_datetime, updated_at: false) end + # Create indexes create index(:monitoring_checks, [:equipment_id]) create index(:monitoring_checks, [:checked_at]) create index(:monitoring_checks, [:equipment_id, :checked_at]) + + # Enable TimescaleDB features only in production + if production?() do + IO.puts("Production environment - enabling TimescaleDB features") + + # Enable TimescaleDB extension + execute("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE") + + # Convert to TimescaleDB hypertable (partitioned by checked_at) + execute("SELECT create_hypertable('monitoring_checks', 'checked_at')") + + # Add retention policy: automatically drop data older than 90 days + execute(""" + SELECT add_retention_policy('monitoring_checks', INTERVAL '90 days') + """) + + # Add compression policy: compress chunks older than 7 days + execute(""" + ALTER TABLE monitoring_checks SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'equipment_id' + ) + """) + + execute(""" + SELECT add_compression_policy('monitoring_checks', INTERVAL '7 days') + """) + else + IO.puts("Development/Test environment - using standard PostgreSQL table") + IO.puts("TimescaleDB features will be enabled in production") + end + end + + defp production? do + # Check if running in production environment + # In production, MIX_ENV will be set to "prod" + System.get_env("MIX_ENV") == "prod" or + Application.get_env(:towerops, :env) == :prod + end + + def down do + drop table(:monitoring_checks) + execute("DROP EXTENSION IF EXISTS timescaledb CASCADE") end end diff --git a/priv/repo/migrations/20251221225717_create_monitoring_aggregates.exs b/priv/repo/migrations/20251221225717_create_monitoring_aggregates.exs new file mode 100644 index 00000000..b3da28b5 --- /dev/null +++ b/priv/repo/migrations/20251221225717_create_monitoring_aggregates.exs @@ -0,0 +1,79 @@ +defmodule Towerops.Repo.Migrations.CreateMonitoringAggregates do + use Ecto.Migration + + def up do + # Only create continuous aggregates in production (requires TimescaleDB) + if production?() do + IO.puts("Production environment - creating TimescaleDB continuous aggregates") + + # Hourly continuous aggregate for monitoring checks + execute(""" + CREATE MATERIALIZED VIEW monitoring_checks_hourly + WITH (timescaledb.continuous) AS + SELECT + equipment_id, + time_bucket('1 hour', checked_at) AS bucket, + COUNT(*) AS total_checks, + COUNT(*) FILTER (WHERE status = 'success') AS successful_checks, + COUNT(*) FILTER (WHERE status = 'failure') AS failed_checks, + AVG(response_time_ms) FILTER (WHERE status = 'success') AS avg_response_time_ms, + MIN(response_time_ms) FILTER (WHERE status = 'success') AS min_response_time_ms, + MAX(response_time_ms) FILTER (WHERE status = 'success') AS max_response_time_ms + FROM monitoring_checks + GROUP BY equipment_id, bucket + """) + + # Add refresh policy for hourly aggregate (refresh every hour) + execute(""" + SELECT add_continuous_aggregate_policy('monitoring_checks_hourly', + start_offset => INTERVAL '3 hours', + end_offset => INTERVAL '1 hour', + schedule_interval => INTERVAL '1 hour') + """) + + # Daily continuous aggregate for monitoring checks + execute(""" + CREATE MATERIALIZED VIEW monitoring_checks_daily + WITH (timescaledb.continuous) AS + SELECT + equipment_id, + time_bucket('1 day', checked_at) AS bucket, + COUNT(*) AS total_checks, + COUNT(*) FILTER (WHERE status = 'success') AS successful_checks, + COUNT(*) FILTER (WHERE status = 'failure') AS failed_checks, + AVG(response_time_ms) FILTER (WHERE status = 'success') AS avg_response_time_ms, + MIN(response_time_ms) FILTER (WHERE status = 'success') AS min_response_time_ms, + MAX(response_time_ms) FILTER (WHERE status = 'success') AS max_response_time_ms, + ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / NULLIF(COUNT(*), 0), 2) AS uptime_percentage + FROM monitoring_checks + GROUP BY equipment_id, bucket + """) + + # Add refresh policy for daily aggregate (refresh every day) + execute(""" + SELECT add_continuous_aggregate_policy('monitoring_checks_daily', + start_offset => INTERVAL '3 days', + end_offset => INTERVAL '1 day', + schedule_interval => INTERVAL '1 day') + """) + + # Create indexes on the materialized views + create index(:monitoring_checks_hourly, [:equipment_id, :bucket]) + create index(:monitoring_checks_daily, [:equipment_id, :bucket]) + else + IO.puts("Development/Test environment - skipping continuous aggregates") + IO.puts("Continuous aggregates will be created in production") + end + end + + defp production? do + # Check if running in production environment + System.get_env("MIX_ENV") == "prod" or + Application.get_env(:towerops, :env) == :prod + end + + def down do + execute("DROP MATERIALIZED VIEW IF EXISTS monitoring_checks_daily") + execute("DROP MATERIALIZED VIEW IF EXISTS monitoring_checks_hourly") + end +end