Skip to content

Commit 8358863

Browse files
authored
Merge pull request #95 from deploymenttheory/dev
Dev
2 parents 73a6d5f + a46aa35 commit 8358863

9 files changed

+208
-120
lines changed

httpclient/httpclient_auth_bearer_token.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"net/http"
1010
"time"
1111

12-
"github.com/deploymenttheory/go-api-http-client/errors"
1312
"github.com/deploymenttheory/go-api-http-client/logger"
1413
"go.uber.org/zap"
1514
)
@@ -40,21 +39,21 @@ func (c *Client) ObtainToken(log logger.Logger) error {
4039

4140
req, err := http.NewRequest("POST", authenticationEndpoint, nil)
4241
if err != nil {
43-
log.Error("Failed to create new request for token", zap.Error(err))
42+
log.LogError("POST", authenticationEndpoint, 0, err, "Failed to create new request for token")
4443
return err
4544
}
4645
req.SetBasicAuth(c.BearerTokenAuthCredentials.Username, c.BearerTokenAuthCredentials.Password)
4746

4847
resp, err := c.httpClient.Do(req)
4948
if err != nil {
50-
log.Error("Failed to make request for token", zap.Error(err))
49+
log.LogError("POST", authenticationEndpoint, 0, err, "Failed to make request for token")
5150
return err
5251
}
5352
defer resp.Body.Close()
5453

5554
if resp.StatusCode != http.StatusOK {
56-
log.Warn("Received non-OK response while obtaining token", zap.Int("StatusCode", resp.StatusCode))
57-
return errors.HandleAPIError(resp, log)
55+
log.Error("Received non-OK response while obtaining token", zap.Int("StatusCode", resp.StatusCode))
56+
return err
5857
}
5958

6059
tokenResp := &TokenResponse{}
@@ -101,7 +100,7 @@ func (c *Client) RefreshToken(log logger.Logger) error {
101100

102101
if resp.StatusCode != http.StatusOK {
103102
log.Warn("Token refresh response status is not OK", zap.Int("StatusCode", resp.StatusCode))
104-
return errors.HandleAPIError(resp, log)
103+
return err
105104
}
106105

107106
tokenResp := &TokenResponse{}

httpclient/httpclient_mocklogger.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
package httpclient
33

44
import (
5+
"time"
6+
57
"github.com/deploymenttheory/go-api-http-client/logger"
68
"github.com/stretchr/testify/mock"
79
"go.uber.org/zap"
@@ -28,6 +30,8 @@ func (m *MockLogger) SetLevel(level logger.LogLevel) {
2830
m.Called(level)
2931
}
3032

33+
// Mock implementations for unstructured logging methods
34+
3135
func (m *MockLogger) Debug(msg string, fields ...zap.Field) {
3236
m.Called(msg, fields)
3337
}
@@ -40,7 +44,6 @@ func (m *MockLogger) Warn(msg string, fields ...zap.Field) {
4044
m.Called(msg, fields)
4145
}
4246

43-
// Error method
4447
func (m *MockLogger) Error(msg string, fields ...zap.Field) error {
4548
args := m.Called(msg, fields)
4649
return args.Error(0)
@@ -63,3 +66,29 @@ func (m *MockLogger) GetLogLevel() logger.LogLevel {
6366
args := m.Called()
6467
return args.Get(0).(logger.LogLevel)
6568
}
69+
70+
// Mock implementations for structured logging methods
71+
72+
func (m *MockLogger) LogRequestStart(requestID string, userID string, method string, url string, headers map[string][]string) {
73+
m.Called(requestID, userID, method, url, headers)
74+
}
75+
76+
func (m *MockLogger) LogRequestEnd(method string, url string, statusCode int, duration time.Duration) {
77+
m.Called(method, url, statusCode, duration)
78+
}
79+
80+
func (m *MockLogger) LogError(method string, url string, statusCode int, err error, stacktrace string) {
81+
m.Called(method, url, statusCode, err, stacktrace)
82+
}
83+
84+
func (m *MockLogger) LogRetryAttempt(method string, url string, attempt int, reason string, waitDuration time.Duration, err error) {
85+
m.Called(method, url, attempt, reason, waitDuration, err)
86+
}
87+
88+
func (m *MockLogger) LogRateLimiting(method string, url string, retryAfter string, waitDuration time.Duration) {
89+
m.Called(method, url, retryAfter, waitDuration)
90+
}
91+
92+
func (m *MockLogger) LogResponse(method string, url string, statusCode int, responseBody string, responseHeaders map[string][]string, duration time.Duration) {
93+
m.Called(method, url, statusCode, responseBody, responseHeaders, duration)
94+
}

httpclient/httpclient_request.go

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"net/http"
88
"time"
99

10-
"github.com/deploymenttheory/go-api-http-client/errors"
1110
"github.com/deploymenttheory/go-api-http-client/logger"
11+
"github.com/deploymenttheory/go-api-http-client/status"
1212
"github.com/google/uuid"
1313
"go.uber.org/zap"
1414
)
@@ -153,71 +153,66 @@ func (c *Client) executeRequestWithRetries(method, endpoint string, body, out in
153153
headerManager.SetRequestHeaders(endpoint)
154154
headerManager.LogHeaders(c)
155155

156-
// Define a retry deadline based on the client's total retry duration configuration
156+
// Calculate the deadline for all retry attempts based on the client configuration.
157157
totalRetryDeadline := time.Now().Add(c.clientConfig.ClientOptions.TotalRetryDuration)
158158

159+
// Execute the HTTP request with retries
159160
var resp *http.Response
160161
var retryCount int
161-
for time.Now().Before(totalRetryDeadline) { // Check if the current time is before the total retry deadline
162-
req = req.WithContext(ctx)
162+
var requestStartTime = time.Now()
163+
164+
for time.Now().Before(totalRetryDeadline) {
163165
resp, err = c.do(req, log, method, endpoint)
164-
// Check for successful status code
165-
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 400 {
166-
if resp.StatusCode >= 300 {
167-
log.Warn("Redirect response received", zap.Int("status_code", resp.StatusCode), zap.String("location", resp.Header.Get("Location")))
168-
}
169-
// Handle the response as successful, even if it's a redirect.
166+
167+
// Successful response handling
168+
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
169+
log.LogRequestEnd(method, endpoint, resp.StatusCode, time.Since(requestStartTime))
170170
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
171171
}
172172

173-
// Leverage TranslateStatusCode for more descriptive error logging
174-
statusMessage := errors.TranslateStatusCode(resp)
175-
176-
// Check for non-retryable errors
177-
if resp != nil && errors.IsNonRetryableStatusCode(resp) {
178-
log.Warn("Non-retryable error received", zap.Int("status_code", resp.StatusCode), zap.String("status_message", statusMessage))
179-
return resp, errors.HandleAPIError(resp, log)
173+
// Log and handle non-retryable errors immediately without retrying
174+
if resp != nil && status.IsNonRetryableStatusCode(resp) {
175+
log.LogError(method, endpoint, resp.StatusCode, err, status.TranslateStatusCode(resp))
176+
return resp, err
180177
}
181178

182-
// Parsing rate limit headers if a rate-limit error is detected
183-
if errors.IsRateLimitError(resp) {
179+
// Handle rate-limiting errors by parsing the 'Retry-After' header and waiting before the next retry
180+
if status.IsRateLimitError(resp) {
184181
waitDuration := parseRateLimitHeaders(resp, log)
185-
if waitDuration > 0 {
186-
log.Warn("Rate limit encountered, waiting before retrying", zap.Duration("waitDuration", waitDuration))
187-
time.Sleep(waitDuration)
188-
continue // Continue to next iteration after waiting
189-
}
182+
log.LogRateLimiting(method, endpoint, resp.Header.Get("Retry-After"), waitDuration)
183+
time.Sleep(waitDuration)
184+
continue
190185
}
191186

192-
// Handling retryable errors with exponential backoff
193-
if errors.IsTransientError(resp) {
187+
// Retry the request for transient errors using exponential backoff with jitter
188+
if status.IsTransientError(resp) {
194189
retryCount++
195190
if retryCount > c.clientConfig.ClientOptions.MaxRetryAttempts {
196-
log.Warn("Max retry attempts reached", zap.String("method", method), zap.String("endpoint", endpoint))
197-
break // Stop retrying if max attempts are reached
191+
// Log max retry attempts reached with structured logging
192+
log.LogError(method, endpoint, resp.StatusCode, err, "Max retry attempts reached")
193+
break
198194
}
199195
waitDuration := calculateBackoff(retryCount)
200-
log.Warn("Retrying request due to transient error", zap.String("method", method), zap.String("endpoint", endpoint), zap.Int("retryCount", retryCount), zap.Duration("waitDuration", waitDuration), zap.Error(err))
201-
time.Sleep(waitDuration) // Wait before retrying
202-
continue // Continue to next iteration after waiting
196+
log.LogRetryAttempt(method, endpoint, retryCount, "Transient error", waitDuration, err)
197+
time.Sleep(waitDuration)
198+
continue
203199
}
204200

205-
// Handle error responses
206-
if err != nil || !errors.IsRetryableStatusCode(resp.StatusCode) {
207-
if apiErr := errors.HandleAPIError(resp, log); apiErr != nil {
208-
err = apiErr
209-
}
210-
log.Error("API error", zap.String("status_message", statusMessage), zap.Error(err))
201+
// Log non-retryable API errors and break the retry loop
202+
if err != nil || !status.IsRetryableStatusCode(resp.StatusCode) {
203+
log.LogError(method, endpoint, resp.StatusCode, err, status.TranslateStatusCode(resp))
211204
break
212205
}
213206
}
214207

215-
// Handles final non-API error.
208+
// Final error handling after all retries are exhausted
216209
if err != nil {
210+
// Log the final error after retries with structured logging
211+
log.LogError(method, endpoint, 0, err, "Final error after retries")
217212
return nil, err
218213
}
219214

220-
return resp, errors.HandleAPIError(resp, log)
215+
return resp, nil
221216
}
222217

223218
// executeRequest executes an HTTP request using the specified method, endpoint, and request body without implementing

logger/zaplogger_logfields.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package logger
2+
3+
import (
4+
"time"
5+
6+
"go.uber.org/zap"
7+
)
8+
9+
// LogRequestStart logs the initiation of an HTTP request if the current log level permits.
10+
func (d *defaultLogger) LogRequestStart(requestID string, userID string, method string, url string, headers map[string][]string) {
11+
if d.logLevel <= LogLevelInfo {
12+
fields := []zap.Field{
13+
zap.String("event", "request_start"),
14+
zap.String("method", method),
15+
zap.String("url", url),
16+
zap.String("request_id", requestID),
17+
zap.String("user_id", userID),
18+
zap.Reflect("headers", headers),
19+
}
20+
d.logger.Info("HTTP request started", fields...)
21+
}
22+
}
23+
24+
// LogRequestEnd logs the completion of an HTTP request if the current log level permits.
25+
func (d *defaultLogger) LogRequestEnd(method string, url string, statusCode int, duration time.Duration) {
26+
if d.logLevel <= LogLevelInfo {
27+
fields := []zap.Field{
28+
zap.String("event", "request_end"),
29+
zap.String("method", method),
30+
zap.String("url", url),
31+
zap.Int("status_code", statusCode),
32+
zap.Duration("duration", duration),
33+
}
34+
d.logger.Info("HTTP request completed", fields...)
35+
}
36+
}
37+
38+
// LogError logs an error that occurs during the processing of an HTTP request if the current log level permits.
39+
func (d *defaultLogger) LogError(method string, url string, statusCode int, err error, stacktrace string) {
40+
if d.logLevel <= LogLevelError {
41+
fields := []zap.Field{
42+
zap.String("event", "request_error"),
43+
zap.String("method", method),
44+
zap.String("url", url),
45+
zap.Int("status_code", statusCode),
46+
zap.String("error_message", err.Error()),
47+
zap.String("stacktrace", stacktrace),
48+
}
49+
d.logger.Error("Error during HTTP request", fields...)
50+
}
51+
}
52+
53+
// LogAuthTokenError logs issues encountered during the authentication token acquisition process.
54+
func (d *defaultLogger) LogAuthTokenError(method string, url string, statusCode int, err error) {
55+
if d.logLevel <= LogLevelError {
56+
fields := []zap.Field{
57+
zap.String("event", "auth_token_error"),
58+
zap.String("method", method),
59+
zap.String("url", url),
60+
zap.Int("status_code", statusCode),
61+
zap.String("error_message", err.Error()),
62+
}
63+
d.logger.Error("Error obtaining authentication token", fields...)
64+
}
65+
}
66+
67+
// LogRetryAttempt logs a retry attempt for an HTTP request if the current log level permits, including wait duration and the error that triggered the retry.
68+
func (d *defaultLogger) LogRetryAttempt(method string, url string, attempt int, reason string, waitDuration time.Duration, err error) {
69+
if d.logLevel <= LogLevelWarn {
70+
fields := []zap.Field{
71+
zap.String("event", "retry_attempt"),
72+
zap.String("method", method),
73+
zap.String("url", url),
74+
zap.Int("attempt", attempt),
75+
zap.String("reason", reason),
76+
zap.Duration("waitDuration", waitDuration),
77+
zap.String("error_message", err.Error()),
78+
}
79+
d.logger.Warn("HTTP request retry", fields...)
80+
}
81+
}
82+
83+
// LogRateLimiting logs when an HTTP request is rate-limited, including the HTTP method, URL, the value of the 'Retry-After' header, and the actual wait duration.
84+
func (d *defaultLogger) LogRateLimiting(method string, url string, retryAfter string, waitDuration time.Duration) {
85+
if d.logLevel <= LogLevelWarn {
86+
fields := []zap.Field{
87+
zap.String("event", "rate_limited"),
88+
zap.String("method", method),
89+
zap.String("url", url),
90+
zap.String("retry_after", retryAfter),
91+
zap.Duration("wait_duration", waitDuration),
92+
}
93+
d.logger.Warn("Rate limit encountered, waiting before retrying", fields...)
94+
}
95+
}
96+
97+
// LogResponse logs details about an HTTP response if the current log level permits.
98+
func (d *defaultLogger) LogResponse(method string, url string, statusCode int, responseBody string, responseHeaders map[string][]string, duration time.Duration) {
99+
if d.logLevel <= LogLevelInfo {
100+
fields := []zap.Field{
101+
zap.String("event", "response_received"),
102+
zap.String("method", method),
103+
zap.String("url", url),
104+
zap.Int("status_code", statusCode),
105+
zap.String("response_body", responseBody),
106+
zap.Reflect("response_headers", responseHeaders),
107+
zap.Duration("duration", duration),
108+
}
109+
d.logger.Info("HTTP response details", fields...)
110+
}
111+
}

logger/zaplogger_logfields_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// zaplogger_structured_messaging_test.go
2+
package logger

0 commit comments

Comments
 (0)