#!/bin/bash ################################################################################ # Script Name: restic-backup-exporter.sh # Version: 1.0 # Description: Prometheus exporter for restic backups — last snapshot time, # backup age, repo health, snapshot counts, and size metrics # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - restic installed # - RESTIC_PASSWORD_FILE or RESTIC_PASSWORD env set # - netcat (nc) for HTTP mode # # Usage: # ./restic-backup-exporter.sh --repo /mnt/backup --textfile # ./restic-backup-exporter.sh --repo /mnt/backup --http -p 9200 # RESTIC_REPOSITORY=/mnt/backup ./restic-backup-exporter.sh # # Configuration: # Default HTTP port: 9200 # Textfile directory: /var/lib/node_exporter # ################################################################################ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9200 REPOS=() show_usage() { cat <&2; exit 1 ;; esac done if [ ${#REPOS[@]} -eq 0 ] && [ -n "$RESTIC_REPOSITORY" ]; then REPOS+=("$RESTIC_REPOSITORY") fi } check_restic() { if ! command -v restic >/dev/null 2>&1; then echo "ERROR: restic not found" >&2 return 1 fi return 0 } generate_repo_metrics() { local repo="$1" local repo_label repo_label=$(echo "$repo" | sed 's/[^a-zA-Z0-9_\/-]/_/g') # Check repo exists if ! restic -r "$repo" cat config >/dev/null 2>&1; then echo "restic_backup_repo_initialized{repo=\"$repo_label\"} 0" return fi echo "restic_backup_repo_initialized{repo=\"$repo_label\"} 1" # Snapshots local snapshots_json snapshots_json=$(restic -r "$repo" snapshots --json 2>/dev/null) local snapshot_count snapshot_count=$(echo "$snapshots_json" | jq 'length') echo "restic_backup_snapshot_count_total{repo=\"$repo_label\"} ${snapshot_count:-0}" # Last snapshot local last_ts last_ts=$(echo "$snapshots_json" | jq -r 'sort_by(.time) | last | .time // empty' 2>/dev/null) if [ -n "$last_ts" ]; then local last_unix last_unix=$(date -d "$last_ts" +%s 2>/dev/null || echo 0) local now now=$(date +%s) local age=$((now - last_unix)) echo "restic_backup_last_snapshot_timestamp{repo=\"$repo_label\"} $last_unix" echo "restic_backup_seconds_since_last_snapshot{repo=\"$repo_label\"} $age" else echo "restic_backup_last_snapshot_timestamp{repo=\"$repo_label\"} 0" echo "restic_backup_seconds_since_last_snapshot{repo=\"$repo_label\"} 0" fi # Per-host counts echo "$snapshots_json" | jq -r --arg repo "$repo_label" ' group_by(.hostname) | .[] | "restic_backup_snapshot_count_by_host{repo=\"\($repo)\",hostname=\"\(.[0].hostname)\"} \(length)" ' 2>/dev/null # Stats local stats_json stats_json=$(restic -r "$repo" stats --json 2>/dev/null) local total_size file_count total_size=$(echo "$stats_json" | jq '.total_size // 0') file_count=$(echo "$stats_json" | jq '.total_file_count // 0') echo "restic_backup_size_bytes_total{repo=\"$repo_label\"} ${total_size:-0}" echo "restic_backup_file_count_total{repo=\"$repo_label\"} ${file_count:-0}" # Latest snapshot size local latest_size latest_size=$(echo "$snapshots_json" | jq 'sort_by(.time) | last | .summary.total_bytes_processed // 0' 2>/dev/null) echo "restic_backup_latest_snapshot_size_bytes{repo=\"$repo_label\"} ${latest_size:-0}" # Repo health if restic -r "$repo" check --read-data-subset=1% >/dev/null 2>&1; then echo "restic_backup_repo_healthy{repo=\"$repo_label\"} 1" else echo "restic_backup_repo_healthy{repo=\"$repo_label\"} 0" fi # Lock status local locks locks=$(restic -r "$repo" list locks 2>/dev/null | wc -l) if [ "$locks" -gt 0 ]; then echo "restic_backup_repo_locked{repo=\"$repo_label\"} 1" else echo "restic_backup_repo_locked{repo=\"$repo_label\"} 0" fi } generate_metrics() { local script_start script_start=$(date +%s) if ! check_restic; then echo "# HELP restic_backup_repo_healthy Repo health status" echo "# TYPE restic_backup_repo_healthy gauge" echo "restic_backup_repo_healthy 0" return fi 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" echo "Restic Backup Exporter

Restic Backup Prometheus Exporter

Metrics

" fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } 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}/.restic_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 chmod 644 "$temp_file" mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE" >&2 else generate_metrics fi } main "$@"