@@ -17,7 +17,15 @@ type Metrics struct {
1717 BytesReceived atomic.Uint64
1818 Errors atomic.Uint64
1919
20- LatencyByMethod sync.Map // map["user|bucket|method"]
20+ // LatencyByMethod sync.Map // map["user|bucket|method"]
21+
22+ // LatencyObs records a single request‐latency observation into the
23+ // `requestsDurationHistogram`, which is registered once at startup.
24+ // Because the histogram lives for the entire process life and is never
25+ // re‐initialized or cleared, its buckets continuously accumulate across
26+ // scrape intervals—ensuring a true cumulative histogram that Prometheus
27+ // can derive accurate rates and quantiles from.
28+ LatencyObs func (user string , tenant string , bucket string , method string , seconds float64 )
2129
2230 RequestsByMethod sync.Map // map[string]*atomic.Uint64
2331 RequestsByOperation sync.Map // map[string]*atomic.Uint64
@@ -39,8 +47,17 @@ type Metrics struct {
3947
4048}
4149
42- func NewMetrics () * Metrics {
43- return & Metrics {}
50+ func NewMetrics (obs ... func (user string , tenant string , bucket string , method string , seconds float64 )) * Metrics {
51+ var cb func (user , tenant , bucket , method string , seconds float64 )
52+ if len (obs ) > 0 {
53+ cb = obs [0 ]
54+ } else {
55+ // default no‐op so nobody ever has a nil-pointer
56+ cb = func (_ , _ , _ , _ string , _ float64 ) {}
57+ }
58+ return & Metrics {
59+ LatencyObs : cb ,
60+ }
4461}
4562
4663// Convert metrics to a JSON-friendly struct
@@ -115,11 +132,6 @@ func (m *Metrics) ToJSON(metricsConfig *MetricsConfig) ([]byte, error) {
115132 data ["errors_by_ip_and_bucket" ] = loadSyncMap (& m .ErrorsByIPAndBucket )
116133 }
117134
118- // Latency Tracking
119- if metricsConfig .TrackLatencyByMethod {
120- data ["latency_by_method" ] = loadSyncMap (& m .LatencyByMethod )
121- }
122-
123135 return json .Marshal (data )
124136}
125137
@@ -223,16 +235,15 @@ func (m *Metrics) Update(logEntry S3OperationLog, metricsConfig *MetricsConfig)
223235
224236 // Latency Tracking
225237 if logEntry .TotalTime > 0 {
226- latencyMs := uint64 (logEntry .TotalTime )
227-
228238 if metricsConfig .TrackLatencyByMethod ||
229239 metricsConfig .TrackLatencyByBucket ||
230240 metricsConfig .TrackLatencyByTenant ||
231241 metricsConfig .TrackLatencyByUser ||
232242 metricsConfig .TrackLatencyByBucketAndMethod {
233- // Key format: "user|bucket|method"
234- latencyKey := logEntry .User + "|" + logEntry .Bucket + "|" + method
235- incrementSyncMapValue (& m .LatencyByMethod , latencyKey , latencyMs )
243+
244+ latencySec := float64 (logEntry .TotalTime ) / 1000.0
245+ userStr , tenantStr := extractUserAndTenant (logEntry .User )
246+ m .LatencyObs (userStr , tenantStr , logEntry .Bucket , method , latencySec )
236247 }
237248 }
238249}
@@ -244,7 +255,6 @@ func (m *Metrics) Reset() {
244255 m .BytesReceived .Store (0 )
245256 m .Errors .Store (0 )
246257
247- resetSyncMap (& m .LatencyByMethod )
248258 resetSyncMap (& m .RequestsByMethod )
249259 resetSyncMap (& m .RequestsByOperation )
250260 resetSyncMap (& m .RequestsByStatusCode )
@@ -263,10 +273,6 @@ func (m *Metrics) Reset() {
263273 resetSyncMap (& m .ErrorsByIPAndBucket )
264274}
265275
266- func (m * Metrics ) ResetPerWindowMetrics () {
267- resetSyncMap (& m .LatencyByMethod )
268- }
269-
270276// Helper function: Update max atomic value
271277func updateMaxAtomic (target * atomic.Uint64 , value uint64 ) {
272278 for {
@@ -398,14 +404,13 @@ func ExtractHTTPMethod(uri string) string {
398404
399405// Clone creates a deep copy of the Metrics
400406func (m * Metrics ) Clone () * Metrics {
401- clone := NewMetrics ()
407+ clone := NewMetrics (m . LatencyObs )
402408
403409 clone .TotalRequests .Store (m .TotalRequests .Load ())
404410 clone .BytesSent .Store (m .BytesSent .Load ())
405411 clone .BytesReceived .Store (m .BytesReceived .Load ())
406412 clone .Errors .Store (m .Errors .Load ())
407413
408- copySyncMap (& m .LatencyByMethod , & clone .LatencyByMethod )
409414 copySyncMap (& m .RequestsByMethod , & clone .RequestsByMethod )
410415 copySyncMap (& m .RequestsByOperation , & clone .RequestsByOperation )
411416 copySyncMap (& m .RequestsByStatusCode , & clone .RequestsByStatusCode )
@@ -440,7 +445,7 @@ func copySyncMap(src, dst *sync.Map) {
440445
441446// SubtractMetrics calculates the delta between two metrics objects: total - previous
442447func SubtractMetrics (total , previous * Metrics ) * Metrics {
443- delta := NewMetrics ()
448+ delta := NewMetrics (total . LatencyObs )
444449
445450 // Handle top-level counters
446451 delta .TotalRequests .Store (diff (total .TotalRequests .Load (), previous .TotalRequests .Load ()))
@@ -465,7 +470,6 @@ func SubtractMetrics(total, previous *Metrics) *Metrics {
465470 subtractSyncMap (& total .BytesReceivedByIP , & previous .BytesReceivedByIP , & delta .BytesReceivedByIP )
466471 subtractSyncMap (& total .ErrorsByUserAndBucket , & previous .ErrorsByUserAndBucket , & delta .ErrorsByUserAndBucket )
467472 subtractSyncMap (& total .ErrorsByIPAndBucket , & previous .ErrorsByIPAndBucket , & delta .ErrorsByIPAndBucket )
468- subtractSyncMap (& total .LatencyByMethod , & previous .LatencyByMethod , & delta .LatencyByMethod )
469473
470474 return delta
471475}
0 commit comments