feat: console_log.meshtastic 控制 packet 控制台输出;新增每客户端报文统计
- ConsoleLogConfig 增加 Meshtastic 字段(默认 true);旧配置自动补齐 - meshtasticFilterHook 增加 packetConsoleLog;OnPublish 中根据开关调用 新的 printMeshtasticRecord 输出可读单行(key=value、按 type 着色), 替代原来的 JSON dump;事件型 printJSON 调用保持不变 - 新增 internal/mqttforward/ClientStats,按 client_id 累计 in/out 报文计数 - meshtasticFilterHook 多挂 OnPacketRead / OnPacketSent / OnDisconnect 事件,用于增减计数;Provides() 同步声明 - AdminMQTTClient JSON 视图删除 RemoteHost/RemotePort,新增 packets_in / packets_out;前端 types.ts、AdminDashboard.vue 同步更新 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+20
-12
@@ -83,10 +83,11 @@ type AIConfig struct {
|
||||
|
||||
// ConsoleLogConfig 控制各模块是否在控制台打印日志。后续若新增模块,按需扩展。
|
||||
type ConsoleLogConfig struct {
|
||||
Web bool `yaml:"web"`
|
||||
MQTT bool `yaml:"mqtt"`
|
||||
LLM bool `yaml:"llm"`
|
||||
SQL bool `yaml:"sql"`
|
||||
Web bool `yaml:"web"`
|
||||
MQTT bool `yaml:"mqtt"`
|
||||
LLM bool `yaml:"llm"`
|
||||
SQL bool `yaml:"sql"`
|
||||
Meshtastic bool `yaml:"meshtastic"`
|
||||
}
|
||||
|
||||
type rawConfig struct {
|
||||
@@ -99,10 +100,11 @@ type rawConfig struct {
|
||||
}
|
||||
|
||||
type rawConsoleLogConfig struct {
|
||||
Web *bool `yaml:"web"`
|
||||
MQTT *bool `yaml:"mqtt"`
|
||||
LLM *bool `yaml:"llm"`
|
||||
SQL *bool `yaml:"sql"`
|
||||
Web *bool `yaml:"web"`
|
||||
MQTT *bool `yaml:"mqtt"`
|
||||
LLM *bool `yaml:"llm"`
|
||||
SQL *bool `yaml:"sql"`
|
||||
Meshtastic *bool `yaml:"meshtastic"`
|
||||
}
|
||||
|
||||
type rawAIConfig struct {
|
||||
@@ -200,10 +202,11 @@ func Default() *Config {
|
||||
DataDir: defaultDataDir(),
|
||||
},
|
||||
ConsoleLog: ConsoleLogConfig{
|
||||
Web: true,
|
||||
MQTT: true,
|
||||
LLM: true,
|
||||
SQL: true,
|
||||
Web: true,
|
||||
MQTT: true,
|
||||
LLM: true,
|
||||
SQL: true,
|
||||
Meshtastic: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -515,6 +518,11 @@ func normalize(raw rawConfig) (*Config, bool) {
|
||||
} else {
|
||||
cfg.ConsoleLog.SQL = *raw.ConsoleLog.SQL
|
||||
}
|
||||
if raw.ConsoleLog.Meshtastic == nil {
|
||||
changed = true
|
||||
} else {
|
||||
cfg.ConsoleLog.Meshtastic = *raw.ConsoleLog.Meshtastic
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, changed
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package mqttforward
|
||||
|
||||
import "sync"
|
||||
|
||||
// ClientStats 在内存中维护每个 MQTT 客户端的收/发包数量。
|
||||
// key 取自 mqtt.Client.ID(broker 内部唯一标识),客户端断开时由调用方调用 Delete 清除,
|
||||
// 重新连接同一 client_id 会拿到一份新的零值计数,符合"断链就清空"。
|
||||
type ClientStats struct {
|
||||
mu sync.RWMutex
|
||||
all map[string]*clientCounter
|
||||
}
|
||||
|
||||
type clientCounter struct {
|
||||
In int64 // 客户端 → 服务器(broker 收到的报文数)
|
||||
Out int64 // 服务器 → 客户端(broker 发出的报文数)
|
||||
}
|
||||
|
||||
// NewClientStats 返回一个空的统计器。
|
||||
func NewClientStats() *ClientStats {
|
||||
return &ClientStats{all: make(map[string]*clientCounter)}
|
||||
}
|
||||
|
||||
// IncIn 在 broker 收到客户端报文时调用。clientID 为空直接忽略。
|
||||
func (s *ClientStats) IncIn(clientID string) {
|
||||
if s == nil || clientID == "" {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
c, ok := s.all[clientID]
|
||||
if !ok {
|
||||
c = &clientCounter{}
|
||||
s.all[clientID] = c
|
||||
}
|
||||
c.In++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// IncOut 在 broker 向客户端发出报文时调用。
|
||||
func (s *ClientStats) IncOut(clientID string) {
|
||||
if s == nil || clientID == "" {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
c, ok := s.all[clientID]
|
||||
if !ok {
|
||||
c = &clientCounter{}
|
||||
s.all[clientID] = c
|
||||
}
|
||||
c.Out++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Get 返回指定 clientID 当前的收/发包数量;不存在时返回 0,0。
|
||||
func (s *ClientStats) Get(clientID string) (in, out int64) {
|
||||
if s == nil || clientID == "" {
|
||||
return 0, 0
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if c, ok := s.all[clientID]; ok {
|
||||
return c.In, c.Out
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Delete 在客户端断开连接时清除其计数。重新连接同一 clientID 会从 0 重新计起。
|
||||
func (s *ClientStats) Delete(clientID string) {
|
||||
if s == nil || clientID == "" {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
delete(s.all, clientID)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
+16
-37
@@ -16,11 +16,12 @@ type MQTTStatusProvider interface {
|
||||
// MQTTRuntimeStatus 把 mqtt.Server / 写队列 / 转发统计三个上下文打包成
|
||||
// 实现 MQTTStatusProvider 的具体类型。供 main 包构造后注入 newRouter。
|
||||
type MQTTRuntimeStatus struct {
|
||||
Server *mqtt.Server
|
||||
Address string
|
||||
TLS bool
|
||||
Stats *mqttforwardpkg.Stats
|
||||
DBQueue *storepkg.WriteQueue
|
||||
Server *mqtt.Server
|
||||
Address string
|
||||
TLS bool
|
||||
Stats *mqttforwardpkg.Stats
|
||||
ClientStats *mqttforwardpkg.ClientStats
|
||||
DBQueue *storepkg.WriteQueue
|
||||
}
|
||||
|
||||
// AdminMQTTStatus 是 admin 路由 GET /admin/mqtt-status 返回的 JSON 视图。
|
||||
@@ -51,12 +52,12 @@ type AdminMQTTStatus struct {
|
||||
}
|
||||
|
||||
type AdminMQTTClient struct {
|
||||
ClientID string `json:"client_id"`
|
||||
Username string `json:"username"`
|
||||
Listener string `json:"listener"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
RemoteHost string `json:"remote_host"`
|
||||
RemotePort string `json:"remote_port"`
|
||||
ClientID string `json:"client_id"`
|
||||
Username string `json:"username"`
|
||||
Listener string `json:"listener"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
PacketsIn int64 `json:"packets_in"` // 客户端 → 服务器
|
||||
PacketsOut int64 `json:"packets_out"` // 服务器 → 客户端
|
||||
}
|
||||
|
||||
// Status 实现 MQTTStatusProvider。
|
||||
@@ -94,13 +95,14 @@ func (m MQTTRuntimeStatus) Status() AdminMQTTStatus {
|
||||
continue
|
||||
}
|
||||
info := mqttClientInfo(client)
|
||||
in, out := m.ClientStats.Get(info.ClientID)
|
||||
status.Clients = append(status.Clients, AdminMQTTClient{
|
||||
ClientID: info.ClientID,
|
||||
Username: info.Username,
|
||||
Listener: info.Listener,
|
||||
RemoteAddr: info.RemoteAddr,
|
||||
RemoteHost: info.RemoteHost,
|
||||
RemotePort: info.RemotePort,
|
||||
PacketsIn: in,
|
||||
PacketsOut: out,
|
||||
})
|
||||
}
|
||||
return status
|
||||
@@ -112,39 +114,16 @@ type mqttClientInfoView struct {
|
||||
Username string
|
||||
Listener string
|
||||
RemoteAddr string
|
||||
RemoteHost string
|
||||
RemotePort string
|
||||
}
|
||||
|
||||
func mqttClientInfo(c *mqtt.Client) mqttClientInfoView {
|
||||
if c == nil {
|
||||
return mqttClientInfoView{}
|
||||
}
|
||||
info := mqttClientInfoView{
|
||||
return mqttClientInfoView{
|
||||
ClientID: c.ID,
|
||||
Username: string(c.Properties.Username),
|
||||
Listener: c.Net.Listener,
|
||||
RemoteAddr: c.Net.Remote,
|
||||
}
|
||||
host, port := splitHostPort(c.Net.Remote)
|
||||
info.RemoteHost = host
|
||||
info.RemotePort = port
|
||||
return info
|
||||
}
|
||||
|
||||
func splitHostPort(addr string) (string, string) {
|
||||
if addr == "" {
|
||||
return "", ""
|
||||
}
|
||||
// 复用 net.SplitHostPort,但要兼容 "host" 这种没端口的情况。
|
||||
for i := len(addr) - 1; i >= 0; i-- {
|
||||
if addr[i] == ':' {
|
||||
host := addr[:i]
|
||||
if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
return host, addr[i+1:]
|
||||
}
|
||||
}
|
||||
return addr, ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user