Merge branch 'main' into jackw/nx-task-orchestration

This commit is contained in:
Jack Westbrook
2024-02-13 15:31:33 +01:00
198 changed files with 6813 additions and 4148 deletions
+11 -40
View File
@@ -1296,16 +1296,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/core/components/help/HelpModal.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
],
"public/app/core/navigation/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -2598,10 +2588,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"]
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
@@ -2646,6 +2635,9 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/dashboard-scene/utils/test-utils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
@@ -3619,11 +3611,6 @@ exports[`better eslint`] = {
"public/app/features/explore/TraceView/components/common/CopyIcon.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/explore/TraceView/components/common/Divider.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/features/explore/TraceView/components/common/LabeledList.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
@@ -5750,17 +5737,11 @@ exports[`better eslint`] = {
],
"public/app/plugins/datasource/tempo/datasource.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/datasource/tempo/language_provider.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
@@ -6280,12 +6261,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "6"]
],
"public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"]
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
@@ -6296,11 +6272,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"]
],
"public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
+4
View File
@@ -1111,6 +1111,10 @@ global_file = 1000
# global limit of correlations
global_correlations = -1
# Limit of the number of alert rules per rule group.
# This is not strictly enforced yet, but will be enforced over time.
alerting_rule_group_rules = 100
#################################### Unified Alerting ####################
[unified_alerting]
# 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 when switching. When this configuration section and flag are not defined, the state is defined at runtime. See the documentation for more details.
+4
View File
@@ -1041,6 +1041,10 @@
# global limit of correlations
; global_correlations = -1
# Limit of the number of alert rules per rule group.
# This is not strictly enforced yet, but will be enforced over time.
;alerting_rule_group_rules = 100
#################################### Unified Alerting ####################
[unified_alerting]
#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.```
+21 -7
View File
@@ -6,6 +6,15 @@
# [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes.
# Changes are relevant to this script and the support docs.mk GNU Make interface.
#
# ## 5.4.0 (2024-02-12)
#
# ### Changed
#
# - Set `WEBSITE_MOUNTS=true` when a user includes the `website` project.
#
# Ensures consistent behavior across repositories.
# To disable website mounts, add `export WEBSITE_MOUNTS := false` to your `variables.mk` or `variables.mk.local` file.
# - Use website mounts and container volumes also when a user includes the `grafana-cloud` project.
#
# ## 5.3.0 (2024-02-08)
#
@@ -207,8 +216,6 @@ readonly DOC_VALIDATOR_SKIP_CHECKS="${DOC_VALIDATOR_SKIP_CHECKS:-^image-}"
readonly HUGO_REFLINKSERRORLEVEL="${HUGO_REFLINKSERRORLEVEL:-WARNING}"
readonly VALE_MINALERTLEVEL="${VALE_MINALERTLEVEL:-error}"
readonly WEBSITE_EXEC="${WEBSITE_EXEC:-make server-docs}"
# If set, the docs-base image will run a prebuild script that sets up Hugo mounts.
readonly WEBSITE_MOUNTS="${WEBSITE_MOUNTS:-}"
PODMAN="$(if command -v podman >/dev/null 2>&1; then echo podman; else echo docker; fi)"
@@ -425,7 +432,7 @@ proj_url() {
$1
POSIX_HERESTRING
if [ "${_project}" = 'website' ]; then
if [ "${_project}" = website ]; then
echo "http://localhost:${DOCS_HOST_PORT}/docs/"
unset _project _version
@@ -459,7 +466,7 @@ proj_dst() {
$1
POSIX_HERESTRING
if [ "${_project}" = 'website' ]; then
if [ "${_project}" = website ]; then
echo '/hugo/content'
unset _project _version
@@ -518,7 +525,7 @@ proj_canonical() {
$1
POSIX_HERESTRING
if [ "${_project}" = 'website' ]; then
if [ "${_project}" = website ]; then
echo '/docs'
unset _project _version
@@ -587,7 +594,7 @@ await_build() {
while [ "${i}" -ne "${max}" ]
do
sleep 1
debg "Retrying request to webserver assuming the process is still starting up."
debg "Retrying request to web server assuming the process is still starting up."
i=$((i + 1))
if ${req} "${url}"; then
@@ -640,9 +647,16 @@ for arg in "$@"; do
IFS=: read -r _project _ _repo _ <<POSIX_HERESTRING
${arg}
POSIX_HERESTRING
if [ "${_project}" = website ]; then
if [ "${_project}" = website ] || [ "${_project}" = grafana-cloud ]; then
note "Please be patient, building the website can take some time."
# If set, the docs-base image will run a prebuild script that sets up Hugo mounts.
if [ "${WEBSITE_MOUNTS}" = false ]; then
unset WEBSITE_MOUNTS
else
readonly WEBSITE_MOUNTS=true
fi
_repo="$(repo_path website)"
volumes="--volume=${_repo}/config:/hugo/config"
volumes="${volumes} --volume=${_repo}/layouts:/hugo/layouts"
+2 -2
View File
@@ -92,9 +92,9 @@ Here are some tips on how to create an effective alert management set up for you
- [Introduction to Alerting][fundamentals]
{{% docs/reference %}}
[alerting-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules"
[alerting-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules"
[alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules"
[fundamentals]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals"
[fundamentals]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals"
[fundamentals]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals"
{{% /docs/reference %}}
@@ -41,18 +41,18 @@ For information on how to configure contact points, see [Configure contact point
For information on how to configure notification policies, see [Configure notification policies][create-notification-policy].
{{% docs/reference %}}
[create-mimir-loki-managed-rule]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules/create-mimir-loki-managed-rule"
[create-mimir-loki-managed-rule]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-mimir-loki-managed-rule"
[create-mimir-loki-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-rule"
[create-mimir-loki-managed-recording-rule]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules/create-mimir-loki-managed-recording-rule"
[create-mimir-loki-managed-recording-rule]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-mimir-loki-managed-recording-rule"
[create-mimir-loki-managed-recording-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-recording-rule"
[create-grafana-managed-rule]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules/create-grafana-managed-rule"
[create-grafana-managed-rule]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-grafana-managed-rule"
[create-grafana-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule"
[manage-contact-points]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules/manage-contact-points"
[manage-contact-points]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/manage-contact-points"
[manage-contact-points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/manage-contact-points"
[create-notification-policy]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules/create-notification-policy"
[create-notification-policy]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-notification-policy"
[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy"
{{% /docs/reference %}}
@@ -224,21 +224,21 @@ Create alerts from any panel type. This means you can reuse the queries in the p
This will open the alert rule form, allowing you to configure and create your alert based on the current panel's query.
{{% docs/reference %}}
[add-a-query]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/query-transform-data#add-a-query"
[add-a-query]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/query-transform-data#add-a-query"
[add-a-query]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data#add-a-query"
[add-a-query]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data#add-a-query"
[alerting-on-numeric-data]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/evaluate-grafana-alerts#alerting-on-numeric-data-1"
[alerting-on-numeric-data]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/evaluate-grafana-alerts#alerting-on-numeric-data-1"
[alerting-on-numeric-data]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/evaluate-grafana-alerts#alerting-on-numeric-data-1"
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label"
[expression-queries]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/query-transform-data/expression-queries"
[expression-queries]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/query-transform-data/expression-queries"
[expression-queries]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/expression-queries"
[expression-queries]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/expression-queries"
[fundamentals]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals"
[fundamentals]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals"
[fundamentals]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals"
[time-units-and-relative-ranges]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/dashboards/use-dashboards#time-units-and-relative-ranges"
[time-units-and-relative-ranges]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/dashboards/use-dashboards#time-units-and-relative-ranges"
[time-units-and-relative-ranges]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/dashboards/use-dashboards#time-units-and-relative-ranges"
[time-units-and-relative-ranges]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/dashboards/use-dashboards#time-units-and-relative-ranges"
{{% /docs/reference %}}
@@ -65,9 +65,9 @@ To create recording rules, follow these steps.
1. Click **Save rule** to save the rule or **Save rule and exit** to save the rule and go back to the Alerting page.
{{% docs/reference %}}
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label"
[configure-grafana]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana"
[configure-grafana]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana"
[configure-grafana]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana"
[configure-grafana]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana"
{{% /docs/reference %}}
@@ -126,9 +126,9 @@ Annotations add metadata to provide more information on the alert in your alert
1. Click **Save rule**.
{{% docs/reference %}}
[alerting]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting"
[alerting]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting"
[alerting]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting"
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label"
{{% /docs/reference %}}
@@ -111,6 +111,6 @@ An example of an alert configuration.
- Create specific routes for particular teams that handle their own on-call rotations.
{{% docs/reference %}}
[notification-policies]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/notification-policies"
[notification-policies]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/notification-policies"
[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notification-policies"
{{% /docs/reference %}}
@@ -63,7 +63,7 @@ To set up the Grafana OnCall integration using the Grafana Alerting application,
This redirects you to the Grafana OnCall integration page in the Grafana OnCall application. From there, you can add [routes and escalation chains][escalation-chain].
{{% docs/reference %}}
[create-notification-policy]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules/create-notification-policy"
[create-notification-policy]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-notification-policy"
[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy"
[oncall-integration]: "/docs/grafana/ -> /docs/oncall/latest/integrations/grafana-alerting"
+1 -1
View File
@@ -2,7 +2,7 @@
_build:
list: false
aliases:
- ./unified-alerting/difference-old-new/ # /docs/grafana/<GRAFANA VERSION>/alerting/unified-alerting/difference-old-new/
- ./unified-alerting/difference-old-new/ # /docs/grafana/<GRAFANA_VERSION>/alerting/unified-alerting/difference-old-new/
canonical: https://grafana.com/docs/grafana/latest/alerting/difference-old-new/
description: Learn about how Grafana Alerting compares to legacy alerting
keywords:
+2 -2
View File
@@ -69,8 +69,8 @@ Silences and mute timings allow you to pause notifications for specific alerts o
You can create your alerting resources (alert rules, notification policies, and so on) in the Grafana UI; configmaps, files and configuration management systems using file-based provisioning; and in Terraform using API-based provisioning.
{{% docs/reference %}}
[external-alertmanagers]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/configure-alertmanager"
[external-alertmanagers]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-alertmanager"
[external-alertmanagers]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-alertmanager"
[notification-policies]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/notification-policies"
[notification-policies]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/notification-policies"
[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notification-policies"
{{% /docs/reference %}}
@@ -28,15 +28,15 @@ An interval specifies how frequently an alert rule is evaluated. Duration, when
- [Annotation and labels][annotation-label]
{{% docs/reference %}}
[alert-instances]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/alert-rules/alert-instances"
[alert-instances]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/alert-instances"
[alert-instances]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/alert-instances"
[alert-rule-types]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/alert-rules/alert-rule-types"
[alert-rule-types]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/alert-rule-types"
[alert-rule-types]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/alert-rule-types"
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/annotation-label"
[annotation-label]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label"
[organising-alerts]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/alert-rules/organising-alerts"
[organising-alerts]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/organising-alerts"
[organising-alerts]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/organising-alerts"
{{% /docs/reference %}}
@@ -149,9 +149,9 @@ To solve this problem, you can set a (custom) recovery threshold, which basicall
For example, you could set a threshold of 1000ms and a recovery threshold of 900ms. This way, an alert rule will only stop firing when it goes under 900ms and flapping is reduced.
{{% docs/reference %}}
[data-source-alerting]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/data-source-alerting"
[data-source-alerting]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/data-source-alerting"
[data-source-alerting]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/data-source-alerting"
[query-transform-data]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/query-transform-data"
[query-transform-data]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/query-transform-data"
[query-transform-data]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data"
[query-transform-data]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data"
{{% /docs/reference %}}
@@ -54,6 +54,6 @@ If you are provisioning your data source, set the flag `handleGrafanaManagedAler
[Add an external Alertmanager][configure-alertmanager]
{{% docs/reference %}}
[configure-alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/configure-alertmanager"
[configure-alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-alertmanager"
[configure-alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-alertmanager"
{{% /docs/reference %}}
@@ -48,6 +48,6 @@ It is also important to make sure that the label set for an alert does not have
Annotations are named pairs that add additional information to existing alerts. There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like custom labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. If an annotation contains template code, the template is evaluated once when the alert is fired. It is not re-evaluated, even when the alert is resolved. Documentation on how to template annotations can be found [here][variables-label-annotation].
{{% docs/reference %}}
[variables-label-annotation]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/annotation-label/variables-label-annotation"
[variables-label-annotation]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/annotation-label/variables-label-annotation"
[variables-label-annotation]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label/variables-label-annotation"
{{% /docs/reference %}}
@@ -55,9 +55,9 @@ Grafana reserved labels can be used in the same way as manually configured label
| grafana_folder | Title of the folder containing the alert. |
{{% docs/reference %}}
[alerting-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules"
[alerting-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules"
[alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules"
[unified-alerting-reserved-labels]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana#unified_alertingreserved_labels"
[unified-alerting-reserved-labels]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana#unified_alertingreserved_labels"
[unified-alerting-reserved-labels]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana#unified_alertingreserved_labels"
[unified-alerting-reserved-labels]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana#unified_alertingreserved_labels"
{{% /docs/reference %}}
@@ -445,6 +445,6 @@ example.com:8080
```
{{% docs/reference %}}
[explore]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/explore"
[explore]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/explore"
[explore]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/explore"
[explore]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/explore"
{{% /docs/reference %}}
@@ -42,54 +42,54 @@ These are the data sources that are compatible with and supported by Grafana Ale
- [Grafana data sources][]
{{% docs/reference %}}
[Grafana data sources]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources"
[Grafana data sources]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources"
[Grafana data sources]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources"
[Grafana data sources]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources"
[AWS CloudWatch]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/aws-cloudwatch"
[AWS CloudWatch]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/aws-cloudwatch"
[AWS CloudWatch]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch"
[AWS CloudWatch]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/aws-cloudwatch"
[Azure Monitor]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/azure-monitor"
[Azure Monitor]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/azure-monitor"
[Azure Monitor]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/azure-monitor"
[Azure Monitor]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/azure-monitor"
[Elasticsearch]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/elasticsearch"
[Elasticsearch]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/elasticsearch"
[Elasticsearch]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/elasticsearch"
[Elasticsearch]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/elasticsearch"
[Google Cloud Monitoring]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/google-cloud-monitoring"
[Google Cloud Monitoring]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/google-cloud-monitoring"
[Google Cloud Monitoring]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring"
[Google Cloud Monitoring]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring"
[Graphite]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/graphite"
[Graphite]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/graphite"
[Graphite]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/graphite"
[Graphite]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/graphite"
[InfluxDB]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/influxdb"
[InfluxDB]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/influxdb"
[InfluxDB]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/influxdb"
[InfluxDB]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/influxdb"
[Loki]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/loki"
[Loki]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/loki"
[Loki]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/loki"
[Loki]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/loki"
[Microsoft SQL Server (MSSQL)]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/mssql"
[Microsoft SQL Server (MSSQL)]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/mssql"
[Microsoft SQL Server (MSSQL)]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/mssql"
[Microsoft SQL Server (MSSQL)]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/mssql"
[MySQL]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/mysql"
[MySQL]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/mysql"
[MySQL]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/mysql"
[MySQL]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/mysql"
[Open TSDB]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/opentsdb"
[Open TSDB]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/opentsdb"
[Open TSDB]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/opentsdb"
[Open TSDB]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/opentsdb"
[PostgreSQL]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/postgres"
[PostgreSQL]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/postgres"
[PostgreSQL]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/postgres"
[PostgreSQL]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/postgres"
[Prometheus]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/prometheus"
[Prometheus]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/prometheus"
[Prometheus]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/prometheus"
[Prometheus]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/prometheus"
[Jaeger]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/jaeger"
[Jaeger]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/jaeger"
[Jaeger]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/jaeger"
[Jaeger]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/jaeger"
[Zipkin]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/zipkin"
[Zipkin]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/zipkin"
[Zipkin]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/zipkin"
[Zipkin]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/zipkin"
[Tempo]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/tempo"
[Tempo]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/tempo"
[Tempo]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/tempo"
[Tempo]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/tempo"
[Testdata]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/testdata"
[Testdata]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/testdata"
[Testdata]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/testdata"
[Testdata]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/testdata"
{{% /docs/reference %}}
@@ -109,6 +109,6 @@ When this query is used as the **condition** in an alert rule, then the non-zero
{{% docs/reference %}}
[set-up-grafana-monitoring]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/set-up-grafana-monitoring"
[set-up-grafana-monitoring]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/set-up-grafana-monitoring"
[set-up-grafana-monitoring]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/set-up-grafana-monitoring"
[set-up-grafana-monitoring]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/set-up-grafana-monitoring"
{{% /docs/reference %}}
@@ -38,6 +38,6 @@ It is important to make sure that gossiping is configured and tested. You can fi
[Configure alerting high availability][configure-high-availability]
{{% docs/reference %}}
[configure-high-availability]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/configure-high-availability"
[configure-high-availability]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-high-availability"
[configure-high-availability]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-high-availability"
{{% /docs/reference %}}
@@ -132,6 +132,6 @@ Repeat interval decides how often notifications are repeated if the group has no
**Default** 4 hours
{{% docs/reference %}}
[labels-and-label-matchers]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/annotation-label/labels-and-label-matchers"
[labels-and-label-matchers]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/annotation-label/labels-and-label-matchers"
[labels-and-label-matchers]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label/labels-and-label-matchers"
{{% /docs/reference %}}
@@ -30,18 +30,18 @@ Once you have set up your alert rules, contact points, and notification policies
[View and filter alert rules][view-alert-rules]
{{% docs/reference %}}
[create-silence]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/create-silence"
[create-silence]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/create-silence"
[create-silence]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/create-silence"
[declare-incident-from-firing-alert]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/declare-incident-from-alert"
[declare-incident-from-firing-alert]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/declare-incident-from-alert"
[declare-incident-from-firing-alert]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/declare-incident-from-alert"
[mute-timings]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/mute-timings"
[mute-timings]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/mute-timings"
[mute-timings]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/mute-timings"
[view-alert-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/view-alert-rules"
[view-alert-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/view-alert-rules"
[view-alert-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-alert-rules"
[view-state-health]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/view-state-health"
[view-state-health]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/view-state-health"
[view-state-health]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-state-health"
{{% /docs/reference %}}
@@ -139,9 +139,9 @@ For example, if a screenshot could not be taken within the expected time (10 sec
- `grafana_screenshot_upload_successes_total`
{{% docs/reference %}}
[image-rendering]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/image-rendering"
[image-rendering]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/image-rendering"
[image-rendering]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/image-rendering"
[image-rendering]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/image-rendering"
[paths]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana#paths"
[paths]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana#paths"
[paths]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana#paths"
[paths]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana#paths"
{{% /docs/reference %}}
@@ -81,9 +81,9 @@ If you want to specify an exact duration, specify all the options. For example,
- Days of the month: `1:7`
{{% docs/reference %}}
[datasources/alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/alertmanager"
[datasources/alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/alertmanager"
[datasources/alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/alertmanager"
[datasources/alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/alertmanager"
[fundamentals/alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/alertmanager"
[fundamentals/alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alertmanager"
[fundamentals/alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alertmanager"
{{% /docs/reference %}}
@@ -52,12 +52,12 @@ Use notification templates to send notifications to your contact points.
Data that is available when writing templates.
{{% docs/reference %}}
[reference]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/reference"
[reference]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/reference"
[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference"
[use-notification-templates]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/use-notification-templates"
[use-notification-templates]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/use-notification-templates"
[use-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/use-notification-templates"
[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/using-go-templating-language"
[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/using-go-templating-language"
[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language"
{{% /docs/reference %}}
@@ -35,9 +35,9 @@ In the Contact points tab, you can see a list of your contact points.
1. Click **Save contact point**.
{{% docs/reference %}}
[create-notification-templates]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/create-notification-templates"
[create-notification-templates]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/create-notification-templates"
[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates"
[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/using-go-templating-language"
[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/using-go-templating-language"
[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language"
{{% /docs/reference %}}
@@ -280,12 +280,12 @@ grafana_folder = "Test alerts"
```
{{% docs/reference %}}
[create-notification-templates]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/create-notification-templates"
[create-notification-templates]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/create-notification-templates"
[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates"
[extendeddata]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/reference#extendeddata"
[extendeddata]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/reference#extendeddata"
[extendeddata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference#extendeddata"
[reference]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/reference"
[reference]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/template-notifications/reference"
[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference"
{{% /docs/reference %}}
+7 -10
View File
@@ -54,27 +54,24 @@ Grafana Alerting supports many additional configuration options, from configurin
The following topics provide you with advanced configuration options for Grafana Alerting.
- [Provision alert rules using file provisioning][file-provisioning]
- [Provision alert rules using file provisioning](/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning)
- [Provision alert rules using Terraform][terraform-provisioning]
- [Add an external Alertmanager][configure-alertmanager]
- [Configure high availability][configure-high-availability]
{{% docs/reference %}}
[configure-alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/configure-alertmanager"
[configure-alertmanager]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-alertmanager"
[configure-alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-alertmanager"
[configure-high-availability]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/configure-high-availability"
[configure-high-availability]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-high-availability"
[configure-high-availability]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-high-availability"
[data-source-alerting]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/fundamentals/data-source-alerting"
[data-source-alerting]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/data-source-alerting"
[data-source-alerting]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/data-source-alerting"
[data-source-management]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/administration/data-source-management"
[data-source-management]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/administration/data-source-management"
[data-source-management]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management"
[data-source-management]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management"
[file-provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning"
[file-provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/file-provisioning"
[terraform-provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/provision-alerting-resources/terraform-provisioning"
[terraform-provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/terraform-provisioning"
[terraform-provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning"
{{% /docs/reference %}}
@@ -1,6 +1,6 @@
---
aliases:
- ../migrating-alerts/ # /docs/grafana/<GRAFANA VERSION>/alerting/migrating-alerts/
- ../migrating-alerts/ # /docs/grafana/<GRAFANA_VERSION>/alerting/migrating-alerts/
canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/migrating-alerts/
description: Upgrade to Grafana Alerting
labels:
@@ -52,9 +52,9 @@ Refer to our [upgrade instructions][migrating-alerts].
- [Angular support deprecation][angular_deprecation]
{{% docs/reference %}}
[angular_deprecation]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/developers/angular_deprecation"
[angular_deprecation]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/developers/angular_deprecation"
[angular_deprecation]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/developers/angular_deprecation"
[angular_deprecation]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/developers/angular_deprecation"
[migrating-alerts]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/set-up/migrating-alerts"
[migrating-alerts]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/migrating-alerts"
[migrating-alerts]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/migrating-alerts"
{{% /docs/reference %}}
@@ -2,7 +2,7 @@
aliases:
- ../provision-alerting-resources/
canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/
description: Import and export alerting resources
description: Provision alerting resources
keywords:
- grafana
- alerting
@@ -14,11 +14,11 @@ labels:
- cloud
- enterprise
- oss
title: Import and export Grafana Alerting resources
title: Provision Alerting resources
weight: 300
---
# Import and export Grafana Alerting resources
# Provision Alerting resources
Alerting infrastructure is often complex, with many pieces of the pipeline that often live in different places. Scaling this across multiple teams and organizations is an especially challenging task. Importing and exporting (or provisioning) your alerting resources in Grafana Alerting makes this process easier by enabling you to create, manage, and maintain your alerting data in a way that best suits your organization.
@@ -26,117 +26,56 @@ You can import alert rules, contact points, notification policies, mute timings,
You cannot edit imported alerting resources in the Grafana UI in the same way as alerting resources that were not imported. You can only edit imported contact points, notification policies, templates, and mute timings in the source where they were created. For example, if you manage your alerting resources using files from disk, you cannot edit the data in Terraform or from within Grafana.
## Import alerting resources
Choose from the options below to import (or provision) your Grafana Alerting resources.
1. [Use configuration files to provision your alerting resources](/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning), such as alert rules and contact points, through files on disk.
{{< admonition type="note" >}}
File provisioning is not available in Grafana Cloud instances.
{{< /admonition >}}
1. Use [Terraform to provision alerting resources][alerting_tf_provisioning].
1. Use the [Alerting provisioning HTTP API][alerting_http_provisioning] to manage alerting resources.
{{< admonition type="note" >}}
The JSON output from the majority of Alerting HTTP endpoints isn't compatible for provisioning via configuration files.
Instead, use the [Export Alerting endpoints](/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints) to return or download the alerting resources in provisioning format.
{{< /admonition >}}
## Export alerting resources
You can export both manually created and provisioned alerting resources. For more information, refer to [Export alerting resources][alerting_export].
To modify imported alert rules, you can use the **Modify export** feature to edit and then export.
Choose from the options below to import your Grafana Alerting resources.
## View provisioned alerting resources
1. Use file provisioning to manage your Grafana Alerting resources, such as alert rules and contact points, through files on disk.
To view your provisioned resources in Grafana, complete the following steps.
{{% admonition type="note" %}}
File provisioning is not available in Grafana Cloud instances.
{{% /admonition %}}
1. Open your Grafana instance.
1. Navigate to Alerting.
1. Click an alerting resource folder, for example, Alert rules.
2. Use the Alerting Provisioning HTTP API.
For more information on the Alerting Provisioning HTTP API, refer to [Alerting provisioning HTTP API][alerting_provisioning].
Here is a ready-to-use template for alert rules:
#### Alert rules template
```
{
"title": "TEST-API_1",
"ruleGroup": "API",
"folderUID": "FOLDER",
"noDataState": "OK",
"execErrState": "OK",
"for": "5m",
"orgId": 1,
"uid": "",
"condition": "B",
"annotations": {
"summary": "test_api_1"
},
"labels": {
"API": "test1"
},
"data": [
{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 600,
"to": 0
},
"datasourceUid": " XXXXXXXXX-XXXXXXXXX-XXXXXXXXXX",
"model": {
"expr": "up",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "A"
}
},
{
"refId": "B",
"queryType": "",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "-100",
"model": {
"conditions": [
{
"evaluator": {
"params": [
6
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A"
]
},
"reducer": {
"params": [],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "-100"
},
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "B",
"type": "classic_conditions"
}
}
]
}
```
3. Use [Terraform](https://www.terraform.io/).
Provisioned resources are labeled **Provisioned**, so that it is clear that they were not created manually.
**Useful Links:**
[Grafana provisioning][provisioning]
[Grafana Alerting provisioning API][alerting_provisioning]
{{% docs/reference %}}
[alerting_provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/developers/http_api/alerting_provisioning"
[alerting_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/developers/http_api/alerting_provisioning"
[alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/terraform-provisioning"
[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/terraform-provisioning"
[alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning"
[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning"
[alerting_export]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources"
[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources"
[provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/administration/provisioning"
[provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/administration/provisioning"
[alerting_export_http]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints"
[alerting_export_http]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints"
[provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/administration/provisioning"
[provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/administration/provisioning"
{{% /docs/reference %}}
@@ -0,0 +1,106 @@
---
aliases:
- ../../provision-alerting-resources/view-provisioned-resources/
- ./view-provisioned-resources/
canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/export-alerting-resources/
description: Export alerting resources in Grafana
keywords:
- grafana
- alerting
- alerting resources
- provisioning
labels:
products:
- cloud
- enterprise
- oss
title: Export alerting resources
weight: 300
---
# Export alerting resources
Export your alerting resources, such as alert rules, contact points, and notification policies for provisioning, automatically importing single folders and single groups.
The export options listed below enable you to download resources in YAML, JSON, or Terraform format, facilitating their provisioning through [configuration files](/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning) or [Terraform][alerting_tf_provisioning].
## Export alert rules
To export alert rules from the Grafana UI, complete the following steps.
1. Click **Alerts & IRM** -> **Alert rules**.
1. To export all Grafana-managed rules, click **Export rules**.
1. To export a folder, change the **View as** to **List**.
1. Select the folder you want to export and click the **Export rules folder** icon.
1. To export a group, change the **View as** to **Grouped**.
1. Find the group you want to export and click the **Export rule group** icon.
1. Choose the format to export in.
The exported rule data appears in different formats - YAML, JSON, Terraform.
1. Click **Copy Code** or **Download**.
a. Choose **Copy Code** to go to an existing file and paste in the code.
b. Choose **Download** to download a file with the exported data.
## Modify and export alert rules without saving changes
Use the **Modify export** mode to edit and export an alert rule without updating it.
{{% admonition type="note" %}} This feature is for Grafana-managed alert rules only. It is available to Admin, Viewer, and Editor roles. {{% /admonition %}}
To export a modified alert rule without saving the modifications, complete the following steps from the Grafana UI.
1. Click **Alerts & IRM** -> **Alert rules**.
1. Locate the alert rule you want to edit and click **More** -> **Modify Export** to open the Alert Rule form.
1. From the Alert Rule form, edit the fields you want to change. Changes made are not applied to the alert rule.
1. Click **Export**.
1. Choose the format to export in.
The exported rule data appears in different formats - YAML, JSON, Terraform.
1. Click **Copy Code** or **Download**.
a. Choose **Copy Code** to go to an existing file and paste in the code.
b. Choose **Download** to download a file with the exported data.
## Export API endpoints
You can also use the **Alerting provisioning HTTP API** to export alerting resources in YAML or JSON formats for provisioning.
Note that most Alerting endpoints return a JSON format that is not compatible for provisioning via configuration files, except the ones listed below.
| Method | URI | Summary |
| ------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| GET | /api/v1/provisioning/alert-rules/:uid/export | [Export an alert rule in provisioning file format.][export_rule] |
| GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export | [Export an alert rule group in provisioning file format.][export_rule_group] |
| GET | /api/v1/provisioning/alert-rules/export | [Export all alert rules in provisioning file format.][export_rules] |
| GET | /api/v1/provisioning/contact-points/export | [Export all contact points in provisioning file format.][export_contacts] |
| GET | /api/v1/provisioning/policies/export | [Export the notification policy tree in provisioning file format.][export_notifications] |
These endpoints accept a `download` parameter to download a file containing the exported resources.
{{% docs/reference %}}
[alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/terraform-provisioning"
[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/terraform-provisioning"
[alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning"
[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning"
[export_rule]: "/docs/grafana/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-\_routegetalertruleexport*"
[export_rule]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-\_routegetalertruleexport*"
[export_rule_group]: "/docs/grafana/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-\_routegetalertrulegroupexport*"
[export_rule_group]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-\_routegetalertrulegroupexport*"
[export_rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-\_routegetalertrulesexport*"
[export_rules]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-\_routegetalertrulesexport*"
[export_contacts]: "/docs/grafana/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-\_routegetcontactpointsexport*"
[export_contacts]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-\_routegetcontactpointsexport*"
[export_notifications]: "/docs/grafana/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-\_routegetpolicytreeexport*"
[export_notifications]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA*VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-\_routegetpolicytreeexport*"
{{% /docs/reference %}}
@@ -11,14 +11,14 @@ keywords:
- provisioning
labels:
products:
- cloud
- enterprise
- oss
title: Use file provisioning to manage alerting resources
menuTitle: Use configuration files to provision
title: Use configuration files to provision alerting resources
weight: 100
---
## Use file provisioning to manage alerting resources
# Use configuration files to provision alerting resources
Manage your alerting resources using files from disk. When you start Grafana, the data from these files is created in your Grafana system. Grafana adds any new resources you created, updates any that you changed, and deletes old ones.
@@ -26,26 +26,28 @@ Arrange your files in a directory in a way that best suits your use case. For ex
Details on how to set up the files and which fields are required for each object are listed below depending on which resource you are provisioning.
**Note:**
For a complete guide about how Grafana provisions resources, refer to the [Provision Grafana][provisioning] documentation.
Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API][reload-provisioning-configurations].
{{< admonition type="note" >}}
### Import alert rules
- You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out.
- Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](/docs/grafana/<GRAFANA_VERSION>/developers/http_api/admin#reload-provisioning-configurations).
- Importing an existing alerting resource results in a conflict. First, when present, remove the resources you plan to import.
{{< /admonition >}}
## Import alert rules
Create or delete alert rules in your Grafana instance(s).
1. Create alert rules in Grafana.
1. Use the [Alerting provisioning API][alerting_provisioning] export endpoints to download a provisioning file for your alert rules.
1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory.
1. [Export][alerting_export] and download a provisioning file for your alert rules.
1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory.
Example configuration files can be found below.
1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s).
1. Delete the alert rules in Grafana that are going to be imported.
**Note:**
If you do not delete the alert rule, it will clash with the imported alert rule once uploaded.
1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
Here is an example of a configuration file for creating alert rules.
@@ -134,17 +136,17 @@ deleteRules:
uid: my_id_1
```
### Import contact points
## Import contact points
Create or delete contact points in your Grafana instance(s).
1. Create a contact point in Grafana.
1. Use the [Alerting provisioning API][alerting_provisioning] export endpoints to download a provisioning file for your contact point.
1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory.
1. [Export][alerting_export] and download a provisioning file for your contact point.
1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory.
Example configuration files can be found below.
1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s).
1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
Here is an example of a configuration file for creating contact points.
@@ -184,12 +186,14 @@ deleteContactPoints:
uid: first_uid
```
#### Settings
### Settings
Here are some examples of settings you can use for the different
contact point integrations.
##### Alertmanager
{{< collapse title="Alertmanager" >}}
#### Alertmanager
```yaml
type: prometheus-alertmanager
@@ -202,7 +206,11 @@ settings:
basicAuthPassword: abc123
```
##### DingDing
{{< /collapse >}}
{{< collapse title="DingDing" >}}
#### DingDing
```yaml
type: dingding
@@ -216,7 +224,11 @@ settings:
{{ template "default.message" . }}
```
##### Discord
{{< /collapse >}}
{{< collapse title="Discord" >}}
#### Discord
```yaml
type: discord
@@ -232,7 +244,11 @@ settings:
{{ template "default.message" . }}
```
##### E-Mail
{{< /collapse >}}
{{< collapse title="E-Mail" >}}
#### E-Mail
```yaml
type: email
@@ -248,7 +264,11 @@ settings:
{{ template "default.title" . }}
```
##### Google Chat
{{< /collapse >}}
{{< collapse title="Google Chat" >}}
#### Google Chat
```yaml
type: googlechat
@@ -260,7 +280,11 @@ settings:
{{ template "default.message" . }}
```
##### Kafka
{{< /collapse >}}
{{< collapse title="Kafka" >}}
#### Kafka
```yaml
type: kafka
@@ -271,7 +295,11 @@ settings:
kafkaTopic: topic1
```
##### LINE
{{< /collapse >}}
{{< collapse title="LINE" >}}
#### LINE
```yaml
type: line
@@ -280,7 +308,11 @@ settings:
token: xxx
```
##### Microsoft Teams
{{< /collapse >}}
{{< collapse title="Microsoft Teams" >}}
#### Microsoft Teams
```yaml
type: teams
@@ -297,7 +329,11 @@ settings:
{{ template "default.message" . }}
```
##### OpsGenie
{{< /collapse >}}
{{< collapse title="OpsGenie" >}}
#### OpsGenie
```yaml
type: opsgenie
@@ -319,7 +355,11 @@ settings:
sendTagsAs: both
```
##### PagerDuty
{{< /collapse >}}
{{< collapse title="PagerDuty" >}}
#### PagerDuty
```yaml
type: pagerduty
@@ -339,7 +379,11 @@ settings:
{{ template "default.message" . }}
```
##### Pushover
{{< /collapse >}}
{{< collapse title="Pushover" >}}
#### Pushover
```yaml
type: pushover
@@ -367,7 +411,11 @@ settings:
{{ template "default.message" . }}
```
##### Slack
{{< /collapse >}}
{{< collapse title="Slack" >}}
#### Slack
```yaml
type: slack
@@ -399,7 +447,11 @@ settings:
{{ template "slack.default.text" . }}
```
##### Sensu Go
{{< /collapse >}}
{{< collapse title="Sensu Go" >}}
#### Sensu Go
```yaml
type: sensugo
@@ -421,7 +473,11 @@ settings:
{{ template "default.message" . }}
```
##### Telegram
{{< /collapse >}}
{{< collapse title="Telegram" >}}
#### Telegram
```yaml
type: telegram
@@ -435,7 +491,11 @@ settings:
{{ template "default.message" . }}
```
##### Threema Gateway
{{< /collapse >}}
{{< collapse title="Threema Gateway" >}}
#### Threema Gateway
```yaml
type: threema
@@ -448,7 +508,11 @@ settings:
recipient_id: A9R4KL4S
```
##### VictorOps
{{< /collapse >}}
{{< collapse title="VictorOps" >}}
#### VictorOps
```yaml
type: victorops
@@ -459,7 +523,11 @@ settings:
messageType: CRITICAL
```
##### Webhook
{{< /collapse >}}
{{< collapse title="Webhook" >}}
#### Webhook
```yaml
type: webhook
@@ -480,7 +548,11 @@ settings:
maxAlerts: '10'
```
##### WeCom
{{< /collapse >}}
{{< collapse title="WeCom" >}}
#### WeCom
```yaml
type: wecom
@@ -495,17 +567,27 @@ settings:
{{ template "default.title" . }}
```
### Import notification policies
{{< /collapse >}}
## Import notification policies
Create or reset the notification policy tree in your Grafana instance(s).
In Grafana, the entire notification policy tree is considered a single, large resource. Add new specific policies as sub-policies under the root policy. Since specific policies may depend on each other, you cannot provision subsets of the policy tree; the entire tree must be defined in a single place.
{{% admonition type="warning" %}}
Since the policy tree is a single resource, provisioning it will overwrite a policy tree created through any other means.
{{< /admonition >}}
1. Create a notification policy in Grafana.
1. Use the [Alerting provisioning API][alerting_provisioning] export endpoints to download a provisioning file for your notification policy.
1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory.
1. [Export][alerting_export] and download a provisioning file for your notification policy.
1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory.
Example configuration files can be found below.
1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s).
1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
Here is an example of a configuration file for creating notification policies.
@@ -581,13 +663,7 @@ resetPolicies:
- 1
```
**Note:**
In Grafana, the entire notification policy tree is considered a single, large resource. Add new specific policies as sub-policies under the root policy. Since specific policies may depend on each other, you cannot provision subsets of the policy tree; the entire tree must be defined in a single place.
Since the policy tree is a single resource, applying it will overwrite a policy tree created through any other means.
### Import templates
## Import templates
Create or delete templates in your Grafana instance(s).
@@ -595,7 +671,7 @@ Create or delete templates in your Grafana instance(s).
Example configuration files can be found below.
2. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
Here is an example of a configuration file for creating templates.
@@ -627,7 +703,7 @@ deleteTemplates:
name: my_first_template
```
### Import mute timings
## Import mute timings
Create or delete mute timings in your Grafana instance(s).
@@ -676,65 +752,68 @@ deleteMuteTimes:
name: mti_1
```
### File provisioning using Kubernetes
## File provisioning using Kubernetes
If you are a Kubernetes user, you can leverage file provisioning using Kubernetes configuration maps.
1. Create one or more configuration maps as follows.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-alerting
data:
provisioning.yaml: |
templates:
- name: my_first_template
template: the content for my template
```
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-alerting
data:
provisioning.yaml: |
templates:
- name: my_first_template
template: the content for my template
```
2. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s).
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
name: grafana
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:latest
ports:
- name: grafana
containerPort: 3000
volumeMounts:
- mountPath: /etc/grafana/provisioning/alerting
name: grafana-alerting
readOnly: false
volumes:
- name: grafana-alerting
configMap:
defaultMode: 420
name: grafana-alerting
```
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
name: grafana
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:latest
ports:
- name: grafana
containerPort: 3000
volumeMounts:
- mountPath: /etc/grafana/provisioning/alerting
name: grafana-alerting
readOnly: false
volumes:
- name: grafana-alerting
configMap:
defaultMode: 420
name: grafana-alerting
```
This eliminates the need for a persistent database to use Grafana Alerting in Kubernetes; all your provisioned resources appear after each restart or re-deployment. Grafana still requires a database for normal operation, you do not need to persist the contents of the database between restarts if all objects are provisioned using files.
{{% docs/reference %}}
[alerting_provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/developers/http_api/alerting_provisioning"
[alerting_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/developers/http_api/alerting_provisioning"
**Useful Links:**
[reload-provisioning-configurations]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/developers/http_api/admin#reload-provisioning-configurations"
[reload-provisioning-configurations]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/developers/http_api/admin#reload-provisioning-configurations"
[Grafana provisioning][provisioning]
{{% docs/reference %}}
[alerting_export]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources"
[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources"
[provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/administration/provisioning"
[provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/administration/provisioning"
{{% /docs/reference %}}
@@ -0,0 +1,20 @@
---
canonical: https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/
description: Create and manage alerting resources using the HTTP API
keywords:
- grafana
- alerting
- alerting resources
- provisioning
labels:
products:
- cloud
- enterprise
- oss
title: Use the HTTP API to manage alerting resources
weight: 400
---
# Use the HTTP API to manage alerting resources
{{< docs/shared lookup="alerts/alerting_provisioning.md" source="grafana" version="latest" >}}
@@ -11,41 +11,46 @@ keywords:
- Terraform
labels:
products:
- cloud
- enterprise
- oss
title: Use Terraform to manage alerting resources
menuTitle: Use Terraform to provision
title: Use Terraform to provision alerting resources
weight: 200
---
# Use Terraform to manage alerting resources
# Use Terraform to provision alerting resources
Use Terraform’s Grafana Provider to manage your alerting resources and provision them into your Grafana system. Terraform provider support for Grafana Alerting makes it easy to create, manage, and maintain your entire Grafana Alerting stack as code.
For more information on managing your alerting resources using Terraform, refer to the [Grafana Provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) documentation.
Refer to [Grafana Provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) documentation for more examples and information on Terraform Alerting schemas.
Complete the following tasks to create and manage your alerting resources using Terraform.
1. Create an API key for provisioning.
1. Configure the Terraform provider.
1. Define your alerting resources in Terraform.
1. Define your alerting resources in Terraform. [Export alerting resources][alerting_export] in Terraform format, or implement the [Terraform Alerting schemas](https://registry.terraform.io/providers/grafana/grafana/latest/docs).
1. Run `terraform apply` to provision your alerting resources.
## Before you begin
{{< admonition type="note" >}}
- Ensure you have the grafana/grafana [Terraform provider](https://registry.terraform.io/providers/grafana/grafana/1.28.0) 1.27.0 or higher.
- By default, you cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. To change the default behaviour, refer to [Edit provisioned resources in the Grafana UI](#edit-provisioned-resources-in-the-grafana-ui).
- Ensure you are using Grafana 9.1 or higher.
- Before you begin, ensure you have the [Grafana Terraform Provider](https://registry.terraform.io/providers/grafana/grafana/) 1.27.0 or higher, and are using Grafana 9.1 or higher.
{{< /admonition >}}
## Create an API key for provisioning
You can [create a normal Grafana API key][api-keys] to authenticate Terraform with Grafana. Most existing tooling using API keys should automatically work with the new Grafana Alerting support.
You can create a [service account token][service-accounts] to authenticate Terraform with Grafana. Most existing tooling using API keys should automatically work with the new Grafana Alerting support.
There are also dedicated RBAC roles for alerting provisioning. This lets you easily authenticate as a [service account][service-accounts] with the minimum permissions needed to provision your Alerting infrastructure.
There are also dedicated RBAC roles for alerting provisioning. This lets you easily authenticate as a service account with the minimum permissions needed to provision your Alerting infrastructure.
To create an API key for provisioning, complete the following steps.
1. Create a new service account for your CI pipeline.
1. Assign the role “Access the alert rules Provisioning API.”
1. Create a new service account.
1. Assign the role or permission to access the [Alerting provisioning API][alerting_http_provisioning].
1. Create a new service account token.
1. Name and save the token for use in Terraform.
@@ -73,70 +78,68 @@ provider "grafana" {
}
```
## Provision contact points and templates
## Import contact points and templates
Contact points connect an alerting stack to the outside world. They tell Grafana how to connect to your external systems and where to deliver notifications. There are over fifteen different [integrations](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/contact_point#optional) to choose from.
Contact points connect an alerting stack to the outside world. They tell Grafana how to connect to your external systems and where to deliver notifications.
To provision contact points and templates, complete the following steps.
To provision contact points and templates, refer to the [grafana_contact_point schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/contact_point) and [grafana_message_template schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/message_template), and complete the following steps.
1. Copy this code block into a .tf file on your local machine.
1. Copy this code block into a `.tf` file on your local machine.
This example creates a contact point that sends alert notifications to Slack.
This example creates a contact point that sends alert notifications to Slack.
```HCL
resource "grafana_contact_point" "my_slack_contact_point" {
name = "Send to My Slack Channel"
```HCL
resource "grafana_contact_point" "my_slack_contact_point" {
name = "Send to My Slack Channel"
slack {
url = <YOUR_SLACK_WEBHOOK_URL>
text = <<EOT
{{ len .Alerts.Firing }} alerts are firing!
slack {
url = <YOUR_SLACK_WEBHOOK_URL>
text = <<EOT
{{ len .Alerts.Firing }} alerts are firing!
Alert summaries:
{{ range .Alerts.Firing }}
{{ template "Alert Instance Template" . }}
{{ end }}
EOT
}
}
```
Alert summaries:
{{ range .Alerts.Firing }}
{{ template "Alert Instance Template" . }}
{{ end }}
EOT
}
}
```
You can create multiple external integrations in a single contact point. Notifications routed to this contact point will be sent to all integrations. This example shows multiple integrations in the same Terraform resource.
You can create multiple external integrations in a single contact point. Notifications routed to this contact point will be sent to all integrations. This example shows multiple integrations in the same Terraform resource.
```
resource "grafana_contact_point" "my_multi_contact_point" {
name = "Send to Many Places"
```
resource "grafana_contact_point" "my_multi_contact_point" {
name = "Send to Many Places"
slack {
url = "webhook1"
...
}
slack {
url = "webhook2"
...
}
teams {
...
}
email {
...
}
}
```
slack {
url = "webhook1"
...
}
slack {
url = "webhook2"
...
}
teams {
...
}
email {
...
}
}
```
2. Enter text for your notification in the text field.
1. Enter text for your notification in the text field.
The `text` field supports [Go-style templating](https://pkg.go.dev/text/template). This enables you to manage your Grafana Alerting notification templates directly in Terraform.
The `text` field supports [Go-style templating](https://pkg.go.dev/text/template). This enables you to manage your Grafana Alerting notification templates directly in Terraform.
3. Run the command ‘terraform apply’.
1. Run the command `terraform apply`.
4. Go to the Grafana UI and check the details of your contact point.
1. Go to the Grafana UI and check the details of your contact point.
By default, you cannot edit resources provisioned via Terraform from the UI. This ensures that your alerting stack always stays in sync with your code.
1. Click **Test** to verify that the contact point works correctly.
5. Click **Test** to verify that the contact point works correctly.
**Note:**
### Reuse templates
You can reuse the same templates across many contact points. In the example above, a shared template ie embedded using the statement `{{ template “Alert Instance Template” . }}`
@@ -155,196 +158,193 @@ EOT
}
```
## Provision notification policies and routing
## Import notification policies and routing
Notification policies tell Grafana how to route alert instances to your contact points. They connect firing alerts to your previously defined contact points using a system of labels and matchers.
To provision notification policies and routing, complete the following steps.
To provision notification policies and routing, refer to the [grafana_notification_policy schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/notification_policy), and complete the following steps.
1. Copy this code block into a .tf file on your local machine.
{{% admonition type="warning" %}}
In this example, the alerts are grouped by `alertname`, which means that any notifications coming from alerts which share the same name, are grouped into the same Slack message. You can provide any set of label keys here, or you can use the special label `"..."` to route by all label keys, sending each alert in a separate notification.
Since the policy tree is a single resource, provisioning the `grafana_notification_policy` resource will overwrite a policy tree created through any other means.
If you want to route specific notifications differently, you can add sub-policies. Sub-policies allow you to apply routing to different alerts based on label matching. In this example, we apply a mute timing to all alerts with the label a=b.
{{< /admonition >}}
```HCL
resource "grafana_notification_policy" "my_policy" {
group_by = ["alertname"]
contact_point = grafana_contact_point.my_slack_contact_point.name
1. Copy this code block into a `.tf` file on your local machine.
group_wait = "45s"
group_interval = "6m"
repeat_interval = "3h"
In this example, the alerts are grouped by `alertname`, which means that any notifications coming from alerts which share the same name, are grouped into the same Slack message. You can provide any set of label keys here, or you can use the special label `"..."` to route by all label keys, sending each alert in a separate notification.
policy {
matcher {
label = "a"
match = "="
value = "b"
}
group_by = ["..."]
contact_point = grafana_contact_point.a_different_contact_point.name
mute_timings = [grafana_mute_timing.my_mute_timing.name]
If you want to route specific notifications differently, you can add sub-policies. Sub-policies allow you to apply routing to different alerts based on label matching. In this example, we apply a mute timing to all alerts with the label a=b.
policy {
matcher {
label = "sublabel"
match = "="
value = "subvalue"
}
contact_point = grafana_contact_point.a_third_contact_point.name
group_by = ["..."]
}
}
}
```
```HCL
resource "grafana_notification_policy" "my_policy" {
group_by = ["alertname"]
contact_point = grafana_contact_point.my_slack_contact_point.name
2. In the mute_timings field, link a mute timing to your notification policy.
group_wait = "45s"
group_interval = "6m"
repeat_interval = "3h"
3. Run the command ‘terraform apply’.
policy {
matcher {
label = "a"
match = "="
value = "b"
}
group_by = ["..."]
contact_point = grafana_contact_point.a_different_contact_point.name
mute_timings = [grafana_mute_timing.my_mute_timing.name]
4. Go to the Grafana UI and check the details of your notification policy.
policy {
matcher {
label = "sublabel"
match = "="
value = "subvalue"
}
contact_point = grafana_contact_point.a_third_contact_point.name
group_by = ["..."]
}
}
}
```
**Note:**
1. In the mute_timings field, link a mute timing to your notification policy.
Since the policy tree is a single resource, applying it will overwrite a policy tree created through any other means.
1. Run the command `terraform apply`.
By default, you cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code.
1. Go to the Grafana UI and check the details of your notification policy.
5. Click **Test** to verify that the notification point is working correctly.
1. Click **Test** to verify that the notification point is working correctly.
## Provision mute timings
## Import mute timings
Mute timings provide the ability to mute alert notifications for defined time periods.
To provision mute timings, complete the following steps.
To provision mute timings, refer to the [grafana_mute_timing schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/mute_timing), and complete the following steps.
1. Copy this code block into a .tf file on your local machine.
1. Copy this code block into a `.tf` file on your local machine.
In this example, alert notifications are muted on weekends.
In this example, alert notifications are muted on weekends.
```HCL
resource "grafana_mute_timing" "my_mute_timing" {
name = "My Mute Timing"
```HCL
resource "grafana_mute_timing" "my_mute_timing" {
name = "My Mute Timing"
intervals {
times {
start = "04:56"
end = "14:17"
}
weekdays = ["saturday", "sunday", "tuesday:thursday"]
months = ["january:march", "12"]
years = ["2025:2027"]
}
}
```
intervals {
times {
start = "04:56"
end = "14:17"
}
weekdays = ["saturday", "sunday", "tuesday:thursday"]
months = ["january:march", "12"]
years = ["2025:2027"]
}
}
```
2. Run the command ‘terraform apply’.
3. Go to the Grafana UI and check the details of your mute timing.
4. Reference your newly created mute timing in a notification policy using the `mute_timings` field.
1. Run the command `terraform apply`.
1. Go to the Grafana UI and check the details of your mute timing.
1. Reference your newly created mute timing in a notification policy using the `mute_timings` field.
This will apply your mute timing to some or all of your notifications.
**Note:**
1. Click **Test** to verify that the mute timing is working correctly.
By default, you cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code.
5. Click **Test** to verify that the mute timing is working correctly.
## Provision alert rules
## Import alert rules
[Alert rules][alerting-rules] enable you to alert against any Grafana data source. This can be a data source that you already have configured, or you can [define your data sources in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source) alongside your alert rules.
To provision alert rules, complete the following steps.
To provision alert rules, refer to the [grafana_rule_group schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/rule_group), and complete the following steps.
1. Create a data source to query and a folder to store your rules in.
In this example, the [TestData][testdata] data source is used.
In this example, the [TestData][testdata] data source is used.
Alerts can be defined against any backend datasource in Grafana.
Alerts can be defined against any backend datasource in Grafana.
```HCL
resource "grafana_data_source" "testdata_datasource" {
name = "TestData"
type = "testdata"
}
```HCL
resource "grafana_data_source" "testdata_datasource" {
name = "TestData"
type = "testdata"
}
resource "grafana_folder" "rule_folder" {
title = "My Rule Folder"
}
```
resource "grafana_folder" "rule_folder" {
title = "My Rule Folder"
}
```
2. Define an alert rule.
1. Define an alert rule.
For more information on alert rules, refer to [how to create Grafana-managed alerts](/blog/2022/08/01/grafana-alerting-video-how-to-create-alerts-in-grafana-9/).
For more information on alert rules, refer to [how to create Grafana-managed alerts](/blog/2022/08/01/grafana-alerting-video-how-to-create-alerts-in-grafana-9/).
3. Create a rule group containing one or more rules.
1. Create a rule group containing one or more rules.
In this example, the `grafana_rule_group` resource group is used.
In this example, the `grafana_rule_group` resource group is used.
```HCL
resource "grafana_rule_group" "my_rule_group" {
name = "My Alert Rules"
folder_uid = grafana_folder.rule_folder.uid
interval_seconds = 60
org_id = 1
```HCL
resource "grafana_rule_group" "my_rule_group" {
name = "My Alert Rules"
folder_uid = grafana_folder.rule_folder.uid
interval_seconds = 60
org_id = 1
rule {
name = "My Random Walk Alert"
condition = "C"
for = "0s"
rule {
name = "My Random Walk Alert"
condition = "C"
for = "0s"
// Query the datasource.
data {
ref_id = "A"
relative_time_range {
from = 600
to = 0
}
datasource_uid = grafana_data_source.testdata_datasource.uid
// `model` is a JSON blob that sends datasource-specific data.
// It's different for every datasource. The alert's query is defined here.
model = jsonencode({
intervalMs = 1000
maxDataPoints = 43200
refId = "A"
})
}
// Query the datasource.
data {
ref_id = "A"
relative_time_range {
from = 600
to = 0
}
datasource_uid = grafana_data_source.testdata_datasource.uid
// `model` is a JSON blob that sends datasource-specific data.
// It's different for every datasource. The alert's query is defined here.
model = jsonencode({
intervalMs = 1000
maxDataPoints = 43200
refId = "A"
})
}
// The query was configured to obtain data from the last 60 seconds. Let's alert on the average value of that series using a Reduce stage.
data {
datasource_uid = "__expr__"
// You can also create a rule in the UI, then GET that rule to obtain the JSON.
// This can be helpful when using more complex reduce expressions.
model = <<EOT
{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"params":[],"type":"last"},"type":"avg"}],"datasource":{"name":"Expression","type":"__expr__","uid":"__expr__"},"expression":"A","hide":false,"intervalMs":1000,"maxDataPoints":43200,"reducer":"last","refId":"B","type":"reduce"}
EOT
ref_id = "B"
relative_time_range {
from = 0
to = 0
}
}
// The query was configured to obtain data from the last 60 seconds. Let's alert on the average value of that series using a Reduce stage.
data {
datasource_uid = "__expr__"
// You can also create a rule in the UI, then GET that rule to obtain the JSON.
// This can be helpful when using more complex reduce expressions.
model = <<EOT
{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"params":[],"type":"last"},"type":"avg"}],"datasource":{"name":"Expression","type":"__expr__","uid":"__expr__"},"expression":"A","hide":false,"intervalMs":1000,"maxDataPoints":43200,"reducer":"last","refId":"B","type":"reduce"}
EOT
ref_id = "B"
relative_time_range {
from = 0
to = 0
}
}
// Now, let's use a math expression as our threshold.
// We want to alert when the value of stage "B" above exceeds 70.
data {
datasource_uid = "__expr__"
ref_id = "C"
relative_time_range {
from = 0
to = 0
}
model = jsonencode({
expression = "$B > 70"
type = "math"
refId = "C"
})
}
}
}
```
// Now, let's use a math expression as our threshold.
// We want to alert when the value of stage "B" above exceeds 70.
data {
datasource_uid = "__expr__"
ref_id = "C"
relative_time_range {
from = 0
to = 0
}
model = jsonencode({
expression = "$B > 70"
type = "math"
refId = "C"
})
}
}
}
```
4. Go to the Grafana UI and check your alert rule.
1. Run the command `terraform apply`.
1. Go to the Grafana UI and check your alert rule.
You can see whether or not the alert rule is firing. You can also see a visualization of each of the alert rule’s query stages
@@ -352,16 +352,40 @@ When the alert fires, Grafana routes a notification through the policy you defin
For example, if you chose Slack as a contact point, Grafana’s embedded [Alertmanager](https://github.com/prometheus/alertmanager) automatically posts a message to Slack.
## Edit provisioned resources in the Grafana UI
By default, you cannot edit resources provisioned via Terraform in Grafana. To enable editing these resources in the Grafana UI, use the `disable_provenance` attribute on alerting resources:
```HCL
provider "grafana" {
url = "http://grafana.example.com/"
auth = var.grafana_auth
}
resource "grafana_mute_timing" "mute_all" {
name = "mute all"
disable_provenance = true
intervals {}
}
```
**Useful Links:**
[Grafana Terraform Provider documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs)
{{% docs/reference %}}
[alerting-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/alerting-rules"
[alerting-rules]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules"
[alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules"
[api-keys]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/administration/api-keys"
[api-keys]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/administration/api-keys"
[alerting_export]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources"
[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/export-alerting-resources"
[service-accounts]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/administration/service-accounts"
[service-accounts]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/administration/service-accounts"
[alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning"
[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/http-api-provisioning"
[testdata]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/testdata"
[testdata]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/datasources/testdata"
[service-accounts]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/administration/service-accounts"
[service-accounts]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/administration/service-accounts"
[testdata]: "/docs/grafana/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/testdata"
[testdata]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA_VERSION>/datasources/testdata"
{{% /docs/reference %}}
@@ -1,108 +0,0 @@
---
aliases:
- ../../provision-alerting-resources/view-provisioned-resources/
canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/view-provisioned-resources/
description: Manage provisioned alerting resources in Grafana
keywords:
- grafana
- alerting
- alerting resources
- provisioning
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Manage provisioned alerting resources
title: Manage provisioned alerting resources
weight: 300
---
# Manage provisioned alerting resources
Verify that your alerting resources were created in Grafana, as well as edit or export your provisioned alerting resources.
## View provisioned alerting resoureces
To view your provisioned resources in Grafana, complete the following steps.
1. Open your Grafana instance.
1. Navigate to Alerting.
1. Click an alerting resource folder, for example, Alert rules.
Provisioned resources are labeled **Provisioned**, so that it is clear that they were not created manually.
## Export provisioned alerting resources
Export your alerting resources, such as alert rules, contact points, and notification policies in JSON, YAML, or Terraform format. You can export all Grafana-managed alert rules, single folders, and single groups.
To export provisioned alerting resources from the Grafana UI, complete the following steps.
1. Click **Alerts & IRM** -> **Alert rules**.
1. To export all Grafana-managed rules, click **Export rules**.
1. To export a folder, change the **View as** to **List**.
1. Select the folder you want to export and click the **Export rules folder** icon.
1. To export a group, change the **View as** to **Grouped**.
1. Find the group you want to export and click the **Export rule group** icon.
1. Choose the format to export in.
Note that formats JSON and YAML are suitable only for file provisioning. To get rule definition in provisioning API format, use the provisioning GET API.
1. Click **Copy Code** or **Download**.
1. Choose **Copy Code** to go to an existing file and paste in the code.
1. Choose **Download** to download a file with the exported data.
## Edit provisioned alert rules
Use the **Modify export** mode for alert rules to edit provisioned alert rules and export a modified version.
{{% admonition type="note" %}} This feature is for Grafana-managed alert rules only. It is available to Admin, Viewer, and Editor roles. {{% /admonition %}}
To edit provisioned alerting alert rules from the Grafana UI, complete the following steps.
1. Click **Alerts & IRM** -> **Alert rules**.
1. Locate the alert rule you want to edit and click **More** -> **Modify Export** to open the Alert Rule form.
1. From the Alert Rule form, edit the fields you want to change.
1. Click **Export** to export all alert rules within the group.
You can only export groups of rules; not single rules.
The exported rule data appears in different formats - HTML, JSON, Terraform.
1. Choose the format to export in.
1. Click **Copy Code** or **Download**.
a. Choose **Copy Code** to go to an existing file and paste in the code.
b. Choose **Download** to download a file with the exported data.
## Edit API-provisioned alerting resources
To enable editing of API-provisioned resources in the Grafana UI, add the `X-Disable-Provenance` header to the following requests in the API:
- `POST /api/v1/provisioning/alert-rules`
- `PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}` (calling this endpoint will change provenance for all alert rules within the alert group)
- `POST /api/v1/provisioning/contact-points`
- `POST /api/v1/provisioning/mute-timings`
- `PUT /api/v1/provisioning/policies`
- `PUT /api/v1/provisioning/templates/{name}`
To reset the notification policy tree to the default and unlock it for editing in the Grafana UI, use the `DELETE /api/v1/provisioning/policies` endpoint.
In Terraform, you can use the `disable_provenance` attribute on alerting resources:
```
provider "grafana" {
url = "http://grafana.example.com/"
auth = var.grafana_auth
}
resource "grafana_mute_timing" "mute_all" {
name = "mute all"
disable_provenance = true
intervals {}
}
```
**Note:**
You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out.
File diff suppressed because it is too large Load Diff
@@ -42,6 +42,7 @@ Some features are enabled by default. You can disable these feature by setting t
| `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend | Yes |
| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | Yes |
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes |
| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes |
| `transformationsRedesign` | Enables the transformations redesign | Yes |
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes |
| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | Yes |
@@ -130,7 +131,6 @@ Experimental features might be changed or removed without prior notice.
| `pluginsDynamicAngularDetectionPatterns` | Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones |
| `vizAndWidgetSplit` | Split panels between visualizations and widgets |
| `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries |
| `logsExploreTableVisualisation` | A table visualisation for logs in Explore |
| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers |
| `mlExpressions` | Enable support for Machine Learning in server-side expressions |
| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source |
@@ -173,6 +173,7 @@ Experimental features might be changed or removed without prior notice.
| `promQLScope` | In-development feature that will allow injection of labels into prometheus queries. |
| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph |
| `newPDFRendering` | New implementation for the dashboard to PDF rendering |
| `kubernetesAggregator` | Enable grafana aggregator |
## Development feature toggles
File diff suppressed because it is too large Load Diff
+27 -27
View File
@@ -22,7 +22,7 @@ replace (
// Override Prometheus version because Prometheus v2.X is tagged as v0.X for Go modules purposes and Go assumes
// that v1.Y is higher than v0.X, so when we resolve dependencies if any dependency imports v1.Y we'd
// import that instead of v0.X even though v0.X is newer.
replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0.43.0
replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0.49.0
// The v0.120.0 is needed for now to be compatible with grafana/thema.
replace github.com/getkin/kin-openapi => github.com/getkin/kin-openapi v0.120.0
@@ -59,11 +59,11 @@ require (
github.com/google/uuid v1.6.0 // @grafana/backend-platform
github.com/google/wire v0.5.0 // @grafana/backend-platform
github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20240208110638-b1d7d5f9cbf2 // @grafana/alerting-squad-backend
github.com/grafana/alerting v0.0.0-20240213130827-92f64f0f2a12 // @grafana/alerting-squad-backend
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code
github.com/grafana/grafana-aws-sdk v0.23.1 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources
github.com/grafana/grafana-plugin-sdk-go v0.209.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.211.0 // @grafana/plugins-platform-backend
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform
github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend
github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend
@@ -77,7 +77,7 @@ require (
github.com/linkedin/goavro/v2 v2.10.0 // @grafana/backend-platform
github.com/m3db/prometheus_remote_client_golang v0.4.4 // @grafana/backend-platform
github.com/magefile/mage v1.15.0 // @grafana/grafana-delivery
github.com/mattn/go-isatty v0.0.18 // @grafana/backend-platform
github.com/mattn/go-isatty v0.0.19 // @grafana/backend-platform
github.com/mattn/go-sqlite3 v1.14.19 // @grafana/backend-platform
github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-squad-backend
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // @grafana/grafana-operator-experience-squad
@@ -85,7 +85,7 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible // @grafana/alerting-squad-backend
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/alertmanager v0.25.0 // @grafana/alerting-squad-backend
github.com/prometheus/alertmanager v0.26.0 // @grafana/alerting-squad-backend
github.com/prometheus/client_golang v1.18.0 // @grafana/alerting-squad-backend
github.com/prometheus/client_model v0.5.0 // @grafana/backend-platform
github.com/prometheus/common v0.46.0 // @grafana/alerting-squad-backend
@@ -106,14 +106,14 @@ require (
go.opentelemetry.io/otel/sdk v1.22.0 // @grafana/backend-platform
go.opentelemetry.io/otel/trace v1.22.0 // @grafana/backend-platform
golang.org/x/crypto v0.18.0 // @grafana/backend-platform
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // @grafana/alerting-squad-backend
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // @grafana/alerting-squad-backend
golang.org/x/net v0.20.0 // @grafana/oss-big-tent @grafana/partner-datasources
golang.org/x/oauth2 v0.16.0 // @grafana/grafana-authnz-team
golang.org/x/sync v0.6.0 // @grafana/alerting-squad-backend
golang.org/x/time v0.5.0 // @grafana/backend-platform
golang.org/x/tools v0.17.0 // @grafana/grafana-as-code
gonum.org/v1/gonum v0.12.0 // @grafana/observability-metrics
google.golang.org/api v0.149.0 // @grafana/backend-platform
google.golang.org/api v0.153.0 // @grafana/backend-platform
google.golang.org/grpc v1.60.1 // @grafana/plugins-platform-backend
google.golang.org/protobuf v1.32.0 // @grafana/plugins-platform-backend
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
@@ -135,8 +135,8 @@ require (
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/FZambia/eagle v0.1.0 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andybalholm/brotli v1.0.4 // @grafana/partner-datasources
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect
github.com/andybalholm/brotli v1.0.5 // @grafana/partner-datasources
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
@@ -166,7 +166,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // @grafana/backend-platform
github.com/google/btree v1.1.2 // indirect
github.com/google/flatbuffers v23.1.21+incompatible // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // @grafana/backend-platform
github.com/gorilla/mux v1.8.0 // @grafana/backend-platform
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
@@ -186,7 +186,7 @@ require (
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/miekg/dns v1.1.51 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-squad-backend
@@ -203,7 +203,7 @@ require (
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/segmentio/encoding v0.3.6 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.0 // indirect
@@ -291,7 +291,7 @@ require github.com/grafana/pyroscope/api v0.3.0 // @grafana/observability-traces
require github.com/grafana/pyroscope-go/godeltaprof v0.1.6 // @grafana/observability-traces-and-profiling
require github.com/apache/arrow/go/v13 v13.0.0 // @grafana/observability-metrics
require github.com/apache/arrow/go/v15 v15.0.0 // @grafana/observability-metrics
require (
cloud.google.com/go v0.111.0 // indirect
@@ -317,10 +317,10 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/cristalhq/jwt/v4 v4.0.2 // indirect
github.com/dave/jennifer v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/drone-runners/drone-runner-docker v1.8.2 // indirect
github.com/drone/drone-go v1.7.1 // indirect
@@ -329,12 +329,12 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ecordell/optgen v0.0.6 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/getsentry/sentry-go v0.12.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/status v1.1.1 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
@@ -346,14 +346,14 @@ require (
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/memberlist v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
@@ -410,14 +410,14 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
k8s.io/kms v0.29.0 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/libc v1.22.4 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.20.4 // indirect
modernc.org/sqlite v1.21.2 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect
@@ -434,7 +434,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // @grafana/backend-platform
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // @grafana/plugins-platform-backend
github.com/RoaringBitmap/roaring v0.9.4 // indirect
github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f // indirect
@@ -446,10 +446,10 @@ require (
github.com/blevesearch/vellum v1.0.7 // indirect
github.com/blugelabs/ice v1.0.0 // indirect
github.com/caio/go-tdigest v3.1.0+incompatible // indirect
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/docker/docker v23.0.4+incompatible // @grafana/grafana-delivery
github.com/docker/docker v24.0.7+incompatible // @grafana/grafana-delivery
github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 // indirect
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect
github.com/go-logr/logr v1.4.1 // @grafana/grafana-app-platform-squad
@@ -457,13 +457,13 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
github.com/hmarr/codeowners v1.1.2 // @grafana/grafana-as-code
github.com/imdario/mergo v0.3.16 // indirect
github.com/klauspost/compress v1.17.3 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wk8/go-ordered-map v1.0.0 // @grafana/backend-platform
github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling
+826 -123
View File
File diff suppressed because it is too large Load Diff
@@ -176,4 +176,5 @@ export interface FeatureToggles {
nodeGraphDotLayout?: boolean;
groupToNestedTableTransformation?: boolean;
newPDFRendering?: boolean;
kubernetesAggregator?: boolean;
}
@@ -95,6 +95,7 @@ export const Pages = {
*/
addAnnotationCTA: Components.CallToActionCard.button('Add annotation query'),
addAnnotationCTAV2: Components.CallToActionCard.buttonV2('Add annotation query'),
annotations: 'data-testid list-annotations',
},
Settings: {
name: 'Annotations settings name input',
@@ -103,6 +104,10 @@ export const Pages = {
panelFilterSelect: 'data-testid annotations-panel-filter',
showInLabel: 'show-in-label',
previewInDashboard: 'data-testid annotations-preview',
delete: 'data-testid annotations-delete',
apply: 'data-testid annotations-apply',
enable: 'data-testid annotation-enable',
hide: 'data-testid annotation-hide',
},
},
Variables: {
@@ -208,7 +208,15 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
format: QueryFormat.Table,
};
const response = await this.runMetaQuery(interpolatedQuery, range);
// NOTE: we can remove this try-catch when https://github.com/grafana/grafana/issues/82250
// is fixed.
let response;
try {
response = await this.runMetaQuery(interpolatedQuery, range);
} catch (error) {
console.error(error);
throw new Error('error when executing the sql query');
}
return this.getResponseParser().transformMetricFindResponse(response);
}
@@ -445,24 +445,6 @@ Card can have a disabled state, effectively making it and its actions non-clicka
</Card>
</Preview>
### With overline
```jsx
<Card>
<Card.Overline>Filter option</Card.Overline>
<Card.Heading>Filter by name</Card.Heading>
<Card.Description>Filter data by query.</Card.Description>
</Card>
```
<Preview>
<Card>
<Card.Overline>Filter option</Card.Overline>
<Card.Heading>Filter by name</Card.Heading>
<Card.Description>Filter data by query.</Card.Description>
</Card>
</Preview>
### Props
<ArgTypes of={Card} />
@@ -179,18 +179,4 @@ export const NotSelected: StoryFn<typeof Card> = () => {
);
};
export const WithOverline: StoryFn<typeof Card> = (args) => {
return (
<Card {...args}>
<Card.Overline>Overline text above the title</Card.Overline>
<Card.Heading>Card title</Card.Heading>
<Card.Description>
Description, body text. Greetings! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat.
</Card.Description>
</Card>
);
};
export default meta;
@@ -5,7 +5,6 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { getFocusStyles } from '../../themes/mixins';
import { Text } from '../Text/Text';
import { CardContainer, CardContainerProps, getCardContainerStyles } from './CardContainer';
@@ -29,7 +28,6 @@ export interface Props extends Omit<CardContainerProps, 'disableEvents' | 'disab
}
export interface CardInterface extends FC<Props> {
Overline: typeof Overline;
Heading: typeof Heading;
Tags: typeof Tags;
Figure: typeof Figure;
@@ -166,28 +164,6 @@ const getHeadingStyles = (theme: GrafanaTheme2) => ({
}),
});
/** Card text to be displayed above title */
const Overline = ({ children, className }: ChildProps) => {
const styles = useStyles2(getOverlineStyles);
return (
<div className={cx(styles.overline, className)}>
{children && (
<Text color="info" weight="medium" variant="bodySmall">
{children}
</Text>
)}
</div>
);
};
Overline.displayName = 'Overline';
const getOverlineStyles = (theme: GrafanaTheme2) => ({
overline: css({
gridArea: 'Overline',
marginBottom: theme.spacing(0.5),
}),
});
const Tags = ({ children, className }: ChildProps) => {
const styles = useStyles2(getTagStyles);
return <div className={cx(styles.tagList, className)}>{children}</div>;
@@ -384,7 +360,6 @@ export const getCardStyles = (theme: GrafanaTheme2) => {
};
};
Card.Overline = Overline;
Card.Heading = Heading;
Card.Tags = Tags;
Card.Figure = Figure;
@@ -80,11 +80,10 @@ export const getCardContainerStyles = (
display: 'grid',
position: 'relative',
gridTemplateColumns: 'auto 1fr auto',
gridTemplateRows: 'auto 1fr auto auto auto',
gridTemplateRows: '1fr auto auto auto',
gridAutoColumns: '1fr',
gridAutoFlow: 'row',
gridTemplateAreas: `
"Figure Overline Tags"
"Figure Heading Tags"
"Figure Meta Tags"
"Figure Description Tags"
@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css';
import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/react';
import Prism, { Grammar, LanguageMap } from 'prismjs';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { Popper as ReactPopper } from 'react-popper';
import React, { memo, useEffect, useRef, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import { Value } from 'slate';
import Plain from 'slate-plain-serializer';
@@ -86,6 +86,29 @@ export const DataLinkInput = memo(
const prevLinkUrl = usePrevious<Value>(linkUrl);
const [scrollTop, setScrollTop] = useState(0);
// the order of middleware is important!
const middleware = [
offset(({ rects }) => ({
alignmentAxis: rects.reference.width,
})),
flip({
fallbackAxisSideDirection: 'start',
// see https://floating-ui.com/docs/flip#combining-with-shift
crossAxis: false,
boundary: document.body,
}),
shift(),
];
const { refs, floatingStyles } = useFloating({
open: showingSuggestions,
placement: 'bottom-start',
onOpenChange: setShowingSuggestions,
middleware,
whileElementsMounted: autoUpdate,
strategy: 'fixed',
});
// Workaround for https://github.com/ianstormtaylor/slate/issues/2927
const stateRef = useRef({ showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange });
stateRef.current = { showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange };
@@ -96,12 +119,11 @@ export const DataLinkInput = memo(
setScrollTop(getElementPosition(activeRef.current, suggestionsIndex));
}, [suggestionsIndex]);
// SelectionReference is used to position the variables suggestion relatively to current DOM selection
const selectionRef = useMemo(() => new SelectionReference(), []);
const onKeyDown = React.useCallback((event: React.KeyboardEvent, next: () => void) => {
if (!stateRef.current.showingSuggestions) {
if (event.key === '=' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) {
const selectionRef = new SelectionReference();
refs.setReference(selectionRef);
return setShowingSuggestions(true);
}
return next();
@@ -181,49 +203,21 @@ export const DataLinkInput = memo(
<div id="data-link-input" className="slate-query-field">
{showingSuggestions && (
<Portal>
<ReactPopper
referenceElement={selectionRef}
placement="bottom-end"
modifiers={[
{
name: 'preventOverflow',
enabled: true,
options: {
rootBoundary: 'viewport',
},
},
{
name: 'arrow',
enabled: false,
},
{
name: 'offset',
options: {
offset: [250, 0],
},
},
]}
>
{({ ref, style, placement }) => {
return (
<div ref={ref} style={style} data-placement={placement} className={styles.suggestionsWrapper}>
<CustomScrollbar
scrollTop={scrollTop}
autoHeightMax="300px"
setScrollTop={({ scrollTop }) => setScrollTop(scrollTop)}
>
<DataLinkSuggestions
activeRef={activeRef}
suggestions={stateRef.current.suggestions}
onSuggestionSelect={onVariableSelect}
onClose={() => setShowingSuggestions(false)}
activeIndex={suggestionsIndex}
/>
</CustomScrollbar>
</div>
);
}}
</ReactPopper>
<div ref={refs.setFloating} style={floatingStyles}>
<CustomScrollbar
scrollTop={scrollTop}
autoHeightMax="300px"
setScrollTop={({ scrollTop }) => setScrollTop(scrollTop)}
>
<DataLinkSuggestions
activeRef={activeRef}
suggestions={stateRef.current.suggestions}
onSuggestionSelect={onVariableSelect}
onClose={() => setShowingSuggestions(false)}
activeIndex={suggestionsIndex}
/>
</CustomScrollbar>
</div>
</Portal>
)}
<Editor
@@ -237,6 +237,17 @@ export const Table = memo((props: Props) => {
setPageSize(pageSize);
}, [pageSize, setPageSize]);
useEffect(() => {
// Reset page index when data changes
// This is needed because react-table does not do this automatically
// autoResetPage is set to false because setting it to true causes the issue described in
// https://github.com/grafana/grafana/pull/67477
if (data.length / pageSize < state.pageIndex) {
gotoPage(0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
useResetVariableListSizeCache(extendedState, listRef, data, hasUniqueId);
useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef);
@@ -1,8 +1,9 @@
/**
* This API allows popovers to update Popper's position when e.g. popover content changes
* update is delivered to content by react-popper.
*/
export interface ToggletipContentProps {
/**
* @deprecated
* This prop is deprecated and no longer has any effect as popper position updates automatically.
* It will be removed in a future release.
*/
update?: () => void;
}
@@ -1,10 +1,11 @@
import { Placement } from '@floating-ui/react';
/**
* This API allows popovers to update Popper's position when e.g. popover content changes
* updatePopperPosition is delivered to content by react-popper.
*/
export interface PopoverContentProps {
// Is this used anywhere in plugins? Can we remove it or rename it to just update?
/**
* @deprecated
* This prop is deprecated and no longer has any effect as popper position updates automatically.
* It will be removed in a future release.
*/
updatePopperPosition?: () => void;
}
+1 -1
View File
@@ -30,7 +30,7 @@ func (pm *fakePluginInstaller) Add(_ context.Context, pluginID, version string,
return nil
}
func (pm *fakePluginInstaller) Remove(_ context.Context, pluginID string) error {
func (pm *fakePluginInstaller) Remove(_ context.Context, pluginID, _ string) error {
delete(pm.plugins, pluginID)
return nil
}
+2 -1
View File
@@ -207,7 +207,8 @@ func setupDB(b testing.TB) benchScenario {
quotaService := quotatest.New(false, nil)
cfg := setting.NewCfg()
teamSvc := teamimpl.ProvideService(db, cfg)
teamSvc, err := teamimpl.ProvideService(db, cfg)
require.NoError(b, err)
orgService, err := orgimpl.ProvideService(db, cfg, quotaService)
require.NoError(b, err)
+16 -7
View File
@@ -274,7 +274,12 @@ func (hs *HTTPServer) GetPluginMarkdown(c *contextmodel.ReqContext) response.Res
pluginID := web.Params(c.Req)[":pluginId"]
name := web.Params(c.Req)[":name"]
content, err := hs.pluginMarkdown(c.Req.Context(), pluginID, name)
p, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
}
content, err := hs.pluginMarkdown(c.Req.Context(), pluginID, p.Info.Version, name)
if err != nil {
var notFound plugins.NotFoundError
if errors.As(err, &notFound) {
@@ -286,7 +291,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *contextmodel.ReqContext) response.Res
// fallback try readme
if len(content) == 0 {
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme")
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, p.Info.Version, "readme")
if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) {
return response.Error(http.StatusNotFound, plugins.ErrFileNotExist.Error(), nil)
@@ -354,7 +359,7 @@ func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) {
// serveLocalPluginAsset returns the content of a plugin asset file from the local filesystem to the http client.
func (hs *HTTPServer) serveLocalPluginAsset(c *contextmodel.ReqContext, plugin pluginstore.Plugin, assetPath string) {
f, err := hs.pluginFileStore.File(c.Req.Context(), plugin.ID, assetPath)
f, err := hs.pluginFileStore.File(c.Req.Context(), plugin.ID, plugin.Info.Version, assetPath)
if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) {
c.JsonApiErr(404, "Plugin file not found", nil)
@@ -476,8 +481,12 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
func (hs *HTTPServer) UninstallPlugin(c *contextmodel.ReqContext) response.Response {
pluginID := web.Params(c.Req)[":pluginId"]
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
}
err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID)
err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID, plugin.Info.Version)
if err != nil {
if errors.Is(err, plugins.ErrPluginNotInstalled) {
return response.Error(http.StatusNotFound, "Plugin not installed", err)
@@ -494,19 +503,19 @@ func translatePluginRequestErrorToAPIError(err error) response.Response {
return response.ErrOrFallback(http.StatusInternalServerError, "Plugin request failed", err)
}
func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginID string, name string) ([]byte, error) {
func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginID, pluginVersion, name string) ([]byte, error) {
file, err := mdFilepath(strings.ToUpper(name))
if err != nil {
return make([]byte, 0), err
}
md, err := hs.pluginFileStore.File(ctx, pluginID, file)
md, err := hs.pluginFileStore.File(ctx, pluginID, pluginVersion, file)
if err != nil {
if errors.Is(err, plugins.ErrPluginNotInstalled) {
return make([]byte, 0), plugins.NotFoundError{PluginID: pluginID}
}
md, err = hs.pluginFileStore.File(ctx, pluginID, strings.ToLower(file))
md, err = hs.pluginFileStore.File(ctx, pluginID, pluginVersion, strings.ToLower(file))
if err != nil {
return make([]byte, 0), nil
}
+19 -11
View File
@@ -49,6 +49,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
canInstall := []ac.Permission{{Action: pluginaccesscontrol.ActionInstall}}
cannotInstall := []ac.Permission{{Action: "plugins:cannotinstall"}}
pluginID := "grafana-test-datasource"
localOrg := int64(1)
globalOrg := int64(ac.GlobalOrgID)
@@ -88,11 +89,17 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
hs.pluginInstaller = NewFakePluginInstaller()
hs.pluginFileStore = &fakes.FakePluginFileStore{}
hs.pluginStore = pluginstore.NewFakePluginStore(pluginstore.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
},
})
})
t.Run(testName("Install", tc), func(t *testing.T) {
input := strings.NewReader(`{"version": "1.0.2"}`)
req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/install", input), userWithPermissions(tc.permissionOrg, tc.permissions))
endpoint := fmt.Sprintf("/api/plugins/%s/install", pluginID)
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(tc.permissionOrg, tc.permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, res.StatusCode)
@@ -101,7 +108,8 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
t.Run(testName("Uninstall", tc), func(t *testing.T) {
input := strings.NewReader("{ }")
req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/uninstall", input), userWithPermissions(tc.permissionOrg, tc.permissions))
endpoint := fmt.Sprintf("/api/plugins/%s/uninstall", pluginID)
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(tc.permissionOrg, tc.permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, res.StatusCode)
@@ -401,14 +409,14 @@ func TestMakePluginResourceRequestContentTypeEmpty(t *testing.T) {
func TestPluginMarkdown(t *testing.T) {
t.Run("Plugin not installed returns error", func(t *testing.T) {
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
return nil, plugins.ErrPluginNotInstalled
},
}
hs := HTTPServer{pluginFileStore: pluginFileStore}
pluginID := "test-datasource"
md, err := hs.pluginMarkdown(context.Background(), pluginID, "test")
md, err := hs.pluginMarkdown(context.Background(), pluginID, "", "test")
require.ErrorAs(t, err, &plugins.NotFoundError{PluginID: pluginID})
require.Equal(t, []byte{}, md)
})
@@ -416,7 +424,7 @@ func TestPluginMarkdown(t *testing.T) {
t.Run("File fetch will be retried using different casing if error occurs", func(t *testing.T) {
var requestedFiles []string
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
requestedFiles = append(requestedFiles, filename)
return nil, errors.New("some error")
},
@@ -424,7 +432,7 @@ func TestPluginMarkdown(t *testing.T) {
hs := HTTPServer{pluginFileStore: pluginFileStore}
md, err := hs.pluginMarkdown(context.Background(), "", "reAdMe")
md, err := hs.pluginMarkdown(context.Background(), "", "", "reAdMe")
require.NoError(t, err)
require.Equal(t, []byte{}, md)
require.Equal(t, []string{"README.md", "readme.md"}, requestedFiles)
@@ -453,7 +461,7 @@ func TestPluginMarkdown(t *testing.T) {
data := []byte{123}
var requestedFiles []string
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
requestedFiles = append(requestedFiles, filename)
return &plugins.File{Content: data}, nil
},
@@ -461,7 +469,7 @@ func TestPluginMarkdown(t *testing.T) {
hs := HTTPServer{pluginFileStore: pluginFileStore}
md, err := hs.pluginMarkdown(context.Background(), "test-datasource", tc.filePath)
md, err := hs.pluginMarkdown(context.Background(), "test-datasource", "", tc.filePath)
require.NoError(t, err)
require.Equal(t, data, md)
require.Equal(t, tc.expected, requestedFiles)
@@ -471,7 +479,7 @@ func TestPluginMarkdown(t *testing.T) {
t.Run("Non markdown file request returns an error", func(t *testing.T) {
hs := HTTPServer{pluginFileStore: &fakes.FakePluginFileStore{}}
md, err := hs.pluginMarkdown(context.Background(), "", "test.json")
md, err := hs.pluginMarkdown(context.Background(), "", "", "test.json")
require.ErrorIs(t, err, ErrUnexpectedFileExtension)
require.Equal(t, []byte{}, md)
})
@@ -480,14 +488,14 @@ func TestPluginMarkdown(t *testing.T) {
data := []byte{1, 2, 3}
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
return &plugins.File{Content: data}, nil
},
}
hs := HTTPServer{pluginFileStore: pluginFileStore}
md, err := hs.pluginMarkdown(context.Background(), "", "someFile")
md, err := hs.pluginMarkdown(context.Background(), "", "", "someFile")
require.NoError(t, err)
require.Equal(t, data, md)
})
@@ -613,9 +613,10 @@ func TestIntegrationMergeUser(t *testing.T) {
t.Run("should be able to merge user", func(t *testing.T) {
// Restore after destructive operation
sqlStore := db.InitTestDB(t)
teamSvc := teamimpl.ProvideService(sqlStore, setting.NewCfg())
teamSvc, err := teamimpl.ProvideService(sqlStore, setting.NewCfg())
require.NoError(t, err)
team1, err := teamSvc.CreateTeam("team1 name", "", 1)
require.Nil(t, err)
require.NoError(t, err)
usrSvc := setupTestUserService(t, sqlStore)
const testOrgID int64 = 1
+1
View File
@@ -74,6 +74,7 @@ func (l *loggerImpl) Middleware() web.Middleware {
duration := time.Since(start)
timeTaken := duration / time.Millisecond
ctx := contexthandler.FromContext(r.Context())
if ctx != nil && ctx.PerfmonTimer != nil {
ctx.PerfmonTimer.Observe(float64(timeTaken))
}
+2 -2
View File
@@ -14,7 +14,7 @@ type Installer interface {
// Add adds a new plugin.
Add(ctx context.Context, pluginID, version string, opts CompatOpts) error
// Remove removes an existing plugin.
Remove(ctx context.Context, pluginID string) error
Remove(ctx context.Context, pluginID, version string) error
}
type PluginSource interface {
@@ -25,7 +25,7 @@ type PluginSource interface {
type FileStore interface {
// File retrieves a plugin file.
File(ctx context.Context, pluginID, filename string) (*File, error)
File(ctx context.Context, pluginID, pluginVersion, filename string) (*File, error)
}
type File struct {
+9 -9
View File
@@ -44,7 +44,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
return nil, errNilRequest
}
p, exists := s.plugin(ctx, req.PluginContext.PluginID)
p, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
@@ -87,7 +87,7 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq
return errNilSender
}
p, exists := s.plugin(ctx, req.PluginContext.PluginID)
p, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return plugins.ErrPluginNotRegistered
}
@@ -130,7 +130,7 @@ func (s *Service) CollectMetrics(ctx context.Context, req *backend.CollectMetric
return nil, errNilRequest
}
p, exists := s.plugin(ctx, req.PluginContext.PluginID)
p, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
@@ -152,7 +152,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
return nil, errNilRequest
}
p, exists := s.plugin(ctx, req.PluginContext.PluginID)
p, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
@@ -182,7 +182,7 @@ func (s *Service) SubscribeStream(ctx context.Context, req *backend.SubscribeStr
return nil, errNilRequest
}
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID)
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
@@ -195,7 +195,7 @@ func (s *Service) PublishStream(ctx context.Context, req *backend.PublishStreamR
return nil, errNilRequest
}
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID)
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
@@ -212,7 +212,7 @@ func (s *Service) RunStream(ctx context.Context, req *backend.RunStreamRequest,
return errNilSender
}
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID)
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return plugins.ErrPluginNotRegistered
}
@@ -221,8 +221,8 @@ func (s *Service) RunStream(ctx context.Context, req *backend.RunStreamRequest,
}
// plugin finds a plugin with `pluginID` from the registry that is not decommissioned
func (s *Service) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) {
p, exists := s.pluginRegistry.Plugin(ctx, pluginID)
func (s *Service) plugin(ctx context.Context, pluginID, pluginVersion string) (*plugins.Plugin, bool) {
p, exists := s.pluginRegistry.Plugin(ctx, pluginID, pluginVersion)
if !exists {
return nil, false
}
+5 -5
View File
@@ -176,7 +176,7 @@ func NewFakePluginRegistry() *FakePluginRegistry {
}
}
func (f *FakePluginRegistry) Plugin(_ context.Context, id string) (*plugins.Plugin, bool) {
func (f *FakePluginRegistry) Plugin(_ context.Context, id, _ string) (*plugins.Plugin, bool) {
p, exists := f.Store[id]
return p, exists
}
@@ -195,7 +195,7 @@ func (f *FakePluginRegistry) Add(_ context.Context, p *plugins.Plugin) error {
return nil
}
func (f *FakePluginRegistry) Remove(_ context.Context, id string) error {
func (f *FakePluginRegistry) Remove(_ context.Context, id, _ string) error {
delete(f.Store, id)
return nil
}
@@ -423,12 +423,12 @@ func (s *FakePluginSource) DefaultSignature(ctx context.Context) (plugins.Signat
}
type FakePluginFileStore struct {
FileFunc func(ctx context.Context, pluginID, filename string) (*plugins.File, error)
FileFunc func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error)
}
func (f *FakePluginFileStore) File(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
func (f *FakePluginFileStore) File(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
if f.FileFunc != nil {
return f.FileFunc(ctx, pluginID, filename)
return f.FileFunc(ctx, pluginID, pluginVersion, filename)
}
return nil, nil
}
+2 -2
View File
@@ -21,8 +21,8 @@ func ProvideService(pluginRegistry registry.Service) *Service {
}
}
func (s *Service) File(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
if p, exists := s.pluginRegistry.Plugin(ctx, pluginID); exists {
func (s *Service) File(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
if p, exists := s.pluginRegistry.Plugin(ctx, pluginID, pluginVersion); exists {
f, err := p.File(filename)
if err != nil {
return nil, err
+6 -6
View File
@@ -55,7 +55,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
}
var pluginArchive *repo.PluginArchive
if plugin, exists := m.plugin(ctx, pluginID); exists {
if plugin, exists := m.plugin(ctx, pluginID, version); exists {
if plugin.IsCorePlugin() || plugin.IsBundledPlugin() {
return plugins.ErrInstallCorePlugin
}
@@ -84,7 +84,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
}
// remove existing installation of plugin
err = m.Remove(ctx, plugin.ID)
err = m.Remove(ctx, plugin.ID, plugin.Info.Version)
if err != nil {
return err
}
@@ -139,8 +139,8 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
return nil
}
func (m *PluginInstaller) Remove(ctx context.Context, pluginID string) error {
plugin, exists := m.plugin(ctx, pluginID)
func (m *PluginInstaller) Remove(ctx context.Context, pluginID, version string) error {
plugin, exists := m.plugin(ctx, pluginID, version)
if !exists {
return plugins.ErrPluginNotInstalled
}
@@ -168,8 +168,8 @@ func (m *PluginInstaller) Remove(ctx context.Context, pluginID string) error {
}
// plugin finds a plugin with `pluginID` from the store
func (m *PluginInstaller) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) {
p, exists := m.pluginRegistry.Plugin(ctx, pluginID)
func (m *PluginInstaller) plugin(ctx context.Context, pluginID, pluginVersion string) (*plugins.Plugin, bool) {
p, exists := m.pluginRegistry.Plugin(ctx, pluginID, pluginVersion)
if !exists {
return nil, false
}
+5 -7
View File
@@ -23,6 +23,8 @@ func TestPluginManager_Add_Remove(t *testing.T) {
const (
pluginID, v1 = "test-panel", "1.0.0"
zipNameV1 = "test-panel-1.0.0.zip"
v2 = "2.0.0"
zipNameV2 = "test-panel-2.0.0.zip"
)
// mock a plugin to be returned automatically by the plugin loader
@@ -83,10 +85,6 @@ func TestPluginManager_Add_Remove(t *testing.T) {
})
t.Run("Update plugin to different version", func(t *testing.T) {
const (
v2 = "2.0.0"
zipNameV2 = "test-panel-2.0.0.zip"
)
// mock a plugin to be returned automatically by the plugin loader
pluginV2 := createPlugin(t, pluginID, plugins.ClassExternal, true, true, func(plugin *plugins.Plugin) {
plugin.Info.Version = v2
@@ -138,7 +136,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
},
}
err = inst.Remove(context.Background(), pluginID)
err = inst.Remove(context.Background(), pluginID, v2)
require.NoError(t, err)
require.Equal(t, []string{pluginID}, unloadedPlugins)
@@ -146,7 +144,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
t.Run("Won't remove if not exists", func(t *testing.T) {
inst.pluginRegistry = fakes.NewFakePluginRegistry()
err = inst.Remove(context.Background(), pluginID)
err = inst.Remove(context.Background(), pluginID, v2)
require.Equal(t, plugins.ErrPluginNotInstalled, err)
})
})
@@ -179,7 +177,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
require.Equal(t, plugins.ErrInstallCorePlugin, err)
t.Run(fmt.Sprintf("Can't uninstall %s plugin", tc.class), func(t *testing.T) {
err = pm.Remove(context.Background(), p.ID)
err = pm.Remove(context.Background(), p.ID, p.Info.Version)
require.Equal(t, plugins.ErrUninstallCorePlugin, err)
})
}
@@ -6,9 +6,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
)
// DefaultFindFunc is the default function used for the Find step of the Discovery stage. It will scan the local
@@ -17,44 +15,6 @@ func DefaultFindFunc(cfg *config.Cfg) FindFunc {
return finder.NewLocalFinder(cfg.DevMode, cfg.Features).Find
}
// DuplicatePluginValidation is a filter step that will filter out any plugins that are already registered with the
// registry. This includes both the primary plugin and any child plugins, which are matched using the plugin ID field.
type DuplicatePluginValidation struct {
registry registry.Service
log log.Logger
}
// NewDuplicatePluginFilterStep returns a new DuplicatePluginValidation.
func NewDuplicatePluginFilterStep(registry registry.Service) *DuplicatePluginValidation {
return &DuplicatePluginValidation{
registry: registry,
log: log.New("plugins.dedupe"),
}
}
// Filter will filter out any plugins that are already registered with the registry.
func (d *DuplicatePluginValidation) Filter(ctx context.Context, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) {
res := make([]*plugins.FoundBundle, 0, len(bundles))
for _, b := range bundles {
_, exists := d.registry.Plugin(ctx, b.Primary.JSONData.ID)
if exists {
d.log.Warn("Skipping loading of plugin as it's a duplicate", "pluginId", b.Primary.JSONData.ID)
continue
}
for _, child := range b.Children {
_, exists = d.registry.Plugin(ctx, child.JSONData.ID)
if exists {
d.log.Warn("Skipping loading of child plugin as it's a duplicate", "pluginId", child.JSONData.ID)
continue
}
}
res = append(res, b)
}
return res, nil
}
// PermittedPluginTypesFilter is a filter step that will filter out any plugins that are not of a permitted type.
type PermittedPluginTypesFilter struct {
permittedTypes []plugins.Type
@@ -54,7 +54,7 @@ func newDeregister(pluginRegistry registry.Service) *Deregister {
// Deregister removes a plugin from the plugin registry.
func (d *Deregister) Deregister(ctx context.Context, p *plugins.Plugin) error {
if err := d.pluginRegistry.Remove(ctx, p.ID); err != nil {
if err := d.pluginRegistry.Remove(ctx, p.ID, p.Info.Version); err != nil {
return err
}
d.log.Debug("Plugin unregistered", "pluginId", p.ID)
+3 -3
View File
@@ -8,12 +8,12 @@ import (
// Service is responsible for the internal storing and retrieval of plugins.
type Service interface {
// Plugin finds a plugin by its ID.
Plugin(ctx context.Context, id string) (*plugins.Plugin, bool)
// Plugin finds a plugin by its ID and version.
Plugin(ctx context.Context, id, version string) (*plugins.Plugin, bool)
// Plugins returns all plugins.
Plugins(ctx context.Context) []*plugins.Plugin
// Add adds the provided plugin to the registry.
Add(ctx context.Context, plugin *plugins.Plugin) error
// Remove deletes the requested plugin from the registry.
Remove(ctx context.Context, id string) error
Remove(ctx context.Context, id, version string) error
}
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
)
// InMemory is a registry that only allows a single version of a plugin to be registered at a time.
type InMemory struct {
store map[string]*plugins.Plugin
alias map[string]*plugins.Plugin
@@ -25,7 +26,7 @@ func NewInMemory() *InMemory {
}
}
func (i *InMemory) Plugin(_ context.Context, pluginID string) (*plugins.Plugin, bool) {
func (i *InMemory) Plugin(_ context.Context, pluginID, _ string) (*plugins.Plugin, bool) {
return i.plugin(pluginID)
}
@@ -56,7 +57,7 @@ func (i *InMemory) Add(_ context.Context, p *plugins.Plugin) error {
return nil
}
func (i *InMemory) Remove(_ context.Context, pluginID string) error {
func (i *InMemory) Remove(_ context.Context, pluginID, _ string) error {
p, ok := i.plugin(pluginID)
if !ok {
return fmt.Errorf("plugin %s is not registered", pluginID)
+50 -16
View File
@@ -11,29 +11,41 @@ import (
"github.com/grafana/grafana/pkg/plugins"
)
const pluginID = "test-ds"
const (
pluginID = "test-ds"
v1 = "1.0.0"
v2 = "2.0.0"
)
func TestInMemory(t *testing.T) {
t.Run("Test mix of registry operations", func(t *testing.T) {
i := NewInMemory()
ctx := context.Background()
p, exists := i.Plugin(ctx, pluginID)
p, exists := i.Plugin(ctx, pluginID, v1)
require.False(t, exists)
require.Nil(t, p)
err := i.Remove(ctx, pluginID)
err := i.Remove(ctx, pluginID, v1)
require.EqualError(t, err, fmt.Errorf("plugin %s is not registered", pluginID).Error())
pv1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: pluginID, Info: plugins.Info{Version: v1}}}
err = i.Add(ctx, pv1)
require.NoError(t, err)
pv2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: pluginID, Info: plugins.Info{Version: v2}}}
err = i.Add(ctx, pv2)
require.Errorf(t, err, fmt.Sprintf("plugin %s is already registered", pluginID))
existingP, exists := i.Plugin(ctx, pluginID, v1)
require.True(t, exists)
require.Equal(t, pv1, existingP)
p = &plugins.Plugin{JSONData: plugins.JSONData{ID: pluginID}}
err = i.Add(ctx, p)
require.NoError(t, err)
require.Errorf(t, err, fmt.Sprintf("plugin %s is already registered", pluginID))
existingP, exists := i.Plugin(ctx, pluginID)
require.True(t, exists)
require.Equal(t, p, existingP)
err = i.Remove(ctx, pluginID)
err = i.Remove(ctx, pluginID, v1)
require.NoError(t, err)
existingPlugins := i.Plugins(ctx)
@@ -87,6 +99,28 @@ func TestInMemory_Add(t *testing.T) {
},
err: fmt.Errorf("plugin %s is already registered", pluginID),
},
{
name: "Cannot add a plugin to the registry even if it has a different version",
mocks: mocks{
store: map[string]*plugins.Plugin{
pluginID: {
JSONData: plugins.JSONData{
ID: pluginID,
Info: plugins.Info{Version: v1},
},
},
},
},
args: args{
p: &plugins.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
Info: plugins.Info{Version: v2},
},
},
},
err: fmt.Errorf("plugin %s is already registered", pluginID),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -151,7 +185,7 @@ func TestInMemory_Plugin(t *testing.T) {
i := &InMemory{
store: tt.mocks.store,
}
p, exists := i.Plugin(context.Background(), tt.args.pluginID)
p, exists := i.Plugin(context.Background(), tt.args.pluginID, "")
if exists != tt.exists {
t.Errorf("Plugin() got1 = %v, expected %v", exists, tt.exists)
}
@@ -265,7 +299,7 @@ func TestInMemory_Remove(t *testing.T) {
i := &InMemory{
store: tt.mocks.store,
}
err := i.Remove(context.Background(), tt.args.pluginID)
err := i.Remove(context.Background(), tt.args.pluginID, "")
require.Equal(t, tt.err, err)
})
}
@@ -280,7 +314,7 @@ func TestAliasSupport(t *testing.T) {
pluginIdOld := "plugin-old"
pluginIdOld2 := "plugin-old2"
p, exists := i.Plugin(ctx, pluginIdNew)
p, exists := i.Plugin(ctx, pluginIdNew, "")
require.False(t, exists)
require.Nil(t, p)
@@ -294,17 +328,17 @@ func TestAliasSupport(t *testing.T) {
require.NoError(t, err)
// Can lookup by the new ID
found, exists := i.Plugin(ctx, pluginIdNew)
found, exists := i.Plugin(ctx, pluginIdNew, "")
require.True(t, exists)
require.Equal(t, pluginNew, found)
// Can lookup by the old ID
found, exists = i.Plugin(ctx, pluginIdOld)
found, exists = i.Plugin(ctx, pluginIdOld, "")
require.True(t, exists)
require.Equal(t, pluginNew, found)
// Can lookup by the other old ID
found, exists = i.Plugin(ctx, pluginIdOld2)
found, exists = i.Plugin(ctx, pluginIdOld2, "")
require.True(t, exists)
require.Equal(t, pluginNew, found)
@@ -313,7 +347,7 @@ func TestAliasSupport(t *testing.T) {
ID: pluginIdOld,
}}
require.NoError(t, i.Add(ctx, pluginOld))
found, exists = i.Plugin(ctx, pluginIdOld)
found, exists = i.Plugin(ctx, pluginIdOld, "")
require.True(t, exists)
require.Equal(t, pluginOld, found)
})
+4
View File
@@ -26,6 +26,10 @@ func NewServiceAPIBuilder() *ServiceAPIBuilder {
}
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar) *ServiceAPIBuilder {
if !features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
return nil // skip registration unless opting into aggregator mode
}
builder := NewServiceAPIBuilder()
apiregistration.RegisterAPI(NewServiceAPIBuilder())
return builder
@@ -322,7 +322,8 @@ func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, te
cfg.AutoAssignOrgId = 1
acstore := ProvideService(sql)
permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures())
teamService := teamimpl.ProvideService(sql, cfg)
teamService, err := teamimpl.ProvideService(sql, cfg)
require.NoError(t, err)
orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil))
require.NoError(t, err)
@@ -508,7 +508,8 @@ func checkSeededPermissions(t *testing.T, permissions []resourcePermissionDTO) {
func seedPermissions(t *testing.T, resourceID string, sql *sqlstore.SQLStore, service *Service) {
t.Helper()
// seed team 1 with "Edit" permission on dashboard 1
teamSvc := teamimpl.ProvideService(sql, sql.Cfg)
teamSvc, err := teamimpl.ProvideService(sql, sql.Cfg)
require.NoError(t, err)
team, err := teamSvc.CreateTeam("test", "test@test.com", 1)
require.NoError(t, err)
_, err = service.SetTeamPermission(context.Background(), team.OrgID, team.ID, resourceID, "Edit")
@@ -237,8 +237,9 @@ func setupTestEnvironment(t *testing.T, ops Options) (*Service, *sqlstore.SQLSto
sql := db.InitTestDB(t)
cfg := setting.NewCfg()
teamSvc := teamimpl.ProvideService(sql, cfg)
userSvc, err := userimpl.ProvideService(sql, nil, cfg, teamimpl.ProvideService(sql, cfg), nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService())
teamSvc, err := teamimpl.ProvideService(sql, cfg)
require.NoError(t, err)
userSvc, err := userimpl.ProvideService(sql, nil, cfg, teamSvc, nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService())
require.NoError(t, err)
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
@@ -138,7 +138,8 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *stor
}
func generateTeamsAndUsers(b *testing.B, db *sqlstore.SQLStore, users int) ([]int64, []int64) {
teamSvc := teamimpl.ProvideService(db, db.Cfg)
teamSvc, err := teamimpl.ProvideService(db, db.Cfg)
require.NoError(b, err)
numberOfTeams := int(math.Ceil(float64(users) / UsersPerTeam))
globalUserId := 0
qs := quotatest.New(false, nil)
+4 -2
View File
@@ -4,7 +4,6 @@
```ini
[feature_toggles]
grafanaAPIServer = true
kubernetesPlaylists = true
```
@@ -51,6 +50,10 @@ data/grafana-apiserver
└── hi.json
```
## Enable aggregation
See [aggregator/README.md](./aggregator/README.md) for more information.
### `kubectl` access
For kubectl to work, grafana needs to run over https. To simplify development, you can use:
@@ -59,7 +62,6 @@ For kubectl to work, grafana needs to run over https. To simplify development,
app_mode = development
[feature_toggles]
grafanaAPIServer = true
grafanaAPIServerEnsureKubectlAccess = true
kubernetesPlaylists = true
```
+69 -8
View File
@@ -15,10 +15,13 @@ roll out features for each service without downtime.
To read more about the concept, see
[here](https://kubernetes.io/docs/tasks/extend-kubernetes/setup-extension-api-server/).
Note that, this aggregation will be a totally internal detail to Grafana. External fully functional APIServers that
may themselves act as parent API Servers to Grafana will never be made aware of them. Any of the `APIService`
related to Grafana Groups registered in a real K8s environment will take the address of Grafana's
parent server (which will bundle grafana-aggregator).
Note that this aggregation will be a totally internal detail to Grafana. External fully functional API Servers that
may themselves act as parent API Servers to Grafana will never be made aware of internal Grafana API Servers.
Thus, any `APIService` objects corresponding to Grafana's API groups will take the address of
Grafana's main API Server (the one that bundles grafana-aggregator).
Also, note that the single binary OSS offering of Grafana doesn't make use of the aggregator component at all, instead
opting for local installation of all the Grafana API groups.
### kube-aggregator versus grafana-aggregator
@@ -41,7 +44,65 @@ live under that instead.
### Gotchas (Pay Attention)
1. `grafana-aggregator` uses file storage under `data/grafana-aggregator` (`apiregistration.k8s.io`,
`service.grafana.app`) and `data/grafana-apiextensions` (`apiextensions.k8s.io`).
2. Since `grafana-aggregator` outputs configuration (TLS and kubeconfig) that is used in the invocation of aggregated
servers, ensure you start the aggregated service after launching the aggregator during local development.
1. `grafana-aggregator` uses file storage under `data/grafana-apiserver` (`apiregistration.k8s.io`,
`service.grafana.app`). Thus, any restarts will still have any prior configured aggregation in effect.
2. During local development, ensure you start the aggregated service after launching the aggregator. This is
so you have TLS and kubeconfig available for use with example aggregated api servers.
3. Ensure you have `grafanaAPIServerWithExperimentalAPIs = false` in your custom.ini. Otherwise, the example
service the following guide uses for the aggregation test is bundled as a `Local` `APIService` and will cause
configuration overwrites on startup.
## Testing aggregation locally
1. Generate the PKI using `openssl` (for development purposes, we will use the CN of `system:masters`):
```shell
./hack/make-aggregator-pki.sh
```
2. Configure the aggregator:
```ini
[feature_toggles]
grafanaAPIServerEnsureKubectlAccess = true
; disable the experimental APIs flag to disable bundling of the example service locally
grafanaAPIServerWithExperimentalAPIs = false
kubernetesAggregator = true
[grafana-apiserver]
proxy_client_cert_file = ./data/grafana-aggregator/client.crt
proxy_client_key_file = ./data/grafana-aggregator/client.key
```
3. Start the server
```shell
make run
```
4. In another tab, apply the manifests:
```shell
export KUBECONFIG=$PWD/data/grafana-apiserver/grafana.kubeconfig
kubectl apply -f ./pkg/services/apiserver/aggregator/examples/
# SAMPLE OUTPUT
# apiservice.apiregistration.k8s.io/v0alpha1.example.grafana.app created
# externalname.service.grafana.app/example-apiserver created
kubectl get apiservice
# SAMPLE OUTPUT
# NAME SERVICE AVAILABLE AGE
# v0alpha1.example.grafana.app grafana/example-apiserver False (FailedDiscoveryCheck) 29m
```
5. In another tab, start the example microservice that will be aggregated by the parent apiserver:
```shell
go run ./pkg/cmd/grafana apiserver \
--runtime-config=example.grafana.app/v0alpha1=true \
--secure-port 7443 \
--client-ca-file=$PWD/data/grafana-aggregator/ca.crt
```
6. After 10 seconds, check `APIService` again. It should report as available.
```shell
export KUBECONFIG=$PWD/data/grafana-apiserver/grafana.kubeconfig
kubectl get apiservice
# SAMPLE OUTPUT
# NAME SERVICE AVAILABLE AGE
# v0alpha1.example.grafana.app grafana/example-apiserver True 30m
```
7. For tear down of the above test:
```shell
kubectl delete -f ./pkg/services/apiserver/aggregator/examples/
```
@@ -104,9 +104,7 @@ func CreateAggregatorServer(aggregatorConfig *aggregatorapiserver.Config, shared
}
err = aggregatorServer.GenericAPIServer.AddPostStartHook("grafana-apiserver-autoregistration", func(context genericapiserver.PostStartHookContext) error {
go func() {
autoRegistrationController.Run(5, context.StopCh)
}()
go autoRegistrationController.Run(5, context.StopCh)
return nil
})
if err != nil {
@@ -198,16 +196,20 @@ func makeAPIServiceAvailableHealthCheck(name string, apiServices []*v1.APIServic
}
// Watch add/update events for APIServices
_, _ = apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
_, err := apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { handleAPIServiceChange(obj.(*v1.APIService)) },
UpdateFunc: func(old, new interface{}) { handleAPIServiceChange(new.(*v1.APIService)) },
})
if err != nil {
klog.Errorf("Failed to watch APIServices for health check: %v", err)
}
// Don't return healthy until the pending list is empty
return healthz.NamedCheck(name, func(r *http.Request) error {
pendingServiceNamesLock.RLock()
defer pendingServiceNamesLock.RUnlock()
if pendingServiceNames.Len() > 0 {
klog.Error("APIServices not yet available", "services", pendingServiceNames.List())
return fmt.Errorf("missing APIService: %v", pendingServiceNames.List())
}
return nil
@@ -0,0 +1,14 @@
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v0alpha1.example.grafana.app
spec:
version: v0alpha1
insecureSkipTLSVerify: true
group: example.grafana.app
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: example-apiserver
namespace: grafana
port: 7443
@@ -0,0 +1,7 @@
apiVersion: service.grafana.app/v0alpha1
kind: ExternalName
metadata:
name: example-apiserver
namespace: grafana
spec:
host: localhost
@@ -4,7 +4,9 @@ import (
"context"
"k8s.io/apimachinery/pkg/runtime/schema"
k8suser "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
"k8s.io/apiserver/pkg/authorization/union"
orgsvc "github.com/grafana/grafana/pkg/services/org"
@@ -21,6 +23,7 @@ type GrafanaAuthorizer struct {
func NewGrafanaAuthorizer(cfg *setting.Cfg, orgService orgsvc.Service) *GrafanaAuthorizer {
authorizers := []authorizer.Authorizer{
&impersonationAuthorizer{},
authorizerfactory.NewPrivilegedGroups(k8suser.SystemPrivilegedGroup),
}
// In Hosted grafana, the StackID replaces the orgID as a valid namespace
+8 -3
View File
@@ -29,20 +29,25 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o
host := fmt.Sprintf("%s:%d", ip, port)
o.RecommendedOptions.Etcd.StorageConfig.Transport.ServerList = cfg.SectionWithEnvOverrides("grafana-apiserver").Key("etcd_servers").Strings(",")
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
o.RecommendedOptions.Etcd.StorageConfig.Transport.ServerList = apiserverCfg.Key("etcd_servers").Strings(",")
o.RecommendedOptions.SecureServing.BindAddress = ip
o.RecommendedOptions.SecureServing.BindPort = port
o.RecommendedOptions.Authentication.RemoteKubeConfigFileOptional = true
o.RecommendedOptions.Authorization.RemoteKubeConfigFileOptional = true
o.AggregatorOptions.ProxyClientCertFile = apiserverCfg.Key("proxy_client_cert_file").MustString("")
o.AggregatorOptions.ProxyClientKeyFile = apiserverCfg.Key("proxy_client_key_file").MustString("")
o.RecommendedOptions.Admission = nil
o.RecommendedOptions.CoreAPI = nil
o.StorageOptions.StorageType = options.StorageType(cfg.SectionWithEnvOverrides("grafana-apiserver").Key("storage_type").MustString(string(options.StorageTypeLegacy)))
o.StorageOptions.StorageType = options.StorageType(apiserverCfg.Key("storage_type").MustString(string(options.StorageTypeLegacy)))
o.StorageOptions.DataPath = filepath.Join(cfg.DataPath, "grafana-apiserver")
o.ExtraOptions.DevMode = features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerEnsureKubectlAccess)
o.ExtraOptions.ExternalAddress = host
o.ExtraOptions.APIURL = apiURL
o.ExtraOptions.Verbosity = defaultLogLevel
o.ExtraOptions.Verbosity = apiserverCfg.Key("log_level").MustInt(defaultLogLevel)
}
@@ -0,0 +1,132 @@
package responsewriter
import (
"bufio"
"fmt"
"io"
"net/http"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/klog/v2"
)
var _ responsewriter.CloseNotifierFlusher = (*ResponseAdapter)(nil)
var _ http.ResponseWriter = (*ResponseAdapter)(nil)
var _ io.ReadCloser = (*ResponseAdapter)(nil)
func WrapHandler(handler http.Handler) func(req *http.Request) (*http.Response, error) {
// ignore the lint error because the response is passed directly to the client,
// so the client will be responsible for closing the response body.
//nolint:bodyclose
return func(req *http.Request) (*http.Response, error) {
w := NewAdapter(req)
resp := w.Response()
go func() {
handler.ServeHTTP(w, req)
if err := w.CloseWriter(); err != nil {
klog.Errorf("error closing writer: %v", err)
}
}()
return resp, nil
}
}
// ResponseAdapter is an implementation of [http.ResponseWriter] that allows conversion to a [http.Response].
type ResponseAdapter struct {
req *http.Request
res *http.Response
reader io.ReadCloser
writer io.WriteCloser
buffered *bufio.ReadWriter
}
// NewAdapter returns an initialized [ResponseAdapter].
func NewAdapter(req *http.Request) *ResponseAdapter {
r, w := io.Pipe()
writer := bufio.NewWriter(w)
reader := bufio.NewReader(r)
buffered := bufio.NewReadWriter(reader, writer)
return &ResponseAdapter{
req: req,
res: &http.Response{
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: make(http.Header),
},
reader: r,
writer: w,
buffered: buffered,
}
}
// Header implements [http.ResponseWriter].
// It returns the response headers to mutate within a handler.
func (ra *ResponseAdapter) Header() http.Header {
return ra.res.Header
}
// Write implements [http.ResponseWriter].
func (ra *ResponseAdapter) Write(buf []byte) (int, error) {
return ra.buffered.Write(buf)
}
// Read implements [io.Reader].
func (ra *ResponseAdapter) Read(buf []byte) (int, error) {
return ra.buffered.Read(buf)
}
// WriteHeader implements [http.ResponseWriter].
func (ra *ResponseAdapter) WriteHeader(code int) {
ra.res.StatusCode = code
ra.res.Status = fmt.Sprintf("%03d %s", code, http.StatusText(code))
}
// Flush implements [http.Flusher].
func (ra *ResponseAdapter) Flush() {
if ra.buffered.Writer.Buffered() == 0 {
return
}
if err := ra.buffered.Writer.Flush(); err != nil {
klog.Error("Error flushing response buffer: ", "error", err)
}
}
// Response returns the [http.Response] generated by the [http.Handler].
func (ra *ResponseAdapter) Response() *http.Response {
// make sure to set the status code to 200 if the request is a watch
// this is to ensure that client-go uses a streamwatcher:
// https://github.com/kubernetes/client-go/blob/76174b8af8cfd938018b04198595d65b48a69334/rest/request.go#L737
if ra.res.StatusCode == 0 && ra.req.URL.Query().Get("watch") == "true" {
ra.WriteHeader(http.StatusOK)
}
ra.res.Body = ra
return ra.res
}
// Decorate implements [responsewriter.UserProvidedDecorator].
func (ra *ResponseAdapter) Unwrap() http.ResponseWriter {
return ra
}
// CloseNotify implements [http.CloseNotifier].
func (ra *ResponseAdapter) CloseNotify() <-chan bool {
ch := make(chan bool)
go func() {
<-ra.req.Context().Done()
ch <- true
}()
return ch
}
// Close implements [io.Closer].
func (ra *ResponseAdapter) Close() error {
return ra.reader.Close()
}
// CloseWriter should be called after the http.Handler has returned.
func (ra *ResponseAdapter) CloseWriter() error {
ra.Flush()
return ra.writer.Close()
}
@@ -0,0 +1,136 @@
package responsewriter_test
import (
"io"
"math/rand"
"net/http"
"testing"
"time"
grafanaresponsewriter "github.com/grafana/grafana/pkg/services/apiserver/endpoints/responsewriter"
"github.com/stretchr/testify/require"
)
func TestResponseAdapter(t *testing.T) {
t.Run("should handle synchronous write", func(t *testing.T) {
client := &http.Client{
Transport: &roundTripperFunc{
ready: make(chan struct{}),
// ignore the lint error because the response is passed directly to the client,
// so the client will be responsible for closing the response body.
//nolint:bodyclose
fn: grafanaresponsewriter.WrapHandler(http.HandlerFunc(syncHandler)),
},
}
close(client.Transport.(*roundTripperFunc).ready)
req, err := http.NewRequest("GET", "http://localhost/test", nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
defer func() {
err := resp.Body.Close()
require.NoError(t, err)
}()
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "OK", string(bodyBytes))
})
t.Run("should handle synchronous write", func(t *testing.T) {
generateRandomStrings(10)
client := &http.Client{
Transport: &roundTripperFunc{
ready: make(chan struct{}),
// ignore the lint error because the response is passed directly to the client,
// so the client will be responsible for closing the response body.
//nolint:bodyclose
fn: grafanaresponsewriter.WrapHandler(http.HandlerFunc(asyncHandler)),
},
}
close(client.Transport.(*roundTripperFunc).ready)
req, err := http.NewRequest("GET", "http://localhost/test?watch=true", nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
defer func() {
err := resp.Body.Close()
require.NoError(t, err)
}()
// ensure that watch request is a 200
require.Equal(t, http.StatusOK, resp.StatusCode)
// limit to 100 bytes to test the reader buffer
buf := make([]byte, 100)
// holds the read bytes between iterations
cache := []byte{}
for i := 0; i < 10; {
n, err := resp.Body.Read(buf)
require.NoError(t, err)
if n == 0 {
continue
}
cache = append(cache, buf[:n]...)
if len(cache) >= len(randomStrings[i]) {
str := cache[:len(randomStrings[i])]
require.Equal(t, randomStrings[i], string(str))
cache = cache[len(randomStrings[i]):]
i++
}
}
})
}
func syncHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}
func asyncHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
for _, s := range randomStrings {
time.Sleep(100 * time.Millisecond)
// write the current iteration
_, _ = w.Write([]byte(s))
w.(http.Flusher).Flush()
}
}
var randomStrings = []string{}
func generateRandomStrings(n int) {
for i := 0; i < n; i++ {
randomString := generateRandomString(1000 * (i + 1))
randomStrings = append(randomStrings, randomString)
}
}
func generateRandomString(n int) string {
gen := rand.New(rand.NewSource(time.Now().UnixNano()))
var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, n)
for i := range b {
b[i] = chars[gen.Intn(len(chars))]
}
return string(b)
}
type roundTripperFunc struct {
ready chan struct{}
fn func(req *http.Request) (*http.Response, error)
}
func (f *roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
if f.fn == nil {
<-f.ready
}
res, err := f.fn(req)
return res, err
}
@@ -97,6 +97,9 @@ func (o *AggregatorServerOptions) ApplyTo(aggregatorConfig *aggregatorapiserver.
}
genericConfig.MergedResourceConfig = mergedResourceConfig
aggregatorConfig.ExtraConfig.ProxyClientCertFile = o.ProxyClientCertFile
aggregatorConfig.ExtraConfig.ProxyClientKeyFile = o.ProxyClientKeyFile
namer := openapinamer.NewDefinitionNamer(aggregatorscheme.Scheme)
genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(o.getMergedOpenAPIDefinitions, namer)
genericConfig.OpenAPIV3Config.Info.Title = "Kubernetes"
+5 -6
View File
@@ -75,6 +75,9 @@ func (o *Options) Validate() []error {
func (o *Options) ApplyTo(serverConfig *genericapiserver.RecommendedConfig) error {
serverConfig.AggregatedDiscoveryGroupManager = aggregated.NewResourceManager("apis")
// avoid picking up an in-cluster service account token
o.RecommendedOptions.Authentication.SkipInClusterLookup = true
if err := o.ExtraOptions.ApplyTo(serverConfig); err != nil {
return err
}
@@ -87,12 +90,8 @@ func (o *Options) ApplyTo(serverConfig *genericapiserver.RecommendedConfig) erro
return err
}
if o.ExtraOptions.DevMode {
// NOTE: Only consider authn for dev mode - resolves the failure due to missing extension apiserver auth-config
// in parent k8s
if err := o.RecommendedOptions.Authentication.ApplyTo(&serverConfig.Authentication, serverConfig.SecureServing, serverConfig.OpenAPIConfig); err != nil {
return err
}
if err := o.RecommendedOptions.Authentication.ApplyTo(&serverConfig.Authentication, serverConfig.SecureServing, serverConfig.OpenAPIConfig); err != nil {
return err
}
if !o.ExtraOptions.DevMode {
+87 -44
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"path"
"github.com/grafana/dskit/services"
@@ -26,8 +25,10 @@ import (
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/modules"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/apiserver/aggregator"
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
grafanaresponsewriter "github.com/grafana/grafana/pkg/services/apiserver/endpoints/responsewriter"
grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options"
entitystorage "github.com/grafana/grafana/pkg/services/apiserver/storage/entity"
filestorage "github.com/grafana/grafana/pkg/services/apiserver/storage/file"
@@ -189,12 +190,17 @@ func (s *service) start(ctx context.Context) error {
groupVersions := make([]schema.GroupVersion, 0, len(builders))
// Install schemas
for _, b := range builders {
for i, b := range builders {
groupVersions = append(groupVersions, b.GetGroupVersion())
if err := b.InstallSchema(Scheme); err != nil {
return err
}
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
// set the priority for the group+version
aggregator.APIVersionPriorities[b.GetGroupVersion()] = aggregator.Priority{Group: 15000, Version: int32(i + 1)}
}
auth := b.GetAuthorizer()
if auth != nil {
s.authorizer.Register(b.GetGroupVersion(), auth)
@@ -216,7 +222,7 @@ func (s *service) start(ctx context.Context) error {
serverConfig.Authorization.Authorizer = s.authorizer
serverConfig.TracerProvider = s.tracing.GetTracerProvider()
// setup loopback transport
// setup loopback transport for the aggregator server
transport := &roundTripperFunc{ready: make(chan struct{})}
serverConfig.LoopbackClientConfig.Transport = transport
serverConfig.LoopbackClientConfig.TLSClientConfig = clientrest.TLSClientConfig{}
@@ -283,41 +289,100 @@ func (s *service) start(ctx context.Context) error {
return err
}
// dual writing is only enabled when the storage type is not legacy.
// this is needed to support setting a default RESTOptionsGetter for new APIs that don't
// support the legacy storage type.
dualWriteEnabled := o.StorageOptions.StorageType != grafanaapiserveroptions.StorageTypeLegacy
// Install the API Group+version
// Install the API group+version
err = builder.InstallAPIs(Scheme, Codecs, server, serverConfig.RESTOptionsGetter, builders, dualWriteEnabled)
if err != nil {
return err
}
// set the transport function and signal that it's ready
transport.fn = func(req *http.Request) (*http.Response, error) {
w := newWrappedResponseWriter()
resp := responsewriter.WrapForHTTP1Or2(w)
server.Handler.ServeHTTP(resp, req)
return w.Result(), nil
}
close(transport.ready)
// stash the options for later use
s.options = o
// only write kubeconfig in dev mode
if o.ExtraOptions.DevMode {
if err := ensureKubeConfig(server.LoopbackClientConfig, o.StorageOptions.DataPath); err != nil {
var runningServer *genericapiserver.GenericAPIServer
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
runningServer, err = s.startAggregator(transport, serverConfig, server)
if err != nil {
return err
}
} else {
runningServer, err = s.startCoreServer(transport, serverConfig, server)
if err != nil {
return err
}
}
// Used by the proxy wrapper registered in ProvideService
s.handler = server.Handler
s.restConfig = server.LoopbackClientConfig
s.options = o
// only write kubeconfig in dev mode
if o.ExtraOptions.DevMode {
if err := ensureKubeConfig(runningServer.LoopbackClientConfig, o.StorageOptions.DataPath); err != nil {
return err
}
}
// used by the proxy wrapper registered in ProvideService
s.handler = runningServer.Handler
// used by local clients to make requests to the server
s.restConfig = runningServer.LoopbackClientConfig
return nil
}
func (s *service) startCoreServer(
transport *roundTripperFunc,
serverConfig *genericapiserver.RecommendedConfig,
server *genericapiserver.GenericAPIServer,
) (*genericapiserver.GenericAPIServer, error) {
// setup the loopback transport and signal that it's ready.
// ignore the lint error because the response is passed directly to the client,
// so the client will be responsible for closing the response body.
// nolint:bodyclose
transport.fn = grafanaresponsewriter.WrapHandler(server.Handler)
close(transport.ready)
prepared := server.PrepareRun()
go func() {
s.stoppedCh <- prepared.Run(s.stopCh)
}()
return server, nil
}
func (s *service) startAggregator(
transport *roundTripperFunc,
serverConfig *genericapiserver.RecommendedConfig,
server *genericapiserver.GenericAPIServer,
) (*genericapiserver.GenericAPIServer, error) {
aggregatorConfig, aggregatorInformers, err := aggregator.CreateAggregatorConfig(s.options, *serverConfig)
if err != nil {
return nil, err
}
aggregatorServer, err := aggregator.CreateAggregatorServer(aggregatorConfig, aggregatorInformers, server)
if err != nil {
return nil, err
}
// setup the loopback transport for the aggregator server and signal that it's ready
// ignore the lint error because the response is passed directly to the client,
// so the client will be responsible for closing the response body.
// nolint:bodyclose
transport.fn = grafanaresponsewriter.WrapHandler(aggregatorServer.GenericAPIServer.Handler)
close(transport.ready)
prepared, err := aggregatorServer.PrepareRun()
if err != nil {
return nil, err
}
go func() {
s.stoppedCh <- prepared.Run(s.stopCh)
}()
return nil
return aggregatorServer.GenericAPIServer, nil
}
func (s *service) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config {
@@ -325,9 +390,8 @@ func (s *service) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Co
Transport: &roundTripperFunc{
fn: func(req *http.Request) (*http.Response, error) {
ctx := appcontext.WithUser(req.Context(), c.SignedInUser)
w := httptest.NewRecorder()
s.handler.ServeHTTP(w, req.WithContext(ctx))
return w.Result(), nil
wrapped := grafanaresponsewriter.WrapHandler(s.handler)
return wrapped(req.WithContext(ctx))
},
},
}
@@ -367,24 +431,3 @@ func (f *roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error)
}
return f.fn(req)
}
var _ http.ResponseWriter = (*wrappedResponseWriter)(nil)
var _ responsewriter.UserProvidedDecorator = (*wrappedResponseWriter)(nil)
type wrappedResponseWriter struct {
*httptest.ResponseRecorder
}
func newWrappedResponseWriter() *wrappedResponseWriter {
w := httptest.NewRecorder()
return &wrappedResponseWriter{w}
}
func (w *wrappedResponseWriter) Unwrap() http.ResponseWriter {
return w.ResponseRecorder
}
func (w *wrappedResponseWriter) CloseNotify() <-chan bool {
// TODO: this is probably not the right thing to do here
return make(<-chan bool)
}
+37 -23
View File
@@ -12,6 +12,7 @@ import (
"path/filepath"
"reflect"
"strings"
"sync"
"time"
"github.com/bwmarrin/snowflake"
@@ -37,15 +38,16 @@ var errResourceVersionSetOnCreate = errors.New("resourceVersion should not be se
// Storage implements storage.Interface and storage resources as JSON files on disk.
type Storage struct {
root string
gr schema.GroupResource
codec runtime.Codec
keyFunc func(obj runtime.Object) (string, error)
newFunc func() runtime.Object
newListFunc func() runtime.Object
getAttrsFunc storage.AttrFunc
trigger storage.IndexerFuncs
indexers *cache.Indexers
root string
resourcePrefix string
gr schema.GroupResource
codec runtime.Codec
keyFunc func(obj runtime.Object) (string, error)
newFunc func() runtime.Object
newListFunc func() runtime.Object
getAttrsFunc storage.AttrFunc
trigger storage.IndexerFuncs
indexers *cache.Indexers
watchSet *WatchSet
}
@@ -56,8 +58,16 @@ var ErrFileNotExists = fmt.Errorf("file doesn't exist")
// ErrNamespaceNotExists means the directory for the namespace doesn't actually exist.
var ErrNamespaceNotExists = errors.New("namespace does not exist")
var (
node *snowflake.Node
once sync.Once
)
func getResourceVersion() (*uint64, error) {
node, err := snowflake.NewNode(1)
var err error
once.Do(func() {
node, err = snowflake.NewNode(1)
})
if err != nil {
return nil, err
}
@@ -78,20 +88,22 @@ func NewStorage(
trigger storage.IndexerFuncs,
indexers *cache.Indexers,
) (storage.Interface, factory.DestroyFunc, error) {
if err := ensureDir(resourcePrefix); err != nil {
return nil, func() {}, fmt.Errorf("could not establish a writable directory at path=%s", resourcePrefix)
root := config.Prefix
if err := ensureDir(root); err != nil {
return nil, func() {}, fmt.Errorf("could not establish a writable directory at path=%s", root)
}
ws := NewWatchSet()
return &Storage{
root: resourcePrefix,
gr: config.GroupResource,
codec: config.Codec,
keyFunc: keyFunc,
newFunc: newFunc,
newListFunc: newListFunc,
getAttrsFunc: getAttrsFunc,
trigger: trigger,
indexers: indexers,
root: root,
resourcePrefix: resourcePrefix,
gr: config.GroupResource,
codec: config.Codec,
keyFunc: keyFunc,
newFunc: newFunc,
newListFunc: newListFunc,
getAttrsFunc: getAttrsFunc,
trigger: trigger,
indexers: indexers,
watchSet: ws,
}, func() {
@@ -352,7 +364,9 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti
}
}
objs, err := readDirRecursive(s.codec, key, s.newFunc)
dirname := s.dirPath(key)
objs, err := readDirRecursive(s.codec, dirname, s.newFunc)
if err != nil {
return err
}
@@ -524,5 +538,5 @@ func (s *Storage) validateMinimumResourceVersion(minimumResourceVersion string,
}
func (s *Storage) nameFromKey(key string) string {
return strings.Replace(key, s.root+"/", "", 1)
return strings.Replace(key, s.resourcePrefix+"/", "", 1)
}
@@ -3,7 +3,8 @@
package file
import (
"path"
"os"
"path/filepath"
"time"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -21,7 +22,7 @@ type RESTOptionsGetter struct {
func NewRESTOptionsGetter(path string, originalStorageConfig storagebackend.Config) *RESTOptionsGetter {
if path == "" {
path = "/tmp/grafana-apiserver"
path = filepath.Join(os.TempDir(), "grafana-apiserver")
}
return &RESTOptionsGetter{path: path, original: originalStorageConfig}
@@ -47,11 +48,12 @@ func (r *RESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (gener
}
ret := generic.RESTOptions{
StorageConfig: storageConfig,
Decorator: NewStorage,
DeleteCollectionWorkers: 0,
EnableGarbageCollection: false,
ResourcePrefix: path.Join(storageConfig.Prefix, resource.Group, resource.Resource),
StorageConfig: storageConfig,
Decorator: NewStorage,
DeleteCollectionWorkers: 0,
EnableGarbageCollection: false,
// k8s expects forward slashes here, we'll convert them to os path separators in the storage
ResourcePrefix: "/" + resource.Group + "/" + resource.Resource,
CountMetricPollPeriod: 1 * time.Second,
StorageObjectCountTracker: storageConfig.Config.StorageObjectCountTracker,
}
+12 -1
View File
@@ -10,12 +10,23 @@ import (
"errors"
"os"
"path/filepath"
"strings"
"k8s.io/apimachinery/pkg/runtime"
)
func (s *Storage) filePath(key string) string {
return key + ".json"
// Replace backslashes with underscores to avoid creating bogus subdirectories
key = strings.Replace(key, "\\", "_", -1)
fileName := filepath.Join(s.root, filepath.Clean(key+".json"))
return fileName
}
func (s *Storage) dirPath(key string) string {
// Replace backslashes with underscores to avoid creating bogus subdirectories
key = strings.Replace(key, "\\", "_", -1)
dirName := filepath.Join(s.root, filepath.Clean(key))
return dirName
}
func writeFile(codec runtime.Codec, path string, obj runtime.Object) error {
+9 -1
View File
@@ -564,7 +564,8 @@ var (
{
Name: "logsExploreTableVisualisation",
Description: "A table visualisation for logs in Explore",
Stage: FeatureStageExperimental,
Stage: FeatureStageGeneralAvailability,
Expression: "true", // enabled by default,
FrontendOnly: true,
Owner: grafanaObservabilityLogsSquad,
},
@@ -1175,6 +1176,13 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaSharingSquad,
},
{
Name: "kubernetesAggregator",
Description: "Enable grafana aggregator",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
RequiresRestart: true,
},
}
)
+2 -1
View File
@@ -74,7 +74,7 @@ recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false
pluginsDynamicAngularDetectionPatterns,experimental,@grafana/plugins-platform-backend,false,false,false
vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,true
prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,true
logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,true
logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true
awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false
transformationsRedesign,GA,@grafana/observability-metrics,false,false,true
mlExpressions,experimental,@grafana/alerting-squad,false,false,false
@@ -157,3 +157,4 @@ promQLScope,experimental,@grafana/observability-metrics,false,false,false
nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,false,false,true
groupToNestedTableTransformation,preview,@grafana/dataviz-squad,false,false,true
newPDFRendering,experimental,@grafana/sharing-squad,false,false,false
kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
74 pluginsDynamicAngularDetectionPatterns experimental @grafana/plugins-platform-backend false false false
75 vizAndWidgetSplit experimental @grafana/dashboards-squad false false true
76 prometheusIncrementalQueryInstrumentation experimental @grafana/observability-metrics false false true
77 logsExploreTableVisualisation experimental GA @grafana/observability-logs false false true
78 awsDatasourcesTempCredentials experimental @grafana/aws-datasources false false false
79 transformationsRedesign GA @grafana/observability-metrics false false true
80 mlExpressions experimental @grafana/alerting-squad false false false
157 nodeGraphDotLayout experimental @grafana/observability-traces-and-profiling false false true
158 groupToNestedTableTransformation preview @grafana/dataviz-squad false false true
159 newPDFRendering experimental @grafana/sharing-squad false false false
160 kubernetesAggregator experimental @grafana/grafana-app-platform-squad false true false
+4
View File
@@ -638,4 +638,8 @@ const (
// FlagNewPDFRendering
// New implementation for the dashboard to PDF rendering
FlagNewPDFRendering = "newPDFRendering"
// FlagKubernetesAggregator
// Enable grafana aggregator
FlagKubernetesAggregator = "kubernetesAggregator"
)
+19 -3
View File
@@ -966,12 +966,15 @@
{
"metadata": {
"name": "logsExploreTableVisualisation",
"resourceVersion": "1689163200000",
"creationTimestamp": "2023-07-12T12:00:00Z"
"resourceVersion": "1707747885704",
"creationTimestamp": "2023-07-12T12:00:00Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-02-12 14:24:45.704022 +0000 UTC"
}
},
"spec": {
"description": "A table visualisation for logs in Explore",
"stage": "experimental",
"stage": "GA",
"codeowner": "@grafana/observability-logs",
"frontend": true
}
@@ -2063,6 +2066,19 @@
"codeowner": "@grafana/dashboards-squad",
"frontend": true
}
},
{
"metadata": {
"name": "kubernetesAggregator",
"resourceVersion": "1707775742552",
"creationTimestamp": "2024-02-12T22:09:02Z"
},
"spec": {
"description": "Enable grafana aggregator",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad",
"requiresRestart": true
}
}
]
}
@@ -1632,7 +1632,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log),
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log),
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, env.log),
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, 100, env.log),
}
}
+16
View File
@@ -251,6 +251,10 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGro
return toNamespaceErrorResponse(err)
}
if err := srv.checkGroupLimits(ruleGroupConfig); err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
rules, err := validateRuleGroup(&ruleGroupConfig, c.SignedInUser.GetOrgID(), namespace, srv.cfg)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
@@ -265,6 +269,18 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGro
return srv.updateAlertRulesInGroup(c, groupKey, rules)
}
func (srv RulerSrv) checkGroupLimits(group apimodels.PostableRuleGroupConfig) error {
if srv.cfg.RulesPerRuleGroupLimit > 0 && int64(len(group.Rules)) > srv.cfg.RulesPerRuleGroupLimit {
srv.log.Warn("Large rule group was edited. Large groups are discouraged and may be rejected in the future.",
"limit", srv.cfg.RulesPerRuleGroupLimit,
"actual", len(group.Rules),
"group", group.Name,
)
}
return nil
}
// updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database.
// All operations are performed in a single transaction
func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey ngmodels.AlertRuleGroupKey, rules []*ngmodels.AlertRuleWithOptionals) response.Response {
@@ -57,7 +57,8 @@ func NewTestMigrationStore(t testing.TB, sqlStore *sqlstore.SQLStore, cfg *setti
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
teamSvc := teamimpl.ProvideService(sqlStore, cfg)
teamSvc, err := teamimpl.ProvideService(sqlStore, cfg)
require.NoError(t, err)
orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService)
require.NoError(t, err)
userSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamSvc, cache, quotaService, bundleregistry.ProvideService())
+3 -1
View File
@@ -301,6 +301,7 @@ func (ng *AlertNG) init() error {
DoNotSaveNormalState: ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingNoNormalState),
ApplyNoDataAndErrorToAllStates: ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingNoDataErrorExecution),
MaxStateSaveConcurrency: ng.Cfg.UnifiedAlerting.MaxStateSaveConcurrency,
RulesPerRuleGroupLimit: ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit,
Tracer: ng.tracer,
Log: log.New("ngalert.state.manager"),
}
@@ -330,7 +331,8 @@ func (ng *AlertNG) init() error {
muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log)
alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.dashboardService, ng.QuotaService, ng.store,
int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()), ng.Log)
int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit, ng.Log)
ng.api = &api.API{
Cfg: ng.Cfg,
@@ -17,6 +17,7 @@ import (
type AlertRuleService struct {
defaultIntervalSeconds int64
baseIntervalSeconds int64
rulesPerRuleGroupLimit int64
ruleStore RuleStore
provenanceStore ProvisioningStore
dashboardService dashboards.DashboardService
@@ -32,10 +33,12 @@ func NewAlertRuleService(ruleStore RuleStore,
xact TransactionManager,
defaultIntervalSeconds int64,
baseIntervalSeconds int64,
rulesPerRuleGroupLimit int64,
log log.Logger) *AlertRuleService {
return &AlertRuleService{
defaultIntervalSeconds: defaultIntervalSeconds,
baseIntervalSeconds: baseIntervalSeconds,
rulesPerRuleGroupLimit: rulesPerRuleGroupLimit,
ruleStore: ruleStore,
provenanceStore: provenanceStore,
dashboardService: dashboardService,
@@ -248,6 +251,10 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int
}
}
if err := service.checkGroupLimits(group); err != nil {
return fmt.Errorf("write rejected due to exceeded limits: %w", err)
}
key := models.AlertRuleGroupKey{
OrgID: orgID,
NamespaceUID: group.FolderUID,
@@ -533,3 +540,15 @@ func withoutNilAlertRules(ptrs []*models.AlertRule) []models.AlertRule {
}
return result
}
func (service *AlertRuleService) checkGroupLimits(group models.AlertRuleGroup) error {
if service.rulesPerRuleGroupLimit > 0 && int64(len(group.Rules)) > service.rulesPerRuleGroupLimit {
service.log.Warn("Large rule group was edited. Large groups are discouraged and may be rejected in the future.",
"limit", service.rulesPerRuleGroupLimit,
"actual", len(group.Rules),
"group", group.Title,
)
}
return nil
}

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