From 29b8d5d295c33205d2bad71fe27409902612000f Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Thu, 7 Apr 2022 15:45:03 +0200 Subject: [PATCH 001/156] remove the parallele to avoid concurrency (#47447) --- pkg/schema/load/load_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/schema/load/load_test.go b/pkg/schema/load/load_test.go index e7d3c1e310c..65d7655bac5 100644 --- a/pkg/schema/load/load_test.go +++ b/pkg/schema/load/load_test.go @@ -36,7 +36,6 @@ type testfunc func(*testing.T, schema.VersionedCueSchema, []byte, fs.FileInfo, s // TODO: remove validdir once we can test directly with devenv folder var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, fn testfunc) func(t *testing.T) { return func(t *testing.T) { - t.Parallel() require.NoError(t, filepath.Walk(validdir, func(path string, d fs.FileInfo, err error) error { require.NoError(t, err) From b857f8339ed58a20a7bb6f61c6a3e784164faa1c Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 7 Apr 2022 14:45:14 +0100 Subject: [PATCH 002/156] TestData: Interpolate variables in more fields (#47158) * TestData: Interpolate variables in more fields * only interpolate variables if the field is set * Correctly type scenarioId which can be undefined --- .../datasource/testdata/QueryEditor.tsx | 2 +- .../plugins/datasource/testdata/datasource.ts | 30 ++++++++++++++----- .../app/plugins/datasource/testdata/types.ts | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index ae68e35ffb8..a10116b875f 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -163,7 +163,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) .sort((a, b) => a.label.localeCompare(b.label)), [scenarioList] ); - const showLabels = useMemo(() => showLabelsFor.includes(query.scenarioId), [query]); + const showLabels = useMemo(() => showLabelsFor.includes(query.scenarioId ?? ''), [query]); if (loading) { return null; diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index a92f578133f..b75d23016be 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -87,10 +87,6 @@ export class TestDataDataSource extends DataSourceWithBackend { } default: - if (target.alias) { - target.alias = this.templateSrv.replace(target.alias, options.scopedVars); - } - backendQueries.push(target); } } @@ -111,7 +107,24 @@ export class TestDataDataSource extends DataSourceWithBackend { } resolveTemplateVariables(query: TestDataQuery, scopedVars: ScopedVars) { - query.labels = this.templateSrv.replace(query.labels!, scopedVars); + if (query.labels) { + query.labels = this.templateSrv.replace(query.labels, scopedVars); + } + if (query.alias) { + query.alias = this.templateSrv.replace(query.alias, scopedVars); + } + if (query.scenarioId) { + query.scenarioId = this.templateSrv.replace(query.scenarioId, scopedVars); + } + if (query.stringInput) { + query.stringInput = this.templateSrv.replace(query.stringInput, scopedVars); + } + if (query.csvContent) { + query.csvContent = this.templateSrv.replace(query.csvContent, scopedVars); + } + if (query.rawFrameContent) { + query.rawFrameContent = this.templateSrv.replace(query.rawFrameContent, scopedVars); + } } annotationDataTopicTest(target: TestDataQuery, req: DataQueryRequest): Observable { @@ -145,10 +158,13 @@ export class TestDataDataSource extends DataSourceWithBackend { } getQueryDisplayText(query: TestDataQuery) { + const scenario = query.scenarioId ?? 'Default scenario'; + if (query.alias) { - return query.scenarioId + ' as ' + query.alias; + return scenario + ' as ' + query.alias; } - return query.scenarioId; + + return scenario; } testDatasource() { diff --git a/public/app/plugins/datasource/testdata/types.ts b/public/app/plugins/datasource/testdata/types.ts index 0f23a903de6..56ce9f569f4 100644 --- a/public/app/plugins/datasource/testdata/types.ts +++ b/public/app/plugins/datasource/testdata/types.ts @@ -9,7 +9,7 @@ export interface Scenario { export interface TestDataQuery extends DataQuery { alias?: string; - scenarioId: string; + scenarioId?: string; stringInput?: string; stream?: StreamingQuery; pulseWave?: PulseWaveQuery; From 21d82132bb862f5ed0324e27766d1176c1bf77a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 15:54:26 +0200 Subject: [PATCH 003/156] Update dependency webpack to v5.72.0 (#47309) Co-authored-by: Renovate Bot --- package.json | 4 +-- packages/grafana-e2e/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 45 ++++++++++++++++++++++++++++--- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index e5a9937695c..2383eb001a3 100644 --- a/package.json +++ b/package.json @@ -237,7 +237,7 @@ "ts-node": "10.7.0", "typescript": "4.4.4", "wait-on": "6.0.1", - "webpack": "5.70.0", + "webpack": "5.72.0", "webpack-bundle-analyzer": "4.5.0", "webpack-cli": "4.9.2", "webpack-dev-server": "4.7.4", @@ -396,7 +396,7 @@ "@rushstack/node-core-library": "3.45.1", "@rushstack/rig-package": "0.3.8", "@rushstack/ts-command-line": "4.10.7", - "@storybook/react/webpack": "5.70.0", + "@storybook/react/webpack": "5.72.0", "node-fetch": "2.6.7" }, "workspaces": { diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 9f892d541c6..6fb70e16064 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -41,7 +41,7 @@ "rollup-plugin-copy": "3.4.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", - "webpack": "5.70.0" + "webpack": "5.72.0" }, "types": "src/index.ts", "dependencies": { diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 03daae61a61..10130b923a7 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -175,7 +175,7 @@ "terser-webpack-plugin": "5.3.1", "ts-loader": "8.0.11", "typescript": "4.4.4", - "webpack": "5.70.0", + "webpack": "5.72.0", "webpack-filter-warnings-plugin": "1.2.1" }, "types": "src/index.ts" diff --git a/yarn.lock b/yarn.lock index 4b3b5d24640..b63315ebeb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4094,7 +4094,7 @@ __metadata: tslib: 2.3.1 typescript: 4.4.4 uuid: 8.3.2 - webpack: 5.70.0 + webpack: 5.72.0 yaml: ^1.8.3 bin: grafana-e2e: bin/grafana-e2e.js @@ -4490,7 +4490,7 @@ __metadata: typescript: 4.4.4 uplot: 1.6.19 uuid: 8.3.2 - webpack: 5.70.0 + webpack: 5.72.0 webpack-filter-warnings-plugin: 1.2.1 languageName: unknown linkType: soft @@ -20695,7 +20695,7 @@ __metadata: vendor: "link:./public/vendor" visjs-network: 4.25.0 wait-on: 6.0.1 - webpack: 5.70.0 + webpack: 5.72.0 webpack-bundle-analyzer: 4.5.0 webpack-cli: 4.9.2 webpack-dev-server: 4.7.4 @@ -36984,7 +36984,44 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5.70.0, webpack@npm:^5.9.0": +"webpack@npm:5.72.0": + version: 5.72.0 + resolution: "webpack@npm:5.72.0" + dependencies: + "@types/eslint-scope": ^3.7.3 + "@types/estree": ^0.0.51 + "@webassemblyjs/ast": 1.11.1 + "@webassemblyjs/wasm-edit": 1.11.1 + "@webassemblyjs/wasm-parser": 1.11.1 + acorn: ^8.4.1 + acorn-import-assertions: ^1.7.6 + browserslist: ^4.14.5 + chrome-trace-event: ^1.0.2 + enhanced-resolve: ^5.9.2 + es-module-lexer: ^0.9.0 + eslint-scope: 5.1.1 + events: ^3.2.0 + glob-to-regexp: ^0.4.1 + graceful-fs: ^4.2.9 + json-parse-better-errors: ^1.0.2 + loader-runner: ^4.2.0 + mime-types: ^2.1.27 + neo-async: ^2.6.2 + schema-utils: ^3.1.0 + tapable: ^2.1.1 + terser-webpack-plugin: ^5.1.3 + watchpack: ^2.3.1 + webpack-sources: ^3.2.3 + peerDependenciesMeta: + webpack-cli: + optional: true + bin: + webpack: bin/webpack.js + checksum: 8365f1466d0f7adbf80ebc9b780f263a28eeeabcd5fb515249bfd9a56ab7fe8d29ea53df3d9364d0732ab39ae774445eb28abce694ed375b13882a6b2fe93ffc + languageName: node + linkType: hard + +"webpack@npm:^5.9.0": version: 5.70.0 resolution: "webpack@npm:5.70.0" dependencies: From 59388bf546fbb70c1794e5a43e64824dfd4aae61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 7 Apr 2022 16:17:37 +0200 Subject: [PATCH 004/156] Config: fix buildInfo initialization (#47402) * Config: fix buildInfo initialization * change default to be parseable * apply default values for buildInfo --- packages/grafana-runtime/src/config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 6a0e015626b..f2599a42894 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -119,7 +119,6 @@ export class GrafanaBootConfig implements GrafanaConfig { this.theme2 = createTheme({ colors: { mode } }); this.theme = this.theme2.v1; this.bootData = options.bootData; - this.buildInfo = options.buildInfo; const defaults = { datasources: {}, @@ -131,7 +130,7 @@ export class GrafanaBootConfig implements GrafanaConfig { appUrl: '', appSubUrl: '', buildInfo: { - version: 'v1.0', + version: '1.0', commit: '1', env: 'production', }, @@ -142,6 +141,8 @@ export class GrafanaBootConfig implements GrafanaConfig { merge(this, defaults, options); + this.buildInfo = options.buildInfo || defaults.buildInfo; + if (this.dateFormats) { systemDateFormats.update(this.dateFormats); } From 94f72acbb38722b7a942262c1513dc14032ffde2 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Thu, 7 Apr 2022 15:24:26 +0100 Subject: [PATCH 005/156] Alerting: Introduce an internal changelog (#47390) * Alerting: Introduce an internal changelog Please note that this is not intented to replace Grafana's "add to changelog" label. It is _mostly_ for internal consumption of the Alerting team that owns this part of Grafana. * Fix markdown formatting * Fix changelog entry --- pkg/services/ngalert/CHANGELOG.md | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 pkg/services/ngalert/CHANGELOG.md diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md new file mode 100644 index 00000000000..0b6fff8a661 --- /dev/null +++ b/pkg/services/ngalert/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +**Please note that this is not a direct replacement of Grafana's "add to changelog" label. It is _mostly_ for internal consumption of the Alerting team that owns this part of Grafana.** + +## Scope Glossary + +### `[ADMIN]` +The ADMIN scope denotes a change that affect the structure and layout of this repository. This includes updates to the following: + +- CODEOWNERS +- README +- DotFiles (.gitignore, .git-attributes, etc) + +Anything that a developer working on this repo should be aware of from a standards and practice perspective. + +### `[BUGFIX]` + +The BUGFIX scope denotes a change that fixes an issue with the project in question. A BUGFIX should align the behaviour of the service with the current expected behaviour of the service. If a BUGFIX introduces new unexpected behaviour to ameliorate the issue, a corresponding FEATURE or ENHANCEMENT scope should also be added to the changelog. + +### `[CHANGE]` + +The CHANGE scope denotes a change that changes the expected behavior of the project while not adding new functionality or fixing an underling issue. This commonly occurs when renaming things to make them more consistent or to accommodate updated versions of vendored dependencies. + +### `[FEATURE]` + +The FEATURE scope denotes a change that adds new functionality to the project/service. + +### `[ENHANCEMENT]` + +The ENHANCEMENT scope denotes a change that improves upon the current functionality of the project/service. Generally, an enhancement is something that improves upon something that is already present. Either by making it simpler, more powerful, or more performant. For Example: + +An optimization on a particular process in a service that makes it more performant +Simpler syntax for setting a configuration value, like allowing 1m instead of 60 for a duration setting. + +## Order + +Scopes must have an order to ensure consistency and ease of search, this helps us identify which section do we need to look for what. The order must be: + +1. `[CHANGE]` +2. `[FEATURE]` +3. `[BUGFIX]` +4. `[ENHANCEMENT]` +5. `[ADMIN]` + + +## Grafana Alerting - main / unreleased + +- [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216 +- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971 \ No newline at end of file From 74a5650156287e7f1c368f616d78137641f65519 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 18:23:22 +0200 Subject: [PATCH 006/156] Update dependency jest-junit to v13.1.0 (#47446) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2383eb001a3..37f76582ca8 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "jest-canvas-mock": "2.3.1", "jest-date-mock": "1.0.8", "jest-fail-on-console": "2.3.0", - "jest-junit": "13.0.0", + "jest-junit": "13.1.0", "jest-matcher-utils": "27.5.1", "jest-mock-console": "1.2.3", "lerna": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index b63315ebeb1..568fe87e119 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20589,7 +20589,7 @@ __metadata: jest-canvas-mock: 2.3.1 jest-date-mock: 1.0.8 jest-fail-on-console: 2.3.0 - jest-junit: 13.0.0 + jest-junit: 13.1.0 jest-matcher-utils: 27.5.1 jest-mock-console: 1.2.3 jquery: 3.6.0 @@ -23417,7 +23417,19 @@ __metadata: languageName: node linkType: hard -"jest-junit@npm:13.0.0, jest-junit@npm:^13.0.0": +"jest-junit@npm:13.1.0": + version: 13.1.0 + resolution: "jest-junit@npm:13.1.0" + dependencies: + mkdirp: ^1.0.4 + strip-ansi: ^6.0.1 + uuid: ^8.3.2 + xml: ^1.0.1 + checksum: a12ec522012cf9d0e97d4c665d8ca05b97dc3d25ef3cd0dbba2b095da4b68e1cabac9751a2718a9593b55c620a2b980e0b6c7dd7d33dfc4b73193bc67b9c9ae0 + languageName: node + linkType: hard + +"jest-junit@npm:^13.0.0": version: 13.0.0 resolution: "jest-junit@npm:13.0.0" dependencies: From 883f9f718f4a648eeb48872532adc4a8a21f5745 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Thu, 7 Apr 2022 11:25:58 -0500 Subject: [PATCH 007/156] Docs: corrects typos (#47464) --- docs/sources/enterprise/license/license-restrictions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/enterprise/license/license-restrictions.md b/docs/sources/enterprise/license/license-restrictions.md index 2e4b068a450..080803aac4c 100644 --- a/docs/sources/enterprise/license/license-restrictions.md +++ b/docs/sources/enterprise/license/license-restrictions.md @@ -29,7 +29,7 @@ Your Grafana license includes a maximum number of _Viewer_ and _Editor/Admin_ ac - An _active user_ is a user who has signed in to Grafana within the last 30 days. This is a rolling window that is updated daily. - When you reach the number of maximum active viewers or editor/admins, only currently active users can sign in; new users and non-active users cannot sign in when you reach the limit. -- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/ddmins, new editors and admins can still sign in. +- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/admins, new editors and admins can still sign in. - The number of dashboards that a user can view or edit, and the number of organizations that they can access does not affect the active user count. A user with editor permissions for many dashboards across many different organizations counts as one editor. - A license limit banner appears to administrators when Grafana reaches its active user limit; editors and viewers do not see the banner. To change user roles to make better use of your licenses, refer to [Optimize your tiered license](#optimize-your-tiered-license). @@ -123,7 +123,7 @@ After you apply the token, Grafana Enterprise resets your license and updates th > If you are running Grafana Enterprise 8.2 or earlier, the license grants you the total number of licensed users _for each user type_. -For example, if your current license includes 60 viewers and 40 dditor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel. +For example, if your current license includes 60 viewers and 40 editor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel. Before you upgrade to Grafana 8.3, ensure that the total number of active users in Grafana does not exceed the number of users in your combined license. If it does, then new users cannot sign in to Grafana 8.3 until the active user count returns below the licensed limit. From 222325c7f192ec8644b46c417bc2d09946181291 Mon Sep 17 00:00:00 2001 From: Stephanie Closson Date: Thu, 7 Apr 2022 13:49:54 -0300 Subject: [PATCH 008/156] Fix: Parse exemplars before parsing heatmap data (#47463) * parse exemplars before parsing the heatmap. Exemplars are overlayed. Not part of the main heatmap * added tests --- .../prometheus/result_transformer.test.ts | 65 +++++++++++++++++++ .../prometheus/result_transformer.ts | 21 +++--- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts index d61dee2fbad..b7087c11d1e 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -251,6 +251,71 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[2].values.toArray()).toEqual([10, 0, 30]); expect(series.data[0].fields[3].values.toArray()).toEqual([10, 0, 10]); }); + + it('Retains exemplar frames when data returned is a heatmap', () => { + const options = { + targets: [ + { + format: 'heatmap', + refId: 'A', + }, + ], + } as unknown as DataQueryRequest; + const response = { + state: 'Done', + data: [ + new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'Value', + type: FieldType.number, + values: [10, 10, 0], + labels: { le: '1' }, + }, + ], + }), + new MutableDataFrame({ + refId: 'A', + name: 'exemplar', + meta: { + custom: { + resultType: 'exemplar', + }, + }, + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4, 3, 2, 1] }, + { + name: 'Value', + type: FieldType.number, + values: [30, 10, 40, 90, 14, 21], + labels: { le: '6' }, + }, + { + name: 'Test', + type: FieldType.string, + values: ['hello', 'doctor', 'name', 'continue', 'yesterday', 'tomorrow'], + labels: { le: '6' }, + }, + ], + }), + ], + } as unknown as DataQueryResponse; + + const series = transformV2(response, options, {}); + expect(series.data[0].fields.length).toEqual(2); + expect(series.data.length).toEqual(2); + expect(series.data[1].fields[2].values.toArray()).toEqual([ + 'hello', + 'doctor', + 'name', + 'continue', + 'yesterday', + 'tomorrow', + ]); + expect(series.data[1].fields.length).toEqual(3); + }); }); describe('transformDFToTable', () => { it('transforms dataFrame with response length 1 to table dataFrame', () => { diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index d3d5b1f9595..62b8d2f3422 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -72,16 +72,8 @@ export function transformV2( const [tableFrames, framesWithoutTable] = partition(response.data, (df) => isTableResult(df, request)); const processedTableFrames = transformDFToTable(tableFrames); - const [heatmapResults, framesWithoutTableAndHeatmaps] = partition(framesWithoutTable, (df) => - isHeatmapResult(df, request) - ); - - const processedHeatmapFrames = mergeHeatmapFrames( - transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel)) - ); - - const [exemplarFrames, framesWithoutTableHeatmapsAndExemplars] = partition( - framesWithoutTableAndHeatmaps, + const [exemplarFrames, framesWithoutTableAndExemplars] = partition( + framesWithoutTable, (df) => df.meta?.custom?.resultType === 'exemplar' ); @@ -103,6 +95,15 @@ export function transformV2( return { ...dataFrame, meta: { ...dataFrame.meta, dataTopic: DataTopic.Annotations } }; }); + const [heatmapResults, framesWithoutTableHeatmapsAndExemplars] = partition( + framesWithoutTableAndExemplars, + (df) => isHeatmapResult(df, request) + ); + + const processedHeatmapFrames = mergeHeatmapFrames( + transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel)) + ); + // Everything else is processed as time_series result and graph preferredVisualisationType const otherFrames = framesWithoutTableHeatmapsAndExemplars.map((dataFrame) => { const df = { From c3ad36ba72c5c581737d45ef86206156200c46e4 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Thu, 7 Apr 2022 12:52:00 -0500 Subject: [PATCH 009/156] Temporarily skip intermittent test (#47471) --- pkg/services/ngalert/notifier/alertmanager_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go index 94ec8a365fb..a3b0ebece1d 100644 --- a/pkg/services/ngalert/notifier/alertmanager_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_test.go @@ -346,6 +346,10 @@ func TestPutAlert(t *testing.T) { // implement a custom maintenance function for silences, because we snapshot // our data differently, so we test that functionality. func TestSilenceCleanup(t *testing.T) { + // TODO: This test intermittently fails. Un-skip me! + // https://github.com/grafana/grafana/issues/47470 + t.Skip("intermittent test") + require := require.New(t) oldRetention := retentionNotificationsAndSilences From 4a4d87dbdcaf4dca753bcf29205ac036b6c59862 Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Thu, 7 Apr 2022 10:30:19 -0800 Subject: [PATCH 010/156] add public dashboards feature flag (#47364) --- packages/grafana-data/src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 6 ++++++ pkg/services/featuremgmt/toggles_gen.go | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index dc9564e190e..23e997e4dd6 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -41,6 +41,7 @@ export interface FeatureToggles { showFeatureFlagsInUI?: boolean; disable_http_request_histogram?: boolean; validatedQueries?: boolean; + publicDashboards?: boolean; lokiLive?: boolean; swaggerUi?: boolean; featureHighlights?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index af7f1baf424..60b46d46b6a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -143,6 +143,12 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, + { + Name: "publicDashboards", + Description: "enables public access to dashboards", + State: FeatureStateAlpha, + RequiresDevMode: true, + }, { Name: "lokiLive", Description: "support websocket streaming for loki (early prototype)", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 078ecbda7e7..7548ed76643 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -107,6 +107,10 @@ const ( // only execute the query saved in a panel FlagValidatedQueries = "validatedQueries" + // FlagPublicDashboards + // enables public access to dashboards + FlagPublicDashboards = "publicDashboards" + // FlagLokiLive // support websocket streaming for loki (early prototype) FlagLokiLive = "lokiLive" From 631dd718a228823e6570cd02e71cfb910a822c3c Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Fri, 8 Apr 2022 14:52:08 +0800 Subject: [PATCH 011/156] 47470: Add additional delay to silences in test. (#47482) This test of silence cleanup was flaky because of its use of real wall time. In CI environments with slow execution, delays could cause the test to fail. This change mitigates the problem by increasing the end time of silences in the test. After Prometheus merges this PR: https://github.com/prometheus/alertmanager/pull/2867 we can make the test fully deterministic by using a fake clock. Fixes #47470 Signed-off-by: Joe Blubaugh --- pkg/services/ngalert/notifier/alertmanager_test.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go index a3b0ebece1d..5e2f10e7420 100644 --- a/pkg/services/ngalert/notifier/alertmanager_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_test.go @@ -346,10 +346,6 @@ func TestPutAlert(t *testing.T) { // implement a custom maintenance function for silences, because we snapshot // our data differently, so we test that functionality. func TestSilenceCleanup(t *testing.T) { - // TODO: This test intermittently fails. Un-skip me! - // https://github.com/grafana/grafana/issues/47470 - t.Skip("intermittent test") - require := require.New(t) oldRetention := retentionNotificationsAndSilences @@ -390,10 +386,10 @@ func TestSilenceCleanup(t *testing.T) { makeSilence("", "tests", dt(now.Add(5*time.Hour)), dt(now.Add(6*time.Hour)), matchers), // Active now makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(6*time.Hour)), matchers), - // Expiring soon - makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(2*time.Second)), matchers), + // Expiring soon. + makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(5*time.Second)), matchers), // Expiring *very* soon - makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(20*time.Millisecond)), matchers), + makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(2*time.Second)), matchers), } for _, s := range silences { @@ -407,12 +403,12 @@ func TestSilenceCleanup(t *testing.T) { found, err := am.ListSilences(nil) require.NoError(err) return len(found) == 3 - }, 1500*time.Millisecond, 150*time.Millisecond) + }, 3*time.Second, 150*time.Millisecond) // Wait again for another silence to expire. require.Eventually(func() bool { found, err := am.ListSilences(nil) require.NoError(err) return len(found) == 2 - }, 2*time.Second, 150*time.Millisecond) + }, 6*time.Second, 150*time.Millisecond) } From 7ab910af480497b6e102fe5c59a968b18968e1ee Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:35:34 +0200 Subject: [PATCH 012/156] Query history: Search in comments (#46761) --- pkg/services/queryhistory/queryhistory_search_test.go | 2 +- pkg/services/queryhistory/queryhistory_test.go | 6 ++++++ pkg/services/queryhistory/writers.go | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/services/queryhistory/queryhistory_search_test.go b/pkg/services/queryhistory/queryhistory_search_test.go index f459cd4fa7e..9d3db5ca328 100644 --- a/pkg/services/queryhistory/queryhistory_search_test.go +++ b/pkg/services/queryhistory/queryhistory_search_test.go @@ -111,7 +111,7 @@ func TestGetQueriesFromQueryHistory(t *testing.T) { err := json.Unmarshal(resp.Body(), &response) require.NoError(t, err) require.Equal(t, 200, resp.Status()) - require.Equal(t, 1, response.Result.TotalCount) + require.Equal(t, 2, response.Result.TotalCount) require.Equal(t, true, response.Result.QueryHistory[0].Starred) }) } diff --git a/pkg/services/queryhistory/queryhistory_test.go b/pkg/services/queryhistory/queryhistory_test.go index fe9dfecedeb..b88c3161309 100644 --- a/pkg/services/queryhistory/queryhistory_test.go +++ b/pkg/services/queryhistory/queryhistory_test.go @@ -115,6 +115,12 @@ func testScenarioWithMultipleQueriesInQueryHistory(t *testing.T, desc string, fn resp1 := sc.service.createHandler(sc.reqContext) sc.initialResult = validateAndUnMarshalResponse(t, resp1) + // Add comment + cmd := PatchQueryCommentInQueryHistoryCommand{Comment: "test comment 2"} + sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) + sc.reqContext.Req.Body = mockRequestBody(cmd) + sc.service.patchCommentHandler(sc.reqContext) + time.Sleep(1 * time.Second) command2 := CreateQueryInQueryHistoryCommand{ DatasourceUID: testDsUID1, diff --git a/pkg/services/queryhistory/writers.go b/pkg/services/queryhistory/writers.go index 20f94ab1705..034a055ecfb 100644 --- a/pkg/services/queryhistory/writers.go +++ b/pkg/services/queryhistory/writers.go @@ -23,12 +23,12 @@ func writeStarredSQL(query SearchInQueryHistoryQuery, sqlStore *sqlstore.SQLStor } func writeFiltersSQL(query SearchInQueryHistoryQuery, user *models.SignedInUser, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) { - params := []interface{}{user.OrgId, user.UserId, "%" + query.SearchString + "%"} + params := []interface{}{user.OrgId, user.UserId, "%" + query.SearchString + "%", "%" + query.SearchString + "%"} for _, uid := range query.DatasourceUIDs { params = append(params, uid) } var sql bytes.Buffer - sql.WriteString(" WHERE query_history.org_id = ? AND query_history.created_by = ? AND query_history.queries " + sqlStore.Dialect.LikeStr() + " ? AND query_history.datasource_uid IN (? " + strings.Repeat(",?", len(query.DatasourceUIDs)-1) + ") ") + sql.WriteString(" WHERE query_history.org_id = ? AND query_history.created_by = ? AND (query_history.queries " + sqlStore.Dialect.LikeStr() + " ? OR query_history.comment " + sqlStore.Dialect.LikeStr() + " ?) AND query_history.datasource_uid IN (? " + strings.Repeat(",?", len(query.DatasourceUIDs)-1) + ") ") builder.Write(sql.String(), params...) } From e1438990aebe18debaa697bcafddf4ea1554a9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 8 Apr 2022 09:57:06 +0200 Subject: [PATCH 013/156] Loki: use the same dataframe-format for both live and normal queries (#47153) * loki: use single dataframe-format for both live and normal queries * better comment * better function-name * simplified code * logs_model: added test --- public/app/core/logs_model.test.ts | 146 +++++++++++++++++- public/app/core/logs_model.ts | 75 ++++++--- .../app/plugins/datasource/loki/datasource.ts | 55 +++---- .../plugins/datasource/loki/live_streams.ts | 6 +- .../loki/result_transformer.test.ts | 53 +++---- .../datasource/loki/result_transformer.ts | 73 ++++----- 6 files changed, 290 insertions(+), 118 deletions(-) diff --git a/public/app/core/logs_model.test.ts b/public/app/core/logs_model.test.ts index 06d81158942..e09b6205849 100644 --- a/public/app/core/logs_model.test.ts +++ b/public/app/core/logs_model.test.ts @@ -332,7 +332,10 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ label: COMMON_LABELS, - value: series[0].fields[1].labels, + value: { + filename: '/var/log/grafana/grafana.log', + job: 'grafana', + }, kind: LogsMetaKind.LabelsMap, }); expect(logsModel.meta![1]).toMatchObject({ @@ -342,6 +345,147 @@ describe('dataFrameToLogsModel', () => { }); }); + it('given one series with labels-field should return expected logs model', () => { + const series: DataFrame[] = [ + new MutableDataFrame({ + fields: [ + { + name: 'labels', + type: FieldType.other, + values: [ + { + filename: '/var/log/grafana/grafana.log', + job: 'grafana', + }, + { + filename: '/var/log/grafana/grafana.log', + job: 'grafana', + }, + ], + }, + { + name: 'time', + type: FieldType.time, + values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'], + }, + { + name: 'message', + type: FieldType.string, + values: [ + 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server', + 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7', + ], + }, + { + name: 'id', + type: FieldType.string, + values: ['foo', 'bar'], + }, + ], + meta: { + limit: 1000, + }, + }), + ]; + const logsModel = dataFrameToLogsModel(series, 1); + expect(logsModel.hasUniqueLabels).toBeFalsy(); + expect(logsModel.rows).toHaveLength(2); + expect(logsModel.rows).toMatchObject([ + { + entry: 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server', + labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + logLevel: 'info', + uniqueLabels: {}, + uid: 'foo', + }, + { + entry: 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7', + labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + logLevel: 'error', + uniqueLabels: {}, + uid: 'bar', + }, + ]); + + expect(logsModel.series).toHaveLength(2); + expect(logsModel.series).toMatchObject([ + { + name: 'info', + fields: [ + { type: 'time', values: new ArrayVector([1556270891000, 1556289770000]) }, + { type: 'number', values: new ArrayVector([1, 0]) }, + ], + }, + { + name: 'error', + fields: [ + { type: 'time', values: new ArrayVector([1556289770000]) }, + { type: 'number', values: new ArrayVector([1]) }, + ], + }, + ]); + expect(logsModel.meta).toHaveLength(2); + expect(logsModel.meta![0]).toMatchObject({ + label: COMMON_LABELS, + value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + kind: LogsMetaKind.LabelsMap, + }); + expect(logsModel.meta![1]).toMatchObject({ + label: LIMIT_LABEL, + value: `1000 (2 returned)`, + kind: LogsMetaKind.String, + }); + }); + + it('given one series with labels-field it should work regardless the label-fields position', () => { + const labels = { + name: 'labels', + type: FieldType.other, + values: [ + { + node: 'first', + mode: 'slow', + }, + ], + }; + + const time = { + name: 'time', + type: FieldType.time, + values: ['2019-04-26T09:28:11.352440161Z'], + }; + + const line = { + name: 'line', + type: FieldType.string, + values: ['line1'], + }; + + const frame1 = new MutableDataFrame({ + fields: [labels, time, line], + }); + + const frame2 = new MutableDataFrame({ + fields: [time, labels, line], + }); + + const frame3 = new MutableDataFrame({ + fields: [time, line, labels], + }); + + const logsModel1 = dataFrameToLogsModel([frame1], 1); + expect(logsModel1.rows).toHaveLength(1); + expect(logsModel1.rows[0].labels).toStrictEqual({ mode: 'slow', node: 'first' }); + + const logsModel2 = dataFrameToLogsModel([frame2], 1); + expect(logsModel2.rows).toHaveLength(1); + expect(logsModel2.rows[0].labels).toStrictEqual({ mode: 'slow', node: 'first' }); + + const logsModel3 = dataFrameToLogsModel([frame3], 1); + expect(logsModel3.rows).toHaveLength(1); + expect(logsModel3.rows[0].labels).toStrictEqual({ mode: 'slow', node: 'first' }); + }); + it('given one series with error should return expected logs model', () => { const series: DataFrame[] = [ new MutableDataFrame({ diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 3bee4f2b5d3..36316d6a85e 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -303,11 +303,38 @@ interface LogFields { timeField: FieldWithIndex; stringField: FieldWithIndex; + labelsField?: FieldWithIndex; timeNanosecondField?: FieldWithIndex; logLevelField?: FieldWithIndex; idField?: FieldWithIndex; } +function getAllLabels(fields: LogFields): Labels[] { + // there are two types of dataframes we handle: + // 1. labels are in a separate field (more efficient when labels change by every log-row) + // 2. labels are in in the string-field's `.labels` attribute + + const { stringField, labelsField } = fields; + + const fieldLabels = stringField.labels !== undefined ? [stringField.labels] : []; + + const labelsFieldLabels: Labels[] = labelsField !== undefined ? labelsField.values.toArray() : []; + + return [...fieldLabels, ...labelsFieldLabels]; +} + +function getLabelsForFrameRow(fields: LogFields, index: number): Labels { + // there are two types of dataframes we handle. + // either labels-on-the-string-field, or labels-in-the-labels-field + + const { stringField, labelsField } = fields; + + return { + ...stringField.labels, + ...labelsField?.values.get(index), + }; +} + /** * Converts dataFrames into LogsModel. This involves merging them into one list, sorting them and computing metadata * like common labels. @@ -316,7 +343,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi if (logSeries.length === 0) { return undefined; } - const allLabels: Labels[] = []; + const allLabels: Labels[][] = []; // Find the fields we care about and collect all labels let allSeries: LogFields[] = []; @@ -330,43 +357,39 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi const fieldCache = new FieldCache(series); const stringField = fieldCache.getFirstFieldOfType(FieldType.string); const timeField = fieldCache.getFirstFieldOfType(FieldType.time); + const labelsField = fieldCache.getFieldByName('labels'); if (stringField !== undefined && timeField !== undefined) { - if (stringField?.labels) { - allLabels.push(stringField.labels); - } - - allSeries.push({ + const info = { series, timeField, + labelsField, timeNanosecondField: fieldCache.hasFieldWithNameAndType('tsNs', FieldType.time) ? fieldCache.getFieldByName('tsNs') : undefined, stringField, logLevelField: fieldCache.getFieldByName('level'), idField: getIdField(fieldCache), - }); + }; + + allSeries.push(info); + + const labels = getAllLabels(info); + if (labels.length > 0) { + allLabels.push(labels); + } } }); } - const commonLabels = allLabels.length > 0 ? findCommonLabels(allLabels) : {}; + const flatAllLabels = allLabels.flat(); + const commonLabels = flatAllLabels.length > 0 ? findCommonLabels(flatAllLabels) : {}; const rows: LogRowModel[] = []; let hasUniqueLabels = false; for (const info of allSeries) { const { timeField, timeNanosecondField, stringField, logLevelField, idField, series } = info; - const labels = stringField.labels; - const uniqueLabels = findUniqueLabels(labels, commonLabels); - if (Object.keys(uniqueLabels).length > 0) { - hasUniqueLabels = true; - } - - let seriesLogLevel: LogLevel | undefined = undefined; - if (labels && Object.keys(labels).indexOf('level') !== -1) { - seriesLogLevel = getLogLevelFromKey(labels['level']); - } for (let j = 0; j < series.length; j++) { const ts = timeField.values.get(j); @@ -386,14 +409,20 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : []; const entry = hasAnsi ? ansicolor.strip(message) : message; + const labels = getLabelsForFrameRow(info, j); + const uniqueLabels = findUniqueLabels(labels, commonLabels); + if (Object.keys(uniqueLabels).length > 0) { + hasUniqueLabels = true; + } + let logLevel = LogLevel.unknown; - if (logLevelField && logLevelField.values.get(j)) { - logLevel = getLogLevelFromKey(logLevelField.values.get(j)); - } else if (seriesLogLevel) { - logLevel = seriesLogLevel; + const logLevelKey = (logLevelField && logLevelField.values.get(j)) || (labels && labels['level']); + if (logLevelKey) { + logLevel = getLogLevelFromKey(logLevelKey); } else { logLevel = getLogLevel(entry); } + rows.push({ entryFieldIndex: stringField.index, rowIndex: j, @@ -410,7 +439,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi searchWords, entry, raw: message, - labels: stringField.labels || {}, + labels: labels || {}, uid: idField ? idField.values.get(j) : j.toString(), }); } diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index f8439136914..1259918bdfc 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -40,8 +40,8 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { convertToWebSocketUrl } from 'app/core/utils/explore'; import { lokiResultsToTableModel, - lokiStreamResultToDataFrame, lokiStreamsToDataFrames, + lokiStreamsToRawDataFrame, processRangeQueryResponse, } from './result_transformer'; import { transformBackendResult } from './backendResultTransformer'; @@ -54,7 +54,6 @@ import { LokiRangeQueryRequest, LokiResultType, LokiStreamResponse, - LokiStreamResult, } from './types'; import { LiveStreams, LokiLiveTarget } from './live_streams'; import LanguageProvider from './language_provider'; @@ -538,9 +537,7 @@ export class LokiDatasource }), switchMap((res) => of({ - data: res.data - ? res.data.data.result.map((stream: LokiStreamResult) => lokiStreamResultToDataFrame(stream, reverse)) - : [], + data: res.data ? [lokiStreamsToRawDataFrame(res.data.data.result, reverse)] : [], }) ) ) @@ -667,33 +664,31 @@ export class LokiDatasource const splitKeys: string[] = tagKeys.split(',').filter((v: string) => v !== ''); for (const frame of data) { - const labels: { [key: string]: string } = {}; - for (const field of frame.fields) { - if (field.labels) { - for (const [key, value] of Object.entries(field.labels)) { - labels[key] = String(value).trim(); - } - } - } - - const tags: string[] = [ - ...new Set( - Object.entries(labels).reduce((acc: string[], [key, val]) => { - if (val === '') { - return acc; - } - if (splitKeys.length && !splitKeys.includes(key)) { - return acc; - } - acc.push.apply(acc, [val]); - return acc; - }, []) - ), - ]; - - const view = new DataFrameView<{ ts: string; line: string }>(frame); + const view = new DataFrameView<{ ts: string; line: string; labels: Labels }>(frame); view.forEach((row) => { + const { labels } = row; + + const maybeDuplicatedTags = Object.entries(labels) + .map(([key, val]) => [key, val.trim()]) // trim all label-values + .filter(([key, val]) => { + if (val === '') { + // remove empty + return false; + } + + // if tags are specified, remove label if does not match tags + if (splitKeys.length && !splitKeys.includes(key)) { + return false; + } + + return true; + }) + .map(([key, val]) => val); // keep only the label-value + + // remove duplicates + const tags = Array.from(new Set(maybeDuplicatedTags)); + annotations.push({ time: new Date(row.ts).valueOf(), title: renderLegendFormat(titleFormat, labels), diff --git a/public/app/plugins/datasource/loki/live_streams.ts b/public/app/plugins/datasource/loki/live_streams.ts index be252116786..a2a68ec9fd6 100644 --- a/public/app/plugins/datasource/loki/live_streams.ts +++ b/public/app/plugins/datasource/loki/live_streams.ts @@ -30,11 +30,11 @@ export class LiveStreams { } const data = new CircularDataFrame({ capacity: target.size }); - data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } }); - data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } }); - data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query); data.addField({ name: 'labels', type: FieldType.other }); // The labels for each line + data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } }); + data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query); data.addField({ name: 'id', type: FieldType.string }); + data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } }); data.meta = { ...data.meta, preferredVisualisationType: 'logs' }; data.refId = target.refId; diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index 396386e6419..db35da689a6 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -64,18 +64,20 @@ describe('loki result transformer', () => { jest.clearAllMocks(); }); - describe('lokiStreamResultToDataFrame', () => { + describe('lokiStreamsToRawDataFrame', () => { it('converts streams to series', () => { - const data = streamResult.map((stream) => ResultTransformer.lokiStreamResultToDataFrame(stream)); + const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResult); - expect(data.length).toBe(2); - expect(data[0].fields[1].labels!['foo']).toEqual('bar'); - expect(data[0].fields[0].values.get(0)).toEqual('2020-01-24T09:19:22.021Z'); - expect(data[0].fields[1].values.get(0)).toEqual(streamResult[0].values[0][1]); - expect(data[0].fields[2].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc'); - expect(data[1].fields[0].values.get(0)).toEqual('2020-01-24T09:19:22.031Z'); - expect(data[1].fields[1].values.get(0)).toEqual(streamResult[1].values[0][1]); - expect(data[1].fields[2].values.get(0)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1'); + expect(data.fields[0].values.get(0)).toStrictEqual({ foo: 'bar' }); + expect(data.fields[1].values.get(0)).toEqual('2020-01-24T09:19:22.021Z'); + expect(data.fields[2].values.get(0)).toEqual(streamResult[0].values[0][1]); + expect(data.fields[3].values.get(0)).toEqual(streamResult[0].values[0][0]); + expect(data.fields[4].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc'); + expect(data.fields[0].values.get(1)).toStrictEqual({ bar: 'foo' }); + expect(data.fields[1].values.get(1)).toEqual('2020-01-24T09:19:22.031Z'); + expect(data.fields[2].values.get(1)).toEqual(streamResult[1].values[0][1]); + expect(data.fields[3].values.get(1)).toEqual(streamResult[1].values[0][0]); + expect(data.fields[4].values.get(1)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1'); }); it('should always generate unique ids for logs', () => { @@ -100,20 +102,19 @@ describe('loki result transformer', () => { }, ]; - const data = streamResultWithDuplicateLogs.map((stream) => ResultTransformer.lokiStreamResultToDataFrame(stream)); + const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResultWithDuplicateLogs); - expect(data[0].fields[2].values.get(0)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa'); - expect(data[0].fields[2].values.get(1)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_1'); - expect(data[0].fields[2].values.get(2)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2'); - expect(data[0].fields[2].values.get(3)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2'); - expect(data[1].fields[2].values.get(0)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_3'); + expect(data.fields[4].values.get(0)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa'); + expect(data.fields[4].values.get(1)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_1'); + expect(data.fields[4].values.get(2)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2'); + expect(data.fields[4].values.get(3)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2'); + expect(data.fields[4].values.get(4)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_3'); }); it('should append refId to the unique ids if refId is provided', () => { - const data = streamResult.map((stream) => ResultTransformer.lokiStreamResultToDataFrame(stream, false, 'B')); - expect(data.length).toBe(2); - expect(data[0].fields[2].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc_B'); - expect(data[1].fields[2].values.get(0)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1_B'); + const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResult, false, 'B'); + expect(data.fields[4].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc_B'); + expect(data.fields[4].values.get(1)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1_B'); }); }); @@ -159,11 +160,11 @@ describe('loki result transformer', () => { }; const data = new CircularDataFrame({ capacity: 1 }); - data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } }); - data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } }); - data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' }; data.addField({ name: 'labels', type: FieldType.other }); + data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } }); + data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' }; data.addField({ name: 'id', type: FieldType.string }); + data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } }); ResultTransformer.appendResponseToBufferedData(tailResponse, data); expect(data.get(0)).toEqual({ @@ -196,11 +197,11 @@ describe('loki result transformer', () => { }; const data = new CircularDataFrame({ capacity: 6 }); - data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } }); - data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } }); - data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' }; data.addField({ name: 'labels', type: FieldType.other }); + data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } }); + data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' }; data.addField({ name: 'id', type: FieldType.string }); + data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } }); data.refId = 'C'; ResultTransformer.appendResponseToBufferedData(tailResponse, data); diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 79fde9b62b2..8270288c32f 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -43,15 +43,10 @@ import { renderLegendFormat } from '../prometheus/legend'; const UUID_NAMESPACE = '6ec946da-0f49-47a8-983a-1d76d17e7c92'; /** - * Transforms LokiStreamResult structure into a dataFrame. Used when doing standard queries and newer version of Loki. + * Transforms LokiStreamResult structure into a dataFrame. Used when doing standard queries */ -export function lokiStreamResultToDataFrame(stream: LokiStreamResult, reverse?: boolean, refId?: string): DataFrame { - const labels: Labels = stream.stream; - const labelsString = Object.entries(labels) - .map(([key, val]) => `${key}="${val}"`) - .sort() - .join(''); - +export function lokiStreamsToRawDataFrame(streams: LokiStreamResult[], reverse?: boolean, refId?: string): DataFrame { + const labels = new ArrayVector<{}>([]); const times = new ArrayVector([]); const timesNs = new ArrayVector([]); const lines = new ArrayVector([]); @@ -60,12 +55,21 @@ export function lokiStreamResultToDataFrame(stream: LokiStreamResult, reverse?: // We need to store and track all used uids to ensure that uids are unique const usedUids: { string?: number } = {}; - for (const [ts, line] of stream.values) { - // num ns epoch in string, we convert it to iso string here so it matches old format - times.add(new Date(parseInt(ts.slice(0, -6), 10)).toISOString()); - timesNs.add(ts); - lines.add(line); - uids.add(createUid(ts, labelsString, line, usedUids, refId)); + for (const stream of streams) { + const streamLabels: Labels = stream.stream; + const labelsString = Object.entries(streamLabels) + .map(([key, val]) => `${key}="${val}"`) + .sort() + .join(''); + + for (const [ts, line] of stream.values) { + labels.add(streamLabels); + // num ns epoch in string, we convert it to iso string here so it matches old format + times.add(new Date(parseInt(ts.slice(0, -6), 10)).toISOString()); + timesNs.add(ts); + lines.add(line); + uids.add(createUid(ts, labelsString, line, usedUids, refId)); + } } return constructDataFrame(times, timesNs, lines, uids, labels, reverse, refId); @@ -79,17 +83,18 @@ function constructDataFrame( timesNs: ArrayVector, lines: ArrayVector, uids: ArrayVector, - labels: Labels, + labels: ArrayVector<{}>, reverse?: boolean, refId?: string ) { const dataFrame = { refId, fields: [ + { name: 'labels', type: FieldType.other, config: {}, values: labels }, { name: 'ts', type: FieldType.time, config: { displayName: 'Time' }, values: times }, // Time - { name: 'line', type: FieldType.string, config: {}, values: lines, labels }, // Line - needs to be the first field with string type - { name: 'id', type: FieldType.string, config: {}, values: uids }, + { name: 'line', type: FieldType.string, config: {}, values: lines }, // Line - needs to be the first field with string type { name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' }, values: timesNs }, // Time + { name: 'id', type: FieldType.string, config: {}, values: uids }, ], length: times.length, }; @@ -128,11 +133,11 @@ export function appendResponseToBufferedData(response: LokiTailResponse, data: M } } - const tsField = data.fields[0]; - const tsNsField = data.fields[1]; + const labelsField = data.fields[0]; + const tsField = data.fields[1]; const lineField = data.fields[2]; - const labelsField = data.fields[3]; - const idField = data.fields[4]; + const idField = data.fields[3]; + const tsNsField = data.fields[4]; // We are comparing used ids only within the received stream. This could be a problem if the same line + labels + nanosecond timestamp came in 2 separate batches. // As this is very unlikely, and the result would only affect live-tailing css animation we have decided to not compare all received uids from data param as this would slow down processing. @@ -346,20 +351,12 @@ export function lokiStreamsToDataFrames( preferredVisualisationType: 'logs', }; - const series: DataFrame[] = data.map((stream) => { - const dataFrame = lokiStreamResultToDataFrame(stream, reverse, target.refId); - enhanceDataFrame(dataFrame, config); + const dataFrame = lokiStreamsToRawDataFrame(data, reverse, target.refId); + enhanceDataFrame(dataFrame, config); - if (meta.custom && dataFrame.fields.some((f) => f.labels && Object.keys(f.labels).some((l) => l === '__error__'))) { - meta.custom.error = 'Error when parsing some of the logs'; - } - - return { - ...dataFrame, - refId: target.refId, - meta, - }; - }); + if (meta.custom && dataFrame.fields.some((f) => f.labels && Object.keys(f.labels).some((l) => l === '__error__'))) { + meta.custom.error = 'Error when parsing some of the logs'; + } if (stats.length && !data.length) { return [ @@ -372,7 +369,13 @@ export function lokiStreamsToDataFrames( ]; } - return series; + return [ + { + ...dataFrame, + refId: target.refId, + meta, + }, + ]; } /** From ce2a9252c281aab8d1f8f974abd95dbee3cd8be7 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Fri, 8 Apr 2022 11:06:45 +0300 Subject: [PATCH 014/156] Replace with secret (#47488) --- .drone.yml | 10 +++++++--- scripts/drone/pipelines/release.star | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index a3de2242549..81b2be1109e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2934,12 +2934,14 @@ steps: image: byrnedo/alpine-curl:0.1.8 name: grabpl - commands: - - ./bin/grabpl artifacts publish --security --tag ${TAG} --src-bucket grafana-prerelease + - ./bin/grabpl artifacts publish --security --tag ${TAG} --src-bucket $${PRERELEASE_BUCKET} depends_on: - grabpl environment: GCP_KEY: from_secret: gcp_key + PRERELEASE_BUCKET: + from_secret: prerelease_bucket image: grafana/grafana-ci-deploy:1.3.1 name: publish-artifacts trigger: @@ -2970,12 +2972,14 @@ steps: image: byrnedo/alpine-curl:0.1.8 name: grabpl - commands: - - ./bin/grabpl artifacts publish --tag ${TAG} --src-bucket grafana-prerelease + - ./bin/grabpl artifacts publish --tag ${TAG} --src-bucket $${PRERELEASE_BUCKET} depends_on: - grabpl environment: GCP_KEY: from_secret: gcp_key + PRERELEASE_BUCKET: + from_secret: prerelease_bucket image: grafana/grafana-ci-deploy:1.3.1 name: publish-artifacts trigger: @@ -4448,6 +4452,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 0fbba9c5099f1a3585f866b624f198058332624a4de32193a5326450a39cb8f3 +hmac: 9b87d4bb17d422973023c6ad1a59980c37832ae60ba6bb0d11ee024ea1e35b1a ... diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index de794ca93c1..0e10c9892fa 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -339,8 +339,9 @@ def publish_artifacts_step(mode): 'image': publish_image, 'environment': { 'GCP_KEY': from_secret('gcp_key'), + 'PRERELEASE_BUCKET': from_secret('prerelease_bucket'), }, - 'commands': ['./bin/grabpl artifacts publish {}--tag ${{TAG}} --src-bucket grafana-prerelease'.format(security)], + 'commands': ['./bin/grabpl artifacts publish {}--tag ${{TAG}} --src-bucket $${{PRERELEASE_BUCKET}}'.format(security)], 'depends_on': ['grabpl'], } From c8514756be162a4b09061bfc6a859b4e214dfa99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:26:30 +0100 Subject: [PATCH 015/156] Bump actions/checkout from 2 to 3 (#46068) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/backport.yml | 2 +- .github/workflows/bump-version.yml | 4 ++-- .github/workflows/close-milestone.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 2 +- .github/workflows/detect-breaking-changes-build.yml | 6 +++--- .github/workflows/detect-breaking-changes-report.yml | 2 +- .github/workflows/github-release.yml | 2 +- .github/workflows/metrics-collector.yml | 2 +- .github/workflows/pr-checks.yml | 2 +- .github/workflows/pr-codeql-analysis-go.yml | 2 +- .github/workflows/pr-codeql-analysis-javascript.yml | 2 +- .github/workflows/pr-codeql-analysis-python.yml | 2 +- .github/workflows/pr-commands.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/remove-milestone.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 55dc5c8da02..97042213f2c 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index f0aface7844..619ac66bd71 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -50,7 +50,7 @@ jobs: example: 7.4.3 or 7.4.3-beta1" exit 1 - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set intermedia variables id: intermedia @@ -69,7 +69,7 @@ jobs: exit 1 - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/close-milestone.yml b/.github/workflows/close-milestone.yml index a495eab7a8b..e798f0cd4bf 100644 --- a/.github/workflows/close-milestone.yml +++ b/.github/workflows/close-milestone.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c1578b4840f..6fd5101abc8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 7b5c492590f..ac5a03f9d5c 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/detect-breaking-changes-build.yml b/.github/workflows/detect-breaking-changes-build.yml index 87096eaf159..8e76bbe441b 100644 --- a/.github/workflows/detect-breaking-changes-build.yml +++ b/.github/workflows/detect-breaking-changes-build.yml @@ -11,7 +11,7 @@ jobs: working-directory: './pr' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: path: './pr' @@ -51,7 +51,7 @@ jobs: working-directory: './base' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: path: './base' ref: ${{ github.event.pull_request.base.ref }} @@ -92,7 +92,7 @@ jobs: GITHUB_STEP_NUMBER: 7 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Get built packages from pr uses: actions/download-artifact@v2 diff --git a/.github/workflows/detect-breaking-changes-report.yml b/.github/workflows/detect-breaking-changes-report.yml index fe5f2f1f490..fec1f33b93d 100644 --- a/.github/workflows/detect-breaking-changes-report.yml +++ b/.github/workflows/detect-breaking-changes-report.yml @@ -14,7 +14,7 @@ jobs: ARTIFACT_NAME: 'levitate' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: 'Download artifact' uses: actions/github-script@v6 diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 9f1f1fe31c5..f0223cfdcca 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/metrics-collector.yml b/.github/workflows/metrics-collector.yml index 84dec4e204a..874ccc4c3e7 100644 --- a/.github/workflows/metrics-collector.yml +++ b/.github/workflows/metrics-collector.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 16e00f6d094..bf41c507509 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -20,7 +20,7 @@ jobs: if: github.event.pull_request.draft == false steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/pr-codeql-analysis-go.yml b/.github/workflows/pr-codeql-analysis-go.yml index 6e6bd524cc7..d80c62f4f5c 100644 --- a/.github/workflows/pr-codeql-analysis-go.yml +++ b/.github/workflows/pr-codeql-analysis-go.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/pr-codeql-analysis-javascript.yml b/.github/workflows/pr-codeql-analysis-javascript.yml index 943bbfc1543..9fc371a61ad 100644 --- a/.github/workflows/pr-codeql-analysis-javascript.yml +++ b/.github/workflows/pr-codeql-analysis-javascript.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/pr-codeql-analysis-python.yml b/.github/workflows/pr-codeql-analysis-python.yml index 4f887526812..d9fc18b4f68 100644 --- a/.github/workflows/pr-codeql-analysis-python.yml +++ b/.github/workflows/pr-codeql-analysis-python.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/pr-commands.yml b/.github/workflows/pr-commands.yml index 7f47cfd8e92..ecd7361c550 100644 --- a/.github/workflows/pr-commands.yml +++ b/.github/workflows/pr-commands.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cc7d6bf4953..742dc7f9bac 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync - name: generate-packages-docs uses: actions/setup-node@v2.5.1 diff --git a/.github/workflows/remove-milestone.yml b/.github/workflows/remove-milestone.yml index ac2504a45f8..78305864e09 100644 --- a/.github/workflows/remove-milestone.yml +++ b/.github/workflows/remove-milestone.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 9ea0da2388e..fe03bdeed01 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "grafana/grafana-github-actions" path: ./actions From 2cf88cfec88373a61a22faf62469d50a35d21835 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Fri, 8 Apr 2022 10:33:19 +0200 Subject: [PATCH 016/156] Chore: Remove bus from contexthandler (#47458) * Chore: remove bus from contexthandler * remove bus from orgredirect --- pkg/api/common_test.go | 4 +++- pkg/api/http_server.go | 2 +- pkg/middleware/middleware_basic_auth_test.go | 6 ++---- pkg/middleware/middleware_test.go | 6 ++++-- pkg/middleware/org_redirect.go | 6 +++--- pkg/middleware/org_redirect_test.go | 4 +--- pkg/middleware/recovery_test.go | 2 +- pkg/services/contexthandler/auth_jwt.go | 3 +-- pkg/services/contexthandler/auth_proxy_test.go | 9 ++++++++- pkg/services/contexthandler/contexthandler.go | 10 +++++++--- pkg/services/login/logintest/logintest.go | 10 ++++++++++ pkg/services/sqlstore/mockstore/mockstore.go | 5 +++-- 12 files changed, 44 insertions(+), 23 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index b256c1454d7..f1870cca517 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -197,7 +197,9 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, sqlStore) - ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy) + loginService := &logintest.LoginServiceFake{} + authenticator := &logintest.AuthenticatorFake{} + ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, authenticator) return ctxHdlr } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 6d026fc5f26..fc87b642ca5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -507,7 +507,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(hs.pluginMetricsEndpoint) m.Use(hs.ContextHandler.Middleware) - m.Use(middleware.OrgRedirect(hs.Cfg)) + m.Use(middleware.OrgRedirect(hs.Cfg, hs.SQLStore)) m.Use(acmiddleware.LoadPermissionsMiddleware(hs.AccessControl)) // needs to be after context handler diff --git a/pkg/middleware/middleware_basic_auth_test.go b/pkg/middleware/middleware_basic_auth_test.go index 7588d2172e1..dda1b609269 100644 --- a/pkg/middleware/middleware_basic_auth_test.go +++ b/pkg/middleware/middleware_basic_auth_test.go @@ -88,6 +88,7 @@ func TestMiddlewareBasicAuth(t *testing.T) { }, configure) middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) { + sc.mockSQLStore.ExpectedError = models.ErrUserNotFound sc.fakeReq("GET", "/") sc.req.SetBasicAuth("user", "password") sc.exec() @@ -100,10 +101,7 @@ func TestMiddlewareBasicAuth(t *testing.T) { }, configure) middlewareScenario(t, "Should return error if user & password do not match", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("user-query", func(ctx context.Context, loginUserQuery *models.GetUserByLoginQuery) error { - return nil - }) - + sc.mockSQLStore.ExpectedError = models.ErrUserNotFound sc.fakeReq("GET", "/") sc.req.SetBasicAuth("killa", "gorilla") sc.exec() diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index fe645b87cd2..b52083dbeed 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/login/loginservice" + "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" @@ -602,7 +603,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( sc.sqlStore = ctxHdlr.SQLStore sc.contextHandler = ctxHdlr sc.m.Use(ctxHdlr.Middleware) - sc.m.Use(OrgRedirect(sc.cfg)) + sc.m.Use(OrgRedirect(sc.cfg, sc.mockSQLStore)) sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*auth.FakeUserAuthTokenService) sc.jwtAuthService = ctxHdlr.JWTAuthService.(*models.FakeJWTService) @@ -644,8 +645,9 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *mockstore.S authJWTSvc := models.NewFakeJWTService() tracer, err := tracing.InitializeTracerForTest() authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, mockSQLStore) + authenticator := &logintest.AuthenticatorFake{ExpectedUser: &models.User{}} require.NoError(t, err) - return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy) + return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, authenticator) } type fakeRenderService struct { diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index e6f8b638f52..ecf0d0800b0 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -6,16 +6,16 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) // OrgRedirect changes org and redirects users if the // querystring `orgId` doesn't match the active org. -func OrgRedirect(cfg *setting.Cfg) web.Handler { +func OrgRedirect(cfg *setting.Cfg, store sqlstore.Store) web.Handler { return func(res http.ResponseWriter, req *http.Request, c *web.Context) { orgIdValue := req.URL.Query().Get("orgId") orgId, err := strconv.ParseInt(orgIdValue, 10, 64) @@ -34,7 +34,7 @@ func OrgRedirect(cfg *setting.Cfg) web.Handler { } cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId} - if err := bus.Dispatch(ctx.Req.Context(), &cmd); err != nil { + if err := store.SetUsingOrg(ctx.Req.Context(), &cmd); err != nil { if ctx.IsApiRequest() { ctx.JsonApiErr(404, "Not found", nil) } else { diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index 5917e9a5377..22450ec880e 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -68,9 +68,7 @@ func TestOrgRedirectMiddleware(t *testing.T) { middlewareScenario(t, "when setting an invalid org for user", func(t *testing.T, sc *scenarioContext) { sc.withTokenSessionCookie("token") - bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error { - return fmt.Errorf("") - }) + sc.mockSQLStore.ExpectedSetUsingOrgError = fmt.Errorf("") sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12} sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 51b1c6c1c4d..7e3e4a5939f 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -73,7 +73,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { contextHandler := getContextHandler(t, nil, nil, nil) sc.m.Use(contextHandler.Middleware) // mock out gc goroutine - sc.m.Use(OrgRedirect(cfg)) + sc.m.Use(OrgRedirect(cfg, sc.mockSQLStore)) sc.defaultHandler = func(c *models.ReqContext) { sc.context = c diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index d29b4b07d65..cda763f2c37 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -3,7 +3,6 @@ package contexthandler import ( "errors" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" ) @@ -67,7 +66,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) SignupAllowed: h.Cfg.JWTAuthAutoSignUp, ExternalUser: extUser, } - if err := bus.Dispatch(ctx.Req.Context(), upsert); err != nil { + if err := h.loginService.UpsertUser(ctx.Req.Context(), upsert); err != nil { ctx.Logger.Error("Failed to upsert JWT user", "error", err) return false } diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 82e6245c428..48fea6227bb 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -84,8 +84,9 @@ func getContextHandler(t *testing.T) *ContextHandler { loginService := loginservice.LoginServiceMock{ExpectedUser: &models.User{Id: userID}} authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, &FakeGetSignUserStore{}) + authenticator := &fakeAuthenticator{} - return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy) + return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, authenticator) } type FakeGetSignUserStore struct { @@ -103,3 +104,9 @@ func (f *FakeGetSignUserStore) GetSignedInUser(ctx context.Context, query *model } return nil } + +type fakeAuthenticator struct{} + +func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { + return nil +} diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 99111ac9194..407b89fa326 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -9,12 +9,12 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" + loginpkg "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" @@ -36,7 +36,7 @@ const ServiceName = "ContextHandler" func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtService models.JWTService, remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore sqlstore.Store, - tracer tracing.Tracer, authProxy *authproxy.AuthProxy) *ContextHandler { + tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service, authenticator loginpkg.Authenticator) *ContextHandler { return &ContextHandler{ Cfg: cfg, AuthTokenService: tokenService, @@ -46,6 +46,8 @@ func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtS SQLStore: sqlStore, tracer: tracer, authProxy: authProxy, + authenticator: authenticator, + loginService: loginService, } } @@ -59,6 +61,8 @@ type ContextHandler struct { SQLStore sqlstore.Store tracer tracing.Tracer authProxy *authproxy.AuthProxy + authenticator loginpkg.Authenticator + loginService login.Service // GetTime returns the current time. // Stubbable by tests. GetTime func() time.Time @@ -291,7 +295,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, Password: password, Cfg: h.Cfg, } - if err := bus.Dispatch(reqContext.Req.Context(), &authQuery); err != nil { + if err := h.authenticator.AuthenticateUser(reqContext.Req.Context(), &authQuery); err != nil { reqContext.Logger.Debug( "Failed to authorize the user", "username", username, diff --git a/pkg/services/login/logintest/logintest.go b/pkg/services/login/logintest/logintest.go index 6197e8d5ce9..020714632e8 100644 --- a/pkg/services/login/logintest/logintest.go +++ b/pkg/services/login/logintest/logintest.go @@ -49,3 +49,13 @@ func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, qu query.Result = a.ExpectedExternalUser return a.ExpectedError } + +type AuthenticatorFake struct { + ExpectedUser *models.User + ExpectedError error +} + +func (a *AuthenticatorFake) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { + query.User = a.ExpectedUser + return a.ExpectedError +} diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 2094f0100da..f66b16b723e 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -44,7 +44,8 @@ type SQLStoreMock struct { ExpectedUserStars map[int64]bool ExpectedLoginAttempts int64 - ExpectedError error + ExpectedError error + ExpectedSetUsingOrgError error } func NewSQLStoreMock() *SQLStoreMock { @@ -178,7 +179,7 @@ func (m *SQLStoreMock) UpdateUserLastSeenAt(ctx context.Context, cmd *models.Upd } func (m *SQLStoreMock) SetUsingOrg(ctx context.Context, cmd *models.SetUsingOrgCommand) error { - return m.ExpectedError + return m.ExpectedSetUsingOrgError } func (m *SQLStoreMock) GetUserProfile(ctx context.Context, query *models.GetUserProfileQuery) error { From 9574f447bbcf8a60ff1d693041f294d7e9a0aec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 8 Apr 2022 11:00:16 +0200 Subject: [PATCH 017/156] grafana-data: removed DataQueryRequest.reverse (#47487) --- packages/grafana-data/src/types/datasource.ts | 1 - public/app/plugins/datasource/loki/datasource.ts | 3 +-- public/app/plugins/datasource/loki/result_transformer.ts | 5 ++--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 94276a9114d..69717854e2b 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -481,7 +481,6 @@ export interface DataQueryRequest { intervalMs: number; maxDataPoints?: number; range: TimeRange; - reverse?: boolean; scopedVars: ScopedVars; targets: TQuery[]; timezone: string; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 1259918bdfc..944d62bbe07 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -309,8 +309,7 @@ export class LokiDatasource responseListLength, maxDataPoints, this.instanceSettings.jsonData, - (options as DataQueryRequest).scopedVars, - (options as DataQueryRequest).reverse + (options as DataQueryRequest).scopedVars ) ) ); diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 8270288c32f..31760ee0d08 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -520,13 +520,12 @@ export function processRangeQueryResponse( responseListLength: number, limit: number, config: LokiOptions, - scopedVars: ScopedVars, - reverse = false + scopedVars: ScopedVars ) { switch (response.data.resultType) { case LokiResultType.Stream: return of({ - data: lokiStreamsToDataFrames(response as LokiStreamResponse, target, limit, config, reverse), + data: lokiStreamsToDataFrames(response as LokiStreamResponse, target, limit, config), key: `${target.refId}_log`, }); From beed3fd5deab3144377eef615d2275491bd1838d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 8 Apr 2022 11:24:09 +0200 Subject: [PATCH 018/156] loki: remove unused reverse-attribute (#47494) --- public/app/plugins/datasource/loki/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index c72954e7c38..da887ec18a9 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -35,7 +35,6 @@ export interface LokiQuery extends DataQuery { expr: string; query?: string; format?: string; - reverse?: boolean; legendFormat?: string; valueWithRefId?: boolean; maxLines?: number; From 416da59c438b64bcc8f75453c15d34a34e3ccbc9 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 8 Apr 2022 10:44:17 +0100 Subject: [PATCH 019/156] Routing: Fix links to different port from being treated as internal links (#45192) * Add some failing tests that trigger the bug * start at refactor of stripBaseFromUrl --- .../grafana-data/src/utils/location.test.ts | 132 +++++++++++------- packages/grafana-data/src/utils/location.ts | 29 +++- 2 files changed, 108 insertions(+), 53 deletions(-) diff --git a/packages/grafana-data/src/utils/location.test.ts b/packages/grafana-data/src/utils/location.test.ts index 174a505601d..b56660bbd01 100644 --- a/packages/grafana-data/src/utils/location.test.ts +++ b/packages/grafana-data/src/utils/location.test.ts @@ -3,7 +3,7 @@ import { locationUtil } from './location'; describe('locationUtil', () => { const { location } = window; - beforeAll(() => { + beforeEach(() => { // @ts-ignore delete window.location; @@ -21,63 +21,97 @@ describe('locationUtil', () => { }; }); - afterAll(() => { + afterEach(() => { window.location = location; }); - describe('strip base when appSubUrl configured', () => { - beforeEach(() => { - locationUtil.initialize({ - config: { appSubUrl: '/subUrl' } as any, - getVariablesUrlParams: (() => {}) as any, - getTimeRangeForUrl: (() => {}) as any, + describe('stripBaseFromUrl', () => { + describe('when appSubUrl configured', () => { + beforeEach(() => { + locationUtil.initialize({ + config: { appSubUrl: '/subUrl' } as any, + getVariablesUrlParams: (() => {}) as any, + getTimeRangeForUrl: (() => {}) as any, + }); }); - }); - test('relative url', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/'); - expect(urlWithoutMaster).toBe('/thisShouldRemain/'); - }); - test('relative url with multiple subUrl in path', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/'); - expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/'); - }); - test('relative url with subdirectory subUrl', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/'); - expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/'); - }); - test('absolute url', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/'); - expect(urlWithoutMaster).toBe('/thisShouldRemain/'); - }); - test('absolute url with multiple subUrl in path', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl( - 'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/' - ); - expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/'); - }); - test('absolute url with subdirectory subUrl', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/'); - expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/'); - }); - }); - - describe('strip base when appSubUrl not configured', () => { - beforeEach(() => { - locationUtil.initialize({ - config: {} as any, - getVariablesUrlParams: (() => {}) as any, - getTimeRangeForUrl: (() => {}) as any, + test('relative url', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/'); + expect(urlWithoutMaster).toBe('/thisShouldRemain/'); + }); + test('relative url with multiple subUrl in path', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/'); + expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/'); + }); + test('relative url with subdirectory subUrl', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/'); + expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/'); + }); + test('absolute url', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/'); + expect(urlWithoutMaster).toBe('/thisShouldRemain/'); + }); + test('absolute url with multiple subUrl in path', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl( + 'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/' + ); + expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/'); + }); + test('absolute url with subdirectory subUrl', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/'); + expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/'); }); }); - test('relative url', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/'); - expect(urlWithoutMaster).toBe('/subUrl/grafana/'); + describe('when appSubUrl not configured', () => { + beforeEach(() => { + locationUtil.initialize({ + config: {} as any, + getVariablesUrlParams: (() => {}) as any, + getTimeRangeForUrl: (() => {}) as any, + }); + }); + + test('relative url', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('/subUrl/grafana/'); + }); + + test('absolute url', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('/subUrl/grafana/'); + }); }); - test('absolute url', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/'); - expect(urlWithoutMaster).toBe('/subUrl/grafana/'); + describe('when origin does not have a port in it', () => { + beforeEach(() => { + window.location = { + ...location, + hash: '#hash', + host: 'www.domain.com', + hostname: 'www.domain.com', + href: 'http://www.domain.com/path/b?search=a&b=c&d#hash', + origin: 'http://www.domain.com', + pathname: '/path/b', + port: '', + protocol: 'http:', + search: '?search=a&b=c&d', + }; + }); + + test('relative url', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('/subUrl/grafana/'); + }); + + test('URL with same host, different port', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('http://www.domain.com:9877/subUrl/grafana/'); + }); + + test('URL of a completely different origin', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.another-domain.com/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('http://www.another-domain.com/subUrl/grafana/'); + }); }); }); diff --git a/packages/grafana-data/src/utils/location.ts b/packages/grafana-data/src/utils/location.ts index 83f481067bb..7df8796b41b 100644 --- a/packages/grafana-data/src/utils/location.ts +++ b/packages/grafana-data/src/utils/location.ts @@ -7,22 +7,43 @@ let grafanaConfig: GrafanaConfig = { appSubUrl: '' } as any; let getTimeRangeUrlParams: () => RawTimeRange; let getVariablesUrlParams: (scopedVars?: ScopedVars) => UrlQueryMap; +const maybeParseUrl = (input: string): URL | undefined => { + try { + return new URL(input); + } catch { + return undefined; + } +}; + /** * * @param url * @internal */ -const stripBaseFromUrl = (url: string): string => { +const stripBaseFromUrl = (urlOrPath: string): string => { + // Will only return a URL object if the input is actually a valid URL + const parsedUrl = maybeParseUrl(urlOrPath); + if (parsedUrl) { + // If the input is a URL, and for a different origin that we're on, just bail + // and return it. There's no need to strip anything from it + if (parsedUrl.origin !== window.location.origin) { + return urlOrPath; + } + } + const appSubUrl = grafanaConfig.appSubUrl ?? ''; const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; - const isAbsoluteUrl = url.startsWith('http'); + const isAbsoluteUrl = urlOrPath.startsWith('http'); + let segmentToStrip = appSubUrl; - if (!url.startsWith('/') || isAbsoluteUrl) { + if (!urlOrPath.startsWith('/') || isAbsoluteUrl) { segmentToStrip = `${window.location.origin}${appSubUrl}`; } - return url.length > 0 && url.indexOf(segmentToStrip) === 0 ? url.slice(segmentToStrip.length - stripExtraChars) : url; + return urlOrPath.length > 0 && urlOrPath.indexOf(segmentToStrip) === 0 + ? urlOrPath.slice(segmentToStrip.length - stripExtraChars) + : urlOrPath; }; /** From f1a1070d41993c0dabba8162082fc8a22b2ac738 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Fri, 8 Apr 2022 12:42:33 +0200 Subject: [PATCH 020/156] UsageStats: track enabled features (#47407) * #47127: include enabled features in the usage stats reports * #47127: convert feature names to snake cased metric names * #47127: remove dead code * #47127: lint fix * #47127: convert GetUsageStats to return `map[string]interface{}` * #47127: fix testssssssss * #47127: fix testssssssss --- pkg/infra/usagestats/service/service.go | 5 +- pkg/infra/usagestats/service/usage_stats.go | 5 + .../usagestats/service/usage_stats_test.go | 10 ++ pkg/services/featuremgmt/strcase/snake.go | 118 ++++++++++++++++++ pkg/services/featuremgmt/usage_stats.go | 21 ++++ pkg/services/featuremgmt/usage_stats_test.go | 34 +++++ 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 pkg/services/featuremgmt/strcase/snake.go create mode 100644 pkg/services/featuremgmt/usage_stats.go create mode 100644 pkg/services/featuremgmt/usage_stats_test.go diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index ceb8a321fbc..486f986f697 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -22,6 +23,7 @@ type UsageStats struct { SocialService social.Service kvStore *kvstore.NamespacedKVStore RouteRegister routing.RouteRegister + features *featuremgmt.FeatureManager log log.Logger @@ -33,11 +35,12 @@ type UsageStats struct { } func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store, - socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister, + socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister, features *featuremgmt.FeatureManager, ) *UsageStats { s := &UsageStats{ Cfg: cfg, SQLStore: sqlStore, + features: features, oauthProviders: socialService.GetOAuthProviders(), RouteRegister: routeRegister, pluginStore: pluginStore, diff --git a/pkg/infra/usagestats/service/usage_stats.go b/pkg/infra/usagestats/service/usage_stats.go index b67e1db0aa9..a9e7b4fdf6b 100644 --- a/pkg/infra/usagestats/service/usage_stats.go +++ b/pkg/infra/usagestats/service/usage_stats.go @@ -230,6 +230,11 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e metrics["stats.uptime"] = int64(time.Since(uss.startTime).Seconds()) + featureUsageStats := uss.features.GetUsageStats(ctx) + for name, val := range featureUsageStats { + metrics[name] = val + } + return report, nil } diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index 13acd1155aa..a150e26d316 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" @@ -405,6 +406,14 @@ func TestMetrics(t *testing.T) { metric := report.Metrics[metricName] assert.Equal(t, 1, metric) }) + + t.Run("Should include feature usage stats", func(t *testing.T) { + report, err := uss.GetUsageReport(context.Background()) + require.NoError(t, err, "Expected no error") + + assert.Equal(t, 1, report.Metrics["stats.features.feature_1.count"]) + assert.Equal(t, 1, report.Metrics["stats.features.feature_2.count"]) + }) }) t.Run("When registering external metrics", func(t *testing.T) { @@ -511,5 +520,6 @@ func createService(t *testing.T, cfg setting.Cfg, sqlStore sqlstore.Store, withD log: log.New("infra.usagestats"), startTime: time.Now().Add(-1 * time.Minute), RouteRegister: routing.NewRouteRegister(), + features: featuremgmt.WithFeatures("feature1", "feature2"), } } diff --git a/pkg/services/featuremgmt/strcase/snake.go b/pkg/services/featuremgmt/strcase/snake.go new file mode 100644 index 00000000000..012fdb181d2 --- /dev/null +++ b/pkg/services/featuremgmt/strcase/snake.go @@ -0,0 +1,118 @@ +// https://github.com/iancoleman/strcase/blob/master/snake.go + +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Ian Coleman + * Copyright (c) 2018 Ma_124, + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, Subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or Substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package strcase + +import ( + "strings" +) + +// ToSnake converts a string to snake_case +func ToSnake(s string) string { + return ToDelimited(s, '_') +} + +func ToSnakeWithIgnore(s string, ignore string) string { + return ToScreamingDelimited(s, '_', ignore, false) +} + +// ToScreamingSnake converts a string to SCREAMING_SNAKE_CASE +func ToScreamingSnake(s string) string { + return ToScreamingDelimited(s, '_', "", true) +} + +// ToKebab converts a string to kebab-case +func ToKebab(s string) string { + return ToDelimited(s, '-') +} + +// ToScreamingKebab converts a string to SCREAMING-KEBAB-CASE +func ToScreamingKebab(s string) string { + return ToScreamingDelimited(s, '-', "", true) +} + +// ToDelimited converts a string to delimited.snake.case +// (in this case `delimiter = '.'`) +func ToDelimited(s string, delimiter uint8) string { + return ToScreamingDelimited(s, delimiter, "", false) +} + +// ToScreamingDelimited converts a string to SCREAMING.DELIMITED.SNAKE.CASE +// (in this case `delimiter = '.'; screaming = true`) +// or delimited.snake.case +// (in this case `delimiter = '.'; screaming = false`) +//nolint: gocyclo +func ToScreamingDelimited(s string, delimiter uint8, ignore string, screaming bool) string { + s = strings.TrimSpace(s) + n := strings.Builder{} + n.Grow(len(s) + 2) // nominal 2 bytes of extra space for inserted delimiters + for i, v := range []byte(s) { + vIsCap := v >= 'A' && v <= 'Z' + vIsLow := v >= 'a' && v <= 'z' + if vIsLow && screaming { + v += 'A' + v -= 'a' + } else if vIsCap && !screaming { + v += 'a' + v -= 'A' + } + + // treat acronyms as words, eg for JSONData -> JSON is a whole word + if i+1 < len(s) { + next := s[i+1] + vIsNum := v >= '0' && v <= '9' + nextIsCap := next >= 'A' && next <= 'Z' + nextIsLow := next >= 'a' && next <= 'z' + nextIsNum := next >= '0' && next <= '9' + // add underscore if next letter case type is changed + if (vIsCap && (nextIsLow || nextIsNum)) || (vIsLow && (nextIsCap || nextIsNum)) || (vIsNum && (nextIsCap || nextIsLow)) { + prevIgnore := ignore != "" && i > 0 && strings.ContainsAny(string(s[i-1]), ignore) + if !prevIgnore { + if vIsCap && nextIsLow { + if prevIsCap := i > 0 && s[i-1] >= 'A' && s[i-1] <= 'Z'; prevIsCap { + n.WriteByte(delimiter) + } + } + n.WriteByte(v) + if vIsLow || vIsNum || nextIsNum { + n.WriteByte(delimiter) + } + continue + } + } + } + + if (v == ' ' || v == '_' || v == '-' || v == '.') && !strings.ContainsAny(string(v), ignore) { + // replace space/underscore/hyphen/dot with delimiter + n.WriteByte(delimiter) + } else { + n.WriteByte(v) + } + } + + return n.String() +} diff --git a/pkg/services/featuremgmt/usage_stats.go b/pkg/services/featuremgmt/usage_stats.go new file mode 100644 index 00000000000..f33e77834a6 --- /dev/null +++ b/pkg/services/featuremgmt/usage_stats.go @@ -0,0 +1,21 @@ +package featuremgmt + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/services/featuremgmt/strcase" +) + +func (fm *FeatureManager) GetUsageStats(ctx context.Context) map[string]interface{} { + enabled := fm.GetEnabled(ctx) + stats := make(map[string]interface{}, len(enabled)) + for featureName := range enabled { + stats[asMetricName(featureName)] = 1 + } + return stats +} + +func asMetricName(featureName string) string { + return fmt.Sprintf("stats.features.%s.count", strcase.ToSnake(featureName)) +} diff --git a/pkg/services/featuremgmt/usage_stats_test.go b/pkg/services/featuremgmt/usage_stats_test.go new file mode 100644 index 00000000000..1bc5420e4ff --- /dev/null +++ b/pkg/services/featuremgmt/usage_stats_test.go @@ -0,0 +1,34 @@ +package featuremgmt + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFeatureUsageStats(t *testing.T) { + featureManagerWithAllFeatures := WithFeatures( + "trimDefaults", + "httpclientprovider_azure_auth", + "service-accounts", + "database_metrics", + "dashboardPreviews", + "live-config", + "showFeatureFlagsInUI", + "UPPER_SNAKE_CASE", + "feature.with.a.dot", + ) + + require.Equal(t, map[string]interface{}{ + "stats.features.trim_defaults.count": 1, + "stats.features.httpclientprovider_azure_auth.count": 1, + "stats.features.service_accounts.count": 1, + "stats.features.database_metrics.count": 1, + "stats.features.dashboard_previews.count": 1, + "stats.features.live_config.count": 1, + "stats.features.show_feature_flags_in_ui.count": 1, + "stats.features.upper_snake_case.count": 1, + "stats.features.feature_with_a_dot.count": 1, + }, featureManagerWithAllFeatures.GetUsageStats(context.Background())) +} From 87383b1c8b27a4b0b8cc59714ae97720505bc7b9 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 8 Apr 2022 13:34:02 +0200 Subject: [PATCH 021/156] Alerting: FGAC for alert rules page (#47418) * Apply FGAC on the alert rules list page * Add tests for edit, delete and silence buttons * Unify access-control helpers * Fix import * Add route permissions for alert groups, unify access control helpers * Improve buttons with data source explore permission * Fix test --- public/app/features/alerting/routes.tsx | 21 ++ .../alerting/unified/AlertGroups.test.tsx | 1 + .../components/alert-groups/AlertDetails.tsx | 28 ++- .../rule-viewer/RuleViewerVisualization.tsx | 32 +-- .../components/rules/RuleDetails.test.tsx | 194 ++++++++++++++++++ .../unified/components/rules/RuleDetails.tsx | 12 +- .../rules/RuleDetailsActionButtons.tsx | 72 ++++--- .../unified/components/rules/RulesGroup.tsx | 6 +- .../alerting/unified/utils/access-control.ts | 63 ++++-- 9 files changed, 351 insertions(+), 78 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 1e64912cc57..ea93183aa40 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types'; import { uniq } from 'lodash'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; +import { OrgRole } from '@grafana/data'; import { evaluateAccess } from './unified/utils/access-control'; const commonRoutes: RouteDescriptor[] = [ @@ -90,6 +91,10 @@ const unifiedRoutes: RouteDescriptor[] = [ ...commonRoutes, { path: '/alerting/list', + roles: evaluateAccess( + [AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead], + [OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertRuleListIndex" */ 'app/features/alerting/unified/RuleList') ), @@ -214,6 +219,10 @@ const unifiedRoutes: RouteDescriptor[] = [ }, { path: '/alerting/groups/', + roles: evaluateAccess( + [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead], + [OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups') ), @@ -221,6 +230,10 @@ const unifiedRoutes: RouteDescriptor[] = [ { path: '/alerting/new', pageClass: 'page-alerting', + roles: evaluateAccess( + [AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite], + [OrgRole.Editor, OrgRole.Admin] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') ), @@ -228,6 +241,10 @@ const unifiedRoutes: RouteDescriptor[] = [ { path: '/alerting/:id/edit', pageClass: 'page-alerting', + roles: evaluateAccess( + [AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite], + [OrgRole.Editor, OrgRole.Admin] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') ), @@ -235,6 +252,10 @@ const unifiedRoutes: RouteDescriptor[] = [ { path: '/alerting/:sourceName/:id/view', pageClass: 'page-alerting', + roles: evaluateAccess( + [AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead], + [OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertingRule"*/ 'app/features/alerting/unified/RuleViewer') ), diff --git a/public/app/features/alerting/unified/AlertGroups.test.tsx b/public/app/features/alerting/unified/AlertGroups.test.tsx index ec9704c7ede..765af678612 100644 --- a/public/app/features/alerting/unified/AlertGroups.test.tsx +++ b/public/app/features/alerting/unified/AlertGroups.test.tsx @@ -16,6 +16,7 @@ jest.mock('app/core/services/context_srv', () => ({ contextSrv: { isEditor: true, hasAccess: () => true, + hasPermission: () => true, }, })); const mocks = { diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx index 53818045141..878620f5fd5 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx @@ -3,11 +3,13 @@ import { GrafanaTheme2 } from '@grafana/data'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types'; +import { AccessControlAction } from 'app/types'; import React, { FC } from 'react'; +import { getInstancesPermissions } from '../../utils/access-control'; +import { isGrafanaRulesSource } from '../../utils/datasource'; import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc'; import { AnnotationDetailsField } from '../AnnotationDetailsField'; import { Authorize } from '../Authorize'; -import { getInstancesPermissions } from '../../utils/access-control'; interface AmNotificationsAlertDetailsProps { alertManagerSourceName: string; @@ -16,11 +18,19 @@ interface AmNotificationsAlertDetailsProps { export const AlertDetails: FC = ({ alert, alertManagerSourceName }) => { const styles = useStyles2(getStyles); - const permissions = getInstancesPermissions(alertManagerSourceName); + const instancePermissions = getInstancesPermissions(alertManagerSourceName); + + // For Grafana Managed alerts the Generator URL redirects to the alert rule edit page, so update permission is required + // For external alert manager the Generator URL redirects to an external service which we don't control + const isGrafanaSource = isGrafanaRulesSource(alertManagerSourceName); + const isSeeSourceButtonEnabled = isGrafanaSource + ? contextSrv.hasPermission(AccessControlAction.AlertingRuleUpdate) + : true; + return ( <>
- + {alert.status.state === AlertState.Suppressed && ( = ({ alert, aler )} - - {alert.generatorURL && ( - - See source - - )} - + {isSeeSourceButtonEnabled && alert.generatorURL && ( + + See source + + )}
{Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => ( diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx index 26eb3ed2fd0..5d419399fcc 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx @@ -9,6 +9,8 @@ import { AlertQuery } from 'app/types/unified-alerting-dto'; import AutoSizer from 'react-virtualized-auto-sizer'; import { PanelPluginsButtonGroup, SupportedPanelPlugins } from '../PanelPluginsButtonGroup'; import { TABLE, TIMESERIES } from '../../utils/constants'; +import { Authorize } from '../Authorize'; +import { AccessControlAction } from 'app/types'; type RuleViewerVisualizationProps = { data?: PanelData; @@ -91,20 +93,22 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS /> ) : null} - {!isExpressionQuery(query.model) && ( - <> -
- - View in Explore - - - )} + + {!isExpressionQuery(query.model) && ( + <> +
+ + View in Explore + + + )} +
{ + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true }); + + describe('Grafana rules action buttons', () => { + const grafanaRule = getGrafanaRule({ name: 'Grafana' }); + it('Should not render Edit button for users without the update permission', () => { + // Arrange + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + // Act + renderRuleDetails(grafanaRule); + + // Assert + expect(ui.actionButtons.edit.query()).not.toBeInTheDocument(); + }); + + it('Should not render Delete button for users without the delete permission', () => { + // Arrange + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + // Act + renderRuleDetails(grafanaRule); + + // Assert + expect(ui.actionButtons.delete.query()).not.toBeInTheDocument(); + }); + + it('Should render Edit button for users with the update permission', () => { + // Arrange + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => action === AccessControlAction.AlertingRuleUpdate); + + // Act + renderRuleDetails(grafanaRule); + + // Assert + expect(ui.actionButtons.edit.query()).toBeInTheDocument(); + }); + + it('Should render Delete button for users with the delete permission', () => { + // Arrange + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => action === AccessControlAction.AlertingRuleDelete); + + // Act + renderRuleDetails(grafanaRule); + + // Assert + expect(ui.actionButtons.delete.query()).toBeInTheDocument(); + }); + + it('Should not render Silence button for users wihout the instance create permission', () => { + // Arrange + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + // Act + renderRuleDetails(grafanaRule); + + // Assert + expect(ui.actionButtons.silence.query()).not.toBeInTheDocument(); + }); + + it('Should render Silence button for users with the instance create permissions', () => { + // Arrange + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate); + + // Act + renderRuleDetails(grafanaRule); + + // Assert + expect(ui.actionButtons.silence.query()).toBeInTheDocument(); + }); + }); + + describe('Cloud rules action buttons', () => { + const cloudRule = getCloudRule({ name: 'Cloud' }); + it('Should not render Edit button for users without the update permission', () => { + // Arrange + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + // Act + renderRuleDetails(cloudRule); + + // Assert + expect(ui.actionButtons.edit.query()).not.toBeInTheDocument(); + }); + + it('Should not render Delete button for users without the delete permission', () => { + // Arrange + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + // Act + renderRuleDetails(cloudRule); + + // Assert + expect(ui.actionButtons.delete.query()).not.toBeInTheDocument(); + }); + + it('Should render Edit button for users with the update permission', () => { + // Arrange + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite); + + // Act + renderRuleDetails(cloudRule); + + // Assert + expect(ui.actionButtons.edit.query()).toBeInTheDocument(); + }); + + it('Should render Delete button for users with the delete permission', () => { + // Arrange + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite); + + // Act + renderRuleDetails(cloudRule); + + // Assert + expect(ui.actionButtons.delete.query()).toBeInTheDocument(); + }); + }); +}); + +function renderRuleDetails(rule: CombinedRule) { + const store = configureStore(); + + render( + + + + + + ); +} + +function getGrafanaRule(override?: Partial) { + return mockCombinedRule({ + namespace: { + groups: [], + name: 'Grafana', + rulesSource: 'grafana', + }, + ...override, + }); +} + +function getCloudRule(override?: Partial) { + return mockCombinedRule({ + namespace: { + groups: [], + name: 'Cortex', + rulesSource: mockDataSource(), + }, + promRule: mockPromAlertingRule(), + rulerRule: mockRulerAlertingRule(), + ...override, + }); +} diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx index af21b4d4ab2..db8a5fc8e8c 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx @@ -1,15 +1,15 @@ -import { CombinedRule } from 'app/types/unified-alerting'; -import React, { FC } from 'react'; -import { useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; +import { CombinedRule } from 'app/types/unified-alerting'; +import React, { FC } from 'react'; import { AlertLabels } from '../AlertLabels'; import { DetailsField } from '../DetailsField'; import { RuleDetailsActionButtons } from './RuleDetailsActionButtons'; -import { RuleDetailsDataSources } from './RuleDetailsDataSources'; -import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances'; -import { RuleDetailsExpression } from './RuleDetailsExpression'; import { RuleDetailsAnnotations } from './RuleDetailsAnnotations'; +import { RuleDetailsDataSources } from './RuleDetailsDataSources'; +import { RuleDetailsExpression } from './RuleDetailsExpression'; +import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances'; interface Props { rule: CombinedRule; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 3e6640a24ef..7f26d28ba87 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -1,24 +1,25 @@ -import React, { FC, Fragment, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import { css } from '@emotion/css'; import { GrafanaTheme2, urlUtil } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Button, ConfirmModal, ClipboardButton, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui'; -import { contextSrv } from 'app/core/services/context_srv'; +import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control'; +import { AccessControlAction } from 'app/types'; +import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; +import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; +import React, { FC, Fragment, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; import { useIsRuleEditable } from '../../hooks/useIsRuleEditable'; +import { useStateHistoryModal } from '../../hooks/useStateHistoryModal'; +import { deleteRuleAction } from '../../state/actions'; +import { getAlertmanagerByUid } from '../../utils/alertmanager'; import { Annotation } from '../../utils/constants'; import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource'; import { createExploreLink, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc'; import * as ruleId from '../../utils/rule-id'; -import { deleteRuleAction } from '../../state/actions'; -import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; -import { getAlertmanagerByUid } from '../../utils/alertmanager'; -import { useStateHistoryModal } from '../../hooks/useStateHistoryModal'; -import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { isFederatedRuleGroup } from '../../utils/rules'; -import { AccessControlAction } from 'app/types'; interface Props { rule: CombinedRule; @@ -38,12 +39,18 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { const alertmanagerSourceName = isGrafanaRulesSource(rulesSource) ? rulesSource : getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name; + const rulesSourceName = getRulesSourceName(rulesSource); + + const rulesPermissions = getRulesPermissions(rulesSourceName); + const hasEditPermission = contextSrv.hasPermission(rulesPermissions.update); + const hasDeletePermission = contextSrv.hasPermission(rulesPermissions.delete); + const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore); const leftButtons: JSX.Element[] = []; const rightButtons: JSX.Element[] = []; const isFederated = isFederatedRuleGroup(group); - const { isEditable } = useIsRuleEditable(getRulesSourceName(rulesSource), rulerRule); + const { isEditable } = useIsRuleEditable(rulesSourceName, rulerRule); const returnTo = location.pathname + location.search; const isViewMode = inViewMode(location.pathname); @@ -74,7 +81,7 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { // explore does not support grafana rule queries atm // neither do "federated rules" - if (isCloudRulesSource(rulesSource) && contextSrv.isEditor && !isFederated) { + if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) { leftButtons.push( = ({ rule, rulesSource }) => { ); } + // TODO Maybe there is a way to unify isEditable with FGAC permissions if (isEditable && rulerRule && !isFederated) { const sourceName = getRulesSourceName(rulesSource); const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule); @@ -210,22 +218,28 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { ); } - rightButtons.push( - - Edit - , - - ); + if (hasEditPermission) { + rightButtons.push( + + Edit + + ); + } + if (hasDeletePermission) { + rightButtons.push( + + ); + } } if (leftButtons.length || rightButtons.length) { return ( diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index b534a3e8592..ef668398c0c 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -1,7 +1,9 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui'; +import { contextSrv } from 'app/core/services/context_srv'; import kbn from 'app/core/utils/kbn'; +import { AccessControlAction } from 'app/types'; import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; import pluralize from 'pluralize'; import React, { FC, useEffect, useState } from 'react'; @@ -32,6 +34,8 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } const [isDeletingGroup, setIsDeletingGroup] = useState(false); const [isCollapsed, setIsCollapsed] = useState(!expandAll); + const canEditCloudRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite); + useEffect(() => { setIsCollapsed(!expandAll); }, [expandAll]); @@ -88,7 +92,7 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } ); } } - } else if (hasRuler(rulesSource)) { + } else if (canEditCloudRules && hasRuler(rulesSource)) { if (!isFederated) { actionIcons.push( Date: Fri, 8 Apr 2022 12:41:26 +0100 Subject: [PATCH 022/156] UsageStats: Move stats collection to separate service (#47381) * Remove specific stats from usage stats service * Create statscollector service * refactor * Update and move tests Mostly equivalent tests to before, but they've been divided over the two services and removed the behavior driven legacy from GoConvey to reduce the complexity of the tests. * Collect featuremgmr metrics (copied over from #47407) I removed the metrics registration from the feature manager in the merge and re-add them in this commit. Separated to make things easier to review. --- pkg/infra/usagestats/mock.go | 5 +- pkg/infra/usagestats/service/service.go | 89 +-- pkg/infra/usagestats/service/types.go | 10 - pkg/infra/usagestats/service/usage_stats.go | 258 +------- .../usagestats/service/usage_stats_test.go | 549 +++++------------- .../statscollector/concurrent_users.go | 58 ++ .../concurrent_users_test.go} | 80 ++- .../usagestats/statscollector/service.go | 321 ++++++++++ .../usagestats/statscollector/service_test.go | 362 ++++++++++++ .../backgroundsvcs/background_services.go | 12 +- pkg/server/wire.go | 3 + 11 files changed, 964 insertions(+), 783 deletions(-) delete mode 100644 pkg/infra/usagestats/service/types.go create mode 100644 pkg/infra/usagestats/statscollector/concurrent_users.go rename pkg/infra/usagestats/{service/usage_stats_service_test.go => statscollector/concurrent_users_test.go} (57%) create mode 100644 pkg/infra/usagestats/statscollector/service.go create mode 100644 pkg/infra/usagestats/statscollector/service_test.go diff --git a/pkg/infra/usagestats/mock.go b/pkg/infra/usagestats/mock.go index bead572f455..0510233fbb8 100644 --- a/pkg/infra/usagestats/mock.go +++ b/pkg/infra/usagestats/mock.go @@ -2,6 +2,7 @@ package usagestats import ( "context" + "strings" "testing" "github.com/stretchr/testify/require" @@ -29,8 +30,8 @@ func (usm *UsageStatsMock) GetUsageReport(ctx context.Context) (Report, error) { return Report{Metrics: all}, nil } -func (usm *UsageStatsMock) ShouldBeReported(_ context.Context, _ string) bool { - return true +func (usm *UsageStatsMock) ShouldBeReported(_ context.Context, s string) bool { + return !strings.HasPrefix(s, "unknown") } func (usm *UsageStatsMock) RegisterSendReportCallback(_ SendReportCallbackFunc) {} diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index 486f986f697..c31385d7d5a 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -2,51 +2,35 @@ package service import ( "context" - "fmt" "time" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) type UsageStats struct { Cfg *setting.Cfg - SQLStore sqlstore.Store - pluginStore plugins.Store - SocialService social.Service kvStore *kvstore.NamespacedKVStore RouteRegister routing.RouteRegister - features *featuremgmt.FeatureManager + pluginStore plugins.Store log log.Logger - oauthProviders map[string]bool - externalMetrics []usagestats.MetricsFunc - concurrentUserStatsCache memoConcurrentUserStats - startTime time.Time - sendReportCallbacks []usagestats.SendReportCallbackFunc + externalMetrics []usagestats.MetricsFunc + sendReportCallbacks []usagestats.SendReportCallbackFunc } -func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store, - socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister, features *featuremgmt.FeatureManager, -) *UsageStats { +func ProvideService(cfg *setting.Cfg, pluginStore plugins.Store, kvStore kvstore.KVStore, routeRegister routing.RouteRegister) *UsageStats { s := &UsageStats{ - Cfg: cfg, - SQLStore: sqlStore, - features: features, - oauthProviders: socialService.GetOAuthProviders(), - RouteRegister: routeRegister, - pluginStore: pluginStore, - kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"), - log: log.New("infra.usagestats"), - startTime: time.Now(), + Cfg: cfg, + RouteRegister: routeRegister, + pluginStore: pluginStore, + kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"), + log: log.New("infra.usagestats"), } s.registerAPIEndpoints() @@ -55,8 +39,6 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p } func (uss *UsageStats) Run(ctx context.Context) error { - uss.updateTotalStats(ctx) - // try to load last sent time from kv store lastSent := time.Now() if val, ok, err := uss.kvStore.Get(ctx, "last_sent"); err != nil { @@ -77,10 +59,8 @@ func (uss *UsageStats) Run(ctx context.Context) error { } sendReportTicker := time.NewTicker(nextSendInterval) - updateStatsTicker := time.NewTicker(time.Minute * 30) defer sendReportTicker.Stop() - defer updateStatsTicker.Stop() for { select { @@ -102,54 +82,21 @@ func (uss *UsageStats) Run(ctx context.Context) error { for _, callback := range uss.sendReportCallbacks { callback() } - case <-updateStatsTicker.C: - uss.updateTotalStats(ctx) case <-ctx.Done(): return ctx.Err() } } } -type memoConcurrentUserStats struct { - stats *concurrentUsersStats - - memoized time.Time -} - -const concurrentUserStatsCacheLifetime = time.Hour - -func (uss *UsageStats) GetConcurrentUsersStats(ctx context.Context) (*concurrentUsersStats, error) { - memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime) - if !uss.concurrentUserStatsCache.memoized.Before(memoizationPeriod) { - return uss.concurrentUserStatsCache.stats, nil - } - - uss.concurrentUserStatsCache.stats = &concurrentUsersStats{} - err := uss.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - // Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive. - rawSQL := ` -SELECT - COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3, - COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6, - COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9, - COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12, - COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15, - COUNT(1) AS bucket_le_inf -FROM (select count(1) as tokens from user_auth_token group by user_id) uat;` - _, err := sess.SQL(rawSQL).Get(uss.concurrentUserStatsCache.stats) - if err != nil { - return err - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err) - } - - uss.concurrentUserStatsCache.memoized = time.Now() - return uss.concurrentUserStatsCache.stats, nil -} - func (uss *UsageStats) RegisterSendReportCallback(c usagestats.SendReportCallbackFunc) { uss.sendReportCallbacks = append(uss.sendReportCallbacks, c) } + +func (uss *UsageStats) ShouldBeReported(ctx context.Context, dsType string) bool { + ds, exists := uss.pluginStore.Plugin(ctx, dsType) + if !exists { + return false + } + + return ds.Signature.IsValid() || ds.Signature.IsInternal() +} diff --git a/pkg/infra/usagestats/service/types.go b/pkg/infra/usagestats/service/types.go deleted file mode 100644 index c9e2a4d8361..00000000000 --- a/pkg/infra/usagestats/service/types.go +++ /dev/null @@ -1,10 +0,0 @@ -package service - -type concurrentUsersStats struct { - BucketLE3 int32 `xorm:"bucket_le_3"` - BucketLE6 int32 `xorm:"bucket_le_6"` - BucketLE9 int32 `xorm:"bucket_le_9"` - BucketLE12 int32 `xorm:"bucket_le_12"` - BucketLE15 int32 `xorm:"bucket_le_15"` - BucketLEInf int32 `xorm:"bucket_le_inf"` -} diff --git a/pkg/infra/usagestats/service/usage_stats.go b/pkg/infra/usagestats/service/usage_stats.go index a9e7b4fdf6b..95df118a6c0 100644 --- a/pkg/infra/usagestats/service/usage_stats.go +++ b/pkg/infra/usagestats/service/usage_stats.go @@ -4,17 +4,14 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "runtime" "strings" "time" "github.com/google/uuid" - "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/plugins" ) var usageStatsURL = "https://stats.grafana.org/grafana-usage-report" @@ -38,64 +35,7 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e UsageStatsId: uss.GetUsageStatsId(ctx), } - statsQuery := models.GetSystemStatsQuery{} - if err := uss.SQLStore.GetSystemStats(ctx, &statsQuery); err != nil { - uss.log.Error("Failed to get system stats", "error", err) - return report, err - } - - metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards - metrics["stats.users.count"] = statsQuery.Result.Users - metrics["stats.admins.count"] = statsQuery.Result.Admins - metrics["stats.editors.count"] = statsQuery.Result.Editors - metrics["stats.viewers.count"] = statsQuery.Result.Viewers - metrics["stats.orgs.count"] = statsQuery.Result.Orgs - metrics["stats.playlist.count"] = statsQuery.Result.Playlists - metrics["stats.plugins.apps.count"] = uss.appCount(ctx) - metrics["stats.plugins.panels.count"] = uss.panelCount(ctx) - metrics["stats.plugins.datasources.count"] = uss.dataSourceCount(ctx) - metrics["stats.alerts.count"] = statsQuery.Result.Alerts - metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers - metrics["stats.active_admins.count"] = statsQuery.Result.ActiveAdmins - metrics["stats.active_editors.count"] = statsQuery.Result.ActiveEditors - metrics["stats.active_viewers.count"] = statsQuery.Result.ActiveViewers - metrics["stats.active_sessions.count"] = statsQuery.Result.ActiveSessions - metrics["stats.monthly_active_users.count"] = statsQuery.Result.MonthlyActiveUsers - metrics["stats.daily_active_users.count"] = statsQuery.Result.DailyActiveUsers - metrics["stats.daily_active_admins.count"] = statsQuery.Result.DailyActiveAdmins - metrics["stats.daily_active_editors.count"] = statsQuery.Result.DailyActiveEditors - metrics["stats.daily_active_viewers.count"] = statsQuery.Result.DailyActiveViewers - metrics["stats.daily_active_sessions.count"] = statsQuery.Result.DailyActiveSessions - metrics["stats.datasources.count"] = statsQuery.Result.Datasources - metrics["stats.stars.count"] = statsQuery.Result.Stars - metrics["stats.folders.count"] = statsQuery.Result.Folders - metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions - metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions - metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards - metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots - metrics["stats.teams.count"] = statsQuery.Result.Teams - metrics["stats.total_auth_token.count"] = statsQuery.Result.AuthTokens - metrics["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions - metrics["stats.annotations.count"] = statsQuery.Result.Annotations - metrics["stats.alert_rules.count"] = statsQuery.Result.AlertRules - metrics["stats.library_panels.count"] = statsQuery.Result.LibraryPanels - metrics["stats.library_variables.count"] = statsQuery.Result.LibraryVariables - metrics["stats.dashboards_viewers_can_edit.count"] = statsQuery.Result.DashboardsViewersCanEdit - metrics["stats.dashboards_viewers_can_admin.count"] = statsQuery.Result.DashboardsViewersCanAdmin - metrics["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit - metrics["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin - metrics["stats.api_keys.count"] = statsQuery.Result.APIKeys - - ossEditionCount := 1 - enterpriseEditionCount := 0 - if uss.Cfg.IsEnterprise { - enterpriseEditionCount = 1 - ossEditionCount = 0 - } - metrics["stats.edition.oss.count"] = ossEditionCount - metrics["stats.edition.enterprise.count"] = enterpriseEditionCount - - uss.registerExternalMetrics(ctx, metrics) + uss.gatherMetrics(ctx, metrics) // must run after registration of external metrics if v, ok := metrics["stats.valid_license.count"]; ok { @@ -104,141 +44,10 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e metrics["stats.valid_license.count"] = 0 } - userCount := statsQuery.Result.Users - avgAuthTokensPerUser := statsQuery.Result.AuthTokens - if userCount != 0 { - avgAuthTokensPerUser /= userCount - } - - metrics["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser - - dsStats := models.GetDataSourceStatsQuery{} - if err := uss.SQLStore.GetDataSourceStats(ctx, &dsStats); err != nil { - uss.log.Error("Failed to get datasource stats", "error", err) - return report, err - } - - // send counters for each data source - // but ignore any custom data sources - // as sending that name could be sensitive information - dsOtherCount := 0 - for _, dsStat := range dsStats.Result { - if uss.ShouldBeReported(ctx, dsStat.Type) { - metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count - } else { - dsOtherCount += dsStat.Count - } - } - metrics["stats.ds.other.count"] = dsOtherCount - - esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES} - if err := uss.SQLStore.GetDataSourcesByType(ctx, &esDataSourcesQuery); err != nil { - uss.log.Error("Failed to get elasticsearch json data", "error", err) - return report, err - } - - for _, data := range esDataSourcesQuery.Result { - esVersion, err := data.JsonData.Get("esVersion").Int() - if err != nil { - continue - } - - statName := fmt.Sprintf("stats.ds.elasticsearch.v%d.count", esVersion) - - count, _ := metrics[statName].(int64) - - metrics[statName] = count + 1 - } - - metrics["stats.packaging."+uss.Cfg.Packaging+".count"] = 1 - metrics["stats.distributor."+uss.Cfg.ReportingDistributor+".count"] = 1 - - // fetch datasource access stats - dsAccessStats := models.GetDataSourceAccessStatsQuery{} - if err := uss.SQLStore.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil { - uss.log.Error("Failed to get datasource access stats", "error", err) - return report, err - } - - // send access counters for each data source - // but ignore any custom data sources - // as sending that name could be sensitive information - dsAccessOtherCount := make(map[string]int64) - for _, dsAccessStat := range dsAccessStats.Result { - if dsAccessStat.Access == "" { - continue - } - - access := strings.ToLower(dsAccessStat.Access) - - if uss.ShouldBeReported(ctx, dsAccessStat.Type) { - metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count - } else { - old := dsAccessOtherCount[access] - dsAccessOtherCount[access] = old + dsAccessStat.Count - } - } - - for access, count := range dsAccessOtherCount { - metrics["stats.ds_access.other."+access+".count"] = count - } - - // get stats about alert notifier usage - anStats := models.GetAlertNotifierUsageStatsQuery{} - if err := uss.SQLStore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil { - uss.log.Error("Failed to get alert notification stats", "error", err) - return report, err - } - - for _, stats := range anStats.Result { - metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count - } - - // Add stats about auth configuration - authTypes := map[string]bool{} - authTypes["anonymous"] = uss.Cfg.AnonymousEnabled - authTypes["basic_auth"] = uss.Cfg.BasicAuthEnabled - authTypes["ldap"] = uss.Cfg.LDAPEnabled - authTypes["auth_proxy"] = uss.Cfg.AuthProxyEnabled - - for provider, enabled := range uss.oauthProviders { - authTypes["oauth_"+provider] = enabled - } - - for authType, enabled := range authTypes { - enabledValue := 0 - if enabled { - enabledValue = 1 - } - metrics["stats.auth_enabled."+authType+".count"] = enabledValue - } - - // Get concurrent users stats as histogram - concurrentUsersStats, err := uss.GetConcurrentUsersStats(ctx) - if err != nil { - uss.log.Error("Failed to get concurrent users stats", "error", err) - return report, err - } - - // Histogram is cumulative and metric name has a postfix of le_"" - metrics["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3 - metrics["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6 - metrics["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9 - metrics["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12 - metrics["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15 - metrics["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf - - metrics["stats.uptime"] = int64(time.Since(uss.startTime).Seconds()) - - featureUsageStats := uss.features.GetUsageStats(ctx) - for name, val := range featureUsageStats { - metrics[name] = val - } - return report, nil } -func (uss *UsageStats) registerExternalMetrics(ctx context.Context, metrics map[string]interface{}) { +func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics map[string]interface{}) { for _, fn := range uss.externalMetrics { fnMetrics, err := fn(ctx) if err != nil { @@ -295,55 +104,6 @@ var sendUsageStats = func(uss *UsageStats, data *bytes.Buffer) { }() } -func (uss *UsageStats) updateTotalStats(ctx context.Context) { - if !uss.Cfg.MetricsEndpointEnabled || uss.Cfg.MetricsEndpointDisableTotalStats { - return - } - - statsQuery := models.GetSystemStatsQuery{} - if err := uss.SQLStore.GetSystemStats(ctx, &statsQuery); err != nil { - uss.log.Error("Failed to get system stats", "error", err) - return - } - - metrics.MStatTotalDashboards.Set(float64(statsQuery.Result.Dashboards)) - metrics.MStatTotalFolders.Set(float64(statsQuery.Result.Folders)) - metrics.MStatTotalUsers.Set(float64(statsQuery.Result.Users)) - metrics.MStatActiveUsers.Set(float64(statsQuery.Result.ActiveUsers)) - metrics.MStatTotalPlaylists.Set(float64(statsQuery.Result.Playlists)) - metrics.MStatTotalOrgs.Set(float64(statsQuery.Result.Orgs)) - metrics.StatsTotalViewers.Set(float64(statsQuery.Result.Viewers)) - metrics.StatsTotalActiveViewers.Set(float64(statsQuery.Result.ActiveViewers)) - metrics.StatsTotalEditors.Set(float64(statsQuery.Result.Editors)) - metrics.StatsTotalActiveEditors.Set(float64(statsQuery.Result.ActiveEditors)) - metrics.StatsTotalAdmins.Set(float64(statsQuery.Result.Admins)) - metrics.StatsTotalActiveAdmins.Set(float64(statsQuery.Result.ActiveAdmins)) - metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions)) - metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations)) - metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules)) - metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels)) - metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables)) - - dsStats := models.GetDataSourceStatsQuery{} - if err := uss.SQLStore.GetDataSourceStats(ctx, &dsStats); err != nil { - uss.log.Error("Failed to get datasource stats", "error", err) - return - } - - for _, dsStat := range dsStats.Result { - metrics.StatsTotalDataSources.WithLabelValues(dsStat.Type).Set(float64(dsStat.Count)) - } -} - -func (uss *UsageStats) ShouldBeReported(ctx context.Context, dsType string) bool { - ds, exists := uss.pluginStore.Plugin(ctx, dsType) - if !exists { - return false - } - - return ds.Signature.IsValid() || ds.Signature.IsInternal() -} - func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string { anonId, ok, err := uss.kvStore.Get(ctx, "anonymous_id") if err != nil { @@ -371,15 +131,3 @@ func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string { return anonId } - -func (uss *UsageStats) appCount(ctx context.Context) int { - return len(uss.pluginStore.Plugins(ctx, plugins.App)) -} - -func (uss *UsageStats) panelCount(ctx context.Context) int { - return len(uss.pluginStore.Plugins(ctx, plugins.Panel)) -} - -func (uss *UsageStats) dataSourceCount(ctx context.Context) int { - return len(uss.pluginStore.Plugins(ctx, plugins.DataSource)) -} diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index a150e26d316..7c36eead42e 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -3,6 +3,7 @@ package service import ( "bytes" "context" + "encoding/json" "errors" "io/ioutil" "net/http" @@ -11,19 +12,16 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // This is to ensure that the interface contract is held by the implementation @@ -38,419 +36,163 @@ func Test_InterfaceContractValidity(t *testing.T) { } func TestMetrics(t *testing.T) { - t.Run("When sending usage stats", func(t *testing.T) { - sqlStore := mockstore.NewSQLStoreMock() - uss := createService(t, setting.Cfg{}, sqlStore, false) + const metricName = "stats.test_metric.count" - sqlStore.ExpectedSystemStats = &models.SystemStats{ - Dashboards: 1, - Datasources: 2, - Users: 3, - Admins: 31, - Editors: 32, - Viewers: 33, - ActiveUsers: 4, - ActiveAdmins: 21, - ActiveEditors: 22, - ActiveViewers: 23, - ActiveSessions: 24, - DailyActiveUsers: 25, - DailyActiveAdmins: 26, - DailyActiveEditors: 27, - DailyActiveViewers: 28, - DailyActiveSessions: 29, - Orgs: 5, - Playlists: 6, - Alerts: 7, - Stars: 8, - Folders: 9, - DashboardPermissions: 10, - FolderPermissions: 11, - ProvisionedDashboards: 12, - Snapshots: 13, - Teams: 14, - AuthTokens: 15, - DashboardVersions: 16, - Annotations: 17, - AlertRules: 18, - LibraryPanels: 19, - LibraryVariables: 20, - DashboardsViewersCanAdmin: 3, - DashboardsViewersCanEdit: 2, - FoldersViewersCanAdmin: 1, - FoldersViewersCanEdit: 5, - APIKeys: 2, + sqlStore := mockstore.NewSQLStoreMock() + uss := createService(t, setting.Cfg{}, sqlStore, false) + + uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { + return map[string]interface{}{metricName: 1}, nil + }) + + err := uss.sendUsageStats(context.Background()) + require.NoError(t, err) + + t.Run("Given reporting not enabled and sending usage stats", func(t *testing.T) { + origSendUsageStats := sendUsageStats + t.Cleanup(func() { + sendUsageStats = origSendUsageStats + }) + statsSent := false + sendUsageStats = func(uss *UsageStats, b *bytes.Buffer) { + statsSent = true } - setupSomeDataSourcePlugins(t, uss) + uss.Cfg.ReportingEnabled = false + err := uss.sendUsageStats(context.Background()) + require.NoError(t, err) - sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{ - { - Type: models.DS_ES, - Count: 9, - }, - { - Type: models.DS_PROMETHEUS, - Count: 10, - }, - { - Type: "unknown_ds", - Count: 11, - }, - { - Type: "unknown_ds2", - Count: 12, - }, + require.False(t, statsSent) + }) + + t.Run("Given reporting enabled, stats should be gathered and sent to HTTP endpoint", func(t *testing.T) { + origCfg := uss.Cfg + t.Cleanup(func() { + uss.Cfg = origCfg + }) + uss.Cfg = &setting.Cfg{ + ReportingEnabled: true, + BuildVersion: "5.0.0", + AnonymousEnabled: true, + BasicAuthEnabled: true, + LDAPEnabled: true, + AuthProxyEnabled: true, + Packaging: "deb", + ReportingDistributor: "hosted-grafana", } - sqlStore.ExpectedDataSources = []*models.DataSource{ - { - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "esVersion": 2, - }), - }, - { - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "esVersion": 2, - }), - }, - { - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "esVersion": 70, - }), - }, - } - - sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{ - { - Type: models.DS_ES, - Access: "direct", - Count: 1, - }, - { - Type: models.DS_ES, - Access: "proxy", - Count: 2, - }, - { - Type: models.DS_PROMETHEUS, - Access: "proxy", - Count: 3, - }, - { - Type: "unknown_ds", - Access: "proxy", - Count: 4, - }, - { - Type: "unknown_ds2", - Access: "", - Count: 5, - }, - { - Type: "unknown_ds3", - Access: "direct", - Count: 6, - }, - { - Type: "unknown_ds4", - Access: "direct", - Count: 7, - }, - { - Type: "unknown_ds5", - Access: "proxy", - Count: 8, - }, - } - - sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{ - { - Type: "slack", - Count: 1, - }, - { - Type: "webhook", - Count: 2, - }, - } - - uss.SQLStore = sqlStore - - createConcurrentTokens(t, uss.SQLStore) - - uss.oauthProviders = map[string]bool{ - "github": true, - "gitlab": true, - "azuread": true, - "google": true, - "generic_oauth": true, - "grafana_com": true, - } + ch := make(chan httpResp) + ticker := time.NewTicker(2 * time.Second) + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + buf, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Logf("Fake HTTP handler received an error: %s", err.Error()) + ch <- httpResp{ + err: err, + } + return + } + require.NoError(t, err, "Failed to read response body, err=%v", err) + t.Logf("Fake HTTP handler received a response") + ch <- httpResp{ + responseBuffer: bytes.NewBuffer(buf), + req: r, + } + })) + t.Cleanup(ts.Close) + t.Cleanup(func() { + close(ch) + }) + usageStatsURL = ts.URL err := uss.sendUsageStats(context.Background()) require.NoError(t, err) - t.Run("Given reporting not enabled and sending usage stats", func(t *testing.T) { - origSendUsageStats := sendUsageStats - t.Cleanup(func() { - sendUsageStats = origSendUsageStats - }) - statsSent := false - sendUsageStats = func(uss *UsageStats, b *bytes.Buffer) { - statsSent = true - } + // Wait for fake HTTP server to receive a request + var resp httpResp + select { + case resp = <-ch: + require.NoError(t, resp.err, "Fake server experienced an error") + case <-ticker.C: + t.Fatalf("Timed out waiting for HTTP request") + } - uss.Cfg.ReportingEnabled = false - err := uss.sendUsageStats(context.Background()) - require.NoError(t, err) + t.Logf("Received response from fake HTTP server: %+v\n", resp) - require.False(t, statsSent) - }) + assert.NotNil(t, resp.req) - t.Run("Given reporting enabled, stats should be gathered and sent to HTTP endpoint", func(t *testing.T) { - origCfg := uss.Cfg - t.Cleanup(func() { - uss.Cfg = origCfg - }) - uss.Cfg = &setting.Cfg{ - ReportingEnabled: true, - BuildVersion: "5.0.0", - AnonymousEnabled: true, - BasicAuthEnabled: true, - LDAPEnabled: true, - AuthProxyEnabled: true, - Packaging: "deb", - ReportingDistributor: "hosted-grafana", - } + assert.Equal(t, http.MethodPost, resp.req.Method) + assert.Equal(t, "application/json", resp.req.Header.Get("Content-Type")) - ch := make(chan httpResp) - ticker := time.NewTicker(2 * time.Second) - ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - buf, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Logf("Fake HTTP handler received an error: %s", err.Error()) - ch <- httpResp{ - err: err, - } - return - } - require.NoError(t, err, "Failed to read response body, err=%v", err) - t.Logf("Fake HTTP handler received a response") - ch <- httpResp{ - responseBuffer: bytes.NewBuffer(buf), - req: r, - } - })) - t.Cleanup(ts.Close) - t.Cleanup(func() { - close(ch) - }) - usageStatsURL = ts.URL + require.NotNil(t, resp.responseBuffer) - err := uss.sendUsageStats(context.Background()) - require.NoError(t, err) + j := make(map[string]interface{}) + err = json.Unmarshal(resp.responseBuffer.Bytes(), &j) + require.NoError(t, err) - // Wait for fake HTTP server to receive a request - var resp httpResp - select { - case resp = <-ch: - require.NoError(t, resp.err, "Fake server experienced an error") - case <-ticker.C: - t.Fatalf("Timed out waiting for HTTP request") - } + assert.Equal(t, "5_0_0", j["version"]) + assert.Equal(t, runtime.GOOS, j["os"]) + assert.Equal(t, runtime.GOARCH, j["arch"]) - t.Logf("Received response from fake HTTP server: %+v\n", resp) + usageId := uss.GetUsageStatsId(context.Background()) + assert.NotEmpty(t, usageId) - assert.NotNil(t, resp.req) + metrics, ok := j["metrics"].(map[string]interface{}) + require.True(t, ok) + assert.EqualValues(t, 1, metrics[metricName]) + }) +} - assert.Equal(t, http.MethodPost, resp.req.Method) - assert.Equal(t, "application/json", resp.req.Header.Get("Content-Type")) +func TestGetUsageReport_IncludesMetrics(t *testing.T) { + sqlStore := mockstore.NewSQLStoreMock() + uss := createService(t, setting.Cfg{}, sqlStore, true) + metricName := "stats.test_metric.count" - require.NotNil(t, resp.responseBuffer) - - j, err := simplejson.NewFromReader(resp.responseBuffer) - require.NoError(t, err) - - assert.Equal(t, "5_0_0", j.Get("version").MustString()) - assert.Equal(t, runtime.GOOS, j.Get("os").MustString()) - assert.Equal(t, runtime.GOARCH, j.Get("arch").MustString()) - - usageId := uss.GetUsageStatsId(context.Background()) - assert.NotEmpty(t, usageId) - - metrics := j.Get("metrics") - assert.Equal(t, 15, metrics.Get("stats.total_auth_token.count").MustInt()) - assert.Equal(t, 2, metrics.Get("stats.api_keys.count").MustInt()) - assert.Equal(t, 5, metrics.Get("stats.avg_auth_token_per_user.count").MustInt()) - assert.Equal(t, 16, metrics.Get("stats.dashboard_versions.count").MustInt()) - assert.Equal(t, 17, metrics.Get("stats.annotations.count").MustInt()) - assert.Equal(t, 18, metrics.Get("stats.alert_rules.count").MustInt()) - assert.Equal(t, 19, metrics.Get("stats.library_panels.count").MustInt()) - assert.Equal(t, 20, metrics.Get("stats.library_variables.count").MustInt()) - assert.Equal(t, 0, metrics.Get("stats.live_users.count").MustInt()) - assert.Equal(t, 0, metrics.Get("stats.live_clients.count").MustInt()) - - assert.Equal(t, 9, metrics.Get("stats.ds."+models.DS_ES+".count").MustInt()) - assert.Equal(t, 10, metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt()) - - assert.Equal(t, 11+12, metrics.Get("stats.ds.other.count").MustInt()) - - assert.Equal(t, 1, metrics.Get("stats.ds_access."+models.DS_ES+".direct.count").MustInt()) - assert.Equal(t, 2, metrics.Get("stats.ds_access."+models.DS_ES+".proxy.count").MustInt()) - assert.Equal(t, 3, metrics.Get("stats.ds_access."+models.DS_PROMETHEUS+".proxy.count").MustInt()) - assert.Equal(t, 6+7, metrics.Get("stats.ds_access.other.direct.count").MustInt()) - assert.Equal(t, 4+8, metrics.Get("stats.ds_access.other.proxy.count").MustInt()) - - assert.Equal(t, 1, metrics.Get("stats.alert_notifiers.slack.count").MustInt()) - assert.Equal(t, 2, metrics.Get("stats.alert_notifiers.webhook.count").MustInt()) - - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.anonymous.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.basic_auth.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.ldap.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.auth_proxy.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_github.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_gitlab.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_google.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_azuread.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt()) - - assert.Equal(t, 1, metrics.Get("stats.packaging.deb.count").MustInt()) - assert.Equal(t, 1, metrics.Get("stats.distributor.hosted-grafana.count").MustInt()) - - assert.LessOrEqual(t, 60, metrics.Get("stats.uptime").MustInt()) - assert.Greater(t, 70, metrics.Get("stats.uptime").MustInt()) - }) + uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { + return map[string]interface{}{metricName: 1}, nil }) - t.Run("When updating total stats", func(t *testing.T) { - sqlStore := mockstore.NewSQLStoreMock() - uss := createService(t, setting.Cfg{}, sqlStore, false) - uss.Cfg.MetricsEndpointEnabled = true - uss.Cfg.MetricsEndpointDisableTotalStats = false + report, err := uss.GetUsageReport(context.Background()) + require.NoError(t, err, "Expected no error") - sqlStore.ExpectedSystemStats = &models.SystemStats{} + metric := report.Metrics[metricName] + assert.Equal(t, 1, metric) +} - t.Run("When metrics is disabled and total stats is enabled, stats should not be updated", func(t *testing.T) { - uss.Cfg.MetricsEndpointEnabled = false - uss.Cfg.MetricsEndpointDisableTotalStats = false - uss.updateTotalStats(context.Background()) - }) +func TestRegisterMetrics(t *testing.T) { + const goodMetricName = "stats.test_external_metric.count" - t.Run("When metrics is enabled and total stats is disabled, stats should not be updated", func(t *testing.T) { - uss.Cfg.MetricsEndpointEnabled = true - uss.Cfg.MetricsEndpointDisableTotalStats = true + sqlStore := mockstore.NewSQLStoreMock() + uss := createService(t, setting.Cfg{}, sqlStore, false) + metrics := map[string]interface{}{"stats.test_metric.count": 1, "stats.test_metric_second.count": 2} - uss.updateTotalStats(context.Background()) - }) - - t.Run("When metrics is disabled and total stats is disabled, stats should not be updated", func(t *testing.T) { - uss.Cfg.MetricsEndpointEnabled = false - uss.Cfg.MetricsEndpointDisableTotalStats = true - - uss.updateTotalStats(context.Background()) - }) - - t.Run("When metrics is enabled and total stats is enabled, stats should be updated", func(t *testing.T) { - uss.Cfg.MetricsEndpointEnabled = true - uss.Cfg.MetricsEndpointDisableTotalStats = false - - uss.updateTotalStats(context.Background()) - }) + uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { + return map[string]interface{}{goodMetricName: 1}, nil }) - t.Run("When registering a metric", func(t *testing.T) { - sqlStore := mockstore.NewSQLStoreMock() - uss := createService(t, setting.Cfg{}, sqlStore, false) - metricName := "stats.test_metric.count" + { + extMetrics, err := uss.externalMetrics[0](context.Background()) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{goodMetricName: 1}, extMetrics) + } - t.Run("Adds a new metric to the external metrics", func(t *testing.T) { - uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { - return map[string]interface{}{metricName: 1}, nil - }) + uss.gatherMetrics(context.Background(), metrics) + assert.Equal(t, 1, metrics[goodMetricName]) - metrics, err := uss.externalMetrics[0](context.Background()) - require.NoError(t, err) - assert.Equal(t, map[string]interface{}{metricName: 1}, metrics) - }) - }) - - t.Run("When getting usage report", func(t *testing.T) { - sqlStore := mockstore.NewSQLStoreMock() - uss := createService(t, setting.Cfg{}, sqlStore, true) - metricName := "stats.test_metric.count" - - createConcurrentTokens(t, uss.SQLStore) - - t.Run("Should include metrics for concurrent users", func(t *testing.T) { - report, err := uss.GetUsageReport(context.Background()) - require.NoError(t, err) - - assert.Equal(t, int32(1), report.Metrics["stats.auth_token_per_user_le_3"]) - assert.Equal(t, int32(2), report.Metrics["stats.auth_token_per_user_le_6"]) - assert.Equal(t, int32(3), report.Metrics["stats.auth_token_per_user_le_9"]) - assert.Equal(t, int32(4), report.Metrics["stats.auth_token_per_user_le_12"]) - assert.Equal(t, int32(5), report.Metrics["stats.auth_token_per_user_le_15"]) - assert.Equal(t, int32(6), report.Metrics["stats.auth_token_per_user_le_inf"]) - }) - - t.Run("Should include external metrics", func(t *testing.T) { - uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { - return map[string]interface{}{metricName: 1}, nil - }) - - report, err := uss.GetUsageReport(context.Background()) - require.NoError(t, err, "Expected no error") - - metric := report.Metrics[metricName] - assert.Equal(t, 1, metric) - }) - - t.Run("Should include feature usage stats", func(t *testing.T) { - report, err := uss.GetUsageReport(context.Background()) - require.NoError(t, err, "Expected no error") - - assert.Equal(t, 1, report.Metrics["stats.features.feature_1.count"]) - assert.Equal(t, 1, report.Metrics["stats.features.feature_2.count"]) - }) - }) - - t.Run("When registering external metrics", func(t *testing.T) { - sqlStore := mockstore.NewSQLStoreMock() - uss := createService(t, setting.Cfg{}, sqlStore, false) - metrics := map[string]interface{}{"stats.test_metric.count": 1, "stats.test_metric_second.count": 2} - extMetricName := "stats.test_external_metric.count" + t.Run("do not add metrics that return an error when fetched", func(t *testing.T) { + const badMetricName = "stats.test_external_metric_error.count" uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { - return map[string]interface{}{extMetricName: 1}, nil + return map[string]interface{}{badMetricName: 1}, errors.New("some error") }) + uss.gatherMetrics(context.Background(), metrics) - uss.registerExternalMetrics(context.Background(), metrics) + extErrorMetric := metrics[badMetricName] + extMetric := metrics[goodMetricName] - assert.Equal(t, 1, metrics[extMetricName]) - - t.Run("When loading a metric results to an error", func(t *testing.T) { - uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { - return map[string]interface{}{extMetricName: 1}, nil - }) - extErrorMetricName := "stats.test_external_metric_error.count" - - t.Run("Should not add it to metrics", func(t *testing.T) { - uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { - return map[string]interface{}{extErrorMetricName: 1}, errors.New("some error") - }) - - uss.registerExternalMetrics(context.Background(), metrics) - - extErrorMetric := metrics[extErrorMetricName] - extMetric := metrics[extMetricName] - - require.Nil(t, extErrorMetric, "Invalid metric should not be added") - assert.Equal(t, 1, extMetric) - assert.Len(t, metrics, 3, "Expected only one available metric") - }) - }) + require.Nil(t, extErrorMetric, "Invalid metric should not be added") + assert.Equal(t, 1, extMetric) + assert.Len(t, metrics, 3, "Expected only one available metric") }) } @@ -479,27 +221,6 @@ func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type return result } -func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStats) { - t.Helper() - - uss.pluginStore = &fakePluginStore{ - plugins: map[string]plugins.PluginDTO{ - models.DS_ES: { - Signature: "internal", - }, - models.DS_PROMETHEUS: { - Signature: "internal", - }, - models.DS_GRAPHITE: { - Signature: "internal", - }, - models.DS_MYSQL: { - Signature: "internal", - }, - }, - } -} - type httpResp struct { req *http.Request responseBuffer *bytes.Buffer @@ -511,15 +232,11 @@ func createService(t *testing.T, cfg setting.Cfg, sqlStore sqlstore.Store, withD if withDB { sqlStore = sqlstore.InitTestDB(t) } - return &UsageStats{ - Cfg: &cfg, - SQLStore: sqlStore, - externalMetrics: make([]usagestats.MetricsFunc, 0), - pluginStore: &fakePluginStore{}, - kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"), - log: log.New("infra.usagestats"), - startTime: time.Now().Add(-1 * time.Minute), - RouteRegister: routing.NewRouteRegister(), - features: featuremgmt.WithFeatures("feature1", "feature2"), - } + + return ProvideService( + &cfg, + &fakePluginStore{}, + kvstore.ProvideService(sqlStore), + routing.NewRouteRegister(), + ) } diff --git a/pkg/infra/usagestats/statscollector/concurrent_users.go b/pkg/infra/usagestats/statscollector/concurrent_users.go new file mode 100644 index 00000000000..55d23881ebc --- /dev/null +++ b/pkg/infra/usagestats/statscollector/concurrent_users.go @@ -0,0 +1,58 @@ +package statscollector + +import ( + "context" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +const concurrentUserStatsCacheLifetime = time.Hour + +type concurrentUsersStats struct { + BucketLE3 int32 `xorm:"bucket_le_3"` + BucketLE6 int32 `xorm:"bucket_le_6"` + BucketLE9 int32 `xorm:"bucket_le_9"` + BucketLE12 int32 `xorm:"bucket_le_12"` + BucketLE15 int32 `xorm:"bucket_le_15"` + BucketLEInf int32 `xorm:"bucket_le_inf"` +} + +type memoConcurrentUserStats struct { + stats *concurrentUsersStats + + memoized time.Time +} + +func (s *Service) concurrentUsers(ctx context.Context) (*concurrentUsersStats, error) { + memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime) + if !s.concurrentUserStatsCache.memoized.Before(memoizationPeriod) { + return s.concurrentUserStatsCache.stats, nil + } + + s.concurrentUserStatsCache.stats = &concurrentUsersStats{} + err := s.sqlstore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + // Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive. + rawSQL := ` +SELECT + COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3, + COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6, + COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9, + COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12, + COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15, + COUNT(1) AS bucket_le_inf +FROM (select count(1) as tokens from user_auth_token group by user_id) uat;` + _, err := sess.SQL(rawSQL).Get(s.concurrentUserStatsCache.stats) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err) + } + + s.concurrentUserStatsCache.memoized = time.Now() + return s.concurrentUserStatsCache.stats, nil +} diff --git a/pkg/infra/usagestats/service/usage_stats_service_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go similarity index 57% rename from pkg/infra/usagestats/service/usage_stats_service_test.go rename to pkg/infra/usagestats/statscollector/concurrent_users_test.go index 5960c0fb8c6..dfa027d0b85 100644 --- a/pkg/infra/usagestats/service/usage_stats_service_test.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go @@ -1,4 +1,4 @@ -package service +package statscollector import ( "context" @@ -8,21 +8,35 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util" ) -func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) { +func TestConcurrentUsersMetrics(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - uss := &UsageStats{ - SQLStore: sqlStore, - kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"), - log: log.New("infra.usagestats"), - } + s := createService(t, setting.NewCfg(), sqlStore) + + createConcurrentTokens(t, sqlStore) + + stats, err := s.collect(context.Background()) + require.NoError(t, err) + + assert.Equal(t, int32(1), stats["stats.auth_token_per_user_le_3"]) + assert.Equal(t, int32(2), stats["stats.auth_token_per_user_le_6"]) + assert.Equal(t, int32(3), stats["stats.auth_token_per_user_le_9"]) + assert.Equal(t, int32(4), stats["stats.auth_token_per_user_le_12"]) + assert.Equal(t, int32(5), stats["stats.auth_token_per_user_le_15"]) + assert.Equal(t, int32(6), stats["stats.auth_token_per_user_le_inf"]) +} + +func TestConcurrentUsersStats(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + s := createService(t, setting.NewCfg(), sqlStore) createConcurrentTokens(t, sqlStore) @@ -31,10 +45,10 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) { cancel() }) - actualResult, err := uss.GetConcurrentUsersStats(ctx) + actualResult, err := s.concurrentUsers(ctx) require.NoError(t, err) - expectedCachedResult := &concurrentUsersStats{ + expectedResult := &concurrentUsersStats{ BucketLE3: 1, BucketLE6: 2, BucketLE9: 3, @@ -42,14 +56,39 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) { BucketLE15: 5, BucketLEInf: 6, } - assert.Equal(t, expectedCachedResult, actualResult) + assert.Equal(t, expectedResult, actualResult) createToken(t, 8, sqlStore) require.NoError(t, err) - actualResult, err = uss.GetConcurrentUsersStats(ctx) + // Cached, new token is not reflected yet. + actualResult, err = s.concurrentUsers(ctx) require.NoError(t, err) - assert.Equal(t, expectedCachedResult, actualResult) + assert.Equal(t, expectedResult, actualResult) + + // Bust cache + s.concurrentUserStatsCache = memoConcurrentUserStats{} + + expectedResult = &concurrentUsersStats{ + BucketLE3: 2, + BucketLE6: 3, + BucketLE9: 4, + BucketLE12: 5, + BucketLE15: 6, + BucketLEInf: 7, + } + actualResult, err = s.concurrentUsers(ctx) + require.NoError(t, err) + assert.Equal(t, expectedResult, actualResult) +} + +func createConcurrentTokens(t *testing.T, sqlStore sqlstore.Store) { + t.Helper() + for u := 1; u <= 6; u++ { + for tkn := 1; tkn <= u*3; tkn++ { + createToken(t, u, sqlStore) + } + } } func createToken(t *testing.T, uID int, sqlStore sqlstore.Store) { @@ -83,15 +122,6 @@ func createToken(t *testing.T, uID int, sqlStore sqlstore.Store) { require.NoError(t, err) } -func createConcurrentTokens(t *testing.T, sqlStore sqlstore.Store) { - t.Helper() - for u := 1; u <= 6; u++ { - for tkn := 1; tkn <= u*3; tkn++ { - createToken(t, u, sqlStore) - } - } -} - type userAuthToken struct { UserID int64 `xorm:"user_id"` AuthToken string diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go new file mode 100644 index 00000000000..b96abfb4e68 --- /dev/null +++ b/pkg/infra/usagestats/statscollector/service.go @@ -0,0 +1,321 @@ +package statscollector + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + + "github.com/grafana/grafana/pkg/infra/usagestats" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +type Service struct { + cfg *setting.Cfg + sqlstore sqlstore.Store + plugins plugins.Store + social social.Service + usageStats usagestats.Service + features *featuremgmt.FeatureManager + + log log.Logger + + startTime time.Time + concurrentUserStatsCache memoConcurrentUserStats +} + +func ProvideService( + usagestats usagestats.Service, + cfg *setting.Cfg, + store sqlstore.Store, + social social.Service, + plugins plugins.Store, + features *featuremgmt.FeatureManager, +) *Service { + s := &Service{ + cfg: cfg, + sqlstore: store, + plugins: plugins, + social: social, + usageStats: usagestats, + features: features, + + startTime: time.Now(), + log: log.New("infra.usagestats.collector"), + } + + usagestats.RegisterMetricsFunc(s.collect) + + return s +} + +func (s *Service) Run(ctx context.Context) error { + s.updateTotalStats(ctx) + updateStatsTicker := time.NewTicker(time.Minute * 30) + defer updateStatsTicker.Stop() + + for { + select { + case <-updateStatsTicker.C: + s.updateTotalStats(ctx) + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} + + statsQuery := models.GetSystemStatsQuery{} + if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil { + s.log.Error("Failed to get system stats", "error", err) + return nil, err + } + + m["stats.dashboards.count"] = statsQuery.Result.Dashboards + m["stats.users.count"] = statsQuery.Result.Users + m["stats.admins.count"] = statsQuery.Result.Admins + m["stats.editors.count"] = statsQuery.Result.Editors + m["stats.viewers.count"] = statsQuery.Result.Viewers + m["stats.orgs.count"] = statsQuery.Result.Orgs + m["stats.playlist.count"] = statsQuery.Result.Playlists + m["stats.plugins.apps.count"] = s.appCount(ctx) + m["stats.plugins.panels.count"] = s.panelCount(ctx) + m["stats.plugins.datasources.count"] = s.dataSourceCount(ctx) + m["stats.alerts.count"] = statsQuery.Result.Alerts + m["stats.active_users.count"] = statsQuery.Result.ActiveUsers + m["stats.active_admins.count"] = statsQuery.Result.ActiveAdmins + m["stats.active_editors.count"] = statsQuery.Result.ActiveEditors + m["stats.active_viewers.count"] = statsQuery.Result.ActiveViewers + m["stats.active_sessions.count"] = statsQuery.Result.ActiveSessions + m["stats.monthly_active_users.count"] = statsQuery.Result.MonthlyActiveUsers + m["stats.daily_active_users.count"] = statsQuery.Result.DailyActiveUsers + m["stats.daily_active_admins.count"] = statsQuery.Result.DailyActiveAdmins + m["stats.daily_active_editors.count"] = statsQuery.Result.DailyActiveEditors + m["stats.daily_active_viewers.count"] = statsQuery.Result.DailyActiveViewers + m["stats.daily_active_sessions.count"] = statsQuery.Result.DailyActiveSessions + m["stats.datasources.count"] = statsQuery.Result.Datasources + m["stats.stars.count"] = statsQuery.Result.Stars + m["stats.folders.count"] = statsQuery.Result.Folders + m["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions + m["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions + m["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards + m["stats.snapshots.count"] = statsQuery.Result.Snapshots + m["stats.teams.count"] = statsQuery.Result.Teams + m["stats.total_auth_token.count"] = statsQuery.Result.AuthTokens + m["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions + m["stats.annotations.count"] = statsQuery.Result.Annotations + m["stats.alert_rules.count"] = statsQuery.Result.AlertRules + m["stats.library_panels.count"] = statsQuery.Result.LibraryPanels + m["stats.library_variables.count"] = statsQuery.Result.LibraryVariables + m["stats.dashboards_viewers_can_edit.count"] = statsQuery.Result.DashboardsViewersCanEdit + m["stats.dashboards_viewers_can_admin.count"] = statsQuery.Result.DashboardsViewersCanAdmin + m["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit + m["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin + m["stats.api_keys.count"] = statsQuery.Result.APIKeys + + ossEditionCount := 1 + enterpriseEditionCount := 0 + if s.cfg.IsEnterprise { + enterpriseEditionCount = 1 + ossEditionCount = 0 + } + m["stats.edition.oss.count"] = ossEditionCount + m["stats.edition.enterprise.count"] = enterpriseEditionCount + + userCount := statsQuery.Result.Users + avgAuthTokensPerUser := statsQuery.Result.AuthTokens + if userCount != 0 { + avgAuthTokensPerUser /= userCount + } + + m["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser + + dsStats := models.GetDataSourceStatsQuery{} + if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil { + s.log.Error("Failed to get datasource stats", "error", err) + return nil, err + } + + // send counters for each data source + // but ignore any custom data sources + // as sending that name could be sensitive information + dsOtherCount := 0 + for _, dsStat := range dsStats.Result { + if s.usageStats.ShouldBeReported(ctx, dsStat.Type) { + m["stats.ds."+dsStat.Type+".count"] = dsStat.Count + } else { + dsOtherCount += dsStat.Count + } + } + m["stats.ds.other.count"] = dsOtherCount + + esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES} + if err := s.sqlstore.GetDataSourcesByType(ctx, &esDataSourcesQuery); err != nil { + s.log.Error("Failed to get elasticsearch json data", "error", err) + return nil, err + } + + for _, data := range esDataSourcesQuery.Result { + esVersion, err := data.JsonData.Get("esVersion").Int() + if err != nil { + continue + } + + statName := fmt.Sprintf("stats.ds.elasticsearch.v%d.count", esVersion) + + count, _ := m[statName].(int64) + + m[statName] = count + 1 + } + + m["stats.packaging."+s.cfg.Packaging+".count"] = 1 + m["stats.distributor."+s.cfg.ReportingDistributor+".count"] = 1 + + // fetch datasource access stats + dsAccessStats := models.GetDataSourceAccessStatsQuery{} + if err := s.sqlstore.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil { + s.log.Error("Failed to get datasource access stats", "error", err) + return nil, err + } + + // send access counters for each data source + // but ignore any custom data sources + // as sending that name could be sensitive information + dsAccessOtherCount := make(map[string]int64) + for _, dsAccessStat := range dsAccessStats.Result { + if dsAccessStat.Access == "" { + continue + } + + access := strings.ToLower(dsAccessStat.Access) + + if s.usageStats.ShouldBeReported(ctx, dsAccessStat.Type) { + m["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count + } else { + old := dsAccessOtherCount[access] + dsAccessOtherCount[access] = old + dsAccessStat.Count + } + } + + for access, count := range dsAccessOtherCount { + m["stats.ds_access.other."+access+".count"] = count + } + + // get stats about alert notifier usage + anStats := models.GetAlertNotifierUsageStatsQuery{} + if err := s.sqlstore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil { + s.log.Error("Failed to get alert notification stats", "error", err) + return nil, err + } + + for _, stats := range anStats.Result { + m["stats.alert_notifiers."+stats.Type+".count"] = stats.Count + } + + // Add stats about auth configuration + authTypes := map[string]bool{} + authTypes["anonymous"] = s.cfg.AnonymousEnabled + authTypes["basic_auth"] = s.cfg.BasicAuthEnabled + authTypes["ldap"] = s.cfg.LDAPEnabled + authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled + + for provider, enabled := range s.social.GetOAuthProviders() { + authTypes["oauth_"+provider] = enabled + } + + for authType, enabled := range authTypes { + enabledValue := 0 + if enabled { + enabledValue = 1 + } + m["stats.auth_enabled."+authType+".count"] = enabledValue + } + + // Get concurrent users stats as histogram + concurrentUsersStats, err := s.concurrentUsers(ctx) + if err != nil { + s.log.Error("Failed to get concurrent users stats", "error", err) + return nil, err + } + + // Histogram is cumulative and metric name has a postfix of le_"" + m["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3 + m["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6 + m["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9 + m["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12 + m["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15 + m["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf + + m["stats.uptime"] = int64(time.Since(s.startTime).Seconds()) + + featureUsageStats := s.features.GetUsageStats(ctx) + for k, v := range featureUsageStats { + m[k] = v + } + + return m, nil +} + +func (s *Service) updateTotalStats(ctx context.Context) bool { + if !s.cfg.MetricsEndpointEnabled || s.cfg.MetricsEndpointDisableTotalStats { + return false + } + + statsQuery := models.GetSystemStatsQuery{} + if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil { + s.log.Error("Failed to get system stats", "error", err) + return false + } + + metrics.MStatTotalDashboards.Set(float64(statsQuery.Result.Dashboards)) + metrics.MStatTotalFolders.Set(float64(statsQuery.Result.Folders)) + metrics.MStatTotalUsers.Set(float64(statsQuery.Result.Users)) + metrics.MStatActiveUsers.Set(float64(statsQuery.Result.ActiveUsers)) + metrics.MStatTotalPlaylists.Set(float64(statsQuery.Result.Playlists)) + metrics.MStatTotalOrgs.Set(float64(statsQuery.Result.Orgs)) + metrics.StatsTotalViewers.Set(float64(statsQuery.Result.Viewers)) + metrics.StatsTotalActiveViewers.Set(float64(statsQuery.Result.ActiveViewers)) + metrics.StatsTotalEditors.Set(float64(statsQuery.Result.Editors)) + metrics.StatsTotalActiveEditors.Set(float64(statsQuery.Result.ActiveEditors)) + metrics.StatsTotalAdmins.Set(float64(statsQuery.Result.Admins)) + metrics.StatsTotalActiveAdmins.Set(float64(statsQuery.Result.ActiveAdmins)) + metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions)) + metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations)) + metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules)) + metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels)) + metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables)) + + dsStats := models.GetDataSourceStatsQuery{} + if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil { + s.log.Error("Failed to get datasource stats", "error", err) + return true + } + + for _, dsStat := range dsStats.Result { + metrics.StatsTotalDataSources.WithLabelValues(dsStat.Type).Set(float64(dsStat.Count)) + } + return true +} + +func (s *Service) appCount(ctx context.Context) int { + return len(s.plugins.Plugins(ctx, plugins.App)) +} + +func (s *Service) panelCount(ctx context.Context) int { + return len(s.plugins.Plugins(ctx, plugins.Panel)) +} + +func (s *Service) dataSourceCount(ctx context.Context) int { + return len(s.plugins.Plugins(ctx, plugins.DataSource)) +} diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go new file mode 100644 index 00000000000..6ff76c947bd --- /dev/null +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -0,0 +1,362 @@ +package statscollector + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/setting" +) + +func TestTotalStatsUpdate(t *testing.T) { + sqlStore := mockstore.NewSQLStoreMock() + s := createService(t, setting.NewCfg(), sqlStore) + s.cfg.MetricsEndpointEnabled = true + s.cfg.MetricsEndpointDisableTotalStats = false + + sqlStore.ExpectedSystemStats = &models.SystemStats{} + + tests := []struct { + MetricsEndpointEnabled bool + MetricsEndpointDisableTotalStats bool + ExpectedUpdate bool + }{ + { + MetricsEndpointEnabled: false, + MetricsEndpointDisableTotalStats: false, + ExpectedUpdate: false, + }, + { + MetricsEndpointEnabled: false, + MetricsEndpointDisableTotalStats: true, + ExpectedUpdate: false, + }, + { + MetricsEndpointEnabled: true, + MetricsEndpointDisableTotalStats: true, + ExpectedUpdate: false, + }, + { + MetricsEndpointEnabled: true, + MetricsEndpointDisableTotalStats: false, + ExpectedUpdate: true, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(fmt.Sprintf( + "metricsEnabled(%v) * totalStatsDisabled(%v) = %v", + tc.MetricsEndpointEnabled, + tc.MetricsEndpointDisableTotalStats, + tc.ExpectedUpdate, + ), func(t *testing.T) { + s.cfg.MetricsEndpointEnabled = tc.MetricsEndpointEnabled + s.cfg.MetricsEndpointDisableTotalStats = tc.MetricsEndpointDisableTotalStats + + assert.Equal(t, tc.ExpectedUpdate, s.updateTotalStats(context.Background())) + }) + } +} + +func TestFeatureUsageStats(t *testing.T) { + store := mockstore.NewSQLStoreMock() + mockSystemStats(store) + s := createService(t, setting.NewCfg(), store) + + m, err := s.collect(context.Background()) + require.NoError(t, err, "Expected no error") + + assert.Equal(t, 1, m["stats.features.feature_1.count"]) + assert.Equal(t, 1, m["stats.features.feature_2.count"]) +} + +func TestCollectingUsageStats(t *testing.T) { + sqlStore := mockstore.NewSQLStoreMock() + s := createService(t, &setting.Cfg{ + ReportingEnabled: true, + BuildVersion: "5.0.0", + AnonymousEnabled: true, + BasicAuthEnabled: true, + LDAPEnabled: true, + AuthProxyEnabled: true, + Packaging: "deb", + ReportingDistributor: "hosted-grafana", + }, sqlStore) + + s.startTime = time.Now().Add(-1 * time.Minute) + + mockSystemStats(sqlStore) + setupSomeDataSourcePlugins(t, s) + + sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{ + { + Type: models.DS_ES, + Count: 9, + }, + { + Type: models.DS_PROMETHEUS, + Count: 10, + }, + { + Type: "unknown_ds", + Count: 11, + }, + { + Type: "unknown_ds2", + Count: 12, + }, + } + + sqlStore.ExpectedDataSources = []*models.DataSource{ + { + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "esVersion": 2, + }), + }, + { + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "esVersion": 2, + }), + }, + { + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "esVersion": 70, + }), + }, + } + + sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{ + { + Type: models.DS_ES, + Access: "direct", + Count: 1, + }, + { + Type: models.DS_ES, + Access: "proxy", + Count: 2, + }, + { + Type: models.DS_PROMETHEUS, + Access: "proxy", + Count: 3, + }, + { + Type: "unknown_ds", + Access: "proxy", + Count: 4, + }, + { + Type: "unknown_ds2", + Access: "", + Count: 5, + }, + { + Type: "unknown_ds3", + Access: "direct", + Count: 6, + }, + { + Type: "unknown_ds4", + Access: "direct", + Count: 7, + }, + { + Type: "unknown_ds5", + Access: "proxy", + Count: 8, + }, + } + + sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{ + { + Type: "slack", + Count: 1, + }, + { + Type: "webhook", + Count: 2, + }, + } + + createConcurrentTokens(t, sqlStore) + + s.social = &mockSocial{ + OAuthProviders: map[string]bool{ + "github": true, + "gitlab": true, + "azuread": true, + "google": true, + "generic_oauth": true, + "grafana_com": true, + }, + } + + metrics, err := s.collect(context.Background()) + require.NoError(t, err) + + assert.EqualValues(t, 15, metrics["stats.total_auth_token.count"]) + assert.EqualValues(t, 2, metrics["stats.api_keys.count"]) + assert.EqualValues(t, 5, metrics["stats.avg_auth_token_per_user.count"]) + assert.EqualValues(t, 16, metrics["stats.dashboard_versions.count"]) + assert.EqualValues(t, 17, metrics["stats.annotations.count"]) + assert.EqualValues(t, 18, metrics["stats.alert_rules.count"]) + assert.EqualValues(t, 19, metrics["stats.library_panels.count"]) + assert.EqualValues(t, 20, metrics["stats.library_variables.count"]) + + assert.EqualValues(t, 9, metrics["stats.ds."+models.DS_ES+".count"]) + assert.EqualValues(t, 10, metrics["stats.ds."+models.DS_PROMETHEUS+".count"]) + + assert.EqualValues(t, 11+12, metrics["stats.ds.other.count"]) + + assert.EqualValues(t, 1, metrics["stats.ds_access."+models.DS_ES+".direct.count"]) + assert.EqualValues(t, 2, metrics["stats.ds_access."+models.DS_ES+".proxy.count"]) + assert.EqualValues(t, 3, metrics["stats.ds_access."+models.DS_PROMETHEUS+".proxy.count"]) + assert.EqualValues(t, 6+7, metrics["stats.ds_access.other.direct.count"]) + assert.EqualValues(t, 4+8, metrics["stats.ds_access.other.proxy.count"]) + + assert.EqualValues(t, 1, metrics["stats.alert_notifiers.slack.count"]) + assert.EqualValues(t, 2, metrics["stats.alert_notifiers.webhook.count"]) + + assert.EqualValues(t, 1, metrics["stats.auth_enabled.anonymous.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.basic_auth.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.ldap.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.auth_proxy.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_github.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_gitlab.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_google.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_azuread.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_generic_oauth.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_grafana_com.count"]) + + assert.EqualValues(t, 1, metrics["stats.packaging.deb.count"]) + assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"]) + + assert.InDelta(t, int64(65), metrics["stats.uptime"], 6) +} + +func mockSystemStats(sqlStore *mockstore.SQLStoreMock) { + sqlStore.ExpectedSystemStats = &models.SystemStats{ + Dashboards: 1, + Datasources: 2, + Users: 3, + Admins: 31, + Editors: 32, + Viewers: 33, + ActiveUsers: 4, + ActiveAdmins: 21, + ActiveEditors: 22, + ActiveViewers: 23, + ActiveSessions: 24, + DailyActiveUsers: 25, + DailyActiveAdmins: 26, + DailyActiveEditors: 27, + DailyActiveViewers: 28, + DailyActiveSessions: 29, + Orgs: 5, + Playlists: 6, + Alerts: 7, + Stars: 8, + Folders: 9, + DashboardPermissions: 10, + FolderPermissions: 11, + ProvisionedDashboards: 12, + Snapshots: 13, + Teams: 14, + AuthTokens: 15, + DashboardVersions: 16, + Annotations: 17, + AlertRules: 18, + LibraryPanels: 19, + LibraryVariables: 20, + DashboardsViewersCanAdmin: 3, + DashboardsViewersCanEdit: 2, + FoldersViewersCanAdmin: 1, + FoldersViewersCanEdit: 5, + APIKeys: 2, + } +} + +type mockSocial struct { + social.Service + + OAuthProviders map[string]bool +} + +func (m *mockSocial) GetOAuthProviders() map[string]bool { + return m.OAuthProviders +} + +type fakePluginStore struct { + plugins.Store + + plugins map[string]plugins.PluginDTO +} + +func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) { + p, exists := pr.plugins[pluginID] + + return p, exists +} + +func setupSomeDataSourcePlugins(t *testing.T, s *Service) { + t.Helper() + + s.plugins = &fakePluginStore{ + plugins: map[string]plugins.PluginDTO{ + models.DS_ES: { + Signature: "internal", + }, + models.DS_PROMETHEUS: { + Signature: "internal", + }, + models.DS_GRAPHITE: { + Signature: "internal", + }, + models.DS_MYSQL: { + Signature: "internal", + }, + }, + } +} + +func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO { + var result []plugins.PluginDTO + for _, v := range pr.plugins { + for _, t := range pluginTypes { + if v.Type == t { + result = append(result, v) + } + } + } + + return result +} + +func createService(t testing.TB, cfg *setting.Cfg, store sqlstore.Store) *Service { + t.Helper() + + return ProvideService( + &usagestats.UsageStatsMock{}, + cfg, + store, + &mockSocial{}, + &fakePluginStore{}, + featuremgmt.WithFeatures("feature1", "feature2"), + ) +} diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index 8521761435e..529e7abde09 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" uss "github.com/grafana/grafana/pkg/infra/usagestats/service" + "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" @@ -32,9 +33,10 @@ func ProvideBackgroundServiceRegistry( pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService, pm *manager.PluginManager, rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService, tracing tracing.Tracer, provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, usageStats *uss.UsageStats, - grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService, - metrics *metrics.InternalMetricsService, secretsService *secretsManager.SecretsService, - remoteCache *remotecache.RemoteCache, thumbnailsService thumbs.Service, StorageService store.StorageService, + statsCollector *statscollector.Service, grafanaUpdateChecker *updatechecker.GrafanaService, + pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService, + secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache, + thumbnailsService thumbs.Service, StorageService store.StorageService, // Need to make sure these are initialized, is there a better place to put them? _ *dashboardsnapshots.Service, _ *alerting.AlertNotificationService, _ serviceaccounts.Service, _ *guardian.Provider, @@ -56,11 +58,13 @@ func ProvideBackgroundServiceRegistry( pluginsUpdateChecker, metrics, usageStats, + statsCollector, tracing, remoteCache, secretsService, StorageService, - thumbnailsService) + thumbnailsService, + ) } // BackgroundServiceRegistry provides background services. diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 566aa24de28..61a10765e42 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -5,6 +5,7 @@ package server import ( "github.com/google/wire" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/avatar" @@ -21,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" uss "github.com/grafana/grafana/pkg/infra/usagestats/service" + "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" loginpkg "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" @@ -235,6 +237,7 @@ var wireBasicSet = wire.NewSet( guardian.ProvideService, avatar.ProvideAvatarCacheServer, authproxy.ProvideAuthProxy, + statscollector.ProvideService, ) var wireSet = wire.NewSet( From ad432108e66cc9fa2c2eb232c1a888088122ee03 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Fri, 8 Apr 2022 13:56:38 +0200 Subject: [PATCH 023/156] Chore: Remove bus from dashboards provisioning (#47495) * Chore: Remove bus from dashboards provisioning * fix symlink test, make it run on darwin * remove unused mock --- .../provisioning/dashboards/dashboard.go | 10 ++-- .../provisioning/dashboards/file_reader.go | 8 +-- ...ux_test.go => file_reader_symlink_test.go} | 6 +-- .../dashboards/file_reader_test.go | 52 +++++++++---------- .../provisioning/dashboards/validator_test.go | 28 +++++----- pkg/services/provisioning/provisioning.go | 2 +- .../provisioning/provisioning_test.go | 2 +- pkg/services/provisioning/utils/utils.go | 4 ++ 8 files changed, 59 insertions(+), 53 deletions(-) rename pkg/services/provisioning/dashboards/{file_reader_linux_test.go => file_reader_symlink_test.go} (91%) diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index 60752f263ac..881af5dd2cf 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -23,7 +23,7 @@ type DashboardProvisioner interface { } // DashboardProvisionerFactory creates DashboardProvisioners based on input -type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, utils.OrgStore) (DashboardProvisioner, error) +type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, utils.OrgStore, utils.DashboardStore) (DashboardProvisioner, error) // Provisioner is responsible for syncing dashboard from disk to Grafana's database. type Provisioner struct { @@ -35,7 +35,7 @@ type Provisioner struct { } // New returns a new DashboardProvisioner -func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgStore utils.OrgStore) (DashboardProvisioner, error) { +func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgStore utils.OrgStore, dashboardStore utils.DashboardStore) (DashboardProvisioner, error) { logger := log.New("provisioning.dashboard") cfgReader := &configReader{path: configDirectory, log: logger, orgStore: orgStore} configs, err := cfgReader.readConfig(ctx) @@ -43,7 +43,7 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das return nil, errutil.Wrap("Failed to read dashboards config", err) } - fileReaders, err := getFileReaders(configs, logger, provisioner) + fileReaders, err := getFileReaders(configs, logger, provisioner, dashboardStore) if err != nil { return nil, errutil.Wrap("Failed to initialize file readers", err) } @@ -123,14 +123,14 @@ func (provider *Provisioner) GetAllowUIUpdatesFromConfig(name string) bool { } func getFileReaders( - configs []*config, logger log.Logger, service dashboards.DashboardProvisioningService, + configs []*config, logger log.Logger, service dashboards.DashboardProvisioningService, store utils.DashboardStore, ) ([]*FileReader, error) { var readers []*FileReader for _, config := range configs { switch config.Type { case "file": - fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name), service) + fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name), service, store) if err != nil { return nil, errutil.Wrapf(err, "Failed to create file reader for config %v", config.Name) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 36dfdf7c541..827a941a87a 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -11,12 +11,12 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/util" ) @@ -33,6 +33,7 @@ type FileReader struct { Path string log log.Logger dashboardProvisioningService dashboards.DashboardProvisioningService + dashboardStore utils.DashboardStore FoldersFromFilesStructure bool mux sync.RWMutex @@ -41,7 +42,7 @@ type FileReader struct { } // NewDashboardFileReader returns a new filereader based on `config` -func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.DashboardProvisioningService) (*FileReader, error) { +func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.DashboardProvisioningService, dashboardStore utils.DashboardStore) (*FileReader, error) { var path string path, ok := cfg.Options["path"].(string) if !ok { @@ -63,6 +64,7 @@ func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.Dash Path: path, log: log, dashboardProvisioningService: service, + dashboardStore: dashboardStore, FoldersFromFilesStructure: foldersFromFilesStructure, usageTracker: newUsageTracker(), }, nil @@ -298,7 +300,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv } cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(folderName), OrgId: cfg.OrgID} - err := bus.Dispatch(ctx, cmd) + err := fr.dashboardStore.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, models.ErrDashboardNotFound) { return 0, err diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go similarity index 91% rename from pkg/services/provisioning/dashboards/file_reader_linux_test.go rename to pkg/services/provisioning/dashboards/file_reader_symlink_test.go index c8f288d52f4..91b356e251a 100644 --- a/pkg/services/provisioning/dashboards/file_reader_linux_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go @@ -1,5 +1,5 @@ -//go:build linux -// +build linux +//go:build linux || darwin +// +build linux darwin package dashboards @@ -25,7 +25,7 @@ func TestProvisionedSymlinkedFolder(t *testing.T) { Options: map[string]interface{}{"path": symlinkedFolder}, } - reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil) if err != nil { t.Error("expected err to be nil") } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 8e6b01f55d1..627ed6041b8 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" @@ -42,7 +41,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { t.Run("using path parameter", func(t *testing.T) { cfg := setup() cfg.Options["path"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil) require.NoError(t, err) require.NotEqual(t, reader.Path, "") }) @@ -50,7 +49,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { t.Run("using folder as options", func(t *testing.T) { cfg := setup() cfg.Options["folder"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil) require.NoError(t, err) require.NotEqual(t, reader.Path, "") }) @@ -59,7 +58,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { cfg := setup() cfg.Options["path"] = foldersFromFilesStructure cfg.Options["foldersFromFilesStructure"] = true - reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil) require.NoError(t, err) require.NotEqual(t, reader.Path, "") }) @@ -72,7 +71,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { } cfg.Options["folder"] = fullPath - reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil) require.NoError(t, err) require.Equal(t, reader.Path, fullPath) @@ -82,7 +81,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { t.Run("using relative path", func(t *testing.T) { cfg := setup() cfg.Options["folder"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil) require.NoError(t, err) resolvedPath := reader.resolvedPath() @@ -96,9 +95,8 @@ func TestDashboardFileReader(t *testing.T) { fakeService := &dashboards.FakeDashboardProvisioning{} defer fakeService.AssertExpectations(t) + fakeStore := &fakeDashboardStore{} setup := func() { - bus.ClearBusHandlers() - bus.AddHandler("test", mockGetDashboardQuery) cfg = &config{ Name: configName, Type: "file", @@ -118,7 +116,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 2}, nil).Times(2) - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -138,7 +136,7 @@ func TestDashboardFileReader(t *testing.T) { inserted++ }) - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -175,7 +173,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -203,7 +201,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -238,7 +236,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -266,7 +264,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -281,7 +279,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -298,7 +296,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3) - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -315,7 +313,7 @@ func TestDashboardFileReader(t *testing.T) { Folder: "", } - _, err := NewDashboardFileReader(cfg, logger, nil) + _, err := NewDashboardFileReader(cfg, logger, nil, nil) require.NotNil(t, err) }) @@ -323,7 +321,7 @@ func TestDashboardFileReader(t *testing.T) { setup() cfg.Options["path"] = brokenDashboards - _, err := NewDashboardFileReader(cfg, logger, nil) + _, err := NewDashboardFileReader(cfg, logger, nil, nil) require.NoError(t, err) }) @@ -336,14 +334,14 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) - reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader1.walkDisk(context.Background()) require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -363,7 +361,7 @@ func TestDashboardFileReader(t *testing.T) { "folder": defaultDashboards, }, } - r, err := NewDashboardFileReader(cfg, logger, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, nil) require.NoError(t, err) _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) @@ -383,7 +381,7 @@ func TestDashboardFileReader(t *testing.T) { } fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() - r, err := NewDashboardFileReader(cfg, logger, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) @@ -403,7 +401,7 @@ func TestDashboardFileReader(t *testing.T) { }, } - r, err := NewDashboardFileReader(cfg, logger, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) @@ -458,7 +456,7 @@ func TestDashboardFileReader(t *testing.T) { cfg.DisableDeletion = true - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -473,7 +471,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -512,6 +510,8 @@ func (ffi FakeFileInfo) Sys() interface{} { return nil } -func mockGetDashboardQuery(_ context.Context, _ *models.GetDashboardQuery) error { +type fakeDashboardStore struct{} + +func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *models.GetDashboardQuery) error { return models.ErrDashboardNotFound } diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index 1af1e7c05ff..b3e39e3072d 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -5,7 +5,6 @@ import ( "sort" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" @@ -19,11 +18,9 @@ const ( ) func TestDuplicatesValidator(t *testing.T) { - bus.ClearBusHandlers() fakeService := &dashboards.FakeDashboardProvisioning{} defer fakeService.AssertExpectations(t) - bus.AddHandler("test", mockGetDashboardQuery) cfg := &config{ Name: "Default", Type: "file", @@ -36,7 +33,8 @@ func TestDuplicatesValidator(t *testing.T) { t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) { const folderName = "duplicates-validator-folder" - r, err := NewDashboardFileReader(cfg, logger, nil) + fakeStore := &fakeDashboardStore{} + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6) fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(4) @@ -55,11 +53,11 @@ func TestDuplicatesValidator(t *testing.T) { Options: map[string]interface{}{"path": dashboardContainingUID}, } - reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -91,7 +89,8 @@ func TestDuplicatesValidator(t *testing.T) { t.Run("Duplicates validator should not collect info about duplicate UIDs and titles within folders for different orgs", func(t *testing.T) { const folderName = "duplicates-validator-folder" - r, err := NewDashboardFileReader(cfg, logger, nil) + fakeStore := &fakeDashboardStore{} + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) require.NoError(t, err) @@ -107,11 +106,11 @@ func TestDuplicatesValidator(t *testing.T) { Options: map[string]interface{}{"path": dashboardContainingUID}, } - reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -154,6 +153,7 @@ func TestDuplicatesValidator(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(3) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + fakeStore := &fakeDashboardStore{} cfg1 := &config{ Name: "first", Type: "file", OrgID: 1, Folder: "duplicates-validator-folder", @@ -167,15 +167,15 @@ func TestDuplicatesValidator(t *testing.T) { Name: "third", Type: "file", OrgID: 2, Folder: "duplicates-validator-folder", Options: map[string]interface{}{"path": twoDashboardsWithUID}, } - reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) - reader3, err := NewDashboardFileReader(cfg3, logger, nil) + reader3, err := NewDashboardFileReader(cfg3, logger, nil, fakeStore) reader3.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -192,7 +192,7 @@ func TestDuplicatesValidator(t *testing.T) { duplicates := duplicateValidator.getDuplicates() - r, err := NewDashboardFileReader(cfg, logger, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder) require.NoError(t, err) @@ -209,7 +209,7 @@ func TestDuplicatesValidator(t *testing.T) { sort.Strings(titleUsageReaders) require.Equal(t, []string{"first"}, titleUsageReaders) - r, err = NewDashboardFileReader(cfg3, logger, nil) + r, err = NewDashboardFileReader(cfg3, logger, nil, fakeStore) require.NoError(t, err) folderID, err = r.getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder) require.NoError(t, err) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 4fb9202524f..091d3329aed 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -187,7 +187,7 @@ func (ps *ProvisioningServiceImpl) ProvisionNotifications(ctx context.Context) e func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) error { dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards") - dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService, ps.SQLStore) + dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService, ps.SQLStore, ps.SQLStore) if err != nil { return errutil.Wrap("Failed to create provisioner", err) } diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 4493d6569dd..ad2547e3ed6 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -93,7 +93,7 @@ func setup() *serviceTestStruct { } serviceTest.service = newProvisioningServiceImpl( - func(context.Context, string, dashboardstore.DashboardProvisioningService, utils.OrgStore) (dashboards.DashboardProvisioner, error) { + func(context.Context, string, dashboardstore.DashboardProvisioningService, utils.OrgStore, utils.DashboardStore) (dashboards.DashboardProvisioner, error) { return serviceTest.mock, nil }, nil, diff --git a/pkg/services/provisioning/utils/utils.go b/pkg/services/provisioning/utils/utils.go index 18f74408b52..315f4fb251e 100644 --- a/pkg/services/provisioning/utils/utils.go +++ b/pkg/services/provisioning/utils/utils.go @@ -12,6 +12,10 @@ type OrgStore interface { GetOrgById(context.Context, *models.GetOrgByIdQuery) error } +type DashboardStore interface { + GetDashboard(context.Context, *models.GetDashboardQuery) error +} + func CheckOrgExists(ctx context.Context, store OrgStore, orgID int64) error { query := models.GetOrgByIdQuery{Id: orgID} if err := store.GetOrgById(ctx, &query); err != nil { From b31c7d36548d831bbd1e8a621cbac8f3fc9a5a68 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Fri, 8 Apr 2022 14:30:25 +0200 Subject: [PATCH 024/156] Chore: Remove bus from alerting rule (#47508) * Chore: Remove bus from alerting rule * fix alerting tests * fix provide service --- pkg/api/alerting.go | 3 +-- pkg/services/alerting/engine.go | 1 + pkg/services/alerting/engine_test.go | 4 ++++ pkg/services/alerting/extractor.go | 6 ++++-- pkg/services/alerting/extractor_test.go | 6 ++++-- pkg/services/alerting/reader.go | 2 +- pkg/services/alerting/rule.go | 13 ++++++------- pkg/services/alerting/rule_test.go | 8 ++++---- pkg/services/alerting/test_rule.go | 2 +- .../libraryelements/libraryelements_test.go | 2 +- pkg/services/librarypanels/librarypanels_test.go | 2 +- 11 files changed, 28 insertions(+), 21 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 4b58ace4bce..487d4ac9905 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/guardian" @@ -491,7 +490,7 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response { SecureSettings: dto.SecureSettings, } - if err := bus.Dispatch(c.Req.Context(), cmd); err != nil { + if err := hs.AlertNotificationService.HandleNotificationTestCommand(c.Req.Context(), cmd); err != nil { if errors.Is(err, models.ErrSmtpNotEnabled) { return response.Error(412, err.Error(), err) } diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index cb39ff67448..9566d7725b4 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -30,6 +30,7 @@ type AlertStore interface { GetDashboardUIDById(context.Context, *models.GetDashboardRefByIdQuery) error SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error + GetAlertNotificationUidWithId(context.Context, *models.GetAlertNotificationUidQuery) error GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error SetAlertState(context.Context, *models.SetAlertStateCommand) error diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go index 6e7fb674d68..3ed3ee42362 100644 --- a/pkg/services/alerting/engine_test.go +++ b/pkg/services/alerting/engine_test.go @@ -65,6 +65,10 @@ func (a *AlertStoreMock) GetAllAlertQueryHandler(c context.Context, cmd *models. return nil } +func (a *AlertStoreMock) GetAlertNotificationUidWithId(c context.Context, query *models.GetAlertNotificationUidQuery) error { + return nil +} + func (a *AlertStoreMock) GetAlertNotificationsWithUidToSend(c context.Context, cmd *models.GetAlertNotificationsWithUidToSendQuery) error { if a.getAlertNotificationsWithUidToSend != nil { return a.getAlertNotificationsWithUidToSend(c, cmd) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 3238b5b60ac..460489b0a3d 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -22,13 +22,15 @@ type DashAlertExtractor interface { type DashAlertExtractorService struct { datasourcePermissionsService permissions.DatasourcePermissionsService datasourceService datasources.DataSourceService + alertStore AlertStore log log.Logger } -func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService) *DashAlertExtractorService { +func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService, alertStore AlertStore) *DashAlertExtractorService { return &DashAlertExtractorService{ datasourcePermissionsService: datasourcePermissionsService, datasourceService: datasourceService, + alertStore: alertStore, log: log.New("alerting.extractor"), } } @@ -231,7 +233,7 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json alert.Settings = jsonAlert // validate - _, err = NewRuleFromDBAlert(ctx, alert, logTranslationFailures) + _, err = NewRuleFromDBAlert(ctx, e.alertStore, alert, logTranslationFailures) if err != nil { return nil, err } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 90c7499d9e3..9b2dba8b0fc 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -36,7 +37,8 @@ func TestAlertRuleExtraction(t *testing.T) { } dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs} - extractor := ProvideDashAlertExtractorService(dsPermissions, dsService) + store := mockstore.NewSQLStoreMock() + extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, store) t.Run("Parsing alert rules from dashboard json", func(t *testing.T) { dashJSON, err := simplejson.NewJson(json) @@ -312,7 +314,7 @@ func TestFilterPermissionsErrors(t *testing.T) { dsPermissions := permissions.NewMockDatasourcePermissionService() dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs} - extractor := ProvideDashAlertExtractorService(dsPermissions, dsService) + extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, nil) tc := []struct { name string diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go index 6d4cc8dd80f..ebd41626164 100644 --- a/pkg/services/alerting/reader.go +++ b/pkg/services/alerting/reader.go @@ -38,7 +38,7 @@ func (arr *defaultRuleReader) fetch(ctx context.Context) []*Rule { res := make([]*Rule, 0) for _, ruleDef := range cmd.Result { - if model, err := NewRuleFromDBAlert(ctx, ruleDef, false); err != nil { + if model, err := NewRuleFromDBAlert(ctx, arr.sqlStore, ruleDef, false); err != nil { arr.log.Error("Could not build alert model for rule", "ruleId", ruleDef.Id, "error", err) } else { res = append(res, model) diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 05ffc47805d..7dd920d2bfa 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -9,7 +9,6 @@ import ( "strconv" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" ) @@ -145,7 +144,7 @@ func getForValue(rawFor string) (time.Duration, error) { // NewRuleFromDBAlert maps a db version of // alert to an in-memory version. -func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) { +func NewRuleFromDBAlert(ctx context.Context, store AlertStore, ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) { model := &Rule{} model.ID = ruleDef.Id model.OrgID = ruleDef.OrgId @@ -170,7 +169,7 @@ func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslati for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) if id, err := jsonModel.Get("id").Int64(); err == nil { - uid, err := translateNotificationIDToUID(ctx, id, ruleDef.OrgId) + uid, err := translateNotificationIDToUID(ctx, store, id, ruleDef.OrgId) if err != nil { if !errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID) { logger.Error("Failed to translate notification id to uid", "error", err.Error(), "dashboardId", model.DashboardID, "alert", model.Name, "panelId", model.PanelID, "notificationId", id) @@ -211,8 +210,8 @@ func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslati return model, nil } -func translateNotificationIDToUID(ctx context.Context, id int64, orgID int64) (string, error) { - notificationUID, err := getAlertNotificationUIDByIDAndOrgID(ctx, id, orgID) +func translateNotificationIDToUID(ctx context.Context, store AlertStore, id int64, orgID int64) (string, error) { + notificationUID, err := getAlertNotificationUIDByIDAndOrgID(ctx, store, id, orgID) if err != nil { return "", err } @@ -220,13 +219,13 @@ func translateNotificationIDToUID(ctx context.Context, id int64, orgID int64) (s return notificationUID, nil } -func getAlertNotificationUIDByIDAndOrgID(ctx context.Context, notificationID int64, orgID int64) (string, error) { +func getAlertNotificationUIDByIDAndOrgID(ctx context.Context, store AlertStore, notificationID int64, orgID int64) (string, error) { query := &models.GetAlertNotificationUidQuery{ OrgId: orgID, Id: notificationID, } - if err := bus.Dispatch(ctx, query); err != nil { + if err := store.GetAlertNotificationUidWithId(ctx, query); err != nil { return "", err } diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go index 55c603a4c2b..506b7cb03b6 100644 --- a/pkg/services/alerting/rule_test.go +++ b/pkg/services/alerting/rule_test.go @@ -130,7 +130,7 @@ func TestAlertRuleModel(t *testing.T) { Settings: alertJSON, } - alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false) + alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false) require.Nil(t, err) require.Len(t, alertRule.Conditions, 1) @@ -169,7 +169,7 @@ func TestAlertRuleModel(t *testing.T) { Settings: alertJSON, } - alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false) + alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false) require.Nil(t, err) require.NotContains(t, alertRule.Notifications, "999") require.Contains(t, alertRule.Notifications, "notifier2") @@ -200,7 +200,7 @@ func TestAlertRuleModel(t *testing.T) { Settings: alertJSON, } - alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false) + alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false) require.Nil(t, err) require.EqualValues(t, alertRule.Frequency, 60) }) @@ -238,7 +238,7 @@ func TestAlertRuleModel(t *testing.T) { Settings: alertJSON, } - _, err := NewRuleFromDBAlert(context.Background(), alert, false) + _, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false) require.NotNil(t, err) require.EqualValues(t, err.Error(), "alert validation error: Neither id nor uid is specified in 'notifications' block, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1") }) diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index b8c4e2e5edf..011990faad7 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -25,7 +25,7 @@ func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID if alert.PanelId != panelID { continue } - rule, err := NewRuleFromDBAlert(context.Background(), alert, true) + rule, err := NewRuleFromDBAlert(context.Background(), e.sqlStore, alert, true) if err != nil { return nil, err } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 2358b8f6342..a4bc6ed6cfa 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -201,7 +201,7 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.Sign } dashboardStore := database.ProvideDashboardStore(sqlStore) - dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil) + dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil, nil) service := dashboardservice.ProvideDashboardService( setting.NewCfg(), dashboardStore, dashAlertExtractor, featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(), diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 0d13913c966..26f28a798c2 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -1422,7 +1422,7 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user *models.Sig } dashboardStore := database.ProvideDashboardStore(sqlStore) - dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil) + dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil, nil) service := dashboardservice.ProvideDashboardService( setting.NewCfg(), dashboardStore, dashAlertService, featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(), From c530100d45a7235acc773c43970e8a5d99faede7 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 8 Apr 2022 15:26:33 +0200 Subject: [PATCH 025/156] fix: don't allow editing rule types for existing rules (#47512) --- .../components/rule-editor/AlertTypeStep.tsx | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx index aa2179f2265..b06e7e2497c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx @@ -38,27 +38,24 @@ export const AlertTypeStep: FC = ({ editingExistingRule }) => { return ( - - ( - - )} - name="type" - control={control} - rules={{ - required: { value: true, message: 'Please select alert type' }, - }} - /> - + {!editingExistingRule && ( + + ( + + )} + name="type" + control={control} + rules={{ + required: { value: true, message: 'Please select alert type' }, + }} + /> + + )} Date: Fri, 8 Apr 2022 16:09:01 +0200 Subject: [PATCH 026/156] fixed dashboard spelling (#47515) --- docs/sources/dashboards/dashboard-ui/dashboard-row.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/dashboards/dashboard-ui/dashboard-row.md b/docs/sources/dashboards/dashboard-ui/dashboard-row.md index b00e1f1c80c..52e637ed689 100644 --- a/docs/sources/dashboards/dashboard-ui/dashboard-row.md +++ b/docs/sources/dashboards/dashboard-ui/dashboard-row.md @@ -4,7 +4,7 @@ aliases = ["/docs/grafana/latest/features/dashboard/dashboards/"] weight = 80 +++ -# Dshboard rows +# Dashboard rows A dashboard row is a logical divider within a dashboard. It is used to group panels together. From 18e93c70773141b9a43857300b89a69e543cfdea Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Fri, 8 Apr 2022 16:15:06 +0200 Subject: [PATCH 027/156] Chore: Remove bus (#47511) * Chore: Remove bus * remove unused const --- pkg/api/admin_users_test.go | 28 ---- pkg/api/alerting_test.go | 7 - pkg/api/annotations_test.go | 9 -- pkg/api/common_test.go | 4 - pkg/api/dashboard_snapshot_test.go | 8 -- pkg/api/dashboard_test.go | 20 --- pkg/api/folder_permission_test.go | 3 - pkg/api/folder_test.go | 4 - pkg/api/pluginproxy/ds_proxy_test.go | 13 -- pkg/api/plugins_test.go | 3 - pkg/api/short_url_test.go | 3 - pkg/api/user_test.go | 19 --- pkg/api/user_token_test.go | 15 -- pkg/bus/bus.go | 107 -------------- pkg/bus/bus_test.go | 135 ------------------ pkg/expr/service_test.go | 7 - pkg/login/auth.go | 2 - pkg/middleware/middleware_basic_auth_test.go | 16 --- pkg/middleware/middleware_jwt_auth_test.go | 9 -- pkg/middleware/middleware_test.go | 3 - pkg/middleware/org_redirect_test.go | 5 - pkg/middleware/recovery_test.go | 3 - pkg/services/alerting/notifier_test.go | 9 -- pkg/services/alerting/service.go | 14 -- .../manager/dashboard_service_test.go | 3 - .../dashboardsnapshots/dashboardsnapshots.go | 6 - .../datasources/service/datasource_service.go | 8 -- .../authinfoservice/database/database.go | 9 -- .../login/loginservice/loginservice.go | 1 - .../login/loginservice/loginservice_test.go | 2 - pkg/services/notifications/notifications.go | 7 - .../notifications/notifications_test.go | 37 +++-- .../service/dashboard_updater_test.go | 2 - pkg/services/search/service.go | 1 - pkg/services/sqlstore/alert.go | 11 -- pkg/services/sqlstore/alert_notification.go | 5 - .../sqlstore/alert_notification_test.go | 9 +- pkg/services/sqlstore/apikey.go | 9 -- pkg/services/sqlstore/dashboard.go | 13 -- pkg/services/sqlstore/dashboard_acl.go | 5 - pkg/services/sqlstore/dashboard_version.go | 7 - pkg/services/sqlstore/login_attempt.go | 7 - pkg/services/sqlstore/org.go | 11 -- pkg/services/sqlstore/org_users.go | 8 -- pkg/services/sqlstore/playlist.go | 10 -- pkg/services/sqlstore/preferences.go | 8 -- pkg/services/sqlstore/quota.go | 11 -- pkg/services/sqlstore/sqlstore.go | 22 --- pkg/services/sqlstore/star.go | 8 -- pkg/services/sqlstore/stats.go | 10 -- pkg/services/sqlstore/team.go | 14 -- pkg/services/sqlstore/temp_user.go | 10 -- pkg/services/sqlstore/user.go | 20 --- 53 files changed, 19 insertions(+), 701 deletions(-) diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index b2d331eb213..0ba11679d3c 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "fmt" "testing" @@ -49,11 +48,6 @@ func TestAdminAPIEndpoint(t *testing.T) { mock := mockstore.NewSQLStoreMock() adminLogoutUserScenario(t, "Should not be allowed when calling POST on", "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { - bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error { - cmd.Result = &models.User{Id: testUserID} - return nil - }) - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 400, sc.resp.Code) }, mock) @@ -182,8 +176,6 @@ func TestAdminAPIEndpoint(t *testing.T) { } adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { - bus.ClearBusHandlers() - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -202,8 +194,6 @@ func TestAdminAPIEndpoint(t *testing.T) { } adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { - bus.ClearBusHandlers() - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -222,8 +212,6 @@ func TestAdminAPIEndpoint(t *testing.T) { } adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { - bus.ClearBusHandlers() - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 400, sc.resp.Code) @@ -241,8 +229,6 @@ func TestAdminAPIEndpoint(t *testing.T) { } adminCreateUserScenario(t, "Should return an error", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { - bus.ClearBusHandlers() - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 412, sc.resp.Code) @@ -256,8 +242,6 @@ func TestAdminAPIEndpoint(t *testing.T) { func putAdminScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - hs := &HTTPServer{ Cfg: setting.NewCfg(), SQLStore: sqlStore, @@ -284,8 +268,6 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - hs := HTTPServer{ Bus: bus.GetBus(), AuthTokenService: auth.NewFakeUserAuthTokenService(), @@ -312,8 +294,6 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ @@ -343,8 +323,6 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ @@ -372,8 +350,6 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route func adminDisableUserScenario(t *testing.T, desc string, action string, url string, routePattern string, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() authInfoService := &logintest.AuthInfoServiceFake{} @@ -410,8 +386,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern SQLStore: mockstore.NewSQLStoreMock(), } t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(t, url) sc.sqlStore = hs.SQLStore sc.authInfoService = &logintest.AuthInfoServiceFake{} @@ -430,8 +404,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - hs := HTTPServer{ Bus: bus.GetBus(), Login: loginservice.LoginServiceMock{ diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 1f09ca06a19..12deadcc9d5 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/search" @@ -124,18 +123,12 @@ func TestAlertingAPIEndpoint(t *testing.T) { } func callPauseAlert(sc *scenarioContext) { - bus.AddHandler("test", func(ctx context.Context, cmd *models.PauseAlertCommand) error { - return nil - }) - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } func postAlertScenario(t *testing.T, hs *HTTPServer, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PauseAlertCommand, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 30b0af8b746..300a751454a 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" @@ -293,8 +292,6 @@ var fakeAnnoRepo *fakeAnnotationsRepo func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PostAnnotationsCmd, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - hs := setupSimpleHTTPServer(nil) store := sqlstore.InitTestDB(t) store.Cfg = hs.Cfg @@ -324,8 +321,6 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern func putAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - hs := setupSimpleHTTPServer(nil) store := sqlstore.InitTestDB(t) store.Cfg = hs.Cfg @@ -354,8 +349,6 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - hs := setupSimpleHTTPServer(nil) store := sqlstore.InitTestDB(t) store.Cfg = hs.Cfg @@ -385,8 +378,6 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.MassDeleteAnnotationsCmd, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - hs := setupSimpleHTTPServer(nil) store := sqlstore.InitTestDB(t) store.Cfg = hs.Cfg diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index f1870cca517..41ccf6ddd1c 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -53,8 +53,6 @@ func loggedInUserScenario(t *testing.T, desc string, url string, routePattern st func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(t, url) sc.sqlStore = sqlStore sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { @@ -82,8 +80,6 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url func anonymousUserScenario(t *testing.T, desc string, method string, url string, routePattern string, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.context = c diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index a6d1bcef985..d34d285c46d 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "fmt" "net/http" "net/http/httptest" @@ -9,7 +8,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" @@ -52,12 +50,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { sqlmock.ExpectedDashboardAclInfoList = aclMockResp sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{} - // we need it here for now for the guadian service to work - bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { - query.Result = aclMockResp - return nil - }) - return mockSnapshotResult } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 54b77fbb520..24ec9479b2d 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -957,31 +957,17 @@ func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) { } func (hs *HTTPServer) callGetDashboardVersion(sc *scenarioContext) { - bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionQuery) error { - query.Result = &models.DashboardVersion{} - return nil - }) - sc.handlerFunc = hs.GetDashboardVersion sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) { - bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionsQuery) error { - query.Result = []*models.DashboardVersionDTO{} - return nil - }) - sc.handlerFunc = hs.GetDashboardVersions sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T, sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) { - bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error { - return nil - }) - hs.dashboardService = mockDashboard sc.handlerFunc = hs.DeleteDashboardByUID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -1003,8 +989,6 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) { func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, cmd models.SaveDashboardCommand, dashboardService dashboards.DashboardService, folderService dashboards.FolderService, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - cfg := setting.NewCfg() hs := HTTPServer{ Bus: bus.GetBus(), @@ -1040,8 +1024,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role models.RoleType, fn scenarioFunc, sqlmock sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - cfg := setting.NewCfg() hs := HTTPServer{ Cfg: cfg, @@ -1076,8 +1058,6 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - cfg := setting.NewCfg() mockSQLStore := mockstore.NewSQLStoreMock() hs := HTTPServer{ diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 18b45c069cb..9198f15e55b 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" service "github.com/grafana/grafana/pkg/services/dashboards/manager" @@ -355,8 +354,6 @@ func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) { func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) { t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(t, ctx.url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index e61f232c1f3..aa437ea2cf5 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -138,8 +138,6 @@ func callCreateFolder(sc *scenarioContext) { func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService, cmd models.CreateFolderCommand, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - hs := HTTPServer{ Bus: bus.GetBus(), Cfg: setting.NewCfg(), @@ -170,8 +168,6 @@ func callUpdateFolder(sc *scenarioContext) { func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService, cmd models.UpdateFolderCommand, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - hs := HTTPServer{ Cfg: setting.NewCfg(), folderService: folderService, diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 370cf09883f..9af527368de 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -462,19 +462,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) { }) t.Run("When proxying a datasource that has OAuth token pass-through enabled", func(t *testing.T) { - bus.AddHandler("test", func(ctx context.Context, query *models.GetAuthInfoQuery) error { - query.Result = &models.UserAuth{ - Id: 1, - UserId: 1, - AuthModule: "generic_oauth", - OAuthAccessToken: "testtoken", - OAuthRefreshToken: "testrefreshtoken", - OAuthTokenType: "Bearer", - OAuthExpiry: time.Now().AddDate(0, 0, 1), - } - return nil - }) - ds := &models.DataSource{ Type: "custom-datasource", Url: "http://host/root/", diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 8c8674fd6c4..3504d51e4cf 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -218,8 +217,6 @@ func callGetPluginAsset(sc *scenarioContext) { func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store, logger log.Logger, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - hs := HTTPServer{ Cfg: setting.NewCfg(), pluginStore: pluginStore, diff --git a/pkg/api/short_url_test.go b/pkg/api/short_url_test.go index 752004738f2..bb592a9d30a 100644 --- a/pkg/api/short_url_test.go +++ b/pkg/api/short_url_test.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/shorturls" @@ -54,8 +53,6 @@ func callCreateShortURL(sc *scenarioContext) { func createShortURLScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CreateShortURLCmd, shortURLService shorturls.Service, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() - hs := HTTPServer{ Cfg: setting.NewCfg(), ShortURLService: shortURLService, diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index bc4df9b7ec9..fbf77357ad7 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -107,25 +107,6 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { }, mock) loggedInUserScenario(t, "When calling GET on", "/api/users/lookup", "/api/users/lookup", func(sc *scenarioContext) { - fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) - bus.AddHandler("test", func(ctx context.Context, query *models.GetUserByLoginQuery) error { - require.Equal(t, "danlee", query.LoginOrEmail) - - query.Result = &models.User{ - Id: int64(1), - Email: "daniel@grafana.com", - Name: "Daniel", - Login: "danlee", - Theme: "light", - IsAdmin: true, - OrgId: int64(2), - IsDisabled: false, - Updated: fakeNow, - Created: fakeNow, - } - - return nil - }) createUserCmd := models.CreateUserCommand{ Email: fmt.Sprint("admin", "@test.com"), Name: "admin", diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index 37766385f13..4a07a6b737e 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -79,11 +79,6 @@ func TestUserTokenAPIEndpoint(t *testing.T) { token := &models.UserToken{Id: 2} mock := mockstore.NewSQLStoreMock() revokeUserAuthTokenInternalScenario(t, "Should not be successful", cmd, testUserID, token, func(sc *scenarioContext) { - bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error { - cmd.Result = &models.User{Id: testUserID} - return nil - }) - sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { return token, nil } @@ -153,8 +148,6 @@ func TestUserTokenAPIEndpoint(t *testing.T) { func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ @@ -183,8 +176,6 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ @@ -212,8 +203,6 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(desc, func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - hs := HTTPServer{ Bus: bus.GetBus(), AuthTokenService: auth.NewFakeUserAuthTokenService(), @@ -239,8 +228,6 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.RevokeAuthTokenCmd, userId int64, token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(desc, func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ @@ -267,8 +254,6 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(desc, func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index e3f207ef814..28fdc4b002e 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -21,41 +21,17 @@ type Msg interface{} // ErrHandlerNotFound defines an error if a handler is not found var ErrHandlerNotFound = errors.New("handler not found") -// TransactionManager defines a transaction interface -type TransactionManager interface { - InTransaction(ctx context.Context, fn func(ctx context.Context) error) error -} - // Bus type defines the bus interface structure type Bus interface { - Dispatch(ctx context.Context, msg Msg) error - Publish(ctx context.Context, msg Msg) error - - // InTransaction starts a transaction and store it in the context. - // The caller can then pass a function with multiple DispatchCtx calls that - // all will be executed in the same transaction. InTransaction will rollback if the - // callback returns an error. - InTransaction(ctx context.Context, fn func(ctx context.Context) error) error - - AddHandler(handler HandlerFunc) - AddEventListener(handler HandlerFunc) - - // SetTransactionManager allows the user to replace the internal - // noop TransactionManager that is responsible for managing - // transactions in `InTransaction` - SetTransactionManager(tm TransactionManager) } // InProcBus defines the bus structure type InProcBus struct { logger log.Logger - handlers map[string]HandlerFunc - handlersWithCtx map[string]HandlerFunc listeners map[string][]HandlerFunc listenersWithCtx map[string][]HandlerFunc - txMng TransactionManager tracer tracing.Tracer } @@ -64,11 +40,6 @@ func ProvideBus(tracer tracing.Tracer) *InProcBus { return globalBus } -// InTransaction defines an in transaction function -func (b *InProcBus) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error { - return b.txMng.InTransaction(ctx, fn) -} - // temp stuff, not sure how to handle bus instance, and init yet var globalBus = New() @@ -76,11 +47,8 @@ var globalBus = New() func New() *InProcBus { bus := &InProcBus{ logger: log.New("bus"), - handlers: make(map[string]HandlerFunc), - handlersWithCtx: make(map[string]HandlerFunc), listeners: make(map[string][]HandlerFunc), listenersWithCtx: make(map[string][]HandlerFunc), - txMng: &noopTransactionManager{}, } bus.tracer = tracing.InitializeForBus() return bus @@ -91,46 +59,6 @@ func GetBus() Bus { return globalBus } -// SetTransactionManager function assign a transaction manager to the bus. -func (b *InProcBus) SetTransactionManager(tm TransactionManager) { - b.txMng = tm -} - -// DispatchCtx function dispatch a message to the bus context. -func (b *InProcBus) Dispatch(ctx context.Context, msg Msg) error { - var msgName = reflect.TypeOf(msg).Elem().Name() - - ctx, span := b.tracer.Start(ctx, "bus - "+msgName) - defer span.End() - - span.SetAttributes("msg", msgName, attribute.Key("msg").String(msgName)) - - withCtx := true - var handler = b.handlersWithCtx[msgName] - if handler == nil { - withCtx = false - handler = b.handlers[msgName] - if handler == nil { - return ErrHandlerNotFound - } - } - - var params = []reflect.Value{} - if withCtx { - params = append(params, reflect.ValueOf(ctx)) - } else if setting.Env == setting.Dev { - b.logger.Warn("DispatchCtx called with message handler registered using AddHandler and should be changed to use AddHandler", "msgName", msgName) - } - params = append(params, reflect.ValueOf(msg)) - - ret := reflect.ValueOf(handler).Call(params) - err := ret[0].Interface() - if err == nil { - return nil - } - return err.(error) -} - // PublishCtx function publish a message to the bus listener. func (b *InProcBus) Publish(ctx context.Context, msg Msg) error { var msgName = reflect.TypeOf(msg).Elem().Name() @@ -176,17 +104,6 @@ func callListeners(listeners []HandlerFunc, params []reflect.Value) error { return nil } -func (b *InProcBus) AddHandler(handler HandlerFunc) { - handlerType := reflect.TypeOf(handler) - queryTypeName := handlerType.In(1).Elem().Name() - b.handlersWithCtx[queryTypeName] = handler -} - -// GetHandlerCtx returns the handler function for the given struct name. -func (b *InProcBus) GetHandlerCtx(name string) HandlerFunc { - return b.handlersWithCtx[name] -} - func (b *InProcBus) AddEventListener(handler HandlerFunc) { handlerType := reflect.TypeOf(handler) eventName := handlerType.In(1).Elem().Name() @@ -197,36 +114,12 @@ func (b *InProcBus) AddEventListener(handler HandlerFunc) { b.listenersWithCtx[eventName] = append(b.listenersWithCtx[eventName], handler) } -// AddHandler attaches a handler function to the global bus context. -// Package level function. -func AddHandler(implName string, handler HandlerFunc) { - globalBus.AddHandler(handler) -} - // AddEventListenerCtx attaches a handler function to the event listener. // Package level function. func AddEventListener(handler HandlerFunc) { globalBus.AddEventListener(handler) } -func Dispatch(ctx context.Context, msg Msg) error { - return globalBus.Dispatch(ctx, msg) -} - func Publish(ctx context.Context, msg Msg) error { return globalBus.Publish(ctx, msg) } - -func GetHandlerCtx(name string) HandlerFunc { - return globalBus.GetHandlerCtx(name) -} - -func ClearBusHandlers() { - globalBus = New() -} - -type noopTransactionManager struct{} - -func (*noopTransactionManager) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error { - return fn(ctx) -} diff --git a/pkg/bus/bus_test.go b/pkg/bus/bus_test.go index 05789ef1f8b..3bb6a6bb86e 100644 --- a/pkg/bus/bus_test.go +++ b/pkg/bus/bus_test.go @@ -2,7 +2,6 @@ package bus import ( "context" - "errors" "testing" "github.com/grafana/grafana/pkg/infra/tracing" @@ -14,140 +13,6 @@ type testQuery struct { Resp string } -func TestDispatch(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - var invoked bool - - bus.AddHandler(func(ctx context.Context, query *testQuery) error { - invoked = true - return nil - }) - - require.NoError(t, err) - err = bus.Dispatch(context.Background(), &testQuery{}) - require.NoError(t, err) - - require.True(t, invoked, "expected handler to be called") -} - -func TestDispatch_NoRegisteredHandler(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - err = bus.Dispatch(context.Background(), &testQuery{}) - require.Equal(t, err, ErrHandlerNotFound, - "expected bus to return HandlerNotFound since no handler is registered") -} - -func TestDispatch_ContextHandler(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - var invoked bool - - bus.AddHandler(func(ctx context.Context, query *testQuery) error { - invoked = true - return nil - }) - - err = bus.Dispatch(context.Background(), &testQuery{}) - require.NoError(t, err) - - require.True(t, invoked, "expected handler to be called") -} - -func TestDispatchCtx(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - var invoked bool - - bus.AddHandler(func(ctx context.Context, query *testQuery) error { - invoked = true - return nil - }) - - err = bus.Dispatch(context.Background(), &testQuery{}) - require.NoError(t, err) - - require.True(t, invoked, "expected handler to be called") -} - -func TestDispatchCtx_NoContextHandler(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - var invoked bool - - bus.AddHandler(func(ctx context.Context, query *testQuery) error { - invoked = true - return nil - }) - - err = bus.Dispatch(context.Background(), &testQuery{}) - require.NoError(t, err) - - require.True(t, invoked, "expected handler to be called") -} - -func TestDispatchCtx_NoRegisteredHandler(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - err = bus.Dispatch(context.Background(), &testQuery{}) - require.Equal(t, err, ErrHandlerNotFound, - "expected bus to return HandlerNotFound since no handler is registered") -} - -func TestQuery(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - want := "hello from handler" - - bus.AddHandler(func(ctx context.Context, q *testQuery) error { - q.Resp = want - return nil - }) - - q := &testQuery{} - - err = bus.Dispatch(context.Background(), q) - require.NoError(t, err, "unable to dispatch query") - - require.Equal(t, want, q.Resp) -} - -func TestQuery_HandlerReturnsError(t *testing.T) { - bus := New() - tracer, err := tracing.InitializeTracerForTest() - require.NoError(t, err) - bus.tracer = tracer - - bus.AddHandler(func(ctx context.Context, query *testQuery) error { - return errors.New("handler error") - }) - - err = bus.Dispatch(context.Background(), &testQuery{}) - require.Error(t, err, "expected error but got none") -} - func TestEventPublish(t *testing.T) { bus := New() tracer, err := tracing.InitializeTracerForTest() diff --git a/pkg/expr/service_test.go b/pkg/expr/service_test.go index 0449a98a4cc..6741174baad 100644 --- a/pkg/expr/service_test.go +++ b/pkg/expr/service_test.go @@ -10,8 +10,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" @@ -38,11 +36,6 @@ func TestService(t *testing.T) { secretsService: secretsService, } - bus.AddHandler("test", func(_ context.Context, query *models.GetDataSourceQuery) error { - query.Result = &models.DataSource{Uid: "1", OrgId: 1, Type: "test", JsonData: simplejson.New()} - return nil - }) - queries := []Query{ { RefID: "A", diff --git a/pkg/login/auth.go b/pkg/login/auth.go index ae76ee3bec5..d75b1cd8ad2 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" @@ -41,7 +40,6 @@ func ProvideService(store sqlstore.Store, loginService login.Service) *Authentic store: store, loginService: loginService, } - bus.AddHandler("auth", a.AuthenticateUser) return a } diff --git a/pkg/middleware/middleware_basic_auth_test.go b/pkg/middleware/middleware_basic_auth_test.go index dda1b609269..5836a0d315b 100644 --- a/pkg/middleware/middleware_basic_auth_test.go +++ b/pkg/middleware/middleware_basic_auth_test.go @@ -1,11 +1,9 @@ package middleware import ( - "context" "encoding/json" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -42,22 +40,8 @@ func TestMiddlewareBasicAuth(t *testing.T) { middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) { const password = "MyPass" - const salt = "Salt" const orgID int64 = 2 - bus.AddHandler("grafana-auth", func(ctx context.Context, query *models.LoginUserQuery) error { - t.Log("Handling LoginUserQuery") - encoded, err := util.EncodePassword(password, salt) - if err != nil { - return err - } - query.User = &models.User{ - Password: encoded, - Salt: salt, - } - return nil - }) - sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: orgID, UserId: id} authHeader := util.GetBasicAuthHeader("myUser", password) diff --git a/pkg/middleware/middleware_jwt_auth_test.go b/pkg/middleware/middleware_jwt_auth_test.go index 02d4075201d..31403464d69 100644 --- a/pkg/middleware/middleware_jwt_auth_test.go +++ b/pkg/middleware/middleware_jwt_auth_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/setting" @@ -109,14 +108,6 @@ func TestMiddlewareJWTAuth(t *testing.T) { }, nil } sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: id, OrgId: orgID, Email: myEmail} - bus.AddHandler("upsert-user", func(ctx context.Context, command *models.UpsertUserCommand) error { - command.Result = &models.User{ - Id: id, - Name: command.ExternalUser.Name, - Email: command.ExternalUser.Email, - } - return nil - }) sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec() assert.Equal(t, verifiedToken, token) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index b52083dbeed..76dbd87b1be 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" @@ -570,8 +569,6 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( t.Helper() t.Run(desc, func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - logger := log.New("test") loginMaxLifetime, err := gtime.ParseDuration("30d") diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index 22450ec880e..f943d12bd0a 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -5,7 +5,6 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/require" ) @@ -47,10 +46,6 @@ func TestOrgRedirectMiddleware(t *testing.T) { middlewareScenario(t, tc.desc, func(t *testing.T, sc *scenarioContext) { sc.withTokenSessionCookie("token") sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12} - bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error { - return nil - }) - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { return &models.UserToken{ UserId: 0, diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 7e3e4a5939f..251558056dc 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -5,7 +5,6 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" @@ -48,8 +47,6 @@ func panicHandler(c *models.ReqContext) { func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { t.Run(desc, func(t *testing.T) { - defer bus.ClearBusHandlers() - cfg := setting.NewCfg() cfg.ErrTemplateName = "error-template" sc := &scenarioContext{ diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index 558697083c8..c921908c6b8 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" - "github.com/grafana/grafana/pkg/bus" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/imguploader" @@ -205,14 +204,6 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext return nil } - bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error { - return nil - }) - - bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error { - return nil - }) - setting.AlertingNotificationTimeout = 30 * time.Second scenarioCtx := &scenarioContext{ diff --git a/pkg/services/alerting/service.go b/pkg/services/alerting/service.go index 5031b4cb9cc..8db32e18728 100644 --- a/pkg/services/alerting/service.go +++ b/pkg/services/alerting/service.go @@ -28,20 +28,6 @@ func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, encryptionService enc NotificationService: notificationService, } - s.Bus.AddHandler(s.GetAlertNotifications) - s.Bus.AddHandler(s.CreateAlertNotificationCommand) - s.Bus.AddHandler(s.UpdateAlertNotification) - s.Bus.AddHandler(s.DeleteAlertNotification) - s.Bus.AddHandler(s.GetAllAlertNotifications) - s.Bus.AddHandler(s.GetOrCreateAlertNotificationState) - s.Bus.AddHandler(s.SetAlertNotificationStateToCompleteCommand) - s.Bus.AddHandler(s.SetAlertNotificationStateToPendingCommand) - s.Bus.AddHandler(s.GetAlertNotificationsWithUid) - s.Bus.AddHandler(s.UpdateAlertNotificationWithUid) - s.Bus.AddHandler(s.DeleteAlertNotificationWithUid) - s.Bus.AddHandler(s.GetAlertNotificationsWithUidToSend) - s.Bus.AddHandler(s.HandleNotificationTestCommand) - return s } diff --git a/pkg/services/dashboards/manager/dashboard_service_test.go b/pkg/services/dashboards/manager/dashboard_service_test.go index 7e303d70471..b1a11805674 100644 --- a/pkg/services/dashboards/manager/dashboard_service_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -22,8 +21,6 @@ import ( func TestDashboardService(t *testing.T) { t.Run("Dashboard service tests", func(t *testing.T) { - bus.ClearBusHandlers() - fakeStore := m.FakeDashboardStore{} defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ diff --git a/pkg/services/dashboardsnapshots/dashboardsnapshots.go b/pkg/services/dashboardsnapshots/dashboardsnapshots.go index 990e60bb818..88745a181ed 100644 --- a/pkg/services/dashboardsnapshots/dashboardsnapshots.go +++ b/pkg/services/dashboardsnapshots/dashboardsnapshots.go @@ -23,12 +23,6 @@ func ProvideService(bus bus.Bus, store sqlstore.Store, secretsService secrets.Se SecretsService: secretsService, } - s.Bus.AddHandler(s.CreateDashboardSnapshot) - s.Bus.AddHandler(s.GetDashboardSnapshot) - s.Bus.AddHandler(s.DeleteDashboardSnapshot) - s.Bus.AddHandler(s.SearchDashboardSnapshots) - s.Bus.AddHandler(s.DeleteExpiredSnapshots) - return s } diff --git a/pkg/services/datasources/service/datasource_service.go b/pkg/services/datasources/service/datasource_service.go index 52ffe634707..30381852578 100644 --- a/pkg/services/datasources/service/datasource_service.go +++ b/pkg/services/datasources/service/datasource_service.go @@ -79,14 +79,6 @@ func ProvideService( permissionsService: permissionsServices.GetDataSourceService(), } - s.Bus.AddHandler(s.GetDataSources) - s.Bus.AddHandler(s.GetDataSourcesByType) - s.Bus.AddHandler(s.GetDataSource) - s.Bus.AddHandler(s.AddDataSource) - s.Bus.AddHandler(s.DeleteDataSource) - s.Bus.AddHandler(s.UpdateDataSource) - s.Bus.AddHandler(s.GetDefaultDataSource) - ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store)) ac.RegisterAttributeScopeResolver(NewIDScopeResolver(store)) diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go index 5fa3ee4a89e..2ac7ad076e1 100644 --- a/pkg/services/login/authinfoservice/database/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -28,18 +28,9 @@ func ProvideAuthInfoStore(sqlStore sqlstore.Store, bus bus.Bus, secretsService s secretsService: secretsService, logger: log.New("login.authinfo.store"), } - store.registerBusHandlers() return store } -func (s *AuthInfoStore) registerBusHandlers() { - s.bus.AddHandler(s.GetExternalUserInfoByLogin) - s.bus.AddHandler(s.GetAuthInfo) - s.bus.AddHandler(s.SetAuthInfo) - s.bus.AddHandler(s.UpdateAuthInfo) - s.bus.AddHandler(s.DeleteAuthInfo) -} - func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { userQuery := models.GetUserByLoginQuery{LoginOrEmail: query.LoginOrEmail} err := s.sqlStore.GetUserByLogin(ctx, &userQuery) diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index db5ccf002cb..a8f4f30837b 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -23,7 +23,6 @@ func ProvideService(sqlStore sqlstore.Store, bus bus.Bus, quotaService *quota.Qu QuotaService: quotaService, AuthInfoService: authInfoService, } - bus.AddHandler(s.UpsertUser) return s } diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 4bace3556f9..924506e5845 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -80,8 +80,6 @@ func Test_teamSync(t *testing.T) { Login: "test_user", } authInfoMock.ExpectedUser = expectedUser - bus.ClearBusHandlers() - t.Cleanup(func() { bus.ClearBusHandlers() }) var actualUser *models.User var actualExternalUser *models.ExternalUserInfo diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 221b3223891..7c86ebb8348 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -45,13 +45,6 @@ func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer, store TempUser store: store, } - ns.Bus.AddHandler(ns.SendResetPasswordEmail) - ns.Bus.AddHandler(ns.ValidateResetPasswordCode) - ns.Bus.AddHandler(ns.SendEmailCommandHandler) - - ns.Bus.AddHandler(ns.SendEmailCommandHandlerSync) - ns.Bus.AddHandler(ns.SendWebhookSync) - ns.Bus.AddEventListener(ns.signUpStartedHandler) ns.Bus.AddEventListener(ns.signUpCompletedHandler) diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index bd07a4dd291..0244c7df558 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -35,7 +35,7 @@ func TestSendEmailSync(t *testing.T) { bus := bus.New() t.Run("When sending emails synchronously", func(t *testing.T) { - _, mailer := createSut(t, bus) + ns, mailer := createSut(t, bus) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ Subject: "subject", @@ -44,8 +44,7 @@ func TestSendEmailSync(t *testing.T) { Template: "welcome_on_signup", }, } - - err := bus.Dispatch(context.Background(), cmd) + err := ns.SendEmailCommandHandlerSync(context.Background(), cmd) require.NoError(t, err) require.NotEmpty(t, mailer.Sent) @@ -55,7 +54,7 @@ func TestSendEmailSync(t *testing.T) { }) t.Run("When using Single Email mode with multiple recipients", func(t *testing.T) { - _, mailer := createSut(t, bus) + ns, mailer := createSut(t, bus) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ Subject: "subject", @@ -65,14 +64,14 @@ func TestSendEmailSync(t *testing.T) { }, } - err := bus.Dispatch(context.Background(), cmd) + err := ns.SendEmailCommandHandlerSync(context.Background(), cmd) require.NoError(t, err) require.Len(t, mailer.Sent, 1) }) t.Run("When using Multi Email mode with multiple recipients", func(t *testing.T) { - _, mailer := createSut(t, bus) + ns, mailer := createSut(t, bus) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ Subject: "subject", @@ -82,14 +81,14 @@ func TestSendEmailSync(t *testing.T) { }, } - err := bus.Dispatch(context.Background(), cmd) + err := ns.SendEmailCommandHandlerSync(context.Background(), cmd) require.NoError(t, err) require.Len(t, mailer.Sent, 3) }) t.Run("When attaching files to emails", func(t *testing.T) { - _, mailer := createSut(t, bus) + ns, mailer := createSut(t, bus) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ Subject: "subject", @@ -105,7 +104,7 @@ func TestSendEmailSync(t *testing.T) { }, } - err := bus.Dispatch(context.Background(), cmd) + err := ns.SendEmailCommandHandlerSync(context.Background(), cmd) require.NoError(t, err) require.NotEmpty(t, mailer.Sent) @@ -119,7 +118,7 @@ func TestSendEmailSync(t *testing.T) { t.Run("When SMTP disabled in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.Enabled = false - _, mailer, err := createSutWithConfig(t, bus, cfg) + ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ @@ -130,7 +129,7 @@ func TestSendEmailSync(t *testing.T) { }, } - err = bus.Dispatch(context.Background(), cmd) + err = ns.SendEmailCommandHandlerSync(context.Background(), cmd) require.ErrorIs(t, err, models.ErrSmtpNotEnabled) require.Empty(t, mailer.Sent) @@ -139,7 +138,7 @@ func TestSendEmailSync(t *testing.T) { t.Run("When invalid content type in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") - _, mailer, err := createSutWithConfig(t, bus, cfg) + ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ @@ -150,14 +149,14 @@ func TestSendEmailSync(t *testing.T) { }, } - err = bus.Dispatch(context.Background(), cmd) + err = ns.SendEmailCommandHandlerSync(context.Background(), cmd) require.Error(t, err) require.Empty(t, mailer.Sent) }) t.Run("When SMTP dialer is disconnected", func(t *testing.T) { - _ = createDisconnectedSut(t, bus) + ns := createDisconnectedSut(t, bus) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ Subject: "subject", @@ -167,7 +166,7 @@ func TestSendEmailSync(t *testing.T) { }, } - err := bus.Dispatch(context.Background(), cmd) + err := ns.SendEmailCommandHandlerSync(context.Background(), cmd) require.Error(t, err) }) @@ -210,7 +209,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When invalid content type in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") - _, mailer, err := createSutWithConfig(t, bus, cfg) + ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommand{ Subject: "subject", @@ -219,14 +218,14 @@ func TestSendEmailAsync(t *testing.T) { Template: "welcome_on_signup", } - err = bus.Dispatch(context.Background(), cmd) + err = ns.SendEmailCommandHandler(context.Background(), cmd) require.Error(t, err) require.Empty(t, mailer.Sent) }) t.Run("When SMTP dialer is disconnected", func(t *testing.T) { - _ = createDisconnectedSut(t, bus) + ns := createDisconnectedSut(t, bus) cmd := &models.SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, @@ -234,7 +233,7 @@ func TestSendEmailAsync(t *testing.T) { Template: "welcome_on_signup", } - err := bus.Dispatch(context.Background(), cmd) + err := ns.SendEmailCommandHandler(context.Background(), cmd) // The async version should not surface connection errors via Bus. It should only log them. require.NoError(t, err) diff --git a/pkg/services/plugindashboards/service/dashboard_updater_test.go b/pkg/services/plugindashboards/service/dashboard_updater_test.go index 5648bb613be..05a0e33630e 100644 --- a/pkg/services/plugindashboards/service/dashboard_updater_test.go +++ b/pkg/services/plugindashboards/service/dashboard_updater_test.go @@ -569,8 +569,6 @@ func scenario(t *testing.T, desc string, input scenarioInput, f func(ctx *scenar sCtx.dashboardService, ) - t.Cleanup(bus.ClearBusHandlers) - t.Run(desc, func(t *testing.T) { f(sCtx) }) diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index d8b3eb2e95a..1656f6ee123 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -21,7 +21,6 @@ func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlstore *sqlstore.SQLStore) }, sqlstore: sqlstore, } - s.Bus.AddHandler(s.SearchHandler) return s } diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 7e79cb6d24c..d4b07cb64db 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -7,23 +7,12 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) // timeNow makes it possible to test usage of time var timeNow = time.Now -func (ss *SQLStore) addAlertQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.HandleAlertsQuery) - bus.AddHandler("sql", ss.GetAlertById) - bus.AddHandler("sql", ss.GetAllAlertQueryHandler) - bus.AddHandler("sql", ss.SetAlertState) - bus.AddHandler("sql", ss.GetAlertStatesForDashboard) - bus.AddHandler("sql", ss.PauseAlert) - bus.AddHandler("sql", ss.PauseAllAlerts) -} - func (ss *SQLStore) GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error { return ss.WithDbSession(ctx, func(sess *DBSession) error { alert := models.Alert{} diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 0d6bd261415..c8c515e81ee 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -8,7 +8,6 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) @@ -79,10 +78,6 @@ func (ss *SQLStore) GetAlertNotifications(ctx context.Context, query *models.Get return getAlertNotificationInternal(ctx, query, newSession(ctx)) } -func (ss *SQLStore) addAlertNotificationUidByIdHandler() { - bus.AddHandler("sql", ss.GetAlertNotificationUidWithId) -} - func (ss *SQLStore) GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error { cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id) diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 6fae4142bab..6d2b7a3d9b7 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -19,13 +18,7 @@ import ( func TestAlertNotificationSQLAccess(t *testing.T) { var sqlStore *SQLStore - setup := func() { - sqlStore = InitTestDB(t) - // Set up bus handlers - bus.AddHandler("deleteAlertNotification", func(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error { - return sqlStore.DeleteAlertNotification(ctx, cmd) - }) - } + setup := func() { sqlStore = InitTestDB(t) } t.Run("Alert notification state", func(t *testing.T) { setup() diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index eb31a56efaf..876d41382d2 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -6,18 +6,9 @@ import ( "xorm.io/xorm" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func (ss *SQLStore) addAPIKeysQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetAPIKeys) - bus.AddHandler("sql", ss.GetApiKeyById) - bus.AddHandler("sql", ss.GetApiKeyByName) - bus.AddHandler("sql", ss.DeleteApiKey) - bus.AddHandler("sql", ss.AddAPIKey) -} - // GetAPIKeys queries the database based // on input on GetApiKeysQuery func (ss *SQLStore) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuery) error { diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c53c577a886..4017e8d8872 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -6,7 +6,6 @@ import ( "github.com/prometheus/client_golang/prometheus" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" @@ -26,18 +25,6 @@ func init() { prometheus.MustRegister(shadowSearchCounter) } -func (ss *SQLStore) addDashboardQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetDashboard) - bus.AddHandler("sql", ss.GetDashboardUIDById) - bus.AddHandler("sql", ss.GetDashboardTags) - bus.AddHandler("sql", ss.SearchDashboards) - bus.AddHandler("sql", ss.GetDashboards) - bus.AddHandler("sql", ss.HasEditPermissionInFolders) - bus.AddHandler("sql", ss.GetDashboardPermissionsForUser) - bus.AddHandler("sql", ss.GetDashboardSlugById) - bus.AddHandler("sql", ss.HasAdminPermissionInFolders) -} - var generateNewUid func() string = util.GenerateShortUID func (ss *SQLStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error { diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index bcc04b2a17a..df4a4fcf129 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -3,14 +3,9 @@ package sqlstore import ( "context" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func (ss *SQLStore) addDashboardACLQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetDashboardAclInfoList) -} - // GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three // different places. // 1) Permissions for the dashboard diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 3c6c6832ef8..e8bd87b6461 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -4,17 +4,10 @@ import ( "context" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) -func (ss *SQLStore) addDashboardVersionQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetDashboardVersion) - bus.AddHandler("sql", ss.GetDashboardVersions) - bus.AddHandler("sql", ss.DeleteExpiredVersions) -} - // GetDashboardVersion gets the dashboard version for the given dashboard ID and version number. func (ss *SQLStore) GetDashboardVersion(ctx context.Context, query *models.GetDashboardVersionQuery) error { return ss.WithDbSession(ctx, func(sess *DBSession) error { diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go index fbbf85d786b..dd93a249552 100644 --- a/pkg/services/sqlstore/login_attempt.go +++ b/pkg/services/sqlstore/login_attempt.go @@ -5,18 +5,11 @@ import ( "strconv" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) var getTimeNow = time.Now -func (ss *SQLStore) addLoginAttemptQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.CreateLoginAttempt) - bus.AddHandler("sql", ss.DeleteOldLoginAttempts) - bus.AddHandler("sql", ss.GetUserLoginAttemptCount) -} - func (ss *SQLStore) CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error { return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { loginAttempt := models.LoginAttempt{ diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go index 6cac1cea15f..1cdd72a938a 100644 --- a/pkg/services/sqlstore/org.go +++ b/pkg/services/sqlstore/org.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -15,16 +14,6 @@ import ( // MainOrgName is the name of the main organization. const MainOrgName = "Main Org." -func (ss *SQLStore) addOrgQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetOrgById) - bus.AddHandler("sql", CreateOrg) - bus.AddHandler("sql", ss.UpdateOrg) - bus.AddHandler("sql", ss.UpdateOrgAddress) - bus.AddHandler("sql", ss.GetOrgByNameHandler) - bus.AddHandler("sql", ss.SearchOrgs) - bus.AddHandler("sql", ss.DeleteOrg) -} - func (ss *SQLStore) SearchOrgs(ctx context.Context, query *models.SearchOrgsQuery) error { return ss.WithDbSession(ctx, func(dbSession *DBSession) error { query.Result = make([]*models.OrgDTO, 0) diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 24f1a9e8119..60d7e5ca8f7 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -6,20 +6,12 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" ) -func (ss *SQLStore) addOrgUsersQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.AddOrgUser) - bus.AddHandler("sql", ss.RemoveOrgUser) - bus.AddHandler("sql", ss.GetOrgUsers) - bus.AddHandler("sql", ss.UpdateOrgUser) -} - func (ss *SQLStore) AddOrgUser(ctx context.Context, cmd *models.AddOrgUserCommand) error { return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { // check if user exists diff --git a/pkg/services/sqlstore/playlist.go b/pkg/services/sqlstore/playlist.go index b0d8e96652d..49f3aa311c8 100644 --- a/pkg/services/sqlstore/playlist.go +++ b/pkg/services/sqlstore/playlist.go @@ -3,19 +3,9 @@ package sqlstore import ( "context" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func (ss *SQLStore) addPlaylistQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.CreatePlaylist) - bus.AddHandler("sql", ss.UpdatePlaylist) - bus.AddHandler("sql", ss.DeletePlaylist) - bus.AddHandler("sql", ss.SearchPlaylists) - bus.AddHandler("sql", ss.GetPlaylist) - bus.AddHandler("sql", ss.GetPlaylistItem) -} - func (ss *SQLStore) CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error { return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { playlist := models.Playlist{ diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 1fccba8adc3..da3a5388ef1 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -5,17 +5,9 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func (ss *SQLStore) addPreferencesQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetPreferences) - bus.AddHandler("sql", ss.GetPreferencesWithDefaults) - bus.AddHandler("sql", ss.SavePreferences) - bus.AddHandler("sql", ss.PatchPreferences) -} - func (ss *SQLStore) GetPreferencesWithDefaults(ctx context.Context, query *models.GetPreferencesWithDefaultsQuery) error { return ss.WithDbSession(ctx, func(dbSession *DBSession) error { params := make([]interface{}, 0) diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index abc0c39b98c..3ed4428fa71 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -15,16 +14,6 @@ const ( dashboardTarget = "dashboard" ) -func (ss *SQLStore) addQuotaQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetOrgQuotaByTarget) - bus.AddHandler("sql", ss.GetOrgQuotas) - bus.AddHandler("sql", ss.UpdateOrgQuota) - bus.AddHandler("sql", ss.GetUserQuotaByTarget) - bus.AddHandler("sql", ss.GetUserQuotas) - bus.AddHandler("sql", ss.UpdateUserQuota) - bus.AddHandler("sql", ss.GetGlobalQuotaByTarget) -} - type targetCount struct { Count int64 } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 792ce846329..ca6ec8d6135 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -111,28 +111,6 @@ func newSQLStore(cfg *setting.Cfg, cacheService *localcache.CacheService, b bus. // Init repo instances annotations.SetRepository(&SQLAnnotationRepo{sql: ss}) annotations.SetAnnotationCleaner(&AnnotationCleanupService{batchSize: ss.Cfg.AnnotationCleanupJobBatchSize, log: log.New("annotationcleaner")}) - ss.Bus.SetTransactionManager(ss) - - // Register handlers - ss.addStatsQueryAndCommandHandlers() - ss.addUserQueryAndCommandHandlers() - ss.addAlertNotificationUidByIdHandler() - ss.addPreferencesQueryAndCommandHandlers() - ss.addDashboardQueryAndCommandHandlers() - ss.addDashboardACLQueryAndCommandHandlers() - ss.addQuotaQueryAndCommandHandlers() - ss.addOrgUsersQueryAndCommandHandlers() - ss.addStarQueryAndCommandHandlers() - ss.addAlertQueryAndCommandHandlers() - ss.addTempUserQueryAndCommandHandlers() - ss.addDashboardVersionQueryAndCommandHandlers() - ss.addAPIKeysQueryAndCommandHandlers() - ss.addPlaylistQueryAndCommandHandlers() - ss.addLoginAttemptQueryAndCommandHandlers() - ss.addTeamQueryAndCommandHandlers() - ss.addOrgQueryAndCommandHandlers() - - bus.AddHandler("sql", ss.GetDBHealthQuery) // if err := ss.Reset(); err != nil { // return nil, err diff --git a/pkg/services/sqlstore/star.go b/pkg/services/sqlstore/star.go index fef91f9e9b4..847fdfec6e0 100644 --- a/pkg/services/sqlstore/star.go +++ b/pkg/services/sqlstore/star.go @@ -3,17 +3,9 @@ package sqlstore import ( "context" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func (ss *SQLStore) addStarQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.StarDashboard) - bus.AddHandler("sql", ss.UnstarDashboard) - bus.AddHandler("sql", ss.GetUserStars) - bus.AddHandler("sql", ss.IsStarredByUserCtx) -} - func (ss *SQLStore) IsStarredByUserCtx(ctx context.Context, query *models.IsStarredByUserQuery) error { return ss.WithDbSession(ctx, func(sess *DBSession) error { rawSQL := "SELECT 1 from star where user_id=? and dashboard_id=?" diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 43777190fe3..e1e1500994e 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -5,20 +5,10 @@ import ( "strconv" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) -func (ss *SQLStore) addStatsQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.GetAdminStats) - bus.AddHandler("sql", ss.GetSystemUserCountStats) - bus.AddHandler("sql", ss.GetAlertNotifiersUsageStats) - bus.AddHandler("sql", ss.GetDataSourceAccessStats) - bus.AddHandler("sql", ss.GetDataSourceStats) - bus.AddHandler("sql", ss.GetSystemStats) -} - const activeUserTimeLimit = time.Hour * 24 * 30 const dailyActiveUserTimeLimit = time.Hour * 24 diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index abedc2110b3..6e266fed697 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -7,25 +7,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" ) -func (ss *SQLStore) addTeamQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.UpdateTeam) - bus.AddHandler("sql", ss.DeleteTeam) - bus.AddHandler("sql", ss.SearchTeams) - bus.AddHandler("sql", ss.GetTeamById) - bus.AddHandler("sql", ss.GetTeamsByUser) - - bus.AddHandler("sql", ss.UpdateTeamMember) - bus.AddHandler("sql", ss.RemoveTeamMember) - bus.AddHandler("sql", ss.GetTeamMembers) - bus.AddHandler("sql", IsAdminOfTeams) -} - type TeamStore interface { UpdateTeam(ctx context.Context, cmd *models.UpdateTeamCommand) error DeleteTeam(ctx context.Context, cmd *models.DeleteTeamCommand) error diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go index 97f32f77a41..80115ade956 100644 --- a/pkg/services/sqlstore/temp_user.go +++ b/pkg/services/sqlstore/temp_user.go @@ -4,19 +4,9 @@ import ( "context" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func (ss *SQLStore) addTempUserQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.CreateTempUser) - bus.AddHandler("sql", ss.GetTempUsersQuery) - bus.AddHandler("sql", ss.UpdateTempUserStatus) - bus.AddHandler("sql", ss.GetTempUserByCode) - bus.AddHandler("sql", ss.UpdateTempUserWithEmailSent) - bus.AddHandler("sql", ss.ExpireOldUserInvites) -} - func (ss *SQLStore) UpdateTempUserStatus(ctx context.Context, cmd *models.UpdateTempUserStatusCommand) error { return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { var rawSQL = "UPDATE temp_user SET status=? WHERE code=?" diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index c9ce4d9b098..b159dcd6bf8 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -8,7 +8,6 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -16,25 +15,6 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func (ss *SQLStore) addUserQueryAndCommandHandlers() { - ss.Bus.AddHandler(ss.GetSignedInUserWithCacheCtx) - - bus.AddHandler("sql", ss.GetUserById) - bus.AddHandler("sql", ss.UpdateUser) - bus.AddHandler("sql", ss.ChangeUserPassword) - bus.AddHandler("sql", ss.GetUserByLogin) - bus.AddHandler("sql", ss.GetUserByEmail) - bus.AddHandler("sql", ss.SetUsingOrg) - bus.AddHandler("sql", ss.UpdateUserLastSeenAt) - bus.AddHandler("sql", ss.GetUserProfile) - bus.AddHandler("sql", ss.SearchUsers) - bus.AddHandler("sql", ss.GetUserOrgList) - bus.AddHandler("sql", ss.DisableUser) - bus.AddHandler("sql", ss.BatchDisableUsers) - bus.AddHandler("sql", ss.DeleteUser) - bus.AddHandler("sql", ss.SetUserHelpFlag) -} - func getOrgIdForNewUser(sess *DBSession, cmd models.CreateUserCommand) (int64, error) { if cmd.SkipOrgSetup { return -1, nil From 7e844064a4c216d7468fbbdf122c05a7d97ffbc0 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 8 Apr 2022 17:15:19 +0200 Subject: [PATCH 028/156] Alerting: fix collapsable toggle text regression (#47517) --- .../components/CollapseToggle.test.tsx | 29 +++++++++++++++++++ .../unified/components/CollapseToggle.tsx | 4 ++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 public/app/features/alerting/unified/components/CollapseToggle.test.tsx diff --git a/public/app/features/alerting/unified/components/CollapseToggle.test.tsx b/public/app/features/alerting/unified/components/CollapseToggle.test.tsx new file mode 100644 index 00000000000..384da5879fd --- /dev/null +++ b/public/app/features/alerting/unified/components/CollapseToggle.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; +import { noop } from 'lodash'; +import React from 'react'; +import { CollapseToggle } from './CollapseToggle'; + +describe('TestToggle', () => { + it('should render text', () => { + render(); + expect(screen.getByRole('button')).toHaveTextContent('Hello, world'); + }); + + it('should respect isCollapsed', () => { + const { rerender } = render(); + expect(screen.getByRole('button', { expanded: true })).toBeInTheDocument(); + + rerender(); + expect(screen.getByRole('button', { expanded: false })).toBeInTheDocument(); + }); + + it('should call onToggle', () => { + const onToggle = jest.fn(); + render(); + screen.getByRole('button').click(); + + expect(onToggle).toHaveBeenCalledWith(false); + // it should also not have any impact on the actual expanded state since the component does not track its own state + expect(screen.getByRole('button', { expanded: false })).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/alerting/unified/components/CollapseToggle.tsx b/public/app/features/alerting/unified/components/CollapseToggle.tsx index c63256e077f..3e670a396be 100644 --- a/public/app/features/alerting/unified/components/CollapseToggle.tsx +++ b/public/app/features/alerting/unified/components/CollapseToggle.tsx @@ -34,7 +34,9 @@ export const CollapseToggle: FC = ({ icon={isCollapsed ? 'angle-right' : 'angle-down'} onClick={() => onToggle(!isCollapsed)} {...restOfProps} - /> + > + {text} + ); }; From 883ce5ad307a991492c51e2e470c7490b4f68a0f Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 8 Apr 2022 16:23:03 +0100 Subject: [PATCH 029/156] Service accounts: UI polish improvements (#47461) * ui polishes * change icon --- pkg/api/index.go | 2 +- ...al.tsx => CreateServiceAccountTokenModal.tsx} | 5 ++++- .../serviceaccounts/ServiceAccountPage.tsx | 2 +- .../serviceaccounts/ServiceAccountProfile.tsx | 16 ++++++++++++---- .../serviceaccounts/ServiceAccountsListPage.tsx | 3 ++- 5 files changed, 20 insertions(+), 8 deletions(-) rename public/app/features/serviceaccounts/{CreateTokenModal.tsx => CreateServiceAccountTokenModal.tsx} (96%) diff --git a/pkg/api/index.go b/pkg/api/index.go index 20c21a3c7c6..d4e0bc8ce3d 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -307,7 +307,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto Id: "serviceaccounts", Description: "Manage service accounts", // TODO: change icon to "key-skeleton-alt" when it's available - Icon: "key-skeleton-alt", + Icon: "keyhole-circle", Url: hs.Cfg.AppSubURL + "/org/serviceaccounts", }) } diff --git a/public/app/features/serviceaccounts/CreateTokenModal.tsx b/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx similarity index 96% rename from public/app/features/serviceaccounts/CreateTokenModal.tsx rename to public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx index 4cf8338a0d1..996ee89962e 100644 --- a/public/app/features/serviceaccounts/CreateTokenModal.tsx +++ b/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx @@ -64,8 +64,11 @@ export const CreateTokenModal = ({ isOpen, token, onCreateToken, onClose }: Crea
{ diff --git a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx index 2c94e38b171..408d4346d25 100644 --- a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx @@ -66,10 +66,18 @@ export function ServiceAccountProfile({ return ( <> -

Information

- -