mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-06-10 11:32:48 +00:00
v2.5.0: compact traffic history retention
This commit is contained in:
@@ -453,6 +453,8 @@ switch_language ru|en
|
|||||||
|
|
||||||
Функции: overview, проверка сайта на HTTP 200, service status/restart, чтение/запись `[access.users]`, enable/disable ключей через `/api/users/<name>/enabled`, генерация proxy links, общий traffic history из `/opt/gotelegram/stats_history.csv`, per-user traffic history из `/opt/gotelegram/user_stats_history.csv` с периодами 15m/1h/24h/month, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, структурированные journal logs (`service`, `ok`, `exit_code`, `line_count`, `text`).
|
Функции: overview, проверка сайта на HTTP 200, service status/restart, чтение/запись `[access.users]`, enable/disable ключей через `/api/users/<name>/enabled`, генерация proxy links, общий traffic history из `/opt/gotelegram/stats_history.csv`, per-user traffic history из `/opt/gotelegram/user_stats_history.csv` с периодами 15m/1h/24h/month, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, структурированные journal logs (`service`, `ok`, `exit_code`, `line_count`, `text`).
|
||||||
|
|
||||||
|
Traffic retention: обе CSV-истории хранятся максимум 365 дней. Последние 31 день остаются с поминутным разрешением, более старые точки автоматически уплотняются до одной последней cumulative-точки в час. Чистка/уплотнение запускается не чаще одного раза в час, а per-user сбор пишет данные не чаще одного раза в минуту, даже если systemd-loop вызывает `stats_collect` каждую секунду. Параметры можно переопределить переменными `STATS_RETENTION_DAYS`, `STATS_MINUTE_RETENTION_DAYS`, `STATS_CLEANUP_INTERVAL`.
|
||||||
|
|
||||||
### 13.1.1 Shared TCP/443 with 3x-ui/Xray
|
### 13.1.1 Shared TCP/443 with 3x-ui/Xray
|
||||||
|
|
||||||
`lib/shared443.sh` добавляет управляемую схему shared-443 через nginx stream `ssl_preread`:
|
`lib/shared443.sh` добавляет управляемую схему shared-443 через nginx stream `ssl_preread`:
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ CLI и бот переведены на русский и английский.
|
|||||||
|
|
||||||
В админке есть dashboard, проверка сайта `https://домен/` на HTTP 200, статус сервисов, полезный блок «кто слушает порт 443» по данным `ss`, управление ключами `[access.users]` с добавлением, удалением и быстрым отключением через switch, генерация ссылок, traffic history по периодам 15 минут / 1 час / 24 часа / месяц с переключателем график/строки, такая же статистика по каждому ключу, кнопка разового обновления статистики, кнопка перезапуска сборщика, список бекапов и просмотр логов с количеством строк и статусом `journalctl`.
|
В админке есть dashboard, проверка сайта `https://домен/` на HTTP 200, статус сервисов, полезный блок «кто слушает порт 443» по данным `ss`, управление ключами `[access.users]` с добавлением, удалением и быстрым отключением через switch, генерация ссылок, traffic history по периодам 15 минут / 1 час / 24 часа / месяц с переключателем график/строки, такая же статистика по каждому ключу, кнопка разового обновления статистики, кнопка перезапуска сборщика, список бекапов и просмотр логов с количеством строк и статусом `journalctl`.
|
||||||
|
|
||||||
|
История трафика хранится максимум 1 год. Чтобы файлы не разрастались, последние 31 день пишутся поминутно, а более старая история автоматически уплотняется до одной точки в час. Для обычного просмотра 15 минут / 1 час / 24 часа / месяц детализация остаётся полной.
|
||||||
|
|
||||||
## 8.1 3x-ui / VLESS на том же 443
|
## 8.1 3x-ui / VLESS на том же 443
|
||||||
|
|
||||||
Один порт `443` не могут одновременно слушать `telemt` и Xray напрямую. Для совместной работы используется схема shared-443: публичный `443` занимает nginx stream-диспетчер, goTelegram `telemt` переносится на `127.0.0.1:7443`, сайт остаётся на `127.0.0.1:8443`, а inbound 3x-ui/Xray нужно в панели перенести на внутренний адрес, например `127.0.0.1:9443`.
|
Один порт `443` не могут одновременно слушать `telemt` и Xray напрямую. Для совместной работы используется схема shared-443: публичный `443` занимает nginx stream-диспетчер, goTelegram `telemt` переносится на `127.0.0.1:7443`, сайт остаётся на `127.0.0.1:8443`, а inbound 3x-ui/Xray нужно в панели перенести на внутренний адрес, например `127.0.0.1:9443`.
|
||||||
|
|||||||
@@ -691,9 +691,26 @@ def load_user_stats_history(name: str | None = None, limit: int | None = 240) ->
|
|||||||
|
|
||||||
def latest_user_stats() -> dict[str, dict[str, Any]]:
|
def latest_user_stats() -> dict[str, dict[str, Any]]:
|
||||||
latest: dict[str, dict[str, Any]] = {}
|
latest: dict[str, dict[str, Any]] = {}
|
||||||
for row in load_user_stats_history(limit=None):
|
if not USER_HISTORY_FILE.exists():
|
||||||
if row["epoch"] >= latest.get(row["user"], {}).get("epoch", 0):
|
return latest
|
||||||
latest[row["user"]] = row
|
try:
|
||||||
|
with USER_HISTORY_FILE.open("r", encoding="utf-8", newline="") as fh:
|
||||||
|
for row in csv.DictReader(fh):
|
||||||
|
user = str(row.get("user") or "").strip()
|
||||||
|
if not USER_RE.match(user):
|
||||||
|
continue
|
||||||
|
item = {
|
||||||
|
"epoch": _int_value(row.get("epoch")),
|
||||||
|
"user": user,
|
||||||
|
"total_octets": _int_value(row.get("total_octets")),
|
||||||
|
"current_connections": _int_value(row.get("current_connections")),
|
||||||
|
"active_unique_ips": _int_value(row.get("active_unique_ips")),
|
||||||
|
"recent_unique_ips": _int_value(row.get("recent_unique_ips")),
|
||||||
|
}
|
||||||
|
if item["epoch"] >= latest.get(user, {}).get("epoch", 0):
|
||||||
|
latest[user] = item
|
||||||
|
except OSError:
|
||||||
|
return {}
|
||||||
return latest
|
return latest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
100
lib/stats.sh
100
lib/stats.sh
@@ -17,6 +17,11 @@ SNAPSHOTS_DIR="$STATS_DIR/snapshots"
|
|||||||
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
|
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
|
||||||
CONFIG_FILE="/opt/gotelegram/config.json"
|
CONFIG_FILE="/opt/gotelegram/config.json"
|
||||||
TELEMT_CONFIG_FILE="/etc/telemt/config.toml"
|
TELEMT_CONFIG_FILE="/etc/telemt/config.toml"
|
||||||
|
STATS_RETENTION_DAYS="${STATS_RETENTION_DAYS:-365}"
|
||||||
|
STATS_MINUTE_RETENTION_DAYS="${STATS_MINUTE_RETENTION_DAYS:-31}"
|
||||||
|
STATS_CLEANUP_INTERVAL="${STATS_CLEANUP_INTERVAL:-3600}"
|
||||||
|
STATS_CLEANUP_STAMP="$STATS_DIR/last_history_cleanup"
|
||||||
|
USER_STATS_COLLECT_STAMP="$STATS_DIR/last_user_stats_minute"
|
||||||
|
|
||||||
# Initialize stats infrastructure
|
# Initialize stats infrastructure
|
||||||
stats_init() {
|
stats_init() {
|
||||||
@@ -124,7 +129,7 @@ EOF
|
|||||||
if [[ "$last_ts" -eq 0 ]] || [[ $((current_minute - last_ts)) -ge 60 ]]; then
|
if [[ "$last_ts" -eq 0 ]] || [[ $((current_minute - last_ts)) -ge 60 ]]; then
|
||||||
echo "$current_minute,$proxy_bytes,$site_bytes" >> "$HISTORY_FILE" 2>/dev/null
|
echo "$current_minute,$proxy_bytes,$site_bytes" >> "$HISTORY_FILE" 2>/dev/null
|
||||||
|
|
||||||
# Cleanup old entries (keep only 365 days)
|
# Cleanup/compact history at most once per hour.
|
||||||
stats_cleanup_history
|
stats_cleanup_history
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -163,10 +168,17 @@ stats_collect_users() {
|
|||||||
command -v curl &>/dev/null || return 0
|
command -v curl &>/dev/null || return 0
|
||||||
command -v jq &>/dev/null || return 0
|
command -v jq &>/dev/null || return 0
|
||||||
|
|
||||||
|
if [[ -f "$USER_STATS_COLLECT_STAMP" ]] && [[ "$(cat "$USER_STATS_COLLECT_STAMP" 2>/dev/null)" == "$current_minute" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local existing_users=""
|
||||||
|
existing_users=$(awk -F, -v ts="$current_minute" '$1 == ts { print $2 }' "$USER_HISTORY_FILE" 2>/dev/null || true)
|
||||||
|
|
||||||
local user payload total conns active_ips recent_ips
|
local user payload total conns active_ips recent_ips
|
||||||
while IFS= read -r user; do
|
while IFS= read -r user; do
|
||||||
[[ -n "$user" ]] || continue
|
[[ -n "$user" ]] || continue
|
||||||
if awk -F, -v ts="$current_minute" -v user="$user" '$1 == ts && $2 == user { found=1 } END { exit found ? 0 : 1 }' "$USER_HISTORY_FILE" 2>/dev/null; then
|
if printf '%s\n' "$existing_users" | grep -Fxq "$user"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -183,6 +195,7 @@ stats_collect_users() {
|
|||||||
echo "$current_minute,$user,$total,$conns,$active_ips,$recent_ips" >> "$USER_HISTORY_FILE" 2>/dev/null
|
echo "$current_minute,$user,$total,$conns,$active_ips,$recent_ips" >> "$USER_HISTORY_FILE" 2>/dev/null
|
||||||
done < <(stats_active_users)
|
done < <(stats_active_users)
|
||||||
|
|
||||||
|
echo "$current_minute" > "$USER_STATS_COLLECT_STAMP" 2>/dev/null || true
|
||||||
stats_cleanup_user_history
|
stats_cleanup_user_history
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,20 +361,66 @@ show_traffic_stats() {
|
|||||||
} >&2
|
} >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clean up history older than 365 days
|
_stats_positive_int() {
|
||||||
|
local value="${1:-0}"
|
||||||
|
[[ "$value" =~ ^[0-9]+$ ]] && [[ "$value" -gt 0 ]] && echo "$value" || echo "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_should_cleanup() {
|
||||||
|
local stamp="$1"
|
||||||
|
local now last interval
|
||||||
|
mkdir -p "$STATS_DIR" 2>/dev/null || true
|
||||||
|
now=$(date +%s)
|
||||||
|
interval=$(_stats_positive_int "$STATS_CLEANUP_INTERVAL" 3600)
|
||||||
|
last=$(cat "$stamp" 2>/dev/null || echo 0)
|
||||||
|
[[ "$last" =~ ^[0-9]+$ ]] || last=0
|
||||||
|
if (( now - last < interval )); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$now" > "$stamp" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_retention_cutoffs() {
|
||||||
|
local now retention_days minute_days
|
||||||
|
now=$(date +%s)
|
||||||
|
retention_days=$(_stats_positive_int "$STATS_RETENTION_DAYS" 365)
|
||||||
|
minute_days=$(_stats_positive_int "$STATS_MINUTE_RETENTION_DAYS" 31)
|
||||||
|
if (( minute_days > retention_days )); then
|
||||||
|
minute_days="$retention_days"
|
||||||
|
fi
|
||||||
|
echo "$((now - retention_days * 86400)) $((now - minute_days * 86400))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep history for at most one year. Recent points stay per-minute; older
|
||||||
|
# points are compacted to one last cumulative snapshot per hour.
|
||||||
stats_cleanup_history() {
|
stats_cleanup_history() {
|
||||||
if [[ ! -f "$HISTORY_FILE" ]]; then
|
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local now=$(date +%s)
|
stats_should_cleanup "$STATS_CLEANUP_STAMP" || return 0
|
||||||
local ts_365d=$((now - 31536000))
|
|
||||||
local temp_file=$(mktemp)
|
local retention_cutoff minute_cutoff temp_file
|
||||||
|
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
|
||||||
# Keep header + entries from last 365 days
|
|
||||||
{
|
{
|
||||||
head -1 "$HISTORY_FILE"
|
head -1 "$HISTORY_FILE"
|
||||||
awk -F, -v ts="$ts_365d" '$1 >= ts' "$HISTORY_FILE" | tail -n +2
|
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
|
||||||
|
BEGIN { OFS="," }
|
||||||
|
NR == 1 { next }
|
||||||
|
$1 !~ /^[0-9]+$/ { next }
|
||||||
|
$1 < keep { next }
|
||||||
|
$1 >= minute { print $1, $2, $3; next }
|
||||||
|
{
|
||||||
|
bucket = int($1 / 3600)
|
||||||
|
compact[bucket] = $1 OFS $2 OFS $3
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
for (bucket in compact) print compact[bucket]
|
||||||
|
}
|
||||||
|
' "$HISTORY_FILE" | sort -t, -k1,1n
|
||||||
} > "$temp_file" 2>/dev/null
|
} > "$temp_file" 2>/dev/null
|
||||||
|
|
||||||
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
|
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
|
||||||
@@ -372,13 +431,28 @@ stats_cleanup_user_history() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local now=$(date +%s)
|
stats_should_cleanup "${STATS_CLEANUP_STAMP}.users" || return 0
|
||||||
local ts_365d=$((now - 31536000))
|
|
||||||
local temp_file=$(mktemp)
|
local retention_cutoff minute_cutoff temp_file
|
||||||
|
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
|
||||||
{
|
{
|
||||||
head -1 "$USER_HISTORY_FILE"
|
head -1 "$USER_HISTORY_FILE"
|
||||||
awk -F, -v ts="$ts_365d" '$1 >= ts' "$USER_HISTORY_FILE" | tail -n +2
|
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
|
||||||
|
BEGIN { OFS="," }
|
||||||
|
NR == 1 { next }
|
||||||
|
$1 !~ /^[0-9]+$/ { next }
|
||||||
|
$1 < keep { next }
|
||||||
|
$1 >= minute { print $1, $2, $3, $4, $5, $6; next }
|
||||||
|
{
|
||||||
|
bucket = $2 SUBSEP int($1 / 3600)
|
||||||
|
compact[bucket] = $1 OFS $2 OFS $3 OFS $4 OFS $5 OFS $6
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
for (bucket in compact) print compact[bucket]
|
||||||
|
}
|
||||||
|
' "$USER_HISTORY_FILE" | sort -t, -k1,1n -k2,2
|
||||||
} > "$temp_file" 2>/dev/null
|
} > "$temp_file" 2>/dev/null
|
||||||
|
|
||||||
mv "$temp_file" "$USER_HISTORY_FILE" 2>/dev/null
|
mv "$temp_file" "$USER_HISTORY_FILE" 2>/dev/null
|
||||||
@@ -516,5 +590,5 @@ remove_stats_collector() {
|
|||||||
# Export functions for external use
|
# Export functions for external use
|
||||||
export -f stats_init stats_collect stats_collect_users stats_active_users stats_read_current stats_calculate_rates
|
export -f stats_init stats_collect stats_collect_users stats_active_users stats_read_current stats_calculate_rates
|
||||||
export -f show_traffic_stats format_bytes format_rate toggle_stats
|
export -f show_traffic_stats format_bytes format_rate toggle_stats
|
||||||
export -f stats_cleanup_history stats_cleanup_user_history install_stats_collector remove_stats_collector
|
export -f stats_cleanup_history stats_cleanup_user_history stats_should_cleanup stats_retention_cutoffs install_stats_collector remove_stats_collector
|
||||||
export -f json_get
|
export -f json_get
|
||||||
|
|||||||
Reference in New Issue
Block a user