Files
linux-scripts/linux-log-analyzer.sh
T
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

1718 lines
61 KiB
Bash
Executable File

#!/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 <<EOF
Usage: $0 [OPTIONS]
Analyze Linux system logs (v${VERSION}).
LOG TYPES:
--type TYPE Log type: system, auth, docker, java, gitlab, postgresql
--all-types Run all available analyzers in one pass
OPTIONS:
--since RANGE Time filter: 1h, 24h, 7d, 30d, or ISO date (default: all)
--top N Entries per section (default: ${TOP_N})
--json Output as JSON
--no-color Disable colored output
--output FILE Save report to file
--log PATH Override default log path
-h, --help Show this help
EXAMPLES:
sudo $0 --type system
sudo $0 --type auth --since 24h
sudo $0 --type docker --since 7d
$0 --type java --log /opt/tomcat/logs/catalina.out
sudo $0 --type gitlab
$0 --type postgresql --log /var/log/postgresql/postgresql-16-main.log
sudo $0 --all-types --no-color > 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 "$@"