2026-05-23 12:27:44 +08:00

174 lines
4.1 KiB
Go

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 || master.Pos < checkpoint.Pos {
return nil
}
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)
}