Skip to content

Commit 5aa6eda

Browse files
authored
add debug log formatter feature (#966)
1 parent 2e3e58b commit 5aa6eda

8 files changed

+244
-134
lines changed

client.go

+25-22
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ type (
8989
// ResponseMiddleware type is for response middleware, called after a response has been received
9090
ResponseMiddleware func(*Client, *Response) error
9191

92-
// DebugLogCallback type is for request and response debug log callback purpose.
93-
// It gets called before Resty logs it
94-
DebugLogCallback func(*DebugLog)
95-
9692
// ErrorHook type is for reacting to request errors, called after all retries were attempted
9793
ErrorHook func(*Request, error)
9894

@@ -207,8 +203,8 @@ type Client struct {
207203
ctx context.Context
208204
httpClient *http.Client
209205
proxyURL *url.URL
210-
requestDebugLog DebugLogCallback
211-
responseDebugLog DebugLogCallback
206+
debugLogFormatter DebugLogFormatterFunc
207+
debugLogCallback DebugLogCallbackFunc
212208
generateCurlCmd bool
213209
debugLogCurlCmd bool
214210
unescapeQueryParams bool
@@ -1021,29 +1017,36 @@ func (c *Client) SetDebugBodyLimit(sl int) *Client {
10211017
return c
10221018
}
10231019

1024-
// OnRequestDebugLog method sets the request debug log callback to the client instance.
1020+
func (c *Client) debugLogCallbackFunc() DebugLogCallbackFunc {
1021+
c.lock.RLock()
1022+
defer c.lock.RUnlock()
1023+
return c.debugLogCallback
1024+
}
1025+
1026+
// OnDebugLog method sets the debug log callback function to the client instance.
10251027
// Registered callback gets called before the Resty logs the information.
1026-
func (c *Client) OnRequestDebugLog(dlc DebugLogCallback) *Client {
1028+
func (c *Client) OnDebugLog(dlc DebugLogCallbackFunc) *Client {
10271029
c.lock.Lock()
10281030
defer c.lock.Unlock()
1029-
if c.requestDebugLog != nil {
1030-
c.log.Warnf("Overwriting an existing on-request-debug-log callback from=%s to=%s",
1031-
functionName(c.requestDebugLog), functionName(dlc))
1031+
if c.debugLogCallback != nil {
1032+
c.log.Warnf("Overwriting an existing on-debug-log callback from=%s to=%s",
1033+
functionName(c.debugLogCallback), functionName(dlc))
10321034
}
1033-
c.requestDebugLog = dlc
1035+
c.debugLogCallback = dlc
10341036
return c
10351037
}
10361038

1037-
// OnResponseDebugLog method sets the response debug log callback to the client instance.
1038-
// Registered callback gets called before the Resty logs the information.
1039-
func (c *Client) OnResponseDebugLog(dlc DebugLogCallback) *Client {
1039+
func (c *Client) debugLogFormatterFunc() DebugLogFormatterFunc {
1040+
c.lock.RLock()
1041+
defer c.lock.RUnlock()
1042+
return c.debugLogFormatter
1043+
}
1044+
1045+
// SetDebugLogFormatter method sets the Resty debug log formatter to the client instance.
1046+
func (c *Client) SetDebugLogFormatter(df DebugLogFormatterFunc) *Client {
10401047
c.lock.Lock()
10411048
defer c.lock.Unlock()
1042-
if c.responseDebugLog != nil {
1043-
c.log.Warnf("Overwriting an existing on-response-debug-log callback from=%s to=%s",
1044-
functionName(c.responseDebugLog), functionName(dlc))
1045-
}
1046-
c.responseDebugLog = dlc
1049+
c.debugLogFormatter = df
10471050
return c
10481051
}
10491052

@@ -2245,7 +2248,7 @@ func (c *Client) execute(req *Request) (*Response, error) {
22452248
req.RawRequest.Host = hostHeader
22462249
}
22472250

2248-
requestDebugLogger(c, req)
2251+
prepareRequestDebugInfo(c, req)
22492252

22502253
req.Time = time.Now()
22512254
resp, err := c.Client().Do(req.withTimeout())
@@ -2278,7 +2281,7 @@ func (c *Client) execute(req *Request) (*Response, error) {
22782281
}
22792282
}
22802283

2281-
responseDebugLogger(c, response)
2284+
debugLogger(c, response)
22822285

22832286
// Apply Response middleware
22842287
for _, f := range c.responseMiddlewares() {

client_test.go

+10-21
Original file line numberDiff line numberDiff line change
@@ -898,13 +898,14 @@ func TestClientLogCallbacks(t *testing.T) {
898898

899899
c, lb := dcldb()
900900

901-
c.OnRequestDebugLog(func(r *DebugLog) {
901+
c.OnDebugLog(func(dl *DebugLog) {
902+
// request
902903
// masking authorization header
903-
r.Header.Set("Authorization", "Bearer *******************************")
904-
})
905-
c.OnResponseDebugLog(func(r *DebugLog) {
906-
r.Header.Add("X-Debug-Response-Log", "Modified :)")
907-
r.Body += "\nModified the response body content"
904+
dl.Request.Header.Set("Authorization", "Bearer *******************************")
905+
906+
// response
907+
dl.Response.Header.Add("X-Debug-Response-Log", "Modified :)")
908+
dl.Response.Body += "\nModified the response body content"
908909
})
909910

910911
c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
@@ -924,28 +925,16 @@ func TestClientLogCallbacks(t *testing.T) {
924925
assertEqual(t, true, strings.Contains(logInfo, "Modified the response body content"))
925926

926927
// overwrite scenario
927-
c.OnRequestDebugLog(func(r *DebugLog) {
928-
// overwrite request debug log
929-
})
930-
resp, err = c.R().
931-
SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF-Request").
932-
Get(ts.URL + "/profile")
933-
assertNil(t, err)
934-
assertNotNil(t, resp)
935-
assertEqual(t, int64(50), resp.Size())
936-
assertEqual(t, true, strings.Contains(lb.String(), "Overwriting an existing on-request-debug-log callback from=resty.dev/v3.TestClientLogCallbacks.func1 to=resty.dev/v3.TestClientLogCallbacks.func3"))
937-
938-
c.OnRequestDebugLog(nil)
939-
c.OnResponseDebugLog(func(r *DebugLog) {
940-
// overwrite response debug log
928+
c.OnDebugLog(func(dl *DebugLog) {
929+
// overwrite debug log
941930
})
942931
resp, err = c.R().
943932
SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF-Request").
944933
Get(ts.URL + "/profile")
945934
assertNil(t, err)
946935
assertNotNil(t, resp)
947936
assertEqual(t, int64(50), resp.Size())
948-
assertEqual(t, true, strings.Contains(lb.String(), "Overwriting an existing on-response-debug-log callback from=resty.dev/v3.TestClientLogCallbacks.func2 to=resty.dev/v3.TestClientLogCallbacks.func4"))
937+
assertEqual(t, true, strings.Contains(lb.String(), "Overwriting an existing on-debug-log callback from=resty.dev/v3.TestClientLogCallbacks.func1 to=resty.dev/v3.TestClientLogCallbacks.func2"))
949938
}
950939

951940
func TestDebugLogSimultaneously(t *testing.T) {

debug.go

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright (c) 2015-present Jeevanandam M ([email protected]), All rights reserved.
2+
// resty source code and usage is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
// SPDX-License-Identifier: MIT
5+
6+
package resty
7+
8+
import (
9+
"fmt"
10+
"net/http"
11+
"time"
12+
)
13+
14+
type (
15+
// DebugLogCallbackFunc function type is for request and response debug log callback purposes.
16+
// It gets called before Resty logs it
17+
DebugLogCallbackFunc func(*DebugLog)
18+
19+
// DebugLogFormatterFunc function type is used to implement debug log formatting.
20+
// See out of the box [DebugLogStringFormatter], [DebugLogJSONFormatter]
21+
DebugLogFormatterFunc func(*DebugLog) string
22+
23+
// DebugLog struct is used to collect details from Resty request and response
24+
// for debug logging callback purposes.
25+
DebugLog struct {
26+
Request *DebugLogRequest `json:"request"`
27+
Response *DebugLogResponse `json:"response"`
28+
TraceInfo *TraceInfo `json:"trace_info"`
29+
}
30+
31+
// DebugLogRequest type used to capture debug info about the [Request].
32+
DebugLogRequest struct {
33+
Host string `json:"host"`
34+
URI string `json:"uri"`
35+
Method string `json:"method"`
36+
Proto string `json:"proto"`
37+
Header http.Header `json:"header"`
38+
CurlCmd string `json:"curl_cmd"`
39+
RetryTraceID string `json:"retry_trace_id"`
40+
Attempt int `json:"attempt"`
41+
Body string `json:"body"`
42+
}
43+
44+
// DebugLogResponse type used to capture debug info about the [Response].
45+
DebugLogResponse struct {
46+
StatusCode int `json:"status_code"`
47+
Status string `json:"status"`
48+
Proto string `json:"proto"`
49+
ReceivedAt time.Time `json:"received_at"`
50+
Duration time.Duration `json:"duration"`
51+
Size int64 `json:"size"`
52+
Header http.Header `json:"header"`
53+
Body string `json:"body"`
54+
}
55+
)
56+
57+
// DebugLogFormatter function formats the given debug log info in human readable
58+
// format.
59+
//
60+
// This is the default debug log formatter in the Resty.
61+
func DebugLogFormatter(dl *DebugLog) string {
62+
debugLog := "\n==============================================================================\n"
63+
64+
req := dl.Request
65+
if len(req.CurlCmd) > 0 {
66+
debugLog += "~~~ REQUEST(CURL) ~~~\n" +
67+
fmt.Sprintf(" %v\n", req.CurlCmd)
68+
}
69+
debugLog += "~~~ REQUEST ~~~\n" +
70+
fmt.Sprintf("%s %s %s\n", req.Method, req.URI, req.Proto) +
71+
fmt.Sprintf("HOST : %s\n", req.Host) +
72+
fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(req.Header)) +
73+
fmt.Sprintf("BODY :\n%v\n", req.Body) +
74+
"------------------------------------------------------------------------------\n"
75+
if len(req.RetryTraceID) > 0 {
76+
debugLog += fmt.Sprintf("RETRY TRACE ID: %s\n", req.RetryTraceID) +
77+
fmt.Sprintf("ATTEMPT : %d\n", req.Attempt) +
78+
"------------------------------------------------------------------------------\n"
79+
}
80+
81+
res := dl.Response
82+
debugLog += "~~~ RESPONSE ~~~\n" +
83+
fmt.Sprintf("STATUS : %s\n", res.Status) +
84+
fmt.Sprintf("PROTO : %s\n", res.Proto) +
85+
fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt.Format(time.RFC3339Nano)) +
86+
fmt.Sprintf("DURATION : %v\n", res.Duration) +
87+
"HEADERS :\n" +
88+
composeHeaders(res.Header) + "\n" +
89+
fmt.Sprintf("BODY :\n%v\n", res.Body)
90+
if dl.TraceInfo != nil {
91+
debugLog += "------------------------------------------------------------------------------\n"
92+
debugLog += fmt.Sprintf("%v\n", dl.TraceInfo)
93+
}
94+
debugLog += "==============================================================================\n"
95+
96+
return debugLog
97+
}
98+
99+
// DebugLogJSONFormatter function formats the given debug log info in JSON format.
100+
func DebugLogJSONFormatter(dl *DebugLog) string {
101+
return toJSON(dl)
102+
}
103+
104+
func debugLogger(c *Client, res *Response) {
105+
req := res.Request
106+
if !req.Debug {
107+
return
108+
}
109+
110+
rdl := &DebugLogResponse{
111+
StatusCode: res.StatusCode(),
112+
Status: res.Status(),
113+
Proto: res.Proto(),
114+
ReceivedAt: res.ReceivedAt(),
115+
Duration: res.Time(),
116+
Size: res.Size(),
117+
Header: sanitizeHeaders(res.Header().Clone()),
118+
Body: res.fmtBodyString(res.Request.DebugBodyLimit),
119+
}
120+
121+
dl := &DebugLog{
122+
Request: req.values[debugRequestLogKey].(*DebugLogRequest),
123+
Response: rdl,
124+
}
125+
126+
if res.Request.IsTrace {
127+
ti := req.TraceInfo()
128+
dl.TraceInfo = &ti
129+
}
130+
131+
dblCallback := c.debugLogCallbackFunc()
132+
if dblCallback != nil {
133+
dblCallback(dl)
134+
}
135+
136+
formatterFunc := c.debugLogFormatterFunc()
137+
if formatterFunc != nil {
138+
debugLog := formatterFunc(dl)
139+
req.log.Debugf("%s", debugLog)
140+
}
141+
}
142+
143+
const debugRequestLogKey = "__restyDebugRequestLog"
144+
145+
func prepareRequestDebugInfo(c *Client, r *Request) {
146+
if !r.Debug {
147+
return
148+
}
149+
150+
rr := r.RawRequest
151+
rh := rr.Header.Clone()
152+
if c.Client().Jar != nil {
153+
for _, cookie := range c.Client().Jar.Cookies(r.RawRequest.URL) {
154+
s := fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)
155+
if c := rh.Get(hdrCookieKey); isStringEmpty(c) {
156+
rh.Set(hdrCookieKey, s)
157+
} else {
158+
rh.Set(hdrCookieKey, c+"; "+s)
159+
}
160+
}
161+
}
162+
163+
rdl := &DebugLogRequest{
164+
Host: rr.URL.Host,
165+
URI: rr.URL.RequestURI(),
166+
Method: r.Method,
167+
Proto: rr.Proto,
168+
Header: sanitizeHeaders(rh),
169+
Body: r.fmtBodyString(r.DebugBodyLimit),
170+
}
171+
if r.generateCurlCmd && r.debugLogCurlCmd {
172+
rdl.CurlCmd = r.resultCurlCmd
173+
}
174+
if len(r.RetryTraceID) > 0 {
175+
rdl.Attempt = r.Attempt
176+
rdl.RetryTraceID = r.RetryTraceID
177+
}
178+
179+
r.initValuesMap()
180+
r.values[debugRequestLogKey] = rdl
181+
}

middleware.go

-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import (
2121
"strings"
2222
)
2323

24-
const debugRequestLogKey = "__restyDebugRequestLog"
25-
2624
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
2725
// Request Middleware(s)
2826
//_______________________________________________________________________

request_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -1850,6 +1850,24 @@ func TestTraceInfo(t *testing.T) {
18501850
assertEqual(t, len(requestURLs), len(matches))
18511851
})
18521852

1853+
t.Run("enable trace and debug on request json formatter", func(t *testing.T) {
1854+
c, logBuf := dcldb()
1855+
c.SetBaseURL(ts.URL)
1856+
c.SetDebugLogFormatter(DebugLogJSONFormatter)
1857+
1858+
requestURLs := []string{"/", "/json", "/long-text", "/long-json"}
1859+
for _, u := range requestURLs {
1860+
resp, err := c.R().EnableTrace().EnableDebug().Get(u)
1861+
assertNil(t, err)
1862+
assertNotNil(t, resp)
1863+
}
1864+
1865+
logContent := logBuf.String()
1866+
regexTraceInfoHeader := regexp.MustCompile(`"trace_info":{"`)
1867+
matches := regexTraceInfoHeader.FindAllStringIndex(logContent, -1)
1868+
assertEqual(t, len(requestURLs), len(matches))
1869+
})
1870+
18531871
// for sake of hook funcs
18541872
_, _ = client.R().SetTrace(true).Get("https://httpbin.org/get")
18551873
}

response.go

+4
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ func (r *Response) fmtBodyString(sl int) string {
185185
return "***** DO NOT PARSE RESPONSE - Enabled *****"
186186
}
187187

188+
if r.Request.IsSaveResponse {
189+
return "***** RESPONSE WRITTEN INTO FILE *****"
190+
}
191+
188192
bl := len(r.bodyBytes)
189193
if r.IsRead && bl == 0 {
190194
return "***** RESPONSE BODY IS ALREADY READ - see Response.{Result()/Error()} *****"

resty.go

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func createClient(hc *http.Client) *Client {
182182

183183
// Logger
184184
c.SetLogger(createLogger())
185+
c.SetDebugLogFormatter(DebugLogFormatter)
185186

186187
c.AddContentTypeEncoder(jsonKey, encodeJSON)
187188
c.AddContentTypeEncoder(xmlKey, encodeXML)

0 commit comments

Comments
 (0)