- Pure-Python scraper replaces shell+Python hybrid (fixes pushgateway RemoteDisconnected crash caused by duplicate volume name labels) - Scraper dynamically discovers all Scaleway resources via API: billing, instances, K8s pools, block volumes, IPs - 40-panel cockpit dashboard with fully dynamic tables (auto-adapts when infra changes — no hardcoded instance/volume names) - Volume table keyed by UUID (not truncated name) to prevent collisions - Label sanitization for Prometheus text format safety - Pushgateway push via PUT + Content-Type: text/plain; version=0.0.4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
5.8 KiB
Bash
Executable File
213 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Import all Grafana dashboards via API (Postgres-backed, no ConfigMaps).
|
|
#
|
|
# Dashboards use ${DS_PROMETHEUS}, ${DS_LOKI}, ${DS_TEMPO} variables.
|
|
# This script auto-detects datasource UIDs, resolves variables, and
|
|
# imports via the Grafana API. Dashboards are stored in Postgres.
|
|
#
|
|
# Required environment variables:
|
|
# GRAFANA_PASS - Password for admin user
|
|
#
|
|
# Optional overrides:
|
|
# GRAFANA_URL - Grafana base URL (default: https://grafana.fxhnt.ai)
|
|
# GRAFANA_USER - Grafana user (default: admin)
|
|
# GRAFANA_PROM_UID - Override Prometheus datasource UID
|
|
# GRAFANA_LOKI_UID - Override Loki datasource UID
|
|
# GRAFANA_TEMPO_UID - Override Tempo datasource UID
|
|
#
|
|
# Usage:
|
|
# GRAFANA_PASS="xxx" ./import.sh
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
GRAFANA_USER="${GRAFANA_USER:-admin}"
|
|
GRAFANA_URL="${GRAFANA_URL:-https://grafana.fxhnt.ai}"
|
|
GRAFANA_URL="${GRAFANA_URL%/}"
|
|
|
|
if [[ -z "${GRAFANA_PASS:-}" ]]; then
|
|
echo "ERROR: GRAFANA_PASS is not set" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- Datasource auto-detection ---
|
|
|
|
detect_datasource_uid() {
|
|
local ds_type="$1"
|
|
curl -sf -u "${GRAFANA_USER}:${GRAFANA_PASS}" "${GRAFANA_URL}/api/datasources" 2>/dev/null | \
|
|
python3 -c "
|
|
import sys, json
|
|
for ds in json.load(sys.stdin):
|
|
if ds.get('type') == '${ds_type}':
|
|
print(ds['uid'])
|
|
break
|
|
" 2>/dev/null || true
|
|
}
|
|
|
|
echo "Detecting datasource UIDs..."
|
|
PROM_UID="${GRAFANA_PROM_UID:-$(detect_datasource_uid prometheus)}"
|
|
LOKI_UID="${GRAFANA_LOKI_UID:-$(detect_datasource_uid loki)}"
|
|
TEMPO_UID="${GRAFANA_TEMPO_UID:-$(detect_datasource_uid tempo)}"
|
|
|
|
echo " Prometheus: ${PROM_UID:-NOT FOUND}"
|
|
echo " Loki: ${LOKI_UID:-NOT FOUND}"
|
|
echo " Tempo: ${TEMPO_UID:-NOT FOUND}"
|
|
|
|
if [[ -z "$PROM_UID" ]]; then
|
|
echo "ERROR: Could not detect Prometheus datasource. Set GRAFANA_PROM_UID." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- Resolve datasource variables ---
|
|
|
|
resolve_dashboard() {
|
|
local file="$1"
|
|
python3 -c "
|
|
import sys, json
|
|
|
|
with open('${file}') as f:
|
|
d = json.load(f)
|
|
|
|
d.pop('__inputs', None)
|
|
d.pop('__requires', None)
|
|
|
|
raw = json.dumps(d)
|
|
raw = raw.replace('\${DS_PROMETHEUS}', '${PROM_UID}')
|
|
raw = raw.replace('\${DS_LOKI}', '${LOKI_UID}')
|
|
raw = raw.replace('\${DS_TEMPO}', '${TEMPO_UID}')
|
|
|
|
print(raw)
|
|
"
|
|
}
|
|
|
|
# --- Ensure folders exist ---
|
|
|
|
ensure_folder() {
|
|
local title="$1"
|
|
local uid
|
|
uid=$(curl -sf -u "${GRAFANA_USER}:${GRAFANA_PASS}" "${GRAFANA_URL}/api/folders" 2>/dev/null | \
|
|
python3 -c "
|
|
import sys, json
|
|
for f in json.load(sys.stdin):
|
|
if f['title'] == '${title}':
|
|
print(f['uid'])
|
|
break
|
|
" 2>/dev/null || true)
|
|
|
|
if [[ -z "$uid" ]]; then
|
|
uid=$(curl -sf -u "${GRAFANA_USER}:${GRAFANA_PASS}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"title\": \"${title}\"}" \
|
|
"${GRAFANA_URL}/api/folders" 2>/dev/null | \
|
|
python3 -c "import sys,json; print(json.load(sys.stdin)['uid'])" 2>/dev/null)
|
|
echo " Created folder: ${title} (${uid})"
|
|
fi
|
|
echo "$uid"
|
|
}
|
|
|
|
echo ""
|
|
echo "Ensuring folders..."
|
|
FOLDER_CI=$(ensure_folder "CI")
|
|
FOLDER_INFRA=$(ensure_folder "Infrastructure")
|
|
FOLDER_OPS=$(ensure_folder "Operations")
|
|
FOLDER_TRADING=$(ensure_folder "Trading")
|
|
FOLDER_TRAINING=$(ensure_folder "Training")
|
|
FOLDER_SERVICES=$(ensure_folder "Services")
|
|
FOLDER_PLATFORM=$(ensure_folder "Platform")
|
|
|
|
# --- Import a single dashboard to a folder ---
|
|
|
|
import_dashboard() {
|
|
local file="$1"
|
|
local folder_uid="$2"
|
|
local name
|
|
name="$(basename "$file" .json)"
|
|
|
|
local resolved
|
|
resolved=$(resolve_dashboard "$file")
|
|
|
|
local response
|
|
response=$(curl -s -w "\n%{http_code}" \
|
|
-u "${GRAFANA_USER}:${GRAFANA_PASS}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"dashboard\": ${resolved}, \"folderUid\": \"${folder_uid}\", \"overwrite\": true}" \
|
|
"${GRAFANA_URL}/api/dashboards/db")
|
|
|
|
local http_code
|
|
http_code=$(echo "$response" | tail -1)
|
|
|
|
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
|
|
echo " OK ${name}"
|
|
else
|
|
local body
|
|
body=$(echo "$response" | sed '$d')
|
|
echo " FAIL ${name} (${http_code}): ${body}" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# --- Dashboard → Folder mapping ---
|
|
|
|
declare -A DASHBOARD_FOLDERS=(
|
|
# Original dashboards
|
|
[foxhunt-cockpit]="${FOLDER_OPS}"
|
|
[foxhunt-trading]="${FOLDER_TRADING}"
|
|
[foxhunt-training]="${FOLDER_TRAINING}"
|
|
[foxhunt-infrastructure]="${FOLDER_INFRA}"
|
|
[foxhunt-observability]="${FOLDER_INFRA}"
|
|
[foxhunt-ci-logs]="${FOLDER_CI}"
|
|
[foxhunt-training-logs]="${FOLDER_TRAINING}"
|
|
[foxhunt-training-deep-dive]="${FOLDER_TRAINING}"
|
|
# Application services
|
|
[svc-api-gateway]="${FOLDER_SERVICES}"
|
|
[svc-trading-service]="${FOLDER_SERVICES}"
|
|
[svc-backtesting]="${FOLDER_SERVICES}"
|
|
[svc-data-acquisition]="${FOLDER_SERVICES}"
|
|
[svc-ml-training]="${FOLDER_SERVICES}"
|
|
[svc-trading-agent]="${FOLDER_SERVICES}"
|
|
[svc-web-gateway]="${FOLDER_SERVICES}"
|
|
# Platform services
|
|
[platform-prometheus]="${FOLDER_PLATFORM}"
|
|
[platform-grafana]="${FOLDER_PLATFORM}"
|
|
[platform-loki]="${FOLDER_PLATFORM}"
|
|
[platform-tempo]="${FOLDER_PLATFORM}"
|
|
[platform-redis]="${FOLDER_PLATFORM}"
|
|
[platform-postgres]="${FOLDER_PLATFORM}"
|
|
[platform-argo]="${FOLDER_PLATFORM}"
|
|
[platform-questdb]="${FOLDER_PLATFORM}"
|
|
[platform-costs]="${FOLDER_PLATFORM}"
|
|
[platform-scaleway]="${FOLDER_PLATFORM}"
|
|
)
|
|
|
|
# --- Deploy all dashboards ---
|
|
|
|
echo ""
|
|
echo "Importing dashboards..."
|
|
|
|
failed=0
|
|
total=0
|
|
|
|
for dashboard in "${SCRIPT_DIR}"/*.json; do
|
|
[[ -f "$dashboard" ]] || continue
|
|
name="$(basename "$dashboard" .json)"
|
|
folder_uid="${DASHBOARD_FOLDERS[$name]:-}"
|
|
|
|
if [[ -z "$folder_uid" ]]; then
|
|
echo " SKIP ${name} (no folder mapping)"
|
|
continue
|
|
fi
|
|
|
|
total=$((total + 1))
|
|
if ! import_dashboard "$dashboard" "$folder_uid"; then
|
|
failed=$((failed + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [[ $failed -eq 0 ]]; then
|
|
echo "All ${total} dashboards deployed successfully."
|
|
else
|
|
echo "ERROR: ${failed}/${total} dashboard(s) failed." >&2
|
|
exit 1
|
|
fi
|