Instrumentation: Handle context.Canceled (#75867)

Ref #68480

Co-authored-by: Giuseppe Guerra <giuseppe.guerra@grafana.com>
This commit is contained in:
Marcus Efraimsson 2023-10-10 12:28:39 +02:00 committed by GitHub
parent aee8c91ac8
commit 90631360eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 72 additions and 0 deletions

View File

@ -31,6 +31,7 @@ functions, e.g.
- `errutil.Forbidden(messageID, opts...)` - `errutil.Forbidden(messageID, opts...)`
- `errutil.TooManyRequests(messageID, opts...)` - `errutil.TooManyRequests(messageID, opts...)`
- `errutil.NotImplemented(messageID, opts...)` - `errutil.NotImplemented(messageID, opts...)`
- `errutil.ClientClosedRequest(messageID, opts...)`
Above functions uses `errutil.NewBase(status, messageID, opts...)` under the covers, and that function should in general only be used outside the `errutil` package for `errutil.StatusUnknown`, e.g. when there are no accurate status code available/provided. Above functions uses `errutil.NewBase(status, messageID, opts...)` under the covers, and that function should in general only be used outside the `errutil` package for `errutil.StatusUnknown`, e.g. when there are no accurate status code available/provided.

View File

@ -74,8 +74,14 @@ datasources:
type: prometheus type: prometheus
access: proxy access: proxy
url: http://localhost:3011 url: http://localhost:3011
basicAuth: true #username: admin, password: admin
basicAuthUser: admin
jsonData: jsonData:
manageAlerts: false manageAlerts: false
prometheusType: Prometheus #Cortex | Mimir | Prometheus | Thanos
prometheusVersion: 2.40.0
secureJsonData:
basicAuthPassword: admin #https://grafana.com/docs/grafana/latest/administration/provisioning/#using-environment-variables
- name: gdev-testdata - name: gdev-testdata
isDefault: true isDefault: true

View File

@ -2,6 +2,7 @@ package response
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -18,6 +19,9 @@ import (
"github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/util/errutil"
) )
var errRequestCanceledBase = errutil.ClientClosedRequest("api.requestCanceled",
errutil.WithPublicMessage("Request canceled"))
// Response is an HTTP response interface. // Response is an HTTP response interface.
type Response interface { type Response interface {
// WriteTo writes to a context. // WriteTo writes to a context.
@ -286,12 +290,18 @@ func Err(err error) *NormalResponse {
// The signature is equivalent to that of Error which allows us to // The signature is equivalent to that of Error which allows us to
// rename this to Error when we're confident that that would be safe to // rename this to Error when we're confident that that would be safe to
// do. // do.
// If the error provided is not an errutil.Error and is/wraps context.Canceled
// the function returns an Err(errRequestCanceledBase).
func ErrOrFallback(status int, message string, err error) *NormalResponse { func ErrOrFallback(status int, message string, err error) *NormalResponse {
grafanaErr := errutil.Error{} grafanaErr := errutil.Error{}
if errors.As(err, &grafanaErr) { if errors.As(err, &grafanaErr) {
return Err(err) return Err(err)
} }
if errors.Is(err, context.Canceled) {
return Err(errRequestCanceledBase.Errorf("response: request canceled: %w", err))
}
return Error(status, message, err) return Error(status, message, err)
} }

View File

@ -29,4 +29,10 @@ var (
ErrPluginDownstreamErrorBase = errutil.Internal("plugin.downstreamError", ErrPluginDownstreamErrorBase = errutil.Internal("plugin.downstreamError",
errutil.WithPublicMessage("An error occurred within the plugin"), errutil.WithPublicMessage("An error occurred within the plugin"),
errutil.WithDownstream()) errutil.WithDownstream())
// ErrPluginRequestCanceledErrorBase error returned when a plugin request
// is cancelled by the client (context is cancelled).
// Exposed as a base error to wrap it with plugin cancelled errors.
ErrPluginRequestCanceledErrorBase = errutil.ClientClosedRequest("plugin.requestCanceled",
errutil.WithPublicMessage("Plugin request canceled"))
) )

View File

@ -59,6 +59,10 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
return nil, err return nil, err
} }
if errors.Is(err, context.Canceled) {
return nil, plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: query data request canceled: %w", err)
}
return nil, plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to query data: %w", err) return nil, plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to query data: %w", err)
} }
@ -111,6 +115,10 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq
err := p.CallResource(ctx, req, wrappedSender) err := p.CallResource(ctx, req, wrappedSender)
if err != nil { if err != nil {
if errors.Is(err, context.Canceled) {
return plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: call resource request canceled: %w", err)
}
return plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to call resources: %w", err) return plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to call resources: %w", err)
} }
@ -129,6 +137,10 @@ func (s *Service) CollectMetrics(ctx context.Context, req *backend.CollectMetric
resp, err := p.CollectMetrics(ctx, req) resp, err := p.CollectMetrics(ctx, req)
if err != nil { if err != nil {
if errors.Is(err, context.Canceled) {
return nil, plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: collect metrics request canceled: %w", err)
}
return nil, plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to collect metrics: %w", err) return nil, plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to collect metrics: %w", err)
} }
@ -155,6 +167,10 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
return nil, err return nil, err
} }
if errors.Is(err, context.Canceled) {
return nil, plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: check health request canceled: %w", err)
}
return nil, plugins.ErrPluginHealthCheck.Errorf("client: failed to check health: %w", err) return nil, plugins.ErrPluginHealthCheck.Errorf("client: failed to check health: %w", err)
} }

View File

@ -41,6 +41,10 @@ func TestQueryData(t *testing.T) {
err: errors.New("surprise surprise"), err: errors.New("surprise surprise"),
expectedError: plugins.ErrPluginDownstreamErrorBase, expectedError: plugins.ErrPluginDownstreamErrorBase,
}, },
{
err: context.Canceled,
expectedError: plugins.ErrPluginRequestCanceledErrorBase,
},
} }
for _, tc := range tcs { for _, tc := range tcs {
@ -99,6 +103,10 @@ func TestCheckHealth(t *testing.T) {
err: errors.New("surprise surprise"), err: errors.New("surprise surprise"),
expectedError: plugins.ErrPluginHealthCheck, expectedError: plugins.ErrPluginHealthCheck,
}, },
{
err: context.Canceled,
expectedError: plugins.ErrPluginRequestCanceledErrorBase,
},
} }
for _, tc := range tcs { for _, tc := range tcs {

View File

@ -133,6 +133,17 @@ func TooManyRequests(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusTooManyRequests, msgID, opts...) return NewBase(StatusTooManyRequests, msgID, opts...)
} }
// ClientClosedRequest initializes a new [Base] error with reason StatusClientClosedRequest
// that is used to construct [Error]. The msgID is passed to the caller
// to serve as the base for user facing error messages.
//
// msgID should be structured as component.errorBrief, for example
//
// plugin.requestCanceled
func ClientClosedRequest(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusClientClosedRequest, msgID, opts...)
}
// NotImplemented initializes a new [Base] error with reason StatusNotImplemented // NotImplemented initializes a new [Base] error with reason StatusNotImplemented
// that is used to construct [Error]. The msgID is passed to the caller // that is used to construct [Error]. The msgID is passed to the caller
// to serve as the base for user facing error messages. // to serve as the base for user facing error messages.

View File

@ -28,6 +28,13 @@ const (
// parameters or payload for the request. // parameters or payload for the request.
// HTTP status code 400. // HTTP status code 400.
StatusBadRequest CoreStatus = "Bad request" StatusBadRequest CoreStatus = "Bad request"
// StatusClientClosedRequest means that a client closes the connection
// while the server is processing the request.
//
// This is a non-standard HTTP status code introduced by nginx, see
// https://httpstatus.in/499/ for more information.
// HTTP status code 499.
StatusClientClosedRequest CoreStatus = "Client closed request"
// StatusValidationFailed means that the server was able to parse // StatusValidationFailed means that the server was able to parse
// the payload for the request but it failed one or more validation // the payload for the request but it failed one or more validation
// checks. // checks.
@ -57,6 +64,11 @@ const (
StatusGatewayTimeout CoreStatus = "Gateway timeout" StatusGatewayTimeout CoreStatus = "Gateway timeout"
) )
// HTTPStatusClientClosedRequest A non-standard status code introduced by nginx
// for the case when a client closes the connection while nginx is processing
// the request. See https://httpstatus.in/499/ for more information.
const HTTPStatusClientClosedRequest = 499
// StatusReason allows for wrapping of CoreStatus. // StatusReason allows for wrapping of CoreStatus.
type StatusReason interface { type StatusReason interface {
Status() CoreStatus Status() CoreStatus
@ -84,6 +96,8 @@ func (s CoreStatus) HTTPStatus() int {
return http.StatusTooManyRequests return http.StatusTooManyRequests
case StatusBadRequest, StatusValidationFailed: case StatusBadRequest, StatusValidationFailed:
return http.StatusBadRequest return http.StatusBadRequest
case StatusClientClosedRequest:
return HTTPStatusClientClosedRequest
case StatusNotImplemented: case StatusNotImplemented:
return http.StatusNotImplemented return http.StatusNotImplemented
case StatusBadGateway: case StatusBadGateway: