393 lines
14 KiB
Bash
393 lines
14 KiB
Bash
#!/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 "$SCRIPT_DIR")"
|
|
|
|
# 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 "$PROJECT_ROOT/sql/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 "$PROJECT_ROOT/sql/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" <<EOF
|
|
================================================================================
|
|
JELLYFIN DATABASE WEEKLY PERFORMANCE SUMMARY
|
|
================================================================================
|
|
Report Date: $(date +"%Y-%m-%d %H:%M:%S")
|
|
Database: $DB_NAME at $DB_HOST
|
|
Week Number: $(get_week_number)
|
|
================================================================================
|
|
|
|
REPORTS GENERATED:
|
|
* diagnostics_$timestamp.txt : Full diagnostic report
|
|
* query_analysis_$timestamp.txt : Slow query analysis
|
|
* index_usage_$timestamp.txt : Index usage statistics
|
|
* table_sizes_$timestamp.txt : Table size and bloat info
|
|
* summary_$timestamp.txt : This summary
|
|
|
|
================================================================================
|
|
QUICK HEALTH CHECK:
|
|
================================================================================
|
|
|
|
To view critical issues check diagnostics file for:
|
|
* Section 5: TABLES WITH HIGH SEQUENTIAL SCANS
|
|
* Section 8: UNUSED OR RARELY USED INDEXES
|
|
* Section 10: SLOWEST QUERIES
|
|
|
|
To identify slow queries check query_analysis file for:
|
|
* Section 2: TOP 20 SLOWEST QUERIES (by max execution time)
|
|
* Section 5: BASEITEMS TABLE QUERIES ANALYSIS
|
|
|
|
================================================================================
|
|
RECOMMENDED ACTIONS:
|
|
================================================================================
|
|
|
|
* Review slow queries taking longer than 10 seconds
|
|
* Check if new indexes are being utilized
|
|
* Look for tables with many sequential scans
|
|
* Monitor cache hit ratio (should be above 95 percent)
|
|
* Check for tables with high dead tuples percentage (need VACUUM)
|
|
|
|
================================================================================
|
|
AUTOMATION SETUP:
|
|
================================================================================
|
|
|
|
To schedule this script weekly, add the following to your crontab:
|
|
0 6 * * 1 /path/to/$(basename "$0")
|
|
|
|
================================================================================
|
|
NEXT REPORT: $(date -d "+7 days" +"%Y-%m-%d")
|
|
================================================================================
|
|
EOF
|
|
|
|
write_color_output "[OK] Summary generated: $summary_file" "$GREEN"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main Execution
|
|
# ============================================================================
|
|
|
|
write_color_output "\n${CYAN}========================================${NC}"
|
|
write_color_output "Jellyfin Database Weekly Monitor" "$CYAN"
|
|
write_color_output "========================================${NC}"
|
|
write_color_output "Database: $DB_NAME" "$YELLOW"
|
|
write_color_output "Host: $DB_HOST" "$YELLOW"
|
|
write_color_output "Week: $(get_week_number)" "$YELLOW"
|
|
write_color_output "Date: $(date +'%Y-%m-%d %H:%M:%S')" "$YELLOW"
|
|
write_color_output "========================================\n${NC}"
|
|
|
|
# Create output directory
|
|
report_dir="./$OUTPUT_DIRECTORY"
|
|
mkdir -p "$report_dir"
|
|
write_color_output "Created report directory: $report_dir" "$GREEN"
|
|
|
|
# Generate timestamp for this run
|
|
timestamp=$(get_report_timestamp)
|
|
|
|
# Test connection
|
|
if ! test_database_connection; then
|
|
write_color_output "\n[ERROR] Cannot connect to database. Exiting." "$RED"
|
|
exit 1
|
|
fi
|
|
|
|
# Run diagnostics
|
|
diagnostics_file="$report_dir/diagnostics_$timestamp.txt"
|
|
invoke_diagnostic_report "$diagnostics_file"
|
|
|
|
# Run query analysis
|
|
query_analysis_file="$report_dir/query_analysis_$timestamp.txt"
|
|
invoke_query_analysis "$query_analysis_file"
|
|
|
|
# Gather index usage stats
|
|
index_usage_file="$report_dir/index_usage_$timestamp.txt"
|
|
get_index_usage_stats "$index_usage_file"
|
|
|
|
# Gather table size stats
|
|
table_sizes_file="$report_dir/table_sizes_$timestamp.txt"
|
|
get_table_size_stats "$table_sizes_file"
|
|
|
|
# Optionally vacuum/analyze tables
|
|
if [ "$AUTO_VACUUM_ANALYZE" = true ]; then
|
|
invoke_conditional_vacuum_analyze "$DEAD_TUPLE_THRESHOLD" "$DEAD_TUPLE_PERCENT_THRESHOLD"
|
|
fi
|
|
|
|
# Compare with previous week
|
|
if [ "$COMPARE_WITH_PREVIOUS" = true ]; then
|
|
compare_with_previous "$diagnostics_file" "$report_dir"
|
|
fi
|
|
|
|
# Generate summary
|
|
new_summary_report "$report_dir" "$timestamp"
|
|
|
|
# Cleanup old reports (keep last 12 weeks)
|
|
write_color_output "\nCleaning up old reports (keeping last 12 weeks)..." "$CYAN"
|
|
find "$report_dir" -type f -mtime +84 -delete
|
|
write_color_output "[OK] Cleanup complete" "$GREEN"
|
|
|
|
write_color_output "\n${GREEN}========================================${NC}"
|
|
write_color_output "[OK] WEEKLY MONITORING COMPLETE" "$GREEN"
|
|
write_color_output "========================================${NC}"
|
|
write_color_output "Reports saved to: $report_dir" "$YELLOW"
|
|
write_color_output "Summary: summary_$timestamp.txt" "$YELLOW"
|
|
write_color_output "\nNext steps:" "$CYAN"
|
|
write_color_output "1. Review summary_$timestamp.txt" "$NC"
|
|
write_color_output "2. Check for critical issues in diagnostics report" "$NC"
|
|
write_color_output "3. Analyze slow queries in query_analysis report" "$NC"
|
|
write_color_output "========================================\n${NC}"
|
|
|
|
# Email report if requested
|
|
if [ "$EMAIL_REPORT" = true ]; then
|
|
write_color_output "[WARN] Email functionality not yet implemented" "$YELLOW"
|
|
write_color_output " Configure a mail client like 'mail' or 'sendmail'" "$GRAY"
|
|
fi
|
|
|
|
exit 0 |