Skip to content

Commit 89bd24a

Browse files
authored
Create ubuntu and windows healthcheck scripts (fleetdm#50128)
Adds scripts for Windows and Ubuntu that checks the health of all fleetd components and collects logs and recent events into a timestamped archive for support and troubleshooting. These have been used by multiple customers now, and has simplified the collection of logs from multiple locations while including events that can assist with troubleshooting. Adding to solutions for wider use.
1 parent 600fbf4 commit 89bd24a

2 files changed

Lines changed: 709 additions & 0 deletions

File tree

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
#!/usr/bin/env bash
2+
# fleetd_healthcheck_ubuntu.sh
3+
#
4+
# Checks the health of all fleetd components on Ubuntu and collects logs into
5+
# a timestamped archive for support/troubleshooting.
6+
#
7+
# Components checked:
8+
# - orbit.service (systemd service)
9+
# - orbit (process: /opt/orbit/bin/orbit/orbit)
10+
# - osqueryd (process: spawned and managed by orbit)
11+
# - fleet-desktop (process: optional, only present if packaged with --fleet-desktop)
12+
#
13+
# Sources:
14+
# - Service name/unit: orbit/pkg/packaging/linux_shared.go (writeSystemdUnit)
15+
# - Binary path: /opt/orbit/bin/orbit/orbit (symlinked to /usr/local/bin/orbit)
16+
# - Process name: constant.DesktopAppExecName = "fleet-desktop"
17+
# - Log paths: /var/log/orbit/, /var/log/osquery/ (created at install time)
18+
# - Env file: /etc/default/orbit (written by writeEnvFile)
19+
#
20+
# Also collects a lookback window (default 72h, override with LOOKBACK_HOURS env
21+
# var) of system events — reboots, package changes, systemd failures, journal
22+
# errors — to help correlate a reported problem with what changed beforehand.
23+
#
24+
# Must be run as root.
25+
26+
set -euo pipefail
27+
28+
# ── Colour helpers ─────────────────────────────────────────────────────────────
29+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
30+
ok() { echo -e " ${GREEN}[OK]${NC} $*"; }
31+
warn() { echo -e " ${YELLOW}[WARN]${NC} $*"; }
32+
fail() { echo -e " ${RED}[FAIL]${NC} $*"; }
33+
info() { echo -e " [INFO] $*"; }
34+
35+
if [[ $EUID -ne 0 ]]; then
36+
echo "This script must be run as root." >&2
37+
exit 1
38+
fi
39+
40+
LOOKBACK_HOURS="${LOOKBACK_HOURS:-72}"
41+
42+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
43+
HOSTNAME_SAFE=$(hostname | tr '.' '_')
44+
ARCHIVE_NAME="fleetd_healthcheck_${HOSTNAME_SAFE}_${TIMESTAMP}"
45+
WORK_DIR=$(mktemp -d "/tmp/${ARCHIVE_NAME}.XXXXXX")
46+
SUMMARY="${WORK_DIR}/summary.txt"
47+
OVERALL_EXIT=0
48+
49+
log() { echo "$*" | tee -a "${SUMMARY}"; }
50+
51+
# ── Header ─────────────────────────────────────────────────────────────────────
52+
log "============================================================"
53+
log " Fleet fleetd Health Check"
54+
log " Host: $(hostname)"
55+
log " Date: $(date)"
56+
log " Kernel: $(uname -r)"
57+
log " OS: $(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || echo "unknown")"
58+
log "============================================================"
59+
log ""
60+
61+
# ══════════════════════════════════════════════════════════════════════════════
62+
# 1. SYSTEMD SERVICE
63+
# ══════════════════════════════════════════════════════════════════════════════
64+
log "── 1. systemd service (orbit.service) ──────────────────────"
65+
66+
SERVICE="orbit.service"
67+
if systemctl is-active --quiet "${SERVICE}" 2>/dev/null; then
68+
ok "${SERVICE} is active (running)"
69+
else
70+
fail "${SERVICE} is NOT active"
71+
OVERALL_EXIT=1
72+
fi
73+
74+
if systemctl is-enabled --quiet "${SERVICE}" 2>/dev/null; then
75+
ok "${SERVICE} is enabled"
76+
else
77+
warn "${SERVICE} is not enabled — will not start on boot"
78+
fi
79+
80+
SYSTEMD_STATUS=$(systemctl status "${SERVICE}" --no-pager 2>&1 || true)
81+
echo "${SYSTEMD_STATUS}" >> "${SUMMARY}"
82+
83+
# ══════════════════════════════════════════════════════════════════════════════
84+
# 2. PROCESS CHECKS
85+
# ══════════════════════════════════════════════════════════════════════════════
86+
log ""
87+
log "── 2. Processes ────────────────────────────────────────────"
88+
89+
check_process() {
90+
local label="$1"
91+
local pattern="$2"
92+
local result
93+
result=$(pgrep -af "${pattern}" 2>/dev/null || true)
94+
if [[ -n "${result}" ]]; then
95+
ok "${label} is running"
96+
echo " ${result}" | tee -a "${SUMMARY}"
97+
else
98+
fail "${label} is NOT running (pattern: ${pattern})"
99+
OVERALL_EXIT=1
100+
fi
101+
}
102+
103+
# orbit binary path is /opt/orbit/bin/orbit/orbit
104+
# Source: linux_shared.go ExecStart=/opt/orbit/bin/orbit/orbit
105+
check_process "orbit" "/opt/orbit/bin/orbit/orbit"
106+
107+
# osqueryd is spawned by orbit; match the binary name
108+
check_process "osqueryd" "osqueryd"
109+
110+
# fleet-desktop: only present if package was built with --fleet-desktop
111+
# Source: constant.DesktopAppExecName = "fleet-desktop"
112+
if pgrep -af "fleet-desktop" >/dev/null 2>&1; then
113+
ok "fleet-desktop is running"
114+
pgrep -af "fleet-desktop" | tee -a "${SUMMARY}" | sed 's/^/ /'
115+
else
116+
warn "fleet-desktop is NOT running (expected if not packaged with --fleet-desktop)"
117+
fi
118+
119+
# ══════════════════════════════════════════════════════════════════════════════
120+
# 3. KEY FILES
121+
# ══════════════════════════════════════════════════════════════════════════════
122+
log ""
123+
log "── 3. Key files and directories ────────────────────────────"
124+
125+
check_file() {
126+
local label="$1"
127+
local path="$2"
128+
if [[ -e "${path}" ]]; then
129+
ok "${label}: ${path}"
130+
else
131+
fail "${label} not found: ${path}"
132+
OVERALL_EXIT=1
133+
fi
134+
}
135+
136+
check_file "orbit binary" "/opt/orbit/bin/orbit/orbit"
137+
check_file "orbit symlink" "/usr/local/bin/orbit"
138+
check_file "osquery pidfile" "/opt/orbit/osquery.pid"
139+
check_file "env file" "/etc/default/orbit"
140+
check_file "orbit node key" "/opt/orbit/secret-orbit-node-key.txt"
141+
check_file "enroll secret" "/opt/orbit/secret.txt"
142+
143+
# Report orbit node key presence without printing value
144+
if [[ -s "/opt/orbit/secret-orbit-node-key.txt" ]]; then
145+
ok "orbit node key is non-empty (enrolled)"
146+
else
147+
fail "orbit node key is missing or empty (not enrolled)"
148+
OVERALL_EXIT=1
149+
fi
150+
151+
# ══════════════════════════════════════════════════════════════════════════════
152+
# 4. ENV FILE SUMMARY
153+
# ══════════════════════════════════════════════════════════════════════════════
154+
log ""
155+
log "── 4. Orbit environment (/etc/default/orbit) ───────────────"
156+
if [[ -f /etc/default/orbit ]]; then
157+
# Print contents but redact any secrets
158+
awk 'BEGIN { IGNORECASE=1 } !/secret|password|token|key/' /etc/default/orbit \
159+
| tee -a "${SUMMARY}" \
160+
| sed 's/^/ /'
161+
else
162+
fail "/etc/default/orbit not found"
163+
OVERALL_EXIT=1
164+
fi
165+
166+
# ══════════════════════════════════════════════════════════════════════════════
167+
# 5. ORBIT VERSION
168+
# ══════════════════════════════════════════════════════════════════════════════
169+
log ""
170+
log "── 5. Orbit version ────────────────────────────────────────"
171+
if command -v orbit >/dev/null 2>&1; then
172+
ORBIT_VERSION=$(orbit version 2>/dev/null || echo "unknown")
173+
info "${ORBIT_VERSION}"
174+
echo "${ORBIT_VERSION}" >> "${SUMMARY}"
175+
else
176+
warn "orbit not found on PATH (/usr/local/bin/orbit missing or not in PATH)"
177+
fi
178+
179+
# ══════════════════════════════════════════════════════════════════════════════
180+
# 6. LOG COLLECTION
181+
# ══════════════════════════════════════════════════════════════════════════════
182+
log ""
183+
log "── 6. Log collection ───────────────────────────────────────"
184+
185+
collect_log() {
186+
local label="$1"
187+
local src="$2"
188+
local dest_dir="$3"
189+
if [[ -f "${src}" ]]; then
190+
mkdir -p "${dest_dir}"
191+
cp "${src}" "${dest_dir}/"
192+
ok "Collected ${label}: ${src}"
193+
elif [[ -d "${src}" ]]; then
194+
mkdir -p "${dest_dir}"
195+
cp -r "${src}/." "${dest_dir}/"
196+
ok "Collected ${label} directory: ${src}"
197+
else
198+
warn "${label} not found at ${src} (skipping)"
199+
fi
200+
}
201+
202+
# orbit logs — /var/log/orbit/ created at install time by linux_shared.go
203+
# (usually empty; orbit's stdout/stderr goes to syslog/journal by default — see below)
204+
collect_log "orbit logs" "/var/log/orbit" "${WORK_DIR}/logs/orbit"
205+
206+
# osquery logs — /var/log/osquery/ created at install time by linux_shared.go
207+
# (usually empty; osqueryd's stdout/stderr goes to syslog/journal by default — see below)
208+
collect_log "osquery logs" "/var/log/osquery" "${WORK_DIR}/logs/osquery"
209+
210+
# osquery filesystem logger output — only populated when logger_path/logger_plugin
211+
# is set to "filesystem" in agent options. This is where result/status logs
212+
# (osqueryd.INFO*, osqueryd.results.log, osqueryd.snapshots.log, etc.) actually live.
213+
collect_log "osquery filesystem logger" "/opt/orbit/osquery_log" "${WORK_DIR}/logs/osquery_log"
214+
215+
# systemd journal for orbit.service (last 500 lines)
216+
mkdir -p "${WORK_DIR}/logs"
217+
if command -v journalctl >/dev/null 2>&1; then
218+
journalctl -u orbit.service --no-pager -n 500 \
219+
> "${WORK_DIR}/logs/orbit_journal.log" 2>&1 && \
220+
ok "Collected systemd journal (last 500 lines)" || \
221+
warn "journalctl failed for orbit.service"
222+
fi
223+
224+
# syslog fallback — orbit/osqueryd stderr goes to syslog on Debian/Ubuntu
225+
for syslog_path in /var/log/syslog /var/log/messages; do
226+
if [[ -f "${syslog_path}" ]]; then
227+
grep -i "orbit\|osquery\|fleet" "${syslog_path}" \
228+
> "${WORK_DIR}/logs/syslog_orbit_grep.log" 2>/dev/null || true
229+
ok "Grepped syslog for orbit/osquery/fleet: ${syslog_path}"
230+
break
231+
fi
232+
done
233+
234+
# ══════════════════════════════════════════════════════════════════════════════
235+
# 7. SYSTEM EVENTS (lookback window)
236+
# ══════════════════════════════════════════════════════════════════════════════
237+
# Captures what changed on the box before the user noticed a problem — reboots,
238+
# package installs/upgrades/removals, systemd failures, and journal errors.
239+
# Users rarely remember every change; having this saves a round trip of
240+
# clarifying questions when triaging a report.
241+
log ""
242+
log "── 7. System events, last ${LOOKBACK_HOURS}h ───────────────"
243+
244+
mkdir -p "${WORK_DIR}/logs/system_events"
245+
SINCE_TS=$(date -d "-${LOOKBACK_HOURS} hours" '+%Y-%m-%d %H:%M:%S')
246+
247+
# Reboots/shutdowns
248+
if command -v last >/dev/null 2>&1; then
249+
last -x reboot shutdown -F 2>/dev/null | head -20 \
250+
> "${WORK_DIR}/logs/system_events/reboots.log" || true
251+
ok "Collected reboot/shutdown history"
252+
fi
253+
254+
# Package installs/upgrades/removals (dpkg.log lines are ISO-timestamped,
255+
# so a lexicographic compare against SINCE_TS also orders them chronologically)
256+
if [[ -f /var/log/dpkg.log ]]; then
257+
awk -v since="${SINCE_TS}" '$0 >= since' /var/log/dpkg.log \
258+
> "${WORK_DIR}/logs/system_events/dpkg_recent.log" || true
259+
RECENT_PKG_COUNT=$(grep -cE ' (install|upgrade|remove|purge) ' \
260+
"${WORK_DIR}/logs/system_events/dpkg_recent.log" 2>/dev/null || echo 0)
261+
if [[ "${RECENT_PKG_COUNT}" -gt 0 ]]; then
262+
warn "${RECENT_PKG_COUNT} package install/upgrade/remove event(s) in last ${LOOKBACK_HOURS}h"
263+
else
264+
ok "No package install/upgrade/remove events in last ${LOOKBACK_HOURS}h"
265+
fi
266+
fi
267+
268+
# Currently-failed systemd units (unrelated failures often explain "it stopped working")
269+
FAILED_UNITS=$(systemctl --failed --no-legend 2>/dev/null || true)
270+
echo "${FAILED_UNITS}" > "${WORK_DIR}/logs/system_events/systemd_failed_units.log"
271+
if [[ -n "${FAILED_UNITS}" ]]; then
272+
warn "systemd reports failed units:"
273+
echo "${FAILED_UNITS}" | tee -a "${SUMMARY}" | sed 's/^/ /'
274+
else
275+
ok "No failed systemd units"
276+
fi
277+
278+
# Journal errors (priority err and above) in the window
279+
if command -v journalctl >/dev/null 2>&1; then
280+
journalctl -p 3 --since "${LOOKBACK_HOURS} hours ago" --no-pager \
281+
> "${WORK_DIR}/logs/system_events/journal_errors.log" 2>&1 || true
282+
JOURNAL_ERR_COUNT=$(wc -l < "${WORK_DIR}/logs/system_events/journal_errors.log" 2>/dev/null || echo 0)
283+
if [[ "${JOURNAL_ERR_COUNT}" -gt 0 ]]; then
284+
warn "${JOURNAL_ERR_COUNT} journal error-level line(s) in last ${LOOKBACK_HOURS}h (see journal_errors.log)"
285+
else
286+
ok "No journal errors in last ${LOOKBACK_HOURS}h"
287+
fi
288+
289+
# OOM killer events — a frequent, easily-missed cause of "it just stopped"
290+
OOM_HITS=$(journalctl --since "${LOOKBACK_HOURS} hours ago" --no-pager 2>/dev/null \
291+
| grep -i "out of memory\|oom-killer" || true)
292+
if [[ -n "${OOM_HITS}" ]]; then
293+
warn "OOM killer activity detected in last ${LOOKBACK_HOURS}h"
294+
echo "${OOM_HITS}" > "${WORK_DIR}/logs/system_events/oom_events.log"
295+
fi
296+
fi
297+
298+
# ══════════════════════════════════════════════════════════════════════════════
299+
# 8. PACKAGE THE ARCHIVE
300+
# ══════════════════════════════════════════════════════════════════════════════
301+
log ""
302+
log "── 8. Packaging archive ────────────────────────────────────"
303+
304+
ARCHIVE_PATH="/tmp/${ARCHIVE_NAME}.tar.gz"
305+
tar -czf "${ARCHIVE_PATH}" -C "$(dirname "${WORK_DIR}")" "$(basename "${WORK_DIR}")"
306+
rm -rf "${WORK_DIR}"
307+
308+
info "Archive created: ${ARCHIVE_PATH}"
309+
log ""
310+
311+
# ══════════════════════════════════════════════════════════════════════════════
312+
# FINAL RESULT
313+
# ══════════════════════════════════════════════════════════════════════════════
314+
log "============================================================"
315+
if [[ ${OVERALL_EXIT} -eq 0 ]]; then
316+
log " Result: ALL CHECKS PASSED"
317+
else
318+
log " Result: ONE OR MORE CHECKS FAILED — review summary above"
319+
fi
320+
log " Archive: ${ARCHIVE_PATH}"
321+
log "============================================================"
322+
323+
exit ${OVERALL_EXIT}

0 commit comments

Comments
 (0)