#!/bin/bash ################################################################################ # Script Name: linux-log-analyzer.sh # Version: 1.00 # Description: Analyze Linux system logs across five log types — system, auth, # Docker, Java, GitLab, and PostgreSQL. Parses syslog, auth.log, # journalctl, Java/log4j logs, GitLab JSON logs, and PostgreSQL # logs to surface errors, failed logins, service failures, stack # traces, slow queries, and crash events. # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - bash, awk, grep, sort # - journalctl (optional, for systemd-based systems) # - Root/sudo for auth and system log types # # Usage: # sudo ./linux-log-analyzer.sh --type system # sudo ./linux-log-analyzer.sh --type auth --since 24h # sudo ./linux-log-analyzer.sh --type docker # ./linux-log-analyzer.sh --type java --log /opt/app/logs/application.log # sudo ./linux-log-analyzer.sh --type gitlab # ./linux-log-analyzer.sh --type postgresql # sudo ./linux-log-analyzer.sh --all-types # sudo ./linux-log-analyzer.sh --type system --json # sudo ./linux-log-analyzer.sh --all-types --no-color > report.txt # # Log Types: # system - syslog/messages: service failures, OOM, disk errors, panics # auth - auth.log/secure: SSH brute force, sudo, account changes # docker - Docker daemon: container crashes, restart loops, OOM # java - log4j/logback/catalina: stack traces, OOM, GC pauses, errors # gitlab - GitLab Omnibus: 5xx errors, slow requests, Sidekiq failures # postgresql - PostgreSQL: deadlocks, slow queries, connection errors ################################################################################ set -uo pipefail # ============================================================================ # DEFAULTS # ============================================================================ VERSION="1.00" LOG_TYPE="" ALL_TYPES=false SINCE="" SINCE_EPOCH="" TOP_N=20 JSON_MODE=false NO_COLOR=false OUTPUT_FILE="" CUSTOM_LOG="" # Colors RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # JSON accumulator JSON_OUTPUT="" # ============================================================================ # USAGE # ============================================================================ show_usage() { cat < report.txt sudo $0 --type system --json | jq . EOF exit 0 } # ============================================================================ # HELPERS # ============================================================================ disable_colors() { RED="" YELLOW="" GREEN="" CYAN="" BOLD="" DIM="" NC="" } log_info() { echo -e "${CYAN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } check_root() { if [[ $EUID -ne 0 ]]; then log_error "This log type requires root. Run with: sudo $0 $*" return 1 fi return 0 } section_header() { local title="$1" echo "" echo -e "${BOLD}====================================================" echo -e " ${title}" echo -e "====================================================${NC}" } subsection_header() { local title="$1" echo "" echo -e "${BOLD}── ${title} ─────────────────────────────────────────${NC}" } # Convert relative time strings to epoch parse_since() { local since="$1" local now now=$(date +%s) case "$since" in *h) local hours="${since%h}" SINCE_EPOCH=$((now - hours * 3600)) ;; *d) local days="${since%d}" SINCE_EPOCH=$((now - days * 86400)) ;; *) # Try ISO date SINCE_EPOCH=$(date -d "$since" +%s 2>/dev/null || echo "") if [[ -z "$SINCE_EPOCH" ]]; then log_error "Invalid --since value: $since" exit 1 fi ;; esac } # Get journalctl --since string from SINCE journalctl_since_arg() { if [[ -n "$SINCE" ]]; then case "$SINCE" in *h) echo "--since=${SINCE%h} hours ago" ;; *d) echo "--since=${SINCE%d} days ago" ;; *) echo "--since=$SINCE" ;; esac fi } # Filter lines by timestamp — uses a reference date to avoid per-line date calls filter_by_time() { if [[ -z "$SINCE_EPOCH" ]]; then cat return fi local cutoff_date cutoff_date=$(date -d "@${SINCE_EPOCH}" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) local cutoff_syslog cutoff_syslog=$(date -d "@${SINCE_EPOCH}" "+%b %d %H:%M:%S" 2>/dev/null) local current_year current_year=$(date +%Y) awk -v cutoff_iso="$cutoff_date" -v cutoff_syslog="$cutoff_syslog" -v yr="$current_year" ' BEGIN { # Month name to number for syslog comparison split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", m) for (i = 1; i <= 12; i++) mon_num[m[i]] = sprintf("%02d", i) } { # ISO: 2026-04-13 14:22:03 or 2026-04-13T14:22:03 if (match($0, /^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}/)) { ts = substr($0, RSTART, RLENGTH) gsub(/T/, " ", ts) if (ts >= cutoff_iso) print next } # Syslog: Apr 13 14:22:03 if (match($0, /^[A-Z][a-z]{2} [ 0-9][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}/)) { ts = substr($0, RSTART, RLENGTH) # Convert to sortable: YYYY-MM-DD HH:MM:SS mon_name = substr(ts, 1, 3) day = substr(ts, 5, 2) gsub(/ /, "0", day) time_part = substr(ts, 8) sortable = yr "-" mon_num[mon_name] "-" day " " time_part if (sortable >= cutoff_iso) print next } # No timestamp — include line (continuation / multi-line) print }' } # Find first available log file from a list find_log() { for f in "$@"; do if [[ -r "$f" ]]; then echo "$f" return 0 fi done return 1 } # Read log source — file or journalctl read_log_source() { local log_file="$1" local journal_unit="${2:-}" if [[ -n "$CUSTOM_LOG" && -r "$CUSTOM_LOG" ]]; then cat "$CUSTOM_LOG" | filter_by_time return fi if [[ -n "$log_file" && -r "$log_file" ]]; then cat "$log_file" | filter_by_time return fi if [[ -n "$journal_unit" ]] && command -v journalctl &>/dev/null; then local since_arg since_arg=$(journalctl_since_arg) if [[ -n "$since_arg" ]]; then journalctl -u "$journal_unit" --no-pager -q $since_arg 2>/dev/null else journalctl -u "$journal_unit" --no-pager -q 2>/dev/null fi return fi return 1 } # Emit a JSON key-value pair (accumulates into JSON_OUTPUT) json_add() { local key="$1" local value="$2" if [[ -n "$JSON_OUTPUT" ]]; then JSON_OUTPUT="${JSON_OUTPUT}," fi JSON_OUTPUT="${JSON_OUTPUT}\"${key}\":${value}" } json_string() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" printf '"%s"' "$s" } # ============================================================================ # SYSTEM LOG ANALYZER # ============================================================================ analyze_system() { if ! check_root; then return 1; fi local log_file log_file=$(find_log /var/log/syslog /var/log/messages) local log_data log_data=$(read_log_source "${log_file:-}" "") # Also pull journalctl if available and no file source if [[ -z "$log_data" ]] && command -v journalctl &>/dev/null; then local since_arg since_arg=$(journalctl_since_arg) if [[ -n "$since_arg" ]]; then log_data=$(journalctl --no-pager -q $since_arg 2>/dev/null) else log_data=$(journalctl --no-pager -q --since "24 hours ago" 2>/dev/null) fi fi if [[ -z "$log_data" ]]; then log_warn "No system log sources found (syslog, messages, or journalctl)" return 1 fi local total_lines total_lines=$(echo "$log_data" | wc -l) if ! $JSON_MODE; then section_header "System Log Analysis" echo "" echo -e " Source: ${DIM}${log_file:-journalctl}${NC}" echo -e " Lines: ${total_lines}" fi # Service failures local service_failures service_failures=$(echo "$log_data" | grep -iE "(failed|error|fatal)" | \ grep -oP '(\S+\.service)' | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Service Failures" if [[ -n "$service_failures" ]]; then printf " ${BOLD}%-4s %-35s %s${NC}\n" "#" "Service" "Failures" echo "$service_failures" | awk '{printf " %-4d %-35s %d\n", NR, $2, $1}' | head -"$TOP_N" else echo " None found." fi fi # OOM kills local oom_kills oom_kills=$(echo "$log_data" | grep -i "oom-kill\|oom_kill\|out of memory\|killed process" | head -"$TOP_N") local oom_count=0 if [[ -n "$oom_kills" ]]; then oom_count=$(echo "$oom_kills" | wc -l) fi if ! $JSON_MODE; then subsection_header "OOM Kills" if [[ -n "$oom_kills" ]]; then echo "$oom_kills" | while IFS= read -r line; do local proc proc=$(echo "$line" | grep -oP "process \d+ \(\K[^)]+|Killed process \d+ \(\K[^)]+|oom_kill.*task=\K\S+" | head -1) local pid pid=$(echo "$line" | grep -oP "process \K\d+|Killed process \K\d+" | head -1) local ts ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") printf " %-22s PID %-7s %s\n" "${ts:-unknown}" "${pid:-?}" "${proc:-unknown}" done else echo " None found." fi fi # Disk errors local disk_errors disk_errors=$(echo "$log_data" | grep -iE "(I/O error|EXT[234]-fs.*(warning|error)|XFS.*(error|corruption)|BTRFS.*error|SMART.*error|end_request.*I/O|medium error|blk_update_request.*error|Buffer I/O error|remount.*read-only)" | head -"$TOP_N") local disk_error_count=0 if [[ -n "$disk_errors" ]]; then disk_error_count=$(echo "$disk_errors" | wc -l) fi if ! $JSON_MODE; then subsection_header "Disk Errors" if [[ -n "$disk_errors" ]]; then echo "$disk_errors" | while IFS= read -r line; do local ts ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}") local msg msg=$(echo "$line" | sed -E 's/^.*kernel(\[[0-9.]+\])?: //' | head -c 80) printf " %-22s %s\n" "${ts:-unknown}" "$msg" done else echo " None found." fi fi # Failed logins (from syslog) local failed_logins failed_logins=$(echo "$log_data" | grep -iE "(failed password|authentication failure|failed login)" | \ grep -oP "user[ =]\K\S+|for \K\S+(?= from)" | sort | uniq -c | sort -rn | head -"$TOP_N") local failed_login_total=0 if [[ -n "$failed_logins" ]]; then failed_login_total=$(echo "$failed_logins" | awk '{s+=$1} END {print s+0}') fi if ! $JSON_MODE; then subsection_header "Failed Logins" if [[ -n "$failed_logins" ]]; then printf " ${BOLD}%-4s %-25s %s${NC}\n" "#" "User" "Failures" echo "$failed_logins" | awk '{printf " %-4d %-25s %d\n", NR, $2, $1}' | head -"$TOP_N" else echo " None found." fi fi # Sudo usage local sudo_usage sudo_usage=$(echo "$log_data" | grep -i "sudo:" | grep "COMMAND=" | \ grep -oP "^\S+\s+\S+\s+\S+\s+\S+\s+\K\S+(?=\s)" | sort | uniq -c | sort -rn | head -"$TOP_N") local sudo_total=0 if [[ -n "$sudo_usage" ]]; then sudo_total=$(echo "$sudo_usage" | awk '{s+=$1} END {print s+0}') fi if ! $JSON_MODE; then subsection_header "Sudo Usage" if [[ -n "$sudo_usage" ]]; then printf " ${BOLD}%-4s %-25s %s${NC}\n" "#" "User" "Commands" echo "$sudo_usage" | awk '{printf " %-4d %-25s %d\n", NR, $2, $1}' | head -"$TOP_N" else echo " None found." fi fi # Kernel panics / segfaults local kernel_panics kernel_panics=$(echo "$log_data" | grep -iE "(kernel panic|segfault|general protection fault|BUG:|Oops:)" | head -"$TOP_N") local panic_count=0 if [[ -n "$kernel_panics" ]]; then panic_count=$(echo "$kernel_panics" | wc -l) fi if ! $JSON_MODE; then subsection_header "Kernel Panics / Segfaults" if [[ -n "$kernel_panics" ]]; then echo "$kernel_panics" | while IFS= read -r line; do local ts ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") local msg msg=$(echo "$line" | sed 's/^.*\(kernel\)[^:]*: //') printf " %-22s %s\n" "${ts:-unknown}" "${msg:0:80}" done else echo " None found." fi fi # Cron failures local cron_failures cron_failures=$(echo "$log_data" | grep -iE "(cron.*error|CRON.*FAILED|cron.*exit status [^0])" | head -"$TOP_N") local cron_fail_count=0 if [[ -n "$cron_failures" ]]; then cron_fail_count=$(echo "$cron_failures" | wc -l) fi if ! $JSON_MODE; then subsection_header "Cron Failures" if [[ -n "$cron_failures" ]]; then echo "$cron_failures" | while IFS= read -r line; do local ts ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") local msg msg=$(echo "$line" | sed 's/^.*CRON[^:]*: //' | head -c 80) printf " %-22s %s\n" "${ts:-unknown}" "$msg" done else echo " None found." fi fi # Top error patterns local error_patterns error_patterns=$(echo "$log_data" | grep -iE "(error|fail|fatal|critical)" | \ sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[^ ]+ [^ ]+ [^:]+: //' | \ sed -E 's/^[A-Z][a-z]{2} +[0-9]{1,2} [0-9:]{8} [^ ]+ [^:]+: //' | \ sed -E ':a; s/^(time|ts|level|caller|source|component|host)=[^ ]+ //; ta' | \ sed -E 's/^\[[0-9T:.Z+ -]+ [A-Z]+ [^]]+\] //' | \ sed 's/[0-9]\{2,\}/#/g' | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Top Error Patterns" if [[ -n "$error_patterns" ]]; then printf " ${BOLD}%-4s %-6s %s${NC}\n" "#" "Count" "Pattern" echo "$error_patterns" | awk '{ count = $1 $1 = "" sub(/^ +/, "") msg = substr($0, 1, 70) printf " %-4d %-6d %s\n", NR, count, msg }' | head -"$TOP_N" else echo " None found." fi fi # Service failure count local svc_fail_count=0 if [[ -n "$service_failures" ]]; then svc_fail_count=$(echo "$service_failures" | awk '{s+=$1} END {print s+0}') fi # Summary if ! $JSON_MODE; then section_header "Summary" echo "" printf " %-25s %d\n" "Service failures:" "$svc_fail_count" printf " %-25s %d\n" "OOM kills:" "$oom_count" printf " %-25s %d\n" "Disk errors:" "$disk_error_count" printf " %-25s %d\n" "Failed logins:" "$failed_login_total" printf " %-25s %d\n" "Sudo commands:" "$sudo_total" printf " %-25s %d\n" "Kernel panics:" "$panic_count" printf " %-25s %d\n" "Cron failures:" "$cron_fail_count" printf " %-25s %d\n" "Total lines parsed:" "$total_lines" else json_add "system" "{\"service_failures\":${svc_fail_count},\"oom_kills\":${oom_count},\"disk_errors\":${disk_error_count},\"failed_logins\":${failed_login_total},\"sudo_commands\":${sudo_total},\"kernel_panics\":${panic_count},\"cron_failures\":${cron_fail_count},\"total_lines\":${total_lines}}" fi } # ============================================================================ # AUTH LOG ANALYZER # ============================================================================ analyze_auth() { if ! check_root; then return 1; fi local log_file log_file=$(find_log /var/log/auth.log /var/log/secure) local log_data log_data=$(read_log_source "${log_file:-}" "ssh") if [[ -z "$log_data" ]]; then log_warn "No auth log sources found (auth.log, secure, or journalctl)" return 1 fi local total_lines total_lines=$(echo "$log_data" | wc -l) if ! $JSON_MODE; then section_header "Auth Log Analysis" echo "" echo -e " Source: ${DIM}${log_file:-journalctl}${NC}" echo -e " Lines: ${total_lines}" fi # SSH login failures by IP local ssh_fail_ips ssh_fail_ips=$(echo "$log_data" | grep -i "failed password" | \ grep -oP "from \K[0-9a-f.:]+(?= port)" | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "SSH Failed Logins by IP" if [[ -n "$ssh_fail_ips" ]]; then printf " ${BOLD}%-4s %-40s %s${NC}\n" "#" "IP Address" "Failures" echo "$ssh_fail_ips" | awk '{printf " %-4d %-40s %d\n", NR, $2, $1}' | head -"$TOP_N" else echo " None found." fi fi # Brute force detection (>10 failures) local brute_force brute_force=$(echo "$log_data" | grep -i "failed password" | \ grep -oP "from \K[0-9a-f.:]+(?= port)" | sort | uniq -c | sort -rn | awk '$1 > 10') local brute_force_count=0 if [[ -n "$brute_force" ]]; then brute_force_count=$(echo "$brute_force" | wc -l) fi if ! $JSON_MODE; then subsection_header "Brute Force Suspects (>10 failures)" if [[ -n "$brute_force" ]]; then printf " ${BOLD}%-40s %s${NC}\n" "IP Address" "Failures" echo "$brute_force" | awk '{printf " %-40s %s\n", $2, $1}' echo "" echo -e " ${RED}${brute_force_count} IP(s) with >10 failed attempts${NC}" else echo -e " ${GREEN}None found.${NC}" fi fi # SSH login successes by user local ssh_success ssh_success=$(echo "$log_data" | grep -i "accepted" | \ grep -oP "for \K\S+(?= from)" | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "SSH Successful Logins by User" if [[ -n "$ssh_success" ]]; then printf " ${BOLD}%-4s %-25s %s${NC}\n" "#" "User" "Logins" echo "$ssh_success" | awk '{printf " %-4d %-25s %d\n", NR, $2, $1}' | head -"$TOP_N" else echo " None found." fi fi # SSH key vs password local key_logins=0 pass_logins=0 key_logins=$(echo "$log_data" | grep -ic "accepted publickey" || true) pass_logins=$(echo "$log_data" | grep -ic "accepted password" || true) if ! $JSON_MODE; then subsection_header "Auth Method Breakdown" printf " %-25s %d\n" "SSH key:" "$key_logins" printf " %-25s %d\n" "Password:" "$pass_logins" fi # Root login attempts local root_attempts root_attempts=$(echo "$log_data" | grep -iE "(failed password|accepted).* for root " | wc -l || true) local root_success root_success=$(echo "$log_data" | grep -i "accepted.* for root " | wc -l || true) if ! $JSON_MODE; then subsection_header "Root Login Attempts" printf " %-25s %d\n" "Total attempts:" "$root_attempts" printf " %-25s %d\n" "Successful:" "$root_success" if [[ "$root_success" -gt 0 ]]; then echo "" echo "$log_data" | grep -i "accepted.* for root " | tail -5 | while IFS= read -r line; do local ts ip ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") ip=$(echo "$line" | grep -oP "from \K[0-9a-f.:]+") printf " %-22s from %s\n" "${ts:-unknown}" "${ip:-unknown}" done fi fi # Sudo failures local sudo_failures sudo_failures=$(echo "$log_data" | grep -iE "sudo.*authentication failure|sudo.*incorrect password|sudo.*3 incorrect" | head -"$TOP_N") local sudo_fail_count=0 if [[ -n "$sudo_failures" ]]; then sudo_fail_count=$(echo "$sudo_failures" | wc -l) fi if ! $JSON_MODE; then subsection_header "Sudo Failures" if [[ -n "$sudo_failures" ]]; then echo "$sudo_failures" | while IFS= read -r line; do local ts user ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") user=$(echo "$line" | grep -oP "user[ =]\K\S+|;\s+\K\S+(?=\s+:)" | head -1) printf " %-22s user: %s\n" "${ts:-unknown}" "${user:-unknown}" done else echo " None found." fi fi # Account changes (useradd, usermod, groupadd, userdel, passwd) local account_changes account_changes=$(echo "$log_data" | grep -iE "(useradd|usermod|userdel|groupadd|groupdel|passwd|new user|new group|delete user)" | head -"$TOP_N") local account_change_count=0 if [[ -n "$account_changes" ]]; then account_change_count=$(echo "$account_changes" | wc -l) fi if ! $JSON_MODE; then subsection_header "Account Changes" if [[ -n "$account_changes" ]]; then echo "$account_changes" | while IFS= read -r line; do local ts msg ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") msg=$(echo "$line" | sed 's/^.*\(useradd\|usermod\|userdel\|groupadd\|passwd\)/\1/' | head -c 70) printf " %-22s %s\n" "${ts:-unknown}" "$msg" done else echo " None found." fi fi # Totals local total_failures total_failures=$(echo "$log_data" | grep -ic "failed password" || true) local total_success total_success=$(echo "$log_data" | grep -ic "accepted" || true) local total_auth=$((total_failures + total_success)) local success_rate=0 if [[ "$total_auth" -gt 0 ]]; then success_rate=$(awk "BEGIN {printf \"%.1f\", ($total_success / $total_auth) * 100}") fi if ! $JSON_MODE; then section_header "Summary" echo "" printf " %-25s %d\n" "Total auth events:" "$total_auth" printf " %-25s %d\n" "Successful logins:" "$total_success" printf " %-25s %d\n" "Failed logins:" "$total_failures" printf " %-25s %s%%\n" "Success rate:" "$success_rate" printf " %-25s %d\n" "Brute force IPs:" "$brute_force_count" printf " %-25s %d\n" "Sudo failures:" "$sudo_fail_count" printf " %-25s %d\n" "Account changes:" "$account_change_count" else json_add "auth" "{\"total_auth_events\":${total_auth},\"successful_logins\":${total_success},\"failed_logins\":${total_failures},\"success_rate\":${success_rate},\"brute_force_ips\":${brute_force_count},\"sudo_failures\":${sudo_fail_count},\"account_changes\":${account_change_count}}" fi } # ============================================================================ # DOCKER LOG ANALYZER # ============================================================================ analyze_docker() { if ! command -v docker &>/dev/null && ! [[ -r /var/log/docker.log ]]; then log_warn "Docker not found and no /var/log/docker.log — skipping" return 1 fi local log_data="" # Try journalctl first (most common on systemd systems) if command -v journalctl &>/dev/null; then local since_arg since_arg=$(journalctl_since_arg) if [[ -n "$since_arg" ]]; then log_data=$(journalctl -u docker.service --no-pager -q $since_arg 2>/dev/null) else log_data=$(journalctl -u docker.service --no-pager -q 2>/dev/null) fi fi # Fallback to docker.log if [[ -z "$log_data" && -r /var/log/docker.log ]]; then log_data=$(cat /var/log/docker.log | filter_by_time) fi # Also pull docker events if docker is available local docker_ps_data="" if command -v docker &>/dev/null; then docker_ps_data=$(docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.CreatedAt}}' 2>/dev/null || true) fi if [[ -z "$log_data" && -z "$docker_ps_data" ]]; then log_warn "No Docker log data available" return 1 fi local total_lines=0 if [[ -n "$log_data" ]]; then total_lines=$(echo "$log_data" | wc -l) fi if ! $JSON_MODE; then section_header "Docker Log Analysis" echo "" echo -e " Source: ${DIM}journalctl/docker.log${NC}" echo -e " Lines: ${total_lines}" fi # Container start/stop/restart events local container_events container_events=$(echo "$log_data" | grep -oP "container \K(start|stop|restart|kill|die|create|destroy)\S*" 2>/dev/null | \ sort | uniq -c | sort -rn) if ! $JSON_MODE; then subsection_header "Container Events" if [[ -n "$container_events" ]]; then printf " ${BOLD}%-20s %s${NC}\n" "Event" "Count" echo "$container_events" | awk '{printf " %-20s %d\n", $2, $1}' else echo " None found in daemon logs." fi fi # Container restart loops (from docker ps) local restart_loops="" if [[ -n "$docker_ps_data" ]]; then restart_loops=$(echo "$docker_ps_data" | grep -i "restarting" || true) fi if ! $JSON_MODE; then subsection_header "Containers in Restart Loop" if [[ -n "$restart_loops" ]]; then echo "$restart_loops" | while IFS=$'\t' read -r name status created; do printf " %-30s %s\n" "$name" "$status" done else echo -e " ${GREEN}None found.${NC}" fi fi # OOM kills in containers local docker_oom docker_oom=$(echo "$log_data" | grep -iE "(oom|out of memory)" | head -"$TOP_N") local docker_oom_count=0 if [[ -n "$docker_oom" ]]; then docker_oom_count=$(echo "$docker_oom" | wc -l) fi if ! $JSON_MODE; then subsection_header "Container OOM Kills" if [[ -n "$docker_oom" ]]; then echo "$docker_oom" | while IFS= read -r line; do local ts ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") local msg msg=$(echo "$line" | sed 's/^.*dockerd[^:]*: //' | head -c 80) printf " %-22s %s\n" "${ts:-unknown}" "$msg" done else echo " None found." fi fi # Docker daemon errors local daemon_errors daemon_errors=$(echo "$log_data" | grep -iE "(error|fatal|panic)" | \ grep -v "level=info" | grep -v "level=warning" | head -"$TOP_N") local daemon_error_count=0 if [[ -n "$daemon_errors" ]]; then daemon_error_count=$(echo "$daemon_errors" | wc -l) fi if ! $JSON_MODE; then subsection_header "Daemon Errors" if [[ -n "$daemon_errors" ]]; then echo "$daemon_errors" | while IFS= read -r line; do local ts msg ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") msg=$(echo "$line" | grep -oP 'msg="\K[^"]+|msg=\K\S+' | head -1) printf " %-22s %s\n" "${ts:-unknown}" "${msg:-$(echo "$line" | tail -c 80)}" done else echo " None found." fi fi # Health check failures local health_failures health_failures=$(echo "$log_data" | grep -iE "(health.*unhealthy|health check|health_status)" | head -"$TOP_N") local health_fail_count=0 if [[ -n "$health_failures" ]]; then health_fail_count=$(echo "$health_failures" | wc -l) fi if ! $JSON_MODE; then subsection_header "Health Check Failures" if [[ -n "$health_failures" ]]; then echo "$health_failures" | while IFS= read -r line; do local ts msg ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+") msg=$(echo "$line" | sed 's/^.*dockerd[^:]*: //' | head -c 80) printf " %-22s %s\n" "${ts:-unknown}" "$msg" done else echo " None found." fi fi # Docker warnings local daemon_warnings daemon_warnings=$(echo "$log_data" | grep -i "level=warning\|WARN" | \ grep -oP 'msg="\K[^"]+|msg=\K\S+' | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Top Warning Patterns" if [[ -n "$daemon_warnings" ]]; then printf " ${BOLD}%-6s %s${NC}\n" "Count" "Warning" echo "$daemon_warnings" | awk '{ count = $1; $1 = "" sub(/^ +/, "") printf " %-6d %s\n", count, substr($0, 1, 70) }' | head -"$TOP_N" else echo " None found." fi fi # Exited containers (from docker ps -a) local exited_containers="" if [[ -n "$docker_ps_data" ]]; then exited_containers=$(echo "$docker_ps_data" | grep -i "exited" | head -"$TOP_N" || true) fi if ! $JSON_MODE; then subsection_header "Exited Containers" if [[ -n "$exited_containers" ]]; then printf " ${BOLD}%-30s %s${NC}\n" "Container" "Status" echo "$exited_containers" | while IFS=$'\t' read -r name status created; do printf " %-30s %s\n" "$name" "$status" done else echo " None found." fi fi local restart_count=0 if [[ -n "$restart_loops" ]]; then restart_count=$(echo "$restart_loops" | wc -l) fi if ! $JSON_MODE; then section_header "Summary" echo "" printf " %-25s %d\n" "Daemon errors:" "$daemon_error_count" printf " %-25s %d\n" "OOM kills:" "$docker_oom_count" printf " %-25s %d\n" "Health check failures:" "$health_fail_count" printf " %-25s %d\n" "Restart loops:" "$restart_count" printf " %-25s %d\n" "Log lines parsed:" "$total_lines" else json_add "docker" "{\"daemon_errors\":${daemon_error_count},\"oom_kills\":${docker_oom_count},\"health_check_failures\":${health_fail_count},\"restart_loops\":${restart_count},\"total_lines\":${total_lines}}" fi } # ============================================================================ # JAVA LOG ANALYZER # ============================================================================ analyze_java() { local log_file="" if [[ -n "$CUSTOM_LOG" ]]; then log_file="$CUSTOM_LOG" else # Search common Java/Tomcat log locations log_file=$(find_log \ /opt/tomcat/logs/catalina.out \ /var/log/tomcat*/catalina.out \ /opt/tomcat*/logs/catalina.out \ /var/log/wildfly/server.log \ /opt/wildfly/standalone/log/server.log \ /var/log/jetty/jetty.log \ /opt/sonarqube/logs/sonar.log \ /opt/nexus/sonatype-work/nexus3/log/nexus.log \ /opt/jenkins/log/jenkins.log \ /var/log/jenkins/jenkins.log) fi if [[ -z "$log_file" || ! -r "$log_file" ]]; then log_warn "No Java log sources found — use --log to specify the path" return 1 fi local log_data log_data=$(cat "$log_file" | filter_by_time) if [[ -z "$log_data" ]]; then log_warn "No log data in ${log_file} for the specified time range" return 1 fi local total_lines total_lines=$(echo "$log_data" | wc -l) if ! $JSON_MODE; then section_header "Java Log Analysis" echo "" echo -e " Source: ${DIM}${log_file}${NC}" echo -e " Lines: ${total_lines}" fi # ERROR / WARN / FATAL counts local error_count warn_count fatal_count error_count=$(echo "$log_data" | grep -cE "\bERROR\b" || true) warn_count=$(echo "$log_data" | grep -cE "\bWARN\b" || true) fatal_count=$(echo "$log_data" | grep -cE "\bFATAL\b" || true) if ! $JSON_MODE; then subsection_header "Log Level Breakdown" printf " %-15s %d\n" "FATAL:" "$fatal_count" printf " %-15s %d\n" "ERROR:" "$error_count" printf " %-15s %d\n" "WARN:" "$warn_count" fi # Stack traces (lines starting with "at " or "Caused by:") local stack_traces stack_traces=$(echo "$log_data" | grep -cE "^\s+at |^Caused by:" || true) local exception_types exception_types=$(echo "$log_data" | grep -oP "^\S+Exception|^\S+Error|Caused by: \K\S+Exception|\S+Exception(?=:)" | \ sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Exceptions / Stack Traces" printf " %-25s %d\n" "Stack trace lines:" "$stack_traces" echo "" if [[ -n "$exception_types" ]]; then printf " ${BOLD}%-4s %-6s %s${NC}\n" "#" "Count" "Exception" echo "$exception_types" | awk '{printf " %-4d %-6d %s\n", NR, $1, $2}' | head -"$TOP_N" else echo " No exceptions found." fi fi # OOM errors local oom_errors oom_errors=$(echo "$log_data" | grep -iE "(OutOfMemoryError|java.lang.OutOfMemoryError|GC overhead limit exceeded|Java heap space|Metaspace)" | head -"$TOP_N") local oom_count=0 if [[ -n "$oom_errors" ]]; then oom_count=$(echo "$oom_errors" | wc -l) fi if ! $JSON_MODE; then subsection_header "OutOfMemoryErrors" if [[ -n "$oom_errors" ]]; then echo "$oom_errors" | while IFS= read -r line; do local ts ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}|^[A-Z][a-z]{2} [0-9]{2}, [0-9]{4} [0-9:]+") local msg msg=$(echo "$line" | grep -oP "OutOfMemoryError.*|GC overhead limit exceeded|Java heap space|Metaspace" | head -c 70) printf " %-22s %s\n" "${ts:-unknown}" "${msg:-OOM}" done echo "" echo -e " ${RED}${oom_count} OOM event(s) — check JVM heap settings${NC}" else echo -e " ${GREEN}None found.${NC}" fi fi # GC pauses (from GC log lines embedded in app logs or GC-related messages) local gc_events gc_events=$(echo "$log_data" | grep -iE "(GC pause|Full GC|GC\(|Allocation Failure|G1 Evacuation Pause|CMS-concurrent|to-space exhausted)" | head -"$TOP_N") local gc_count=0 if [[ -n "$gc_events" ]]; then gc_count=$(echo "$gc_events" | wc -l) fi if ! $JSON_MODE; then subsection_header "GC Events" if [[ -n "$gc_events" ]]; then # Show Full GC vs minor GC breakdown local full_gc_count minor_gc_count full_gc_count=$(echo "$gc_events" | grep -c "Full GC" || true) minor_gc_count=$((gc_count - full_gc_count)) printf " %-25s %d\n" "Full GC:" "$full_gc_count" printf " %-25s %d\n" "Minor GC:" "$minor_gc_count" printf " %-25s %d\n" "Total:" "$gc_count" else echo " None found in application log (check dedicated GC log if separate)." fi fi # Thread deadlocks local deadlocks deadlocks=$(echo "$log_data" | grep -iE "(deadlock|DEADLOCK|Found one Java-level deadlock)" | head -"$TOP_N") local deadlock_count=0 if [[ -n "$deadlocks" ]]; then deadlock_count=$(echo "$deadlocks" | wc -l) fi if ! $JSON_MODE; then subsection_header "Thread Deadlocks" if [[ -n "$deadlocks" ]]; then echo "$deadlocks" | while IFS= read -r line; do local ts ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}|^[A-Z][a-z]{2} [0-9]{2}, [0-9]{4} [0-9:]+") printf " %s\n" "${ts:-unknown} — deadlock detected" done echo "" echo -e " ${RED}${deadlock_count} deadlock(s) found${NC}" else echo " None found." fi fi # Connection/network errors local conn_errors conn_errors=$(echo "$log_data" | grep -iE "(ConnectException|SocketTimeoutException|ConnectionRefused|Connection reset|NoRouteToHostException|UnknownHostException)" | \ grep -oP "\S*(Exception|Error)\S*" | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Connection / Network Errors" if [[ -n "$conn_errors" ]]; then printf " ${BOLD}%-6s %s${NC}\n" "Count" "Exception" echo "$conn_errors" | awk '{printf " %-6d %s\n", $1, $2}' | head -"$TOP_N" else echo " None found." fi fi # Top ERROR messages (deduped) local error_patterns error_patterns=$(echo "$log_data" | grep -E "\bERROR\b" | \ sed 's/^[0-9T:.+Z -]*//' | sed 's/\[.*\] //' | \ sed 's/[0-9]\{3,\}/#/g' | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Top ERROR Patterns" if [[ -n "$error_patterns" ]]; then printf " ${BOLD}%-6s %s${NC}\n" "Count" "Pattern" echo "$error_patterns" | awk '{ count = $1; $1 = "" sub(/^ +/, "") printf " %-6d %s\n", count, substr($0, 1, 65) }' | head -"$TOP_N" else echo " None found." fi fi # Top WARN messages (deduped) local warn_patterns warn_patterns=$(echo "$log_data" | grep -E "\bWARN\b" | \ sed 's/^[0-9T:.+Z -]*//' | sed 's/\[.*\] //' | \ sed 's/[0-9]\{3,\}/#/g' | sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Top WARN Patterns" if [[ -n "$warn_patterns" ]]; then printf " ${BOLD}%-6s %s${NC}\n" "Count" "Pattern" echo "$warn_patterns" | awk '{ count = $1; $1 = "" sub(/^ +/, "") printf " %-6d %s\n", count, substr($0, 1, 65) }' | head -"$TOP_N" else echo " None found." fi fi # Unique exception count local unique_exceptions=0 if [[ -n "$exception_types" ]]; then unique_exceptions=$(echo "$exception_types" | wc -l) fi if ! $JSON_MODE; then section_header "Summary" echo "" printf " %-25s %d\n" "FATAL entries:" "$fatal_count" printf " %-25s %d\n" "ERROR entries:" "$error_count" printf " %-25s %d\n" "WARN entries:" "$warn_count" printf " %-25s %d\n" "Stack trace lines:" "$stack_traces" printf " %-25s %d\n" "Unique exceptions:" "$unique_exceptions" printf " %-25s %d\n" "OOM errors:" "$oom_count" printf " %-25s %d\n" "GC events:" "$gc_count" printf " %-25s %d\n" "Deadlocks:" "$deadlock_count" printf " %-25s %d\n" "Total lines parsed:" "$total_lines" else json_add "java" "{\"fatal\":${fatal_count},\"error\":${error_count},\"warn\":${warn_count},\"stack_trace_lines\":${stack_traces},\"unique_exceptions\":${unique_exceptions},\"oom_errors\":${oom_count},\"gc_events\":${gc_count},\"deadlocks\":${deadlock_count},\"total_lines\":${total_lines}}" fi } # ============================================================================ # GITLAB LOG ANALYZER # ============================================================================ analyze_gitlab() { local gitlab_log_dir="/var/log/gitlab" if [[ -n "$CUSTOM_LOG" ]]; then gitlab_log_dir="$CUSTOM_LOG" fi if [[ ! -d "$gitlab_log_dir" ]]; then log_warn "GitLab log directory not found: ${gitlab_log_dir}" return 1 fi if ! $JSON_MODE; then section_header "GitLab Log Analysis" echo "" echo -e " Source: ${DIM}${gitlab_log_dir}${NC}" fi # Production JSON log (Rails) local prod_log="${gitlab_log_dir}/gitlab-rails/production_json.log" local total_requests=0 error_5xx=0 slow_requests=0 auth_failures=0 if [[ -r "$prod_log" ]]; then local prod_data prod_data=$(cat "$prod_log" | filter_by_time) total_requests=$(echo "$prod_data" | wc -l) # 5xx errors local fivexx_data fivexx_data=$(echo "$prod_data" | awk -F'"status":' '{ if (NF > 1) { split($2, a, /[,}]/) status = a[1] + 0 if (status >= 500) print } }') error_5xx=0 if [[ -n "$fivexx_data" ]]; then error_5xx=$(echo "$fivexx_data" | wc -l) fi if ! $JSON_MODE; then subsection_header "HTTP 5xx Errors (Rails)" if [[ "$error_5xx" -gt 0 ]]; then # Top paths with 5xx echo "$fivexx_data" | awk -F'"path":"' '{ if (NF > 1) { split($2, a, "\"") print a[1] } }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \ awk '{printf " %-6d %s\n", $1, $2}' else echo -e " ${GREEN}None found.${NC}" fi fi # Slow requests (>5s) local slow_data slow_data=$(echo "$prod_data" | awk -F'"duration_s":' '{ if (NF > 1) { split($2, a, /[,}]/) dur = a[1] + 0 if (dur > 5.0) print } }') slow_requests=0 if [[ -n "$slow_data" ]]; then slow_requests=$(echo "$slow_data" | wc -l) fi if ! $JSON_MODE; then subsection_header "Slow Requests (>5s)" if [[ "$slow_requests" -gt 0 ]]; then echo "$slow_data" | awk -F'"' '{ path = ""; dur = "" for (i = 1; i <= NF; i++) { if ($i == "path") path = $(i+2) if ($i == "duration_s") { split($(i+1), d, /[:,}]/) dur = d[2] } } if (path != "") printf " %.2fs %s\n", dur+0, path }' | sort -rn | head -"$TOP_N" else echo " None found." fi fi # Auth failures local auth_fail_data auth_fail_data=$(echo "$prod_data" | grep -i '"status":401\|"status":403' || true) auth_failures=0 if [[ -n "$auth_fail_data" ]]; then auth_failures=$(echo "$auth_fail_data" | wc -l) fi if ! $JSON_MODE; then subsection_header "Authentication Failures (401/403)" if [[ "$auth_failures" -gt 0 ]]; then echo "$auth_fail_data" | awk -F'"' '{ path = ""; remote_ip = "" for (i = 1; i <= NF; i++) { if ($i == "path") path = $(i+2) if ($i == "remote_ip") remote_ip = $(i+2) } if (remote_ip != "") printf " %-20s %s\n", remote_ip, path }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \ awk '{printf " %-6d %-20s %s\n", $1, $2, $3}' else echo " None found." fi fi # Top endpoints by request count if ! $JSON_MODE; then subsection_header "Top Endpoints by Request Count" echo "$prod_data" | awk -F'"path":"' '{ if (NF > 1) { split($2, a, "\"") print a[1] } }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \ awk '{printf " %-6d %s\n", $1, $2}' fi else if ! $JSON_MODE; then log_warn "GitLab production log not found: ${prod_log}" fi fi # Sidekiq failures local sidekiq_log="${gitlab_log_dir}/sidekiq/current" local sidekiq_failures=0 if [[ -r "$sidekiq_log" ]]; then local sidekiq_data sidekiq_data=$(cat "$sidekiq_log" | filter_by_time) local sidekiq_fail_data sidekiq_fail_data=$(echo "$sidekiq_data" | grep -i '"job_status":"fail"\|"severity":"ERROR"' || true) if [[ -n "$sidekiq_fail_data" ]]; then sidekiq_failures=$(echo "$sidekiq_fail_data" | wc -l) fi if ! $JSON_MODE; then subsection_header "Sidekiq Failed Jobs" if [[ "$sidekiq_failures" -gt 0 ]]; then echo "$sidekiq_fail_data" | awk -F'"' '{ job_class = ""; error = "" for (i = 1; i <= NF; i++) { if ($i == "class") job_class = $(i+2) if ($i == "error_class") error = $(i+2) } if (job_class != "") printf " %s — %s\n", job_class, error }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \ awk '{ count = $1; $1 = "" sub(/^ +/, "") printf " %-6d %s\n", count, $0 }' else echo " None found." fi fi fi # Gitaly errors local gitaly_log="${gitlab_log_dir}/gitaly/current" local gitaly_errors=0 if [[ -r "$gitaly_log" ]]; then local gitaly_data gitaly_data=$(cat "$gitaly_log" | filter_by_time) local gitaly_error_data gitaly_error_data=$(echo "$gitaly_data" | grep -iE '"level":"error"|"level":"fatal"' || true) if [[ -n "$gitaly_error_data" ]]; then gitaly_errors=$(echo "$gitaly_error_data" | wc -l) fi if ! $JSON_MODE; then subsection_header "Gitaly Errors" if [[ "$gitaly_errors" -gt 0 ]]; then echo "$gitaly_error_data" | awk -F'"' '{ msg = "" for (i = 1; i <= NF; i++) { if ($i == "msg" || $i == "error") { msg = $(i+2) break } } if (msg != "") print msg }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \ awk '{ count = $1; $1 = "" sub(/^ +/, "") printf " %-6d %s\n", count, substr($0, 1, 70) }' else echo " None found." fi fi fi # Error rate local error_rate="0.0" if [[ "$total_requests" -gt 0 ]]; then error_rate=$(awk "BEGIN {printf \"%.2f\", ($error_5xx / $total_requests) * 100}") fi if ! $JSON_MODE; then section_header "Summary" echo "" printf " %-25s %d\n" "Total requests:" "$total_requests" printf " %-25s %d (%.2f%%)\n" "5xx errors:" "$error_5xx" "$error_rate" printf " %-25s %d\n" "Slow requests (>5s):" "$slow_requests" printf " %-25s %d\n" "Auth failures:" "$auth_failures" printf " %-25s %d\n" "Sidekiq failures:" "$sidekiq_failures" printf " %-25s %d\n" "Gitaly errors:" "$gitaly_errors" else json_add "gitlab" "{\"total_requests\":${total_requests},\"5xx_errors\":${error_5xx},\"error_rate\":${error_rate},\"slow_requests\":${slow_requests},\"auth_failures\":${auth_failures},\"sidekiq_failures\":${sidekiq_failures},\"gitaly_errors\":${gitaly_errors}}" fi } # ============================================================================ # POSTGRESQL LOG ANALYZER # ============================================================================ analyze_postgresql() { local log_file="" if [[ -n "$CUSTOM_LOG" ]]; then log_file="$CUSTOM_LOG" else # Search common PostgreSQL log locations log_file=$(find_log \ /var/log/postgresql/postgresql-*-main.log \ /var/log/postgresql/postgresql.log \ /var/lib/pgsql/data/log/postgresql-*.log \ /var/lib/pgsql/data/pg_log/postgresql-*.log \ /var/log/pgsql/postgresql.log) fi local log_data="" if [[ -n "$log_file" && -r "$log_file" ]]; then log_data=$(cat "$log_file" | filter_by_time) elif command -v journalctl &>/dev/null; then local since_arg since_arg=$(journalctl_since_arg) if [[ -n "$since_arg" ]]; then log_data=$(journalctl -u postgresql* --no-pager -q $since_arg 2>/dev/null) else log_data=$(journalctl -u postgresql* --no-pager -q 2>/dev/null) fi fi if [[ -z "$log_data" ]]; then log_warn "No PostgreSQL log sources found" return 1 fi local total_lines total_lines=$(echo "$log_data" | wc -l) if ! $JSON_MODE; then section_header "PostgreSQL Log Analysis" echo "" echo -e " Source: ${DIM}${log_file:-journalctl}${NC}" echo -e " Lines: ${total_lines}" fi # FATAL/ERROR/PANIC counts local fatal_count error_count panic_count fatal_count=$(echo "$log_data" | grep -c " FATAL: " || true) error_count=$(echo "$log_data" | grep -c " ERROR: " || true) panic_count=$(echo "$log_data" | grep -c " PANIC: " || true) if ! $JSON_MODE; then subsection_header "Severity Breakdown" printf " %-15s %d\n" "PANIC:" "$panic_count" printf " %-15s %d\n" "FATAL:" "$fatal_count" printf " %-15s %d\n" "ERROR:" "$error_count" fi # Connection errors local conn_errors conn_errors=$(echo "$log_data" | grep -iE "(too many connections|connection refused|remaining connection slots|sorry, too many clients)" | head -"$TOP_N") local conn_error_count=0 if [[ -n "$conn_errors" ]]; then conn_error_count=$(echo "$conn_errors" | wc -l) fi if ! $JSON_MODE; then subsection_header "Connection Errors" if [[ -n "$conn_errors" ]]; then echo "$conn_errors" | while IFS= read -r line; do local ts msg ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+") msg=$(echo "$line" | grep -oP "(FATAL|ERROR): \K.*" | head -c 70) printf " %-22s %s\n" "${ts:-unknown}" "${msg:-$line}" done else echo -e " ${GREEN}None found.${NC}" fi fi # Deadlocks local deadlocks deadlocks=$(echo "$log_data" | grep -i "deadlock detected" | head -"$TOP_N") local deadlock_count=0 if [[ -n "$deadlocks" ]]; then deadlock_count=$(echo "$deadlocks" | wc -l) fi if ! $JSON_MODE; then subsection_header "Deadlocks" if [[ -n "$deadlocks" ]]; then echo "$deadlocks" | while IFS= read -r line; do local ts ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+") printf " %s\n" "${ts:-unknown}" done echo "" echo " Total: ${deadlock_count}" else echo " None found." fi fi # Slow queries (log_min_duration_statement) local slow_queries slow_queries=$(echo "$log_data" | grep -i "duration:" | grep -v "LOG: connection\|LOG: disconnection" | head -"$TOP_N") local slow_query_count=0 if [[ -n "$slow_queries" ]]; then slow_query_count=$(echo "$slow_queries" | wc -l) fi if ! $JSON_MODE; then subsection_header "Slow Queries" if [[ "$slow_query_count" -gt 0 ]]; then echo "$slow_queries" | while IFS= read -r line; do local ts dur stmt ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+") dur=$(echo "$line" | grep -oP "duration: \K[0-9.]+ ms") stmt=$(echo "$line" | grep -oP "statement: \K.*" | head -c 60) printf " %-22s %-12s %s\n" "${ts:-unknown}" "${dur:-?}" "${stmt:-...}" done else echo " None found (requires log_min_duration_statement in postgresql.conf)." fi fi # Checkpoint warnings local checkpoint_warns checkpoint_warns=$(echo "$log_data" | grep -iE "(checkpoint.*too frequent|checkpoints are occurring too frequently|checkpoint.*taking)" | head -"$TOP_N") local checkpoint_count=0 if [[ -n "$checkpoint_warns" ]]; then checkpoint_count=$(echo "$checkpoint_warns" | wc -l) fi if ! $JSON_MODE; then subsection_header "Checkpoint Warnings" if [[ -n "$checkpoint_warns" ]]; then echo "$checkpoint_warns" | while IFS= read -r line; do local ts msg ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+") msg=$(echo "$line" | grep -oP "LOG: \K.*|WARNING: \K.*" | head -c 70) printf " %-22s %s\n" "${ts:-unknown}" "${msg:-checkpoint warning}" done else echo " None found." fi fi # WAL/replication errors local wal_errors wal_errors=$(echo "$log_data" | grep -iE "(WAL.*error|replication.*error|could not receive|streaming replication|recovery target)" | head -"$TOP_N") local wal_error_count=0 if [[ -n "$wal_errors" ]]; then wal_error_count=$(echo "$wal_errors" | wc -l) fi if ! $JSON_MODE; then subsection_header "WAL / Replication Errors" if [[ -n "$wal_errors" ]]; then echo "$wal_errors" | while IFS= read -r line; do local ts msg ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+") msg=$(echo "$line" | grep -oP "(FATAL|ERROR|LOG): \K.*" | head -c 70) printf " %-22s %s\n" "${ts:-unknown}" "${msg:-$line}" done else echo " None found." fi fi # Auth failures local pg_auth_failures pg_auth_failures=$(echo "$log_data" | grep -iE "(password authentication failed|no pg_hba.conf entry|authentication failed)" | head -"$TOP_N") local pg_auth_fail_count=0 if [[ -n "$pg_auth_failures" ]]; then pg_auth_fail_count=$(echo "$pg_auth_failures" | wc -l) fi if ! $JSON_MODE; then subsection_header "Authentication Failures" if [[ -n "$pg_auth_failures" ]]; then echo "$pg_auth_failures" | awk -F'"' '{ # Try to extract user and database line = $0 } { match($0, /user "([^"]+)"/, u) match($0, /database "([^"]+)"/, d) user = (u[1] != "") ? u[1] : "?" db = (d[1] != "") ? d[1] : "?" printf " user=%-15s db=%-15s\n", user, db }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \ awk '{printf " %-6d %s %s\n", $1, $2, $3}' else echo " None found." fi fi # Top error patterns local pg_error_patterns pg_error_patterns=$(echo "$log_data" | grep -E " (ERROR|FATAL): " | \ sed 's/^[^:]*: //' | sed 's/[0-9]\{2,\}/#/g' | \ sort | uniq -c | sort -rn | head -"$TOP_N") if ! $JSON_MODE; then subsection_header "Top Error Patterns" if [[ -n "$pg_error_patterns" ]]; then printf " ${BOLD}%-6s %s${NC}\n" "Count" "Pattern" echo "$pg_error_patterns" | awk '{ count = $1; $1 = "" sub(/^ +/, "") printf " %-6d %s\n", count, substr($0, 1, 65) }' | head -"$TOP_N" else echo " None found." fi fi if ! $JSON_MODE; then section_header "Summary" echo "" printf " %-25s %d\n" "PANIC entries:" "$panic_count" printf " %-25s %d\n" "FATAL entries:" "$fatal_count" printf " %-25s %d\n" "ERROR entries:" "$error_count" printf " %-25s %d\n" "Connection errors:" "$conn_error_count" printf " %-25s %d\n" "Deadlocks:" "$deadlock_count" printf " %-25s %d\n" "Slow queries:" "$slow_query_count" printf " %-25s %d\n" "Checkpoint warnings:" "$checkpoint_count" printf " %-25s %d\n" "WAL/replication errors:" "$wal_error_count" printf " %-25s %d\n" "Auth failures:" "$pg_auth_fail_count" printf " %-25s %d\n" "Total lines parsed:" "$total_lines" else json_add "postgresql" "{\"panic\":${panic_count},\"fatal\":${fatal_count},\"error\":${error_count},\"connection_errors\":${conn_error_count},\"deadlocks\":${deadlock_count},\"slow_queries\":${slow_query_count},\"checkpoint_warnings\":${checkpoint_count},\"wal_errors\":${wal_error_count},\"auth_failures\":${pg_auth_fail_count},\"total_lines\":${total_lines}}" fi } # ============================================================================ # ARGUMENT PARSING # ============================================================================ parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) show_usage ;; --type) LOG_TYPE="$2"; shift 2 ;; --all-types) ALL_TYPES=true; shift ;; --since) SINCE="$2"; parse_since "$2"; shift 2 ;; --top) TOP_N="$2"; shift 2 ;; --json) JSON_MODE=true; shift ;; --no-color) NO_COLOR=true; shift ;; --output) OUTPUT_FILE="$2"; shift 2 ;; --log) CUSTOM_LOG="$2"; shift 2 ;; *) log_error "Unknown option: $1"; show_usage ;; esac done if [[ -z "$LOG_TYPE" ]] && ! $ALL_TYPES; then log_error "Specify --type or --all-types" echo "" show_usage fi } # ============================================================================ # MAIN # ============================================================================ main() { parse_args "$@" # Disable colors if requested or not a terminal if $NO_COLOR || [[ ! -t 1 ]]; then disable_colors fi # Redirect output to file if specified if [[ -n "$OUTPUT_FILE" ]]; then disable_colors exec > >(tee "$OUTPUT_FILE") 2>&1 fi if ! $JSON_MODE; then echo -e "${BOLD}Linux Log Analyzer v${VERSION}${NC}" if [[ -n "$SINCE" ]]; then echo -e "Since: ${SINCE}" fi fi if $ALL_TYPES; then # Run all available analyzers local ran_any=false for type in system auth docker java gitlab postgresql; do if ! $JSON_MODE; then echo "" echo -e "${DIM}────────────────────────────────────────────────────${NC}" echo -e "${BOLD} Analyzing: ${type}${NC}" echo -e "${DIM}────────────────────────────────────────────────────${NC}" fi case "$type" in system) analyze_system && ran_any=true || log_warn "system: skipped" ;; auth) analyze_auth && ran_any=true || log_warn "auth: skipped" ;; docker) analyze_docker && ran_any=true || log_warn "docker: skipped" ;; java) analyze_java && ran_any=true || log_warn "java: skipped" ;; gitlab) analyze_gitlab && ran_any=true || log_warn "gitlab: skipped" ;; postgresql) analyze_postgresql && ran_any=true || log_warn "postgresql: skipped" ;; esac done if ! $ran_any; then log_error "No log sources found for any type" exit 1 fi else case "$LOG_TYPE" in system) analyze_system ;; auth) analyze_auth ;; docker) analyze_docker ;; java) analyze_java ;; gitlab) analyze_gitlab ;; postgresql) analyze_postgresql ;; *) log_error "Unknown log type: $LOG_TYPE"; show_usage ;; esac fi if $JSON_MODE; then echo "{${JSON_OUTPUT}}" fi } main "$@"