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/api/annotations.go

290 lines
7.2 KiB

package api
import (
"errors"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
func GetAnnotations(c *models.ReqContext) response.Response {
query := &annotations.ItemQuery{
From: c.QueryInt64("from"),
To: c.QueryInt64("to"),
OrgId: c.OrgId,
UserId: c.QueryInt64("userId"),
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"),
Tags: c.QueryStrings("tags"),
Type: c.Query("type"),
MatchAny: c.QueryBool("matchAny"),
}
repo := annotations.GetRepository()
items, err := repo.Find(query)
if err != nil {
return response.Error(500, "Failed to get annotations", err)
}
for _, item := range items {
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
}
return response.JSON(200, items)
}
type CreateAnnotationError struct {
message string
}
func (e *CreateAnnotationError) Error() string {
return e.message
}
func PostAnnotation(c *models.ReqContext, cmd dtos.PostAnnotationsCmd) response.Response {
if canSave, err := canSaveByDashboardID(c, cmd.DashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
repo := annotations.GetRepository()
if cmd.Text == "" {
err := &CreateAnnotationError{"text field should not be empty"}
return response.Error(400, "Failed to save annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
Epoch: cmd.Time,
EpochEnd: cmd.TimeEnd,
Text: cmd.Text,
Data: cmd.Data,
Tags: cmd.Tags,
}
if err := repo.Save(&item); err != nil {
if errors.Is(err, annotations.ErrTimerangeMissing) {
return response.Error(400, "Failed to save annotation", err)
}
return response.Error(500, "Failed to save annotation", err)
}
startID := item.Id
return response.JSON(200, util.DynMap{
"message": "Annotation added",
"id": startID,
})
}
func formatGraphiteAnnotation(what string, data string) string {
text := what
if data != "" {
text = text + "\n" + data
}
return text
}
func PostGraphiteAnnotation(c *models.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd) response.Response {
repo := annotations.GetRepository()
if cmd.What == "" {
err := &CreateAnnotationError{"what field should not be empty"}
return response.Error(400, "Failed to save Graphite annotation", err)
}
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
var tagsArray []string
switch tags := cmd.Tags.(type) {
case string:
if tags != "" {
tagsArray = strings.Split(tags, " ")
} else {
tagsArray = []string{}
}
case []interface{}:
for _, t := range tags {
if tagStr, ok := t.(string); ok {
tagsArray = append(tagsArray, tagStr)
} else {
err := &CreateAnnotationError{"tag should be a string"}
return response.Error(400, "Failed to save Graphite annotation", err)
}
}
default:
err := &CreateAnnotationError{"unsupported tags format"}
return response.Error(400, "Failed to save Graphite annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Epoch: cmd.When * 1000,
Text: text,
Tags: tagsArray,
}
if err := repo.Save(&item); err != nil {
return response.Error(500, "Failed to save Graphite annotation", err)
}
return response.JSON(200, util.DynMap{
"message": "Graphite annotation added",
"id": item.Id,
})
}
func UpdateAnnotation(c *models.ReqContext, cmd dtos.UpdateAnnotationsCmd) response.Response {
annotationID := c.ParamsInt64(":annotationId")
repo := annotations.GetRepository()
if resp := canSave(c, repo, annotationID); resp != nil {
return resp
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Id: annotationID,
Epoch: cmd.Time,
EpochEnd: cmd.TimeEnd,
Text: cmd.Text,
Tags: cmd.Tags,
}
if err := repo.Update(&item); err != nil {
return response.Error(500, "Failed to update annotation", err)
}
return response.Success("Annotation updated")
}
func PatchAnnotation(c *models.ReqContext, cmd dtos.PatchAnnotationsCmd) response.Response {
annotationID := c.ParamsInt64(":annotationId")
repo := annotations.GetRepository()
if resp := canSave(c, repo, annotationID); resp != nil {
return resp
}
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return response.Error(404, "Could not find annotation to update", err)
}
existing := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Id: annotationID,
Epoch: items[0].Time,
EpochEnd: items[0].TimeEnd,
Text: items[0].Text,
Tags: items[0].Tags,
}
if cmd.Tags != nil {
existing.Tags = cmd.Tags
}
if cmd.Text != "" && cmd.Text != existing.Text {
existing.Text = cmd.Text
}
if cmd.Time > 0 && cmd.Time != existing.Epoch {
existing.Epoch = cmd.Time
}
if cmd.TimeEnd > 0 && cmd.TimeEnd != existing.EpochEnd {
existing.EpochEnd = cmd.TimeEnd
}
if err := repo.Update(&existing); err != nil {
return response.Error(500, "Failed to update annotation", err)
}
return response.Success("Annotation patched")
}
func DeleteAnnotations(c *models.ReqContext, cmd dtos.DeleteAnnotationsCmd) response.Response {
repo := annotations.GetRepository()
err := repo.Delete(&annotations.DeleteParams{
OrgId: c.OrgId,
Id: cmd.AnnotationId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
})
if err != nil {
return response.Error(500, "Failed to delete annotations", err)
}
return response.Success("Annotations deleted")
}
func DeleteAnnotationByID(c *models.ReqContext) response.Response {
repo := annotations.GetRepository()
annotationID := c.ParamsInt64(":annotationId")
if resp := canSave(c, repo, annotationID); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{
OrgId: c.OrgId,
Id: annotationID,
})
if err != nil {
return response.Error(500, "Failed to delete annotation", err)
}
return response.Success("Annotation deleted")
}
func canSaveByDashboardID(c *models.ReqContext, dashboardID int64) (bool, error) {
if dashboardID == 0 && !c.SignedInUser.HasRole(models.ROLE_EDITOR) {
return false, nil
}
if dashboardID != 0 {
guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err
}
}
return true, nil
}
func canSave(c *models.ReqContext, repo annotations.Repository, annotationID int64) response.Response {
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return response.Error(500, "Could not find annotation to update", err)
}
dashboardID := items[0].DashboardId
if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
return nil
}