From 0d4176cf98748a18481b5893c282980d30864836 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 28 Aug 2025 10:01:35 +0000 Subject: [PATCH] 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 --- cookies.txt | 5 - internal/audio/metrics.go | 21 ++-- internal/audio/metrics_registry.go | 151 +++++++++++++++++++++++++++++ web.go | 18 +--- 4 files changed, 161 insertions(+), 34 deletions(-) delete mode 100644 cookies.txt create mode 100644 internal/audio/metrics_registry.go diff --git a/cookies.txt b/cookies.txt deleted file mode 100644 index 3fb6bea..0000000 --- a/cookies.txt +++ /dev/null @@ -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 diff --git a/internal/audio/metrics.go b/internal/audio/metrics.go index d7d8b92..d3597f4 100644 --- a/internal/audio/metrics.go +++ b/internal/audio/metrics.go @@ -721,26 +721,17 @@ func GetLastMetricsUpdate() time.Time { // StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics func StartMetricsUpdater() { + // Start the centralized metrics collector + registry := GetMetricsRegistry() + registry.StartMetricsCollector() + + // Start a separate goroutine for periodic updates go func() { ticker := time.NewTicker(5 * time.Second) // Update every 5 seconds defer ticker.Stop() for range ticker.C { - // Update audio output metrics - 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 + // Update memory metrics (not part of centralized registry) UpdateMemoryMetrics() } }() diff --git a/internal/audio/metrics_registry.go b/internal/audio/metrics_registry.go new file mode 100644 index 0000000..7ecdffd --- /dev/null +++ b/internal/audio/metrics_registry.go @@ -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) + } + }() +} diff --git a/web.go b/web.go index 95822d9..60b1c04 100644 --- a/web.go +++ b/web.go @@ -212,7 +212,8 @@ func setupRouter() *gin.Engine { }) protected.GET("/audio/metrics", func(c *gin.Context) { - metrics := audio.GetAudioMetrics() + registry := audio.GetMetricsRegistry() + metrics := registry.GetAudioMetrics() c.JSON(200, gin.H{ "frames_received": metrics.FramesReceived, "frames_dropped": metrics.FramesDropped, @@ -399,19 +400,8 @@ func setupRouter() *gin.Engine { }) protected.GET("/microphone/metrics", func(c *gin.Context) { - if currentSession == nil || currentSession.AudioInputManager == nil { - c.JSON(200, gin.H{ - "frames_sent": 0, - "frames_dropped": 0, - "bytes_processed": 0, - "last_frame_time": "", - "connection_drops": 0, - "average_latency": "0.0ms", - }) - return - } - - metrics := currentSession.AudioInputManager.GetMetrics() + registry := audio.GetMetricsRegistry() + metrics := registry.GetAudioInputMetrics() c.JSON(200, gin.H{ "frames_sent": metrics.FramesSent, "frames_dropped": metrics.FramesDropped,