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