feat: logs descending sort, gap/offline indicator, endpoint resolution
- wg_events: sort_order param (desc default), --ascending/--descending flags - wg_events: endpoint cache fallback via _endpoint() helper - wg_events: gap computed ascending always, then sliced/reversed correctly - fw_events: sort_order param, descending default - ui::logs::wg_row: gap suffix with 'offline' label when gap > threshold - logs.command.sh: --ascending/--descending flags, pass sort_order to both functions - daemon: endpoint cache fallback in poll_handshakes - json.sh: json::wg_events passes ctx::endpoint_cache as arg
This commit is contained in:
parent
cf71e9f51a
commit
3c3f870427
8 changed files with 159 additions and 72 deletions
|
|
@ -20,6 +20,8 @@ function cmd::logs::on_load() {
|
|||
flag::register --detailed
|
||||
flag::register --service
|
||||
flag::register --event
|
||||
flag::register --ascending
|
||||
flag::register --descending
|
||||
}
|
||||
|
||||
function cmd::logs::help() {
|
||||
|
|
@ -104,6 +106,7 @@ function cmd::logs::show() {
|
|||
local fw_only=false wg_only=false follow=false merged=false
|
||||
local raw=false detailed=false
|
||||
local filter_service="" filter_event=""
|
||||
local sort_order="desc"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -181,11 +184,11 @@ function cmd::logs::show() {
|
|||
|
||||
$wg_only || fw_output=$(cmd::logs::show_fw_events \
|
||||
"$filter_ip" "$name" "$type" "$limit" "$net_file" \
|
||||
"$collapse" "$since" "$filter_dest_ip" "$filter_dest_port")
|
||||
"$collapse" "$since" "$filter_dest_ip" "$filter_dest_port" "$sort_order")
|
||||
|
||||
$fw_only || wg_output=$(cmd::logs::show_wg_events \
|
||||
"$filter_ip" "$name" "$type" "$limit" \
|
||||
"$collapse" "$since" "$filter_event")
|
||||
"$collapse" "$since" "$filter_event" "$sort_order")
|
||||
|
||||
if [[ -z "$(echo "$fw_output" | tr -d '[:space:]')" && \
|
||||
-z "$(echo "$wg_output" | tr -d '[:space:]')" ]]; then
|
||||
|
|
@ -209,16 +212,18 @@ function cmd::logs::show() {
|
|||
function cmd::logs::show_fw_events() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" net_file="${5:-}" collapse="${6:-1}" \
|
||||
since="${7:-}" filter_dest_ip="${8:-}" filter_dest_port="${9:-}"
|
||||
|
||||
since="${7:-}" filter_dest_ip="${8:-}" filter_dest_port="${9:-}" \
|
||||
sort_order="${10:-desc}"
|
||||
|
||||
[[ ! -f "$FW_EVENTS_LOG" ]] && return 0
|
||||
|
||||
|
||||
local data
|
||||
data=$(json::fw_events \
|
||||
"$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
||||
"$(ctx::clients)" "${net_file:-}" \
|
||||
"$limit" "$collapse" "$since" \
|
||||
"$filter_dest_ip" "$filter_dest_port" \
|
||||
"$sort_order" \
|
||||
2>/dev/null)
|
||||
|
||||
[[ -z "$data" ]] && return 0
|
||||
|
|
@ -254,14 +259,15 @@ function cmd::logs::show_fw_events() {
|
|||
function cmd::logs::show_wg_events() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" collapse="${5:-1}" \
|
||||
since="${6:-}" filter_event="${7:-}"
|
||||
|
||||
since="${6:-}" filter_event="${7:-}" sort_order="${8:-desc}"
|
||||
|
||||
[[ ! -f "$WG_EVENTS_LOG" ]] && return 0
|
||||
|
||||
|
||||
local data
|
||||
data=$(json::wg_events \
|
||||
"$WG_EVENTS_LOG" "$filter_name" "$filter_type" \
|
||||
"$limit" "$collapse" "$since" "$filter_event" \
|
||||
"$(ctx::endpoint_cache)" "$sort_order" \
|
||||
2>/dev/null)
|
||||
|
||||
[[ -z "$data" ]] && return 0
|
||||
|
|
@ -269,12 +275,12 @@ function cmd::logs::show_wg_events() {
|
|||
# Resolve endpoints and measure column widths
|
||||
local w_client=16 w_endpoint=16
|
||||
local resolved_data=""
|
||||
while IFS='|' read -r ts client endpoint event count; do
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
local endpoint_display
|
||||
endpoint_display=$(resolve::ip "$endpoint")
|
||||
[[ -z "$endpoint_display" ]] && endpoint_display="$endpoint"
|
||||
resolved_data+="${ts}|${client}|${endpoint_display}|${event}|${count}"$'\n'
|
||||
resolved_data+="${ts}|${client}|${endpoint_display}|${event}|${count}|${gap_seconds}"$'\n'
|
||||
(( ${#client} > w_client )) && w_client=${#client}
|
||||
(( ${#endpoint_display} > w_endpoint )) && w_endpoint=${#endpoint_display}
|
||||
done <<< "$data"
|
||||
|
|
@ -282,10 +288,10 @@ function cmd::logs::show_wg_events() {
|
|||
(( w_endpoint += 2 ))
|
||||
|
||||
ui::logs::wg_section_header
|
||||
while IFS='|' read -r ts client endpoint event count; do
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \
|
||||
"$count" "$w_client" "$w_endpoint"
|
||||
"$count" "$w_client" "$w_endpoint" "$gap_seconds"
|
||||
done <<< "$resolved_data"
|
||||
printf "\n"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ function ctx::events_log() { echo "$(ctx::daemon)/events.log"; }
|
|||
function ctx::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; }
|
||||
function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
||||
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
||||
function ctx::endpoint_cache() { echo "${_CTX_DAEMON}/endpoint_cache.json"; }
|
||||
|
||||
# ============================================
|
||||
# Path Helpers
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function json::filter_values() { python3 "$JSON_HELPER" filter_values
|
|||
function json::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; }
|
||||
function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </dev/null; }
|
||||
function json::fw_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME_SHORT" python3 "$JSON_HELPER" fw_events "$@" </dev/null; }
|
||||
function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME_SHORT" python3 "$JSON_HELPER" wg_events "$@" </dev/null; }
|
||||
function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME_SHORT" python3 "$JSON_HELPER" wg_events "$@" "$(ctx::endpoint_cache)" </dev/null; }
|
||||
function json::format_fw_event() { echo "$1" | python3 "$JSON_HELPER" format_fw_event "$2"; }
|
||||
function json::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
|
||||
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
|
||||
|
|
|
|||
|
|
@ -1689,13 +1689,16 @@ commands = {
|
|||
args[6] if len(args) > 6 else '1',
|
||||
args[7] if len(args) > 7 else '',
|
||||
args[8] if len(args) > 8 else '',
|
||||
args[9] if len(args) > 9 else ''),
|
||||
args[9] if len(args) > 9 else '',
|
||||
args[10] if len(args) > 10 else 'desc'),
|
||||
'wg_events': lambda args: __import__('lib.events', fromlist=['wg_events']).wg_events(
|
||||
args[0], args[1], args[2],
|
||||
args[3] if len(args) > 3 else '50',
|
||||
args[4] if len(args) > 4 else '1',
|
||||
args[5] if len(args) > 5 else '',
|
||||
args[6] if len(args) > 6 else ''),
|
||||
args[6] if len(args) > 6 else '',
|
||||
args[7] if len(args) > 7 else '',
|
||||
args[8] if len(args) > 8 else 'desc'),
|
||||
'parse_event': lambda args: __import__('lib.events', fromlist=['parse_event']).parse_event(args[0]),
|
||||
'parse_fw_event': lambda args: __import__('lib.events', fromlist=['parse_fw_event']).parse_fw_event(args[0]),
|
||||
'format_fw_event': lambda args: __import__('lib.events', fromlist=['format_fw_event']).format_fw_event(args[0], args[1]),
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -22,7 +22,7 @@ from lib.util import (
|
|||
# ──────────────────────────────────────────
|
||||
|
||||
def fw_events(file, filter_ip, filter_type, clients_dir, net_file,
|
||||
limit, collapse='1', since='', filter_dest_ip='', filter_dest_port=''):
|
||||
limit, collapse='1', since='', filter_dest_ip='', filter_dest_port='', sort_order='desc'):
|
||||
"""
|
||||
Format firewall drop events with dedup, counts, and service annotation.
|
||||
|
||||
|
|
@ -124,7 +124,10 @@ def fw_events(file, filter_ip, filter_type, clients_dir, net_file,
|
|||
continue
|
||||
|
||||
sorted_buckets = sorted(hourly_ts.items(), key=lambda x: x[1])
|
||||
for hour_key, dt in sorted_buckets[-limit:]:
|
||||
output = sorted_buckets[-limit:]
|
||||
if sort_order != 'asc':
|
||||
output = list(reversed(output))
|
||||
for hour_key, dt in output:
|
||||
client, dst, port, proto, svc_name, _ = hour_key
|
||||
count = hourly[hour_key]
|
||||
ts_fmt = fmt_ts_hour(dt.isoformat())
|
||||
|
|
@ -159,7 +162,10 @@ def fw_events(file, filter_ip, filter_type, clients_dir, net_file,
|
|||
deduped.append(e)
|
||||
counts.append(1)
|
||||
|
||||
for e, count in list(zip(deduped, counts))[-limit:]:
|
||||
pairs = list(zip(deduped, counts))[-limit:]
|
||||
if sort_order != 'asc':
|
||||
pairs = list(reversed(pairs))
|
||||
for e, count in pairs:
|
||||
src = e.get('src_ip', '')
|
||||
dst = e.get('dest_ip', '')
|
||||
port = str(e.get('dest_port', ''))
|
||||
|
|
@ -176,21 +182,28 @@ def fw_events(file, filter_ip, filter_type, clients_dir, net_file,
|
|||
# ──────────────────────────────────────────
|
||||
|
||||
def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
||||
since='', filter_event=''):
|
||||
since='', filter_event='', endpoint_cache_file='', sort_order='desc'):
|
||||
"""
|
||||
Format WireGuard events with dedup and counts.
|
||||
|
||||
collapse='1' (default): hourly aggregation for attempt events
|
||||
collapse='0': show all deduplicated events (--detailed mode)
|
||||
since: relative or absolute time string
|
||||
filter_event: 'attempt' | 'handshake' | '' (all)
|
||||
|
||||
Output per line: ts|client|endpoint|event|count
|
||||
Format WireGuard events with dedup, counts, gap and endpoint resolution.
|
||||
sort_order: 'desc' (default, newest first) | 'asc' (oldest first)
|
||||
Output per line: ts|client|endpoint|event|count|gap_seconds
|
||||
"""
|
||||
do_collapse = str(collapse) != '0'
|
||||
limit = int(limit) if limit else 50
|
||||
since_dt = parse_since(since) if since else None
|
||||
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
do_collapse = str(collapse) != '0'
|
||||
limit = int(limit) if limit else 50
|
||||
since_dt = parse_since(since) if since else None
|
||||
descending = sort_order != 'asc'
|
||||
|
||||
# Load endpoint cache once
|
||||
endpoint_cache = {}
|
||||
if endpoint_cache_file and os.path.exists(endpoint_cache_file):
|
||||
try:
|
||||
with open(endpoint_cache_file) as f:
|
||||
endpoint_cache = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
events = []
|
||||
try:
|
||||
with open(file) as f:
|
||||
|
|
@ -206,43 +219,45 @@ def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
|||
continue
|
||||
if filter_event and e.get('event', '') != filter_event:
|
||||
continue
|
||||
|
||||
if since_dt:
|
||||
ts_str = e.get('timestamp', '')
|
||||
try:
|
||||
from datetime import timezone
|
||||
ev_dt = datetime.fromisoformat(ts_str)
|
||||
if ev_dt.tzinfo is None:
|
||||
from datetime import timezone
|
||||
ev_dt = ev_dt.replace(tzinfo=timezone.utc)
|
||||
if ev_dt < since_dt:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
events.append(e)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _endpoint(e):
|
||||
ep = e.get('endpoint', '')
|
||||
return ep or endpoint_cache.get(e.get('client', ''), '')
|
||||
|
||||
if do_collapse:
|
||||
hourly_attempts = defaultdict(int)
|
||||
hourly_ts = {}
|
||||
handshakes = []
|
||||
handshake_counts = []
|
||||
|
||||
|
||||
for e in events:
|
||||
ts_str = e.get('timestamp', '')
|
||||
client = e.get('client', '')
|
||||
endpoint = e.get('endpoint', '')
|
||||
endpoint = _endpoint(e)
|
||||
event = e.get('event', '')
|
||||
ts = ts_to_unix(ts_str)
|
||||
|
||||
|
||||
try:
|
||||
dt = datetime.fromisoformat(ts_str)
|
||||
except Exception:
|
||||
dt = None
|
||||
|
||||
|
||||
if event == 'attempt':
|
||||
if dt:
|
||||
hour_key = (client, endpoint, event,
|
||||
|
|
@ -255,70 +270,105 @@ def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
|||
if handshakes:
|
||||
prev = handshakes[-1]
|
||||
prev_ts = ts_to_unix(prev.get('timestamp', ''))
|
||||
prev_key = (
|
||||
prev.get('client', ''),
|
||||
prev.get('event', ''),
|
||||
prev.get('endpoint', '')[:15]
|
||||
)
|
||||
prev_key = (prev.get('client', ''), prev.get('event', ''),
|
||||
_endpoint(prev)[:15])
|
||||
if key == prev_key and (ts - prev_ts) < 300:
|
||||
handshake_counts[-1] += 1
|
||||
continue
|
||||
handshakes.append(e)
|
||||
handshake_counts.append(1)
|
||||
|
||||
|
||||
# Build output list — always ascending first for correct gap calculation
|
||||
output = []
|
||||
|
||||
for hour_key, dt in hourly_ts.items():
|
||||
client, endpoint, event, _ = hour_key
|
||||
count = hourly_attempts[hour_key]
|
||||
ts_fmt = fmt_ts_hour(dt.isoformat())
|
||||
output.append((dt.timestamp(), f"{ts_fmt}|{client}|{endpoint}|{event}|{count}"))
|
||||
|
||||
output.append((dt.timestamp(),
|
||||
f"{ts_fmt}|{client}|{endpoint}|{event}|{count}|"))
|
||||
|
||||
# Compute gaps ascending
|
||||
last_handshake_ts = {}
|
||||
hs_output = []
|
||||
for e, count in zip(handshakes, handshake_counts):
|
||||
ts_str = e.get('timestamp', '')
|
||||
client = e.get('client', '')
|
||||
endpoint = e.get('endpoint', '')
|
||||
endpoint = _endpoint(e)
|
||||
event = e.get('event', '')
|
||||
ts = ts_to_unix(ts_str)
|
||||
ts_fmt = fmt_ts(ts_str)
|
||||
output.append((ts, f"{ts_fmt}|{client}|{endpoint}|{event}|{count}"))
|
||||
|
||||
|
||||
gap_seconds = ''
|
||||
if event == 'handshake':
|
||||
prev_ts = last_handshake_ts.get(client, 0)
|
||||
if prev_ts > 0:
|
||||
gap = int(ts - prev_ts)
|
||||
if gap > 0:
|
||||
gap_seconds = str(gap)
|
||||
last_handshake_ts[client] = ts
|
||||
|
||||
hs_output.append((ts, f"{ts_fmt}|{client}|{endpoint}|{event}|{count}|{gap_seconds}"))
|
||||
|
||||
output.extend(hs_output)
|
||||
# Sort ascending first to get correct limit slice, then reverse if needed
|
||||
output.sort(key=lambda x: x[0])
|
||||
for _, line in output[-limit:]:
|
||||
output = output[-limit:]
|
||||
if descending:
|
||||
output.reverse()
|
||||
for _, line in output:
|
||||
print(line)
|
||||
|
||||
|
||||
else:
|
||||
deduped = []
|
||||
counts = []
|
||||
|
||||
for e in events:
|
||||
client = e.get('client', '')
|
||||
event = e.get('event', '')
|
||||
endpoint = e.get('endpoint', '')
|
||||
endpoint = _endpoint(e)
|
||||
key = (client, event, endpoint[:15])
|
||||
ts = ts_to_unix(e.get('timestamp', ''))
|
||||
|
||||
|
||||
if deduped:
|
||||
prev = deduped[-1]
|
||||
prev_ts = ts_to_unix(prev.get('timestamp', ''))
|
||||
prev_key = (
|
||||
prev.get('client', ''),
|
||||
prev.get('event', ''),
|
||||
prev.get('endpoint', '')[:15]
|
||||
)
|
||||
prev_key = (prev.get('client', ''), prev.get('event', ''),
|
||||
_endpoint(prev)[:15])
|
||||
if key == prev_key and (ts - prev_ts) < 300:
|
||||
counts[-1] += 1
|
||||
continue
|
||||
|
||||
|
||||
deduped.append(e)
|
||||
counts.append(1)
|
||||
|
||||
for e, count in list(zip(deduped, counts))[-limit:]:
|
||||
ts_fmt = fmt_ts(e.get('timestamp', ''))
|
||||
|
||||
# Compute gaps ascending, then slice, then reverse if needed
|
||||
last_handshake_ts = {}
|
||||
result = []
|
||||
for e, count in zip(deduped, counts):
|
||||
ts_str = e.get('timestamp', '')
|
||||
client = e.get('client', '')
|
||||
endpoint = e.get('endpoint', '')
|
||||
endpoint = _endpoint(e)
|
||||
event = e.get('event', '')
|
||||
print(f"{ts_fmt}|{client}|{endpoint}|{event}|{count}")
|
||||
|
||||
|
||||
ts = ts_to_unix(ts_str)
|
||||
ts_fmt = fmt_ts(ts_str)
|
||||
|
||||
gap_seconds = ''
|
||||
if event == 'handshake':
|
||||
prev_ts = last_handshake_ts.get(client, 0)
|
||||
if prev_ts > 0:
|
||||
gap = int(ts - prev_ts)
|
||||
if gap > 0:
|
||||
gap_seconds = str(gap)
|
||||
last_handshake_ts[client] = ts
|
||||
|
||||
result.append((ts, f"{ts_fmt}|{client}|{endpoint}|{event}|{count}|{gap_seconds}"))
|
||||
|
||||
result = result[-limit:]
|
||||
if descending:
|
||||
result.reverse()
|
||||
for _, line in result:
|
||||
print(line)
|
||||
# ──────────────────────────────────────────
|
||||
# Single event parsers (used by watch)
|
||||
# ──────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -208,7 +208,15 @@ def poll_handshakes():
|
|||
|
||||
# Get endpoint
|
||||
endpoint = get_endpoint(pubkey) or ''
|
||||
|
||||
|
||||
if not endpoint:
|
||||
try:
|
||||
cache = json.loads(ENDPOINT_CACHE_FILE.read_text())
|
||||
endpoint = cache.get(client, '')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# New session, log it
|
||||
entry = {
|
||||
"timestamp": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
|
||||
|
|
|
|||
|
|
@ -56,23 +56,42 @@ function ui::logs::fw_row_table() {
|
|||
}
|
||||
|
||||
function ui::logs::wg_row() {
|
||||
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" count="${5:-1}" \
|
||||
w_client="${6:-20}" w_endpoint="${7:-20}"
|
||||
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
|
||||
count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \
|
||||
gap_seconds="${8:-}"
|
||||
|
||||
local event_color
|
||||
case "$event" in
|
||||
handshake) event_color="\033[1;32m" ;;
|
||||
attempt) event_color="\033[1;31m" ;;
|
||||
*) event_color="\033[0;37m" ;;
|
||||
esac
|
||||
|
||||
local count_suffix=""
|
||||
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m"
|
||||
|
||||
# Gap suffix — only for handshakes with a meaningful gap
|
||||
local gap_suffix=""
|
||||
if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then
|
||||
local gap_int="$gap_seconds"
|
||||
local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}"
|
||||
local offline_label=""
|
||||
[[ "$gap_int" -gt "$threshold" ]] && offline_label=" offline"
|
||||
if (( gap_int >= 3600 )); then
|
||||
gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m"
|
||||
elif (( gap_int >= 60 )); then
|
||||
gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m"
|
||||
fi
|
||||
fi
|
||||
|
||||
local client_pad endpoint_pad_n
|
||||
client_pad=$(printf "%-${w_client}s" "$client")
|
||||
endpoint_pad_n=$(( w_endpoint - ${#endpoint} ))
|
||||
[[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0
|
||||
printf " %s %s %s%*s %b%s\033[0m%b\n" \
|
||||
|
||||
printf " %s %s %s%*s %b%s\033[0m%b%b\n" \
|
||||
"$ts" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \
|
||||
"$event_color" "$event" "$count_suffix"
|
||||
"$event_color" "$event" "$count_suffix" "$gap_suffix"
|
||||
}
|
||||
|
||||
function ui::logs::wg_row_table() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue