#!/bin/bash # ============================================================================================== # ================================= Clear Logs ================================================= # ============================================================================================== # Clears system and Docker container logs to prevent rootfs fill over time. # Runs weekly via WEEKLY_MAINTENANCE_SCRIPTS — Sunday 2:30am. # Uses size thresholds — only clears logs that have grown large enough to matter. # # ── WHAT IT CLEARS ──────────────────────────────────────────────────────────────────────────── # System logs — LOG_FILES from master.conf (/var/log/syslog, messages, dmesg) # Cleared if size exceeds LOG_MIN_SIZE_MB # These grow continuously — weekly clearing keeps rootfs healthy # # Docker logs — /var/lib/docker/containers/**/*-json.log # Cleared only if individual container log exceeds LOG_DOCKER_MAX_MB # Active containers (Emby, SABnzbd) grow fastest — 100MB+ easily # Inactive containers not cleared — their logs are typically small # # ── SIZE THRESHOLD APPROACH ─────────────────────────────────────────────────────────────────── # Truncating everything blindly destroys useful diagnostic context. # A 2MB log is not worth clearing — it contains useful recent history. # A 500MB log is consuming rootfs and contains mostly noise — clear it. # # LOG_MIN_SIZE_MB — system logs under this size are left alone # LOG_DOCKER_MAX_MB — Docker logs under this size are left alone # # ── WHY NOT LOGROTATE ───────────────────────────────────────────────────────────────────────── # unRAID writes to tmpfs (/var/log) — logrotate's compress + archive approach # would consume even more tmpfs space. Truncation (: > file) keeps the file # descriptor open and valid while emptying content — safe for running services. # # ── SAFEGUARDS ──────────────────────────────────────────────────────────────────────────────── # acquire_lock — prevents concurrent runs corrupting logs # Root check — truncating system logs requires root # Size thresholds — only clears logs that have grown large enough # Byte tracking — reports MB freed for weekly digest # validate_unraid — notify validated before use # Silent on clean — small logs = nothing to clear = no output ✅ # # ── CONFIGURATION (master.conf) ─────────────────────────────────────────────────────────────── # LOG_FILES — system log paths to check and clear # LOG_MIN_SIZE_MB — minimum system log size before clearing (default 10MB) # LOG_DOCKER_MAX_MB — clear Docker log only if above this size (default 100MB) # # ── USAGE ───────────────────────────────────────────────────────────────────────────────────── # clear_logs.sh — normal run (threshold-based clearing) # clear_logs.sh --dry-run — show what would be cleared and sizes # clear_logs.sh --status — show current log sizes # clear_logs.sh --log — verbose output per file # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" parse_args "$@" # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root — truncating system logs requires root" exit 1 fi validate_unraid_cmd \ "/usr/local/emhttp/plugins/dynamix/scripts/notify" \ "" "" \ "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled" acquire_lock detect_hosts [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be cleared" # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY LOG STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_GEAR Min system log: ${LOG_MIN_SIZE_MB:-10}MB before clearing" echo "$ICON_GEAR Max Docker log: ${LOG_DOCKER_MAX_MB:-100}MB before clearing" echo "" echo "━━━ System Logs ━━━" for f in "${LOG_FILES[@]}"; do if [[ -f "$f" ]]; then size=$(du -sh "$f" 2>/dev/null | cut -f1) size_mb=$(du -sm "$f" 2>/dev/null | cut -f1) threshold="${LOG_MIN_SIZE_MB:-10}" if [[ "${size_mb:-0}" -ge "$threshold" ]]; then echo " $ICON_WARN $f — $size (above ${threshold}MB threshold — would clear)" else echo " $ICON_SUCCESS $f — $size (under threshold)" fi else echo " $ICON_SKIP $f — not found" fi done echo "" echo "━━━ Docker Logs (top 10 by size) ━━━" if [[ -d /var/lib/docker/containers ]]; then find /var/lib/docker/containers/ -name "*-json.log" 2>/dev/null | \ while IFS= read -r logfile; do size_mb=$(du -sm "$logfile" 2>/dev/null | cut -f1) container_id=$(basename "$(dirname "$logfile")" | cut -c1-12) container_name=$(docker inspect --format '{{.Name}}' "$container_id" \ 2>/dev/null | tr -d '/' || echo "$container_id") echo "${size_mb:-0} $container_name $logfile" done | sort -rn | head -10 | \ while read -r size_mb name logfile; do threshold="${LOG_DOCKER_MAX_MB:-100}" if [[ "$size_mb" -ge "$threshold" ]]; then echo " $ICON_WARN ${size_mb}MB — $name (above ${threshold}MB — would clear)" else echo " $ICON_SUCCESS ${size_mb}MB — $name" fi done else echo " Docker directory not found" fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi # ============================================================================================== # ━━━ Clear Logs ━━━ # ============================================================================================== START=$(date +%s) SYS_CLEARED=0 SYS_SKIPPED=0 SYS_BYTES=0 DOCKER_CLEARED=0 DOCKER_SKIPPED=0 DOCKER_BYTES=0 FAILED=() # ── System Logs ─────────────────────────────────────────────────────────────────────────────── for logfile in "${LOG_FILES[@]}"; do if [[ ! -f "$logfile" ]]; then log "$logfile — not found, skipping" continue fi size_bytes=$(stat -c%s "$logfile" 2>/dev/null || echo 0) size_mb=$(( size_bytes / 1048576 )) size_h=$(du -sh "$logfile" 2>/dev/null | cut -f1) threshold="${LOG_MIN_SIZE_MB:-10}" if [[ "$size_mb" -lt "$threshold" ]]; then log "$logfile — ${size_h} (under ${threshold}MB — skipping)" (( SYS_SKIPPED++ )) continue fi if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would clear: $logfile (${size_h})" (( SYS_CLEARED++ )) SYS_BYTES=$(( SYS_BYTES + size_bytes )) continue fi if : > "$logfile" 2>/dev/null; then log "Cleared: $logfile (freed ${size_h})" (( SYS_CLEARED++ )) SYS_BYTES=$(( SYS_BYTES + size_bytes )) else error "Failed to clear: $logfile" FAILED+=("$logfile") fi done # ── Docker Logs ─────────────────────────────────────────────────────────────────────────────── if [[ ! -d /var/lib/docker/containers ]]; then log "Docker containers directory not found — skipping Docker log clear" else while IFS= read -r logfile; do [[ -z "$logfile" ]] && continue size_bytes=$(stat -c%s "$logfile" 2>/dev/null || echo 0) size_mb=$(( size_bytes / 1048576 )) size_h=$(du -sh "$logfile" 2>/dev/null | cut -f1) threshold="${LOG_DOCKER_MAX_MB:-100}" # Get container name for display container_id=$(basename "$(dirname "$logfile")" | cut -c1-12) container_name=$(docker inspect --format '{{.Name}}' "$container_id" \ 2>/dev/null | tr -d '/' || echo "$container_id") if [[ "$size_mb" -lt "$threshold" ]]; then log "Docker $container_name — ${size_h} (under ${threshold}MB — skipping)" (( DOCKER_SKIPPED++ )) continue fi if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would clear Docker log: $container_name (${size_h})" (( DOCKER_CLEARED++ )) DOCKER_BYTES=$(( DOCKER_BYTES + size_bytes )) continue fi if : > "$logfile" 2>/dev/null; then log "Cleared Docker log: $container_name (freed ${size_h})" (( DOCKER_CLEARED++ )) DOCKER_BYTES=$(( DOCKER_BYTES + size_bytes )) else error "Failed to clear Docker log: $container_name" FAILED+=("docker:$container_name") fi done < <(find /var/lib/docker/containers/ -name "*-json.log" 2>/dev/null) fi END=$(date +%s) TOTAL_BYTES=$(( SYS_BYTES + DOCKER_BYTES )) TOTAL_FREED_H=$(awk "BEGIN {printf \"%.1fMB\", $TOTAL_BYTES / 1048576}") TOTAL_CLEARED=$(( SYS_CLEARED + DOCKER_CLEARED )) # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== if [[ "$TOTAL_CLEARED" -gt 0 || ${#FAILED[@]} -gt 0 ]]; then echo "" echo "━━━━━ $ICON_SUMMARY LOG CLEANER SUMMARY ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_HEALTH System cleared: $SYS_CLEARED file(s)" echo "$ICON_CONTAINERS Docker cleared: $DOCKER_CLEARED file(s)" echo "$ICON_HEALTH Total freed: $TOTAL_FREED_H" echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" [[ "$SYS_SKIPPED" -gt 0 || "$DOCKER_SKIPPED" -gt 0 ]] && \ log "Skipped: ${SYS_SKIPPED} system + ${DOCKER_SKIPPED} Docker (under threshold)" echo "" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — no files cleared" elif [[ ${#FAILED[@]} -gt 0 ]]; then echo "$ICON_ERROR Status: SOME FILES FAILED — ${FAILED[*]}" notify "Log clear failed on $(hostname) ($MY_ID) — ${FAILED[*]}" \ "Clear Logs" "warning" else log "$ICON_DONE Status: done — ${TOTAL_FREED_H} freed" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" else # All logs under threshold — completely silent log "All logs under threshold — nothing to clear" fi [[ ${#FAILED[@]} -gt 0 ]] && exit 1 exit 0