Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ DB_PASSWORD=your_database_password_here
# MeteoSIX
# Get your key at https://www.meteogalicia.gal/web/servizos/meteosix/solicitudeClave.action
METEOSIX_API_KEY=YOUR_KEY_HERE

# Alerting
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

Chronodash is a multi-datasource monitoring and monitoring application built with **Elixir**, **Phoenix**, and the **Ash Framework**. It is designed to collect time-series metrics from various providers (starting with MeteoSIX) and visualize them in **Grafana** using **TimescaleDB**.

---

## Key Architecture Concepts

Expand Down
5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ config :chronodash, :polling_jobs, [
}
]

# Alerts configuration
config :chronodash, :alerts,
rules: [],
channels: %{}

# Finch configuration
config :chronodash, :http_client, Chronodash.HttpClient.Finch

Expand Down
30 changes: 30 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,36 @@ config :chronodash, :meteosix,
api_key: System.get_env("METEOSIX_API_KEY") || "KEY",
base_url: "https://servizos.meteogalicia.gal/apiv5"

config :chronodash, :alerts,
rules: [
# Alert if wind speed > 30 km/h at any point in the next 24h
%{
id: "high_wind_warning",
metric: "wind",
threshold: 30.0,
condition: :max_gt,
window_hours: 24,
cooldown_hours: 12,
message: "⚠️ High Wind Warning in %{location} in the next 24h: %{value} km/h",
channels: [:discord]
},
# Alert if no precipitation is expected in the next 6 hours
%{
id: "no_rain_forecast",
metric: "precipitation_amount",
threshold: 1.0,
# Maximum in the window is less than 1.0
condition: :max_lt,
window_hours: 6,
cooldown_hours: 6,
message: "☀️ Good news! No significant rain predicted in %{location} for the next 6 hours.",
channels: [:discord]
}
],
channels: %{
discord: %{url: System.get_env("DISCORD_WEBHOOK_URL")}
}

if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.tel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ services:
volumes:
- "../etc/grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml"
- "../etc/grafana/dashboard_provider.yml:/etc/grafana/provisioning/dashboards/main.yaml"
- "../etc/grafana/provisioning/alerting:/etc/grafana/provisioning/alerting"
- "../etc/grafana/dashboards:/var/lib/grafana/dashboards"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
Expand Down
50 changes: 47 additions & 3 deletions etc/grafana/dashboards/meteosix_wrf.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,34 @@
"y": 0
},
"id": 2,
"alert": {
"conditions": [
{
"evaluator": {
"params": [5],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": ["A", "5m", "now"]
},
"reducer": {
"params": [],
"type": "max"
},
"type": "query"
}
],
"executionErrorState": "alerting",
"for": "5m",
"frequency": "1m",
"handler": 1,
"name": "High Precipitation Alert",
"noDataState": "no_data",
"notifications": []
},
"options": {
"legend": {
"displayMode": "list",
Expand Down Expand Up @@ -125,10 +153,26 @@
{
"options": {
"1": { "color": "yellow", "text": "SUNNY" },
"2": { "color": "light-blue", "text": "PARTLY_CLOUDY" },
"3": { "color": "blue", "text": "HIGH_CLOUDS" },
"2": { "color": "light-blue", "text": "HIGH_CLOUDS" },
"3": { "color": "light-blue", "text": "PARTLY_CLOUDY" },
"4": { "color": "grey", "text": "CLOUDY" },
"5": { "color": "dark-grey", "text": "OVERCAST" }
"5": { "color": "dark-grey", "text": "OVERCAST" },
"6": { "color": "orange", "text": "FOG" },
"7": { "color": "blue", "text": "SHOWERS" },
"8": { "color": "blue", "text": "OVERCAST_AND_SHOWERS" },
"9": { "color": "light-grey", "text": "INTERMITENT_SNOW" },
"10": { "color": "light-blue", "text": "DRIZZLE" },
"11": { "color": "blue", "text": "RAIN" },
"12": { "color": "white", "text": "SNOW" },
"13": { "color": "red", "text": "STORMS" },
"14": { "color": "orange", "text": "MIST" },
"15": { "color": "orange", "text": "FOG_BANK" },
"16": { "color": "light-blue", "text": "MID_CLOUDS" },
"17": { "color": "light-blue", "text": "WEAK_RAIN" },
"18": { "color": "light-blue", "text": "WEAK_SHOWERS" },
"19": { "color": "red", "text": "STORM_THEN_CLOUDY" },
"20": { "color": "white", "text": "MELTED_SNOW" },
"21": { "color": "purple", "text": "RAIN_HAIL" }
},
"type": "value"
}
Expand Down
1 change: 1 addition & 0 deletions etc/grafana/datasource.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ datasources:
editable: true

- name: PostgreSQL
uid: postgres_db
type: postgres
access: proxy
url: postgres:5432
Expand Down
10 changes: 10 additions & 0 deletions etc/grafana/provisioning/alerting/contact_points.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: 1

contactPoints:
- orgId: 1
name: Discord
receivers:
- uid: discord_alert
type: discord
settings:
url: ${DISCORD_WEBHOOK_URL}
10 changes: 10 additions & 0 deletions etc/grafana/provisioning/alerting/notification_policies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: 1

# Notification policies for Grafana alerting
policies:
- orgId: 1
receiver: Discord
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
81 changes: 81 additions & 0 deletions etc/grafana/provisioning/alerting/rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
apiVersion: 1

groups:
- orgId: 1
name: Weather Alerts
folder: MeteoSIX Alerts
interval: 1m
rules:
- uid: high_wind_alert
title: High Wind Warning
condition: C
data:
- refId: A
datasourceUid: postgres_db
relativeTimeRange:
from: 3600
to: 0
model:
rawSql: "SELECT value, timestamp FROM observations WHERE metric_type = 'wind_module' AND timestamp > now() AND timestamp < now() + interval '24 hours' ORDER BY timestamp ASC"
format: table
- refId: B
datasourceUid: __expr__
model:
expression: A
type: reduce
reducer: max
- refId: C
datasourceUid: __expr__
model:
expression: B
type: threshold
conditions:
- evaluator:
params: [30]
type: gt
operator:
type: and
noDataState: OK
execErrState: Error
for: 0s
annotations:
summary: "High wind predicted: {{ index $values \"B\" }} km/h"
labels:
severity: warning

- uid: high_precip_alert
title: High Precipitation Warning
condition: C
data:
- refId: A
datasourceUid: postgres_db
relativeTimeRange:
from: 3600
to: 0
model:
rawSql: "SELECT value, timestamp FROM observations WHERE metric_type = 'precipitation_amount' AND timestamp > now() AND timestamp < now() + interval '6 hours' ORDER BY timestamp ASC"
format: table
- refId: B
datasourceUid: __expr__
model:
expression: A
type: reduce
reducer: max
- refId: C
datasourceUid: __expr__
model:
expression: B
type: threshold
conditions:
- evaluator:
params: [5]
type: gt
operator:
type: and
noDataState: OK
execErrState: Error
for: 0s
annotations:
summary: "Heavy rain predicted: {{ index $values \"B\" }} mm"
labels:
severity: critical
47 changes: 47 additions & 0 deletions lib/chronodash/alerting/dispatcher.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule Chronodash.Alerting.Dispatcher do
@moduledoc """
Dispatches alert messages to configured channel providers.
"""
require Logger

alias Chronodash.Alerting.Providers

@providers %{
discord: Providers.Discord
}

# ============================================================================
# Public API
# ============================================================================

@doc """
Sends a message to a list of channels.
"""
def dispatch(message, channel_keys) when is_list(channel_keys) do
config = Application.get_env(:chronodash, :alerts, [])
channel_configs = Keyword.get(config, :channels, %{})

Enum.each(channel_keys, fn key ->
case Map.get(channel_configs, key) do
nil ->
Logger.warning("Attempted to dispatch to unknown channel: #{key}")

chan_config ->
do_dispatch(message, key, chan_config)
end
end)
end

defp do_dispatch(message, key, config) do
case Map.get(@providers, key) do
nil ->
Logger.error("No provider implementation for channel type: #{key}")

provider ->
# We wrap in Task.start to ensure alerting doesn't block the caller
Task.start(fn ->
provider.deliver(message, config)
end)
end
end
end
Loading
Loading