| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- package prom
- import (
- "fmt"
- "reflect"
- "strings"
- "sync"
- "github.com/opencost/opencost/pkg/log"
- )
- // errorType used to check HasError
- var errorType = reflect.TypeOf((*error)(nil)).Elem()
- // NoStoreAPIWarning is a warning that we would consider an error. It returns partial data relating only to the
- // store apis which were reachable. In order to ensure integrity of data across all clusters, we'll need to identify
- // this warning and convert it to an error.
- const NoStoreAPIWarning string = "No StoreAPIs matched for this query"
- // IsNoStoreAPIWarning checks a warning to determine if it is equivalent to a no store API query.
- func IsNoStoreAPIWarning(warning string) bool {
- return strings.EqualFold(warning, NoStoreAPIWarning)
- }
- //--------------------------------------------------------------------------
- // Prometheus Error Collection
- //--------------------------------------------------------------------------
- type QueryError struct {
- Query string `json:"query"`
- Error error `json:"error"`
- ParseError error `json:"parseError"`
- }
- // String returns a string representation of the QueryError
- func (qe *QueryError) String() string {
- var sb strings.Builder
- sb.WriteString("Errors:\n")
- if qe.Error != nil {
- sb.WriteString(fmt.Sprintf(" Request Error: %s\n", qe.Error))
- }
- if qe.ParseError != nil {
- sb.WriteString(fmt.Sprintf(" Parse Error: %s\n", qe.ParseError))
- }
- sb.WriteString(fmt.Sprintf("for Query: %s\n", qe.Query))
- return sb.String()
- }
- type QueryWarning struct {
- Query string `json:"query"`
- Warnings []string `json:"warnings"`
- }
- // String returns a string representation of the QueryWarning
- func (qw *QueryWarning) String() string {
- var sb strings.Builder
- sb.WriteString("Warnings:\n")
- for i, w := range qw.Warnings {
- sb.WriteString(fmt.Sprintf(" %d) %s\n", i+1, w))
- }
- sb.WriteString(fmt.Sprintf("for Query: %s\n", qw.Query))
- return sb.String()
- }
- // QueryErrorCollection represents a collection of query errors and warnings made via context.
- type QueryErrorCollection interface {
- // Warnings is a slice of the QueryWarning instances
- Warnings() []*QueryWarning
- // Errors is a slice of the QueryError instances
- Errors() []*QueryError
- // ToErrorAndWarningStrings returns the errors and warnings in the collection
- // as two string slices.
- ToErrorAndWarningStrings() (errors []string, warnings []string)
- }
- // ErrorsAndWarningStrings is a container struct for string representation storage/caching
- type ErrorsAndWarningStrings struct {
- Errors []string
- Warnings []string
- }
- // QueryErrorCollector is used to collect prometheus query errors and warnings, and also meets the
- // Error interface
- type QueryErrorCollector struct {
- m sync.RWMutex
- errors []*QueryError
- warnings []*QueryWarning
- }
- // Reports an error to the collector. Ignores if the error is nil and the warnings
- // are empty
- func (ec *QueryErrorCollector) Report(query string, warnings []string, requestError error, parseError error) {
- if requestError == nil && parseError == nil && len(warnings) == 0 {
- return
- }
- ec.m.Lock()
- defer ec.m.Unlock()
- if requestError != nil || parseError != nil {
- ec.errors = append(ec.errors, &QueryError{
- Query: query,
- Error: requestError,
- ParseError: parseError,
- })
- }
- if len(warnings) > 0 {
- ec.warnings = append(ec.warnings, &QueryWarning{
- Query: query,
- Warnings: warnings,
- })
- }
- }
- // Whether or not the collector caught any warnings
- func (ec *QueryErrorCollector) IsWarning() bool {
- ec.m.RLock()
- defer ec.m.RUnlock()
- return len(ec.warnings) > 0
- }
- // Whether or not the collector caught errors
- func (ec *QueryErrorCollector) IsError() bool {
- ec.m.RLock()
- defer ec.m.RUnlock()
- return len(ec.errors) > 0
- }
- // Warnings caught by the collector
- func (ec *QueryErrorCollector) Warnings() []*QueryWarning {
- ec.m.RLock()
- defer ec.m.RUnlock()
- warns := make([]*QueryWarning, len(ec.warnings))
- copy(warns, ec.warnings)
- return warns
- }
- // Errors caught by the collector
- func (ec *QueryErrorCollector) Errors() []*QueryError {
- ec.m.RLock()
- defer ec.m.RUnlock()
- errs := make([]*QueryError, len(ec.errors))
- copy(errs, ec.errors)
- return errs
- }
- // Implement the error interface to allow returning as an aggregated error
- func (ec *QueryErrorCollector) Error() string {
- ec.m.RLock()
- defer ec.m.RUnlock()
- var sb strings.Builder
- if len(ec.errors) > 0 {
- sb.WriteString("Error Collection:\n")
- for i, e := range ec.errors {
- sb.WriteString(fmt.Sprintf("%d) %s\n", i, e))
- }
- }
- if len(ec.warnings) > 0 {
- sb.WriteString("Warning Collection:\n")
- for _, w := range ec.warnings {
- sb.WriteString(w.String())
- }
- }
- return sb.String()
- }
- // ToErrorAndWarningStrings returns the errors and warnings in the collection as two string slices.
- func (ec *QueryErrorCollector) ToErrorAndWarningStrings() (errors []string, warnings []string) {
- for _, e := range ec.Errors() {
- errors = append(errors, e.String())
- }
- for _, w := range ec.Warnings() {
- warnings = append(warnings, w.String())
- }
- return
- }
- // As is a special method that implicitly works with the `errors.As()` go
- // helper to locate the _first_ instance of the provided target type in the
- // collection.
- func (ec *QueryErrorCollector) As(target interface{}) bool {
- if target == nil {
- log.Errorf("ErrorCollection.As() target cannot be nil")
- return false
- }
- val := reflect.ValueOf(target)
- typ := val.Type()
- if typ.Kind() != reflect.Ptr || val.IsNil() {
- log.Errorf("ErrorCollection.As() target must be a non-nil pointer")
- return false
- }
- if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) {
- log.Errorf("ErrorCollection.As() *target must be interface or implement error")
- return false
- }
- targetType := typ.Elem()
- for _, err := range AllErrorsFor(ec) {
- if reflect.TypeOf(err).AssignableTo(targetType) {
- val.Elem().Set(reflect.ValueOf(err))
- return true
- }
- if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
- return true
- }
- }
- return false
- }
- // IsErrorCollection returns true if the provided error is an ErrorCollection
- func IsErrorCollection(err error) bool {
- _, ok := err.(QueryErrorCollection)
- return ok
- }
- func AllErrorsFor(collection QueryErrorCollection) []error {
- var errs []error
- for _, qe := range collection.Errors() {
- if qe.Error != nil {
- errs = append(errs, qe.Error)
- }
- if qe.ParseError != nil {
- errs = append(errs, qe.ParseError)
- }
- }
- return errs
- }
- // WrapError wraps the given error with the given message, usually for adding
- // context, but persists the existing type of error.
- func WrapError(err error, msg string) error {
- switch e := err.(type) {
- case CommError:
- return e.Wrap(msg)
- case NoDataError:
- return e.Wrap(msg)
- default:
- return fmt.Errorf("%s: %s", msg, err)
- }
- }
- // CommError describes an error communicating with Prometheus
- type CommError struct {
- messages []string
- }
- // NewCommError creates a new CommError
- func NewCommError(messages ...string) CommError {
- return CommError{messages: messages}
- }
- // CommErrorf creates a new CommError using a string formatter
- func CommErrorf(format string, args ...interface{}) CommError {
- return NewCommError(fmt.Sprintf(format, args...))
- }
- // IsCommError returns true if the given error is a CommError
- func IsCommError(err error) bool {
- _, ok := err.(CommError)
- return ok
- }
- // Error prints the error as a string
- func (pce CommError) Error() string {
- return fmt.Sprintf("Prometheus communication error: %s", strings.Join(pce.messages, ": "))
- }
- // Wrap wraps the error with the given message, but persists the error type.
- func (pce CommError) Wrap(message string) CommError {
- pce.messages = append([]string{message}, pce.messages...)
- return pce
- }
- // NoDataError indicates that no data was returned by Prometheus. This should
- // be treated like an EOF error, in that it may be expected.
- type NoDataError struct {
- messages []string
- }
- // NewNoDataError creates a new NoDataError
- func NewNoDataError(messages ...string) NoDataError {
- return NoDataError{messages: messages}
- }
- // IsNoDataError returns true if the given error is a NoDataError
- func IsNoDataError(err error) bool {
- _, ok := err.(NoDataError)
- return ok
- }
- // Error prints the error as a string
- func (nde NoDataError) Error() string {
- return fmt.Sprintf("No data error: %s", strings.Join(nde.messages, ": "))
- }
- // Wrap wraps the error with the given message, but persists the error type.
- func (nde NoDataError) Wrap(message string) NoDataError {
- nde.messages = append([]string{message}, nde.messages...)
- return nde
- }
|