From 3c3f8704275e2f941bfd60bef1be39b4adc6823f Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Tue, 26 May 2026 01:34:48 +0000 Subject: [PATCH] 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 --- commands/logs.command.sh | 30 ++-- core/context.sh | 1 + core/json.sh | 2 +- core/json_helper.py | 7 +- core/lib/__pycache__/events.cpython-311.pyc | Bin 31056 -> 33035 bytes core/lib/events.py | 154 +++++++++++++------- daemon/wgctl-monitor.py | 10 +- modules/ui/logs.module.sh | 27 +++- 8 files changed, 159 insertions(+), 72 deletions(-) diff --git a/commands/logs.command.sh b/commands/logs.command.sh index 99b1203..de5762d 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -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" } diff --git a/core/context.sh b/core/context.sh index bbeada5..bd56cb1 100644 --- a/core/context.sh +++ b/core/context.sh @@ -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 diff --git a/core/json.sh b/core/json.sh index 80401cb..afd0ef4 100644 --- a/core/json.sh +++ b/core/json.sh @@ -13,7 +13,7 @@ function json::filter_values() { python3 "$JSON_HELPER" filter_values function json::last_event() { python3 "$JSON_HELPER" last_event "$@" 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]), diff --git a/core/lib/__pycache__/events.cpython-311.pyc b/core/lib/__pycache__/events.cpython-311.pyc index be3ab5604abcc306005425bebb6ba2dfa5fc18ca..0152321633c6cb850ac2c628c73422baacf026d8 100644 GIT binary patch delta 10203 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 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) # ────────────────────────────────────────── diff --git a/daemon/wgctl-monitor.py b/daemon/wgctl-monitor.py index 9fbb342..a4e3236 100755 --- a/daemon/wgctl-monitor.py +++ b/daemon/wgctl-monitor.py @@ -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(), diff --git a/modules/ui/logs.module.sh b/modules/ui/logs.module.sh index c028422..f8dddd4 100644 --- a/modules/ui/logs.module.sh +++ b/modules/ui/logs.module.sh @@ -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() {