github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/http-tracer.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  	"context"
    22  	"net"
    23  	"net/http"
    24  	"reflect"
    25  	"regexp"
    26  	"runtime"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/minio/madmin-go/v3"
    32  	"github.com/minio/minio/internal/handlers"
    33  	xhttp "github.com/minio/minio/internal/http"
    34  	"github.com/minio/minio/internal/mcontext"
    35  )
    36  
    37  var ldapPwdRegex = regexp.MustCompile("(^.*?)LDAPPassword=([^&]*?)(&(.*?))?$")
    38  
    39  // redact LDAP password if part of string
    40  func redactLDAPPwd(s string) string {
    41  	parts := ldapPwdRegex.FindStringSubmatch(s)
    42  	if len(parts) > 3 {
    43  		return parts[1] + "LDAPPassword=*REDACTED*" + parts[3]
    44  	}
    45  	return s
    46  }
    47  
    48  // getOpName sanitizes the operation name for mc
    49  func getOpName(name string) (op string) {
    50  	op = strings.TrimPrefix(name, "github.com/minio/minio/cmd.")
    51  	op = strings.TrimSuffix(op, "Handler-fm")
    52  	op = strings.Replace(op, "objectAPIHandlers", "s3", 1)
    53  	op = strings.Replace(op, "adminAPIHandlers", "admin", 1)
    54  	op = strings.Replace(op, "(*storageRESTServer)", "storageR", 1)
    55  	op = strings.Replace(op, "(*peerRESTServer)", "peer", 1)
    56  	op = strings.Replace(op, "(*lockRESTServer)", "lockR", 1)
    57  	op = strings.Replace(op, "(*stsAPIHandlers)", "sts", 1)
    58  	op = strings.Replace(op, "(*peerS3Server)", "s3", 1)
    59  	op = strings.Replace(op, "ClusterCheckHandler", "health.Cluster", 1)
    60  	op = strings.Replace(op, "ClusterReadCheckHandler", "health.ClusterRead", 1)
    61  	op = strings.Replace(op, "LivenessCheckHandler", "health.Liveness", 1)
    62  	op = strings.Replace(op, "ReadinessCheckHandler", "health.Readiness", 1)
    63  	op = strings.Replace(op, "-fm", "", 1)
    64  	return op
    65  }
    66  
    67  // If trace is enabled, execute the request if it is traced by other handlers
    68  // otherwise, generate a trace event with request information but no response.
    69  func httpTracerMiddleware(h http.Handler) http.Handler {
    70  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    71  		// Setup a http request response recorder - this is needed for
    72  		// http stats requests and audit if enabled.
    73  		respRecorder := xhttp.NewResponseRecorder(w)
    74  
    75  		// Setup a http request body recorder
    76  		reqRecorder := &xhttp.RequestRecorder{Reader: r.Body}
    77  		r.Body = reqRecorder
    78  
    79  		// Create tracing data structure and associate it to the request context
    80  		tc := mcontext.TraceCtxt{
    81  			AmzReqID:         w.Header().Get(xhttp.AmzRequestID),
    82  			RequestRecorder:  reqRecorder,
    83  			ResponseRecorder: respRecorder,
    84  		}
    85  
    86  		r = r.WithContext(context.WithValue(r.Context(), mcontext.ContextTraceKey, &tc))
    87  
    88  		reqStartTime := time.Now().UTC()
    89  		h.ServeHTTP(respRecorder, r)
    90  		reqEndTime := time.Now().UTC()
    91  
    92  		if globalTrace.NumSubscribers(madmin.TraceS3|madmin.TraceInternal) == 0 {
    93  			// no subscribers nothing to trace.
    94  			return
    95  		}
    96  
    97  		tt := madmin.TraceInternal
    98  		if strings.HasPrefix(tc.FuncName, "s3.") {
    99  			tt = madmin.TraceS3
   100  		}
   101  
   102  		// Calculate input body size with headers
   103  		reqHeaders := r.Header.Clone()
   104  		reqHeaders.Set("Host", r.Host)
   105  		if len(r.TransferEncoding) == 0 {
   106  			reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
   107  		} else {
   108  			reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
   109  		}
   110  		inputBytes := reqRecorder.Size()
   111  		for k, v := range reqHeaders {
   112  			inputBytes += len(k) + len(v)
   113  		}
   114  
   115  		// Calculate node name
   116  		nodeName := r.Host
   117  		if globalIsDistErasure {
   118  			nodeName = globalLocalNodeName
   119  		}
   120  		if host, port, err := net.SplitHostPort(nodeName); err == nil {
   121  			if port == "443" || port == "80" {
   122  				nodeName = host
   123  			}
   124  		}
   125  
   126  		// Calculate reqPath
   127  		reqPath := r.URL.RawPath
   128  		if reqPath == "" {
   129  			reqPath = r.URL.Path
   130  		}
   131  
   132  		// Calculate function name
   133  		funcName := tc.FuncName
   134  		if funcName == "" {
   135  			funcName = "<unknown>"
   136  		}
   137  
   138  		t := madmin.TraceInfo{
   139  			TraceType: tt,
   140  			FuncName:  funcName,
   141  			NodeName:  nodeName,
   142  			Time:      reqStartTime,
   143  			Duration:  reqEndTime.Sub(respRecorder.StartTime),
   144  			Path:      reqPath,
   145  			HTTP: &madmin.TraceHTTPStats{
   146  				ReqInfo: madmin.TraceRequestInfo{
   147  					Time:     reqStartTime,
   148  					Proto:    r.Proto,
   149  					Method:   r.Method,
   150  					RawQuery: redactLDAPPwd(r.URL.RawQuery),
   151  					Client:   handlers.GetSourceIP(r),
   152  					Headers:  reqHeaders,
   153  					Path:     reqPath,
   154  					Body:     reqRecorder.Data(),
   155  				},
   156  				RespInfo: madmin.TraceResponseInfo{
   157  					Time:       reqEndTime,
   158  					Headers:    respRecorder.Header().Clone(),
   159  					StatusCode: respRecorder.StatusCode,
   160  					Body:       respRecorder.Body(),
   161  				},
   162  				CallStats: madmin.TraceCallStats{
   163  					Latency:         reqEndTime.Sub(respRecorder.StartTime),
   164  					InputBytes:      inputBytes,
   165  					OutputBytes:     respRecorder.Size(),
   166  					TimeToFirstByte: respRecorder.TimeToFirstByte,
   167  				},
   168  			},
   169  		}
   170  
   171  		globalTrace.Publish(t)
   172  	})
   173  }
   174  
   175  func httpTrace(f http.HandlerFunc, logBody bool) http.HandlerFunc {
   176  	return func(w http.ResponseWriter, r *http.Request) {
   177  		tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
   178  		if !ok {
   179  			// Tracing is not enabled for this request
   180  			f.ServeHTTP(w, r)
   181  			return
   182  		}
   183  
   184  		tc.FuncName = getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
   185  		tc.RequestRecorder.LogBody = logBody
   186  		tc.ResponseRecorder.LogAllBody = logBody
   187  		tc.ResponseRecorder.LogErrBody = true
   188  
   189  		f.ServeHTTP(w, r)
   190  	}
   191  }
   192  
   193  func httpTraceAll(f http.HandlerFunc) http.HandlerFunc {
   194  	return httpTrace(f, true)
   195  }
   196  
   197  func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
   198  	return httpTrace(f, false)
   199  }