Skip to content

Commit 4225157

Browse files
authored
feat: add retry trace id and attempt details into debug log #897 (#909)
1 parent 9e0e070 commit 4225157

File tree

4 files changed

+167
-5
lines changed

4 files changed

+167
-5
lines changed

request.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ type Request struct {
6868
RetryStrategy RetryStrategyFunc
6969
IsRetryDefaultConditions bool
7070

71+
// RetryTraceID provides GUID for retry count > 0
72+
RetryTraceID string
73+
7174
// Attempt provides insights into no. of attempts
7275
// Resty made.
7376
//
@@ -1260,6 +1263,7 @@ func (r *Request) Execute(method, url string) (res *Response, err error) {
12601263
var backoff *backoffWithJitter
12611264
if r.RetryCount > 0 && isIdempotent {
12621265
backoff = newBackoffWithJitter(r.RetryWaitTime, r.RetryMaxWaitTime)
1266+
r.RetryTraceID = newGUID()
12631267
}
12641268

12651269
isInvalidRequestErr := false

retry_test.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ func TestConditionalGetRequestLevel(t *testing.T) {
6262
})
6363

6464
// Clear the default client.
65-
c := dcnl()
65+
c, lb := dcldb()
66+
6667
resp, err := c.R().
68+
EnableDebug().
6769
AddRetryCondition(check).
6870
SetRetryCount(1).
6971
SetRetryWaitTime(50*time.Millisecond).
@@ -77,6 +79,7 @@ func TestConditionalGetRequestLevel(t *testing.T) {
7779
assertEqual(t, "TestGet: text response", resp.String())
7880
assertEqual(t, 1, resp.Request.Attempt)
7981
assertEqual(t, 1, externalCounter)
82+
assertEqual(t, true, strings.Contains(lb.String(), "RETRY TRACE ID:"))
8083

8184
logResponse(t, resp)
8285
}
@@ -111,8 +114,9 @@ func TestClientRetryWithMinAndMaxWaitTime(t *testing.T) {
111114
retryWaitTime := 10 * time.Millisecond
112115
retryMaxWaitTime := 100 * time.Millisecond
113116

114-
c := dcnl().
115-
SetRetryCount(retryCount).
117+
c, lb := dcldb()
118+
119+
c.SetRetryCount(retryCount).
116120
SetRetryWaitTime(retryWaitTime).
117121
SetRetryMaxWaitTime(retryMaxWaitTime).
118122
AddRetryCondition(
@@ -121,13 +125,15 @@ func TestClientRetryWithMinAndMaxWaitTime(t *testing.T) {
121125
return true
122126
},
123127
)
124-
res, _ := c.R().Get(ts.URL + "/set-retrywaittime-test")
128+
res, _ := c.R().EnableDebug().Get(ts.URL + "/set-retrywaittime-test")
125129

126130
retryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())
127131

128132
// retryCount+1 == attempts were made
129133
assertEqual(t, retryCount+1, res.Request.Attempt)
130134

135+
assertEqual(t, true, strings.Contains(lb.String(), "RETRY TRACE ID:"))
136+
131137
// Initial attempt has 0 time slept since last request
132138
assertEqual(t, retryIntervals[0], uint64(0))
133139

util.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ package resty
77

88
import (
99
"bytes"
10+
"crypto/md5"
11+
"crypto/rand"
12+
"encoding/binary"
13+
"encoding/hex"
14+
"errors"
1015
"fmt"
1116
"io"
1217
"log"
@@ -17,6 +22,7 @@ import (
1722
"runtime"
1823
"sort"
1924
"strings"
25+
"sync/atomic"
2026
"time"
2127
)
2228

@@ -348,11 +354,16 @@ func requestDebugLogger(c *Client, r *Request) {
348354
reqLog += "~~~ REQUEST ~~~\n" +
349355
fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) +
350356
fmt.Sprintf("HOST : %s\n", rr.URL.Host) +
351-
fmt.Sprintf("ATTEMPT: %d\n", r.Attempt) +
352357
fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(rl.Header)) +
353358
fmt.Sprintf("BODY :\n%v\n", rl.Body) +
354359
"------------------------------------------------------------------------------\n"
355360

361+
if len(r.RetryTraceID) > 0 {
362+
reqLog += fmt.Sprintf("RETRY TRACE ID: %s\n", r.RetryTraceID) +
363+
fmt.Sprintf("ATTEMPT : %d\n", r.Attempt) +
364+
"------------------------------------------------------------------------------\n"
365+
}
366+
356367
r.initValuesMap()
357368
r.values[debugRequestLogKey] = reqLog
358369
}
@@ -392,3 +403,88 @@ func responseDebugLogger(c *Client, res *Response) {
392403

393404
res.Request.log.Debugf("%s", debugLog)
394405
}
406+
407+
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
408+
// GUID generation
409+
// Code inspired from mgo/bson ObjectId
410+
// Code obtained from https://github.com/go-aah/aah/blob/edge/essentials/guid.go
411+
//___________________________________
412+
413+
var (
414+
// guidCounter is atomically incremented when generating a new GUID
415+
// using UniqueID() function. It's used as a counter part of an id.
416+
guidCounter = readRandomUint32()
417+
418+
// machineID stores machine id generated once and used in subsequent calls
419+
// to UniqueId function.
420+
machineID = readMachineID()
421+
422+
// processID is current Process Id
423+
processID = os.Getpid()
424+
)
425+
426+
// newGUID method returns a new Globally Unique Identifier (GUID).
427+
//
428+
// The 12-byte `UniqueId` consists of-
429+
// - 4-byte value representing the seconds since the Unix epoch,
430+
// - 3-byte machine identifier,
431+
// - 2-byte process id, and
432+
// - 3-byte counter, starting with a random value.
433+
//
434+
// Uses Mongo Object ID algorithm to generate globally unique ids -
435+
// https://docs.mongodb.com/manual/reference/method/ObjectId/
436+
func newGUID() string {
437+
var b [12]byte
438+
// Timestamp, 4 bytes, big endian
439+
binary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix()))
440+
441+
// Machine, first 3 bytes of md5(hostname)
442+
b[4], b[5], b[6] = machineID[0], machineID[1], machineID[2]
443+
444+
// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
445+
b[7], b[8] = byte(processID>>8), byte(processID)
446+
447+
// Increment, 3 bytes, big endian
448+
i := atomic.AddUint32(&guidCounter, 1)
449+
b[9], b[10], b[11] = byte(i>>16), byte(i>>8), byte(i)
450+
451+
return hex.EncodeToString(b[:])
452+
}
453+
454+
var ioReadFull = io.ReadFull
455+
456+
// readRandomUint32 returns a random guidCounter.
457+
func readRandomUint32() uint32 {
458+
var b [4]byte
459+
if _, err := ioReadFull(rand.Reader, b[:]); err == nil {
460+
return (uint32(b[0]) << 0) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
461+
}
462+
463+
// To initialize package unexported variable 'guidCounter'.
464+
// This panic would happen at program startup, so no worries at runtime panic.
465+
panic(errors.New("resty - guid: unable to generate random object id"))
466+
}
467+
468+
var osHostname = os.Hostname
469+
470+
// readMachineID generates and returns a machine id.
471+
// If this function fails to get the hostname it will cause a runtime error.
472+
func readMachineID() []byte {
473+
var sum [3]byte
474+
id := sum[:]
475+
476+
if hostname, err := osHostname(); err == nil {
477+
hw := md5.New()
478+
_, _ = hw.Write([]byte(hostname))
479+
copy(id, hw.Sum(nil))
480+
return id
481+
}
482+
483+
if _, err := ioReadFull(rand.Reader, id); err == nil {
484+
return id
485+
}
486+
487+
// To initialize package unexported variable 'machineID'.
488+
// This panic would happen at program startup, so no worries at runtime panic.
489+
panic(errors.New("resty - guid: unable to get hostname and random bytes"))
490+
}

util_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package resty
88
import (
99
"bytes"
1010
"errors"
11+
"io"
1112
"net/url"
1213
"os"
1314
"path/filepath"
@@ -124,6 +125,61 @@ func Test_createDirectory(t *testing.T) {
124125
assertEqual(t, errMsg, err.Error())
125126
}
126127

128+
func TestUtil_readRandomUint32(t *testing.T) {
129+
defer func() {
130+
if r := recover(); r == nil {
131+
// panic: resty - guid: unable to generate random object id
132+
t.Errorf("The code did not panic")
133+
}
134+
}()
135+
errMsg := "read full error"
136+
ioReadFull = func(_ io.Reader, _ []byte) (int, error) {
137+
return 0, errors.New(errMsg)
138+
}
139+
t.Cleanup(func() {
140+
ioReadFull = io.ReadFull
141+
})
142+
143+
readRandomUint32()
144+
}
145+
146+
func TestUtil_readMachineID(t *testing.T) {
147+
t.Run("hostname error", func(t *testing.T) {
148+
errHostMsg := "hostname error"
149+
osHostname = func() (string, error) {
150+
return "", errors.New(errHostMsg)
151+
}
152+
t.Cleanup(func() {
153+
osHostname = os.Hostname
154+
})
155+
156+
readMachineID()
157+
})
158+
159+
t.Run("hostname and read full error", func(t *testing.T) {
160+
defer func() {
161+
if r := recover(); r == nil {
162+
// panic: resty - guid: unable to get hostname and random bytes
163+
t.Errorf("The code did not panic")
164+
}
165+
}()
166+
errHostMsg := "hostname error"
167+
osHostname = func() (string, error) {
168+
return "", errors.New(errHostMsg)
169+
}
170+
errReadMsg := "read full error"
171+
ioReadFull = func(_ io.Reader, _ []byte) (int, error) {
172+
return 0, errors.New(errReadMsg)
173+
}
174+
t.Cleanup(func() {
175+
osHostname = os.Hostname
176+
ioReadFull = io.ReadFull
177+
})
178+
179+
readMachineID()
180+
})
181+
}
182+
127183
// This test methods exist for test coverage purpose
128184
// to validate the getter and setter
129185
func TestUtilMiscTestCoverage(t *testing.T) {

0 commit comments

Comments
 (0)