package service import ( "context" "strconv" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/tsdb/grafanads" ) // FindAnnotations returns annotations for a public dashboard func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDTO models.AnnotationsQueryDTO, accessToken string) ([]models.AnnotationEvent, error) { pub, dash, err := pd.FindEnabledPublicDashboardAndDashboardByAccessToken(ctx, accessToken) if err != nil { return nil, err } if !pub.AnnotationsEnabled { return []models.AnnotationEvent{}, nil } annoDto, err := UnmarshalDashboardAnnotations(dash.Data) if err != nil { return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to unmarshal dashboard annotations: %w", err) } // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the annotations. svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dash.OrgID) uniqueEvents := make(map[int64]models.AnnotationEvent, 0) for _, anno := range annoDto.Annotations.List { // skip annotations that are not enabled or are not a grafana datasource if !anno.Enable || (*anno.Datasource.Uid != grafanads.DatasourceUID && *anno.Datasource.Uid != grafanads.DatasourceName) { continue } annoQuery := &annotations.ItemQuery{ From: reqDTO.From, To: reqDTO.To, OrgID: dash.OrgID, DashboardID: dash.ID, DashboardUID: dash.UID, SignedInUser: svcIdent, } if anno.Target != nil { annoQuery.Limit = anno.Target.Limit annoQuery.MatchAny = anno.Target.MatchAny if anno.Target.Type == "tags" { annoQuery.DashboardID = 0 // nolint: staticcheck annoQuery.DashboardUID = "" annoQuery.Tags = anno.Target.Tags } } annotationItems, err := pd.AnnotationsRepo.Find(svcCtx, annoQuery) if err != nil { return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to find annotations: %w", err) } for _, item := range annotationItems { event := models.AnnotationEvent{ Id: item.ID, DashboardId: item.DashboardID, // nolint: staticcheck Tags: item.Tags, IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd, Text: item.Text, Color: anno.IconColor, Time: item.Time, TimeEnd: item.TimeEnd, Source: anno, } if item.DashboardUID != nil { event.DashboardUID = *item.DashboardUID } // We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels // which is only intended for tag and org annotations. if anno.Type != nil && *anno.Type == "dashboard" { event.PanelId = item.PanelID } // We want events from tag queries to overwrite existing events _, has := uniqueEvents[event.Id] if !has || (has && anno.Target != nil && anno.Target.Type == "tags") { uniqueEvents[event.Id] = event } } } results := make([]models.AnnotationEvent, 0, len(uniqueEvents)) for _, result := range uniqueEvents { results = append(results, result) } return results, nil } // GetMetricRequest returns a metric request for the given panel and query func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { err := validation.ValidateQueryPublicDashboardRequest(queryDto, publicDashboard) if err != nil { return dtos.MetricRequest{}, err } metricReqDTO, err := pd.buildMetricRequest( dashboard, publicDashboard, panelId, queryDto, ) if err != nil { return dtos.MetricRequest{}, err } return metricReqDTO, nil } // GetQueryDataResponse returns a query data response for the given panel and query func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, skipDSCache bool, queryDto models.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) { publicDashboard, dashboard, err := pd.FindEnabledPublicDashboardAndDashboardByAccessToken(ctx, accessToken) if err != nil { return nil, err } metricReq, err := pd.GetMetricRequest(ctx, dashboard, publicDashboard, panelId, queryDto) if err != nil { return nil, err } if len(metricReq.Queries) == 0 { return nil, models.ErrPanelQueriesNotFound.Errorf("GetQueryDataResponse: failed to extract queries from panel") } // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the datasource. svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dashboard.OrgID) res, err := pd.QueryDataService.QueryData(svcCtx, svcIdent, skipDSCache, metricReq) reqDatasources := metricReq.GetUniqueDatasourceTypes() if err != nil { LogQueryFailure(reqDatasources, pd.log, err) return nil, err } LogQuerySuccess(reqDatasources, pd.log) sanitizeMetadataFromQueryData(res) return res, nil } // buildMetricRequest merges public dashboard parameters with dashboard and returns a metrics request to be sent to query backend func (pd *PublicDashboardServiceImpl) buildMetricRequest(dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelID int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { isV2 := dashboard.Data.Get("elements").Interface() != nil if isV2 { return pd.buildMetricRequestV2(dashboard, publicDashboard, panelID, reqDTO) } // group queries by panel queriesByPanel := groupQueriesByPanelId(dashboard.Data) queries, ok := queriesByPanel[panelID] if !ok { return dtos.MetricRequest{}, models.ErrPanelNotFound.Errorf("buildMetricRequest: public dashboard panel not found") } ts := buildTimeSettings(dashboard, reqDTO, publicDashboard, panelID) // determine safe resolution to query data at safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts) for i := range queries { queries[i].Set("intervalMs", safeInterval) queries[i].Set("maxDataPoints", safeResolution) queries[i].Set("queryCachingTTL", reqDTO.QueryCachingTTL) } return dtos.MetricRequest{ From: ts.From, To: ts.To, Queries: queries, }, nil } func (pd *PublicDashboardServiceImpl) buildMetricRequestV2(dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelID int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { // group queries by panel for V2 queriesByPanel := groupQueriesByPanelIdV2(dashboard.Data) queries, ok := queriesByPanel[panelID] if !ok { return dtos.MetricRequest{}, models.ErrPanelNotFound.Errorf("buildMetricRequestV2: public dashboard panel not found") } ts := buildTimeSettingsV2(dashboard, reqDTO, publicDashboard, panelID) // determine safe resolution to query data at safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts) for i := range queries { queries[i].Set("intervalMs", safeInterval) queries[i].Set("maxDataPoints", safeResolution) queries[i].Set("queryCachingTTL", reqDTO.QueryCachingTTL) } return dtos.MetricRequest{ From: ts.From, To: ts.To, Queries: queries, }, nil } func groupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json { result := make(map[int64][]*simplejson.Json) extractQueriesFromPanels(dashboard.Get("panels").MustArray(), result) return result } func groupQueriesByPanelIdV2(dashboard *simplejson.Json) map[int64][]*simplejson.Json { result := make(map[int64][]*simplejson.Json) elementsMap := dashboard.Get("elements").MustMap() for _, element := range elementsMap { element := simplejson.NewFromAny(element) var panelQueries []*simplejson.Json hasExpression := panelHasAnExpressionSchemaV2(element) // For schema v2, queries are nested in element.spec.data.spec.queries spec := element.Get("spec") if spec.Interface() == nil { result[element.Get("spec").Get("id").MustInt64()] = panelQueries continue } data := spec.Get("data") if data.Interface() == nil { result[element.Get("spec").Get("id").MustInt64()] = panelQueries continue } dataSpec := data.Get("spec") if dataSpec.Interface() == nil { result[element.Get("spec").Get("id").MustInt64()] = panelQueries continue } queries := dataSpec.Get("queries") if queries.Interface() == nil { result[element.Get("spec").Get("id").MustInt64()] = panelQueries continue } for _, queryObj := range queries.MustArray() { query := simplejson.NewFromAny(queryObj) // Check if query is hidden (PanelQuery.spec.hidden) panelQuerySpec := query.Get("spec") if panelQuerySpec.Interface() == nil { continue } if !hasExpression && panelQuerySpec.Get("hidden").MustBool() { continue } // Extract the actual query from PanelQuery.spec.query dataQueryKind := panelQuerySpec.Get("query") if dataQueryKind.Interface() == nil { continue } dataQuerySpec := dataQueryKind.Get("spec") if dataQuerySpec.Interface() == nil { continue } dataQuerySpec.Del("exemplar") group := dataQueryKind.Get("group").MustString() // if query target has no datasource, set it to have the datasource on the panel if _, ok := dataQuerySpec.CheckGet("datasource"); !ok { uid := getDataSourceUidFromJsonSchemaV2(dataQueryKind) datasource := map[string]any{"type": group, "uid": uid} dataQuerySpec.Set("datasource", datasource) } // We don't support exemplars for public dashboards currently dataQuerySpec.Del("exemplar") // The query object contains the DataQuery with the actual expression panelQueries = append(panelQueries, dataQuerySpec) } result[element.Get("spec").Get("id").MustInt64()] = panelQueries } return result } func extractQueriesFromPanels(panels []any, result map[int64][]*simplejson.Json) { for _, panelObj := range panels { panel := simplejson.NewFromAny(panelObj) // if the panel is a row and it is collapsed, get the queries from the panels inside the row if panel.Get("type").MustString() == "row" && panel.Get("collapsed").MustBool() { // recursive call to get queries from panels inside a row extractQueriesFromPanels(panel.Get("panels").MustArray(), result) continue } var panelQueries []*simplejson.Json hasExpression := panelHasAnExpression(panel) for _, queryObj := range panel.Get("targets").MustArray() { query := simplejson.NewFromAny(queryObj) // it the panel doesn't have an expression and the query is disabled (hide is true), skip the query // the expression handler will take care later of removing hidden queries which could be necessary to calculate // the value of other queries if !hasExpression && query.Get("hide").MustBool() { continue } // We don't support exemplars for public dashboards currently query.Del("exemplar") // if query target has no datasource, set it to have the datasource on the panel if _, ok := query.CheckGet("datasource"); !ok { uid := getDataSourceUidFromJson(panel) datasource := map[string]any{"type": "public-ds", "uid": uid} query.Set("datasource", datasource) } panelQueries = append(panelQueries, query) } result[panel.Get("id").MustInt64()] = panelQueries } } func panelHasAnExpression(panel *simplejson.Json) bool { var hasExpression bool for _, queryObj := range panel.Get("targets").MustArray() { query := simplejson.NewFromAny(queryObj) if expr.NodeTypeFromDatasourceUID(getDataSourceUidFromJson(query)) == expr.TypeCMDNode { hasExpression = true } } return hasExpression } func panelHasAnExpressionSchemaV2(panel *simplejson.Json) bool { var hasExpression bool // For schema v2, check the nested structure: spec.data.spec.queries[].spec.query spec := panel.Get("spec") if spec.Interface() == nil { return hasExpression } data := spec.Get("data") if data.Interface() == nil { return hasExpression } dataSpec := data.Get("spec") if dataSpec.Interface() == nil { return hasExpression } queries := dataSpec.Get("queries") if queries.Interface() == nil { return hasExpression } for _, queryObj := range queries.MustArray() { query := simplejson.NewFromAny(queryObj) // Navigate to the actual query object querySpec := query.Get("spec") if querySpec.Interface() == nil { continue } queryData := querySpec.Get("query") if queryData.Interface() == nil { continue } // Check if this query is an expression if expr.NodeTypeFromDatasourceUID(getDataSourceUidFromJsonSchemaV2(queryData)) == expr.TypeCMDNode { hasExpression = true break } } return hasExpression } func getDataSourceUidFromJson(query *simplejson.Json) string { uid := query.Get("datasource").Get("uid").MustString() // before 8.3 special types could be sent as datasource (expr) if uid == "" { uid = query.Get("datasource").MustString() } return uid } func getDataSourceUidFromJsonSchemaV2(query *simplejson.Json) string { // For schema v2, datasource info is in query.datasource uid := query.Get("datasource").Get("name").MustString() // before 8.3 special types could be sent as datasource (expr) if uid == "" { uid = query.Get("datasource").MustString() } return uid } func sanitizeMetadataFromQueryData(res *backend.QueryDataResponse) { for k := range res.Responses { frames := res.Responses[k].Frames for i := range frames { if frames[i].Meta != nil { frames[i].Meta.ExecutedQueryString = "" } } } } // sanitizeData removes the query expressions from the dashboard data func sanitizeData(data *simplejson.Json) { for _, panelObj := range data.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) // if the panel is a row and it is collapsed, get the queries from the panels inside the row if panel.Get("type").MustString() == "row" && panel.Get("collapsed").MustBool() { // recursive call to get queries from panels inside a row sanitizeData(panel) continue } for _, targetObj := range panel.Get("targets").MustArray() { target := simplejson.NewFromAny(targetObj) target.Del("expr") target.Del("query") target.Del("rawSql") } } } // NewTimeRange declared to be able to stub this function in tests var NewTimeRange = gtime.NewTimeRange // BuildTimeSettings build time settings object using selected values if enabled and are valid or dashboard default values func buildTimeSettings(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, pd *models.PublicDashboard, panelID int64) models.TimeSettings { from, to, timezone := getTimeRangeValuesOrDefault(reqDTO, d, pd.TimeSelectionEnabled, panelID) timeRange := NewTimeRange(from, to) timeFrom, _ := timeRange.ParseFrom( gtime.WithLocation(timezone), ) timeTo, _ := timeRange.ParseTo( gtime.WithLocation(timezone), ) timeToAsEpoch := timeTo.UnixMilli() timeFromAsEpoch := timeFrom.UnixMilli() // Were using epoch ms because this is used to build a MetricRequest, which is used by query caching, which want the time range in epoch milliseconds. return models.TimeSettings{ From: strconv.FormatInt(timeFromAsEpoch, 10), To: strconv.FormatInt(timeToAsEpoch, 10), } } // buildTimeSettingsV2 builds time settings for V2 dashboards func buildTimeSettingsV2(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, pd *models.PublicDashboard, panelID int64) models.TimeSettings { from, to, timezone := getTimeRangeValuesOrDefaultV2(d, reqDTO, pd.TimeSelectionEnabled, panelID) timeRange := NewTimeRange(from, to) timeFrom, _ := timeRange.ParseFrom( gtime.WithLocation(timezone), ) timeTo, _ := timeRange.ParseTo( gtime.WithLocation(timezone), ) timeToAsEpoch := timeTo.UnixMilli() timeFromAsEpoch := timeFrom.UnixMilli() // Were using epoch ms because this is used to build a MetricRequest, which is used by query caching, which want the time range in epoch milliseconds. return models.TimeSettings{ From: strconv.FormatInt(timeFromAsEpoch, 10), To: strconv.FormatInt(timeToAsEpoch, 10), } } // returns from, to and timezone from the request if the timeSelection is enabled or the dashboard default values func getTimeRangeValuesOrDefault(reqDTO models.PublicDashboardQueryDTO, d *dashboards.Dashboard, timeSelectionEnabled bool, panelID int64) (string, string, *time.Location) { from := d.Data.GetPath("time", "from").MustString() to := d.Data.GetPath("time", "to").MustString() dashboardTimezone := d.Data.GetPath("timezone").MustString() panelRelativeTime := getPanelRelativeTimeRange(d.Data, panelID) if panelRelativeTime != "" { from = panelRelativeTime } // we use the values from the request if the time selection is enabled and the values are valid if timeSelectionEnabled { if reqDTO.TimeRange.From != "" && reqDTO.TimeRange.To != "" { from = reqDTO.TimeRange.From to = reqDTO.TimeRange.To } if reqDTO.TimeRange.Timezone != "" { if userTimezone, err := time.LoadLocation(reqDTO.TimeRange.Timezone); err == nil { return from, to, userTimezone } } } // if the dashboardTimezone is blank or there is an error default is UTC timezone, err := time.LoadLocation(dashboardTimezone) if err != nil { return from, to, time.UTC } return from, to, timezone } // getTimeRangeValuesOrDefaultV2 returns from, to and timezone from the request if the timeSelection is enabled or the dashboard default values for V2 func getTimeRangeValuesOrDefaultV2(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, timeSelectionEnabled bool, panelID int64) (string, string, *time.Location) { // In V2, time settings are in dashboard.timeSettings timeSettings := d.Data.Get("timeSettings") from := timeSettings.Get("from").MustString() to := timeSettings.Get("to").MustString() dashboardTimezone := timeSettings.Get("timezone").MustString() // Check for panel-specific time override in V2 structure panelRelativeTime := getPanelRelativeTimeRangeV2(d.Data, panelID) if panelRelativeTime != "" { from = panelRelativeTime } // we use the values from the request if the time selection is enabled and the values are valid if timeSelectionEnabled { if reqDTO.TimeRange.From != "" && reqDTO.TimeRange.To != "" { from = reqDTO.TimeRange.From to = reqDTO.TimeRange.To } if reqDTO.TimeRange.Timezone != "" { if userTimezone, err := time.LoadLocation(reqDTO.TimeRange.Timezone); err == nil { return from, to, userTimezone } } } // if the dashboardTimezone is blank or there is an error default is UTC timezone, err := time.LoadLocation(dashboardTimezone) if err != nil { return from, to, time.UTC } return from, to, timezone } func getPanelRelativeTimeRange(dashboard *simplejson.Json, panelID int64) string { for _, panelObj := range dashboard.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) if panel.Get("id").MustInt64() == panelID { return panel.Get("timeFrom").MustString() } } return "" } func getPanelRelativeTimeRangeV2(dashboard *simplejson.Json, panelID int64) string { // In V2, check elements for panel-specific time settings elements := dashboard.Get("elements") if elements.Interface() == nil { return "" } elementsMap := elements.MustMap() for _, element := range elementsMap { element := simplejson.NewFromAny(element) // Check if this is the panel we're looking for if element.Get("spec").Get("id").MustInt64() != panelID { continue } // Check for time override in data.spec.queryOptions.timeFrom spec := element.Get("spec") if spec.Interface() == nil { return "" } data := spec.Get("data") if data.Interface() == nil { return "" } dataSpec := data.Get("spec") if dataSpec.Interface() == nil { return "" } queryOptions := dataSpec.Get("queryOptions") if queryOptions.Interface() == nil { return "" } timeFrom := queryOptions.Get("timeFrom") if timeFrom.Interface() != nil { return timeFrom.MustString() } return "" } return "" }