1//go:build go1.16 2// +build go1.16 3 4// Copyright (c) Microsoft Corporation. All rights reserved. 5// Licensed under the MIT License. 6 7package azcore 8 9import ( 10 "bytes" 11 "fmt" 12 "net/http" 13 "strings" 14 "time" 15 16 "github.com/Azure/azure-sdk-for-go/sdk/internal/diag" 17 "github.com/Azure/azure-sdk-for-go/sdk/internal/log" 18) 19 20// LogOptions configures the logging policy's behavior. 21type LogOptions struct { 22 // IncludeBody indicates if request and response bodies should be included in logging. 23 // The default value is false. 24 // NOTE: enabling this can lead to disclosure of sensitive information, use with care. 25 IncludeBody bool 26} 27 28type logPolicy struct { 29 options LogOptions 30} 31 32// NewLogPolicy creates a RequestLogPolicy object configured using the specified options. 33// Pass nil to accept the default values; this is the same as passing a zero-value options. 34func NewLogPolicy(o *LogOptions) Policy { 35 if o == nil { 36 o = &LogOptions{} 37 } 38 return &logPolicy{options: *o} 39} 40 41// logPolicyOpValues is the struct containing the per-operation values 42type logPolicyOpValues struct { 43 try int32 44 start time.Time 45} 46 47func (p *logPolicy) Do(req *Request) (*http.Response, error) { 48 // Get the per-operation values. These are saved in the Message's map so that they persist across each retry calling into this policy object. 49 var opValues logPolicyOpValues 50 if req.OperationValue(&opValues); opValues.start.IsZero() { 51 opValues.start = time.Now() // If this is the 1st try, record this operation's start time 52 } 53 opValues.try++ // The first try is #1 (not #0) 54 req.SetOperationValue(opValues) 55 56 // Log the outgoing request as informational 57 if log.Should(log.Request) { 58 b := &bytes.Buffer{} 59 fmt.Fprintf(b, "==> OUTGOING REQUEST (Try=%d)\n", opValues.try) 60 writeRequestWithResponse(b, req, nil, nil) 61 var err error 62 if p.options.IncludeBody { 63 err = req.writeBody(b) 64 } 65 log.Write(log.Request, b.String()) 66 if err != nil { 67 return nil, err 68 } 69 } 70 71 // Set the time for this particular retry operation and then Do the operation. 72 tryStart := time.Now() 73 response, err := req.Next() // Make the request 74 tryEnd := time.Now() 75 tryDuration := tryEnd.Sub(tryStart) 76 opDuration := tryEnd.Sub(opValues.start) 77 78 if log.Should(log.Response) { 79 // We're going to log this; build the string to log 80 b := &bytes.Buffer{} 81 fmt.Fprintf(b, "==> REQUEST/RESPONSE (Try=%d/%v, OpTime=%v) -- ", opValues.try, tryDuration, opDuration) 82 if err != nil { // This HTTP request did not get a response from the service 83 fmt.Fprint(b, "REQUEST ERROR\n") 84 } else { 85 fmt.Fprint(b, "RESPONSE RECEIVED\n") 86 } 87 88 writeRequestWithResponse(b, req, response, err) 89 if err != nil { 90 // skip frames runtime.Callers() and runtime.StackTrace() 91 b.WriteString(diag.StackTrace(2, StackFrameCount)) 92 } else if p.options.IncludeBody { 93 err = writeBody(response, b) 94 } 95 log.Write(log.Response, b.String()) 96 } 97 return response, err 98} 99 100// returns true if the request/response body should be logged. 101// this is determined by looking at the content-type header value. 102func shouldLogBody(b *bytes.Buffer, contentType string) bool { 103 contentType = strings.ToLower(contentType) 104 if strings.HasPrefix(contentType, "text") || 105 strings.Contains(contentType, "json") || 106 strings.Contains(contentType, "xml") { 107 return true 108 } 109 fmt.Fprintf(b, " Skip logging body for %s\n", contentType) 110 return false 111} 112