#!/bin/bash # ============================================================================================== # ================================= Clear Logs ================================================= # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Clears system and Docker container logs to prevent rootfs fill over time. # Called weekly via WEEKLY_MAINTENANCE_SCRIPTS. Uses size thresholds — only # clears logs large enough to be worth clearing. Small logs are left intact, # preserving recent diagnostic context. # # System logs (LOG_FILES): cleared if size exceeds LOG_MIN_SIZE_MB. # Docker logs (/var/lib/docker/containers/**/*-json.log): cleared only if the # individual container log exceeds LOG_DOCKER_MAX_MB. Active containers (Emby, # SABnzbd) grow fastest — inactive containers typically remain small. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Size Thresholds, Not Blind Truncation # A 2MB syslog contains useful recent diagnostic history — not worth clearing. # A 500MB Docker log is consuming rootfs and contains mostly noise — clear it. # Blind truncation destroys diagnostic context for no benefit. # # Truncation, Not Logrotate # unRAID writes logs to tmpfs (/var/log). Logrotate's compress + archive approach # would consume more tmpfs space, not less. Truncation (`: > file`) keeps the # file descriptor open and valid while emptying content — syslogd continues # writing to the same fd without interruption. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Single Instance Lock # acquire_lock prevents concurrent runs from corrupting logs. # # Root Required # Truncating system logs requires root. # # Size Thresholds # Each file checked against its threshold before clearing. # # Silent When Clean # All logs below threshold = no visible output. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # master.conf # # LOG_FILES # System log paths to check and clear. (default: /var/log/syslog /var/log/messages /var/log/dmesg) # # LOG_MIN_SIZE_MB # Skip system log if under this size — keep recent history. (default: 10) # # LOG_DOCKER_MAX_MB # Clear Docker container log only if over this size. (default: 100) # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # clear_logs.sh # Check all configured logs. Clear those above threshold. Silent when all are small. # # clear_logs.sh --dry-run # Show which logs would be cleared and their current sizes. No clearing. # # clear_logs.sh --status # Show current log sizes vs thresholds. # # clear_logs.sh --log # Verbose output — show each file evaluated, its size, and action taken. # # ============================================================================================== 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 if ! command -v docker &>/dev/null; then error "Docker command not found" exit 1 fi 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 echo "$ICON_DONE Status: done — ${TOTAL_FREED_H} freed" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" else # All logs under threshold — completely silent echo "All logs under threshold — nothing to clear" fi [[ ${#FAILED[@]} -gt 0 ]] && exit 1 exit 0