318 lines
13 KiB
Bash
Executable File
318 lines
13 KiB
Bash
Executable File
#!/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.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# Two independent passes, each with its own threshold:
|
|
#
|
|
# System logs — every path in LOG_FILES
|
|
# size < LOG_MIN_SIZE_MB → skip, recent diagnostic history is worth keeping
|
|
# size >= LOG_MIN_SIZE_MB → truncate in place
|
|
#
|
|
# Docker logs — /var/lib/docker/containers/**/*-json.log
|
|
# container name resolved for reporting via docker inspect
|
|
# size < LOG_DOCKER_MAX_MB → skip
|
|
# size >= LOG_DOCKER_MAX_MB → truncate in place
|
|
# containers directory missing → whole pass skipped, not an error
|
|
#
|
|
# Truncation is always `: > file`, never rm — see Truncate, Never Delete below.
|
|
# Freed bytes are totalled per pass and reported in the summary.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Truncate, Never Delete
|
|
# Logs are emptied in place, never removed. The writing process keeps its open file
|
|
# handle and keeps logging; deleting the inode would leave a running daemon writing
|
|
# to a file nothing can read, and would consume more tmpfs, not less.
|
|
#
|
|
# 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
|
|
|
|
|
|
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
|
|
# One traversal, then formatted — this used to run du twice over the same path,
|
|
# once for the display string and once for the comparison.
|
|
size_mb=$(dir_size_mb "$f") || size_mb=0
|
|
size=$(format_mb "$size_mb")
|
|
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=$(dir_size_mb "$logfile") || size_mb=0
|
|
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 ━━━
|
|
# ==============================================================================================
|
|
log "$ICON_GEAR Config: system-threshold=${LOG_MIN_SIZE_MB:-10}MB docker-threshold=${LOG_DOCKER_MAX_MB:-100}MB"
|
|
log "$ICON_GEAR System logs: ${LOG_FILES[*]}"
|
|
|
|
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
|
|
|
|
# Same basis as the dry-run preview above. This measured stat -c%s (apparent size) while the
|
|
# preview measured du (allocated blocks), so a file sitting on LOG_MIN_SIZE_MB could be shown
|
|
# as under the threshold and then cleared, or the reverse. A dry run that disagrees with the
|
|
# real run about what it will touch is worse than no dry run.
|
|
size_mb=$(dir_size_mb "$logfile") || size_mb=0
|
|
size_h=$(format_mb "$size_mb")
|
|
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
|
|
|
|
# du basis, matching the "top 10 by size" listing above — that ranked and previewed on
|
|
# du while this cleared on stat, so the two could disagree about the same file.
|
|
size_mb=$(dir_size_mb "$logfile") || size_mb=0
|
|
size_h=$(format_mb "$size_mb")
|
|
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 |