@@ -78,6 +78,12 @@ type MetricsCollector struct {
78
78
maxMemoryBytes int64
79
79
currentMemoryBytes int64
80
80
privacyLevel int
81
+
82
+ // Caching for expensive calculations
83
+ cacheMutex sync.RWMutex
84
+ cachedMetrics map [string ]interface {}
85
+ cacheLastUpdate time.Time
86
+ cacheTTL time.Duration
81
87
}
82
88
83
89
// QueryLogEntry - Entry for the query log
@@ -112,6 +118,12 @@ type MonitoringUI struct {
112
118
clients map [* websocket.Conn ]bool
113
119
clientsMutex sync.Mutex
114
120
proxy * Proxy
121
+
122
+ // WebSocket broadcast rate limiting
123
+ broadcastMutex sync.Mutex
124
+ lastBroadcast time.Time
125
+ broadcastMinDelay time.Duration
126
+ pendingBroadcast bool
115
127
}
116
128
117
129
// NewMonitoringUI - Creates a new monitoring UI
@@ -145,6 +157,9 @@ func NewMonitoringUI(proxy *Proxy) *MonitoringUI {
145
157
maxMemoryBytes : int64 (maxMemoryMB * 1024 * 1024 ),
146
158
currentMemoryBytes : 0 ,
147
159
privacyLevel : proxy .monitoringUI .PrivacyLevel ,
160
+ // Initialize caching with 1 second TTL
161
+ cacheTTL : time .Second ,
162
+ cachedMetrics : make (map [string ]interface {}),
148
163
}
149
164
150
165
dlog .Debugf ("Metrics collector initialized with privacy level: %d" , metricsCollector .privacyLevel )
@@ -173,6 +188,8 @@ func NewMonitoringUI(proxy *Proxy) *MonitoringUI {
173
188
},
174
189
clients : make (map [* websocket.Conn ]bool ),
175
190
proxy : proxy ,
191
+ // Initialize broadcast rate limiting with 100ms minimum delay
192
+ broadcastMinDelay : 100 * time .Millisecond ,
176
193
}
177
194
}
178
195
@@ -265,6 +282,9 @@ func (ui *MonitoringUI) UpdateMetrics(pluginsState PluginsState, msg *dns.Msg, s
265
282
}
266
283
mc .countersMutex .Unlock ()
267
284
285
+ // Invalidate cache since counters changed
286
+ mc .invalidateCache ()
287
+
268
288
// Update query types - separate lock
269
289
if msg != nil && len (msg .Question ) > 0 {
270
290
question := msg .Question [0 ]
@@ -392,14 +412,31 @@ func (ui *MonitoringUI) UpdateMetrics(pluginsState PluginsState, msg *dns.Msg, s
392
412
mc .queryLogMutex .Unlock ()
393
413
}
394
414
395
- // Broadcast updates to WebSocket clients
396
- go ui .broadcastMetrics ()
415
+ // Broadcast updates to WebSocket clients (rate limited)
416
+ ui .scheduleBroadcast ()
417
+ }
418
+
419
+ // invalidateCache - Marks the cache as stale (call when data changes)
420
+ func (mc * MetricsCollector ) invalidateCache () {
421
+ mc .cacheMutex .Lock ()
422
+ mc .cacheLastUpdate = time.Time {} // Zero time to force refresh
423
+ mc .cacheMutex .Unlock ()
397
424
}
398
425
399
426
// GetMetrics - Returns the current metrics
400
427
func (mc * MetricsCollector ) GetMetrics () map [string ]interface {} {
401
428
dlog .Debugf ("GetMetrics called" )
402
429
430
+ // Check cache first
431
+ mc .cacheMutex .RLock ()
432
+ if time .Since (mc .cacheLastUpdate ) < mc .cacheTTL && mc .cachedMetrics != nil {
433
+ cached := mc .cachedMetrics
434
+ mc .cacheMutex .RUnlock ()
435
+ dlog .Debugf ("Returning cached metrics" )
436
+ return cached
437
+ }
438
+ mc .cacheMutex .RUnlock ()
439
+
403
440
// Read basic counters first
404
441
mc .countersMutex .RLock ()
405
442
totalQueries := mc .totalQueries
@@ -551,8 +588,8 @@ func (mc *MetricsCollector) GetMetrics() map[string]interface{} {
551
588
copy (recentQueries , mc .recentQueries )
552
589
mc .queryLogMutex .RUnlock ()
553
590
554
- // Return all metrics
555
- return map [string ]interface {}{
591
+ // Return all metrics and cache the result
592
+ metrics := map [string ]interface {}{
556
593
"total_queries" : totalQueries ,
557
594
"queries_per_second" : queriesPerSecond ,
558
595
"uptime_seconds" : time .Since (startTime ).Seconds (),
@@ -566,22 +603,44 @@ func (mc *MetricsCollector) GetMetrics() map[string]interface{} {
566
603
"query_types" : queryTypesList ,
567
604
"recent_queries" : recentQueries ,
568
605
}
606
+
607
+ // Cache the computed metrics
608
+ mc .cacheMutex .Lock ()
609
+ mc .cachedMetrics = metrics
610
+ mc .cacheLastUpdate = time .Now ()
611
+ mc .cacheMutex .Unlock ()
612
+
613
+ dlog .Debugf ("Computed and cached new metrics" )
614
+ return metrics
569
615
}
570
616
571
617
// setCORSHeaders - Sets standard CORS headers for all responses
572
618
func setCORSHeaders (w http.ResponseWriter ) {
573
619
w .Header ().Set ("Access-Control-Allow-Origin" , "*" )
574
620
w .Header ().Set ("Access-Control-Allow-Methods" , "GET, OPTIONS" )
575
621
w .Header ().Set ("Access-Control-Allow-Headers" , "Content-Type" )
622
+ }
623
+
624
+ // setDynamicCacheHeaders - Sets cache headers for dynamic content (metrics, API)
625
+ func setDynamicCacheHeaders (w http.ResponseWriter ) {
576
626
w .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
577
627
w .Header ().Set ("Pragma" , "no-cache" )
578
628
w .Header ().Set ("Expires" , "0" )
579
629
}
580
630
631
+ // setStaticCacheHeaders - Sets cache headers for static content
632
+ func setStaticCacheHeaders (w http.ResponseWriter , maxAge int ) {
633
+ w .Header ().Set ("Cache-Control" , fmt .Sprintf ("public, max-age=%d" , maxAge ))
634
+ w .Header ().Set ("Expires" , time .Now ().Add (time .Duration (maxAge )* time .Second ).Format (http .TimeFormat ))
635
+ }
636
+
581
637
// handleTestQuery - Handles test query requests for debugging
582
638
func (ui * MonitoringUI ) handleTestQuery (w http.ResponseWriter , r * http.Request ) {
583
639
dlog .Debugf ("Adding test query" )
584
640
641
+ // Test queries modify state - no cache
642
+ setDynamicCacheHeaders (w )
643
+
585
644
// Create a fake DNS message
586
645
msg := & dns.Msg {}
587
646
msg .SetQuestion ("test.example.com." , dns .TypeA )
@@ -629,6 +688,9 @@ func (ui *MonitoringUI) handleRoot(w http.ResponseWriter, r *http.Request) {
629
688
630
689
// If this is a simple version request, return a simple page
631
690
if r .URL .Query ().Get ("simple" ) == "1" {
691
+ // Simple page has dynamic content - no cache
692
+ setDynamicCacheHeaders (w )
693
+
632
694
metrics := ui .metricsCollector .GetMetrics ()
633
695
634
696
// Create a simple HTML page with the metrics
@@ -647,7 +709,8 @@ func (ui *MonitoringUI) handleRoot(w http.ResponseWriter, r *http.Request) {
647
709
return
648
710
}
649
711
650
- // Serve the main dashboard page
712
+ // Serve the main dashboard page - cache for 5 minutes since template is static
713
+ setStaticCacheHeaders (w , 300 )
651
714
w .Header ().Set ("Content-Type" , "text/html" )
652
715
w .Write ([]byte (MainHTMLTemplate ))
653
716
}
@@ -656,8 +719,9 @@ func (ui *MonitoringUI) handleRoot(w http.ResponseWriter, r *http.Request) {
656
719
func (ui * MonitoringUI ) handleMetrics (w http.ResponseWriter , r * http.Request ) {
657
720
dlog .Debugf ("Received metrics request from %s" , r .RemoteAddr )
658
721
659
- // Set CORS headers
722
+ // Set CORS headers and dynamic cache headers for API
660
723
setCORSHeaders (w )
724
+ setDynamicCacheHeaders (w )
661
725
662
726
// Handle preflight OPTIONS request
663
727
if r .Method == "OPTIONS" {
@@ -837,6 +901,8 @@ func (ui *MonitoringUI) handleStatic(w http.ResponseWriter, r *http.Request) {
837
901
// handleStaticJS - Serves the JavaScript for the monitoring UI
838
902
func (ui * MonitoringUI ) handleStaticJS (w http.ResponseWriter , r * http.Request ) {
839
903
setCORSHeaders (w )
904
+ // JavaScript is static - cache for 1 hour
905
+ setStaticCacheHeaders (w , 3600 )
840
906
w .Header ().Set ("Content-Type" , "application/javascript" )
841
907
w .Write ([]byte (MonitoringJSContent ))
842
908
}
@@ -863,6 +929,40 @@ func (ui *MonitoringUI) basicAuthMiddleware(next http.Handler) http.Handler {
863
929
})
864
930
}
865
931
932
+ // scheduleBroadcast - Rate-limited scheduling of WebSocket broadcasts
933
+ func (ui * MonitoringUI ) scheduleBroadcast () {
934
+ ui .broadcastMutex .Lock ()
935
+ defer ui .broadcastMutex .Unlock ()
936
+
937
+ now := time .Now ()
938
+ timeSinceLastBroadcast := now .Sub (ui .lastBroadcast )
939
+
940
+ if timeSinceLastBroadcast >= ui .broadcastMinDelay {
941
+ // Enough time has passed, broadcast immediately
942
+ ui .lastBroadcast = now
943
+ ui .pendingBroadcast = false
944
+ go ui .broadcastMetrics ()
945
+ } else {
946
+ // Too soon, schedule a delayed broadcast if not already pending
947
+ if ! ui .pendingBroadcast {
948
+ ui .pendingBroadcast = true
949
+ delay := ui .broadcastMinDelay - timeSinceLastBroadcast
950
+ go func () {
951
+ time .Sleep (delay )
952
+ ui .broadcastMutex .Lock ()
953
+ if ui .pendingBroadcast {
954
+ ui .lastBroadcast = time .Now ()
955
+ ui .pendingBroadcast = false
956
+ ui .broadcastMutex .Unlock ()
957
+ ui .broadcastMetrics ()
958
+ } else {
959
+ ui .broadcastMutex .Unlock ()
960
+ }
961
+ }()
962
+ }
963
+ }
964
+ }
965
+
866
966
// broadcastMetrics - Broadcasts metrics to all connected WebSocket clients
867
967
func (ui * MonitoringUI ) broadcastMetrics () {
868
968
metrics := ui .metricsCollector .GetMetrics ()
0 commit comments