|
| 1 | +package plugin |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "net/http" |
| 8 | + "net/url" |
| 9 | + "time" |
| 10 | + |
| 11 | + "github.com/grafana/grafana-plugin-sdk-go/backend" |
| 12 | + "github.com/grafana/grafana-plugin-sdk-go/backend/log" |
| 13 | +) |
| 14 | + |
| 15 | +type annotationRequest struct { |
| 16 | + Range struct { |
| 17 | + From time.Time `json:"from"` |
| 18 | + To time.Time `json:"to"` |
| 19 | + } `json:"range"` |
| 20 | + Annotation struct { |
| 21 | + Query string `json:"query"` |
| 22 | + } `json:"annotation"` |
| 23 | +} |
| 24 | + |
| 25 | +func setQueryParams(u *url.URL, areq *annotationRequest) *url.URL { |
| 26 | + q := u.Query() |
| 27 | + q.Set("order", `timestamp+`) |
| 28 | + q.Set("filter", fmt.Sprintf("timestamp.>=.%d,timestamp.<=.%d", areq.Range.From.UnixNano(), areq.Range.To.UnixNano())) |
| 29 | + u.RawQuery = q.Encode() |
| 30 | + return u |
| 31 | +} |
| 32 | + |
| 33 | +func (d *Datasource) handleAnnotations(_ context.Context, creq *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { |
| 34 | + |
| 35 | + // Decode the incoming request from Grafana |
| 36 | + |
| 37 | + log.DefaultLogger.Info("handleAnnotations", "req.Body", string(creq.Body)) |
| 38 | + var areq annotationRequest |
| 39 | + if err := json.Unmarshal(creq.Body, &areq); err != nil { |
| 40 | + log.DefaultLogger.Error("handleAnnotations Error", "req.Body", string(creq.Body), "err", err.Error()) |
| 41 | + return sender.Send(&backend.CallResourceResponse{ |
| 42 | + Status: http.StatusBadRequest, |
| 43 | + Body: []byte("failed to unmarshal annotation request: " + err.Error()), |
| 44 | + }) |
| 45 | + } |
| 46 | + |
| 47 | + // Request detections from the Prequel API |
| 48 | + u := setQueryParams(d.url, &areq) |
| 49 | + |
| 50 | + url := renderURL(u, "detections") |
| 51 | + log.DefaultLogger.With("url", url).Debug("Fetching detections for annotations") |
| 52 | + |
| 53 | + resp, err := d.httpClient.Get(url) |
| 54 | + if err != nil { |
| 55 | + return sendCallResourceError(sender, "Failed request detections API", err) |
| 56 | + } |
| 57 | + defer resp.Body.Close() |
| 58 | + |
| 59 | + if resp.StatusCode != http.StatusOK { |
| 60 | + return sendCallResourceError(sender, "Failed to fetch detections from API", fmt.Errorf("status code %d", resp.StatusCode)) |
| 61 | + } |
| 62 | + |
| 63 | + var dresp detectionResponse |
| 64 | + err = json.NewDecoder(resp.Body).Decode(&dresp) |
| 65 | + if err != nil { |
| 66 | + return sendCallResourceError(sender, "Failed to decode detections response", err) |
| 67 | + } |
| 68 | + |
| 69 | + annotations := []map[string]any{} |
| 70 | + for _, item := range dresp.Items { |
| 71 | + annotations = append(annotations, map[string]interface{}{ |
| 72 | + "time": item.Timestamp / int64(time.Millisecond), // Convert to milliseconds |
| 73 | + "title": item.RuleTitle, |
| 74 | + "text": fmt.Sprintf(`<a href="https://app-dev.prequel.dev/detections/%s">See full incident report</a>`, item.DetectionID), |
| 75 | + "tags": []string{item.Category}, |
| 76 | + }) |
| 77 | + } |
| 78 | + |
| 79 | + // 3. Marshal the response and send it back to Grafana |
| 80 | + responseJSON, err := json.Marshal(annotations) |
| 81 | + if err != nil { |
| 82 | + return sendCallResourceError(sender, "Failed to marshal annotation response", err) |
| 83 | + } |
| 84 | + |
| 85 | + return sender.Send(&backend.CallResourceResponse{ |
| 86 | + Status: http.StatusOK, |
| 87 | + Body: responseJSON, |
| 88 | + Headers: map[string][]string{ |
| 89 | + "Content-Type": {"application/json"}, |
| 90 | + }, |
| 91 | + }) |
| 92 | +} |
| 93 | + |
| 94 | +type detectionResponseItem struct { |
| 95 | + Timestamp int64 `json:"timestamp"` |
| 96 | + RuleTitle string `json:"rule_title"` |
| 97 | + DetectionID string `json:"detection_id"` |
| 98 | + Category string `json:"category"` |
| 99 | + Namespace string `json:"namespace"` |
| 100 | + ContainerName string `json:"container_name"` |
| 101 | + K8sObject string `json:"k8s_object"` |
| 102 | +} |
| 103 | + |
| 104 | +type detectionResponse struct { |
| 105 | + Items []detectionResponseItem `json:"rows"` |
| 106 | +} |
| 107 | + |
| 108 | +func sendCallResourceError(sender backend.CallResourceResponseSender, msg string, errs ...error) error { |
| 109 | + // Log the error details |
| 110 | + if len(errs) > 0 && errs[0] != nil { |
| 111 | + log.DefaultLogger.Error(msg, "error", errs[0].Error()) |
| 112 | + } |
| 113 | + |
| 114 | + return sender.Send(&backend.CallResourceResponse{ |
| 115 | + Status: http.StatusBadRequest, |
| 116 | + Body: []byte(msg), |
| 117 | + }) |
| 118 | +} |
0 commit comments