package service import ( "context" "errors" "strconv" "testing" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/internal" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) const ( dashboardWithNoQueries = ` { "panels": [ { "id": 2, "title": "Panel Title", "type": "timeseries" } ], "schemaVersion": 35 }` dashboardWithTargetsWithNoDatasources = ` { "panels": [ { "id": 2, "datasource": { "type": "postgres", "uid": "abc123" }, "targets": [ { "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }, { "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" } ], "title": "Panel Title", "type": "timeseries" } ], "schemaVersion": 35 }` dashboardWithQueriesExemplarEnabled = ` { "panels": [ { "id": 2, "targets": [ { "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" } ], "title": "Panel Title", "type": "timeseries" } ], "schemaVersion": 35 }` oldStyleDashboard = ` { "panels": [ { "datasource": "_yxMP8Ynk", "id": 2, "targets": [ { "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "title": "Panel Title", "type": "timeseries" } ], "schemaVersion": 21 }` dashboardWithOneHiddenQuery = ` { "panels": [ { "id": 2, "targets": [ { "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A", "hide": true }, { "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" }, { "datasource": { "name": "Expression", "type": "__expr__", "uid": "__expr__" }, "expression": "$A + $B", "refId": "C", "type": "math" } ], "title": "Panel Title", "type": "timeseries" } ], "schemaVersion": 35 }` dashboardWithAllHiddenQueries = ` { "panels": [ { "id": 2, "targets": [ { "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A", "hide": true }, { "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B", "hide": true }, { "datasource": { "name": "Expression", "type": "__expr__", "uid": "__expr__" }, "expression": "$A + $B", "refId": "C", "type": "math" } ], "title": "Panel Title", "type": "timeseries" } ], "schemaVersion": 35 }` dashboardWithRowsAndOneHiddenQuery = ` { "panels": [ { "id": 2, "targets": [ { "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "hide": true, "refId": "B" } ], "title": "Panel Title", "type": "timeseries" }, { "id": 3, "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, "title": "This panel is a Row", "type": "row", "panels": [ { "id": 4, "targets": [ { "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" } ], "title": "Panel inside a row", "type": "timeseries" } ] } ], "schemaVersion": 35 }` ) func TestGetQueryDataResponse(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, _ := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) fakeQueryService := &query.FakeQueryService{} fakeQueryService.On("QueryData", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&backend.QueryDataResponse{}, nil) service.QueryDataService = fakeQueryService dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, service.cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) publicDashboardQueryDTO := PublicDashboardQueryDTO{ IntervalMs: int64(1), MaxDataPoints: int64(1), } t.Run("Returns query data even when the query is hidden", func(t *testing.T) { hiddenQuery := map[string]interface{}{ "datasource": map[string]interface{}{ "name": "Expression", "type": "__expr__", "uid": "__expr__", }, "hide": true, "refId": "A", } customPanels := []interface{}{ map[string]interface{}{ "id": 1, "datasource": map[string]interface{}{ "uid": "ds1", }, "targets": []interface{}{hiddenQuery}, }} dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, "", true, []map[string]interface{}{}, customPanels) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, OrgID: dashboard.OrgID, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } pubdashDto, err := service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) resp, _ := service.GetQueryDataResponse(context.Background(), true, publicDashboardQueryDTO, 1, pubdashDto.AccessToken) require.NotNil(t, resp) }) } func TestFindAnnotations(t *testing.T) { color := "red" name := "annoName" t.Run("service identity has correct permissions to get annotations dashboards and query datasources", func(t *testing.T) { fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboards.NewDashboard("dash1"), nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, nil) reqDTO := AnnotationsQueryDTO{ From: 1, To: 2, } dash := dashboards.NewDashboard("testDashboard") items, err := service.FindAnnotations(context.Background(), reqDTO, "abc123") require.NoError(t, err) assert.Len(t, items, 0) _, svcIdent := identity.WithServiceIdentity(context.Background(), dash.OrgID) require.Equal(t, "*", svcIdent.GetPermissions()["datasources:query"][0]) require.Equal(t, "*", svcIdent.GetPermissions()["dashboards:read"][0]) require.Equal(t, "*", svcIdent.GetPermissions()["annotations:read"][0]) }) t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, Name: name, IconColor: color, Target: &dashboard2.AnnotationTarget{ Limit: 100, MatchAny: false, Tags: nil, Type: "dashboard", }, Type: util.Pointer("dashboard"), } grafanaTagAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, Name: name, IconColor: color, Target: &dashboard2.AnnotationTarget{ Limit: 100, MatchAny: false, Tags: []string{"tag1"}, Type: "tags", }, } annos := []DashAnnotation{grafanaAnnotation, grafanaTagAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} annotationsRepo := &annotations.FakeAnnotationsRepo{} fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, annotationsRepo) annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { ID: 1, DashboardID: 1, PanelID: 1, Tags: []string{"tag1"}, TimeEnd: 2, Time: 2, Text: "text", }, }, nil).Maybe() items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") expected := AnnotationEvent{ Id: 1, DashboardId: 1, PanelId: 0, Tags: []string{"tag1"}, IsRegion: false, Text: "text", Color: color, Time: 2, TimeEnd: 2, Source: grafanaTagAnnotation, } require.NoError(t, err) assert.Len(t, items, 1) assert.Equal(t, expected, items[0]) }) t.Run("Test panelId set to zero when annotation event is for a tags query", func(t *testing.T) { dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, Name: name, IconColor: color, Target: &dashboard2.AnnotationTarget{ Limit: 100, MatchAny: false, Tags: []string{"tag1"}, Type: "tags", }, } annos := []DashAnnotation{grafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) annotationsRepo := &annotations.FakeAnnotationsRepo{} fakeStore := &FakePublicDashboardStore{} pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, annotationsRepo) annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { ID: 1, DashboardID: 1, PanelID: 1, Tags: []string{}, TimeEnd: 1, Time: 2, Text: "text", }, }, nil).Maybe() items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") expected := AnnotationEvent{ Id: 1, DashboardId: 1, PanelId: 0, Tags: []string{}, IsRegion: true, Text: "text", Color: color, Time: 2, TimeEnd: 1, Source: grafanaAnnotation, } require.NoError(t, err) assert.Len(t, items, 1) assert.Equal(t, expected, items[0]) }) t.Run("Test can get grafana annotations and will skip annotation queries and disabled annotations", func(t *testing.T) { dash := dashboards.NewDashboard("test") disabledGrafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: false, Name: name, IconColor: color, } grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, Name: name, IconColor: color, Target: &dashboard2.AnnotationTarget{ Limit: 100, MatchAny: true, Tags: nil, Type: "dashboard", }, Type: util.Pointer("dashboard"), } queryAnnotation := DashAnnotation{ Datasource: CreateDatasource("prometheus", "abc123"), Enable: true, Name: name, } annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) annotationsRepo := &annotations.FakeAnnotationsRepo{} pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, annotationsRepo) annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { ID: 1, DashboardID: 1, PanelID: 1, Tags: []string{}, TimeEnd: 1, Time: 2, Text: "text", }, }, nil).Maybe() items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") expected := AnnotationEvent{ Id: 1, DashboardId: 1, PanelId: 1, Tags: []string{}, IsRegion: true, Text: "text", Color: color, Time: 2, TimeEnd: 1, Source: grafanaAnnotation, } require.NoError(t, err) assert.Len(t, items, 1) assert.Equal(t, expected, items[0]) }) t.Run("test will return nothing when dashboard has no annotations", func(t *testing.T) { dashboard := dashboards.NewDashboard("dashWithNoAnnotations") pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, nil) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") require.NoError(t, err) assert.Empty(t, items) }) t.Run("test will return nothing when pubdash annotations are disabled", func(t *testing.T) { dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, Name: name, IconColor: color, Target: &dashboard2.AnnotationTarget{ Limit: 100, MatchAny: false, Tags: nil, Type: "dashboard", }, Type: util.Pointer("dashboard"), } annos := []DashAnnotation{grafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: false} fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, nil) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") require.NoError(t, err) assert.Empty(t, items) }) t.Run("test will error when annotations repo returns an error", func(t *testing.T) { grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, Name: name, IconColor: color, Target: &dashboard2.AnnotationTarget{ Limit: 100, MatchAny: false, Tags: []string{"tag1"}, Type: "tags", }, } dash := dashboards.NewDashboard("test") annotationsRepo := &annotations.FakeAnnotationsRepo{} annotationsRepo.On("Find", mock.Anything, mock.Anything).Return(nil, errors.New("failed")).Maybe() annos := []DashAnnotation{grafanaAnnotation} dash = AddAnnotationsToDashboard(t, dash, annos) pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.UID, AnnotationsEnabled: true} fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, annotationsRepo) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") require.Error(t, err) require.Nil(t, items) }) t.Run("Test find annotations does not panics when Target in datasource is nil", func(t *testing.T) { dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, Name: name, IconColor: color, Type: util.Pointer("dashboard"), Target: nil, } annos := []DashAnnotation{grafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) annotationsRepo := &annotations.FakeAnnotationsRepo{} annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { ID: 1, DashboardID: 1, PanelID: 1, Tags: []string{"tag1"}, TimeEnd: 2, Time: 2, Text: "this is an annotation", }, }, nil).Maybe() service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, annotationsRepo) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") expected := AnnotationEvent{ Id: 1, DashboardId: 1, PanelId: 1, Tags: []string{"tag1"}, IsRegion: false, Text: "this is an annotation", Color: color, Time: 2, TimeEnd: 2, Source: grafanaAnnotation, } require.NoError(t, err) assert.Len(t, items, 1) assert.Equal(t, expected, items[0]) }) } func TestGetMetricRequest(t *testing.T) { service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, nil, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) publicDashboard := &PublicDashboard{ Uid: "1", DashboardUid: dashboard.UID, IsEnabled: true, AccessToken: "abc123", } t.Run("will return an error when validation fails", func(t *testing.T) { publicDashboardQueryDTO := PublicDashboardQueryDTO{ IntervalMs: int64(-1), MaxDataPoints: int64(-1), } _, err := service.GetMetricRequest(context.Background(), dashboard, publicDashboard, 1, publicDashboardQueryDTO) require.Error(t, err) }) t.Run("will not return an error when validation succeeds", func(t *testing.T) { publicDashboardQueryDTO := PublicDashboardQueryDTO{ IntervalMs: int64(1), MaxDataPoints: int64(1), } from, to := internal.GetTimeRangeFromDashboard(t, dashboard.Data) metricReq, err := service.GetMetricRequest(context.Background(), dashboard, publicDashboard, 1, publicDashboardQueryDTO) require.NoError(t, err) require.Equal(t, from, metricReq.From) require.Equal(t, to, metricReq.To) }) } func TestBuildMetricRequest(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, "", true, []map[string]interface{}{}, nil) from, to := internal.GetTimeRangeFromDashboard(t, publicDashboard.Data) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(publicDashboard, nil) publicDashboardQueryDTO := PublicDashboardQueryDTO{ IntervalMs: int64(10000000), MaxDataPoints: int64(200), } isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: publicDashboard.UID, OrgID: 9999999, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } publicDashboardPD, err := service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) isEnabled = false nonPublicDto := &SavePublicDashboardDTO{ DashboardUid: nonPublicDashboard.UID, OrgID: 9999999, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } _, err = service.Create(context.Background(), SignedInUser, nonPublicDto) require.NoError(t, err) t.Run("extracts queries from provided dashboard", func(t *testing.T) { reqDTO, err := service.buildMetricRequest( publicDashboard, publicDashboardPD, 1, publicDashboardQueryDTO, ) require.NoError(t, err) require.Equal(t, from, reqDTO.From) require.Equal(t, to, reqDTO.To) for i := range reqDTO.Queries { require.Equal(t, publicDashboardQueryDTO.IntervalMs, reqDTO.Queries[i].Get("intervalMs").MustInt64()) require.Equal(t, publicDashboardQueryDTO.MaxDataPoints, reqDTO.Queries[i].Get("maxDataPoints").MustInt64()) } require.Len(t, reqDTO.Queries, 2) require.Equal( t, simplejson.NewFromAny(map[string]interface{}{ "datasource": map[string]interface{}{ "type": "mysql", "uid": "ds1", }, "intervalMs": int64(10000000), "maxDataPoints": int64(200), "queryCachingTTL": int64(0), "refId": "A", }), reqDTO.Queries[0], ) require.Equal( t, simplejson.NewFromAny(map[string]interface{}{ "datasource": map[string]interface{}{ "type": "prometheus", "uid": "ds2", }, "intervalMs": int64(10000000), "maxDataPoints": int64(200), "queryCachingTTL": int64(0), "refId": "B", }), reqDTO.Queries[1], ) }) t.Run("returns an error when panel missing", func(t *testing.T) { _, err := service.buildMetricRequest( publicDashboard, publicDashboardPD, 49, publicDashboardQueryDTO, ) require.ErrorContains(t, err, ErrPanelNotFound.Error()) }) t.Run("metric request built with hidden query", func(t *testing.T) { hiddenQuery := map[string]interface{}{ "datasource": map[string]interface{}{ "type": "mysql", "uid": "ds1", }, "hide": true, "refId": "A", } nonHiddenQuery := map[string]interface{}{ "datasource": map[string]interface{}{ "type": "prometheus", "uid": "ds2", }, "refId": "B", } customPanels := []interface{}{ map[string]interface{}{ "id": 1, "datasource": map[string]interface{}{ "uid": "ds1", }, "targets": []interface{}{hiddenQuery, nonHiddenQuery}, }} publicDashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, "", true, []map[string]interface{}{}, customPanels) reqDTO, err := service.buildMetricRequest( publicDashboard, publicDashboardPD, 1, publicDashboardQueryDTO, ) require.NoError(t, err) require.Equal(t, from, reqDTO.From) require.Equal(t, to, reqDTO.To) for i := range reqDTO.Queries { require.Equal(t, publicDashboardQueryDTO.IntervalMs, reqDTO.Queries[i].Get("intervalMs").MustInt64()) require.Equal(t, publicDashboardQueryDTO.MaxDataPoints, reqDTO.Queries[i].Get("maxDataPoints").MustInt64()) } require.Len(t, reqDTO.Queries, 1) require.Equal( t, simplejson.NewFromAny(nonHiddenQuery), reqDTO.Queries[0], ) }) } func TestGroupQueriesByPanelId(t *testing.T) { t.Run("can extract queries from dashboard with panel datasource string that has no datasource on panel targets", func(t *testing.T) { json, err := simplejson.NewJson([]byte(oldStyleDashboard)) require.NoError(t, err) queries := groupQueriesByPanelId(json) panelId := int64(2) queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) require.Len(t, queriesByDatasource[0], 1) }) t.Run("will delete exemplar property from target if exists", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithQueriesExemplarEnabled)) require.NoError(t, err) queries := groupQueriesByPanelId(json) panelId := int64(2) queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) for _, query := range queriesByDatasource[0] { _, ok := query.CheckGet("exemplar") require.False(t, ok) } }) t.Run("can extract queries from dashboard with panel json datasource that has no datasource on panel targets", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithTargetsWithNoDatasources)) require.NoError(t, err) queries := groupQueriesByPanelId(json) panelId := int64(2) queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) require.Len(t, queriesByDatasource[0], 2) }) t.Run("can extract no queries from empty dashboard", func(t *testing.T) { json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) require.NoError(t, err) queries := groupQueriesByPanelId(json) require.Len(t, queries, 0) }) t.Run("can extract no queries from empty panel", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithNoQueries)) require.NoError(t, err) queries := groupQueriesByPanelId(json) require.Len(t, queries, 1) require.Contains(t, queries, int64(2)) require.Len(t, queries[2], 0) }) t.Run("can extract queries from panels", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithQueriesExemplarEnabled)) require.NoError(t, err) queries := groupQueriesByPanelId(json) require.Len(t, queries, 1) require.Contains(t, queries, int64(2)) require.Len(t, queries[2], 2) query, err := queries[2][0].MarshalJSON() require.NoError(t, err) require.JSONEq(t, `{ "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }`, string(query)) query, err = queries[2][1].MarshalJSON() require.NoError(t, err) require.JSONEq(t, `{ "datasource": { "type": "prometheus", "uid": "promds2" }, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" }`, string(query)) }) t.Run("can extract queries from old-style panels", func(t *testing.T) { json, err := simplejson.NewJson([]byte(oldStyleDashboard)) require.NoError(t, err) queries := groupQueriesByPanelId(json) require.Len(t, queries, 1) require.Contains(t, queries, int64(2)) require.Len(t, queries[2], 1) query, err := queries[2][0].MarshalJSON() require.NoError(t, err) require.JSONEq(t, `{ "datasource": { "uid": "_yxMP8Ynk", "type": "public-ds" }, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }`, string(query)) }) t.Run("hidden queries in a panel with an expression not filtered", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithOneHiddenQuery)) require.NoError(t, err) queries := groupQueriesByPanelId(json)[2] require.Len(t, queries, 3) }) t.Run("all hidden queries in a panel with an expression not filtered", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithAllHiddenQueries)) require.NoError(t, err) queries := groupQueriesByPanelId(json)[2] require.Len(t, queries, 3) }) t.Run("queries inside panels inside rows are returned", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithRowsAndOneHiddenQuery)) require.NoError(t, err) queries := groupQueriesByPanelId(json) for idx := range queries { assert.NotNil(t, queries[idx]) } assert.Len(t, queries, 2) }) t.Run("hidden queries are not returned", func(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithRowsAndOneHiddenQuery)) require.NoError(t, err) queries := groupQueriesByPanelId(json) var totalQueries int for idx := range queries { totalQueries += len(queries[idx]) assert.NotNil(t, queries[idx]) } assert.Equal(t, 3, totalQueries) }) } func TestGroupQueriesByDataSource(t *testing.T) { t.Run("can divide queries by datasource", func(t *testing.T) { queries := []*simplejson.Json{ simplejson.MustJson([]byte(`{ "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }`)), simplejson.MustJson([]byte(`{ "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" }`)), } queriesByDatasource := groupQueriesByDataSource(t, queries) require.Len(t, queriesByDatasource, 2) require.Contains(t, queriesByDatasource, []*simplejson.Json{simplejson.MustJson([]byte(`{ "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }`))}) require.Contains(t, queriesByDatasource, []*simplejson.Json{simplejson.MustJson([]byte(`{ "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" }`))}) }) } func TestSanitizeMetadataFromQueryData(t *testing.T) { t.Run("can remove ExecutedQueryString from metadata", func(t *testing.T) { fakeResponse := &backend.QueryDataResponse{ Responses: backend.Responses{ "A": backend.DataResponse{ Frames: data.Frames{ &data.Frame{ Name: "1", Meta: &data.FrameMeta{ ExecutedQueryString: "Test1", Custom: map[string]string{ "test1": "test1", }, }, }, &data.Frame{ Name: "2", Meta: &data.FrameMeta{ ExecutedQueryString: "Test2", Custom: map[string]string{ "test2": "test2", }, }, }, }, }, "B": backend.DataResponse{ Frames: data.Frames{ &data.Frame{ Name: "3", Meta: &data.FrameMeta{ ExecutedQueryString: "Test3", }, }, }, }, }, } sanitizeMetadataFromQueryData(fakeResponse) assert.Equal(t, fakeResponse.Responses["A"].Frames[0].Meta.ExecutedQueryString, "") assert.Equal(t, fakeResponse.Responses["A"].Frames[0].Meta.Custom, map[string]string{"test1": "test1"}) assert.Equal(t, fakeResponse.Responses["A"].Frames[1].Meta.ExecutedQueryString, "") assert.Equal(t, fakeResponse.Responses["A"].Frames[1].Meta.Custom, map[string]string{"test2": "test2"}) assert.Equal(t, fakeResponse.Responses["B"].Frames[0].Meta.ExecutedQueryString, "") assert.Nil(t, fakeResponse.Responses["B"].Frames[0].Meta.Custom) }) } func TestBuildTimeSettings(t *testing.T) { var defaultDashboardData = simplejson.NewFromAny(map[string]interface{}{ "time": map[string]interface{}{ "from": "2022-09-01T00:00:00.000Z", "to": "2022-09-01T12:00:00.000Z", }, "timezone": "America/Argentina/Mendoza", }) defaultFromMs, defaultToMs := internal.GetTimeRangeFromDashboard(t, defaultDashboardData) dashboardDataWithPanelRelativeTime, err := simplejson.NewJson([]byte(` { "panels": [ {"id": 1, "timeFrom": "now-1d/d"} ], "time": { "from": "now-6h", "to": "now" }, "timezone": "Europe/Madrid" }`)) require.NoError(t, err) fakeTimezone, _ := time.LoadLocation("Europe/Madrid") fakeNow := time.Date(2018, 12, 9, 20, 30, 0, 0, fakeTimezone) // stub time range construction to have a fixed time.Now and be able to tests relative time ranges NewTimeRange = func(from, to string) gtime.TimeRange { return gtime.TimeRange{ From: from, To: to, Now: fakeNow, } } startOfYesterdayMadrid, endOfYesterdayMadrid := getStartAndEndOfTheDayBefore(fakeNow, "Europe/Madrid") // the day before fakeNow in Australia/Sydney timezone is not the same day before as in Europe/Madrid startOfYesterdaySydney, endOfYesterdaySydney := getStartAndEndOfTheDayBefore(fakeNow, "Australia/Sydney") startOfYesterdayUTC, endOfYesterdayUTC := getStartAndEndOfTheDayBefore(fakeNow, "UTC") selectionFromMs := strconv.FormatInt(time.Now().UnixMilli(), 10) selectionToMs := strconv.FormatInt(time.Now().Add(time.Hour).UnixMilli(), 10) testCases := []struct { name string dashboard *dashboards.Dashboard pubdash *PublicDashboard reqDTO PublicDashboardQueryDTO panelID int64 want TimeSettings }{ { name: "should return default time range with timezone with relative time range", dashboard: &dashboards.Dashboard{Data: buildJsonDataWithTimeRange("now-1d/d", "now-1d/d", "Australia/Sydney")}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{}, want: TimeSettings{ From: strconv.FormatInt(startOfYesterdaySydney.UnixMilli(), 10), To: strconv.FormatInt(endOfYesterdaySydney.UnixMilli(), 10), }, }, { name: "should return default time range with UTC timezone with relative time range with unknown timezone", dashboard: &dashboards.Dashboard{Data: buildJsonDataWithTimeRange("now-1d/d", "now-1d/d", "browser")}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{}, want: TimeSettings{ From: strconv.FormatInt(startOfYesterdayUTC.UnixMilli(), 10), To: strconv.FormatInt(endOfYesterdayUTC.UnixMilli(), 10), }, }, { name: "should return default time range with timezone with relative time range if time selection is not enabled", dashboard: &dashboards.Dashboard{Data: buildJsonDataWithTimeRange("now-1d/d", "now-1d/d", "Australia/Sydney")}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeRangeDTO{ Timezone: "Europe/Madrid", }}, want: TimeSettings{ From: strconv.FormatInt(startOfYesterdaySydney.UnixMilli(), 10), To: strconv.FormatInt(endOfYesterdaySydney.UnixMilli(), 10), }, }, { name: "should return user time range with dashboard timezone with relative time range", dashboard: &dashboards.Dashboard{Data: buildJsonDataWithTimeRange("now-1d/d", "now-1d/d", "Europe/Madrid")}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{}, want: TimeSettings{ From: strconv.FormatInt(startOfYesterdayMadrid.UnixMilli(), 10), To: strconv.FormatInt(endOfYesterdayMadrid.UnixMilli(), 10), }, }, { name: "should return user time range with dashboard timezone with relative time range for the last hour", dashboard: &dashboards.Dashboard{Data: buildJsonDataWithTimeRange("now-1h", "now", "Europe/Madrid")}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{}, want: TimeSettings{ From: strconv.FormatInt(fakeNow.Add(-time.Hour).UnixMilli(), 10), To: strconv.FormatInt(fakeNow.UnixMilli(), 10), }, }, { name: "should use dashboard time if pubdash time empty", dashboard: &dashboards.Dashboard{Data: defaultDashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{}, want: TimeSettings{ From: defaultFromMs, To: defaultToMs, }, }, { name: "should use dashboard time when time selection is disabled", dashboard: &dashboards.Dashboard{Data: defaultDashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeRangeDTO{ From: selectionFromMs, To: selectionToMs, }, }, want: TimeSettings{ From: defaultFromMs, To: defaultToMs, }, }, { name: "should use selected values if time selection is enabled", dashboard: &dashboards.Dashboard{Data: defaultDashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: true}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeRangeDTO{ From: selectionFromMs, To: selectionToMs, }, }, want: TimeSettings{ From: selectionFromMs, To: selectionToMs, }, }, { name: "should use default values if time selection is enabled but the time range is empty", dashboard: &dashboards.Dashboard{Data: defaultDashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: true}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeRangeDTO{}, }, want: TimeSettings{ From: defaultFromMs, To: defaultToMs, }, }, { name: "should use panel relative time when time selection is disabled", dashboard: &dashboards.Dashboard{Data: dashboardDataWithPanelRelativeTime}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeRangeDTO{ From: selectionFromMs, To: selectionToMs, }, }, panelID: 1, want: TimeSettings{ From: strconv.FormatInt(startOfYesterdayMadrid.UnixMilli(), 10), To: strconv.FormatInt(fakeNow.UnixMilli(), 10), }, }, { name: "should use selected values if time selection is enabled for panels with relative time set", dashboard: &dashboards.Dashboard{Data: dashboardDataWithPanelRelativeTime}, pubdash: &PublicDashboard{TimeSelectionEnabled: true}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeRangeDTO{ From: selectionFromMs, To: selectionToMs, }, }, panelID: 1, want: TimeSettings{ From: selectionFromMs, To: selectionToMs, }, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { assert.Equal(t, test.want, buildTimeSettings(test.dashboard, test.reqDTO, test.pubdash, test.panelID)) }) } } func groupQueriesByDataSource(t *testing.T, queries []*simplejson.Json) (result [][]*simplejson.Json) { t.Helper() byDataSource := make(map[string][]*simplejson.Json) for _, query := range queries { uid := getDataSourceUidFromJson(query) byDataSource[uid] = append(byDataSource[uid], query) } for _, queries := range byDataSource { result = append(result, queries) } return } func getStartAndEndOfTheDayBefore(fakeNow time.Time, timezoneName string) (time.Time, time.Time) { timezone, _ := time.LoadLocation(timezoneName) fakeNowWithTimezone := fakeNow.In(timezone) yy, mm, dd := fakeNowWithTimezone.Add(-24 * time.Hour).Date() startOfYesterdaySydney := time.Date(yy, mm, dd, 0, 0, 0, 0, timezone) endOfYesterdaySydney := time.Date(yy, mm, dd, 23, 59, 59, 999999999, timezone) return startOfYesterdaySydney, endOfYesterdaySydney } func buildJsonDataWithTimeRange(from, to, timezone string) *simplejson.Json { return simplejson.NewFromAny(map[string]interface{}{ "time": map[string]interface{}{ "from": from, "to": to, }, "timezone": timezone, }) }