grafana/pkg/expr/ml/outlier.go

121 lines
3.8 KiB
Go

package ml
import (
"fmt"
"net/http"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/response"
)
// OutlierCommand implements Command that sends a request to outlier proxy API and converts response to backend.QueryDataResponse
type OutlierCommand struct {
config OutlierCommandConfiguration
appURL string
interval time.Duration
}
var _ Command = OutlierCommand{}
func (c OutlierCommand) Type() string {
return "outlier"
}
func (c OutlierCommand) DatasourceUID() string {
return c.config.DatasourceUID
}
// Execute copies the original configuration JSON and appends (overwrites) a field "start_end_attributes" and "grafana_url" to the root object.
// The value of "start_end_attributes" is JSON object that configures time range and interval.
// The value of "grafana_url" is app URL that should be used by ML to query the data source.
// After payload is generated it sends it to POST /proxy/api/v1/outlier endpoint and parses the response.
// The proxy API normally responds with a structured data. It recognizes status 200 and 204 as successful result.
// Other statuses are considered unsuccessful and result in error. Tries to extract error from the structured payload.
// Otherwise, mentions the full message in error
func (c OutlierCommand) Execute(from, to time.Time, sendRequest func(method string, path string, payload []byte) (response.Response, error)) (*backend.QueryDataResponse, error) {
payload := outlierRequestBody{
Data: outlierData{
Attributes: outlierAttributes{
OutlierCommandConfiguration: c.config,
GrafanaURL: c.appURL,
StartEndAttributes: newTimeRangeAndInterval(from, to, c.interval),
},
},
}
requestBody, err := json.Marshal(payload)
if err != nil {
return nil, err
}
resp, err := sendRequest(http.MethodPost, "/proxy/api/v1/outlier", requestBody)
if err != nil {
return nil, fmt.Errorf("failed to call ML API: %w", err)
}
if resp == nil {
return nil, fmt.Errorf("response is nil")
}
// Outlier proxy API usually returns all responses with this body.
var respData outlierResponse
respBody := resp.Body()
err = json.Unmarshal(respBody, &respData)
if err != nil {
return nil, fmt.Errorf("unexpected format of the response from ML API, status: %d, response: %s", resp.Status(), respBody)
}
if respData.Status == "error" {
return nil, fmt.Errorf("ML API responded with error: %s", respData.Error)
}
if resp.Status() == http.StatusNoContent {
return nil, nil
}
if resp.Status() == http.StatusOK {
return respData.Data, nil
}
return nil, fmt.Errorf("unexpected status %d returned by ML API, response: %s", resp.Status(), respBody)
}
// unmarshalOutlierCommand parses the CommandConfiguration.Config, validates data and produces OutlierCommand.
func unmarshalOutlierCommand(expr CommandConfiguration, appURL string) (*OutlierCommand, error) {
var cfg OutlierCommandConfiguration
err := json.Unmarshal(expr.Config, &cfg)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal outlier command: %w", err)
}
if len(cfg.DatasourceUID) == 0 {
return nil, fmt.Errorf("required field `config.datasource_uid` is not specified")
}
if len(cfg.Query) == 0 && len(cfg.QueryParams) == 0 {
return nil, fmt.Errorf("neither of required fields `config.query_params` or `config.query` are specified")
}
if len(cfg.ResponseType) == 0 {
return nil, fmt.Errorf("required field `config.response_type` is not specified")
}
if len(cfg.Algorithm) == 0 {
return nil, fmt.Errorf("required field `config.algorithm` is not specified")
}
interval := defaultInterval
if expr.IntervalMs != nil {
i := time.Duration(*expr.IntervalMs) * time.Millisecond
if i > 0 {
interval = i
}
}
return &OutlierCommand{
config: cfg,
interval: interval,
appURL: appURL,
}, nil
}