344 lines
8.4 KiB
Go
344 lines
8.4 KiB
Go
// Filename: internal/service/db_log_writer_service.go
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"gemini-balancer/internal/models"
|
|
"gemini-balancer/internal/settings"
|
|
"gemini-balancer/internal/store"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type DBLogWriterService struct {
|
|
db *gorm.DB
|
|
store store.Store
|
|
logger *logrus.Entry
|
|
settingsManager *settings.SettingsManager
|
|
|
|
logBuffer chan *models.RequestLog
|
|
stopChan chan struct{}
|
|
wg sync.WaitGroup
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
// 统计指标
|
|
totalReceived atomic.Uint64
|
|
totalFlushed atomic.Uint64
|
|
totalDropped atomic.Uint64
|
|
flushCount atomic.Uint64
|
|
lastFlushTime time.Time
|
|
lastFlushMutex sync.RWMutex
|
|
}
|
|
|
|
func NewDBLogWriterService(
|
|
db *gorm.DB,
|
|
s store.Store,
|
|
settingsManager *settings.SettingsManager,
|
|
logger *logrus.Logger,
|
|
) *DBLogWriterService {
|
|
cfg := settingsManager.GetSettings()
|
|
bufferCapacity := cfg.LogBufferCapacity
|
|
if bufferCapacity <= 0 {
|
|
bufferCapacity = 1000
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &DBLogWriterService{
|
|
db: db,
|
|
store: s,
|
|
settingsManager: settingsManager,
|
|
logger: logger.WithField("component", "DBLogWriter📝"),
|
|
logBuffer: make(chan *models.RequestLog, bufferCapacity),
|
|
stopChan: make(chan struct{}),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
lastFlushTime: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (s *DBLogWriterService) Start() {
|
|
s.wg.Add(2)
|
|
go s.eventListenerLoop()
|
|
go s.dbWriterLoop()
|
|
|
|
// 定期输出统计信息
|
|
s.wg.Add(1)
|
|
go s.metricsReporter()
|
|
|
|
s.logger.WithFields(logrus.Fields{
|
|
"buffer_capacity": cap(s.logBuffer),
|
|
}).Info("DBLogWriterService started")
|
|
}
|
|
|
|
func (s *DBLogWriterService) Stop() {
|
|
s.logger.Info("DBLogWriterService stopping...")
|
|
close(s.stopChan)
|
|
s.cancel() // 取消上下文
|
|
s.wg.Wait()
|
|
|
|
// 输出最终统计
|
|
s.logger.WithFields(logrus.Fields{
|
|
"total_received": s.totalReceived.Load(),
|
|
"total_flushed": s.totalFlushed.Load(),
|
|
"total_dropped": s.totalDropped.Load(),
|
|
"flush_count": s.flushCount.Load(),
|
|
}).Info("DBLogWriterService stopped")
|
|
}
|
|
|
|
// 事件监听循环
|
|
func (s *DBLogWriterService) eventListenerLoop() {
|
|
defer s.wg.Done()
|
|
|
|
sub, err := s.store.Subscribe(s.ctx, models.TopicRequestFinished)
|
|
if err != nil {
|
|
s.logger.WithError(err).Error("Failed to subscribe to request events, log writing disabled")
|
|
return
|
|
}
|
|
defer func() {
|
|
if err := sub.Close(); err != nil {
|
|
s.logger.WithError(err).Warn("Failed to close subscription")
|
|
}
|
|
}()
|
|
|
|
s.logger.Info("Subscribed to request events for database logging")
|
|
|
|
for {
|
|
select {
|
|
case msg := <-sub.Channel():
|
|
s.handleMessage(msg)
|
|
|
|
case <-s.stopChan:
|
|
s.logger.Info("Event listener loop stopping")
|
|
close(s.logBuffer)
|
|
return
|
|
|
|
case <-s.ctx.Done():
|
|
s.logger.Info("Event listener context cancelled")
|
|
close(s.logBuffer)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// 处理单条消息
|
|
func (s *DBLogWriterService) handleMessage(msg *store.Message) {
|
|
var event models.RequestFinishedEvent
|
|
if err := json.Unmarshal(msg.Payload, &event); err != nil {
|
|
s.logger.WithError(err).Error("Failed to unmarshal request event")
|
|
return
|
|
}
|
|
|
|
s.totalReceived.Add(1)
|
|
|
|
select {
|
|
case s.logBuffer <- &event.RequestLog:
|
|
// 成功入队
|
|
default:
|
|
// 缓冲区满,丢弃日志
|
|
dropped := s.totalDropped.Add(1)
|
|
if dropped%100 == 1 { // 每100条丢失输出一次警告
|
|
s.logger.WithFields(logrus.Fields{
|
|
"total_dropped": dropped,
|
|
"buffer_capacity": cap(s.logBuffer),
|
|
"buffer_len": len(s.logBuffer),
|
|
}).Warn("Log buffer full, messages being dropped")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 数据库写入循环
|
|
func (s *DBLogWriterService) dbWriterLoop() {
|
|
defer s.wg.Done()
|
|
|
|
cfg := s.settingsManager.GetSettings()
|
|
batchSize := cfg.LogFlushBatchSize
|
|
if batchSize <= 0 {
|
|
batchSize = 100
|
|
}
|
|
flushInterval := time.Duration(cfg.LogFlushIntervalSeconds) * time.Second
|
|
if flushInterval <= 0 {
|
|
flushInterval = 5 * time.Second
|
|
}
|
|
|
|
s.logger.WithFields(logrus.Fields{
|
|
"batch_size": batchSize,
|
|
"flush_interval": flushInterval,
|
|
}).Info("DB writer loop started")
|
|
|
|
batch := make([]*models.RequestLog, 0, batchSize)
|
|
ticker := time.NewTicker(flushInterval)
|
|
defer ticker.Stop()
|
|
|
|
// 配置热更新检查(每分钟)
|
|
configTicker := time.NewTicker(1 * time.Minute)
|
|
defer configTicker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case logEntry, ok := <-s.logBuffer:
|
|
if !ok {
|
|
// 通道关闭,刷新剩余日志
|
|
if len(batch) > 0 {
|
|
s.flushBatch(batch)
|
|
}
|
|
s.logger.Info("DB writer loop finished")
|
|
return
|
|
}
|
|
|
|
batch = append(batch, logEntry)
|
|
if len(batch) >= batchSize {
|
|
s.flushBatch(batch)
|
|
batch = make([]*models.RequestLog, 0, batchSize)
|
|
}
|
|
|
|
case <-ticker.C:
|
|
if len(batch) > 0 {
|
|
s.flushBatch(batch)
|
|
batch = make([]*models.RequestLog, 0, batchSize)
|
|
}
|
|
|
|
case <-configTicker.C:
|
|
// 热更新配置
|
|
cfg := s.settingsManager.GetSettings()
|
|
newBatchSize := cfg.LogFlushBatchSize
|
|
if newBatchSize <= 0 {
|
|
newBatchSize = 100
|
|
}
|
|
newFlushInterval := time.Duration(cfg.LogFlushIntervalSeconds) * time.Second
|
|
if newFlushInterval <= 0 {
|
|
newFlushInterval = 5 * time.Second
|
|
}
|
|
|
|
if newBatchSize != batchSize {
|
|
s.logger.WithFields(logrus.Fields{
|
|
"old": batchSize,
|
|
"new": newBatchSize,
|
|
}).Info("Batch size updated")
|
|
batchSize = newBatchSize
|
|
if len(batch) >= batchSize {
|
|
s.flushBatch(batch)
|
|
batch = make([]*models.RequestLog, 0, batchSize)
|
|
}
|
|
}
|
|
|
|
if newFlushInterval != flushInterval {
|
|
s.logger.WithFields(logrus.Fields{
|
|
"old": flushInterval,
|
|
"new": newFlushInterval,
|
|
}).Info("Flush interval updated")
|
|
flushInterval = newFlushInterval
|
|
ticker.Reset(flushInterval)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 批量刷写到数据库
|
|
func (s *DBLogWriterService) flushBatch(batch []*models.RequestLog) {
|
|
if len(batch) == 0 {
|
|
return
|
|
}
|
|
|
|
start := time.Now()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
err := s.db.WithContext(ctx).CreateInBatches(batch, len(batch)).Error
|
|
duration := time.Since(start)
|
|
|
|
s.lastFlushMutex.Lock()
|
|
s.lastFlushTime = time.Now()
|
|
s.lastFlushMutex.Unlock()
|
|
|
|
if err != nil {
|
|
s.logger.WithFields(logrus.Fields{
|
|
"batch_size": len(batch),
|
|
"duration": duration,
|
|
}).WithError(err).Error("Failed to flush log batch to database")
|
|
} else {
|
|
flushed := s.totalFlushed.Add(uint64(len(batch)))
|
|
flushCount := s.flushCount.Add(1)
|
|
|
|
// 只在慢写入或大批量时输出日志
|
|
if duration > 1*time.Second || len(batch) > 500 {
|
|
s.logger.WithFields(logrus.Fields{
|
|
"batch_size": len(batch),
|
|
"duration": duration,
|
|
"total_flushed": flushed,
|
|
"flush_count": flushCount,
|
|
}).Info("Log batch flushed to database")
|
|
} else {
|
|
s.logger.WithFields(logrus.Fields{
|
|
"batch_size": len(batch),
|
|
"duration": duration,
|
|
}).Debug("Log batch flushed to database")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 定期输出统计信息
|
|
func (s *DBLogWriterService) metricsReporter() {
|
|
defer s.wg.Done()
|
|
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
s.reportMetrics()
|
|
case <-s.stopChan:
|
|
return
|
|
case <-s.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *DBLogWriterService) reportMetrics() {
|
|
s.lastFlushMutex.RLock()
|
|
lastFlush := s.lastFlushTime
|
|
s.lastFlushMutex.RUnlock()
|
|
|
|
received := s.totalReceived.Load()
|
|
flushed := s.totalFlushed.Load()
|
|
dropped := s.totalDropped.Load()
|
|
pending := uint64(len(s.logBuffer))
|
|
|
|
s.logger.WithFields(logrus.Fields{
|
|
"received": received,
|
|
"flushed": flushed,
|
|
"dropped": dropped,
|
|
"pending": pending,
|
|
"flush_count": s.flushCount.Load(),
|
|
"last_flush": time.Since(lastFlush).Round(time.Second),
|
|
"buffer_usage": float64(pending) / float64(cap(s.logBuffer)) * 100,
|
|
"success_rate": float64(flushed) / float64(received) * 100,
|
|
}).Info("DBLogWriter metrics")
|
|
}
|
|
|
|
// GetMetrics 返回当前统计指标(供监控使用)
|
|
func (s *DBLogWriterService) GetMetrics() map[string]interface{} {
|
|
s.lastFlushMutex.RLock()
|
|
lastFlush := s.lastFlushTime
|
|
s.lastFlushMutex.RUnlock()
|
|
|
|
return map[string]interface{}{
|
|
"total_received": s.totalReceived.Load(),
|
|
"total_flushed": s.totalFlushed.Load(),
|
|
"total_dropped": s.totalDropped.Load(),
|
|
"flush_count": s.flushCount.Load(),
|
|
"buffer_pending": len(s.logBuffer),
|
|
"buffer_capacity": cap(s.logBuffer),
|
|
"last_flush_ago": time.Since(lastFlush).Seconds(),
|
|
}
|
|
}
|