package metrics import ( "encoding/json" "fmt" "os" "sync" "time" ) type CommandStats struct { AllTime int `json:"all_time"` Last30 [30]int `json:"last_30"` Last30Day int64 `json:"last_30_day"` // Unix day of the first slot } type ChatInfo struct { IsGroup bool `json:"is_group"` LastActive int64 `json:"last_active"` Blocked bool `json:"blocked"` LastError string `json:"last_error,omitempty"` } type Metrics struct { Commands map[string]*CommandStats `json:"commands"` Chats map[int64]*ChatInfo `json:"chats"` } var ( metricsFile = "metrics/metrics.json" metrics *Metrics mu sync.Mutex ) func loadMetrics() { if metrics != nil { return } metrics = &Metrics{ Commands: make(map[string]*CommandStats), Chats: make(map[int64]*ChatInfo), } f, err := os.Open(metricsFile) if err != nil { return } defer f.Close() json.NewDecoder(f).Decode(metrics) } func saveMetrics() { // Do not lock here; caller must hold the lock else it will shitty deadlock loadMetrics() f, err := os.Create(metricsFile) if err != nil { return } defer f.Close() enc := json.NewEncoder(f) enc.SetIndent("", " ") enc.Encode(metrics) } func IncrementCommandUsage(cmd string) { fmt.Printf("[metrics] IncrementCommandUsage called with cmd=%s\n", cmd) mu.Lock() defer mu.Unlock() loadMetrics() stat, ok := metrics.Commands[cmd] if !ok { stat = &CommandStats{Last30Day: time.Now().Unix() / 86400} metrics.Commands[cmd] = stat } today := time.Now().Unix() / 86400 shift := int(today - stat.Last30Day) if shift > 0 && shift < 30 { copy(stat.Last30[shift:], stat.Last30[:30-shift]) for i := 0; i < shift; i++ { stat.Last30[i] = 0 } stat.Last30Day = today } else if shift >= 30 { for i := 0; i < 30; i++ { stat.Last30[i] = 0 } stat.Last30Day = today } stat.AllTime++ stat.Last30[0]++ saveMetrics() } func UpdateChat(chatID int64, isGroup bool) { fmt.Printf("[metrics] UpdateChat called with chatID=%d isGroup=%v\n", chatID, isGroup) mu.Lock() defer mu.Unlock() loadMetrics() info, ok := metrics.Chats[chatID] if !ok { info = &ChatInfo{IsGroup: isGroup} metrics.Chats[chatID] = info } info.LastActive = time.Now().Unix() info.Blocked = false info.LastError = "" saveMetrics() } func MarkChatBlocked(chatID int64, errMsg string) { fmt.Printf("[metrics] MarkChatBlocked called with chatID=%d errMsg=%s\n", chatID, errMsg) mu.Lock() defer mu.Unlock() loadMetrics() info, ok := metrics.Chats[chatID] if !ok { info = &ChatInfo{} metrics.Chats[chatID] = info } info.Blocked = true info.LastError = errMsg saveMetrics() } func GetStats() *Metrics { fmt.Printf("[metrics] GetStats called\n") mu.Lock() defer mu.Unlock() loadMetrics() copy := *metrics return © } func BroadcastMessage(sendFunc func(chatID int64) error) { fmt.Printf("[metrics] BroadcastMessage called\n") mu.Lock() loadMetrics() ids := make([]int64, 0, len(metrics.Chats)) for id := range metrics.Chats { ids = append(ids, id) } mu.Unlock() for _, id := range ids { err := sendFunc(id) if err != nil { MarkChatBlocked(id, err.Error()) } } }