Initial Boone stack: observability and automation baseline
This commit is contained in:
commit
ba22ab548f
6
.env.example
Normal file
6
.env.example
Normal file
@ -0,0 +1,6 @@
|
||||
POSTGRES_USER=farmadmin
|
||||
POSTGRES_PASSWORD=change_this_now
|
||||
POSTGRES_DB=farm
|
||||
GRAFANA_ADMIN_USER=admin
|
||||
GRAFANA_ADMIN_PASSWORD=change_this_now
|
||||
TZ=America/New_York
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# secrets
|
||||
.env
|
||||
|
||||
# persistent runtime data
|
||||
data/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
34
README.md
Normal file
34
README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Farm Control Stack
|
||||
|
||||
This folder contains a starter central-control stack for your farm:
|
||||
- PostgreSQL (operational + business data)
|
||||
- Mosquitto MQTT (sensor/device messaging)
|
||||
- Grafana (dashboards/alerts)
|
||||
- Node-RED (automation workflows)
|
||||
- Prometheus (metrics collection)
|
||||
|
||||
## First-run on this Mac
|
||||
1. Open `Rancher Desktop` from Applications.
|
||||
2. Complete first-run setup in the GUI.
|
||||
3. In Rancher Desktop settings, ensure Docker CLI is enabled.
|
||||
4. Open a new terminal and run:
|
||||
```bash
|
||||
cd /Users/farm/blackfish/projects/farm-control
|
||||
./start.sh
|
||||
```
|
||||
|
||||
## Default endpoints
|
||||
- Grafana: http://localhost:3000
|
||||
- Node-RED: http://localhost:1880
|
||||
- MQTT broker: `localhost:1883`
|
||||
- PostgreSQL: `localhost:5432`
|
||||
- Prometheus: http://localhost:9090
|
||||
- Mac host metrics exporter: http://localhost:9100/metrics
|
||||
|
||||
## Mac performance metrics
|
||||
This Mac is monitored via `node_exporter` (installed with Homebrew and started as a service).
|
||||
Prometheus scrapes it at `host.docker.internal:9100`, and Grafana uses Prometheus as default datasource.
|
||||
Grafana also auto-loads a dashboard: `Mac Host Overview` (folder: `Farm Control`).
|
||||
|
||||
## Important
|
||||
Edit `.env` and change default passwords before exposing this machine on a farm network.
|
||||
85
docker-compose.yml
Normal file
85
docker-compose.yml
Normal file
@ -0,0 +1,85 @@
|
||||
name: farm-control
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: farm-postgres
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- TZ=${TZ}
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
mosquitto:
|
||||
image: eclipse-mosquitto:2
|
||||
container_name: farm-mqtt
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
|
||||
- ./data/mosquitto:/mosquitto/data
|
||||
- ./data/mosquitto-log:/mosquitto/log
|
||||
ports:
|
||||
- "1883:1883"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:11.6.0
|
||||
container_name: farm-grafana
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
||||
- TZ=${TZ}
|
||||
volumes:
|
||||
- ./data/grafana:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./grafana/dashboards:/etc/grafana/dashboards:ro
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- postgres
|
||||
- prometheus
|
||||
|
||||
nodered:
|
||||
image: nodered/node-red:4.0.9
|
||||
container_name: farm-nodered
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=${TZ}
|
||||
volumes:
|
||||
- ./data/nodered:/data
|
||||
ports:
|
||||
- "1880:1880"
|
||||
depends_on:
|
||||
- mosquitto
|
||||
- postgres
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.2.1
|
||||
container_name: farm-prometheus
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --storage.tsdb.retention.time=30d
|
||||
- --storage.tsdb.retention.size=20GB
|
||||
volumes:
|
||||
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./data/prometheus:/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: farm-control-net
|
||||
187
grafana/dashboards/mac-host-overview.json
Normal file
187
grafana/dashboards/mac-host-overview.json
Normal file
@ -0,0 +1,187 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {"type": "grafana", "uid": "-- Grafana --"},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {
|
||||
"defaults": {"unit": "percent", "decimals": 1},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 0},
|
||||
"id": 1,
|
||||
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "(1 - avg(rate(node_cpu_seconds_total{instance=\"$instance\",mode=\"idle\"}[5m]))) * 100",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "CPU Usage",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {
|
||||
"defaults": {"unit": "percent", "decimals": 1},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 0},
|
||||
"id": 2,
|
||||
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "(1 - ((node_memory_free_bytes{instance=\"$instance\"} + node_memory_inactive_bytes{instance=\"$instance\"} + node_memory_purgeable_bytes{instance=\"$instance\"}) / node_memory_total_bytes{instance=\"$instance\"})) * 100",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Memory Usage",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {
|
||||
"defaults": {"unit": "percent", "decimals": 1},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 0},
|
||||
"id": 3,
|
||||
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "100 * (1 - (node_filesystem_avail_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"} / node_filesystem_size_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"}))",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Root Disk Usage",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {
|
||||
"defaults": {"unit": "none", "decimals": 2},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 0},
|
||||
"id": 4,
|
||||
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "node_load5{instance=\"$instance\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Load (5m)",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {"defaults": {"unit": "percentunit", "decimals": 2}, "overrides": []},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 4},
|
||||
"id": 5,
|
||||
"options": {"legend": {"displayMode": "list", "placement": "bottom"}, "tooltip": {"mode": "single"}},
|
||||
"targets": [
|
||||
{"expr": "1 - avg(rate(node_cpu_seconds_total{instance=\"$instance\",mode=\"idle\"}[5m]))", "legendFormat": "used", "refId": "A"},
|
||||
{"expr": "avg(rate(node_cpu_seconds_total{instance=\"$instance\",mode=\"idle\"}[5m]))", "legendFormat": "idle", "refId": "B"}
|
||||
],
|
||||
"title": "CPU Usage (ratio)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {"defaults": {"unit": "bytes", "decimals": 2}, "overrides": []},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 4},
|
||||
"id": 6,
|
||||
"options": {"legend": {"displayMode": "list", "placement": "bottom"}, "tooltip": {"mode": "single"}},
|
||||
"targets": [
|
||||
{"expr": "node_memory_total_bytes{instance=\"$instance\"} - (node_memory_free_bytes{instance=\"$instance\"} + node_memory_inactive_bytes{instance=\"$instance\"} + node_memory_purgeable_bytes{instance=\"$instance\"})", "legendFormat": "used", "refId": "A"},
|
||||
{"expr": "node_memory_free_bytes{instance=\"$instance\"} + node_memory_inactive_bytes{instance=\"$instance\"} + node_memory_purgeable_bytes{instance=\"$instance\"}", "legendFormat": "available", "refId": "B"}
|
||||
],
|
||||
"title": "Memory Bytes",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {"defaults": {"unit": "Bps", "decimals": 2}, "overrides": []},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 12},
|
||||
"id": 7,
|
||||
"options": {"legend": {"displayMode": "table", "placement": "bottom"}, "tooltip": {"mode": "multi"}},
|
||||
"targets": [
|
||||
{"expr": "sum by (device) (rate(node_network_receive_bytes_total{instance=\"$instance\",device!~\"lo0|utun.*|awdl0|llw0|gif0|stf0\"}[5m]))", "legendFormat": "{{device}} rx", "refId": "A"},
|
||||
{"expr": "sum by (device) (rate(node_network_transmit_bytes_total{instance=\"$instance\",device!~\"lo0|utun.*|awdl0|llw0|gif0|stf0\"}[5m]))", "legendFormat": "{{device}} tx", "refId": "B"}
|
||||
],
|
||||
"title": "Network Throughput",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"fieldConfig": {"defaults": {"unit": "bytes", "decimals": 2}, "overrides": []},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 12},
|
||||
"id": 8,
|
||||
"options": {"legend": {"displayMode": "list", "placement": "bottom"}, "tooltip": {"mode": "single"}},
|
||||
"targets": [
|
||||
{"expr": "node_filesystem_size_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"}", "legendFormat": "size", "refId": "A"},
|
||||
{"expr": "node_filesystem_avail_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"}", "legendFormat": "available", "refId": "B"}
|
||||
],
|
||||
"title": "Root Disk Bytes",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "15s",
|
||||
"schemaVersion": 39,
|
||||
"style": "dark",
|
||||
"tags": ["farm", "mac", "host"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {"selected": false, "text": "Prometheus", "value": "prometheus"},
|
||||
"hide": 2,
|
||||
"label": "datasource",
|
||||
"name": "datasource",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"current": {"selected": false, "text": "host.docker.internal:9100", "value": "host.docker.internal:9100"},
|
||||
"datasource": {"type": "prometheus", "uid": "$datasource"},
|
||||
"definition": "label_values(node_uname_info, instance)",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Instance",
|
||||
"multi": false,
|
||||
"name": "instance",
|
||||
"options": [],
|
||||
"query": {"qryType": 1, "query": "label_values(node_uname_info, instance)", "refId": "PromVar"},
|
||||
"refresh": 2,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Mac Host Overview",
|
||||
"uid": "mac-host-overview",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
11
grafana/provisioning/dashboards/default.yml
Normal file
11
grafana/provisioning/dashboards/default.yml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Farm Dashboards'
|
||||
orgId: 1
|
||||
folder: 'Farm Control'
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /etc/grafana/dashboards
|
||||
10
grafana/provisioning/datasources/prometheus.yml
Normal file
10
grafana/provisioning/datasources/prometheus.yml
Normal file
@ -0,0 +1,10 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
uid: prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
11
monitoring/prometheus/prometheus.yml
Normal file
11
monitoring/prometheus/prometheus.yml
Normal file
@ -0,0 +1,11 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: prometheus
|
||||
static_configs:
|
||||
- targets: ['prometheus:9090']
|
||||
|
||||
- job_name: mac_host
|
||||
static_configs:
|
||||
- targets: ['host.docker.internal:9100']
|
||||
5
mosquitto/mosquitto.conf
Normal file
5
mosquitto/mosquitto.conf
Normal file
@ -0,0 +1,5 @@
|
||||
persistence true
|
||||
persistence_location /mosquitto/data/
|
||||
log_dest stdout
|
||||
listener 1883
|
||||
allow_anonymous true
|
||||
35
start.sh
Executable file
35
start.sh
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env zsh
|
||||
set -euo pipefail
|
||||
|
||||
RD_BIN_DIR="/Applications/Rancher Desktop.app/Contents/Resources/resources/darwin/bin"
|
||||
export PATH="$RD_BIN_DIR:$PATH"
|
||||
|
||||
DOCKER_BIN=""
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
DOCKER_BIN="$(command -v docker)"
|
||||
elif [ -x "$RD_BIN_DIR/docker" ]; then
|
||||
DOCKER_BIN="$RD_BIN_DIR/docker"
|
||||
else
|
||||
echo "docker CLI not found."
|
||||
echo "Enable Docker CLI in Rancher Desktop Preferences > Application and reopen terminal."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! "$DOCKER_BIN" info >/dev/null 2>&1; then
|
||||
echo "Docker runtime is not ready. Open Rancher Desktop and wait for status 'Running'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
"$DOCKER_BIN" compose pull
|
||||
"$DOCKER_BIN" compose up -d
|
||||
"$DOCKER_BIN" compose ps
|
||||
|
||||
cat <<MSG
|
||||
|
||||
Farm stack started.
|
||||
- Grafana: http://localhost:3000
|
||||
- Node-RED: http://localhost:1880
|
||||
- MQTT: mqtt://localhost:1883
|
||||
- Postgres: localhost:5432
|
||||
MSG
|
||||
Loading…
Reference in New Issue
Block a user