#!/bin/bash # # SYNOPSIS # Automated weekly performance monitoring for Jellyfin PostgreSQL database # # DESCRIPTION # This script: # 1. Runs comprehensive diagnostics # 2. Analyzes slow queries # 3. Tracks index usage over time # 4. Generates weekly performance reports # 5. Optionally runs VACUUM ANALYZE on tables with high dead tuples # 6. Can be scheduled via cron # # USAGE # ./Weekly-Performance-Monitor.sh [OPTIONS] # # OPTIONS # -o, --output-directory DIR Directory to save reports (default: reports/weekly) # -e, --email-report If specified, emails the report (requires email configuration) # -c, --compare-with-previous Compare current stats with previous week # -a, --auto-vacuum-analyze Enable auto vacuum analyze # --dead-tuple-threshold NUM Minimum dead tuples to trigger vacuum (default: 100) # --dead-tuple-percent-threshold NUM Minimum dead tuple percentage to trigger vacuum (default: 0.02) # # EXAMPLE # ./Weekly-Performance-Monitor.sh # ./Weekly-Performance-Monitor.sh -o "/reports/jellyfin" -c # ./Weekly-Performance-Monitor.sh -a --dead-tuple-threshold 10000 --dead-tuple-percent-threshold 20 # # CRON JOB EXAMPLE # # Run every Monday at 6am # 0 6 * * 1 /path/to/Weekly-Performance-Monitor.sh -a # # ============================================================================ # Default Configuration # ============================================================================ OUTPUT_DIRECTORY="reports/weekly" EMAIL_REPORT=false COMPARE_WITH_PREVIOUS=false AUTO_VACUUM_ANALYZE=false DEAD_TUPLE_THRESHOLD=100 DEAD_TUPLE_PERCENT_THRESHOLD=0.02 # ============================================================================ # Helper Functions # ============================================================================ # Color definitions CYAN='\033[0;36m' GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' GRAY='\033[0;90m' NC='\033[0m' # No Color write_color_output() { local message="$1" local color="$2" echo -e "${color}${message}${NC}" } get_report_timestamp() { date +"%Y-%m-%d_%H%M%S" } get_week_number() { date +%V } # ============================================================================ # Script Argument Parsing # ============================================================================ while [[ "$#" -gt 0 ]]; do case $1 in -o|--output-directory) OUTPUT_DIRECTORY="$2"; shift ;; -e|--email-report) EMAIL_REPORT=true ;; -c|--compare-with-previous) COMPARE_WITH_PREVIOUS=true ;; -a|--auto-vacuum-analyze) AUTO_VACUUM_ANALYZE=true ;; --dead-tuple-threshold) DEAD_TUPLE_THRESHOLD="$2"; shift ;; --dead-tuple-percent-threshold) DEAD_TUPLE_PERCENT_THRESHOLD="$2"; shift ;; *) write_color_output "Unknown parameter passed: $1" "$RED"; exit 1 ;; esac shift done # ============================================================================ # Configuration # ============================================================================ set -e # Exit on error SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" PROGRAM_DIR="$PROJECT_ROOT" # Assuming program directory is the project root SQL_DIR="$PROGRAM_DIR/sql/diagnostics" # Load database configuration source "./db-config.sh" # ============================================================================ # Functions # ============================================================================ test_database_connection() { write_color_output "\nTesting database connection..." "$CYAN" if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "SELECT version();" > /dev/null 2>&1; then write_color_output "[OK] Database connection successful" "$GREEN" return 0 else write_color_output "[ERROR] Database connection failed" "$RED" return 1 fi } invoke_diagnostic_report() { local output_file="$1" write_color_output "\nRunning comprehensive diagnostics..." "$CYAN" if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$SQL_DIR/diagnostics.sql" > "$output_file" 2>&1; then write_color_output "[OK] Diagnostics completed: $output_file" "$GREEN" else write_color_output "[WARN] Diagnostics completed with warnings" "$YELLOW" fi } invoke_query_analysis() { local output_file="$1" write_color_output "\nAnalyzing slow queries..." "$CYAN" if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$SQL_DIR/query-analysis.sql" > "$output_file" 2>&1; then write_color_output "[OK] Query analysis completed: $output_file" "$GREEN" else write_color_output "[WARN] Query analysis completed with warnings" "$YELLOW" fi } get_index_usage_stats() { local output_file="$1" write_color_output "\nGathering index usage statistics..." "$CYAN" local query=" SELECT schemaname, relname AS tablename, indexrelname, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size FROM pg_stat_user_indexes WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog') ORDER BY schemaname, relname, idx_scan DESC; " if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "$query" > "$output_file" 2>&1; then write_color_output "[OK] Index stats gathered: $output_file" "$GREEN" else write_color_output "[WARN] Index stats completed with warnings" "$YELLOW" fi } get_table_size_stats() { local output_file="$1" write_color_output "\nGathering table size statistics..." "$CYAN" local query=" SELECT schemaname, relname, pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size, pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) AS table_size, pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) AS index_size, n_live_tup AS row_count, n_dead_tup AS dead_rows, ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct FROM pg_stat_user_tables WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog') ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC; " if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "$query" > "$output_file" 2>&1; then write_color_output "[OK] Table size stats gathered: $output_file" "$GREEN" else write_color_output "[WARN] Table size stats completed with warnings" "$YELLOW" fi } invoke_conditional_vacuum_analyze() { local min_dead_tuples="$1" local min_dead_percent="$2" write_color_output "\nChecking for tables that need VACUUM ANALYZE..." "$CYAN" write_color_output "Thresholds: dead tuples >= $min_dead_tuples and dead percent >= $min_dead_percent" "$GRAY" local table_query=" SELECT format('%I.%I', schemaname, relname) AS qualified_table, n_dead_tup, ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct FROM pg_stat_user_tables WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog') AND n_live_tup > 0 AND n_dead_tup >= $min_dead_tuples AND (100.0 * n_dead_tup / NULLIF(n_live_tup, 0)) >= $min_dead_percent ORDER BY n_dead_tup DESC; " tables=$(PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -t -A -F "|" -c "$table_query") if [ -z "$tables" ]; then write_color_output "[OK] No tables exceeded dead tuple thresholds" "$GREEN" return fi write_color_output "Found $(echo "$tables" | wc -l) table(s) requiring VACUUM ANALYZE" "$YELLOW" while IFS= read -r line; do qualified_table=$(echo "$line" | cut -d'|' -f1) dead_tuples=$(echo "$line" | cut -d'|' -f2) dead_percent=$(echo "$line" | cut -d'|' -f3) write_color_output "Running VACUUM ANALYZE on $qualified_table (dead tuples: $dead_tuples, dead percent: $dead_percent)" "$CYAN" if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "VACUUM (ANALYZE, VERBOSE) $qualified_table;"; then write_color_output "[OK] VACUUM ANALYZE completed for $qualified_table" "$GREEN" else write_color_output "[ERROR] VACUUM ANALYZE failed for $qualified_table" "$RED" fi done <<< "$tables" } compare_with_previous() { local current_report="$1" local output_dir="$2" write_color_output "\nComparing with previous week's report..." "$CYAN" previous_report=$(find "$output_dir" -name "diagnostics_*.txt" ! -name "$(basename "$current_report")" -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2-) if [ -z "$previous_report" ]; then write_color_output "[WARN] No previous reports found for comparison" "$YELLOW" return fi write_color_output "Comparing with: $(basename "$previous_report")" "$CYAN" # TODO: Add detailed comparison logic write_color_output "[OK] Comparison data available" "$GREEN" write_color_output " Current: $current_report" "$GRAY" write_color_output " Previous: $previous_report" "$GRAY" } new_summary_report() { local output_dir="$1" local timestamp="$2" write_color_output "\nGenerating summary report..." "$CYAN" local summary_file="$output_dir/summary_$timestamp.txt" cat > "$summary_file" <