github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/http-stats.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "net/http" 22 "strings" 23 "sync" 24 "sync/atomic" 25 26 xhttp "github.com/minio/minio/internal/http" 27 "github.com/prometheus/client_golang/prometheus" 28 ) 29 30 // connStats - Network statistics 31 // Count total input/output transferred bytes during 32 // the server's life. 33 type connStats struct { 34 internodeInputBytes uint64 35 internodeOutputBytes uint64 36 s3InputBytes uint64 37 s3OutputBytes uint64 38 } 39 40 // Increase internode total input bytes 41 func (s *connStats) incInternodeInputBytes(n int64) { 42 atomic.AddUint64(&s.internodeInputBytes, uint64(n)) 43 } 44 45 // Increase internode total output bytes 46 func (s *connStats) incInternodeOutputBytes(n int64) { 47 atomic.AddUint64(&s.internodeOutputBytes, uint64(n)) 48 } 49 50 // Return internode total input bytes 51 func (s *connStats) getInternodeInputBytes() uint64 { 52 return atomic.LoadUint64(&s.internodeInputBytes) 53 } 54 55 // Return total output bytes 56 func (s *connStats) getInternodeOutputBytes() uint64 { 57 return atomic.LoadUint64(&s.internodeOutputBytes) 58 } 59 60 // Increase S3 total input bytes 61 func (s *connStats) incS3InputBytes(n int64) { 62 atomic.AddUint64(&s.s3InputBytes, uint64(n)) 63 } 64 65 // Increase S3 total output bytes 66 func (s *connStats) incS3OutputBytes(n int64) { 67 atomic.AddUint64(&s.s3OutputBytes, uint64(n)) 68 } 69 70 // Return S3 total input bytes 71 func (s *connStats) getS3InputBytes() uint64 { 72 return atomic.LoadUint64(&s.s3InputBytes) 73 } 74 75 // Return S3 total output bytes 76 func (s *connStats) getS3OutputBytes() uint64 { 77 return atomic.LoadUint64(&s.s3OutputBytes) 78 } 79 80 // Return connection stats (total input/output bytes and total s3 input/output bytes) 81 func (s *connStats) toServerConnStats() serverConnStats { 82 return serverConnStats{ 83 internodeInputBytes: s.getInternodeInputBytes(), // Traffic internode received 84 internodeOutputBytes: s.getInternodeOutputBytes(), // Traffic internode sent 85 s3InputBytes: s.getS3InputBytes(), // Traffic S3 received 86 s3OutputBytes: s.getS3OutputBytes(), // Traffic S3 sent 87 } 88 } 89 90 // Prepare new ConnStats structure 91 func newConnStats() *connStats { 92 return &connStats{} 93 } 94 95 type bucketS3RXTX struct { 96 s3InputBytes uint64 97 s3OutputBytes uint64 98 } 99 100 type bucketHTTPAPIStats struct { 101 currentS3Requests *HTTPAPIStats 102 totalS3Requests *HTTPAPIStats 103 totalS34xxErrors *HTTPAPIStats 104 totalS35xxErrors *HTTPAPIStats 105 totalS3Canceled *HTTPAPIStats 106 } 107 108 type bucketHTTPStats struct { 109 sync.RWMutex 110 httpStats map[string]bucketHTTPAPIStats 111 } 112 113 func newBucketHTTPStats() *bucketHTTPStats { 114 return &bucketHTTPStats{ 115 httpStats: make(map[string]bucketHTTPAPIStats), 116 } 117 } 118 119 func (bh *bucketHTTPStats) delete(bucket string) { 120 bh.Lock() 121 defer bh.Unlock() 122 123 delete(bh.httpStats, bucket) 124 } 125 126 func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.ResponseRecorder) { 127 if bh == nil { 128 return 129 } 130 131 if w != nil { 132 // Increment the prometheus http request response histogram with API, Bucket 133 bucketHTTPRequestsDuration.With(prometheus.Labels{ 134 "api": api, 135 "bucket": bucket, 136 }).Observe(w.TimeToFirstByte.Seconds()) 137 } 138 139 bh.Lock() 140 defer bh.Unlock() 141 142 hstats, ok := bh.httpStats[bucket] 143 if !ok { 144 hstats = bucketHTTPAPIStats{ 145 currentS3Requests: &HTTPAPIStats{}, 146 totalS3Requests: &HTTPAPIStats{}, 147 totalS3Canceled: &HTTPAPIStats{}, 148 totalS34xxErrors: &HTTPAPIStats{}, 149 totalS35xxErrors: &HTTPAPIStats{}, 150 } 151 } 152 153 if w == nil { // when response recorder nil, this is an active request 154 hstats.currentS3Requests.Inc(api) 155 bh.httpStats[bucket] = hstats 156 return 157 } // else { 158 hstats.currentS3Requests.Dec(api) // decrement this once we have the response recorder. 159 160 hstats.totalS3Requests.Inc(api) 161 code := w.StatusCode 162 163 switch { 164 case code == 0: 165 case code == 499: 166 // 499 is a good error, shall be counted as canceled. 167 hstats.totalS3Canceled.Inc(api) 168 case code >= http.StatusBadRequest: 169 if code >= http.StatusInternalServerError { 170 hstats.totalS35xxErrors.Inc(api) 171 } else { 172 hstats.totalS34xxErrors.Inc(api) 173 } 174 } 175 176 bh.httpStats[bucket] = hstats 177 } 178 179 func (bh *bucketHTTPStats) load(bucket string) bucketHTTPAPIStats { 180 if bh == nil { 181 return bucketHTTPAPIStats{ 182 currentS3Requests: &HTTPAPIStats{}, 183 totalS3Requests: &HTTPAPIStats{}, 184 totalS3Canceled: &HTTPAPIStats{}, 185 totalS34xxErrors: &HTTPAPIStats{}, 186 totalS35xxErrors: &HTTPAPIStats{}, 187 } 188 } 189 190 bh.RLock() 191 defer bh.RUnlock() 192 193 val, ok := bh.httpStats[bucket] 194 if ok { 195 return val 196 } 197 198 return bucketHTTPAPIStats{ 199 currentS3Requests: &HTTPAPIStats{}, 200 totalS3Requests: &HTTPAPIStats{}, 201 totalS3Canceled: &HTTPAPIStats{}, 202 totalS34xxErrors: &HTTPAPIStats{}, 203 totalS35xxErrors: &HTTPAPIStats{}, 204 } 205 } 206 207 type bucketConnStats struct { 208 sync.RWMutex 209 stats map[string]*bucketS3RXTX 210 } 211 212 func newBucketConnStats() *bucketConnStats { 213 return &bucketConnStats{ 214 stats: make(map[string]*bucketS3RXTX), 215 } 216 } 217 218 // Increase S3 total input bytes for input bucket 219 func (s *bucketConnStats) incS3InputBytes(bucket string, n int64) { 220 s.Lock() 221 defer s.Unlock() 222 stats, ok := s.stats[bucket] 223 if !ok { 224 stats = &bucketS3RXTX{ 225 s3InputBytes: uint64(n), 226 } 227 } else { 228 stats.s3InputBytes += uint64(n) 229 } 230 s.stats[bucket] = stats 231 } 232 233 // Increase S3 total output bytes for input bucket 234 func (s *bucketConnStats) incS3OutputBytes(bucket string, n int64) { 235 s.Lock() 236 defer s.Unlock() 237 stats, ok := s.stats[bucket] 238 if !ok { 239 stats = &bucketS3RXTX{ 240 s3OutputBytes: uint64(n), 241 } 242 } else { 243 stats.s3OutputBytes += uint64(n) 244 } 245 s.stats[bucket] = stats 246 } 247 248 type inOutBytes struct { 249 In uint64 250 Out uint64 251 } 252 253 // Return S3 total input bytes for input bucket 254 func (s *bucketConnStats) getS3InOutBytes() map[string]inOutBytes { 255 s.RLock() 256 defer s.RUnlock() 257 258 if len(s.stats) == 0 { 259 return nil 260 } 261 262 bucketStats := make(map[string]inOutBytes, len(s.stats)) 263 for k, v := range s.stats { 264 bucketStats[k] = inOutBytes{ 265 In: v.s3InputBytes, 266 Out: v.s3OutputBytes, 267 } 268 } 269 return bucketStats 270 } 271 272 // Return S3 total input/output bytes for each 273 func (s *bucketConnStats) getBucketS3InOutBytes(buckets []string) map[string]inOutBytes { 274 s.RLock() 275 defer s.RUnlock() 276 277 if len(s.stats) == 0 || len(buckets) == 0 { 278 return nil 279 } 280 281 bucketStats := make(map[string]inOutBytes, len(buckets)) 282 for _, bucket := range buckets { 283 if stats, ok := s.stats[bucket]; ok { 284 bucketStats[bucket] = inOutBytes{ 285 In: stats.s3InputBytes, 286 Out: stats.s3OutputBytes, 287 } 288 } 289 } 290 291 return bucketStats 292 } 293 294 // delete metrics once bucket is deleted. 295 func (s *bucketConnStats) delete(bucket string) { 296 s.Lock() 297 defer s.Unlock() 298 299 delete(s.stats, bucket) 300 } 301 302 // HTTPAPIStats holds statistics information about 303 // a given API in the requests. 304 type HTTPAPIStats struct { 305 apiStats map[string]int 306 sync.RWMutex 307 } 308 309 // Inc increments the api stats counter. 310 func (stats *HTTPAPIStats) Inc(api string) { 311 if stats == nil { 312 return 313 } 314 stats.Lock() 315 defer stats.Unlock() 316 if stats.apiStats == nil { 317 stats.apiStats = make(map[string]int) 318 } 319 stats.apiStats[api]++ 320 } 321 322 // Dec increments the api stats counter. 323 func (stats *HTTPAPIStats) Dec(api string) { 324 if stats == nil { 325 return 326 } 327 stats.Lock() 328 defer stats.Unlock() 329 if val, ok := stats.apiStats[api]; ok && val > 0 { 330 stats.apiStats[api]-- 331 } 332 } 333 334 // Get returns the current counter on input API string 335 func (stats *HTTPAPIStats) Get(api string) int { 336 if stats == nil { 337 return 0 338 } 339 340 stats.RLock() 341 defer stats.RUnlock() 342 343 val, ok := stats.apiStats[api] 344 if ok { 345 return val 346 } 347 348 return 0 349 } 350 351 // Load returns the recorded stats. 352 func (stats *HTTPAPIStats) Load(toLower bool) map[string]int { 353 if stats == nil { 354 return map[string]int{} 355 } 356 357 stats.RLock() 358 defer stats.RUnlock() 359 360 apiStats := make(map[string]int, len(stats.apiStats)) 361 for k, v := range stats.apiStats { 362 if toLower { 363 k = strings.ToLower(k) 364 } 365 apiStats[k] = v 366 } 367 return apiStats 368 } 369 370 // HTTPStats holds statistics information about 371 // HTTP requests made by all clients 372 type HTTPStats struct { 373 s3RequestsInQueue int32 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG 374 _ int32 // For 64 bits alignment 375 s3RequestsIncoming uint64 376 rejectedRequestsAuth uint64 377 rejectedRequestsTime uint64 378 rejectedRequestsHeader uint64 379 rejectedRequestsInvalid uint64 380 currentS3Requests HTTPAPIStats 381 totalS3Requests HTTPAPIStats 382 totalS3Errors HTTPAPIStats 383 totalS34xxErrors HTTPAPIStats 384 totalS35xxErrors HTTPAPIStats 385 totalS3Canceled HTTPAPIStats 386 } 387 388 func (st *HTTPStats) loadRequestsInQueue() int32 { 389 return atomic.LoadInt32(&st.s3RequestsInQueue) 390 } 391 392 func (st *HTTPStats) addRequestsInQueue(i int32) { 393 atomic.AddInt32(&st.s3RequestsInQueue, i) 394 } 395 396 func (st *HTTPStats) incS3RequestsIncoming() { 397 // Golang automatically resets to zero if this overflows 398 atomic.AddUint64(&st.s3RequestsIncoming, 1) 399 } 400 401 // Converts http stats into struct to be sent back to the client. 402 func (st *HTTPStats) toServerHTTPStats(toLowerKeys bool) ServerHTTPStats { 403 serverStats := ServerHTTPStats{} 404 serverStats.S3RequestsIncoming = atomic.SwapUint64(&st.s3RequestsIncoming, 0) 405 serverStats.S3RequestsInQueue = atomic.LoadInt32(&st.s3RequestsInQueue) 406 serverStats.TotalS3RejectedAuth = atomic.LoadUint64(&st.rejectedRequestsAuth) 407 serverStats.TotalS3RejectedTime = atomic.LoadUint64(&st.rejectedRequestsTime) 408 serverStats.TotalS3RejectedHeader = atomic.LoadUint64(&st.rejectedRequestsHeader) 409 serverStats.TotalS3RejectedInvalid = atomic.LoadUint64(&st.rejectedRequestsInvalid) 410 serverStats.CurrentS3Requests = ServerHTTPAPIStats{ 411 APIStats: st.currentS3Requests.Load(toLowerKeys), 412 } 413 serverStats.TotalS3Requests = ServerHTTPAPIStats{ 414 APIStats: st.totalS3Requests.Load(toLowerKeys), 415 } 416 serverStats.TotalS3Errors = ServerHTTPAPIStats{ 417 APIStats: st.totalS3Errors.Load(toLowerKeys), 418 } 419 serverStats.TotalS34xxErrors = ServerHTTPAPIStats{ 420 APIStats: st.totalS34xxErrors.Load(toLowerKeys), 421 } 422 serverStats.TotalS35xxErrors = ServerHTTPAPIStats{ 423 APIStats: st.totalS35xxErrors.Load(toLowerKeys), 424 } 425 serverStats.TotalS3Canceled = ServerHTTPAPIStats{ 426 APIStats: st.totalS3Canceled.Load(toLowerKeys), 427 } 428 return serverStats 429 } 430 431 // Update statistics from http request and response data 432 func (st *HTTPStats) updateStats(api string, w *xhttp.ResponseRecorder) { 433 st.totalS3Requests.Inc(api) 434 435 // Increment the prometheus http request response histogram with appropriate label 436 httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TimeToFirstByte.Seconds()) 437 438 code := w.StatusCode 439 440 switch { 441 case code == 0: 442 case code == 499: 443 // 499 is a good error, shall be counted as canceled. 444 st.totalS3Canceled.Inc(api) 445 case code >= http.StatusBadRequest: 446 st.totalS3Errors.Inc(api) 447 if code >= http.StatusInternalServerError { 448 st.totalS35xxErrors.Inc(api) 449 } else { 450 st.totalS34xxErrors.Inc(api) 451 } 452 } 453 } 454 455 // Prepare new HTTPStats structure 456 func newHTTPStats() *HTTPStats { 457 return &HTTPStats{} 458 }