mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 14:16:07 +00:00
891d3a8759
* feat(memory): add memory threshold alerts
Add memory (RAM) threshold alerts following the same architecture as
CPU alerts: CheckMemJob with @every 1m cadence, memoryAlarmWanted gate,
tgMemory/smtpMemory per-subscriber settings (default 80%), EventBusCheckboxes
with inline threshold input, i18n for en-US/ru-RU with English defaults.
# Conflicts:
# internal/web/translation/ar-EG.json
# internal/web/translation/es-ES.json
# internal/web/translation/fa-IR.json
# internal/web/translation/id-ID.json
# internal/web/translation/ja-JP.json
# internal/web/translation/pt-BR.json
# internal/web/translation/ru-RU.json
# internal/web/translation/tr-TR.json
# internal/web/translation/uk-UA.json
# internal/web/translation/vi-VN.json
# internal/web/translation/zh-CN.json
# internal/web/translation/zh-TW.json
* fix: address code review findings for memory alerts
- Remove dead settingService field from CheckMemJob
- Fix cpuThreshold double-emoji in 12 locale files (code prepends 🔴)
- Align TgCpu/TgMemory fields in entity.go
- Add missing SetTgMemory function
* fix: restore settingService in CheckMemJob for consistency with CheckCpuJob
163 lines
4.7 KiB
Go
163 lines
4.7 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/eventbus"
|
|
)
|
|
|
|
var cachedHostname string
|
|
|
|
func getHostname() string {
|
|
if cachedHostname != "" {
|
|
return cachedHostname
|
|
}
|
|
h, err := os.Hostname()
|
|
if err != nil {
|
|
cachedHostname = "unknown"
|
|
} else {
|
|
cachedHostname = h
|
|
}
|
|
return cachedHostname
|
|
}
|
|
|
|
var tgEventLimiter = eventbus.NewRateLimiter(1 * time.Minute)
|
|
|
|
// HandleEvent is the eventbus subscriber callback. It formats incoming events
|
|
// as Telegram messages and sends them to all admin chats.
|
|
func (t *Tgbot) HandleEvent(e eventbus.Event) {
|
|
if !t.isEventEnabled(e.Type) {
|
|
return
|
|
}
|
|
if e.Type != eventbus.EventLoginAttempt {
|
|
if !tgEventLimiter.Allow(e.Type, e.Source) {
|
|
return
|
|
}
|
|
}
|
|
msg := t.formatEventMessage(e)
|
|
if msg != "" {
|
|
t.SendMsgToTgbotAdmins(msg)
|
|
}
|
|
}
|
|
|
|
func (t *Tgbot) isEventEnabled(eventType eventbus.EventType) bool {
|
|
events, err := t.settingService.GetTgEnabledEvents()
|
|
if err != nil || events == "" {
|
|
return false
|
|
}
|
|
for e := range strings.SplitSeq(events, ",") {
|
|
if strings.TrimSpace(e) == string(eventType) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *Tgbot) formatEventMessage(e eventbus.Event) string {
|
|
host := getHostname()
|
|
header := fmt.Sprintf("<b>📡 %s</b>\n", host)
|
|
|
|
switch e.Type {
|
|
case eventbus.EventOutboundDown:
|
|
msg := header + t.I18nBot("tgbot.messages.eventOutboundDown",
|
|
"Tag=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.OutboundHealthData); ok {
|
|
if data.Error != "" {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventErrorDetail",
|
|
"Error=="+data.Error)
|
|
}
|
|
if data.Delay > 0 {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventDelayDetail",
|
|
"Delay=="+fmt.Sprintf("%d", data.Delay))
|
|
}
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventOutboundUp:
|
|
msg := header + t.I18nBot("tgbot.messages.eventOutboundUp",
|
|
"Tag=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.OutboundHealthData); ok && data.Delay > 0 {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventDelayDetail",
|
|
"Delay=="+fmt.Sprintf("%d", data.Delay))
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventXrayCrash:
|
|
errStr := ""
|
|
if e.Data != nil {
|
|
errStr = fmt.Sprint(e.Data)
|
|
}
|
|
msg := header + "🔥 " + t.I18nBot("tgbot.messages.eventXrayCrash")
|
|
if errStr != "" {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventXrayCrashError", "Error=="+errStr)
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventNodeDown:
|
|
msg := header + "🔴 " + t.I18nBot("tgbot.messages.eventNodeDown", "Name=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.XrayError != "" {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventErrorDetail", "Error=="+data.XrayError)
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventNodeUp:
|
|
msg := header + "🟢 " + t.I18nBot("tgbot.messages.eventNodeUp", "Name=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.LatencyMs > 0 {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventDelayDetail", "Delay=="+fmt.Sprintf("%d", data.LatencyMs))
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventCPUHigh:
|
|
if data, ok := e.Data.(*eventbus.SystemMetricData); ok {
|
|
tgCpu, err := t.settingService.GetTgCpu()
|
|
if err != nil || tgCpu <= 0 || data.Percent <= float64(tgCpu) {
|
|
return ""
|
|
}
|
|
return header + "🔴 " + t.I18nBot("tgbot.messages.cpuThreshold",
|
|
"Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64),
|
|
"Threshold=="+strconv.Itoa(tgCpu))
|
|
}
|
|
return ""
|
|
|
|
case eventbus.EventMemoryHigh:
|
|
if data, ok := e.Data.(*eventbus.SystemMetricData); ok {
|
|
tgMemory, err := t.settingService.GetTgMemory()
|
|
if err != nil || tgMemory <= 0 || data.Percent <= float64(tgMemory) {
|
|
return ""
|
|
}
|
|
return header + "🔴 " + t.I18nBot("tgbot.messages.memoryThreshold",
|
|
"Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64),
|
|
"Threshold=="+strconv.Itoa(tgMemory))
|
|
}
|
|
return ""
|
|
|
|
case eventbus.EventLoginAttempt:
|
|
if data, ok := e.Data.(*eventbus.LoginEventData); ok {
|
|
if data.Status == "success" {
|
|
msg := t.I18nBot("tgbot.messages.loginSuccess")
|
|
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+host)
|
|
msg += t.I18nBot("tgbot.messages.username", "Username=="+data.Username)
|
|
msg += t.I18nBot("tgbot.messages.ip", "IP=="+data.IP)
|
|
msg += t.I18nBot("tgbot.messages.time", "Time=="+data.Time)
|
|
return msg
|
|
}
|
|
msg := t.I18nBot("tgbot.messages.loginFailed")
|
|
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+host)
|
|
if data.Reason != "" {
|
|
msg += t.I18nBot("tgbot.messages.reason", "Reason=="+data.Reason)
|
|
}
|
|
msg += t.I18nBot("tgbot.messages.username", "Username=="+data.Username)
|
|
msg += t.I18nBot("tgbot.messages.ip", "IP=="+data.IP)
|
|
msg += t.I18nBot("tgbot.messages.time", "Time=="+data.Time)
|
|
return msg
|
|
}
|
|
return header + t.I18nBot("tgbot.messages.eventLoginFallback", "Source=="+e.Source)
|
|
}
|
|
|
|
return ""
|
|
}
|