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