From 93fd2693295c0cfeb875bfa5c283651c7a4d1cb2 Mon Sep 17 00:00:00 2001 From: Anis Elleuch Date: Mon, 6 Feb 2017 18:29:53 +0100 Subject: [PATCH] stats: Add network and http statisics (#3686) Network: total bytes of incoming and outgoing server's data by taking advantage of our ConnMux Read/Write wrapping HTTP: total number of different http verbs passed in http requests and different status codes passed in http responses. This is counted in a new http handler. --- cmd/generic-handlers.go | 49 +++++++++++++++ cmd/globals.go | 5 ++ cmd/routers.go | 2 + cmd/server-mux.go | 35 +++++++---- cmd/stats.go | 129 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 cmd/stats.go diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index 229e752b4..b118f1725 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -356,3 +356,52 @@ func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Serve HTTP. h.handler.ServeHTTP(w, r) } + +// httpResponseRecorder wraps http.ResponseWriter +// to record some useful http response data. +type httpResponseRecorder struct { + http.ResponseWriter + http.Flusher + respStatusCode int +} + +// Wraps ResponseWriter's Write() +func (rww *httpResponseRecorder) Write(b []byte) (int, error) { + return rww.ResponseWriter.Write(b) +} + +// Wraps ResponseWriter's Flush() +func (rww *httpResponseRecorder) Flush() { + f, ok := rww.ResponseWriter.(http.Flusher) + if ok { + f.Flush() + } +} + +// Wraps ResponseWriter's WriteHeader() and record +// the response status code +func (rww *httpResponseRecorder) WriteHeader(httpCode int) { + rww.respStatusCode = httpCode + rww.ResponseWriter.WriteHeader(httpCode) +} + +// httpStatsHandler definition: gather HTTP statistics +type httpStatsHandler struct { + handler http.Handler +} + +// setHttpStatsHandler sets a http Stats Handler +func setHTTPStatsHandler(h http.Handler) http.Handler { + return httpStatsHandler{handler: h} +} + +func (h httpStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Wraps w to record http response information + ww := &httpResponseRecorder{ResponseWriter: w} + + // Execute the request + h.handler.ServeHTTP(ww, r) + + // Update http statistics + globalHTTPStats.updateStats(r, ww) +} diff --git a/cmd/globals.go b/cmd/globals.go index be77a78db..8b9f312cb 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -122,6 +122,11 @@ var ( // url.URL endpoints of disks that belong to the object storage. globalEndpoints = []*url.URL{} + // Global server's network statistics + globalConnStats = newConnStats() + // Global HTTP request statisitics + globalHTTPStats = newHTTPStats() + // Add new variable global values here. ) diff --git a/cmd/routers.go b/cmd/routers.go index dfee5e590..9213f65be 100644 --- a/cmd/routers.go +++ b/cmd/routers.go @@ -84,6 +84,8 @@ func configureServerHandler(srvCmdConfig serverCmdConfig) (http.Handler, error) // List of some generic handlers which are applied for all incoming requests. var handlerFns = []HandlerFunc{ + // Network statistics + setHTTPStatsHandler, // Limits all requests size to a maximum fixed limit setRequestSizeLimitHandler, // Adds 'crossdomain.xml' policy handler to serve legacy flash clients. diff --git a/cmd/server-mux.go b/cmd/server-mux.go index ae2eb0b73..8ea33a3d5 100644 --- a/cmd/server-mux.go +++ b/cmd/server-mux.go @@ -58,16 +58,16 @@ var defaultHTTP1Methods = []string{ // connections on the same listeners. type ConnMux struct { net.Conn - bufrw *bufio.ReadWriter + // To peek net.Conn incoming data + peeker *bufio.Reader } // NewConnMux - creates a new ConnMux instance func NewConnMux(c net.Conn) *ConnMux { br := bufio.NewReader(c) - bw := bufio.NewWriter(c) return &ConnMux{ - Conn: c, - bufrw: bufio.NewReadWriter(br, bw), + Conn: c, + peeker: bufio.NewReader(br), } } @@ -83,7 +83,7 @@ const ( // errors in peeking over the connection. func (c *ConnMux) PeekProtocol() (string, error) { // Peek for HTTP verbs. - buf, err := c.bufrw.Peek(maxHTTPVerbLen) + buf, err := c.peeker.Peek(maxHTTPVerbLen) if err != nil { return "", err } @@ -110,20 +110,31 @@ func (c *ConnMux) PeekProtocol() (string, error) { // Read - streams the ConnMux buffer when reset flag is activated, otherwise // streams from the incoming network connection -func (c *ConnMux) Read(b []byte) (int, error) { +func (c *ConnMux) Read(b []byte) (n int, e error) { // Push read deadline c.Conn.SetReadDeadline(time.Now().Add(defaultTCPReadTimeout)) - return c.bufrw.Read(b) + + // Update server's connection statistics + defer func() { + globalConnStats.incInputBytes(n) + }() + + return c.peeker.Read(b) +} + +func (c *ConnMux) Write(b []byte) (n int, e error) { + // Update server's connection statistics + defer func() { + globalConnStats.incOutputBytes(n) + }() + // Run the underlying net.Conn Write() func + return c.Conn.Write(b) } // Close the connection. func (c *ConnMux) Close() (err error) { // Make sure that we always close a connection, - // even if the bufioWriter flush sends an error. - defer c.Conn.Close() - - // Flush and write to the connection. - return c.bufrw.Flush() + return c.Conn.Close() } // ListenerMux wraps the standard net.Listener to inspect diff --git a/cmd/stats.go b/cmd/stats.go new file mode 100644 index 000000000..9dc691839 --- /dev/null +++ b/cmd/stats.go @@ -0,0 +1,129 @@ +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "net/http" + "sync/atomic" +) + +// counter - simplify atomic counting +type counter struct { + val uint64 +} + +// Inc increases counter atomically +func (c *counter) Inc(n uint64) { + atomic.AddUint64(&c.val, n) +} + +// Value fetches counter's value atomically +func (c *counter) Value() uint64 { + return atomic.LoadUint64(&c.val) +} + +// ConnStats - Network statistics +// Count total input/output transferred bytes during +// the server's life. +type ConnStats struct { + totalInputBytes counter + totalOutputBytes counter +} + +// Increase total input bytes +func (s *ConnStats) incInputBytes(n int) { + s.totalInputBytes.Inc(uint64(n)) +} + +// Increase total output bytes +func (s *ConnStats) incOutputBytes(n int) { + s.totalOutputBytes.Inc(uint64(n)) +} + +// Return total input bytes +func (s *ConnStats) getTotalInputBytes() uint64 { + return s.totalInputBytes.Value() +} + +// Return total output bytes +func (s *ConnStats) getTotalOutputBytes() uint64 { + return s.totalOutputBytes.Value() +} + +// Prepare new ConnStats structure +func newConnStats() *ConnStats { + return &ConnStats{} +} + +// httpStats holds statistics information about +// HTTP requests made by all clients +type httpStats struct { + // HEAD request stats + totalHEADs counter + successHEADs counter + // GET request stats + totalGETs counter + successGETs counter + // PUT request + totalPUTs counter + successPUTs counter + // POST request + totalPOSTs counter + successPOSTs counter + // DELETE request + totalDELETEs counter + successDELETEs counter +} + +// Update statistics from http request and response data +func (st *httpStats) updateStats(r *http.Request, w *httpResponseRecorder) { + // A successful request has a 2xx response code + successReq := (w.respStatusCode >= 200 && w.respStatusCode < 300) + // Update stats according to method verb + switch r.Method { + case "HEAD": + st.totalHEADs.Inc(1) + if successReq { + st.successHEADs.Inc(1) + } + case "GET": + st.totalGETs.Inc(1) + if successReq { + st.successGETs.Inc(1) + } + case "PUT": + st.totalPUTs.Inc(1) + if successReq { + st.successPUTs.Inc(1) + } + case "POST": + st.totalPOSTs.Inc(1) + if successReq { + st.successPOSTs.Inc(1) + } + case "DELETE": + st.totalDELETEs.Inc(1) + if successReq { + st.successDELETEs.Inc(1) + } + } +} + +// Prepare new HttpStats structure +func newHTTPStats() *httpStats { + return &httpStats{} +}