storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/http-tracer.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2017 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"bytes"
    21  	"io"
    22  	"io/ioutil"
    23  	"net"
    24  	"net/http"
    25  	"reflect"
    26  	"regexp"
    27  	"runtime"
    28  	"strconv"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/gorilla/mux"
    33  
    34  	"storj.io/minio/cmd/logger"
    35  	"storj.io/minio/pkg/handlers"
    36  	jsonrpc "storj.io/minio/pkg/rpc"
    37  	trace "storj.io/minio/pkg/trace"
    38  )
    39  
    40  // recordRequest - records the first recLen bytes
    41  // of a given io.Reader
    42  type recordRequest struct {
    43  	// Data source to record
    44  	io.Reader
    45  	// Response body should be logged
    46  	logBody bool
    47  	// Internal recording buffer
    48  	buf bytes.Buffer
    49  	// request headers
    50  	headers http.Header
    51  	// total bytes read including header size
    52  	bytesRead int
    53  }
    54  
    55  func (r *recordRequest) Read(p []byte) (n int, err error) {
    56  	n, err = r.Reader.Read(p)
    57  	r.bytesRead += n
    58  
    59  	if r.logBody {
    60  		r.buf.Write(p[:n])
    61  	}
    62  	if err != nil {
    63  		return n, err
    64  	}
    65  	return n, err
    66  }
    67  func (r *recordRequest) Size() int {
    68  	sz := r.bytesRead
    69  	for k, v := range r.headers {
    70  		sz += len(k) + len(v)
    71  	}
    72  	return sz
    73  }
    74  
    75  // Return the bytes that were recorded.
    76  func (r *recordRequest) Data() []byte {
    77  	// If body logging is enabled then we return the actual body
    78  	if r.logBody {
    79  		return r.buf.Bytes()
    80  	}
    81  	// ... otherwise we return <BODY> placeholder
    82  	return logger.BodyPlaceHolder
    83  }
    84  
    85  var ldapPwdRegex = regexp.MustCompile("(^.*?)LDAPPassword=([^&]*?)(&(.*?))?$")
    86  
    87  // redact LDAP password if part of string
    88  func redactLDAPPwd(s string) string {
    89  	parts := ldapPwdRegex.FindStringSubmatch(s)
    90  	if len(parts) > 0 {
    91  		return parts[1] + "LDAPPassword=*REDACTED*" + parts[3]
    92  	}
    93  	return s
    94  }
    95  
    96  // getOpName sanitizes the operation name for mc
    97  func getOpName(name string) (op string) {
    98  	op = strings.TrimPrefix(name, "github.com/minio/minio/cmd.")
    99  	op = strings.TrimSuffix(op, "Handler-fm")
   100  	op = strings.Replace(op, "ObjectAPIHandlers", "s3", 1)
   101  	op = strings.Replace(op, "adminAPIHandlers", "admin", 1)
   102  	op = strings.Replace(op, "(*webAPIHandlers)", "web", 1)
   103  	op = strings.Replace(op, "(*storageRESTServer)", "internal", 1)
   104  	op = strings.Replace(op, "(*peerRESTServer)", "internal", 1)
   105  	op = strings.Replace(op, "(*lockRESTServer)", "internal", 1)
   106  	op = strings.Replace(op, "(*stsAPIHandlers)", "sts", 1)
   107  	op = strings.Replace(op, "LivenessCheckHandler", "healthcheck", 1)
   108  	op = strings.Replace(op, "ReadinessCheckHandler", "healthcheck", 1)
   109  	op = strings.Replace(op, "-fm", "", 1)
   110  	return op
   111  }
   112  
   113  // WebTrace gets trace of web request
   114  func WebTrace(ri *jsonrpc.RequestInfo) trace.Info {
   115  	r := ri.Request
   116  	w := ri.ResponseWriter
   117  
   118  	name := ri.Method
   119  	// Setup a http request body recorder
   120  	reqHeaders := r.Header.Clone()
   121  	reqHeaders.Set("Host", r.Host)
   122  	if len(r.TransferEncoding) == 0 {
   123  		reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
   124  	} else {
   125  		reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
   126  	}
   127  
   128  	now := time.Now().UTC()
   129  	t := trace.Info{TraceType: trace.HTTP, FuncName: name, Time: now}
   130  	t.NodeName = r.Host
   131  	if globalIsDistErasure {
   132  		t.NodeName = globalLocalNodeName
   133  	}
   134  	if t.NodeName == "" {
   135  		t.NodeName = globalLocalNodeName
   136  	}
   137  
   138  	// strip only standard port from the host address
   139  	if host, port, err := net.SplitHostPort(t.NodeName); err == nil {
   140  		if port == "443" || port == "80" {
   141  			t.NodeName = host
   142  		}
   143  	}
   144  
   145  	vars := mux.Vars(r)
   146  	rq := trace.RequestInfo{
   147  		Time:     now,
   148  		Proto:    r.Proto,
   149  		Method:   r.Method,
   150  		Path:     SlashSeparator + pathJoin(vars["bucket"], vars["object"]),
   151  		RawQuery: redactLDAPPwd(r.URL.RawQuery),
   152  		Client:   handlers.GetSourceIP(r),
   153  		Headers:  reqHeaders,
   154  	}
   155  
   156  	rw, ok := w.(*logger.ResponseWriter)
   157  	if ok {
   158  		rs := trace.ResponseInfo{
   159  			Time:       time.Now().UTC(),
   160  			Headers:    rw.Header().Clone(),
   161  			StatusCode: rw.StatusCode,
   162  			Body:       logger.BodyPlaceHolder,
   163  		}
   164  
   165  		if rs.StatusCode == 0 {
   166  			rs.StatusCode = http.StatusOK
   167  		}
   168  
   169  		t.RespInfo = rs
   170  		t.CallStats = trace.CallStats{
   171  			Latency:         rs.Time.Sub(rw.StartTime),
   172  			InputBytes:      int(r.ContentLength),
   173  			OutputBytes:     rw.Size(),
   174  			TimeToFirstByte: rw.TimeToFirstByte,
   175  		}
   176  	}
   177  
   178  	t.ReqInfo = rq
   179  	return t
   180  }
   181  
   182  // Trace gets trace of http request
   183  func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Request) trace.Info {
   184  	name := getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
   185  
   186  	// Setup a http request body recorder
   187  	reqHeaders := r.Header.Clone()
   188  	reqHeaders.Set("Host", r.Host)
   189  	if len(r.TransferEncoding) == 0 {
   190  		reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
   191  	} else {
   192  		reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
   193  	}
   194  
   195  	reqBodyRecorder := &recordRequest{Reader: r.Body, logBody: logBody, headers: reqHeaders}
   196  	r.Body = ioutil.NopCloser(reqBodyRecorder)
   197  
   198  	now := time.Now().UTC()
   199  	t := trace.Info{TraceType: trace.HTTP, FuncName: name, Time: now}
   200  
   201  	t.NodeName = r.Host
   202  	if globalIsDistErasure {
   203  		t.NodeName = globalLocalNodeName
   204  	}
   205  
   206  	if t.NodeName == "" {
   207  		t.NodeName = globalLocalNodeName
   208  	}
   209  
   210  	// strip only standard port from the host address
   211  	if host, port, err := net.SplitHostPort(t.NodeName); err == nil {
   212  		if port == "443" || port == "80" {
   213  			t.NodeName = host
   214  		}
   215  	}
   216  
   217  	rq := trace.RequestInfo{
   218  		Time:     now,
   219  		Proto:    r.Proto,
   220  		Method:   r.Method,
   221  		Path:     r.URL.Path,
   222  		RawQuery: redactLDAPPwd(r.URL.RawQuery),
   223  		Client:   handlers.GetSourceIP(r),
   224  		Headers:  reqHeaders,
   225  	}
   226  
   227  	rw := logger.NewResponseWriter(w)
   228  	rw.LogErrBody = true
   229  	rw.LogAllBody = logBody
   230  
   231  	// Execute call.
   232  	f(rw, r)
   233  
   234  	rs := trace.ResponseInfo{
   235  		Time:       time.Now().UTC(),
   236  		Headers:    rw.Header().Clone(),
   237  		StatusCode: rw.StatusCode,
   238  		Body:       rw.Body(),
   239  	}
   240  
   241  	// Transfer request body
   242  	rq.Body = reqBodyRecorder.Data()
   243  
   244  	if rs.StatusCode == 0 {
   245  		rs.StatusCode = http.StatusOK
   246  	}
   247  
   248  	t.ReqInfo = rq
   249  	t.RespInfo = rs
   250  
   251  	t.CallStats = trace.CallStats{
   252  		Latency:         rs.Time.Sub(rw.StartTime),
   253  		InputBytes:      reqBodyRecorder.Size(),
   254  		OutputBytes:     rw.Size(),
   255  		TimeToFirstByte: rw.TimeToFirstByte,
   256  	}
   257  	return t
   258  }