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 }