unitel/unitelhttp/transport.go

159 lines
3.9 KiB
Go
Raw Permalink Normal View History

package unitelhttp
import (
"fmt"
"net/http"
"time"
"git.devminer.xyz/devminer/unitel"
"git.devminer.xyz/devminer/unitel/unitelutils"
"github.com/getsentry/sentry-go"
"github.com/go-logr/logr"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
)
const transportClientID = unitelutils.Base + "#HTTPTansport@" + unitelutils.Version
type HTTPTransportOpt func(t *HTTPTransport)
func WithLogger(l logr.Logger) HTTPTransportOpt {
return func(t *HTTPTransport) {
t.logger = l
}
}
type TracePropagator = func(r *http.Request) bool
func WithTracePropagation(propagator TracePropagator) HTTPTransportOpt {
return func(t *HTTPTransport) {
t.tracePropagator = propagator
}
}
func PropagateAllTraces(req *http.Request) bool {
return true
}
func PropagateNoTraces(req *http.Request) bool {
return false
}
type HTTPTransport struct {
logger logr.Logger
telemetry *unitel.Telemetry
transport http.RoundTripper
tracePropagator TracePropagator
tracedRequestHeaders []string
tracedResponseHeaders []string
tracer trace.Tracer
}
func NewTracedTransport(t *unitel.Telemetry, inner http.RoundTripper, tracedRequestHeaders []string, tracedResponseHeaders []string, opts ...HTTPTransportOpt) *HTTPTransport {
transport := &HTTPTransport{
logger: logr.Discard(),
telemetry: t,
transport: inner,
tracePropagator: PropagateNoTraces,
tracedRequestHeaders: tracedRequestHeaders,
tracedResponseHeaders: tracedResponseHeaders,
tracer: t.TracerProvider.Tracer(transportClientID, trace.WithInstrumentationVersion(unitelutils.Version)),
}
for _, opt := range opts {
opt(transport)
}
return transport
}
func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
s := t.telemetry.
StartSpan(req.Context(), "http.client", fmt.Sprintf("%s %s", req.Method, req.URL), unitel.WithOtelOptions(trace.WithSpanKind(trace.SpanKindClient))).
AddAttributes(
semconv.HTTPRequestMethodKey.String(req.Method),
semconv.URLFull(req.URL.String()),
)
defer s.End()
ctx := s.Context()
req = req.WithContext(ctx)
if t.tracePropagator(req) {
t.telemetry.InjectIntoHeaders(ctx, req.Header)
}
for _, header := range t.tracedRequestHeaders {
vv := req.Header.Values(header)
for i, v := range vv {
s.AddAttributes(attribute.String(formatHeaderAttribute("http.request", header, vv, i), v))
}
}
start := time.Now()
resp, err := t.transport.RoundTrip(req)
elapsed := time.Since(start)
if err == nil {
t.logger.V(1).Info("fetch succeeded", "url", req.URL.String(), "status", resp.StatusCode, "duration", elapsed)
} else {
t.logger.V(1).Error(err, "fetch failed", "url", req.URL.String(), "duration", elapsed)
s.CaptureBreadcrumb(
unitel.SeverityError,
unitel.BreadcrumbTypeHTTP,
unitel.BreadcrumbCatagoryHTTP,
fmt.Sprintf("Failed to send request to %s: %v", req.URL.String(), err),
map[string]any{
"url": req.URL.String(),
"method": req.Method,
"duration": elapsed,
},
)
if hub := sentry.GetHubFromContext(ctx); hub != nil {
hub.CaptureException(err)
}
return resp, err
}
{
severity := unitel.SeverityDebug
if resp.StatusCode < http.StatusBadRequest {
severity = unitel.SeverityInfo
} else if resp.StatusCode < http.StatusInternalServerError {
severity = unitel.SeverityError
}
s.CaptureBreadcrumb(
severity,
unitel.BreadcrumbTypeHTTP,
unitel.BreadcrumbCatagoryHTTP,
req.URL.String(),
map[string]any{
"url": req.URL.String(),
"method": req.Method,
"status": resp.StatusCode,
},
)
}
s.AddAttributes(semconv.HTTPResponseStatusCode(resp.StatusCode)).
SetStatus(httpStatusToSpanStatus(resp.StatusCode, false), "")
for _, header := range t.tracedResponseHeaders {
vv := resp.Header.Values(header)
for i, v := range vv {
s.AddAttributes(attribute.String(formatHeaderAttribute("http.response", header, vv, i), v))
}
}
return resp, err
}