2025-04-01 10:38:02 +09:00

1394 lines
42 KiB
Go

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,
})
}