github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/pusher/v2/errors.go (about)

     1  package v2
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"strings"
     9  
    10  	"github.com/grafana/synthetic-monitoring-agent/internal/pkg/prom"
    11  )
    12  
    13  // errKind enum used to carry the category of a push error.
    14  type errKind int8
    15  
    16  const (
    17  	errKindNoError    errKind = iota // No error (never returned, always nil error).
    18  	errKindNetwork                   // Transient network error or other retriable error
    19  	errKindPayload                   // There is a problem with the data being sent. Discard it.
    20  	errKindWait                      // Sending too much data, delay publishing
    21  	errKindTenant                    // A problem with the tenant remotes. Fetch the tenant again.
    22  	errKindFatal                     // There is a problem that can't be fixed by fetching the tenant.
    23  	errKindTerminated                // Push terminated (context canceled)
    24  	errKindLimit                     // There is a problem with the limits of the tenant. Discard it.
    25  )
    26  
    27  func (k errKind) String() string {
    28  	switch k {
    29  	case errKindNoError:
    30  		return "no error"
    31  	case errKindNetwork:
    32  		return "network error"
    33  	case errKindPayload:
    34  		return "payload error"
    35  	case errKindWait:
    36  		return "waitable error"
    37  	case errKindTenant:
    38  		return "tenant error"
    39  	case errKindFatal:
    40  		return "fatal error"
    41  	case errKindTerminated:
    42  		return "terminate error"
    43  	case errKindLimit:
    44  		return "limit error"
    45  	}
    46  
    47  	return "unknown error"
    48  }
    49  
    50  // pushError encapsulates an existing error with an errKind
    51  type pushError struct {
    52  	kind  errKind
    53  	inner error
    54  }
    55  
    56  func (e pushError) Error() string {
    57  	return fmt.Sprintf("%s: %s", e.kind.String(), e.inner)
    58  }
    59  
    60  func (e pushError) Unwrap() error {
    61  	return e.inner
    62  }
    63  
    64  func (e pushError) Kind() errKind {
    65  	return e.kind
    66  }
    67  
    68  func (e pushError) IsRetriable() bool {
    69  	return e.kind == errKindNetwork
    70  }
    71  
    72  type alternativeMapping struct {
    73  	substr string
    74  	kind   errKind
    75  }
    76  
    77  // httpCodeMappings maps an HTTP Response Code to an errKind.
    78  // Alternative mappings can be provided by inspecting the error returned by the server.
    79  var httpCodeMappings = map[int]struct {
    80  	kind         errKind
    81  	alternatives []alternativeMapping
    82  }{
    83  	1: { // 1xx: retriable error
    84  		kind: errKindNetwork,
    85  	},
    86  	2: { // 2xx: No error
    87  		kind: errKindNoError,
    88  	},
    89  	3: { // 3xx: These usually indicate a misconfiguration (tenant is pointing to the wrong url?)
    90  		kind: errKindFatal,
    91  	},
    92  	4: { // 4xx: Not an error. Just part of the payload is unacceptable.
    93  		kind: errKindPayload,
    94  	},
    95  	5: { // 5xx: Transient error.
    96  		kind: errKindNetwork,
    97  	},
    98  
    99  	http.StatusInternalServerError: { // 500
   100  		kind: errKindNetwork,
   101  		alternatives: []alternativeMapping{
   102  			{
   103  				substr: "looks like there is an issue with this instance",
   104  				kind:   errKindTenant,
   105  			},
   106  		},
   107  	},
   108  
   109  	http.StatusBadRequest: { // 400
   110  		kind: errKindPayload,
   111  		alternatives: []alternativeMapping{
   112  			{
   113  				substr: "err-mimir-max-series-per-user",
   114  				kind:   errKindFatal,
   115  			},
   116  		},
   117  	},
   118  
   119  	http.StatusTooManyRequests: { // 429
   120  		kind: errKindWait,
   121  		alternatives: []alternativeMapping{
   122  			{
   123  				substr: "limit: 0 ",
   124  				kind:   errKindFatal,
   125  			},
   126  			{
   127  				// We are hitting a limit in the maximum number of active streams allowed by Loki.
   128  				// There's nothing we can do about it other than reporting it. Because of the
   129  				// way Loki works, it's possible that we hit this limit once and don't hit it the next
   130  				// time we try to publish data.
   131  				substr: "Maximum active stream limit exceeded",
   132  				kind:   errKindLimit,
   133  			},
   134  		},
   135  	},
   136  
   137  	// Specific 4xx messages that don't translate to data error
   138  
   139  	http.StatusUnauthorized: { // 401
   140  		kind: errKindTenant,
   141  	},
   142  
   143  	http.StatusForbidden: { // 403
   144  		kind: errKindFatal,
   145  	},
   146  
   147  	http.StatusNotFound: { // 404
   148  		kind: errKindFatal,
   149  	},
   150  
   151  	http.StatusMethodNotAllowed: { // 405
   152  		kind: errKindFatal,
   153  	},
   154  
   155  	http.StatusRequestTimeout: { // 408
   156  		kind: errKindNetwork,
   157  	},
   158  }
   159  
   160  // parsePublishError parses the error resulting from a publish operation and converts it into a pushError.
   161  // The only exception is any error that wraps a context.Canceled error. In that case, context.Canceled
   162  // is returned.
   163  func parsePublishError(err error) (httpStatusCode int, pushErr pushError) {
   164  	const noHTTPCode = 0
   165  
   166  	if err == nil {
   167  		return http.StatusOK, pushError{
   168  			kind:  errKindNoError,
   169  			inner: nil,
   170  		}
   171  	}
   172  
   173  	// Context errors can be wrapped by various other error types, like
   174  	// prom.recoverableError and url.Error.
   175  	if errors.Is(err, context.Canceled) {
   176  		return noHTTPCode, pushError{
   177  			kind:  errKindTerminated,
   178  			inner: context.Canceled,
   179  		}
   180  	}
   181  
   182  	// Any DeadlineExceeded is assumed to be a network timeout of some kind.
   183  	if errors.Is(err, context.DeadlineExceeded) {
   184  		return noHTTPCode, pushError{
   185  			kind:  errKindNetwork,
   186  			inner: err,
   187  		}
   188  	}
   189  
   190  	code, hasStatusCode := prom.GetHttpStatusCode(err)
   191  	if !hasStatusCode {
   192  		// Errors without an HTTP Status code are treated as network errors.
   193  		return noHTTPCode, pushError{
   194  			kind:  errKindNetwork,
   195  			inner: err,
   196  		}
   197  	}
   198  
   199  	mapping, found := httpCodeMappings[code]
   200  	if !found {
   201  		// No mapping for this specific HTTP status code. Try a general 5xx/4xx/etc.
   202  		if mapping, found = httpCodeMappings[code/100]; !found {
   203  			// No mapping for this http status at all?
   204  			// This should never happen.
   205  			return code, pushError{
   206  				kind:  errKindFatal,
   207  				inner: err,
   208  			}
   209  		}
   210  	}
   211  
   212  	// Check specific alternatives that look into the error message.
   213  	errText := err.Error()
   214  	for _, alt := range mapping.alternatives {
   215  		if strings.Contains(errText, alt.substr) {
   216  			return code, pushError{
   217  				kind:  alt.kind,
   218  				inner: err,
   219  			}
   220  		}
   221  	}
   222  
   223  	// return base mapping for this status code.
   224  	return code, pushError{
   225  		kind:  mapping.kind,
   226  		inner: err,
   227  	}
   228  }