package monitor import ( "strconv" "strings" "sync" "time" "github.com/go-mysql-org/go-mysql/mysql" ) type Metrics struct { mu sync.RWMutex startedAt time.Time masterPosition mysql.Position checkpointPosition mysql.Position lastSyncedPosition mysql.Position startPosition mysql.Position lastFlushAt time.Time lastFlushEvents int totalFlushes uint64 totalEvents uint64 totalRetries uint64 totalLockWaits uint64 totalCleanupRows uint64 lastCleanupAt time.Time lastCleanupRows int64 lastError string lastErrorAt time.Time lastBatchDuration time.Duration } func New() *Metrics { return &Metrics{startedAt: time.Now()} } func (m *Metrics) SetStartPosition(pos mysql.Position) { m.mu.Lock() defer m.mu.Unlock() m.startPosition = pos } func (m *Metrics) SetMasterPosition(pos mysql.Position) { m.mu.Lock() defer m.mu.Unlock() m.masterPosition = pos } func (m *Metrics) SetCheckpointPosition(pos mysql.Position) { m.mu.Lock() defer m.mu.Unlock() m.checkpointPosition = pos } func (m *Metrics) SetLastSyncedPosition(pos mysql.Position) { m.mu.Lock() defer m.mu.Unlock() m.lastSyncedPosition = pos } func (m *Metrics) RecordBatch(events int, duration time.Duration) { m.mu.Lock() defer m.mu.Unlock() m.lastFlushAt = time.Now() m.lastFlushEvents = events m.totalFlushes++ m.totalEvents += uint64(events) m.lastBatchDuration = duration } func (m *Metrics) RecordRetry() { m.mu.Lock() defer m.mu.Unlock() m.totalRetries++ } func (m *Metrics) RecordLockWait() { m.mu.Lock() defer m.mu.Unlock() m.totalLockWaits++ } func (m *Metrics) RecordCleanup(rows int64) { m.mu.Lock() defer m.mu.Unlock() m.lastCleanupAt = time.Now() m.lastCleanupRows = rows if rows > 0 { m.totalCleanupRows += uint64(rows) } } func (m *Metrics) RecordError(err error) { if err == nil { return } m.mu.Lock() defer m.mu.Unlock() m.lastError = err.Error() m.lastErrorAt = time.Now() } func (m *Metrics) Snapshot() map[string]interface{} { m.mu.RLock() defer m.mu.RUnlock() return map[string]interface{}{ "started_at": formatTime(m.startedAt), "start_position": positionMap(m.startPosition), "master_position": positionMap(m.masterPosition), "checkpoint_position": positionMap(m.checkpointPosition), "last_synced_position": positionMap(m.lastSyncedPosition), "binlog_file_gap": fileGap(m.masterPosition, m.checkpointPosition), "binlog_position_gap": positionGap(m.masterPosition, m.checkpointPosition), "last_flush_at": formatTime(m.lastFlushAt), "last_flush_events": m.lastFlushEvents, "last_batch_duration_ms": m.lastBatchDuration.Milliseconds(), "total_flushes": m.totalFlushes, "total_events": m.totalEvents, "total_retries": m.totalRetries, "total_lock_waits": m.totalLockWaits, "last_cleanup_at": formatTime(m.lastCleanupAt), "last_cleanup_rows": m.lastCleanupRows, "total_cleanup_rows": m.totalCleanupRows, "last_error": m.lastError, "last_error_at": formatTime(m.lastErrorAt), } } func positionMap(pos mysql.Position) map[string]interface{} { return map[string]interface{}{ "file": pos.Name, "pos": pos.Pos, } } func positionGap(master mysql.Position, checkpoint mysql.Position) interface{} { if master.Name == "" || checkpoint.Name == "" || master.Name != checkpoint.Name { return nil } if master.Pos < checkpoint.Pos { return uint32(0) } return master.Pos - checkpoint.Pos } func fileGap(master mysql.Position, checkpoint mysql.Position) interface{} { masterSeq, ok := binlogSequence(master.Name) if !ok { return nil } checkpointSeq, ok := binlogSequence(checkpoint.Name) if !ok { return nil } return masterSeq - checkpointSeq } func binlogSequence(file string) (int64, bool) { index := strings.LastIndex(file, ".") if index < 0 || index == len(file)-1 { return 0, false } value, err := strconv.ParseInt(file[index+1:], 10, 64) return value, err == nil } func formatTime(value time.Time) string { if value.IsZero() { return "" } return value.Format(time.RFC3339) }