mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
* Search: add request metrics * Search: consistent naming * Search: remove success counter * Search: update buckets * Search: remove log
251 lines
7.1 KiB
Go
251 lines
7.1 KiB
Go
package searchV2
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/registry"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/services/store"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
)
|
|
|
|
var (
|
|
namespace = "grafana"
|
|
subsystem = "search"
|
|
dashboardSearchFailureRequestsCounter = promauto.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Namespace: namespace,
|
|
Subsystem: subsystem,
|
|
Name: "dashboard_search_failures_total",
|
|
Help: "A counter for failed dashboard search requests",
|
|
},
|
|
[]string{"reason"},
|
|
)
|
|
dashboardSearchSuccessRequestsDuration = promauto.NewHistogram(
|
|
prometheus.HistogramOpts{
|
|
Name: "dashboard_search_successes_duration_seconds",
|
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100},
|
|
Namespace: namespace,
|
|
Subsystem: subsystem,
|
|
})
|
|
dashboardSearchFailureRequestsDuration = promauto.NewHistogram(
|
|
prometheus.HistogramOpts{
|
|
Name: "dashboard_search_failures_duration_seconds",
|
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100},
|
|
Namespace: namespace,
|
|
Subsystem: subsystem,
|
|
})
|
|
)
|
|
|
|
type StandardSearchService struct {
|
|
registry.BackgroundService
|
|
|
|
cfg *setting.Cfg
|
|
sql *sqlstore.SQLStore
|
|
auth FutureAuthService // eventually injected from elsewhere
|
|
ac accesscontrol.AccessControl
|
|
|
|
logger log.Logger
|
|
dashboardIndex *searchIndex
|
|
extender DashboardIndexExtender
|
|
reIndexCh chan struct{}
|
|
}
|
|
|
|
func ProvideService(cfg *setting.Cfg, sql *sqlstore.SQLStore, entityEventStore store.EntityEventsService, ac accesscontrol.AccessControl) SearchService {
|
|
extender := &NoopExtender{}
|
|
s := &StandardSearchService{
|
|
cfg: cfg,
|
|
sql: sql,
|
|
ac: ac,
|
|
auth: &simpleSQLAuthService{
|
|
sql: sql,
|
|
ac: ac,
|
|
},
|
|
dashboardIndex: newSearchIndex(
|
|
newSQLDashboardLoader(sql),
|
|
entityEventStore,
|
|
extender.GetDocumentExtender(),
|
|
newFolderIDLookup(sql),
|
|
),
|
|
logger: log.New("searchV2"),
|
|
extender: extender,
|
|
reIndexCh: make(chan struct{}, 1),
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *StandardSearchService) IsDisabled() bool {
|
|
if s.cfg == nil {
|
|
return true
|
|
}
|
|
return !s.cfg.IsFeatureToggleEnabled(featuremgmt.FlagPanelTitleSearch)
|
|
}
|
|
|
|
func (s *StandardSearchService) Run(ctx context.Context) error {
|
|
orgQuery := &models.SearchOrgsQuery{}
|
|
err := s.sql.SearchOrgs(ctx, orgQuery)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get org list: %w", err)
|
|
}
|
|
orgIDs := make([]int64, 0, len(orgQuery.Result))
|
|
for _, org := range orgQuery.Result {
|
|
orgIDs = append(orgIDs, org.Id)
|
|
}
|
|
return s.dashboardIndex.run(ctx, orgIDs, s.reIndexCh)
|
|
}
|
|
|
|
func (s *StandardSearchService) TriggerReIndex() {
|
|
select {
|
|
case s.reIndexCh <- struct{}{}:
|
|
default:
|
|
// channel is full => re-index will happen soon anyway.
|
|
}
|
|
}
|
|
|
|
func (s *StandardSearchService) RegisterDashboardIndexExtender(ext DashboardIndexExtender) {
|
|
s.extender = ext
|
|
s.dashboardIndex.extender = ext.GetDocumentExtender()
|
|
}
|
|
|
|
func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backend.User, orgId int64) (*models.SignedInUser, error) {
|
|
// TODO: get user & user's permissions from the request context
|
|
|
|
var user *models.SignedInUser
|
|
if s.cfg.AnonymousEnabled && backendUser.Email == "" && backendUser.Login == "" {
|
|
org, err := s.sql.GetOrgByName(s.cfg.AnonymousOrgName)
|
|
if err != nil {
|
|
s.logger.Error("Anonymous access organization error.", "org_name", s.cfg.AnonymousOrgName, "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
user = &models.SignedInUser{
|
|
OrgId: org.Id,
|
|
OrgName: org.Name,
|
|
OrgRole: models.RoleType(s.cfg.AnonymousOrgRole),
|
|
IsAnonymous: true,
|
|
}
|
|
} else {
|
|
getSignedInUserQuery := &models.GetSignedInUserQuery{
|
|
Login: backendUser.Login,
|
|
Email: backendUser.Email,
|
|
OrgId: orgId,
|
|
}
|
|
err := s.sql.GetSignedInUser(ctx, getSignedInUserQuery)
|
|
if err != nil {
|
|
s.logger.Error("Error while retrieving user", "error", err, "email", backendUser.Email, "login", getSignedInUserQuery.Login)
|
|
return nil, errors.New("auth error")
|
|
}
|
|
|
|
if getSignedInUserQuery.Result == nil {
|
|
s.logger.Error("No user found", "email", backendUser.Email)
|
|
return nil, errors.New("auth error")
|
|
}
|
|
|
|
user = getSignedInUserQuery.Result
|
|
}
|
|
|
|
if s.ac.IsDisabled() {
|
|
return user, nil
|
|
}
|
|
|
|
if user.Permissions == nil {
|
|
user.Permissions = make(map[int64]map[string][]string)
|
|
}
|
|
|
|
if _, ok := user.Permissions[orgId]; ok {
|
|
// permissions as part of the `s.sql.GetSignedInUser` query - return early
|
|
return user, nil
|
|
}
|
|
|
|
// TODO: ensure this is cached
|
|
permissions, err := s.ac.GetUserPermissions(ctx, user,
|
|
accesscontrol.Options{ReloadCache: false})
|
|
if err != nil {
|
|
s.logger.Error("failed to retrieve user permissions", "error", err, "email", backendUser.Email)
|
|
return nil, errors.New("auth error")
|
|
}
|
|
|
|
user.Permissions[orgId] = accesscontrol.GroupScopesByAction(permissions)
|
|
return user, nil
|
|
}
|
|
|
|
func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgID int64, q DashboardQuery) *backend.DataResponse {
|
|
start := time.Now()
|
|
query := s.doDashboardQuery(ctx, user, orgID, q)
|
|
|
|
duration := time.Since(start).Seconds()
|
|
if query.Error != nil {
|
|
dashboardSearchFailureRequestsDuration.Observe(duration)
|
|
} else {
|
|
dashboardSearchSuccessRequestsDuration.Observe(duration)
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
func (s *StandardSearchService) doDashboardQuery(ctx context.Context, user *backend.User, orgID int64, q DashboardQuery) *backend.DataResponse {
|
|
rsp := &backend.DataResponse{}
|
|
signedInUser, err := s.getUser(ctx, user, orgID)
|
|
if err != nil {
|
|
dashboardSearchFailureRequestsCounter.With(prometheus.Labels{
|
|
"reason": "get_user_error",
|
|
}).Inc()
|
|
rsp.Error = err
|
|
return rsp
|
|
}
|
|
|
|
filter, err := s.auth.GetDashboardReadFilter(signedInUser)
|
|
if err != nil {
|
|
dashboardSearchFailureRequestsCounter.With(prometheus.Labels{
|
|
"reason": "get_dashboard_filter_error",
|
|
}).Inc()
|
|
rsp.Error = err
|
|
return rsp
|
|
}
|
|
|
|
index, err := s.dashboardIndex.getOrCreateOrgIndex(ctx, orgID)
|
|
if err != nil {
|
|
dashboardSearchFailureRequestsCounter.With(prometheus.Labels{
|
|
"reason": "get_index_error",
|
|
}).Inc()
|
|
rsp.Error = err
|
|
return rsp
|
|
}
|
|
|
|
err = s.dashboardIndex.sync(ctx)
|
|
if err != nil {
|
|
dashboardSearchFailureRequestsCounter.With(prometheus.Labels{
|
|
"reason": "dashboard_index_sync_error",
|
|
}).Inc()
|
|
rsp.Error = err
|
|
return rsp
|
|
}
|
|
|
|
response := doSearchQuery(ctx, s.logger, index, filter, q, s.extender.GetQueryExtender(q), s.cfg.AppSubURL)
|
|
|
|
if q.WithAllowedActions {
|
|
if err := s.addAllowedActionsField(ctx, orgID, signedInUser, response); err != nil {
|
|
s.logger.Error("error when adding the allowedActions field", "err", err)
|
|
}
|
|
}
|
|
|
|
if response.Error != nil {
|
|
dashboardSearchFailureRequestsCounter.With(prometheus.Labels{
|
|
"reason": "search_query_error",
|
|
}).Inc()
|
|
}
|
|
|
|
return response
|
|
}
|