#!/bin/bash ################################################################################ # Backup Metrics JSON Generator ################################################################################ # # Author: Peter Wood # Description: Generates comprehensive JSON metrics for all backup services # to support web application monitoring and management interface. # # Features: # - Scans backup directory structure automatically # - Extracts metadata from backup files (size, timestamps, checksums) # - Generates standardized JSON metrics per service # - Handles scheduled backup subdirectories # - Includes performance metrics from log files # - Creates consolidated metrics index # # Output Structure: # /mnt/share/media/backups/metrics/ # ├── index.json # Service directory index # ├── {service_name}/ # │ ├── metrics.json # Service backup metrics # │ └── history.json # Historical backup data # └── consolidated.json # All services summary # # Usage: # ./generate-backup-metrics.sh # Generate all metrics # ./generate-backup-metrics.sh plex # Generate metrics for specific service # ./generate-backup-metrics.sh --watch # Monitor mode with auto-refresh # ################################################################################ set -e # Colors for output GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # Configuration BACKUP_ROOT="${BACKUP_ROOT:-/mnt/share/media/backups}" METRICS_ROOT="${BACKUP_ROOT}/metrics" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" LOG_FILE="${SCRIPT_DIR}/logs/backup-metrics-$(date +%Y%m%d).log" # Ensure required directories exist mkdir -p "${METRICS_ROOT}" "${SCRIPT_DIR}/logs" # Logging functions log_message() { local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${CYAN}[${timestamp}]${NC} ${message}" echo "[${timestamp}] $message" >> "$LOG_FILE" 2>/dev/null || true } log_error() { local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${RED}[${timestamp}] ERROR:${NC} ${message}" >&2 echo "[${timestamp}] ERROR: $message" >> "$LOG_FILE" 2>/dev/null || true } log_success() { local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${GREEN}[${timestamp}] SUCCESS:${NC} ${message}" echo "[${timestamp}] SUCCESS: $message" >> "$LOG_FILE" 2>/dev/null || true } log_warning() { local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${YELLOW}[${timestamp}] WARNING:${NC} ${message}" echo "[${timestamp}] WARNING: $message" >> "$LOG_FILE" 2>/dev/null || true } # Check dependencies check_dependencies() { local missing_deps=() for cmd in jq stat find; do if ! command -v "$cmd" >/dev/null 2>&1; then missing_deps+=("$cmd") fi done if [ ${#missing_deps[@]} -gt 0 ]; then log_error "Missing required dependencies: ${missing_deps[*]}" log_error "Install with: sudo apt-get install jq coreutils findutils" return 1 fi return 0 } # Get file metadata in JSON format get_file_metadata() { local file_path="$1" if [ ! -f "$file_path" ]; then echo "{}" return 1 fi local size_bytes=$(stat -c%s "$file_path" 2>/dev/null || echo "0") local size_mb=$((size_bytes / 1048576)) local modified_epoch=$(stat -c%Y "$file_path" 2>/dev/null || echo "0") local modified_iso=$(date -d "@$modified_epoch" --iso-8601=seconds 2>/dev/null || echo "") local checksum="" # Calculate checksum for smaller files (< 100MB) to avoid long delays if [ "$size_mb" -lt 100 ]; then checksum=$(md5sum "$file_path" 2>/dev/null | cut -d' ' -f1 || echo "") fi jq -n \ --arg path "$file_path" \ --arg filename "$(basename "$file_path")" \ --argjson size_bytes "$size_bytes" \ --argjson size_mb "$size_mb" \ --arg size_human "$(numfmt --to=iec-i --suffix=B "$size_bytes" 2>/dev/null || echo "${size_mb}MB")" \ --argjson modified_epoch "$modified_epoch" \ --arg modified_iso "$modified_iso" \ --arg checksum "$checksum" \ '{ path: $path, filename: $filename, size: { bytes: $size_bytes, mb: $size_mb, human: $size_human }, modified: { epoch: $modified_epoch, iso: $modified_iso }, checksum: $checksum }' } # Extract timestamp from filename patterns extract_timestamp_from_filename() { local filename="$1" local timestamp="" # Try various timestamp patterns if [[ "$filename" =~ ([0-9]{8}_[0-9]{6}) ]]; then # Format: YYYYMMDD_HHMMSS local date_part="${BASH_REMATCH[1]}" timestamp=$(date -d "${date_part:0:8} ${date_part:9:2}:${date_part:11:2}:${date_part:13:2}" --iso-8601=seconds 2>/dev/null || echo "") elif [[ "$filename" =~ ([0-9]{8}-[0-9]{6}) ]]; then # Format: YYYYMMDD-HHMMSS local date_part="${BASH_REMATCH[1]}" timestamp=$(date -d "${date_part:0:8} ${date_part:9:2}:${date_part:11:2}:${date_part:13:2}" --iso-8601=seconds 2>/dev/null || echo "") elif [[ "$filename" =~ ([0-9]{4}-[0-9]{2}-[0-9]{2}) ]]; then # Format: YYYY-MM-DD (assume midnight) timestamp=$(date -d "${BASH_REMATCH[1]}" --iso-8601=seconds 2>/dev/null || echo "") fi echo "$timestamp" } # Parse performance logs for runtime metrics parse_performance_logs() { local service_name="$1" local service_dir="$2" local performance_data="{}" # Look for performance logs in various locations local log_patterns=( "${service_dir}/logs/*.json" "${BACKUP_ROOT}/logs/*${service_name}*.json" "${SCRIPT_DIR}/logs/*${service_name}*.json" ) for pattern in "${log_patterns[@]}"; do for log_file in ${pattern}; do if [ -f "$log_file" ]; then log_message "Found performance log: $log_file" # Try to parse JSON performance data if jq empty "$log_file" 2>/dev/null; then local log_data=$(cat "$log_file") performance_data=$(echo "$performance_data" | jq --argjson new_data "$log_data" '. + $new_data') fi fi done done echo "$performance_data" } # Get backup metrics for a service get_service_metrics() { local service_name="$1" local service_dir="${BACKUP_ROOT}/${service_name}" if [ ! -d "$service_dir" ]; then log_warning "Service directory not found: $service_dir" return 1 fi log_message "Processing service: $service_name" local backup_files=() local scheduled_files=() local total_size_bytes=0 local latest_backup="" local latest_timestamp=0 # Find backup files in main directory while IFS= read -r -d '' file; do if [ -f "$file" ]; then backup_files+=("$file") local file_size=$(stat -c%s "$file" 2>/dev/null || echo "0") total_size_bytes=$((total_size_bytes + file_size)) # Check if this is the latest backup local file_timestamp=$(stat -c%Y "$file" 2>/dev/null || echo "0") if [ "$file_timestamp" -gt "$latest_timestamp" ]; then latest_timestamp="$file_timestamp" latest_backup="$file" fi fi done < <(find "$service_dir" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sql" -o -name "*.sql.gz" -o -name "*.db" \) -print0 2>/dev/null || true) # Find backup files in scheduled subdirectory local scheduled_dir="${service_dir}/scheduled" if [ -d "$scheduled_dir" ]; then while IFS= read -r -d '' file; do if [ -f "$file" ]; then scheduled_files+=("$file") local file_size=$(stat -c%s "$file" 2>/dev/null || echo "0") total_size_bytes=$((total_size_bytes + file_size)) # Check if this is the latest backup local file_timestamp=$(stat -c%Y "$file" 2>/dev/null || echo "0") if [ "$file_timestamp" -gt "$latest_timestamp" ]; then latest_timestamp="$file_timestamp" latest_backup="$file" fi fi done < <(find "$scheduled_dir" -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sql" -o -name "*.sql.gz" -o -name "*.db" \) -print0 2>/dev/null || true) fi # Calculate metrics local total_files=$((${#backup_files[@]} + ${#scheduled_files[@]})) local total_size_mb=$((total_size_bytes / 1048576)) local total_size_human=$(numfmt --to=iec-i --suffix=B "$total_size_bytes" 2>/dev/null || echo "${total_size_mb}MB") # Get latest backup metadata local latest_backup_metadata="{}" if [ -n "$latest_backup" ]; then latest_backup_metadata=$(get_file_metadata "$latest_backup") fi # Parse performance logs local performance_metrics performance_metrics=$(parse_performance_logs "$service_name" "$service_dir") # Generate service metrics JSON local service_metrics service_metrics=$(jq -n \ --arg service_name "$service_name" \ --arg backup_path "$service_dir" \ --arg scheduled_path "$scheduled_dir" \ --argjson total_files "$total_files" \ --argjson main_files "${#backup_files[@]}" \ --argjson scheduled_files "${#scheduled_files[@]}" \ --argjson total_size_bytes "$total_size_bytes" \ --argjson total_size_mb "$total_size_mb" \ --arg total_size_human "$total_size_human" \ --argjson latest_backup "$latest_backup_metadata" \ --argjson performance "$performance_metrics" \ --arg generated_at "$(date --iso-8601=seconds)" \ --argjson generated_epoch "$(date +%s)" \ '{ service_name: $service_name, backup_path: $backup_path, scheduled_path: $scheduled_path, summary: { total_files: $total_files, main_directory_files: $main_files, scheduled_directory_files: $scheduled_files, total_size: { bytes: $total_size_bytes, mb: $total_size_mb, human: $total_size_human } }, latest_backup: $latest_backup, performance_metrics: $performance, metadata: { generated_at: $generated_at, generated_epoch: $generated_epoch } }') # Create service metrics directory local service_metrics_dir="${METRICS_ROOT}/${service_name}" mkdir -p "$service_metrics_dir" # Write service metrics echo "$service_metrics" | jq '.' > "${service_metrics_dir}/metrics.json" log_success "Generated metrics for $service_name (${total_files} files, ${total_size_human})" # Generate detailed file history generate_service_history "$service_name" "$service_dir" "$service_metrics_dir" echo "$service_metrics" } # Generate detailed backup history for a service generate_service_history() { local service_name="$1" local service_dir="$2" local output_dir="$3" local history_array="[]" local file_count=0 # Process all backup files local search_dirs=("$service_dir") if [ -d "${service_dir}/scheduled" ]; then search_dirs+=("${service_dir}/scheduled") fi for search_dir in "${search_dirs[@]}"; do if [ ! -d "$search_dir" ]; then continue fi while IFS= read -r -d '' file; do if [ -f "$file" ]; then local file_metadata file_metadata=$(get_file_metadata "$file") # Add extracted timestamp local filename_timestamp filename_timestamp=$(extract_timestamp_from_filename "$(basename "$file")") file_metadata=$(echo "$file_metadata" | jq --arg ts "$filename_timestamp" '. + {filename_timestamp: $ts}') # Determine if file is in scheduled directory local is_scheduled=false if [[ "$file" == *"/scheduled/"* ]]; then is_scheduled=true fi file_metadata=$(echo "$file_metadata" | jq --argjson scheduled "$is_scheduled" '. + {is_scheduled: $scheduled}') history_array=$(echo "$history_array" | jq --argjson item "$file_metadata" '. + [$item]') file_count=$((file_count + 1)) fi done < <(find "$search_dir" -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sql" -o -name "*.sql.gz" -o -name "*.db" \) -print0 2>/dev/null || true) done # Sort by modification time (newest first) history_array=$(echo "$history_array" | jq 'sort_by(.modified.epoch) | reverse') # Create history JSON local history_json history_json=$(jq -n \ --arg service_name "$service_name" \ --argjson total_files "$file_count" \ --argjson files "$history_array" \ --arg generated_at "$(date --iso-8601=seconds)" \ '{ service_name: $service_name, total_files: $total_files, files: $files, generated_at: $generated_at }') echo "$history_json" | jq '.' > "${output_dir}/history.json" log_message "Generated history for $service_name ($file_count files)" } # Discover all backup services discover_services() { local services=() if [ ! -d "$BACKUP_ROOT" ]; then log_error "Backup root directory not found: $BACKUP_ROOT" return 1 fi # Find all subdirectories that contain backup files while IFS= read -r -d '' dir; do local service_name=$(basename "$dir") # Skip metrics directory if [ "$service_name" = "metrics" ]; then continue fi # Check if directory contains backup files local has_backups=false # Check main directory if find "$dir" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sql" -o -name "*.sql.gz" -o -name "*.db" \) -print -quit 2>/dev/null | grep -q .; then has_backups=true fi # Check scheduled subdirectory if [ -d "${dir}/scheduled" ] && find "${dir}/scheduled" -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sql" -o -name "*.sql.gz" -o -name "*.db" \) -print -quit 2>/dev/null | grep -q .; then has_backups=true fi if [ "$has_backups" = true ]; then services+=("$service_name") fi done < <(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null || true) printf '%s\n' "${services[@]}" } # Generate consolidated metrics index generate_consolidated_metrics() { local services=("$@") local consolidated_data="[]" local total_services=${#services[@]} local total_size_bytes=0 local total_files=0 for service in "${services[@]}"; do local service_metrics_file="${METRICS_ROOT}/${service}/metrics.json" if [ -f "$service_metrics_file" ]; then local service_data=$(cat "$service_metrics_file") consolidated_data=$(echo "$consolidated_data" | jq --argjson service "$service_data" '. + [$service]') # Add to totals local service_size=$(echo "$service_data" | jq -r '.summary.total_size.bytes // 0') local service_files=$(echo "$service_data" | jq -r '.summary.total_files // 0') total_size_bytes=$((total_size_bytes + service_size)) total_files=$((total_files + service_files)) fi done # Generate consolidated summary local total_size_mb=$((total_size_bytes / 1048576)) local total_size_human=$(numfmt --to=iec-i --suffix=B "$total_size_bytes" 2>/dev/null || echo "${total_size_mb}MB") local consolidated_json consolidated_json=$(jq -n \ --argjson services "$consolidated_data" \ --argjson total_services "$total_services" \ --argjson total_files "$total_files" \ --argjson total_size_bytes "$total_size_bytes" \ --argjson total_size_mb "$total_size_mb" \ --arg total_size_human "$total_size_human" \ --arg generated_at "$(date --iso-8601=seconds)" \ '{ summary: { total_services: $total_services, total_files: $total_files, total_size: { bytes: $total_size_bytes, mb: $total_size_mb, human: $total_size_human } }, services: $services, generated_at: $generated_at }') echo "$consolidated_json" | jq '.' > "${METRICS_ROOT}/consolidated.json" log_success "Generated consolidated metrics ($total_services services, $total_files files, $total_size_human)" } # Generate service index generate_service_index() { local services=("$@") local index_array="[]" for service in "${services[@]}"; do local service_info service_info=$(jq -n \ --arg name "$service" \ --arg metrics_path "/metrics/${service}/metrics.json" \ --arg history_path "/metrics/${service}/history.json" \ '{ name: $name, metrics_path: $metrics_path, history_path: $history_path }') index_array=$(echo "$index_array" | jq --argjson service "$service_info" '. + [$service]') done local index_json index_json=$(jq -n \ --argjson services "$index_array" \ --arg generated_at "$(date --iso-8601=seconds)" \ '{ services: $services, generated_at: $generated_at }') echo "$index_json" | jq '.' > "${METRICS_ROOT}/index.json" log_success "Generated service index (${#services[@]} services)" } # Watch mode for continuous updates watch_mode() { log_message "Starting watch mode - generating metrics every 60 seconds" log_message "Press Ctrl+C to stop" while true; do log_message "Generating metrics..." main_generate_metrics "" log_message "Next update in 60 seconds..." sleep 60 done } # Main metrics generation function main_generate_metrics() { local target_service="$1" log_message "Starting backup metrics generation" # Check dependencies if ! check_dependencies; then return 1 fi # Discover services log_message "Discovering backup services..." local services readarray -t services < <(discover_services) if [ ${#services[@]} -eq 0 ]; then log_warning "No backup services found in $BACKUP_ROOT" return 0 fi log_message "Found ${#services[@]} backup services: ${services[*]}" # Generate metrics for specific service or all services if [ -n "$target_service" ]; then if [[ " ${services[*]} " =~ " $target_service " ]]; then get_service_metrics "$target_service" else log_error "Service not found: $target_service" log_message "Available services: ${services[*]}" return 1 fi else # Generate metrics for all services for service in "${services[@]}"; do get_service_metrics "$service" done # Generate consolidated metrics and index generate_consolidated_metrics "${services[@]}" generate_service_index "${services[@]}" fi log_success "Metrics generation completed" log_message "Metrics location: $METRICS_ROOT" } # Help function show_help() { echo -e "${BLUE}Backup Metrics JSON Generator${NC}" echo "" echo "Usage: $0 [options] [service_name]" echo "" echo "Options:" echo " -h, --help Show this help message" echo " --watch Monitor mode with auto-refresh every 60 seconds" echo "" echo "Examples:" echo " $0 # Generate metrics for all services" echo " $0 plex # Generate metrics for Plex service only" echo " $0 --watch # Monitor mode with auto-refresh" echo "" echo "Output:" echo " Metrics are generated in: $METRICS_ROOT" echo " - index.json: Service directory" echo " - consolidated.json: All services summary" echo " - {service}/metrics.json: Individual service metrics" echo " - {service}/history.json: Individual service file history" } # Main script logic main() { case "${1:-}" in -h|--help) show_help exit 0 ;; --watch) watch_mode ;; *) main_generate_metrics "$1" ;; esac } # Run main function main "$@"