脚本已写到 D:\202609\collect-linux-logs.sh,内容如下(可直接复制到 Linux 上使用):
#!/usr/bin/env bash
#===============================================================================
# collect-linux-logs.sh — Linux 故障诊断日志一键收集
#
# 收集范围:
# 1. 基础系统信息 uname / 发行版 / CPU / 内存 / 磁盘 / 显卡 / 失败服务
# 2. OS 系统日志 /var/log/messages /var/log/syslog /var/log/secure ...
# + journalctl (当前启动全量 + 最近 N 天 warning 以上)
# 3. 内核日志 dmesg -TT / journalctl -k / /var/log/dmesg / kern.log
# 4. VNC 日志 所有用户的 ~/.vnc/*.log|*.pid|config|xstartup ...
# /tmp/.X11-unix / 监听端口 / vnc 相关 systemd 单元
# 5. 显示管理器 /var/log/gdm|gdm3|lightdm|sddm|lxdm|...
# + journalctl -u gdm -u lightdm -u sddm ...
# 6. Xorg 日志 /var/log/Xorg.*.log* / ~/.local/share/xorg/ ~/.xsession-errors
#
# 用法:
# sudo ./collect-linux-logs.sh [-d 天数] [-o 输出目录] [-n] [-h]
# -d 天数 journalctl 回溯天数,默认 3
# -o 目录 输出目录,默认当前目录
# -n 只生成目录,不打包成 tar.gz
# -h 显示帮助
#
# 建议以 root 运行,否则部分日志(secure/audit/dmesg)无法读取。
#===============================================================================
set -uo pipefail
#------------------------------ 参数解析 --------------------------------------
DAYS=3
OUTBASE="$PWD"
PACK=1
usage() { sed -n '3,24p' "$0" 2>/dev/null || echo "用法: sudo $0 [-d 天数] [-o 输出目录] [-n]"; }
while getopts ":d:o:nh" opt; do
case "$opt" in
d) DAYS="$OPTARG" ;;
o) OUTBASE="$OPTARG" ;;
n) PACK=0 ;;
h) usage; exit 0 ;;
:) echo "参数 -$OPTARG 需要值" >&2; exit 1 ;;
*) echo "未知参数: -$OPTARG" >&2; usage; exit 1 ;;
esac
done
#------------------------------ 提权 ------------------------------------------
if [ "$(id -u)" -ne 0 ]; then
echo "[!] 当前非 root,部分日志无法读取。"
if command -v sudo >/dev/null 2>&1; then
echo "[*] 尝试通过 sudo 重新执行..."
exec sudo -E bash "$0" "$@"
fi
echo "[!] 未找到 sudo,将以普通用户身份继续(结果可能不完整)。"
fi
#------------------------------ 全局变量 --------------------------------------
TS="$(date +%Y%m%d-%H%M%S)"
HOST="$(hostname -s 2>/dev/null || echo unknown)"
OUTDIR="$OUTBASE/logs-${HOST}-${TS}"
SUMMARY="$OUTDIR/00-收集摘要.txt"
MAXBYTES=$((200 * 1024 * 1024)) # 单文件超过 200MB 只保留末尾
OK_CNT=0
MISS_CNT=0
mkdir -p "$OUTDIR" && chmod 700 "$OUTDIR"
: > "$SUMMARY"
#------------------------------ 辅助函数 --------------------------------------
note() { printf '%s\n' "$*" >> "$SUMMARY"; }
ok() { OK_CNT=$((OK_CNT+1)); printf ' [OK] %s\n' "$*"; note "[OK] $*"; }
miss(){ MISS_CNT=$((MISS_CNT+1)); printf ' [MISS] %s\n' "$*"; note "[MISS] $*"; }
warn(){ printf ' [WARN] %s\n' "$*"; note "[WARN] $*"; }
# 拷贝文件或目录到目标目录(自动截断超大文件)
copy_path() {
local src="$1" dest="$2"
[ -e "$src" ] || { miss "$src"; return 1; }
if [ -f "$src" ]; then
local sz
sz="$(stat -c %s "$src" 2>/dev/null || echo 0)"
if [ "$sz" -gt "$MAXBYTES" ]; then
local base; base="$(basename "$src")"
mkdir -p "$dest"
echo "### 源文件 ${sz} 字节,超限,仅保留末尾 ${MAXBYTES} 字节 ###" > "$dest/$base"
tail -c "$MAXBYTES" "$src" >> "$dest/$base" 2>/dev/null
warn "TRUNC $src (${sz} bytes)"
return 0
fi
fi
mkdir -p "$dest"
if cp -a "$src" "$dest/" 2>/dev/null; then
ok "$src"
else
warn "COPY-FAIL $src"
fi
}
# 按通配符拷贝一批文件
copy_glob() {
# shellcheck disable=SC2086
local pat="$1" dest="$2" f found=0
mkdir -p "$dest"
for f in $pat; do
[ -e "$f" ] || continue
found=1
copy_path "$f" "$dest"
done
[ "$found" -eq 0 ] && miss "$pat"
return 0
}
# 执行命令并落盘
run_cmd() {
local out="$1"; shift
mkdir -p "$(dirname "$out")"
if "$@" > "$out" 2>&1; then
ok "CMD: $*"
else
warn "CMD-FAIL($?): $*"
fi
}
has() { command -v "$1" >/dev/null 2>&1; }
have_journal=0
if has journalctl && [ -d /run/systemd/system ]; then have_journal=1; fi
echo "==============================================================="
echo " Linux 日志收集 -> $OUTDIR"
echo " 时间: $(date '+%F %T') 主机: $HOST 回溯: ${DAYS} 天"
echo "==============================================================="
#===============================================================================
# 01 基础系统信息
#===============================================================================
echo "[1/6] 基础系统信息 ..."
S1="$OUTDIR/01-system"
{ echo "===== uname -a ====="; uname -a
echo; echo "===== /etc/os-release ====="; cat /etc/os-release 2>/dev/null
echo; echo "===== uptime / 运行时长 ====="; uptime
echo; echo "===== date / timezone ====="; date; timedatectl 2>/dev/null
echo; echo "===== CPU ====="; lscpu 2>/dev/null || cat /proc/cpuinfo
echo; echo "===== 内存 ====="; free -h 2>/dev/null; echo; cat /proc/meminfo 2>/dev/null
echo; echo "===== 磁盘 ====="; df -hT 2>/dev/null; echo; lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT 2>/dev/null
echo; echo "===== 已登录用户 ====="; who 2>/dev/null; echo; w 2>/dev/null
echo; echo "===== last -n 50 ====="; last -n 50 2>/dev/null
echo; echo "===== 环境变量(去敏) ====="
env | grep -Ei '^(DISPLAY|WAYLAND_DISPLAY|XDG_SESSION|SSH_|LANG|LC_)' 2>/dev/null
} > "$S1/系统信息.txt" 2>&1 && ok "$S1/系统信息.txt" || warn "写系统信息失败"
has lspci && run_cmd "$S1/显卡设备.txt" lspci -nnk
has lsmod && { lsmod 2>/dev/null | grep -Ei 'nvidia|nouveau|amdgpu|radeon|i915|vmwgfx|vboxvideo|qxl|virtio_gpu' >
"$S1/显卡驱动模块.txt" 2>&1; ok "$S1/显卡驱动模块.txt"; }
[ -r /proc/cmdline ] && copy_path /proc/cmdline "$S1"
[ "$have_journal" -eq 1 ] && {
run_cmd "$S1/失败的服务.txt" systemctl --failed --no-pager
run_cmd "$S1/显示管理器状态.txt" systemctl status display-manager --no-pager -l
run_cmd "$S1/全部单元文件列表.txt" systemctl list-unit-files --type=service --no-legend
}
has dmesg && run_cmd "$S1/内核版本.txt" uname -r
has systemd-detect-virt && run_cmd "$S1/虚拟化类型.txt" systemd-detect-virt
#===============================================================================
# 02 OS 系统日志
#===============================================================================
echo "[2/6] OS 系统日志 ..."
S2="$OUTDIR/02-os"
copy_glob "/var/log/messages" "$S2"
copy_glob "/var/log/messages.*" "$S2"
copy_glob "/var/log/syslog" "$S2"
copy_glob "/var/log/syslog.*" "$S2"
copy_glob "/var/log/secure" "$S2"
copy_glob "/var/log/secure.*" "$S2"
copy_glob "/var/log/auth.log" "$S2"
copy_glob "/var/log/auth.log.*" "$S2"
copy_glob "/var/log/boot.log" "$S2"
copy_glob "/var/log/boot.log.*" "$S2"
copy_glob "/var/log/cron" "$S2"
copy_glob "/var/log/cron.*" "$S2"
copy_glob "/var/log/audit/audit.log" "$S2/audit"
copy_glob "/var/log/wtmp" "$S2"
copy_glob "/var/log/btmp" "$S2"
copy_glob "/var/log/lastlog" "$S2"
copy_glob "/var/log/dpkg.log" "$S2"
if [ "$have_journal" -eq 1 ]; then
run_cmd "$S2/journalctl-本次启动全量.log" journalctl --no-pager -b -o short-iso
run_cmd "$S2/journalctl-最近${DAYS}天-警告以上.log" journalctl --no-pager -o short-iso \
--since "-${DAYS} days" -p warning
run_cmd "$S2/journalctl-磁盘占用.log" journalctl --disk-usage
else
warn "未检测到 systemd-journald,跳过 journalctl 采集"
fi
#===============================================================================
# 03 内核日志
#===============================================================================
echo "[3/6] 内核日志 ..."
S3="$OUTDIR/03-kernel"
if has dmesg; then
if dmesg -TT > "$S3/dmesg-TT.log" 2>&1; then
ok "dmesg -TT"
elif dmesg -T > "$S3/dmesg-T.log" 2>&1; then
ok "dmesg -T (不支持 -TT)"
else
warn "dmesg 读取失败(可能受 kernel.dmesg_restrict 限制)"
fi
run_cmd "$S3/dmesg-仅错误级别.log" dmesg -T -l err,crit,alert,emerg
else
miss "dmesg 命令"
fi
copy_glob "/var/log/dmesg" "$S3"
copy_glob "/var/log/dmesg.*" "$S3"
copy_glob "/var/log/kern.log" "$S3"
copy_glob "/var/log/kern.log.*" "$S3"
copy_glob "/var/log/kernel.log" "$S3"
copy_glob "/var/log/kernel.log.*" "$S3"
[ "$have_journal" -eq 1 ] && {
run_cmd "$S3/journalctl-kernel-本次启动.log" journalctl --no-pager -k -b -o short-iso
run_cmd "$S3/journalctl-kernel-最近${DAYS}天.log" journalctl --no-pager -k -o short-iso --since "-${DAYS} days"
}
[ -r /proc/cmdline ] && copy_path /proc/cmdline "$S3"
#===============================================================================
# 04 用户 VNC 日志
#===============================================================================
echo "[4/6] 用户 VNC 日志 ..."
S4="$OUTDIR/04-vnc"
# 枚举所有用户及其家目录
list_users() {
if has getent; then
getent passwd | awk -F: '{print $1" "$6}'
else
awk -F: '{print $1" "$6}' /etc/passwd
fi
}
USER_FOUND=0
while read -r u uhome; do
[ -n "${uhome:-}" ] || continue
[ -d "$uhome" ] || continue
# 只处理确实存在 VNC / X 会话痕迹的用户
[ -d "$uhome/.vnc" ] || [ -d "$uhome/.x11vnc" ] || \
[ -e "$uhome/.xsession-errors" ] || [ -d "$uhome/.local/share/xorg" ] || continue
dest="$S4/home/$u"
USER_FOUND=1
echo " - 用户 $u ($uhome)"
# TigerVNC / TightVNC / RealVNC: ~/.vnc/{*.log,*.pid,config,xstartup}
copy_glob "$uhome/.vnc/*.log" "$dest/.vnc"
copy_glob "$uhome/.vnc/*.log.old" "$dest/.vnc"
copy_glob "$uhome/.vnc/*.pid" "$dest/.vnc"
copy_glob "$uhome/.vnc/config" "$dest/.vnc"
copy_glob "$uhome/.vnc/xstartup" "$dest/.vnc"
copy_glob "$uhome/.vnc/*.conf" "$dest/.vnc"
# x11vnc
copy_glob "$uhome/.x11vnc/*" "$dest/.x11vnc"
copy_glob "$uhome/.config/x11vnc*" "$dest/.config"
# X 会话错误输出(VNC 桌面起不来时最常看这个)
copy_glob "$uhome/.xsession-errors" "$dest"
copy_glob "$uhome/.xsession-errors.old" "$dest"
copy_glob "$uhome/.vnc/xsession-errors" "$dest/.vnc"
done < <(list_users)
[ "$USER_FOUND" -eq 0 ] && miss "未在任何用户家目录发现 .vnc / x11vnc / X 会话文件"
# 系统级 VNC 日志
copy_glob "/var/log/vnc*" "$S4/system"
copy_glob "/var/log/tigervnc*" "$S4/system"
copy_glob "/var/log/vncserver*" "$S4/system"
copy_glob "/var/log/x11vnc*" "$S4/system"
copy_glob "/etc/vnc/*" "$S4/system/etc-vnc"
copy_glob "/etc/tigervnc/*" "$S4/system/etc-vnc"
# 运行态信息
has ps && { ps -eo pid,ppid,user,lstart,cmd 2>/dev/null | grep -Ei 'vnc|x11vnc|Xvnc|tightvnc|realvnc' | grep -v grep >
"$S4/VNC进程.txt" 2>&1; ok "$S4/VNC进程.txt"; }
has ss && run_cmd "$S4/监听端口-ss.txt" ss -tulnp
has netstat && run_cmd "$S4/监听端口-netstat.txt" netstat -tulnp
run_cmd "$S4/X11-socket列表.txt" ls -la /tmp/.X11-unix
copy_glob "/tmp/.X*-lock" "$S4/tmp"
copy_glob "/tmp/.vnc-*" "$S4/tmp"
# VNC 相关 systemd 单元
if [ "$have_journal" -eq 1 ]; then
run_cmd "$S4/VNC相关单元.txt" systemctl list-unit-files --type=service --no-legend
vnc_units="$(systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Ei 'vnc'
|| true)"
if [ -n "$vnc_units" ]; then
# shellcheck disable=SC2086
run_cmd "$S4/journalctl-VNC单元.log" journalctl --no-pager -b -o short-iso \
$(printf -- '-u %s ' $vnc_units)
fi
# 按关键字过滤全局日志,兜底抓取
run_cmd "$S4/journalctl-含vnc关键字.log" journalctl --no-pager -b -o short-iso --grep=vnc
fi
#===============================================================================
# 05 显示管理器日志 (gdm / gdm3 / lightdm / sddm / lxdm / xdm)
#===============================================================================
echo "[5/6] 显示管理器日志 ..."
S5="$OUTDIR/05-display-manager"
copy_glob "/var/log/gdm/*" "$S5/gdm"
copy_glob "/var/log/gdm3/*" "$S5/gdm3"
copy_glob "/var/log/lightdm/*" "$S5/lightdm"
copy_glob "/var/log/lightdm*" "$S5/lightdm"
copy_glob "/var/log/sddm.log" "$S5/sddm"
copy_glob "/var/log/sddm/*" "$S5/sddm"
copy_glob "/var/log/lxdm/*" "$S5/lxdm"
copy_glob "/var/log/xdm.log" "$S5/xdm"
copy_glob "/var/log/slim.log" "$S5/slim"
for dm in gdm gdm3 lightdm sddm lxdm xdm lxdm-greeter; do
if systemctl list-unit-files "${dm}.service" >/dev/null 2>&1 && \
systemctl status "${dm}.service" >/dev/null 2>&1; then
run_cmd "$S5/状态-${dm}.txt" systemctl status "${dm}.service" --no-pager -l
fi
done
[ "$have_journal" -eq 1 ] && {
run_cmd "$S5/journalctl-gdm.log" journalctl --no-pager -b -o short-iso -u gdm -u gdm3
run_cmd "$S5/journalctl-lightdm.log" journalctl --no-pager -b -o short-iso -u lightdm
run_cmd "$S5/journalctl-sddm.log" journalctl --no-pager -b -o short-iso -u sddm
run_cmd "$S5/journalctl-lxdm.log" journalctl --no-pager -b -o short-iso -u lxdm -u xdm
run_cmd "$S5/journalctl-显示管理器相关.log" journalctl --no-pager -b -o short-iso
--grep='gdm|lightdm|sddm|lxdm|Xorg|X server'
}
copy_glob "/etc/gdm3/*" "$S5/etc-config/gdm3"
copy_glob "/etc/gdm/*" "$S5/etc-config/gdm"
copy_glob "/etc/lightdm/*" "$S5/etc-config/lightdm"
copy_glob "/etc/sddm.conf" "$S5/etc-config"
copy_glob "/etc/sddm.conf.d/*" "$S5/etc-config/sddm.conf.d"
copy_glob "/etc/lxdm/*" "$S5/etc-config/lxdm"
#===============================================================================
# 06 Xorg 日志
#===============================================================================
echo "[6/6] Xorg 日志 ..."
S6="$OUTDIR/06-xorg"
copy_glob "/var/log/Xorg.*.log" "$S6"
copy_glob "/var/log/Xorg.*.log.old" "$S6"
copy_glob "/var/log/Xorg.failsafe.log" "$S6"
copy_glob "/var/log/xorg.*.log" "$S6"
copy_glob "/var/log/X.log" "$S6"
copy_glob "/etc/X11/xorg.conf" "$S6/etc-X11"
copy_glob "/etc/X11/xorg.conf.d/*" "$S6/etc-X11/xorg.conf.d"
copy_glob "/etc/X11/Xwrapper.config" "$S6/etc-X11"
copy_glob "/usr/share/X11/xorg.conf.d/*" "$S6/usr-share-X11/xorg.conf.d"
# 当前会话相关
run_cmd "$S6/X11-socket.txt" ls -la /tmp/.X11-unix
copy_glob "/tmp/.X*-lock" "$S6"
[ -r /var/lib/xdm/authdir ] && copy_path /var/lib/xdm/authdir "$S6"
[ "$have_journal" -eq 1 ] && \
run_cmd "$S6/journalctl-Xorg关键字.log" journalctl --no-pager -b -o short-iso --grep='Xorg|X
server|glx|drm|modeset|EE'
#===============================================================================
# 打包 + 摘要
#===============================================================================
echo
echo "---------------------------------------------------------------"
{
echo "收集完成时间 : $(date '+%F %T')"
echo "主机名 : $HOST"
echo "输出目录 : $OUTDIR"
echo "成功条目 : $OK_CNT"
echo "缺失条目 : $MISS_CNT"
echo
echo "目录结构:"
find "$OUTDIR" -maxdepth 2 -type d | sed "s|$OUTDIR|.|" | sort
echo
echo "各目录大小:"
du -sh "$OUTDIR"/* 2>/dev/null
echo
echo "各文件大小:"
find "$OUTDIR" -type f -printf '%10s %p\n' 2>/dev/null | sed "s|$OUTDIR|.|g" | sort -k2
echo
echo "===== 明细日志 ====="
} >> "$SUMMARY"
if [ "$PACK" -eq 1 ] && has tar; then
TARBALL="${OUTDIR}.tar.gz"
if tar -czf "$TARBALL" -C "$(dirname "$OUTDIR")" "$(basename "$OUTDIR")" 2>/dev/null; then
chmod 600 "$TARBALL"
echo "[✓] 已打包: $TARBALL ($(du -h "$TARBALL" | cut -f1))"
else
warn "打包失败,请直接使用目录: $OUTDIR"
fi
fi
echo "[✓] 明细目录: $OUTDIR"
echo "[✓] 收集摘要: $SUMMARY"
echo "---------------------------------------------------------------"
tail -n 20 "$SUMMARY" | grep -E '成功条目|缺失条目' || true
echo
echo "提示: 摘要中含敏感信息(如 secure/auth.log),外发前请自行脱敏。"
使用方式
# 上传后(Windows 编辑器传过去记得转 LF)
sed -i 's/\r$//' collect-linux-logs.sh
chmod +x collect-linux-logs.sh
sudo ./collect-linux-logs.sh # 默认回溯 3 天
sudo ./collect-linux-logs.sh -d 7 -o /tmp # 回溯 7 天,输出到 /tmp
sudo ./collect-linux-logs.sh -n # 只留目录不打包
输出结构:
logs-<主机名>-<时间戳>/
├── 00-收集摘要.txt # 成功/缺失清单 + 文件大小列表
├── 01-system/ # 系统信息、显卡、失败服务
├── 02-os/ # messages/syslog/secure/auth.log + journalctl
├── 03-kernel/ # dmesg -TT / journalctl -k / kern.log
├── 04-vnc/ # home/<用户>/.vnc/*.log、进程、端口、systemd 单元
├── 05-display-manager/ # gdm|gdm3|lightdm|sddm|lxdm + journalctl
└── 06-xorg/ # Xorg.*.log、xorg.conf、X11 socket
几个设计点
- 兼容新旧发行版:/var/log/messages 是 RHEL/CentOS 系,Debian/Ubuntu 系是
syslog/auth.log,脚本用通配符全试一遍,缺了记 [MISS] 不报错;同时判断 /run/systemd/system 决定是否走 journalctl。
- dmesg -TT 降级:老内核不支持 -TT(可读时间戳)时自动退回 dmesg -T。
- VNC 按用户枚举:从 getent passwd 取所有家目录,只处理真有 .vnc / .x11vnc / .xsession-errors
的用户,目录里按用户名分开放,多用户机器不会混。额外抓了 .xsession-errors —— VNC
连上了但桌面是黑屏/灰屏时,问题基本都在这个文件里。
- 超大文件保护:单文件超 200MB 只保留末尾 200MB 并打 [TRUNC] 标记,避免 /var/log/messages 几十 G 把机器撑爆。
- 非致命:set -u -o pipefail 但不开 -e,任何一个日志缺失都不会中断整体收集。
- 权限:输出目录 700、tar 包 600,因为里面含 secure/auth.log 等敏感内容,脚本末尾也给了脱敏提醒。
一个提醒:脚本会把 /etc/gdm3/*、/etc/lightdm/* 等配置一并收进去,LightDM 的 lightdm.conf
里如果手工配了自动登录密码,会在包里 —— 外发前建议扫一眼。