From e86ad4f4fa0a1e75db6f56fd7fdbbe37f7da41ca Mon Sep 17 00:00:00 2001 From: zepatrik Date: Thu, 11 Apr 2024 17:46:27 +0200 Subject: [PATCH] feat: log external latency --- cmd/daemon/middleware.go | 39 ---- cmd/daemon/serve.go | 1 + go.mod | 6 +- go.sum | 4 +- internal/client-go/go.sum | 1 + selfservice/hook/web_hook.go | 430 +++++++++++++++++------------------ 6 files changed, 214 insertions(+), 267 deletions(-) delete mode 100644 cmd/daemon/middleware.go diff --git a/cmd/daemon/middleware.go b/cmd/daemon/middleware.go deleted file mode 100644 index 64c2994881c5..000000000000 --- a/cmd/daemon/middleware.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package daemon - -import ( - "net/http" - "time" - - "github.com/sirupsen/logrus" - "github.com/urfave/negroni" - - "github.com/ory/x/logrusx" - - "github.com/ory/x/healthx" - "github.com/ory/x/reqlog" -) - -func NewNegroniLoggerMiddleware(l *logrusx.Logger, name string) *reqlog.Middleware { - n := reqlog.NewMiddlewareFromLogger(l, name).ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath) - n.Before = func(entry *logrusx.Logger, req *http.Request, remoteAddr string) *logrusx.Logger { - return entry.WithFields(logrus.Fields{ - "name": name, - "request": req.RequestURI, - "method": req.Method, - "remote": remoteAddr, - }) - } - - n.After = func(entry *logrusx.Logger, req *http.Request, res negroni.ResponseWriter, latency time.Duration, name string) *logrusx.Logger { - return entry.WithFields(logrus.Fields{ - "name": name, - "status": res.Status(), - "text_status": http.StatusText(res.Status()), - "took": latency, - }) - } - return n -} diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go index fb7557033e01..1aa3f84a5298 100644 --- a/cmd/daemon/serve.go +++ b/cmd/daemon/serve.go @@ -85,6 +85,7 @@ func servePublic(r driver.Registry, cmd *cobra.Command, eg *errgroup.Group, slOp n.UseFunc(mw) } + n.UseFunc(reqlog.ExternalCallsMiddleware) publicLogger := reqlog.NewMiddlewareFromLogger( l, "public#"+c.SelfPublicURL(ctx).String(), diff --git a/go.mod b/go.mod index 655a07bbf9e2..fc1200d11704 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/ory/kratos -go 1.21 +go 1.22 + +toolchain go1.22.2 replace ( github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb @@ -77,7 +79,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.623 + github.com/ory/x v0.0.626-0.20240411151622-8fb5e48fd2c7 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index f344856176b4..0f46dc957d75 100644 --- a/go.sum +++ b/go.sum @@ -827,8 +827,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.623 h1:sFJiw2i/itTkBRJbhGXtrso9NcdscnjFlHBFitCzf8A= -github.com/ory/x v0.0.623/go.mod h1:CUw8/O3X8lUMheyV0iH+6LQ0tePrH+FBsW39MccCHgw= +github.com/ory/x v0.0.626-0.20240411151622-8fb5e48fd2c7 h1:4xu00w+ApXLkZ8xmTiXWQfEuxfTBiFI/p4KzwhKxmp8= +github.com/ory/x v0.0.626-0.20240411151622-8fb5e48fd2c7/go.mod h1:LCk8Az5FzSGtZqQC8rTbhxysRDgi5b1TCQ32NjauWDo= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index cbb9f0a0b0d4..89d2af9d7a76 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -39,6 +39,7 @@ import ( "github.com/ory/kratos/x/events" "github.com/ory/x/jsonnetsecure" "github.com/ory/x/otelx" + "github.com/ory/x/reqlog" ) var _ interface { @@ -120,90 +121,76 @@ func NewWebHook(r webHookDependencies, c json.RawMessage) *WebHook { } func (e *WebHook) ExecuteLoginPreHook(_ http.ResponseWriter, req *http.Request, flow *login.Flow) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginPreHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }, "LoginPreHook") } func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, _ node.UiNodeGroup, flow *login.Flow, session *session.Session) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginPostHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - Identity: session.Identity, - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: session.Identity, + }, "LoginPostHook") } func (e *WebHook) ExecuteVerificationPreHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteVerificationPreHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }, "VerificationPreHook") } func (e *WebHook) ExecutePostVerificationHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow, id *identity.Identity) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostVerificationHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - Identity: id, - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: id, + }, "PostVerificationHook") } func (e *WebHook) ExecuteRecoveryPreHook(_ http.ResponseWriter, req *http.Request, flow *recovery.Flow) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteRecoveryPreHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestCookies: cookies(req), - RequestURL: x.RequestURL(req).String(), - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestCookies: cookies(req), + RequestURL: x.RequestURL(req).String(), + }, "RecoveryPreHook") } func (e *WebHook) ExecutePostRecoveryHook(_ http.ResponseWriter, req *http.Request, flow *recovery.Flow, session *session.Session) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostRecoveryHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - Identity: session.Identity, - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: session.Identity, + }, "PostRecoveryHook") } func (e *WebHook) ExecuteRegistrationPreHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteRegistrationPreHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }, "RegistrationPreHook") } func (e *WebHook) ExecutePostRegistrationPrePersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, id *identity.Identity) error { @@ -211,16 +198,14 @@ func (e *WebHook) ExecutePostRegistrationPrePersistHook(_ http.ResponseWriter, r return nil } - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostRegistrationPrePersistHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - Identity: id, - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: id, + }, "PostRegistrationPrePersistHook") } func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, session *session.Session) error { @@ -232,192 +217,189 @@ func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, // if the request is canceled. ctx := context.WithoutCancel(req.Context()) - return otelx.WithSpan(ctx, "selfservice.hook.WebHook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - Identity: session.Identity, - }) - }) + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: session.Identity, + }, "PostRegistrationPostPersistHook") } func (e *WebHook) ExecuteSettingsPreHook(_ http.ResponseWriter, req *http.Request, flow *settings.Flow) error { - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPreHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }, "SettingsPreHook") } func (e *WebHook) ExecuteSettingsPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *settings.Flow, id *identity.Identity, _ *session.Session) error { if gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool() { return nil } - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPostPersistHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - Identity: id, - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: id, + }, "SettingsPostPersistHook") } func (e *WebHook) ExecuteSettingsPrePersistHook(_ http.ResponseWriter, req *http.Request, flow *settings.Flow, id *identity.Identity) error { if !(gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool()) { return nil } - return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPrePersistHook", func(ctx context.Context) error { - return e.execute(ctx, &templateContext{ - Flow: flow, - RequestHeaders: req.Header, - RequestMethod: req.Method, - RequestURL: x.RequestURL(req).String(), - RequestCookies: cookies(req), - Identity: id, - }) - }) + return e.execute(req.Context(), &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + Identity: id, + }, "SettingsPrePersistHook") } -func (e *WebHook) execute(ctx context.Context, data *templateContext) error { - var ( - httpClient = e.deps.HTTPClient(ctx) - ignoreResponse = gjson.GetBytes(e.conf, "response.ignore").Bool() - canInterrupt = gjson.GetBytes(e.conf, "can_interrupt").Bool() - parseResponse = gjson.GetBytes(e.conf, "response.parse").Bool() - emitEvent = gjson.GetBytes(e.conf, "emit_analytics_event").Bool() || !gjson.GetBytes(e.conf, "emit_analytics_event").Exists() // default true - tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks") - ) - if ignoreResponse && (parseResponse || canInterrupt) { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("A webhook is configured to ignore the response but also to parse the response. This is not possible.")) - } - - makeRequest := func() (finalErr error) { - if ignoreResponse { - // This means we want to run this closure asynchronously and not be - // canceled when the parent context is canceled. - // - // The webhook will still cancel after 30 seconds as that is the - // configured timeout for the HTTP client. - ctx = context.WithoutCancel(ctx) +func (e *WebHook) execute(ctx context.Context, data *templateContext, hookName string) error { + return otelx.WithSpan(ctx, "selfservice.hook.WebHook.Execute"+hookName, func(ctx context.Context) error { + var ( + httpClient = e.deps.HTTPClient(ctx) + ignoreResponse = gjson.GetBytes(e.conf, "response.ignore").Bool() + canInterrupt = gjson.GetBytes(e.conf, "can_interrupt").Bool() + parseResponse = gjson.GetBytes(e.conf, "response.parse").Bool() + emitEvent = gjson.GetBytes(e.conf, "emit_analytics_event").Bool() || !gjson.GetBytes(e.conf, "emit_analytics_event").Exists() // default true + tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks") + ) + if ignoreResponse && (parseResponse || canInterrupt) { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("A webhook is configured to ignore the response but also to parse the response. This is not possible.")) } - ctx, span := tracer.Start(ctx, "selfservice.webhook") - defer otelx.End(span, &finalErr) - - if emitEvent { - instrumentHTTPClientForEvents(ctx, httpClient) + if !emitEvent { + ctx = reqlog.WithDisableExternalLatencyMeasurement(ctx) } - defer func(startTime time.Time) { - traceID, spanID := span.SpanContext().TraceID(), span.SpanContext().SpanID() - logger := e.deps.Logger().WithField("otel", map[string]string{ - "trace_id": traceID.String(), - "span_id": spanID.String(), - }).WithField("duration", time.Since(startTime)) - if finalErr != nil { - if emitEvent && !errors.Is(finalErr, context.Canceled) { - span.AddEvent(events.NewWebhookFailed(ctx, finalErr)) - } - if ignoreResponse { - logger.WithError(finalErr).Warning("Webhook request failed but the error was ignored because the configuration indicated that the upstream response should be ignored") - } else { - logger.WithError(finalErr).Error("Webhook request failed") - } - } else { - logger.Info("Webhook request succeeded") - if emitEvent { - span.AddEvent(events.NewWebhookSucceeded(ctx)) - } + makeRequest := func() (finalErr error) { + if ignoreResponse { + // This means we want to run this closure asynchronously and not be + // canceled when the parent context is canceled. + // + // The webhook will still cancel after 30 seconds as that is the + // configured timeout for the HTTP client. + ctx = context.WithoutCancel(ctx) } - }(time.Now()) + ctx, span := tracer.Start(ctx, "selfservice.webhook") + defer otelx.End(span, &finalErr) - builder, err := request.NewBuilder(ctx, e.conf, e.deps, jsonnetCache) - if err != nil { - return err - } + if emitEvent { + instrumentHTTPClientForEvents(ctx, httpClient) + } - span.SetAttributes( - attribute.String("webhook.jsonnet.template-uri", builder.Config.TemplateURI), - attribute.Bool("webhook.can_interrupt", canInterrupt), - attribute.Bool("webhook.response.ignore", ignoreResponse), - attribute.Bool("webhook.response.parse", parseResponse), - ) + defer func(startTime time.Time) { + traceID, spanID := span.SpanContext().TraceID(), span.SpanContext().SpanID() + logger := e.deps.Logger().WithField("otel", map[string]string{ + "trace_id": traceID.String(), + "span_id": spanID.String(), + }).WithField("duration", time.Since(startTime)) + if finalErr != nil { + if emitEvent && !errors.Is(finalErr, context.Canceled) { + span.AddEvent(events.NewWebhookFailed(ctx, finalErr)) + } + if ignoreResponse { + logger.WithError(finalErr).Warning("Webhook request failed but the error was ignored because the configuration indicated that the upstream response should be ignored") + } else { + logger.WithError(finalErr).Error("Webhook request failed") + } + } else { + logger.Info("Webhook request succeeded") + if emitEvent { + span.AddEvent(events.NewWebhookSucceeded(ctx)) + } + } + }(time.Now()) - req, err := builder.BuildRequest(ctx, data) - if errors.Is(err, request.ErrCancel) { - span.SetAttributes(attribute.Bool("webhook.jsonnet.canceled", true)) - return nil - } else if err != nil { - return err - } + builder, err := request.NewBuilder(ctx, e.conf, e.deps, jsonnetCache) + if err != nil { + return err + } - if data.Identity != nil { span.SetAttributes( - attribute.String("webhook.identity.id", data.Identity.ID.String()), - attribute.String("webhook.identity.nid", data.Identity.NID.String()), + attribute.String("webhook.jsonnet.template-uri", builder.Config.TemplateURI), + attribute.Bool("webhook.can_interrupt", canInterrupt), + attribute.Bool("webhook.response.ignore", ignoreResponse), + attribute.Bool("webhook.response.parse", parseResponse), ) - } - e.deps.Logger().WithRequest(req.Request).Info("Dispatching webhook") + req, err := builder.BuildRequest(ctx, data) + if errors.Is(err, request.ErrCancel) { + span.SetAttributes(attribute.Bool("webhook.jsonnet.canceled", true)) + return nil + } else if err != nil { + return err + } + + if data.Identity != nil { + span.SetAttributes( + attribute.String("webhook.identity.id", data.Identity.ID.String()), + attribute.String("webhook.identity.nid", data.Identity.NID.String()), + ) + } - req = req.WithContext(ctx) + e.deps.Logger().WithRequest(req.Request).Info("Dispatching webhook") - resp, err := httpClient.Do(req) - if err != nil { - if isTimeoutError(err) { - return herodot.DefaultError{ - CodeField: http.StatusGatewayTimeout, - StatusField: http.StatusText(http.StatusGatewayTimeout), - GRPCCodeField: grpccodes.DeadlineExceeded, - ErrorField: err.Error(), - ReasonField: "A third-party upstream service could not be reached. Please try again later.", - }.WithWrap(errors.WithStack(err)) + req = req.WithContext(ctx) + + resp, err := httpClient.Do(req) + if err != nil { + if isTimeoutError(err) { + return herodot.DefaultError{ + CodeField: http.StatusGatewayTimeout, + StatusField: http.StatusText(http.StatusGatewayTimeout), + GRPCCodeField: grpccodes.DeadlineExceeded, + ErrorField: err.Error(), + ReasonField: "A third-party upstream service could not be reached. Please try again later.", + }.WithWrap(errors.WithStack(err)) + } + return errors.WithStack(err) } - return errors.WithStack(err) - } - defer resp.Body.Close() - resp.Body = io.NopCloser(io.LimitReader(resp.Body, 5<<20)) // read at most 5 MB from the response - span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) - - if resp.StatusCode >= http.StatusBadRequest { - span.SetStatus(codes.Error, "HTTP status code >= 400") - if canInterrupt || parseResponse { - if err := parseWebhookResponse(resp, data.Identity); err != nil { - return err + defer resp.Body.Close() + resp.Body = io.NopCloser(io.LimitReader(resp.Body, 5<<20)) // read at most 5 MB from the response + span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) + + if resp.StatusCode >= http.StatusBadRequest { + span.SetStatus(codes.Error, "HTTP status code >= 400") + if canInterrupt || parseResponse { + if err := parseWebhookResponse(resp, data.Identity); err != nil { + return err + } + } + return herodot.DefaultError{ + CodeField: http.StatusBadGateway, + StatusField: http.StatusText(http.StatusBadGateway), + GRPCCodeField: grpccodes.Aborted, + ReasonField: "A third-party upstream service responded improperly. Please try again later.", + ErrorField: fmt.Sprintf("webhook failed with status code %v", resp.StatusCode), } } - return herodot.DefaultError{ - CodeField: http.StatusBadGateway, - StatusField: http.StatusText(http.StatusBadGateway), - GRPCCodeField: grpccodes.Aborted, - ReasonField: "A third-party upstream service responded improperly. Please try again later.", - ErrorField: fmt.Sprintf("webhook failed with status code %v", resp.StatusCode), + + if parseResponse { + return parseWebhookResponse(resp, data.Identity) } + return nil } - if parseResponse { - return parseWebhookResponse(resp, data.Identity) + if !ignoreResponse { + return makeRequest() } + go func() { + // we cannot handle the error as we are running async, and it is logged anyway + _ = makeRequest() + }() return nil - } - - if !ignoreResponse { - return makeRequest() - } - go func() { - // we cannot handle the error as we are running async, and it is logged anyway - _ = makeRequest() - }() - return nil + }) } func parseWebhookResponse(resp *http.Response, id *identity.Identity) (err error) {