diff --git a/.github/commands.json b/.github/commands.json index 29106576726..0db8a607b53 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -1226,5 +1226,13 @@ "addToProject": { "url": "https://github.com/orgs/grafana/projects/69" } + }, + { + "type": "label", + "name": "area/suggestions", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/56" + } } ] diff --git a/.github/pr-commands.json b/.github/pr-commands.json index 8b4472fe02a..ab42e204fce 100644 --- a/.github/pr-commands.json +++ b/.github/pr-commands.json @@ -469,5 +469,15 @@ "addToProject": { "url": "https://github.com/orgs/grafana/projects/190" } + }, + { + "type": "changedfiles", + "matches": [ + "public/app/features/panel/suggestions/**/*", + "public/app/plugins/panel/**/suggestions.ts", + "packages/grafana-data/src/types/suggestions*" + ], + "action": "updateLabel", + "addLabel": "area/suggestions" } ] diff --git a/.github/workflows/auto-triager/labels.txt b/.github/workflows/auto-triager/labels.txt index 18ef2df93d8..451719ee0f7 100644 --- a/.github/workflows/auto-triager/labels.txt +++ b/.github/workflows/auto-triager/labels.txt @@ -85,6 +85,7 @@ area/scenes area/search area/security area/streaming +area/suggestions area/templating/repeating area/tooltip area/transformations diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 0a50abe77dd..0b22bce3774 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -853,8 +853,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= -github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388 h1:ZGYKVzeRlBWSf1e17Q8ME2WT1WmEKP8g0tMFfaBjgfI= -github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= diff --git a/conf/defaults.ini b/conf/defaults.ini index a72b0183290..c2d7e4da3b6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1747,9 +1747,8 @@ enabled = true #################################### Short Links ############################# [short_links] -# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is 7 days. Maximum value is 365. -# 0 means they will be deleted approximately every 10 minutes. A negative value (such as -1) will disable expiration. -expire_time = 7 +# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365. +expire_time = -1 #################################### Internal Grafana Metrics ############ # Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId diff --git a/conf/sample.ini b/conf/sample.ini index d6397f894e4..d1d50f0a72a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1689,8 +1689,8 @@ default_datasource_uid = #################################### Short Links ############################# [short_links] -# Short links which are never accessed will be deleted as cleanup. Time is in days. Default is 7 days. Max is 365. 0 means they will be deleted approximately every 10 minutes. -;expire_time = 7 +# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365. +;expire_time = -1 #################################### Internal Grafana Metrics ########################## # Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 13c2b77fd2f..1a3e4aea652 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2142,17 +2142,13 @@ Configures settings around the short link feature. #### `expire_time` -Short links that are never accessed are considered expired or stale and are deleted as cleanup. +Short links that are never accessed are considered expired or stale and can be deleted as cleanup. Set the expiration time in days. -The default is `7` days. +The default is `-1` days (never expire). The maximum is `365` days. -A setting above the maximum uses the value `365` instead. -Setting `0` means the short links are cleaned up approximately every 10 minutes. -A negative value such as `-1` disables expiry. -{{< admonition type="caution" >}} -Short links without an expiration increase the size of the database and can't be deleted. Grafana recommends setting a duration based on your specific use case -{{< /admonition >}} +A setting above the maximum uses the value `365` instead. +A negative value such as `-1` disables expiry.
diff --git a/go.mod b/go.mod index 526ca9abc27..785d5c39043 100644 --- a/go.mod +++ b/go.mod @@ -250,6 +250,7 @@ require ( github.com/grafana/grafana/apps/plugins v0.0.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/preferences v0.0.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/apps/provisioning v0.0.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 // @grafana/grafana-search-and-storage github.com/grafana/grafana/apps/scope v0.0.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/apps/secret v0.0.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/apps/shorturl v0.0.0 // @grafana/sharing-squad diff --git a/go.sum b/go.sum index ae0854810ad..f4f91335d2b 100644 --- a/go.sum +++ b/go.sum @@ -1653,8 +1653,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= -github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388 h1:ZGYKVzeRlBWSf1e17Q8ME2WT1WmEKP8g0tMFfaBjgfI= -github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index b9da9e6f2c5..85d8bc4e4f1 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -90,11 +90,11 @@ func RegisterAPIService( builder := &IdentityAccessManagementAPIBuilder{ store: store, - userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation), - saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation), - legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation), - teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation), - ssoLegacyStore: sso.NewLegacyStore(ssoService), + userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation, tracing), + teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation, tracing), + ssoLegacyStore: sso.NewLegacyStore(ssoService, tracing), coreRolesStorage: coreRolesStorage, rolesStorage: rolesStorage, resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), @@ -114,7 +114,7 @@ func RegisterAPIService( dual: dual, unified: unified, userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), - unified, user.NewUserLegacySearchClient(userService), features), + unified, user.NewUserLegacySearchClient(userService, tracing), features), teamSearch: NewTeamSearchHandler(tracing, dual, team.NewLegacyTeamSearchClient(teamService), unified, features), } apiregistration.RegisterAPI(builder) diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go index 2980322cb27..db4709a4e2e 100644 --- a/pkg/registry/apis/iam/serviceaccount/store.go +++ b/pkg/registry/apis/iam/serviceaccount/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.ServiceAccountResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // DeleteCollection implements rest.CollectionDeleter. @@ -52,6 +54,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -95,6 +100,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } @@ -165,6 +173,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.ServiceAccount], error) { @@ -228,6 +239,9 @@ func extractPluginNameFromTitle(title string) string { } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/sso/store.go b/pkg/registry/apis/iam/sso/store.go index c9965adc152..9ab09fa6ef4 100644 --- a/pkg/registry/apis/iam/sso/store.go +++ b/pkg/registry/apis/iam/sso/store.go @@ -7,6 +7,7 @@ import ( commonv1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,12 +33,13 @@ var ( var resource = iamv0.SSOSettingResourceInfo -func NewLegacyStore(service ssosettings.Service) *LegacyStore { - return &LegacyStore{service} +func NewLegacyStore(service ssosettings.Service, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{service, tracer} } type LegacyStore struct { service ssosettings.Service + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -71,6 +73,9 @@ func (s *LegacyStore) NewList() runtime.Object { // List implements rest.Lister. func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.List") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) settings, err := s.service.List(ctx) @@ -88,6 +93,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt // Get implements rest.Getter. func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.Get") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) setting, err := s.service.GetForProviderWithRedactedSecrets(ctx, name) @@ -112,6 +120,9 @@ func (s *LegacyStore) Update( _ bool, _ *metav1.UpdateOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Update") + defer span.End() + const created = false ident, err := identity.GetRequester(ctx) if err != nil { @@ -148,6 +159,9 @@ func (s *LegacyStore) Delete( _ rest.ValidateObjectFunc, options *metav1.DeleteOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Delete") + defer span.End() + obj, err := s.Get(ctx, name, nil) if err != nil { return obj, false, err diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index e048fdb7a6d..c667c8ec374 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.TeamResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } func (s *LegacyStore) New() runtime.Object { @@ -74,6 +76,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "team.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -112,6 +117,9 @@ func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation // Update implements rest.Updater. func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "team.Update") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") } @@ -161,6 +169,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.Team], error) { @@ -197,6 +208,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -219,6 +233,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO } func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } diff --git a/pkg/registry/apis/iam/teambinding/store.go b/pkg/registry/apis/iam/teambinding/store.go index eaaa4766e39..6f6fd4ce162 100644 --- a/pkg/registry/apis/iam/teambinding/store.go +++ b/pkg/registry/apis/iam/teambinding/store.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,13 +36,14 @@ var ( _ rest.CollectionDeleter = (*LegacyBindingStore)(nil) ) -func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool) *LegacyBindingStore { - return &LegacyBindingStore{store, enableAuthnMutation} +func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool, tracer trace.Tracer) *LegacyBindingStore { + return &LegacyBindingStore{store, enableAuthnMutation, tracer} } type LegacyBindingStore struct { store legacy.LegacyIdentityStore enableAuthnMutation bool + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -73,6 +75,9 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime. } func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Update") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update") } @@ -125,6 +130,9 @@ func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo re } func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Delete") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete") } @@ -160,6 +168,9 @@ func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidat } func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Create") + defer span.End() + if !l.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "create") } @@ -230,6 +241,9 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre // Get implements rest.Getter. func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -254,6 +268,9 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta // List implements rest.Lister. func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.List") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/user/legacy_search.go b/pkg/registry/apis/iam/user/legacy_search.go index 534a4f016eb..7fb6c13f7a6 100644 --- a/pkg/registry/apis/iam/user/legacy_search.go +++ b/pkg/registry/apis/iam/user/legacy_search.go @@ -6,6 +6,7 @@ import ( "log/slog" "math" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -27,13 +28,15 @@ type UserLegacySearchClient struct { resourcepb.ResourceIndexClient userService user.Service log *slog.Logger + tracer trace.Tracer } // NewUserLegacySearchClient creates a new UserLegacySearchClient. -func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient { +func NewUserLegacySearchClient(userService user.Service, tracer trace.Tracer) *UserLegacySearchClient { return &UserLegacySearchClient{ userService: userService, log: slog.Default().With("logger", "legacy-user-search-client"), + tracer: tracer, } } @@ -41,6 +44,9 @@ func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient // It only supports exact matching for title, login, or email. // FIXME: This implementation only supports a single field query and will be extended in the future. func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { + ctx, span := c.tracer.Start(ctx, "user.Search") + defer span.End() + signedInUser, err := identity.GetRequester(ctx) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/user/legacy_search_test.go b/pkg/registry/apis/iam/user/legacy_search_test.go index b8a754f34ce..6bd49185786 100644 --- a/pkg/registry/apis/iam/user/legacy_search_test.go +++ b/pkg/registry/apis/iam/user/legacy_search_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" res "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -17,7 +18,7 @@ import ( func TestUserLegacySearchClient_Search(t *testing.T) { t.Run("should return error if no query fields are provided", func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ @@ -66,7 +67,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Limit: 10, @@ -125,7 +126,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) { t.Run("title should have precedence over login and email", func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 803b2972b85..97daee3af18 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -36,18 +37,22 @@ var ( var resource = iamv0alpha1.UserResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // Update implements rest.Updater. func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "user.Update") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") } @@ -105,6 +110,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "user.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -171,6 +179,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.User], error) { @@ -206,6 +217,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -229,6 +243,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f6c0b3d3f19..cc59427da6f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1330,7 +1330,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(true) shortLinks := iniFile.Section("short_links") - cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(7) + cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(-1) if cfg.ShortLinkExpiration > 365 { cfg.Logger.Warn("short_links expire_time must be less than 366 days. Setting to 365 days") diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index bdfb2e8c7ca..c4052938603 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -1618,6 +1618,7 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) { )) if s.overridesService == nil { + s.log.FromContext(ctx).Debug("overrides service not configured, skipping quota check", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) return } @@ -1632,6 +1633,12 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) { s.log.FromContext(ctx).Error("failed to get resource stats for quota checking", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "error", err) return } + if len(stats) > 0 { + s.log.FromContext(ctx).Debug("stats found", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "count", stats[0].Count) + } else { + s.log.FromContext(ctx).Debug("no stats found for resource", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) + } + if len(stats) > 0 && stats[0].Count >= int64(quota.Limit) { s.log.FromContext(ctx).Info("Quota exceeded on create", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "quota", quota.Limit, "count", stats[0].Count, "stats_resource", stats[0].Resource) } diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index ea232baffcc..2cbda7827e3 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -236,6 +236,10 @@ export class KeybindingSrv { appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl })); }); + this.bind('t =', () => { + appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl })); + }); + this.bind('t -', () => { appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); }); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts index 9a70858f39b..8772d49f138 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts @@ -269,6 +269,13 @@ describe('setupKeyboardShortcuts', () => { expect(tPlusBinding).toBeDefined(); }); + it('should setup t = zoom in shortcut', () => { + setupKeyboardShortcuts(mockScene); + + const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't ='); + expect(tEqualsBinding).toBeDefined(); + }); + it('should setup t - zoom out shortcut with keypress type', () => { setupKeyboardShortcuts(mockScene); @@ -302,9 +309,11 @@ describe('setupKeyboardShortcuts', () => { setupKeyboardShortcuts(mockScene); const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't ='); const tMinusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't -'); expect(tPlusBinding).toBeUndefined(); + expect(tEqualsBinding).toBeUndefined(); expect(tMinusBinding).toBeUndefined(); }); }); @@ -364,6 +373,28 @@ describe('setupKeyboardShortcuts', () => { expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds }); + it('should zoom in (scale 0.5) when t = is pressed', () => { + setupKeyboardShortcuts(mockScene); + + const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't ='); + const handler = tEqualsBinding![0].onTrigger; + + handler(); + + // Scale 0.5 should result in 3 hour span (half of 6) + expect(mockTimeRange.onTimeRangeChange).toHaveBeenCalledWith( + expect.objectContaining({ + from: expect.any(Object), + to: expect.any(Object), + raw: expect.any(Object), + }) + ); + + const call = mockTimeRange.onTimeRangeChange.mock.calls[0][0]; + const newSpan = call.to.valueOf() - call.from.valueOf(); + expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds + }); + it('should keep center point when zooming in', () => { setupKeyboardShortcuts(mockScene); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index 1113b8a8a8c..f1878aeb466 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -138,6 +138,13 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { }, }); + keybindings.addBinding({ + key: 't =', + onTrigger: () => { + handleZoom(scene, 0.5); + }, + }); + keybindings.addBinding({ key: 't -', type: 'keypress', // NOTE: Because some browsers/OS identify minus symbol differently.