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 }