-
Notifications
You must be signed in to change notification settings - Fork 0
/
resty.go
246 lines (216 loc) · 7.31 KB
/
resty.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
/*
package xopresty adds to the resty package to
propagate xop context to through an HTTP request.
As of March 29th, 2023, the released resty package does not provide
a way to have a logger that knows which request it is logging about.
The resty package does not provide a way to know when requests are
complete.
Pull requests to fix these issues have been merged but not
made part of a release.
In the meantime, this package depends upon https://github.com/muir/resty.
The agumented resty Client requires that a context that
has the parent log span be provided:
client.R().SetContext(log.IntoContext(context.Background()))
If there is no logger in the context, the request will fail.
If you use resty's Client.SetDebug(true), note that the output
will be logged at Debug level which is below the default
minimum level for xop.
*/
package xopresty
import (
"context"
"fmt"
"strings"
"time"
"github.com/xoplog/xop-go"
"github.com/xoplog/xop-go/xopconst"
"github.com/xoplog/xop-go/xoptrace"
"github.com/muir/resty"
"github.com/pkg/errors"
)
var _ resty.Logger = restyLogger{}
type restyLogger struct {
log *xop.Logger
}
func (rl restyLogger) Errorf(format string, v ...interface{}) { rl.log.Error().Msgf(format, v...) }
func (rl restyLogger) Warnf(format string, v ...interface{}) { rl.log.Warn().Msgf(format, v...) }
func (rl restyLogger) Debugf(format string, v ...interface{}) { rl.log.Debug().Msgf(format, v...) }
type contextKeyType struct{}
var contextKey = contextKeyType{}
type contextNameType struct{}
var contextNameKey = contextNameType{}
type contextValue struct {
b3Sent bool
b3Trace xoptrace.Trace
response bool
log *xop.Logger
retryCount int
originalStartTime time.Time
}
type config struct {
requestToName func(r *resty.Request) string
extraLogging ExtraLogging
}
type ClientOpt func(*config)
var traceResponseHeaderKey = xop.Key("header")
var requestTimeKey = xop.Key("request_time.total")
var requestTimeServerKey = xop.Key("request_time.server")
var requestTimeDNSKey = xop.Key("request_time.dns")
// WithNameGenerate provides a function to convert a request into
// a description for the span.
func WithNameGenerate(f func(*resty.Request) string) ClientOpt {
return func(config *config) {
config.requestToName = f
}
}
// ExtraLogging provides a hook for extra logging to be done.
// It is possible that the response parameter will be null.
// If error is not null, then the request has failed.
// ExtraLogging should only be called once but if another resty
// callback panic's, it is possible ExtraLogging will be called
// twice.
type ExtraLogging func(log *xop.Logger, originalStartTime time.Time, retryCount int, request *resty.Request, response *resty.Response, err error)
func WithExtraLogging(f ExtraLogging) ClientOpt {
return func(config *config) {
config.extraLogging = f
}
}
// WithNameInDescription adds a span name to a context. If present,
// a name in context overrides WithNameGenerate.
func WithNameInContext(ctx context.Context, nameOrDescription string) context.Context {
return context.WithValue(ctx, contextNameKey, nameOrDescription)
}
func Client(client *resty.Client, opts ...ClientOpt) *resty.Client {
config := &config{
requestToName: func(r *resty.Request) string {
url := r.URL
i := strings.IndexByte(url, '?')
if i != -1 {
url = url[:i]
}
return r.Method + " " + url
},
extraLogging: func(log *xop.Logger, originalStartTime time.Time, retryCount int, request *resty.Request, response *resty.Response, err error) {
},
}
for _, f := range opts {
f(config)
}
// c := *client
// c.Header = client.Header.Clone()
// clinet = &c
return client.
OnBeforeRequest(func(_ *resty.Client, r *resty.Request) error {
// OnBeforeRequest can execute multiple times for the same attempt if there
// are retries. It won't execute at all of the request is invalid.
ctx := r.Context()
cvRaw := ctx.Value(contextKey)
var cv *contextValue
if cvRaw != nil {
cv = cvRaw.(*contextValue)
cv.retryCount++
return nil
}
log, ok := xop.FromContext(r.Context())
if !ok {
return errors.Errorf("context is missing logger, use Request.SetContext(Log.IntoContext(request.Context()))")
}
nameRaw := ctx.Value(contextNameKey)
var name string
if nameRaw != nil {
name = nameRaw.(string)
} else {
name = config.requestToName(r)
}
log = log.Sub().Step(name)
cv = &contextValue{
originalStartTime: time.Now(),
log: log,
}
r.SetContext(context.WithValue(ctx, contextKey, cv))
r.SetLogger(restyLogger{log: log})
if r.Body != nil {
log.Trace().Model(r.Body, "request")
}
log.Span().EmbeddedEnum(xopconst.SpanTypeHTTPClientRequest)
log.Span().String(xopconst.URL, r.URL)
log.Span().String(xopconst.HTTPMethod, r.Method)
r.Header.Set("traceparent", log.Span().Bundle().Trace.String())
if !log.Span().TraceBaggage().IsZero() {
r.Header.Set("baggage", log.Span().TraceBaggage().String())
}
if !log.Span().TraceState().IsZero() {
r.Header.Set("state", log.Span().TraceState().String())
}
if log.Config().UseB3 {
b3Trace := log.Span().Bundle().Trace
b3Trace.SpanID().SetRandom()
r.Header.Set("b3",
b3Trace.GetTraceID().String()+"-"+
b3Trace.TraceID().String()+"-"+
b3Trace.GetFlags().String()[1:2]+"-"+
log.Span().Trace().GetSpanID().String())
cv.b3Trace = b3Trace
cv.b3Sent = true
}
return nil
}).
OnAfterResponse(func(_ *resty.Client, resp *resty.Response) error {
// OnAfterRequest is run for each individual request attempt
r := resp.Request
ctx := r.Context()
cvRaw := ctx.Value(contextKey)
var cv *contextValue
if cvRaw == nil {
return fmt.Errorf("xopresty: internal error, context missing in response")
}
cv = cvRaw.(*contextValue)
log := cv.log
tr := resp.Header().Get("traceresponse")
if tr != "" {
trace, ok := xoptrace.TraceFromString(tr)
if ok {
cv.response = true
log.Info().Link(trace, xopconst.RemoteTrace.Key().String())
log.Span().Link(xopconst.RemoteTrace, trace)
} else {
log.Warn().String(traceResponseHeaderKey, tr).Msg("invalid traceresponse received")
}
}
if r.Result != nil {
log.Info().Model(resp.Result(), "response")
}
ti := r.TraceInfo()
if ti.TotalTime != 0 {
log.Info().
Duration(requestTimeKey, ti.TotalTime).
Duration(requestTimeServerKey, ti.ServerTime).
Duration(requestTimeDNSKey, ti.DNSLookup).
Msg("timings")
}
return nil
}).
OnError(func(r *resty.Request, err error) {
ctx := r.Context()
cv := ctx.Value(contextKey).(*contextValue)
log := cv.log
var re *resty.ResponseError
if errors.As(err, &re) {
config.extraLogging(log, cv.originalStartTime, cv.retryCount, r, re.Response, re.Err)
} else {
config.extraLogging(log, cv.originalStartTime, cv.retryCount, r, nil, err)
}
}).
OnPanic(func(r *resty.Request, err error) {
ctx := r.Context()
cv := ctx.Value(contextKey).(*contextValue)
log := cv.log
config.extraLogging(log, cv.originalStartTime, cv.retryCount, r, nil, err)
}).
OnSuccess(func(c *resty.Client, resp *resty.Response) {
ctx := resp.Request.Context()
cv := ctx.Value(contextKey).(*contextValue)
log := cv.log
config.extraLogging(log, cv.originalStartTime, cv.retryCount, resp.Request, resp, nil)
})
}