diff --git a/incusos/deploy-observability b/incusos/deploy-observability
index 88533a5..48bca3b 100755
--- a/incusos/deploy-observability
+++ b/incusos/deploy-observability
@@ -88,6 +88,7 @@ ${BOLD}ACTIONS${RESET}
--deploy Full deploy: monitoring container, Prometheus, Grafana,
Loki, Promtail, node-exporters, ACLs, network forward,
HAProxy exporter route, dashboards
+ --workloads Deploy dummy workloads to generate varied metrics
--status Show status of all observability components
--cleanup Remove all observability infrastructure
--doctor Diagnose common issues
@@ -462,13 +463,14 @@ phase_install_grafana() {
apt-get install -y -qq grafana 2>&1 | tail -1
"
- # Provision datasources
+ # Provision datasources with explicit UIDs for dashboard portability
container_exec monitoring bash -c "mkdir -p /etc/grafana/provisioning/datasources
cat > /etc/grafana/provisioning/datasources/observability.yaml << 'DSCFG'
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
+ uid: prometheus
access: proxy
url: http://localhost:9090
isDefault: true
@@ -476,28 +478,12 @@ datasources:
- name: Loki
type: loki
+ uid: loki
access: proxy
url: http://localhost:3100
editable: true
DSCFG"
- # Provision dashboard directory
- container_exec monitoring bash -c "mkdir -p /etc/grafana/provisioning/dashboards
-cat > /etc/grafana/provisioning/dashboards/observability.yaml << 'DBCFG'
-apiVersion: 1
-providers:
- - name: Observability
- orgId: 1
- folder: ''
- type: file
- disableDeletion: false
- editable: true
- options:
- path: /var/lib/grafana/dashboards
- foldersFromFilesStructure: false
-DBCFG
-mkdir -p /var/lib/grafana/dashboards"
-
container_exec monitoring systemctl enable grafana-server
container_exec monitoring systemctl restart grafana-server
sleep 2
@@ -725,35 +711,269 @@ phase_create_network_forward() {
# ---------------------------------------------------------------------------
phase_upload_dashboards() {
- step "Phase 9: Upload Grafana dashboards"
+ step "Phase 9: Install Grafana dashboards"
- if ! dry_run_guard "Upload dashboards from ${DASHBOARDS_DIR}"; then
+ if ! dry_run_guard "Install dashboards via manage-dashboards"; then
return 0
fi
- if [[ ! -d "$DASHBOARDS_DIR" ]]; then
- warn "Dashboard directory not found: ${DASHBOARDS_DIR}"
- warn "Skipping dashboard upload -- create dashboards manually in Grafana."
+ local manage_script="${SCRIPT_DIR}/manage-dashboards"
+ if [[ ! -x "$manage_script" ]]; then
+ warn "manage-dashboards script not found at ${manage_script}"
+ warn "Falling back to direct file upload."
+
+ if [[ ! -d "$DASHBOARDS_DIR" ]]; then
+ warn "Dashboard directory not found: ${DASHBOARDS_DIR}"
+ return 0
+ fi
+
+ local count=0
+ for dashboard in "${DASHBOARDS_DIR}"/core/*.json; do
+ [[ -f "$dashboard" ]] || continue
+ local basename
+ basename=$(basename "$dashboard")
+ incus file push "$dashboard" "$(container_ref monitoring)/var/lib/grafana/dashboards/${basename}"
+ detail "Uploaded ${basename}"
+ count=$((count + 1))
+ done
+
+ if [[ $count -gt 0 ]]; then
+ container_exec monitoring systemctl restart grafana-server 2>/dev/null || true
+ success "${count} dashboard(s) uploaded (direct push)"
+ else
+ info "No dashboard files found in ${DASHBOARDS_DIR}/core/"
+ fi
return 0
fi
- local count=0
- for dashboard in "${DASHBOARDS_DIR}"/*.json; do
- [[ -f "$dashboard" ]] || continue
- local basename
- basename=$(basename "$dashboard")
- incus file push "$dashboard" "$(container_ref monitoring)/var/lib/grafana/dashboards/${basename}"
- detail "Uploaded ${basename}"
- count=$((count + 1))
+ # Wait for Grafana API to be ready
+ info "Waiting for Grafana API..."
+ local elapsed=0
+ while true; do
+ local code
+ code=$(container_exec monitoring curl -s -o /dev/null -w '%{http_code}' \
+ http://localhost:3000/api/health 2>/dev/null || echo "000")
+ if [[ "$code" == "200" ]]; then
+ break
+ fi
+ sleep 2
+ elapsed=$((elapsed + 2))
+ if [[ $elapsed -ge 30 ]]; then
+ warn "Grafana API not ready after 30s -- falling back to file push"
+ return 0
+ fi
done
- if [[ $count -gt 0 ]]; then
- # Restart Grafana to pick up new dashboards
- container_exec monitoring systemctl restart grafana-server 2>/dev/null || true
- success "${count} dashboard(s) uploaded"
- else
- info "No dashboard files found in ${DASHBOARDS_DIR}"
+ # Use manage-dashboards to install with proper folder organization
+ local flags="-r ${CLUSTER_REMOTE}"
+ [[ "$VERBOSE" == true ]] && flags="${flags} -v"
+ [[ "$QUIET" == true ]] && flags="${flags} -q"
+
+ "${manage_script}" --install ${flags}
+}
+
+# ---------------------------------------------------------------------------
+# Workloads action
+# ---------------------------------------------------------------------------
+
+WORKLOAD_WEB_IP="10.10.10.80"
+WORKLOAD_API_IP="10.10.10.81"
+
+action_workloads() {
+ step "Deploying dummy workloads for metric generation"
+ echo
+ info " workload-web: ${WORKLOAD_WEB_IP} (nginx + CPU/disk/network crons)"
+ info " workload-api: ${WORKLOAD_API_IP} (Python HTTP + memory/network crons)"
+ echo
+
+ phase_deploy_workload_web
+ phase_deploy_workload_api
+
+ echo
+ success "Dummy workloads deployed!"
+ info " Metrics will appear within 1-2 minutes"
+ info " workload-web: nginx on ${WORKLOAD_WEB_IP}:80, CPU/disk/net crons"
+ info " workload-api: Python on ${WORKLOAD_API_IP}:8080, memory sawtooth cron"
+}
+
+phase_deploy_workload_web() {
+ step "Deploy workload-web (nginx + varied load)"
+
+ if container_exists "workload-web"; then
+ if container_running "workload-web"; then
+ success "workload-web already running"
+ return 0
+ fi
fi
+
+ if ! dry_run_guard "Launch workload-web on ${OVN_NETWORK} at ${WORKLOAD_WEB_IP}"; then
+ return 0
+ fi
+
+ incus launch images:alpine/3.20 "$(container_ref workload-web)" \
+ -n "$OVN_NETWORK" \
+ --target "oc-node-01" \
+ -c limits.memory=128MiB
+
+ wait_for_container_agent workload-web 30
+
+ container_exec workload-web sh -c "
+ apk add --no-cache nginx curl gzip coreutils > /dev/null 2>&1
+
+ # Configure nginx
+ mkdir -p /var/www/html
+ cat > /var/www/html/index.html << 'WEBHTML'
+
workload-web
Observability test workload
+WEBHTML
+ cat > /etc/nginx/http.d/default.conf << 'NGXCFG'
+server {
+ listen 80 default_server;
+ root /var/www/html;
+ index index.html;
+ location / { try_files \$uri \$uri/ =404; }
+}
+NGXCFG
+ rc-update add nginx default 2>/dev/null || true
+ rc-service nginx start 2>/dev/null || true
+
+ # Cron: every minute -> 10 HTTP requests (network traffic)
+ # Cron: every 5 min -> gzip random data (CPU spike)
+ # Cron: every 15 min -> dd 5 MiB write+delete (disk I/O burst)
+ cat > /etc/crontabs/root << 'CRONCFG'
+* * * * * for i in \$(seq 1 10); do wget -q -O /dev/null http://localhost/ 2>/dev/null; done
+*/5 * * * * dd if=/dev/urandom bs=1M count=2 2>/dev/null | gzip > /dev/null 2>&1
+*/15 * * * * dd if=/dev/urandom of=/tmp/disktest bs=1M count=5 2>/dev/null && rm -f /tmp/disktest
+CRONCFG
+ rc-update add crond default 2>/dev/null || true
+ rc-service crond start 2>/dev/null || true
+ "
+
+ # Configure static IP
+ container_exec workload-web sh -c "
+ cat > /etc/network/interfaces << NETCFG
+auto lo
+iface lo inet loopback
+
+auto eth0
+iface eth0 inet static
+ address ${WORKLOAD_WEB_IP}/24
+ gateway 10.10.10.1
+NETCFG
+ rc-service networking restart 2>/dev/null || true
+ "
+
+ # Attach monitoring ACL
+ incus config device set "$(container_ref workload-web)" eth0 \
+ security.acls=monitoring-allow security.acls.default.ingress.action=allow \
+ security.acls.default.egress.action=allow 2>/dev/null || true
+
+ success "workload-web deployed at ${WORKLOAD_WEB_IP}"
+}
+
+phase_deploy_workload_api() {
+ step "Deploy workload-api (Python HTTP + memory patterns)"
+
+ if container_exists "workload-api"; then
+ if container_running "workload-api"; then
+ success "workload-api already running"
+ return 0
+ fi
+ fi
+
+ if ! dry_run_guard "Launch workload-api on ${OVN_NETWORK} at ${WORKLOAD_API_IP}"; then
+ return 0
+ fi
+
+ incus launch images:alpine/3.20 "$(container_ref workload-api)" \
+ -n "$OVN_NETWORK" \
+ --target "oc-node-03" \
+ -c limits.memory=128MiB
+
+ wait_for_container_agent workload-api 30
+
+ container_exec workload-api sh -c "
+ apk add --no-cache python3 curl coreutils > /dev/null 2>&1
+
+ # Create a simple JSON status endpoint
+ mkdir -p /opt/api
+ cat > /opt/api/server.py << 'PYAPI'
+import http.server
+import json
+import time
+import os
+
+class Handler(http.server.BaseHTTPRequestHandler):
+ def do_GET(self):
+ self.send_response(200)
+ self.send_header('Content-Type', 'application/json')
+ self.end_headers()
+ data = {
+ 'status': 'ok',
+ 'uptime': time.time() - START_TIME,
+ 'hostname': os.uname().nodename,
+ 'pid': os.getpid()
+ }
+ self.wfile.write(json.dumps(data).encode())
+
+ def log_message(self, format, *args):
+ pass # suppress access logs
+
+START_TIME = time.time()
+http.server.HTTPServer(('0.0.0.0', 8080), Handler).serve_forever()
+PYAPI
+
+ # Create memory stress script
+ cat > /opt/api/mem-stress.py << 'PYMEM'
+import time
+# Allocate ~20 MiB, hold for 30 seconds, then release
+data = bytearray(20 * 1024 * 1024)
+time.sleep(30)
+del data
+PYMEM
+
+ # Start the API server as a background service
+ cat > /etc/init.d/api-server << 'INITSCRIPT'
+#!/sbin/openrc-run
+name=\"api-server\"
+command=\"/usr/bin/python3\"
+command_args=\"/opt/api/server.py\"
+command_background=true
+pidfile=\"/run/api-server.pid\"
+INITSCRIPT
+ chmod +x /etc/init.d/api-server
+ rc-update add api-server default 2>/dev/null || true
+ rc-service api-server start 2>/dev/null || true
+
+ # Cron: every 2 min -> curl workload-web (inter-container traffic)
+ # Cron: every 10 min -> allocate 20 MiB for 30s (memory sawtooth)
+ cat > /etc/crontabs/root << 'CRONCFG'
+*/2 * * * * wget -q -O /dev/null http://${WORKLOAD_WEB_IP}/ 2>/dev/null
+*/10 * * * * python3 /opt/api/mem-stress.py 2>/dev/null &
+CRONCFG
+ rc-update add crond default 2>/dev/null || true
+ rc-service crond start 2>/dev/null || true
+ "
+
+ # Configure static IP
+ container_exec workload-api sh -c "
+ cat > /etc/network/interfaces << NETCFG
+auto lo
+iface lo inet loopback
+
+auto eth0
+iface eth0 inet static
+ address ${WORKLOAD_API_IP}/24
+ gateway 10.10.10.1
+NETCFG
+ rc-service networking restart 2>/dev/null || true
+ "
+
+ # Attach monitoring ACL
+ incus config device set "$(container_ref workload-api)" eth0 \
+ security.acls=monitoring-allow security.acls.default.ingress.action=allow \
+ security.acls.default.egress.action=allow 2>/dev/null || true
+
+ success "workload-api deployed at ${WORKLOAD_API_IP}"
}
# ---------------------------------------------------------------------------
@@ -798,6 +1018,17 @@ action_status() {
idx=$((idx + 1))
done
+ # Workload containers (optional)
+ for wl in workload-web workload-api; do
+ if container_exists "$wl"; then
+ if container_running "$wl"; then
+ success " ${wl}: running"
+ else
+ warn " ${wl}: stopped"
+ fi
+ fi
+ done
+
if [[ "$all_ok" == false ]]; then
echo
error "Some containers are missing or stopped."
@@ -914,6 +1145,9 @@ action_cleanup() {
if container_exists "$(node_exp_name "$idx")"; then has_anything=true; fi
idx=$((idx + 1))
done
+ for wl in workload-web workload-api; do
+ if container_exists "$wl"; then has_anything=true; fi
+ done
if incus network forward show "${CLUSTER_REMOTE}:${OVN_NETWORK}" "$FORWARD_VIP" &>/dev/null 2>&1; then
has_anything=true
fi
@@ -936,6 +1170,11 @@ action_cleanup() {
fi
idx=$((idx + 1))
done
+ for wl in workload-web workload-api; do
+ if container_exists "$wl"; then
+ warn " - ${wl}"
+ fi
+ done
if incus network forward show "${CLUSTER_REMOTE}:${OVN_NETWORK}" "$FORWARD_VIP" &>/dev/null 2>&1; then
warn " - network forward ${FORWARD_VIP}"
fi
@@ -1003,6 +1242,15 @@ except: pass
idx=$((idx + 1))
done
+ # Delete workload containers
+ for wl in workload-web workload-api; do
+ if container_exists "$wl"; then
+ info "Deleting ${wl}..."
+ incus delete "$(container_ref "$wl")" --force 2>/dev/null || true
+ success "Deleted ${wl}"
+ fi
+ done
+
# Delete network forward
step "Removing network forward"
if incus network forward show "${CLUSTER_REMOTE}:${OVN_NETWORK}" "$FORWARD_VIP" &>/dev/null 2>&1; then
@@ -1153,6 +1401,7 @@ parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--deploy) ACTION="deploy";;
+ --workloads) ACTION="workloads";;
--status) ACTION="status";;
--cleanup) ACTION="cleanup";;
--doctor) ACTION="doctor";;
@@ -1187,10 +1436,11 @@ main() {
echo
case "$ACTION" in
- deploy) action_deploy;;
- status) action_status;;
- cleanup) action_cleanup;;
- doctor) action_doctor;;
+ deploy) action_deploy;;
+ workloads) action_workloads;;
+ status) action_status;;
+ cleanup) action_cleanup;;
+ doctor) action_doctor;;
esac
}
diff --git a/incusos/manage-dashboards b/incusos/manage-dashboards
new file mode 100755
index 0000000..a1b21ed
--- /dev/null
+++ b/incusos/manage-dashboards
@@ -0,0 +1,854 @@
+#!/usr/bin/env bash
+# manage-dashboards - Manage Grafana dashboards for the Incus observability stack
+#
+# Push, export, validate, and inspect Grafana dashboards via the Grafana HTTP
+# API. Dashboards are organized into folders and use datasource UID variables
+# for portability across deployments.
+#
+# Usage: manage-dashboards [OPTIONS]
+# Run 'manage-dashboards --help' for full usage information.
+
+set -euo pipefail
+
+readonly VERSION="0.1.0"
+readonly SCRIPT_NAME="manage-dashboards"
+readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+readonly DASHBOARDS_DIR="${SCRIPT_DIR}/observability-dashboards"
+readonly MANIFEST_FILE="${DASHBOARDS_DIR}/dashboard.yaml"
+
+# ---------------------------------------------------------------------------
+# Defaults (overridden by CLI flags)
+# ---------------------------------------------------------------------------
+
+CLUSTER_REMOTE="oc-node-01"
+MONITORING_CONTAINER="monitoring"
+GRAFANA_URL="http://localhost:3000"
+GRAFANA_USER="admin"
+GRAFANA_PASSWORD="admin"
+
+# Runtime flags
+DRY_RUN=false
+VERBOSE=false
+QUIET=false
+ACTION=""
+FILTER_CATEGORY=""
+FILTER_FILE=""
+FILTER_UID=""
+
+# ---------------------------------------------------------------------------
+# Color and formatting
+# ---------------------------------------------------------------------------
+
+setup_colors() {
+ if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
+ RED='\033[0;31m'
+ GREEN='\033[0;32m'
+ YELLOW='\033[0;33m'
+ BLUE='\033[0;34m'
+ CYAN='\033[0;36m'
+ BOLD='\033[1m'
+ DIM='\033[2m'
+ RESET='\033[0m'
+ else
+ RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
+ fi
+}
+
+info() { [[ "$QUIET" == true ]] && return; echo -e "${BLUE}[info]${RESET} $*"; }
+success() { [[ "$QUIET" == true ]] && return; echo -e "${GREEN}[ok]${RESET} $*"; }
+warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; }
+error() { echo -e "${RED}[error]${RESET} $*" >&2; }
+step() { [[ "$QUIET" == true ]] && return; echo -e "${CYAN}[step]${RESET} ${BOLD}$*${RESET}"; }
+detail() { [[ "$VERBOSE" != true ]] && return; echo -e "${DIM} $*${RESET}"; }
+
+dry_run_guard() {
+ if [[ "$DRY_RUN" == true ]]; then
+ info "(dry-run) $*"
+ return 1
+ fi
+ return 0
+}
+
+# ---------------------------------------------------------------------------
+# Help
+# ---------------------------------------------------------------------------
+
+usage() {
+ cat </dev/null || echo "")
+
+ LOKI_UID=$(echo "$ds_json" | python3 -c "
+import sys, json
+ds = json.load(sys.stdin)
+for d in ds:
+ if d.get('type') == 'loki':
+ print(d['uid'])
+ break
+" 2>/dev/null || echo "")
+
+ if [[ -z "$PROMETHEUS_UID" ]]; then
+ error "Could not find Prometheus datasource in Grafana"
+ return 1
+ fi
+ detail "Prometheus datasource UID: ${PROMETHEUS_UID}"
+ detail "Loki datasource UID: ${LOKI_UID:-not found}"
+}
+
+# Get or create a Grafana folder, returns folder ID
+ensure_folder() {
+ local folder_title="$1"
+
+ # Check if folder exists
+ local folder_id
+ folder_id=$(grafana_api GET "/api/folders" | python3 -c "
+import sys, json
+folders = json.load(sys.stdin)
+for f in folders:
+ if f.get('title') == '${folder_title}':
+ print(f['id'])
+ break
+" 2>/dev/null || echo "")
+
+ if [[ -n "$folder_id" ]]; then
+ detail "Folder '${folder_title}' exists (ID: ${folder_id})"
+ echo "$folder_id"
+ return 0
+ fi
+
+ # Create folder
+ local folder_uid
+ folder_uid=$(echo "$folder_title" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
+ local result
+ result=$(grafana_api POST "/api/folders" \
+ -H "Content-Type: application/json" \
+ -d "{\"title\": \"${folder_title}\", \"uid\": \"${folder_uid}\"}")
+
+ folder_id=$(echo "$result" | python3 -c "
+import sys, json
+print(json.load(sys.stdin).get('id', ''))
+" 2>/dev/null || echo "")
+
+ if [[ -n "$folder_id" ]]; then
+ detail "Created folder '${folder_title}' (ID: ${folder_id})"
+ echo "$folder_id"
+ else
+ error "Failed to create folder '${folder_title}': ${result}"
+ return 1
+ fi
+}
+
+# Read manifest and output entries as tab-separated lines
+read_manifest() {
+ if [[ ! -f "$MANIFEST_FILE" ]]; then
+ error "Dashboard manifest not found: ${MANIFEST_FILE}"
+ error "Expected a YAML file listing dashboard metadata."
+ return 1
+ fi
+
+ python3 -c "
+import yaml, sys
+
+with open('${MANIFEST_FILE}') as f:
+ data = yaml.safe_load(f)
+
+for d in data.get('dashboards', []):
+ file_path = d.get('file', '')
+ uid = d.get('uid', '')
+ title = d.get('title', '')
+ folder = d.get('folder', 'General')
+ category = d.get('category', '')
+ version = d.get('version', '1')
+ description = d.get('description', '')
+ print(f'{file_path}\t{uid}\t{title}\t{folder}\t{category}\t{version}\t{description}')
+" 2>/dev/null
+}
+
+# Substitute datasource variables in JSON content
+inject_datasource_uids() {
+ local json_content="$1"
+ echo "$json_content" \
+ | sed "s/\\\${DS_PROMETHEUS}/${PROMETHEUS_UID}/g" \
+ | sed "s/\\\${DS_LOKI}/${LOKI_UID}/g"
+}
+
+# Normalize datasource UIDs back to variables for portability
+normalize_datasource_uids() {
+ local json_content="$1"
+ local prom_uid="$2"
+ local loki_uid="$3"
+
+ local result="$json_content"
+ if [[ -n "$prom_uid" ]]; then
+ result=$(echo "$result" | sed "s/${prom_uid}/\\\${DS_PROMETHEUS}/g")
+ fi
+ if [[ -n "$loki_uid" ]]; then
+ result=$(echo "$result" | sed "s/${loki_uid}/\\\${DS_LOKI}/g")
+ fi
+ echo "$result"
+}
+
+# ---------------------------------------------------------------------------
+# Install action
+# ---------------------------------------------------------------------------
+
+action_install() {
+ step "Installing dashboards to Grafana"
+ echo
+
+ if ! dry_run_guard "Resolve datasource UIDs and push dashboards"; then
+ info "(dry-run) Would install dashboards from ${DASHBOARDS_DIR}"
+
+ local count=0
+ while IFS=$'\t' read -r file uid title folder category version description; do
+ if [[ -n "$FILTER_CATEGORY" && "$category" != "$FILTER_CATEGORY" ]]; then
+ continue
+ fi
+ if [[ -n "$FILTER_FILE" && "$file" != "$FILTER_FILE" ]]; then
+ continue
+ fi
+ info "(dry-run) Would install: ${title} -> folder '${folder}'"
+ count=$((count + 1))
+ done < <(read_manifest)
+
+ info "(dry-run) ${count} dashboard(s) would be installed"
+ return 0
+ fi
+
+ resolve_datasource_uids
+
+ local installed=0
+ local failed=0
+
+ while IFS=$'\t' read -r file uid title folder category version description; do
+ if [[ -n "$FILTER_CATEGORY" && "$category" != "$FILTER_CATEGORY" ]]; then
+ continue
+ fi
+ if [[ -n "$FILTER_FILE" && "$file" != "$FILTER_FILE" ]]; then
+ continue
+ fi
+
+ local full_path="${DASHBOARDS_DIR}/${file}"
+ if [[ ! -f "$full_path" ]]; then
+ warn "Dashboard file not found: ${full_path}"
+ failed=$((failed + 1))
+ continue
+ fi
+
+ # Get or create the target folder
+ local folder_id
+ folder_id=$(ensure_folder "$folder") || { failed=$((failed + 1)); continue; }
+
+ # Read and process the dashboard JSON
+ local raw_json
+ raw_json=$(cat "$full_path")
+
+ # Inject actual datasource UIDs
+ local processed_json
+ processed_json=$(inject_datasource_uids "$raw_json")
+
+ # Wrap in Grafana API envelope
+ local api_payload
+ api_payload=$(python3 -c "
+import json, sys
+
+dashboard = json.loads('''${processed_json//\'/\\\'}''')
+
+# Remove Grafana-internal fields that shouldn't be in the import
+dashboard.pop('id', None)
+dashboard.pop('version', None)
+dashboard.pop('iteration', None)
+
+payload = {
+ 'dashboard': dashboard,
+ 'folderId': ${folder_id},
+ 'overwrite': True,
+ 'message': 'Installed by manage-dashboards v${VERSION}'
+}
+print(json.dumps(payload))
+" 2>/dev/null)
+
+ if [[ -z "$api_payload" ]]; then
+ # Fallback: use a simpler approach for large/complex JSON
+ api_payload=$(python3 << 'PYEOF'
+import json, sys
+
+with open(sys.argv[1]) as f:
+ dashboard = json.load(f)
+
+# Inject datasource UIDs
+json_str = json.dumps(dashboard)
+json_str = json_str.replace('${DS_PROMETHEUS}', sys.argv[2])
+json_str = json_str.replace('${DS_LOKI}', sys.argv[3])
+dashboard = json.loads(json_str)
+
+# Remove Grafana-internal fields
+dashboard.pop('id', None)
+dashboard.pop('version', None)
+dashboard.pop('iteration', None)
+
+payload = {
+ 'dashboard': dashboard,
+ 'folderId': int(sys.argv[4]),
+ 'overwrite': True,
+ 'message': 'Installed by manage-dashboards'
+}
+print(json.dumps(payload))
+PYEOF
+ "$full_path" "$PROMETHEUS_UID" "${LOKI_UID:-}" "$folder_id" 2>/dev/null)
+ fi
+
+ if [[ -z "$api_payload" ]]; then
+ error "Failed to process dashboard JSON: ${file}"
+ failed=$((failed + 1))
+ continue
+ fi
+
+ # Push via API - write payload to temp file to avoid command-line size limits
+ local tmpfile
+ tmpfile=$(mktemp /tmp/dashboard-payload.XXXXXX)
+ echo "$api_payload" > "$tmpfile"
+
+ local result
+ result=$(incus file push "$tmpfile" "${CLUSTER_REMOTE}:${MONITORING_CONTAINER}/tmp/dashboard-payload.json" 2>/dev/null && \
+ grafana_api POST "/api/dashboards/db" \
+ -H "Content-Type: application/json" \
+ -d @/tmp/dashboard-payload.json 2>/dev/null)
+ rm -f "$tmpfile"
+
+ local status_str
+ status_str=$(echo "$result" | python3 -c "
+import sys, json
+try:
+ d = json.load(sys.stdin)
+ print(d.get('status', d.get('message', 'unknown')))
+except:
+ print('error')
+" 2>/dev/null || echo "error")
+
+ if [[ "$status_str" == "success" ]]; then
+ success " ${title} -> ${folder}"
+ installed=$((installed + 1))
+ else
+ error " ${title}: ${status_str}"
+ detail " Response: ${result}"
+ failed=$((failed + 1))
+ fi
+
+ # Clean up temp file in container
+ incus exec "${CLUSTER_REMOTE}:${MONITORING_CONTAINER}" -- rm -f /tmp/dashboard-payload.json 2>/dev/null || true
+
+ done < <(read_manifest)
+
+ echo
+ if [[ $failed -eq 0 ]]; then
+ success "Installed ${installed} dashboard(s)"
+ else
+ warn "Installed ${installed}, failed ${failed}"
+ fi
+}
+
+# ---------------------------------------------------------------------------
+# Export action
+# ---------------------------------------------------------------------------
+
+action_export() {
+ step "Exporting dashboards from Grafana"
+ echo
+
+ resolve_datasource_uids
+
+ local exported=0
+
+ if [[ -n "$FILTER_UID" ]]; then
+ # Export a single dashboard by UID
+ export_single_dashboard "$FILTER_UID"
+ return $?
+ fi
+
+ # Export all dashboards listed in the manifest
+ while IFS=$'\t' read -r file uid title folder category version description; do
+ if [[ -n "$FILTER_CATEGORY" && "$category" != "$FILTER_CATEGORY" ]]; then
+ continue
+ fi
+
+ if export_single_dashboard "$uid" "${DASHBOARDS_DIR}/${file}"; then
+ exported=$((exported + 1))
+ fi
+ done < <(read_manifest)
+
+ echo
+ success "Exported ${exported} dashboard(s)"
+}
+
+export_single_dashboard() {
+ local uid="$1"
+ local output_file="${2:-}"
+
+ # Fetch from Grafana API
+ local raw_response
+ raw_response=$(grafana_api GET "/api/dashboards/uid/${uid}" 2>/dev/null)
+
+ local has_dashboard
+ has_dashboard=$(echo "$raw_response" | python3 -c "
+import sys, json
+try:
+ d = json.load(sys.stdin)
+ if 'dashboard' in d:
+ print('yes')
+ else:
+ print('no')
+except:
+ print('no')
+" 2>/dev/null || echo "no")
+
+ if [[ "$has_dashboard" != "yes" ]]; then
+ warn "Dashboard '${uid}' not found in Grafana"
+ return 1
+ fi
+
+ # If no output file specified, find it in the manifest
+ if [[ -z "$output_file" ]]; then
+ output_file=$(read_manifest | while IFS=$'\t' read -r file muid title folder category version description; do
+ if [[ "$muid" == "$uid" ]]; then
+ echo "${DASHBOARDS_DIR}/${file}"
+ break
+ fi
+ done)
+ fi
+
+ if [[ -z "$output_file" ]]; then
+ output_file="${DASHBOARDS_DIR}/core/${uid}.json"
+ warn "Dashboard '${uid}' not in manifest, exporting to ${output_file}"
+ fi
+
+ if ! dry_run_guard "Export ${uid} to ${output_file}"; then
+ return 0
+ fi
+
+ # Clean and normalize the dashboard JSON
+ python3 << PYEOF > "$output_file"
+import json, sys
+
+raw = json.loads('''$(echo "$raw_response" | sed "s/'/\\\\'/g")''')
+dashboard = raw['dashboard']
+
+# Remove Grafana-internal fields
+for key in ['id', 'version', 'iteration']:
+ dashboard.pop(key, None)
+
+# Normalize datasource UIDs to variables
+json_str = json.dumps(dashboard, indent=2)
+prom_uid = "${PROMETHEUS_UID}"
+loki_uid = "${LOKI_UID:-}"
+if prom_uid:
+ json_str = json_str.replace(prom_uid, r'\${DS_PROMETHEUS}')
+if loki_uid:
+ json_str = json_str.replace(loki_uid, r'\${DS_LOKI}')
+
+# Re-parse to ensure valid JSON
+dashboard = json.loads(json_str.replace(r'\${DS_PROMETHEUS}', '\${DS_PROMETHEUS}').replace(r'\${DS_LOKI}', '\${DS_LOKI}'))
+
+# Write with consistent formatting
+json_str = json.dumps(dashboard, indent=2)
+# Restore the variable syntax that json.dumps may have escaped
+json_str = json_str.replace(r'\${DS_PROMETHEUS}', '\${DS_PROMETHEUS}')
+json_str = json_str.replace(r'\${DS_LOKI}', '\${DS_LOKI}')
+print(json_str)
+PYEOF
+
+ if [[ $? -eq 0 ]]; then
+ success " Exported: ${uid} -> $(basename "$output_file")"
+ return 0
+ else
+ error " Failed to export: ${uid}"
+ return 1
+ fi
+}
+
+# ---------------------------------------------------------------------------
+# List action
+# ---------------------------------------------------------------------------
+
+action_list() {
+ step "Dashboard Inventory"
+ echo
+
+ # Try to get installed dashboards from Grafana
+ local installed_json=""
+ installed_json=$(grafana_api GET "/api/search?type=dash-db" 2>/dev/null || echo "[]")
+
+ printf " ${BOLD}%-35s %-30s %-20s %s${RESET}\n" "UID" "Title" "Folder" "Status"
+ printf " %-35s %-30s %-20s %s\n" "---" "-----" "------" "------"
+
+ while IFS=$'\t' read -r file uid title folder category version description; do
+ if [[ -n "$FILTER_CATEGORY" && "$category" != "$FILTER_CATEGORY" ]]; then
+ continue
+ fi
+
+ local full_path="${DASHBOARDS_DIR}/${file}"
+ local local_status="missing"
+ if [[ -f "$full_path" ]]; then
+ local_status="local"
+ fi
+
+ # Check if installed in Grafana
+ local grafana_status
+ grafana_status=$(echo "$installed_json" | python3 -c "
+import sys, json
+try:
+ dashboards = json.load(sys.stdin)
+ for d in dashboards:
+ if d.get('uid') == '${uid}':
+ print('installed')
+ break
+ else:
+ print('not installed')
+except:
+ print('unknown')
+" 2>/dev/null || echo "unknown")
+
+ local status_display
+ if [[ "$local_status" == "missing" ]]; then
+ status_display="${RED}file missing${RESET}"
+ elif [[ "$grafana_status" == "installed" ]]; then
+ status_display="${GREEN}installed${RESET}"
+ elif [[ "$grafana_status" == "not installed" ]]; then
+ status_display="${YELLOW}not installed${RESET}"
+ else
+ status_display="${DIM}${local_status}${RESET}"
+ fi
+
+ printf " %-35s %-30s %-20s " "$uid" "$title" "$folder"
+ echo -e "$status_display"
+
+ done < <(read_manifest)
+}
+
+# ---------------------------------------------------------------------------
+# Validate action
+# ---------------------------------------------------------------------------
+
+action_validate() {
+ step "Validating dashboard JSON files"
+ echo
+
+ local total=0
+ local passed=0
+ local warnings=0
+ local errors=0
+
+ while IFS=$'\t' read -r file uid title folder category version description; do
+ if [[ -n "$FILTER_CATEGORY" && "$category" != "$FILTER_CATEGORY" ]]; then
+ continue
+ fi
+
+ local full_path="${DASHBOARDS_DIR}/${file}"
+ total=$((total + 1))
+
+ if [[ ! -f "$full_path" ]]; then
+ error " ${file}: FILE NOT FOUND"
+ errors=$((errors + 1))
+ continue
+ fi
+
+ local issues=0
+ local warns=0
+ local basename_file
+ basename_file=$(basename "$full_path")
+
+ # Check 1: Valid JSON
+ if ! python3 -c "import json; json.load(open('${full_path}'))" 2>/dev/null; then
+ error " ${basename_file}: INVALID JSON"
+ errors=$((errors + 1))
+ continue
+ fi
+
+ # Run all checks via python
+ local check_result
+ check_result=$(python3 << PYEOF
+import json
+
+with open('${full_path}') as f:
+ d = json.load(f)
+
+issues = []
+warns = []
+
+# Check 2: UID present and matches manifest
+json_uid = d.get('uid', '')
+if not json_uid:
+ issues.append('missing uid field')
+elif json_uid != '${uid}':
+ issues.append(f'uid mismatch: JSON has "{json_uid}", manifest has "${uid}"')
+
+# Check 3: Title present
+if not d.get('title'):
+ issues.append('missing title field')
+
+# Check 4: Description present
+if not d.get('description'):
+ warns.append('missing description field')
+
+# Check 5: No hardcoded datasource UIDs (should use variables)
+json_str = json.dumps(d)
+# Known Grafana internal UIDs that are OK
+safe_uids = ['-- Grafana --']
+# Check for UIDs that look like Grafana-generated ones
+import re
+# Match patterns like "uid": "PBFA97CFB590B2093" (hex-like, not a variable)
+uid_pattern = re.compile(r'"uid":\s*"([A-Za-z0-9]{16,})"')
+for match in uid_pattern.finditer(json_str):
+ found_uid = match.group(1)
+ if found_uid not in safe_uids and not found_uid.startswith('\${'):
+ issues.append(f'hardcoded datasource UID: {found_uid}')
+ break
+
+# Check 6: Datasource variable usage
+if '\${DS_PROMETHEUS}' not in json_str and '\${DS_LOKI}' not in json_str:
+ # Check if it has any datasource references at all
+ if '"type": "prometheus"' in json_str or '"type": "loki"' in json_str:
+ warns.append('no datasource variables found (uses hardcoded UIDs?)')
+
+# Check 7: Has panels
+panels = d.get('panels', [])
+if not panels:
+ warns.append('no panels defined')
+
+# Check 8: Check for id field (should not be present in stored files)
+if 'id' in d and d['id'] is not None:
+ warns.append('contains "id" field (should be null or absent for portability)')
+
+for i in issues:
+ print(f'ERROR:{i}')
+for w in warns:
+ print(f'WARN:{w}')
+if not issues:
+ print('OK')
+PYEOF
+ )
+
+ local has_error=false
+ while IFS= read -r line; do
+ case "$line" in
+ ERROR:*)
+ error " ${basename_file}: ${line#ERROR:}"
+ issues=$((issues + 1))
+ has_error=true
+ ;;
+ WARN:*)
+ warn " ${basename_file}: ${line#WARN:}"
+ warns=$((warns + 1))
+ ;;
+ OK)
+ ;;
+ esac
+ done <<< "$check_result"
+
+ if [[ "$has_error" == true ]]; then
+ errors=$((errors + 1))
+ else
+ if [[ $warns -gt 0 ]]; then
+ warnings=$((warnings + warns))
+ fi
+ passed=$((passed + 1))
+ success " ${basename_file}: valid"
+ fi
+
+ done < <(read_manifest)
+
+ echo
+ info "${total} dashboard(s) checked: ${passed} passed, ${errors} errors, ${warnings} warnings"
+
+ if [[ $errors -gt 0 ]]; then
+ return 1
+ fi
+}
+
+# ---------------------------------------------------------------------------
+# Status action
+# ---------------------------------------------------------------------------
+
+action_status() {
+ step "Dashboard Status (local vs installed)"
+ echo
+
+ local installed_json
+ installed_json=$(grafana_api GET "/api/search?type=dash-db" 2>/dev/null || echo "[]")
+
+ printf " ${BOLD}%-30s %-15s %-15s %s${RESET}\n" "Dashboard" "Local" "Grafana" "Match"
+ printf " %-30s %-15s %-15s %s\n" "---------" "-----" "-------" "-----"
+
+ while IFS=$'\t' read -r file uid title folder category version description; do
+ local full_path="${DASHBOARDS_DIR}/${file}"
+
+ local local_ver="missing"
+ if [[ -f "$full_path" ]]; then
+ local_ver="v${version}"
+ fi
+
+ local grafana_ver
+ grafana_ver=$(echo "$installed_json" | python3 -c "
+import sys, json
+try:
+ for d in json.load(sys.stdin):
+ if d.get('uid') == '${uid}':
+ print('installed')
+ break
+ else:
+ print('absent')
+except:
+ print('unknown')
+" 2>/dev/null || echo "unknown")
+
+ local match_str
+ if [[ "$local_ver" == "missing" ]]; then
+ match_str="${RED}file missing${RESET}"
+ elif [[ "$grafana_ver" == "absent" ]]; then
+ match_str="${YELLOW}not installed${RESET}"
+ elif [[ "$grafana_ver" == "installed" ]]; then
+ match_str="${GREEN}synced${RESET}"
+ else
+ match_str="${DIM}unknown${RESET}"
+ fi
+
+ printf " %-30s %-15s %-15s " "$title" "$local_ver" "$grafana_ver"
+ echo -e "$match_str"
+
+ done < <(read_manifest)
+}
+
+# ---------------------------------------------------------------------------
+# Argument parsing
+# ---------------------------------------------------------------------------
+
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --install) ACTION="install";;
+ --export) ACTION="export";;
+ --list) ACTION="list";;
+ --validate) ACTION="validate";;
+ --status) ACTION="status";;
+ --category) FILTER_CATEGORY="$2"; shift;;
+ --file) FILTER_FILE="$2"; shift;;
+ --uid) FILTER_UID="$2"; shift;;
+ -r|--remote) CLUSTER_REMOTE="$2"; shift;;
+ -n|--dry-run) DRY_RUN=true;;
+ -v|--verbose) VERBOSE=true;;
+ -q|--quiet) QUIET=true;;
+ -h|--help) usage; exit 0;;
+ *)
+ error "Unknown option: $1"
+ echo "Run '${SCRIPT_NAME} --help' for usage."
+ exit 1
+ ;;
+ esac
+ shift
+ done
+
+ if [[ -z "$ACTION" ]]; then
+ error "No action specified"
+ echo "Run '${SCRIPT_NAME} --help' for usage."
+ exit 1
+ fi
+}
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+main() {
+ setup_colors
+ parse_args "$@"
+
+ echo
+ case "$ACTION" in
+ install) action_install;;
+ export) action_export;;
+ list) action_list;;
+ validate) action_validate;;
+ status) action_status;;
+ esac
+}
+
+main "$@"
diff --git a/incusos/observability-dashboards/community/.gitkeep b/incusos/observability-dashboards/community/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/incusos/observability-dashboards/host-resources.json b/incusos/observability-dashboards/core/capacity-planning.json
similarity index 53%
rename from incusos/observability-dashboards/host-resources.json
rename to incusos/observability-dashboards/core/capacity-planning.json
index 282f7fa..2356711 100644
--- a/incusos/observability-dashboards/host-resources.json
+++ b/incusos/observability-dashboards/core/capacity-planning.json
@@ -1,31 +1,35 @@
{
- "uid": "incus-host-resources",
- "title": "Host Resources (IncusOS Nodes)",
- "description": "Physical host monitoring for IncusOS cluster nodes: CPU, memory, disk, and network from node_exporter metrics.",
- "tags": ["incusos", "host", "node-exporter"],
+ "uid": "incus-capacity-planning",
+ "title": "Capacity Planning",
+ "description": "CPU/memory headroom per node, resource trends, disk space forecast with predict_linear, instance count trends.",
+ "tags": ["incus", "capacity", "operations"],
"timezone": "browser",
"schemaVersion": 39,
- "version": 1,
- "refresh": "30s",
- "time": {
- "from": "now-1h",
- "to": "now"
- },
+ "refresh": "5m",
+ "time": { "from": "now-7d", "to": "now" },
"fiscalYearStartMonth": 0,
"liveNow": false,
"editable": true,
"graphTooltip": 1,
- "templating": {
- "list": []
- },
+ "links": [
+ {
+ "asDropdown": true,
+ "icon": "external link",
+ "includeVars": true,
+ "keepTime": true,
+ "tags": ["incus"],
+ "targetBlank": false,
+ "title": "Incus Dashboards",
+ "tooltip": "Navigate to other Incus dashboards",
+ "type": "dashboards"
+ }
+ ],
+ "templating": { "list": [] },
"annotations": {
"list": [
{
"builtIn": 1,
- "datasource": {
- "type": "grafana",
- "uid": "-- Grafana --"
- },
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
@@ -37,25 +41,114 @@
"panels": [
{
"type": "row",
- "title": "CPU",
+ "title": "Current Headroom",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 1,
"panels": []
},
{
- "type": "timeseries",
- "title": "CPU Usage % by Node",
- "description": "Overall CPU utilization per host node, calculated as 1 minus the average idle CPU rate.",
- "gridPos": { "h": 8, "w": 24, "x": 0, "y": 1 },
+ "type": "bargauge",
+ "title": "CPU Headroom per Node",
+ "description": "Available (idle) CPU percentage per host node. Higher values mean more headroom for additional workloads.",
+ "gridPos": { "h": 6, "w": 12, "x": 0, "y": 1 },
"id": 2,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "(1 - avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[$__rate_interval]))) * 100",
+ "expr": "avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[$__rate_interval])) * 100",
+ "legendFormat": "{{instance}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "red", "value": null },
+ { "color": "orange", "value": 20 },
+ { "color": "yellow", "value": 40 },
+ { "color": "green", "value": 60 }
+ ]
+ },
+ "unit": "percent",
+ "min": 0,
+ "max": 100,
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "horizontal",
+ "displayMode": "gradient",
+ "showUnfilled": true,
+ "minVizWidth": 8,
+ "minVizHeight": 16,
+ "valueMode": "color"
+ }
+ },
+ {
+ "type": "bargauge",
+ "title": "Memory Headroom per Node",
+ "description": "Available memory percentage per host node. Higher values mean more headroom for additional workloads.",
+ "gridPos": { "h": 6, "w": 12, "x": 12, "y": 1 },
+ "id": 3,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100",
+ "legendFormat": "{{instance}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "red", "value": null },
+ { "color": "orange", "value": 20 },
+ { "color": "yellow", "value": 40 },
+ { "color": "green", "value": 60 }
+ ]
+ },
+ "unit": "percent",
+ "min": 0,
+ "max": 100,
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "horizontal",
+ "displayMode": "gradient",
+ "showUnfilled": true,
+ "minVizWidth": 8,
+ "minVizHeight": 16,
+ "valueMode": "color"
+ }
+ },
+ {
+ "type": "row",
+ "title": "Resource Trends",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 7 },
+ "id": 4,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "CPU Usage Trend (7-day)",
+ "description": "CPU utilization percentage per host node over the past 7 days. Helps identify sustained usage patterns and growth trends.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
+ "id": 5,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(1 - avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m]))) * 100",
"legendFormat": "{{instance}}",
"refId": "A"
}
@@ -75,7 +168,7 @@
"axisPlacement": "auto",
"axisLabel": "",
"scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "line" }
+ "thresholdsStyle": { "mode": "off" }
},
"unit": "percent",
"min": 0,
@@ -97,24 +190,13 @@
"legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
}
},
- {
- "type": "row",
- "title": "Memory",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 },
- "id": 3,
- "panels": []
- },
{
"type": "timeseries",
- "title": "Memory Usage % by Node",
- "description": "Memory utilization percentage per host node, calculated from available vs total memory.",
- "gridPos": { "h": 8, "w": 24, "x": 0, "y": 10 },
- "id": 4,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "title": "Memory Usage Trend (7-day)",
+ "description": "Memory utilization percentage per host node over the past 7 days. Helps identify sustained usage patterns and growth trends.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
+ "id": 6,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100",
@@ -137,7 +219,7 @@
"axisPlacement": "auto",
"axisLabel": "",
"scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "line" }
+ "thresholdsStyle": { "mode": "off" }
},
"unit": "percent",
"min": 0,
@@ -161,27 +243,29 @@
},
{
"type": "row",
- "title": "Disk",
+ "title": "Disk Space Forecast",
"collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 18 },
- "id": 5,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 16 },
+ "id": 7,
"panels": []
},
{
"type": "timeseries",
- "title": "Root Filesystem Available Space",
- "description": "Available disk space on the root filesystem (mountpoint='/') per host node.",
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 19 },
- "id": 6,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "title": "Disk Space Forecast (30-day projection)",
+ "description": "Current available disk space on root filesystem with a 30-day linear projection based on the past 7 days of data. Dashed lines show the forecast; if a forecast line drops below zero, the disk will fill up within 30 days at current consumption rates.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 17 },
+ "id": 8,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "node_filesystem_avail_bytes{mountpoint=\"/\"}",
- "legendFormat": "{{instance}}",
+ "legendFormat": "{{instance}} actual",
"refId": "A"
+ },
+ {
+ "expr": "predict_linear(node_filesystem_avail_bytes{mountpoint=\"/\"}[7d], 30*24*3600)",
+ "legendFormat": "{{instance}} forecast",
+ "refId": "B"
}
],
"fieldConfig": {
@@ -189,7 +273,7 @@
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
- "lineWidth": 1,
+ "lineWidth": 2,
"fillOpacity": 15,
"gradientMode": "none",
"spanNulls": false,
@@ -205,140 +289,43 @@
"color": { "mode": "palette-classic" },
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
}
},
- "overrides": []
+ "overrides": [
+ {
+ "matcher": { "id": "byRegexp", "options": ".*forecast$" },
+ "properties": [
+ { "id": "custom.lineStyle", "value": { "fill": "dash", "dash": [10, 10] } },
+ { "id": "custom.fillOpacity", "value": 0 }
+ ]
+ }
+ ]
},
"options": {
"tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["lastNotNull", "min"] }
- }
- },
- {
- "type": "timeseries",
- "title": "Disk Read Rate by Node",
- "description": "Disk read throughput (bytes/sec) aggregated per host node.",
- "gridPos": { "h": 8, "w": 6, "x": 12, "y": 19 },
- "id": 7,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum by (instance) (rate(node_disk_read_bytes_total[$__rate_interval]))",
- "legendFormat": "{{instance}}",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 15,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "Bps",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "list", "placement": "bottom", "calcs": [] }
- }
- },
- {
- "type": "timeseries",
- "title": "Disk Write Rate by Node",
- "description": "Disk write throughput (bytes/sec) aggregated per host node.",
- "gridPos": { "h": 8, "w": 6, "x": 18, "y": 19 },
- "id": 8,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum by (instance) (rate(node_disk_written_bytes_total[$__rate_interval]))",
- "legendFormat": "{{instance}}",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 15,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "Bps",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "list", "placement": "bottom", "calcs": [] }
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
}
},
{
"type": "row",
- "title": "Network",
+ "title": "Instance Trends",
"collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 27 },
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 25 },
"id": 9,
"panels": []
},
{
"type": "timeseries",
- "title": "Network Receive Rate by Device & Node",
- "description": "Inbound network throughput per network device and host node.",
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 28 },
+ "title": "Instance Count Over Time",
+ "description": "Total number of running Incus instances over time. Tracks growth and churn in the cluster workload count.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 26 },
"id": 10,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "rate(node_network_receive_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[$__rate_interval])",
- "legendFormat": "{{instance}} - {{device}} (rx)",
+ "expr": "count(incus_uptime_seconds)",
+ "legendFormat": "Instances",
"refId": "A"
}
],
@@ -347,8 +334,8 @@
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 15,
+ "lineWidth": 2,
+ "fillOpacity": 25,
"gradientMode": "none",
"spanNulls": false,
"showPoints": "never",
@@ -359,37 +346,37 @@
"scaleDistribution": { "type": "linear" },
"thresholdsStyle": { "mode": "off" }
},
- "unit": "Bps",
+ "unit": "short",
"color": { "mode": "palette-classic" },
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
}
},
"overrides": []
},
"options": {
"tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
}
},
{
"type": "timeseries",
- "title": "Network Transmit Rate by Device & Node",
- "description": "Outbound network throughput per network device and host node.",
- "gridPos": { "h": 8, "w": 12, "x": 12, "y": 28 },
+ "title": "Memory Allocation vs Usage per Instance",
+ "description": "Compares total allocated memory (MemTotal) against actual resident memory (RSS) for each Incus instance. A large gap between allocated and used indicates overprovisioning; converging lines indicate an instance may need more memory.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 26 },
"id": 11,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "rate(node_network_transmit_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[$__rate_interval])",
- "legendFormat": "{{instance}} - {{device}} (tx)",
+ "expr": "incus_memory_MemTotal_bytes",
+ "legendFormat": "{{name}} allocated",
"refId": "A"
+ },
+ {
+ "expr": "incus_memory_RSS_bytes",
+ "legendFormat": "{{name}} used (RSS)",
+ "refId": "B"
}
],
"fieldConfig": {
@@ -398,7 +385,7 @@
"drawStyle": "line",
"lineInterpolation": "smooth",
"lineWidth": 1,
- "fillOpacity": 15,
+ "fillOpacity": 0,
"gradientMode": "none",
"spanNulls": false,
"showPoints": "never",
@@ -409,20 +396,32 @@
"scaleDistribution": { "type": "linear" },
"thresholdsStyle": { "mode": "off" }
},
- "unit": "Bps",
+ "unit": "bytes",
"color": { "mode": "palette-classic" },
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
}
},
- "overrides": []
+ "overrides": [
+ {
+ "matcher": { "id": "byRegexp", "options": ".*allocated$" },
+ "properties": [
+ { "id": "custom.lineStyle", "value": { "fill": "dash", "dash": [10, 10] } },
+ { "id": "custom.lineWidth", "value": 2 }
+ ]
+ },
+ {
+ "matcher": { "id": "byRegexp", "options": ".*used \\(RSS\\)$" },
+ "properties": [
+ { "id": "custom.fillOpacity", "value": 15 }
+ ]
+ }
+ ]
},
"options": {
"tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
}
}
]
diff --git a/incusos/observability-dashboards/core/cluster-overview.json b/incusos/observability-dashboards/core/cluster-overview.json
new file mode 100644
index 0000000..28e9e7f
--- /dev/null
+++ b/incusos/observability-dashboards/core/cluster-overview.json
@@ -0,0 +1,674 @@
+{
+ "uid": "incus-cluster-overview",
+ "title": "Incus Cluster Overview",
+ "description": "Overview of all Incus instances: CPU, memory, network, disk I/O, and instance status table with drill-down links.",
+ "tags": ["incus", "cluster", "containers"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "refresh": "30s",
+ "time": { "from": "now-1h", "to": "now" },
+ "fiscalYearStartMonth": 0,
+ "liveNow": false,
+ "editable": true,
+ "graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true,
+ "icon": "external link",
+ "includeVars": true,
+ "keepTime": true,
+ "tags": ["incus"],
+ "targetBlank": false,
+ "title": "Incus Dashboards",
+ "tooltip": "Navigate to other Incus dashboards",
+ "type": "dashboards"
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(incus_uptime_seconds, name)",
+ "includeAll": true,
+ "multi": true,
+ "name": "instance",
+ "query": "label_values(incus_uptime_seconds, name)",
+ "refresh": 2,
+ "regex": "",
+ "sort": 1,
+ "type": "query",
+ "label": "Instance"
+ },
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(incus_uptime_seconds, instance)",
+ "includeAll": true,
+ "multi": true,
+ "name": "node",
+ "query": "label_values(incus_uptime_seconds, instance)",
+ "refresh": 2,
+ "regex": "",
+ "sort": 1,
+ "type": "query",
+ "label": "Node"
+ }
+ ]
+ },
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "panels": [
+ {
+ "type": "row",
+ "title": "Cluster Status",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 1,
+ "panels": []
+ },
+ {
+ "type": "stat",
+ "title": "Total Instances",
+ "description": "Number of running Incus instances (reporting uptime).",
+ "gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
+ "id": 2,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "count(incus_uptime_seconds{instance=~\"$node\", name=~\"$instance\"})",
+ "legendFormat": "Instances",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 10 },
+ { "color": "red", "value": 50 }
+ ]
+ },
+ "unit": "none",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Total CPUs",
+ "description": "Sum of effective CPUs across all Incus instances.",
+ "gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
+ "id": 3,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(incus_cpu_effective_total{instance=~\"$node\", name=~\"$instance\"})",
+ "legendFormat": "CPUs",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ },
+ "unit": "none",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Total Memory",
+ "description": "Sum of total memory across all Incus instances.",
+ "gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
+ "id": 4,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(incus_memory_MemTotal_bytes{instance=~\"$node\", name=~\"$instance\"})",
+ "legendFormat": "Total Memory",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ },
+ "unit": "bytes",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "gauge",
+ "title": "Cluster Memory Utilization",
+ "description": "Overall cluster memory utilization (RSS / Total) across all instances.",
+ "gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
+ "id": 40,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(sum(incus_memory_RSS_bytes{instance=~\"$node\", name=~\"$instance\"}) / sum(incus_memory_MemTotal_bytes{instance=~\"$node\", name=~\"$instance\"})) * 100",
+ "legendFormat": "Utilization",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 60 },
+ { "color": "orange", "value": 80 },
+ { "color": "red", "value": 90 }
+ ]
+ },
+ "unit": "percent",
+ "min": 0,
+ "max": 100,
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "orientation": "auto"
+ }
+ },
+ {
+ "type": "row",
+ "title": "Instance Status",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
+ "id": 50,
+ "panels": []
+ },
+ {
+ "type": "table",
+ "title": "Instance Status Table",
+ "description": "Per-instance summary: uptime, CPU usage, memory RSS, total memory. Click instance name to drill down.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 6 },
+ "id": 51,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "incus_uptime_seconds{instance=~\"$node\", name=~\"$instance\"}",
+ "legendFormat": "",
+ "refId": "A",
+ "format": "table",
+ "instant": true
+ },
+ {
+ "expr": "sum by (name, instance) (rate(incus_cpu_seconds_total{instance=~\"$node\", name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "",
+ "refId": "B",
+ "format": "table",
+ "instant": true
+ },
+ {
+ "expr": "incus_memory_RSS_bytes{instance=~\"$node\", name=~\"$instance\"}",
+ "legendFormat": "",
+ "refId": "C",
+ "format": "table",
+ "instant": true
+ },
+ {
+ "expr": "incus_memory_MemTotal_bytes{instance=~\"$node\", name=~\"$instance\"}",
+ "legendFormat": "",
+ "refId": "D",
+ "format": "table",
+ "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "align": "auto",
+ "displayMode": "auto",
+ "inspect": false,
+ "filterable": true
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ },
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": [
+ {
+ "matcher": { "id": "byName", "options": "name" },
+ "properties": [
+ { "id": "displayName", "value": "Instance" },
+ { "id": "custom.width", "value": 200 },
+ {
+ "id": "links",
+ "value": [
+ {
+ "title": "View instance details",
+ "url": "/d/incus-instance-deep-dive?var-instance=${__value.text}&${__url_time_range}",
+ "targetBlank": false
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "instance" },
+ "properties": [{ "id": "displayName", "value": "Node" }]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Value #A" },
+ "properties": [
+ { "id": "displayName", "value": "Uptime" },
+ { "id": "unit", "value": "s" }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Value #B" },
+ "properties": [
+ { "id": "displayName", "value": "CPU/s" },
+ { "id": "unit", "value": "short" },
+ { "id": "decimals", "value": 3 }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Value #C" },
+ "properties": [
+ { "id": "displayName", "value": "Memory RSS" },
+ { "id": "unit", "value": "bytes" }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Value #D" },
+ "properties": [
+ { "id": "displayName", "value": "Total Memory" },
+ { "id": "unit", "value": "bytes" }
+ ]
+ }
+ ]
+ },
+ "options": {
+ "showHeader": true,
+ "sortBy": [{ "displayName": "Instance", "desc": false }],
+ "footer": { "show": false, "reducer": ["sum"], "fields": "" }
+ },
+ "transformations": [
+ { "id": "merge", "options": {} },
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": { "Time": true, "__name__": true, "job": true },
+ "indexByName": {
+ "name": 0, "instance": 1,
+ "Value #A": 2, "Value #B": 3, "Value #C": 4, "Value #D": 5
+ },
+ "renameByName": {}
+ }
+ }
+ ]
+ },
+ {
+ "type": "row",
+ "title": "Per-Instance CPU",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
+ "id": 5,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "CPU Usage by Instance",
+ "description": "Per-second CPU time consumed by each Incus instance.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 15 },
+ "id": 6,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_cpu_seconds_total{instance=~\"$node\", name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "scheme",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ },
+ "links": [
+ {
+ "title": "Drill down to ${__field.labels.name}",
+ "url": "/d/incus-instance-deep-dive?var-instance=${__field.labels.name}&${__url_time_range}",
+ "targetBlank": false
+ }
+ ]
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Per-Instance Memory",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
+ "id": 7,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Memory RSS by Instance",
+ "description": "Resident set size (RSS) memory usage per Incus instance over time.",
+ "gridPos": { "h": 8, "w": 14, "x": 0, "y": 24 },
+ "id": 8,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "incus_memory_RSS_bytes{instance=~\"$node\", name=~\"$instance\"}",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "bytes",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "bargauge",
+ "title": "Memory Usage % (RSS / Total)",
+ "description": "Current memory utilization per instance as RSS divided by total memory.",
+ "gridPos": { "h": 8, "w": 10, "x": 14, "y": 24 },
+ "id": 9,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(incus_memory_RSS_bytes{instance=~\"$node\", name=~\"$instance\"} / incus_memory_MemTotal_bytes{instance=~\"$node\", name=~\"$instance\"}) * 100",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 60 },
+ { "color": "orange", "value": 80 },
+ { "color": "red", "value": 90 }
+ ]
+ },
+ "unit": "percent",
+ "min": 0,
+ "max": 100,
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "horizontal",
+ "displayMode": "gradient",
+ "showUnfilled": true,
+ "minVizWidth": 8,
+ "minVizHeight": 16,
+ "valueMode": "color"
+ }
+ },
+ {
+ "type": "row",
+ "title": "Network I/O",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 },
+ "id": 10,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Network Receive Rate by Instance",
+ "description": "Inbound network throughput per Incus instance.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 },
+ "id": 11,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_network_receive_bytes_total{instance=~\"$node\", name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Network Transmit Rate by Instance",
+ "description": "Outbound network throughput per Incus instance.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 },
+ "id": 12,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_network_transmit_bytes_total{instance=~\"$node\", name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Disk I/O",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 41 },
+ "id": 13,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk Read Rate by Instance",
+ "description": "Disk read throughput per Incus instance.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 42 },
+ "id": 14,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_disk_read_bytes_total{instance=~\"$node\", name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk Write Rate by Instance",
+ "description": "Disk write throughput per Incus instance.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 42 },
+ "id": 15,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_disk_written_bytes_total{instance=~\"$node\", name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ }
+ ]
+}
diff --git a/incusos/observability-dashboards/core/haproxy-traffic.json b/incusos/observability-dashboards/core/haproxy-traffic.json
new file mode 100644
index 0000000..4d65a0b
--- /dev/null
+++ b/incusos/observability-dashboards/core/haproxy-traffic.json
@@ -0,0 +1,910 @@
+{
+ "uid": "incus-haproxy-traffic",
+ "title": "HAProxy Traffic",
+ "description": "HAProxy load balancer monitoring: request rates, response codes, backend health, traffic volume, sessions, and performance metrics.",
+ "tags": ["incus", "haproxy", "loadbalancer"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "version": 2,
+ "refresh": "30s",
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "fiscalYearStartMonth": 0,
+ "liveNow": false,
+ "editable": true,
+ "graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true,
+ "icon": "external link",
+ "includeVars": true,
+ "keepTime": true,
+ "tags": ["incus"],
+ "targetBlank": false,
+ "title": "Incus Dashboards",
+ "tooltip": "Navigate to other Incus dashboards",
+ "type": "dashboards"
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(haproxy_backend_status, proxy)",
+ "includeAll": true,
+ "multi": true,
+ "name": "proxy",
+ "query": "label_values(haproxy_backend_status, proxy)",
+ "refresh": 2,
+ "regex": "",
+ "sort": 1,
+ "type": "query",
+ "label": "Proxy"
+ },
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(haproxy_server_status, server)",
+ "includeAll": true,
+ "multi": true,
+ "name": "server",
+ "query": "label_values(haproxy_server_status, server)",
+ "refresh": 2,
+ "regex": "",
+ "sort": 1,
+ "type": "query",
+ "label": "Server"
+ }
+ ]
+ },
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "panels": [
+ {
+ "type": "row",
+ "title": "Overview",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 1,
+ "panels": []
+ },
+ {
+ "type": "stat",
+ "title": "Active Backend Servers",
+ "description": "Total number of active (healthy) servers across selected HAProxy backends.",
+ "gridPos": { "h": 4, "w": 5, "x": 0, "y": 1 },
+ "id": 2,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(haproxy_backend_active_servers{proxy=~\"$proxy\"})",
+ "legendFormat": "Active Servers",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "red", "value": null },
+ { "color": "yellow", "value": 1 },
+ { "color": "green", "value": 2 }
+ ]
+ },
+ "unit": "none",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Current Sessions",
+ "description": "Total current active sessions across selected HAProxy backends.",
+ "gridPos": { "h": 4, "w": 5, "x": 5, "y": 1 },
+ "id": 3,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(haproxy_backend_current_sessions{proxy=~\"$proxy\"})",
+ "legendFormat": "Sessions",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 100 },
+ { "color": "red", "value": 500 }
+ ]
+ },
+ "unit": "none",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "HTTP Requests/s",
+ "description": "Current rate of HTTP requests per second across selected HAProxy frontends.",
+ "gridPos": { "h": 4, "w": 5, "x": 10, "y": 1 },
+ "id": 4,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(haproxy_frontend_http_requests_total{proxy=~\"$proxy\"}[$__rate_interval]))",
+ "legendFormat": "Requests/s",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ },
+ "unit": "reqps",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "HTTP 4xx Rate",
+ "description": "Rate of HTTP 4xx client error responses per second. Elevated rates may indicate bad requests or missing resources.",
+ "gridPos": { "h": 4, "w": 4, "x": 15, "y": 1 },
+ "id": 14,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(haproxy_frontend_http_responses_total{proxy=~\"$proxy\", code=\"4xx\"}[$__rate_interval]))",
+ "legendFormat": "4xx/s",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 0.1 },
+ { "color": "red", "value": 1 }
+ ]
+ },
+ "unit": "reqps",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "HTTP 5xx Rate",
+ "description": "Rate of HTTP 5xx server error responses per second. Any sustained rate indicates backend failures requiring investigation.",
+ "gridPos": { "h": 4, "w": 5, "x": 19, "y": 1 },
+ "id": 15,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(haproxy_frontend_http_responses_total{proxy=~\"$proxy\", code=\"5xx\"}[$__rate_interval]))",
+ "legendFormat": "5xx/s",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 0.01 },
+ { "color": "red", "value": 0.1 }
+ ]
+ },
+ "unit": "reqps",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "row",
+ "title": "HTTP Requests",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
+ "id": 5,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "HTTP Request Rate by Frontend",
+ "description": "Rate of HTTP requests per second, broken down by HAProxy frontend/proxy. Useful for identifying which frontends carry the most traffic.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
+ "id": 6,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (proxy) (rate(haproxy_frontend_http_requests_total{proxy=~\"$proxy\"}[$__rate_interval]))",
+ "legendFormat": "{{proxy}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 20,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "reqps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Response Code Distribution",
+ "description": "HTTP response code distribution across selected frontends, stacked by response class (2xx, 3xx, 4xx, 5xx). Helps identify error trends and traffic composition.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
+ "id": 16,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (code) (rate(haproxy_frontend_http_responses_total{proxy=~\"$proxy\"}[$__rate_interval]))",
+ "legendFormat": "{{code}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 50,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "normal", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "reqps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": { "id": "byName", "options": "2xx" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "3xx" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "blue" } }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "4xx" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "yellow" } }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "5xx" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }
+ ]
+ }
+ ]
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Backend Health",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
+ "id": 7,
+ "panels": []
+ },
+ {
+ "type": "table",
+ "title": "Backend Status",
+ "description": "Current status, active server count, and average response time for each HAProxy backend. Filterable by proxy and server template variables.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
+ "id": 8,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "haproxy_backend_status{proxy=~\"$proxy\"}",
+ "legendFormat": "",
+ "refId": "A",
+ "format": "table",
+ "instant": true
+ },
+ {
+ "expr": "haproxy_backend_active_servers{proxy=~\"$proxy\"}",
+ "legendFormat": "",
+ "refId": "B",
+ "format": "table",
+ "instant": true
+ },
+ {
+ "expr": "haproxy_server_response_time_average_seconds{proxy=~\"$proxy\", server=~\"$server\"}",
+ "legendFormat": "",
+ "refId": "C",
+ "format": "table",
+ "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "align": "auto",
+ "displayMode": "auto",
+ "inspect": false,
+ "filterable": true
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ },
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": [
+ {
+ "matcher": { "id": "byName", "options": "Value #A" },
+ "properties": [
+ { "id": "displayName", "value": "Status" },
+ {
+ "id": "mappings",
+ "value": [
+ { "type": "value", "options": { "1": { "text": "UP", "color": "green" }, "0": { "text": "DOWN", "color": "red" } } }
+ ]
+ }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Value #B" },
+ "properties": [
+ { "id": "displayName", "value": "Active Servers" }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Value #C" },
+ "properties": [
+ { "id": "displayName", "value": "Avg Response Time" },
+ { "id": "unit", "value": "s" }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "proxy" },
+ "properties": [
+ { "id": "displayName", "value": "Backend" }
+ ]
+ }
+ ]
+ },
+ "options": {
+ "showHeader": true,
+ "sortBy": [],
+ "footer": {
+ "show": false,
+ "reducer": ["sum"],
+ "fields": ""
+ }
+ },
+ "transformations": [
+ {
+ "id": "merge",
+ "options": {}
+ },
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true,
+ "__name__": true,
+ "instance": false,
+ "job": true
+ },
+ "indexByName": {},
+ "renameByName": {}
+ }
+ }
+ ]
+ },
+ {
+ "type": "state-timeline",
+ "title": "Server Status",
+ "description": "Timeline view of individual HAProxy server health status. Green indicates UP, red indicates DOWN. Helps identify flapping servers and outage windows.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
+ "id": 17,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "haproxy_server_status{proxy=~\"$proxy\", server=~\"$server\"}",
+ "legendFormat": "{{proxy}} / {{server}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "lineWidth": 0,
+ "fillOpacity": 80
+ },
+ "color": { "mode": "thresholds" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "red", "value": null },
+ { "color": "green", "value": 1 }
+ ]
+ },
+ "mappings": [
+ {
+ "type": "value",
+ "options": {
+ "0": { "text": "DOWN", "color": "red" },
+ "1": { "text": "UP", "color": "green" }
+ }
+ }
+ ]
+ },
+ "overrides": []
+ },
+ "options": {
+ "showValue": "auto",
+ "mergeValues": true,
+ "alignValue": "left",
+ "rowHeight": 0.9,
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "list", "placement": "bottom" }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Traffic",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
+ "id": 9,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Frontend Bytes In",
+ "description": "Inbound traffic rate per HAProxy frontend. Shows how much data clients are sending to each frontend.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
+ "id": 10,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (proxy) (rate(haproxy_frontend_bytes_in_total{proxy=~\"$proxy\"}[$__rate_interval]))",
+ "legendFormat": "{{proxy}} (in)",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 20,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Frontend Bytes Out",
+ "description": "Outbound traffic rate per HAProxy frontend. Shows how much data is being served to clients from each frontend.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
+ "id": 11,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (proxy) (rate(haproxy_frontend_bytes_out_total{proxy=~\"$proxy\"}[$__rate_interval]))",
+ "legendFormat": "{{proxy}} (out)",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 20,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Sessions & Performance",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 },
+ "id": 12,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Current Sessions by Proxy",
+ "description": "Current active sessions for each HAProxy backend/proxy over time. Sustained high values may indicate slow backends or session buildup.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 },
+ "id": 13,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "haproxy_backend_current_sessions{proxy=~\"$proxy\"}",
+ "legendFormat": "{{proxy}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 20,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "none",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Backend Response Time",
+ "description": "Average response time from backend servers. High or increasing values indicate backend performance degradation.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 },
+ "id": 18,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "haproxy_server_response_time_average_seconds{proxy=~\"$proxy\", server=~\"$server\"}",
+ "legendFormat": "{{proxy}} / {{server}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "scheme",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "line" }
+ },
+ "unit": "s",
+ "color": { "mode": "continuous-GrYlRd" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 0.5 },
+ { "color": "red", "value": 2 }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Queue Time",
+ "description": "Average time requests spend in the backend queue before being assigned to a server. High values indicate insufficient backend capacity.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 41 },
+ "id": 19,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "haproxy_backend_queue_time_average_seconds{proxy=~\"$proxy\"}",
+ "legendFormat": "{{proxy}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "scheme",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "line" }
+ },
+ "unit": "s",
+ "color": { "mode": "continuous-GrYlRd" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 0.1 },
+ { "color": "red", "value": 1 }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Connection Errors",
+ "description": "Rate of backend connection errors and connection retries per proxy. Non-zero values indicate connectivity problems between HAProxy and backend servers.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 41 },
+ "id": 20,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (proxy) (rate(haproxy_backend_connection_errors_total{proxy=~\"$proxy\"}[$__rate_interval]))",
+ "legendFormat": "{{proxy}} errors",
+ "refId": "A"
+ },
+ {
+ "expr": "sum by (proxy) (rate(haproxy_backend_retry_warnings_total{proxy=~\"$proxy\"}[$__rate_interval]))",
+ "legendFormat": "{{proxy}} retries",
+ "refId": "B"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "reqps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": { "id": "byRegexp", "options": ".*errors$" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }
+ ]
+ },
+ {
+ "matcher": { "id": "byRegexp", "options": ".*retries$" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "orange" } }
+ ]
+ }
+ ]
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ }
+ ]
+}
diff --git a/incusos/observability-dashboards/core/host-resources.json b/incusos/observability-dashboards/core/host-resources.json
new file mode 100644
index 0000000..79b7eb7
--- /dev/null
+++ b/incusos/observability-dashboards/core/host-resources.json
@@ -0,0 +1,584 @@
+{
+ "uid": "incus-host-resources",
+ "title": "Host Resources (IncusOS Nodes)",
+ "description": "Physical host monitoring: CPU mode breakdown, load averages, memory breakdown, filesystem usage, disk IOPS, network errors.",
+ "tags": ["incus", "incusos", "host", "node-exporter"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "refresh": "30s",
+ "time": { "from": "now-1h", "to": "now" },
+ "fiscalYearStartMonth": 0,
+ "liveNow": false,
+ "editable": true,
+ "graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true, "icon": "external link", "includeVars": true,
+ "keepTime": true, "tags": ["incus"], "targetBlank": false,
+ "title": "Incus Dashboards", "type": "dashboards"
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(node_uname_info, instance)",
+ "includeAll": true,
+ "multi": true,
+ "name": "node",
+ "query": "label_values(node_uname_info, instance)",
+ "refresh": 2,
+ "sort": 1,
+ "type": "query",
+ "label": "Node"
+ }
+ ]
+ },
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
+ "enable": true, "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts", "type": "dashboard"
+ }
+ ]
+ },
+ "panels": [
+ {
+ "type": "row", "title": "System Info",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 30, "panels": []
+ },
+ {
+ "type": "table",
+ "title": "System Information",
+ "description": "Kernel version, uptime, total RAM, and CPU count per node.",
+ "gridPos": { "h": 5, "w": 24, "x": 0, "y": 1 },
+ "id": 31,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "node_uname_info{instance=~\"$node\"}",
+ "legendFormat": "", "refId": "A", "format": "table", "instant": true
+ },
+ {
+ "expr": "node_boot_time_seconds{instance=~\"$node\"}",
+ "legendFormat": "", "refId": "B", "format": "table", "instant": true
+ },
+ {
+ "expr": "node_memory_MemTotal_bytes{instance=~\"$node\"}",
+ "legendFormat": "", "refId": "C", "format": "table", "instant": true
+ },
+ {
+ "expr": "count by (instance) (node_cpu_seconds_total{instance=~\"$node\", mode=\"idle\"})",
+ "legendFormat": "", "refId": "D", "format": "table", "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": { "align": "auto", "displayMode": "auto", "inspect": false, "filterable": true },
+ "color": { "mode": "thresholds" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": [
+ { "matcher": { "id": "byName", "options": "instance" }, "properties": [{ "id": "displayName", "value": "Node" }] },
+ { "matcher": { "id": "byName", "options": "release" }, "properties": [{ "id": "displayName", "value": "Kernel" }] },
+ { "matcher": { "id": "byName", "options": "Value #B" }, "properties": [{ "id": "displayName", "value": "Boot Time" }, { "id": "unit", "value": "dateTimeFromNow" }] },
+ { "matcher": { "id": "byName", "options": "Value #C" }, "properties": [{ "id": "displayName", "value": "Total RAM" }, { "id": "unit", "value": "bytes" }] },
+ { "matcher": { "id": "byName", "options": "Value #D" }, "properties": [{ "id": "displayName", "value": "CPUs" }] }
+ ]
+ },
+ "options": { "showHeader": true, "sortBy": [], "footer": { "show": false, "reducer": ["sum"], "fields": "" } },
+ "transformations": [
+ { "id": "merge", "options": {} },
+ { "id": "organize", "options": { "excludeByName": { "Time": true, "__name__": true, "job": true, "domainname": true, "machine": true, "nodename": true, "sysname": true, "version": true, "Value #A": true }, "renameByName": {} } }
+ ]
+ },
+ {
+ "type": "row", "title": "CPU",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 },
+ "id": 1, "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "CPU Usage % by Node",
+ "description": "Overall CPU utilization per host node.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 },
+ "id": 2,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(1 - avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\", instance=~\"$node\"}[$__rate_interval]))) * 100",
+ "legendFormat": "{{instance}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 2,
+ "fillOpacity": 25, "gradientMode": "scheme", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "line" }
+ },
+ "unit": "percent", "min": 0, "max": 100,
+ "color": { "mode": "continuous-GrYlRd" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "red", "value": 90 }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "CPU Mode Breakdown",
+ "description": "CPU time by mode (user, system, iowait, idle) stacked per node.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 },
+ "id": 32,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "avg by (instance, mode) (rate(node_cpu_seconds_total{instance=~\"$node\", mode=~\"user|system|iowait|idle\"}[$__rate_interval])) * 100", "legendFormat": "{{instance}} {{mode}}", "refId": "A" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 50, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "normal", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "percent", "min": 0,
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Load Average",
+ "description": "System load averages (1m, 5m, 15m) with CPU count threshold line.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 15 },
+ "id": 33,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "node_load1{instance=~\"$node\"}", "legendFormat": "{{instance}} load1", "refId": "A" },
+ { "expr": "node_load5{instance=~\"$node\"}", "legendFormat": "{{instance}} load5", "refId": "B" },
+ { "expr": "node_load15{instance=~\"$node\"}", "legendFormat": "{{instance}} load15", "refId": "C" },
+ { "expr": "count by (instance) (node_cpu_seconds_total{instance=~\"$node\", mode=\"idle\"})", "legendFormat": "{{instance}} CPUs", "refId": "D" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 0, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": [
+ {
+ "matcher": { "id": "byRegexp", "options": ".*CPUs$" },
+ "properties": [
+ { "id": "custom.lineStyle", "value": { "fill": "dash", "dash": [10, 10] } },
+ { "id": "custom.lineWidth", "value": 2 },
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }
+ ]
+ }
+ ]
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row", "title": "Memory",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
+ "id": 3, "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Memory Usage % by Node",
+ "description": "Memory utilization percentage per host node.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
+ "id": 4,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(1 - node_memory_MemAvailable_bytes{instance=~\"$node\"} / node_memory_MemTotal_bytes{instance=~\"$node\"}) * 100",
+ "legendFormat": "{{instance}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 2,
+ "fillOpacity": 25, "gradientMode": "scheme", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "line" }
+ },
+ "unit": "percent", "min": 0, "max": 100,
+ "color": { "mode": "continuous-GrYlRd" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "red", "value": 90 }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Memory Breakdown by Node",
+ "description": "Memory breakdown: used, buffers, cached, available (stacked).",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
+ "id": 34,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "node_memory_MemTotal_bytes{instance=~\"$node\"} - node_memory_MemAvailable_bytes{instance=~\"$node\"} - node_memory_Buffers_bytes{instance=~\"$node\"} - node_memory_Cached_bytes{instance=~\"$node\"}", "legendFormat": "{{instance}} used", "refId": "A" },
+ { "expr": "node_memory_Buffers_bytes{instance=~\"$node\"}", "legendFormat": "{{instance}} buffers", "refId": "B" },
+ { "expr": "node_memory_Cached_bytes{instance=~\"$node\"}", "legendFormat": "{{instance}} cached", "refId": "C" },
+ { "expr": "node_memory_MemAvailable_bytes{instance=~\"$node\"}", "legendFormat": "{{instance}} available", "refId": "D" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 50, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "normal", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "bytes",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row", "title": "Disk",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 },
+ "id": 5, "panels": []
+ },
+ {
+ "type": "bargauge",
+ "title": "Filesystem Usage %",
+ "description": "Current filesystem usage percentage for important mount points.",
+ "gridPos": { "h": 6, "w": 8, "x": 0, "y": 33 },
+ "id": 35,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(1 - node_filesystem_avail_bytes{instance=~\"$node\", fstype!~\"tmpfs|overlay\", mountpoint=~\"/.*\"} / node_filesystem_size_bytes{instance=~\"$node\", fstype!~\"tmpfs|overlay\", mountpoint=~\"/.*\"}) * 100",
+ "legendFormat": "{{instance}} {{mountpoint}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "orange", "value": 85 }, { "color": "red", "value": 95 }] },
+ "unit": "percent", "min": 0, "max": 100,
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "horizontal", "displayMode": "gradient", "showUnfilled": true,
+ "minVizWidth": 8, "minVizHeight": 16, "valueMode": "color"
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Root Filesystem Available Space",
+ "description": "Available disk space on the root filesystem per host node.",
+ "gridPos": { "h": 6, "w": 8, "x": 8, "y": 33 },
+ "id": 6,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "node_filesystem_avail_bytes{instance=~\"$node\", mountpoint=\"/\"}", "legendFormat": "{{instance}}", "refId": "A" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "bytes",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["lastNotNull", "min"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk IOPS by Node",
+ "description": "Disk read and write operations per second by node.",
+ "gridPos": { "h": 6, "w": 8, "x": 16, "y": 33 },
+ "id": 36,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "sum by (instance) (rate(node_disk_reads_completed_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}} reads", "refId": "A" },
+ { "expr": "sum by (instance) (rate(node_disk_writes_completed_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}} writes", "refId": "B" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "iops",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk Read Rate by Node",
+ "description": "Disk read throughput aggregated per host node.",
+ "gridPos": { "h": 6, "w": 8, "x": 0, "y": 39 },
+ "id": 7,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "sum by (instance) (rate(node_disk_read_bytes_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}}", "refId": "A" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk Write Rate by Node",
+ "description": "Disk write throughput aggregated per host node.",
+ "gridPos": { "h": 6, "w": 8, "x": 8, "y": 39 },
+ "id": 8,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "sum by (instance) (rate(node_disk_written_bytes_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}}", "refId": "A" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk I/O Time",
+ "description": "Time spent on disk I/O operations (disk utilization indicator).",
+ "gridPos": { "h": 6, "w": 8, "x": 16, "y": 39 },
+ "id": 37,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "sum by (instance) (rate(node_disk_io_time_seconds_total{instance=~\"$node\"}[$__rate_interval])) * 100", "legendFormat": "{{instance}}", "refId": "A" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "scheme", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "line" }
+ },
+ "unit": "percent", "min": 0, "max": 100,
+ "color": { "mode": "continuous-GrYlRd" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 50 }, { "color": "red", "value": 80 }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ }
+ },
+ {
+ "type": "row", "title": "Network",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 45 },
+ "id": 9, "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Network Receive Rate by Device & Node",
+ "description": "Inbound network throughput per network device and host node.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 46 },
+ "id": 10,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "rate(node_network_receive_bytes_total{instance=~\"$node\", device!~\"lo|veth.*|br-.*|docker.*\"}[$__rate_interval])", "legendFormat": "{{instance}} {{device}} rx", "refId": "A" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Network Transmit Rate by Device & Node",
+ "description": "Outbound network throughput per network device and host node.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 46 },
+ "id": 11,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "rate(node_network_transmit_bytes_total{instance=~\"$node\", device!~\"lo|veth.*|br-.*|docker.*\"}[$__rate_interval])", "legendFormat": "{{instance}} {{device}} tx", "refId": "A" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Network Errors & Drops",
+ "description": "Network receive/transmit errors and dropped packets per node.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 54 },
+ "id": 38,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ { "expr": "sum by (instance) (rate(node_network_receive_errs_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}} rx errors", "refId": "A" },
+ { "expr": "sum by (instance) (rate(node_network_transmit_errs_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}} tx errors", "refId": "B" },
+ { "expr": "sum by (instance) (rate(node_network_receive_drop_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}} rx drops", "refId": "C" },
+ { "expr": "sum by (instance) (rate(node_network_transmit_drop_total{instance=~\"$node\"}[$__rate_interval]))", "legendFormat": "{{instance}} tx drops", "refId": "D" }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never",
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "pps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ }
+ ]
+}
diff --git a/incusos/observability-dashboards/cluster-overview.json b/incusos/observability-dashboards/core/instance-deep-dive.json
similarity index 56%
rename from incusos/observability-dashboards/cluster-overview.json
rename to incusos/observability-dashboards/core/instance-deep-dive.json
index 059d736..fcfa693 100644
--- a/incusos/observability-dashboards/cluster-overview.json
+++ b/incusos/observability-dashboards/core/instance-deep-dive.json
@@ -1,31 +1,59 @@
{
- "uid": "incus-cluster-overview",
- "title": "Incus Cluster Overview",
- "description": "Overview of all Incus containers: CPU, memory, network, and disk I/O per instance.",
+ "uid": "incus-instance-deep-dive",
+ "title": "Instance Deep Dive",
+ "description": "Single-instance drill-down: CPU, memory, network, and disk metrics with detailed timeseries and utilization gauges.",
"tags": ["incus", "cluster", "containers"],
"timezone": "browser",
"schemaVersion": 39,
- "version": 1,
"refresh": "30s",
- "time": {
- "from": "now-1h",
- "to": "now"
- },
+ "time": { "from": "now-1h", "to": "now" },
"fiscalYearStartMonth": 0,
"liveNow": false,
"editable": true,
"graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true,
+ "icon": "external link",
+ "includeVars": true,
+ "keepTime": true,
+ "tags": ["incus"],
+ "targetBlank": false,
+ "title": "Incus Dashboards",
+ "tooltip": "Navigate to other Incus dashboards",
+ "type": "dashboards"
+ },
+ {
+ "title": "Back to Cluster Overview",
+ "url": "/d/incus-cluster-overview?${__url_time_range}",
+ "targetBlank": false,
+ "type": "link",
+ "icon": "arrow-left"
+ }
+ ],
"templating": {
- "list": []
+ "list": [
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(incus_uptime_seconds, name)",
+ "includeAll": false,
+ "multi": false,
+ "name": "instance",
+ "query": "label_values(incus_uptime_seconds, name)",
+ "refresh": 2,
+ "regex": "",
+ "sort": 1,
+ "type": "query",
+ "label": "Instance"
+ }
+ ]
},
"annotations": {
"list": [
{
"builtIn": 1,
- "datasource": {
- "type": "grafana",
- "uid": "-- Grafana --"
- },
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
@@ -37,7 +65,7 @@
"panels": [
{
"type": "row",
- "title": "Cluster Status",
+ "title": "Instance Status",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 1,
@@ -45,18 +73,15 @@
},
{
"type": "stat",
- "title": "Total Containers",
- "description": "Number of running Incus containers (instances reporting uptime).",
- "gridPos": { "h": 4, "w": 8, "x": 0, "y": 1 },
+ "title": "Uptime",
+ "description": "How long this instance has been running.",
+ "gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
"id": 2,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "count(incus_uptime_seconds)",
- "legendFormat": "Containers",
+ "expr": "incus_uptime_seconds{name=~\"$instance\"}",
+ "legendFormat": "Uptime",
"refId": "A"
}
],
@@ -64,43 +89,32 @@
"defaults": {
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null },
- { "color": "yellow", "value": 10 },
- { "color": "red", "value": 50 }
- ]
+ "steps": [{ "color": "green", "value": null }]
},
- "unit": "none",
+ "unit": "s",
"color": { "mode": "thresholds" }
},
"overrides": []
},
"options": {
- "reduceOptions": {
- "calcs": ["lastNotNull"],
- "fields": "",
- "values": false
- },
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
- "graphMode": "none",
+ "graphMode": "area",
"justifyMode": "auto"
}
},
{
"type": "stat",
- "title": "Total CPUs",
- "description": "Sum of effective CPUs across all Incus instances.",
- "gridPos": { "h": 4, "w": 8, "x": 8, "y": 1 },
+ "title": "Effective CPUs",
+ "description": "Number of effective CPUs allocated to this instance.",
+ "gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
"id": 3,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "sum(incus_cpu_effective_total)",
+ "expr": "incus_cpu_effective_total{name=~\"$instance\"}",
"legendFormat": "CPUs",
"refId": "A"
}
@@ -109,9 +123,7 @@
"defaults": {
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
},
"unit": "none",
"color": { "mode": "thresholds" }
@@ -119,31 +131,24 @@
"overrides": []
},
"options": {
- "reduceOptions": {
- "calcs": ["lastNotNull"],
- "fields": "",
- "values": false
- },
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
- "graphMode": "none",
+ "graphMode": "area",
"justifyMode": "auto"
}
},
{
"type": "stat",
"title": "Total Memory",
- "description": "Sum of total memory across all Incus instances.",
- "gridPos": { "h": 4, "w": 8, "x": 16, "y": 1 },
+ "description": "Total memory allocated to this instance.",
+ "gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
"id": 4,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "sum(incus_memory_MemTotal_bytes)",
+ "expr": "incus_memory_MemTotal_bytes{name=~\"$instance\"}",
"legendFormat": "Total Memory",
"refId": "A"
}
@@ -152,9 +157,7 @@
"defaults": {
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
},
"unit": "bytes",
"color": { "mode": "thresholds" }
@@ -162,148 +165,25 @@
"overrides": []
},
"options": {
- "reduceOptions": {
- "calcs": ["lastNotNull"],
- "fields": "",
- "values": false
- },
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
- "graphMode": "none",
+ "graphMode": "area",
"justifyMode": "auto"
}
},
{
- "type": "row",
- "title": "Per-Instance CPU",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
+ "type": "gauge",
+ "title": "Memory Utilization",
+ "description": "Current memory utilization (RSS / Total) for this instance.",
+ "gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
"id": 5,
- "panels": []
- },
- {
- "type": "timeseries",
- "title": "CPU Usage by Instance",
- "description": "Per-second CPU time consumed by each Incus instance.",
- "gridPos": { "h": 8, "w": 24, "x": 0, "y": 6 },
- "id": 6,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "sum by (name) (rate(incus_cpu_seconds_total[$__rate_interval]))",
- "legendFormat": "{{name}}",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 15,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "short",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
- }
- },
- {
- "type": "row",
- "title": "Per-Instance Memory",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
- "id": 7,
- "panels": []
- },
- {
- "type": "timeseries",
- "title": "Memory RSS by Instance",
- "description": "Resident set size (RSS) memory usage per Incus instance over time.",
- "gridPos": { "h": 8, "w": 14, "x": 0, "y": 15 },
- "id": 8,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "incus_memory_RSS_bytes",
- "legendFormat": "{{name}}",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 15,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "bytes",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
- }
- },
- {
- "type": "bargauge",
- "title": "Memory Usage % (RSS / Total)",
- "description": "Current memory utilization per instance as RSS divided by total memory.",
- "gridPos": { "h": 8, "w": 10, "x": 14, "y": 15 },
- "id": 9,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "(incus_memory_RSS_bytes / incus_memory_MemTotal_bytes) * 100",
- "legendFormat": "{{name}}",
+ "expr": "(incus_memory_RSS_bytes{name=~\"$instance\"} / incus_memory_MemTotal_bytes{name=~\"$instance\"}) * 100",
+ "legendFormat": "Utilization",
"refId": "A"
}
],
@@ -326,40 +206,83 @@
"overrides": []
},
"options": {
- "reduceOptions": {
- "calcs": ["lastNotNull"],
- "fields": "",
- "values": false
- },
- "orientation": "horizontal",
- "displayMode": "gradient",
- "showUnfilled": true,
- "minVizWidth": 8,
- "minVizHeight": 16,
- "valueMode": "color"
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "orientation": "auto"
}
},
{
"type": "row",
- "title": "Network I/O",
+ "title": "CPU",
"collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
"id": 10,
"panels": []
},
{
"type": "timeseries",
- "title": "Network Receive Rate by Instance",
- "description": "Inbound network throughput per Incus instance.",
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
+ "title": "CPU Usage Over Time",
+ "description": "Per-second CPU time consumed by this instance over time.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 6 },
"id": 11,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "sum by (name) (rate(incus_network_receive_bytes_total[$__rate_interval]))",
+ "expr": "sum by (name) (rate(incus_cpu_seconds_total{name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 25,
+ "gradientMode": "scheme",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Memory",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
+ "id": 20,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Memory RSS Over Time",
+ "description": "Resident set size (RSS) memory usage for this instance over time.",
+ "gridPos": { "h": 8, "w": 14, "x": 0, "y": 15 },
+ "id": 21,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "incus_memory_RSS_bytes{name=~\"$instance\"}",
"legendFormat": "{{name}}",
"refId": "A"
}
@@ -381,39 +304,99 @@
"scaleDistribution": { "type": "linear" },
"thresholdsStyle": { "mode": "off" }
},
- "unit": "Bps",
+ "unit": "bytes",
"color": { "mode": "palette-classic" },
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
}
},
"overrides": []
},
"options": {
"tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
}
},
{
"type": "timeseries",
- "title": "Network Transmit Rate by Instance",
- "description": "Outbound network throughput per Incus instance.",
- "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
- "id": 12,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "title": "Memory Utilization %",
+ "description": "Memory utilization percentage (RSS / Total) for this instance over time.",
+ "gridPos": { "h": 8, "w": 10, "x": 14, "y": 15 },
+ "id": 22,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "sum by (name) (rate(incus_network_transmit_bytes_total[$__rate_interval]))",
+ "expr": "(incus_memory_RSS_bytes{name=~\"$instance\"} / incus_memory_MemTotal_bytes{name=~\"$instance\"}) * 100",
"legendFormat": "{{name}}",
"refId": "A"
}
],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "fillOpacity": 25,
+ "gradientMode": "scheme",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "line" }
+ },
+ "unit": "percent",
+ "min": 0,
+ "max": 100,
+ "color": { "mode": "continuous-GrYlRd" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 60 },
+ { "color": "orange", "value": 80 },
+ { "color": "red", "value": 90 }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Network",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
+ "id": 30,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Network Traffic",
+ "description": "Network receive and transmit throughput for this instance. Transmit is shown as negative (below axis).",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 },
+ "id": 31,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_network_receive_bytes_total{name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "Receive",
+ "refId": "A"
+ },
+ {
+ "expr": "-sum by (name) (rate(incus_network_transmit_bytes_total{name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "Transmit",
+ "refId": "B"
+ }
+ ],
"fieldConfig": {
"defaults": {
"custom": {
@@ -435,16 +418,27 @@
"color": { "mode": "palette-classic" },
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
}
},
- "overrides": []
+ "overrides": [
+ {
+ "matcher": { "id": "byName", "options": "Receive" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Transmit" },
+ "properties": [
+ { "id": "color", "value": { "mode": "fixed", "fixedColor": "blue" } }
+ ]
+ }
+ ]
},
"options": {
"tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
}
},
{
@@ -452,24 +446,26 @@
"title": "Disk I/O",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 },
- "id": 13,
+ "id": 40,
"panels": []
},
{
"type": "timeseries",
- "title": "Disk Read Rate by Instance",
- "description": "Disk read throughput per Incus instance.",
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 },
- "id": 14,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
+ "title": "Disk Read/Write Rate",
+ "description": "Disk read and write throughput for this instance.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 33 },
+ "id": 41,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
- "expr": "sum by (name) (rate(incus_disk_read_bytes_total[$__rate_interval]))",
- "legendFormat": "{{name}}",
+ "expr": "sum by (name) (rate(incus_disk_read_bytes_total{name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "Read",
"refId": "A"
+ },
+ {
+ "expr": "sum by (name) (rate(incus_disk_written_bytes_total{name=~\"$instance\"}[$__rate_interval]))",
+ "legendFormat": "Write",
+ "refId": "B"
}
],
"fieldConfig": {
@@ -493,66 +489,14 @@
"color": { "mode": "palette-classic" },
"thresholds": {
"mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
+ "steps": [{ "color": "green", "value": null }]
}
},
"overrides": []
},
"options": {
"tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
- }
- },
- {
- "type": "timeseries",
- "title": "Disk Write Rate by Instance",
- "description": "Disk write throughput per Incus instance.",
- "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 },
- "id": 15,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum by (name) (rate(incus_disk_written_bytes_total[$__rate_interval]))",
- "legendFormat": "{{name}}",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 15,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "Bps",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
}
}
]
diff --git a/incusos/observability-dashboards/core/logs-explorer.json b/incusos/observability-dashboards/core/logs-explorer.json
new file mode 100644
index 0000000..5f4808c
--- /dev/null
+++ b/incusos/observability-dashboards/core/logs-explorer.json
@@ -0,0 +1,268 @@
+{
+ "uid": "incus-logs-explorer",
+ "title": "Logs Explorer",
+ "description": "Log volume over time, full log stream with search, error and warning rates by systemd unit. Datasource: Loki.",
+ "tags": ["incus", "logs", "loki"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "refresh": "30s",
+ "time": { "from": "now-1h", "to": "now" },
+ "fiscalYearStartMonth": 0,
+ "liveNow": false,
+ "editable": true,
+ "graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true,
+ "icon": "external link",
+ "includeVars": true,
+ "keepTime": true,
+ "tags": ["incus"],
+ "targetBlank": false,
+ "title": "Incus Dashboards",
+ "tooltip": "Navigate to other Incus dashboards",
+ "type": "dashboards"
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "type": "query",
+ "datasource": { "type": "loki", "uid": "${DS_LOKI}" },
+ "query": "label_values(job)",
+ "includeAll": true,
+ "multi": true,
+ "name": "job",
+ "label": "Job",
+ "refresh": 2,
+ "sort": 1
+ },
+ {
+ "type": "query",
+ "datasource": { "type": "loki", "uid": "${DS_LOKI}" },
+ "query": "label_values(host)",
+ "includeAll": true,
+ "multi": true,
+ "name": "host",
+ "label": "Host",
+ "refresh": 2,
+ "sort": 1
+ },
+ {
+ "type": "query",
+ "datasource": { "type": "loki", "uid": "${DS_LOKI}" },
+ "query": "label_values(unit)",
+ "includeAll": true,
+ "multi": true,
+ "name": "unit",
+ "label": "Unit",
+ "refresh": 2,
+ "sort": 1
+ },
+ {
+ "type": "textbox",
+ "name": "search",
+ "label": "Search",
+ "current": { "text": "", "value": "" }
+ }
+ ]
+ },
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "panels": [
+ {
+ "type": "row",
+ "title": "Log Volume",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 1,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Log Volume Over Time",
+ "description": "Total log line count over time, broken down by job. Shows the rate at which logs are being generated across all selected sources.",
+ "gridPos": { "h": 6, "w": 24, "x": 0, "y": 1 },
+ "id": 2,
+ "datasource": { "type": "loki", "uid": "${DS_LOKI}" },
+ "targets": [
+ {
+ "expr": "sum by (job) (count_over_time({job=~\"$job\", host=~\"$host\"} [$__auto]))",
+ "legendFormat": "{{job}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "bars",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 80,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "normal", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["sum", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Log Stream",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 7 },
+ "id": 3,
+ "panels": []
+ },
+ {
+ "type": "logs",
+ "title": "Logs",
+ "description": "Live log stream filtered by job, host, unit, and free-text search. Displays timestamps, labels, and full log messages with expand-on-click detail view.",
+ "gridPos": { "h": 16, "w": 24, "x": 0, "y": 8 },
+ "id": 4,
+ "datasource": { "type": "loki", "uid": "${DS_LOKI}" },
+ "targets": [
+ {
+ "expr": "{job=~\"$job\", host=~\"$host\", unit=~\"$unit\"} |= \"$search\"",
+ "refId": "A"
+ }
+ ],
+ "options": {
+ "showTime": true,
+ "showLabels": true,
+ "showCommonLabels": false,
+ "wrapLogMessage": true,
+ "prettifyLogMessage": false,
+ "enableLogDetails": true,
+ "sortOrder": "Descending",
+ "dedupStrategy": "none"
+ }
+ },
+ {
+ "type": "row",
+ "title": "Error & Warning Rates",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 },
+ "id": 5,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Error Rate by Unit",
+ "description": "Rate of log lines matching error, fail, or fatal (case-insensitive) over time, grouped by systemd unit. Spikes indicate services experiencing failures.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 },
+ "id": 6,
+ "datasource": { "type": "loki", "uid": "${DS_LOKI}" },
+ "targets": [
+ {
+ "expr": "sum by (unit) (count_over_time({job=~\"$job\", host=~\"$host\"} |~ \"(?i)error|fail|fatal\" [$__auto]))",
+ "legendFormat": "{{unit}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "fixed", "fixedColor": "red" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "red", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["sum", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Warning Rate by Unit",
+ "description": "Rate of log lines matching warn (case-insensitive) over time, grouped by systemd unit. Helps identify services generating warnings that may precede errors.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 25 },
+ "id": 7,
+ "datasource": { "type": "loki", "uid": "${DS_LOKI}" },
+ "targets": [
+ {
+ "expr": "sum by (unit) (count_over_time({job=~\"$job\", host=~\"$host\"} |~ \"(?i)warn\" [$__auto]))",
+ "legendFormat": "{{unit}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "fixed", "fixedColor": "yellow" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "yellow", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["sum", "max", "lastNotNull"] }
+ }
+ }
+ ]
+}
diff --git a/incusos/observability-dashboards/core/network-deep-dive.json b/incusos/observability-dashboards/core/network-deep-dive.json
new file mode 100644
index 0000000..89598bb
--- /dev/null
+++ b/incusos/observability-dashboards/core/network-deep-dive.json
@@ -0,0 +1,529 @@
+{
+ "uid": "incus-network-deep-dive",
+ "title": "Network Deep Dive",
+ "description": "Per-node and per-device network traffic, instance-level network metrics, receive/transmit errors, and packet drops.",
+ "tags": ["incus", "incusos", "network", "node-exporter"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "refresh": "30s",
+ "time": { "from": "now-1h", "to": "now" },
+ "fiscalYearStartMonth": 0,
+ "liveNow": false,
+ "editable": true,
+ "graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true,
+ "icon": "external link",
+ "includeVars": true,
+ "keepTime": true,
+ "tags": ["incus"],
+ "targetBlank": false,
+ "title": "Incus Dashboards",
+ "tooltip": "Navigate to other Incus dashboards",
+ "type": "dashboards"
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(node_network_receive_bytes_total, instance)",
+ "includeAll": false,
+ "multi": true,
+ "name": "node",
+ "query": "label_values(node_network_receive_bytes_total, instance)",
+ "refresh": 2,
+ "sort": 1,
+ "type": "query",
+ "label": "Node"
+ },
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(node_network_receive_bytes_total{instance=~\"$node\"}, device)",
+ "includeAll": true,
+ "multi": true,
+ "name": "device",
+ "query": "label_values(node_network_receive_bytes_total{instance=~\"$node\"}, device)",
+ "refresh": 2,
+ "regex": "/^(?!lo$).*/",
+ "sort": 1,
+ "type": "query",
+ "label": "Device"
+ }
+ ]
+ },
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "panels": [
+ {
+ "type": "row",
+ "title": "Cluster Summary",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 1,
+ "panels": []
+ },
+ {
+ "type": "stat",
+ "title": "Total Ingress",
+ "description": "Aggregate inbound network throughput across all selected nodes and devices.",
+ "gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
+ "id": 2,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(node_network_receive_bytes_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval]))",
+ "legendFormat": "Ingress",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ },
+ "unit": "Bps",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Total Egress",
+ "description": "Aggregate outbound network throughput across all selected nodes and devices.",
+ "gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
+ "id": 3,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(node_network_transmit_bytes_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval]))",
+ "legendFormat": "Egress",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ },
+ "unit": "Bps",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Total Errors",
+ "description": "Combined receive and transmit error rate across all selected nodes. Any non-zero value warrants investigation.",
+ "gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
+ "id": 4,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(node_network_receive_errs_total{instance=~\"$node\"}[$__rate_interval])) + sum(rate(node_network_transmit_errs_total{instance=~\"$node\"}[$__rate_interval]))",
+ "legendFormat": "Errors",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 0.1 },
+ { "color": "red", "value": 1 }
+ ]
+ },
+ "unit": "pps",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Total Drops",
+ "description": "Combined receive and transmit packet drop rate across all selected nodes. Drops indicate buffer or queue exhaustion.",
+ "gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
+ "id": 5,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(node_network_receive_drop_total{instance=~\"$node\"}[$__rate_interval])) + sum(rate(node_network_transmit_drop_total{instance=~\"$node\"}[$__rate_interval]))",
+ "legendFormat": "Drops",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 0.1 },
+ { "color": "red", "value": 1 }
+ ]
+ },
+ "unit": "pps",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "row",
+ "title": "Per-Node Traffic",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
+ "id": 10,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Receive Rate by Node & Device",
+ "description": "Inbound network throughput broken down by node and network device. Identifies which interfaces are handling the most receive traffic.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
+ "id": 11,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(node_network_receive_bytes_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}} rx",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Transmit Rate by Node & Device",
+ "description": "Outbound network throughput broken down by node and network device. Identifies which interfaces are handling the most transmit traffic.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
+ "id": 12,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(node_network_transmit_bytes_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}} tx",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Instance Network (Incus metrics)",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
+ "id": 20,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Instance Receive Rate",
+ "description": "Inbound network throughput per Incus instance, aggregated across all instance network devices. Uses Incus-native metrics for container and VM visibility.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
+ "id": 21,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_network_receive_bytes_total{instance=~\"$node\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Instance Transmit Rate",
+ "description": "Outbound network throughput per Incus instance, aggregated across all instance network devices. Uses Incus-native metrics for container and VM visibility.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
+ "id": 22,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (name) (rate(incus_network_transmit_bytes_total{instance=~\"$node\"}[$__rate_interval]))",
+ "legendFormat": "{{name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "Errors & Drops",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
+ "id": 30,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Receive/Transmit Errors",
+ "description": "Network interface error rates broken down by node and device. Receive errors may indicate CRC failures or framing issues; transmit errors may indicate collisions or driver problems.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
+ "id": 31,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(node_network_receive_errs_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}} rx errors",
+ "refId": "A"
+ },
+ {
+ "expr": "rate(node_network_transmit_errs_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}} tx errors",
+ "refId": "B"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "pps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Packet Drops",
+ "description": "Network interface packet drop rates broken down by node and device. Drops indicate kernel buffer or queue exhaustion and may cause retransmissions or connection failures.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
+ "id": 32,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(node_network_receive_drop_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}} rx drops",
+ "refId": "A"
+ },
+ {
+ "expr": "rate(node_network_transmit_drop_total{instance=~\"$node\", device=~\"$device\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}} tx drops",
+ "refId": "B"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "pps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ }
+ ]
+}
diff --git a/incusos/observability-dashboards/core/prometheus-health.json b/incusos/observability-dashboards/core/prometheus-health.json
new file mode 100644
index 0000000..f8f3eee
--- /dev/null
+++ b/incusos/observability-dashboards/core/prometheus-health.json
@@ -0,0 +1,748 @@
+{
+ "uid": "incus-prometheus-health",
+ "title": "Prometheus Health",
+ "description": "Prometheus self-monitoring: targets up/down, scrape duration, samples ingested, TSDB size, head series, WAL stats.",
+ "tags": ["incus", "prometheus", "operations"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "refresh": "30s",
+ "time": { "from": "now-1h", "to": "now" },
+ "fiscalYearStartMonth": 0,
+ "liveNow": false,
+ "editable": true,
+ "graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true,
+ "icon": "external link",
+ "includeVars": true,
+ "keepTime": true,
+ "tags": ["incus"],
+ "targetBlank": false,
+ "title": "Incus Dashboards",
+ "tooltip": "Navigate to other Incus dashboards",
+ "type": "dashboards"
+ }
+ ],
+ "templating": {
+ "list": []
+ },
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "panels": [
+ {
+ "type": "row",
+ "title": "Targets",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 1,
+ "panels": []
+ },
+ {
+ "type": "stat",
+ "title": "Targets Up",
+ "description": "Number of scrape targets currently reporting as healthy (up == 1).",
+ "gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
+ "id": 2,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "count(up == 1)",
+ "legendFormat": "Up",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ },
+ "unit": "none",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Targets Down",
+ "description": "Number of scrape targets currently unreachable or failing (up == 0). Any value above 0 requires investigation.",
+ "gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
+ "id": 3,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "count(up == 0)",
+ "legendFormat": "Down",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "red", "value": 1 }
+ ]
+ },
+ "unit": "none",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Avg Scrape Duration",
+ "description": "Average time Prometheus takes to scrape all targets. High values may indicate slow or overloaded exporters.",
+ "gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
+ "id": 4,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "avg(scrape_duration_seconds)",
+ "legendFormat": "Avg Duration",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 1 },
+ { "color": "red", "value": 5 }
+ ]
+ },
+ "unit": "s",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Samples Ingested/s",
+ "description": "Total rate of samples being scraped and ingested across all targets per second.",
+ "gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
+ "id": 5,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum(rate(scrape_samples_scraped[$__rate_interval]))",
+ "legendFormat": "Samples/s",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ },
+ "unit": "short",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "table",
+ "title": "Target Health",
+ "description": "Per-target health status showing each scrape target's current state. UP (1) means healthy, DOWN (0) means unreachable or failing.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 5 },
+ "id": 6,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "up",
+ "legendFormat": "",
+ "refId": "A",
+ "format": "table",
+ "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "align": "auto",
+ "displayMode": "auto",
+ "inspect": false,
+ "filterable": true
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null }
+ ]
+ },
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": [
+ {
+ "matcher": { "id": "byName", "options": "instance" },
+ "properties": [
+ { "id": "displayName", "value": "Instance" },
+ { "id": "custom.width", "value": 300 }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "job" },
+ "properties": [
+ { "id": "displayName", "value": "Job" },
+ { "id": "custom.width", "value": 200 }
+ ]
+ },
+ {
+ "matcher": { "id": "byName", "options": "Value" },
+ "properties": [
+ { "id": "displayName", "value": "Status" },
+ { "id": "custom.displayMode", "value": "color-background" },
+ {
+ "id": "mappings",
+ "value": [
+ {
+ "type": "value",
+ "options": {
+ "1": { "text": "UP", "color": "green" },
+ "0": { "text": "DOWN", "color": "red" }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "options": {
+ "showHeader": true,
+ "sortBy": [{ "displayName": "Job", "desc": false }],
+ "footer": { "show": false, "reducer": ["sum"], "fields": "" }
+ },
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": { "Time": true, "__name__": true },
+ "indexByName": {
+ "instance": 0,
+ "job": 1,
+ "Value": 2
+ },
+ "renameByName": {}
+ }
+ }
+ ]
+ },
+ {
+ "type": "row",
+ "title": "Scrape Performance",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 },
+ "id": 7,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Scrape Duration by Job",
+ "description": "How long each scrape takes per job. Spikes indicate slow exporters or network issues between Prometheus and targets.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 },
+ "id": 8,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "scrape_duration_seconds",
+ "legendFormat": "{{job}} - {{instance}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "s",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Samples Scraped by Job",
+ "description": "Rate of samples scraped per job. Sudden drops may indicate target failures; spikes may indicate new metrics being exposed.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 },
+ "id": 9,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(scrape_samples_scraped[$__rate_interval])",
+ "legendFormat": "{{job}} - {{instance}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "TSDB",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 },
+ "id": 10,
+ "panels": []
+ },
+ {
+ "type": "stat",
+ "title": "TSDB Size",
+ "description": "Current on-disk size of the Prometheus TSDB storage directory including all blocks and WAL.",
+ "gridPos": { "h": 4, "w": 6, "x": 0, "y": 23 },
+ "id": 11,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "prometheus_tsdb_storage_size_bytes",
+ "legendFormat": "TSDB Size",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 1073741824 },
+ { "color": "red", "value": 5368709120 }
+ ]
+ },
+ "unit": "bytes",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "stat",
+ "title": "Head Series",
+ "description": "Number of active time series in the TSDB head block. High cardinality here impacts query performance and memory usage.",
+ "gridPos": { "h": 4, "w": 6, "x": 6, "y": 23 },
+ "id": 12,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "prometheus_tsdb_head_series",
+ "legendFormat": "Head Series",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ { "color": "green", "value": null },
+ { "color": "yellow", "value": 100000 },
+ { "color": "red", "value": 500000 }
+ ]
+ },
+ "unit": "short",
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Head Chunks",
+ "description": "Number of chunks in the TSDB head block. Growth correlates with active series count and scrape frequency.",
+ "gridPos": { "h": 8, "w": 6, "x": 12, "y": 23 },
+ "id": 13,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "prometheus_tsdb_head_chunks",
+ "legendFormat": "Head Chunks",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "WAL Size",
+ "description": "Size of the Prometheus write-ahead log (WAL). The WAL buffers recent writes before compaction into blocks.",
+ "gridPos": { "h": 8, "w": 6, "x": 18, "y": 23 },
+ "id": 14,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "prometheus_tsdb_wal_storage_size_bytes",
+ "legendFormat": "WAL Size",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "bytes",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row",
+ "title": "TSDB Trends",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 31 },
+ "id": 15,
+ "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "TSDB Size Over Time",
+ "description": "Prometheus TSDB total storage size trend. Useful for capacity planning and detecting unexpected growth.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 32 },
+ "id": 16,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "prometheus_tsdb_storage_size_bytes",
+ "legendFormat": "TSDB Size",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "bytes",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Samples Ingested Trend",
+ "description": "Rate of new samples appended to the TSDB head block. Correlates with scrape frequency and number of active series.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 32 },
+ "id": 17,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(prometheus_tsdb_head_samples_appended_total[$__rate_interval])",
+ "legendFormat": "Samples/s",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Compactions",
+ "description": "Rate of TSDB compaction operations. Compactions merge head block data into persistent blocks on disk for efficient storage.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 40 },
+ "id": 18,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(prometheus_tsdb_compactions_total[$__rate_interval])",
+ "legendFormat": "Compactions/s",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Head Series Count",
+ "description": "Number of active time series in the TSDB head block over time. Trend helps identify cardinality growth or label explosion.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 40 },
+ "id": 19,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "prometheus_tsdb_head_series",
+ "legendFormat": "Head Series",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "smooth",
+ "lineWidth": 1,
+ "fillOpacity": 15,
+ "gradientMode": "none",
+ "spanNulls": false,
+ "showPoints": "never",
+ "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto",
+ "axisLabel": "",
+ "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "short",
+ "color": { "mode": "palette-classic" },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [{ "color": "green", "value": null }]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ }
+ ]
+}
diff --git a/incusos/observability-dashboards/core/storage-filesystem.json b/incusos/observability-dashboards/core/storage-filesystem.json
new file mode 100644
index 0000000..0a8cec0
--- /dev/null
+++ b/incusos/observability-dashboards/core/storage-filesystem.json
@@ -0,0 +1,382 @@
+{
+ "uid": "incus-storage-filesystem",
+ "title": "Storage & Filesystem",
+ "description": "Filesystem usage, available space trends, disk throughput, I/O latency, disk utilization, and inode usage per node.",
+ "tags": ["incus", "incusos", "storage", "node-exporter"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "refresh": "30s",
+ "time": { "from": "now-1h", "to": "now" },
+ "fiscalYearStartMonth": 0,
+ "liveNow": false,
+ "editable": true,
+ "graphTooltip": 1,
+ "links": [
+ {
+ "asDropdown": true, "icon": "external link", "includeVars": true,
+ "keepTime": true, "tags": ["incus"], "targetBlank": false,
+ "title": "Incus Dashboards", "type": "dashboards"
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(node_filesystem_size_bytes, instance)",
+ "includeAll": false,
+ "multi": true,
+ "name": "node",
+ "query": "label_values(node_filesystem_size_bytes, instance)",
+ "refresh": 2,
+ "sort": 1,
+ "type": "query",
+ "label": "Node"
+ },
+ {
+ "current": {},
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "definition": "label_values(node_filesystem_size_bytes{instance=~\"$node\"}, mountpoint)",
+ "includeAll": true,
+ "multi": true,
+ "name": "mountpoint",
+ "query": "label_values(node_filesystem_size_bytes{instance=~\"$node\"}, mountpoint)",
+ "refresh": 2,
+ "sort": 1,
+ "type": "query",
+ "label": "Mountpoint"
+ }
+ ]
+ },
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": { "type": "grafana", "uid": "-- Grafana --" },
+ "enable": true, "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts", "type": "dashboard"
+ }
+ ]
+ },
+ "panels": [
+ {
+ "type": "row", "title": "Filesystem Overview",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
+ "id": 1, "panels": []
+ },
+ {
+ "type": "table",
+ "title": "Filesystem Usage Table",
+ "description": "Current filesystem size, available space, and usage percentage per node and mountpoint. High usage is highlighted with color thresholds.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 1 },
+ "id": 2,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "node_filesystem_size_bytes{instance=~\"$node\", mountpoint=~\"$mountpoint\", fstype!~\"tmpfs|overlay\"}",
+ "legendFormat": "", "refId": "A", "format": "table", "instant": true
+ },
+ {
+ "expr": "node_filesystem_avail_bytes{instance=~\"$node\", mountpoint=~\"$mountpoint\", fstype!~\"tmpfs|overlay\"}",
+ "legendFormat": "", "refId": "B", "format": "table", "instant": true
+ },
+ {
+ "expr": "(1 - node_filesystem_avail_bytes{instance=~\"$node\", mountpoint=~\"$mountpoint\", fstype!~\"tmpfs|overlay\"} / node_filesystem_size_bytes{instance=~\"$node\", mountpoint=~\"$mountpoint\", fstype!~\"tmpfs|overlay\"}) * 100",
+ "legendFormat": "", "refId": "C", "format": "table", "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": { "align": "auto", "displayMode": "auto", "inspect": false, "filterable": true },
+ "color": { "mode": "thresholds" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": [
+ { "matcher": { "id": "byName", "options": "instance" }, "properties": [{ "id": "displayName", "value": "Node" }] },
+ { "matcher": { "id": "byName", "options": "mountpoint" }, "properties": [{ "id": "displayName", "value": "Mountpoint" }] },
+ { "matcher": { "id": "byName", "options": "fstype" }, "properties": [{ "id": "displayName", "value": "FS Type" }] },
+ { "matcher": { "id": "byName", "options": "Value #A" }, "properties": [{ "id": "displayName", "value": "Size" }, { "id": "unit", "value": "bytes" }] },
+ { "matcher": { "id": "byName", "options": "Value #B" }, "properties": [{ "id": "displayName", "value": "Available" }, { "id": "unit", "value": "bytes" }] },
+ {
+ "matcher": { "id": "byName", "options": "Value #C" },
+ "properties": [
+ { "id": "displayName", "value": "Used %" },
+ { "id": "unit", "value": "percent" },
+ { "id": "custom.displayMode", "value": "color-background" },
+ { "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "orange", "value": 85 }, { "color": "red", "value": 95 }] } }
+ ]
+ }
+ ]
+ },
+ "options": { "showHeader": true, "sortBy": [], "footer": { "show": false, "reducer": ["sum"], "fields": "" } },
+ "transformations": [
+ { "id": "merge", "options": {} },
+ { "id": "organize", "options": { "excludeByName": { "Time": true, "__name__": true, "job": true, "device": true }, "renameByName": {} } }
+ ]
+ },
+ {
+ "type": "row", "title": "Space Trends",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 },
+ "id": 3, "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Available Space Over Time",
+ "description": "Tracks available filesystem space over time per node and mountpoint. Use this to identify gradual space consumption trends before they become critical.",
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 10 },
+ "id": 4,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "node_filesystem_avail_bytes{instance=~\"$node\", mountpoint=~\"$mountpoint\", fstype!~\"tmpfs|overlay\"}",
+ "legendFormat": "{{instance}} {{mountpoint}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 2,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "bytes",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row", "title": "Disk Throughput",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 18 },
+ "id": 5, "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk Read Throughput",
+ "description": "Disk read throughput in bytes per second, broken down by instance and device. Identifies which disks are handling the most read traffic.",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 19 },
+ "id": 6,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (instance, device) (rate(node_disk_read_bytes_total{instance=~\"$node\"}[$__rate_interval]))",
+ "legendFormat": "{{instance}} {{device}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk Write Throughput",
+ "description": "Disk write throughput in bytes per second, broken down by instance and device. Identifies which disks are handling the most write traffic.",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 19 },
+ "id": 7,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (instance, device) (rate(node_disk_written_bytes_total{instance=~\"$node\"}[$__rate_interval]))",
+ "legendFormat": "{{instance}} {{device}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "Bps",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row", "title": "I/O Performance",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 27 },
+ "id": 8, "panels": []
+ },
+ {
+ "type": "timeseries",
+ "title": "Average Read Latency",
+ "description": "Average time per completed read operation. Calculated as total read time divided by completed reads. High values indicate slow disk response.",
+ "gridPos": { "h": 8, "w": 8, "x": 0, "y": 28 },
+ "id": 9,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(node_disk_read_time_seconds_total{instance=~\"$node\"}[$__rate_interval]) / rate(node_disk_reads_completed_total{instance=~\"$node\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "s",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Average Write Latency",
+ "description": "Average time per completed write operation. Calculated as total write time divided by completed writes. High values indicate slow disk response or write saturation.",
+ "gridPos": { "h": 8, "w": 8, "x": 8, "y": 28 },
+ "id": 10,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "rate(node_disk_write_time_seconds_total{instance=~\"$node\"}[$__rate_interval]) / rate(node_disk_writes_completed_total{instance=~\"$node\"}[$__rate_interval])",
+ "legendFormat": "{{instance}} {{device}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 1,
+ "fillOpacity": 15, "gradientMode": "none", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "off" }
+ },
+ "unit": "s",
+ "color": { "mode": "palette-classic" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "timeseries",
+ "title": "Disk Utilization %",
+ "description": "Percentage of time the disk is busy processing I/O requests. Values near 100% indicate the disk is saturated and may be a bottleneck.",
+ "gridPos": { "h": 8, "w": 8, "x": 16, "y": 28 },
+ "id": 11,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "sum by (instance) (rate(node_disk_io_time_seconds_total{instance=~\"$node\"}[$__rate_interval])) * 100",
+ "legendFormat": "{{instance}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line", "lineInterpolation": "smooth", "lineWidth": 2,
+ "fillOpacity": 25, "gradientMode": "scheme", "spanNulls": false,
+ "showPoints": "never", "pointSize": 5,
+ "stacking": { "mode": "none", "group": "A" },
+ "axisPlacement": "auto", "scaleDistribution": { "type": "linear" },
+ "thresholdsStyle": { "mode": "line" }
+ },
+ "unit": "percent", "min": 0, "max": 100,
+ "color": { "mode": "continuous-GrYlRd" },
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 50 }, { "color": "red", "value": 80 }] }
+ },
+ "overrides": []
+ },
+ "options": {
+ "tooltip": { "mode": "multi", "sort": "desc" },
+ "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
+ }
+ },
+ {
+ "type": "row", "title": "Inodes",
+ "collapsed": false,
+ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 36 },
+ "id": 12, "panels": []
+ },
+ {
+ "type": "bargauge",
+ "title": "Inode Usage %",
+ "description": "Percentage of used inodes per filesystem. Running out of inodes prevents creating new files even when disk space is available. Monitor for filesystems with many small files.",
+ "gridPos": { "h": 6, "w": 24, "x": 0, "y": 37 },
+ "id": 13,
+ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
+ "targets": [
+ {
+ "expr": "(1 - node_filesystem_files_free{instance=~\"$node\", mountpoint=~\"$mountpoint\", fstype!~\"tmpfs|overlay\"} / node_filesystem_files{instance=~\"$node\", mountpoint=~\"$mountpoint\", fstype!~\"tmpfs|overlay\"}) * 100",
+ "legendFormat": "{{instance}} {{mountpoint}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "red", "value": 90 }] },
+ "unit": "percent", "min": 0, "max": 100,
+ "color": { "mode": "thresholds" }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
+ "orientation": "horizontal", "displayMode": "gradient", "showUnfilled": true,
+ "minVizWidth": 8, "minVizHeight": 16, "valueMode": "color"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/incusos/observability-dashboards/dashboard.yaml b/incusos/observability-dashboards/dashboard.yaml
new file mode 100644
index 0000000..f97a7bf
--- /dev/null
+++ b/incusos/observability-dashboards/dashboard.yaml
@@ -0,0 +1,97 @@
+# Dashboard manifest for the Incus observability stack
+#
+# Each entry maps a dashboard JSON file to its Grafana folder, UID, and metadata.
+# Used by manage-dashboards for install, export, list, and validate operations.
+
+dashboards:
+ # --- Core dashboards (ship with product) ---
+
+ - file: core/cluster-overview.json
+ uid: incus-cluster-overview
+ title: Incus Cluster Overview
+ folder: Incus Cluster
+ category: core
+ version: "2"
+ description: >-
+ Overview of all Incus instances: CPU, memory, network, disk I/O,
+ and instance status table with drill-down links.
+
+ - file: core/host-resources.json
+ uid: incus-host-resources
+ title: Host Resources (IncusOS Nodes)
+ folder: Incus Infrastructure
+ category: core
+ version: "2"
+ description: >-
+ Physical host monitoring: CPU mode breakdown, load averages,
+ memory breakdown, filesystem usage, disk IOPS, network errors.
+
+ - file: core/haproxy-traffic.json
+ uid: incus-haproxy-traffic
+ title: HAProxy Traffic
+ folder: Incus Services
+ category: core
+ version: "2"
+ description: >-
+ HAProxy load balancer monitoring: request rates, response codes,
+ backend health, server status timeline, connection errors.
+
+ - file: core/instance-deep-dive.json
+ uid: incus-instance-deep-dive
+ title: Instance Deep Dive
+ folder: Incus Cluster
+ category: core
+ version: "1"
+ description: >-
+ Single-instance drill-down: CPU, memory, network, and disk
+ metrics with detailed timeseries and utilization gauges.
+
+ - file: core/storage-filesystem.json
+ uid: incus-storage-filesystem
+ title: Storage & Filesystem
+ folder: Incus Infrastructure
+ category: core
+ version: "1"
+ description: >-
+ Filesystem usage, available space trends, disk throughput,
+ I/O latency, disk utilization, and inode usage per node.
+
+ - file: core/network-deep-dive.json
+ uid: incus-network-deep-dive
+ title: Network Deep Dive
+ folder: Incus Infrastructure
+ category: core
+ version: "1"
+ description: >-
+ Per-node and per-device network traffic, instance-level network
+ metrics, receive/transmit errors, and packet drops.
+
+ - file: core/logs-explorer.json
+ uid: incus-logs-explorer
+ title: Logs Explorer
+ folder: Incus Operations
+ category: core
+ version: "1"
+ description: >-
+ Log volume over time, full log stream with search, error
+ and warning rates by systemd unit. Datasource: Loki.
+
+ - file: core/prometheus-health.json
+ uid: incus-prometheus-health
+ title: Prometheus Health
+ folder: Incus Operations
+ category: core
+ version: "1"
+ description: >-
+ Prometheus self-monitoring: targets up/down, scrape duration,
+ samples ingested, TSDB size, head series, WAL stats.
+
+ - file: core/capacity-planning.json
+ uid: incus-capacity-planning
+ title: Capacity Planning
+ folder: Incus Operations
+ category: core
+ version: "1"
+ description: >-
+ CPU/memory headroom per node, resource trends, disk space
+ forecast with predict_linear, instance count trends.
diff --git a/incusos/observability-dashboards/haproxy-traffic.json b/incusos/observability-dashboards/haproxy-traffic.json
deleted file mode 100644
index 8f89cbe..0000000
--- a/incusos/observability-dashboards/haproxy-traffic.json
+++ /dev/null
@@ -1,518 +0,0 @@
-{
- "uid": "incus-haproxy-traffic",
- "title": "HAProxy Traffic",
- "description": "HAProxy load balancer monitoring: request rates, backend health, traffic volume, and sessions.",
- "tags": ["haproxy", "loadbalancer", "incus"],
- "timezone": "browser",
- "schemaVersion": 39,
- "version": 1,
- "refresh": "30s",
- "time": {
- "from": "now-1h",
- "to": "now"
- },
- "fiscalYearStartMonth": 0,
- "liveNow": false,
- "editable": true,
- "graphTooltip": 1,
- "templating": {
- "list": []
- },
- "annotations": {
- "list": [
- {
- "builtIn": 1,
- "datasource": {
- "type": "grafana",
- "uid": "-- Grafana --"
- },
- "enable": true,
- "hide": true,
- "iconColor": "rgba(0, 211, 255, 1)",
- "name": "Annotations & Alerts",
- "type": "dashboard"
- }
- ]
- },
- "panels": [
- {
- "type": "row",
- "title": "Overview",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
- "id": 1,
- "panels": []
- },
- {
- "type": "stat",
- "title": "Active Backend Servers",
- "description": "Total number of active servers across all HAProxy backends.",
- "gridPos": { "h": 4, "w": 8, "x": 0, "y": 1 },
- "id": 2,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum(haproxy_backend_active_servers)",
- "legendFormat": "Active Servers",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "red", "value": null },
- { "color": "yellow", "value": 1 },
- { "color": "green", "value": 2 }
- ]
- },
- "unit": "none",
- "color": { "mode": "thresholds" }
- },
- "overrides": []
- },
- "options": {
- "reduceOptions": {
- "calcs": ["lastNotNull"],
- "fields": "",
- "values": false
- },
- "orientation": "auto",
- "textMode": "auto",
- "colorMode": "value",
- "graphMode": "none",
- "justifyMode": "auto"
- }
- },
- {
- "type": "stat",
- "title": "Current Sessions",
- "description": "Total current sessions across all HAProxy backends.",
- "gridPos": { "h": 4, "w": 8, "x": 8, "y": 1 },
- "id": 3,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum(haproxy_backend_current_sessions)",
- "legendFormat": "Sessions",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null },
- { "color": "yellow", "value": 100 },
- { "color": "red", "value": 500 }
- ]
- },
- "unit": "none",
- "color": { "mode": "thresholds" }
- },
- "overrides": []
- },
- "options": {
- "reduceOptions": {
- "calcs": ["lastNotNull"],
- "fields": "",
- "values": false
- },
- "orientation": "auto",
- "textMode": "auto",
- "colorMode": "value",
- "graphMode": "area",
- "justifyMode": "auto"
- }
- },
- {
- "type": "stat",
- "title": "Total HTTP Requests",
- "description": "Total HTTP requests across all HAProxy frontends (counter).",
- "gridPos": { "h": 4, "w": 8, "x": 16, "y": 1 },
- "id": 4,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum(rate(haproxy_frontend_http_requests_total[$__rate_interval]))",
- "legendFormat": "Requests/s",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- },
- "unit": "reqps",
- "color": { "mode": "thresholds" }
- },
- "overrides": []
- },
- "options": {
- "reduceOptions": {
- "calcs": ["lastNotNull"],
- "fields": "",
- "values": false
- },
- "orientation": "auto",
- "textMode": "auto",
- "colorMode": "value",
- "graphMode": "area",
- "justifyMode": "auto"
- }
- },
- {
- "type": "row",
- "title": "HTTP Requests",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
- "id": 5,
- "panels": []
- },
- {
- "type": "timeseries",
- "title": "HTTP Request Rate by Frontend",
- "description": "Rate of HTTP requests per second, broken down by HAProxy frontend/proxy.",
- "gridPos": { "h": 8, "w": 24, "x": 0, "y": 6 },
- "id": 6,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum by (proxy) (rate(haproxy_frontend_http_requests_total[$__rate_interval]))",
- "legendFormat": "{{proxy}}",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 20,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "reqps",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
- }
- },
- {
- "type": "row",
- "title": "Backend Health",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
- "id": 7,
- "panels": []
- },
- {
- "type": "table",
- "title": "Backend Status",
- "description": "Current status, active server count, and average response time for each HAProxy backend.",
- "gridPos": { "h": 8, "w": 24, "x": 0, "y": 15 },
- "id": 8,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "haproxy_backend_status",
- "legendFormat": "",
- "refId": "A",
- "format": "table",
- "instant": true
- },
- {
- "expr": "haproxy_backend_active_servers",
- "legendFormat": "",
- "refId": "B",
- "format": "table",
- "instant": true
- },
- {
- "expr": "haproxy_server_response_time_average_seconds",
- "legendFormat": "",
- "refId": "C",
- "format": "table",
- "instant": true
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "align": "auto",
- "displayMode": "auto",
- "inspect": false,
- "filterable": true
- },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- },
- "color": { "mode": "thresholds" }
- },
- "overrides": [
- {
- "matcher": { "id": "byName", "options": "Value #A" },
- "properties": [
- { "id": "displayName", "value": "Status" }
- ]
- },
- {
- "matcher": { "id": "byName", "options": "Value #B" },
- "properties": [
- { "id": "displayName", "value": "Active Servers" }
- ]
- },
- {
- "matcher": { "id": "byName", "options": "Value #C" },
- "properties": [
- { "id": "displayName", "value": "Avg Response Time" },
- { "id": "unit", "value": "s" }
- ]
- },
- {
- "matcher": { "id": "byName", "options": "proxy" },
- "properties": [
- { "id": "displayName", "value": "Backend" }
- ]
- }
- ]
- },
- "options": {
- "showHeader": true,
- "sortBy": [],
- "footer": {
- "show": false,
- "reducer": ["sum"],
- "fields": ""
- }
- },
- "transformations": [
- {
- "id": "merge",
- "options": {}
- },
- {
- "id": "organize",
- "options": {
- "excludeByName": {
- "Time": true,
- "__name__": true,
- "instance": false,
- "job": true
- },
- "indexByName": {},
- "renameByName": {}
- }
- }
- ]
- },
- {
- "type": "row",
- "title": "Traffic",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
- "id": 9,
- "panels": []
- },
- {
- "type": "timeseries",
- "title": "Frontend Bytes In",
- "description": "Inbound traffic rate per HAProxy frontend.",
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
- "id": 10,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum by (proxy) (rate(haproxy_frontend_bytes_in_total[$__rate_interval]))",
- "legendFormat": "{{proxy}} (in)",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 20,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "Bps",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
- }
- },
- {
- "type": "timeseries",
- "title": "Frontend Bytes Out",
- "description": "Outbound traffic rate per HAProxy frontend.",
- "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
- "id": 11,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "sum by (proxy) (rate(haproxy_frontend_bytes_out_total[$__rate_interval]))",
- "legendFormat": "{{proxy}} (out)",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 20,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "Bps",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max"] }
- }
- },
- {
- "type": "row",
- "title": "Sessions",
- "collapsed": false,
- "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 },
- "id": 12,
- "panels": []
- },
- {
- "type": "timeseries",
- "title": "Current Sessions by Proxy",
- "description": "Current active sessions for each HAProxy backend/proxy over time.",
- "gridPos": { "h": 8, "w": 24, "x": 0, "y": 33 },
- "id": 13,
- "datasource": {
- "type": "prometheus",
- "uid": "PBFA97CFB590B2093"
- },
- "targets": [
- {
- "expr": "haproxy_backend_current_sessions",
- "legendFormat": "{{proxy}}",
- "refId": "A"
- }
- ],
- "fieldConfig": {
- "defaults": {
- "custom": {
- "drawStyle": "line",
- "lineInterpolation": "smooth",
- "lineWidth": 1,
- "fillOpacity": 20,
- "gradientMode": "none",
- "spanNulls": false,
- "showPoints": "never",
- "pointSize": 5,
- "stacking": { "mode": "none", "group": "A" },
- "axisPlacement": "auto",
- "axisLabel": "",
- "scaleDistribution": { "type": "linear" },
- "thresholdsStyle": { "mode": "off" }
- },
- "unit": "none",
- "color": { "mode": "palette-classic" },
- "thresholds": {
- "mode": "absolute",
- "steps": [
- { "color": "green", "value": null }
- ]
- }
- },
- "overrides": []
- },
- "options": {
- "tooltip": { "mode": "multi", "sort": "desc" },
- "legend": { "displayMode": "table", "placement": "right", "calcs": ["mean", "max", "lastNotNull"] }
- }
- }
- ]
-}
diff --git a/notes/observability-guide.md b/notes/observability-guide.md
index a0e7dea..63fd180 100644
--- a/notes/observability-guide.md
+++ b/notes/observability-guide.md
@@ -122,40 +122,106 @@ datasource proxy.
## Dashboards
-Three dashboards are provisioned automatically during deployment.
+Nine dashboards are provisioned automatically during deployment, organized
+into four Grafana folders. All dashboards support template variables for
+filtering and include cross-dashboard navigation links.
-### Incus Cluster Overview
+Use `manage-dashboards` to install, export, validate, or list dashboards:
-Visualizes metrics scraped from the Incus `/1.0/metrics` endpoint on each
-cluster node.
+```bash
+incusos/manage-dashboards --install # push all dashboards to Grafana
+incusos/manage-dashboards --list # show install status
+incusos/manage-dashboards --validate # check JSON quality
+incusos/manage-dashboards --export # export from Grafana to clean JSON
+incusos/manage-dashboards --status # compare local vs installed
+```
-- Instance CPU usage (per-instance, per-node)
-- Memory usage and allocation
-- Network I/O (bytes in/out per interface)
-- Disk I/O (reads/writes, latency)
-- Instance count and state
+### Incus Cluster — Incus Cluster Overview
-### HAProxy Traffic
+Overview of all instances with instance/node template variables.
-Visualizes metrics from the HAProxy stats endpoint (`:8404/metrics`).
+- Instance count, total CPUs, total memory, cluster memory utilization gauge
+- Instance status table with drill-down links to Instance Deep Dive
+- Per-instance CPU usage, memory RSS, memory usage % bar gauge
+- Network receive/transmit rates, disk read/write rates
-- Request rate (frontend and backend)
-- Backend health status (UP/DOWN per server)
-- Active sessions and session rate
-- Traffic volume (bytes in/out)
-- HTTP response codes (2xx, 4xx, 5xx)
-- Connection errors and retries
+### Incus Cluster — Instance Deep Dive
-### Host Resources
+Single-instance drill-down selected via template variable.
-Visualizes metrics from node_exporter on each cluster node.
+- Uptime, effective CPUs, total memory, memory utilization gauge
+- CPU usage over time with gradient color scheme
+- Memory RSS and memory utilization % over time
+- Network traffic (receive above axis, transmit below on same panel)
+- Disk read/write rates
-- CPU utilization (%)
-- Memory utilization (%)
-- Disk usage and I/O
-- Network throughput per interface
-- System load averages
-- Filesystem free space
+### Incus Services — HAProxy Traffic
+
+HAProxy monitoring with proxy/server template variables.
+
+- Active servers, current sessions, HTTP request rate
+- HTTP 4xx/5xx rate stat panels with warning thresholds
+- Response code distribution (stacked, color-coded by class)
+- Backend status table and server status state timeline
+- Frontend traffic, sessions by proxy, response times
+- Queue time and connection errors
+
+### Incus Infrastructure — Host Resources (IncusOS Nodes)
+
+Physical host monitoring with node template variable.
+
+- System information table (kernel, uptime, RAM, CPU count)
+- CPU usage % and CPU mode breakdown (user/system/iowait/idle stacked)
+- Load average (1m/5m/15m) with CPU count threshold line
+- Memory usage % and memory breakdown (used/buffers/cached/available)
+- Filesystem usage % bar gauge, IOPS, disk I/O time
+- Network traffic by device, network errors and drops
+
+### Incus Infrastructure — Storage & Filesystem
+
+Filesystem and disk I/O with node/mountpoint template variables.
+
+- Filesystem usage table with color-coded Used % column
+- Available space trend over time
+- Disk read/write throughput by device
+- Average read/write latency, disk utilization %
+- Inode usage % bar gauge
+
+### Incus Infrastructure — Network Deep Dive
+
+Network metrics with node/device template variables.
+
+- Cluster ingress/egress/errors/drops summary stats
+- Per-node per-device receive/transmit rates
+- Instance-level network traffic (Incus `incus_network_*` metrics)
+- Receive/transmit errors and packet drops
+
+### Incus Operations — Logs Explorer
+
+Log exploration powered by Loki with job/host/unit/search variables.
+
+- Log volume over time (stacked by job)
+- Full log stream with search filtering
+- Error rate and warning rate by systemd unit
+
+### Incus Operations — Prometheus Health
+
+Prometheus self-monitoring (no template variables).
+
+- Targets up/down, average scrape duration, samples ingested/s
+- Target health table with UP/DOWN status
+- Scrape duration and samples scraped by job
+- TSDB size, head series, head chunks, WAL size
+- TSDB size trend, samples ingested trend, compactions
+
+### Incus Operations — Capacity Planning
+
+Resource planning with 7-day default time range (no template variables).
+
+- CPU and memory headroom per node (inverted bar gauges)
+- CPU and memory usage trends (7-day)
+- Disk space forecast with 30-day `predict_linear` projection
+- Instance count trend, memory allocation vs usage per instance
## ACL Configuration
@@ -208,11 +274,18 @@ manually or restart the container.
| node-exp-02 | Alpine | 128 MiB | — | oc-node-02 |
| node-exp-03 | Alpine | 128 MiB | — | oc-node-03 |
+**Dummy workloads** (optional, deployed via `--workloads`):
+
+| Container | Image | RAM | Placement | Purpose |
+|-----------|-------|-----|-----------|---------|
+| workload-web | Alpine | 128 MiB | oc-node-01 | nginx + CPU/disk/network crons |
+| workload-api | Alpine | 128 MiB | oc-node-03 | Python HTTP + memory sawtooth |
+
**Totals:**
-- RAM: ~2.4 GiB (2 GiB + 3 x 128 MiB)
+- RAM: ~2.4 GiB core (2 GiB + 3 x 128 MiB), +256 MiB with workloads
- Disk: 20 GiB (only the monitoring container needs significant storage)
- OVN forward IPs: 1 (192.168.103.201)
-- OVN network IPs: 4 (10.10.10.70-73)
+- OVN network IPs: 4 core (10.10.10.70-73), +2 with workloads (.80, .81)
## Management
@@ -229,14 +302,30 @@ installs and configures Prometheus, Grafana, Loki, and Promtail,
provisions dashboards, sets up the OVN network forward, and configures
ACLs.
+### Deploy dummy workloads
+
+```bash
+incusos/deploy-observability --workloads
+```
+
+Deploys two lightweight containers that generate varied metric patterns:
+
+- **workload-web** (10.10.10.80, oc-node-01): nginx serving static
+ content, cron jobs generating HTTP traffic (every minute), CPU spikes
+ via gzip (every 5 min), and disk I/O bursts via dd (every 15 min).
+- **workload-api** (10.10.10.81, oc-node-03): Python HTTP server on
+ port 8080, cron jobs for inter-container traffic (every 2 min) and
+ memory sawtooth via 20 MiB allocation/release (every 10 min).
+
### Check status
```bash
incusos/deploy-observability --status
```
-Shows the state of all containers, scrape target health, Grafana
-accessibility, and OVN forward configuration.
+Shows the state of all containers (including workloads if deployed),
+scrape target health, Grafana accessibility, and OVN forward
+configuration.
### Clean up