#!/bin/bash ################################################################################ # Script Name: caprover-exporter.sh # Version: 1.0 # Description: Prometheus exporter for CapRover PaaS providing operational # metrics via the CapRover API — app deployment status, container # health, resource usage, and platform metrics # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - CapRover installed and running # - CapRover API accessible (default: http://localhost:3000) # - curl for API calls # - jq for JSON parsing # - netcat (nc) for HTTP mode # # Usage: # ./caprover-exporter.sh # Output to stdout # ./caprover-exporter.sh --http -p 9196 # HTTP server mode # ./caprover-exporter.sh --textfile # Textfile collector mode # ./caprover-exporter.sh --password secret # Custom password # # Metrics Exported: # - caprover_up - API reachability (1=up, 0=down) # - caprover_info{version} - CapRover version info # - caprover_apps_total - Total app count # - caprover_apps_running - Running app count # - caprover_apps_stopped - Stopped app count # - caprover_app_running{app} - Per-app running status (1/0) # - caprover_app_instance_count{app} - Per-app replica count # - caprover_app_has_ssl{app} - Per-app SSL status (1/0) # - caprover_app_force_ssl{app} - Per-app force SSL status (1/0) # - caprover_nodes_total - Swarm node count # - caprover_volumes_total - Docker volume count # - caprover_disk_used_bytes - Disk usage in bytes # - caprover_disk_total_bytes - Total disk in bytes # - caprover_exporter_duration_seconds - Script execution time # - caprover_exporter_last_run_timestamp - Last run timestamp # # Configuration: # Default HTTP port: 9196 # Default CapRover URL: http://localhost:3000 # Default password: captain42 # Textfile directory: /var/lib/node_exporter # ################################################################################ set -euo pipefail # ============================================================================ # CONFIGURATION VARIABLES # ============================================================================ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9196 CAPROVER_URL="http://localhost:3000" CAPROVER_PASSWORD="captain42" AUTH_TOKEN="" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ show_usage() { cat <&2; exit 1 ;; esac done } check_prerequisites() { if ! command -v curl >/dev/null 2>&1; then echo "ERROR: curl not found" >&2; return 1 fi if ! command -v jq >/dev/null 2>&1; then echo "ERROR: jq not found (required for JSON parsing)" >&2; return 1 fi return 0 } prom_escape() { local val="$1" val="${val//\\/\\\\}" val="${val//\"/\\\"}" val="${val//$'\n'/}" echo "$val" } authenticate() { if [ -n "$AUTH_TOKEN" ]; then return 0; fi local response response=$(curl -s -X POST \ -H "Content-Type: application/json" \ -H "x-namespace: captain" \ -d "{\"password\":\"${CAPROVER_PASSWORD}\"}" \ "${CAPROVER_URL}/api/v2/login" 2>/dev/null) || return 1 local status status=$(echo "$response" | jq -r '.status // 0' 2>/dev/null) if [ "$status" != "100" ]; then echo "ERROR: Failed to authenticate with CapRover API" >&2; return 1 fi AUTH_TOKEN=$(echo "$response" | jq -r '.data.token // empty' 2>/dev/null) if [ -z "$AUTH_TOKEN" ]; then echo "ERROR: No auth token received from CapRover API" >&2; return 1 fi return 0 } api_call() { local endpoint="$1" curl -s -X POST \ -H "Content-Type: application/json" \ -H "x-namespace: captain" \ -H "x-captain-auth: ${AUTH_TOKEN}" \ -d "{}" \ "${CAPROVER_URL}${endpoint}" 2>/dev/null } api_get() { local endpoint="$1" curl -s -X GET \ -H "Content-Type: application/json" \ -H "x-namespace: captain" \ -H "x-captain-auth: ${AUTH_TOKEN}" \ "${CAPROVER_URL}${endpoint}" 2>/dev/null } # ============================================================================ # METRIC GENERATION # ============================================================================ generate_metrics() { local script_start script_start=$(date +%s) if ! check_prerequisites; then echo "# HELP caprover_up CapRover API reachability (1=up, 0=down)" echo "# TYPE caprover_up gauge" echo "caprover_up 0" return fi AUTH_TOKEN="" if ! authenticate; then echo "# HELP caprover_up CapRover API reachability (1=up, 0=down)" echo "# TYPE caprover_up gauge" echo "caprover_up 0" return fi cat </dev/null) if [ "$info_status" = "100" ]; then caprover_version=$(echo "$system_info" | jq -r '.data.caproverVersion // "unknown"' 2>/dev/null) node_count=$(echo "$system_info" | jq -r '.data.swarmNodesCount // 0' 2>/dev/null) node_count=${node_count:-0} disk_used=$(echo "$system_info" | jq -r '.data.diskUsedInMb // 0' 2>/dev/null) disk_total=$(echo "$system_info" | jq -r '.data.diskTotalInMb // 0' 2>/dev/null) disk_used=${disk_used:-0}; disk_total=${disk_total:-0} [ "$disk_used" != "0" ] && disk_used=$((disk_used * 1024 * 1024)) [ "$disk_total" != "0" ] && disk_total=$((disk_total * 1024 * 1024)) volume_count=$(echo "$system_info" | jq -r '.data.dockerVolumesCount // 0' 2>/dev/null) volume_count=${volume_count:-0} fi fi cat </dev/null) if [ "$apps_status" = "100" ]; then total_apps=$(echo "$apps_response" | jq '.data.appDefinitions | length // 0' 2>/dev/null) total_apps=${total_apps:-0} running_apps=$(echo "$apps_response" | jq '[.data.appDefinitions[] | select(.deployedVersion != null and .deployedVersion != 0)] | length' 2>/dev/null) stopped_apps=$(echo "$apps_response" | jq '[.data.appDefinitions[] | select(.deployedVersion == null or .deployedVersion == 0)] | length' 2>/dev/null) running_apps=${running_apps:-0}; stopped_apps=${stopped_apps:-0} cat </dev/null | while read -r name val; do [ -z "$name" ] && continue echo "caprover_app_running{app=\"$(prom_escape "$name")\"} $val" done echo "" # Per-app instance count echo "# HELP caprover_app_instance_count Number of replicas per app" echo "# TYPE caprover_app_instance_count gauge" echo "$apps_response" | jq -r '.data.appDefinitions[] | "\(.appName) \(.instanceCount // 1)"' 2>/dev/null | while read -r name val; do [ -z "$name" ] && continue echo "caprover_app_instance_count{app=\"$(prom_escape "$name")\"} $val" done echo "" # Per-app SSL status echo "# HELP caprover_app_has_ssl SSL enabled per app (1=yes, 0=no)" echo "# TYPE caprover_app_has_ssl gauge" echo "$apps_response" | jq -r '.data.appDefinitions[] | "\(.appName) \(if .hasDefaultSubDomainSsl == true then 1 else 0 end)"' 2>/dev/null | while read -r name val; do [ -z "$name" ] && continue echo "caprover_app_has_ssl{app=\"$(prom_escape "$name")\"} $val" done echo "" # Per-app force SSL status echo "# HELP caprover_app_force_ssl Force SSL per app (1=yes, 0=no)" echo "# TYPE caprover_app_force_ssl gauge" echo "$apps_response" | jq -r '.data.appDefinitions[] | "\(.appName) \(if .forceSsl == true then 1 else 0 end)"' 2>/dev/null | while read -r name val; do [ -z "$name" ] && continue echo "caprover_app_force_ssl{app=\"$(prom_escape "$name")\"} $val" done else echo "# HELP caprover_apps_total Total number of deployed apps" echo "# TYPE caprover_apps_total gauge" echo "caprover_apps_total 0" fi else echo "# HELP caprover_apps_total Total number of deployed apps" echo "# TYPE caprover_apps_total gauge" echo "caprover_apps_total 0" fi echo "" # ======================================================================== # EXPORTER RUNTIME # ======================================================================== local script_end script_duration script_end=$(date +%s) script_duration=$((script_end - script_start)) cat <&2 if ! command -v nc >/dev/null 2>&1; then echo "ERROR: netcat (nc) required for HTTP mode" >&2 exit 1 fi while true; do { read -r request if [[ "$request" =~ ^GET\ /metrics ]]; then echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r" generate_metrics else echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r" cat < CapRover Exporter v1.0

CapRover Prometheus Exporter v1.0

Metrics

Operational metrics from the CapRover API.

EOF fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } # ============================================================================ # MAIN EXECUTION # ============================================================================ main() { parse_args "$@" if [ "$HTTP_MODE" = true ]; then run_http_server elif [ -n "$OUTPUT_FILE" ]; then local output_dir output_dir="$(dirname "$OUTPUT_FILE")" mkdir -p "$output_dir" local temp_file temp_file=$(mktemp "${output_dir}/.caprover_metrics.XXXXXX") if ! generate_metrics > "$temp_file" 2>/dev/null; then rm -f "$temp_file" echo "ERROR: Failed to generate metrics" >&2 exit 1 fi local file_lines file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0) if [ "$file_lines" -lt 10 ]; then rm -f "$temp_file" echo "ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2 exit 1 fi chmod 644 "$temp_file" mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2 else generate_metrics fi } main "$@"