From 981aad6b7743730590929d2b3e6324da0b4c764f Mon Sep 17 00:00:00 2001 From: Santiago Date: Mon, 14 Oct 2024 13:16:27 +0200 Subject: [PATCH 01/45] Docs: Fix description for GeneratorURL template value (#92578) --- .../template-notifications/reference.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/template-notifications/reference.md b/docs/sources/alerting/configure-notifications/template-notifications/reference.md index 3bb4fe3f216..f0b84f67d16 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/reference.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/reference.md @@ -23,20 +23,20 @@ weight: 400 ### Alert -| Name | Kind | Description | Example | -| ------------ | -------- | ------------------------------------------------------------------------------------ | --------------------- | -| Status | `string` | Firing or resolved | `{{ .Status }}` | -| Labels | `KV` | The labels for this alert | `{{ .Labels }}` | -| Annotations | `KV` | The annotations for this alert | `{{ .Annotations }}` | -| Values | `KV` | The values of all expressions, including Classic Conditions | `{{ .Values }}` | -| StartsAt | `Time` | The time the alert fired | `{{ .StartsAt }}` | -| EndsAt | `Time` | | `{{ .EndsAt }}` | -| GeneratorURL | `string` | A link to Grafana, or the Alertmanager if using an external Alertmanager | `{{ .GeneratorURL }}` | -| SilenceURL | `string` | A link to silence the alert | `{{ .SilenceURL }}` | -| DashboardURL | `string` | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation | `{{ .DashboardURL }}` | -| PanelURL | `string` | A link to the panel if the alert has a Panel ID annotation | `{{ .PanelURL }}` | -| Fingerprint | `string` | A unique string that identifies the alert | `{{ .Fingerprint }}` | -| ValueString | `string` | A string that contains the labels and value of each reduced expression in the alert. | `{{ .ValueString }}` | +| Name | Kind | Description | Example | +| ------------ | -------- | ----------------------------------------------------------------------------------- | --------------------- | +| Status | `string` | Firing or resolved | `{{ .Status }}` | +| Labels | `KV` | The labels for this alert | `{{ .Labels }}` | +| Annotations | `KV` | The annotations for this alert | `{{ .Annotations }}` | +| Values | `KV` | The values of all expressions, including Classic Conditions | `{{ .Values }}` | +| StartsAt | `Time` | The time the alert fired | `{{ .StartsAt }}` | +| EndsAt | `Time` | | `{{ .EndsAt }}` | +| GeneratorURL | `string` | A link to Grafana, or the source of the alert if using an external alert generator | `{{ .GeneratorURL }}` | +| SilenceURL | `string` | A link to silence the alert | `{{ .SilenceURL }}` | +| DashboardURL | `string` | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation | `{{ .DashboardURL }}` | +| PanelURL | `string` | A link to the panel if the alert has a Panel ID annotation | `{{ .PanelURL }}` | +| Fingerprint | `string` | A unique string that identifies the alert | `{{ .Fingerprint }}` | +| ValueString | `string` | A string that contains the labels and value of each reduced expression in the alert | `{{ .ValueString }}` | ### ExtendedData From fcfa4aa777dcd17836d81ea9dcf513bb95cc270f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 14 Oct 2024 13:44:47 +0200 Subject: [PATCH 02/45] Zanzana: Add config options for Check and ListObjects queries (#94619) * Zanzana: Add config options for Check and ListObjects queries * remove fixme * pass only zanzana settings --- pkg/services/authz/zanzana.go | 4 ++-- .../authz/zanzana/client/client_test.go | 2 +- pkg/services/authz/zanzana/server.go | 4 ++-- pkg/services/authz/zanzana/server/server.go | 7 +++++-- pkg/setting/settings_zanzana.go | 17 +++++++++++++++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 1c83006e7c4..c47e680f709 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -49,7 +49,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg return nil, fmt.Errorf("failed to start zanzana: %w", err) } - srv, err := zanzana.NewServer(store, logger) + srv, err := zanzana.NewServer(cfg, store, logger) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } @@ -104,7 +104,7 @@ func (z *Zanzana) start(ctx context.Context) error { return fmt.Errorf("failed to initilize zanana store: %w", err) } - srv, err := zanzana.NewServer(store, z.logger) + srv, err := zanzana.NewServer(z.cfg, store, z.logger) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } diff --git a/pkg/services/authz/zanzana/client/client_test.go b/pkg/services/authz/zanzana/client/client_test.go index 050a0cafcce..35bdde46088 100644 --- a/pkg/services/authz/zanzana/client/client_test.go +++ b/pkg/services/authz/zanzana/client/client_test.go @@ -107,7 +107,7 @@ func zanzanaServerIntegrationTest(tb testing.TB) *inprocgrpc.Channel { store, err := zstore.NewEmbeddedStore(cfg, db, logger) require.NoError(tb, err) - srv, err := zserver.New(store, logger) + srv, err := zserver.New(&cfg.Zanzana, store, logger) require.NoError(tb, err) channel := &inprocgrpc.Channel{} diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index 3d6b0dc4a6a..33730ae9341 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -11,8 +11,8 @@ import ( zserver "github.com/grafana/grafana/pkg/services/authz/zanzana/server" ) -func NewServer(store storage.OpenFGADatastore, logger log.Logger) (*server.Server, error) { - return zserver.New(store, logger) +func NewServer(cfg *setting.Cfg, store storage.OpenFGADatastore, logger log.Logger) (*server.Server, error) { + return zserver.New(&cfg.Zanzana, store, logger) } func StartOpenFGAHttpSever(cfg *setting.Cfg, srv grpcserver.Provider, logger log.Logger) error { diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index 49baf954fde..dc1f935c810 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -27,11 +27,14 @@ import ( zlogger "github.com/grafana/grafana/pkg/services/authz/zanzana/logger" ) -func New(store storage.OpenFGADatastore, logger log.Logger) (*server.Server, error) { - // FIXME(kalleep): add support for more options, tracing etc +func New(cfg *setting.ZanzanaSettings, store storage.OpenFGADatastore, logger log.Logger) (*server.Server, error) { opts := []server.OpenFGAServiceV1Option{ server.WithDatastore(store), server.WithLogger(zlogger.New(logger)), + server.WithCheckQueryCacheEnabled(cfg.CheckQueryCache), + server.WithCheckQueryCacheTTL(cfg.CheckQueryCacheTTL), + server.WithListObjectsMaxResults(cfg.ListObjectsMaxResults), + server.WithListObjectsDeadline(cfg.ListObjectsDeadline), } // FIXME(kalleep): Interceptors diff --git a/pkg/setting/settings_zanzana.go b/pkg/setting/settings_zanzana.go index 158d71d191f..c9179ad9df0 100644 --- a/pkg/setting/settings_zanzana.go +++ b/pkg/setting/settings_zanzana.go @@ -2,6 +2,7 @@ package setting import ( "slices" + "time" ) type ZanzanaMode string @@ -20,11 +21,19 @@ type ZanzanaSettings struct { ListenHTTP bool // OpenFGA http server address which allows to connect with fga cli HttpAddr string - // Number of check requests running concurrently - ConcurrentChecks int64 // If enabled, authorization cheks will be only performed by zanzana. // This bypasses the performance comparison with the legacy system. ZanzanaOnlyEvaluation bool + // Number of concurrent check requests running by Grafana. + ConcurrentChecks int64 + // Enable cache for Check() requests + CheckQueryCache bool + // TTL for cached requests. Default is 10 seconds. + CheckQueryCacheTTL time.Duration + // Max number of results returned by ListObjects() query. Default is 1000. + ListObjectsMaxResults uint32 + // Deadline for the ListObjects() query. Default is 3 seconds. + ListObjectsDeadline time.Duration } func (cfg *Cfg) readZanzanaSettings() { @@ -45,6 +54,10 @@ func (cfg *Cfg) readZanzanaSettings() { s.HttpAddr = sec.Key("http_addr").MustString("127.0.0.1:8080") s.ConcurrentChecks = sec.Key("concurrent_checks").MustInt64(10) s.ZanzanaOnlyEvaluation = sec.Key("zanzana_only_evaluation").MustBool(false) + s.CheckQueryCache = sec.Key("check_query_cache").MustBool(true) + s.CheckQueryCacheTTL = sec.Key("check_query_cache_ttl").MustDuration(10 * time.Second) + s.ListObjectsDeadline = sec.Key("list_objects_deadline").MustDuration(3 * time.Second) + s.ListObjectsMaxResults = uint32(sec.Key("list_objects_max_results").MustUint(1000)) cfg.Zanzana = s } From 517975a4b3799f77a2fb06b058f1f3487b30cce1 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 14 Oct 2024 09:34:19 -0400 Subject: [PATCH 03/45] Chore: update feature toggle git stats (#93820) --- pkg/services/featuremgmt/toggles-gitlog.csv | 41 +++++++++--- pkg/services/featuremgmt/toggles_gen.json | 69 +++++++++++---------- 2 files changed, 67 insertions(+), 43 deletions(-) diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index a0c15aada46..7ef568102f2 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -46,7 +46,7 @@ export,2022-04-25T23:59:18Z,2023-03-01T17:42:53Z,e0aeb83786731769e870d79c0d8a21e savedItems,2022-04-26T10:27:01Z,2022-06-27T14:41:00Z,e420252d45cd48fdcf5cc68ad84f89e36d5c936c,Ashley Harrison tracing,2022-04-26T13:31:27Z,2023-03-21T09:35:21Z,3b4d237ade03e67a971ca8899e8e0371e1d83f2a,kay delaney cloudWatchDynamicLabels,2022-04-29T09:43:04Z,2023-05-02T08:48:17Z,7bb4f5cd9baff81b354e4bb65fd4b6141e3488f6,Shirley -datasourceQueryMultiStatus,2022-05-03T16:02:20Z,,4ecd57f49c75d72927be62aba23a15dca02f8eb6,Will Browne +datasourceQueryMultiStatus,2022-05-03T16:02:20Z,2024-07-10T09:15:10Z,4ecd57f49c75d72927be62aba23a15dca02f8eb6,Will Browne azureMonitorExperimentalUI,2022-05-04T13:54:09Z,2022-07-14T13:07:31Z,b2644de6c8a08181cbe0a9f545393bd42753477c,Adam Simpson traceToMetrics,2022-05-05T20:46:18Z,2024-02-22T13:30:41Z,c1b5ea3e54169c7a4bbd58bd8be2c4c192a093c0,Connor Lindsey prometheusStreamingJSONParser,2022-05-13T18:28:54Z,2022-11-11T16:53:12Z,87e8521591534bcfd8a7c78fbd39b226c13a99b2,Todd Treece @@ -128,7 +128,7 @@ clientTokenRotation,2023-03-23T13:39:04Z,2024-02-16T14:03:37Z,382b24742ab8b124af disableAngular,2023-03-23T15:43:45Z,,58704390269c8eeedd32c571245d5ef08f0ffb82,Ryan McKinley disableElasticsearchBackendExploreQuery,2023-03-27T13:52:27Z,2023-04-12T12:20:43Z,f3da91f53fa32a8e870124e9514a010b97359529,Ivana Huckova emptyDashboardPage,2023-03-28T09:42:23Z,2024-01-25T14:04:29Z,221c5efedc593fb42d87c849b3d0827e325fc6cd,Polina Boneva -prometheusDataplane,2023-03-29T15:26:32Z,,674144c8e84cb4aca3bfc448f08b88bb516d3b77,Kyle Brandt +prometheusDataplane,2023-03-29T15:26:32Z,2024-08-26T12:53:38Z,674144c8e84cb4aca3bfc448f08b88bb516d3b77,Kyle Brandt alertStateHistoryLokiOnly,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver alertStateHistoryLokiPrimary,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver alertStateHistoryLokiSecondary,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver @@ -206,20 +206,20 @@ lokiRunQueriesInParallel,2023-09-19T09:34:01Z,,98aa7db64ab4c4d0f3699a9bcbde98944 pluginsAPIMetrics,2023-09-21T11:36:32Z,,8e8bd2760b8c05df9015900ebdf865c1629881de,Esteban Beltran externalCorePlugins,2023-09-22T08:50:13Z,,61cdfba87a36bd4b8e1bb227a14c5d88fc943aff,Andres Martinez Gotor httpSLOLevels,2023-09-22T08:52:28Z,2024-02-06T08:29:41Z,e5fbc4a4cd587e4b342afd0ab0136754cf8ebf76,Carl Bergquist -idForwarding,2023-09-25T15:21:28Z,,d15661c726b582212e1329f0ebe938664445e44a,Karl Persson +idForwarding,2023-09-25T15:21:28Z,2024-08-21T13:30:17Z,d15661c726b582212e1329f0ebe938664445e44a,Karl Persson cloudwatchNewRegionsHandler,2023-09-25T18:19:12Z,2024-01-30T12:11:52Z,ef441f02d09730449f3f7bbdcf3b31dc00ed797a,Sarah Zinger cloudWatchWildCardDimensionValues,2023-09-27T14:41:48Z,2024-03-15T13:49:53Z,06a35f55ac56ca5699031b2af9202ac7a26918fc,Isabella Siu externalServiceAccounts,2023-09-28T07:26:37Z,,4563fc48afe81ff1af54b6a5bf6d65355f961155,Gabriel MABILLE alertingModifiedExport,2023-09-28T14:07:45Z,2023-10-12T17:17:32Z,169c5262a5e7d04234119a3733b00bb787b8bf90,Sonia Aguilar enableNativeHTTPHistogram,2023-10-03T18:23:55Z,,0fc403d116b9cbd0b93e497cff0ddded55f9fe60,Carl Bergquist transformationsVariableSupport,2023-10-04T14:28:46Z,,40cdb3033697e8d8e43d0ee0342f0d16fac7d633,Oscar Kilhed -kubernetesPlaylists,2023-10-05T19:00:36Z,,664ebf771e2a1d2f94bc14d3dababb634c4ac40f,Todd Treece +kubernetesPlaylists,2023-10-05T19:00:36Z,2024-08-13T08:03:28Z,664ebf771e2a1d2f94bc14d3dababb634c4ac40f,Todd Treece grafanaAPIServerWithExperimentalAPIs,2023-10-06T18:55:22Z,,717a9dd6160e352ff7c184bfcddf2410eed9d908,Ryan McKinley panelMonitoring,2023-10-09T05:19:08Z,,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin navAdminSubsections,2023-10-10T10:50:44Z,2023-11-17T10:04:34Z,f56cc6fdc01dbf2efb802f22650cb2bef0c40179,Ashley Harrison recoveryThreshold,2023-10-10T14:51:50Z,,810fbc3327841da6d21f945e75cad9daf040e625,Yuri Tseretyan libraryPanelRBAC,2023-10-11T23:30:50Z,,a12cb8cbf3a9b33841b2f2cb1522be11de78c86a,kay delaney -awsDatasourcesNewFormStyling,2023-10-12T08:59:10Z,,2771fb940342aa152377b26b9554eb15082f90ac,Ida Štambuk +awsDatasourcesNewFormStyling,2023-10-12T08:59:10Z,2024-07-22T12:48:17Z,2771fb940342aa152377b26b9554eb15082f90ac,Ida Štambuk cachingOptimizeSerializationMemoryUsage,2023-10-12T16:56:49Z,,94ce87571ddfcede0fb7a229a65502b385d5bca3,Michael Mandrus panelTitleSearchInV1,2023-10-13T12:04:24Z,,bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69,Arati R exploreContentOutline,2023-10-13T16:57:13Z,2024-06-24T15:45:42Z,4ec54bc2c39ba43843c693fdb2a4529b6a4703f2,Haris Rozajac @@ -245,7 +245,7 @@ pdfTables,2023-11-06T13:39:22Z,,95b48339f89c7d267bce7d894404d26ccd75d0e3,Agnès newVizTooltips,2023-11-06T16:35:59Z,2024-04-03T00:32:01Z,6b4b7127544865b78f712d907e6f1719595f4232,Adela Almasan ssoSettingsApi,2023-11-08T09:50:01Z,,5285e9503be5702680acb2b52a6bda0632f4603d,Misi logsInfiniteScrolling,2023-11-09T10:54:03Z,,174c2ab45a2af912519153c5c3e671f04396d7d7,Matias Chomicki -flameGraphItemCollapsing,2023-11-09T14:31:07Z,,494a07b522df4e3ff9512b47767745a94c15f080,Andrej Ocenas +flameGraphItemCollapsing,2023-11-09T14:31:07Z,2024-07-15T12:45:41Z,494a07b522df4e3ff9512b47767745a94c15f080,Andrej Ocenas alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8e71c780d797fabac7041a673,Gilles De Mey alertingSimplifiedRouting,2023-11-10T13:14:39Z,,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar dashboardScene,2023-11-13T08:51:21Z,,4bc322ca1d6ed63d7e79eecb1ed09f3043f9aedb,Torkel Ödegaard @@ -259,7 +259,7 @@ displayAnonymousStats,2023-11-29T16:58:41Z,2024-02-23T15:53:37Z,59bdff0280d52ca5 influxqlStreamingParser,2023-11-29T17:29:35Z,,5845f140758473ab5ffe789bec4077032fd22839,ismail simsek kubernetesSnapshots,2023-12-05T22:31:49Z,,439edebcd605a1b63bf3a9b0ab5c2b83341cd5cd,Ryan McKinley grafanaAPIServerEnsureKubectlAccess,2023-12-06T20:21:21Z,,c4c9bfaf2e7fa12a8e453df0f089c8b4f914a3d3,Dan Cech -unifiedStorage,2023-12-06T20:21:21Z,,c4c9bfaf2e7fa12a8e453df0f089c8b4f914a3d3,Dan Cech +unifiedStorage,2023-12-06T20:21:21Z,2024-08-21T16:28:30Z,c4c9bfaf2e7fa12a8e453df0f089c8b4f914a3d3,Dan Cech alertStateHistoryAnnotationsFromLoki,2023-12-11T19:17:01Z,2024-01-25T17:56:09Z,4c1bf86ae11696277025296b86e8514386b4bb31,William Wernert tableSharedCrosshair,2023-12-13T09:33:14Z,,5aff3389f4633d6970eb2b3629ac015e564c626c,Victor Marin lokiQueryHints,2023-12-18T20:43:16Z,,2165c9b3f000f59c9fbda80d2bfe3bc74cd6d9dc,Sven Grossmann @@ -334,7 +334,7 @@ pinNavItems,2024-06-10T11:40:03Z,,84b638fb26cecf856374bb3d09b123061b4b8a6b,Laura authZGRPCServer,2024-06-13T09:41:35Z,,afcb5a855c26e985e43861bff6fab36b1b008109,Gabriel MABILLE openSearchBackendFlowEnabled,2024-06-17T09:41:50Z,,ab2af9b8f75cd13595f4d487c1168e849768a518,Ida Štambuk ssoSettingsLDAP,2024-06-18T11:31:27Z,,d074cc7892b96a1333bd07011baff146ea71e21d,Mihai Doarna -databaseReadReplica,2024-06-18T15:07:15Z,,50244ed4a1435cbf3e3c87d4af34fd7937f7c259,Kristin Laemmert +databaseReadReplica,2024-06-18T15:07:15Z,2024-09-25T23:21:39Z,50244ed4a1435cbf3e3c87d4af34fd7937f7c259,Kristin Laemmert disableClassicHTTPHistogram,2024-06-18T19:37:44Z,,3bbc821131f1b10ace139dbb4a6880fb77686646,Dave Henderson zanzana,2024-06-19T13:59:47Z,,3fe29809bec39239c45d672d686392725773f2e1,Karl Persson failWrongDSUID,2024-06-20T10:56:39Z,,44fd13c742e606b8409e23eb62cab8bab24310f1,Andres Martinez Gotor @@ -342,5 +342,28 @@ passScopeToDashboardApi,2024-06-20T15:49:19Z,,543e71eb2862187d12e8ee7742badb06e4 alertingApiServer,2024-06-20T20:52:03Z,,b07592620279f16b0353444e7aba3c457c50d7ec,Yuri Tseretyan dashboardRestoreUI,2024-06-25T14:43:13Z,,a3879e02bb3b7e8e917ba1bb4163bb230f917f2c,Laura Fernández cloudWatchRoundUpEndTime,2024-06-27T15:10:28Z,,ba5b33227c343cb2c7dad15ff85a745869c68da9,Ida Štambuk -bodyScrolling,2024-07-01T10:28:39Z,,c0058f9c7e390d8a196f5b375382334287633ea9,Ashley Harrison +bodyScrolling,2024-07-01T10:28:39Z,2024-09-24T12:23:18Z,c0058f9c7e390d8a196f5b375382334287633ea9,Ashley Harrison cloudwatchMetricInsightsCrossAccount,2024-07-02T10:34:12Z,,36ff0fe63a7710eb496f2f048feb71e6eb6e3c56,Ida Štambuk +dataplaneAggregator,2024-08-09T08:41:07Z,,122e291134c689ff57eae4461cd5953914b5a36c,Todd Treece +adhocFilterOneOf,2024-08-12T08:56:42Z,2024-09-05T12:49:24Z,ab3e8652aa865e43a3f2c164b626c42dfffab33e,Ashley Harrison +prometheusRunQueriesInParallel,2024-08-12T12:31:39Z,,c9ddc688a2b2c4ebb49e8ecf71dc01e33704da32,Vijay Samuel +backgroundPluginInstaller,2024-08-12T14:39:31Z,2024-09-23T13:49:18Z,d342e76f636e3a5751c5e7baccd1c8910b50d863,Andres Martinez Gotor +pluginsDetailsRightPanel,2024-08-13T09:55:30Z,,8044cb50f17a021a32e7ac0b83cf8d723457a9ae,Yulia Shanyrova +lokiSendDashboardPanelNames,2024-08-22T19:30:43Z,,ec857e1de99d228668e3fb2c0bfd30230a96e180,Sven Grossmann +mysqlParseTime,2024-08-27T11:16:04Z,,c59dddf7afb227f756259a50fc1d2b1946a4a72f,Ryan McKinley +singleTopNav,2024-08-29T08:48:32Z,,8aaa155cb0215119d2455732a9e37f7c2aeff35c,Laura Fernández +exploreLogsAggregatedMetrics,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa9766a995b5711,Sven Grossmann +exploreLogsLimitedTimeRange,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa9766a995b5711,Sven Grossmann +exploreLogsShardSplitting,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa9766a995b5711,Sven Grossmann +newFiltersUI,2024-08-30T12:48:13Z,,00ae49a61adff11e5fbac3341941b35185d9e8d9,Sergej-Vlasov +appPlatformAccessTokens,2024-09-05T16:18:44Z,,d5ebaa0ef92edecfb9511dc7c8080be25f8cc7e7,Claudiu Dragalina-Paraipan +appSidecar,2024-09-09T12:45:05Z,,5e2ac24890906e5070323d87730dd78a4f885963,Andrej Ocenas +vizActions,2024-09-09T14:11:55Z,,af48d3db1eb2d8681843f5997e50fea5e5ea3096,Adela Almasan +groupAttributeSync,2024-09-09T15:29:43Z,,6ded6a8872204a818b3795dc733cc5fe5db066a0,Aaron Godin +kubernetesFolders,2024-09-10T09:22:08Z,,b12a29a1dac8b9aec4a99be08e1665939cb27dc5,Arati R. +alertingFilterV2,2024-09-11T11:29:26Z,,90ee52e8d9c14237f8a57b622c0def7512e657cd,Gilles De Mey +improvedExternalSessionHandling,2024-09-17T10:54:39Z,,41cd0f51800d4849345fc0980ca4173967fc8e9e,Misi +datasourceAPIServers,2024-09-19T08:28:27Z,,f21a5987a22bcdb596d6a258d2960e4151348b63,Ryan McKinley +useSessionStorageForRedirection,2024-09-23T09:31:23Z,,b369341868ec182f076e2e6003b371fdf071f690,Misi +homeSetupGuide,2024-09-25T17:20:04Z,,c822feff9edd6da63da44d935f7cac4365871f01,Serena +alertingQueryAndExpressionsStepMode,2024-09-26T06:33:14Z,,536edee7bff0e393054775b02a9362c8d49ed699,Sonia Aguilar diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 40e0388ab28..f0486fc919b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -56,8 +56,8 @@ "metadata": { "name": "adhocFilterOneOf", "resourceVersion": "1723119716623", - "creationTimestamp": "2024-08-08T12:21:56Z", - "deletionTimestamp": "2024-09-05T12:30:12Z" + "creationTimestamp": "2024-08-12T08:56:42Z", + "deletionTimestamp": "2024-09-05T12:49:24Z" }, "spec": { "description": "Exposes a new 'one of' operator for ad-hoc filters. This operator allows users to filter by multiple values in a single filter.", @@ -170,7 +170,7 @@ "metadata": { "name": "alertingFilterV2", "resourceVersion": "1723028774805", - "creationTimestamp": "2024-08-07T11:06:14Z" + "creationTimestamp": "2024-09-11T11:29:26Z" }, "spec": { "description": "Enable the new alerting search experience", @@ -260,7 +260,7 @@ "metadata": { "name": "alertingQueryAndExpressionsStepMode", "resourceVersion": "1725978395461", - "creationTimestamp": "2024-09-10T14:26:35Z" + "creationTimestamp": "2024-09-26T06:33:14Z" }, "spec": { "description": "Enables step mode for alerting queries and expressions", @@ -386,8 +386,8 @@ "metadata": { "name": "appPlatformAccessTokens", "resourceVersion": "1725549369316", - "creationTimestamp": "2024-09-05T15:16:09Z", - "deletionTimestamp": "2024-10-11T15:54:21Z" + "creationTimestamp": "2024-09-05T16:18:44Z", + "deletionTimestamp": "2024-10-14T13:14:46Z" }, "spec": { "description": "Enables the use of access tokens for the App Platform", @@ -415,7 +415,7 @@ "metadata": { "name": "appSidecar", "resourceVersion": "1722872007883", - "creationTimestamp": "2024-08-05T15:33:27Z" + "creationTimestamp": "2024-09-09T12:45:05Z" }, "spec": { "description": "Enable the app sidecar feature that allows rendering 2 apps at the same time", @@ -580,7 +580,7 @@ "name": "awsDatasourcesNewFormStyling", "resourceVersion": "1720021873452", "creationTimestamp": "2023-10-12T08:59:10Z", - "deletionTimestamp": "2024-07-05T10:20:55Z", + "deletionTimestamp": "2024-07-22T12:48:17Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -624,8 +624,8 @@ "metadata": { "name": "backgroundPluginInstaller", "resourceVersion": "1723202510081", - "creationTimestamp": "2024-08-09T11:21:50Z", - "deletionTimestamp": "2024-09-20T13:20:41Z" + "creationTimestamp": "2024-08-12T14:39:31Z", + "deletionTimestamp": "2024-09-23T13:49:18Z" }, "spec": { "description": "Enable background plugin installer", @@ -639,7 +639,7 @@ "name": "bodyScrolling", "resourceVersion": "1721723807004", "creationTimestamp": "2024-07-01T10:28:39Z", - "deletionTimestamp": "2024-09-24T09:17:00Z", + "deletionTimestamp": "2024-09-24T12:23:18Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-23 08:36:47.004393 +0000 UTC" } @@ -927,7 +927,7 @@ "name": "databaseReadReplica", "resourceVersion": "1720021873452", "creationTimestamp": "2024-06-18T15:07:15Z", - "deletionTimestamp": "2024-09-20T20:03:26Z", + "deletionTimestamp": "2024-09-25T23:21:39Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -943,7 +943,7 @@ "metadata": { "name": "dataplaneAggregator", "resourceVersion": "1723151074613", - "creationTimestamp": "2024-08-08T21:04:34Z" + "creationTimestamp": "2024-08-09T08:41:07Z" }, "spec": { "description": "Enable grafana dataplane aggregator", @@ -974,7 +974,7 @@ "metadata": { "name": "datasourceAPIServers", "resourceVersion": "1726731672938", - "creationTimestamp": "2024-09-12T07:32:40Z", + "creationTimestamp": "2024-09-19T08:28:27Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-19 07:41:12.938146 +0000 UTC" } @@ -1008,7 +1008,7 @@ "name": "datasourceQueryMultiStatus", "resourceVersion": "1718727528075", "creationTimestamp": "2022-05-03T16:02:20Z", - "deletionTimestamp": "2024-07-08T14:46:08Z" + "deletionTimestamp": "2024-07-10T09:15:10Z" }, "spec": { "description": "Introduce HTTP 207 Multi Status for api/ds/query", @@ -1171,7 +1171,7 @@ "metadata": { "name": "exploreLogsAggregatedMetrics", "resourceVersion": "1724938092041", - "creationTimestamp": "2024-08-29T13:28:12Z" + "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { "description": "Used in Explore Logs to query by aggregated metrics", @@ -1184,7 +1184,7 @@ "metadata": { "name": "exploreLogsLimitedTimeRange", "resourceVersion": "1724938092041", - "creationTimestamp": "2024-08-29T13:28:12Z" + "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { "description": "Used in Explore Logs to limit the time range", @@ -1197,7 +1197,7 @@ "metadata": { "name": "exploreLogsShardSplitting", "resourceVersion": "1724938092041", - "creationTimestamp": "2024-08-29T13:28:12Z" + "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { "description": "Used in Explore Logs to split queries into multiple queries based on the number of shards", @@ -1348,7 +1348,7 @@ "name": "flameGraphItemCollapsing", "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-09T14:31:07Z", - "deletionTimestamp": "2024-07-08T14:17:01Z" + "deletionTimestamp": "2024-07-15T12:45:41Z" }, "spec": { "description": "Allow collapsing of flame graph items", @@ -1448,7 +1448,7 @@ "metadata": { "name": "groupAttributeSync", "resourceVersion": "1725893018130", - "creationTimestamp": "2024-09-09T14:43:38Z" + "creationTimestamp": "2024-09-09T15:29:43Z" }, "spec": { "description": "Enable the groupsync extension for managing Group Attribute Sync feature", @@ -1508,7 +1508,7 @@ "metadata": { "name": "homeSetupGuide", "resourceVersion": "1726258153467", - "creationTimestamp": "2024-09-10T15:46:32Z", + "creationTimestamp": "2024-09-25T17:20:04Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-13 20:09:13.467989 +0000 UTC" } @@ -1525,7 +1525,7 @@ "name": "idForwarding", "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-25T15:21:28Z", - "deletionTimestamp": "2024-08-21T11:35:56Z" + "deletionTimestamp": "2024-08-21T13:30:17Z" }, "spec": { "description": "Generate signed id token for identity that can be forwarded to plugins and external services", @@ -1537,7 +1537,7 @@ "metadata": { "name": "improvedExternalSessionHandling", "resourceVersion": "1726560214520", - "creationTimestamp": "2024-09-17T08:03:34Z" + "creationTimestamp": "2024-09-17T10:54:39Z" }, "spec": { "description": "Enable improved support for external sessions in Grafana", @@ -1662,7 +1662,7 @@ "metadata": { "name": "kubernetesFolders", "resourceVersion": "1725863636605", - "creationTimestamp": "2024-09-09T06:29:38Z", + "creationTimestamp": "2024-09-10T09:22:08Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-09 06:33:56.605329 +0000 UTC" } @@ -1678,6 +1678,7 @@ "name": "kubernetesPlaylists", "resourceVersion": "1720021873452", "creationTimestamp": "2023-10-05T19:00:36Z", + "deletionTimestamp": "2024-08-13T08:03:28Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -1945,7 +1946,7 @@ "metadata": { "name": "lokiSendDashboardPanelNames", "resourceVersion": "1724089497989", - "creationTimestamp": "2024-08-19T17:44:18Z", + "creationTimestamp": "2024-08-22T19:30:43Z", "annotations": { "grafana.app/updatedTimestamp": "2024-08-19 17:44:57.989565 +0000 UTC" } @@ -2029,7 +2030,7 @@ "metadata": { "name": "mysqlParseTime", "resourceVersion": "1724750152191", - "creationTimestamp": "2024-08-27T09:15:52Z" + "creationTimestamp": "2024-08-27T11:16:04Z" }, "spec": { "description": "Ensure the parseTime flag is set for MySQL driver", @@ -2088,7 +2089,7 @@ "metadata": { "name": "newFiltersUI", "resourceVersion": "1724228641625", - "creationTimestamp": "2024-08-21T08:24:01Z" + "creationTimestamp": "2024-08-30T12:48:13Z" }, "spec": { "description": "Enables new combobox style UI for the Ad hoc filters variable in scenes architecture", @@ -2355,7 +2356,7 @@ "metadata": { "name": "pluginsDetailsRightPanel", "resourceVersion": "1720788722220", - "creationTimestamp": "2024-07-12T08:39:21Z", + "creationTimestamp": "2024-08-13T09:55:30Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-12 12:52:02.22099 +0000 UTC" } @@ -2487,7 +2488,7 @@ "name": "prometheusDataplane", "resourceVersion": "1720021873452", "creationTimestamp": "2023-03-29T15:26:32Z", - "deletionTimestamp": "2024-08-21T13:35:19Z", + "deletionTimestamp": "2024-08-26T12:53:38Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -2549,7 +2550,7 @@ "metadata": { "name": "prometheusRunQueriesInParallel", "resourceVersion": "1720677541862", - "creationTimestamp": "2024-07-11T05:59:01Z" + "creationTimestamp": "2024-08-12T12:31:39Z" }, "spec": { "description": "Enables running Prometheus queries in parallel", @@ -2814,7 +2815,7 @@ "metadata": { "name": "singleTopNav", "resourceVersion": "1724861961030", - "creationTimestamp": "2024-08-28T16:19:21Z" + "creationTimestamp": "2024-08-29T08:48:32Z" }, "spec": { "description": "Unifies the top search bar and breadcrumb bar into one", @@ -3053,7 +3054,7 @@ "name": "unifiedStorage", "resourceVersion": "1724096690370", "creationTimestamp": "2023-12-06T20:21:21Z", - "deletionTimestamp": "2024-08-21T09:30:06Z", + "deletionTimestamp": "2024-08-21T16:28:30Z", "annotations": { "grafana.app/updatedTimestamp": "2024-08-19 19:44:50.370023815 +0000 UTC" } @@ -3111,7 +3112,7 @@ "metadata": { "name": "useSessionStorageForRedirection", "resourceVersion": "1727082618788", - "creationTimestamp": "2024-09-23T09:10:18Z" + "creationTimestamp": "2024-09-23T09:31:23Z" }, "spec": { "description": "Use session storage for handling the redirection after login", @@ -3123,7 +3124,7 @@ "metadata": { "name": "vizActions", "resourceVersion": "1722461779830", - "creationTimestamp": "2024-07-31T21:36:19Z" + "creationTimestamp": "2024-09-09T14:11:55Z" }, "spec": { "description": "Allow actions in visualizations", From 97258ca1ebe1dc5de583031dde02413949d64b4c Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 14 Oct 2024 09:37:04 -0400 Subject: [PATCH 04/45] Chore: vscode devenv cleanup (#94290) --- .github/CODEOWNERS | 1 - .vscode/launch.json | 7 ++++++- devenv/README.md | 2 +- devenv/vscode/launch.json | 21 --------------------- 4 files changed, 7 insertions(+), 24 deletions(-) delete mode 100644 devenv/vscode/launch.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d9c88232b09..bfecf44889b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -241,7 +241,6 @@ /devenv/docker/rpmtest/ @grafana/grafana-backend-services-squad /devenv/jsonnet/ @grafana/dataviz-squad /devenv/local-npm/ @grafana/frontend-ops -/devenv/vscode/ @grafana/frontend-ops /devenv/setup.sh @grafana/grafana-backend-services-squad /devenv/plugins.yaml @grafana/plugins-platform-frontend diff --git a/.vscode/launch.json b/.vscode/launch.json index 5cfa698f5f0..28499f1fb2e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,12 @@ "program": "${workspaceFolder}/pkg/cmd/grafana/", "env": {}, "cwd": "${workspaceFolder}", - "args": ["server", "--homepath", "${workspaceFolder}", "--packaging", "dev", "cfg:app_mode=development"] + "args": [ + "server", + "--homepath", "${workspaceFolder}", + "--packaging", "dev", + "cfg:app_mode=development", + ] }, { "name": "Run API Server (testdata)", diff --git a/devenv/README.md b/devenv/README.md index a07506e2516..cc37e031d0a 100644 --- a/devenv/README.md +++ b/devenv/README.md @@ -81,7 +81,7 @@ host = "localhost:1025" You can access the web UI at http://localhost:12080/#/ ## Debugging setup in VS Code -An example of launch.json is provided in `devenv/vscode/launch.json`. It basically does what Makefile and .bra.toml do. The 'program' field is set to the folder name so VS Code loads all *.go files in it instead of just main.go. +An example of launch.json is provided in `.vscode/launch.json`. It basically does what Makefile and .bra.toml do. The 'program' field is set to the folder name so VS Code loads all *.go files in it instead of just main.go. ## Troubleshooting diff --git a/devenv/vscode/launch.json b/devenv/vscode/launch.json deleted file mode 100644 index d988713128a..00000000000 --- a/devenv/vscode/launch.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "grafana-server", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/pkg/cmd/grafana-server", - "env": {}, - "args": [ - "--homepath=${workspaceFolder}", - "--packaging=dev", - "cfg:app_mode=development", - ] - } - ] -} \ No newline at end of file From 2867f929743f56d2cbd5f675b7e5145eabe9e86d Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Mon, 14 Oct 2024 15:44:47 +0200 Subject: [PATCH 05/45] Extensions: Show error and warning logs in the console (#94682) fix: log extensions framework errors and warnings to the console --- public/app/features/plugins/extensions/logs/log.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/plugins/extensions/logs/log.ts b/public/app/features/plugins/extensions/logs/log.ts index a78d42ccea0..fc87ba3019b 100644 --- a/public/app/features/plugins/extensions/logs/log.ts +++ b/public/app/features/plugins/extensions/logs/log.ts @@ -32,10 +32,12 @@ export class ExtensionsLog { } warning(message: string, labels?: Labels): void { + console.warn(message, labels); this.log(LogLevel.warning, message, labels); } error(message: string, labels?: Labels): void { + console.error(message, labels); this.log(LogLevel.error, message, labels); } From d5fe9ce87f17b51080437fc466626503c49e3b4e Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Mon, 14 Oct 2024 16:14:11 +0100 Subject: [PATCH 06/45] LBAC for datasources: Fix generated swagger updates (#94587) fix: generated swagger updates --- public/api-enterprise-spec.json | 5 +---- public/api-merged.json | 5 +---- public/openapi3.json | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 7a4e13be50b..d163f81c51a 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -9420,10 +9420,7 @@ "getTeamLBACRulesResponse": { "description": "", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/TeamLBACRules" - } + "$ref": "#/definitions/TeamLBACRules" } }, "getTeamMembersResponse": { diff --git a/public/api-merged.json b/public/api-merged.json index 51e16c1393a..4c1adf3ebe7 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -23756,10 +23756,7 @@ "getTeamLBACRulesResponse": { "description": "(empty)", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/TeamLBACRules" - } + "$ref": "#/definitions/TeamLBACRules" } }, "getTeamMembersResponse": { diff --git a/public/openapi3.json b/public/openapi3.json index fb2ee8f68ca..6008a4f8f97 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -1314,10 +1314,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/TeamLBACRules" - }, - "type": "array" + "$ref": "#/components/schemas/TeamLBACRules" } } }, From e48351fbd32596dd34146ab1bd04c5c1e718cd89 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 14 Oct 2024 16:21:15 +0100 Subject: [PATCH 07/45] AppChromeService: Improve `useChromeHeaderHeight` to only react to necessary state changes (#94624) make useChromeHeaderHeight only react to necessary state changes --- .../components/AppChrome/AppChromeService.tsx | 28 ++++++++++++++++++- public/app/core/context/GrafanaContext.ts | 24 ++-------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index c041574aff9..ba7fb666478 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -1,5 +1,5 @@ import { useObservable } from 'react-use'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, distinctUntilChanged, map } from 'rxjs'; import { AppEvents, NavModel, NavModelItem, PageLayoutType, UrlQueryValue } from '@grafana/data'; import { config, locationService, reportInteraction } from '@grafana/runtime'; @@ -12,6 +12,7 @@ import { KioskMode } from 'app/types'; import { RouteDescriptor } from '../../navigation/types'; import { ReturnToPreviousProps } from './ReturnToPrevious/ReturnToPrevious'; +import { TOP_BAR_LEVEL_HEIGHT } from './types'; export interface AppChromeState { chromeless?: boolean; @@ -56,6 +57,31 @@ export class AppChromeService { returnToPrevious: this.returnToPreviousData, }); + public headerHeightObservable = this.state + .pipe( + map(({ actions, chromeless, kioskMode, searchBarHidden }) => { + if (config.featureToggles.singleTopNav) { + if (kioskMode || chromeless) { + return 0; + } else if (actions) { + return TOP_BAR_LEVEL_HEIGHT * 2; + } else { + return TOP_BAR_LEVEL_HEIGHT; + } + } else { + if (kioskMode || chromeless) { + return 0; + } else if (searchBarHidden) { + return TOP_BAR_LEVEL_HEIGHT; + } else { + return TOP_BAR_LEVEL_HEIGHT * 2; + } + } + }) + ) + // only emit if the state has actually changed + .pipe(distinctUntilChanged()); + public setMatchedRoute(route: RouteDescriptor) { if (this.currentRoute !== route) { this.currentRoute = route; diff --git a/public/app/core/context/GrafanaContext.ts b/public/app/core/context/GrafanaContext.ts index cf3ed6f1f1c..1a82d7f5c3d 100644 --- a/public/app/core/context/GrafanaContext.ts +++ b/public/app/core/context/GrafanaContext.ts @@ -1,10 +1,10 @@ import { createContext, useCallback, useContext } from 'react'; +import { useObservable } from 'react-use'; import { GrafanaConfig } from '@grafana/data'; -import { LocationService, locationService, BackendSrv, config } from '@grafana/runtime'; +import { LocationService, locationService, BackendSrv } from '@grafana/runtime'; import { AppChromeService } from '../components/AppChrome/AppChromeService'; -import { TOP_BAR_LEVEL_HEIGHT } from '../components/AppChrome/types'; import { NewFrontendAssetsChecker } from '../services/NewFrontendAssetsChecker'; import { KeybindingSrv } from '../services/keybindingSrv'; @@ -45,23 +45,5 @@ export function useReturnToPreviousInternal() { export function useChromeHeaderHeight() { const { chrome } = useGrafana(); - const { actions, kioskMode, searchBarHidden, chromeless } = chrome.useState(); - - if (config.featureToggles.singleTopNav) { - if (kioskMode || chromeless) { - return 0; - } else if (actions) { - return TOP_BAR_LEVEL_HEIGHT * 2; - } else { - return TOP_BAR_LEVEL_HEIGHT; - } - } else { - if (kioskMode || chromeless) { - return 0; - } else if (searchBarHidden) { - return TOP_BAR_LEVEL_HEIGHT; - } else { - return TOP_BAR_LEVEL_HEIGHT * 2; - } - } + return useObservable(chrome.headerHeightObservable, 0); } From bcf62612f3e563925e24bf61c003eb64bf330a80 Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Mon, 14 Oct 2024 18:57:19 +0200 Subject: [PATCH 08/45] Table: Improve code readability (#94690) feat(table): improve code radability --- packages/grafana-ui/src/components/Table/Table.tsx | 4 +++- packages/grafana-ui/src/components/Table/reducer.ts | 4 ++-- packages/grafana-ui/src/components/Table/types.ts | 6 ++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 3beb04bda7c..bf7559f498c 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -44,6 +44,7 @@ export const Table = memo((props: Props) => { data, height, onCellFilterAdded, + onColumnResize, width, columnMinWidth = COLUMN_MIN_WIDTH, noHeader, @@ -128,7 +129,7 @@ export const Table = memo((props: Props) => { // Internal react table state reducer const stateReducer = useTableStateReducer({ - ...props, + onColumnResize, onSortByChange: (state) => { // Collapse all rows. This prevents a known bug that causes the size of the rows to be incorrect due to // using `VariableSizeList` and `useExpanded` together. @@ -138,6 +139,7 @@ export const Table = memo((props: Props) => { props.onSortByChange(state); } }, + data, }); const hasUniqueId = !!data.meta?.uniqueRowIdFields?.length; diff --git a/packages/grafana-ui/src/components/Table/reducer.ts b/packages/grafana-ui/src/components/Table/reducer.ts index 10e52e351e5..97ebf8869dd 100644 --- a/packages/grafana-ui/src/components/Table/reducer.ts +++ b/packages/grafana-ui/src/components/Table/reducer.ts @@ -2,14 +2,14 @@ import { useCallback } from 'react'; import { getFieldDisplayName } from '@grafana/data'; -import { TableSortByFieldState, GrafanaTableColumn, GrafanaTableState, Props } from './types'; +import { TableSortByFieldState, GrafanaTableColumn, GrafanaTableState, TableStateReducerProps, Props } from './types'; export interface ActionType { type: string; id: string | undefined; } -export function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) { +export function useTableStateReducer({ onColumnResize, onSortByChange, data }: TableStateReducerProps) { return useCallback( (newState: GrafanaTableState, action: ActionType) => { switch (action.type) { diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index 47801e3acdd..dda99074bd3 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -75,6 +75,12 @@ export interface GrafanaTableState extends TableState { export interface GrafanaTableRow extends Row, UseExpandedRowProps<{}> {} +export interface TableStateReducerProps { + onColumnResize?: TableColumnResizeActionCallback; + onSortByChange?: TableSortByActionCallback; + data: DataFrame; +} + export interface Props { ariaLabel?: string; data: DataFrame; From a8c1c15235ac6513562109c7a3a5d5a826de7440 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Mon, 14 Oct 2024 18:23:48 +0100 Subject: [PATCH 09/45] Fix list indentation to ensure continuous numbering (#94692) --- docs/sources/datasources/prometheus/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/datasources/prometheus/_index.md b/docs/sources/datasources/prometheus/_index.md index e5fa61ef1d6..433e14aedf8 100644 --- a/docs/sources/datasources/prometheus/_index.md +++ b/docs/sources/datasources/prometheus/_index.md @@ -155,7 +155,7 @@ We also bundle a dashboard within Grafana so you can start viewing your metrics 1. Navigate to the data source's [configuration page](ref:configure-prometheus-data-source). 1. Select the **Dashboards** tab. -This displays dashboards for Grafana and Prometheus. + This displays dashboards for Grafana and Prometheus. 1. Select **Import** for the dashboard to import. From 4d8d916434d2acbd35d17f20bd58fb904d955e1f Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Mon, 14 Oct 2024 15:31:36 -0500 Subject: [PATCH 10/45] Alerting: Separate write errors from Prometheus/Mimir into 400/500 categories (#94699) * Separate errors from Prometheus/Mimir into categories * Drop duplicate * tests --- pkg/services/ngalert/writer/prom.go | 36 ++++++++++++++++++------ pkg/services/ngalert/writer/prom_test.go | 17 +++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/pkg/services/ngalert/writer/prom.go b/pkg/services/ngalert/writer/prom.go index ea46b816125..5bfa688c10b 100644 --- a/pkg/services/ngalert/writer/prom.go +++ b/pkg/services/ngalert/writer/prom.go @@ -25,14 +25,18 @@ const backendType = "prometheus" const ( // Fixed error messages MimirDuplicateTimestampError = "err-mimir-sample-duplicate-timestamp" + MimirInvalidLabelError = "err-mimir-label-invalid" // Best effort error messages PrometheusDuplicateTimestampError = "duplicate sample for timestamp" ) var ( - ErrWriteFailure = errors.New("failed to write time series") - ErrBadFrame = errors.New("failed to read dataframe") + // Unexpected, 500-like write errors. + ErrUnexpectedWriteFailure = errors.New("failed to write time series") + // Expected, user-level write errors like trying to write an invalid series. + ErrRejectedWrite = errors.New("series was rejected") + ErrBadFrame = errors.New("failed to read dataframe") ) var DuplicateTimestampErrors = [...]string{ @@ -213,10 +217,12 @@ func (w PrometheusWriter) Write(ctx context.Context, name string, t time.Time, f lvs = append(lvs, fmt.Sprint(res.StatusCode)) w.metrics.WritesTotal.WithLabelValues(lvs...).Inc() - if err, ignored := checkWriteError(writeErr); err != nil { - return errors.Join(ErrWriteFailure, err) - } else if ignored { - l.Debug("Ignored write error", "error", err, "status_code", res.StatusCode) + if writeErr != nil { + if err, ignored := checkWriteError(writeErr); err != nil { + return err + } else if ignored { + l.Debug("Ignored write error", "error", err, "status_code", res.StatusCode) + } } return nil @@ -242,7 +248,12 @@ func checkWriteError(writeErr promremote.WriteError) (err error, ignored bool) { return nil, false } - // special case for 400 status code + // All 500-range statuses are automatically unexpected and not the fault of the data. + if writeErr.StatusCode()/100 == 5 { + return errors.Join(ErrUnexpectedWriteFailure, writeErr), false + } + + // Special case for 400 status code. 400s may be ignorable in the event of HA writers, or the fault of the written data. if writeErr.StatusCode() == 400 { msg := writeErr.Error() // HA may potentially write different values for the same timestamp, so we ignore this error @@ -252,7 +263,16 @@ func checkWriteError(writeErr promremote.WriteError) (err error, ignored bool) { return nil, true } } + + if strings.Contains(msg, MimirInvalidLabelError) { + return errors.Join(ErrRejectedWrite, writeErr), false + } + + // For now, all 400s that are not previously known are considered unexpected. + // TODO: Consider blanket-converting all 400s to be known errors. This should only be done once we are confident this is not a problem with this client. + return errors.Join(ErrUnexpectedWriteFailure, writeErr), false } - return writeErr, false + // All other errors which do not fit into the above categories are also unexpected. + return errors.Join(ErrUnexpectedWriteFailure, writeErr), false } diff --git a/pkg/services/ngalert/writer/prom_test.go b/pkg/services/ngalert/writer/prom_test.go index 3aa49d85831..43297948354 100644 --- a/pkg/services/ngalert/writer/prom_test.go +++ b/pkg/services/ngalert/writer/prom_test.go @@ -166,6 +166,7 @@ func TestPrometheusWriter_Write(t *testing.T) { err := writer.Write(ctx, "test", now, frames, 1, map[string]string{}) require.Error(t, err) require.ErrorIs(t, err, clientErr) + require.ErrorIs(t, err, ErrUnexpectedWriteFailure) }) t.Run("writes expected points", func(t *testing.T) { @@ -204,6 +205,22 @@ func TestPrometheusWriter_Write(t *testing.T) { }) } }) + + t.Run("bad labels fit under the client error category", func(t *testing.T) { + msg := MimirInvalidLabelError + clientErr := testClientWriteError{ + statusCode: http.StatusBadRequest, + msg: &msg, + } + client.writeSeriesFunc = func(ctx context.Context, ts promremote.TSList, opts promremote.WriteOptions) (promremote.WriteResult, promremote.WriteError) { + return promremote.WriteResult{}, clientErr + } + + err := writer.Write(ctx, "test", now, frames, 1, map[string]string{"extra": "label"}) + + require.Error(t, err) + require.ErrorIs(t, err, ErrRejectedWrite) + }) } func extractValue(t *testing.T, frames data.Frames, labels map[string]string, frameType data.FrameType) float64 { From bfd35065490ce6f7e240edc5a215cdfabe6ed204 Mon Sep 17 00:00:00 2001 From: maicon Date: Tue, 15 Oct 2024 00:41:12 -0300 Subject: [PATCH 11/45] UniStore: Enable DataSyncer in Mode1 + better logging (#94688) * UniStore: Enable DataSyncer Mode1 + better logging Signed-off-by: Maicon Costa --------- Signed-off-by: Maicon Costa Co-authored-by: Diego Augusto Molina --- pkg/apiserver/rest/dualwriter.go | 52 ---- pkg/apiserver/rest/dualwriter_mode2.go | 215 --------------- pkg/apiserver/rest/dualwriter_mode2_test.go | 196 -------------- pkg/apiserver/rest/dualwriter_syncer.go | 271 +++++++++++++++++++ pkg/apiserver/rest/dualwriter_syncer_test.go | 238 ++++++++++++++++ pkg/apiserver/rest/metrics.go | 9 +- 6 files changed, 514 insertions(+), 467 deletions(-) create mode 100644 pkg/apiserver/rest/dualwriter_syncer.go create mode 100644 pkg/apiserver/rest/dualwriter_syncer_test.go diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index 09eafb93da6..1d42944d3e9 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "math/rand" "time" "github.com/prometheus/client_golang/prometheus" @@ -306,54 +305,3 @@ func getName(o runtime.Object) string { } return accessor.GetName() } - -const dataSyncerInterval = 60 * time.Minute - -// StartPeriodicDataSyncer starts a background job that will execute the DataSyncer every 60 minutes -func StartPeriodicDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, - kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) { - klog.Info("Starting periodic data syncer for mode mode: ", mode) - - // run in background - go func() { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - timeWindow := 600 // 600 seconds (10 minutes) - jitterSeconds := r.Int63n(int64(timeWindow)) - klog.Info("data syncer is going to start at: ", time.Now().Add(time.Second*time.Duration(jitterSeconds))) - time.Sleep(time.Second * time.Duration(jitterSeconds)) - - // run it immediately - syncOK, err := runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) - klog.Info("data syncer finished, syncOK: ", syncOK, ", error: ", err) - - ticker := time.NewTicker(dataSyncerInterval) - for { - select { - case <-ticker.C: - syncOK, err = runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) - klog.Info("data syncer finished, syncOK: ", syncOK, ", error: ", err) - case <-ctx.Done(): - return - } - } - }() -} - -// runDataSyncer will ensure that data between legacy storage and unified storage are in sync. -// The sync implementation depends on the DualWriter mode -func runDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, - kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) { - // ensure that execution takes no longer than necessary - const timeout = dataSyncerInterval - time.Minute - ctx, cancelFn := context.WithTimeout(ctx, timeout) - defer cancelFn() - - // implementation depends on the current DualWriter mode - switch mode { - case Mode2: - return mode2DataSyncer(ctx, legacy, storage, kind, reg, serverLockService, requestInfo) - default: - klog.Info("data syncer not implemented for mode mode:", mode) - return false, nil - } -} diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index c1356c5de16..8a2a989cdb7 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -2,21 +2,16 @@ package rest import ( "context" - "fmt" "time" - "github.com/prometheus/client_golang/prometheus" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/klog/v2" - "github.com/grafana/authlib/claims" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -392,213 +387,3 @@ func enrichLegacyObject(originalObj, returnedObj runtime.Object) error { accessorReturned.SetUID(accessorOriginal.GetUID()) return nil } - -func getSyncRequester(orgId int64) *identity.StaticRequester { - return &identity.StaticRequester{ - Type: claims.TypeServiceAccount, // system:apiserver - UserID: 1, - OrgID: orgId, - Name: "admin", - Login: "admin", - OrgRole: identity.RoleAdmin, - IsGrafanaAdmin: true, - Permissions: map[int64]map[string][]string{ - orgId: { - "*": {"*"}, // all resources, all scopes - }, - }, - } -} - -type syncItem struct { - name string - objStorage runtime.Object - objLegacy runtime.Object -} - -func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalversion.ListOptions) ([]runtime.Object, error) { - ll, err := obj.List(ctx, listOptions) - if err != nil { - return nil, err - } - - return meta.ExtractList(ll) -} - -func mode2DataSyncer(ctx context.Context, legacy LegacyStorage, storage Storage, resource string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) { - metrics := &dualWriterMetrics{} - metrics.init(reg) - - log := klog.NewKlogr().WithName("DualWriterMode2Syncer") - - everythingSynced := false - outOfSync := 0 - syncSuccess := 0 - syncErr := 0 - - maxInterval := dataSyncerInterval + 5*time.Minute - - var errSync error - const maxRecordsSync = 1000 - - // LockExecuteAndRelease ensures that just a single Grafana server acquires a lock at a time - // The parameter 'maxInterval' is a timeout safeguard, if the LastExecution in the - // database is older than maxInterval, we will assume the lock as timeouted. The 'maxInterval' parameter should be so long - // that is impossible for 2 processes to run at the same time. - err := serverLockService.LockExecuteAndRelease(ctx, "dualwriter mode 2 sync", maxInterval, func(context.Context) { - log.Info("starting dualwriter mode 2 sync") - startSync := time.Now() - - orgId := int64(1) - - ctx = klog.NewContext(ctx, log) - ctx = identity.WithRequester(ctx, getSyncRequester(orgId)) - ctx = request.WithNamespace(ctx, requestInfo.Namespace) - ctx = request.WithRequestInfo(ctx, requestInfo) - - storageList, err := getList(ctx, storage, &metainternalversion.ListOptions{ - Limit: maxRecordsSync, - }) - if err != nil { - log.Error(err, "unable to extract list from storage") - return - } - - if len(storageList) >= maxRecordsSync { - errSync = fmt.Errorf("unified storage has more than %d records. Aborting sync", maxRecordsSync) - log.Error(errSync, "Unified storage has more records to be synced than allowed") - return - } - - log.Info("got items from unified storage", "items", len(storageList)) - - legacyList, err := getList(ctx, legacy, &metainternalversion.ListOptions{}) - if err != nil { - log.Error(err, "unable to extract list from legacy storage") - return - } - log.Info("got items from legacy storage", "items", len(legacyList)) - - itemsByName := map[string]syncItem{} - for _, obj := range legacyList { - accessor, err := utils.MetaAccessor(obj) - if err != nil { - log.Error(err, "error retrieving accessor data for object from legacy storage") - continue - } - name := accessor.GetName() - - item, ok := itemsByName[name] - if !ok { - item = syncItem{} - } - item.name = name - item.objLegacy = obj - itemsByName[name] = item - } - - for _, obj := range storageList { - accessor, err := utils.MetaAccessor(obj) - if err != nil { - log.Error(err, "error retrieving accessor data for object from storage") - continue - } - name := accessor.GetName() - - item, ok := itemsByName[name] - if !ok { - item = syncItem{} - } - item.name = name - item.objStorage = obj - itemsByName[name] = item - } - log.Info("got list of items to be synced", "items", len(itemsByName)) - - for name, item := range itemsByName { - // upsert if: - // - existing in both legacy and storage, but objects are different, or - // - if it's missing from storage - if item.objLegacy != nil && - ((item.objStorage != nil && !Compare(item.objLegacy, item.objStorage)) || (item.objStorage == nil)) { - outOfSync++ - - accessor, err := utils.MetaAccessor(item.objLegacy) - if err != nil { - log.Error(err, "error retrieving accessor data for object from storage") - continue - } - - if item.objStorage != nil { - accessorStorage, err := utils.MetaAccessor(item.objStorage) - if err != nil { - log.Error(err, "error retrieving accessor data for object from storage") - continue - } - accessor.SetResourceVersion(accessorStorage.GetResourceVersion()) - accessor.SetUID(accessorStorage.GetUID()) - - log.Info("updating item on unified storage", "name", name) - } else { - accessor.SetResourceVersion("") - accessor.SetUID("") - - log.Info("inserting item on unified storage", "name", name) - } - - objInfo := rest.DefaultUpdatedObjectInfo(item.objLegacy, []rest.TransformFunc{}...) - res, _, err := storage.Update(ctx, - name, - objInfo, - func(ctx context.Context, obj runtime.Object) error { return nil }, - func(ctx context.Context, obj, old runtime.Object) error { return nil }, - true, // force creation - &metav1.UpdateOptions{}, - ) - if err != nil { - log.WithValues("object", res).Error(err, "could not update in storage") - syncErr++ - } else { - syncSuccess++ - } - } - - // delete if object does not exists on legacy but exists on storage - if item.objLegacy == nil && item.objStorage != nil { - outOfSync++ - - ctx = request.WithRequestInfo(ctx, &request.RequestInfo{ - APIGroup: requestInfo.APIGroup, - Resource: requestInfo.Resource, - Name: name, - Namespace: requestInfo.Namespace, - }) - - log.Info("deleting item from unified storage", "name", name) - - deletedS, _, err := storage.Delete(ctx, name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) - if err != nil { - if !apierrors.IsNotFound(err) { - log.WithValues("objectList", deletedS).Error(err, "could not delete from storage") - } - syncErr++ - } else { - syncSuccess++ - } - } - } - - everythingSynced = outOfSync == syncSuccess - - metrics.recordDataSyncerOutcome(mode2Str, resource, everythingSynced) - metrics.recordDataSyncerDuration(err != nil, mode2Str, resource, startSync) - - log.Info("finished syncing items", "items", len(itemsByName), "updated", syncSuccess, "failed", syncErr, "outcome", everythingSynced) - }) - - if errSync != nil { - err = errSync - } - - return everythingSynced, err -} diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index 809e10fe645..fd7e168b84a 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -16,7 +15,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/apis/example" - "k8s.io/apiserver/pkg/endpoints/request" ) var createFn = func(context.Context, runtime.Object) error { return nil } @@ -609,197 +607,3 @@ func TestEnrichReturnedObject(t *testing.T) { }) } } - -var legacyObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} -var legacyObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} -var legacyObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} -var legacyObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} - -var legacyObj2WithHostname = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{Hostname: "hostname"}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} - -var storageObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} -var storageObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} -var storageObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} -var storageObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} - -var legacyListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, - Items: []example.Pod{ - *legacyObj1, - *legacyObj2, - *legacyObj3, - }} - -var legacyListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, - Items: []example.Pod{ - *legacyObj1, - *legacyObj2, - *legacyObj3, - *legacyObj4, - }} - -var legacyListWith3itemsObj2IsDifferent = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, - Items: []example.Pod{ - *legacyObj1, - *legacyObj2WithHostname, - *legacyObj3, - }} - -var storageListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, - Items: []example.Pod{ - *storageObj1, - *storageObj2, - *storageObj3, - }} - -var storageListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, - Items: []example.Pod{ - *storageObj1, - *storageObj2, - *storageObj3, - *storageObj4, - }} - -var storageListWith3itemsMissingFoo2 = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, - Items: []example.Pod{ - *storageObj1, - *storageObj3, - *storageObj4, - }} - -func TestMode2_DataSyncer(t *testing.T) { - type testCase struct { - setupLegacyFn func(m *mock.Mock) - setupStorageFn func(m *mock.Mock) - name string - expectedOutcome bool - wantErr bool - } - tests := - []testCase{ - { - name: "both stores are in sync", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) - }, - expectedOutcome: true, - }, - { - name: "both stores are in sync - fail to list from legacy", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, errors.New("error")) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) - }, - expectedOutcome: false, - }, - { - name: "both stores are in sync - fail to list from storage", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, errors.New("error")) - }, - expectedOutcome: false, - }, - { - name: "storage is missing 1 entry (foo4)", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) - m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - expectedOutcome: true, - }, - { - name: "storage needs to be update (foo2 is different)", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3itemsObj2IsDifferent, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) - m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - expectedOutcome: true, - }, - { - name: "storage is missing 1 entry (foo4) - fail to upsert", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) - m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error")) - }, - expectedOutcome: false, - }, - { - name: "storage has an extra 1 entry (foo4)", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil) - m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - expectedOutcome: true, - }, - { - name: "storage has an extra 1 entry (foo4) - fail to delete", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil) - m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error")) - }, - expectedOutcome: false, - }, - { - name: "storage is missing 1 entry (foo3) and has an extra 1 entry (foo4)", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(storageListWith3itemsMissingFoo2, nil) - m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) - m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - expectedOutcome: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) - s := (Storage)(nil) - lm := &mock.Mock{} - um := &mock.Mock{} - - ls := legacyStoreMock{lm, l} - us := storageMock{um, s} - - if tt.setupLegacyFn != nil { - tt.setupLegacyFn(lm) - } - if tt.setupStorageFn != nil { - tt.setupStorageFn(um) - } - - outcome, err := mode2DataSyncer(context.Background(), ls, us, "test.kind", p, &fakeServerLock{}, &request.RequestInfo{}) - if tt.wantErr { - assert.Error(t, err) - return - } - - assert.NoError(t, err) - assert.Equal(t, tt.expectedOutcome, outcome) - }) - } -} diff --git a/pkg/apiserver/rest/dualwriter_syncer.go b/pkg/apiserver/rest/dualwriter_syncer.go new file mode 100644 index 00000000000..34bfef41a19 --- /dev/null +++ b/pkg/apiserver/rest/dualwriter_syncer.go @@ -0,0 +1,271 @@ +package rest + +import ( + "context" + "fmt" + "math/rand" + "time" + + "github.com/prometheus/client_golang/prometheus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/klog/v2" + + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +type syncItem struct { + name string + objStorage runtime.Object + objLegacy runtime.Object + accessorStorage utils.GrafanaMetaAccessor + accessorLegacy utils.GrafanaMetaAccessor +} + +const dataSyncerInterval = 60 * time.Minute + +// StartPeriodicDataSyncer starts a background job that will execute the DataSyncer every 60 minutes +func StartPeriodicDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, + kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) { + log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", mode, "resource", kind) + + log.Info("Starting periodic data syncer") + + // run in background + go func() { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + timeWindow := 600 // 600 seconds (10 minutes) + jitterSeconds := r.Int63n(int64(timeWindow)) + log.Info("data syncer scheduled", "starting time", time.Now().Add(time.Second*time.Duration(jitterSeconds))) + time.Sleep(time.Second * time.Duration(jitterSeconds)) + + // run it immediately + syncOK, err := runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) + log.Info("data syncer finished", "syncOK", syncOK, "error", err) + + ticker := time.NewTicker(dataSyncerInterval) + for { + select { + case <-ticker.C: + syncOK, err = runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) + log.Info("data syncer finished", "syncOK", syncOK, ", error", err) + case <-ctx.Done(): + return + } + } + }() +} + +// runDataSyncer will ensure that data between legacy storage and unified storage are in sync. +// The sync implementation depends on the DualWriter mode +func runDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, + kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) { + // ensure that execution takes no longer than necessary + const timeout = dataSyncerInterval - time.Minute + ctx, cancelFn := context.WithTimeout(ctx, timeout) + defer cancelFn() + + // implementation depends on the current DualWriter mode + switch mode { + case Mode1, Mode2: + return legacyToUnifiedStorageDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) + default: + klog.Info("data syncer not implemented for mode mode:", mode) + return false, nil + } +} + +func legacyToUnifiedStorageDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, resource string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) { + metrics := &dualWriterMetrics{} + metrics.init(reg) + + log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", mode, "resource", resource) + + everythingSynced := false + outOfSync := 0 + syncSuccess := 0 + syncErr := 0 + + maxInterval := dataSyncerInterval + 5*time.Minute + + var errSync error + const maxRecordsSync = 1000 + + // LockExecuteAndRelease ensures that just a single Grafana server acquires a lock at a time + // The parameter 'maxInterval' is a timeout safeguard, if the LastExecution in the + // database is older than maxInterval, we will assume the lock as timeouted. The 'maxInterval' parameter should be so long + // that is impossible for 2 processes to run at the same time. + err := serverLockService.LockExecuteAndRelease(ctx, fmt.Sprintf("legacyToUnifiedStorageDataSyncer-%d-%s", mode, resource), maxInterval, func(context.Context) { + log.Info("starting legacyToUnifiedStorageDataSyncer") + startSync := time.Now() + + orgId := int64(1) + + ctx = klog.NewContext(ctx, log) + ctx = identity.WithRequester(ctx, getSyncRequester(orgId)) + ctx = request.WithNamespace(ctx, requestInfo.Namespace) + ctx = request.WithRequestInfo(ctx, requestInfo) + + storageList, err := getList(ctx, storage, &metainternalversion.ListOptions{ + Limit: maxRecordsSync, + }) + if err != nil { + log.Error(err, "unable to extract list from storage") + return + } + + if len(storageList) >= maxRecordsSync { + errSync = fmt.Errorf("unified storage has more than %d records. Aborting sync", maxRecordsSync) + log.Error(errSync, "Unified storage has more records to be synced than allowed") + return + } + + log.Info("got items from unified storage", "items", len(storageList)) + + legacyList, err := getList(ctx, legacy, &metainternalversion.ListOptions{}) + if err != nil { + log.Error(err, "unable to extract list from legacy storage") + return + } + log.Info("got items from legacy storage", "items", len(legacyList)) + + itemsByName := map[string]syncItem{} + for _, obj := range legacyList { + accessor, err := utils.MetaAccessor(obj) + if err != nil { + log.Error(err, "error retrieving accessor data for object from legacy storage") + continue + } + name := accessor.GetName() + + item := itemsByName[name] + item.name = name + item.objLegacy = obj + item.accessorLegacy = accessor + itemsByName[name] = item + } + + for _, obj := range storageList { + accessor, err := utils.MetaAccessor(obj) + if err != nil { + log.Error(err, "error retrieving accessor data for object from storage") + continue + } + name := accessor.GetName() + + item := itemsByName[name] + item.name = name + item.objStorage = obj + item.accessorStorage = accessor + itemsByName[name] = item + } + log.Info("got list of items to be synced", "items", len(itemsByName)) + + for name, item := range itemsByName { + // upsert if: + // - existing in both legacy and storage, but objects are different, or + // - if it's missing from storage + if item.objLegacy != nil && + (item.objStorage == nil || !Compare(item.objLegacy, item.objStorage)) { + outOfSync++ + + if item.objStorage != nil { + item.accessorLegacy.SetResourceVersion(item.accessorStorage.GetResourceVersion()) + item.accessorLegacy.SetUID(item.accessorStorage.GetUID()) + + log.Info("updating item on unified storage", "name", name) + } else { + item.accessorLegacy.SetResourceVersion("") + item.accessorLegacy.SetUID("") + + log.Info("inserting item on unified storage", "name", name) + } + + objInfo := rest.DefaultUpdatedObjectInfo(item.objLegacy, []rest.TransformFunc{}...) + res, _, err := storage.Update(ctx, + name, + objInfo, + func(ctx context.Context, obj runtime.Object) error { return nil }, + func(ctx context.Context, obj, old runtime.Object) error { return nil }, + true, // force creation + &metav1.UpdateOptions{}, + ) + if err != nil { + log.WithValues("object", res).Error(err, "could not update in storage") + syncErr++ + } else { + syncSuccess++ + } + } + + // delete if object does not exists on legacy but exists on storage + if item.objLegacy == nil && item.objStorage != nil { + outOfSync++ + + ctx = request.WithRequestInfo(ctx, &request.RequestInfo{ + APIGroup: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Name: name, + Namespace: requestInfo.Namespace, + }) + + log.Info("deleting item from unified storage", "name", name) + + deletedS, _, err := storage.Delete(ctx, name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + log.WithValues("objectList", deletedS).Error(err, "could not delete from storage") + syncErr++ + continue + } + + syncSuccess++ + } + } + + everythingSynced = outOfSync == syncSuccess + + metrics.recordDataSyncerOutcome(mode, resource, everythingSynced) + metrics.recordDataSyncerDuration(err != nil, mode, resource, startSync) + + log.Info("finished syncing items", "items", len(itemsByName), "updated", syncSuccess, "failed", syncErr, "outcome", everythingSynced) + }) + + if errSync != nil { + err = errSync + } + + return everythingSynced, err +} + +func getSyncRequester(orgId int64) *identity.StaticRequester { + return &identity.StaticRequester{ + Type: claims.TypeServiceAccount, // system:apiserver + UserID: 1, + OrgID: orgId, + Name: "admin", + Login: "admin", + OrgRole: identity.RoleAdmin, + IsGrafanaAdmin: true, + Permissions: map[int64]map[string][]string{ + orgId: { + "*": {"*"}, // all resources, all scopes + }, + }, + } +} + +func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalversion.ListOptions) ([]runtime.Object, error) { + ll, err := obj.List(ctx, listOptions) + if err != nil { + return nil, err + } + + return meta.ExtractList(ll) +} diff --git a/pkg/apiserver/rest/dualwriter_syncer_test.go b/pkg/apiserver/rest/dualwriter_syncer_test.go new file mode 100644 index 00000000000..868cad182a4 --- /dev/null +++ b/pkg/apiserver/rest/dualwriter_syncer_test.go @@ -0,0 +1,238 @@ +package rest + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/apis/example" + "k8s.io/apiserver/pkg/endpoints/request" +) + +var legacyObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} +var legacyObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} +var legacyObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} +var legacyObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} + +var legacyObj2WithHostname = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{Hostname: "hostname"}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} + +var storageObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} +var storageObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} +var storageObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} +var storageObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} + +var legacyListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, + Items: []example.Pod{ + *legacyObj1, + *legacyObj2, + *legacyObj3, + }} + +var legacyListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, + Items: []example.Pod{ + *legacyObj1, + *legacyObj2, + *legacyObj3, + *legacyObj4, + }} + +var legacyListWith3itemsObj2IsDifferent = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, + Items: []example.Pod{ + *legacyObj1, + *legacyObj2WithHostname, + *legacyObj3, + }} + +var storageListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, + Items: []example.Pod{ + *storageObj1, + *storageObj2, + *storageObj3, + }} + +var storageListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, + Items: []example.Pod{ + *storageObj1, + *storageObj2, + *storageObj3, + *storageObj4, + }} + +var storageListWith3itemsMissingFoo2 = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, + Items: []example.Pod{ + *storageObj1, + *storageObj3, + *storageObj4, + }} + +func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { + type testCase struct { + setupLegacyFn func(m *mock.Mock) + setupStorageFn func(m *mock.Mock) + name string + expectedOutcome bool + wantErr bool + } + tests := + []testCase{ + { + name: "both stores are in sync", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) + }, + expectedOutcome: true, + }, + { + name: "both stores are in sync - fail to list from legacy", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, errors.New("error")) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) + }, + expectedOutcome: false, + }, + { + name: "both stores are in sync - fail to list from storage", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, errors.New("error")) + }, + expectedOutcome: false, + }, + { + name: "storage is missing 1 entry (foo4)", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) + m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + expectedOutcome: true, + }, + { + name: "storage needs to be update (foo2 is different)", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3itemsObj2IsDifferent, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) + m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + expectedOutcome: true, + }, + { + name: "storage is missing 1 entry (foo4) - fail to upsert", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) + m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error")) + }, + expectedOutcome: false, + }, + { + name: "storage has an extra 1 entry (foo4)", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil) + m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + expectedOutcome: true, + }, + { + name: "storage has an extra 1 entry (foo4) - fail to delete", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil) + m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error")) + }, + expectedOutcome: false, + }, + { + name: "storage is missing 1 entry (foo3) and has an extra 1 entry (foo4)", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(storageListWith3itemsMissingFoo2, nil) + m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + expectedOutcome: true, + }, + } + + // mode 1 + for _, tt := range tests { + t.Run("Mode-1-"+tt.name, func(t *testing.T) { + l := (LegacyStorage)(nil) + s := (Storage)(nil) + lm := &mock.Mock{} + um := &mock.Mock{} + + ls := legacyStoreMock{lm, l} + us := storageMock{um, s} + + if tt.setupLegacyFn != nil { + tt.setupLegacyFn(lm) + } + if tt.setupStorageFn != nil { + tt.setupStorageFn(um) + } + + outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), Mode1, ls, us, "test.kind", p, &fakeServerLock{}, &request.RequestInfo{}) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expectedOutcome, outcome) + }) + } + + // mode 2 + for _, tt := range tests { + t.Run("Mode-2-"+tt.name, func(t *testing.T) { + l := (LegacyStorage)(nil) + s := (Storage)(nil) + lm := &mock.Mock{} + um := &mock.Mock{} + + ls := legacyStoreMock{lm, l} + us := storageMock{um, s} + + if tt.setupLegacyFn != nil { + tt.setupLegacyFn(lm) + } + if tt.setupStorageFn != nil { + tt.setupStorageFn(um) + } + + outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), Mode1, ls, us, "test.kind", p, &fakeServerLock{}, &request.RequestInfo{}) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expectedOutcome, outcome) + }) + } +} diff --git a/pkg/apiserver/rest/metrics.go b/pkg/apiserver/rest/metrics.go index 780b9b8a4ea..436f6a7dabe 100644 --- a/pkg/apiserver/rest/metrics.go +++ b/pkg/apiserver/rest/metrics.go @@ -1,6 +1,7 @@ package rest import ( + "fmt" "strconv" "time" @@ -97,15 +98,15 @@ func (m *dualWriterMetrics) recordOutcome(mode string, name string, areEqual boo m.outcome.WithLabelValues(mode, name, method).Observe(observeValue) } -func (m *dualWriterMetrics) recordDataSyncerDuration(isError bool, mode string, resource string, startFrom time.Time) { +func (m *dualWriterMetrics) recordDataSyncerDuration(isError bool, mode DualWriterMode, resource string, startFrom time.Time) { duration := time.Since(startFrom).Seconds() - m.syncer.WithLabelValues(strconv.FormatBool(isError), mode, resource).Observe(duration) + m.syncer.WithLabelValues(strconv.FormatBool(isError), fmt.Sprintf("%d", mode), resource).Observe(duration) } -func (m *dualWriterMetrics) recordDataSyncerOutcome(mode string, resource string, synced bool) { +func (m *dualWriterMetrics) recordDataSyncerOutcome(mode DualWriterMode, resource string, synced bool) { var observeValue float64 if !synced { observeValue = 1 } - m.syncerOutcome.WithLabelValues(mode, resource).Observe(observeValue) + m.syncerOutcome.WithLabelValues(fmt.Sprintf("%d", mode), resource).Observe(observeValue) } From 36c38b531066cf93666aa618f050128f98096aca Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 15 Oct 2024 07:46:08 +0300 Subject: [PATCH 12/45] APIServer: add prometheus.Registerer to every init request (#94684) --- .../apis/alerting/notifications/register.go | 8 +++--- pkg/registry/apis/dashboard/register.go | 7 ++++-- .../apis/dashboardsnapshot/register.go | 4 +-- pkg/registry/apis/datasource/register.go | 25 +++++++++---------- pkg/registry/apis/featuretoggle/register.go | 4 +-- pkg/registry/apis/folders/register.go | 8 +++--- pkg/registry/apis/iam/register.go | 4 +-- pkg/registry/apis/peakq/register.go | 6 ++--- pkg/registry/apis/playlist/register.go | 11 +++++--- pkg/registry/apis/query/register.go | 4 +-- pkg/registry/apis/scope/register.go | 7 +++--- pkg/registry/apis/search/register.go | 11 ++++---- pkg/registry/apis/service/register.go | 7 +++--- .../apiserver/aggregator/aggregator.go | 9 ++++--- pkg/services/apiserver/builder/common.go | 16 +++++++----- pkg/services/apiserver/builder/helper.go | 7 +++++- 16 files changed, 75 insertions(+), 63 deletions(-) diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go index f073a3c7d93..72a0b0f3c16 100644 --- a/pkg/registry/apis/alerting/notifications/register.go +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -7,14 +7,12 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" notificationsModels "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" receiver "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/receiver" "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/template_group" timeInterval "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval" @@ -71,7 +69,11 @@ func (t *NotificationsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(notificationsModels.SchemeGroupVersion) } -func (t *NotificationsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder) error { +func (t *NotificationsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + scheme := opts.Scheme + optsGetter := opts.OptsGetter + dualWriteBuilder := opts.DualWriteBuilder + intervals, err := timeInterval.NewStorage(t.ng.Api.MuteTimings, t.namespacer, scheme, optsGetter, dualWriteBuilder) if err != nil { return fmt.Errorf("failed to initialize time-interval storage: %w", err) diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index ae57adfe147..26b38f0b791 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -5,7 +5,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" @@ -124,7 +123,11 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(resourceInfo.GroupVersion()) } -func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder) error { +func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + scheme := opts.Scheme + optsGetter := opts.OptsGetter + dualWriteBuilder := opts.DualWriteBuilder + dash := b.legacy.resource legacyStore, err := b.legacy.newStore(scheme, optsGetter) if err != nil { diff --git a/pkg/registry/apis/dashboardsnapshot/register.go b/pkg/registry/apis/dashboardsnapshot/register.go index a8bc461a3c0..08df1d7b691 100644 --- a/pkg/registry/apis/dashboardsnapshot/register.go +++ b/pkg/registry/apis/dashboardsnapshot/register.go @@ -12,7 +12,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" @@ -22,7 +21,6 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/identity" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" @@ -119,7 +117,7 @@ func (b *SnapshotsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(gv) } -func (b *SnapshotsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ *runtime.Scheme, _ generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *SnapshotsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { storage := map[string]rest.Storage{} legacyStore := &legacyStorage{ diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index d7a3fd963ea..bac6da1f8d9 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -5,11 +5,21 @@ import ( "encoding/json" "fmt" + "github.com/prometheus/client_golang/prometheus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + openapi "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/utils/strings/slices" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/apimachinery/utils" datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/promlib/models" @@ -19,17 +29,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds" - "github.com/prometheus/client_golang/prometheus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/registry/rest" - genericapiserver "k8s.io/apiserver/pkg/server" - openapi "k8s.io/kube-openapi/pkg/common" - "k8s.io/kube-openapi/pkg/spec3" - "k8s.io/utils/strings/slices" ) var _ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil) @@ -204,7 +203,7 @@ func resourceFromPluginID(pluginID string) (utils.ResourceInfo, error) { return datasource.GenericConnectionResourceInfo.WithGroupAndShortName(group, pluginID+"-connection"), nil } -func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ *runtime.Scheme, _ generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { storage := map[string]rest.Storage{} conn := b.connectionResourceInfo diff --git a/pkg/registry/apis/featuretoggle/register.go b/pkg/registry/apis/featuretoggle/register.go index 52cbb6d7a65..524bf62c0a8 100644 --- a/pkg/registry/apis/featuretoggle/register.go +++ b/pkg/registry/apis/featuretoggle/register.go @@ -5,7 +5,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" @@ -15,7 +14,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -81,7 +79,7 @@ func (b *FeatureFlagAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(gv) } -func (b *FeatureFlagAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ *runtime.Scheme, _ generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *FeatureFlagAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { featureStore := NewFeaturesStorage() toggleStore := NewTogglesStorage(b.features) diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index a891d5f249d..10239db549e 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -8,7 +8,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" @@ -18,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -96,7 +94,11 @@ func (b *FolderAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(b.gv) } -func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder) error { +func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + scheme := opts.Scheme + optsGetter := opts.OptsGetter + dualWriteBuilder := opts.DualWriteBuilder + legacyStore := &legacyStorage{ service: b.folderSvc, namespacer: b.namespacer, diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 1ef15561291..859618b58fa 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -7,7 +7,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" @@ -15,7 +14,6 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/identity" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" @@ -93,7 +91,7 @@ func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Schem return scheme.SetVersionPriority(iamv0.SchemeGroupVersion) } -func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ *runtime.Scheme, _ generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { storage := map[string]rest.Storage{} teamResource := iamv0.TeamResourceInfo diff --git a/pkg/registry/apis/peakq/register.go b/pkg/registry/apis/peakq/register.go index bb2d38c356d..54306e85daa 100644 --- a/pkg/registry/apis/peakq/register.go +++ b/pkg/registry/apis/peakq/register.go @@ -6,7 +6,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" @@ -15,7 +14,6 @@ import ( peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) @@ -65,11 +63,11 @@ func (b *PeakQAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(gv) } -func (b *PeakQAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *PeakQAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { resourceInfo := peakq.QueryTemplateResourceInfo storage := map[string]rest.Storage{} - peakqStorage, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter) + peakqStorage, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter) if err != nil { return err } diff --git a/pkg/registry/apis/playlist/register.go b/pkg/registry/apis/playlist/register.go index dc1e3e5ea4d..d31c5218672 100644 --- a/pkg/registry/apis/playlist/register.go +++ b/pkg/registry/apis/playlist/register.go @@ -8,20 +8,19 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" + "github.com/prometheus/client_golang/prometheus" + playlist "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" playlistsvc "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*PlaylistAPIBuilder)(nil) @@ -78,7 +77,11 @@ func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(b.gv) } -func (b *PlaylistAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder) error { +func (b *PlaylistAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + scheme := opts.Scheme + optsGetter := opts.OptsGetter + dualWriteBuilder := opts.DualWriteBuilder + storage := map[string]rest.Storage{} gvr := schema.GroupVersionResource{ diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index fdd8388b961..a08af9909cc 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -8,14 +8,12 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -138,7 +136,7 @@ func (b *QueryAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(query.SchemeGroupVersion) } -func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ *runtime.Scheme, _ generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { gv := query.SchemeGroupVersion storage := map[string]rest.Storage{} diff --git a/pkg/registry/apis/scope/register.go b/pkg/registry/apis/scope/register.go index b41c1bea43b..8e4965666d3 100644 --- a/pkg/registry/apis/scope/register.go +++ b/pkg/registry/apis/scope/register.go @@ -6,7 +6,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" @@ -16,7 +15,6 @@ import ( "github.com/prometheus/client_golang/prometheus" scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) @@ -111,7 +109,10 @@ func (b *ScopeAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(scope.SchemeGroupVersion) } -func (b *ScopeAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *ScopeAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + scheme := opts.Scheme + optsGetter := opts.OptsGetter + scopeResourceInfo := scope.ScopeResourceInfo scopeDashboardResourceInfo := scope.ScopeDashboardBindingResourceInfo scopeNodeResourceInfo := scope.ScopeNodeResourceInfo diff --git a/pkg/registry/apis/search/register.go b/pkg/registry/apis/search/register.go index 683f9dfbcf5..c5162e5d963 100644 --- a/pkg/registry/apis/search/register.go +++ b/pkg/registry/apis/search/register.go @@ -6,18 +6,17 @@ import ( "net/url" "strconv" - "github.com/grafana/grafana/pkg/api/response" - request2 "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/setting" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/api/response" + request2 "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -144,7 +143,7 @@ func (b *SearchAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAP return oas, nil } -func (b *SearchAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder) error { +func (b *SearchAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { apiGroupInfo.PrioritizedVersions = []schema.GroupVersion{b.GetGroupVersion()} return nil } diff --git a/pkg/registry/apis/service/register.go b/pkg/registry/apis/service/register.go index 39138660134..52c26bf3ce0 100644 --- a/pkg/registry/apis/service/register.go +++ b/pkg/registry/apis/service/register.go @@ -6,14 +6,12 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" service "github.com/grafana/grafana/pkg/apis/service/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) @@ -70,7 +68,10 @@ func (b *ServiceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return scheme.SetVersionPriority(gv) } -func (b *ServiceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriteBuilder) error { +func (b *ServiceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + scheme := opts.Scheme + optsGetter := opts.OptsGetter + resourceInfo := service.ExternalNameResourceInfo storage := map[string]rest.Storage{} diff --git a/pkg/services/apiserver/aggregator/aggregator.go b/pkg/services/apiserver/aggregator/aggregator.go index c5eb66498c7..0a7a9b7227b 100644 --- a/pkg/services/apiserver/aggregator/aggregator.go +++ b/pkg/services/apiserver/aggregator/aggregator.go @@ -288,9 +288,12 @@ func CreateAggregatorServer(config *Config, delegateAPIServer genericapiserver.D for _, b := range config.Builders { err := b.UpdateAPIGroupInfo( &serviceAPIGroupInfo, - aggregatorscheme.Scheme, - aggregatorConfig.GenericConfig.RESTOptionsGetter, - nil, // no dual writer + builder.APIGroupOptions{ + Scheme: aggregatorscheme.Scheme, + OptsGetter: aggregatorConfig.GenericConfig.RESTOptionsGetter, + DualWriteBuilder: nil, // no dual writer + MetricsRegister: reg, + }, ) if err != nil { return nil, err diff --git a/pkg/services/apiserver/builder/common.go b/pkg/services/apiserver/builder/common.go index 746ee55e2c1..5e2f9b14ba9 100644 --- a/pkg/services/apiserver/builder/common.go +++ b/pkg/services/apiserver/builder/common.go @@ -11,6 +11,8 @@ import ( "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" + "github.com/prometheus/client_golang/prometheus" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) @@ -30,12 +32,7 @@ type APIGroupBuilder interface { // // The caller should share the apiGroupInfo passed into this function across builder versions of the same group. // UpdateAPIGroupInfo builds the group+version behavior updating the passed in apiGroupInfo in place - UpdateAPIGroupInfo( - apiGroupInfo *genericapiserver.APIGroupInfo, - scheme *runtime.Scheme, - optsGetter generic.RESTOptionsGetter, - dualWriteBuilder grafanarest.DualWriteBuilder, - ) error + UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts APIGroupOptions) error // Get OpenAPI definitions GetOpenAPIDefinitions() common.GetOpenAPIDefinitions @@ -49,6 +46,13 @@ type APIGroupBuilder interface { GetAuthorizer() authorizer.Authorizer } +type APIGroupOptions struct { + Scheme *runtime.Scheme + OptsGetter generic.RESTOptionsGetter + DualWriteBuilder grafanarest.DualWriteBuilder + MetricsRegister prometheus.Registerer +} + // Builders that implement OpenAPIPostProcessor are given a chance to modify the schema directly type OpenAPIPostProcessor interface { PostProcessOpenAPI(*spec3.OpenAPI) (*spec3.OpenAPI, error) diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index fd2a0e2fab9..ae5b214ecbf 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -235,7 +235,12 @@ func InstallAPIs( for group, buildersForGroup := range buildersGroupMap { g := genericapiserver.NewDefaultAPIGroupInfo(group, scheme, metav1.ParameterCodec, codecs) for _, b := range buildersForGroup { - if err := b.UpdateAPIGroupInfo(&g, scheme, optsGetter, dualWrite); err != nil { + if err := b.UpdateAPIGroupInfo(&g, APIGroupOptions{ + Scheme: scheme, + OptsGetter: optsGetter, + DualWriteBuilder: dualWrite, + MetricsRegister: reg, + }); err != nil { return err } if len(g.PrioritizedVersions) < 1 { From 5fb685dcc64b5f68b14992b3f0019206430d85fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:43:54 +0200 Subject: [PATCH 13/45] Scenes: add clearSceneCache to DashboardScenePageStateManager (#94227) --- .../dashboard-scene/pages/DashboardScenePageStateManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 3f7ade11335..b954cdd3ecc 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -255,6 +255,10 @@ export class DashboardScenePageStateManager extends StateManagerBase Date: Tue, 15 Oct 2024 09:44:41 +0200 Subject: [PATCH 14/45] CloudMigrations: create snapshot for Mute Timings (#94668) * CloudMigrations: Create snapshot for Mute Timings * CloudMigrations: add mute timings icon and copies to frontend --- .../cloudmigrationimpl/cloudmigration_test.go | 38 ++++++++-- .../cloudmigrationimpl/snapshot_mgmt.go | 19 ++++- .../snapshot_mgmt_alerts.go | 41 +++++++++++ .../snapshot_mgmt_alerts_test.go | 69 +++++++++++++++++++ .../migrate-to-cloud/onprem/NameCell.tsx | 2 + .../migrate-to-cloud/onprem/TypeCell.tsx | 2 + .../onprem/useNotifyOnSuccess.tsx | 2 + public/locales/en-US/grafana.json | 4 +- public/locales/pseudo-LOCALE/grafana.json | 4 +- 9 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go create mode 100644 pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index d087684946b..3b0825e67ae 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -34,6 +34,7 @@ import ( libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/metrics" + "github.com/grafana/grafana/pkg/services/ngalert/models" ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store" ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -773,7 +774,11 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi }, } - featureToggles := featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagDashboardRestore) + featureToggles := featuremgmt.WithFeatures( + featuremgmt.FlagOnPremToCloudMigrations, + featuremgmt.FlagOnPremToCloudMigrationsAlerts, + featuremgmt.FlagDashboardRestore, // needed for skipping creating soft-deleted dashboards in the snapshot. + ) kvStore := kvstore.ProvideService(sqlStore) @@ -793,12 +798,37 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi ) require.NoError(t, err) + var validConfig = `{ + "template_files": { + "a": "template" + }, + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email" + }, + "receivers": [{ + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [{ + "uid": "", + "name": "email receiver", + "type": "email", + "settings": { + "addresses": "" + } + }] + }] + } + }` + require.NoError(t, ng.Api.AlertingStore.SaveAlertmanagerConfiguration(context.Background(), &models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: validConfig, + OrgID: 1, + LastApplied: time.Now().Unix(), + })) + s, err := ProvideService( cfg, httpclient.NewProvider(), - featuremgmt.WithFeatures( - featuremgmt.FlagOnPremToCloudMigrations, - featuremgmt.FlagDashboardRestore), + featureToggles, sqlStore, dsService, secretskv.NewFakeSQLSecretsKVStore(t, sqlStore), diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 5c9df830110..94c82a101da 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -32,6 +32,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{ cloudmigration.FolderDataType, cloudmigration.LibraryElementDataType, cloudmigration.DashboardDataType, + cloudmigration.MuteTimingType, } func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) { @@ -58,9 +59,16 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + // Alerts: Mute Timings + muteTimings, err := s.getAlertMuteTimings(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert mute timings", "err", err) + return nil, err + } + migrationDataSlice := make( []cloudmigration.MigrateDataRequestItem, 0, - len(dataSources)+len(dashs)+len(folders)+len(libraryElements), + len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+len(muteTimings), ) for _, ds := range dataSources { @@ -107,6 +115,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + for _, muteTiming := range muteTimings { + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.MuteTimingType, + RefID: muteTiming.Name, + Name: muteTiming.Name, + Data: muteTiming, + }) + } + // Obtain the names of parent elements for Dashboard and Folders data types parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements) if err != nil { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go new file mode 100644 index 00000000000..8869882e8a6 --- /dev/null +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -0,0 +1,41 @@ +package cloudmigrationimpl + +import ( + "context" + "fmt" + + "github.com/prometheus/alertmanager/config" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" +) + +type muteTimeInterval struct { + // There is a lot of custom (de)serialization logic from Alertmanager, + // and this is the same type used by the underlying API, hence we can use the type as-is. + config.MuteTimeInterval `json:",inline"` +} + +func (s *Service) getAlertMuteTimings(ctx context.Context, signedInUser *user.SignedInUser) ([]muteTimeInterval, error) { + if !s.features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrationsAlerts) { + return nil, nil + } + + muteTimings, err := s.ngAlert.Api.MuteTimings.GetMuteTimings(ctx, signedInUser.OrgID) + if err != nil { + return nil, fmt.Errorf("fetching ngalert mute timings: %w", err) + } + + muteTimeIntervals := make([]muteTimeInterval, 0, len(muteTimings)) + + for _, muteTiming := range muteTimings { + muteTimeIntervals = append(muteTimeIntervals, muteTimeInterval{ + MuteTimeInterval: config.MuteTimeInterval{ + Name: muteTiming.Name, + TimeIntervals: muteTiming.TimeIntervals, + }, + }) + } + + return muteTimeIntervals, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go new file mode 100644 index 00000000000..7ff8cadc1bc --- /dev/null +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -0,0 +1,69 @@ +package cloudmigrationimpl + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/user" +) + +func TestGetAlertMuteTimings(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("when the feature flag `onPremToCloudMigrationsAlerts` is not enabled it returns nil", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations) + + muteTimeIntervals, err := s.getAlertMuteTimings(ctx, nil) + require.NoError(t, err) + require.Nil(t, muteTimeIntervals) + }) + + t.Run("when the feature flag `onPremToCloudMigrationsAlerts` is enabled it returns the mute timings", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts) + + var orgID int64 = 1 + user := &user.SignedInUser{OrgID: orgID} + + createdMuteTiming := createMuteTiming(t, ctx, s, orgID) + + muteTimeIntervals, err := s.getAlertMuteTimings(ctx, user) + require.NoError(t, err) + require.NotNil(t, muteTimeIntervals) + require.Len(t, muteTimeIntervals, 1) + require.Equal(t, createdMuteTiming.Name, muteTimeIntervals[0].Name) + }) +} + +func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.MuteTimeInterval { + t.Helper() + + muteTiming := `{ + "name": "My Unique MuteTiming 1", + "time_intervals": [ + { + "times": [{"start_time": "12:12","end_time": "23:23"}], + "weekdays": ["monday","wednesday","friday","sunday"], + "days_of_month": ["10:20","25:-1"], + "months": ["1:6","10:12"], + "years": ["2022:2054"], + "location": "Africa/Douala" + } + ] + }` + + var mt definitions.MuteTimeInterval + require.NoError(t, json.Unmarshal([]byte(muteTiming), &mt)) + + createdTiming, err := service.ngAlert.Api.MuteTimings.CreateMuteTiming(ctx, mt, orgID) + require.NoError(t, err) + + return createdTiming +} diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index b978b1d1b27..94fd342ad6c 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -219,6 +219,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) { return ; case 'LIBRARY_ELEMENT': return ; + case 'MUTE_TIMING': + return ; default: return undefined; } diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx index 37e962d7b53..ca8e29b046a 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -13,6 +13,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) { return t('migrate-to-cloud.resource-type.folder', 'Folder'); case 'LIBRARY_ELEMENT': return t('migrate-to-cloud.resource-type.library_element', 'Library Element'); + case 'MUTE_TIMING': + return t('migrate-to-cloud.resource-type.mute_timing', 'Mute Timing'); default: return t('migrate-to-cloud.resource-type.unknown', 'Unknown'); } diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index 705da66b693..0f50d7b6fe0 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -52,6 +52,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { types.push(t('migrate-to-cloud.migrated-counts.folders', 'folders')); } else if (type === 'LIBRARY_ELEMENT') { types.push(t('migrate-to-cloud.migrated-counts.library_elements', 'library elements')); + } else if (type === 'MUTE_TIMING') { + types.push(t('migrate-to-cloud.migrated-counts.mute_timings', 'mute timings')); } distinctItems += 1; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 376282bc21d..03f75af7079 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1408,7 +1408,8 @@ "dashboards": "dashboards", "datasources": "data sources", "folders": "folders", - "library_elements": "library elements" + "library_elements": "library elements", + "mute_timings": "mute timings" }, "migration-token": { "delete-button": "Delete token", @@ -1492,6 +1493,7 @@ "datasource": "Data source", "folder": "Folder", "library_element": "Library Element", + "mute_timing": "Mute Timing", "unknown": "Unknown" }, "summary": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 5a68f130569..b65e42aa873 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1408,7 +1408,8 @@ "dashboards": "đäşĥþőäřđş", "datasources": "đäŧä şőūřčęş", "folders": "ƒőľđęřş", - "library_elements": "ľįþřäřy ęľęmęʼnŧş" + "library_elements": "ľįþřäřy ęľęmęʼnŧş", + "mute_timings": "mūŧę ŧįmįʼnģş" }, "migration-token": { "delete-button": "Đęľęŧę ŧőĸęʼn", @@ -1492,6 +1493,7 @@ "datasource": "Đäŧä şőūřčę", "folder": "Főľđęř", "library_element": "Ŀįþřäřy Ēľęmęʼnŧ", + "mute_timing": "Mūŧę Ŧįmįʼnģ", "unknown": "Ůʼnĸʼnőŵʼn" }, "summary": { From ef805f271e50d5df4b142f6e7012213b49565536 Mon Sep 17 00:00:00 2001 From: oksanaphmn Date: Tue, 15 Oct 2024 12:38:09 +0300 Subject: [PATCH 15/45] docs: Readme (#94657) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f111b8c58cb..7322e6a1c7d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ If you're interested in contributing to the Grafana project: ## Get involved -- Follow [@grafana on Twitter](https://twitter.com/grafana/). +- Follow [@grafana on X (formerly Twitter)](https://x.com/grafana/). - Read and subscribe to the [Grafana blog](https://grafana.com/blog/). - If you have a specific question, check out our [discussion forums](https://community.grafana.com/). - For general discussions, join us on the [official Slack](https://slack.grafana.com) team. From aaba5a43bd2aa3d29955767b678ed6a02a1f7100 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:39:36 +0300 Subject: [PATCH 16/45] DashboardScene: Rerender dashboard links on timerange change (#94570) * fix * refactor * refactor --- .../scene/DashboardControls.test.tsx | 65 +++--------- .../scene/DashboardControls.tsx | 5 +- .../scene/DashboardLinksControls.test.tsx | 100 ++++++++++++++++++ .../scene/DashboardLinksControls.tsx | 10 +- 4 files changed, 123 insertions(+), 57 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/DashboardLinksControls.test.tsx diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx index 34c5f3fe057..878f6cc5e87 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx @@ -1,36 +1,22 @@ -import { act, render } from '@testing-library/react'; +import { render } from '@testing-library/react'; -import { toUtc } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { SceneDataLayerControls, SceneVariableSet, TextBoxVariable, VariableValueSelectors } from '@grafana/scenes'; import { DashboardControls, DashboardControlsState } from './DashboardControls'; import { DashboardScene } from './DashboardScene'; -const mockGetAnchorInfo = jest.fn((link) => ({ - href: `/dashboard/${link.title}`, - title: link.title, - tooltip: link.tooltip || null, -})); - -// Mock the getLinkSrv function -jest.mock('app/features/panel/panellinks/link_srv', () => ({ - getLinkSrv: jest.fn(() => ({ - getAnchorInfo: mockGetAnchorInfo, - })), -})); - describe('DashboardControls', () => { describe('Given a standard scene', () => { it('should initialize with default values', () => { - const { controls: scene } = buildTestScene(); + const scene = buildTestScene(); expect(scene.state.variableControls).toEqual([]); expect(scene.state.timePicker).toBeDefined(); expect(scene.state.refreshPicker).toBeDefined(); }); it('should return if time controls are hidden', () => { - const { controls: scene } = buildTestScene({ + const scene = buildTestScene({ hideTimeControls: false, hideVariableControls: false, hideLinksControls: false, @@ -45,14 +31,14 @@ describe('DashboardControls', () => { describe('Component', () => { it('should render', () => { - const { controls: scene } = buildTestScene(); + const scene = buildTestScene(); expect(() => { render(); }).not.toThrow(); }); it('should render visible controls', async () => { - const { controls: scene } = buildTestScene({ + const scene = buildTestScene({ variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], }); const renderer = render(); @@ -65,7 +51,7 @@ describe('DashboardControls', () => { }); it('should render with hidden controls', async () => { - const { controls: scene } = buildTestScene({ + const scene = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true, @@ -79,13 +65,13 @@ describe('DashboardControls', () => { describe('UrlSync', () => { it('should return keys', () => { - const { controls: scene } = buildTestScene(); + const scene = buildTestScene(); // @ts-expect-error expect(scene._urlSync.getKeys()).toEqual(['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks']); }); it('should not return url state for hide flags', () => { - const { controls: scene } = buildTestScene(); + const scene = buildTestScene(); expect(scene.getUrlState()).toEqual({}); scene.setState({ hideTimeControls: true, @@ -96,7 +82,7 @@ describe('DashboardControls', () => { }); it('should update from url', () => { - const { controls: scene } = buildTestScene(); + const scene = buildTestScene(); scene.updateFromUrl({ '_dash.hideTimePicker': 'true', '_dash.hideVariables': 'true', @@ -116,7 +102,7 @@ describe('DashboardControls', () => { }); it('should not override state if no new state comes from url', () => { - const { controls: scene } = buildTestScene({ + const scene = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true, @@ -128,7 +114,7 @@ describe('DashboardControls', () => { }); it('should not call setState if no changes', () => { - const { controls: scene } = buildTestScene({ + const scene = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true, @@ -144,34 +130,9 @@ describe('DashboardControls', () => { expect(setState).toHaveBeenCalledTimes(0); }); }); - - it('Should update link hrefs when time range changes', () => { - const { controls, dashboard } = buildTestScene(); - render(); - - //clear initial calls to getAnchorInfo - mockGetAnchorInfo.mockClear(); - - act(() => { - // Update time range - dashboard.state.$timeRange?.setState({ - value: { - from: toUtc('2021-01-01'), - to: toUtc('2021-01-02'), - raw: { from: toUtc('2020-01-01'), to: toUtc('2020-01-02') }, - }, - }); - }); - - //expect getAnchorInfo to be called after time range change - expect(mockGetAnchorInfo).toHaveBeenCalledTimes(1); - }); }); -function buildTestScene(state?: Partial): { - dashboard: DashboardScene; - controls: DashboardControls; -} { +function buildTestScene(state?: Partial): DashboardControls { const variable = new TextBoxVariable({ name: 'A', label: 'A', @@ -206,5 +167,5 @@ function buildTestScene(state?: Partial): { dashboard.activate(); variable.activate(); - return { dashboard, controls: dashboard.state.controls as DashboardControls }; + return dashboard.state.controls as DashboardControls; } diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 41e125957a5..78fbc64afd8 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -122,10 +122,9 @@ function DashboardControlsRenderer({ model }: SceneComponentProps {!hideVariableControls && variableControls.map((c) => )} - {!hideLinksControls && !editPanel && } + {!hideLinksControls && !editPanel && } {editPanel && } {!hideTimeControls && ( diff --git a/public/app/features/dashboard-scene/scene/DashboardLinksControls.test.tsx b/public/app/features/dashboard-scene/scene/DashboardLinksControls.test.tsx new file mode 100644 index 00000000000..d6d89a2e63f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardLinksControls.test.tsx @@ -0,0 +1,100 @@ +import { act, render } from '@testing-library/react'; + +import { toUtc } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { SceneTimeRange } from '@grafana/scenes'; + +import { DashboardControls } from './DashboardControls'; +import { DashboardScene } from './DashboardScene'; + +const mockGetAnchorInfo = jest.fn((link) => ({ + href: `/dashboard/${link.title}`, + title: link.title, + tooltip: link.tooltip || null, +})); + +// Mock the getLinkSrv function +jest.mock('app/features/panel/panellinks/link_srv', () => ({ + getLinkSrv: jest.fn(() => ({ + getAnchorInfo: mockGetAnchorInfo, + })), +})); + +describe('DashboardLinksControls', () => { + it('renders dashboard links correctly', () => { + const { controls } = buildTestScene(); + const renderer = render(); + + // // Expect two dashboard link containers to be rendered + const linkContainers = renderer.getAllByTestId(selectors.components.DashboardLinks.container); + expect(linkContainers).toHaveLength(2); + + // Check link titles and hrefs + const links = renderer.getAllByTestId(selectors.components.DashboardLinks.link); + expect(links[0]).toHaveTextContent('Link 1'); + expect(links[1]).toHaveTextContent('Link 2'); + }); + + it('updates link hrefs when time range changes', () => { + const { controls, dashboard } = buildTestScene(); + render(); + + //clear initial calls to getAnchorInfo + mockGetAnchorInfo.mockClear(); + + act(() => { + // Update time range + dashboard.state.$timeRange?.setState({ + value: { + from: toUtc('2021-01-01'), + to: toUtc('2021-01-02'), + raw: { from: toUtc('2020-01-01'), to: toUtc('2020-01-02') }, + }, + }); + }); + + //expect getAnchorInfo to be called twice, once for each link, after time range change + expect(mockGetAnchorInfo).toHaveBeenCalledTimes(2); + }); +}); + +function buildTestScene(): { controls: DashboardControls; dashboard: DashboardScene } { + const dashboard = new DashboardScene({ + uid: 'A', + links: [ + { + title: 'Link 1', + url: 'http://localhost:3000/$A', + type: 'link', + asDropdown: false, + icon: '', + includeVars: true, + keepTime: true, + tags: [], + targetBlank: false, + tooltip: 'Link 1', + }, + { + title: 'Link 2', + url: 'http://localhost:3000/$A', + type: 'link', + asDropdown: false, + icon: '', + includeVars: true, + keepTime: true, + tags: [], + targetBlank: false, + tooltip: 'Link 2', + }, + ], + controls: new DashboardControls({}), + $timeRange: new SceneTimeRange({ + from: 'now-1', + to: 'now', + }), + }); + + dashboard.activate(); + + return { controls: dashboard.state.controls as DashboardControls, dashboard }; +} diff --git a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx index 0337580526a..72a046fc6e5 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx @@ -1,5 +1,6 @@ import { sanitizeUrl } from '@grafana/data/src/text/sanitize'; import { selectors } from '@grafana/e2e-selectors'; +import { sceneGraph } from '@grafana/scenes'; import { DashboardLink } from '@grafana/schema'; import { Tooltip } from '@grafana/ui'; import { @@ -10,12 +11,17 @@ import { getLinkSrv } from 'app/features/panel/panellinks/link_srv'; import { LINK_ICON_MAP } from '../settings/links/utils'; +import { DashboardScene } from './DashboardScene'; + export interface Props { links: DashboardLink[]; - uid?: string; + dashboard: DashboardScene; } -export function DashboardLinksControls({ links, uid }: Props) { +export function DashboardLinksControls({ links, dashboard }: Props) { + sceneGraph.getTimeRange(dashboard).useState(); + const uid = dashboard.state.uid; + if (!links || !uid) { return null; } From 284c2d6f7184e921a522a0af027500e67b6f9b0d Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 15 Oct 2024 10:45:07 +0100 Subject: [PATCH 17/45] Folders: Correctly show new folder button under root folder (#94687) show new folder button under root folder if nested folders are disabled and user has the right perms --- public/app/features/browse-dashboards/permissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/browse-dashboards/permissions.ts b/public/app/features/browse-dashboards/permissions.ts index d4043434bce..3347fb9f442 100644 --- a/public/app/features/browse-dashboards/permissions.ts +++ b/public/app/features/browse-dashboards/permissions.ts @@ -8,7 +8,7 @@ function checkFolderPermission(action: AccessControlAction, folderDTO?: FolderDT function checkCanCreateFolders(folderDTO?: FolderDTO) { // Can only create a folder if we have permissions and either we're at root or nestedFolders is enabled - if (folderDTO && !config.featureToggles.nestedFolders) { + if (folderDTO && folderDTO.uid !== 'general' && !config.featureToggles.nestedFolders) { return false; } From f97f489c2c580c8bfd24351e59279b6bdae3ff7a Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 15 Oct 2024 11:35:45 +0100 Subject: [PATCH 18/45] Plugins: Skip install errors if dependency plugin already exists (#94710) * skip install errors if dependency plugin already exists * add test --- pkg/plugins/manager/installer.go | 5 +++ pkg/plugins/manager/installer_test.go | 44 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pkg/plugins/manager/installer.go b/pkg/plugins/manager/installer.go index b1db5662c51..227d8a35450 100644 --- a/pkg/plugins/manager/installer.go +++ b/pkg/plugins/manager/installer.go @@ -75,6 +75,11 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt err = m.Add(ctx, dep.ID, dep.Version, opts) if err != nil { + var dupeErr plugins.DuplicateError + if errors.As(err, &dupeErr) { + m.log.Info("Dependency already installed", "pluginId", dep.ID) + continue + } return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err) } } diff --git a/pkg/plugins/manager/installer_test.go b/pkg/plugins/manager/installer_test.go index f90c3ba11c3..bb4564c7d25 100644 --- a/pkg/plugins/manager/installer_test.go +++ b/pkg/plugins/manager/installer_test.go @@ -279,6 +279,50 @@ func TestPluginManager_Add_Remove(t *testing.T) { require.NoError(t, err) require.Equal(t, []string{p2Zip, p1Zip}, loadedPaths) }) + + t.Run("Plugin can successfully install even if dependency plugin is already installed", func(t *testing.T) { + const pluginDependencyID = "test-plugin-dependency" + reg := &fakes.FakePluginRegistry{ + Store: map[string]*plugins.Plugin{ + pluginDependencyID: createPlugin(t, pluginDependencyID, plugins.ClassExternal, false, false), + }, + } + + var loadedPaths []string + loader := &fakes.FakeLoader{ + LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { + loadedPaths = append(loadedPaths, src.PluginURIs(ctx)...) + return []*plugins.Plugin{}, nil + }, + } + + pluginRepo := &fakes.FakePluginRepo{ + GetPluginArchiveFunc: func(_ context.Context, id, version string, _ repo.CompatOpts) (*repo.PluginArchive, error) { + return &repo.PluginArchive{File: &zip.ReadCloser{Reader: zip.Reader{File: []*zip.File{{ + FileHeader: zip.FileHeader{Name: fmt.Sprintf("%s.zip", id)}, + }}}}}, nil + }, + } + + fs := &fakes.FakePluginStorage{ + ExtractFunc: func(_ context.Context, id string, _ storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { + switch id { + case testPluginID: + return &storage.ExtractedPluginArchive{ + Dependencies: []*storage.Dependency{{ID: pluginDependencyID}}, + Path: "test-plugin.zip", + }, nil + default: + return nil, fmt.Errorf("unknown plugin %s", id) + } + }, + } + + inst := New(reg, loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) + err := inst.Add(context.Background(), testPluginID, "", testCompatOpts()) + require.NoError(t, err) + require.Equal(t, []string{"test-plugin.zip"}, loadedPaths) + }) } func createPlugin(t *testing.T, pluginID string, class plugins.Class, managed, backend bool, cbs ...func(*plugins.Plugin)) *plugins.Plugin { From 016dea11434a3269c565a3f5b2615bb03e856cc4 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Tue, 15 Oct 2024 12:55:33 +0200 Subject: [PATCH 19/45] CloudMigrations: create snapshot for Notification Templates (#94676) * CloudMigrations: create snapshot for Notification Templates * CloudMigrations: add notification templates resource to frontend --- .../cloudmigrationimpl/cloudmigration_test.go | 3 -- .../cloudmigrationimpl/snapshot_mgmt.go | 20 ++++++++- .../snapshot_mgmt_alerts.go | 27 ++++++++++++ .../snapshot_mgmt_alerts_test.go | 42 +++++++++++++++++++ .../migrate-to-cloud/onprem/NameCell.tsx | 2 + .../migrate-to-cloud/onprem/TypeCell.tsx | 2 + .../onprem/useNotifyOnSuccess.tsx | 2 + public/locales/en-US/grafana.json | 4 +- public/locales/pseudo-LOCALE/grafana.json | 4 +- 9 files changed, 100 insertions(+), 6 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 3b0825e67ae..cbe6b060754 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -799,9 +799,6 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi require.NoError(t, err) var validConfig = `{ - "template_files": { - "a": "template" - }, "alertmanager_config": { "route": { "receiver": "grafana-default-email" diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 94c82a101da..952106e1608 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -33,6 +33,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{ cloudmigration.LibraryElementDataType, cloudmigration.DashboardDataType, cloudmigration.MuteTimingType, + cloudmigration.NotificationTemplateType, } func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) { @@ -66,9 +67,17 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + // Alerts: Notification Templates + notificationTemplates, err := s.getNotificationTemplates(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert notification templates", "err", err) + return nil, err + } + migrationDataSlice := make( []cloudmigration.MigrateDataRequestItem, 0, - len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+len(muteTimings), + len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+ + len(muteTimings)+len(notificationTemplates), ) for _, ds := range dataSources { @@ -124,6 +133,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + for _, notificationTemplate := range notificationTemplates { + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.NotificationTemplateType, + RefID: notificationTemplate.Name, + Name: notificationTemplate.Name, + Data: notificationTemplate, + }) + } + // Obtain the names of parent elements for Dashboard and Folders data types parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements) if err != nil { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go index 8869882e8a6..94879e227f2 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -39,3 +39,30 @@ func (s *Service) getAlertMuteTimings(ctx context.Context, signedInUser *user.Si return muteTimeIntervals, nil } + +type notificationTemplate struct { + Name string `json:"name"` + Template string `json:"template"` +} + +func (s *Service) getNotificationTemplates(ctx context.Context, signedInUser *user.SignedInUser) ([]notificationTemplate, error) { + if !s.features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrationsAlerts) { + return nil, nil + } + + templates, err := s.ngAlert.Api.Templates.GetTemplates(ctx, signedInUser.OrgID) + if err != nil { + return nil, fmt.Errorf("fetching ngalert notification templates: %w", err) + } + + notificationTemplates := make([]notificationTemplate, 0, len(templates)) + + for _, template := range templates { + notificationTemplates = append(notificationTemplates, notificationTemplate{ + Name: template.Name, + Template: template.Template, + }) + } + + return notificationTemplates, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index 7ff8cadc1bc..e1fed6720f9 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -42,6 +42,36 @@ func TestGetAlertMuteTimings(t *testing.T) { }) } +func TestGetNotificationTemplates(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("when the feature flag `onPremToCloudMigrationsAlerts` is not enabled it returns nil", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations) + + notificationTemplates, err := s.getNotificationTemplates(ctx, nil) + require.NoError(t, err) + require.Nil(t, notificationTemplates) + }) + + t.Run("when the feature flag `onPremToCloudMigrationsAlerts` is enabled it returns the notification templates", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts) + + var orgID int64 = 1 + user := &user.SignedInUser{OrgID: orgID} + + createdTemplate := createNotificationTemplate(t, ctx, s, orgID) + + notificationTemplates, err := s.getNotificationTemplates(ctx, user) + require.NoError(t, err) + require.NotNil(t, notificationTemplates) + require.Len(t, notificationTemplates, 1) + require.Equal(t, createdTemplate.Name, notificationTemplates[0].Name) + }) +} + func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.MuteTimeInterval { t.Helper() @@ -67,3 +97,15 @@ func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID return createdTiming } + +func createNotificationTemplate(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.NotificationTemplate { + tmpl := definitions.NotificationTemplate{ + Name: "MyTestNotificationTemplate", + Template: "This is a test template\n{{ .ExternalURL }}", + } + + createdTemplate, err := service.ngAlert.Api.Templates.CreateTemplate(ctx, orgID, tmpl) + require.NoError(t, err) + + return createdTemplate +} diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index 94fd342ad6c..3f5ea7984ed 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -221,6 +221,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) { return ; case 'MUTE_TIMING': return ; + case 'NOTIFICATION_TEMPLATE': + return ; default: return undefined; } diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx index ca8e29b046a..afecc880de4 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -15,6 +15,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) { return t('migrate-to-cloud.resource-type.library_element', 'Library Element'); case 'MUTE_TIMING': return t('migrate-to-cloud.resource-type.mute_timing', 'Mute Timing'); + case 'NOTIFICATION_TEMPLATE': + return t('migrate-to-cloud.resource-type.notification_template', 'Notification Template'); default: return t('migrate-to-cloud.resource-type.unknown', 'Unknown'); } diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index 0f50d7b6fe0..e83c6c5b553 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -54,6 +54,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { types.push(t('migrate-to-cloud.migrated-counts.library_elements', 'library elements')); } else if (type === 'MUTE_TIMING') { types.push(t('migrate-to-cloud.migrated-counts.mute_timings', 'mute timings')); + } else if (type === 'NOTIFICATION_TEMPLATE') { + types.push(t('migrate-to-cloud.migrated-counts.notification_templates', 'notification templates')); } distinctItems += 1; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 03f75af7079..bd56d8aff4e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1409,7 +1409,8 @@ "datasources": "data sources", "folders": "folders", "library_elements": "library elements", - "mute_timings": "mute timings" + "mute_timings": "mute timings", + "notification_templates": "notification templates" }, "migration-token": { "delete-button": "Delete token", @@ -1494,6 +1495,7 @@ "folder": "Folder", "library_element": "Library Element", "mute_timing": "Mute Timing", + "notification_template": "Notification Template", "unknown": "Unknown" }, "summary": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index b65e42aa873..a6f27524603 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1409,7 +1409,8 @@ "datasources": "đäŧä şőūřčęş", "folders": "ƒőľđęřş", "library_elements": "ľįþřäřy ęľęmęʼnŧş", - "mute_timings": "mūŧę ŧįmįʼnģş" + "mute_timings": "mūŧę ŧįmįʼnģş", + "notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş" }, "migration-token": { "delete-button": "Đęľęŧę ŧőĸęʼn", @@ -1494,6 +1495,7 @@ "folder": "Főľđęř", "library_element": "Ŀįþřäřy Ēľęmęʼnŧ", "mute_timing": "Mūŧę Ŧįmįʼnģ", + "notification_template": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧę", "unknown": "Ůʼnĸʼnőŵʼn" }, "summary": { From 0841497cad90f8662a623e8ea14090d18508f139 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Tue, 15 Oct 2024 12:00:20 +0100 Subject: [PATCH 20/45] Alerting: Show a warning when a template has been potentially misconfigured (#94698) --- .../components/common/TextVariants.tsx | 1 + .../contact-points/ContactPoints.test.tsx | 7 + .../__mocks__/alertmanager.config.mock.json | 7 +- .../useNotificationTemplates.ts | 4 + .../receivers/NewReceiverView.test.tsx | 6 +- .../components/receivers/TemplatesTable.tsx | 28 ++- .../NewReceiverView.test.tsx.snap | 231 ++++++++---------- .../form/fields/TemplateSelector.test.tsx | 3 +- public/locales/en-US/grafana.json | 5 + public/locales/pseudo-LOCALE/grafana.json | 5 + 10 files changed, 159 insertions(+), 138 deletions(-) diff --git a/public/app/features/alerting/unified/components/common/TextVariants.tsx b/public/app/features/alerting/unified/components/common/TextVariants.tsx index 3842c2b8287..1df419b883d 100644 --- a/public/app/features/alerting/unified/components/common/TextVariants.tsx +++ b/public/app/features/alerting/unified/components/common/TextVariants.tsx @@ -6,3 +6,4 @@ import { Text } from '@grafana/ui'; export const PrimaryText = ({ content }: { content: string }) => {content}; +export const CodeText = ({ content }: { content: string }) => {content}; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index fa70dacd4ab..dea25168694 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -126,6 +126,13 @@ describe('contact points', () => { }); }); + describe('templates tab', () => { + it('shows a warning when a template is misconfigured', async () => { + renderWithProvider(, { initialEntries: ['/?tab=templates'] }); + expect((await screen.findAllByText(/^misconfigured$/i))[0]).toBeInTheDocument(); + }); + }); + it('should show / hide loading states, have all actions enabled', async () => { renderWithProvider(); diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json index 784c8ca555f..0077f89f3a0 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json @@ -3,10 +3,13 @@ "slack-template": "{{ define \"slack-template\" }} Custom slack template {{ end }}", "custom-email": "{{ define \"custom-email\" }} Custom email template {{ end }}", "provisioned-template": "{{ define \"provisioned-template\" }} Custom provisioned template {{ end }}", - "template with spaces": "{{ define \"template with spaces\" }} Custom template with spaces in the name {{ end }}" + "template with spaces": "{{ define \"template with spaces\" }} Custom template with spaces in the name {{ end }}", + "misconfigured-template": "{{ define \"misconfigured template\" }} Template that is defined in template_files but not templates {{ end }}", + "misconfigured and provisioned": "{{ define \"misconfigured and provisioned template\" }} Provisioned template that is defined in template_files but not templates {{ end }}" }, "template_file_provenances": { - "provisioned-template": "api" + "provisioned-template": "api", + "misconfigured and provisioned": "api" }, "alertmanager_config": { "route": { diff --git a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts index e98aca41847..f1d15f6dfe5 100644 --- a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts +++ b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts @@ -26,6 +26,7 @@ export interface NotificationTemplate { title: string; content: string; provenance: string; + missing?: boolean; } const { useGetAlertmanagerConfigurationQuery, useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; @@ -84,12 +85,15 @@ function templateGroupToTemplate( } function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemplate[] { + const { alertmanager_config } = config; + const { templates = [] } = alertmanager_config; return Object.entries(config.template_files).map(([title, content]) => ({ uid: title, title, content, // Undefined, null or empty string should be converted to PROVENANCE_NONE provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE, + missing: !templates.includes(title), })); } diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx index a7d1df7ffb5..93a0114bc17 100644 --- a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx @@ -97,7 +97,11 @@ describe('alerting API server disabled', () => { ); const testBody = await testRequest?.json(); - const saveBody = await saveRequest?.json(); + const fullSaveBody = await saveRequest?.json(); + + // Only snapshot and check the receivers, as we don't want other tests to break this + // just because we added something new to the mock config + const saveBody = fullSaveBody.alertmanager_config.receivers; expect([testBody]).toMatchSnapshot(); expect([saveBody]).toMatchSnapshot(); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index 240997db6b7..c644a3b5fb7 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -1,8 +1,10 @@ import { Fragment, useState } from 'react'; import { logError } from '@grafana/runtime'; -import { ConfirmModal, useStyles2 } from '@grafana/ui'; +import { Badge, ConfirmModal, Tooltip, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { t, Trans } from 'app/core/internationalization'; +import { CodeText } from 'app/features/alerting/unified/components/common/TextVariants'; import { Authorize } from '../../components/Authorize'; import { AlertmanagerAction } from '../../hooks/useAbilities'; @@ -116,8 +118,8 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic const [isExpanded, setIsExpanded] = useState(false); const { isProvisioned } = useNotificationTemplateMetadata(notificationTemplate); - const { uid, title: name, content: template } = notificationTemplate; - + const { uid, title: name, content: template, missing } = notificationTemplate; + const misconfiguredBadgeText = t('alerting.templates.misconfigured-badge-text', 'Misconfigured'); return ( @@ -125,7 +127,25 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic setIsExpanded(!isExpanded)} /> - {name} {isProvisioned && } + {name} {isProvisioned && }{' '} + {missing && ( + + This template is misconfigured. +
+ + Templates must be defined in both the and{' '} + sections of your alertmanager configuration. + + + } + > + + + +
+ )} {isProvisioned && ( diff --git a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap index 681e1ee181f..ededb25feb5 100644 --- a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap +++ b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap @@ -34,142 +34,113 @@ exports[`alerting API server disabled should be able to test and save a receiver exports[`alerting API server disabled should be able to test and save a receiver 2`] = ` [ - { - "alertmanager_config": { - "mute_time_intervals": [], - "receivers": [ + [ + { + "grafana_managed_receiver_configs": [ { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "grafana-default-email", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "xeKQrBrnk", - }, - ], + "disableResolveMessage": false, "name": "grafana-default-email", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "provisioned-contact-point", - "provenance": "api", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "s8SdCVjnk", - }, - ], - "name": "provisioned-contact-point", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "lotsa-emails", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", - }, - ], - "name": "lotsa-emails", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "Slack with multiple channels", - "secureFields": { - "token": true, - }, - "settings": { - "recipient": "test-alerts", - }, - "type": "slack", - "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", - }, - { - "disableResolveMessage": false, - "name": "Slack with multiple channels", - "secureFields": { - "token": true, - }, - "settings": { - "recipient": "test-alerts2", - }, - "type": "slack", - "uid": "b286a3be-f690-49e2-8605-b075cbace2df", - }, - ], - "name": "Slack with multiple channels", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "Oncall-integration", - "settings": { - "url": "https://oncall-endpoint.example.com", - }, - "type": "oncall", - }, - ], - "name": "OnCall Conctact point", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "my new receiver", - "secureSettings": {}, - "settings": { - "addresses": "tester@grafana.com", - "singleEmail": false, - }, - "type": "email", - }, - ], - "name": "my new receiver", - }, - ], - "route": { - "receiver": "grafana-default-email", - "routes": [ - { - "receiver": "provisioned-contact-point", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, }, - ], - }, - "templates": [ - "slack-template", - "custom-email", - "provisioned-template", - "template with spaces", + "type": "email", + "uid": "xeKQrBrnk", + }, ], - "time_intervals": [], + "name": "grafana-default-email", }, - "template_file_provenances": { - "provisioned-template": "api", + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "provisioned-contact-point", + "provenance": "api", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "s8SdCVjnk", + }, + ], + "name": "provisioned-contact-point", }, - "template_files": { - "custom-email": "{{ define "custom-email" }} Custom email template {{ end }}", - "provisioned-template": "{{ define "provisioned-template" }} Custom provisioned template {{ end }}", - "slack-template": "{{ define "slack-template" }} Custom slack template {{ end }}", - "template with spaces": "{{ define "template with spaces" }} Custom template with spaces in the name {{ end }}", + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "lotsa-emails", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", + }, + ], + "name": "lotsa-emails", }, - }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts", + }, + "type": "slack", + "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", + }, + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts2", + }, + "type": "slack", + "uid": "b286a3be-f690-49e2-8605-b075cbace2df", + }, + ], + "name": "Slack with multiple channels", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "Oncall-integration", + "settings": { + "url": "https://oncall-endpoint.example.com", + }, + "type": "oncall", + }, + ], + "name": "OnCall Conctact point", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "my new receiver", + "secureSettings": {}, + "settings": { + "addresses": "tester@grafana.com", + "singleEmail": false, + }, + "type": "email", + }, + ], + "name": "my new receiver", + }, + ], ] `; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx index 73b8d455436..03266950cac 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { render, screen, userEvent } from 'test/test-utils'; import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types'; +import alertmanagerConfigMock from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; @@ -98,7 +99,7 @@ describe('TemplatesPicker', () => { const input = screen.getByRole('combobox'); expect(screen.queryByText('slack-template')).not.toBeInTheDocument(); await userEvent.click(input); - expect(screen.getAllByRole('option')).toHaveLength(7); // 4 templates in mock plus 3 in the default template + expect(screen.getAllByRole('option')).toHaveLength(Object.keys(alertmanagerConfigMock.template_files).length + 3); // 4 templates in mock plus 3 in the default template const template = screen.getByRole('option', { name: 'slack-template' }); await userEvent.click(template); expect(screen.getByText('slack-template')).toBeInTheDocument(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bd56d8aff4e..09aeebf2b0d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "TO" } + }, + "templates": { + "misconfigured-badge-text": "Misconfigured", + "misconfigured-warning": "This template is misconfigured.", + "misconfigured-warning-details": "Templates must be defined in both the <1> and <4> sections of your alertmanager configuration." } }, "annotations": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index a6f27524603..281d8fd3cbf 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "ŦØ" } + }, + "templates": { + "misconfigured-badge-text": "Mįşčőʼnƒįģūřęđ", + "misconfigured-warning": "Ŧĥįş ŧęmpľäŧę įş mįşčőʼnƒįģūřęđ.", + "misconfigured-warning-details": "Ŧęmpľäŧęş mūşŧ þę đęƒįʼnęđ įʼn þőŧĥ ŧĥę <1> äʼnđ <4> şęčŧįőʼnş őƒ yőūř äľęřŧmäʼnäģęř čőʼnƒįģūřäŧįőʼn." } }, "annotations": { From 940a9e014492a3b437064f319767c48b8e702473 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:20:18 +0300 Subject: [PATCH 21/45] I18n: Download translations from Crowdin (#94707) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 11 ++++++++++- public/locales/es-ES/grafana.json | 11 ++++++++++- public/locales/fr-FR/grafana.json | 11 ++++++++++- public/locales/pt-BR/grafana.json | 11 ++++++++++- public/locales/zh-Hans/grafana.json | 11 ++++++++++- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index ac7243cb160..2071409b755 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "" } + }, + "templates": { + "misconfigured-badge-text": "", + "misconfigured-warning": "", + "misconfigured-warning-details": "" } }, "annotations": { @@ -1408,7 +1413,9 @@ "dashboards": "", "datasources": "", "folders": "", - "library_elements": "" + "library_elements": "", + "mute_timings": "", + "notification_templates": "" }, "migration-token": { "delete-button": "", @@ -1492,6 +1499,8 @@ "datasource": "Datenquelle", "folder": "Ordner", "library_element": "", + "mute_timing": "", + "notification_template": "", "unknown": "Unbekannt" }, "summary": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index fde74558ed1..8a2f3b9e438 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "" } + }, + "templates": { + "misconfigured-badge-text": "", + "misconfigured-warning": "", + "misconfigured-warning-details": "" } }, "annotations": { @@ -1408,7 +1413,9 @@ "dashboards": "", "datasources": "", "folders": "", - "library_elements": "" + "library_elements": "", + "mute_timings": "", + "notification_templates": "" }, "migration-token": { "delete-button": "", @@ -1492,6 +1499,8 @@ "datasource": "Fuente de datos", "folder": "", "library_element": "", + "mute_timing": "", + "notification_template": "", "unknown": "Desconocido" }, "summary": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 744b041a763..c302a57f275 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "" } + }, + "templates": { + "misconfigured-badge-text": "", + "misconfigured-warning": "", + "misconfigured-warning-details": "" } }, "annotations": { @@ -1408,7 +1413,9 @@ "dashboards": "", "datasources": "", "folders": "", - "library_elements": "" + "library_elements": "", + "mute_timings": "", + "notification_templates": "" }, "migration-token": { "delete-button": "", @@ -1492,6 +1499,8 @@ "datasource": "Source de données", "folder": "", "library_element": "", + "mute_timing": "", + "notification_template": "", "unknown": "Inconnu" }, "summary": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index e4fcf72a3b7..a14467b178a 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "" } + }, + "templates": { + "misconfigured-badge-text": "", + "misconfigured-warning": "", + "misconfigured-warning-details": "" } }, "annotations": { @@ -1408,7 +1413,9 @@ "dashboards": "", "datasources": "", "folders": "", - "library_elements": "" + "library_elements": "", + "mute_timings": "", + "notification_templates": "" }, "migration-token": { "delete-button": "", @@ -1492,6 +1499,8 @@ "datasource": "Fonte de dados", "folder": "", "library_element": "", + "mute_timing": "", + "notification_template": "", "unknown": "Desconhecido" }, "summary": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 37c08f6b213..2fc554f0ec1 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -280,6 +280,11 @@ "ofQuery": { "To": "" } + }, + "templates": { + "misconfigured-badge-text": "", + "misconfigured-warning": "", + "misconfigured-warning-details": "" } }, "annotations": { @@ -1398,7 +1403,9 @@ "dashboards": "", "datasources": "", "folders": "", - "library_elements": "" + "library_elements": "", + "mute_timings": "", + "notification_templates": "" }, "migration-token": { "delete-button": "", @@ -1482,6 +1489,8 @@ "datasource": "数据源", "folder": "", "library_element": "", + "mute_timing": "", + "notification_template": "", "unknown": "未知" }, "summary": { From 22a3c9976fbab317e5f564d13f6614446f39e7d8 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:57:58 +0100 Subject: [PATCH 22/45] Chore: Convert QueryGroupOptions to functional component (#94342) Also rewrites styles --- .betterer.results | 21 +- .../query/components/QueryGroupOptions.tsx | 688 +++++++++--------- 2 files changed, 332 insertions(+), 377 deletions(-) diff --git a/.betterer.results b/.betterer.results index 31d1f23e1c8..3b6f2de23a9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4770,8 +4770,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "35"], [0, 0, 0, "No untranslated strings. Wrap text with ", "36"], [0, 0, 0, "No untranslated strings. Wrap text with ", "37"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "38"], - [0, 0, 0, "Styles should be written using objects.", "39"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "38"] ], "public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -7310,24 +7309,6 @@ exports[`no gf-form usage`] = { "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "public/app/features/query/components/QueryGroupOptions.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/features/variables/adhoc/picker/AdHocFilter.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], diff --git a/public/app/features/query/components/QueryGroupOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx index 8fc36b47e1e..c949e1767b2 100644 --- a/public/app/features/query/components/QueryGroupOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -1,11 +1,9 @@ -import { css } from '@emotion/css'; -import { PureComponent, ChangeEvent, FocusEvent } from 'react'; -import * as React from 'react'; +import { css, cx } from '@emotion/css'; +import React, { useState, ChangeEvent, FocusEvent, useCallback } from 'react'; -import { rangeUtil, PanelData, DataSourceApi } from '@grafana/data'; -import { Input, InlineFormLabel, stylesFactory, InlineFieldRow, InlineSwitch } from '@grafana/ui'; +import { rangeUtil, PanelData, DataSourceApi, GrafanaTheme2 } from '@grafana/data'; +import { Input, InlineSwitch, useStyles2, InlineLabel } from '@grafana/ui'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; -import { config } from 'app/core/config'; import { QueryGroupOptions } from 'app/types'; interface Props { @@ -15,149 +13,142 @@ interface Props { onChange: (options: QueryGroupOptions) => void; } -interface State { - timeRangeFrom: string; - timeRangeShift: string; - timeRangeHide: boolean; - isOpen: boolean; - relativeTimeIsValid: boolean; - timeShiftIsValid: boolean; -} +export const QueryGroupOptionsEditor = React.memo(({ options, dataSource, data, onChange }: Props) => { + const [timeRangeFrom, setTimeRangeFrom] = useState(options.timeRange?.from || ''); + const [timeRangeShift, setTimeRangeShift] = useState(options.timeRange?.shift || ''); + const [timeRangeHide, setTimeRangeHide] = useState(options.timeRange?.hide ?? false); + const [isOpen, setIsOpen] = useState(false); + const [relativeTimeIsValid, setRelativeTimeIsValid] = useState(true); + const [timeShiftIsValid, setTimeShiftIsValid] = useState(true); -export class QueryGroupOptionsEditor extends PureComponent { - constructor(props: Props) { - super(props); + const styles = useStyles2(getStyles); - const { options } = props; + const onRelativeTimeChange = useCallback((event: ChangeEvent) => { + setTimeRangeFrom(event.target.value); + }, []); - this.state = { - timeRangeFrom: options.timeRange?.from || '', - timeRangeShift: options.timeRange?.shift || '', - timeRangeHide: options.timeRange?.hide ?? false, - isOpen: false, - relativeTimeIsValid: true, - timeShiftIsValid: true, - }; - } + const onTimeShiftChange = useCallback((event: ChangeEvent) => { + setTimeRangeShift(event.target.value); + }, []); - onRelativeTimeChange = (event: ChangeEvent) => { - this.setState({ - timeRangeFrom: event.target.value, - }); - }; + const onOverrideTime = useCallback( + (event: FocusEvent) => { + const newValue = emptyToNull(event.target.value); + const isValid = timeRangeValidation(newValue); - onTimeShiftChange = (event: ChangeEvent) => { - this.setState({ - timeRangeShift: event.target.value, - }); - }; + if (isValid && options.timeRange?.from !== newValue) { + onChange({ + ...options, + timeRange: { + ...(options.timeRange ?? {}), + from: newValue, + }, + }); + } - onOverrideTime = (event: FocusEvent) => { - const { options, onChange } = this.props; + setRelativeTimeIsValid(isValid); + }, + [onChange, options] + ); - const newValue = emptyToNull(event.target.value); - const isValid = timeRangeValidation(newValue); + const onTimeShift = useCallback( + (event: FocusEvent) => { + const newValue = emptyToNull(event.target.value); + const isValid = timeRangeValidation(newValue); - if (isValid && options.timeRange?.from !== newValue) { - onChange({ - ...options, - timeRange: { - ...(options.timeRange ?? {}), - from: newValue, - }, - }); - } + if (isValid && options.timeRange?.shift !== newValue) { + onChange({ + ...options, + timeRange: { + ...(options.timeRange ?? {}), + shift: newValue, + }, + }); + } - this.setState({ relativeTimeIsValid: isValid }); - }; + setTimeShiftIsValid(isValid); + }, + [onChange, options] + ); - onTimeShift = (event: FocusEvent) => { - const { options, onChange } = this.props; - - const newValue = emptyToNull(event.target.value); - const isValid = timeRangeValidation(newValue); - - if (isValid && options.timeRange?.shift !== newValue) { - onChange({ - ...options, - timeRange: { - ...(options.timeRange ?? {}), - shift: newValue, - }, - }); - } - - this.setState({ timeShiftIsValid: isValid }); - }; - - onToggleTimeOverride = () => { - const { onChange, options } = this.props; - - this.setState({ timeRangeHide: !this.state.timeRangeHide }, () => { - onChange({ - ...options, - timeRange: { - ...(options.timeRange ?? {}), - hide: this.state.timeRangeHide, - }, - }); - }); - }; - - onCacheTimeoutBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; + const onToggleTimeOverride = useCallback(() => { + const newTimeRangeHide = !timeRangeHide; + setTimeRangeHide(newTimeRangeHide); onChange({ ...options, - cacheTimeout: emptyToNull(event.target.value), + timeRange: { + ...(options.timeRange ?? {}), + hide: newTimeRangeHide, + }, }); - }; + }, [onChange, options, timeRangeHide]); - onQueryCachingTTLBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; - - let ttl: number | null = parseInt(event.target.value, 10); - - if (isNaN(ttl) || ttl === 0) { - ttl = null; - } - - onChange({ - ...options, - queryCachingTTL: ttl, - }); - }; - - onMaxDataPointsBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; - - let maxDataPoints: number | null = parseInt(event.currentTarget.value, 10); - - if (isNaN(maxDataPoints) || maxDataPoints === 0) { - maxDataPoints = null; - } - - if (maxDataPoints !== options.maxDataPoints) { + const onCacheTimeoutBlur = useCallback( + (event: ChangeEvent) => { onChange({ ...options, - maxDataPoints, + cacheTimeout: emptyToNull(event.target.value), }); - } - }; + }, + [onChange, options] + ); + + const onQueryCachingTTLBlur = useCallback( + (event: ChangeEvent) => { + let ttl: number | null = parseInt(event.target.value, 10); + + if (isNaN(ttl) || ttl === 0) { + ttl = null; + } - onMinIntervalBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; - const minInterval = emptyToNull(event.target.value); - if (minInterval !== options.minInterval) { onChange({ ...options, - minInterval, + queryCachingTTL: ttl, }); - } - }; + }, + [onChange, options] + ); - renderCacheTimeoutOption() { - const { dataSource, options } = this.props; + const onMaxDataPointsBlur = useCallback( + (event: ChangeEvent) => { + let maxDataPoints: number | null = parseInt(event.currentTarget.value, 10); + if (isNaN(maxDataPoints) || maxDataPoints === 0) { + maxDataPoints = null; + } + + if (maxDataPoints !== options.maxDataPoints) { + onChange({ + ...options, + maxDataPoints, + }); + } + }, + [onChange, options] + ); + + const onMinIntervalBlur = useCallback( + (event: ChangeEvent) => { + const minInterval = emptyToNull(event.target.value); + if (minInterval !== options.minInterval) { + onChange({ + ...options, + minInterval, + }); + } + }, + [onChange, options] + ); + + const onOpenOptions = useCallback(() => { + setIsOpen(true); + }, []); + + const onCloseOptions = useCallback(() => { + setIsOpen(false); + }, []); + + const renderCacheTimeoutOption = () => { const tooltip = `If your time series store has a query cache this option can override the default cache timeout. Specify a numeric value in seconds.`; @@ -166,27 +157,23 @@ export class QueryGroupOptionsEditor extends PureComponent { } return ( -
-
- - Cache timeout - - -
-
+ <> + + Cache timeout + + + ); - } - - renderQueryCachingTTLOption() { - const { dataSource, options } = this.props; + }; + const renderQueryCachingTTLOption = () => { const tooltip = `Cache time-to-live: How long results from this queries in this panel will be cached, in milliseconds. Defaults to the TTL in the caching configuration for this datasource.`; if (!dataSource.cachingConfig?.enabled) { @@ -194,129 +181,101 @@ export class QueryGroupOptionsEditor extends PureComponent { } return ( -
-
- - Cache TTL - - -
-
+ <> + Cache TTL + + ); - } + }; - renderMaxDataPointsOption() { - const { data, options } = this.props; + const renderMaxDataPointsOption = () => { const realMd = data.request?.maxDataPoints; const value = options.maxDataPoints ?? ''; const isAuto = value === ''; return ( -
-
- - The maximum data points per series. Used directly by some data sources and used in calculation of auto - interval. With streaming data this value is used for the rolling buffer. - - } - > - Max data points - - - {isAuto && ( + <> + -
=
-
Width of panel
+ The maximum data points per series. Used directly by some data sources and used in calculation of auto + interval. With streaming data this value is used for the rolling buffer. - )} -
-
+ } + > + Max data points + + + {isAuto && ( + <> + = + Width of panel + + )} + ); - } + }; - renderIntervalOption() { - const { data, dataSource, options } = this.props; + const renderIntervalOption = () => { const realInterval = data.request?.interval; const minIntervalOnDs = dataSource.interval ?? 'No limit'; return ( <> -
-
- - A lower limit for the interval. Recommended to be set to write frequency, for example 1m{' '} - if your data is written every minute. Default value can be set in data source settings for most data - sources. - - } - > - Min interval - - -
-
-
-
- - The evaluated interval that is sent to data source and is used in $__interval and{' '} - $__interval_ms. This value is not exactly equal to{' '} - Time range / max data points, it will approximate a series of magic number. - - } - > - Interval - - {realInterval} -
=
-
Time range / max data points
-
-
+ + A lower limit for the interval. Recommended to be set to write frequency, for example 1m if + your data is written every minute. Default value can be set in data source settings for most data sources. + + } + htmlFor="min-interval-input" + > + Min interval + + + + The evaluated interval that is sent to data source and is used in $__interval and{' '} + $__interval_ms. This value is not exactly equal to Time range / max data points, + it will approximate a series of magic number. + + } + > + Interval + + {realInterval} + = + Time range / max data points ); - } - - onOpenOptions = () => { - this.setState({ isOpen: true }); }; - onCloseOptions = () => { - this.setState({ isOpen: false }); - }; - - renderCollapsedText(styles: StylesType): React.ReactNode | undefined { - const { data, options } = this.props; - const { isOpen } = this.state; - + const renderCollapsedText = (): React.ReactNode | undefined => { if (isOpen) { return undefined; } @@ -326,120 +285,135 @@ export class QueryGroupOptionsEditor extends PureComponent { mdDesc = `auto = ${data.request.maxDataPoints}`; } - let intervalDesc = options.minInterval; - if (data.request) { - intervalDesc = `${data.request.interval}`; - } + const intervalDesc = data.request?.interval ?? options.minInterval; return ( <> - {
MD = {mdDesc}
} - {
Interval = {intervalDesc}
} + {MD = {mdDesc}} + {Interval = {intervalDesc}} ); - } - - render() { - const { timeRangeHide: hideTimeOverride, relativeTimeIsValid, timeShiftIsValid } = this.state; - const { timeRangeFrom: relativeTime, timeRangeShift: timeShift, isOpen } = this.state; - const styles = getStyles(); - - return ( - - {this.renderMaxDataPointsOption()} - {this.renderIntervalOption()} - {this.renderCacheTimeoutOption()} - {this.renderQueryCachingTTLOption()} - -
- - Overrides the relative time range for individual panels, which causes them to be different than what is - selected in the dashboard time picker in the top-right corner of the dashboard. For example to configure - the Last 5 minutes the Relative time should be now-5m and 5m, or variables - like $_relativeTime. - - } - > - Relative time - - -
- -
- - Overrides the time range for individual panels by shifting its start and end relative to the time - picker. For example to configure the Last 1h the Time shift should be now-1h and{' '} - 1h, or variables like $_timeShift. - - } - > - Time shift - - -
- {(timeShift || relativeTime) && ( - - Hide time info - - - )} -
- ); - } -} - -const timeRangeValidation = (value: string | null) => { - if (!value) { - return true; - } - - return rangeUtil.isValidTimeSpan(value); -}; - -const emptyToNull = (value: string) => { - return value === '' ? null : value; -}; - -const getStyles = stylesFactory(() => { - const { theme } = config; - - return { - collapsedText: css` - margin-left: ${theme.spacing.md}; - font-size: ${theme.typography.size.sm}; - color: ${theme.colors.textWeak}; - `, }; + + return ( + +
+ {renderMaxDataPointsOption()} + {renderIntervalOption()} + {renderCacheTimeoutOption()} + {renderQueryCachingTTLOption()} + + + Overrides the relative time range for individual panels, which causes them to be different than what is + selected in the dashboard time picker in the top-right corner of the dashboard. For example to configure + the Last 5 minutes the Relative time should be now-5m and 5m, or variables like{' '} + $_relativeTime. + + } + > + Relative time + + + + Overrides the time range for individual panels by shifting its start and end relative to the time picker. + For example to configure the Last 1h the Time shift should be now-1h and 1h, or + variables like $_timeShift. + + } + > + Time shift + + + {(timeRangeShift || timeRangeFrom) && ( + <> + + Hide time info + + + + )} +
+
+ ); }); -type StylesType = ReturnType; +QueryGroupOptionsEditor.displayName = 'QueryGroupOptionsEditor'; + +function timeRangeValidation(value: string | null) { + return !value || rangeUtil.isValidTimeSpan(value); +} + +function emptyToNull(value: string) { + return value === '' ? null : value; +} + +function getStyles(theme: GrafanaTheme2) { + return { + grid: css({ + display: 'grid', + gridTemplateColumns: `auto minmax(5em, 1fr) auto 1fr`, + gap: theme.spacing(0.5), + gridAutoRows: theme.spacing(4), + whiteSpace: 'nowrap', + }), + firstColumn: css({ + gridColumn: 1, + }), + collapsedText: css({ + marginLeft: theme.spacing(2), + fontSize: theme.typography.size.sm, + color: theme.colors.text.secondary, + }), + noSquish: css({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + fontWeight: theme.typography.fontWeightMedium, + fontSize: theme.typography.size.sm, + backgroundColor: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + }), + left: css({ + justifySelf: 'left', + }), + operator: css({ + color: theme.v1.palette.orange, + }), + }; +} From 9adba8537f704b3dd8d773c40671c2497e56fd61 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 15 Oct 2024 13:58:09 +0200 Subject: [PATCH 23/45] Plugin Extension: use correct `PluginExtensionAddedLinkConfig` component in deprecation note (#94708) --- packages/grafana-data/src/types/pluginExtensions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 54c1170681f..d9e53bb1a7b 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -223,7 +223,7 @@ type Dashboard = { // deprecated types -/** @deprecated - use PluginAddedComponentConfig instead */ +/** @deprecated - use PluginExtensionAddedLinkConfig instead */ export type PluginExtensionLinkConfig = { type: PluginExtensionTypes.link; title: string; From 4a3c6325a4ca92c9e0912d50bd7a4a900ac9c37d Mon Sep 17 00:00:00 2001 From: Yves Siegrist Date: Tue, 15 Oct 2024 22:00:40 +1000 Subject: [PATCH 24/45] Docs: correct typo in login solution documentation (#92393) --- conf/defaults.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 662e300834f..8daf5ef4a88 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -553,7 +553,7 @@ token_expiration_day_limit = # Login cookie name login_cookie_name = grafana_session -# Disable usage of Grafana build-in login solution. +# Disable usage of Grafana's built-in login solution. disable_login = false # The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation (token_rotation_interval_minutes). From b28085110dc5bce1af0261c2d216087dbba9fdfd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 15 Oct 2024 14:02:34 +0200 Subject: [PATCH 25/45] Plugins: Auto instrumentation improvements (#94193) --- go.mod | 2 +- go.sum | 4 +- go.work.sum | 15 +--- pkg/aggregator/go.mod | 2 +- pkg/aggregator/go.sum | 4 +- .../http_client_provider.go | 2 + .../http_client_provider_test.go | 13 ++- .../pluginrequestmeta/plugin_request_meta.go | 59 ------------- .../plugin_request_meta_test.go | 50 ----------- pkg/promlib/go.mod | 2 +- pkg/promlib/go.sum | 4 +- .../clientmiddleware/logger_middleware.go | 6 +- .../clientmiddleware/metrics_middleware.go | 3 +- .../metrics_middleware_test.go | 22 +++-- .../plugin_request_meta_middleware.go | 86 ------------------ .../plugin_request_meta_middleware_test.go | 39 -------- .../status_source_middleware.go | 58 ------------ .../status_source_middleware_test.go | 88 ------------------- .../pluginsintegration/pluginsintegration.go | 7 +- .../kinds/query.go | 42 +++++---- .../kinds/query.panel.schema.json | 12 ++- .../kinds/query.request.schema.json | 12 ++- .../kinds/query.types.json | 14 ++- .../kinds/query_test.go | 1 + .../grafana-testdata-datasource/scenarios.go | 30 +++++++ public/api-enterprise-spec.json | 10 +-- public/api-merged.json | 6 +- .../QueryEditor.tsx | 7 ++ .../components/ErrorWithSourceEditor.tsx | 32 +++++++ .../grafana-testdata-datasource/dataquery.ts | 2 + public/openapi3.json | 6 +- 31 files changed, 188 insertions(+), 452 deletions(-) delete mode 100644 pkg/plugins/pluginrequestmeta/plugin_request_meta.go delete mode 100644 pkg/plugins/pluginrequestmeta/plugin_request_meta_test.go delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go create mode 100644 public/app/plugins/datasource/grafana-testdata-datasource/components/ErrorWithSourceEditor.tsx diff --git a/go.mod b/go.mod index 5ab6b92c69d..e1732d80c7c 100644 --- a/go.mod +++ b/go.mod @@ -87,7 +87,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.251.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.253.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index f707fdc6534..cd9c5652d01 100644 --- a/go.sum +++ b/go.sum @@ -2284,8 +2284,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= -github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.253.0 h1:KaCrqqsDgVIoT8hwvwuUMKV7QbHVlvRoFN5+U2rOXR8= +github.com/grafana/grafana-plugin-sdk-go v0.253.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990 h1:uQMZE/z+Y+o/U0z/g8ckAHss7U7LswedilByA2535DU= github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990/go.mod h1:3Vi0xv/4OBkBw4R9GAERkSrBnx06qrjpmNBRisucuSM= github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI= diff --git a/go.work.sum b/go.work.sum index eb78119b883..44d2a672ed8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,7 +1,6 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1 h1:tdpHgTbmbvEIARu+bixzmleMi14+3imnpoFXz+Qzjp4= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= cel.dev/expr v0.15.0 h1:O1jzfJCQBfL5BFoYktaxwIhuttaQPsVWerH9/EEKx0w= -cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cloud.google.com/go/accessapproval v1.7.11 h1:MgtE8CI+YJWPGGHnxQ9z1VQqV87h+vSGy2MeM/m0ggQ= @@ -516,6 +515,8 @@ github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJ github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s= github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -598,7 +599,6 @@ github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= -github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4= @@ -906,7 +906,6 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93whDM5o= github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk= github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= @@ -1063,6 +1062,7 @@ go.opentelemetry.io/collector/service v0.95.0 h1:t6RUHV7ByFjkjPKGz5n6n4wIoXZLC8H go.opentelemetry.io/collector/service v0.95.0/go.mod h1:4yappQmDE5UZmLE9wwtj6IPM4W5KGLIYfObEAaejtQc= go.opentelemetry.io/contrib/config v0.4.0 h1:Xb+ncYOqseLroMuBesGNRgVQolXcXOhMj7EhGwJCdHs= go.opentelemetry.io/contrib/config v0.4.0/go.mod h1:drNk2xRqLWW4/amk6Uh1S+sDAJTc7bcEEN1GfJzj418= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= go.opentelemetry.io/contrib/propagators/b3 v1.23.0 h1:aaIGWc5JdfRGpCafLRxMJbD65MfTa206AwSKkvGS0Hg= go.opentelemetry.io/contrib/propagators/b3 v1.23.0/go.mod h1:Gyz7V7XghvwTq+mIhLFlTgcc03UDroOg8vezs4NLhwU= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= @@ -1074,7 +1074,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 h1:ZqR go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1/go.mod h1:D7ynngPWlGJrqyGSDOdscuv7uqttfCE3jcBvffDv9y4= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1 h1:q/Nj5/2TZRIt6PderQ9oU0M00fzoe8UZuINGw6ETGTw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1/go.mod h1:DTE9yAu6r08jU3xa68GiSeI7oRcSEQ2RpKbbQGO+dWM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ= @@ -1085,7 +1084,6 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= @@ -1118,11 +1116,9 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1134,8 +1130,6 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -1150,15 +1144,12 @@ gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE= gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf h1:T4tsZBlZYXK3j40sQNP5MBO32I+rn6ypV1PpklsiV8k= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 601011db495..b62d1c5cb5d 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,7 +4,7 @@ go 1.23.1 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.251.0 + github.com/grafana/grafana-plugin-sdk-go v0.253.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 51002ade3a7..a113f3c027b 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -130,8 +130,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= -github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.253.0 h1:KaCrqqsDgVIoT8hwvwuUMKV7QbHVlvRoFN5+U2rOXR8= +github.com/grafana/grafana-plugin-sdk-go v0.253.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index c40dc894ee9..143e30756ca 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -42,6 +42,8 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer middlewares = append(middlewares, GrafanaRequestIDHeaderMiddleware(cfg, logger)) } + middlewares = append(middlewares, sdkhttpclient.ErrorSourceMiddleware()) + // SigV4 signing should be performed after all headers are added if cfg.SigV4AuthEnabled { authSettings := awsds.AuthSettings{ diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go index 652372a0de4..84279cca2e4 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go @@ -27,7 +27,7 @@ func TestHTTPClientProvider(t *testing.T) { _ = New(&setting.Cfg{SigV4AuthEnabled: false}, &validations.OSSPluginRequestValidator{}, tracer) require.Len(t, providerOpts, 1) o := providerOpts[0] - require.Len(t, o.Middlewares, 8) + require.Len(t, o.Middlewares, 9) require.Equal(t, TracingMiddlewareName, o.Middlewares[0].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, DataSourceMetricsMiddlewareName, o.Middlewares[1].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.ContextualMiddlewareName, o.Middlewares[2].(sdkhttpclient.MiddlewareName).MiddlewareName()) @@ -35,6 +35,8 @@ func TestHTTPClientProvider(t *testing.T) { require.Equal(t, sdkhttpclient.BasicAuthenticationMiddlewareName, o.Middlewares[4].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.CustomHeadersMiddlewareName, o.Middlewares[5].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName()) }) t.Run("When creating new provider and SigV4 is enabled should apply expected middleware", func(t *testing.T) { @@ -51,7 +53,7 @@ func TestHTTPClientProvider(t *testing.T) { _ = New(&setting.Cfg{SigV4AuthEnabled: true}, &validations.OSSPluginRequestValidator{}, tracer) require.Len(t, providerOpts, 1) o := providerOpts[0] - require.Len(t, o.Middlewares, 9) + require.Len(t, o.Middlewares, 10) require.Equal(t, TracingMiddlewareName, o.Middlewares[0].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, DataSourceMetricsMiddlewareName, o.Middlewares[1].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.ContextualMiddlewareName, o.Middlewares[2].(sdkhttpclient.MiddlewareName).MiddlewareName()) @@ -59,7 +61,9 @@ func TestHTTPClientProvider(t *testing.T) { require.Equal(t, sdkhttpclient.BasicAuthenticationMiddlewareName, o.Middlewares[4].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.CustomHeadersMiddlewareName, o.Middlewares[5].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName()) - require.Equal(t, awssdk.SigV4MiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, awssdk.SigV4MiddlewareName, o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName()) }) t.Run("When creating new provider and http logging is enabled for one plugin, it should apply expected middleware", func(t *testing.T) { @@ -76,7 +80,7 @@ func TestHTTPClientProvider(t *testing.T) { _ = New(&setting.Cfg{PluginSettings: setting.PluginSettings{"example": {"har_log_enabled": "true"}}}, &validations.OSSPluginRequestValidator{}, tracer) require.Len(t, providerOpts, 1) o := providerOpts[0] - require.Len(t, o.Middlewares, 9) + require.Len(t, o.Middlewares, 10) require.Equal(t, TracingMiddlewareName, o.Middlewares[0].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, DataSourceMetricsMiddlewareName, o.Middlewares[1].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.ContextualMiddlewareName, o.Middlewares[2].(sdkhttpclient.MiddlewareName).MiddlewareName()) @@ -86,5 +90,6 @@ func TestHTTPClientProvider(t *testing.T) { require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, HTTPLoggerMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName()) }) } diff --git a/pkg/plugins/pluginrequestmeta/plugin_request_meta.go b/pkg/plugins/pluginrequestmeta/plugin_request_meta.go deleted file mode 100644 index 1a189bf33fb..00000000000 --- a/pkg/plugins/pluginrequestmeta/plugin_request_meta.go +++ /dev/null @@ -1,59 +0,0 @@ -package pluginrequestmeta - -import ( - "context" - "errors" - - "github.com/grafana/grafana-plugin-sdk-go/backend" -) - -// StatusSource is an enum-like string value representing the source of a -// plugin query data response status code -type StatusSource string - -const ( - StatusSourcePlugin StatusSource = "plugin" - StatusSourceDownstream StatusSource = "downstream" -) - -// DefaultStatusSource is the default StatusSource that should be used when it is not explicitly set by the plugin. -const DefaultStatusSource StatusSource = StatusSourcePlugin - -type statusSourceCtxKey struct{} - -// StatusSourceFromContext returns the plugin request status source stored in the context. -// If no plugin request status source is stored in the context, [DefaultStatusSource] is returned. -func StatusSourceFromContext(ctx context.Context) StatusSource { - value, ok := ctx.Value(statusSourceCtxKey{}).(*StatusSource) - if ok { - return *value - } - return DefaultStatusSource -} - -// WithStatusSource sets the plugin request status source for the context. -func WithStatusSource(ctx context.Context, s StatusSource) context.Context { - return context.WithValue(ctx, statusSourceCtxKey{}, &s) -} - -// WithDownstreamStatusSource mutates the provided context by setting the plugin request status source to -// StatusSourceDownstream. If the provided context does not have a plugin request status source, the context -// will not be mutated. This means that [WithStatusSource] has to be called before this function. -func WithDownstreamStatusSource(ctx context.Context) error { - v, ok := ctx.Value(statusSourceCtxKey{}).(*StatusSource) - if !ok { - return errors.New("the provided context does not have a plugin request status source") - } - *v = StatusSourceDownstream - return nil -} - -// StatusSourceFromPluginErrorSource takes an error source returned by a plugin and returns the corresponding -// StatusSource. If the provided value is a zero-value (i.e.: the plugin did not set it), the function returns -// DefaultStatusSource. -func StatusSourceFromPluginErrorSource(pluginErrorSource backend.ErrorSource) StatusSource { - if pluginErrorSource == "" { - return DefaultStatusSource - } - return StatusSource(pluginErrorSource) -} diff --git a/pkg/plugins/pluginrequestmeta/plugin_request_meta_test.go b/pkg/plugins/pluginrequestmeta/plugin_request_meta_test.go deleted file mode 100644 index ffec48c1e30..00000000000 --- a/pkg/plugins/pluginrequestmeta/plugin_request_meta_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package pluginrequestmeta - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestStatusSource(t *testing.T) { - t.Run("WithStatusSource", func(t *testing.T) { - ctx := context.Background() - ss := StatusSourceFromContext(ctx) - require.Equal(t, StatusSourcePlugin, ss) - - ctx = WithStatusSource(ctx, StatusSourceDownstream) - ss = StatusSourceFromContext(ctx) - require.Equal(t, StatusSourceDownstream, ss) - }) - - t.Run("WithDownstreamStatusSource", func(t *testing.T) { - t.Run("Returns error if no status source is set", func(t *testing.T) { - ctx := context.Background() - err := WithDownstreamStatusSource(ctx) - require.Error(t, err) - require.Equal(t, StatusSourcePlugin, StatusSourceFromContext(ctx)) - }) - - t.Run("Should mutate context if status source is set", func(t *testing.T) { - ctx := WithStatusSource(context.Background(), StatusSourcePlugin) - err := WithDownstreamStatusSource(ctx) - require.NoError(t, err) - require.Equal(t, StatusSourceDownstream, StatusSourceFromContext(ctx)) - }) - }) - - t.Run("StatusSourceFromContext", func(t *testing.T) { - t.Run("Background returns StatusSourcePlugin", func(t *testing.T) { - ctx := context.Background() - ss := StatusSourceFromContext(ctx) - require.Equal(t, StatusSourcePlugin, ss) - }) - - t.Run("Context with status source returns the set status source", func(t *testing.T) { - ctx := WithStatusSource(context.Background(), StatusSourcePlugin) - ss := StatusSourceFromContext(ctx) - require.Equal(t, StatusSourcePlugin, ss) - }) - }) -} diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 51e5bc9cc60..538022bba05 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,7 +4,7 @@ go 1.23.1 require ( github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 - github.com/grafana/grafana-plugin-sdk-go v0.251.0 + github.com/grafana/grafana-plugin-sdk-go v0.253.0 github.com/json-iterator/go v1.1.12 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.20.4 diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index af656a4d453..ee2510d54d8 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -98,8 +98,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 h1:as4PmrFoYI1byS5JjsgPC7uSGTMh+SgS0ePv6hOyDGU= github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3/go.mod h1:lcjGB6SuaZ2o44A9nD6p/tR4QXSPbzViRY520Gy6pTQ= -github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= -github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.253.0 h1:KaCrqqsDgVIoT8hwvwuUMKV7QbHVlvRoFN5+U2rOXR8= +github.com/grafana/grafana-plugin-sdk-go v0.253.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= diff --git a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go index ab1eb9f751a..1870f77daa6 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/instrumentationutils" plog "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) // NewLoggerMiddleware creates a new backend.HandlerMiddleware that will @@ -61,7 +60,7 @@ func (m *LoggerMiddleware) logRequest(ctx context.Context, pCtx backend.PluginCo if err != nil { logParams = append(logParams, "error", err) } - logParams = append(logParams, "statusSource", pluginrequestmeta.StatusSourceFromContext(ctx)) + logParams = append(logParams, "statusSource", backend.ErrorSourceFromContext(ctx)) if status > instrumentationutils.RequestStatusOK { logFunc = ctxLogger.Error @@ -93,7 +92,8 @@ func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryData "refID", refID, "status", int(dr.Status), "error", dr.Error, - "statusSource", pluginrequestmeta.StatusSourceFromPluginErrorSource(dr.ErrorSource), + "statusSource", dr.ErrorSource.String(), + "target", m.pluginTarget(ctx, req.PluginContext), } ctxLogger.Error("Partial data response error", logParams...) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go index 29e7b1ec998..a75f7a43889 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/instrumentationutils" "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) // pluginMetrics contains the prometheus metrics used by the MetricsMiddleware. @@ -115,7 +114,7 @@ func (m *MetricsMiddleware) instrumentPluginRequest(ctx context.Context, pluginC status, err := fn(ctx) elapsed := time.Since(start) - statusSource := pluginrequestmeta.StatusSourceFromContext(ctx) + statusSource := backend.ErrorSourceFromContext(ctx) endpoint := backend.EndpointFromContext(ctx) pluginRequestDurationWithLabels := m.pluginRequestDuration.WithLabelValues(pluginCtx.PluginID, string(endpoint), target, string(statusSource)) diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go index 1af03e7af79..79580bddfaa 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/instrumentationutils" "github.com/grafana/grafana/pkg/plugins/manager/fakes" - "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) const ( @@ -90,7 +89,7 @@ func TestInstrumentationMiddleware(t *testing.T) { require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationMs)) require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationS)) - counter := mw.pluginMetrics.pluginRequestCounter.WithLabelValues(pluginID, string(tc.expEndpoint), instrumentationutils.RequestStatusOK.String(), string(backendplugin.TargetUnknown), string(pluginrequestmeta.DefaultStatusSource)) + counter := mw.pluginMetrics.pluginRequestCounter.WithLabelValues(pluginID, string(tc.expEndpoint), instrumentationutils.RequestStatusOK.String(), string(backendplugin.TargetUnknown), string(backend.DefaultErrorSource)) require.Equal(t, 1.0, testutil.ToFloat64(counter)) for _, m := range []string{metricRequestDurationMs, metricRequestDurationS} { require.NoError(t, checkHistogram(promRegistry, m, map[string]string{ @@ -155,12 +154,11 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { })) metricsMw := newMetricsMiddleware(promRegistry, pluginsRegistry) cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares( - NewPluginRequestMetaMiddleware(), backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { metricsMw.BaseHandler = backend.NewBaseHandler(next) return metricsMw }), - NewStatusSourceMiddleware(), + backend.NewErrorSourceMiddleware(), )) t.Run("Metrics", func(t *testing.T) { @@ -185,12 +183,12 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { for _, tc := range []struct { name string responses map[string]backend.DataResponse - expStatusSource pluginrequestmeta.StatusSource + expStatusSource backend.ErrorSource }{ { "Default status source for ok responses should be plugin", map[string]backend.DataResponse{"A": okResponse}, - pluginrequestmeta.StatusSourcePlugin, + backend.ErrorSourcePlugin, }, { "Plugin errors should have higher priority than downstream errors", @@ -198,12 +196,12 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { "A": pluginErrorResponse, "B": downstreamErrorResponse, }, - pluginrequestmeta.StatusSourcePlugin, + backend.ErrorSourcePlugin, }, { "Errors without ErrorSource should be reported as plugin status source", map[string]backend.DataResponse{"A": legacyErrorResponse}, - pluginrequestmeta.StatusSourcePlugin, + backend.ErrorSourcePlugin, }, { "Downstream errors should have higher priority than ok responses", @@ -211,7 +209,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { "A": okResponse, "B": downstreamErrorResponse, }, - pluginrequestmeta.StatusSourceDownstream, + backend.ErrorSourceDownstream, }, { "Plugin errors should have higher priority than ok responses", @@ -219,7 +217,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { "A": okResponse, "B": pluginErrorResponse, }, - pluginrequestmeta.StatusSourcePlugin, + backend.ErrorSourcePlugin, }, { "Legacy errors should have higher priority than ok responses", @@ -227,7 +225,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { "A": okResponse, "B": legacyErrorResponse, }, - pluginrequestmeta.StatusSourcePlugin, + backend.ErrorSourcePlugin, }, } { t.Run(tc.name, func(t *testing.T) { @@ -242,7 +240,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { } _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) require.NoError(t, err) - ctxStatusSource := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) + ctxStatusSource := backend.ErrorSourceFromContext(cdt.QueryDataCtx) require.Equal(t, tc.expStatusSource, ctxStatusSource) }) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go deleted file mode 100644 index d24e5dc1472..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go +++ /dev/null @@ -1,86 +0,0 @@ -package clientmiddleware - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" -) - -// NewPluginRequestMetaMiddleware returns a new backend.HandlerMiddleware that sets up the default -// values for the plugin request meta in the context.Context. All middlewares that are executed -// after this one are be able to access plugin request meta via the pluginrequestmeta package. -func NewPluginRequestMetaMiddleware() backend.HandlerMiddleware { - return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { - return &PluginRequestMetaMiddleware{ - BaseHandler: backend.NewBaseHandler(next), - defaultStatusSource: pluginrequestmeta.DefaultStatusSource, - } - }) -} - -type PluginRequestMetaMiddleware struct { - backend.BaseHandler - defaultStatusSource pluginrequestmeta.StatusSource -} - -func (m *PluginRequestMetaMiddleware) withDefaultPluginRequestMeta(ctx context.Context) context.Context { - // Setup plugin request status source - ctx = pluginrequestmeta.WithStatusSource(ctx, m.defaultStatusSource) - - return ctx -} - -func (m *PluginRequestMetaMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.QueryData(ctx, req) -} - -func (m *PluginRequestMetaMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.CallResource(ctx, req, sender) -} - -func (m *PluginRequestMetaMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.CheckHealth(ctx, req) -} - -func (m *PluginRequestMetaMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.CollectMetrics(ctx, req) -} - -func (m *PluginRequestMetaMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.SubscribeStream(ctx, req) -} - -func (m *PluginRequestMetaMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.PublishStream(ctx, req) -} - -func (m *PluginRequestMetaMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.RunStream(ctx, req, sender) -} - -// ValidateAdmission implements backend.AdmissionHandler. -func (m *PluginRequestMetaMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.ValidateAdmission(ctx, req) -} - -// MutateAdmission implements backend.AdmissionHandler. -func (m *PluginRequestMetaMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.MutateAdmission(ctx, req) -} - -// ConvertObject implements backend.AdmissionHandler. -func (m *PluginRequestMetaMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - ctx = m.withDefaultPluginRequestMeta(ctx) - return m.BaseHandler.ConvertObjects(ctx, req) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go deleted file mode 100644 index b87fcfb84d5..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package clientmiddleware - -import ( - "context" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" -) - -func TestPluginRequestMetaMiddleware(t *testing.T) { - t.Run("default", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - handlertest.WithMiddlewares(NewPluginRequestMetaMiddleware()), - ) - _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) - require.NoError(t, err) - ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) - require.Equal(t, pluginrequestmeta.StatusSourcePlugin, ss) - }) - - t.Run("other value", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - handlertest.WithMiddlewares(backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { - return &PluginRequestMetaMiddleware{ - BaseHandler: backend.NewBaseHandler(next), - defaultStatusSource: "test", - } - })), - ) - _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) - require.NoError(t, err) - ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) - require.Equal(t, pluginrequestmeta.StatusSource("test"), ss) - }) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go deleted file mode 100644 index d9af8df3887..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go +++ /dev/null @@ -1,58 +0,0 @@ -package clientmiddleware - -import ( - "context" - "fmt" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" -) - -// NewStatusSourceMiddleware returns a new backend.HandlerMiddleware that sets the status source in the -// plugin request meta stored in the context.Context, according to the query data responses returned by QueryError. -// If at least one query data response has a "downstream" status source and there isn't one with a "plugin" status source, -// the plugin request meta in the context is set to "downstream". -func NewStatusSourceMiddleware() backend.HandlerMiddleware { - return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { - return &StatusSourceMiddleware{ - BaseHandler: backend.NewBaseHandler(next), - } - }) -} - -type StatusSourceMiddleware struct { - backend.BaseHandler -} - -func (m *StatusSourceMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - resp, err := m.BaseHandler.QueryData(ctx, req) - if resp == nil || len(resp.Responses) == 0 { - return resp, err - } - - // Set downstream status source in the context if there's at least one response with downstream status source, - // and if there's no plugin error - var hasPluginError bool - var hasDownstreamError bool - for _, r := range resp.Responses { - if r.Error == nil { - continue - } - if r.ErrorSource == backend.ErrorSourceDownstream { - hasDownstreamError = true - } else { - hasPluginError = true - } - } - - // A plugin error has higher priority than a downstream error, - // so set to downstream only if there's no plugin error - if hasDownstreamError && !hasPluginError { - if err := pluginrequestmeta.WithDownstreamStatusSource(ctx); err != nil { - return resp, fmt.Errorf("failed to set downstream status source: %w", err) - } - } - - return resp, err -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go deleted file mode 100644 index c3544d4e9df..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package clientmiddleware - -import ( - "context" - "errors" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" -) - -func TestStatusSourceMiddleware(t *testing.T) { - someErr := errors.New("oops") - - for _, tc := range []struct { - name string - - queryDataResponse *backend.QueryDataResponse - - expStatusSource pluginrequestmeta.StatusSource - }{ - { - name: `no error should be "plugin" status source`, - queryDataResponse: nil, - expStatusSource: pluginrequestmeta.StatusSourcePlugin, - }, - { - name: `single downstream error should be "downstream" status source`, - queryDataResponse: &backend.QueryDataResponse{ - Responses: map[string]backend.DataResponse{ - "A": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream}, - }, - }, - expStatusSource: pluginrequestmeta.StatusSourceDownstream, - }, - { - name: `single plugin error should be "plugin" status source`, - queryDataResponse: &backend.QueryDataResponse{ - Responses: map[string]backend.DataResponse{ - "A": {Error: someErr, ErrorSource: backend.ErrorSourcePlugin}, - }, - }, - expStatusSource: pluginrequestmeta.StatusSourcePlugin, - }, - { - name: `multiple downstream errors should be "downstream" status source`, - queryDataResponse: &backend.QueryDataResponse{ - Responses: map[string]backend.DataResponse{ - "A": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream}, - "B": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream}, - }, - }, - expStatusSource: pluginrequestmeta.StatusSourceDownstream, - }, - { - name: `single plugin error mixed with downstream errors should be "plugin" status source`, - queryDataResponse: &backend.QueryDataResponse{ - Responses: map[string]backend.DataResponse{ - "A": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream}, - "B": {Error: someErr, ErrorSource: backend.ErrorSourcePlugin}, - "C": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream}, - }, - }, - expStatusSource: pluginrequestmeta.StatusSourcePlugin, - }, - } { - t.Run(tc.name, func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - handlertest.WithMiddlewares( - NewPluginRequestMetaMiddleware(), - NewStatusSourceMiddleware(), - ), - ) - cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - cdt.QueryDataCtx = ctx - return tc.queryDataResponse, nil - } - - _, _ = cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) - - ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) - require.Equal(t, tc.expStatusSource, ss) - }) - } -} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 36c20fa8cbc..7d25ae72446 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -171,7 +171,6 @@ func NewMiddlewareHandler( func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service) []backend.HandlerMiddleware { middlewares := []backend.HandlerMiddleware{ - clientmiddleware.NewPluginRequestMetaMiddleware(), clientmiddleware.NewTracingMiddleware(tracer), clientmiddleware.NewMetricsMiddleware(promRegisterer, registry), clientmiddleware.NewContextualLoggerMiddleware(), @@ -202,9 +201,9 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken middlewares = append(middlewares, clientmiddleware.NewHTTPClientMiddleware()) - // StatusSourceMiddleware should be at the very bottom, or any middlewares below it won't see the - // correct status source in their context.Context - middlewares = append(middlewares, clientmiddleware.NewStatusSourceMiddleware()) + // ErrorSourceMiddleware should be at the very bottom, or any middlewares below it won't see the + // correct error source in their context.Context + middlewares = append(middlewares, backend.NewErrorSourceMiddleware()) return middlewares } diff --git a/pkg/tsdb/grafana-testdata-datasource/kinds/query.go b/pkg/tsdb/grafana-testdata-datasource/kinds/query.go index 6b96c1a6ee9..a2fc0bf6f05 100644 --- a/pkg/tsdb/grafana-testdata-datasource/kinds/query.go +++ b/pkg/tsdb/grafana-testdata-datasource/kinds/query.go @@ -38,6 +38,16 @@ const ( ErrorTypeServerPanic ErrorType = "server_panic" ) +// ErrorSource defines model for TestDataQuery.ErrorSource. +// +enum +type ErrorSource string + +// Defines values for ErrorSource. +const ( + ErrorSourcePlugin ErrorSource = "plugin" + ErrorSourceDownstream ErrorSource = "downstream" +) + // TestDataQueryType defines model for TestDataQueryType. // +enum type TestDataQueryType string @@ -50,6 +60,7 @@ const ( TestDataQueryTypeCsvFile TestDataQueryType = "csv_file" TestDataQueryTypeCsvMetricValues TestDataQueryType = "csv_metric_values" TestDataQueryTypeDatapointsOutsideRange TestDataQueryType = "datapoints_outside_range" + TestDataQueryTypeErrorWithSource TestDataQueryType = "error_with_source" TestDataQueryTypeExponentialHeatmapBucketData TestDataQueryType = "exponential_heatmap_bucket_data" TestDataQueryTypeFlameGraph TestDataQueryType = "flame_graph" TestDataQueryTypeGrafanaApi TestDataQueryType = "grafana_api" @@ -92,21 +103,22 @@ type TestDataQuery struct { Channel string `json:"channel,omitempty"` // Drop percentage (the chance we will lose a point 0-100) - DropPercent float64 `json:"dropPercent,omitempty"` - ErrorType ErrorType `json:"errorType,omitempty"` - FlamegraphDiff bool `json:"flamegraphDiff,omitempty"` - LevelColumn bool `json:"levelColumn,omitempty"` - StartValue float64 `json:"startValue,omitempty"` - Spread float64 `json:"spread,omitempty"` - Noise float64 `json:"noise,omitempty"` - Min *float64 `json:"min,omitempty"` - Max *float64 `json:"max,omitempty"` - WithNil bool `json:"withNil,omitempty"` - Lines int64 `json:"lines,omitempty"` - Points [][]any `json:"points,omitempty"` - RawFrameContent string `json:"rawFrameContent,omitempty"` - SeriesCount int `json:"seriesCount,omitempty"` - SpanCount int `json:"spanCount,omitempty"` + DropPercent float64 `json:"dropPercent,omitempty"` + ErrorType ErrorType `json:"errorType,omitempty"` + FlamegraphDiff bool `json:"flamegraphDiff,omitempty"` + LevelColumn bool `json:"levelColumn,omitempty"` + StartValue float64 `json:"startValue,omitempty"` + Spread float64 `json:"spread,omitempty"` + Noise float64 `json:"noise,omitempty"` + Min *float64 `json:"min,omitempty"` + Max *float64 `json:"max,omitempty"` + WithNil bool `json:"withNil,omitempty"` + Lines int64 `json:"lines,omitempty"` + Points [][]any `json:"points,omitempty"` + RawFrameContent string `json:"rawFrameContent,omitempty"` + SeriesCount int `json:"seriesCount,omitempty"` + SpanCount int `json:"spanCount,omitempty"` + ErrorSource ErrorSource `json:"errorSource,omitempty"` Nodes *NodesQuery `json:"nodes,omitempty"` PulseWave *PulseWaveQuery `json:"pulseWave,omitempty"` diff --git a/pkg/tsdb/grafana-testdata-datasource/kinds/query.panel.schema.json b/pkg/tsdb/grafana-testdata-datasource/kinds/query.panel.schema.json index b0a71f0d307..ab679740dd0 100644 --- a/pkg/tsdb/grafana-testdata-datasource/kinds/query.panel.schema.json +++ b/pkg/tsdb/grafana-testdata-datasource/kinds/query.panel.schema.json @@ -73,6 +73,15 @@ "description": "Drop percentage (the chance we will lose a point 0-100)", "type": "number" }, + "errorSource": { + "description": "Possible enum values:\n - `\"plugin\"` \n - `\"downstream\"` ", + "type": "string", + "enum": [ + "plugin", + "downstream" + ], + "x-enum-description": {} + }, "errorType": { "description": "Possible enum values:\n - `\"frontend_exception\"` \n - `\"frontend_observable\"` \n - `\"server_panic\"` ", "type": "string", @@ -220,7 +229,7 @@ "additionalProperties": false }, "scenarioId": { - "description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ", + "description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"error_with_source\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ", "type": "string", "enum": [ "annotations", @@ -229,6 +238,7 @@ "csv_file", "csv_metric_values", "datapoints_outside_range", + "error_with_source", "exponential_heatmap_bucket_data", "flame_graph", "grafana_api", diff --git a/pkg/tsdb/grafana-testdata-datasource/kinds/query.request.schema.json b/pkg/tsdb/grafana-testdata-datasource/kinds/query.request.schema.json index c449cc2e4aa..6d3826164e6 100644 --- a/pkg/tsdb/grafana-testdata-datasource/kinds/query.request.schema.json +++ b/pkg/tsdb/grafana-testdata-datasource/kinds/query.request.schema.json @@ -83,6 +83,15 @@ "description": "Drop percentage (the chance we will lose a point 0-100)", "type": "number" }, + "errorSource": { + "description": "Possible enum values:\n - `\"plugin\"` \n - `\"downstream\"` ", + "type": "string", + "enum": [ + "plugin", + "downstream" + ], + "x-enum-description": {} + }, "errorType": { "description": "Possible enum values:\n - `\"frontend_exception\"` \n - `\"frontend_observable\"` \n - `\"server_panic\"` ", "type": "string", @@ -230,7 +239,7 @@ "additionalProperties": false }, "scenarioId": { - "description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ", + "description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"error_with_source\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ", "type": "string", "enum": [ "annotations", @@ -239,6 +248,7 @@ "csv_file", "csv_metric_values", "datapoints_outside_range", + "error_with_source", "exponential_heatmap_bucket_data", "flame_graph", "grafana_api", diff --git a/pkg/tsdb/grafana-testdata-datasource/kinds/query.types.json b/pkg/tsdb/grafana-testdata-datasource/kinds/query.types.json index 50b7dca76d1..2a6a17b811f 100644 --- a/pkg/tsdb/grafana-testdata-datasource/kinds/query.types.json +++ b/pkg/tsdb/grafana-testdata-datasource/kinds/query.types.json @@ -8,7 +8,7 @@ { "metadata": { "name": "default", - "resourceVersion": "1711119846950", + "resourceVersion": "1728405292506", "creationTimestamp": "2024-03-01T02:53:35Z" }, "spec": { @@ -56,6 +56,15 @@ "description": "Drop percentage (the chance we will lose a point 0-100)", "type": "number" }, + "errorSource": { + "description": "Possible enum values:\n - `\"plugin\"` \n - `\"downstream\"` ", + "enum": [ + "plugin", + "downstream" + ], + "type": "string", + "x-enum-description": {} + }, "errorType": { "description": "Possible enum values:\n - `\"frontend_exception\"` \n - `\"frontend_observable\"` \n - `\"server_panic\"` ", "enum": [ @@ -142,7 +151,7 @@ "type": "string" }, "scenarioId": { - "description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ", + "description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"error_with_source\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ", "enum": [ "annotations", "arrow", @@ -150,6 +159,7 @@ "csv_file", "csv_metric_values", "datapoints_outside_range", + "error_with_source", "exponential_heatmap_bucket_data", "flame_graph", "grafana_api", diff --git a/pkg/tsdb/grafana-testdata-datasource/kinds/query_test.go b/pkg/tsdb/grafana-testdata-datasource/kinds/query_test.go index 584d4876748..522bad3afad 100644 --- a/pkg/tsdb/grafana-testdata-datasource/kinds/query_test.go +++ b/pkg/tsdb/grafana-testdata-datasource/kinds/query_test.go @@ -21,6 +21,7 @@ func TestQueryTypeDefinitions(t *testing.T) { reflect.TypeOf(NodesQueryTypeRandom), // pick an example value (not the root) reflect.TypeOf(StreamingQueryTypeFetch), // pick an example value (not the root) reflect.TypeOf(ErrorTypeServerPanic), // pick an example value (not the root) + reflect.TypeOf(ErrorSourcePlugin), // pick an example value (not the root) reflect.TypeOf(TestDataQueryTypeAnnotations), // pick an example value (not the root) }, }) diff --git a/pkg/tsdb/grafana-testdata-datasource/scenarios.go b/pkg/tsdb/grafana-testdata-datasource/scenarios.go index de265313f54..982da8c8232 100644 --- a/pkg/tsdb/grafana-testdata-datasource/scenarios.go +++ b/pkg/tsdb/grafana-testdata-datasource/scenarios.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "math" "math/rand" @@ -198,6 +199,12 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means Name: "Trace", }) + s.registerScenario(&Scenario{ + ID: kinds.TestDataQueryTypeErrorWithSource, + Name: "Error with source", + handler: s.handleErrorWithSourceScenario, + }) + s.queryMux.HandleFunc("", s.handleFallbackScenario) } @@ -663,6 +670,29 @@ func (s *Service) handleLogsScenario(ctx context.Context, req *backend.QueryData return resp, nil } +func (s *Service) handleErrorWithSourceScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + anErr := errors.New("error") + resp := backend.NewQueryDataResponse() + + for _, q := range req.Queries { + model, err := GetJSONModel(q.JSON) + if err != nil { + continue + } + + respD := resp.Responses[q.RefID] + respD.Error = anErr + + if model.ErrorSource == kinds.ErrorSourceDownstream { + respD.Error = backend.DownstreamError(respD.Error) + } + + resp.Responses[q.RefID] = respD + } + + return resp, nil +} + func RandomWalk(query backend.DataQuery, model kinds.TestDataQuery, index int) *data.Frame { rand := rand.New(rand.NewSource(time.Now().UnixNano() + int64(index))) timeWalkerMs := query.TimeRange.From.UnixNano() / int64(time.Millisecond) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index d163f81c51a..4999e495ab9 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -4064,7 +4064,7 @@ "type": "string" }, "ErrorSource": { - "$ref": "#/definitions/ErrorSource" + "$ref": "#/definitions/Source" }, "Frames": { "$ref": "#/definitions/Frames" @@ -4361,10 +4361,6 @@ } } }, - "ErrorSource": { - "description": "ErrorSource type defines the source of the error", - "type": "string" - }, "ExplorePanelsState": { "description": "This is an object constructed with the keys as the values of the enum VisType and the value being a bag of properties" }, @@ -7284,6 +7280,10 @@ } } }, + "Source": { + "type": "string", + "title": "Source type defines the status source." + }, "State": { "description": "+enum", "type": "string" diff --git a/public/api-merged.json b/public/api-merged.json index 4c1adf3ebe7..b1bd32864ed 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -14503,7 +14503,7 @@ "type": "string" }, "ErrorSource": { - "$ref": "#/definitions/ErrorSource" + "$ref": "#/definitions/Source" }, "Frames": { "$ref": "#/definitions/Frames" @@ -20478,6 +20478,10 @@ } } }, + "Source": { + "type": "string", + "title": "Source type defines the status source." + }, "Span": { "type": "object", "title": "A Span defines a continuous sequence of buckets.", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx index 8c4e6599073..66eb259dbde 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx @@ -10,6 +10,7 @@ import { CSVContentEditor } from './components/CSVContentEditor'; import { CSVFileEditor } from './components/CSVFileEditor'; import { CSVWavesEditor } from './components/CSVWaveEditor'; import ErrorEditor from './components/ErrorEditor'; +import ErrorWithSourceQueryEditor from './components/ErrorWithSourceEditor'; import { GrafanaLiveEditor } from './components/GrafanaLiveEditor'; import { NodeGraphEditor } from './components/NodeGraphEditor'; import { PredictablePulseEditor } from './components/PredictablePulseEditor'; @@ -120,6 +121,9 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) update.usa = { mode: usaQueryModes[0].value, }; + break; + case TestDataQueryType.ErrorWithSource: + update.errorSource = 'plugin'; } onUpdate(update); @@ -379,6 +383,9 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) /> )} + {scenarioId === TestDataQueryType.ErrorWithSource && ( + + )} {description &&

{description}

} diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/components/ErrorWithSourceEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/components/ErrorWithSourceEditor.tsx new file mode 100644 index 00000000000..a013a68b6bf --- /dev/null +++ b/public/app/plugins/datasource/grafana-testdata-datasource/components/ErrorWithSourceEditor.tsx @@ -0,0 +1,32 @@ +import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; + +import { EditorProps } from '../QueryEditor'; + +const OPTIONS = [ + { + label: 'Plugin', + value: 'plugin', + }, + { + label: 'Downstream', + value: 'downstream', + }, +]; + +const ErrorWithSourceQueryEditor = ({ query, onChange }: EditorProps) => { + return ( + + +