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  }