timescaledb
This commit is contained in:
parent
d431db37bb
commit
8f618291a0
7 changed files with 683 additions and 16 deletions
|
|
@ -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
|
||||
|
|
|
|||
75
README.md
75
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
|
||||
|
||||
|
|
|
|||
284
TIMESCALEDB.md
Normal file
284
TIMESCALEDB.md
Normal file
|
|
@ -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/)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue