The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/expr/sql/errors.go

411 lines
14 KiB

package sql
import (
"errors"
"fmt"
"sort"
"strings"
"time"
mysql "github.com/dolthub/go-mysql-server/sql"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
)
const sseErrBase = "sse.sql."
// GoMySQLServerError represents an error from the underlying Go MySQL Server
type GoMySQLServerError struct {
err error
category string
}
// CategorizedError is an Error with a Category string for use with metrics, logs, and traces.
type CategorizedError interface {
error
Category() string
}
// ErrorWithCategory is a concrete implementation of CategorizedError that holds an error and its category.
type ErrorWithCategory struct {
category string
err error
}
func (e *ErrorWithCategory) Error() string {
return e.err.Error()
}
func (e *ErrorWithCategory) Category() string {
return e.category
}
// Unwrap provides the original error for errors.Is/As
func (e *ErrorWithCategory) Unwrap() error {
return e.err
}
// Error implements the error interface
func (e *GoMySQLServerError) Error() string {
return e.err.Error()
}
// Unwrap provides the original error for errors.Is/As
func (e *GoMySQLServerError) Unwrap() error {
return e.err
}
func (e *GoMySQLServerError) Category() string {
return e.category
}
// MakeGMSError creates a GoMySQLServerError with the given refID and error.
// It also used to wrap GMS errors into a GeneralGMSError or specific CategorizedError.
func MakeGMSError(refID string, err error) error {
err = WrapGoMySQLServerError(refID, err)
gmsError := &GoMySQLServerError{}
if errors.As(err, &gmsError) {
return MakeGeneralGMSError(gmsError, refID)
}
return err
}
const ErrCategoryGMSFunctionNotFound = "gms_function_not_found"
const ErrCategoryGMSTableNotFound = "gms_table_not_found"
// WrapGoMySQLServerError wraps errors from Go MySQL Server with additional context
// and a category.
func WrapGoMySQLServerError(refID string, err error) error {
// Don't wrap nil errors
if err == nil {
return nil
}
switch {
case mysql.ErrFunctionNotFound.Is(err):
return &GoMySQLServerError{err: err, category: ErrCategoryGMSFunctionNotFound}
case mysql.ErrTableNotFound.Is(err):
// This is different from the TableNotFoundError, which is used when the engine can't find the dependency before it gets to the SQL engine.
return &GoMySQLServerError{err: err, category: ErrCategoryGMSTableNotFound}
case mysql.ErrColumnNotFound.Is(err):
return MakeColumnNotFoundError(refID, err)
default:
// For all other errors, wrap them as a general GMS error
return MakeGeneralGMSError(&GoMySQLServerError{
err: err,
category: ErrCategoryGeneralGMSError,
}, refID)
}
}
const ErrCategoryGeneralGMSError = "general_gms_error"
var generalGMSErrorStr = "sql expression failed due to error from the sql expression engine: {{ .Error }}"
var GeneralGMSError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryGeneralGMSError).MustTemplate(
generalGMSErrorStr,
errutil.WithPublic(generalGMSErrorStr))
// MakeGeneralGMSError is for errors returned from the GMS engine that we have not make a more specific error for.
func MakeGeneralGMSError(err *GoMySQLServerError, refID string) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
},
Error: err,
}
return &ErrorWithCategory{category: err.Category(), err: GeneralGMSError.Build(data)}
}
const ErrCategoryInputLimitExceeded = "input_limit_exceeded"
var inputLimitExceededStr = "sql expression [{{ .Public.refId }}] was not run because the number of input cells (columns*rows) to the sql expression exceeded the configured limit of {{ .Public.inputLimit }}"
var InputLimitExceededError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryInputLimitExceeded).MustTemplate(
inputLimitExceededStr,
errutil.WithPublic(inputLimitExceededStr))
func MakeInputLimitExceededError(refID string, inputLimit int64) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"inputLimit": inputLimit,
},
}
return &ErrorWithCategory{category: ErrCategoryInputLimitExceeded, err: InputLimitExceededError.Build(data)}
}
const ErrCategoryDuplicateStringColumns = "duplicate_string_columns"
var duplicateStringColumnErrorStr = "sql expression [{{ .Public.refId }}] failed because it returned duplicate values across the string columns, which is not allowed for alerting. Examples: ({{ .Public.examples }}). Hint: use GROUP BY or aggregation (e.g. MAX(), AVG()) to return one row per unique combination."
var DuplicateStringColumnError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryDuplicateStringColumns).MustTemplate(
duplicateStringColumnErrorStr,
errutil.WithPublic(duplicateStringColumnErrorStr),
)
func MakeDuplicateStringColumnError(examples []string) CategorizedError {
const limit = 5
sort.Strings(examples)
exampleStr := strings.Join(truncateExamples(examples, limit), ", ")
data := errutil.TemplateData{
Public: map[string]interface{}{
"examples": exampleStr,
"count": len(examples),
},
}
return &ErrorWithCategory{
category: ErrCategoryDuplicateStringColumns,
err: DuplicateStringColumnError.Build(data),
}
}
func truncateExamples(examples []string, limit int) []string {
if len(examples) <= limit {
return examples
}
truncated := examples[:limit]
truncated = append(truncated, fmt.Sprintf("... and %d more", len(examples)-limit))
return truncated
}
const ErrCategoryTimeout = "timeout"
var timeoutStr = "sql expression [{{ .Public.refId }}] timed out after {{ .Public.timeout }}"
var TimeoutError = errutil.NewBase(
errutil.StatusTimeout, sseErrBase+ErrCategoryTimeout).MustTemplate(
timeoutStr,
errutil.WithPublic(timeoutStr))
// MakeTimeOutError creates an error for when a query times out because it took longer that the configured timeout.
func MakeTimeOutError(err error, refID string, timeout time.Duration) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"timeout": timeout.String(),
},
Error: err,
}
return &ErrorWithCategory{category: ErrCategoryTimeout, err: TimeoutError.Build(data)}
}
var ErrCategoryCancelled = "cancelled"
var cancelStr = "sql expression [{{ .Public.refId }}] was cancelled before completion"
var CancelError = errutil.NewBase(
errutil.StatusClientClosedRequest, sseErrBase+ErrCategoryCancelled).MustTemplate(
cancelStr,
errutil.WithPublic(cancelStr))
// MakeCancelError creates an error for when a query is cancelled before completion.
// Users won't see this error in the browser, rather an empty response when the browser cancels the connection.
func MakeCancelError(err error, refID string) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
},
Error: err,
}
return &ErrorWithCategory{category: ErrCategoryCancelled, err: CancelError.Build(data)}
}
var ErrCategoryTableNotFound = "table_not_found"
var tableNotFoundStr = "failed to run sql expression [{{ .Public.refId }}] because it selects from table (refId/query) [{{ .Public.table }}] and that table was not found"
var TableNotFoundError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryTableNotFound).MustTemplate(
tableNotFoundStr,
errutil.WithPublic(tableNotFoundStr))
// MakeTableNotFoundError creates an error for when a referenced table
// does not exist.
func MakeTableNotFoundError(refID, table string) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"table": table,
},
Error: fmt.Errorf("sql expression [%s] failed: table (refId)'%s' not found", refID, table),
}
return &ErrorWithCategory{category: ErrCategoryTableNotFound, err: TableNotFoundError.Build(data)}
}
const ErrCategoryDependency = "failed_dependency"
var sqlDepErrStr = "could not run sql expression [{{ .Public.refId }}] because it selects from the results of query [{{.Public.depRefId }}] which has an error"
var DependencyError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryDependency).MustTemplate(
sqlDepErrStr,
errutil.WithPublic(sqlDepErrStr))
func MakeSQLDependencyError(refID, depRefID string) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"depRefId": depRefID,
},
Error: fmt.Errorf("could not run sql expression %v because it selects from the results of query %v which has an error", refID, depRefID),
}
return &ErrorWithCategory{category: ErrCategoryDependency, err: DependencyError.Build(data)}
}
const ErrCategoryInputConversion = "input_conversion"
var sqlInputConvertErrorStr = "failed to convert the results of query [{{.Public.refId}}] (Datasource Type: [{{.Public.dsType}}]) into a SQL/Tabular format for sql expression {{ .Public.forRefID }}: {{ .Error }}"
var InputConvertError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryInputConversion).MustTemplate(
sqlInputConvertErrorStr,
errutil.WithPublic(sqlInputConvertErrorStr))
// MakeInputConvertError creates an error for when the input conversion to a table for a SQL expressions fails.
func MakeInputConvertError(err error, refID string, forRefIDs map[string]struct{}, dsType string) CategorizedError {
forRefIdsSlice := make([]string, 0, len(forRefIDs))
for k := range forRefIDs {
forRefIdsSlice = append(forRefIdsSlice, k)
}
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"forRefID": forRefIdsSlice,
"dsType": dsType,
},
Error: err,
}
return &ErrorWithCategory{category: ErrCategoryInputConversion, err: InputConvertError.Build(data)}
}
const ErrCategoryEmptyQuery = "empty_query"
var errEmptyQueryString = "sql expression [{{.Public.refId}}] failed because it has an empty SQL query"
var ErrEmptySQLQuery = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryEmptyQuery).MustTemplate(
errEmptyQueryString,
errutil.WithPublic(errEmptyQueryString))
// MakeTableNotFoundError creates an error for when a referenced table
// does not exist.
func MakeErrEmptyQuery(refID string) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
},
Error: fmt.Errorf("sql expression [%s] failed because it has an empty SQL query", refID),
}
return &ErrorWithCategory{category: ErrCategoryEmptyQuery, err: ErrEmptySQLQuery.Build(data)}
}
const ErrCategoryInvalidQuery = "invalid_query"
var invalidQueryStr = "sql expression [{{.Public.refId}}] failed because it has an invalid SQL query: {{ .Public.error }}"
var ErrInvalidQuery = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryInvalidQuery).MustTemplate(
invalidQueryStr,
errutil.WithPublic(invalidQueryStr))
func MakeErrInvalidQuery(refID string, err error) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"error": err.Error(),
},
Error: fmt.Errorf("sql expression [%s] failed because it has an invalid SQL query: %w", refID, err),
}
return &ErrorWithCategory{category: ErrCategoryInvalidQuery, err: ErrInvalidQuery.Build(data)}
}
var ErrCategoryBlockedNodeOrFunc = "blocked_node_or_func"
var blockedNodeOrFuncStr = "did not execute the SQL expression {{.Public.refId}} because the sql {{.Public.tokenType}} '{{.Public.token}}' is not in the allowed list of {{.Public.tokenType}}s"
var BlockedNodeOrFuncError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryBlockedNodeOrFunc).MustTemplate(
blockedNodeOrFuncStr,
errutil.WithPublic(blockedNodeOrFuncStr))
// MakeBlockedNodeOrFuncError creates an error for when a sql function or keyword is not allowed.
func MakeBlockedNodeOrFuncError(refID, token string, isFunction bool) CategorizedError {
tokenType := "keyword"
if isFunction {
tokenType = "function"
}
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"token": token,
"tokenType": tokenType,
},
Error: fmt.Errorf("sql expression [%s] failed because the sql function or keyword '%s' is not in the allowed list of keywords and functions", refID, token),
}
return &ErrorWithCategory{category: ErrCategoryBlockedNodeOrFunc, err: BlockedNodeOrFuncError.Build(data)}
}
const ErrCategoryColumnNotFound = "column_not_found"
var columnNotFoundStr = `sql expression [{{.Public.refId}}] failed because it selects from a column (refId/query) that does not exist: {{ .Error }}.
If this happens on a previously working query, it might mean that the query has returned no data, or the resulting schema of the query has changed.`
var ColumnNotFoundError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryColumnNotFound).MustTemplate(
columnNotFoundStr,
errutil.WithPublic(columnNotFoundStr))
func MakeColumnNotFoundError(refID string, err error) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
},
Error: err,
}
return &ErrorWithCategory{category: ErrCategoryColumnNotFound, err: ColumnNotFoundError.Build(data)}
}
const ErrCategoryQueryTooLong = "query_too_long"
var queryTooLongStr = `sql expression [{{.Public.refId}}] was not run because the SQL query exceeded the configured limit of {{ .Public.queryLengthLimit }} characters`
var QueryTooLongError = errutil.NewBase(
errutil.StatusBadRequest, sseErrBase+ErrCategoryQueryTooLong).MustTemplate(
queryTooLongStr,
errutil.WithPublic(queryTooLongStr))
func MakeQueryTooLongError(refID string, queryLengthLimit int64) CategorizedError {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"queryLengthLimit": queryLengthLimit,
},
}
return &ErrorWithCategory{category: ErrCategoryQueryTooLong, err: QueryTooLongError.Build(data)}
}