2021-04-18 15:41:13 -04:00
|
|
|
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
|
|
//
|
|
|
|
// This file is part of MinIO Object Storage stack
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2017-04-07 02:08:33 -04:00
|
|
|
|
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
2019-10-23 00:01:14 -04:00
|
|
|
"sync"
|
2020-05-04 01:35:40 -04:00
|
|
|
"sync/atomic"
|
2017-04-07 02:08:33 -04:00
|
|
|
|
2022-11-28 13:20:27 -05:00
|
|
|
xhttp "github.com/minio/minio/internal/http"
|
2018-04-18 19:01:42 -04:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2017-04-07 02:08:33 -04:00
|
|
|
)
|
|
|
|
|
2023-07-08 10:35:11 -04:00
|
|
|
// connStats - Network statistics
|
2017-04-07 02:08:33 -04:00
|
|
|
// Count total input/output transferred bytes during
|
|
|
|
// the server's life.
|
2023-07-08 10:35:11 -04:00
|
|
|
type connStats struct {
|
|
|
|
internodeInputBytes uint64
|
|
|
|
internodeOutputBytes uint64
|
|
|
|
s3InputBytes uint64
|
|
|
|
s3OutputBytes uint64
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
// Increase internode total input bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) incInternodeInputBytes(n int64) {
|
|
|
|
atomic.AddUint64(&s.internodeInputBytes, uint64(n))
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
// Increase internode total output bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) incInternodeOutputBytes(n int64) {
|
|
|
|
atomic.AddUint64(&s.internodeOutputBytes, uint64(n))
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
// Return internode total input bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) getInternodeInputBytes() uint64 {
|
|
|
|
return atomic.LoadUint64(&s.internodeInputBytes)
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return total output bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) getInternodeOutputBytes() uint64 {
|
|
|
|
return atomic.LoadUint64(&s.internodeOutputBytes)
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
// Increase S3 total input bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) incS3InputBytes(n int64) {
|
2020-05-04 01:35:40 -04:00
|
|
|
atomic.AddUint64(&s.s3InputBytes, uint64(n))
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
// Increase S3 total output bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) incS3OutputBytes(n int64) {
|
2020-05-04 01:35:40 -04:00
|
|
|
atomic.AddUint64(&s.s3OutputBytes, uint64(n))
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
// Return S3 total input bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) getS3InputBytes() uint64 {
|
2020-05-04 01:35:40 -04:00
|
|
|
return atomic.LoadUint64(&s.s3InputBytes)
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
// Return S3 total output bytes
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) getS3OutputBytes() uint64 {
|
2020-05-04 01:35:40 -04:00
|
|
|
return atomic.LoadUint64(&s.s3OutputBytes)
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return connection stats (total input/output bytes and total s3 input/output bytes)
|
2023-07-08 10:35:11 -04:00
|
|
|
func (s *connStats) toServerConnStats() serverConnStats {
|
|
|
|
return serverConnStats{
|
|
|
|
internodeInputBytes: s.getInternodeInputBytes(), // Traffic internode received
|
|
|
|
internodeOutputBytes: s.getInternodeOutputBytes(), // Traffic internode sent
|
|
|
|
s3InputBytes: s.getS3InputBytes(), // Traffic S3 received
|
|
|
|
s3OutputBytes: s.getS3OutputBytes(), // Traffic S3 sent
|
2017-04-21 10:15:53 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-07 02:08:33 -04:00
|
|
|
// Prepare new ConnStats structure
|
2023-07-08 10:35:11 -04:00
|
|
|
func newConnStats() *connStats {
|
|
|
|
return &connStats{}
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
type bucketS3RXTX struct {
|
|
|
|
s3InputBytes uint64
|
|
|
|
s3OutputBytes uint64
|
|
|
|
}
|
|
|
|
|
2023-06-21 12:41:59 -04:00
|
|
|
type bucketHTTPAPIStats struct {
|
|
|
|
currentS3Requests *HTTPAPIStats
|
|
|
|
totalS3Requests *HTTPAPIStats
|
|
|
|
totalS34xxErrors *HTTPAPIStats
|
|
|
|
totalS35xxErrors *HTTPAPIStats
|
|
|
|
totalS3Canceled *HTTPAPIStats
|
|
|
|
}
|
|
|
|
|
|
|
|
type bucketHTTPStats struct {
|
|
|
|
sync.RWMutex
|
|
|
|
httpStats map[string]bucketHTTPAPIStats
|
|
|
|
}
|
|
|
|
|
|
|
|
func newBucketHTTPStats() *bucketHTTPStats {
|
|
|
|
return &bucketHTTPStats{
|
|
|
|
httpStats: make(map[string]bucketHTTPAPIStats),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bh *bucketHTTPStats) delete(bucket string) {
|
|
|
|
bh.Lock()
|
|
|
|
defer bh.Unlock()
|
|
|
|
|
|
|
|
delete(bh.httpStats, bucket)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.ResponseRecorder) {
|
|
|
|
if bh == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
bh.Lock()
|
|
|
|
defer bh.Unlock()
|
|
|
|
|
|
|
|
hstats, ok := bh.httpStats[bucket]
|
|
|
|
if !ok {
|
|
|
|
hstats = bucketHTTPAPIStats{
|
|
|
|
currentS3Requests: &HTTPAPIStats{},
|
|
|
|
totalS3Requests: &HTTPAPIStats{},
|
|
|
|
totalS3Canceled: &HTTPAPIStats{},
|
|
|
|
totalS34xxErrors: &HTTPAPIStats{},
|
|
|
|
totalS35xxErrors: &HTTPAPIStats{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if w == nil { // when response recorder nil, this is an active request
|
|
|
|
hstats.currentS3Requests.Inc(api)
|
|
|
|
bh.httpStats[bucket] = hstats
|
|
|
|
return
|
|
|
|
} // else {
|
|
|
|
hstats.currentS3Requests.Dec(api) // decrement this once we have the response recorder.
|
|
|
|
|
|
|
|
hstats.totalS3Requests.Inc(api)
|
|
|
|
code := w.StatusCode
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case code == 0:
|
|
|
|
case code == 499:
|
|
|
|
// 499 is a good error, shall be counted as canceled.
|
|
|
|
hstats.totalS3Canceled.Inc(api)
|
|
|
|
case code >= http.StatusBadRequest:
|
|
|
|
if code >= http.StatusInternalServerError {
|
|
|
|
hstats.totalS35xxErrors.Inc(api)
|
|
|
|
} else {
|
|
|
|
hstats.totalS34xxErrors.Inc(api)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bh.httpStats[bucket] = hstats
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bh *bucketHTTPStats) load(bucket string) bucketHTTPAPIStats {
|
|
|
|
if bh == nil {
|
|
|
|
return bucketHTTPAPIStats{
|
|
|
|
currentS3Requests: &HTTPAPIStats{},
|
|
|
|
totalS3Requests: &HTTPAPIStats{},
|
|
|
|
totalS3Canceled: &HTTPAPIStats{},
|
|
|
|
totalS34xxErrors: &HTTPAPIStats{},
|
|
|
|
totalS35xxErrors: &HTTPAPIStats{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bh.RLock()
|
|
|
|
defer bh.RUnlock()
|
|
|
|
|
|
|
|
val, ok := bh.httpStats[bucket]
|
|
|
|
if ok {
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
|
|
|
return bucketHTTPAPIStats{
|
|
|
|
currentS3Requests: &HTTPAPIStats{},
|
|
|
|
totalS3Requests: &HTTPAPIStats{},
|
|
|
|
totalS3Canceled: &HTTPAPIStats{},
|
|
|
|
totalS34xxErrors: &HTTPAPIStats{},
|
|
|
|
totalS35xxErrors: &HTTPAPIStats{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-14 18:14:24 -04:00
|
|
|
type bucketConnStats struct {
|
|
|
|
sync.RWMutex
|
|
|
|
stats map[string]*bucketS3RXTX
|
|
|
|
}
|
|
|
|
|
|
|
|
func newBucketConnStats() *bucketConnStats {
|
|
|
|
return &bucketConnStats{
|
|
|
|
stats: make(map[string]*bucketS3RXTX),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Increase S3 total input bytes for input bucket
|
|
|
|
func (s *bucketConnStats) incS3InputBytes(bucket string, n int64) {
|
|
|
|
s.Lock()
|
|
|
|
defer s.Unlock()
|
|
|
|
stats, ok := s.stats[bucket]
|
|
|
|
if !ok {
|
|
|
|
stats = &bucketS3RXTX{
|
|
|
|
s3InputBytes: uint64(n),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
stats.s3InputBytes += uint64(n)
|
|
|
|
}
|
|
|
|
s.stats[bucket] = stats
|
|
|
|
}
|
|
|
|
|
|
|
|
// Increase S3 total output bytes for input bucket
|
|
|
|
func (s *bucketConnStats) incS3OutputBytes(bucket string, n int64) {
|
|
|
|
s.Lock()
|
|
|
|
defer s.Unlock()
|
|
|
|
stats, ok := s.stats[bucket]
|
|
|
|
if !ok {
|
|
|
|
stats = &bucketS3RXTX{
|
|
|
|
s3OutputBytes: uint64(n),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
stats.s3OutputBytes += uint64(n)
|
|
|
|
}
|
|
|
|
s.stats[bucket] = stats
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return S3 total input bytes for input bucket
|
|
|
|
func (s *bucketConnStats) getS3InputBytes(bucket string) uint64 {
|
|
|
|
s.RLock()
|
|
|
|
defer s.RUnlock()
|
|
|
|
|
|
|
|
stats := s.stats[bucket]
|
|
|
|
if stats == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return stats.s3InputBytes
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return S3 total output bytes
|
|
|
|
func (s *bucketConnStats) getS3OutputBytes(bucket string) uint64 {
|
|
|
|
s.RLock()
|
|
|
|
defer s.RUnlock()
|
|
|
|
|
|
|
|
stats := s.stats[bucket]
|
|
|
|
if stats == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return stats.s3OutputBytes
|
|
|
|
}
|
|
|
|
|
|
|
|
// delete metrics once bucket is deleted.
|
|
|
|
func (s *bucketConnStats) delete(bucket string) {
|
|
|
|
s.Lock()
|
|
|
|
defer s.Unlock()
|
|
|
|
|
|
|
|
delete(s.stats, bucket)
|
|
|
|
}
|
|
|
|
|
2019-10-23 00:01:14 -04:00
|
|
|
// HTTPAPIStats holds statistics information about
|
|
|
|
// a given API in the requests.
|
|
|
|
type HTTPAPIStats struct {
|
2020-01-16 04:35:30 -05:00
|
|
|
apiStats map[string]int
|
2019-10-23 00:01:14 -04:00
|
|
|
sync.RWMutex
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
2019-10-23 00:01:14 -04:00
|
|
|
// Inc increments the api stats counter.
|
|
|
|
func (stats *HTTPAPIStats) Inc(api string) {
|
|
|
|
if stats == nil {
|
|
|
|
return
|
|
|
|
}
|
2020-03-18 19:19:29 -04:00
|
|
|
stats.Lock()
|
|
|
|
defer stats.Unlock()
|
2020-01-16 04:35:30 -05:00
|
|
|
if stats.apiStats == nil {
|
|
|
|
stats.apiStats = make(map[string]int)
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
2020-03-18 19:19:29 -04:00
|
|
|
stats.apiStats[api]++
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
2017-04-07 02:08:33 -04:00
|
|
|
|
2019-10-23 00:01:14 -04:00
|
|
|
// Dec increments the api stats counter.
|
|
|
|
func (stats *HTTPAPIStats) Dec(api string) {
|
|
|
|
if stats == nil {
|
|
|
|
return
|
|
|
|
}
|
2020-03-18 19:19:29 -04:00
|
|
|
stats.Lock()
|
|
|
|
defer stats.Unlock()
|
2020-01-16 04:35:30 -05:00
|
|
|
if val, ok := stats.apiStats[api]; ok && val > 0 {
|
|
|
|
stats.apiStats[api]--
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
|
|
|
}
|
2017-04-07 02:08:33 -04:00
|
|
|
|
2023-06-21 12:41:59 -04:00
|
|
|
// Get returns the current counter on input API string
|
|
|
|
func (stats *HTTPAPIStats) Get(api string) int {
|
|
|
|
if stats == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
stats.RLock()
|
|
|
|
defer stats.RUnlock()
|
|
|
|
|
|
|
|
val, ok := stats.apiStats[api]
|
|
|
|
if ok {
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2019-10-23 00:01:14 -04:00
|
|
|
// Load returns the recorded stats.
|
|
|
|
func (stats *HTTPAPIStats) Load() map[string]int {
|
2023-06-21 12:41:59 -04:00
|
|
|
if stats == nil {
|
|
|
|
return map[string]int{}
|
|
|
|
}
|
|
|
|
|
|
|
|
stats.RLock()
|
|
|
|
defer stats.RUnlock()
|
|
|
|
|
2022-01-02 12:15:06 -05:00
|
|
|
apiStats := make(map[string]int, len(stats.apiStats))
|
2020-01-16 04:35:30 -05:00
|
|
|
for k, v := range stats.apiStats {
|
|
|
|
apiStats[k] = v
|
|
|
|
}
|
|
|
|
return apiStats
|
2019-10-23 00:01:14 -04:00
|
|
|
}
|
2017-04-07 02:08:33 -04:00
|
|
|
|
2019-10-23 00:01:14 -04:00
|
|
|
// HTTPStats holds statistics information about
|
|
|
|
// HTTP requests made by all clients
|
|
|
|
type HTTPStats struct {
|
2022-02-17 18:22:26 -05:00
|
|
|
s3RequestsInQueue int32 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
|
|
|
_ int32 // For 64 bits alignment
|
|
|
|
s3RequestsIncoming uint64
|
2021-06-23 10:15:43 -04:00
|
|
|
rejectedRequestsAuth uint64
|
|
|
|
rejectedRequestsTime uint64
|
|
|
|
rejectedRequestsHeader uint64
|
|
|
|
rejectedRequestsInvalid uint64
|
2021-03-31 02:19:36 -04:00
|
|
|
currentS3Requests HTTPAPIStats
|
|
|
|
totalS3Requests HTTPAPIStats
|
|
|
|
totalS3Errors HTTPAPIStats
|
2022-06-08 14:22:34 -04:00
|
|
|
totalS34xxErrors HTTPAPIStats
|
|
|
|
totalS35xxErrors HTTPAPIStats
|
2021-03-31 02:19:36 -04:00
|
|
|
totalS3Canceled HTTPAPIStats
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
2023-02-27 11:34:52 -05:00
|
|
|
func (st *HTTPStats) loadRequestsInQueue() int32 {
|
|
|
|
return atomic.LoadInt32(&st.s3RequestsInQueue)
|
|
|
|
}
|
|
|
|
|
2021-02-20 03:21:55 -05:00
|
|
|
func (st *HTTPStats) addRequestsInQueue(i int32) {
|
|
|
|
atomic.AddInt32(&st.s3RequestsInQueue, i)
|
|
|
|
}
|
|
|
|
|
2022-02-07 19:30:14 -05:00
|
|
|
func (st *HTTPStats) incS3RequestsIncoming() {
|
|
|
|
// Golang automatically resets to zero if this overflows
|
|
|
|
atomic.AddUint64(&st.s3RequestsIncoming, 1)
|
|
|
|
}
|
|
|
|
|
2017-04-07 02:08:33 -04:00
|
|
|
// Converts http stats into struct to be sent back to the client.
|
2019-10-23 00:01:14 -04:00
|
|
|
func (st *HTTPStats) toServerHTTPStats() ServerHTTPStats {
|
2017-04-07 02:08:33 -04:00
|
|
|
serverStats := ServerHTTPStats{}
|
2022-02-07 19:30:14 -05:00
|
|
|
serverStats.S3RequestsIncoming = atomic.SwapUint64(&st.s3RequestsIncoming, 0)
|
2021-02-20 03:21:55 -05:00
|
|
|
serverStats.S3RequestsInQueue = atomic.LoadInt32(&st.s3RequestsInQueue)
|
2021-03-31 02:19:36 -04:00
|
|
|
serverStats.TotalS3RejectedAuth = atomic.LoadUint64(&st.rejectedRequestsAuth)
|
|
|
|
serverStats.TotalS3RejectedTime = atomic.LoadUint64(&st.rejectedRequestsTime)
|
|
|
|
serverStats.TotalS3RejectedHeader = atomic.LoadUint64(&st.rejectedRequestsHeader)
|
|
|
|
serverStats.TotalS3RejectedInvalid = atomic.LoadUint64(&st.rejectedRequestsInvalid)
|
2019-10-23 00:01:14 -04:00
|
|
|
serverStats.CurrentS3Requests = ServerHTTPAPIStats{
|
|
|
|
APIStats: st.currentS3Requests.Load(),
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
2019-10-23 00:01:14 -04:00
|
|
|
serverStats.TotalS3Requests = ServerHTTPAPIStats{
|
|
|
|
APIStats: st.totalS3Requests.Load(),
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
2019-10-23 00:01:14 -04:00
|
|
|
serverStats.TotalS3Errors = ServerHTTPAPIStats{
|
|
|
|
APIStats: st.totalS3Errors.Load(),
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
2022-06-08 14:22:34 -04:00
|
|
|
serverStats.TotalS34xxErrors = ServerHTTPAPIStats{
|
|
|
|
APIStats: st.totalS34xxErrors.Load(),
|
|
|
|
}
|
|
|
|
serverStats.TotalS35xxErrors = ServerHTTPAPIStats{
|
|
|
|
APIStats: st.totalS35xxErrors.Load(),
|
|
|
|
}
|
2021-03-24 13:25:27 -04:00
|
|
|
serverStats.TotalS3Canceled = ServerHTTPAPIStats{
|
|
|
|
APIStats: st.totalS3Canceled.Load(),
|
|
|
|
}
|
2017-04-07 02:08:33 -04:00
|
|
|
return serverStats
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update statistics from http request and response data
|
2023-04-12 17:37:19 -04:00
|
|
|
func (st *HTTPStats) updateStats(api string, w *xhttp.ResponseRecorder) {
|
2022-06-11 03:50:31 -04:00
|
|
|
st.totalS3Requests.Inc(api)
|
|
|
|
|
2020-12-11 02:02:25 -05:00
|
|
|
// Increment the prometheus http request response histogram with appropriate label
|
|
|
|
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TimeToFirstByte.Seconds())
|
2022-06-08 14:22:34 -04:00
|
|
|
|
|
|
|
code := w.StatusCode
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case code == 0:
|
|
|
|
case code == 499:
|
|
|
|
// 499 is a good error, shall be counted as canceled.
|
|
|
|
st.totalS3Canceled.Inc(api)
|
|
|
|
case code >= http.StatusBadRequest:
|
|
|
|
st.totalS3Errors.Inc(api)
|
|
|
|
if code >= http.StatusInternalServerError {
|
|
|
|
st.totalS35xxErrors.Inc(api)
|
|
|
|
} else {
|
|
|
|
st.totalS34xxErrors.Inc(api)
|
|
|
|
}
|
|
|
|
}
|
2017-04-07 02:08:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare new HTTPStats structure
|
|
|
|
func newHTTPStats() *HTTPStats {
|
|
|
|
return &HTTPStats{}
|
|
|
|
}
|