Merge branch 'mmandrus/refactor-secrets-plugin-proto-file' into smp-error-handling
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -72,7 +72,7 @@ Migration should just be a matter of going from
|
||||
|
||||
to
|
||||
|
||||
````tsx
|
||||
```tsx
|
||||
<ClipboardButton
|
||||
{/* other props... */}
|
||||
onClipboardCopy={(copiedText) => {
|
||||
|
||||
@@ -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.
|
||||
@@ -2,7 +2,7 @@
|
||||
title = "Performance considerations"
|
||||
description = "Understanding alerting performance"
|
||||
keywords = ["grafana", "alerting", "performance"]
|
||||
weight = 100
|
||||
weight = 555
|
||||
+++
|
||||
|
||||
# Alerting performance considerations
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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" >}}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 |
|
||||
| ------------------------------------------- | --------- | ---------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user