core/metrics/instrumentation/httpcapture/http.go
// Package httpcapture provides an http.RoundTripper that captures requests and responses.
package httpcapture
import (
"bytes"
"fmt"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/valyala/fastjson"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"io"
"net/http"
)
// NewCaptureTransport returns a new http.Transport that captures requests and responses.
func NewCaptureTransport(underlyingTransport http.RoundTripper, handler metrics.Handler) http.RoundTripper {
return &captureTransport{
transport: underlyingTransport,
metrics: handler,
}
}
// captures requests and responses.
type captureTransport struct {
transport http.RoundTripper
metrics metrics.Handler
}
const (
// RequestSpanName is the name of the span created for each request.
RequestSpanName = "http.request"
// RequestEventName is the name of the event created for each request body.
RequestEventName = "http.request.body"
// ResponseEventName is the name of the event created for each response body.
ResponseEventName = "http.response.body"
// IDSpanName is the name of the span created for each request.
IDSpanName = "rpc.id"
)
// nolint: cyclop
func (t *captureTransport) RoundTrip(req *http.Request) (_ *http.Response, err error) {
var response string
_, span := t.metrics.Tracer().Start(req.Context(), RequestSpanName)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
// Perform the HTTP request using the underlying transport
transport := t.transport
if transport == nil {
transport = http.DefaultTransport
}
// no need to capture if we're not recording
if !span.IsRecording() {
// nolint: wrapcheck
return transport.RoundTrip(req)
}
// Capture the request body
var requestBody bytes.Buffer
if req.Body != nil {
tee := io.TeeReader(req.Body, &requestBody)
req.Body = io.NopCloser(tee)
if req.ContentLength == 0 && req.Header.Get("Content-Encoding") == "" {
req.ContentLength = int64(requestBody.Len())
}
}
resp, err := transport.RoundTrip(req)
if err != nil {
return nil, fmt.Errorf("failed to perform http request: %w", err)
}
// Capture the response body
//nolint: nestif
if resp != nil && resp.Body != nil {
var responseBody bytes.Buffer
if resp.ContentLength == 0 && resp.Header.Get("Content-Encoding") == "" {
// Copy the response body to a buffer so we can compute the content length
tee := io.TeeReader(resp.Body, &responseBody)
resp.Body = io.NopCloser(tee)
bodyBytes, err := io.ReadAll(&responseBody)
if err == nil {
response = string(bodyBytes)
}
resp.ContentLength = int64(len(bodyBytes))
} else {
// Copy the response body to a buffer so we can capture the response data
bodyBytes, err := io.ReadAll(resp.Body)
if err == nil {
response = string(bodyBytes)
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
}
}
// Add the request to the list of captured requests
// Add the request/response body as events to the span
if requestBody.Len() > 0 {
requestBytes := requestBody.Bytes()
// now track the id in a way we can search in signoz.
// will not work on batch requests.
id := fastjson.GetInt(requestBytes, "id")
if id != 0 {
span.SetAttributes(attribute.Int(IDSpanName, id))
}
span.AddEvent(RequestEventName, trace.WithAttributes(attribute.String("body", string(requestBytes))))
}
if len(response) > 0 {
span.AddEvent(ResponseEventName, trace.WithAttributes(attribute.String("body", response)))
}
//nolint: wrapcheck
return resp, err
}