2022-09-19 15:54:37 +08:00
package annotationsimpl
2016-08-01 16:07:00 +08:00
import (
2016-08-30 15:32:56 +08:00
"bytes"
2022-03-22 19:20:57 +08:00
"context"
2017-10-07 16:31:39 +08:00
"errors"
2016-08-30 15:32:56 +08:00
"fmt"
2016-09-14 14:36:44 +08:00
"strings"
2018-03-22 09:22:58 +08:00
"time"
2016-08-30 15:32:56 +08:00
2023-11-15 07:11:01 +08:00
"github.com/grafana/grafana/pkg/services/annotations/accesscontrol"
2025-06-14 02:59:24 +08:00
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
2024-01-13 04:05:04 +08:00
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
2023-11-15 07:11:01 +08:00
2024-06-13 12:11:35 +08:00
"github.com/grafana/grafana/pkg/apimachinery/identity"
2022-10-19 21:02:15 +08:00
"github.com/grafana/grafana/pkg/infra/db"
2022-09-19 15:54:37 +08:00
"github.com/grafana/grafana/pkg/infra/log"
2016-08-01 16:07:00 +08:00
"github.com/grafana/grafana/pkg/services/annotations"
2022-11-04 23:39:26 +08:00
"github.com/grafana/grafana/pkg/services/sqlstore"
2022-09-21 20:04:01 +08:00
"github.com/grafana/grafana/pkg/services/tag"
2022-09-19 15:54:37 +08:00
"github.com/grafana/grafana/pkg/setting"
2016-08-01 16:07:00 +08:00
)
2019-08-16 16:49:30 +08:00
// Update the item so that EpochEnd >= Epoch
func validateTimeRange ( item * annotations . Item ) error {
if item . EpochEnd == 0 {
if item . Epoch == 0 {
2021-03-29 21:47:16 +08:00
return annotations . ErrTimerangeMissing
2019-08-16 16:49:30 +08:00
}
item . EpochEnd = item . Epoch
}
if item . Epoch == 0 {
item . Epoch = item . EpochEnd
}
if item . EpochEnd < item . Epoch {
2020-09-22 22:22:19 +08:00
item . Epoch , item . EpochEnd = item . EpochEnd , item . Epoch
2019-08-16 16:49:30 +08:00
}
return nil
}
2022-09-22 20:27:48 +08:00
type xormRepositoryImpl struct {
2023-11-15 07:11:01 +08:00
cfg * setting . Cfg
db db . DB
log log . Logger
tagService tag . Service
}
func NewXormStore ( cfg * setting . Cfg , l log . Logger , db db . DB , tagService tag . Service ) * xormRepositoryImpl {
2025-06-14 02:59:24 +08:00
// populate dashboard_uid at startup, to ensure safe downgrades & upgrades after
// the initial migration occurs
err := migrations . RunDashboardUIDMigrations ( db . GetEngine ( ) . NewSession ( ) , db . GetEngine ( ) . DriverName ( ) )
if err != nil {
l . Error ( "failed to populate dashboard_uid for annotations" , "error" , err )
}
2023-11-15 07:11:01 +08:00
return & xormRepositoryImpl {
cfg : cfg ,
db : db ,
2023-12-13 06:43:09 +08:00
log : l ,
2023-11-15 07:11:01 +08:00
tagService : tagService ,
}
2022-04-11 20:18:38 +08:00
}
2024-02-29 02:16:37 +08:00
func ( r * xormRepositoryImpl ) Type ( ) string {
return "sql"
}
2022-09-22 20:27:48 +08:00
func ( r * xormRepositoryImpl ) Add ( ctx context . Context , item * annotations . Item ) error {
2022-09-21 20:04:01 +08:00
tags := tag . ParseTagPairs ( item . Tags )
item . Tags = tag . JoinTagPairs ( tags )
item . Created = timeNow ( ) . UnixNano ( ) / int64 ( time . Millisecond )
item . Updated = item . Created
if item . Epoch == 0 {
item . Epoch = item . Created
}
2022-09-23 18:04:41 +08:00
if err := r . validateItem ( item ) ; err != nil {
2022-09-21 20:04:01 +08:00
return err
}
2018-03-22 23:21:47 +08:00
2022-10-19 21:02:15 +08:00
return r . db . WithDbSession ( ctx , func ( sess * db . Session ) error {
2016-08-01 16:07:00 +08:00
if _ , err := sess . Table ( "annotation" ) . Insert ( item ) ; err != nil {
return err
}
2023-07-31 23:19:59 +08:00
return r . ensureTags ( ctx , item . ID , item . Tags )
2022-11-04 23:39:26 +08:00
} )
}
// AddMany inserts large batches of annotations at once.
// It does not return IDs associated with created annotations, and it does not support annotations with tags. If you need this functionality, use the single-item Add instead.
// This is due to a limitation with some supported databases:
// We cannot correlate the IDs of batch-inserted records without acquiring a full table lock in MySQL.
// Annotations have no other uniquifier field, so we also cannot re-query for them after the fact.
// So, callers can only reliably use this endpoint if they don't care about returned IDs.
func ( r * xormRepositoryImpl ) AddMany ( ctx context . Context , items [ ] annotations . Item ) error {
hasTags := make ( [ ] annotations . Item , 0 )
hasNoTags := make ( [ ] annotations . Item , 0 )
2023-01-07 10:23:46 +08:00
if len ( items ) == 0 {
return nil
}
2023-01-05 00:16:54 +08:00
for i := range items {
// The validation logic needs to work in terms of pointers.
// So, force everything else to work in terms of pointers too, to avoid any implicit extra copying.
item := & items [ i ]
2022-11-04 23:39:26 +08:00
tags := tag . ParseTagPairs ( item . Tags )
item . Tags = tag . JoinTagPairs ( tags )
item . Created = timeNow ( ) . UnixNano ( ) / int64 ( time . Millisecond )
item . Updated = item . Created
if item . Epoch == 0 {
item . Epoch = item . Created
}
2023-01-05 00:16:54 +08:00
if err := r . validateItem ( item ) ; err != nil {
2022-11-04 23:39:26 +08:00
return err
}
if len ( item . Tags ) > 0 {
2023-01-05 00:16:54 +08:00
hasTags = append ( hasTags , * item )
2022-11-04 23:39:26 +08:00
} else {
2023-01-05 00:16:54 +08:00
hasNoTags = append ( hasNoTags , * item )
2022-11-04 23:39:26 +08:00
}
}
return r . db . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
// We can batch-insert every annotation with no tags. If an annotation has tags, we need the ID.
opts := sqlstore . NativeSettingsForDialect ( r . db . GetDialect ( ) )
if _ , err := sess . BulkInsert ( "annotation" , hasNoTags , opts ) ; err != nil {
return err
}
for i , item := range hasTags {
if _ , err := sess . Table ( "annotation" ) . Insert ( item ) ; err != nil {
return err
}
2023-07-31 23:19:59 +08:00
itemWithID := & hasTags [ i ]
if err := r . ensureTags ( ctx , itemWithID . ID , itemWithID . Tags ) ; err != nil {
2022-11-04 23:39:26 +08:00
return err
}
}
return nil
} )
}
2016-08-01 16:07:00 +08:00
2022-09-22 20:27:48 +08:00
func ( r * xormRepositoryImpl ) Update ( ctx context . Context , item * annotations . Item ) error {
2023-07-07 21:21:49 +08:00
return r . db . InTransaction ( ctx , func ( ctx context . Context ) error {
return r . update ( ctx , item )
} )
}
func ( r * xormRepositoryImpl ) update ( ctx context . Context , item * annotations . Item ) error {
return r . db . WithDbSession ( ctx , func ( sess * db . Session ) error {
2017-10-07 16:31:39 +08:00
var (
isExist bool
err error
)
existing := new ( annotations . Item )
2023-02-04 00:23:09 +08:00
isExist , err = sess . Table ( "annotation" ) . Where ( "id=? AND org_id=?" , item . ID , item . OrgID ) . Get ( existing )
2017-10-07 16:31:39 +08:00
if err != nil {
return err
}
if ! isExist {
2020-11-05 18:57:20 +08:00
return errors . New ( "annotation not found" )
2017-10-07 16:31:39 +08:00
}
2020-10-26 14:45:30 +08:00
existing . Updated = timeNow ( ) . UnixNano ( ) / int64 ( time . Millisecond )
2017-10-07 16:31:39 +08:00
existing . Text = item . Text
2019-08-16 16:49:30 +08:00
if item . Epoch != 0 {
existing . Epoch = item . Epoch
}
if item . EpochEnd != 0 {
existing . EpochEnd = item . EpochEnd
}
2022-12-26 22:53:52 +08:00
if item . Data != nil {
existing . Data = item . Data
}
2017-10-07 16:31:39 +08:00
if item . Tags != nil {
2023-07-31 23:19:59 +08:00
err := r . ensureTags ( ctx , existing . ID , item . Tags )
Outdent code after if block that ends with return (golint)
This commit fixes the following golint warnings:
pkg/bus/bus.go:64:9: if block ends with a return statement, so drop this else and outdent its block
pkg/bus/bus.go:84:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:137:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:177:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:183:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:199:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:208:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/components/dynmap/dynmap.go:236:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:242:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:257:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:263:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:278:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:284:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:299:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:331:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:350:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:356:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:366:12: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:390:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:396:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:405:12: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:427:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:433:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:442:12: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:459:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:465:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:474:12: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:491:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:497:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:506:12: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:523:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:529:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:538:12: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:555:9: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:561:10: if block ends with a return statement, so drop this else and outdent its block
pkg/components/dynmap/dynmap.go:570:12: if block ends with a return statement, so drop this else and outdent its block
pkg/login/ldap.go:55:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/login/ldap_test.go:372:10: if block ends with a return statement, so drop this else and outdent its block
pkg/middleware/middleware_test.go:213:12: if block ends with a return statement, so drop this else and outdent its block
pkg/plugins/dashboard_importer.go:153:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/plugins/dashboards_updater.go:39:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/plugins/dashboards_updater.go:121:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/plugins/plugins.go:210:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/plugins/plugins.go:235:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/eval_context.go:111:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/notifier.go:92:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/notifier.go:98:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/notifier.go:122:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/rule.go:108:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/rule.go:118:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/rule.go:121:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/alerting/notifiers/telegram.go:94:10: if block ends with a return statement, so drop this else and outdent its block
pkg/services/sqlstore/annotation.go:34:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/sqlstore/annotation.go:99:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/sqlstore/dashboard_test.go:107:13: if block ends with a return statement, so drop this else and outdent its block
pkg/services/sqlstore/plugin_setting.go:78:10: if block ends with a return statement, so drop this else and outdent its block
pkg/services/sqlstore/preferences.go:91:10: if block ends with a return statement, so drop this else and outdent its block
pkg/services/sqlstore/user.go:50:10: if block ends with a return statement, so drop this else and outdent its block
pkg/services/sqlstore/migrator/migrator.go:106:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/services/sqlstore/migrator/postgres_dialect.go:48:10: if block ends with a return statement, so drop this else and outdent its block
pkg/tsdb/time_range.go:59:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/tsdb/time_range.go:67:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
pkg/tsdb/cloudwatch/metric_find_query.go:225:9: if block ends with a return statement, so drop this else and outdent its block
pkg/util/filepath.go:68:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
2018-04-28 04:42:49 +08:00
if err != nil {
return err
}
2017-10-07 16:31:39 +08:00
}
existing . Tags = item . Tags
2017-04-12 22:26:34 +08:00
2022-09-23 18:04:41 +08:00
if err := r . validateItem ( existing ) ; err != nil {
return err
}
2023-02-04 00:23:09 +08:00
_ , err = sess . Table ( "annotation" ) . ID ( existing . ID ) . Cols ( "epoch" , "text" , "epoch_end" , "updated" , "tags" , "data" ) . Update ( existing )
2018-04-17 01:54:23 +08:00
return err
2017-04-12 22:26:34 +08:00
} )
}
2023-07-31 23:19:59 +08:00
func ( r * xormRepositoryImpl ) ensureTags ( ctx context . Context , annotationID int64 , tags [ ] string ) error {
return r . db . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
var tagsInsert [ ] annotationTag
var tagsDelete [ ] int64
expectedTags , err := r . tagService . EnsureTagsExist ( ctx , tag . ParseTagPairs ( tags ) )
if err != nil {
return err
}
expected := tagSet ( func ( t * tag . Tag ) int64 {
return t . Id
} , expectedTags )
existingTags := make ( [ ] annotationTag , 0 )
if err := sess . SQL ( "SELECT annotation_id, tag_id FROM annotation_tag WHERE annotation_id = ?" , annotationID ) . Find ( & existingTags ) ; err != nil {
return err
}
existing := tagSet ( func ( t annotationTag ) int64 {
return t . TagID
} , existingTags )
for t := range expected {
if _ , exists := existing [ t ] ; ! exists {
tagsInsert = append ( tagsInsert , annotationTag {
AnnotationID : annotationID ,
TagID : t ,
} )
}
}
for t := range existing {
if _ , exists := expected [ t ] ; ! exists {
tagsDelete = append ( tagsDelete , t )
}
}
if len ( tagsDelete ) != 0 {
if _ , err := sess . MustCols ( "annotation_id" , "tag_id" ) . In ( "tag_id" , tagsDelete ) . Delete ( annotationTag { AnnotationID : annotationID } ) ; err != nil {
return err
}
}
if len ( tagsInsert ) != 0 {
if _ , err := sess . InsertMulti ( tagsInsert ) ; err != nil {
return err
}
}
return nil
} )
}
func tagSet [ T any ] ( fn func ( T ) int64 , list [ ] T ) map [ int64 ] struct { } {
set := make ( map [ int64 ] struct { } , len ( list ) )
for _ , item := range list {
set [ fn ( item ) ] = struct { } { }
}
return set
}
2024-10-03 15:14:06 +08:00
func ( r * xormRepositoryImpl ) Get ( ctx context . Context , query annotations . ItemQuery , accessResources * accesscontrol . AccessResources ) ( [ ] * annotations . ItemDTO , error ) {
2016-08-30 15:32:56 +08:00
var sql bytes . Buffer
params := make ( [ ] interface { } , 0 )
2022-03-22 19:20:57 +08:00
items := make ( [ ] * annotations . ItemDTO , 0 )
2022-10-19 21:02:15 +08:00
err := r . db . WithDbSession ( ctx , func ( sess * db . Session ) error {
2022-03-22 19:20:57 +08:00
sql . WriteString ( `
SELECT
annotation . id ,
annotation . epoch as time ,
annotation . epoch_end as time_end ,
2025-06-14 02:59:24 +08:00
annotation . dashboard_uid ,
2022-03-22 19:20:57 +08:00
annotation . dashboard_id ,
annotation . panel_id ,
annotation . new_state ,
annotation . prev_state ,
annotation . alert_id ,
annotation . text ,
annotation . tags ,
annotation . data ,
annotation . created ,
annotation . updated ,
usr . email ,
usr . login ,
2025-02-22 05:08:40 +08:00
r . title as alert_name
2022-03-22 19:20:57 +08:00
FROM annotation
2022-09-19 15:54:37 +08:00
LEFT OUTER JOIN ` + r.db.GetDialect().Quote("user") + ` as usr on usr . id = annotation . user_id
2025-02-22 05:08:40 +08:00
LEFT OUTER JOIN alert_rule as r on r . id = annotation . alert_id
2022-03-22 19:20:57 +08:00
INNER JOIN (
SELECT a . id from annotation a
` )
sql . WriteString ( ` WHERE a.org_id = ? ` )
2023-02-04 00:23:09 +08:00
params = append ( params , query . OrgID )
2022-03-22 19:20:57 +08:00
2023-02-04 00:23:09 +08:00
if query . AnnotationID != 0 {
2025-03-27 19:27:53 +08:00
// fmt.Println("annotation query")
2022-03-22 19:20:57 +08:00
sql . WriteString ( ` AND a.id = ? ` )
2023-02-04 00:23:09 +08:00
params = append ( params , query . AnnotationID )
2022-03-22 19:20:57 +08:00
}
2016-08-30 15:32:56 +08:00
2023-02-04 00:23:09 +08:00
if query . AlertID != 0 {
2022-03-22 19:20:57 +08:00
sql . WriteString ( ` AND a.alert_id = ? ` )
2023-02-04 00:23:09 +08:00
params = append ( params , query . AlertID )
2025-02-22 05:08:40 +08:00
} else if query . AlertUID != "" {
sql . WriteString ( ` AND a.alert_id = (SELECT id FROM alert_rule WHERE uid = ? and org_id = ?) ` )
params = append ( params , query . AlertUID , query . OrgID )
2022-03-22 19:20:57 +08:00
}
2017-12-21 07:52:21 +08:00
2025-06-14 02:59:24 +08:00
// nolint: staticcheck
2023-02-04 00:23:09 +08:00
if query . DashboardID != 0 {
2022-03-22 19:20:57 +08:00
sql . WriteString ( ` AND a.dashboard_id = ? ` )
2023-02-04 00:23:09 +08:00
params = append ( params , query . DashboardID )
2022-03-22 19:20:57 +08:00
}
2016-09-09 17:30:55 +08:00
2025-06-14 02:59:24 +08:00
// note: orgID is already required above
if query . DashboardUID != "" {
sql . WriteString ( ` AND a.dashboard_uid = ? ` )
params = append ( params , query . DashboardUID )
}
2023-02-04 00:23:09 +08:00
if query . PanelID != 0 {
2022-03-22 19:20:57 +08:00
sql . WriteString ( ` AND a.panel_id = ? ` )
2023-02-04 00:23:09 +08:00
params = append ( params , query . PanelID )
2022-03-22 19:20:57 +08:00
}
2016-09-09 17:30:55 +08:00
2023-02-04 00:23:09 +08:00
if query . UserID != 0 {
2022-03-22 19:20:57 +08:00
sql . WriteString ( ` AND a.user_id = ? ` )
2023-02-04 00:23:09 +08:00
params = append ( params , query . UserID )
2022-03-22 19:20:57 +08:00
}
2016-09-09 17:30:55 +08:00
2022-03-22 19:20:57 +08:00
if query . From > 0 && query . To > 0 {
sql . WriteString ( ` AND a.epoch <= ? AND a.epoch_end >= ? ` )
params = append ( params , query . To , query . From )
}
2018-03-22 22:52:09 +08:00
2025-04-10 20:42:23 +08:00
switch query . Type {
case "alert" :
2022-03-22 19:20:57 +08:00
sql . WriteString ( ` AND a.alert_id > 0 ` )
2025-04-10 20:42:23 +08:00
case "annotation" :
2022-03-22 19:20:57 +08:00
sql . WriteString ( ` AND a.alert_id = 0 ` )
}
2016-09-08 17:25:45 +08:00
2022-03-22 19:20:57 +08:00
if len ( query . Tags ) > 0 {
keyValueFilters := [ ] string { }
2017-11-21 18:27:53 +08:00
2022-09-21 20:04:01 +08:00
tags := tag . ParseTagPairs ( query . Tags )
2022-03-22 19:20:57 +08:00
for _ , tag := range tags {
if tag . Value == "" {
2022-09-19 15:54:37 +08:00
keyValueFilters = append ( keyValueFilters , "(tag." + r . db . GetDialect ( ) . Quote ( "key" ) + " = ?)" )
2022-03-22 19:20:57 +08:00
params = append ( params , tag . Key )
} else {
2022-09-19 15:54:37 +08:00
keyValueFilters = append ( keyValueFilters , "(tag." + r . db . GetDialect ( ) . Quote ( "key" ) + " = ? AND tag." + r . db . GetDialect ( ) . Quote ( "value" ) + " = ?)" )
2022-03-22 19:20:57 +08:00
params = append ( params , tag . Key , tag . Value )
}
2017-10-07 16:31:39 +08:00
}
2016-08-30 15:32:56 +08:00
2022-03-22 19:20:57 +08:00
if len ( tags ) > 0 {
tagsSubQuery := fmt . Sprintf ( `
2025-03-21 23:10:51 +08:00
SELECT SUM ( 1 ) FROM annotation_tag ` +r.db.Quote("at")+ `
INNER JOIN tag on tag . id = ` +r.db.Quote("at")+ ` . tag_id
WHERE ` +r.db.Quote("at")+ ` . annotation_id = a . id
2022-03-22 19:20:57 +08:00
AND (
% s
)
` , strings . Join ( keyValueFilters , " OR " ) )
if query . MatchAny {
sql . WriteString ( fmt . Sprintf ( " AND (%s) > 0 " , tagsSubQuery ) )
} else {
sql . WriteString ( fmt . Sprintf ( " AND (%s) = %d " , tagsSubQuery , len ( tags ) ) )
}
2018-09-11 21:50:04 +08:00
}
2016-09-14 14:36:44 +08:00
}
2016-08-30 15:32:56 +08:00
2025-06-14 02:59:24 +08:00
acFilter , acParams := r . getAccessControlFilter ( query . SignedInUser , accessResources )
2024-09-23 23:29:29 +08:00
if acFilter != "" {
sql . WriteString ( fmt . Sprintf ( " AND (%s)" , acFilter ) )
2022-03-22 19:20:57 +08:00
}
2025-06-14 02:59:24 +08:00
params = append ( params , acParams ... )
2016-08-30 15:32:56 +08:00
2022-03-22 19:20:57 +08:00
// order of ORDER BY arguments match the order of a sql index for performance
2024-09-23 23:29:29 +08:00
orderBy := " ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC"
if query . Limit > 0 {
orderBy += r . db . GetDialect ( ) . Limit ( query . Limit )
}
sql . WriteString ( orderBy + " ) dt on dt.id = annotation.id" )
2023-04-06 16:16:15 +08:00
2022-03-22 19:20:57 +08:00
if err := sess . SQL ( sql . String ( ) , params ... ) . Find ( & items ) ; err != nil {
items = nil
return err
}
return nil
} ,
)
2016-08-01 16:07:00 +08:00
2022-03-22 19:20:57 +08:00
return items , err
2016-08-01 16:07:00 +08:00
}
2016-10-14 15:33:16 +08:00
2025-06-14 02:59:24 +08:00
func ( r * xormRepositoryImpl ) getAccessControlFilter ( user identity . Requester , accessResources * accesscontrol . AccessResources ) ( string , [ ] any ) {
2024-09-23 23:29:29 +08:00
if accessResources . SkipAccessControlFilter {
return "" , nil
}
2023-11-15 07:11:01 +08:00
var filters [ ] string
2025-06-14 02:59:24 +08:00
var params [ ] any
2023-04-06 16:16:15 +08:00
2023-11-29 18:34:44 +08:00
if accessResources . CanAccessOrgAnnotations {
2023-11-15 07:11:01 +08:00
filters = append ( filters , "a.dashboard_id = 0" )
2022-04-11 20:18:38 +08:00
}
2023-10-06 17:59:48 +08:00
2023-11-29 18:34:44 +08:00
if accessResources . CanAccessDashAnnotations {
2025-06-14 02:59:24 +08:00
if len ( accessResources . Dashboards ) == 0 {
filters = append ( filters , "1=0" ) // empty set
2023-11-15 07:11:01 +08:00
} else {
2025-06-14 02:59:24 +08:00
filters = append ( filters , fmt . Sprintf ( "a.dashboard_uid IN (%s)" , strings . Repeat ( "?," , len ( accessResources . Dashboards ) - 1 ) + "?" ) )
for uid := range accessResources . Dashboards {
params = append ( params , uid )
2023-08-02 15:39:25 +08:00
}
2022-04-11 20:18:38 +08:00
}
}
2023-04-06 16:16:15 +08:00
2025-06-14 02:59:24 +08:00
return strings . Join ( filters , " OR " ) , params
2022-04-11 20:18:38 +08:00
}
2022-09-22 20:27:48 +08:00
func ( r * xormRepositoryImpl ) Delete ( ctx context . Context , params * annotations . DeleteParams ) error {
2022-10-19 21:02:15 +08:00
return r . db . WithTransactionalDbSession ( ctx , func ( sess * db . Session ) error {
2017-10-07 16:31:39 +08:00
var (
2021-12-22 02:04:56 +08:00
sql string
annoTagSQL string
2017-10-07 16:31:39 +08:00
)
2023-02-04 00:23:09 +08:00
r . log . Info ( "delete" , "orgId" , params . OrgID )
if params . ID != 0 {
2020-11-11 13:21:08 +08:00
annoTagSQL = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ? AND org_id = ?)"
2018-06-25 22:02:34 +08:00
sql = "DELETE FROM annotation WHERE id = ? AND org_id = ?"
2021-12-22 02:04:56 +08:00
2023-02-04 00:23:09 +08:00
if _ , err := sess . Exec ( annoTagSQL , params . ID , params . OrgID ) ; err != nil {
2021-12-22 02:04:56 +08:00
return err
}
2023-02-04 00:23:09 +08:00
if _ , err := sess . Exec ( sql , params . ID , params . OrgID ) ; err != nil {
2021-12-22 02:04:56 +08:00
return err
}
2025-06-14 02:59:24 +08:00
} else if params . DashboardUID != "" {
annoTagSQL = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_uid = ? AND panel_id = ? AND org_id = ?)"
sql = "DELETE FROM annotation WHERE dashboard_uid = ? AND panel_id = ? AND org_id = ?"
if _ , err := sess . Exec ( annoTagSQL , params . DashboardUID , params . PanelID , params . OrgID ) ; err != nil {
return err
}
if _ , err := sess . Exec ( sql , params . DashboardUID , params . PanelID , params . OrgID ) ; err != nil {
return err
}
2017-10-07 16:31:39 +08:00
} else {
2020-11-11 13:21:08 +08:00
annoTagSQL = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?)"
2018-06-25 22:02:34 +08:00
sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?"
2019-03-04 23:57:29 +08:00
2025-06-14 02:59:24 +08:00
// nolint: staticcheck
2023-02-04 00:23:09 +08:00
if _ , err := sess . Exec ( annoTagSQL , params . DashboardID , params . PanelID , params . OrgID ) ; err != nil {
2021-12-22 02:04:56 +08:00
return err
}
2019-03-04 23:57:29 +08:00
2025-06-14 02:59:24 +08:00
// nolint: staticcheck
2023-02-04 00:23:09 +08:00
if _ , err := sess . Exec ( sql , params . DashboardID , params . PanelID , params . OrgID ) ; err != nil {
2021-12-22 02:04:56 +08:00
return err
}
2016-10-14 15:33:16 +08:00
}
return nil
} )
}
2021-06-30 19:42:54 +08:00
2024-10-03 15:14:06 +08:00
func ( r * xormRepositoryImpl ) GetTags ( ctx context . Context , query annotations . TagsQuery ) ( annotations . FindTagsResult , error ) {
2022-03-26 01:23:09 +08:00
var items [ ] * annotations . Tag
2022-10-19 21:02:15 +08:00
err := r . db . WithDbSession ( ctx , func ( dbSession * db . Session ) error {
2022-03-26 01:23:09 +08:00
if query . Limit == 0 {
query . Limit = 100
}
2021-06-30 19:42:54 +08:00
2022-03-26 01:23:09 +08:00
var sql bytes . Buffer
params := make ( [ ] interface { } , 0 )
2022-09-19 15:54:37 +08:00
tagKey := ` tag. ` + r . db . GetDialect ( ) . Quote ( "key" )
tagValue := ` tag. ` + r . db . GetDialect ( ) . Quote ( "value" )
2021-06-30 19:42:54 +08:00
2022-03-26 01:23:09 +08:00
sql . WriteString ( `
2021-06-30 19:42:54 +08:00
SELECT
` + tagKey + ` ,
` + tagValue + ` ,
count ( * ) as count
FROM tag
INNER JOIN annotation_tag ON tag . id = annotation_tag . tag_id
2023-04-18 18:27:40 +08:00
INNER JOIN annotation ON annotation . id = annotation_tag . annotation_id
2021-06-30 19:42:54 +08:00
` )
2023-04-18 18:27:40 +08:00
sql . WriteString ( ` WHERE annotation.org_id = ? ` )
2022-03-26 01:23:09 +08:00
params = append ( params , query . OrgID )
2021-06-30 19:42:54 +08:00
2025-05-02 16:23:57 +08:00
sql . WriteString ( ` AND ( ` )
s , p := r . db . GetDialect ( ) . LikeOperator ( tagKey , true , query . Tag , true )
sql . WriteString ( s )
params = append ( params , p )
sql . WriteString ( " OR " )
s , p = r . db . GetDialect ( ) . LikeOperator ( tagValue , true , query . Tag , true )
sql . WriteString ( s )
params = append ( params , p )
sql . WriteString ( ")" )
2021-06-30 19:42:54 +08:00
2022-03-26 01:23:09 +08:00
sql . WriteString ( ` GROUP BY ` + tagKey + ` , ` + tagValue )
sql . WriteString ( ` ORDER BY ` + tagKey + ` , ` + tagValue )
2022-09-19 15:54:37 +08:00
sql . WriteString ( ` ` + r . db . GetDialect ( ) . Limit ( query . Limit ) )
2021-06-30 19:42:54 +08:00
2022-03-26 01:23:09 +08:00
err := dbSession . SQL ( sql . String ( ) , params ... ) . Find ( & items )
return err
} )
if err != nil {
2021-06-30 19:42:54 +08:00
return annotations . FindTagsResult { Tags : [ ] * annotations . TagsDTO { } } , err
}
tags := make ( [ ] * annotations . TagsDTO , 0 )
for _ , item := range items {
tag := item . Key
if len ( item . Value ) > 0 {
tag = item . Key + ":" + item . Value
}
tags = append ( tags , & annotations . TagsDTO {
Tag : tag ,
Count : item . Count ,
} )
}
return annotations . FindTagsResult { Tags : tags } , nil
}
2022-09-22 20:27:48 +08:00
2022-09-23 18:04:41 +08:00
func ( r * xormRepositoryImpl ) validateItem ( item * annotations . Item ) error {
if err := validateTimeRange ( item ) ; err != nil {
return err
}
if err := r . validateTagsLength ( item ) ; err != nil {
return err
}
return nil
}
func ( r * xormRepositoryImpl ) validateTagsLength ( item * annotations . Item ) error {
estimatedTagsLength := 1 // leading: [
for i , t := range item . Tags {
if i == 0 {
estimatedTagsLength += len ( t ) + 2 // quotes
} else {
estimatedTagsLength += len ( t ) + 3 // leading comma and quotes
}
}
estimatedTagsLength += 1 // trailing: ]
2023-11-15 07:11:01 +08:00
if estimatedTagsLength > int ( r . cfg . AnnotationMaximumTagsLength ) {
return annotations . ErrBaseTagLimitExceeded . Errorf ( "tags length (%d) exceeds the maximum allowed (%d): modify the configuration to increase it" , estimatedTagsLength , r . cfg . AnnotationMaximumTagsLength )
2022-09-23 18:04:41 +08:00
}
return nil
}
2022-09-22 20:27:48 +08:00
func ( r * xormRepositoryImpl ) CleanAnnotations ( ctx context . Context , cfg setting . AnnotationCleanupSettings , annotationType string ) ( int64 , error ) {
var totalAffected int64
if cfg . MaxAge > 0 {
2023-11-15 07:11:01 +08:00
cutoffDate := timeNow ( ) . Add ( - cfg . MaxAge ) . UnixNano ( ) / int64 ( time . Millisecond )
2024-01-13 04:05:04 +08:00
// Single-statement approaches, specifically ones using batched sub-queries, seem to deadlock with concurrent inserts on MySQL.
// We have a bounded batch size, so work around this by first loading the IDs into memory and allowing any locks to flush inside each batch.
// This may under-delete when concurrent inserts happen, but any such annotations will simply be cleaned on the next cycle.
//
// We execute the following batched operation repeatedly until either we run out of objects, the context is cancelled, or there is an error.
affected , err := untilDoneOrCancelled ( ctx , func ( ) ( int64 , error ) {
cond := fmt . Sprintf ( ` %s AND created < %v ORDER BY id DESC %s ` , annotationType , cutoffDate , r . db . GetDialect ( ) . Limit ( r . cfg . AnnotationCleanupJobBatchSize ) )
ids , err := r . fetchIDs ( ctx , "annotation" , cond )
if err != nil {
return 0 , err
}
2022-09-22 20:27:48 +08:00
2024-01-17 04:48:11 +08:00
return r . deleteByIDs ( ctx , "annotation" , ids )
2024-01-13 04:05:04 +08:00
} )
2022-09-22 20:27:48 +08:00
totalAffected += affected
if err != nil {
return totalAffected , err
}
}
if cfg . MaxCount > 0 {
2024-01-13 04:05:04 +08:00
// Similar strategy as the above cleanup process, to avoid deadlocks.
affected , err := untilDoneOrCancelled ( ctx , func ( ) ( int64 , error ) {
cond := fmt . Sprintf ( ` %s ORDER BY id DESC %s ` , annotationType , r . db . GetDialect ( ) . LimitOffset ( r . cfg . AnnotationCleanupJobBatchSize , cfg . MaxCount ) )
ids , err := r . fetchIDs ( ctx , "annotation" , cond )
if err != nil {
return 0 , err
}
2024-01-17 04:48:11 +08:00
return r . deleteByIDs ( ctx , "annotation" , ids )
2024-01-13 04:05:04 +08:00
} )
2022-09-22 20:27:48 +08:00
totalAffected += affected
2024-01-13 04:05:04 +08:00
if err != nil {
return totalAffected , err
}
2022-09-22 20:27:48 +08:00
}
return totalAffected , nil
}
func ( r * xormRepositoryImpl ) CleanOrphanedAnnotationTags ( ctx context . Context ) ( int64 , error ) {
2024-01-13 04:05:04 +08:00
return untilDoneOrCancelled ( ctx , func ( ) ( int64 , error ) {
cond := fmt . Sprintf ( ` NOT EXISTS (SELECT 1 FROM annotation a WHERE annotation_id = a.id) %s ` , r . db . GetDialect ( ) . Limit ( r . cfg . AnnotationCleanupJobBatchSize ) )
ids , err := r . fetchIDs ( ctx , "annotation_tag" , cond )
if err != nil {
return 0 , err
}
2024-01-17 04:48:11 +08:00
return r . deleteByIDs ( ctx , "annotation_tag" , ids )
2024-01-13 04:05:04 +08:00
} )
}
func ( r * xormRepositoryImpl ) fetchIDs ( ctx context . Context , table , condition string ) ( [ ] int64 , error ) {
sql := fmt . Sprintf ( ` SELECT id FROM %s ` , table )
if condition == "" {
return nil , fmt . Errorf ( "condition must be supplied; cannot fetch IDs from entire table" )
}
sql += fmt . Sprintf ( ` WHERE %s ` , condition )
ids := make ( [ ] int64 , 0 )
err := r . db . WithDbSession ( ctx , func ( session * db . Session ) error {
return session . SQL ( sql ) . Find ( & ids )
} )
return ids , err
2022-09-22 20:27:48 +08:00
}
2024-01-13 04:05:04 +08:00
func ( r * xormRepositoryImpl ) deleteByIDs ( ctx context . Context , table string , ids [ ] int64 ) ( int64 , error ) {
if len ( ids ) == 0 {
return 0 , nil
}
sql := ""
args := make ( [ ] any , 0 )
// SQLite has a parameter limit of 999.
// If the batch size is bigger than that, and we're on SQLite, we have to put the IDs directly into the statement.
const sqliteParameterLimit = 999
if r . db . GetDBType ( ) == migrator . SQLite && r . cfg . AnnotationCleanupJobBatchSize > sqliteParameterLimit {
values := fmt . Sprint ( ids [ 0 ] )
for _ , v := range ids [ 1 : ] {
values = fmt . Sprintf ( "%s, %d" , values , v )
}
sql = fmt . Sprintf ( ` DELETE FROM %s WHERE id IN (%s) ` , table , values )
} else {
placeholders := "?" + strings . Repeat ( ",?" , len ( ids ) - 1 )
sql = fmt . Sprintf ( ` DELETE FROM %s WHERE id IN (%s) ` , table , placeholders )
args = asAny ( ids )
}
var affected int64
err := r . db . WithDbSession ( ctx , func ( session * db . Session ) error {
res , err := session . Exec ( append ( [ ] any { sql } , args ... ) ... )
if err != nil {
return err
}
affected , err = res . RowsAffected ( )
return err
} )
return affected , err
}
func asAny ( vs [ ] int64 ) [ ] any {
r := make ( [ ] any , len ( vs ) )
for i , v := range vs {
r [ i ] = v
}
return r
}
// untilDoneOrCancelled repeatedly executes batched work until that work is either done (i.e., returns zero affected objects),
// a batch produces an error, or the provided context is cancelled.
// The work to be done is given as a callback that returns the number of affected objects for each batch, plus that batch's errors.
func untilDoneOrCancelled ( ctx context . Context , batchWork func ( ) ( int64 , error ) ) ( int64 , error ) {
2022-09-22 20:27:48 +08:00
var totalAffected int64
for {
select {
case <- ctx . Done ( ) :
return totalAffected , ctx . Err ( )
default :
2024-01-13 04:05:04 +08:00
affected , err := batchWork ( )
totalAffected += affected
2022-09-22 20:27:48 +08:00
if err != nil {
return totalAffected , err
}
if affected == 0 {
return totalAffected , nil
}
}
}
}
2023-07-31 23:19:59 +08:00
type annotationTag struct {
AnnotationID int64 ` xorm:"annotation_id" `
TagID int64 ` xorm:"tag_id" `
}