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 --detailed
|
||||||
flag::register --service
|
flag::register --service
|
||||||
flag::register --event
|
flag::register --event
|
||||||
|
flag::register --ascending
|
||||||
|
flag::register --descending
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::help() {
|
function cmd::logs::help() {
|
||||||
|
|
@ -104,6 +106,7 @@ function cmd::logs::show() {
|
||||||
local fw_only=false wg_only=false follow=false merged=false
|
local fw_only=false wg_only=false follow=false merged=false
|
||||||
local raw=false detailed=false
|
local raw=false detailed=false
|
||||||
local filter_service="" filter_event=""
|
local filter_service="" filter_event=""
|
||||||
|
local sort_order="desc"
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -181,11 +184,11 @@ function cmd::logs::show() {
|
||||||
|
|
||||||
$wg_only || fw_output=$(cmd::logs::show_fw_events \
|
$wg_only || fw_output=$(cmd::logs::show_fw_events \
|
||||||
"$filter_ip" "$name" "$type" "$limit" "$net_file" \
|
"$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 \
|
$fw_only || wg_output=$(cmd::logs::show_wg_events \
|
||||||
"$filter_ip" "$name" "$type" "$limit" \
|
"$filter_ip" "$name" "$type" "$limit" \
|
||||||
"$collapse" "$since" "$filter_event")
|
"$collapse" "$since" "$filter_event" "$sort_order")
|
||||||
|
|
||||||
if [[ -z "$(echo "$fw_output" | tr -d '[:space:]')" && \
|
if [[ -z "$(echo "$fw_output" | tr -d '[:space:]')" && \
|
||||||
-z "$(echo "$wg_output" | tr -d '[:space:]')" ]]; then
|
-z "$(echo "$wg_output" | tr -d '[:space:]')" ]]; then
|
||||||
|
|
@ -209,7 +212,8 @@ function cmd::logs::show() {
|
||||||
function cmd::logs::show_fw_events() {
|
function cmd::logs::show_fw_events() {
|
||||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||||
limit="${4:-50}" net_file="${5:-}" collapse="${6:-1}" \
|
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
|
[[ ! -f "$FW_EVENTS_LOG" ]] && return 0
|
||||||
|
|
||||||
|
|
@ -219,6 +223,7 @@ function cmd::logs::show_fw_events() {
|
||||||
"$(ctx::clients)" "${net_file:-}" \
|
"$(ctx::clients)" "${net_file:-}" \
|
||||||
"$limit" "$collapse" "$since" \
|
"$limit" "$collapse" "$since" \
|
||||||
"$filter_dest_ip" "$filter_dest_port" \
|
"$filter_dest_ip" "$filter_dest_port" \
|
||||||
|
"$sort_order" \
|
||||||
2>/dev/null)
|
2>/dev/null)
|
||||||
|
|
||||||
[[ -z "$data" ]] && return 0
|
[[ -z "$data" ]] && return 0
|
||||||
|
|
@ -254,7 +259,7 @@ function cmd::logs::show_fw_events() {
|
||||||
function cmd::logs::show_wg_events() {
|
function cmd::logs::show_wg_events() {
|
||||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||||
limit="${4:-50}" collapse="${5:-1}" \
|
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
|
[[ ! -f "$WG_EVENTS_LOG" ]] && return 0
|
||||||
|
|
||||||
|
|
@ -262,6 +267,7 @@ function cmd::logs::show_wg_events() {
|
||||||
data=$(json::wg_events \
|
data=$(json::wg_events \
|
||||||
"$WG_EVENTS_LOG" "$filter_name" "$filter_type" \
|
"$WG_EVENTS_LOG" "$filter_name" "$filter_type" \
|
||||||
"$limit" "$collapse" "$since" "$filter_event" \
|
"$limit" "$collapse" "$since" "$filter_event" \
|
||||||
|
"$(ctx::endpoint_cache)" "$sort_order" \
|
||||||
2>/dev/null)
|
2>/dev/null)
|
||||||
|
|
||||||
[[ -z "$data" ]] && return 0
|
[[ -z "$data" ]] && return 0
|
||||||
|
|
@ -269,12 +275,12 @@ function cmd::logs::show_wg_events() {
|
||||||
# Resolve endpoints and measure column widths
|
# Resolve endpoints and measure column widths
|
||||||
local w_client=16 w_endpoint=16
|
local w_client=16 w_endpoint=16
|
||||||
local resolved_data=""
|
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
|
[[ -z "$ts" ]] && continue
|
||||||
local endpoint_display
|
local endpoint_display
|
||||||
endpoint_display=$(resolve::ip "$endpoint")
|
endpoint_display=$(resolve::ip "$endpoint")
|
||||||
[[ -z "$endpoint_display" ]] && endpoint_display="$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}
|
(( ${#client} > w_client )) && w_client=${#client}
|
||||||
(( ${#endpoint_display} > w_endpoint )) && w_endpoint=${#endpoint_display}
|
(( ${#endpoint_display} > w_endpoint )) && w_endpoint=${#endpoint_display}
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
|
|
@ -282,10 +288,10 @@ function cmd::logs::show_wg_events() {
|
||||||
(( w_endpoint += 2 ))
|
(( w_endpoint += 2 ))
|
||||||
|
|
||||||
ui::logs::wg_section_header
|
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
|
[[ -z "$ts" ]] && continue
|
||||||
ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \
|
ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \
|
||||||
"$count" "$w_client" "$w_endpoint"
|
"$count" "$w_client" "$w_endpoint" "$gap_seconds"
|
||||||
done <<< "$resolved_data"
|
done <<< "$resolved_data"
|
||||||
printf "\n"
|
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::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; }
|
||||||
function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
||||||
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
||||||
|
function ctx::endpoint_cache() { echo "${_CTX_DAEMON}/endpoint_cache.json"; }
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Path Helpers
|
# 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::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; }
|
||||||
function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </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::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_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::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
|
||||||
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
|
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[6] if len(args) > 6 else '1',
|
||||||
args[7] if len(args) > 7 else '',
|
args[7] if len(args) > 7 else '',
|
||||||
args[8] if len(args) > 8 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(
|
'wg_events': lambda args: __import__('lib.events', fromlist=['wg_events']).wg_events(
|
||||||
args[0], args[1], args[2],
|
args[0], args[1], args[2],
|
||||||
args[3] if len(args) > 3 else '50',
|
args[3] if len(args) > 3 else '50',
|
||||||
args[4] if len(args) > 4 else '1',
|
args[4] if len(args) > 4 else '1',
|
||||||
args[5] if len(args) > 5 else '',
|
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_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]),
|
'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]),
|
'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,
|
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.
|
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
|
continue
|
||||||
|
|
||||||
sorted_buckets = sorted(hourly_ts.items(), key=lambda x: x[1])
|
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
|
client, dst, port, proto, svc_name, _ = hour_key
|
||||||
count = hourly[hour_key]
|
count = hourly[hour_key]
|
||||||
ts_fmt = fmt_ts_hour(dt.isoformat())
|
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)
|
deduped.append(e)
|
||||||
counts.append(1)
|
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', '')
|
src = e.get('src_ip', '')
|
||||||
dst = e.get('dest_ip', '')
|
dst = e.get('dest_ip', '')
|
||||||
port = str(e.get('dest_port', ''))
|
port = str(e.get('dest_port', ''))
|
||||||
|
|
@ -176,20 +182,27 @@ def fw_events(file, filter_ip, filter_type, clients_dir, net_file,
|
||||||
# ──────────────────────────────────────────
|
# ──────────────────────────────────────────
|
||||||
|
|
||||||
def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
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.
|
Format WireGuard events with dedup, counts, gap and endpoint resolution.
|
||||||
|
sort_order: 'desc' (default, newest first) | 'asc' (oldest first)
|
||||||
collapse='1' (default): hourly aggregation for attempt events
|
Output per line: ts|client|endpoint|event|count|gap_seconds
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import defaultdict
|
||||||
do_collapse = str(collapse) != '0'
|
do_collapse = str(collapse) != '0'
|
||||||
limit = int(limit) if limit else 50
|
limit = int(limit) if limit else 50
|
||||||
since_dt = parse_since(since) if since else None
|
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 = []
|
events = []
|
||||||
try:
|
try:
|
||||||
|
|
@ -206,25 +219,27 @@ def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
||||||
continue
|
continue
|
||||||
if filter_event and e.get('event', '') != filter_event:
|
if filter_event and e.get('event', '') != filter_event:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if since_dt:
|
if since_dt:
|
||||||
ts_str = e.get('timestamp', '')
|
ts_str = e.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
|
from datetime import timezone
|
||||||
ev_dt = datetime.fromisoformat(ts_str)
|
ev_dt = datetime.fromisoformat(ts_str)
|
||||||
if ev_dt.tzinfo is None:
|
if ev_dt.tzinfo is None:
|
||||||
from datetime import timezone
|
|
||||||
ev_dt = ev_dt.replace(tzinfo=timezone.utc)
|
ev_dt = ev_dt.replace(tzinfo=timezone.utc)
|
||||||
if ev_dt < since_dt:
|
if ev_dt < since_dt:
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
events.append(e)
|
events.append(e)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _endpoint(e):
|
||||||
|
ep = e.get('endpoint', '')
|
||||||
|
return ep or endpoint_cache.get(e.get('client', ''), '')
|
||||||
|
|
||||||
if do_collapse:
|
if do_collapse:
|
||||||
hourly_attempts = defaultdict(int)
|
hourly_attempts = defaultdict(int)
|
||||||
hourly_ts = {}
|
hourly_ts = {}
|
||||||
|
|
@ -234,7 +249,7 @@ def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
||||||
for e in events:
|
for e in events:
|
||||||
ts_str = e.get('timestamp', '')
|
ts_str = e.get('timestamp', '')
|
||||||
client = e.get('client', '')
|
client = e.get('client', '')
|
||||||
endpoint = e.get('endpoint', '')
|
endpoint = _endpoint(e)
|
||||||
event = e.get('event', '')
|
event = e.get('event', '')
|
||||||
ts = ts_to_unix(ts_str)
|
ts = ts_to_unix(ts_str)
|
||||||
|
|
||||||
|
|
@ -255,55 +270,71 @@ def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
||||||
if handshakes:
|
if handshakes:
|
||||||
prev = handshakes[-1]
|
prev = handshakes[-1]
|
||||||
prev_ts = ts_to_unix(prev.get('timestamp', ''))
|
prev_ts = ts_to_unix(prev.get('timestamp', ''))
|
||||||
prev_key = (
|
prev_key = (prev.get('client', ''), prev.get('event', ''),
|
||||||
prev.get('client', ''),
|
_endpoint(prev)[:15])
|
||||||
prev.get('event', ''),
|
|
||||||
prev.get('endpoint', '')[:15]
|
|
||||||
)
|
|
||||||
if key == prev_key and (ts - prev_ts) < 300:
|
if key == prev_key and (ts - prev_ts) < 300:
|
||||||
handshake_counts[-1] += 1
|
handshake_counts[-1] += 1
|
||||||
continue
|
continue
|
||||||
handshakes.append(e)
|
handshakes.append(e)
|
||||||
handshake_counts.append(1)
|
handshake_counts.append(1)
|
||||||
|
|
||||||
|
# Build output list — always ascending first for correct gap calculation
|
||||||
output = []
|
output = []
|
||||||
|
|
||||||
for hour_key, dt in hourly_ts.items():
|
for hour_key, dt in hourly_ts.items():
|
||||||
client, endpoint, event, _ = hour_key
|
client, endpoint, event, _ = hour_key
|
||||||
count = hourly_attempts[hour_key]
|
count = hourly_attempts[hour_key]
|
||||||
ts_fmt = fmt_ts_hour(dt.isoformat())
|
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):
|
for e, count in zip(handshakes, handshake_counts):
|
||||||
ts_str = e.get('timestamp', '')
|
ts_str = e.get('timestamp', '')
|
||||||
client = e.get('client', '')
|
client = e.get('client', '')
|
||||||
endpoint = e.get('endpoint', '')
|
endpoint = _endpoint(e)
|
||||||
event = e.get('event', '')
|
event = e.get('event', '')
|
||||||
ts = ts_to_unix(ts_str)
|
ts = ts_to_unix(ts_str)
|
||||||
ts_fmt = fmt_ts(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])
|
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)
|
print(line)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
deduped = []
|
deduped = []
|
||||||
counts = []
|
counts = []
|
||||||
|
|
||||||
for e in events:
|
for e in events:
|
||||||
client = e.get('client', '')
|
client = e.get('client', '')
|
||||||
event = e.get('event', '')
|
event = e.get('event', '')
|
||||||
endpoint = e.get('endpoint', '')
|
endpoint = _endpoint(e)
|
||||||
key = (client, event, endpoint[:15])
|
key = (client, event, endpoint[:15])
|
||||||
ts = ts_to_unix(e.get('timestamp', ''))
|
ts = ts_to_unix(e.get('timestamp', ''))
|
||||||
|
|
||||||
if deduped:
|
if deduped:
|
||||||
prev = deduped[-1]
|
prev = deduped[-1]
|
||||||
prev_ts = ts_to_unix(prev.get('timestamp', ''))
|
prev_ts = ts_to_unix(prev.get('timestamp', ''))
|
||||||
prev_key = (
|
prev_key = (prev.get('client', ''), prev.get('event', ''),
|
||||||
prev.get('client', ''),
|
_endpoint(prev)[:15])
|
||||||
prev.get('event', ''),
|
|
||||||
prev.get('endpoint', '')[:15]
|
|
||||||
)
|
|
||||||
if key == prev_key and (ts - prev_ts) < 300:
|
if key == prev_key and (ts - prev_ts) < 300:
|
||||||
counts[-1] += 1
|
counts[-1] += 1
|
||||||
continue
|
continue
|
||||||
|
|
@ -311,14 +342,33 @@ def wg_events(file, filter_client, filter_type, limit, collapse='1',
|
||||||
deduped.append(e)
|
deduped.append(e)
|
||||||
counts.append(1)
|
counts.append(1)
|
||||||
|
|
||||||
for e, count in list(zip(deduped, counts))[-limit:]:
|
# Compute gaps ascending, then slice, then reverse if needed
|
||||||
ts_fmt = fmt_ts(e.get('timestamp', ''))
|
last_handshake_ts = {}
|
||||||
|
result = []
|
||||||
|
for e, count in zip(deduped, counts):
|
||||||
|
ts_str = e.get('timestamp', '')
|
||||||
client = e.get('client', '')
|
client = e.get('client', '')
|
||||||
endpoint = e.get('endpoint', '')
|
endpoint = _endpoint(e)
|
||||||
event = e.get('event', '')
|
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)
|
# Single event parsers (used by watch)
|
||||||
# ──────────────────────────────────────────
|
# ──────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,14 @@ def poll_handshakes():
|
||||||
# Get endpoint
|
# Get endpoint
|
||||||
endpoint = get_endpoint(pubkey) or ''
|
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
|
# New session, log it
|
||||||
entry = {
|
entry = {
|
||||||
"timestamp": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
|
"timestamp": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
|
||||||
|
|
|
||||||
|
|
@ -56,23 +56,42 @@ function ui::logs::fw_row_table() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ui::logs::wg_row() {
|
function ui::logs::wg_row() {
|
||||||
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" count="${5:-1}" \
|
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
|
||||||
w_client="${6:-20}" w_endpoint="${7:-20}"
|
count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \
|
||||||
|
gap_seconds="${8:-}"
|
||||||
|
|
||||||
local event_color
|
local event_color
|
||||||
case "$event" in
|
case "$event" in
|
||||||
handshake) event_color="\033[1;32m" ;;
|
handshake) event_color="\033[1;32m" ;;
|
||||||
attempt) event_color="\033[1;31m" ;;
|
attempt) event_color="\033[1;31m" ;;
|
||||||
*) event_color="\033[0;37m" ;;
|
*) event_color="\033[0;37m" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
local count_suffix=""
|
local count_suffix=""
|
||||||
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m"
|
[[ "$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
|
local client_pad endpoint_pad_n
|
||||||
client_pad=$(printf "%-${w_client}s" "$client")
|
client_pad=$(printf "%-${w_client}s" "$client")
|
||||||
endpoint_pad_n=$(( w_endpoint - ${#endpoint} ))
|
endpoint_pad_n=$(( w_endpoint - ${#endpoint} ))
|
||||||
[[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0
|
[[ $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" "" \
|
"$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() {
|
function ui::logs::wg_row_table() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue