|
Packit |
63bb0d |
package azblob
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
import (
|
|
Packit |
63bb0d |
"bytes"
|
|
Packit |
63bb0d |
"context"
|
|
Packit |
63bb0d |
"fmt"
|
|
Packit |
63bb0d |
"net/http"
|
|
Packit |
63bb0d |
"net/url"
|
|
Packit |
63bb0d |
"runtime"
|
|
Packit |
63bb0d |
"strings"
|
|
Packit |
63bb0d |
"time"
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
"github.com/Azure/azure-pipeline-go/pipeline"
|
|
Packit |
63bb0d |
)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// RequestLogOptions configures the retry policy's behavior.
|
|
Packit |
63bb0d |
type RequestLogOptions struct {
|
|
Packit |
63bb0d |
// LogWarningIfTryOverThreshold logs a warning if a tried operation takes longer than the specified
|
|
Packit |
63bb0d |
// duration (-1=no logging; 0=default threshold).
|
|
Packit |
63bb0d |
LogWarningIfTryOverThreshold time.Duration
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func (o RequestLogOptions) defaults() RequestLogOptions {
|
|
Packit |
63bb0d |
if o.LogWarningIfTryOverThreshold == 0 {
|
|
Packit |
63bb0d |
// It would be good to relate this to https://azure.microsoft.com/en-us/support/legal/sla/storage/v1_2/
|
|
Packit |
63bb0d |
// But this monitors the time to get the HTTP response; NOT the time to download the response body.
|
|
Packit |
63bb0d |
o.LogWarningIfTryOverThreshold = 3 * time.Second // Default to 3 seconds
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return o
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// NewRequestLogPolicyFactory creates a RequestLogPolicyFactory object configured using the specified options.
|
|
Packit |
63bb0d |
func NewRequestLogPolicyFactory(o RequestLogOptions) pipeline.Factory {
|
|
Packit |
63bb0d |
o = o.defaults() // Force defaults to be calculated
|
|
Packit |
63bb0d |
return pipeline.FactoryFunc(func(next pipeline.Policy, po *pipeline.PolicyOptions) pipeline.PolicyFunc {
|
|
Packit |
63bb0d |
// These variables are per-policy; shared by multiple calls to Do
|
|
Packit |
63bb0d |
var try int32
|
|
Packit |
63bb0d |
operationStart := time.Now() // If this is the 1st try, record the operation state time
|
|
Packit |
63bb0d |
return func(ctx context.Context, request pipeline.Request) (response pipeline.Response, err error) {
|
|
Packit |
63bb0d |
try++ // The first try is #1 (not #0)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Log the outgoing request as informational
|
|
Packit |
63bb0d |
if po.ShouldLog(pipeline.LogInfo) {
|
|
Packit |
63bb0d |
b := &bytes.Buffer{}
|
|
Packit |
63bb0d |
fmt.Fprintf(b, "==> OUTGOING REQUEST (Try=%d)\n", try)
|
|
Packit |
63bb0d |
pipeline.WriteRequestWithResponse(b, prepareRequestForLogging(request), nil, nil)
|
|
Packit |
63bb0d |
po.Log(pipeline.LogInfo, b.String())
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Set the time for this particular retry operation and then Do the operation.
|
|
Packit |
63bb0d |
tryStart := time.Now()
|
|
Packit |
63bb0d |
response, err = next.Do(ctx, request) // Make the request
|
|
Packit |
63bb0d |
tryEnd := time.Now()
|
|
Packit |
63bb0d |
tryDuration := tryEnd.Sub(tryStart)
|
|
Packit |
63bb0d |
opDuration := tryEnd.Sub(operationStart)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
logLevel, forceLog := pipeline.LogInfo, false // Default logging information
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// If the response took too long, we'll upgrade to warning.
|
|
Packit |
63bb0d |
if o.LogWarningIfTryOverThreshold > 0 && tryDuration > o.LogWarningIfTryOverThreshold {
|
|
Packit |
63bb0d |
// Log a warning if the try duration exceeded the specified threshold
|
|
Packit |
63bb0d |
logLevel, forceLog = pipeline.LogWarning, true
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit Service |
3a6627 |
var sc int
|
|
Packit Service |
3a6627 |
if err == nil { // We got a valid response from the service
|
|
Packit Service |
3a6627 |
sc = response.Response().StatusCode
|
|
Packit Service |
3a6627 |
} else { // We got an error, so we should inspect if we got a response
|
|
Packit Service |
3a6627 |
if se, ok := err.(StorageError); ok {
|
|
Packit Service |
3a6627 |
if r := se.Response(); r != nil {
|
|
Packit Service |
3a6627 |
sc = r.StatusCode
|
|
Packit Service |
3a6627 |
}
|
|
Packit |
63bb0d |
}
|
|
Packit Service |
3a6627 |
}
|
|
Packit Service |
3a6627 |
|
|
Packit Service |
3a6627 |
if sc == 0 || ((sc >= 400 && sc <= 499) && sc != http.StatusNotFound && sc != http.StatusConflict && sc != http.StatusPreconditionFailed && sc != http.StatusRequestedRangeNotSatisfiable) || (sc >= 500 && sc <= 599) {
|
|
Packit Service |
3a6627 |
logLevel, forceLog = pipeline.LogError, true // Promote to Error any 4xx (except those listed is an error) or any 5xx
|
|
Packit Service |
3a6627 |
} else {
|
|
Packit Service |
3a6627 |
// For other status codes, we leave the level as is.
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if shouldLog := po.ShouldLog(logLevel); forceLog || shouldLog {
|
|
Packit |
63bb0d |
// We're going to log this; build the string to log
|
|
Packit |
63bb0d |
b := &bytes.Buffer{}
|
|
Packit |
63bb0d |
slow := ""
|
|
Packit |
63bb0d |
if o.LogWarningIfTryOverThreshold > 0 && tryDuration > o.LogWarningIfTryOverThreshold {
|
|
Packit |
63bb0d |
slow = fmt.Sprintf("[SLOW >%v]", o.LogWarningIfTryOverThreshold)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
fmt.Fprintf(b, "==> REQUEST/RESPONSE (Try=%d/%v%s, OpTime=%v) -- ", try, tryDuration, slow, opDuration)
|
|
Packit |
63bb0d |
if err != nil { // This HTTP request did not get a response from the service
|
|
Packit |
63bb0d |
fmt.Fprint(b, "REQUEST ERROR\n")
|
|
Packit |
63bb0d |
} else {
|
|
Packit |
63bb0d |
if logLevel == pipeline.LogError {
|
|
Packit |
63bb0d |
fmt.Fprint(b, "RESPONSE STATUS CODE ERROR\n")
|
|
Packit |
63bb0d |
} else {
|
|
Packit |
63bb0d |
fmt.Fprint(b, "RESPONSE SUCCESSFULLY RECEIVED\n")
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
pipeline.WriteRequestWithResponse(b, prepareRequestForLogging(request), response.Response(), err)
|
|
Packit |
63bb0d |
if logLevel <= pipeline.LogError {
|
|
Packit |
63bb0d |
b.Write(stack()) // For errors (or lower levels), we append the stack trace (an expensive operation)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
msg := b.String()
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if forceLog {
|
|
Packit |
63bb0d |
pipeline.ForceLog(logLevel, msg)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
if shouldLog {
|
|
Packit |
63bb0d |
po.Log(logLevel, msg)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return response, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
})
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// RedactSigQueryParam redacts the 'sig' query parameter in URL's raw query to protect secret.
|
|
Packit |
63bb0d |
func RedactSigQueryParam(rawQuery string) (bool, string) {
|
|
Packit |
63bb0d |
rawQuery = strings.ToLower(rawQuery) // lowercase the string so we can look for ?sig= and &sig=
|
|
Packit |
63bb0d |
sigFound := strings.Contains(rawQuery, "?sig=")
|
|
Packit |
63bb0d |
if !sigFound {
|
|
Packit |
63bb0d |
sigFound = strings.Contains(rawQuery, "&sig=")
|
|
Packit |
63bb0d |
if !sigFound {
|
|
Packit |
63bb0d |
return sigFound, rawQuery // [?|&]sig= not found; return same rawQuery passed in (no memory allocation)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
// [?|&]sig= found, redact its value
|
|
Packit |
63bb0d |
values, _ := url.ParseQuery(rawQuery)
|
|
Packit |
63bb0d |
for name := range values {
|
|
Packit |
63bb0d |
if strings.EqualFold(name, "sig") {
|
|
Packit |
63bb0d |
values[name] = []string{"REDACTED"}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return sigFound, values.Encode()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func prepareRequestForLogging(request pipeline.Request) *http.Request {
|
|
Packit |
63bb0d |
req := request
|
|
Packit |
63bb0d |
if sigFound, rawQuery := RedactSigQueryParam(req.URL.RawQuery); sigFound {
|
|
Packit |
63bb0d |
// Make copy so we don't destroy the query parameters we actually need to send in the request
|
|
Packit |
63bb0d |
req = request.Copy()
|
|
Packit |
63bb0d |
req.Request.URL.RawQuery = rawQuery
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
return prepareRequestForServiceLogging(req)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func stack() []byte {
|
|
Packit |
63bb0d |
buf := make([]byte, 1024)
|
|
Packit |
63bb0d |
for {
|
|
Packit |
63bb0d |
n := runtime.Stack(buf, false)
|
|
Packit |
63bb0d |
if n < len(buf) {
|
|
Packit |
63bb0d |
return buf[:n]
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
buf = make([]byte, 2*len(buf))
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
///////////////////////////////////////////////////////////////////////////////////////
|
|
Packit |
63bb0d |
// Redact phase useful for blob and file service only. For other services,
|
|
Packit |
63bb0d |
// this method can directly return request.Request.
|
|
Packit |
63bb0d |
///////////////////////////////////////////////////////////////////////////////////////
|
|
Packit |
63bb0d |
func prepareRequestForServiceLogging(request pipeline.Request) *http.Request {
|
|
Packit |
63bb0d |
req := request
|
|
Packit |
63bb0d |
if exist, key := doesHeaderExistCaseInsensitive(req.Header, xMsCopySourceHeader); exist {
|
|
Packit |
63bb0d |
req = request.Copy()
|
|
Packit |
63bb0d |
url, err := url.Parse(req.Header.Get(key))
|
|
Packit |
63bb0d |
if err == nil {
|
|
Packit |
63bb0d |
if sigFound, rawQuery := RedactSigQueryParam(url.RawQuery); sigFound {
|
|
Packit |
63bb0d |
url.RawQuery = rawQuery
|
|
Packit |
63bb0d |
req.Header.Set(xMsCopySourceHeader, url.String())
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return req.Request
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
const xMsCopySourceHeader = "x-ms-copy-source"
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func doesHeaderExistCaseInsensitive(header http.Header, key string) (bool, string) {
|
|
Packit |
63bb0d |
for keyInHeader := range header {
|
|
Packit |
63bb0d |
if strings.EqualFold(keyInHeader, key) {
|
|
Packit |
63bb0d |
return true, keyInHeader
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return false, ""
|
|
Packit |
63bb0d |
}
|