refactor(audio): centralize metrics collection with new registry

Introduce MetricsRegistry to serve as single source of truth for audio metrics
Remove duplicate metrics collection logic from web endpoints
Add callback mechanism for metrics updates
This commit is contained in:
Alex P 2025-08-28 10:01:35 +00:00
parent fe4571956d
commit 0d4176cf98
4 changed files with 161 additions and 34 deletions

View File

@ -1,5 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_192.168.100.214 FALSE / FALSE 1756968962 authToken 3b0b77eb-3771-4eb2-9704-ffcdf3ba788b

View File

@ -721,26 +721,17 @@ func GetLastMetricsUpdate() time.Time {
// StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics // StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics
func StartMetricsUpdater() { func StartMetricsUpdater() {
// Start the centralized metrics collector
registry := GetMetricsRegistry()
registry.StartMetricsCollector()
// Start a separate goroutine for periodic updates
go func() { go func() {
ticker := time.NewTicker(5 * time.Second) // Update every 5 seconds ticker := time.NewTicker(5 * time.Second) // Update every 5 seconds
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
// Update audio output metrics // Update memory metrics (not part of centralized registry)
audioMetrics := GetAudioMetrics()
UpdateAudioMetrics(convertAudioMetricsToUnified(audioMetrics))
// Update microphone input metrics
micMetrics := GetAudioInputMetrics()
UpdateMicrophoneMetrics(convertAudioInputMetricsToUnified(micMetrics))
// Update audio configuration metrics
audioConfig := GetAudioConfig()
UpdateAudioConfigMetrics(audioConfig)
micConfig := GetMicrophoneConfig()
UpdateMicrophoneConfigMetrics(micConfig)
// Update memory metrics
UpdateMemoryMetrics() UpdateMemoryMetrics()
} }
}() }()

View File

@ -0,0 +1,151 @@
//go:build cgo
package audio
import (
"sync"
"sync/atomic"
"time"
)
// MetricsRegistry provides a centralized source of truth for all audio metrics
// This eliminates duplication between session-specific and global managers
type MetricsRegistry struct {
mu sync.RWMutex
audioMetrics AudioMetrics
audioInputMetrics AudioInputMetrics
audioConfig AudioConfig
microphoneConfig AudioConfig
lastUpdate int64 // Unix timestamp
}
var (
globalMetricsRegistry *MetricsRegistry
registryOnce sync.Once
)
// GetMetricsRegistry returns the global metrics registry instance
func GetMetricsRegistry() *MetricsRegistry {
registryOnce.Do(func() {
globalMetricsRegistry = &MetricsRegistry{
lastUpdate: time.Now().Unix(),
}
})
return globalMetricsRegistry
}
// UpdateAudioMetrics updates the centralized audio output metrics
func (mr *MetricsRegistry) UpdateAudioMetrics(metrics AudioMetrics) {
mr.mu.Lock()
mr.audioMetrics = metrics
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly to avoid circular dependency
UpdateAudioMetrics(convertAudioMetricsToUnified(metrics))
}
// UpdateAudioInputMetrics updates the centralized audio input metrics
func (mr *MetricsRegistry) UpdateAudioInputMetrics(metrics AudioInputMetrics) {
mr.mu.Lock()
mr.audioInputMetrics = metrics
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly to avoid circular dependency
UpdateMicrophoneMetrics(convertAudioInputMetricsToUnified(metrics))
}
// UpdateAudioConfig updates the centralized audio configuration
func (mr *MetricsRegistry) UpdateAudioConfig(config AudioConfig) {
mr.mu.Lock()
mr.audioConfig = config
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly
UpdateAudioConfigMetrics(config)
}
// UpdateMicrophoneConfig updates the centralized microphone configuration
func (mr *MetricsRegistry) UpdateMicrophoneConfig(config AudioConfig) {
mr.mu.Lock()
mr.microphoneConfig = config
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly
UpdateMicrophoneConfigMetrics(config)
}
// GetAudioMetrics returns the current audio output metrics
func (mr *MetricsRegistry) GetAudioMetrics() AudioMetrics {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.audioMetrics
}
// GetAudioInputMetrics returns the current audio input metrics
func (mr *MetricsRegistry) GetAudioInputMetrics() AudioInputMetrics {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.audioInputMetrics
}
// GetAudioConfig returns the current audio configuration
func (mr *MetricsRegistry) GetAudioConfig() AudioConfig {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.audioConfig
}
// GetMicrophoneConfig returns the current microphone configuration
func (mr *MetricsRegistry) GetMicrophoneConfig() AudioConfig {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.microphoneConfig
}
// GetLastUpdate returns the timestamp of the last metrics update
func (mr *MetricsRegistry) GetLastUpdate() time.Time {
timestamp := atomic.LoadInt64(&mr.lastUpdate)
return time.Unix(timestamp, 0)
}
// StartMetricsCollector starts a background goroutine to collect metrics
func (mr *MetricsRegistry) StartMetricsCollector() {
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
// Collect from session-specific manager if available
if sessionProvider := GetSessionProvider(); sessionProvider != nil && sessionProvider.IsSessionActive() {
if inputManager := sessionProvider.GetAudioInputManager(); inputManager != nil {
metrics := inputManager.GetMetrics()
mr.UpdateAudioInputMetrics(metrics)
}
} else {
// Fallback to global manager if no session is active
globalManager := getAudioInputManager()
metrics := globalManager.GetMetrics()
mr.UpdateAudioInputMetrics(metrics)
}
// Collect audio output metrics directly from global metrics variable to avoid circular dependency
audioMetrics := AudioMetrics{
FramesReceived: atomic.LoadInt64(&metrics.FramesReceived),
FramesDropped: atomic.LoadInt64(&metrics.FramesDropped),
BytesProcessed: atomic.LoadInt64(&metrics.BytesProcessed),
ConnectionDrops: atomic.LoadInt64(&metrics.ConnectionDrops),
LastFrameTime: metrics.LastFrameTime,
AverageLatency: metrics.AverageLatency,
}
mr.UpdateAudioMetrics(audioMetrics)
// Collect configuration directly from global variables to avoid circular dependency
mr.UpdateAudioConfig(currentConfig)
mr.UpdateMicrophoneConfig(currentMicrophoneConfig)
}
}()
}

18
web.go
View File

@ -212,7 +212,8 @@ func setupRouter() *gin.Engine {
}) })
protected.GET("/audio/metrics", func(c *gin.Context) { protected.GET("/audio/metrics", func(c *gin.Context) {
metrics := audio.GetAudioMetrics() registry := audio.GetMetricsRegistry()
metrics := registry.GetAudioMetrics()
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"frames_received": metrics.FramesReceived, "frames_received": metrics.FramesReceived,
"frames_dropped": metrics.FramesDropped, "frames_dropped": metrics.FramesDropped,
@ -399,19 +400,8 @@ func setupRouter() *gin.Engine {
}) })
protected.GET("/microphone/metrics", func(c *gin.Context) { protected.GET("/microphone/metrics", func(c *gin.Context) {
if currentSession == nil || currentSession.AudioInputManager == nil { registry := audio.GetMetricsRegistry()
c.JSON(200, gin.H{ metrics := registry.GetAudioInputMetrics()
"frames_sent": 0,
"frames_dropped": 0,
"bytes_processed": 0,
"last_frame_time": "",
"connection_drops": 0,
"average_latency": "0.0ms",
})
return
}
metrics := currentSession.AudioInputManager.GetMetrics()
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"frames_sent": metrics.FramesSent, "frames_sent": metrics.FramesSent,
"frames_dropped": metrics.FramesDropped, "frames_dropped": metrics.FramesDropped,