Files
ansible-worker/files/files/connstats.sh
2026-02-06 11:36:43 +02:00

677 lines
23 KiB
Bash

#!/bin/sh
# connstats.sh
# Log-derived Wi-Fi connection counters per run (intended for cron every 10 minutes).
# Writes one summary line per run to /root/connstats.log
# Optional: --debug for chatty troubleshooting output in the same log.
# Optional: --always-find-offset to calibrate syslog header time via a logger marker
# and (on fresh start / rotation / truncation) build a true last-10-min slice.
LOG_SRC="/var/log/messages"
OUT_LOG="/root/connstats.log"
STATE="/tmp/connstats.state"
TMPDIR="/tmp"
# How many tail lines to scan when we have no state (first run / after reboot / rotation)
BOOTSTRAP_BACKFILL_LINES=2000
# Used only with --always-find-offset during bootstrap-like runs
BOOTSTRAP_TIME_WINDOW_SEC=600
BOOTSTRAP_TIME_TAIL_LINES=10000
DEBUG=0
ALWAYS_FIND_OFFSET=0
for arg in "$@"; do
case "$arg" in
--debug) DEBUG=1 ;;
--always-find-offset) ALWAYS_FIND_OFFSET=1 ;;
-h|--help)
echo "Usage: $0 [--debug] [--always-find-offset]"
exit 0
;;
*)
echo "Unknown arg: $arg" >&2
echo "Usage: $0 [--debug] [--always-find-offset]" >&2
exit 2
;;
esac
done
umask 077
ts_iso() {
# ISO-ish timestamp; works on BusyBox and GNU date
date "+%Y-%m-%dT%H:%M:%S%z"
}
log() {
# Always append to OUT_LOG
echo "[$(ts_iso)] $*" >> "$OUT_LOG"
}
# Generate a short random token (8 chars) for marker grep
rand_token() {
if [ -r /dev/urandom ]; then
tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 8
else
echo "$(date -u +%s 2>/dev/null)$$" | tr -dc 'a-z0-9' | tail -c 8
fi
}
# Parse syslog header timestamp (Mon DD HH:MM:SS) into epoch seconds (UTC) using awk mktime()
# Expects typical line like: "<13> Jan 31 12:31:00 root[-] [notice]: message"
syslog_header_to_epoch_utc() {
echo "$1" | TZ=UTC awk '
function mon2num(m) {
if (m=="Jan") return 1
if (m=="Feb") return 2
if (m=="Mar") return 3
if (m=="Apr") return 4
if (m=="May") return 5
if (m=="Jun") return 6
if (m=="Jul") return 7
if (m=="Aug") return 8
if (m=="Sep") return 9
if (m=="Oct") return 10
if (m=="Nov") return 11
if (m=="Dec") return 12
return 0
}
{
mon=$2; day=$3; tod=$4
year=strftime("%Y")
m=mon2num(mon)
if (m==0) { print ""; exit 1 }
split(tod, t, ":")
if (length(t) != 3) { print ""; exit 1 }
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
d=day+0
print mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, d, hh, mm, ss))
}
'
}
# Compute offset_sec = payload_epoch_utc - syslog_header_epoch_utc
# Returns offset seconds on stdout (blank on failure)
find_time_offset_sec() {
token="$(rand_token)"
now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')"
[ -z "$now_epoch" ] && now_epoch=0
logger "connstat calculation started. issueing time marker $token epoch=$now_epoch" 2>/dev/null
found_line=""
i=0
while [ $i -lt 3 ]; do
found_line="$(tail -n 300 "$LOG_SRC" 2>/dev/null | grep "$token" | tail -n 1)"
[ -n "$found_line" ] && break
sleep 1
i=$((i + 1))
done
[ -z "$found_line" ] && { echo ""; return; }
hdr_epoch="$(syslog_header_to_epoch_utc "$found_line" | tr -d ' ')"
[ -z "$hdr_epoch" ] && { echo ""; return; }
payload_epoch="$(echo "$found_line" | awk '{
for (i=1; i<=NF; i++) {
if ($i ~ /^epoch=[0-9]+$/) { sub(/^epoch=/,"",$i); print $i; exit }
}
}' | tr -d ' ')"
[ -z "$payload_epoch" ] && payload_epoch="$now_epoch"
echo $((payload_epoch - hdr_epoch))
}
# Basic sanity checks
if [ ! -r "$LOG_SRC" ]; then
log "ERROR log_src_unreadable path=$LOG_SRC"
exit 1
fi
# Get current inode + line count (line-based incremental read is most portable)
cur_inode="$(ls -i "$LOG_SRC" 2>/dev/null | awk '{print $1}')"
cur_lines="$(wc -l < "$LOG_SRC" 2>/dev/null | tr -d ' ')"
if [ -z "$cur_inode" ] || [ -z "$cur_lines" ]; then
log "ERROR cannot_stat_log inode='$cur_inode' lines='$cur_lines'"
exit 1
fi
last_inode=""
last_line=""
if [ -f "$STATE" ]; then
# STATE format:
# inode=<num>
# line=<num>
last_inode="$(grep '^inode=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)"
last_line="$(grep '^line=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)"
fi
# Decide where to start reading
start_line=""
backfill_start() {
start_line=$((cur_lines - BOOTSTRAP_BACKFILL_LINES + 1))
[ "$start_line" -lt 1 ] && start_line=1
}
# Extract the slice
tmp_slice="$TMPDIR/connstats.slice.$$"
# Grep patterns (hostapd transition events)
re_auth='hostapd.*IEEE 802\.11: authenticated'
re_assoc='hostapd.*IEEE 802\.11: associated'
re_disassoc='hostapd.*IEEE 802\.11: disassociated'
# Kernel extras for "seen_any" breadth (optional but useful)
re_kernel_seen='kernel.*(station kicked out|station timed out)'
# Are we in a bootstrap-like condition (no state OR rotation/truncation)?
bootstrap_like=0
if [ -z "$last_inode" ] || [ -z "$last_line" ]; then
bootstrap_like=1
else
if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then
bootstrap_like=1
fi
fi
# Bootstrap handling
if [ "$ALWAYS_FIND_OFFSET" -eq 1 ] && [ "$bootstrap_like" -eq 1 ]; then
offset_sec="$(find_time_offset_sec)"
if [ -n "$offset_sec" ]; then
now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')"
[ -z "$now_epoch" ] && now_epoch=0
cutoff_epoch=$((now_epoch - BOOTSTRAP_TIME_WINDOW_SEC))
if [ $DEBUG -eq 1 ]; then
log "DEBUG bootstrap_time_window_last_10m offset_sec=$offset_sec cutoff_epoch=$cutoff_epoch tail_lines=$BOOTSTRAP_TIME_TAIL_LINES"
fi
# Build slice = last-10-min relevant lines (hostapd transitions + kernel station kicked/timed out)
# using (syslog_header_epoch + offset_sec) >= cutoff_epoch
tail -n "$BOOTSTRAP_TIME_TAIL_LINES" "$LOG_SRC" 2>/dev/null \
| TZ=UTC awk -v cutoff="$cutoff_epoch" -v off="$offset_sec" '
function mon2num(m) {
if (m=="Jan") return 1
if (m=="Feb") return 2
if (m=="Mar") return 3
if (m=="Apr") return 4
if (m=="May") return 5
if (m=="Jun") return 6
if (m=="Jul") return 7
if (m=="Aug") return 8
if (m=="Sep") return 9
if (m=="Oct") return 10
if (m=="Nov") return 11
if (m=="Dec") return 12
return 0
}
function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) {
mon=$2; day=$3; tod=$4
year=strftime("%Y")
m=mon2num(mon)
if (m==0) return -1
split(tod, t, ":")
if (length(t) != 3) return -1
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss))
}
{
line=$0
if (line ~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/ ||
line ~ /kernel.*(station kicked out|station timed out)/) {
he=header_epoch()
if (he < 0) next
corr=he + off
if (corr >= cutoff) print line
}
}
' > "$tmp_slice" 2>/dev/null
start_line="(time_window_last_10m)"
else
# Offset calc failed -> fall back to old bootstrap behavior
backfill_start
if [ $DEBUG -eq 1 ]; then
log "DEBUG time_offset_failed_fallback_to_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
fi
sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null
fi
else
# Original behavior (unchanged)
if [ -z "$last_inode" ] || [ -z "$last_line" ]; then
backfill_start
if [ $DEBUG -eq 1 ]; then
log "DEBUG bootstrap_no_state_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
fi
else
# Rotation / truncation detection
if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then
backfill_start
if [ $DEBUG -eq 1 ]; then
log "DEBUG bootstrap_rotation_or_truncate_backfill last_inode=$last_inode cur_inode=$cur_inode last_line=$last_line cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
fi
else
start_line=$((last_line + 1))
if [ $DEBUG -eq 1 ]; then
log "DEBUG incremental last_line=$last_line cur_lines=$cur_lines start_line=$start_line"
fi
fi
fi
sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null
fi
slice_lines="$(wc -l < "$tmp_slice" 2>/dev/null | tr -d ' ')"
[ -z "$slice_lines" ] && slice_lines=0
if [ $DEBUG -eq 1 ]; then
log "DEBUG slice_path=$tmp_slice slice_lines=$slice_lines"
fi
# Totals (MAC repeats allowed)
auth_events_total="$(grep -E "$re_auth" "$tmp_slice" | wc -l | tr -d ' ')"
assoc_events_total="$(grep -E "$re_assoc" "$tmp_slice" | wc -l | tr -d ' ')"
disassoc_events_total="$(grep -E "$re_disassoc" "$tmp_slice" | wc -l | tr -d ' ')"
# Extract MAC right after token "STA" (from hostapd lines)
extract_sta_macs() {
awk '
{
for (i=1; i<=NF; i++) {
if ($i=="STA") {
print $(i+1);
break;
}
}
}
'
}
# Extract MACs in brackets: [aa:bb:cc:dd:ee:ff]
extract_bracket_macs() {
grep -oE '\[[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}\]' | tr -d '[]'
}
# Count unique valid MACs from stdin
uniq_count() {
grep -E '^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$' \
| sort -u \
| wc -l | tr -d ' '
}
# Unique MACs per event type (hostapd)
unique_macs_authenticated="$(
grep -E "$re_auth" "$tmp_slice" \
| extract_sta_macs \
| uniq_count
)"
unique_macs_associated="$(
grep -E "$re_assoc" "$tmp_slice" \
| extract_sta_macs \
| uniq_count
)"
unique_macs_disassociated="$(
grep -E "$re_disassoc" "$tmp_slice" \
| extract_sta_macs \
| uniq_count
)"
# Unique MACs seen in "any relevant activity" (union: auth/assoc/disassoc + selected kernel)
unique_macs_seen_any="$(
(
grep -E 'hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)' "$tmp_slice" \
| extract_sta_macs
grep -E "$re_kernel_seen" "$tmp_slice" \
| extract_bracket_macs
) | uniq_count
)"
########################################################################
# B-class metrics (ephemeral state in /tmp; reboot loss accepted)
########################################################################
SESSION_STATE="/tmp/connstats.sessions.state"
SESSION_STATE_TMP="$TMPDIR/connstats.sessions.state.$$"
SESSION_STATE_TTL_SEC=21600
AUTH_ASSOC_MAX_SAMPLES=50
now_epoch_b="$(date -u +%s 2>/dev/null | tr -d ' ')"
[ -z "$now_epoch_b" ] && now_epoch_b=0
# Output file (temporary, removed each run)
B_OUT_TMP="$TMPDIR/connstats.b.out.$$"
TZ=UTC awk -v state_in="$SESSION_STATE" \
-v state_out="$SESSION_STATE_TMP" \
-v now="$now_epoch_b" \
-v ttl="$SESSION_STATE_TTL_SEC" \
-v maxs="$AUTH_ASSOC_MAX_SAMPLES" '
function mon2num(m) {
if (m=="Jan") return 1
if (m=="Feb") return 2
if (m=="Mar") return 3
if (m=="Apr") return 4
if (m=="May") return 5
if (m=="Jun") return 6
if (m=="Jul") return 7
if (m=="Aug") return 8
if (m=="Sep") return 9
if (m=="Oct") return 10
if (m=="Nov") return 11
if (m=="Dec") return 12
return 0
}
function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) {
mon=$2; day=$3; tod=$4
year=strftime("%Y")
m=mon2num(mon)
if (m==0) return -1
split(tod, t, ":")
if (length(t) != 3) return -1
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss))
}
function extract_sta_mac( i) {
for (i=1; i<=NF; i++) {
if ($i=="STA") return $(i+1)
}
return ""
}
function mac_valid(m) {
return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/)
}
BEGIN {
if (state_in != "") {
while ((getline line < state_in) > 0) {
n = split(line, a, " ")
if (n >= 2) {
m=a[1]; e=a[2]+0
if (mac_valid(m) && e > 0) last_auth[m]=e
}
}
close(state_in)
}
}
{
line=$0
if (line !~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/) next
e = header_epoch()
if (e < 0) next
mac = extract_sta_mac()
if (!mac_valid(mac)) next
if (line ~ /IEEE 802\.11: authenticated/) {
last_auth[mac]=e
next
}
if (line ~ /IEEE 802\.11: associated/) {
if (mac in last_auth) {
d = e - last_auth[mac]
if (d >= 0) {
ms = d * 1000
if (sample_count < maxs) {
if (samples == "") samples = ms
else samples = samples "," ms
sample_count++
}
}
}
next
}
if (line ~ /IEEE 802\.11: disassociated/) {
if (mac in last_auth) {
d = e - last_auth[mac]
if (d >= 0) {
full_sessions_total++
sum_len += d
if (d < 60) early_drop_sessions_total++
delete last_auth[mac]
}
}
next
}
}
END {
if (full_sessions_total > 0) avg_session_length = int((sum_len / full_sessions_total) + 0.5)
else avg_session_length = 0
if (state_out != "") {
for (m in last_auth) {
e = last_auth[m] + 0
if (e > 0 && (now <= 0 || (e >= (now - ttl)))) {
print m " " e > state_out
}
}
close(state_out)
}
print full_sessions_total "\t" avg_session_length "\t" early_drop_sessions_total "\t" samples
}
' "$tmp_slice" > "$B_OUT_TMP" 2>/dev/null
# Default B fields
full_sessions_total=0
avg_session_length=0
early_drop_sessions_total=0
auth_to_assoc_time_ms_samples=""
# Parse awk output
if [ -s "$B_OUT_TMP" ]; then
b_out_line="$(head -n 1 "$B_OUT_TMP" 2>/dev/null)"
full_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')"
avg_session_length="$(echo "$b_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')"
early_drop_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')"
auth_to_assoc_time_ms_samples="$(echo "$b_out_line" | awk -F'\t' '{print $4}')"
fi
rm -f "$B_OUT_TMP" 2>/dev/null
[ -z "$full_sessions_total" ] && full_sessions_total=0
[ -z "$avg_session_length" ] && avg_session_length=0
[ -z "$early_drop_sessions_total" ] && early_drop_sessions_total=0
# Move state into place (ephemeral but persistent across runs until reboot)
if [ -f "$SESSION_STATE_TMP" ]; then
mv -f "$SESSION_STATE_TMP" "$SESSION_STATE" 2>/dev/null
else
rm -f "$SESSION_STATE_TMP" 2>/dev/null
fi
########################################################################
# C-class metrics (kernel kick reason codes 1..8)
########################################################################
# Only count kernel "station kicked out ..." (avoids PRS DRIVER_LOG "reason" lines)
re_kick_reason_base='kernel.*station kicked out.*reason[[:space:]]+'
reason_code_events_1="$(grep -E "${re_kick_reason_base}1([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_2="$(grep -E "${re_kick_reason_base}2([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_3="$(grep -E "${re_kick_reason_base}3([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_4="$(grep -E "${re_kick_reason_base}4([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_5="$(grep -E "${re_kick_reason_base}5([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_6="$(grep -E "${re_kick_reason_base}6([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_7="$(grep -E "${re_kick_reason_base}7([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_8="$(grep -E "${re_kick_reason_base}8([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
[ -z "$reason_code_events_1" ] && reason_code_events_1=0
[ -z "$reason_code_events_2" ] && reason_code_events_2=0
[ -z "$reason_code_events_3" ] && reason_code_events_3=0
[ -z "$reason_code_events_4" ] && reason_code_events_4=0
[ -z "$reason_code_events_5" ] && reason_code_events_5=0
[ -z "$reason_code_events_6" ] && reason_code_events_6=0
[ -z "$reason_code_events_7" ] && reason_code_events_7=0
[ -z "$reason_code_events_8" ] && reason_code_events_8=0
########################################################################
# D-class metrics (flaps between ath0 and ath1 within the bucket)
########################################################################
D_OUT_TMP="$TMPDIR/connstats.d.out.$$"
awk '
function extract_sta_mac( i) {
for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1)
return ""
}
function mac_valid(m) {
return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/)
}
function extract_iface( i, v) {
for (i=1; i<=NF; i++) {
v = $i
if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v }
}
return ""
}
{
line=$0
if (line !~ /hostapd.*IEEE 802\.11: associated/) next
iface = extract_iface()
if (iface != "ath0" && iface != "ath1") next
mac = extract_sta_mac()
if (!mac_valid(mac)) next
if (mac in last_iface) {
prev = last_iface[mac]
if (prev != iface) {
if (prev=="ath0" && iface=="ath1") {
flap_events_0_to_1++
uniq_0_to_1[mac]=1
} else if (prev=="ath1" && iface=="ath0") {
flap_events_1_to_0++
uniq_1_to_0[mac]=1
}
}
}
last_iface[mac]=iface
}
END {
for (m in uniq_0_to_1) unique_0_to_1++
for (m in uniq_1_to_0) unique_1_to_0++
if (unique_0_to_1+0 < 0) unique_0_to_1=0
if (flap_events_0_to_1+0 < 0) flap_events_0_to_1=0
if (unique_1_to_0+0 < 0) unique_1_to_0=0
if (flap_events_1_to_0+0 < 0) flap_events_1_to_0=0
print unique_0_to_1 "\t" flap_events_0_to_1 "\t" unique_1_to_0 "\t" flap_events_1_to_0
}
' "$tmp_slice" > "$D_OUT_TMP" 2>/dev/null
unique_macs_flap_ath0_to_ath1=0
flap_events_total_ath0_to_ath1=0
unique_macs_flap_ath1_to_ath0=0
flap_events_total_ath1_to_ath0=0
if [ -s "$D_OUT_TMP" ]; then
d_out_line="$(head -n 1 "$D_OUT_TMP" 2>/dev/null)"
unique_macs_flap_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')"
flap_events_total_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')"
unique_macs_flap_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')"
flap_events_total_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $4}' | tr -d ' ')"
fi
rm -f "$D_OUT_TMP" 2>/dev/null
[ -z "$unique_macs_flap_ath0_to_ath1" ] && unique_macs_flap_ath0_to_ath1=0
[ -z "$flap_events_total_ath0_to_ath1" ] && flap_events_total_ath0_to_ath1=0
[ -z "$unique_macs_flap_ath1_to_ath0" ] && unique_macs_flap_ath1_to_ath0=0
[ -z "$flap_events_total_ath1_to_ath0" ] && flap_events_total_ath1_to_ath0=0
########################################################################
# Emit one summary line
########################################################################
log "connstats bucket_run slice_lines=$slice_lines unique_macs_seen_any=$unique_macs_seen_any unique_macs_authenticated=$unique_macs_authenticated auth_events_total=$auth_events_total unique_macs_associated=$unique_macs_associated assoc_events_total=$assoc_events_total unique_macs_disassociated=$unique_macs_disassociated disassoc_events_total=$disassoc_events_total full_sessions_total=$full_sessions_total avg_session_length=$avg_session_length early_drop_sessions_total=$early_drop_sessions_total auth_to_assoc_time_ms_samples=$auth_to_assoc_time_ms_samples reason_code_events_1=$reason_code_events_1 reason_code_events_2=$reason_code_events_2 reason_code_events_3=$reason_code_events_3 reason_code_events_4=$reason_code_events_4 reason_code_events_5=$reason_code_events_5 reason_code_events_6=$reason_code_events_6 reason_code_events_7=$reason_code_events_7 reason_code_events_8=$reason_code_events_8 unique_macs_flap_ath0_to_ath1=$unique_macs_flap_ath0_to_ath1 flap_events_total_ath0_to_ath1=$flap_events_total_ath0_to_ath1 unique_macs_flap_ath1_to_ath0=$unique_macs_flap_ath1_to_ath0 flap_events_total_ath1_to_ath0=$flap_events_total_ath1_to_ath0"
if [ $DEBUG -eq 1 ]; then
log "DEBUG dump_auth_lines_begin"
grep -E "$re_auth" "$tmp_slice" | sed 's/^/DEBUG AUTH: /' >> "$OUT_LOG"
log "DEBUG dump_assoc_lines_begin"
grep -E "$re_assoc" "$tmp_slice" | sed 's/^/DEBUG ASSOC: /' >> "$OUT_LOG"
log "DEBUG dump_disassoc_lines_begin"
grep -E "$re_disassoc" "$tmp_slice" | sed 's/^/DEBUG DISASSOC: /' >> "$OUT_LOG"
log "DEBUG dump_kick_reason_lines_begin"
grep -E "${re_kick_reason_base}[1-8]([^0-9]|$)" "$tmp_slice" | sed 's/^/DEBUG KICK: /' >> "$OUT_LOG"
log "DEBUG dump_flap_assoc_lines_begin"
grep -E 'hostapd.*IEEE 802\.11: associated' "$tmp_slice" | sed 's/^/DEBUG FLAP_ASSOC: /' >> "$OUT_LOG"
log "DEBUG dump_flap_events_begin"
awk '
function extract_sta_mac( i) { for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1); return "" }
function mac_valid(m) { return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/) }
function extract_iface( i, v) {
for (i=1; i<=NF; i++) { v=$i; if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v } }
return ""
}
{
if ($0 !~ /hostapd.*IEEE 802\.11: associated/) next
iface=extract_iface()
if (iface!="ath0" && iface!="ath1") next
mac=extract_sta_mac()
if (!mac_valid(mac)) next
if (mac in last_iface) {
prev=last_iface[mac]
if (prev!=iface) {
if ((prev=="ath0" && iface=="ath1") || (prev=="ath1" && iface=="ath0")) {
print "mac=" mac " from=" prev " to=" iface
}
}
}
last_iface[mac]=iface
}
' "$tmp_slice" | sed 's/^/DEBUG FLAP: /' >> "$OUT_LOG"
fi
# ---------------------------------------------------------------------
# Send one syslog message (wifidebug-style tag), after random delay
# ---------------------------------------------------------------------
# Hostname (as in wifidebug)
HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null)"
[ -z "$HOSTNAME" ] && HOSTNAME="$(hostname 2>/dev/null)"
[ -z "$HOSTNAME" ] && HOSTNAME="unknown"
# Random delay 3..20 seconds (right before sending)
delay="$(hexdump -n2 -e '/2 "%u"' /dev/urandom 2>/dev/null)"
case "$delay" in (''|*[!0-9]*) delay=0;; esac
delay=$(( (delay % 18) + 3 ))
sleep "$delay"
# Exact stats payload (same fields as file line, no extra prefixes)
MSG="slice_lines=$slice_lines unique_macs_seen_any=$unique_macs_seen_any unique_macs_authenticated=$unique_macs_authenticated auth_events_total=$auth_events_total unique_macs_associated=$unique_macs_associated assoc_events_total=$assoc_events_total unique_macs_disassociated=$unique_macs_disassociated disassoc_events_total=$disassoc_events_total full_sessions_total=$full_sessions_total avg_session_length=$avg_session_length early_drop_sessions_total=$early_drop_sessions_total auth_to_assoc_time_ms_samples=$auth_to_assoc_time_ms_samples reason_code_events_1=$reason_code_events_1 reason_code_events_2=$reason_code_events_2 reason_code_events_3=$reason_code_events_3 reason_code_events_4=$reason_code_events_4 reason_code_events_5=$reason_code_events_5 reason_code_events_6=$reason_code_events_6 reason_code_events_7=$reason_code_events_7 reason_code_events_8=$reason_code_events_8 unique_macs_flap_ath0_to_ath1=$unique_macs_flap_ath0_to_ath1 flap_events_total_ath0_to_ath1=$flap_events_total_ath0_to_ath1 unique_macs_flap_ath1_to_ath0=$unique_macs_flap_ath1_to_ath0 flap_events_total_ath1_to_ath0=$flap_events_total_ath1_to_ath0"
logger -t "connstats|${HOSTNAME}" "$MSG" 2>/dev/null
# Update state to current end-of-file line count and inode
{
echo "inode=$cur_inode"
echo "line=$cur_lines"
} > "$STATE"
rm -f "$tmp_slice" 2>/dev/null
exit 0