Skip to content

Commit 24044c1

Browse files
committed
feat: add outgoing requests monitoring
1 parent 417eb5d commit 24044c1

File tree

8 files changed

+231
-2
lines changed

8 files changed

+231
-2
lines changed

Diff for: chi/chi.go

+8
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,11 @@ var WithMetricsEnabled = otelconfig.WithMetricsEnabled
114114
var WithTracesEnabled = otelconfig.WithTracesEnabled
115115
var WithSpanProcessor = otelconfig.WithSpanProcessor
116116
var WithSampler = otelconfig.WithSampler
117+
118+
func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client {
119+
return apt.HTTPClient(ctx, opts...)
120+
}
121+
122+
var WithRedactHeaders = apt.WithRedactHeaders
123+
var WithRedactRequestBody = apt.WithRedactRequestBody
124+
var WithRedactResponseBody = apt.WithRedactResponseBody

Diff for: echo/echo.go

+8
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,11 @@ var WithMetricsEnabled = otelconfig.WithMetricsEnabled
156156
var WithTracesEnabled = otelconfig.WithTracesEnabled
157157
var WithSpanProcessor = otelconfig.WithSpanProcessor
158158
var WithSampler = otelconfig.WithSampler
159+
160+
func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client {
161+
return apt.HTTPClient(ctx, opts...)
162+
}
163+
164+
var WithRedactHeaders = apt.WithRedactHeaders
165+
var WithRedactRequestBody = apt.WithRedactRequestBody
166+
var WithRedactResponseBody = apt.WithRedactResponseBody

Diff for: fiber/fiber.go

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package apitoolkitfiber
33
import (
44
"context"
55
"errors"
6+
"net/http"
67

78
apt "github.com/apitoolkit/apitoolkit-go"
89
fiber "github.com/gofiber/fiber/v2"
@@ -118,3 +119,11 @@ var WithMetricsEnabled = otelconfig.WithMetricsEnabled
118119
var WithTracesEnabled = otelconfig.WithTracesEnabled
119120
var WithSpanProcessor = otelconfig.WithSpanProcessor
120121
var WithSampler = otelconfig.WithSampler
122+
123+
func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client {
124+
return apt.HTTPClient(ctx, opts...)
125+
}
126+
127+
var WithRedactHeaders = apt.WithRedactHeaders
128+
var WithRedactRequestBody = apt.WithRedactRequestBody
129+
var WithRedactResponseBody = apt.WithRedactResponseBody

Diff for: gin/gin.go

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"io"
88
"log"
9+
"net/http"
910

1011
apt "github.com/apitoolkit/apitoolkit-go"
1112
"github.com/gin-gonic/gin"
@@ -141,3 +142,11 @@ var WithMetricsEnabled = otelconfig.WithMetricsEnabled
141142
var WithTracesEnabled = otelconfig.WithTracesEnabled
142143
var WithSpanProcessor = otelconfig.WithSpanProcessor
143144
var WithSampler = otelconfig.WithSampler
145+
146+
func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client {
147+
return apt.HTTPClient(ctx, opts...)
148+
}
149+
150+
var WithRedactHeaders = apt.WithRedactHeaders
151+
var WithRedactRequestBody = apt.WithRedactRequestBody
152+
var WithRedactResponseBody = apt.WithRedactResponseBody

Diff for: gorilla/gorilla.go

+8
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,11 @@ var WithMetricsEnabled = otelconfig.WithMetricsEnabled
108108
var WithTracesEnabled = otelconfig.WithTracesEnabled
109109
var WithSpanProcessor = otelconfig.WithSpanProcessor
110110
var WithSampler = otelconfig.WithSampler
111+
112+
func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client {
113+
return apt.HTTPClient(ctx, opts...)
114+
}
115+
116+
var WithRedactHeaders = apt.WithRedactHeaders
117+
var WithRedactRequestBody = apt.WithRedactRequestBody
118+
var WithRedactResponseBody = apt.WithRedactResponseBody

Diff for: native/native.go

+8
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,11 @@ var WithMetricsEnabled = otelconfig.WithMetricsEnabled
116116
var WithTracesEnabled = otelconfig.WithTracesEnabled
117117
var WithSpanProcessor = otelconfig.WithSpanProcessor
118118
var WithSampler = otelconfig.WithSampler
119+
120+
func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client {
121+
return apt.HTTPClient(ctx, opts...)
122+
}
123+
124+
var WithRedactHeaders = apt.WithRedactHeaders
125+
var WithRedactRequestBody = apt.WithRedactRequestBody
126+
var WithRedactResponseBody = apt.WithRedactResponseBody

Diff for: outgoing.go

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package apitoolkit
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"net/http"
8+
9+
"github.com/google/uuid"
10+
"go.opentelemetry.io/otel"
11+
"go.opentelemetry.io/otel/trace"
12+
)
13+
14+
type roundTripper struct {
15+
base http.RoundTripper
16+
ctx context.Context
17+
cfg *roundTripperConfig
18+
}
19+
20+
func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) {
21+
defer func() {
22+
if err != nil {
23+
ReportError(rt.ctx, err)
24+
}
25+
}()
26+
27+
tracer := otel.GetTracerProvider().Tracer("")
28+
_, span := tracer.Start(rt.ctx, "apitoolkit-http-span", trace.WithSpanKind(trace.SpanKindClient))
29+
30+
// Capture the request body
31+
reqBodyBytes := []byte{}
32+
if req.Body != nil {
33+
reqBodyBytes, _ = io.ReadAll(req.Body)
34+
req.Body = io.NopCloser(bytes.NewBuffer(reqBodyBytes))
35+
}
36+
37+
// Add a header to all outgoing requests "X-APITOOLKIT-TRACE-PARENT-ID"
38+
res, err = rt.base.RoundTrip(req)
39+
var errorList []ATError
40+
if err != nil {
41+
// Add the error for the given request payload
42+
errorList = append(errorList, BuildError(err))
43+
}
44+
45+
var payload Payload
46+
var parentMsgIDPtr *uuid.UUID
47+
parentMsgID, ok := rt.ctx.Value(CurrentRequestMessageID).(uuid.UUID)
48+
if ok {
49+
parentMsgIDPtr = &parentMsgID
50+
}
51+
52+
// Capture the response body
53+
conf := roundTripperConfigToConfig(rt.cfg)
54+
if res != nil {
55+
respBodyBytes, _ := io.ReadAll(res.Body)
56+
res.Body = io.NopCloser(bytes.NewBuffer(respBodyBytes))
57+
payload = BuildPayload(
58+
GoOutgoing,
59+
req, res.StatusCode, reqBodyBytes,
60+
respBodyBytes, res.Header, nil,
61+
req.URL.Path,
62+
rt.cfg.RedactHeaders, rt.cfg.RedactRequestBody, rt.cfg.RedactResponseBody,
63+
errorList,
64+
uuid.Nil,
65+
parentMsgIDPtr,
66+
conf,
67+
)
68+
CreateSpan(payload, conf, span)
69+
70+
} else {
71+
payload = BuildPayload(
72+
GoOutgoing,
73+
req, 503, reqBodyBytes,
74+
nil, nil, nil,
75+
req.URL.Path,
76+
rt.cfg.RedactHeaders, rt.cfg.RedactRequestBody, rt.cfg.RedactResponseBody,
77+
errorList,
78+
uuid.Nil,
79+
parentMsgIDPtr,
80+
conf,
81+
)
82+
CreateSpan(payload, conf, span)
83+
84+
}
85+
return res, err
86+
}
87+
88+
func HTTPClient(ctx context.Context, opts ...RoundTripperOption) *http.Client {
89+
// Run the roundTripperConfig to extract out a httpClient Transport
90+
cfg := new(roundTripperConfig)
91+
for _, opt := range opts {
92+
opt(cfg)
93+
}
94+
95+
httpClientV := *http.DefaultClient
96+
httpClient := &httpClientV
97+
if cfg.HTTPClient != nil {
98+
// Use httpClient supplied by user.
99+
v := *cfg.HTTPClient
100+
httpClient = &v
101+
}
102+
103+
httpClient.Transport = WrapRoundTripper(
104+
ctx, httpClient.Transport,
105+
opts...,
106+
)
107+
return httpClient
108+
}
109+
110+
type roundTripperConfig struct {
111+
HTTPClient *http.Client
112+
RedactHeaders []string
113+
RedactRequestBody []string
114+
RedactResponseBody []string
115+
}
116+
117+
type RoundTripperOption func(*roundTripperConfig)
118+
119+
// WithHTTPClient allows you supply your own custom http client
120+
func WithHTTPClient(httpClient *http.Client) RoundTripperOption {
121+
return func(rc *roundTripperConfig) {
122+
rc.HTTPClient = httpClient
123+
}
124+
}
125+
126+
func WithRedactHeaders(headers ...string) RoundTripperOption {
127+
return func(rc *roundTripperConfig) {
128+
rc.RedactHeaders = headers
129+
}
130+
}
131+
132+
func WithRedactRequestBody(fields ...string) RoundTripperOption {
133+
return func(rc *roundTripperConfig) {
134+
rc.RedactRequestBody = fields
135+
}
136+
}
137+
138+
func WithRedactResponseBody(fields ...string) RoundTripperOption {
139+
return func(rc *roundTripperConfig) {
140+
rc.RedactResponseBody = fields
141+
}
142+
}
143+
144+
// WrapRoundTripper returns a new RoundTripper which traces all requests sent
145+
// over the transport.
146+
func WrapRoundTripper(ctx context.Context, rt http.RoundTripper, opts ...RoundTripperOption) http.RoundTripper {
147+
cfg := new(roundTripperConfig)
148+
for _, opt := range opts {
149+
opt(cfg)
150+
}
151+
152+
// If no rt is passed in, then use the default standard library transport
153+
if rt == nil {
154+
rt = http.DefaultTransport
155+
}
156+
return &roundTripper{
157+
base: rt,
158+
ctx: ctx,
159+
cfg: cfg,
160+
}
161+
}
162+
163+
func roundTripperConfigToConfig(cfg *roundTripperConfig) Config {
164+
return Config{
165+
RedactHeaders: cfg.RedactHeaders,
166+
RedactRequestBody: cfg.RedactRequestBody,
167+
RedactResponseBody: cfg.RedactResponseBody,
168+
CaptureRequestBody: true,
169+
CaptureResponseBody: true,
170+
}
171+
}

Diff for: sdk.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ func CreateSpan(payload Payload, config Config, span trace.Span) {
9090
attribute.String("http.request.query_params", string(queryParams)),
9191
attribute.String("http.target", payload.RawURL),
9292
attribute.String("http.request.path_params", string(pathParams)),
93-
attribute.String("apitoolkit.msg_id", payload.MsgID),
9493
attribute.String("apitoolkit.sdk_type", payload.SdkType),
9594
attribute.String("http.request.body", base64.StdEncoding.EncodeToString(requestBody)),
9695
attribute.String("http.response.body", base64.StdEncoding.EncodeToString(responseBody)),
@@ -106,6 +105,11 @@ func CreateSpan(payload Payload, config Config, span trace.Span) {
106105
for key, value := range payload.ResponseHeaders {
107106
span.SetAttributes(attribute.KeyValue{Key: attribute.Key("http.response.header." + key), Value: attribute.StringSliceValue(value)})
108107
}
108+
if payload.MsgID != "" {
109+
span.SetAttributes(attribute.String("apitoolkit.msg_id", payload.MsgID))
110+
111+
}
112+
109113
}
110114

111115
func RedactJSON(data []byte, redactList []string) []byte {
@@ -179,6 +183,10 @@ func BuildPayload(SDKType string, req *http.Request,
179183
if config.ServiceVersion != "" {
180184
serviceVersion = &config.ServiceVersion
181185
}
186+
msgIDStr := ""
187+
if msgID != uuid.Nil {
188+
msgIDStr = msgID.String()
189+
}
182190
return Payload{
183191
Host: req.Host,
184192
Method: req.Method,
@@ -198,7 +206,7 @@ func BuildPayload(SDKType string, req *http.Request,
198206
Errors: errorList,
199207
ServiceVersion: serviceVersion,
200208
Tags: config.Tags,
201-
MsgID: msgID.String(),
209+
MsgID: msgIDStr,
202210
ParentID: parentIDVal,
203211
}
204212
}

0 commit comments

Comments
 (0)