@@ -11,11 +11,16 @@ import (
1111 "io"
1212 "net/http"
1313 "strings"
14+ "sync"
1415 "time"
1516
1617 "github.com/cenkalti/backoff/v4"
18+ "github.com/rs/zerolog"
1719 "github.com/santhosh-tekuri/jsonschema/v6"
1820 uritemplate "github.com/std-uritemplate/std-uritemplate/go/v2"
21+ "go.opentelemetry.io/otel"
22+ "go.opentelemetry.io/otel/attribute"
23+ "go.opentelemetry.io/otel/metric"
1924 "google.golang.org/protobuf/types/known/structpb"
2025
2126 "github.com/mindersec/minder/internal/engine/eval/rego"
@@ -32,6 +37,12 @@ const (
3237 MaxBytesLimit int64 = 1 << 20
3338)
3439
40+ var (
41+ metricsInit sync.Once
42+
43+ dataSourceLatencyHistogram metric.Int64Histogram
44+ )
45+
3546type restHandler struct {
3647 rawInputSchema * structpb.Struct
3748 inputSchema * jsonschema.Schema
@@ -48,6 +59,21 @@ type restHandler struct {
4859 // TODO implement auth
4960}
5061
62+ func initMetrics () {
63+ metricsInit .Do (func () {
64+ meter := otel .Meter ("minder" )
65+ var err error
66+ dataSourceLatencyHistogram , err = meter .Int64Histogram (
67+ "datasource.rest.latency" ,
68+ metric .WithDescription ("Latency of data source requests in milliseconds" ),
69+ metric .WithUnit ("ms" ),
70+ )
71+ if err != nil {
72+ zerolog .Ctx (context .Background ()).Warn ().Err (err ).Msg ("Creating histogram for data source requests failed" )
73+ }
74+ })
75+ }
76+
5177func newHandlerFromDef (def * minderv1.RestDataSource_Def ) (* restHandler , error ) {
5278 if def == nil {
5379 return nil , errors .New ("rest data source handler definition is nil" )
@@ -61,6 +87,8 @@ func newHandlerFromDef(def *minderv1.RestDataSource_Def) (*restHandler, error) {
6187
6288 bodyFromInput , body := parseRequestBodyConfig (def )
6389
90+ initMetrics ()
91+
6492 return & restHandler {
6593 rawInputSchema : def .GetInputSchema (),
6694 inputSchema : schema ,
@@ -141,14 +169,26 @@ func (h *restHandler) Call(ctx context.Context, _ *interfaces.Result, args any)
141169 return h .doRequest (cli , req )
142170}
143171
172+ func recordMetrics (ctx context.Context , resp * http.Response , start time.Time ) {
173+ attrs := []attribute.KeyValue {
174+ attribute .String ("method" , resp .Request .Method ),
175+ attribute .String ("endpoint" , resp .Request .URL .String ()),
176+ attribute .String ("status_code" , fmt .Sprintf ("%d" , resp .StatusCode )),
177+ }
178+
179+ dataSourceLatencyHistogram .Record (ctx , time .Since (start ).Milliseconds (), metric .WithAttributes (attrs ... ))
180+ }
181+
144182func (h * restHandler ) doRequest (cli * http.Client , req * http.Request ) (any , error ) {
183+ start := time .Now ()
145184 resp , err := retriableDo (cli , req )
146185 if err != nil {
147186 return nil , err
148187 }
149-
150188 defer resp .Body .Close ()
151189
190+ recordMetrics (req .Context (), resp , start )
191+
152192 bout , err := h .parseResponseBody (resp .Body )
153193 if err != nil {
154194 return nil , err
@@ -257,21 +297,36 @@ func buildRestOutput(statusCode int, body any) any {
257297
258298func retriableDo (cli * http.Client , req * http.Request ) (* http.Response , error ) {
259299 var resp * http.Response
300+ retryCount := 0
301+
260302 err := backoff .Retry (func () error {
261303 var err error
262304 resp , err = cli .Do (req )
263305 if err != nil {
306+ zerolog .Ctx (req .Context ()).Debug ().
307+ Err (err ).
308+ Int ("retry" , retryCount ).
309+ Msg ("HTTP request failed, retrying" )
310+ retryCount ++
264311 return err
265312 }
266313
267314 if resp .StatusCode == http .StatusTooManyRequests {
315+ zerolog .Ctx (req .Context ()).Debug ().
316+ Int ("retry" , retryCount ).
317+ Msg ("rate limited, retrying" )
318+ retryCount ++
268319 return errors .New ("rate limited" )
269320 }
270321
271322 return nil
272323 }, backoff .WithMaxRetries (backoff .NewExponentialBackOff (), 5 ))
273324
274325 if err != nil {
326+ zerolog .Ctx (req .Context ()).Warn ().
327+ Err (err ).
328+ Int ("retries" , retryCount ).
329+ Msg ("HTTP request failed after retries" )
275330 return nil , err
276331 }
277332
0 commit comments