timescaledb

This commit is contained in:
Graham McIntire 2025-12-21 17:17:26 -06:00
parent d431db37bb
commit 8f618291a0
No known key found for this signature in database
7 changed files with 683 additions and 16 deletions

View file

@ -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

View file

@ -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
View 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/)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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