Merge branch 'mmandrus/refactor-secrets-plugin-proto-file' into smp-error-handling

This commit is contained in:
Michael Mandrus
2022-06-13 13:30:28 -04:00
245 changed files with 8965 additions and 3924 deletions
-6
View File
@@ -110,9 +110,6 @@ exports[`no enzyme tests`] = {
"public/app/features/alerting/TestRuleResult.test.tsx:2358420489": [
[0, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/api-keys/ApiKeysAddedModal.test.tsx:3246264379": [
[0, 20, 13, "RegExp match", "2409514259"]
],
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:2357087833": [
[0, 35, 13, "RegExp match", "2409514259"]
],
@@ -146,9 +143,6 @@ exports[`no enzyme tests`] = {
"public/app/features/org/OrgDetailsPage.test.tsx:3835042085": [
[0, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/teams/TeamSettings.test.tsx:2043271249": [
[0, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/users/UsersListPage.test.tsx:2518052139": [
[0, 19, 13, "RegExp match", "2409514259"]
],
+33 -34
View File
@@ -141,7 +141,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-backend
- commands:
- ./bin/grabpl test-backend --edition oss
- go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -812,7 +812,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-backend
- commands:
- ./bin/grabpl test-backend --edition oss
- go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -912,7 +912,6 @@ steps:
params:
- SOURCE_BUILD_NUMBER=${DRONE_COMMIT}
- SOURCE_COMMIT=${DRONE_COMMIT}
- OSS_PULL_REQUEST=${DRONE_PULL_REQUEST}
repositories:
- grafana/grafana-enterprise@main
server: https://drone.grafana.net
@@ -1505,7 +1504,7 @@ type: docker
---
depends_on: []
kind: pipeline
name: oss-build-e2e-publish-release
name: release-oss-build-e2e-publish
node:
type: no-parallel
platform:
@@ -1802,7 +1801,7 @@ volumes:
---
depends_on: []
kind: pipeline
name: oss-test-release
name: release-oss-test
node:
type: no-parallel
platform:
@@ -1884,7 +1883,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-frontend
- commands:
- ./bin/grabpl test-backend --edition oss
- go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -1920,7 +1919,7 @@ volumes:
---
depends_on: []
kind: pipeline
name: oss-integration-tests-release
name: release-oss-integration-tests
node:
type: no-parallel
platform:
@@ -2010,11 +2009,11 @@ volumes:
medium: memory
---
depends_on:
- oss-build-e2e-publish-release
- oss-test-release
- oss-integration-tests-release
- release-oss-build-e2e-publish
- release-oss-test
- release-oss-integration-tests
kind: pipeline
name: oss-windows-release
name: release-oss-windows
platform:
arch: amd64
os: windows
@@ -2076,7 +2075,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-build-e2e-publish-release
name: release-enterprise-build-e2e-publish
node:
type: no-parallel
platform:
@@ -2424,7 +2423,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-test-release
name: release-enterprise-test
node:
type: no-parallel
platform:
@@ -2587,7 +2586,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-integration-tests-release
name: release-enterprise-integration-tests
node:
type: no-parallel
platform:
@@ -2728,13 +2727,13 @@ volumes:
clone:
disable: true
depends_on:
- enterprise-build-e2e-publish-release
- enterprise-test-release
- enterprise-integration-tests-release
- release-enterprise-build-e2e-publish
- release-enterprise-test
- release-enterprise-integration-tests
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-windows-release
name: release-enterprise-windows
platform:
arch: amd64
os: windows
@@ -3317,7 +3316,7 @@ volumes:
---
depends_on: []
kind: pipeline
name: oss-build-e2e-publish-release-branch
name: release-branch-oss-build-e2e-publish
node:
type: no-parallel
platform:
@@ -3584,7 +3583,7 @@ volumes:
---
depends_on: []
kind: pipeline
name: oss-test-release-branch
name: release-branch-oss-test
node:
type: no-parallel
platform:
@@ -3666,7 +3665,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-frontend
- commands:
- ./bin/grabpl test-backend --edition oss
- go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -3696,7 +3695,7 @@ volumes:
---
depends_on: []
kind: pipeline
name: oss-integration-tests-release-branch
name: release-branch-oss-integration-tests
node:
type: no-parallel
platform:
@@ -3780,11 +3779,11 @@ volumes:
medium: memory
---
depends_on:
- oss-build-e2e-publish-release-branch
- oss-test-release-branch
- oss-integration-tests-release-branch
- release-branch-oss-build-e2e-publish
- release-branch-oss-test
- release-branch-oss-integration-tests
kind: pipeline
name: oss-windows-release-branch
name: release-branch-oss-windows
platform:
arch: amd64
os: windows
@@ -3835,7 +3834,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-build-e2e-publish-release-branch
name: release-branch-enterprise-build-e2e-publish
node:
type: no-parallel
platform:
@@ -4181,7 +4180,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-test-release-branch
name: release-branch-enterprise-test
node:
type: no-parallel
platform:
@@ -4335,7 +4334,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-integration-tests-release-branch
name: release-branch-enterprise-integration-tests
node:
type: no-parallel
platform:
@@ -4467,13 +4466,13 @@ volumes:
clone:
disable: true
depends_on:
- enterprise-build-e2e-publish-release-branch
- enterprise-test-release-branch
- enterprise-integration-tests-release-branch
- release-branch-enterprise-build-e2e-publish
- release-branch-enterprise-test
- release-branch-enterprise-integration-tests
image_pull_secrets:
- dockerconfigjson
kind: pipeline
name: enterprise-windows-release-branch
name: release-branch-enterprise-windows
platform:
arch: amd64
os: windows
@@ -4683,6 +4682,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: 378147a306d077b0566f5353c9e5c236c40dbb8d81392091a1346ed275b86496
hmac: 5f90961f151491580770a354e6f60ceddf35557f1094ec0ee94369fe52285697
...
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
repository: "grafana/grafana-github-actions"
path: ./actions
ref: main
- uses: actions/setup-node@v3.2.0
- uses: actions/setup-node@v3.3.0
with:
node-version: '16'
- name: Install Actions
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,4 +50,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
@@ -104,7 +104,7 @@ jobs:
- name: Post to Slack
id: slack
if: ${{ steps.levitate-run.outputs.exit_code == 1 && steps.does-label-exist.outputs.result == 0 }}
uses: slackapi/slack-github-action@v1.18.0
uses: slackapi/slack-github-action@v1.19.0
with:
payload: |
{
+2 -2
View File
@@ -21,9 +21,9 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: "go"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
@@ -23,9 +23,9 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: "javascript"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
@@ -21,9 +21,9 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: "python"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- 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@v3.2.0
uses: actions/setup-node@v3.3.0
id: generate-docs
with:
node-version: '16'
+1 -1
View File
@@ -72,7 +72,7 @@ Migration should just be a matter of going from
to
````tsx
```tsx
<ClipboardButton
{/* other props... */}
onClipboardCopy={(copiedText) => {
+1 -1
View File
@@ -8,7 +8,7 @@ weight: 114
# Grafana Alerting
Grafana alerts allow you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to:
Grafana Alerting allows you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to:
- Create and manage Grafana alerts
- Create and manage Grafana Mimir and Loki managed alerts
@@ -1,27 +1,33 @@
---
aliases:
- /docs/grafana/latest/alerting/migrating-alerts/
description: Migrate Grafana alerts
title: Migrate to Grafana Alerting
weight: 113
description: Upgrade Grafana alerts
title: Upgrade to Grafana Alerting
weight: 101
---
# Migrate to Grafana Alerting
# Upgrade to Grafana Alerting
Grafana Alerting is the default for new Cloud, Enterprise, and OSS installations. The new installations will only show the Grafana Alerting icon in the left navigation panel.
Grafana Alerting is enabled by default for new installations or existing installations whether or not legacy alerting is configured.
Existing installations that upgrade to v9.0 will have Grafana Alerting enabled by default.
> **Note**: We recommend that Grafana Enterprise customers with more than a dozen Grafana dashboard alert rules do not upgrade and remain on legacy alerting for now by [opting out]({{< relref "opt-out/" >}}). If you do want to upgrade to Grafana Alerting, contact customer support.
| Grafana instance upgraded to v 90 | |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cloud | Existing Cloud installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting plugin icon and the new Grafana Alerting icon. During upgrade, existing alerts from the Cloud alerting plugin are migrated to Grafana Alerting. Once migration is complete, you can access aman manage the older alerts from the new alerting Grafana Alerting icon in the navigation panel. The (older) Cloud alerting plugin is uninstalled from your cloud instance. Contact customer support if you **do not wish** to migrate to Grafana Alerting for your Cloud stack. If you choose to use legacy alerting, use the You will see the new Grafana Alerting icon as well as the old Cloud alerting plugin in the left navigation panel. |
| Enterprise | Existing Enterprise instances using legacy alerting will have both the old (marked as legacy) and the new alerting icons in the navigation panel. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
| OSS | Existing OSS installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting icon (marked as legacy) and the new Grafana Alerting icon. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration.
During migration from legacy alerting to unified alerting, the legacy alerts are updated to the new alerts type, as a result, the user does not lose alerts or alerting data. However, if a user rolls back to legacy alerting after having migrated to unified alerting, they will only get the legacy alerts they had right before migration.
Likewise, existing installations that use legacy alerting will be automatically upgraded to Grafana Alerting unless you have [opted out]({{< relref "opt-out/" >}}) of Grafana Alerting before migration takes place. During the upgrade, legacy alerts are migrated to the new alerts type and no alerts or alerting data are lost.
## Roll back to legacy alerting
Once the upgrade has taken place, you still have the option to [roll back]({{< relref "roll-back/" >}}) to legacy alerting. However, we do not recommend choosing this option. If you do choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place. All new alerts and changes made exclusively in Grafana Alerting will be deleted.
Although we encourage you to use Grafana Alerting, roll back to legacy alerting is supported in Grafana 9. Rolling back can result in data loss (you will loose all alerts that you created using Grafana Alerting). This is applicable to the fresh installation as well as upgraded setups.
> **Note**: Cloud customers, who do not want to upgrade to Grafana Alerting, should contact customer support.
If you have opted out or rolled back, you can always choose to [opt in]({{< relref "opt-in/" >}}) to Grafana Alerting at a later point in time.
The following table provides details on the upgrade for Cloud, Enterprise, and OSS installations and the new Grafana Alerting UI.
| Grafana instance upgraded to 9.0 | |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Cloud | Existing Cloud installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting plugin icon and the new Grafana Alerting icon. During upgrade, existing alerts from the Cloud alerting plugin are migrated to Grafana Alerting. Once migration is complete, you can access and manage the older alerts from the new alerting Grafana Alerting icon in the navigation panel. The (older) Cloud alerting plugin is uninstalled from your cloud instance. Contact customer support if you **do not wish** to migrate to Grafana Alerting for your Cloud stack. If you choose to use legacy alerting, use the You will see the new Grafana Alerting icon as well as the old Cloud alerting plugin in the left navigation panel. |
| Enterprise | Existing Enterprise instances using legacy alerting will have both the old (marked as legacy) and the new alerting icons in the navigation panel. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
| OSS | Existing OSS installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting icon (marked as legacy) and the new Grafana Alerting icon. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
> **Note:** Legacy alerting will be deprecated in a future release (v10).
@@ -0,0 +1,23 @@
---
aliases:
- /docs/grafana/latest/alerting/migrating-alerts/disable-alerting/
- /docs/grafana/latest/alerting/disable-alerting/
- /docs/grafana/latest/alerting/unified-alerting/disable-alerting/
description: Disable alerting in Grafana
title: Disable alerting in Grafana
weight: 105
---
# Disable alerting in Grafana
To disable alerting in Grafana entirely (including both legacy and Grafana Alerting), enter the following in your configuration:
```
[alerting]
enabled = false
[unified_alerting]
enabled = false
```
If at any time you want to turn alerting back on, you can opt in.
@@ -4,13 +4,14 @@ aliases:
- /docs/grafana/latest/alerting/migrating-legacy-alerts/
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
description: Migrate legacy dashboard alerts
title: Migrating legacy dashboard alerts
weight: 114
title: Differences and limitations
weight: 106
---
# Migrating legacy dashboard alerts
# Differences and limitations
When Grafana Alerting is enabled or Grafana is upgraded to the latest version, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts. This topic explains how legacy dashboard alerts are migrated and some limitations.
When Grafana Alerting is enabled or upgraded to Grafana 9.0 or later, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts.
This topic explains how legacy dashboard alerts are migrated and some limitations of the migration.
> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Cloud stack.
@@ -24,5 +25,7 @@ Read and write access to legacy dashboard alerts and Grafana alerts are governed
Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route.
## Limitations
Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts.
Silences (expiring after one year) are created for all paused dashboard alerts.
@@ -0,0 +1,25 @@
---
aliases:
- /docs/grafana/latest/alerting/migrating-alerts/opt-in/
- /docs/grafana/latest/alerting/opt-in/
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
description: Opt in to Grafana Alerting
title: Opt in to Grafana Alerting
weight: 104
---
# Opt in to Grafana Alerting
If you have previously disabled alerting in Grafana, or opted out of Grafana Alerting and have decided that you would now like to use Grafana Alerting, you can choose to opt in at any time.
If you have been using legacy alerting up until now your existing alerts will be migrated to the new alerts type and no alerts or alerting data are lost. Even if you choose to opt in to Grafana Alerting, you can roll back to legacy alerting at any time.
To opt in to Grafana Alerting, enter the following in your configuration:
```
[alerting]
enabled = false
[unified_alerting]
enabled = true
```
@@ -3,41 +3,47 @@ aliases:
- /docs/grafana/latest/alerting/migrating-alerts/opt-out/
- /docs/grafana/latest/alerting/opt-in/
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
description: Disable Grafana alerts
title: Opt-out of Grafana Alerting
weight: 113
description: Opt out of Grafana Alerting
title: Opt out of Grafana Alerting
weight: 102
---
# Opt-out to Grafana Alerting in OSS
# Opt out of Grafana Alerting
This topic discusses how to disable Grafana Alerting and migrate to legacy dashboard alerting. It also provides guidance on how to enable Grafana Alerting once you are ready to migrate to Grafana Alerting.
If you have an existing installation, you can opt out of alerting in its entirety or opt out of Grafana Alerting in favor of using legacy alerting.
Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration. To keep alerting disabled:
1. Go to your custom configuration file ($WORKING_DIR/conf/custom.ini).
1. Enter the following in your configuration:
```
[alerting]
enabled = false
[unified_alerting]
enabled = false
```
3. Restart Grafana for the configuration changes to take effect.
If at any time you want to turn alerting back on, you can do so.
Existing installations that use legacy alerting will automatically be upgraded to Grafana Alerting unless you have opted-out of Grafana Alerting before migration takes place. During the upgrade, legacy alerts are migrated to the new alerts type and no alerts or alerting data are lost. To keep using legacy alerting and disable Grafana Alerting:
1. Go to your custom configuration file ($WORKING_DIR/conf/custom.ini).
2. Enter the following in your configuration:
```
[alerting]
enabled = false
[unified_alerting]
enabled = true
```
Installations that have been migrated to Grafana Alerting can [roll back]({{< relref "roll-back/" >}}) to legacy alerting at any time.
> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Grafana Cloud stack.
## Before you begin
We recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5.
## Opt-out of Grafana alerts
To opt-out of Grafana alerts and roll back to legacy dashboard alerting:
1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana Alerting]({{< relref "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section.
1. Set the `enabled` property to `false`.
1. For [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#alerting" >}}), set the `enabled` flag to `true`.
1. Restart Grafana for the configuration changes to take effect.
> **Note:** Rolling back from Grafana to legacy alerting can result in data loss. This is applicable to the fresh installation as well as upgraded setups.
## Opt-in to Grafana Alerting
When you are ready to make the switch, the following procedure will help you migrate to Grafana Alerting.
To opt-in to Grafana alerts:
1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section.
1. Set the `enabled` property to `true`.
1. Next, for [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#alerting" >}}), set the `enabled` flag to `false`.
1. Restart Grafana for the configuration changes to take effect.
> **Note:** The `ngalert` toggle previously used to enable or disable Grafana Alerting is no longer available.
The `ngalert` toggle previously used to enable or disable Grafana Alerting is no longer available.
@@ -0,0 +1,29 @@
---
aliases:
- /docs/grafana/latest/alerting/migrating-alerts/roll-back/
- /docs/grafana/latest/alerting/opt-in/
- /docs/grafana/latest/alerting/unified-alerting/roll-back/
description: Roll back to legacy alerting
title: Roll back to legacy alerting
weight: 103
---
# Roll back to legacy alerting
Once the upgrade has taken place, you still have the option to roll back to legacy alerting. If you choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place.
All new alerts and changes made exclusively in Grafana Alerting will be deleted.
To roll back to legacy alerting, enter the following in your configuration:
```
force_migration = true
[alerting]
enabled = true
[unified_alerting]
enabled = false
```
> **Note**: We do not recommend this option. If you choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place. All new alerts and changes made exclusively in Grafana Alerting will be deleted.
+1 -1
View File
@@ -2,7 +2,7 @@
title = "Performance considerations"
description = "Understanding alerting performance"
keywords = ["grafana", "alerting", "performance"]
weight = 100
weight = 555
+++
# Alerting performance considerations
+3 -3
View File
@@ -22,11 +22,11 @@ Folders are a way to organize and group dashboards - very useful if you have a l
## How To Create A Folder
- Create a folder by using the Create Folder link in the side menu (under the create menu (+ icon))
- Use the create Folder button on the Manage Dashboards page.
- Create a folder by using the **New folder** link in the side menu (under the **Dashboards** menu)
- Use the **New folder** button on the **Browse dashboards** page.
- When saving a dashboard, you can either choose a folder for the dashboard to be saved in or create a new folder
On the Create Folder page, fill in a unique name for the folder and then click Create.
On the New folder page, fill in a unique name for the folder and then click Create.
## Manage Dashboards
+1 -1
View File
@@ -34,7 +34,7 @@ the dashboard, and will also be added as a required input when the dashboard is
## Import dashboard
To import a dashboard click the + icon in the side menu, and then click **Import**.
To import a dashboard click **Import** under the **Dashboards** icon in the side menu.
{{< figure src="/static/img/docs/v70/import_step1.png" max-width="700px" >}}
+7 -2
View File
@@ -22,8 +22,7 @@ dashboards, creating users, and updating data sources.
## HTTP APIs
- [Admin API]({{< relref "admin/" >}})
- [Alerting Notification Channels API]({{< relref "alerting_notification_channels/" >}})
- [Alerting API]({{< relref "alerting/" >}})
- [Alerting Provisioning API]({{< relref "alerting_provisioning/" >}})
- [Annotations API]({{< relref "annotations/" >}})
- [Authentication API]({{< relref "auth/" >}})
- [Dashboard API]({{< relref "dashboard/" >}})
@@ -39,10 +38,16 @@ dashboards, creating users, and updating data sources.
- [Playlists API]({{< relref "playlist/" >}})
- [Preferences API]({{< relref "preferences/" >}})
- [Short URL API]({{< relref "short_url/" >}})
- [Query history API]({{< relref "query_history/" >}})
- [Snapshot API]({{< relref "snapshot/" >}})
- [Team API]({{< relref "team/" >}})
- [User API]({{< relref "user/" >}})
## Deprecated HTTP APIs
- [Alerting Notification Channels API]({{< relref "alerting_notification_channels/" >}})
- [Alerting API]({{< relref "alerting/" >}})
## Grafana Enterprise HTTP APIs
Grafana Enterprise includes all of the Grafana OSS APIs as well as those that follow:
+1 -1
View File
@@ -15,7 +15,7 @@ title: 'Alerting HTTP API '
# Alerting API
> **Note:** This topic is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/latest/alerting/old-alerting/) only.
> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/" >}}) only.
You can find Grafana Alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana Alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts.
@@ -185,7 +185,7 @@ Content-Type: application/json
## Create notification channel
You can find the full list of [supported notifiers](https://grafana.com/docs/grafana/latest/alerting/old-alerting/notifications/) on the alert notifiers page.
You can find the full list of [supported notifiers]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/notifications/" >}}) on the alert notifiers page.
`POST /api/alert-notifications`
File diff suppressed because it is too large Load Diff
@@ -82,7 +82,8 @@ Status codes:
- **200** – OK
- **400** - Errors (invalid JSON, missing or invalid fields)
- **500** – Unable to add query to the database
- **401** – Unauthorized
- **500** – Internal error
## Query history search
@@ -144,7 +145,8 @@ Content-Type: application/json
Status codes:
- **200** – OK
- **500** – Unable to add query to the database
- **401** – Unauthorized
- **500** – Internal error
## Delete query from Query history by UID
@@ -176,7 +178,8 @@ Content-Type: application/json
Status codes:
- **200** – OK
- **500** – Unable to delete query from the database
- **401** – Unauthorized
- **500** – Internal error
## Update comment of query in Query history by UID
@@ -232,7 +235,8 @@ Status codes:
- **200** – OK
- **400** - Errors (invalid JSON, missing or invalid fields)
- **500** – Unable to update comment of query in the database
- **401** – Unauthorized
- **500** – Internal error
## Star query in Query history
@@ -280,7 +284,8 @@ Content-Type: application/json
Status codes:
- **200** – OK
- **500** – Unable to star query in the database
- **401** – Unauthorized
- **500** – Internal error
## Unstar query in Query history
@@ -328,4 +333,64 @@ Content-Type: application/json
Status codes:
- **200** – OK
- **500** – Unable to unstar query in the database
- **401** – Unauthorized
- **500** – Internal error
## Migrate queries to Query history
`POST /api/query-history/migrate`
Migrates multiple queries in to query history.
**Example request:**
```http
POST /api/query-history HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"queries": [
{
"datasourceUid": "PE1C5CBDA0504A6A3",
"queries": [
{
"refId": "A",
"key": "Q-87fed8e3-62ba-4eb2-8d2a-4129979bb4de-0",
"scenarioId": "csv_content",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
}
}
],
"starred": false,
"createdAt": 1643630762,
"comment": "debugging"
}
]
}
```
JSON body schema:
- **queries** – JSON of query history items.
**Example response:**
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "Query history successfully migrated",
"totalCount": 105,
"starredCount": 10
}
```
Status codes:
- **200** – OK
- **400** - Errors (invalid JSON, missing or invalid fields)
- **401** – Unauthorized
- **500** – Internal error
@@ -46,7 +46,7 @@ interface Field {
}
```
Let's look an example. The table below demonstrates a data frame with two fields, _time_ and _temperature_.
Let's look at an example. The table below demonstrates a data frame with two fields, _time_ and _temperature_.
| time | temperature |
| ------------------- | ----------- |
@@ -47,12 +47,7 @@ You can take advantage of your current authentication provider to manage user an
For example:
1. Map SAML, LDAP, or Oauth roles to Grafana basic roles (viewer, editor, or admin).
2. Use the Grafana Enterprise team sync feature to synchronize teams from your SAML, LDAP, or Oauth provider to Grafana.
- If a team does not exist in Grafana, team sync creates it.
- If a team exists in Grafana, team sync updates its membership.
For more information about team sync, refer to [Team sync]({{< relref "../../setup-grafana/configure-security/configure-team-sync/" >}}).
2. Use the Grafana Enterprise team sync feature to synchronize teams from your SAML, LDAP, or Oauth provider to Grafana. For more information about team sync, refer to [Team sync]({{< relref "../../setup-grafana/configure-security/configure-team-sync/" >}}).
3. Within Grafana, assign RBAC permissions to users and teams.
@@ -34,7 +34,7 @@ To sign in to Grafana for the first time:
To create your first dashboard:
1. Click the **+** icon on the side menu.
1. Click the **New dashboard** item under the **Dashboards** icon in the side menu.
1. On the dashboard, click **Add an empty panel**.
1. In the New dashboard/Edit panel view, go to the **Query** tab.
1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query/" >}}) by selecting `-- Grafana --` from the data source selector. This generates the Random Walk dashboard.
@@ -16,7 +16,7 @@ Add a Grafana library panel to a dashboard when you want to provide visualizatio
**To add a library panel to a dashboard**:
1. Hover over the **+** option on the left menu, then select **Create** from the drop-down options.
1. Hover over the **Dashboards** option on the left menu, then select **New dashboard** from the drop-down options.
The **Add** panel dialog opens.
{{< figure src="/static/img/docs/library-panels/add-library-panel-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
@@ -1189,7 +1189,7 @@ For more information about the Grafana alerts, refer to [About Grafana Alerting]
### enabled
Enable the Unified Alerting sub-system and interface. When enabled we'll migrate all of your alert rules and notification channels to the new system. New alert rules will be created and your notification channels will be converted into an Alertmanager configuration. Previous data is preserved to enable backwards compatibility but new data is removed. The default value is `false`.
Enable or disable Grafana Alerting. If enabled, we’ll migrate all your alert rules and notification channels to the new system as alert rules and notification channels you had previously defined will be converted into an Alertmanager configuration. Legacy alerting data is preserved to enable backwards compatibility. If disabled, all your legacy alerting data will be available again, but the data you created using Grafana Alerting will be deleted. Set force_migration=true to avoid deletion of data. The default value is `true`.
Alerting Rules migrated from dashboards and panels will include a link back via the `annotations`.
@@ -412,7 +412,7 @@ The keys you provide should look like:
When the user logs in using SAML and gets presented with "origin not allowed", the user might be issuing the login from an IdP (identity provider) service or the user is behind a reverse proxy. This potentially happens as Grafana's CSRF checks deem the requests to be invalid. For more information [CSRF](https://owasp.org/www-community/attacks/csrf).
To solve this issue, you can configure either the [`csrf_trusted_origins`]({{< relref "../../configure-grafana/enterprise-configuration/#csrf-trusted-origins" >}}) or [`csrf_origin_headers`]({{< relref "../../configure-grafana/enterprise-configuration/#csrf-origin-headers" >}}) option in the SAML configuration.
To solve this issue, you can configure either the [`csrf_trusted_origins`]({{< relref "../../configure-grafana/#csrf-trusted-origins" >}}) or [`csrf_additional_headers`]({{< relref "../../configure-grafana/#csrf_additional_headers" >}}) option in the SAML configuration.
Example of a configuration file:
@@ -421,6 +421,6 @@ Example of a configuration file:
...
[security]
csrf_trusted_origins = https://grafana.example.com
csrf_origin_headers = X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Port
csrf_additional_headers = X-Forwarded-Host
...
```
@@ -31,11 +31,11 @@ Grafana will now persist all long term data in the database. How to configure th
Grafana Alerting provides a new [highly-available model]({{< relref "../alerting/high-availability/" >}}). It also preserves the semantics of legacy dashboard alerting by executing all alerts on every server and by sending notifications only once per alert. Load distribution between servers is not supported at this time.
For instructions on setting up alerting high availability, see [enable alerting high availability](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/).
For instructions on setting up alerting high availability, see [Enable alerting high availability]({{< relref "../alerting/high-availability/enable-alerting-ha/" >}}).
**Legacy dashboard alerts**
Legacy Grafana Alerting supports a limited form of high availability. In this model, [alert notifications](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
Legacy Grafana Alerting supports a limited form of high availability. In this model, [alert notifications]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/notifications/" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
## Grafana Live
@@ -20,7 +20,7 @@ Alerting rules can only query backend data sources with alerting enabled:
## Metrics from the alerting engine
The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../setup-grafana/set-up-grafana-monitoring/" >}}). See also, [View alert rules and their current state]({{< relref "../../alerting/old-alerting/view-alerts/" >}}).
The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../setup-grafana/set-up-grafana-monitoring/" >}}). See also, [View alert rules and their current state]({{< relref "../../alerting/fundamentals/state-and-health/" >}}).
| Metric Name | Type | Description |
| ------------------------------------------- | --------- | ---------------------------------------------------------------------------------------- |
+1 -1
View File
@@ -114,7 +114,7 @@ You can now provide detailed information to alert notification recipients by inj
{{< figure src="/static/img/docs/alerting/alert-notification-template-7-4.png" max-width="700px" caption="Variable support in alert notifications" >}}
For more information, refer to the [alert notification docs](https://grafana.com/docs/grafana/latest/alerting/old-alerting/add-notification-template/).
For more information, refer to the [alert notification docs]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/add-notification-template/" >}}).
### Content security policy support
+16 -16
View File
@@ -102,9 +102,9 @@
"@lingui/macro": "3.13.3",
"@microsoft/api-extractor": "7.25.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.6",
"@react-types/button": "3.4.5",
"@react-types/menu": "3.5.3",
"@react-types/overlays": "3.5.5",
"@react-types/button": "3.5.0",
"@react-types/menu": "3.6.0",
"@react-types/overlays": "3.6.0",
"@react-types/shared": "3.12.0",
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
"@swc/core": "1.2.187",
@@ -181,14 +181,14 @@
"cypress": "9.5.1",
"enzyme": "3.11.0",
"enzyme-to-json": "3.6.2",
"eslint": "8.15.0",
"eslint": "8.17.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "26.2.2",
"eslint-plugin-jsdoc": "39.3.0",
"eslint-plugin-lodash": "7.4.0",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "4.3.0",
"eslint-plugin-react-hooks": "4.5.0",
"eslint-webpack-plugin": "3.1.1",
"expose-loader": "4.0.0",
"file-loader": "6.2.0",
@@ -221,7 +221,7 @@
"postcss-scss": "4.0.4",
"prettier": "2.6.2",
"raw-loader": "4.0.2",
"react-refresh": "0.11.0",
"react-refresh": "0.13.0",
"react-select-event": "5.5.0",
"react-test-renderer": "17.0.2",
"redux-mock-store": "1.5.4",
@@ -269,16 +269,16 @@
"@opentelemetry/exporter-collector": "0.25.0",
"@opentelemetry/semantic-conventions": "1.3.1",
"@popperjs/core": "2.11.5",
"@react-aria/button": "3.4.4",
"@react-aria/dialog": "3.1.9",
"@react-aria/focus": "3.5.5",
"@react-aria/interactions": "3.8.4",
"@react-aria/menu": "3.4.4",
"@react-aria/overlays": "3.8.2",
"@react-aria/utils": "3.12.0",
"@react-stately/collections": "3.3.8",
"@react-stately/menu": "3.2.7",
"@react-stately/tree": "3.2.4",
"@react-aria/button": "3.5.0",
"@react-aria/dialog": "3.2.0",
"@react-aria/focus": "3.6.0",
"@react-aria/interactions": "3.9.0",
"@react-aria/menu": "3.5.0",
"@react-aria/overlays": "3.9.0",
"@react-aria/utils": "3.13.0",
"@react-stately/collections": "3.4.0",
"@react-stately/menu": "3.3.0",
"@react-stately/tree": "3.3.0",
"@reduxjs/toolkit": "1.8.1",
"@sentry/browser": "6.19.7",
"@sentry/types": "6.19.7",
@@ -90,7 +90,7 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
background: colors.mode === 'dark' ? 'rgba(63, 62, 62, 0.45)' : 'rgba(208, 209, 211, 0.24)',
},
sidemenu: {
width: 48,
width: 57,
},
menuTabs: {
height: 41,
@@ -13,17 +13,19 @@ export enum DataFrameType {
DirectoryListing = 'directory-listing',
/**
* First field is X, the rest are bucket values
* First field is X, the rest are ordinal values used as rows in the heatmap
*/
HeatmapBuckets = 'heatmap-buckets',
HeatmapRows = 'heatmap-rows',
/**
* Explicit fields for:
* xMin, yMin, count, ...
*
* All values in the grid exist and have regular spacing
*
* If the y value is actually ordinal, use `meta.custom` to specify the bucket lookup values
*/
HeatmapScanlines = 'heatmap-scanlines',
HeatmapCells = 'heatmap-cells',
/**
* WIP sparse heatmap support
@@ -36,6 +36,7 @@ export interface FeatureToggles {
showFeatureFlagsInUI?: boolean;
publicDashboards?: boolean;
lokiLive?: boolean;
lokiDataframeApi?: boolean;
swaggerUi?: boolean;
featureHighlights?: boolean;
dashboardComments?: boolean;
@@ -55,7 +56,11 @@ export interface FeatureToggles {
traceToMetrics?: boolean;
prometheusStreamingJSONParser?: boolean;
validateDashboardsOnSave?: boolean;
autoMigrateGraphPanels?: boolean;
prometheusWideSeries?: boolean;
canvasPanelNesting?: boolean;
cloudMonitoringExperimentalUI?: boolean;
logRequestsInstrumentedAsUnknown?: boolean;
dataConnectionsConsole?: boolean;
internationalization?: boolean;
}
+7 -6
View File
@@ -39,12 +39,12 @@
"@grafana/slate-react": "0.22.10-grafana",
"@monaco-editor/react": "4.3.1",
"@popperjs/core": "2.11.5",
"@react-aria/button": "3.4.4",
"@react-aria/dialog": "3.1.9",
"@react-aria/focus": "3.5.5",
"@react-aria/menu": "3.4.4",
"@react-aria/overlays": "3.8.2",
"@react-stately/menu": "3.2.7",
"@react-aria/button": "3.5.0",
"@react-aria/dialog": "3.2.0",
"@react-aria/focus": "3.6.0",
"@react-aria/menu": "3.5.0",
"@react-aria/overlays": "3.9.0",
"@react-stately/menu": "3.3.0",
"@sentry/browser": "6.19.7",
"ansicolor": "1.1.100",
"calculate-size": "1.1.1",
@@ -97,6 +97,7 @@
"@babel/core": "7.18.2",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mdx-js/react": "1.6.22",
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "22.0.0",
"@rollup/plugin-node-resolve": "13.3.0",
"@storybook/addon-a11y": "6.4.21",
+4
View File
@@ -1,3 +1,4 @@
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import svg from 'rollup-plugin-svg-import';
@@ -39,6 +40,9 @@ const buildCjsPackage = ({ env }) => {
'@emotion/css',
],
plugins: [
// rc-time-picker has a transitive dependency on component-indexof which
// when bundled via `component-classes` imports a nonexistent `indexof` module.
alias({ entries: [{ find: 'indexof', replacement: 'component-indexof' }] }),
commonjs({
include: /node_modules/,
ignoreTryCatch: false,
@@ -82,6 +82,7 @@ export class UnThemedLogMessageAnsi extends PureComponent<Props, State> {
return chunks.map((chunk, index) => {
const chunkText = this.props.highlight?.searchWords ? (
<Highlighter
key={index}
textToHighlight={chunk.text}
searchWords={this.props.highlight.searchWords}
findChunks={findHighlightChunksInText}
@@ -137,10 +137,12 @@ export const LogRowContextGroup: React.FunctionComponent<LogRowContextGroupProps
const listContainerRef = useRef<HTMLDivElement>() as React.RefObject<HTMLDivElement>;
useLayoutEffect(() => {
if (shouldScrollToBottom && listContainerRef.current) {
// We want to scroll to bottom only when we receive first 10 log lines
const shouldScrollRows = rows.length > 0 && rows.length <= 10;
if (shouldScrollToBottom && shouldScrollRows && listContainerRef.current) {
setScrollTop(listContainerRef.current.offsetHeight);
}
}, [shouldScrollToBottom]);
}, [shouldScrollToBottom, rows]);
const headerProps = {
row,
@@ -8,6 +8,7 @@ export type ActionMeta = SelectActionMeta<{}>;
export type InputActionMeta = {
action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close';
};
export type LoadOptionsCallback<T> = (options: Array<SelectableValue<T>>) => void;
export interface SelectCommonProps<T> {
/** Aria label applied to the input field */
@@ -87,8 +88,10 @@ export interface SelectCommonProps<T> {
export interface SelectAsyncProps<T> {
/** When specified as boolean the loadOptions will execute when component is mounted */
defaultOptions?: boolean | Array<SelectableValue<T>>;
/** Asynchronously load select options */
loadOptions?: (query: string) => Promise<Array<SelectableValue<T>>>;
loadOptions?: (query: string, cb?: LoadOptionsCallback<T>) => Promise<Array<SelectableValue<T>>> | void;
/** If cacheOptions is true, then the loaded data will be cached. The cache will remain until cacheOptions changes value. */
cacheOptions?: boolean;
/** Message to display when options are loading */
+179
View File
@@ -0,0 +1,179 @@
package definitions
import (
"github.com/grafana/grafana/pkg/services/queryhistory"
)
// swagger:route GET /query-history query_history searchQueries
//
// Query history search.
//
// Returns a list of queries in the query history that matches the search criteria.
// Query history search supports pagination. Use the `limit` parameter to control the maximum number of queries returned; the default limit is 100.
// You can also use the `page` query parameter to fetch queries from any page other than the first one.
//
// Responses:
// 200: getQueryHistorySearchResponse
// 401: unauthorisedError
// 500: internalServerError
// swagger:route POST /query-history query_history createQuery
//
// Add query to query history.
//
// Adds new query to query history.
//
// Responses:
// 200: getQueryHistoryResponse
// 400: badRequestError
// 401: unauthorisedError
// 500: internalServerError
// swagger:route POST /query-history/star/{query_history_uid} query_history starQuery
//
// Add star to query in query history.
//
// Adds star to query in query history as specified by the UID.
//
// Responses:
// 200: getQueryHistoryResponse
// 401: unauthorisedError
// 500: internalServerError
// swagger:route POST /query-history/migrate query_history migrateQueries
//
// Migrate queries to query history.
//
// Adds multiple queries to query history.
//
// Responses:
// 200: getQueryHistoryMigrationResponse
// 400: badRequestError
// 401: unauthorisedError
// 500: internalServerError
// swagger:route PATCH /query-history/{query_history_uid} query_history patchQueryComment
//
// Update comment for query in query history.
//
// Updates comment for query in query history as specified by the UID.
//
// Responses:
// 200: getQueryHistoryResponse
// 400: badRequestError
// 401: unauthorisedError
// 500: internalServerError
// swagger:route DELETE /query-history/{query_history_uid} query_history deleteQuery
//
// Delete query in query history.
//
// Deletes an existing query in query history as specified by the UID. This operation cannot be reverted.
//
// Responses:
// 200: getQueryHistoryDeleteQueryResponse
// 401: unauthorisedError
// 500: internalServerError
// swagger:route DELETE /query-history/star/{query_history_uid} query_history unstarQuery
//
// Remove star to query in query history.
//
// Removes star from query in query history as specified by the UID.
//
// Responses:
// 200: getQueryHistoryResponse
// 401: unauthorisedError
// 500: internalServerError
// swagger:parameters starQuery patchQueryComment deleteQuery unstarQuery
type QueryHistoryByUID struct {
// in:path
// required:true
UID string `json:"query_history_uid"`
}
// swagger:parameters searchQueries
type SearchQueriesParams struct {
// List of data source UIDs to search for
// in:query
// required: false
// type: array
// collectionFormat: multi
DatasourceUid []string `json:"datasourceUid"`
// Text inside query or comments that is searched for
// in:query
// required: false
SearchString string `json:"searchString"`
// Flag indicating if only starred queries should be returned
// in:query
// required: false
OnlyStarred bool `json:"onlyStarred"`
// Sort method
// in:query
// required: false
// default: time-desc
// Enum: time-desc,time-asc
Sort string `json:"sort"`
// Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size.
// in:query
// required: false
Page int `json:"page"`
// Limit the number of returned results
// in:query
// required: false
Limit int `json:"limit"`
// From range for the query history search
// in:query
// required: false
From int64 `json:"from"`
// To range for the query history search
// in:query
// required: false
To int64 `json:"to"`
}
// swagger:parameters createQuery
type CreateQueryParams struct {
// in:body
// required:true
Body queryhistory.CreateQueryInQueryHistoryCommand `json:"body"`
}
// swagger:parameters patchQueryComment
type PatchQueryCommentParams struct {
// in:body
// required:true
Body queryhistory.PatchQueryCommentInQueryHistoryCommand `json:"body"`
}
// swagger:parameters migrateQueries
type MigrateQueriesParams struct {
// in:body
// required:true
Body queryhistory.MigrateQueriesToQueryHistoryCommand `json:"body"`
}
//swagger:response getQueryHistorySearchResponse
type GetQueryHistorySearchResponse struct {
// in: body
Body queryhistory.QueryHistorySearchResponse `json:"body"`
}
// swagger:response getQueryHistoryResponse
type GetQueryHistoryResponse struct {
// in: body
Body queryhistory.QueryHistoryResponse `json:"body"`
}
// swagger:response getQueryHistoryDeleteQueryResponse
type GetQueryHistoryDeleteQueryResponse struct {
// in: body
Body queryhistory.QueryHistoryDeleteQueryResponse `json:"body"`
}
// swagger:response getQueryHistoryMigrationResponse
type GetQueryHistoryMigrationResponse struct {
// in: body
Body queryhistory.QueryHistoryMigrationResponse `json:"body"`
}
+4
View File
@@ -31,6 +31,10 @@
{
"name": "library_elements",
"description": "The identifier (ID) of a library element is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a library element uniquely identifies library elements between multiple Grafana installs. It’s automatically generated unless you specify it during library element creation. The UID provides consistent URLs for accessing library elements and when syncing library elements between multiple Grafana installs.\nThe maximum length of a UID is 40 characters."
},
{
"name": "query_history",
"description": "The identifier (ID) of a query in query history is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a query history uniquely identifies queries in query history between multiple Grafana installs. It’s automatically generated. The UID provides consistent URLs for accessing queries in query history."
},
{
"name": "orgs",
+2 -1
View File
@@ -42,6 +42,7 @@ const (
WeightDashboard
WeightExplore
WeightAlerting
WeightDataConnections
WeightPlugin
WeightConfig
WeightAdmin
@@ -61,7 +62,7 @@ type NavLink struct {
Description string `json:"description,omitempty"`
Section string `json:"section,omitempty"`
SubTitle string `json:"subTitle,omitempty"`
Icon string `json:"icon,omitempty"`
Icon string `json:"icon,omitempty"` // Available icons can be browsed in Storybook: https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
+76 -8
View File
@@ -245,6 +245,10 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
navTree = append(navTree, hs.buildAlertNavLinks(c)...)
}
if hs.Features.IsEnabled(featuremgmt.FlagDataConnectionsConsole) {
navTree = append(navTree, hs.buildDataConnectionsNavLink(c))
}
appLinks, err := hs.getAppLinks(c)
if err != nil {
return nil, err
@@ -456,6 +460,11 @@ func (hs *HTTPServer) buildStarredItemsNavLinks(c *models.ReqContext, prefs *pre
}
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
hasAccess := ac.HasAccess(hs.AccessControl, c)
hasEditPermInAnyFolder := func(c *models.ReqContext) bool {
return hasEditPerm
}
dashboardChildNavs := []*dtos.NavLink{}
if !hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
@@ -492,19 +501,26 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
})
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "New dashboard", Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "new-dashboard", ShowIconInNavbar: true,
})
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "New dashboard", Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "new-dashboard", ShowIconInNavbar: true,
})
}
if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalPermission(dashboards.ActionFoldersCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "New folder", SubTitle: "Create a new folder to organize your dashboards", Id: "new-folder",
Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new", HideFromTabs: true, ShowIconInNavbar: true,
})
}
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "plus",
Url: hs.Cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, ShowIconInNavbar: true,
})
if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "plus",
Url: hs.Cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, ShowIconInNavbar: true,
})
}
}
return dashboardChildNavs
}
@@ -630,6 +646,58 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink
return children
}
func (hs *HTTPServer) buildDataConnectionsNavLink(c *models.ReqContext) *dtos.NavLink {
var children []*dtos.NavLink
var navLink *dtos.NavLink
baseId := "data-connections"
baseUrl := hs.Cfg.AppSubURL + "/" + baseId
children = append(children, &dtos.NavLink{
Id: baseId + "-datasources",
Text: "Data sources",
Icon: "database",
Description: "Add and configure data sources",
Url: baseUrl + "/datasources",
})
children = append(children, &dtos.NavLink{
Id: baseId + "-plugins",
Text: "Plugins",
Icon: "plug",
Description: "Manage plugins",
Url: baseUrl + "/plugins",
})
children = append(children, &dtos.NavLink{
Id: baseId + "-cloud-integrations",
Text: "Cloud integrations",
Icon: "bolt",
Description: "Manage your cloud integrations",
Url: baseUrl + "/cloud-integrations",
})
children = append(children, &dtos.NavLink{
Id: baseId + "-recorded-queries",
Text: "Recorded queries",
Icon: "record-audio",
Description: "Manage your recorded queries",
Url: baseUrl + "/recorded-queries",
})
navLink = &dtos.NavLink{
Text: "Data Connections",
Icon: "link",
Id: baseId,
Url: baseUrl,
Children: children,
Section: dtos.NavSectionCore,
SortWeight: dtos.WeightDataConnections,
}
return navLink
}
func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
hasAccess := ac.HasAccess(hs.AccessControl, c)
hasGlobalAccess := ac.HasGlobalAccess(hs.AccessControl, c)
+2 -8
View File
@@ -75,19 +75,13 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
loginInfo.AuthModule = name
provider := hs.SocialService.GetOAuthInfoProvider(name)
if provider == nil {
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{
HttpStatus: http.StatusNotFound,
PublicMessage: "OAuth not enabled",
})
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, errors.New("OAuth not enabled"))
return
}
connect, err := hs.SocialService.GetConnector(name)
if err != nil {
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{
HttpStatus: http.StatusNotFound,
PublicMessage: fmt.Sprintf("No OAuth with name %s configured", name),
})
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, fmt.Errorf("no OAuth with name %s configured", name))
return
}
+11 -8
View File
@@ -9,6 +9,8 @@ import (
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -31,11 +33,12 @@ func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux {
sqlStore := sqlstore.InitTestDB(t)
hs := &HTTPServer{
Cfg: cfg,
License: &licensing.OSSLicensingService{Cfg: cfg},
SQLStore: sqlStore,
SocialService: social.ProvideService(cfg),
HooksService: hooks.ProvideService(),
Cfg: cfg,
License: &licensing.OSSLicensingService{Cfg: cfg},
SQLStore: sqlStore,
SocialService: social.ProvideService(cfg),
HooksService: hooks.ProvideService(),
SecretsService: fakes.NewFakeSecretsService(),
}
m := web.New()
@@ -55,9 +58,9 @@ func TestOAuthLogin_UnknownProvider(t *testing.T) {
recorder := httptest.NewRecorder()
m.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusNotFound, recorder.Code)
assert.Contains(t, recorder.Body.String(), "OAuth not enabled")
// expect to be redirected to /login
assert.Equal(t, http.StatusFound, recorder.Code)
assert.Equal(t, "/login", recorder.Header().Get("Location"))
}
func TestOAuthLogin_Base(t *testing.T) {
@@ -170,6 +170,9 @@ func runTests(createCases func() []fsTestCase, t *testing.T) {
}
func TestIntegrationFsStorage(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
//skipTest := true
emptyContents := make([]byte, 0)
pngImage, _ := base64.StdEncoding.DecodeString(pngImageBase64)
@@ -0,0 +1,65 @@
package httpclientprovider_test
import (
"net/http"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
"github.com/stretchr/testify/require"
)
func TestForwardedCookiesMiddleware(t *testing.T) {
tcs := []struct {
desc string
allowedCookies []string
expectedCookieHeader string
}{
{
desc: "With nil allowedCookies should not populate Cookie header",
allowedCookies: nil,
expectedCookieHeader: "",
},
{
desc: "With empty allowed cookies should not populate Cookie header",
allowedCookies: []string{},
expectedCookieHeader: "",
},
{
desc: "When provided with allowed cookies should populate Cookie header",
allowedCookies: []string{"c1", "c3"},
expectedCookieHeader: "c1=1; c3=3",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
ctx := &testContext{}
finalRoundTripper := ctx.createRoundTripper()
forwarded := []*http.Cookie{
{Name: "c1", Value: "1"},
{Name: "c2", Value: "2"},
{Name: "c3", Value: "3"},
}
mw := httpclientprovider.ForwardedCookiesMiddleware(forwarded, tc.allowedCookies)
opts := httpclient.Options{}
rt := mw.CreateMiddleware(opts, finalRoundTripper)
require.NotNil(t, rt)
middlewareName, ok := mw.(httpclient.MiddlewareName)
require.True(t, ok)
require.Equal(t, "forwarded-cookies", middlewareName.MiddlewareName())
req, err := http.NewRequest(http.MethodGet, "http://", nil)
require.NoError(t, err)
res, err := rt.RoundTrip(req)
require.NoError(t, err)
require.NotNil(t, res)
if res.Body != nil {
require.NoError(t, res.Body.Close())
}
require.Len(t, ctx.callChain, 1)
require.ElementsMatch(t, []string{"final"}, ctx.callChain)
require.Equal(t, tc.expectedCookieHeader, ctx.req.Header.Get("Cookie"))
})
}
}
@@ -0,0 +1,24 @@
package httpclientprovider
import (
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/util/proxyutil"
)
const ForwardedCookiesMiddlewareName = "forwarded-cookies"
// ForwardedCookiesMiddleware middleware that sets Cookie header on the
// outgoing request, if forwarded cookies configured/provided.
func ForwardedCookiesMiddleware(forwardedCookies []*http.Cookie, allowedCookies []string) httpclient.Middleware {
return httpclient.NamedMiddlewareFunc(ForwardedCookiesMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
for _, cookie := range forwardedCookies {
req.AddCookie(cookie)
}
proxyutil.ClearCookieHeader(req, allowedCookies)
return next.RoundTrip(req)
})
})
}
@@ -0,0 +1,31 @@
package httpclientprovider
import (
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"golang.org/x/oauth2"
)
const ForwardedOAuthIdentityMiddlewareName = "forwarded-oauth-identity"
// ForwardedOAuthIdentityMiddleware middleware that sets Authorization/X-ID-Token
// headers on the outgoing request if an OAuth Token is provided
func ForwardedOAuthIdentityMiddleware(token *oauth2.Token) httpclient.Middleware {
return httpclient.NamedMiddlewareFunc(ForwardedOAuthIdentityMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
if token == nil {
return next
}
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
idToken, ok := token.Extra("id_token").(string)
if ok && idToken != "" {
req.Header.Set("X-ID-Token", idToken)
}
return next.RoundTrip(req)
})
})
}
@@ -0,0 +1,82 @@
package httpclientprovider_test
import (
"net/http"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)
func TestForwardedOAuthIdentityMiddleware(t *testing.T) {
at := &oauth2.Token{
AccessToken: "access-token",
}
tcs := []struct {
desc string
token *oauth2.Token
expectedAuthorizationHeader string
expectedIDTokenHeader string
}{
{
desc: "With nil token should not populate Cookie headers",
token: nil,
expectedAuthorizationHeader: "",
expectedIDTokenHeader: "",
},
{
desc: "With access token set should populate Authorization header",
token: at,
expectedAuthorizationHeader: "Bearer access-token",
expectedIDTokenHeader: "",
},
{
desc: "With Authorization and X-ID-Token header set should populate Authorization and X-Id-Token header",
token: at.WithExtra(map[string]interface{}{"id_token": "id-token"}),
expectedAuthorizationHeader: "Bearer access-token",
expectedIDTokenHeader: "id-token",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
ctx := &testContext{}
finalRoundTripper := ctx.createRoundTripper()
mw := httpclientprovider.ForwardedOAuthIdentityMiddleware(tc.token)
opts := httpclient.Options{}
rt := mw.CreateMiddleware(opts, finalRoundTripper)
require.NotNil(t, rt)
middlewareName, ok := mw.(httpclient.MiddlewareName)
require.True(t, ok)
require.Equal(t, "forwarded-oauth-identity", middlewareName.MiddlewareName())
req, err := http.NewRequest(http.MethodGet, "http://", nil)
require.NoError(t, err)
res, err := rt.RoundTrip(req)
require.NoError(t, err)
require.NotNil(t, res)
if res.Body != nil {
require.NoError(t, res.Body.Close())
}
require.Len(t, ctx.callChain, 1)
require.ElementsMatch(t, []string{"final"}, ctx.callChain)
require.Equal(t, tc.expectedAuthorizationHeader, ctx.req.Header.Get("Authorization"))
require.Equal(t, tc.expectedIDTokenHeader, ctx.req.Header.Get("X-ID-Token"))
})
}
}
type testContext struct {
callChain []string
req *http.Request
}
func (c *testContext) createRoundTripper() http.RoundTripper {
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
c.callChain = append(c.callChain, "final")
c.req = req
return &http.Response{StatusCode: http.StatusOK}, nil
})
}
@@ -5,12 +5,11 @@ import (
"net/http"
"time"
"github.com/grafana/grafana/pkg/models"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/mwitkow/go-conntrack"
)
+3
View File
@@ -37,6 +37,9 @@ func (t *TestCase) Value() string {
}
func TestIntegrationKVStore(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
kv := createTestableKVStore(t)
ctx := context.Background()
+24 -40
View File
@@ -3,9 +3,9 @@ package metrics
import (
"runtime"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
// ExporterName is used as namespace for exposing prometheus metrics
@@ -199,40 +199,40 @@ func init() {
Namespace: ExporterName,
})
MPageStatus = newCounterVecStartingAtZero(
MPageStatus = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "page_response_status_total",
Help: "page http response status",
Namespace: ExporterName,
}, []string{"code"}, httpStatusCodes...)
}, []string{"code"}, map[string][]string{"code": httpStatusCodes})
MApiStatus = newCounterVecStartingAtZero(
MApiStatus = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "api_response_status_total",
Help: "api http response status",
Namespace: ExporterName,
}, []string{"code"}, httpStatusCodes...)
}, []string{"code"}, map[string][]string{"code": httpStatusCodes})
MProxyStatus = newCounterVecStartingAtZero(
MProxyStatus = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "proxy_response_status_total",
Help: "proxy http response status",
Namespace: ExporterName,
}, []string{"code"}, httpStatusCodes...)
}, []string{"code"}, map[string][]string{"code": httpStatusCodes})
MApiUserSignUpStarted = newCounterStartingAtZero(prometheus.CounterOpts{
MApiUserSignUpStarted = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_started_total",
Help: "amount of users who started the signup flow",
Namespace: ExporterName,
})
MApiUserSignUpCompleted = newCounterStartingAtZero(prometheus.CounterOpts{
MApiUserSignUpCompleted = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_completed_total",
Help: "amount of users who completed the signup flow",
Namespace: ExporterName,
})
MApiUserSignUpInvite = newCounterStartingAtZero(prometheus.CounterOpts{
MApiUserSignUpInvite = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_invite_total",
Help: "amount of users who have been invited",
Namespace: ExporterName,
@@ -259,55 +259,55 @@ func init() {
Namespace: ExporterName,
})
MApiAdminUserCreate = newCounterStartingAtZero(prometheus.CounterOpts{
MApiAdminUserCreate = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_admin_user_created_total",
Help: "api admin user created counter",
Namespace: ExporterName,
})
MApiLoginPost = newCounterStartingAtZero(prometheus.CounterOpts{
MApiLoginPost = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_post_total",
Help: "api login post counter",
Namespace: ExporterName,
})
MApiLoginOAuth = newCounterStartingAtZero(prometheus.CounterOpts{
MApiLoginOAuth = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_oauth_total",
Help: "api login oauth counter",
Namespace: ExporterName,
})
MApiLoginSAML = newCounterStartingAtZero(prometheus.CounterOpts{
MApiLoginSAML = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_saml_total",
Help: "api login saml counter",
Namespace: ExporterName,
})
MApiOrgCreate = newCounterStartingAtZero(prometheus.CounterOpts{
MApiOrgCreate = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_org_create_total",
Help: "api org created counter",
Namespace: ExporterName,
})
MApiDashboardSnapshotCreate = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardSnapshotCreate = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_create_total",
Help: "dashboard snapshots created",
Namespace: ExporterName,
})
MApiDashboardSnapshotExternal = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardSnapshotExternal = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_external_total",
Help: "external dashboard snapshots created",
Namespace: ExporterName,
})
MApiDashboardSnapshotGet = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardSnapshotGet = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_get_total",
Help: "loaded dashboards",
Namespace: ExporterName,
})
MApiDashboardInsert = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardInsert = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_models_dashboard_insert_total",
Help: "dashboards inserted ",
Namespace: ExporterName,
@@ -331,25 +331,25 @@ func init() {
Namespace: ExporterName,
}, []string{"type"})
MAwsCloudWatchGetMetricStatistics = newCounterStartingAtZero(prometheus.CounterOpts{
MAwsCloudWatchGetMetricStatistics = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_get_metric_statistics_total",
Help: "counter for getting metric statistics from aws",
Namespace: ExporterName,
})
MAwsCloudWatchListMetrics = newCounterStartingAtZero(prometheus.CounterOpts{
MAwsCloudWatchListMetrics = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_list_metrics_total",
Help: "counter for getting list of metrics from aws",
Namespace: ExporterName,
})
MAwsCloudWatchGetMetricData = newCounterStartingAtZero(prometheus.CounterOpts{
MAwsCloudWatchGetMetricData = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_get_metric_data_total",
Help: "counter for getting metric data time series from aws",
Namespace: ExporterName,
})
MDBDataSourceQueryByID = newCounterStartingAtZero(prometheus.CounterOpts{
MDBDataSourceQueryByID = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "db_datasource_query_by_id_total",
Help: "counter for getting datasource by id",
Namespace: ExporterName,
@@ -646,19 +646,3 @@ func initMetricVars() {
StatsTotalDataKeys,
)
}
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
counter := prometheus.NewCounterVec(opts, labels)
for _, label := range labelValues {
counter.WithLabelValues(label).Add(0)
}
return counter
}
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
counter := prometheus.NewCounter(opts)
counter.Add(0)
return counter
}
+55
View File
@@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
// SanitizeLabelName removes all invalid chars from the label name.
@@ -29,3 +31,56 @@ func SanitizeLabelName(name string) (string, error) {
return out.String(), nil
}
// NewCounterStartingAtZero initializes a new Prometheus counter with an initial
// observation of zero. Used for to guarantee the existence of the specific metric.
func NewCounterStartingAtZero(opts prometheus.CounterOpts) prometheus.Counter {
counter := prometheus.NewCounter(opts)
counter.Add(0)
return counter
}
// NewCounterVecStartingAtZero initializes a new Prometheus counter with an initial
// observation of zero for every possible value of each label. Used for the sake of
// consistency among all the possible labels and values.
func NewCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues map[string][]string) *prometheus.CounterVec {
counter := prometheus.NewCounterVec(opts, labels)
for _, ls := range buildLabelSets(labels, labelValues) {
counter.With(ls).Add(0)
}
return counter
}
func buildLabelSets(labels []string, labelValues map[string][]string) []prometheus.Labels {
var labelSets []prometheus.Labels
var n func(i int, ls prometheus.Labels)
n = func(i int, ls prometheus.Labels) {
if i == len(labels) {
labelSets = append(labelSets, ls)
return
}
label := labels[i]
values := labelValues[label]
for _, v := range values {
lsCopy := copyLabelSet(ls)
lsCopy[label] = v
n(i+1, lsCopy)
}
}
n(0, prometheus.Labels{})
return labelSets
}
func copyLabelSet(ls prometheus.Labels) prometheus.Labels {
newLs := make(prometheus.Labels, len(ls))
for l, v := range ls {
newLs[l] = v
}
return newLs
}
@@ -3,6 +3,7 @@ package metricutil
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -30,3 +31,89 @@ func TestLabelNameSanitization(t *testing.T) {
}
}
}
func Test_buildLabelSets(t *testing.T) {
testcases := map[string]struct {
labels []string
labelValues map[string][]string
expected []prometheus.Labels
}{
"single label, single value": {
labels: []string{"operation"},
labelValues: map[string][]string{
"operation": {"insert"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert"},
},
},
"single label, multiple values": {
labels: []string{"operation"},
labelValues: map[string][]string{
"operation": {"insert", "delete"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert"},
map[string]string{"operation": "delete"},
},
},
"multiple label, single value": {
labels: []string{"operation", "success"},
labelValues: map[string][]string{
"operation": {"insert"},
"success": {"true"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert", "success": "true"},
},
},
"multiple label, multiple values": {
labels: []string{"operation", "success"},
labelValues: map[string][]string{
"operation": {"insert", "delete"},
"success": {"true", "false"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert", "success": "true"},
map[string]string{"operation": "insert", "success": "false"},
map[string]string{"operation": "delete", "success": "true"},
map[string]string{"operation": "delete", "success": "false"},
},
},
"irregular labels and values": {
labels: []string{"operation", "success", "environment"},
labelValues: map[string][]string{
"operation": {"insert", "update", "delete"},
"success": {"true", "false"},
"environment": {"dev", "test", "staging"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert", "success": "true", "environment": "dev"},
map[string]string{"operation": "insert", "success": "true", "environment": "test"},
map[string]string{"operation": "insert", "success": "true", "environment": "staging"},
map[string]string{"operation": "insert", "success": "false", "environment": "dev"},
map[string]string{"operation": "insert", "success": "false", "environment": "test"},
map[string]string{"operation": "insert", "success": "false", "environment": "staging"},
map[string]string{"operation": "update", "success": "true", "environment": "dev"},
map[string]string{"operation": "update", "success": "true", "environment": "test"},
map[string]string{"operation": "update", "success": "true", "environment": "staging"},
map[string]string{"operation": "update", "success": "false", "environment": "dev"},
map[string]string{"operation": "update", "success": "false", "environment": "test"},
map[string]string{"operation": "update", "success": "false", "environment": "staging"},
map[string]string{"operation": "delete", "success": "true", "environment": "dev"},
map[string]string{"operation": "delete", "success": "true", "environment": "test"},
map[string]string{"operation": "delete", "success": "true", "environment": "staging"},
map[string]string{"operation": "delete", "success": "false", "environment": "dev"},
map[string]string{"operation": "delete", "success": "false", "environment": "test"},
map[string]string{"operation": "delete", "success": "false", "environment": "staging"},
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
got := buildLabelSets(tc.labels, tc.labelValues)
assert.Equal(t, tc.expected, got)
})
}
}
@@ -9,6 +9,9 @@ import (
)
func TestIntegrationServerLok(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sl := createTestableServerLock(t)
counter := 0
+12 -6
View File
@@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -46,6 +47,8 @@ func init() {
// RequestMetrics is a middleware handler that instruments the request.
func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
log := log.New("middleware.request-metrics")
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
if strings.HasPrefix(c.Req.URL.Path, "/public/") || c.Req.URL.Path == "robots.txt" || c.Req.URL.Path == "/metrics" {
c.Next()
@@ -58,15 +61,18 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
defer httpRequestsInFlight.Dec()
c.Next()
handler := "unknown"
if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists {
handler = routeOperation
}
status := rw.Status()
code := sanitizeCode(status)
handler := "unknown"
if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists {
handler = routeOperation
} else {
if features.IsEnabled(featuremgmt.FlagLogRequestsInstrumentedAsUnknown) {
log.Warn("request instrumented as unknown", "path", c.Req.URL.Path, "status_code", status)
}
}
// avoiding the sanitize functions for in the new instrumentation
// since they dont make much sense. We should remove them later.
histogram := httpRequestDurationHistogram.
File diff suppressed because it is too large Load Diff
@@ -3,54 +3,62 @@ package secretsmanagerplugin;
option go_package = "./;secretsmanagerplugin";
message SecretsGetRequest {
Key keyDescriptor = 1;
}
message SecretsSetRequest {
Key keyDescriptor = 1;
string value = 2;
}
message SecretsDelRequest {
Key keyDescriptor = 1;
}
message SecretsKeysRequest {
Key keyDescriptor = 1;
bool allOrganizations = 2;
}
message SecretsRenameRequest {
Key keyDescriptor = 1;
string newNamespace = 2;
}
message Key {
int64 orgId = 1;
string namespace = 2;
string type = 3;
}
message SecretsErrorResponse {
string error = 1;
message GetSecretRequest {
Key keyDescriptor = 1;
}
message SecretsGetResponse {
string error = 1;
message GetSecretResponse {
string userFriendlyError = 1;
string decryptedValue = 2;
bool exists = 3;
}
message SecretsKeysResponse {
string error = 1;
message SetSecretRequest {
Key keyDescriptor = 1;
string value = 2;
}
message SetSecretResponse {
string userFriendlyError = 1;
}
message DelSecretRequest {
Key keyDescriptor = 1;
}
message DelSecretResponse {
string userFriendlyError = 1;
}
message ListSecretsRequest {
Key keyDescriptor = 1;
bool allOrganizations = 2;
}
message ListSecretsResponse {
string userFriendlyError = 1;
repeated Key keys = 2;
}
service RemoteSecretsManager {
rpc Get(SecretsGetRequest) returns (SecretsGetResponse);
rpc Set(SecretsSetRequest) returns (SecretsErrorResponse);
rpc Del(SecretsDelRequest) returns (SecretsErrorResponse);
rpc Keys(SecretsKeysRequest) returns (SecretsKeysResponse);
rpc Rename(SecretsRenameRequest) returns (SecretsErrorResponse);
message RenameSecretRequest {
Key keyDescriptor = 1;
string newNamespace = 2;
}
message RenameSecretResponse {
string userFriendlyError = 1;
}
service SecretsManager {
rpc Get(GetSecretRequest) returns (GetSecretResponse);
rpc Set(SetSecretRequest) returns (SetSecretResponse);
rpc Del(DelSecretRequest) returns (DelSecretResponse);
rpc Keys(ListSecretsRequest) returns (ListSecretsResponse);
rpc Rename(RenameSecretRequest) returns (RenameSecretResponse);
}
@@ -8,7 +8,7 @@ import (
)
type SecretsManagerPlugin interface {
RemoteSecretsManagerClient
SecretsManagerClient
}
type SecretsManagerGRPCPlugin struct {
@@ -20,37 +20,37 @@ func (p *SecretsManagerGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc
}
func (p *SecretsManagerGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &SecretsManagerGRPCClient{NewRemoteSecretsManagerClient(c)}, nil
return &SecretsManagerGRPCClient{NewSecretsManagerClient(c)}, nil
}
type SecretsManagerGRPCClient struct {
RemoteSecretsManagerClient
SecretsManagerClient
}
// Get an item from the store
func (sm *SecretsManagerGRPCClient) Get(ctx context.Context, req *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error) {
return sm.RemoteSecretsManagerClient.Get(ctx, req)
func (sm *SecretsManagerGRPCClient) Get(ctx context.Context, req *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) {
return sm.SecretsManagerClient.Get(ctx, req)
}
// Set an item in the store
func (sm *SecretsManagerGRPCClient) Set(ctx context.Context, req *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
return sm.RemoteSecretsManagerClient.Set(ctx, req)
func (sm *SecretsManagerGRPCClient) Set(ctx context.Context, req *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error) {
return sm.SecretsManagerClient.Set(ctx, req)
}
// Del deletes an item from the store.
func (sm *SecretsManagerGRPCClient) Del(ctx context.Context, req *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
return sm.RemoteSecretsManagerClient.Del(ctx, req)
func (sm *SecretsManagerGRPCClient) Del(ctx context.Context, req *DelSecretRequest, opts ...grpc.CallOption) (*DelSecretResponse, error) {
return sm.SecretsManagerClient.Del(ctx, req)
}
// Keys get all keys for a given namespace.
func (sm *SecretsManagerGRPCClient) Keys(ctx context.Context, req *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error) {
return sm.RemoteSecretsManagerClient.Keys(ctx, req)
func (sm *SecretsManagerGRPCClient) Keys(ctx context.Context, req *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error) {
return sm.SecretsManagerClient.Keys(ctx, req)
}
// Rename an item in the store
func (sm *SecretsManagerGRPCClient) Rename(ctx context.Context, req *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
return sm.RemoteSecretsManagerClient.Rename(ctx, req)
func (sm *SecretsManagerGRPCClient) Rename(ctx context.Context, req *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error) {
return sm.SecretsManagerClient.Rename(ctx, req)
}
var _ RemoteSecretsManagerClient = &SecretsManagerGRPCClient{}
var _ SecretsManagerClient = &SecretsManagerGRPCClient{}
var _ plugin.GRPCPlugin = &SecretsManagerGRPCPlugin{}
@@ -18,230 +18,230 @@ import (
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// RemoteSecretsManagerClient is the client API for RemoteSecretsManager service.
// SecretsManagerClient is the client API for SecretsManager service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type RemoteSecretsManagerClient interface {
Get(ctx context.Context, in *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error)
Set(ctx context.Context, in *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
Del(ctx context.Context, in *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
Keys(ctx context.Context, in *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error)
Rename(ctx context.Context, in *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
type SecretsManagerClient interface {
Get(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error)
Set(ctx context.Context, in *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error)
Del(ctx context.Context, in *DelSecretRequest, opts ...grpc.CallOption) (*DelSecretResponse, error)
Keys(ctx context.Context, in *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error)
Rename(ctx context.Context, in *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error)
}
type remoteSecretsManagerClient struct {
type secretsManagerClient struct {
cc grpc.ClientConnInterface
}
func NewRemoteSecretsManagerClient(cc grpc.ClientConnInterface) RemoteSecretsManagerClient {
return &remoteSecretsManagerClient{cc}
func NewSecretsManagerClient(cc grpc.ClientConnInterface) SecretsManagerClient {
return &secretsManagerClient{cc}
}
func (c *remoteSecretsManagerClient) Get(ctx context.Context, in *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error) {
out := new(SecretsGetResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Get", in, out, opts...)
func (c *secretsManagerClient) Get(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) {
out := new(GetSecretResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Get", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *remoteSecretsManagerClient) Set(ctx context.Context, in *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
out := new(SecretsErrorResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Set", in, out, opts...)
func (c *secretsManagerClient) Set(ctx context.Context, in *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error) {
out := new(SetSecretResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Set", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *remoteSecretsManagerClient) Del(ctx context.Context, in *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
out := new(SecretsErrorResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Del", in, out, opts...)
func (c *secretsManagerClient) Del(ctx context.Context, in *DelSecretRequest, opts ...grpc.CallOption) (*DelSecretResponse, error) {
out := new(DelSecretResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Del", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *remoteSecretsManagerClient) Keys(ctx context.Context, in *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error) {
out := new(SecretsKeysResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Keys", in, out, opts...)
func (c *secretsManagerClient) Keys(ctx context.Context, in *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error) {
out := new(ListSecretsResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Keys", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *remoteSecretsManagerClient) Rename(ctx context.Context, in *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
out := new(SecretsErrorResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Rename", in, out, opts...)
func (c *secretsManagerClient) Rename(ctx context.Context, in *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error) {
out := new(RenameSecretResponse)
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Rename", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// RemoteSecretsManagerServer is the server API for RemoteSecretsManager service.
// All implementations must embed UnimplementedRemoteSecretsManagerServer
// SecretsManagerServer is the server API for SecretsManager service.
// All implementations must embed UnimplementedSecretsManagerServer
// for forward compatibility
type RemoteSecretsManagerServer interface {
Get(context.Context, *SecretsGetRequest) (*SecretsGetResponse, error)
Set(context.Context, *SecretsSetRequest) (*SecretsErrorResponse, error)
Del(context.Context, *SecretsDelRequest) (*SecretsErrorResponse, error)
Keys(context.Context, *SecretsKeysRequest) (*SecretsKeysResponse, error)
Rename(context.Context, *SecretsRenameRequest) (*SecretsErrorResponse, error)
mustEmbedUnimplementedRemoteSecretsManagerServer()
type SecretsManagerServer interface {
Get(context.Context, *GetSecretRequest) (*GetSecretResponse, error)
Set(context.Context, *SetSecretRequest) (*SetSecretResponse, error)
Del(context.Context, *DelSecretRequest) (*DelSecretResponse, error)
Keys(context.Context, *ListSecretsRequest) (*ListSecretsResponse, error)
Rename(context.Context, *RenameSecretRequest) (*RenameSecretResponse, error)
mustEmbedUnimplementedSecretsManagerServer()
}
// UnimplementedRemoteSecretsManagerServer must be embedded to have forward compatible implementations.
type UnimplementedRemoteSecretsManagerServer struct {
// UnimplementedSecretsManagerServer must be embedded to have forward compatible implementations.
type UnimplementedSecretsManagerServer struct {
}
func (UnimplementedRemoteSecretsManagerServer) Get(context.Context, *SecretsGetRequest) (*SecretsGetResponse, error) {
func (UnimplementedSecretsManagerServer) Get(context.Context, *GetSecretRequest) (*GetSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
}
func (UnimplementedRemoteSecretsManagerServer) Set(context.Context, *SecretsSetRequest) (*SecretsErrorResponse, error) {
func (UnimplementedSecretsManagerServer) Set(context.Context, *SetSecretRequest) (*SetSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Set not implemented")
}
func (UnimplementedRemoteSecretsManagerServer) Del(context.Context, *SecretsDelRequest) (*SecretsErrorResponse, error) {
func (UnimplementedSecretsManagerServer) Del(context.Context, *DelSecretRequest) (*DelSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Del not implemented")
}
func (UnimplementedRemoteSecretsManagerServer) Keys(context.Context, *SecretsKeysRequest) (*SecretsKeysResponse, error) {
func (UnimplementedSecretsManagerServer) Keys(context.Context, *ListSecretsRequest) (*ListSecretsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Keys not implemented")
}
func (UnimplementedRemoteSecretsManagerServer) Rename(context.Context, *SecretsRenameRequest) (*SecretsErrorResponse, error) {
func (UnimplementedSecretsManagerServer) Rename(context.Context, *RenameSecretRequest) (*RenameSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Rename not implemented")
}
func (UnimplementedRemoteSecretsManagerServer) mustEmbedUnimplementedRemoteSecretsManagerServer() {}
func (UnimplementedSecretsManagerServer) mustEmbedUnimplementedSecretsManagerServer() {}
// UnsafeRemoteSecretsManagerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to RemoteSecretsManagerServer will
// UnsafeSecretsManagerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to SecretsManagerServer will
// result in compilation errors.
type UnsafeRemoteSecretsManagerServer interface {
mustEmbedUnimplementedRemoteSecretsManagerServer()
type UnsafeSecretsManagerServer interface {
mustEmbedUnimplementedSecretsManagerServer()
}
func RegisterRemoteSecretsManagerServer(s grpc.ServiceRegistrar, srv RemoteSecretsManagerServer) {
s.RegisterService(&RemoteSecretsManager_ServiceDesc, srv)
func RegisterSecretsManagerServer(s grpc.ServiceRegistrar, srv SecretsManagerServer) {
s.RegisterService(&SecretsManager_ServiceDesc, srv)
}
func _RemoteSecretsManager_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SecretsGetRequest)
func _SecretsManager_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RemoteSecretsManagerServer).Get(ctx, in)
return srv.(SecretsManagerServer).Get(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Get",
FullMethod: "/secretsmanagerplugin.SecretsManager/Get",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RemoteSecretsManagerServer).Get(ctx, req.(*SecretsGetRequest))
return srv.(SecretsManagerServer).Get(ctx, req.(*GetSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RemoteSecretsManager_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SecretsSetRequest)
func _SecretsManager_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RemoteSecretsManagerServer).Set(ctx, in)
return srv.(SecretsManagerServer).Set(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Set",
FullMethod: "/secretsmanagerplugin.SecretsManager/Set",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RemoteSecretsManagerServer).Set(ctx, req.(*SecretsSetRequest))
return srv.(SecretsManagerServer).Set(ctx, req.(*SetSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RemoteSecretsManager_Del_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SecretsDelRequest)
func _SecretsManager_Del_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DelSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RemoteSecretsManagerServer).Del(ctx, in)
return srv.(SecretsManagerServer).Del(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Del",
FullMethod: "/secretsmanagerplugin.SecretsManager/Del",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RemoteSecretsManagerServer).Del(ctx, req.(*SecretsDelRequest))
return srv.(SecretsManagerServer).Del(ctx, req.(*DelSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RemoteSecretsManager_Keys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SecretsKeysRequest)
func _SecretsManager_Keys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListSecretsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RemoteSecretsManagerServer).Keys(ctx, in)
return srv.(SecretsManagerServer).Keys(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Keys",
FullMethod: "/secretsmanagerplugin.SecretsManager/Keys",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RemoteSecretsManagerServer).Keys(ctx, req.(*SecretsKeysRequest))
return srv.(SecretsManagerServer).Keys(ctx, req.(*ListSecretsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RemoteSecretsManager_Rename_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SecretsRenameRequest)
func _SecretsManager_Rename_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RenameSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RemoteSecretsManagerServer).Rename(ctx, in)
return srv.(SecretsManagerServer).Rename(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Rename",
FullMethod: "/secretsmanagerplugin.SecretsManager/Rename",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RemoteSecretsManagerServer).Rename(ctx, req.(*SecretsRenameRequest))
return srv.(SecretsManagerServer).Rename(ctx, req.(*RenameSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
// RemoteSecretsManager_ServiceDesc is the grpc.ServiceDesc for RemoteSecretsManager service.
// SecretsManager_ServiceDesc is the grpc.ServiceDesc for SecretsManager service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var RemoteSecretsManager_ServiceDesc = grpc.ServiceDesc{
ServiceName: "secretsmanagerplugin.RemoteSecretsManager",
HandlerType: (*RemoteSecretsManagerServer)(nil),
var SecretsManager_ServiceDesc = grpc.ServiceDesc{
ServiceName: "secretsmanagerplugin.SecretsManager",
HandlerType: (*SecretsManagerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Get",
Handler: _RemoteSecretsManager_Get_Handler,
Handler: _SecretsManager_Get_Handler,
},
{
MethodName: "Set",
Handler: _RemoteSecretsManager_Set_Handler,
Handler: _SecretsManager_Set_Handler,
},
{
MethodName: "Del",
Handler: _RemoteSecretsManager_Del_Handler,
Handler: _SecretsManager_Del_Handler,
},
{
MethodName: "Keys",
Handler: _RemoteSecretsManager_Keys_Handler,
Handler: _SecretsManager_Keys_Handler,
},
{
MethodName: "Rename",
Handler: _RemoteSecretsManager_Rename_Handler,
Handler: _SecretsManager_Rename_Handler,
},
},
Streams: []grpc.StreamDesc{},
@@ -79,7 +79,7 @@ func TestPluginManager_int_init(t *testing.T) {
es := elasticsearch.ProvideService(hcp)
grap := graphite.ProvideService(hcp, tracer)
idb := influxdb.ProvideService(hcp)
lk := loki.ProvideService(hcp, tracer)
lk := loki.ProvideService(hcp, features, tracer)
otsdb := opentsdb.ProvideService(hcp)
pr := prometheus.ProvideService(hcp, cfg, features, tracer)
tmpo := tempo.ProvideService(hcp)
@@ -18,6 +18,9 @@ import (
)
func TestIntegrationEngineTimeouts(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
usMock := &usagestats.UsageStatsMock{T: t}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
+7 -2
View File
@@ -32,7 +32,7 @@ func NewTicker(c clock.Clock, interval time.Duration, metric *metrics.Ticker) *T
t := &Ticker{
C: make(chan time.Time),
clock: c,
last: c.Now(),
last: getStartTick(c, interval),
interval: interval,
metrics: metric,
stopCh: make(chan struct{}),
@@ -42,9 +42,14 @@ func NewTicker(c clock.Clock, interval time.Duration, metric *metrics.Ticker) *T
return t
}
func getStartTick(clk clock.Clock, interval time.Duration) time.Time {
nano := clk.Now().UnixNano()
return time.Unix(0, nano-(nano%interval.Nanoseconds()))
}
func (t *Ticker) run() {
logger := log.New("ticker")
logger.Info("starting")
logger.Info("starting", "first_tick", t.last.Add(t.interval))
LOOP:
for {
next := t.last.Add(t.interval) // calculate the time of the next tick
+17 -2
View File
@@ -35,9 +35,24 @@ func TestTicker(t *testing.T) {
}
return time.Time{}
}
t.Run("should not drop ticks", func(t *testing.T) {
t.Run("should align with clock", func(t *testing.T) {
interval := 10 * time.Second
clk := clock.NewMock()
clk.Add(1 * time.Minute)
require.Equal(t, clk.Now(), getStartTick(clk, interval))
now := clk.Now()
for i := 0; i < 100; i++ {
delta := time.Duration(rand.Int63n(interval.Nanoseconds()))
clk.Set(now.Add(delta))
require.Equal(t, now, getStartTick(clk, interval))
}
})
t.Run("should not drop ticks", func(t *testing.T) {
interval := time.Duration(rand.Int63n(100)+10) * time.Second
clk := clock.NewMock()
clk.Add(interval) // align clock with the start tick
ticker := NewTicker(clk, interval, metrics.NewTickerMetrics(prometheus.NewRegistry()))
ticks := rand.Intn(9) + 1
@@ -119,7 +134,7 @@ func TestTicker(t *testing.T) {
interval := time.Duration(rand.Int63n(9)+1) * time.Second
registry := prometheus.NewPedanticRegistry()
ticker := NewTicker(clk, interval, metrics.NewTickerMetrics(registry))
expectedTick := clk.Now().Add(interval)
expectedTick := getStartTick(clk, interval).Add(interval)
expectedMetricFmt := `# HELP grafana_alerting_ticker_interval_seconds Interval at which the ticker is meant to tick.
# TYPE grafana_alerting_ticker_interval_seconds gauge
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationDashboardAclDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
var sqlStore *sqlstore.SQLStore
var currentUser models.User
var savedFolder, childDash *models.Dashboard
@@ -12,6 +12,9 @@ import (
)
func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("Testing DB", func(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var folder, dashInRoot, childDash *models.Dashboard
@@ -14,6 +14,9 @@ import (
)
func TestIntegrationDashboardProvisioningTest(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
@@ -21,6 +21,9 @@ import (
)
func TestIntegrationDashboardDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
var sqlStore *sqlstore.SQLStore
var savedFolder, savedDash, savedDash2 *models.Dashboard
var dashboardStore *DashboardStore
@@ -476,6 +479,9 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
}
func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
pluginId := "test-app"
@@ -495,6 +501,9 @@ func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *test
}
func TestIntegrationDashboard_SortingOptions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
@@ -541,6 +550,9 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) {
}
func TestIntegrationDashboard_Filter(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false)
@@ -22,6 +22,9 @@ import (
const testOrgID int64 = 1
func TestIntegrationIntegratedDashboardService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) {
// Basic validation tests
@@ -17,6 +17,9 @@ import (
)
func TestIntegrationDashboardService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("Dashboard service tests", func(t *testing.T) {
fakeStore := m.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t)
@@ -23,6 +23,9 @@ var orgID = int64(1)
var user = &models.SignedInUser{UserId: 1}
func TestIntegrationProvideFolderService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("should register scope resolvers", func(t *testing.T) {
cfg := setting.NewCfg()
ac := acmock.New()
@@ -34,6 +37,9 @@ func TestIntegrationProvideFolderService(t *testing.T) {
}
func TestIntegrationFolderService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("Folder service tests", func(t *testing.T) {
store := &dashboards.FakeDashboardStore{}
cfg := setting.NewCfg()
@@ -16,6 +16,9 @@ import (
)
func TestIntegrationGetDashboardVersion(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ss := sqlstore.InitTestDB(t)
dashVerStore := sqlStore{db: ss}
@@ -59,6 +62,9 @@ func TestIntegrationGetDashboardVersion(t *testing.T) {
}
func TestIntegrationDeleteExpiredVersions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
versionsToWrite := 10
ss := sqlstore.InitTestDB(t)
dashVerStore := sqlStore{db: ss}
@@ -80,6 +86,9 @@ func TestIntegrationDeleteExpiredVersions(t *testing.T) {
}
func TestIntegrationListDashboardVersions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ss := sqlstore.InitTestDB(t)
dashVerStore := sqlStore{db: ss, dialect: ss.Dialect}
savedDash := insertTestDashboard(t, ss, "test dash 43", 1, 0, false, "diff-all")
+4 -4
View File
@@ -13,7 +13,7 @@ var (
)
type DashboardVersion struct {
ID int64 `json:"id"`
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
ParentVersion int `json:"parentVersion"`
RestoredFrom int `json:"restoredFrom"`
@@ -45,9 +45,9 @@ type ListDashboardVersionsQuery struct {
}
type DashboardVersionDTO struct {
ID int64 `json:"id"`
DashboardID int64 `json:"dashboardId"`
DashboardUID string `json:"dashboardUid"`
ID int64 `json:"id" xorm:"id"`
DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
DashboardUID string `json:"dashboardUid" xorm:"dashboard_uid"`
ParentVersion int `json:"parentVersion"`
RestoredFrom int `json:"restoredFrom"`
Version int `json:"version"`
@@ -86,6 +86,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte
ds, err := p.DataSourceCache.GetDatasource(c.Req.Context(), dsID, c.SignedInUser, c.SkipCache)
if err != nil {
toAPIError(c, err)
return
}
p.proxyDatasourceRequest(c, ds)
}
+30 -5
View File
@@ -2,6 +2,7 @@
// pkg/services/featuremgmt/registry.go
// Then run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
// twice to generate and validate the feature flag files
package featuremgmt
@@ -116,6 +117,11 @@ var (
Description: "support websocket streaming for loki (early prototype)",
State: FeatureStateAlpha,
},
{
Name: "lokiDataframeApi",
Description: "use experimental loki api for websocket streaming (early prototype)",
State: FeatureStateAlpha,
},
{
Name: "swaggerUi",
Description: "Serves swagger UI",
@@ -198,11 +204,10 @@ var (
State: FeatureStateAlpha,
},
{
Name: "azureMonitorExperimentalUI",
Description: "Use grafana-experimental UI in Azure Monitor",
State: FeatureStateAlpha,
RequiresDevMode: true,
FrontendOnly: true,
Name: "azureMonitorExperimentalUI",
Description: "Use grafana-experimental UI in Azure Monitor",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "traceToMetrics",
@@ -221,6 +226,12 @@ var (
State: FeatureStateAlpha,
RequiresRestart: true,
},
{
Name: "autoMigrateGraphPanels",
Description: "Replace the angular graph panel with timeseries",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "prometheusWideSeries",
Description: "Enable wide series responses in the Prometheus datasource",
@@ -238,5 +249,19 @@ var (
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "logRequestsInstrumentedAsUnknown",
Description: "Logs the path for requests that are instrumented as unknown",
},
{
Name: "dataConnectionsConsole",
Description: "Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins.",
State: FeatureStateAlpha,
},
{
Name: "internationalization",
Description: "Enables work-in-progress internationalization",
State: FeatureStateAlpha,
},
}
)
+20
View File
@@ -87,6 +87,10 @@ const (
// support websocket streaming for loki (early prototype)
FlagLokiLive = "lokiLive"
// FlagLokiDataframeApi
// use experimental loki api for websocket streaming (early prototype)
FlagLokiDataframeApi = "lokiDataframeApi"
// FlagSwaggerUi
// Serves swagger UI
FlagSwaggerUi = "swaggerUi"
@@ -163,6 +167,10 @@ const (
// Validate dashboard JSON POSTed to api/dashboards/db
FlagValidateDashboardsOnSave = "validateDashboardsOnSave"
// FlagAutoMigrateGraphPanels
// Replace the angular graph panel with timeseries
FlagAutoMigrateGraphPanels = "autoMigrateGraphPanels"
// FlagPrometheusWideSeries
// Enable wide series responses in the Prometheus datasource
FlagPrometheusWideSeries = "prometheusWideSeries"
@@ -174,4 +182,16 @@ const (
// FlagCloudMonitoringExperimentalUI
// Use grafana-experimental UI in Cloud Monitoring
FlagCloudMonitoringExperimentalUI = "cloudMonitoringExperimentalUI"
// FlagLogRequestsInstrumentedAsUnknown
// Logs the path for requests that are instrumented as unknown
FlagLogRequestsInstrumentedAsUnknown = "logRequestsInstrumentedAsUnknown"
// FlagDataConnectionsConsole
// Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins.
FlagDataConnectionsConsole = "dataConnectionsConsole"
// FlagInternationalization
// Enables work-in-progress internationalization
FlagInternationalization = "internationalization"
)
+10 -6
View File
@@ -125,7 +125,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
}
err := l.SQLStore.WithTransactionalDbSession(c, func(session *sqlstore.DBSession) error {
if err := l.requirePermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
return err
}
if _, err := session.Insert(&element); err != nil {
@@ -176,7 +176,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn
if err != nil {
return err
}
if err := l.requirePermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
if err := l.requireEditPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
return err
}
var connectionIDs []struct {
@@ -422,13 +422,13 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme
// FolderID was provided in the PATCH request
if toFolderID != -1 && toFolderID != fromFolderID {
if err := l.requirePermissionsOnFolder(ctx, user, toFolderID); err != nil {
if err := l.requireEditPermissionsOnFolder(ctx, user, toFolderID); err != nil {
return err
}
}
// Always check permissions for the folder where library element resides
if err := l.requirePermissionsOnFolder(ctx, user, fromFolderID); err != nil {
if err := l.requireEditPermissionsOnFolder(ctx, user, fromFolderID); err != nil {
return err
}
@@ -638,6 +638,10 @@ func (l *LibraryElementService) getElementsForDashboardID(c context.Context, das
// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard.
func (l *LibraryElementService) connectElementsToDashboardID(c context.Context, signedInUser *models.SignedInUser, elementUIDs []string, dashboardID int64) error {
if err := l.requireEditPermissionsOnDashboard(c, signedInUser, dashboardID); err != nil {
return err
}
err := l.SQLStore.WithTransactionalDbSession(c, func(session *sqlstore.DBSession) error {
_, err := session.Exec("DELETE FROM "+models.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
if err != nil {
@@ -648,7 +652,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c context.Context,
if err != nil {
return err
}
if err := l.requirePermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
if err := l.requireViewPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
return err
}
@@ -704,7 +708,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c context.Conte
folderID := folderUIDs[0].ID
if err := l.requirePermissionsOnFolder(c, signedInUser, folderID); err != nil {
if err := l.requireEditPermissionsOnFolder(c, signedInUser, folderID); err != nil {
return err
}
var connectionIDs []struct {
+38 -1
View File
@@ -23,7 +23,7 @@ func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) err
}
}
func (l *LibraryElementService) requirePermissionsOnFolder(ctx context.Context, user *models.SignedInUser, folderID int64) error {
func (l *LibraryElementService) requireEditPermissionsOnFolder(ctx context.Context, user *models.SignedInUser, folderID int64) error {
if isGeneralFolder(folderID) && user.HasRole(models.ROLE_EDITOR) {
return nil
}
@@ -48,3 +48,40 @@ func (l *LibraryElementService) requirePermissionsOnFolder(ctx context.Context,
return nil
}
func (l *LibraryElementService) requireViewPermissionsOnFolder(ctx context.Context, user *models.SignedInUser, folderID int64) error {
if isGeneralFolder(folderID) && user.HasRole(models.ROLE_VIEWER) {
return nil
}
folder, err := l.folderService.GetFolderByID(ctx, user, folderID, user.OrgId)
if err != nil {
return err
}
g := guardian.New(ctx, folder.Id, user.OrgId, user)
canView, err := g.CanView()
if err != nil {
return err
}
if !canView {
return models.ErrFolderAccessDenied
}
return nil
}
func (l *LibraryElementService) requireEditPermissionsOnDashboard(ctx context.Context, user *models.SignedInUser, dashboardID int64) error {
g := guardian.New(ctx, dashboardID, user.OrgId, user)
canEdit, err := g.CanEdit()
if err != nil {
return err
}
if !canEdit {
return models.ErrDashboardUpdateAccessDenied
}
return nil
}
@@ -48,6 +48,7 @@ func TestLibraryElementPermissions(t *testing.T) {
{models.ROLE_ADMIN, viewerOnlyPermissions, viewerOnlyDesc, 200},
{models.ROLE_ADMIN, everyonePermissions, everyoneDesc, 200},
{models.ROLE_ADMIN, noPermissions, noDesc, 200},
{models.ROLE_EDITOR, defaultPermissions, defaultDesc, 200},
{models.ROLE_EDITOR, adminOnlyPermissions, adminOnlyDesc, 403},
{models.ROLE_EDITOR, editorOnlyPermissions, editorOnlyDesc, 200},
@@ -55,6 +56,7 @@ func TestLibraryElementPermissions(t *testing.T) {
{models.ROLE_EDITOR, viewerOnlyPermissions, viewerOnlyDesc, 403},
{models.ROLE_EDITOR, everyonePermissions, everyoneDesc, 200},
{models.ROLE_EDITOR, noPermissions, noDesc, 403},
{models.ROLE_VIEWER, defaultPermissions, defaultDesc, 403},
{models.ROLE_VIEWER, adminOnlyPermissions, adminOnlyDesc, 403},
{models.ROLE_VIEWER, editorOnlyPermissions, editorOnlyDesc, 403},
+12 -2
View File
@@ -82,7 +82,12 @@ func loadLibraryPanelsRecursively(elements map[string]libraryelements.LibraryEle
elementInDB, ok := elements[UID]
if !ok {
elem := parent.Get("panels").GetIndex(i)
elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap())
gridPos := panelAsJSON.Get("gridPos").MustMap()
if gridPos == nil {
elem.Del("gridPos")
} else {
elem.Set("gridPos", gridPos)
}
elem.Set("id", panelAsJSON.Get("id").MustInt64())
elem.Set("type", fmt.Sprintf("Library panel with UID: \"%s\"", UID))
elem.Set("libraryPanel", map[string]interface{}{
@@ -111,7 +116,12 @@ func loadLibraryPanelsRecursively(elements map[string]libraryelements.LibraryEle
// set dashboard specific props
elem := parent.Get("panels").GetIndex(i)
elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap())
gridPos := panelAsJSON.Get("gridPos").MustMap()
if gridPos == nil {
elem.Del("gridPos")
} else {
elem.Set("gridPos", gridPos)
}
elem.Set("id", panelAsJSON.Get("id").MustInt64())
elem.Set("libraryPanel", map[string]interface{}{
"uid": elementInDB.UID,
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationLiveMessage(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
storage := SetupTestStorage(t)
getQuery := &models.GetLiveMessageQuery{
+2
View File
@@ -45,7 +45,9 @@ Scopes must have an order to ensure consistency and ease of search, this helps u
## Grafana Alerting - main / unreleased
- [FEATURE] use optimistic lock by version field when updating alert rules #50274
- [ENHANCEMENT] Scheduler: Drop ticks if rule evaluation is too slow and adds a metric grafana_alerting_schedule_rule_evaluations_missed_total to track missed evaluations per rule #48885
- [ENHANCEMENT] Ticker to tick at predictable time #50197
## 9.0.0
@@ -206,6 +206,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.R
}
func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response {
mt.Provenance = alerting_models.ProvenanceAPI
created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgId)
if err != nil {
if errors.Is(err, provisioning.ErrValidation) {
@@ -219,6 +220,7 @@ func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definit
func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response {
name := pathParam(c, namePathParam)
mt.Name = name
mt.Provenance = alerting_models.ProvenanceAPI
updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgId)
if err != nil {
if errors.Is(err, provisioning.ErrValidation) {
@@ -256,6 +258,9 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definiti
return ErrResp(http.StatusBadRequest, err, "")
}
if err != nil {
if errors.Is(err, store.ErrOptimisticLock) {
return ErrResp(http.StatusConflict, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "")
}
ar.ID = createdAlertRule.ID
@@ -273,6 +278,9 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar definitio
return ErrResp(http.StatusBadRequest, err, "")
}
if err != nil {
if errors.Is(err, store.ErrOptimisticLock) {
return ErrResp(http.StatusConflict, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "")
}
ar.Updated = updatedAlertRule.Updated
@@ -293,6 +301,9 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag defi
folderUID := pathParam(c, folderUIDPathParam)
err := srv.alertRules.UpdateRuleGroup(c.Req.Context(), c.OrgId, folderUID, rulegroup, ag.Interval)
if err != nil {
if errors.Is(err, store.ErrOptimisticLock) {
return ErrResp(http.StatusConflict, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "")
}
return response.JSON(http.StatusOK, ag)
+2
View File
@@ -467,6 +467,8 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
return ErrResp(http.StatusForbidden, err, "")
} else if errors.Is(err, ErrAuthorization) {
return ErrResp(http.StatusUnauthorized, err, "")
} else if errors.Is(err, store.ErrOptimisticLock) {
return ErrResp(http.StatusConflict, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
}
@@ -54,21 +54,43 @@ type AlertRulePayload struct {
}
type AlertRule struct {
ID int64 `json:"id"`
UID string `json:"uid"`
OrgID int64 `json:"orgID"`
FolderUID string `json:"folderUID"`
RuleGroup string `json:"ruleGroup"`
Title string `json:"title"`
Condition string `json:"condition"`
Data []models.AlertQuery `json:"data"`
Updated time.Time `json:"updated,omitempty"`
NoDataState models.NoDataState `json:"noDataState"`
ID int64 `json:"id"`
UID string `json:"uid"`
// required: true
OrgID int64 `json:"orgID"`
// required: true
// example: project_x
FolderUID string `json:"folderUID"`
// required: true
// minLength: 1
// maxLength: 190
// example: eval_group_1
RuleGroup string `json:"ruleGroup"`
// required: true
// minLength: 1
// maxLength: 190
// example: Always firing
Title string `json:"title"`
// required: true
// example: A
Condition string `json:"condition"`
// required: true
// example: [{"refId":"A","queryType":"","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}]
Data []models.AlertQuery `json:"data"`
// readonly: true
Updated time.Time `json:"updated,omitempty"`
// required: true
NoDataState models.NoDataState `json:"noDataState"`
// required: true
ExecErrState models.ExecutionErrorState `json:"execErrState"`
For time.Duration `json:"for"`
Annotations map[string]string `json:"annotations,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Provenance models.Provenance `json:"provenance,omitempty"`
// required: true
For time.Duration `json:"for"`
// example: {"runbook_url": "https://supercoolrunbook.com/page/13"}
Annotations map[string]string `json:"annotations,omitempty"`
// example: {"team": "sre-team-1"}
Labels map[string]string `json:"labels,omitempty"`
// readonly: true
Provenance models.Provenance `json:"provenance,omitempty"`
}
func (a *AlertRule) UpstreamModel() models.AlertRule {
@@ -50,7 +50,7 @@ import (
// swagger:parameters RoutePutContactpoint RouteDeleteContactpoints
type ContactPointUIDReference struct {
// ContactPointUID should be the contact point UID identifier
// UID should be the contact point unique identifier
// in:path
UID string
}
@@ -64,16 +64,24 @@ type ContactPointPayload struct {
// EmbeddedContactPoint is the contact point type that is used
// by grafanas embedded alertmanager implementation.
type EmbeddedContactPoint struct {
// UID is the unique identifier of the contact point. This will be
// automatically set be the Grafana.
// UID is the unique identifier of the contact point. The UID can be
// set by the user.
// example: my_external_reference
UID string `json:"uid"`
// Name is used as grouping key in the UI. Contact points with the
// same name will be grouped in the UI.
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
Settings *simplejson.Json `json:"settings" binding:"required"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Provenance string `json:"provenance"`
// example: webhook_1
Name string `json:"name" binding:"required"`
// required: true
// example: webhook
// enum: alertmanager, dingding, discord, email, googlechat, kafka, line, opsgenie, pagerduty, pushover, sensugo, slack, teams, telegram, threema, victorops, webhook, wecom
Type string `json:"type" binding:"required"`
// required: true
Settings *simplejson.Json `json:"settings" binding:"required"`
// example: false
DisableResolveMessage bool `json:"disableResolveMessage"`
// readonly: true
Provenance string `json:"provenance,omitempty"`
}
const RedactedValue = "[REDACTED]"
+128 -21
View File
@@ -200,14 +200,63 @@
"additionalProperties": {
"type": "string"
},
"example": {
"runbook_url": "https://supercoolrunbook.com/page/13"
},
"type": "object",
"x-go-name": "Annotations"
},
"condition": {
"example": "A",
"type": "string",
"x-go-name": "Condition"
},
"data": {
"example": [
{
"datasourceUid": "-100",
"model": {
"conditions": [
{
"evaluator": {
"params": [
0,
0
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": []
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "__expr__"
},
"expression": "1 == 1",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "A",
"type": "math"
},
"queryType": "",
"refId": "A",
"relativeTimeRange": {
"from": 0,
"to": 0
}
}
],
"items": {
"$ref": "#/definitions/AlertQuery"
},
@@ -215,9 +264,18 @@
"x-go-name": "Data"
},
"execErrState": {
"$ref": "#/definitions/ExecutionErrorState"
"description": "\nAlerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
"enum": [
"Alerting",
"Error",
"OK"
],
"type": "string",
"x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
"x-go-name": "ExecErrState"
},
"folderUID": {
"example": "project_x",
"type": "string",
"x-go-name": "FolderUID"
},
@@ -233,11 +291,22 @@
"additionalProperties": {
"type": "string"
},
"example": {
"team": "sre-team-1"
},
"type": "object",
"x-go-name": "Labels"
},
"noDataState": {
"$ref": "#/definitions/NoDataState"
"description": "\nAlerting Alerting\nNoData NoData\nOK OK",
"enum": [
"Alerting",
"NoData",
"OK"
],
"type": "string",
"x-go-enum-desc": "Alerting Alerting\nNoData NoData\nOK OK",
"x-go-name": "NoDataState"
},
"orgID": {
"format": "int64",
@@ -248,10 +317,16 @@
"$ref": "#/definitions/Provenance"
},
"ruleGroup": {
"example": "eval_group_1",
"maxLength": 190,
"minLength": 1,
"type": "string",
"x-go-name": "RuleGroup"
},
"title": {
"example": "Always firing",
"maxLength": 190,
"minLength": 1,
"type": "string",
"x-go-name": "Title"
},
@@ -261,10 +336,22 @@
},
"updated": {
"format": "date-time",
"readOnly": true,
"type": "string",
"x-go-name": "Updated"
}
},
"required": [
"orgID",
"folderUID",
"ruleGroup",
"title",
"condition",
"data",
"noDataState",
"execErrState",
"for"
],
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
@@ -673,15 +760,18 @@
"description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.",
"properties": {
"disableResolveMessage": {
"example": false,
"type": "boolean",
"x-go-name": "DisableResolveMessage"
},
"name": {
"description": "Name is used as grouping key in the UI. Contact points with the\nsame name will be grouped in the UI.",
"example": "webhook_1",
"type": "string",
"x-go-name": "Name"
},
"provenance": {
"readOnly": true,
"type": "string",
"x-go-name": "Provenance"
},
@@ -689,15 +779,41 @@
"$ref": "#/definitions/Json"
},
"type": {
"enum": [
"alertmanager",
" dingding",
" discord",
" email",
" googlechat",
" kafka",
" line",
" opsgenie",
" pagerduty",
" pushover",
" sensugo",
" slack",
" teams",
" telegram",
" threema",
" victorops",
" webhook",
" wecom"
],
"example": "webhook",
"type": "string",
"x-go-name": "Type"
},
"uid": {
"description": "UID is the unique identifier of the contact point. This will be\nautomatically set be the Grafana.",
"description": "UID is the unique identifier of the contact point. The UID can be\nset by the user.",
"example": "my_external_reference",
"type": "string",
"x-go-name": "UID"
}
},
"required": [
"type",
"settings"
],
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
@@ -748,10 +864,6 @@
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"EvalQueriesResponse": {},
"ExecutionErrorState": {
"type": "string",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
},
"ExtendedReceiver": {
"properties": {
"email_configs": {
@@ -1552,10 +1664,6 @@
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"NoDataState": {
"type": "string",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
},
"NotFound": {
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -3255,7 +3363,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@@ -3277,7 +3384,9 @@
"labels",
"receiver"
],
"type": "object"
"type": "object",
"x-go-name": "AlertGroup",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroups": {
"description": "AlertGroups alert groups",
@@ -3530,12 +3639,11 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence"
},
"type": "array",
"x-go-name": "GettableSilences",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
"type": "array"
},
"labelSet": {
"additionalProperties": {
@@ -3664,6 +3772,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@@ -3703,9 +3812,7 @@
"matchers",
"startsAt"
],
"type": "object",
"x-go-name": "PostableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
"type": "object"
},
"receiver": {
"properties": {
@@ -5676,7 +5783,7 @@
"operationId": "RouteDeleteContactpoints",
"parameters": [
{
"description": "ContactPointUID should be the contact point UID identifier",
"description": "UID should be the contact point unique identifier",
"in": "path",
"name": "UID",
"required": true,
@@ -5709,7 +5816,7 @@
"operationId": "RoutePutContactpoint",
"parameters": [
{
"description": "ContactPointUID should be the contact point UID identifier",
"description": "UID should be the contact point unique identifier",
"in": "path",
"name": "UID",
"required": true,
+138 -31
View File
@@ -1871,7 +1871,7 @@
"parameters": [
{
"type": "string",
"description": "ContactPointUID should be the contact point UID identifier",
"description": "UID should be the contact point unique identifier",
"name": "UID",
"in": "path",
"required": true
@@ -1912,7 +1912,7 @@
"parameters": [
{
"type": "string",
"description": "ContactPointUID should be the contact point UID identifier",
"description": "UID should be the contact point unique identifier",
"name": "UID",
"in": "path",
"required": true
@@ -2591,31 +2591,100 @@
},
"AlertRule": {
"type": "object",
"required": [
"orgID",
"folderUID",
"ruleGroup",
"title",
"condition",
"data",
"noDataState",
"execErrState",
"for"
],
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"x-go-name": "Annotations"
"x-go-name": "Annotations",
"example": {
"runbook_url": "https://supercoolrunbook.com/page/13"
}
},
"condition": {
"type": "string",
"x-go-name": "Condition"
"x-go-name": "Condition",
"example": "A"
},
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertQuery"
},
"x-go-name": "Data"
"x-go-name": "Data",
"example": [
{
"datasourceUid": "-100",
"model": {
"conditions": [
{
"evaluator": {
"params": [
0,
0
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": []
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "__expr__"
},
"expression": "1 == 1",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "A",
"type": "math"
},
"queryType": "",
"refId": "A",
"relativeTimeRange": {
"from": 0,
"to": 0
}
}
]
},
"execErrState": {
"$ref": "#/definitions/ExecutionErrorState"
"description": "\nAlerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
"type": "string",
"enum": [
"Alerting",
"Error",
"OK"
],
"x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
"x-go-name": "ExecErrState"
},
"folderUID": {
"type": "string",
"x-go-name": "FolderUID"
"x-go-name": "FolderUID",
"example": "project_x"
},
"for": {
"$ref": "#/definitions/Duration"
@@ -2630,10 +2699,21 @@
"additionalProperties": {
"type": "string"
},
"x-go-name": "Labels"
"x-go-name": "Labels",
"example": {
"team": "sre-team-1"
}
},
"noDataState": {
"$ref": "#/definitions/NoDataState"
"description": "\nAlerting Alerting\nNoData NoData\nOK OK",
"type": "string",
"enum": [
"Alerting",
"NoData",
"OK"
],
"x-go-enum-desc": "Alerting Alerting\nNoData NoData\nOK OK",
"x-go-name": "NoDataState"
},
"orgID": {
"type": "integer",
@@ -2645,11 +2725,17 @@
},
"ruleGroup": {
"type": "string",
"x-go-name": "RuleGroup"
"maxLength": 190,
"minLength": 1,
"x-go-name": "RuleGroup",
"example": "eval_group_1"
},
"title": {
"type": "string",
"x-go-name": "Title"
"maxLength": 190,
"minLength": 1,
"x-go-name": "Title",
"example": "Always firing"
},
"uid": {
"type": "string",
@@ -2658,7 +2744,8 @@
"updated": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
"x-go-name": "Updated",
"readOnly": true
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -3068,31 +3155,60 @@
"EmbeddedContactPoint": {
"description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.",
"type": "object",
"required": [
"type",
"settings"
],
"properties": {
"disableResolveMessage": {
"type": "boolean",
"x-go-name": "DisableResolveMessage"
"x-go-name": "DisableResolveMessage",
"example": false
},
"name": {
"description": "Name is used as grouping key in the UI. Contact points with the\nsame name will be grouped in the UI.",
"type": "string",
"x-go-name": "Name"
"x-go-name": "Name",
"example": "webhook_1"
},
"provenance": {
"type": "string",
"x-go-name": "Provenance"
"x-go-name": "Provenance",
"readOnly": true
},
"settings": {
"$ref": "#/definitions/Json"
},
"type": {
"type": "string",
"x-go-name": "Type"
"enum": [
"alertmanager",
" dingding",
" discord",
" email",
" googlechat",
" kafka",
" line",
" opsgenie",
" pagerduty",
" pushover",
" sensugo",
" slack",
" teams",
" telegram",
" threema",
" victorops",
" webhook",
" wecom"
],
"x-go-name": "Type",
"example": "webhook"
},
"uid": {
"description": "UID is the unique identifier of the contact point. This will be\nautomatically set be the Grafana.",
"description": "UID is the unique identifier of the contact point. The UID can be\nset by the user.",
"type": "string",
"x-go-name": "UID"
"x-go-name": "UID",
"example": "my_external_reference"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -3146,10 +3262,6 @@
"EvalQueriesResponse": {
"$ref": "#/definitions/EvalQueriesResponse"
},
"ExecutionErrorState": {
"type": "string",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
},
"ExtendedReceiver": {
"type": "object",
"properties": {
@@ -3951,10 +4063,6 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"NoDataState": {
"type": "string",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
},
"NotFound": {
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -5654,7 +5762,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
"description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@@ -5677,6 +5784,8 @@
"$ref": "#/definitions/receiver"
}
},
"x-go-name": "AlertGroup",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
@@ -5934,12 +6043,11 @@
"$ref": "#/definitions/gettableSilence"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"$ref": "#/definitions/gettableSilence"
},
"x-go-name": "GettableSilences",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableSilences"
},
"labelSet": {
@@ -6069,6 +6177,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",
@@ -6109,8 +6218,6 @@
"x-go-name": "StartsAt"
}
},
"x-go-name": "PostableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/postableSilence"
},
"receiver": {
+3 -1
View File
@@ -24,6 +24,7 @@ var (
ErrAlertRuleUniqueConstraintViolation = errors.New("a conflicting alert rule is found: rule title under the same organisation and folder should be unique")
)
// swagger:enum NoDataState
type NoDataState string
func (noDataState NoDataState) String() string {
@@ -49,6 +50,7 @@ const (
OK NoDataState = "OK"
)
// swagger:enum ExecutionErrorState
type ExecutionErrorState string
func (executionErrorState ExecutionErrorState) String() string {
@@ -109,7 +111,7 @@ type AlertRule struct {
Data []AlertQuery
Updated time.Time
IntervalSeconds int64
Version int64
Version int64 `xorm:"version"` // this tag makes xorm add optimistic lock (see https://xorm.io/docs/chapter-06/1.lock/)
UID string `xorm:"uid"`
NamespaceUID string `xorm:"namespace_uid"`
DashboardUID *string `xorm:"dashboard_uid"`
@@ -53,6 +53,9 @@ func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64,
return *query.Result, provenance, nil
}
// CreateAlertRule creates a new alert rule. This function will ignore any
// interval that is set in the rule struct and use the already existing group
// interval or the default one.
func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) {
if rule.UID == "" {
rule.UID = util.GenerateShortUID()
@@ -117,6 +120,9 @@ func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int6
})
}
// CreateAlertRule creates a new alert rule. This function will ignore any
// interval that is set in the rule struct and fetch the current group interval
// from database.
func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) {
storedRule, storedProvenance, err := service.GetAlertRule(ctx, rule.OrgID, rule.UID)
if err != nil {
@@ -34,7 +34,6 @@ type RuleStore interface {
ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) error
GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error)
InsertAlertRules(ctx context.Context, rule []models.AlertRule) (map[string]int64, error)
UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error
UpdateAlertRules(ctx context.Context, rule []store.UpdateRule) error
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
}
+13 -21
View File
@@ -34,6 +34,7 @@ type UpdateRule struct {
var (
ErrAlertRuleGroupNotFound = errors.New("rulegroup not found")
ErrOptimisticLock = errors.New("version conflict while updating a record in the database with optimistic locking")
)
// RuleStore is the interface for persisting alert rules and instances
@@ -47,8 +48,6 @@ type RuleStore interface {
// GetRuleGroups returns the unique rule groups across all organizations.
GetRuleGroups(ctx context.Context, query *ngmodels.ListRuleGroupsQuery) error
GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error)
// UpdateRuleGroup will update the interval for all rules in the group.
UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error
GetUserVisibleNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error)
GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error)
// InsertAlertRules will insert all alert rules passed into the function
@@ -95,7 +94,7 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI
})
}
// DeleteAlertInstanceByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
// DeleteAlertInstancesByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
func (st DBstore) DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Exec("DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid = ?", orgID, ruleUID)
@@ -207,7 +206,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
for _, r := range rules {
var parentVersion int64
r.New.ID = r.Existing.ID
r.New.Version = r.Existing.Version + 1
r.New.Version = r.Existing.Version // xorm will take care of increasing it (see https://xorm.io/docs/chapter-06/1.lock/)
if err := st.validateAlertRule(r.New); err != nil {
return err
}
@@ -215,11 +214,14 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
return err
}
// no way to update multiple rules at once
if _, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil {
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return ngmodels.ErrAlertRuleUniqueConstraintViolation
if updated, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil || updated == 0 {
if err != nil {
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return ngmodels.ErrAlertRuleUniqueConstraintViolation
}
return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err)
}
return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err)
return fmt.Errorf("%w: alert rule UID %s version %d", ErrOptimisticLock, r.New.UID, r.New.Version)
}
parentVersion = r.Existing.Version
ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{
@@ -228,7 +230,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
RuleNamespaceUID: r.New.NamespaceUID,
RuleGroup: r.New.RuleGroup,
ParentVersion: parentVersion,
Version: r.New.Version,
Version: r.New.Version + 1,
Created: r.New.Updated,
Condition: r.New.Condition,
Title: r.New.Title,
@@ -250,7 +252,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
})
}
// GetOrgAlertRules is a handler for retrieving alert rules of specific organisation.
// ListAlertRules is a handler for retrieving alert rules of specific organisation.
func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
q := sess.Table("alert_rule")
@@ -319,17 +321,7 @@ func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespa
})
}
func (st DBstore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Update(
ngmodels.AlertRule{IntervalSeconds: interval},
ngmodels.AlertRule{OrgID: orgID, RuleGroup: ruleGroup, NamespaceUID: namespaceUID},
)
return err
})
}
// GetNamespaces returns the folders that are visible to the user and have at least one alert in it
// GetUserVisibleNamespaces returns the folders that are visible to the user and have at least one alert in it
func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) {
namespaceMap := make(map[string]*models.Folder)
@@ -0,0 +1,91 @@
package store
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/exp/rand"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
func TestUpdateAlertRules(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
store := DBstore{
SQLStore: sqlStore,
BaseInterval: time.Duration(rand.Int63n(100)) * time.Second,
}
createRule := func(t *testing.T) *models.AlertRule {
t.Helper()
rule := models.AlertRuleGen(withIntervalMatching(store.BaseInterval))()
err := sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
_, err := sess.Table(models.AlertRule{}).InsertOne(rule)
if err != nil {
return err
}
dbRule := &models.AlertRule{}
exist, err := sess.Table(models.AlertRule{}).ID(rule.ID).Get(dbRule)
if err != nil {
return err
}
if !exist {
return errors.New("cannot read inserted record")
}
rule = dbRule
return nil
})
require.NoError(t, err)
return rule
}
t.Run("should increase version", func(t *testing.T) {
rule := createRule(t)
newRule := models.CopyRule(rule)
newRule.Title = util.GenerateShortUID()
err := store.UpdateAlertRules(context.Background(), []UpdateRule{{
Existing: rule,
New: *newRule,
},
})
require.NoError(t, err)
dbrule := &models.AlertRule{}
err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
exist, err := sess.Table(models.AlertRule{}).ID(rule.ID).Get(dbrule)
require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule.ID))
return err
})
require.NoError(t, err)
require.Equal(t, rule.Version+1, dbrule.Version)
})
t.Run("should fail due to optimistic locking if version does not match", func(t *testing.T) {
rule := createRule(t)
rule.Version-- // simulate version discrepancy
newRule := models.CopyRule(rule)
newRule.Title = util.GenerateShortUID()
err := store.UpdateAlertRules(context.Background(), []UpdateRule{{
Existing: rule,
New: *newRule,
},
})
require.ErrorIs(t, err, ErrOptimisticLock)
})
}
func withIntervalMatching(baseInterval time.Duration) func(*models.AlertRule) {
return func(rule *models.AlertRule) {
rule.IntervalSeconds = int64(baseInterval.Seconds()) * rand.Int63n(10)
rule.For = time.Duration(rule.IntervalSeconds*rand.Int63n(9)+1) * time.Second
}
}
@@ -12,6 +12,9 @@ import (
)
func TestIntegrationAlertManagerHash(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := sqlstore.InitTestDB(t)
store := &DBstore{
SQLStore: sqlStore,
+9
View File
@@ -38,6 +38,9 @@ func addToken(img *models.Image) *models.Image {
}
func TestIntegrationSaveAndGetImage(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
mockTimeNow()
ctx := context.Background()
_, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds)
@@ -91,6 +94,9 @@ func TestIntegrationSaveAndGetImage(t *testing.T) {
}
func TestIntegrationGetImages(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
mockTimeNow()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -141,6 +147,9 @@ func TestIntegrationGetImages(t *testing.T) {
}
func TestIntegrationDeleteExpiredImages(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
mockTimeNow()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

Some files were not shown because too many files have changed in this diff Show More