From 69d1d4b124ab7141a7111d9e935acce7959ef6e0 Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Wed, 23 Feb 2022 14:27:50 -0600 Subject: [PATCH 01/34] remove flaky cloudwatch test (#45800) (#45807) * remove flaky cloudwatch tests * Skip flaking templating-dashboard-links-and-variables test (cherry picked from commit cbf96e6a8bafdaecf6937995ea77cd3fbdefe1c0) --- ...ting-dashboard-links-and-variables.spec.ts | 2 +- pkg/tests/api/metrics/api_metrics_test.go | 194 ------------------ 2 files changed, 1 insertion(+), 195 deletions(-) delete mode 100644 pkg/tests/api/metrics/api_metrics_test.go diff --git a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index bf6782e2d64..39c87b73448 100644 --- a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -5,7 +5,7 @@ e2e.scenario({ itName: 'Tests dashboard links and variables in links', addScenarioDataSource: false, addScenarioDashBoard: false, - skipScenario: false, + skipScenario: true, // Skipped because it was causing many failures in main. scenario: () => { e2e.flows.openDashboard({ uid: 'yBCC3aKGk' }); e2e() diff --git a/pkg/tests/api/metrics/api_metrics_test.go b/pkg/tests/api/metrics/api_metrics_test.go deleted file mode 100644 index 02d6cea7383..00000000000 --- a/pkg/tests/api/metrics/api_metrics_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package metrics - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/grafana/grafana/pkg/tsdb/cloudwatch" - - cwapi "github.com/aws/aws-sdk-go/service/cloudwatch" - "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestQueryCloudWatchMetrics(t *testing.T) { - grafDir, cfgPath := testinfra.CreateGrafDir(t) - - addr, sqlStore := testinfra.StartGrafana(t, grafDir, cfgPath) - setUpDatabase(t, sqlStore) - - origNewCWClient := cloudwatch.NewCWClient - t.Cleanup(func() { - cloudwatch.NewCWClient = origNewCWClient - }) - var client cloudwatch.FakeCWClient - cloudwatch.NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { - return client - } - - t.Run("Custom metrics", func(t *testing.T) { - client = cloudwatch.FakeCWClient{ - Metrics: []*cwapi.Metric{ - { - MetricName: aws.String("Test_MetricName"), - Dimensions: []*cwapi.Dimension{ - { - Name: aws.String("Test_DimensionName"), - }, - }, - }, - }, - } - - req := dtos.MetricRequest{ - Queries: []*simplejson.Json{ - simplejson.NewFromAny(map[string]interface{}{ - "type": "metricFindQuery", - "subtype": "metrics", - "region": "us-east-1", - "namespace": "custom", - "datasourceId": 1, - }), - }, - } - result := makeCWRequest(t, req, addr) - - dataFrames := data.Frames{ - &data.Frame{ - RefID: "A", - Fields: []*data.Field{ - data.NewField("text", nil, []string{"Test_MetricName"}), - data.NewField("value", nil, []string{"Test_MetricName"}), - }, - Meta: &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": float64(1), - }, - }, - }, - } - - expect := backend.NewQueryDataResponse() - expect.Responses["A"] = backend.DataResponse{ - Frames: dataFrames, - } - assert.Equal(t, *expect, result) - }) -} - -func TestQueryCloudWatchLogs(t *testing.T) { - grafDir, cfgPath := testinfra.CreateGrafDir(t) - addr, store := testinfra.StartGrafana(t, grafDir, cfgPath) - setUpDatabase(t, store) - - origNewCWLogsClient := cloudwatch.NewCWLogsClient - t.Cleanup(func() { - cloudwatch.NewCWLogsClient = origNewCWLogsClient - }) - - var client cloudwatch.FakeCWLogsClient - cloudwatch.NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { - return client - } - - t.Run("Describe log groups", func(t *testing.T) { - client = cloudwatch.FakeCWLogsClient{} - - req := dtos.MetricRequest{ - Queries: []*simplejson.Json{ - simplejson.NewFromAny(map[string]interface{}{ - "type": "logAction", - "subtype": "DescribeLogGroups", - "region": "us-east-1", - "datasourceId": 1, - }), - }, - } - tr := makeCWRequest(t, req, addr) - - dataFrames := data.Frames{ - &data.Frame{ - Name: "logGroups", - RefID: "A", - Fields: []*data.Field{ - data.NewField("logGroupName", nil, []*string{}), - }, - }, - } - - expect := backend.NewQueryDataResponse() - expect.Responses["A"] = backend.DataResponse{ - Frames: dataFrames, - } - assert.Equal(t, *expect, tr) - }) -} - -func makeCWRequest(t *testing.T, req dtos.MetricRequest, addr string) backend.QueryDataResponse { - t.Helper() - - buf := bytes.Buffer{} - enc := json.NewEncoder(&buf) - err := enc.Encode(&req) - require.NoError(t, err) - u := fmt.Sprintf("http://%s/api/ds/query", addr) - t.Logf("Making POST request to %s", u) - // nolint:gosec - resp, err := http.Post(u, "application/json", &buf) - require.NoError(t, err) - require.NotNil(t, resp) - t.Cleanup(func() { - err := resp.Body.Close() - assert.NoError(t, err) - }) - - buf = bytes.Buffer{} - _, err = io.Copy(&buf, resp.Body) - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) - - var tr backend.QueryDataResponse - err = json.Unmarshal(buf.Bytes(), &tr) - require.NoError(t, err) - - return tr -} - -func setUpDatabase(t *testing.T, store *sqlstore.SQLStore) { - t.Helper() - - err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - _, err := sess.Insert(&models.DataSource{ - Id: 1, - // This will be the ID of the main org - OrgId: 2, - Name: "Test", - Type: "cloudwatch", - Created: time.Now(), - Updated: time.Now(), - }) - return err - }) - require.NoError(t, err) - - // Make sure changes are synced with other goroutines - err = store.Sync() - require.NoError(t, err) -} From f443777309776544fb00fa04dcfc96497a2b542c Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 04:05:44 -0600 Subject: [PATCH 02/34] Alerting: add field for custom slack endpoint (#45751) (#45812) * add field for custom slack endpoint * add test for using custom endpoint * Update pkg/services/ngalert/notifier/channels/slack.go Co-authored-by: Alexander Weaver * specify description for endpoint * remove brittle string constants Co-authored-by: Alexander Weaver (cherry picked from commit f9701d78b16857d78e03d2e3f20c89542abaddac) Co-authored-by: Nathan Rodman --- .../ngalert/notifier/available_channels.go | 8 + .../ngalert/notifier/channels/slack.go | 6 +- .../ngalert/notifier/channels/slack_test.go | 42 + .../alerting/api_available_channel_test.go | 1657 +---------------- 4 files changed, 61 insertions(+), 1652 deletions(-) diff --git a/pkg/services/ngalert/notifier/available_channels.go b/pkg/services/ngalert/notifier/available_channels.go index 5a5e8eeffaa..521548e53b5 100644 --- a/pkg/services/ngalert/notifier/available_channels.go +++ b/pkg/services/ngalert/notifier/available_channels.go @@ -459,6 +459,14 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { PropertyName: "url", Secure: true, }, + { // New in 8.4. + Label: "Endpoint URL", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Optionally provide a custom Slack message API endpoint for non-webhook requests, default is https://slack.com/api/chat.postMessage", + Placeholder: "Slack endpoint url", + PropertyName: "endpointUrl", + }, { // New in 8.0. Label: "Title", Element: alerting.ElementTypeInput, diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index 66713312ed8..e77ef66afec 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -52,9 +52,11 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} } + endpointURL := model.Settings.Get("endpointUrl").MustString(SlackAPIEndpoint) + slackURL := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString()) if slackURL == "" { - slackURL = SlackAPIEndpoint + slackURL = endpointURL } apiURL, err := url.Parse(slackURL) if err != nil { @@ -62,7 +64,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn } recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString()) - if recipient == "" && apiURL.String() == SlackAPIEndpoint { + if recipient == "" && apiURL.String() == endpointURL { return nil, receiverInitError{Cfg: *model, Reason: "recipient must be specified when using the Slack chat API", } diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index ce6eef5bba8..c30b87bf64b 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -160,6 +160,42 @@ func TestSlackNotifier(t *testing.T) { }`, expInitError: `failed to validate receiver "slack_testing" of type "slack": recipient must be specified when using the Slack chat API`, }, + { + name: "Custom endpoint url", + settings: `{ + "token": "1234", + "recipient": "#testchannel", + "endpointUrl": "https://slack-custom.com/api/", + "icon_emoji": ":emoji:" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + }, + }, + }, + expMsg: &slackMessage{ + Channel: "#testchannel", + Username: "Grafana", + IconEmoji: ":emoji:", + Attachments: []attachment{ + { + Title: "[FIRING:1] (val1)", + TitleLink: "http://localhost/alerting/list", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + Fallback: "[FIRING:1] (val1)", + Fields: nil, + Footer: "Grafana v" + setting.BuildVersion, + FooterIcon: "https://grafana.com/assets/img/fav32.png", + Color: "#D63232", + Ts: 0, + }, + }, + }, + expMsgError: nil, + }, } for _, c := range cases { @@ -196,6 +232,12 @@ func TestSlackNotifier(t *testing.T) { _ = request.Body.Close() }() + url := settingsJSON.Get("url").MustString() + if len(url) == 0 { + endpointUrl := settingsJSON.Get("endpointUrl").MustString(SlackAPIEndpoint) + require.Equal(t, endpointUrl, request.URL.String()) + } + b, err := io.ReadAll(request.Body) require.NoError(t, err) body = string(b) diff --git a/pkg/tests/api/alerting/api_available_channel_test.go b/pkg/tests/api/alerting/api_available_channel_test.go index dbd01ad238b..759ca3584c2 100644 --- a/pkg/tests/api/alerting/api_available_channel_test.go +++ b/pkg/tests/api/alerting/api_available_channel_test.go @@ -1,6 +1,7 @@ package alerting import ( + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -11,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/tests/testinfra" ) @@ -45,1654 +47,9 @@ func TestAvailableChannels(t *testing.T) { b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) - require.JSONEq(t, expAvailableChannelJsonOutput, string(b)) -} -var expAvailableChannelJsonOutput = ` -[ - { - "type": "dingding", - "name": "DingDing", - "heading": "DingDing settings", - "description": "Sends HTTP POST request to DingDing", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Url", - "description": "", - "placeholder": "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "Message Type", - "description": "", - "placeholder": "", - "propertyName": "msgType", - "selectOptions": [ - { - "value": "link", - "label": "Link" - }, - { - "value": "actionCard", - "label": "ActionCard" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "kafka", - "name": "Kafka REST Proxy", - "heading": "Kafka settings", - "description": "Sends notifications to Kafka Rest Proxy", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Kafka REST Proxy", - "description": "", - "placeholder": "http://localhost:8082", - "propertyName": "kafkaRestProxy", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Topic", - "description": "", - "placeholder": "topic1", - "propertyName": "kafkaTopic", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "email", - "name": "Email", - "heading": "Email settings", - "description": "Sends notifications using Grafana server configured SMTP settings", - "info": "", - "options": [ - { - "element": "checkbox", - "inputType": "", - "label": "Single email", - "description": "Send a single email to all recipients", - "placeholder": "", - "propertyName": "singleEmail", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Addresses", - "description": "You can enter multiple email addresses using a \";\" separator", - "placeholder": "", - "propertyName": "addresses", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "Optional message to include with the email. You can use template variables", - "placeholder": "", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "pagerduty", - "name": "PagerDuty", - "heading": "PagerDuty settings", - "description": "Sends notifications to PagerDuty", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Integration Key", - "description": "", - "placeholder": "Pagerduty Integration Key", - "propertyName": "integrationKey", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - }, - { - "element": "select", - "inputType": "", - "label": "Severity", - "description": "", - "placeholder": "", - "propertyName": "severity", - "selectOptions": [ - { - "value": "critical", - "label": "Critical" - }, - { - "value": "error", - "label": "Error" - }, - { - "value": "warning", - "label": "Warning" - }, - { - "value": "info", - "label": "Info" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Class", - "description": "The class/type of the event, for example 'ping failure' or 'cpu load'", - "placeholder": "", - "propertyName": "class", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Component", - "description": "Component of the source machine that is responsible for the event, for example mysql or eth0", - "placeholder": "Grafana", - "propertyName": "component", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Group", - "description": "Logical grouping of components of a service, for example 'app-stack'", - "placeholder": "", - "propertyName": "group", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Summary", - "description": "You can use templates for summary", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "summary", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "victorops", - "name": "VictorOps", - "heading": "VictorOps settings", - "description": "Sends notifications to VictorOps", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Url", - "description": "", - "placeholder": "VictorOps url", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "Message Type", - "description": "", - "placeholder": "", - "propertyName": "messageType", - "selectOptions": [ - { - "value": "CRITICAL", - "label": "CRITICAL" - }, - { - "value": "WARNING", - "label": "WARNING" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "pushover", - "name": "Pushover", - "description": "Sends HTTP POST request to the Pushover API", - "heading": "Pushover settings", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "API Token", - "description": "", - "placeholder": "Application token", - "propertyName": "apiToken", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "User key(s)", - "description": "", - "placeholder": "comma-separated list", - "propertyName": "userKey", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "Device(s) (optional)", - "description": "", - "placeholder": "comma-separated list; leave empty to send to all devices", - "propertyName": "device", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "Alerting priority", - "description": "", - "placeholder": "", - "propertyName": "priority", - "selectOptions": [ - { - "value": "2", - "label": "Emergency" - }, - { - "value": "1", - "label": "High" - }, - { - "value": "0", - "label": "Normal" - }, - { - "value": "-1", - "label": "Low" - }, - { - "value": "-2", - "label": "Lowest" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "OK priority", - "description": "", - "placeholder": "", - "propertyName": "okPriority", - "selectOptions": [ - { - "value": "2", - "label": "Emergency" - }, - { - "value": "1", - "label": "High" - }, - { - "value": "0", - "label": "Normal" - }, - { - "value": "-1", - "label": "Low" - }, - { - "value": "-2", - "label": "Lowest" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Retry (Only used for Emergency Priority)", - "description": "How often (in seconds) the Pushover servers will send the same alerting or OK notification to the user.", - "placeholder": "minimum 30 seconds", - "propertyName": "retry", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Expire (Only used for Emergency Priority)", - "description": "How many seconds the alerting or OK notification will continue to be retried.", - "placeholder": "maximum 86400 seconds", - "propertyName": "expire", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "Alerting sound", - "description": "", - "placeholder": "", - "propertyName": "sound", - "selectOptions": [ - { - "value": "default", - "label": "Default" - }, - { - "value": "pushover", - "label": "Pushover" - }, - { - "value": "bike", - "label": "Bike" - }, - { - "value": "bugle", - "label": "Bugle" - }, - { - "value": "cashregister", - "label": "Cashregister" - }, - { - "value": "classical", - "label": "Classical" - }, - { - "value": "cosmic", - "label": "Cosmic" - }, - { - "value": "falling", - "label": "Falling" - }, - { - "value": "gamelan", - "label": "Gamelan" - }, - { - "value": "incoming", - "label": "Incoming" - }, - { - "value": "intermission", - "label": "Intermission" - }, - { - "value": "magic", - "label": "Magic" - }, - { - "value": "mechanical", - "label": "Mechanical" - }, - { - "value": "pianobar", - "label": "Pianobar" - }, - { - "value": "siren", - "label": "Siren" - }, - { - "value": "spacealarm", - "label": "Spacealarm" - }, - { - "value": "tugboat", - "label": "Tugboat" - }, - { - "value": "alien", - "label": "Alien" - }, - { - "value": "climb", - "label": "Climb" - }, - { - "value": "persistent", - "label": "Persistent" - }, - { - "value": "echo", - "label": "Echo" - }, - { - "value": "updown", - "label": "Updown" - }, - { - "value": "none", - "label": "None" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "OK sound", - "description": "", - "placeholder": "", - "propertyName": "okSound", - "selectOptions": [ - { - "value": "default", - "label": "Default" - }, - { - "value": "pushover", - "label": "Pushover" - }, - { - "value": "bike", - "label": "Bike" - }, - { - "value": "bugle", - "label": "Bugle" - }, - { - "value": "cashregister", - "label": "Cashregister" - }, - { - "value": "classical", - "label": "Classical" - }, - { - "value": "cosmic", - "label": "Cosmic" - }, - { - "value": "falling", - "label": "Falling" - }, - { - "value": "gamelan", - "label": "Gamelan" - }, - { - "value": "incoming", - "label": "Incoming" - }, - { - "value": "intermission", - "label": "Intermission" - }, - { - "value": "magic", - "label": "Magic" - }, - { - "value": "mechanical", - "label": "Mechanical" - }, - { - "value": "pianobar", - "label": "Pianobar" - }, - { - "value": "siren", - "label": "Siren" - }, - { - "value": "spacealarm", - "label": "Spacealarm" - }, - { - "value": "tugboat", - "label": "Tugboat" - }, - { - "value": "alien", - "label": "Alien" - }, - { - "value": "climb", - "label": "Climb" - }, - { - "value": "persistent", - "label": "Persistent" - }, - { - "value": "echo", - "label": "Echo" - }, - { - "value": "updown", - "label": "Updown" - }, - { - "value": "none", - "label": "None" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "slack", - "name": "Slack", - "heading": "Slack settings", - "description": "Sends notifications to Slack", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Recipient", - "description": "Specify channel, private group, or IM channel (can be an encoded ID or a name) - required unless you provide a webhook", - "placeholder": "", - "propertyName": "recipient", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Token", - "description": "Provide a Slack API token (starts with \"xoxb\") - required unless you provide a webhook", - "placeholder": "", - "propertyName": "token", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "Username", - "description": "Set the username for the bot's message", - "placeholder": "", - "propertyName": "username", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Icon emoji", - "description": "Provide an emoji to use as the icon for the bot's message. Overrides the icon URL.", - "placeholder": "", - "propertyName": "icon_emoji", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Icon URL", - "description": "Provide a URL to an image to use as the icon for the bot's message", - "placeholder": "", - "propertyName": "icon_url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Mention Users", - "description": "Mention one or more users (comma separated) when notifying in a channel, by ID (you can copy this from the user's Slack profile)", - "placeholder": "", - "propertyName": "mentionUsers", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Mention Groups", - "description": "Mention one or more groups (comma separated) when notifying in a channel (you can copy this from the group's Slack profile URL)", - "placeholder": "", - "propertyName": "mentionGroups", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "Mention Channel", - "description": "Mention whole channel or just active members when notifying", - "placeholder": "", - "propertyName": "mentionChannel", - "selectOptions": [ - { - "value": "", - "label": "Disabled" - }, - { - "value": "here", - "label": "Every active channel member" - }, - { - "value": "channel", - "label": "Every channel member" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Webhook URL", - "description": "Optionally provide a Slack incoming webhook URL for sending messages, in this case the token isn't necessary", - "placeholder": "Slack incoming webhook URL", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "Title", - "description": "Templated title of the slack message", - "placeholder": "{{ template \"slack.default.title\" . }}", - "propertyName": "title", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Text Body", - "description": "Body of the slack message", - "placeholder": "{{ template \"slack.default.text\" . }}", - "propertyName": "text", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "sensugo", - "name": "Sensu Go", - "description": "Sends HTTP POST request to a Sensu Go API", - "heading": "Sensu Go Settings", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Backend URL", - "description": "", - "placeholder": "http://sensu-api.local:8080", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "password", - "label": "API Key", - "description": "API key to auth to Sensu Go backend", - "placeholder": "", - "propertyName": "apikey", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "Proxy entity name", - "description": "", - "placeholder": "default", - "propertyName": "entity", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Check name", - "description": "", - "placeholder": "default", - "propertyName": "check", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Handler", - "description": "", - "placeholder": "", - "propertyName": "handler", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Namespace", - "description": "", - "placeholder": "default", - "propertyName": "namespace", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "teams", - "name": "Microsoft Teams", - "heading": "Teams settings", - "description": "Sends notifications using Incoming Webhook connector to Microsoft Teams", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "URL", - "description": "", - "placeholder": "Teams incoming webhook url", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "telegram", - "name": "Telegram", - "heading": "Telegram API settings", - "description": "Sends notifications to Telegram", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "BOT API Token", - "description": "", - "placeholder": "Telegram BOT API Token", - "propertyName": "bottoken", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "Chat ID", - "description": "Integer Telegram Chat Identifier", - "placeholder": "", - "propertyName": "chatid", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "webhook", - "name": "webhook", - "heading": "Webhook settings", - "description": "Sends HTTP POST request to a URL", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Url", - "description": "", - "placeholder": "", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "Http Method", - "description": "", - "placeholder": "", - "propertyName": "httpMethod", - "selectOptions": [ - { - "value": "POST", - "label": "POST" - }, - { - "value": "PUT", - "label": "PUT" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Username", - "description": "", - "placeholder": "", - "propertyName": "username", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "password", - "label": "Password", - "description": "", - "placeholder": "", - "propertyName": "password", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "Max Alerts", - "description": "Max alerts to include in a notification. Remaining alerts in the same batch will be ignored above this number. 0 means no limit.", - "placeholder": "", - "propertyName": "maxAlerts", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "wecom", - "name": "WeCom", - "heading": "WeCom settings", - "description": "Send alerts generated by Grafana to WeCom", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Url", - "description": "", - "placeholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "Custom WeCom message. You can use template variables.", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "prometheus-alertmanager", - "name": "Alertmanager", - "heading": "Alertmanager Settings", - "description": "Sends notifications to Alertmanager", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "URL", - "description": "", - "placeholder": "http://localhost:9093", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Basic Auth User", - "description": "", - "placeholder": "", - "propertyName": "basicAuthUser", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "input", - "inputType": "password", - "label": "Basic Auth Password", - "description": "", - "placeholder": "", - "propertyName": "basicAuthPassword", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": true - } - ] - }, - { - "type": "discord", - "name": "Discord", - "heading": "Discord settings", - "description": "Sends notifications to Discord", - "info": "", - "options": [ - { - "label": "Message Content", - "description": "Mention a group using @ or a user using <@ID> when notifying in a channel", - "element": "input", - "inputType": "text", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "label": "Webhook URL", - "description": "", - "element": "input", - "inputType": "text", - "placeholder": "Discord webhook URL", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "label": "Avatar URL", - "description": "", - "element": "input", - "inputType": "text", - "placeholder": "", - "propertyName": "avatar_url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "checkbox", - "inputType": "", - "label": "Use Discord's Webhook Username", - "description": "Use the username configured in Discord's webhook settings. Otherwise, the username will be 'Grafana'", - "placeholder": "", - "propertyName": "use_discord_username", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "googlechat", - "name": "Google Hangouts Chat", - "heading": "Google Hangouts Chat settings", - "description": "Sends notifications to Google Hangouts Chat via webhooks based on the official JSON message format", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Url", - "description": "", - "placeholder": "Google Hangouts Chat incoming webhook url", - "propertyName": "url", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "textarea", - "inputType": "", - "label": "Message", - "description": "", - "placeholder": "{{ template \"default.message\" . }}", - "propertyName": "message", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - }, - { - "type": "LINE", - "name": "LINE", - "heading": "LINE notify settings", - "description": "Send notifications to LINE notify", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Token", - "description": "", - "placeholder": "LINE notify token key", - "propertyName": "token", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - } - ] - }, - { - "type": "threema", - "name": "Threema Gateway", - "heading": "Threema Gateway settings", - "description": "Sends notifications to Threema using Threema Gateway (Basic IDs)", - "info": "Notifications can be configured for any Threema Gateway ID of type \"Basic\". End-to-End IDs are not currently supported.The Threema Gateway ID can be set up at https://gateway.threema.ch/.", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "Gateway ID", - "description": "Your 8 character Threema Gateway Basic ID (starting with a *).", - "placeholder": "*3MAGWID", - "propertyName": "gateway_id", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "\\*[0-9A-Z]{7}", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "Recipient ID", - "description": "The 8 character Threema ID that should receive the alerts.", - "placeholder": "YOUR3MID", - "propertyName": "recipient_id", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "[0-9A-Z]{8}", - "secure": false - }, - { - "element": "input", - "inputType": "text", - "label": "API Secret", - "description": "Your Threema Gateway API secret.", - "placeholder": "", - "propertyName": "api_secret", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - } - ] - }, - { - "type": "opsgenie", - "name": "OpsGenie", - "heading": "OpsGenie settings", - "description": "Sends notifications to OpsGenie", - "info": "", - "options": [ - { - "element": "input", - "inputType": "text", - "label": "API Key", - "description": "", - "placeholder": "OpsGenie API Key", - "propertyName": "apiKey", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": true - }, - { - "element": "input", - "inputType": "text", - "label": "Alert API Url", - "description": "", - "placeholder": "https://api.opsgenie.com/v2/alerts", - "propertyName": "apiUrl", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": true, - "validationRule": "", - "secure": false - }, - { - "element": "checkbox", - "inputType": "", - "label": "Auto close incidents", - "description": "Automatically close alerts in OpsGenie once the alert goes back to ok.", - "placeholder": "", - "propertyName": "autoClose", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "checkbox", - "inputType": "", - "label": "Override priority", - "description": "Allow the alert priority to be set using the og_priority annotation", - "placeholder": "", - "propertyName": "overridePriority", - "selectOptions": null, - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - }, - { - "element": "select", - "inputType": "", - "label": "Send notification tags as", - "description": "Send the common annotations to Opsgenie as either Extra Properties, Tags or both", - "placeholder": "", - "propertyName": "sendTagsAs", - "selectOptions": [ - { - "value": "tags", - "label": "Tags" - }, - { - "value": "details", - "label": "Extra Properties" - }, - { - "value": "both", - "label": "Tags & Extra Properties" - } - ], - "showWhen": { - "field": "", - "is": "" - }, - "required": false, - "validationRule": "", - "secure": false - } - ] - } -] -` + expNotifiers := notifier.GetAvailableNotifiers() + expJson, err := json.Marshal(expNotifiers) + require.NoError(t, err) + require.Equal(t, string(expJson), string(b)) +} From 3734b455d74ce35aa89171f59218561fa634c79e Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Thu, 24 Feb 2022 13:25:59 +0200 Subject: [PATCH 03/34] grafana-cli: Diff generated ts directly instead of relying on git (#45815) (#45828) * Add diffing support to grafana-cli cue gen-ts * Rely on diff comparison in cuetsify pipeline step * Ignore *.gen.ts files with eslint * Chore: Fix lint `sdboyer/cuetsify-compare` (#45818) * Sync drone (cherry picked from commit 40645ab19e39ff9b0a12b7ebb13a4dc4c5e1d472) * Fix lint (cherry picked from commit c95ece983984432fea029335b2b729b09d76c7eb) * Sign drone Co-authored-by: Dimitris Sotirakis (cherry picked from commit 60db64398356061e28c10889a7abbc9b48728b48) Co-authored-by: sam boyer --- .drone.yml | 122 ++++++------------ .eslintignore | 3 + pkg/cmd/grafana-cli/commands/commands.go | 5 + .../grafana-cli/commands/cuetsify_command.go | 51 +++++++- public/app/plugins/panel/news/models.gen.ts | 2 + scripts/drone/steps/lib.star | 15 +-- 6 files changed, 97 insertions(+), 101 deletions(-) diff --git a/.drone.yml b/.drone.yml index 8236478e924..327e54b779f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -159,22 +159,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -686,22 +678,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -1290,22 +1274,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -1881,22 +1857,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -3045,22 +3013,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -3563,22 +3523,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -4307,6 +4259,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 689db4e7e75f82f24715aed3f81d93ae70d2739fe763b4fa69bb159b0b29f2e8 +hmac: 2990e0351ffd3bf432e64621ec7d82ffd619ab8cf0aa0e3c648f54392c5f0e6e ... diff --git a/.eslintignore b/.eslintignore index 1fc48c822a9..30d54f55eba 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,6 +10,9 @@ scripts/grafana-server/tmp public/lib/monaco deployment_tools_config.json +# TS generate from cue by cuetsy +**/*.gen.ts + # Auto-generated localisation files public/locales/_build/ public/locales/**/*.js diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 809dd35adf3..82bcb8ffac3 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -257,6 +257,11 @@ so must be recompiled to validate newly-added CUE files.`, Name: "grafana-root", Usage: "path to the root of a Grafana repository in which to generate TypeScript from CUE files", }, + &cli.BoolFlag{ + Name: "diff", + Usage: "diff results of codegen against files already on disk. Exits 1 if diff is non-empty", + Value: false, + }, }, }, } diff --git a/pkg/cmd/grafana-cli/commands/cuetsify_command.go b/pkg/cmd/grafana-cli/commands/cuetsify_command.go index 73ae0d098c7..5e6038b56f2 100644 --- a/pkg/cmd/grafana-cli/commands/cuetsify_command.go +++ b/pkg/cmd/grafana-cli/commands/cuetsify_command.go @@ -17,6 +17,7 @@ import ( "cuelang.org/go/cue/errors" cload "cuelang.org/go/cue/load" "cuelang.org/go/cue/parser" + "github.com/google/go-cmp/cmp" "github.com/grafana/cuetsy" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/grafana/grafana/pkg/schema/load" @@ -62,6 +63,7 @@ var skipPaths = []string{ const prefix = "/" +//nolint: gocyclo func (cmd Command) generateTypescript(c utils.CommandLine) error { root := c.String("grafana-root") if root == "" { @@ -236,13 +238,46 @@ func (cmd Command) generateTypescript(c utils.CommandLine) error { return gerrors.New(errors.Details(err, nil)) } + diff := c.Bool("diff") + var derr bool for of, b := range outfiles { - err := os.WriteFile(filepath.Join(root, of), b, 0644) - if err != nil { - return err + p := filepath.Join(root, of) + if diff { + if _, err := os.Stat(p); err != nil { + if errors.Is(err, os.ErrNotExist) { + fmt.Printf("%s: no generated code file to compare against\n", p) + derr = true + continue + } + return fmt.Errorf("%s: %w", p, err) + } + + f, err := os.Open(filepath.Clean(p)) + if err != nil { + return fmt.Errorf("%s: %w", p, err) + } + + ob, err := io.ReadAll(f) + if err != nil { + return err + } + dstr := cmp.Diff(string(ob), string(b)) + if dstr != "" { + derr = true + fmt.Printf("%s would have changed:\n%s\n", p, dstr) + } + } else { + err := os.WriteFile(p, b, 0644) + if err != nil { + return err + } } } + if derr { + return errors.New("some files changed") + } + return nil } @@ -282,7 +317,7 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error if !filepath.IsAbs(prefix) { return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix) } - err := fs.WalkDir(vfs, ".", (func(path string, d fs.DirEntry, err error) error { + err := fs.WalkDir(vfs, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -295,6 +330,12 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error if err != nil { return err } + defer func(f fs.File) { + err := f.Close() + if err != nil { + return + } + }(f) b, err := io.ReadAll(f) if err != nil { @@ -303,7 +344,7 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error overlay[filepath.Join(prefix, path)] = cload.FromBytes(b) return nil - })) + }) if err != nil { return err diff --git a/public/app/plugins/panel/news/models.gen.ts b/public/app/plugins/panel/news/models.gen.ts index 82db65040ac..61a31557145 100644 --- a/public/app/plugins/panel/news/models.gen.ts +++ b/public/app/plugins/panel/news/models.gen.ts @@ -2,8 +2,10 @@ // This file was autogenerated by cuetsy. DO NOT EDIT! //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + export const modelVersion = Object.freeze([0, 0]); + export interface PanelOptions { feedUrl?: string; showImage?: boolean; diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 10d7a42b145..eaf8795d911 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1134,18 +1134,11 @@ def ensure_cuetsified_step(): 'validate-scuemata', ], 'commands': [ - '# Make sure the git tree is clean.', - '# Stashing changes, since packages that were produced in build-backend step are needed.', - 'git stash', - './bin/linux-amd64/grafana-cli cue gen-ts --grafana-root .', - '# The above command generates Typescript files (*.gen.ts) from all appropriate .cue files.', '# It is required that the generated Typescript be in sync with the input CUE files.', - '# ...Modulo eslint auto-fixes...:', - 'yarn run eslint . --ext .gen.ts --fix', - '# If any filenames are emitted by the below script, run the generator command `grafana-cli cue gen-ts` locally and commit the result.', - './scripts/clean-git-or-error.sh', - '# Un-stash changes.', - 'git stash pop', + '# To enforce this, the following command will attempt to generate Typescript from all', + '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file the generated', + '# code would have been written to. It exits 1 if any diffs are found.', + './bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff', ], } From 4363f9af1f5b8653532e01d9495e4f544e3dd3d7 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 08:05:26 -0600 Subject: [PATCH 04/34] Adding ap-southeast-3 to cloudwatch regions (#45821) (#45836) (cherry picked from commit 0e7b0f16b8fa14055fdfcc4cc07c945da3b4f7a0) Co-authored-by: Yaelle Chaudy <42030685+yaelleC@users.noreply.github.com> --- pkg/tsdb/cloudwatch/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/metrics.go b/pkg/tsdb/cloudwatch/metrics.go index 6234f013913..cf2779ab8e7 100644 --- a/pkg/tsdb/cloudwatch/metrics.go +++ b/pkg/tsdb/cloudwatch/metrics.go @@ -514,7 +514,7 @@ var dimensionsMap = map[string][]string{ // Known AWS regions. var knownRegions = []string{ "af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", - "ap-southeast-2", "ca-central-1", "cn-north-1", "cn-northwest-1", "eu-central-1", "eu-north-1", "eu-south-1", "eu-west-1", + "ap-southeast-2", "ap-southeast-3", "ca-central-1", "cn-north-1", "cn-northwest-1", "eu-central-1", "eu-north-1", "eu-south-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-gov-east-1", "us-gov-west-1", "us-iso-east-1", "us-isob-east-1", "us-west-1", "us-west-2", } From 01411b5f453502188809f02ca8fa433864823937 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 08:14:54 -0600 Subject: [PATCH 05/34] Add a fallback for the clipboard API (#45831) (#45841) (cherry picked from commit 64ad33f31a94b2aca9e705d3f2b6c6c0f6885d0e) Co-authored-by: Ashley Harrison --- .../ClipboardButton/ClipboardButton.tsx | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx index d979d8b6aa2..63c1aba06c9 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx @@ -21,24 +21,46 @@ export interface Props extends ButtonProps { const dummyClearFunc = () => {}; export function ClipboardButton({ onClipboardCopy, onClipboardError, children, getText, ...buttonProps }: Props) { - // Can be removed in 9.x const buttonRef = useRef(null); - const copyText = useCallback(() => { - const copiedText = getText(); + const copyTextCallback = useCallback(async () => { + const textToCopy = getText(); + // Can be removed in 9.x const dummyEvent: ClipboardEvent = { action: 'copy', clearSelection: dummyClearFunc, - text: copiedText, + text: textToCopy, trigger: buttonRef.current!, }; - navigator.clipboard - .writeText(copiedText) - .then(() => (onClipboardCopy?.(dummyEvent), () => onClipboardError?.(dummyEvent))); + try { + await copyText(textToCopy, buttonRef); + onClipboardCopy?.(dummyEvent); + } catch { + onClipboardError?.(dummyEvent); + } }, [getText, onClipboardCopy, onClipboardError]); return ( - ); } + +const copyText = async (text: string, buttonRef: React.MutableRefObject) => { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } else { + // Use a fallback method for browsers/contexts that don't support the Clipboard API. + // See https://web.dev/async-clipboard/#feature-detection. + const input = document.createElement('input'); + // Normally we'd append this to the body. However if we're inside a focus manager + // from react-aria, we can't focus anything outside of the managed area. + // Instead, let's append it to the button. Then we're guaranteed to be able to focus + copy. + buttonRef.current?.appendChild(input); + input.value = text; + input.focus(); + input.select(); + document.execCommand('copy'); + input.remove(); + } +}; From 01628c40451c3d8088d8cae4524b2edc90592111 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 08:42:01 -0600 Subject: [PATCH 06/34] Azure Monitor: Fixes broken log queries that use workspace (#45820) (#45843) * allow log queries to be executed also without a resource * add unit tests (cherry picked from commit b7a2fda2aeb495a92a24df70df00df9adada7d0b) Co-authored-by: Erik Sundell --- .../azure_log_analytics_datasource.test.ts | 27 ++++++++++---- .../azure_log_analytics_datasource.ts | 35 +++++++++++-------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 080cee7d06a..d7031081f88 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -1,11 +1,12 @@ -import AzureMonitorDatasource from '../datasource'; -import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; -import FakeSchemaData from './__mocks__/schema'; -import { TemplateSrv } from 'app/features/templating/template_srv'; -import { AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types'; import { toUtc } from '@grafana/data'; +import { TemplateSrv } from 'app/features/templating/template_srv'; + import createMockQuery from '../__mocks__/query'; import { singleVariable } from '../__mocks__/variables'; +import AzureMonitorDatasource from '../datasource'; +import { AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types'; +import FakeSchemaData from './__mocks__/schema'; +import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; const templateSrv = new TemplateSrv(); @@ -273,7 +274,7 @@ describe('AzureLogAnalyticsDatasource', () => { laDatasource = new AzureLogAnalyticsDatasource(ctx.instanceSettings); }); - it('should run complete queries', () => { + it('should run queries with a resource', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { @@ -285,6 +286,18 @@ describe('AzureLogAnalyticsDatasource', () => { expect(laDatasource.filterQuery(query)).toBeTruthy(); }); + it('should run queries with a workspace', () => { + const query: AzureMonitorQuery = { + refId: 'A', + azureLogAnalytics: { + query: 'perf | take 100', + workspace: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67', + }, + }; + + expect(laDatasource.filterQuery(query)).toBeTruthy(); + }); + it('should not run empty queries', () => { const query: AzureMonitorQuery = { refId: 'A', @@ -317,7 +330,7 @@ describe('AzureLogAnalyticsDatasource', () => { expect(laDatasource.filterQuery(query)).toBeFalsy(); }); - it('should not run queries missing a resource', () => { + it('should not run queries missing a resource and a missing workspace', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 1bedebfc2b7..b4458f8d640 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -1,26 +1,27 @@ -import { map } from 'lodash'; -import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder'; -import ResponseParser, { transformMetadataToKustoSchema } from './response_parser'; -import { - AzureMonitorQuery, - AzureDataSourceJsonData, - AzureLogsVariable, - AzureQueryType, - DatasourceValidationResult, -} from '../types'; import { DataQueryRequest, DataQueryResponse, - ScopedVars, DataSourceInstanceSettings, DataSourceRef, + ScopedVars, } from '@grafana/data'; -import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime'; -import { Observable, from } from 'rxjs'; +import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; +import { map } from 'lodash'; +import { from, Observable } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials'; + import { isGUIDish } from '../components/ResourcePicker/utils'; +import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials'; +import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder'; +import { + AzureDataSourceJsonData, + AzureLogsVariable, + AzureMonitorQuery, + AzureQueryType, + DatasourceValidationResult, +} from '../types'; import { interpolateVariable, routeNames } from '../utils/common'; +import ResponseParser, { transformMetadataToKustoSchema } from './response_parser'; interface AdhocQuery { datasource: DataSourceRef; @@ -60,7 +61,11 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< } filterQuery(item: AzureMonitorQuery): boolean { - return item.hide !== true && !!item.azureLogAnalytics?.query && !!item.azureLogAnalytics.resource; + return ( + item.hide !== true && + !!item.azureLogAnalytics?.query && + (!!item.azureLogAnalytics.resource || !!item.azureLogAnalytics.workspace) + ); } async getSubscriptions(): Promise> { From bfb9c7f249ba13b3cc41dac959b0c3caf7c5c92a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:00:20 -0600 Subject: [PATCH 07/34] Release: Bump version to 8.4.2 (#45849) * "Release: Updated versions in package to 8.4.2" * Update yarn lock Co-authored-by: Stephanie Closson --- lerna.json | 2 +- package.json | 2 +- packages/grafana-data/package.json | 4 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 4 +- packages/grafana-runtime/package.json | 8 ++-- packages/grafana-schema/package.json | 2 +- packages/grafana-toolkit/package.json | 6 +-- packages/grafana-ui/package.json | 8 ++-- packages/jaeger-ui-components/package.json | 6 +-- .../internal/input-datasource/package.json | 8 ++-- yarn.lock | 40 +++++++++---------- 12 files changed, 46 insertions(+), 46 deletions(-) diff --git a/lerna.json b/lerna.json index f1429cb5567..b5f6c46be20 100644 --- a/lerna.json +++ b/lerna.json @@ -4,5 +4,5 @@ "packages": [ "packages/*" ], - "version": "8.4.1" + "version": "8.4.2" } diff --git a/package.json b/package.json index 6912959e9b3..ec2fc291394 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "8.4.1", + "version": "8.4.2", "repository": "github:grafana/grafana", "scripts": { "api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 7021290a182..00c27092da5 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "8.4.1", + "version": "8.4.2", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -22,7 +22,7 @@ }, "dependencies": { "@braintree/sanitize-url": "5.0.2", - "@grafana/schema": "8.4.1", + "@grafana/schema": "8.4.2", "@types/d3-interpolate": "^1.4.0", "d3-interpolate": "1.4.0", "date-fns": "2.28.0", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 3d9e316a74c..9d4376a95e2 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "8.4.1", + "version": "8.4.2", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 3d2b5e38de6..521c85adc54 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e", - "version": "8.4.1", + "version": "8.4.2", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -48,7 +48,7 @@ "@babel/core": "7.16.7", "@babel/preset-env": "7.16.7", "@cypress/webpack-preprocessor": "5.11.0", - "@grafana/e2e-selectors": "8.4.1", + "@grafana/e2e-selectors": "8.4.2", "@grafana/tsconfig": "^1.0.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "8.2.3", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index da1ec0b583c..10b059b50f2 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "8.4.1", + "version": "8.4.2", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -22,9 +22,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@grafana/data": "8.4.1", - "@grafana/e2e-selectors": "8.4.1", - "@grafana/ui": "8.4.1", + "@grafana/data": "8.4.2", + "@grafana/e2e-selectors": "8.4.2", + "@grafana/ui": "8.4.2", "@sentry/browser": "6.17.2", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index f1f3a818d5e..1c7e11ea12d 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "8.4.1", + "version": "8.4.2", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index 6a74e4eadad..af59f9fcb29 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/toolkit", - "version": "8.4.1", + "version": "8.4.2", "description": "Grafana Toolkit", "keywords": [ "grafana", @@ -28,10 +28,10 @@ "dependencies": { "@babel/core": "7.13.14", "@babel/preset-env": "7.13.12", - "@grafana/data": "8.4.1", + "@grafana/data": "8.4.2", "@grafana/eslint-config": "2.5.2", "@grafana/tsconfig": "^1.0.0-rc1", - "@grafana/ui": "8.4.1", + "@grafana/ui": "8.4.2", "@jest/core": "26.6.3", "@rushstack/eslint-patch": "1.0.6", "@types/command-exists": "^1.2.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index cf1b2cdcf37..67e07ac2705 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "8.4.1", + "version": "8.4.2", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -33,9 +33,9 @@ "@emotion/css": "11.7.1", "@emotion/react": "11.7.1", "@grafana/aws-sdk": "0.0.31", - "@grafana/data": "8.4.1", - "@grafana/e2e-selectors": "8.4.1", - "@grafana/schema": "8.4.1", + "@grafana/data": "8.4.2", + "@grafana/e2e-selectors": "8.4.2", + "@grafana/schema": "8.4.2", "@grafana/slate-react": "0.22.10-grafana", "@monaco-editor/react": "4.3.1", "@popperjs/core": "2.11.2", diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 5e10eb17568..168a2d05ec2 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@jaegertracing/jaeger-ui-components", - "version": "8.4.1", + "version": "8.4.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -26,8 +26,8 @@ }, "dependencies": { "@emotion/css": "11.7.1", - "@grafana/data": "8.4.1", - "@grafana/ui": "8.4.1", + "@grafana/data": "8.4.2", + "@grafana/ui": "8.4.2", "chance": "^1.0.10", "classnames": "^2.2.5", "combokeys": "^3.0.0", diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index d438ad3c558..a524514aaf1 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@grafana-plugins/input-datasource", - "version": "8.4.1", + "version": "8.4.2", "description": "Input Datasource", "private": true, "repository": { @@ -24,9 +24,9 @@ "webpack": "5.58.1" }, "dependencies": { - "@grafana/data": "8.4.1", - "@grafana/toolkit": "8.4.1", - "@grafana/ui": "8.4.1", + "@grafana/data": "8.4.2", + "@grafana/toolkit": "8.4.2", + "@grafana/ui": "8.4.2", "jquery": "3.5.1", "react": "17.0.1", "react-dom": "17.0.1", diff --git a/yarn.lock b/yarn.lock index 306363ac499..10ad153e0a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3633,9 +3633,9 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource" dependencies: - "@grafana/data": 8.4.1 - "@grafana/toolkit": 8.4.1 - "@grafana/ui": 8.4.1 + "@grafana/data": 8.4.2 + "@grafana/toolkit": 8.4.2 + "@grafana/ui": 8.4.2 "@types/jest": 26.0.15 "@types/lodash": 4.14.149 "@types/react": 17.0.30 @@ -3676,12 +3676,12 @@ __metadata: languageName: node linkType: hard -"@grafana/data@8.4.1, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@8.4.2, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: "@braintree/sanitize-url": 5.0.2 - "@grafana/schema": 8.4.1 + "@grafana/schema": 8.4.2 "@grafana/tsconfig": ^1.0.0-rc1 "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-json": 4.1.0 @@ -3733,7 +3733,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@8.4.1, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@8.4.2, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": version: 0.0.0-use.local resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: @@ -3757,7 +3757,7 @@ __metadata: "@babel/core": 7.16.7 "@babel/preset-env": 7.16.7 "@cypress/webpack-preprocessor": 5.11.0 - "@grafana/e2e-selectors": 8.4.1 + "@grafana/e2e-selectors": 8.4.2 "@grafana/tsconfig": ^1.0.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-commonjs": 21.0.1 @@ -3837,10 +3837,10 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": 8.4.1 - "@grafana/e2e-selectors": 8.4.1 + "@grafana/data": 8.4.2 + "@grafana/e2e-selectors": 8.4.2 "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.1 + "@grafana/ui": 8.4.2 "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-node-resolve": 13.1.3 "@sentry/browser": 6.17.2 @@ -3869,7 +3869,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/schema@8.4.1, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@8.4.2, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: @@ -3916,16 +3916,16 @@ __metadata: languageName: node linkType: hard -"@grafana/toolkit@8.4.1, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": +"@grafana/toolkit@8.4.2, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": version: 0.0.0-use.local resolution: "@grafana/toolkit@workspace:packages/grafana-toolkit" dependencies: "@babel/core": 7.13.14 "@babel/preset-env": 7.13.12 - "@grafana/data": 8.4.1 + "@grafana/data": 8.4.2 "@grafana/eslint-config": 2.5.2 "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.1 + "@grafana/ui": 8.4.2 "@jest/core": 26.6.3 "@rushstack/eslint-patch": 1.0.6 "@types/command-exists": ^1.2.0 @@ -4016,7 +4016,7 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@8.4.1, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@8.4.2, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: @@ -4024,9 +4024,9 @@ __metadata: "@emotion/css": 11.7.1 "@emotion/react": 11.7.1 "@grafana/aws-sdk": 0.0.31 - "@grafana/data": 8.4.1 - "@grafana/e2e-selectors": 8.4.1 - "@grafana/schema": 8.4.1 + "@grafana/data": 8.4.2 + "@grafana/e2e-selectors": 8.4.2 + "@grafana/schema": 8.4.2 "@grafana/slate-react": 0.22.10-grafana "@grafana/tsconfig": ^1.0.0-rc1 "@mdx-js/react": 1.6.22 @@ -4243,9 +4243,9 @@ __metadata: resolution: "@jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components" dependencies: "@emotion/css": 11.7.1 - "@grafana/data": 8.4.1 + "@grafana/data": 8.4.2 "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.1 + "@grafana/ui": 8.4.2 "@types/classnames": ^2.2.7 "@types/deep-freeze": ^0.1.1 "@types/grafana__slate-react": "npm:@types/slate-react@0.22.5" From 2255628a5afaf9a87895f862d5b40df8c0e6c598 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 11:28:00 -0600 Subject: [PATCH 08/34] ReleaseNotes: Updated changelog and release notes for 8.4.2 (#45850) (#45859) (cherry picked from commit 91af956eb72b52dbe529f93468fb89e8ffa3d844) --- CHANGELOG.md | 14 ++++++++++++++ docs/sources/release-notes/_index.md | 1 + .../release-notes/release-notes-8-4-2.md | 17 +++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-8-4-2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f4e77db4c..2a85334d4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 8.4.2 (2022-02-23) + +### Features and enhancements + +- **OAuth:** Add setting to skip org assignment for external users. [#34834](https://github.com/grafana/grafana/pull/34834), [@baez90](https://github.com/baez90) +- **Tracing:** Add option to map tag names to log label names in trace to logs settings. [#45178](https://github.com/grafana/grafana/pull/45178), [@connorlindsey](https://github.com/connorlindsey) + +### Bug fixes + +- **Explore:** Fix closing split pane when logs panel is used. [#45602](https://github.com/grafana/grafana/pull/45602), [@ifrost](https://github.com/ifrost) + + # 8.4.1 (2022-02-18) diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 0cbb62a40e0..8cb3c83df60 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -8,6 +8,7 @@ weight = 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 8.4.2]({{< relref "release-notes-8-4-2" >}}) - [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}}) - [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) - [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}}) diff --git a/docs/sources/release-notes/release-notes-8-4-2.md b/docs/sources/release-notes/release-notes-8-4-2.md new file mode 100644 index 00000000000..760462b592f --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-4-2.md @@ -0,0 +1,17 @@ ++++ +title = "Release notes for Grafana 8.4.2" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.4.2 + +### Features and enhancements + +- **OAuth:** Add setting to skip org assignment for external users. [#34834](https://github.com/grafana/grafana/pull/34834), [@baez90](https://github.com/baez90) +- **Tracing:** Add option to map tag names to log label names in trace to logs settings. [#45178](https://github.com/grafana/grafana/pull/45178), [@connorlindsey](https://github.com/connorlindsey) + +### Bug fixes + +- **Explore:** Fix closing split pane when logs panel is used. [#45602](https://github.com/grafana/grafana/pull/45602), [@ifrost](https://github.com/ifrost) From 37b6fc70670fd1b0c609fb9bdcd167cede498049 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 24 Feb 2022 17:55:37 +0000 Subject: [PATCH 09/34] Alerting: Use expanded labels in dashboard annotations (#45726) (#45858) --- pkg/services/ngalert/schedule/testing.go | 6 +++ pkg/services/ngalert/state/manager.go | 34 +++++++++++----- pkg/services/ngalert/state/manager_test.go | 47 ++++++++++++++++++++++ pkg/services/ngalert/tests/util.go | 5 +++ 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/pkg/services/ngalert/schedule/testing.go b/pkg/services/ngalert/schedule/testing.go index fbae64209f0..cf12c2aa895 100644 --- a/pkg/services/ngalert/schedule/testing.go +++ b/pkg/services/ngalert/schedule/testing.go @@ -438,6 +438,12 @@ func NewFakeAnnotationsRepo() *FakeAnnotationsRepo { } } +func (repo *FakeAnnotationsRepo) Items() []*annotations.Item { + repo.mtx.Lock() + defer repo.mtx.Unlock() + return repo.items +} + func (repo *FakeAnnotationsRepo) Len() int { repo.mtx.Lock() defer repo.mtx.Unlock() diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index cc6ef325953..c476547518f 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -5,18 +5,19 @@ import ( "fmt" "net/url" "strconv" + "strings" "time" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/annotations" - "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/log" - + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/sqlstore" ) var ResendDelay = 30 * time.Second @@ -185,7 +186,7 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu st.set(currentState) if oldState != currentState.State { - go st.createAlertAnnotation(ctx, currentState.State, alertRule, result, oldState) + go st.createAlertAnnotation(ctx, alertRule, currentState.Labels, result.EvaluatedAt, currentState.State, oldState) } return currentState } @@ -233,18 +234,19 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State { } } -func (st *Manager) createAlertAnnotation(ctx context.Context, new eval.State, alertRule *ngModels.AlertRule, result eval.Result, oldState eval.State) { - st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", new.String(), "oldState", oldState.String()) +func (st *Manager) createAlertAnnotation(ctx context.Context, alertRule *ngModels.AlertRule, labels data.Labels, evaluatedAt time.Time, state eval.State, previousState eval.State) { + st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", state.String(), "oldState", previousState.String()) - annotationText := fmt.Sprintf("%s {%s} - %s", alertRule.Title, result.Instance.String(), new.String()) + labels = removePrivateLabels(labels) + annotationText := fmt.Sprintf("%s {%s} - %s", alertRule.Title, labels.String(), state.String()) item := &annotations.Item{ AlertId: alertRule.ID, OrgId: alertRule.OrgID, - PrevState: oldState.String(), - NewState: new.String(), + PrevState: previousState.String(), + NewState: state.String(), Text: annotationText, - Epoch: result.EvaluatedAt.UnixNano() / int64(time.Millisecond), + Epoch: evaluatedAt.UnixNano() / int64(time.Millisecond), } dashUid, ok := alertRule.Annotations[ngModels.DashboardUIDAnnotation] @@ -302,3 +304,13 @@ func (st *Manager) staleResultsHandler(ctx context.Context, alertRule *ngModels. func isItStale(lastEval time.Time, intervalSeconds int64) bool { return lastEval.Add(2 * time.Duration(intervalSeconds) * time.Second).Before(time.Now()) } + +func removePrivateLabels(labels data.Labels) data.Labels { + result := make(data.Labels) + for k, v := range labels { + if !strings.HasPrefix(k, "__") && !strings.HasSuffix(k, "__") { + result[k] = v + } + } + return result +} diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 70ea4b58500..2e051eee5c7 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "testing" "time" @@ -27,6 +28,52 @@ import ( var testMetrics = metrics.NewNGAlert(prometheus.NewPedanticRegistry()) +func TestDashboardAnnotations(t *testing.T) { + evaluationTime, err := time.Parse("2006-01-02", "2022-01-01") + require.NoError(t, err) + + ctx := context.Background() + _, dbstore := tests.SetupTestEnv(t, 1) + + st := state.NewManager(log.New("test_stale_results_handler"), testMetrics.GetStateMetrics(), nil, dbstore, dbstore) + + fakeAnnoRepo := schedule.NewFakeAnnotationsRepo() + annotations.SetRepository(fakeAnnoRepo) + + const mainOrgID int64 = 1 + + rule := tests.CreateTestAlertRuleWithLabels(t, ctx, dbstore, 600, mainOrgID, map[string]string{ + "test1": "testValue1", + "test2": "{{ $labels.instance_label }}", + }) + + st.Warm(ctx) + _ = st.ProcessEvalResults(ctx, rule, eval.Results{{ + Instance: data.Labels{"instance_label": "testValue2"}, + State: eval.Alerting, + EvaluatedAt: evaluationTime, + }}) + + expected := []string{rule.Title + " {alertname=" + rule.Title + ", instance_label=testValue2, test1=testValue1, test2=testValue2} - Alerting"} + sort.Strings(expected) + require.Eventuallyf(t, func() bool { + var actual []string + for _, next := range fakeAnnoRepo.Items() { + actual = append(actual, next.Text) + } + sort.Strings(actual) + if len(expected) != len(actual) { + return false + } + for i := 0; i < len(expected); i++ { + if expected[i] != actual[i] { + return false + } + } + return true + }, time.Second, 100*time.Millisecond, "unexpected annotations") +} + func TestProcessEvalResults(t *testing.T) { evaluationTime, err := time.Parse("2006-01-02", "2021-03-25") if err != nil { diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 3e11cb1c722..3158b39d6c2 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -52,6 +52,10 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, * // CreateTestAlertRule creates a dummy alert definition to be used by the tests. func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64) *models.AlertRule { + return CreateTestAlertRuleWithLabels(t, ctx, dbstore, intervalSeconds, orgID, nil) +} + +func CreateTestAlertRuleWithLabels(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64, labels map[string]string) *models.AlertRule { ruleGroup := fmt.Sprintf("ruleGroup-%s", util.GenerateShortUID()) err := dbstore.UpdateRuleGroup(ctx, store.UpdateRuleGroupCmd{ OrgID: orgID, @@ -62,6 +66,7 @@ func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBsto Rules: []apimodels.PostableExtendedRuleNode{ { ApiRuleNode: &apimodels.ApiRuleNode{ + Labels: labels, Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, }, GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ From 585b39ac0f0c3ace41a35175a6ad4f0afd792b21 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 15:28:48 -0600 Subject: [PATCH 10/34] Update dashboard_versions.md (#45871) (#45872) Fixes https://github.com/grafana/grafana/issues/45866, change dashboard version example to use `version` instead of `id` `api/dashboards/id/24/versions/1` (cherry picked from commit 8a98354844c49ea7b3cd4a3f463df73a754df124) Co-authored-by: Melori Arellano --- docs/sources/http_api/dashboard_versions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/dashboard_versions.md b/docs/sources/http_api/dashboard_versions.md index 7e301d390b3..23494e40e0e 100644 --- a/docs/sources/http_api/dashboard_versions.md +++ b/docs/sources/http_api/dashboard_versions.md @@ -67,7 +67,7 @@ Status Codes: ## Get dashboard version -`GET /api/dashboards/id/:dashboardId/versions/:id` +`GET /api/dashboards/id/:dashboardId/versions/:version` Get the dashboard version with the given version, for the dashboard with the given id. From c29c1691fdf86d68bfc3954f67f51c51385ecc44 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 18:48:56 -0600 Subject: [PATCH 11/34] SSE: Fix NoData when some series were no data but others not (#45867) (#45875) Co-authored-by: Santiago (cherry picked from commit a578cf0f7cd25fe80e26a1b23f381c2fa3fb1b41) Co-authored-by: Kyle Brandt --- pkg/expr/classic/classic.go | 4 ++-- pkg/expr/classic/classic_test.go | 25 +++++++++++++++++++++++++ pkg/expr/classic/reduce_test.go | 6 ++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pkg/expr/classic/classic.go b/pkg/expr/classic/classic.go index 830d1ba1f1c..3ac96cbd8c4 100644 --- a/pkg/expr/classic/classic.go +++ b/pkg/expr/classic/classic.go @@ -129,7 +129,7 @@ func (ccc *ConditionsCmd) Execute(ctx context.Context, vars mathexp.Vars) (mathe } thisCondFiring := firingCount > 0 - thisCondNoData := nilReducedCount > 0 + thisCondNoData := len(querySeriesSet.Values) == nilReducedCount if i == 0 { firing = thisCondFiring @@ -144,7 +144,7 @@ func (ccc *ConditionsCmd) Execute(ctx context.Context, vars mathexp.Vars) (mathe noDataFound = noDataFound && thisCondNoData } - if len(querySeriesSet.Values) == nilReducedCount { + if thisCondNoData { matches = append(matches, EvalMatch{ Metric: "NoData", }) diff --git a/pkg/expr/classic/classic_test.go b/pkg/expr/classic/classic_test.go index 5e96826f1d8..623ae19f3bc 100644 --- a/pkg/expr/classic/classic_test.go +++ b/pkg/expr/classic/classic_test.go @@ -169,6 +169,31 @@ func TestConditionsCmdExecute(t *testing.T) { return v }, }, + { + name: "single query and single condition - empty series and not empty series", + vars: mathexp.Vars{ + "A": mathexp.Results{ + Values: []mathexp.Value{ + valBasedSeries(), + valBasedSeries(ptr.Float64(3)), + }, + }, + }, + conditionsCmd: &ConditionsCmd{ + Conditions: []condition{ + { + QueryRefID: "A", + Reducer: classicReducer("avg"), + Operator: "and", + Evaluator: &thresholdEvaluator{Type: "gt", Threshold: .5}, + }, + }}, + resultNumber: func() mathexp.Number { + v := valBasedNumber(ptr.Float64(1)) + v.SetMeta([]EvalMatch{{Value: ptr.Float64(3)}}) + return v + }, + }, { name: "single query and two conditions", vars: mathexp.Vars{ diff --git a/pkg/expr/classic/reduce_test.go b/pkg/expr/classic/reduce_test.go index 50871dd61cd..ea2bc7cbbb3 100644 --- a/pkg/expr/classic/reduce_test.go +++ b/pkg/expr/classic/reduce_test.go @@ -102,6 +102,12 @@ func TestReducer(t *testing.T) { inputSeries: valBasedSeries(nil, nil, ptr.Float64(3), ptr.Float64(4)), expectedNumber: valBasedNumber(ptr.Float64(2)), }, + { + name: "count_non_null with mixed null/real values", + reducer: classicReducer("count_non_null"), + inputSeries: valBasedSeries(nil, nil, ptr.Float64(3), ptr.Float64(4)), + expectedNumber: valBasedNumber(ptr.Float64(2)), + }, { name: "count_non_null with no values", reducer: classicReducer("count_non_null"), From cdf8bab0229db0113fe181a9a11e8890c50f51a7 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Fri, 25 Feb 2022 06:38:31 +0000 Subject: [PATCH 12/34] Alerting: Create annotation if Firing alert is removed (#45703) (#45865) This commit changes staleResultsHandler to create an annotation if the current state is Alerting and the result is being removed from the state cache as it has not been updated since 2x the evaluation interval. (cherry picked from commit feae959c9da12885fc79786d57d7c415595c260a) --- pkg/services/ngalert/state/manager.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index c476547518f..bb8e30d33e4 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -186,7 +186,7 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu st.set(currentState) if oldState != currentState.State { - go st.createAlertAnnotation(ctx, alertRule, currentState.Labels, result.EvaluatedAt, currentState.State, oldState) + go st.annotateState(ctx, alertRule, currentState.Labels, result.EvaluatedAt, currentState.State, oldState) } return currentState } @@ -234,7 +234,7 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State { } } -func (st *Manager) createAlertAnnotation(ctx context.Context, alertRule *ngModels.AlertRule, labels data.Labels, evaluatedAt time.Time, state eval.State, previousState eval.State) { +func (st *Manager) annotateState(ctx context.Context, alertRule *ngModels.AlertRule, labels data.Labels, evaluatedAt time.Time, state eval.State, previousState eval.State) { st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", state.String(), "oldState", previousState.String()) labels = removePrivateLabels(labels) @@ -297,6 +297,10 @@ func (st *Manager) staleResultsHandler(ctx context.Context, alertRule *ngModels. if err = st.instanceStore.DeleteAlertInstance(ctx, s.OrgID, s.AlertRuleUID, labelsHash); err != nil { st.log.Error("unable to delete stale instance from database", "error", err.Error(), "orgID", s.OrgID, "alertRuleUID", s.AlertRuleUID, "cacheID", s.CacheId) } + + if s.State == eval.Alerting { + st.annotateState(ctx, alertRule, s.Labels, time.Now(), eval.Normal, s.State) + } } } } From 787940f32eae9eea703b5a57009579b7ea705c69 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 04:20:35 -0600 Subject: [PATCH 13/34] Rename evalCtx to avoid confusion with context.Context (#45144) (#45893) (cherry picked from commit 2ca79ca0c79f628d8ce414f2a6ee6f9b79e214f1) Co-authored-by: George Robinson --- pkg/services/ngalert/schedule/schedule.go | 46 ++++++------- .../ngalert/schedule/schedule_unit_test.go | 66 +++++++++---------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index dd72ee0db9f..eaec8aefbb4 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -61,7 +61,7 @@ type schedule struct { clock clock.Clock - heartbeat *alerting.Ticker + ticker *alerting.Ticker // evalApplied is only used for tests: test code can set it to non-nil // function, and then it'll be called from the event loop whenever the @@ -130,7 +130,7 @@ func NewScheduler(cfg SchedulerCfg, expressionService *expr.Service, appURL *url clock: cfg.C, baseInterval: cfg.BaseInterval, log: cfg.Logger, - heartbeat: ticker, + ticker: ticker, evalAppliedFunc: cfg.EvalAppliedFunc, stopAppliedFunc: cfg.StopAppliedFunc, evaluator: cfg.Evaluator, @@ -157,7 +157,7 @@ func (sch *schedule) Pause() error { if sch == nil { return fmt.Errorf("scheduler is not initialised") } - sch.heartbeat.Pause() + sch.ticker.Pause() sch.log.Info("alert rule scheduler paused", "now", sch.clock.Now()) return nil } @@ -166,7 +166,7 @@ func (sch *schedule) Unpause() error { if sch == nil { return fmt.Errorf("scheduler is not initialised") } - sch.heartbeat.Unpause() + sch.ticker.Unpause() sch.log.Info("alert rule scheduler unpaused", "now", sch.clock.Now()) return nil } @@ -367,7 +367,7 @@ func (sch *schedule) schedulePeriodic(ctx context.Context) error { dispatcherGroup, ctx := errgroup.WithContext(ctx) for { select { - case tick := <-sch.heartbeat.C: + case tick := <-sch.ticker.C: start := time.Now() sch.metrics.BehindSeconds.Set(start.Sub(tick).Seconds()) @@ -468,7 +468,7 @@ func (sch *schedule) schedulePeriodic(ctx context.Context) error { } } -func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRuleKey, evalCh <-chan *evalContext, updateCh <-chan struct{}) error { +func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRuleKey, evalCh <-chan *evaluation, updateCh <-chan struct{}) error { logger := sch.log.New("uid", key.UID, "org", key.OrgID) logger.Debug("alert rule routine started") @@ -541,16 +541,16 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRul return q.Result, nil } - evaluate := func(ctx context.Context, alertRule *models.AlertRule, attempt int64, evalCtx *evalContext) error { - logger := logger.New("version", alertRule.Version, "attempt", attempt, "now", evalCtx.now) + evaluate := func(ctx context.Context, r *models.AlertRule, attempt int64, e *evaluation) error { + logger := logger.New("version", r.Version, "attempt", attempt, "now", e.scheduledAt) start := sch.clock.Now() condition := models.Condition{ - Condition: alertRule.Condition, - OrgID: alertRule.OrgID, - Data: alertRule.Data, + Condition: r.Condition, + OrgID: r.OrgID, + Data: r.Data, } - results, err := sch.evaluator.ConditionEval(&condition, evalCtx.now, sch.expressionService) + results, err := sch.evaluator.ConditionEval(&condition, e.scheduledAt, sch.expressionService) dur := sch.clock.Now().Sub(start) evalTotal.Inc() evalDuration.Observe(dur.Seconds()) @@ -562,7 +562,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRul } logger.Debug("alert rule evaluated", "results", results, "duration", dur) - processedStates := sch.stateManager.ProcessEvalResults(ctx, alertRule, results) + processedStates := sch.stateManager.ProcessEvalResults(ctx, r, results) sch.saveAlertStates(ctx, processedStates) alerts := FromAlertStateToPostableAlerts(processedStates, sch.stateManager, sch.appURL) @@ -616,7 +616,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key models.AlertRul evalRunning = true defer func() { evalRunning = false - sch.evalApplied(key, ctx.now) + sch.evalApplied(key, ctx.scheduledAt) }() err := retryIfError(func(attempt int64) error { @@ -741,7 +741,7 @@ func (r *alertRuleRegistry) keyMap() map[models.AlertRuleKey]struct{} { } type alertRuleInfo struct { - evalCh chan *evalContext + evalCh chan *evaluation updateCh chan struct{} ctx context.Context stop context.CancelFunc @@ -749,15 +749,15 @@ type alertRuleInfo struct { func newAlertRuleInfo(parent context.Context) *alertRuleInfo { ctx, cancel := context.WithCancel(parent) - return &alertRuleInfo{evalCh: make(chan *evalContext), updateCh: make(chan struct{}), ctx: ctx, stop: cancel} + return &alertRuleInfo{evalCh: make(chan *evaluation), updateCh: make(chan struct{}), ctx: ctx, stop: cancel} } // eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped func (a *alertRuleInfo) eval(t time.Time, version int64) bool { select { - case a.evalCh <- &evalContext{ - now: t, - version: version, + case a.evalCh <- &evaluation{ + scheduledAt: t, + version: version, }: return true case <-a.ctx.Done(): @@ -775,16 +775,16 @@ func (a *alertRuleInfo) update() bool { } } -type evalContext struct { - now time.Time - version int64 +type evaluation struct { + scheduledAt time.Time + version int64 } // overrideCfg is only used on tests. func (sch *schedule) overrideCfg(cfg SchedulerCfg) { sch.clock = cfg.C sch.baseInterval = cfg.BaseInterval - sch.heartbeat = alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds())) + sch.ticker = alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds())) sch.evalAppliedFunc = cfg.EvalAppliedFunc sch.stopAppliedFunc = cfg.StopAppliedFunc } diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 69b2f6bf961..6555f11da95 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -361,7 +361,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { for _, evalState := range normalStates { // TODO rewrite when we are able to mock/fake state manager t.Run(fmt.Sprintf("when rule evaluation happens (evaluation state %s)", evalState), func(t *testing.T) { - evalChan := make(chan *evalContext) + evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) sch, ruleStore, instanceStore, _, reg := createSchedule(evalAppliedChan) @@ -375,9 +375,9 @@ func TestSchedule_ruleRoutine(t *testing.T) { expectedTime := time.UnixMicro(rand.Int63()) - evalChan <- &evalContext{ - now: expectedTime, - version: rule.Version, + evalChan <- &evaluation{ + scheduledAt: expectedTime, + version: rule.Version, } actualTime := waitForTimeChannel(t, evalAppliedChan) @@ -467,7 +467,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) go func() { - err := sch.ruleRoutine(ctx, models.AlertRuleKey{}, make(chan *evalContext), make(chan struct{})) + err := sch.ruleRoutine(ctx, models.AlertRuleKey{}, make(chan *evaluation), make(chan struct{})) stoppedChan <- err }() @@ -478,7 +478,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { }) t.Run("should fetch rule from database only if new version is greater than current", func(t *testing.T) { - evalChan := make(chan *evalContext) + evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) ctx := context.Background() @@ -493,9 +493,9 @@ func TestSchedule_ruleRoutine(t *testing.T) { }() expectedTime := time.UnixMicro(rand.Int63()) - evalChan <- &evalContext{ - now: expectedTime, - version: rule.Version, + evalChan <- &evaluation{ + scheduledAt: expectedTime, + version: rule.Version, } actualTime := waitForTimeChannel(t, evalAppliedChan) @@ -508,9 +508,9 @@ func TestSchedule_ruleRoutine(t *testing.T) { // and call with new version expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second) - evalChan <- &evalContext{ - now: expectedTime, - version: newRule.Version, + evalChan <- &evaluation{ + scheduledAt: expectedTime, + version: newRule.Version, } actualTime = waitForTimeChannel(t, evalAppliedChan) @@ -531,7 +531,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { }) t.Run("should not fetch rule if version is equal or less than current", func(t *testing.T) { - evalChan := make(chan *evalContext) + evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) sch, ruleStore, _, _, _ := createSchedule(evalAppliedChan) @@ -545,9 +545,9 @@ func TestSchedule_ruleRoutine(t *testing.T) { }() expectedTime := time.UnixMicro(rand.Int63()) - evalChan <- &evalContext{ - now: expectedTime, - version: rule.Version, + evalChan <- &evaluation{ + scheduledAt: expectedTime, + version: rule.Version, } actualTime := waitForTimeChannel(t, evalAppliedChan) @@ -555,17 +555,17 @@ func TestSchedule_ruleRoutine(t *testing.T) { // try again with the same version expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second) - evalChan <- &evalContext{ - now: expectedTime, - version: rule.Version, + evalChan <- &evaluation{ + scheduledAt: expectedTime, + version: rule.Version, } actualTime = waitForTimeChannel(t, evalAppliedChan) require.Equal(t, expectedTime, actualTime) expectedTime = expectedTime.Add(time.Duration(rand.Intn(10)) * time.Second) - evalChan <- &evalContext{ - now: expectedTime, - version: rule.Version - 1, + evalChan <- &evaluation{ + scheduledAt: expectedTime, + version: rule.Version - 1, } actualTime = waitForTimeChannel(t, evalAppliedChan) require.Equal(t, expectedTime, actualTime) @@ -582,7 +582,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { t.Run("when update channel is not empty", func(t *testing.T) { t.Run("should fetch the alert rule from database", func(t *testing.T) { - evalChan := make(chan *evalContext) + evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) updateChan := make(chan struct{}) @@ -612,9 +612,9 @@ func TestSchedule_ruleRoutine(t *testing.T) { require.Equal(t, rule.OrgID, m.OrgID) // now call evaluation loop to make sure that the rule was persisted - evalChan <- &evalContext{ - now: time.UnixMicro(rand.Int63()), - version: rule.Version, + evalChan <- &evaluation{ + scheduledAt: time.UnixMicro(rand.Int63()), + version: rule.Version, } waitForTimeChannel(t, evalAppliedChan) @@ -637,7 +637,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { go func() { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - _ = sch.ruleRoutine(ctx, rule.GetKey(), make(chan *evalContext), updateChan) + _ = sch.ruleRoutine(ctx, rule.GetKey(), make(chan *evaluation), updateChan) }() ruleStore.hook = func(cmd interface{}) error { @@ -677,7 +677,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { return len(s.Alertmanagers()) == 1 }, 20*time.Second, 200*time.Millisecond, "external Alertmanager was not discovered.") - evalChan := make(chan *evalContext) + evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) updateChan := make(chan struct{}) @@ -783,7 +783,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { return len(s.Alertmanagers()) == 1 }, 20*time.Second, 200*time.Millisecond, "external Alertmanager was not discovered.") - evalChan := make(chan *evalContext) + evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) sch, ruleStore, _, _, _ := createSchedule(evalAppliedChan) @@ -797,9 +797,9 @@ func TestSchedule_ruleRoutine(t *testing.T) { _ = sch.ruleRoutine(ctx, rule.GetKey(), evalChan, make(chan struct{})) }() - evalChan <- &evalContext{ - now: time.Now(), - version: rule.Version, + evalChan <- &evaluation{ + scheduledAt: time.Now(), + version: rule.Version, } waitForTimeChannel(t, evalAppliedChan) @@ -843,7 +843,7 @@ func TestSchedule_alertRuleInfo(t *testing.T) { select { case ctx := <-r.evalCh: require.Equal(t, version, ctx.version) - require.Equal(t, expected, ctx.now) + require.Equal(t, expected, ctx.scheduledAt) require.True(t, <-resultCh) case <-time.After(5 * time.Second): t.Fatal("No message was received on eval channel") From 47c777930deb96774823a06b40b6d8613c27a26e Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 04:39:48 -0600 Subject: [PATCH 14/34] fix: update bump-version-action node runtime to 16 (#45856) (#45896) (cherry picked from commit ab0bbf6715d19a20e6c696301d3131c6c1d1468c) Co-authored-by: Timur Olzhabayev --- .github/workflows/bump-version.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index bb0f40ea205..8d2f329022e 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -80,7 +80,7 @@ jobs: ref: main - uses: actions/setup-node@v2.5.1 with: - node-version: '14' + node-version: '16' - name: Install Actions run: npm install --production --prefix ./actions - name: Run bump version (manually invoked) From d27bc6e788e4438a1a1b7e48c173a668147e8729 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 05:17:48 -0600 Subject: [PATCH 15/34] Escape windows newline. (#45771) (#45857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #45746 (cherry picked from commit dcd98f7819f3af14ef5233e484ffbc10e2b0364c) Co-authored-by: Per Osbäck --- packages/grafana-data/src/utils/logs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-data/src/utils/logs.ts b/packages/grafana-data/src/utils/logs.ts index 96af4c4a602..a95d14fc42e 100644 --- a/packages/grafana-data/src/utils/logs.ts +++ b/packages/grafana-data/src/utils/logs.ts @@ -229,4 +229,4 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM }; export const escapeUnescapedString = (string: string) => - string.replace(/\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); + string.replace(/\\r\\n|\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); From 1d1e81b309695016cb52973dd525c8a0d03ebbca Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 05:36:34 -0600 Subject: [PATCH 16/34] CloudWatch: Fix running go test with count (#45892) (#45903) (cherry picked from commit 304185f682cd94133374dce604e06f3b78b8b00b) Co-authored-by: Shirley <4163034+fridgepoet@users.noreply.github.com> --- pkg/tsdb/cloudwatch/get_metric_data_executor_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/tsdb/cloudwatch/get_metric_data_executor_test.go b/pkg/tsdb/cloudwatch/get_metric_data_executor_test.go index 46612e4ed43..dc9c3893077 100644 --- a/pkg/tsdb/cloudwatch/get_metric_data_executor_test.go +++ b/pkg/tsdb/cloudwatch/get_metric_data_executor_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/require" ) -var counter = 1 - type cloudWatchFakeClient struct { cloudwatchiface.CloudWatchAPI + + counterForGetMetricDataWithContext int } func (client *cloudWatchFakeClient) GetMetricDataWithContext(ctx aws.Context, input *cloudwatch.GetMetricDataInput, opts ...request.Option) (*cloudwatch.GetMetricDataOutput, error) { @@ -23,13 +23,13 @@ func (client *cloudWatchFakeClient) GetMetricDataWithContext(ctx aws.Context, in res := []*cloudwatch.MetricDataResult{{ Values: []*float64{aws.Float64(12.3), aws.Float64(23.5)}, }} - if counter == 0 { + if client.counterForGetMetricDataWithContext == 0 { nextToken = "" res = []*cloudwatch.MetricDataResult{{ Values: []*float64{aws.Float64(100)}, }} } - counter-- + client.counterForGetMetricDataWithContext-- return &cloudwatch.GetMetricDataOutput{ MetricDataResults: res, NextToken: aws.String(nextToken), @@ -39,7 +39,7 @@ func (client *cloudWatchFakeClient) GetMetricDataWithContext(ctx aws.Context, in func TestGetMetricDataExecutorTest(t *testing.T) { executor := &cloudWatchExecutor{} inputs := &cloudwatch.GetMetricDataInput{MetricDataQueries: []*cloudwatch.MetricDataQuery{}} - res, err := executor.executeRequest(context.Background(), &cloudWatchFakeClient{}, inputs) + res, err := executor.executeRequest(context.Background(), &cloudWatchFakeClient{counterForGetMetricDataWithContext: 1}, inputs) require.NoError(t, err) require.Len(t, res, 2) require.Len(t, res[0].MetricDataResults[0].Values, 2) From d17f59f052d1f6352256431a3a3de03bcb4fb088 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 05:45:29 -0600 Subject: [PATCH 17/34] loki: log-volume: improved documentation (#45823) (#45887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * loki: log-volume: improved documentation * spelling fixes (cherry picked from commit 7152deb92f451f0f5a441deb5b64297da56df52a) Co-authored-by: Gábor Farkas --- docs/sources/explore/logs-integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/explore/logs-integration.md b/docs/sources/explore/logs-integration.md index 51b33a7d632..3e19c75196f 100644 --- a/docs/sources/explore/logs-integration.md +++ b/docs/sources/explore/logs-integration.md @@ -31,7 +31,7 @@ If the data source does not support loading full range log volume histogram, the For logs where a level label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with an unknown log level. -> **Tip:** If you use Loki data source and the "level" is in you log content, try to use parsers (JSON, logfmt, regex,..) to extract level information into level label that is used to determine log level. +> **Tip:** If you use Loki data source and the "level" is in your log-line, use parsers (JSON, logfmt, regex,..) to extract the level information into a level label that is used to determine log level. This will allow the histogram to show the various log levels in separate bars. **Supported log levels and mapping of log level abbreviation and expressions:** From e555fd5455afe17ae987a5775da118a7e0501863 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 07:28:08 -0600 Subject: [PATCH 18/34] CloudWatch: Add test to executeStartQuery (#45888) (#45912) * CloudWatch: Add test to executeStartQuery * Add test for absence of limit * Restrict assertions to limit in some tests (cherry picked from commit a68a570e921258de735cb60feb863c2087efb23d) Co-authored-by: Shirley <4163034+fridgepoet@users.noreply.github.com> --- pkg/tsdb/cloudwatch/annotation_query_test.go | 7 -- pkg/tsdb/cloudwatch/log_actions_test.go | 112 ++++++++++++++++++- pkg/tsdb/cloudwatch/test_utils.go | 26 ++++- 3 files changed, 128 insertions(+), 17 deletions(-) diff --git a/pkg/tsdb/cloudwatch/annotation_query_test.go b/pkg/tsdb/cloudwatch/annotation_query_test.go index 6f3d7c71d6b..ad947c538c9 100644 --- a/pkg/tsdb/cloudwatch/annotation_query_test.go +++ b/pkg/tsdb/cloudwatch/annotation_query_test.go @@ -97,10 +97,3 @@ func TestQuery_AnnotationQuery(t *testing.T) { }, client.calls.describeAlarms[0]) }) } - -func pointerString(s string) *string { - return &s -} -func pointerInt64(i int64) *int64 { - return &i -} diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go index 4d3df038de3..bfe6780a52f 100644 --- a/pkg/tsdb/cloudwatch/log_actions_test.go +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -28,7 +28,7 @@ func TestQuery_DescribeLogGroups(t *testing.T) { var cli FakeCWLogsClient NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { - return cli + return &cli } t.Run("Empty log group name prefix", func(t *testing.T) { @@ -155,7 +155,7 @@ func TestQuery_GetLogGroupFields(t *testing.T) { var cli FakeCWLogsClient NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { - return cli + return &cli } cli = FakeCWLogsClient{ @@ -232,7 +232,7 @@ func TestQuery_StartQuery(t *testing.T) { var cli FakeCWLogsClient NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { - return cli + return &cli } t.Run("invalid time range", func(t *testing.T) { @@ -357,6 +357,108 @@ func TestQuery_StartQuery(t *testing.T) { }) } +func Test_executeStartQuery(t *testing.T) { + origNewCWLogsClient := NewCWLogsClient + t.Cleanup(func() { + NewCWLogsClient = origNewCWLogsClient + }) + + var cli FakeCWLogsClient + + NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { + return &cli + } + + t.Run("successfully parses information from JSON to StartQueryWithContext", func(t *testing.T) { + cli = FakeCWLogsClient{} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery", + "limit": 12, + "queryString":"fields @message", + "logGroupNames":["some name","another name"] + }`), + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, []*cloudwatchlogs.StartQueryInput{ + { + StartTime: pointerInt64(0), + EndTime: pointerInt64(1), + Limit: pointerInt64(12), + QueryString: pointerString("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), + LogGroupNames: []*string{pointerString("some name"), pointerString("another name")}, + }, + }, cli.calls.startQueryWithContext) + }) + + t.Run("cannot parse limit as float", func(t *testing.T) { + cli = FakeCWLogsClient{} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery", + "limit": 12.0 + }`), + }, + }, + }) + + assert.NoError(t, err) + require.Len(t, cli.calls.startQueryWithContext, 1) + assert.Nil(t, cli.calls.startQueryWithContext[0].Limit) + }) + + t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) { + cli = FakeCWLogsClient{} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery" + }`), + }, + }, + }) + + assert.NoError(t, err) + require.Len(t, cli.calls.startQueryWithContext, 1) + assert.Nil(t, cli.calls.startQueryWithContext[0].Limit) + }) +} + func TestQuery_StopQuery(t *testing.T) { origNewCWLogsClient := NewCWLogsClient t.Cleanup(func() { @@ -366,7 +468,7 @@ func TestQuery_StopQuery(t *testing.T) { var cli FakeCWLogsClient NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { - return cli + return &cli } cli = FakeCWLogsClient{ @@ -438,7 +540,7 @@ func TestQuery_GetQueryResults(t *testing.T) { var cli FakeCWLogsClient NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { - return cli + return &cli } const refID = "A" diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index 8eac803c83a..d8c8ce1f07a 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -20,32 +20,41 @@ import ( type FakeCWLogsClient struct { cloudwatchlogsiface.CloudWatchLogsAPI + + calls logsQueryCalls + logGroups cloudwatchlogs.DescribeLogGroupsOutput logGroupFields cloudwatchlogs.GetLogGroupFieldsOutput queryResults cloudwatchlogs.GetQueryResultsOutput } -func (m FakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) { +type logsQueryCalls struct { + startQueryWithContext []*cloudwatchlogs.StartQueryInput +} + +func (m *FakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) { return &m.queryResults, nil } -func (m FakeCWLogsClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) { +func (m *FakeCWLogsClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) { + m.calls.startQueryWithContext = append(m.calls.startQueryWithContext, input) + return &cloudwatchlogs.StartQueryOutput{ QueryId: aws.String("abcd-efgh-ijkl-mnop"), }, nil } -func (m FakeCWLogsClient) StopQueryWithContext(ctx context.Context, input *cloudwatchlogs.StopQueryInput, option ...request.Option) (*cloudwatchlogs.StopQueryOutput, error) { +func (m *FakeCWLogsClient) StopQueryWithContext(ctx context.Context, input *cloudwatchlogs.StopQueryInput, option ...request.Option) (*cloudwatchlogs.StopQueryOutput, error) { return &cloudwatchlogs.StopQueryOutput{ Success: aws.Bool(true), }, nil } -func (m FakeCWLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { +func (m *FakeCWLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { return &m.logGroups, nil } -func (m FakeCWLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) { +func (m *FakeCWLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) { return &m.logGroupFields, nil } @@ -192,3 +201,10 @@ func (s fakeSessionCache) GetSession(c awsds.SessionConfig) (*session.Session, e Config: &aws.Config{}, }, nil } + +func pointerString(s string) *string { + return &s +} +func pointerInt64(i int64) *int64 { + return &i +} From 6ea6c611a9545eb30b6501df388fe71324c850ea Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 08:24:44 -0600 Subject: [PATCH 19/34] Fix incorrect metric values for scheduler_behind_seconds (#45830) (#45904) (cherry picked from commit 6cccbb5a0973f66b8e4fc0faa350994c7b48e20e) Co-authored-by: George Robinson --- pkg/services/ngalert/schedule/schedule.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index eaec8aefbb4..8e8c5fd220c 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -368,7 +368,11 @@ func (sch *schedule) schedulePeriodic(ctx context.Context) error { for { select { case tick := <-sch.ticker.C: - start := time.Now() + // We use Round(0) on the start time to remove the monotonic clock. + // This is required as late ticks from the ticker have current monotonic + // timestamps such that start.Sub(tick) does not return the expected + // delta. + start := time.Now().Round(0) sch.metrics.BehindSeconds.Set(start.Sub(tick).Seconds()) tickNum := tick.Unix() / int64(sch.baseInterval.Seconds()) From 87edde2b38cef856cfc6ddb5ae0a1f366a97b3ba Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 25 Feb 2022 08:54:42 -0600 Subject: [PATCH 20/34] Update comment for scheduler_behind_seconds metric (#45918) (#45919) (cherry picked from commit f87bfdf2ffd3e795a9385f475ba957c7ff4ca16b) Co-authored-by: George Robinson --- pkg/services/ngalert/schedule/schedule.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 8e8c5fd220c..7d607778021 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -369,9 +369,9 @@ func (sch *schedule) schedulePeriodic(ctx context.Context) error { select { case tick := <-sch.ticker.C: // We use Round(0) on the start time to remove the monotonic clock. - // This is required as late ticks from the ticker have current monotonic - // timestamps such that start.Sub(tick) does not return the expected - // delta. + // This is required as ticks from the ticker and time.Now() can have + // a monotonic clock that when subtracted do not represent the delta + // in wall clock time. start := time.Now().Round(0) sch.metrics.BehindSeconds.Set(start.Sub(tick).Seconds()) From e7f52218d16f3327eaee0ab4442b4add6e53c320 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sat, 26 Feb 2022 14:16:40 -0600 Subject: [PATCH 21/34] Capitalize Webhook contact point type (#45942) (#45943) (cherry picked from commit 1b2c4dca61648fb65345df329d51b1deaac7a41d) Co-authored-by: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> --- pkg/services/ngalert/notifier/available_channels.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/ngalert/notifier/available_channels.go b/pkg/services/ngalert/notifier/available_channels.go index 521548e53b5..2ac4ec04b04 100644 --- a/pkg/services/ngalert/notifier/available_channels.go +++ b/pkg/services/ngalert/notifier/available_channels.go @@ -597,7 +597,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { }, { Type: "webhook", - Name: "webhook", + Name: "Webhook", Description: "Sends HTTP POST request to a URL", Heading: "Webhook settings", Options: []alerting.NotifierOption{ From b0646b1be3bf3c91c8724830648df5e4b3bff6ba Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 28 Feb 2022 13:15:10 -0600 Subject: [PATCH 22/34] Middleware: Fix IPv6 host parsing in CSRF check (#45911) (#45984) - Also create tests for this middleware Co-authored-by: Kyle Brandt (cherry picked from commit 06ed5efdf09efeaffa766f0009f5272f05e808c7) Co-authored-by: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> --- pkg/api/http_server.go | 2 +- pkg/middleware/csrf.go | 19 ++++-- pkg/middleware/csrf_test.go | 124 ++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 pkg/middleware/csrf_test.go diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 8f736f79070..353c40cea8f 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -441,7 +441,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { } m.Use(middleware.Recovery(hs.Cfg)) - m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName)) + m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName, hs.log)) hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build") hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public", "/public/views/swagger.html") diff --git a/pkg/middleware/csrf.go b/pkg/middleware/csrf.go index bc70d09779d..7bce53f5666 100644 --- a/pkg/middleware/csrf.go +++ b/pkg/middleware/csrf.go @@ -4,10 +4,12 @@ import ( "errors" "net/http" "net/url" - "strings" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/util" ) -func CSRF(loginCookieName string) func(http.Handler) http.Handler { +func CSRF(loginCookieName string, logger log.Logger) func(http.Handler) http.Handler { // As per RFC 7231/4.2.2 these methods are idempotent: // (GET is excluded because it may have side effects in some APIs) safeMethods := []string{"HEAD", "OPTIONS", "TRACE"} @@ -27,12 +29,21 @@ func CSRF(loginCookieName string) func(http.Handler) http.Handler { } } // Otherwise - verify that Origin matches the server origin - host := strings.Split(r.Host, ":")[0] + netAddr, err := util.SplitHostPortDefault(r.Host, "", "0") // we ignore the port + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + origin, err := url.Parse(r.Header.Get("Origin")) - if err != nil || (origin.String() != "" && origin.Hostname() != host) { + if err != nil { + logger.Error("error parsing Origin header", "err", err) + } + if err != nil || netAddr.Host == "" || (origin.String() != "" && origin.Hostname() != netAddr.Host) { http.Error(w, "origin not allowed", http.StatusForbidden) return } + next.ServeHTTP(w, r) }) } diff --git a/pkg/middleware/csrf_test.go b/pkg/middleware/csrf_test.go new file mode 100644 index 00000000000..312356cce3f --- /dev/null +++ b/pkg/middleware/csrf_test.go @@ -0,0 +1,124 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/stretchr/testify/require" +) + +func TestMiddlewareCSRF(t *testing.T) { + tests := []struct { + name string + cookieName string + method string + origin string + host string + code int + }{ + { + name: "mismatched origin and host is forbidden", + cookieName: "foo", + method: "GET", + origin: "http://notLocalhost", + host: "localhost", + code: http.StatusForbidden, + }, + { + name: "mismatched origin and host is NOT forbidden with a 'Safe Method'", + cookieName: "foo", + method: "TRACE", + origin: "http://notLocalhost", + host: "localhost", + code: http.StatusOK, + }, + { + name: "mismatched origin and host is NOT forbidden without a cookie", + cookieName: "", + method: "GET", + origin: "http://notLocalhost", + host: "localhost", + code: http.StatusOK, + }, + { + name: "malformed host is a bad request", + cookieName: "foo", + method: "GET", + host: "localhost:80:80", + code: http.StatusBadRequest, + }, + { + name: "host works without port", + cookieName: "foo", + method: "GET", + host: "localhost", + origin: "http://localhost", + code: http.StatusOK, + }, + { + name: "port does not have to match", + cookieName: "foo", + method: "GET", + host: "localhost:80", + origin: "http://localhost:3000", + code: http.StatusOK, + }, + { + name: "IPv6 host works with port", + cookieName: "foo", + method: "GET", + host: "[::1]:3000", + origin: "http://[::1]:3000", + code: http.StatusOK, + }, + { + name: "IPv6 host (with longer address) works with port", + cookieName: "foo", + method: "GET", + host: "[2001:db8::1]:3000", + origin: "http://[2001:db8::1]:3000", + code: http.StatusOK, + }, + { + name: "IPv6 host (with longer address) works without port", + cookieName: "foo", + method: "GET", + host: "[2001:db8::1]", + origin: "http://[2001:db8::1]", + code: http.StatusOK, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rr := csrfScenario(t, tt.cookieName, tt.method, tt.origin, tt.host) + require.Equal(t, tt.code, rr.Code) + }) + } +} + +func csrfScenario(t *testing.T, cookieName, method, origin, host string) *httptest.ResponseRecorder { + req, err := http.NewRequest(method, "/", nil) + if err != nil { + t.Fatal(err) + } + req.AddCookie(&http.Cookie{ + Name: cookieName, + }) + + // Note: Not sure where host header populates req.Host, or how that works. + req.Host = host + req.Header.Set("HOST", host) + + req.Header.Set("ORIGIN", origin) + + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + }) + + rr := httptest.NewRecorder() + handler := CSRF(cookieName, log.New())(testHandler) + handler.ServeHTTP(rr, req) + return rr +} From be55f8800e7141bc5cbe8be3e366afac212de82c Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 28 Feb 2022 13:37:38 -0600 Subject: [PATCH 23/34] Jaeger: Show loader when search options havent yet loaded (#45936) (#45985) * Swap out Select component for AsyncSelect to Jaeger search panel (cherry picked from commit 83664121bc09399f76ee387c7e5cd0e85aa70020) Co-authored-by: Cat Perry <000.perry@gmail.com> --- .../jaeger/components/SearchForm.test.tsx | 154 ++++++++++++++++++ .../jaeger/components/SearchForm.tsx | 111 ++++++++----- 2 files changed, 224 insertions(+), 41 deletions(-) create mode 100644 public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx new file mode 100644 index 00000000000..77d85ac8526 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx @@ -0,0 +1,154 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { createFetchResponse } from 'test/helpers/createFetchResponse'; +import { DataQueryRequest, DataSourceInstanceSettings, dateTime, PluginType } from '@grafana/data'; +import { of } from 'rxjs'; +import { JaegerDatasource, JaegerJsonData } from '../datasource'; +import { JaegerQuery } from '../types'; +import React from 'react'; +import SearchForm from './SearchForm'; +import { testResponse } from '../testResponse'; +import userEvent from '@testing-library/user-event'; + +describe('SearchForm', () => { + it('should call the `onChange` function on click of the Input', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => promise); + const query = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + }; + const ds = { + async metadataRequest(url: string, params?: Record): Promise { + if (url === '/api/services') { + return Promise.resolve(['jaeger-query', 'service2', 'service3']); + } + }, + } as JaegerDatasource; + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncServiceSelect = await waitFor(() => screen.getByRole('combobox', { name: 'select-service-name' })); + expect(asyncServiceSelect).toBeInTheDocument(); + + userEvent.click(asyncServiceSelect); + + const jaegerService = await screen.findByText('jaeger-query'); + expect(jaegerService).toBeInTheDocument(); + }); + + it('should be able to select operation name if query.service exists', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => promise); + const query2 = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + service: 'jaeger-query', + }; + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncOperationSelect2 = await waitFor(() => screen.getByRole('combobox', { name: 'select-operation-name' })); + expect(asyncOperationSelect2).toBeInTheDocument(); + }); +}); + +describe('SearchForm', () => { + it('should show loader if there is a delay fetching options', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => { + setTimeout(() => { + return promise; + }, 3000); + }); + const query = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + service: 'jaeger-query', + }; + const ds = new JaegerDatasource(defaultSettings); + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' }); + userEvent.click(asyncServiceSelect); + const loader = screen.getByText('Loading options...'); + + expect(loader).toBeInTheDocument(); + await act(() => promise); + }); +}); + +function setupFetchMock(response: any, mock?: any) { + const defaultMock = () => mock ?? of(createFetchResponse(response)); + + const fetchMock = jest.spyOn(backendSrv, 'fetch'); + fetchMock.mockImplementation(defaultMock); + return fetchMock; +} + +const defaultSettings: DataSourceInstanceSettings = { + id: 0, + uid: '0', + type: 'tracing', + name: 'jaeger', + url: 'http://grafana.com', + access: 'proxy', + meta: { + id: 'jaeger', + name: 'jaeger', + type: PluginType.datasource, + info: {} as any, + module: '', + baseUrl: '', + }, + jsonData: { + nodeGraph: { + enabled: true, + }, + }, +}; + +const defaultQuery: DataQueryRequest = { + requestId: '1', + dashboardId: 0, + interval: '0', + intervalMs: 10, + panelId: 0, + scopedVars: {}, + range: { + from: dateTime().subtract(1, 'h'), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + timezone: 'browser', + app: 'explore', + startTime: 0, + targets: [ + { + query: '12345', + refId: '1', + }, + ], +}; diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index 1c4e6de235b..c362314bbed 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -1,11 +1,14 @@ import { css } from '@emotion/css'; import { SelectableValue } from '@grafana/data'; -import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; -import React, { useEffect, useState } from 'react'; +import { AsyncSelect, InlineField, InlineFieldRow, Input } from '@grafana/ui'; +import React, { useCallback, useEffect, useState } from 'react'; import { JaegerDatasource } from '../datasource'; import { JaegerQuery } from '../types'; import { transformToLogfmt } from '../util'; import { AdvancedOptions } from './AdvancedOptions'; +import { dispatch } from 'app/store/store'; +import { notifyApp } from 'app/core/actions'; +import { createErrorNotification } from 'app/core/copy/appNotification'; type Props = { datasource: JaegerDatasource; @@ -22,69 +25,110 @@ const allOperationsOption: SelectableValue = { export function SearchForm({ datasource, query, onChange }: Props) { const [serviceOptions, setServiceOptions] = useState>>(); const [operationOptions, setOperationOptions] = useState>>(); + const [isLoading, setIsLoading] = useState<{ + services: boolean; + operations: boolean; + }>({ + services: false, + operations: false, + }); + + const loadServices = useCallback( + async (url: string, loaderOfType: string): Promise>> => { + setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: true })); + + try { + const values: string[] | null = await datasource.metadataRequest(url); + if (!values) { + return [{ label: `No ${loaderOfType} found`, value: `No ${loaderOfType} found` }]; + } + + const serviceOptions: SelectableValue[] = values.sort().map((service) => ({ + label: service, + value: service, + })); + return serviceOptions; + } catch (error) { + dispatch(notifyApp(createErrorNotification('Error', error))); + return []; + } finally { + setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: false })); + } + }, + [datasource] + ); useEffect(() => { const getServices = async () => { - const services = await loadServices({ - dataSource: datasource, - url: '/api/services', - notFoundLabel: 'No service found', - }); + const services = await loadServices('/api/services', 'services'); setServiceOptions(services); }; getServices(); - }, [datasource]); + }, [datasource, loadServices]); useEffect(() => { const getOperations = async () => { - const operations = await loadServices({ - dataSource: datasource, - url: `/api/services/${encodeURIComponent(query.service!)}/operations`, - notFoundLabel: 'No operation found', - }); + const operations = await loadServices( + `/api/services/${encodeURIComponent(query.service!)}/operations`, + 'operations' + ); setOperationOptions([allOperationsOption, ...operations]); }; if (query.service) { getOperations(); } - }, [datasource, query.service]); + }, [datasource, query.service, loadServices]); return (
- + loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations') + } + onOpenMenu={() => + loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations') + } + isLoading={isLoading.operations} value={operationOptions?.find((v) => v.value === query.operation) || null} onChange={(v) => onChange({ ...query, - operation: v.value!, + operation: v?.value! || undefined, }) } menuPlacement="bottom" isClearable + defaultOptions + aria-label={'select-operation-name'} /> @@ -108,19 +152,4 @@ export function SearchForm({ datasource, query, onChange }: Props) { ); } -type Options = { dataSource: JaegerDatasource; url: string; notFoundLabel: string }; - -const loadServices = async ({ dataSource, url, notFoundLabel }: Options): Promise>> => { - const services: string[] | null = await dataSource.metadataRequest(url); - - if (!services) { - return [{ label: notFoundLabel, value: notFoundLabel }]; - } - - const serviceOptions: SelectableValue[] = services.sort().map((service) => ({ - label: service, - value: service, - })); - - return serviceOptions; -}; +export default SearchForm; From 700fd45f545166df27d2890c009e8339ce638e5f Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 28 Feb 2022 16:33:38 -0500 Subject: [PATCH 24/34] BarChart: fix single group rendering (#45953) (#45992) (cherry picked from commit 1c4b20b2686b38dfa2312dd0c36ec7fce777f493) Co-authored-by: Leon Sorokin --- public/app/plugins/panel/barchart/bars.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index eb9437b53e9..31a0cfdc71b 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -138,7 +138,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { // this expands the distr: 2 scale so that the indicies of each data[0] land at the proper justified positions const xRange: Scale.Range = (u, min, max) => { min = 0; - max = u.data[0].length - 1; + max = Math.max(1, u.data[0].length - 1); let pctOffset = 0; @@ -148,13 +148,17 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { }); // expand scale range by equal amounts on both ends - let rn = max - min; // TODO: clamp to 1? + let rn = max - min; - let upScale = 1 / (1 - pctOffset * 2); - let offset = (upScale * rn - rn) / 2; + if (pctOffset === 0.5) { + min -= rn; + } else { + let upScale = 1 / (1 - pctOffset * 2); + let offset = (upScale * rn - rn) / 2; - min -= offset; - max += offset; + min -= offset; + max += offset; + } return [min, max]; }; From 865723d56e2ccf1d54da76e54a6dd481ee4cfa0c Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 28 Feb 2022 17:55:54 -0500 Subject: [PATCH 25/34] StateTimeline: fix duration in tooltip (#45955) (#45987) - Fixes duration in StateTimeline appearing incorrectly when "merge consecutive values" is enabled. (cherry picked from commit 5aab0063c7335474582b9a7d17b2fc069270500e) Co-authored-by: Leon Sorokin --- public/app/plugins/panel/state-timeline/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 1b5a7ff3036..5db103c4e61 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -553,16 +553,18 @@ export function findNextStateIndex(field: Field, datapointIdx: number) { return null; } + const startValue = field.values.get(datapointIdx); + while (end === undefined) { if (rightPointer >= field.values.length) { return null; } const rightValue = field.values.get(rightPointer); - if (rightValue !== undefined) { - end = rightPointer; - } else { + if (rightValue === undefined || rightValue === startValue) { rightPointer++; + } else { + end = rightPointer; } } From 1f39b05f33717c946a58b46a9bc732410e16081d Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 1 Mar 2022 03:12:36 -0500 Subject: [PATCH 26/34] Histogram: auto-skip x tick labels to avoid overlap (#45996) (#46001) (cherry picked from commit b491d6b4dc5c768fa5f24d9e83f5485e0395a6ce) Co-authored-by: Leon Sorokin --- .../app/plugins/panel/histogram/Histogram.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index a4b21980c2f..29115ecff96 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -15,7 +15,15 @@ import { getFieldSeriesColor, GrafanaTheme2, } from '@grafana/data'; -import { Themeable2, UPlotConfigBuilder, UPlotChart, VizLayout, PlotLegend } from '@grafana/ui'; +import { + Themeable2, + UPlotConfigBuilder, + UPlotChart, + VizLayout, + PlotLegend, + measureText, + UPLOT_AXIS_FONT_SIZE, +} from '@grafana/ui'; import { histogramBucketSizes, @@ -119,7 +127,20 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { placement: AxisPlacement.Bottom, incrs: histogramBucketSizes, splits: xSplits, - values: (u: uPlot, vals: any[]) => vals.map(xAxisFormatter), + values: (u: uPlot, splits: any[]) => { + const tickLabels = splits.map(xAxisFormatter); + + const maxWidth = tickLabels.reduce( + (curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax), + 0 + ); + + const labelSpacing = 10; + const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio); + const keepMod = Math.ceil(tickLabels.length / maxCount); + + return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null)); + }, //incrs: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((mult) => mult * bucketSize), //splits: config.xSplits, //values: config.xValues, From a31a0e898ac90a9ea36dfd2a1c81dc72fcd93230 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 1 Mar 2022 07:40:33 -0500 Subject: [PATCH 27/34] Making yarn.lock bump work (#46016) (#46019) (cherry picked from commit 6f14490c6b48d2a2a4cd66b5308f6a4c84d20fc0) Co-authored-by: Timur Olzhabayev --- .github/workflows/bump-version.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 8d2f329022e..debee02c499 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -16,6 +16,8 @@ on: required: true metricsWriteAPIKey: required: true +env: + YARN_ENABLE_IMMUTABLE_INSTALLS: false jobs: main: runs-on: ubuntu-latest From 6e03fe1f94c5e94f1850a7415531f05bc9b8f921 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 1 Mar 2022 08:03:34 -0500 Subject: [PATCH 28/34] Transformations: Use asterisk for First non-null label (#45940) (#46014) (cherry picked from commit 07dda8a299c3dc7be94e1c6481d87d4ed2f7b574) Co-authored-by: matt abrams <37156449+zuchka@users.noreply.github.com> --- packages/grafana-data/src/transformations/fieldReducer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index 8b2fea28f97..aaf400ba105 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -140,14 +140,14 @@ export const fieldReducers = new Registry(() => [ standard: true, reduce: calculateLast, }, - { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst }, { id: ReducerID.firstNotNull, - name: 'First', + name: 'First *', description: 'First non-null value', standard: true, reduce: calculateFirstNotNull, }, + { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst }, { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true }, { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true }, { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, aliasIds: ['avg'] }, From f26e85029779e619284e294ea27748e5d440fdb8 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 1 Mar 2022 14:17:25 +0100 Subject: [PATCH 29/34] fix conflict (#45957) --- .../__mocks__/CloudWatchDataSource.ts | 7 +- .../cloudwatch/components/QueryHeader.tsx | 9 ++- .../datasource/cloudwatch/datasource.test.ts | 65 +++++++++++++++++-- .../datasource/cloudwatch/datasource.ts | 44 ++++++------- 4 files changed, 89 insertions(+), 36 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index 8070eeb8688..5479778b689 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -1,11 +1,12 @@ import { dateTime } from '@grafana/data'; import { setBackendSrv } from '@grafana/runtime'; -import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock'; +import { TemplateSrv } from 'app/features/templating/template_srv'; import { initialCustomVariableModelState } from 'app/features/variables/custom/reducer'; import { CustomVariableModel } from 'app/features/variables/types'; import { of } from 'rxjs'; + +import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock'; import { CloudWatchDatasource } from '../datasource'; -import { TemplateSrv } from 'app/features/templating/template_srv'; export function setupMockedDataSource({ data = [], variables }: { data?: any; variables?: any } = {}) { let templateService = new TemplateSrvMock({ @@ -16,6 +17,8 @@ export function setupMockedDataSource({ data = [], variables }: { data?: any; va if (variables) { templateService = new TemplateSrv(); templateService.init(variables); + templateService.getVariables = jest.fn().mockReturnValue(variables); + templateService.getVariableName = (name: string) => name; } const datasource = new CloudWatchDatasource( diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx index def9d666832..268abb355e1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx @@ -1,12 +1,11 @@ -import React from 'react'; -import { pick } from 'lodash'; - import { ExploreMode, SelectableValue } from '@grafana/data'; import { EditorHeader, InlineSelect } from '@grafana/experimental'; +import { pick } from 'lodash'; +import React from 'react'; import { CloudWatchDatasource } from '../datasource'; -import { CloudWatchQuery, CloudWatchQueryMode } from '../types'; import { useRegions } from '../hooks'; +import { CloudWatchQuery, CloudWatchQueryMode } from '../types'; import MetricsQueryHeader from './MetricsQueryHeader'; interface QueryHeaderProps { @@ -59,7 +58,7 @@ const QueryHeader: React.FC = ({ v.value === region)} + value={region} placeholder="Select region" allowCustomValue onChange={({ value: region }) => region && onRegion({ value: region })} diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 591e95dcee6..840e90ffbad 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -1,17 +1,17 @@ -import { lastValueFrom, of } from 'rxjs'; -import { setDataSourceSrv } from '@grafana/runtime'; import { ArrayVector, DataFrame, dataFrameToJSON, dateTime, Field, MutableDataFrame } from '@grafana/data'; - +import { setDataSourceSrv } from '@grafana/runtime'; +import { lastValueFrom, of } from 'rxjs'; import { toArray } from 'rxjs/operators'; -import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType, CloudWatchLogsQueryStatus } from './types'; + import { - setupMockedDataSource, - namespaceVariable, - metricVariable, labelsVariable, limitVariable, + metricVariable, + namespaceVariable, + setupMockedDataSource, } from './__mocks__/CloudWatchDataSource'; import { CloudWatchDatasource } from './datasource'; +import { CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from './types'; describe('datasource', () => { describe('query', () => { @@ -91,6 +91,57 @@ describe('datasource', () => { }, ]); }); + + describe('debouncedCustomAlert', () => { + const debouncedAlert = jest.fn(); + beforeEach(() => { + const { datasource } = setupMockedDataSource({ + variables: [ + { ...namespaceVariable, multi: true }, + { ...metricVariable, multi: true }, + ], + }); + datasource.debouncedCustomAlert = debouncedAlert; + datasource.performTimeSeriesQuery = jest.fn().mockResolvedValue([]); + datasource.query({ + targets: [ + { + queryMode: 'Metrics', + id: '', + region: 'us-east-2', + namespace: namespaceVariable.id, + metricName: metricVariable.id, + period: '', + alias: '', + dimensions: {}, + matchExact: true, + statistic: '', + refId: '', + expression: 'x * 2', + metricQueryType: MetricQueryType.Search, + metricEditorMode: MetricEditorMode.Code, + }, + ], + } as any); + }); + it('should show debounced alert for namespace and metric name', async () => { + expect(debouncedAlert).toHaveBeenCalledWith( + 'CloudWatch templating error', + 'Multi template variables are not supported for namespace' + ); + expect(debouncedAlert).toHaveBeenCalledWith( + 'CloudWatch templating error', + 'Multi template variables are not supported for metric name' + ); + }); + + it('should not show debounced alert for region', async () => { + expect(debouncedAlert).not.toHaveBeenCalledWith( + 'CloudWatch templating error', + 'Multi template variables are not supported for region' + ); + }); + }); }); describe('filterMetricQuery', () => { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 3567dc58e9f..32cb083bf7d 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -1,9 +1,3 @@ -import React from 'react'; -import { cloneDeep, find, findLast, isEmpty, isString, set } from 'lodash'; -import { from, lastValueFrom, merge, Observable, of, throwError, zip } from 'rxjs'; -import { catchError, concatMap, finalize, map, mergeMap, repeat, scan, share, takeWhile, tap } from 'rxjs/operators'; -import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; -import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { DataFrame, DataQueryError, @@ -22,45 +16,51 @@ import { TimeRange, toLegacyResponseData, } from '@grafana/data'; - +import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; +import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; +import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; -import { AppNotificationTimeout } from 'app/types'; -import { store } from 'app/store/store'; -import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; +import { VariableWithMultiSupport } from 'app/features/variables/types'; +import { store } from 'app/store/store'; +import { AppNotificationTimeout } from 'app/types'; +import { cloneDeep, find, findLast, isEmpty, isString, set } from 'lodash'; +import React from 'react'; +import { from, lastValueFrom, merge, Observable, of, throwError, zip } from 'rxjs'; +import { catchError, concatMap, finalize, map, mergeMap, repeat, scan, share, takeWhile, tap } from 'rxjs/operators'; + +import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage'; +import { CloudWatchLanguageProvider } from './language_provider'; import memoizedDebounce from './memoizedDebounce'; +import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider'; import { - MetricEditorMode, CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchLogsQueryStatus, + CloudWatchLogsRequest, CloudWatchMetricsQuery, CloudWatchQuery, DescribeLogGroupsRequest, + Dimensions, GetLogEventsRequest, GetLogGroupFieldsRequest, GetLogGroupFieldsResponse, isCloudWatchLogsQuery, LogAction, - MetricQueryType, + MetricEditorMode, + MetricFindSuggestData, MetricQuery, + MetricQueryType, MetricRequest, StartQueryRequest, TSDBResponse, - Dimensions, - MetricFindSuggestData, - CloudWatchLogsRequest, } from './types'; -import { CloudWatchLanguageProvider } from './language_provider'; -import { VariableWithMultiSupport } from 'app/features/variables/types'; -import { increasingInterval } from './utils/rxjs/increasingInterval'; -import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; import { addDataLinksToLogsResponse } from './utils/datalinks'; import { runWithRetry } from './utils/logsRetry'; -import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; -import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider'; +import { increasingInterval } from './utils/rxjs/increasingInterval'; const DS_QUERY_ENDPOINT = '/api/ds/query'; @@ -267,7 +267,7 @@ export class CloudWatchDatasource const validMetricsQueries = metricQueries .filter(this.filterMetricQuery) .map((item: CloudWatchMetricsQuery): MetricQuery => { - item.region = this.replace(this.getActualRegion(item.region), options.scopedVars, true, 'region'); + item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars); item.namespace = this.replace(item.namespace, options.scopedVars, true, 'namespace'); item.metricName = this.replace(item.metricName, options.scopedVars, true, 'metric name'); item.dimensions = this.convertDimensionFormat(item.dimensions ?? {}, options.scopedVars); From 45e484132cac4a517f3f3407102b35e380cf758b Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 1 Mar 2022 10:53:06 -0500 Subject: [PATCH 30/34] Docs update default.ini file description (#46036) (#46038) * remove confusing wording * fixed broken alerting links (cherry picked from commit 77dddf43bc76ec8d887d357569733e3ad2541624) Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- docs/sources/administration/configuration.md | 2 +- .../alerting-rules/alert-annotation-label.md | 2 +- .../alerting/unified-alerting/notifications/_index.md | 6 +++--- .../alerting/unified-alerting/notifications/mute-timings.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index 9287a8a056c..a5f6fab22eb 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -14,7 +14,7 @@ Grafana has default and custom configuration files. You can customize your Grafa ## Configuration file location -The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change the location in this file. +The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change this file. Depending on your OS, your custom configuration file is either the `$WORKING_DIR/conf/defaults.ini` file or the `/usr/local/etc/grafana/grafana.ini` file. The custom configuration file path can be overridden using the `--config` parameter. diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md b/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md index c51c957447c..e7b634905c5 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md +++ b/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md @@ -20,7 +20,7 @@ Labels are key-value pairs that contain information about, and are used to uniqu ### How are labels used? - The complete set of labels for an alert is what uniquely identifies an alert within Grafana Alerts. -- The Alertmanager uses labels to match alerts for [silences]({{< relref "../silences/" >}}) and [alert groups]({{< relref "../alert-groups/" >}}) in [notification policies]({{< relref "../notification-policies/" >}}). +- The Alertmanager uses labels to match alerts for [silences]({{< relref "../silences/" >}}) and [alert groups]({{< relref "../alert-groups/" >}}) in [notification policies]({{< relref "../notifications/_index.md" >}}). - The alerting UI displays labels for every alert instance generated by the evaluation of that rule. - Contact points can access labels to dynamically generate notifications that contain information specific to the alert that is resulting in a notification. - Labels can be added to an [alerting rule]({{< relref "../alerting-rules/" >}}). These manually configured labels are able to use template functions and reference other labels. Labels added to an alerting rule here take precedence in the event of a collision between labels. diff --git a/docs/sources/alerting/unified-alerting/notifications/_index.md b/docs/sources/alerting/unified-alerting/notifications/_index.md index ba1cbf0b638..3a263fb8143 100644 --- a/docs/sources/alerting/unified-alerting/notifications/_index.md +++ b/docs/sources/alerting/unified-alerting/notifications/_index.md @@ -9,7 +9,7 @@ weight = 450 Notification policies determine how alerts are routed to contact points. Policies have a tree structure, where each policy can have one or more child policies. Each policy, except for the root policy, can also match specific alert labels. Each alert is evaluated by the root policy and subsequently by each child policy. If you enable the `Continue matching subsequent sibling nodes` option is enabled for a specific policy, then evaluation continues even after one or more matches. A parent policy’s configuration settings and contact point information govern the behavior of an alert that does not match any of the child policies. A root policy governs any alert that does not match a specific policy. -You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). +You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). ## Grouping @@ -33,7 +33,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. Click **Notification policies**. 1. From the **Alertmanager** dropdown, select an external Alertmanager. By default, the Grafana Alertmanager is selected. 1. In the Root policy section, click **Edit** (pen icon). -1. In **Default contact point**, update the [contact point]({{< relref "./contact-points.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy. +1. In **Default contact point**, update the [contact point]({{< relref "../contact-points.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy. 1. In **Group by**, choose labels to group alerts by. If multiple alerts are matched for this policy, then they are grouped by these labels. A notification is sent per group. If the field is empty (default), then all notifications are sent in a single group. Use a special label `...` to group alerts by all labels (which effectively disables grouping). 1. In **Timing options**, select from the following options: - **Group wait** Time to wait to buffer alerts of the same group before sending an initial notification. Default is 30 seconds. @@ -48,7 +48,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. From the **Alertmanager** dropdown, select an Alertmanager. By default, the Grafana Alertmanager is selected. 1. To add a top level specific policy, go to the **Specific routing** section and click **New specific policy**. 1. In **Matching labels** section, add one or more rules for matching alert labels. For more information, see ["How label matching works"](#how-label-matching-works). -1. In **Contact point**, add the [contact point]({{< relref "./contact-points.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies. +1. In **Contact point**, add the [contact point]({{< relref "../contact-points.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies. 1. Optionally, enable **Continue matching subsequent sibling nodes** to continue matching nested policies even after the alert matched the parent policy. When this option is enabled, you can get more than one notification. Use it to send notification to a catch-all contact point as well as to one of more specific contact points handled by nested policies. 1. Optionally, enable **Override grouping** to specify the same grouping as the root policy. If this option is not enabled, the root policy grouping is used. 1. Optionally, enable **Override general timings** to override the timing options configured in the group notification policy. diff --git a/docs/sources/alerting/unified-alerting/notifications/mute-timings.md b/docs/sources/alerting/unified-alerting/notifications/mute-timings.md index 254a9a0d26b..411f9fdb479 100644 --- a/docs/sources/alerting/unified-alerting/notifications/mute-timings.md +++ b/docs/sources/alerting/unified-alerting/notifications/mute-timings.md @@ -11,7 +11,7 @@ A mute timing is a recurring interval of time when no new notifications for a po Similar to silences, mute timings do not prevent alert rules from being evaluated, nor do they stop alert instances from being shown in the user interface. They only prevent notifications from being created. -You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). +You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). ## Mute timings vs silences From 14acf3e39fe8428a5094d2515654e72ad2f9c3b7 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 1 Mar 2022 12:20:11 -0500 Subject: [PATCH 31/34] Alerting: Fix use of > instead of >= when checking the For duration (#46011) (#46044) (cherry picked from commit 789cfc31e385182379ec2814b41e9d1a870e068d) Co-authored-by: George Robinson --- pkg/services/ngalert/state/manager_test.go | 4 ++-- pkg/services/ngalert/state/state.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 2e051eee5c7..99d6f5ee447 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -550,7 +550,7 @@ func TestProcessEvalResults(t *testing.T) { }, }, }, - expectedAnnotations: 2, + expectedAnnotations: 3, expectedStates: map[string]*state.State{ `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid_2"],["alertname","test_title"],["instance_label","test"],["label","test"]]`: { AlertRuleUID: "test_alert_rule_uid_2", @@ -576,7 +576,7 @@ func TestProcessEvalResults(t *testing.T) { Values: make(map[string]*float64), }, }, - StartsAt: evaluationTime, + StartsAt: evaluationTime.Add(20 * time.Second), EndsAt: evaluationTime.Add(30 * time.Second).Add(state.ResendDelay * 3), LastEvaluationTime: evaluationTime.Add(30 * time.Second), EvaluationDuration: evaluationDuration, diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 2947f789761..6b06eb1a453 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -64,7 +64,7 @@ func (a *State) resultAlerting(alertRule *ngModels.AlertRule, result eval.Result case eval.Alerting: a.setEndsAt(alertRule, result) case eval.Pending: - if result.EvaluatedAt.Sub(a.StartsAt) > alertRule.For { + if result.EvaluatedAt.Sub(a.StartsAt) >= alertRule.For { a.State = eval.Alerting a.StartsAt = result.EvaluatedAt a.setEndsAt(alertRule, result) From 9d0e4efd9b8a5e1ac72c732407d79d35f4aba0b5 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 1 Mar 2022 13:00:12 -0500 Subject: [PATCH 32/34] Docs: Fix typo in Forward OAuth identity for the logged-in user section (#46043) (#46048) Fixes #45938 (cherry picked from commit 843e587a05f4e80bc1e27fcbccc8e1afc5cfe3fc) Co-authored-by: Marcus Efraimsson --- ...dd-authentication-for-data-source-plugins.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md index bb0b333e998..c4a8e4e0bda 100644 --- a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md +++ b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md @@ -291,16 +291,15 @@ When configured, Grafana will pass the user's token to the plugin in an Authoriz ```go func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - for _, q := range req.Queries { - token := strings.Fields(q.Headers.Get("Authorization")) + token := strings.Fields(req.Headers["Authorization"]) + var ( + tokenType = token[0] + accessToken = token[1] + ) - var ( - tokenType = token[0] - accessToken = token[1] - ) - - // ... - } + for _, q := range req.Queries { + // ... + } } ``` From 659ce4bcad03d2d6a8376e84addb0e264d3fffb8 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 2 Mar 2022 00:41:03 -0500 Subject: [PATCH 33/34] Graph (old): use timeField.config.interval to apply null insertion logic (#46069) (#46070) (cherry picked from commit fa99143eee67dab19289b039e881d52a296ce31e) Co-authored-by: Leon Sorokin --- public/app/plugins/panel/graph/data_processor.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index bc5b7a7aae0..37c19b9423e 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -12,6 +12,7 @@ import { } from '@grafana/data'; import TimeSeries from 'app/core/time_series2'; import config from 'app/core/config'; +import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; type Options = { dataList: DataFrame[]; @@ -30,13 +31,15 @@ export class DataProcessor { } for (let i = 0; i < dataList.length; i++) { - const series = dataList[i]; + let series = dataList[i]; const { timeField } = getTimeField(series); if (!timeField) { continue; } + series = applyNullInsertThreshold(series, timeField.name); + for (let j = 0; j < series.fields.length; j++) { const field = series.fields[j]; From 5ad4d91de3c55820d6c4d8c6d260a7cd179845ae Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 2 Mar 2022 07:22:56 -0500 Subject: [PATCH 34/34] Alerting: Fix silence url in notifications (#46031) (#46090) * Update silence url generation * Update tests * Update test to the new silence params format * Fix tests (cherry picked from commit aeec08706584e24bfc7ad6776b6a5ecfc8968f29) Co-authored-by: Konrad Lalik --- .../channels/default_template_test.go | 16 ++++----- .../notifier/channels/dingding_test.go | 2 +- .../ngalert/notifier/channels/discord_test.go | 4 +-- .../ngalert/notifier/channels/email_test.go | 2 +- .../notifier/channels/googlechat_test.go | 4 +-- .../ngalert/notifier/channels/kafka_test.go | 4 +-- .../ngalert/notifier/channels/line_test.go | 4 +-- .../notifier/channels/opsgenie_test.go | 8 ++--- .../notifier/channels/pagerduty_test.go | 4 +-- .../notifier/channels/pushover_test.go | 2 +- .../ngalert/notifier/channels/sensugo_test.go | 2 +- .../ngalert/notifier/channels/slack_test.go | 8 ++--- .../ngalert/notifier/channels/teams_test.go | 2 +- .../notifier/channels/telegram_test.go | 4 +-- .../notifier/channels/template_data.go | 10 +++++- .../ngalert/notifier/channels/threema_test.go | 4 +-- .../notifier/channels/victorops_test.go | 4 +-- .../ngalert/notifier/channels/webhook_test.go | 10 +++--- .../ngalert/notifier/channels/wecom_test.go | 2 +- .../alerting/api_notification_channel_test.go | 34 +++++++++---------- 20 files changed, 69 insertions(+), 61 deletions(-) diff --git a/pkg/services/ngalert/notifier/channels/default_template_test.go b/pkg/services/ngalert/notifier/channels/default_template_test.go index 4e61ac59015..e2eee8533e0 100644 --- a/pkg/services/ngalert/notifier/channels/default_template_test.go +++ b/pkg/services/ngalert/notifier/channels/default_template_test.go @@ -99,7 +99,7 @@ Labels: Annotations: - ann1 = annv1 Source: http://localhost/alert1 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1 Dashboard: http://localhost/grafana/d/dbuid123 Panel: http://localhost/grafana/d/dbuid123?viewPanel=puid123 @@ -110,7 +110,7 @@ Labels: Annotations: - ann1 = annv2 Source: http://localhost/alert2 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2 **Resolved** @@ -122,7 +122,7 @@ Labels: Annotations: - ann1 = annv3 Source: http://localhost/alert3 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval3 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval3 Dashboard: http://localhost/grafana/d/dbuid456 Panel: http://localhost/grafana/d/dbuid456?viewPanel=puid456 @@ -133,7 +133,7 @@ Labels: Annotations: - ann1 = annv4 Source: http://localhost/alert4 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval4 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval4 `, }, { @@ -150,7 +150,7 @@ Annotations: Source: http://localhost/alert1 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1 Dashboard: http://localhost/grafana/d/dbuid123 @@ -168,7 +168,7 @@ Annotations: Source: http://localhost/alert2 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2 @@ -185,7 +185,7 @@ Annotations: Source: http://localhost/alert3 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval3 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval3 Dashboard: http://localhost/grafana/d/dbuid456 @@ -203,7 +203,7 @@ Annotations: Source: http://localhost/alert4 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval4 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval4 `, diff --git a/pkg/services/ngalert/notifier/channels/dingding_test.go b/pkg/services/ngalert/notifier/channels/dingding_test.go index a868eca502e..64df838fdcf 100644 --- a/pkg/services/ngalert/notifier/channels/dingding_test.go +++ b/pkg/services/ngalert/notifier/channels/dingding_test.go @@ -44,7 +44,7 @@ func TestDingdingNotifier(t *testing.T) { "msgtype": "link", "link": map[string]interface{}{ "messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%2Falerting%2Flist", - "text": "**Firing**\n\nValue: 1234\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "text": "**Firing**\n\nValue: 1234\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "title": "[FIRING:1] (val1)", }, }, diff --git a/pkg/services/ngalert/notifier/channels/discord_test.go b/pkg/services/ngalert/notifier/channels/discord_test.go index 51a89589e64..95454a1f79e 100644 --- a/pkg/services/ngalert/notifier/channels/discord_test.go +++ b/pkg/services/ngalert/notifier/channels/discord_test.go @@ -43,7 +43,7 @@ func TestDiscordNotifier(t *testing.T) { }, }, expMsg: map[string]interface{}{ - "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "embeds": []interface{}{map[string]interface{}{ "color": 1.4037554e+07, "footer": map[string]interface{}{ @@ -123,7 +123,7 @@ func TestDiscordNotifier(t *testing.T) { }, }, expMsg: map[string]interface{}{ - "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "embeds": []interface{}{map[string]interface{}{ "color": 1.4037554e+07, "footer": map[string]interface{}{ diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index 42a738ba196..9b15518978b 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -87,7 +87,7 @@ func TestEmailNotifier(t *testing.T) { Labels: template.KV{"alertname": "AlwaysFiring", "severity": "warning"}, Annotations: template.KV{"runbook_url": "http://fix.me"}, Fingerprint: "15a37193dce72bab", - SilenceURL: "http://localhost/base/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DAlwaysFiring%2Cseverity%3Dwarning", + SilenceURL: "http://localhost/base/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DAlwaysFiring&matcher=severity%3Dwarning", DashboardURL: "http://localhost/base/d/abc", PanelURL: "http://localhost/base/d/abc?viewPanel=5", }, diff --git a/pkg/services/ngalert/notifier/channels/googlechat_test.go b/pkg/services/ngalert/notifier/channels/googlechat_test.go index 15a70034dfe..527d77be16f 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat_test.go +++ b/pkg/services/ngalert/notifier/channels/googlechat_test.go @@ -58,7 +58,7 @@ func TestGoogleChatNotifier(t *testing.T) { Widgets: []widget{ textParagraphWidget{ Text: text{ - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, }, buttonWidget{ @@ -117,7 +117,7 @@ func TestGoogleChatNotifier(t *testing.T) { Widgets: []widget{ textParagraphWidget{ Text: text{ - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", }, }, buttonWidget{ diff --git a/pkg/services/ngalert/notifier/channels/kafka_test.go b/pkg/services/ngalert/notifier/channels/kafka_test.go index 9e30bfebdd1..34bf944e592 100644 --- a/pkg/services/ngalert/notifier/channels/kafka_test.go +++ b/pkg/services/ngalert/notifier/channels/kafka_test.go @@ -51,7 +51,7 @@ func TestKafkaNotifier(t *testing.T) { "client": "Grafana", "client_url": "http://localhost/alerting/list", "description": "[FIRING:1] (val1)", - "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733" } } @@ -86,7 +86,7 @@ func TestKafkaNotifier(t *testing.T) { "client": "Grafana", "client_url": "http://localhost/alerting/list", "description": "[FIRING:2] ", - "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", "incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733" } } diff --git a/pkg/services/ngalert/notifier/channels/line_test.go b/pkg/services/ngalert/notifier/channels/line_test.go index 7d6a50cfd81..467e6bf3621 100644 --- a/pkg/services/ngalert/notifier/channels/line_test.go +++ b/pkg/services/ngalert/notifier/channels/line_test.go @@ -46,7 +46,7 @@ func TestLineNotifier(t *testing.T) { "Authorization": "Bearer sometoken", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, - expMsg: "message=%5BFIRING%3A1%5D++%28val1%29%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A", + expMsg: "message=%5BFIRING%3A1%5D++%28val1%29%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A", expMsgError: nil, }, { name: "Multiple alerts", @@ -68,7 +68,7 @@ func TestLineNotifier(t *testing.T) { "Authorization": "Bearer sometoken", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, - expMsg: "message=%5BFIRING%3A2%5D++%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval2%0A", + expMsg: "message=%5BFIRING%3A2%5D++%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval2%0A", expMsgError: nil, }, { name: "Token missing", diff --git a/pkg/services/ngalert/notifier/channels/opsgenie_test.go b/pkg/services/ngalert/notifier/channels/opsgenie_test.go index c45ad66ff6b..5dbb8db481a 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie_test.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie_test.go @@ -44,7 +44,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "details": { "url": "http://localhost/alerting/list" }, @@ -69,7 +69,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", "details": { "url": "http://localhost/alerting/list" }, @@ -94,7 +94,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", "details": { "alertname": "alert1", "lbl1": "val1", @@ -126,7 +126,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", "details": { "alertname": "alert1", "url": "http://localhost/alerting/list" diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go index 3703b83910f..44e1e3bffc6 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty_test.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -59,7 +59,7 @@ func TestPagerdutyNotifier(t *testing.T) { Component: "Grafana", Group: "default", CustomDetails: map[string]string{ - "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "num_firing": "1", "num_resolved": "0", "resolved": "", @@ -105,7 +105,7 @@ func TestPagerdutyNotifier(t *testing.T) { Component: "My Grafana", Group: "my_group", CustomDetails: map[string]string{ - "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", "num_firing": "2", "num_resolved": "0", "resolved": "", diff --git a/pkg/services/ngalert/notifier/channels/pushover_test.go b/pkg/services/ngalert/notifier/channels/pushover_test.go index 2b81a43079d..b9543720ff2 100644 --- a/pkg/services/ngalert/notifier/channels/pushover_test.go +++ b/pkg/services/ngalert/notifier/channels/pushover_test.go @@ -59,7 +59,7 @@ func TestPushoverNotifier(t *testing.T) { "title": "[FIRING:1] (val1)", "url": "http://localhost/alerting/list", "url_title": "Show alert rule", - "message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "html": "1", }, expMsgError: nil, diff --git a/pkg/services/ngalert/notifier/channels/sensugo_test.go b/pkg/services/ngalert/notifier/channels/sensugo_test.go index 2277f09aed5..271265c1d30 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo_test.go +++ b/pkg/services/ngalert/notifier/channels/sensugo_test.go @@ -60,7 +60,7 @@ func TestSensuGoNotifier(t *testing.T) { "ruleURL": "http://localhost/alerting/list", }, }, - "output": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "output": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "issued": timeNow().Unix(), "interval": 86400, "status": 2, diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index c30b87bf64b..62419671619 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -59,7 +59,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "[FIRING:1] (val1)", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", Fallback: "[FIRING:1] (val1)", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, @@ -94,7 +94,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "[FIRING:1] (val1)", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", Fallback: "[FIRING:1] (val1)", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, @@ -136,7 +136,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "2 firing, 0 resolved", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", Fallback: "2 firing, 0 resolved", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, @@ -184,7 +184,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "[FIRING:1] (val1)", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", Fallback: "[FIRING:1] (val1)", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, diff --git a/pkg/services/ngalert/notifier/channels/teams_test.go b/pkg/services/ngalert/notifier/channels/teams_test.go index 92bf8e15fdd..7f743f6fe91 100644 --- a/pkg/services/ngalert/notifier/channels/teams_test.go +++ b/pkg/services/ngalert/notifier/channels/teams_test.go @@ -49,7 +49,7 @@ func TestTeamsNotifier(t *testing.T) { "sections": []map[string]interface{}{ { "title": "Details", - "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, }, "potentialAction": []map[string]interface{}{ diff --git a/pkg/services/ngalert/notifier/channels/telegram_test.go b/pkg/services/ngalert/notifier/channels/telegram_test.go index 1e7d5f24a20..c3d7c9ba921 100644 --- a/pkg/services/ngalert/notifier/channels/telegram_test.go +++ b/pkg/services/ngalert/notifier/channels/telegram_test.go @@ -48,7 +48,7 @@ func TestTelegramNotifier(t *testing.T) { expMsg: map[string]string{ "chat_id": "someid", "parse_mode": "html", - "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, expMsgError: nil, }, { @@ -75,7 +75,7 @@ func TestTelegramNotifier(t *testing.T) { expMsg: map[string]string{ "chat_id": "someid", "parse_mode": "html", - "text": "__Custom Firing__\n2 Firing\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "text": "__Custom Firing__\n2 Firing\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", }, expMsgError: nil, }, { diff --git a/pkg/services/ngalert/notifier/channels/template_data.go b/pkg/services/ngalert/notifier/channels/template_data.go index 1533866b90a..10cd9c6bc47 100644 --- a/pkg/services/ngalert/notifier/channels/template_data.go +++ b/pkg/services/ngalert/notifier/channels/template_data.go @@ -99,7 +99,15 @@ func extendAlert(alert template.Alert, externalURL string, logger log.Logger) *E } sort.Strings(matchers) u.Path = path.Join(externalPath, "/alerting/silence/new") - u.RawQuery = "alertmanager=grafana&matchers=" + url.QueryEscape(strings.Join(matchers, ",")) + + query := make(url.Values) + query.Add("alertmanager", "grafana") + for _, matcher := range matchers { + query.Add("matcher", matcher) + } + + u.RawQuery = query.Encode() + extended.SilenceURL = u.String() return extended diff --git a/pkg/services/ngalert/notifier/channels/threema_test.go b/pkg/services/ngalert/notifier/channels/threema_test.go index 01e4f530411..47364e677f2 100644 --- a/pkg/services/ngalert/notifier/channels/threema_test.go +++ b/pkg/services/ngalert/notifier/channels/threema_test.go @@ -45,7 +45,7 @@ func TestThreemaNotifier(t *testing.T) { }, }, }, - expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D++%28val1%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", + expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D++%28val1%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", expMsgError: nil, }, { name: "Multiple alerts", @@ -67,7 +67,7 @@ func TestThreemaNotifier(t *testing.T) { }, }, }, - expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A2%5D++%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval2%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", + expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A2%5D++%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval2%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", expMsgError: nil, }, { name: "Invalid gateway id", diff --git a/pkg/services/ngalert/notifier/channels/victorops_test.go b/pkg/services/ngalert/notifier/channels/victorops_test.go index 2db53055964..3f11e25429b 100644 --- a/pkg/services/ngalert/notifier/channels/victorops_test.go +++ b/pkg/services/ngalert/notifier/channels/victorops_test.go @@ -47,7 +47,7 @@ func TestVictoropsNotifier(t *testing.T) { "entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", "message_type": "CRITICAL", "monitoring_tool": "Grafana v" + setting.BuildVersion, - "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, expMsgError: nil, }, { @@ -72,7 +72,7 @@ func TestVictoropsNotifier(t *testing.T) { "entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", "message_type": "CRITICAL", "monitoring_tool": "Grafana v" + setting.BuildVersion, - "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", }, expMsgError: nil, }, { diff --git a/pkg/services/ngalert/notifier/channels/webhook_test.go b/pkg/services/ngalert/notifier/channels/webhook_test.go index e043abc4757..97de78fe33c 100644 --- a/pkg/services/ngalert/notifier/channels/webhook_test.go +++ b/pkg/services/ngalert/notifier/channels/webhook_test.go @@ -68,7 +68,7 @@ func TestWebhookNotifier(t *testing.T) { Fingerprint: "fac0861a85de433a", DashboardURL: "http://localhost/d/abcd", PanelURL: "http://localhost/d/abcd?viewPanel=efgh", - SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1", + SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1", }, }, GroupLabels: template.KV{ @@ -87,7 +87,7 @@ func TestWebhookNotifier(t *testing.T) { GroupKey: "alertname", Title: "[FIRING:1] (val1)", State: "alerting", - Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", OrgID: orgID, }, expMsgError: nil, @@ -137,7 +137,7 @@ func TestWebhookNotifier(t *testing.T) { "ann1": "annv1", }, Fingerprint: "fac0861a85de433a", - SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1", + SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1", }, { Status: "firing", Labels: template.KV{ @@ -148,7 +148,7 @@ func TestWebhookNotifier(t *testing.T) { "ann1": "annv2", }, Fingerprint: "fab6861a85d5eeb5", - SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2", + SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2", }, }, GroupLabels: template.KV{ @@ -165,7 +165,7 @@ func TestWebhookNotifier(t *testing.T) { TruncatedAlerts: 1, Title: "[FIRING:2] ", State: "alerting", - Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", OrgID: orgID, }, expMsgError: nil, diff --git a/pkg/services/ngalert/notifier/channels/wecom_test.go b/pkg/services/ngalert/notifier/channels/wecom_test.go index 46676e9b656..77b045107b9 100644 --- a/pkg/services/ngalert/notifier/channels/wecom_test.go +++ b/pkg/services/ngalert/notifier/channels/wecom_test.go @@ -45,7 +45,7 @@ func TestWeComNotifier(t *testing.T) { }, expMsg: map[string]interface{}{ "markdown": map[string]interface{}{ - "content": "# [FIRING:1] (val1)\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n\n", + "content": "# [FIRING:1] (val1)\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n\n", }, "msgtype": "markdown", }, diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index e519221ca75..78794631adb 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -2090,7 +2090,7 @@ var expEmailNotifications = []*models.SendEmailCommandSync{ EndsAt: time.Time{}, GeneratorURL: "http://localhost:3000/alerting/UID_EmailAlert/edit", Fingerprint: "08c220aa26cd0cf5", - SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DEmailAlert", + SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DEmailAlert", DashboardURL: "", PanelURL: "", ValueString: "[ var='A' labels={} value=1 ]", @@ -2148,7 +2148,7 @@ var expNonEmailNotifications = map[string][]string{ { "title": "[FIRING:1] SlackAlert2 ", "title_link": "http://localhost:3000/alerting/list", - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SlackAlert2\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SlackAlert2/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DSlackAlert2\n", + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SlackAlert2\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SlackAlert2/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSlackAlert2\n", "fallback": "[FIRING:1] SlackAlert2 ", "footer": "Grafana v", "footer_icon": "https://grafana.com/assets/img/fav32.png", @@ -2181,7 +2181,7 @@ var expNonEmailNotifications = map[string][]string{ "component": "Integration Test", "group": "testgroup", "custom_details": { - "firing": "\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PagerdutyAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PagerdutyAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DPagerdutyAlert\n", + "firing": "\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PagerdutyAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PagerdutyAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DPagerdutyAlert\n", "num_firing": "1", "num_resolved": "0", "resolved": "" @@ -2201,7 +2201,7 @@ var expNonEmailNotifications = map[string][]string{ `{ "link": { "messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%3A3000%2Falerting%2Flist", - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DingDingAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DingDingAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DDingDingAlert\n", + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DingDingAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DingDingAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDingDingAlert\n", "title": "[FIRING:1] DingDingAlert " }, "msgtype": "link" @@ -2226,7 +2226,7 @@ var expNonEmailNotifications = map[string][]string{ ], "sections": [ { - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TeamsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DTeamsAlert\n", + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TeamsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTeamsAlert\n", "title": "Details" } ], @@ -2252,7 +2252,7 @@ var expNonEmailNotifications = map[string][]string{ "endsAt": "0001-01-01T00:00:00Z", "generatorURL": "http://localhost:3000/alerting/UID_WebhookAlert/edit", "fingerprint": "929467973978d053", - "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DWebhookAlert", + "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert", "dashboardURL": "", "panelURL": "" } @@ -2270,12 +2270,12 @@ var expNonEmailNotifications = map[string][]string{ "truncatedAlerts": 0, "title": "[FIRING:1] WebhookAlert ", "state": "alerting", - "message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = WebhookAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_WebhookAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DWebhookAlert\n" + "message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = WebhookAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_WebhookAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert\n" }`, }, "discord_recv/discord_test": { `{ - "content": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DiscordAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DiscordAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DDiscordAlert\n", + "content": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DiscordAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DiscordAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDiscordAlert\n", "embeds": [ { "color": 14037554, @@ -2303,7 +2303,7 @@ var expNonEmailNotifications = map[string][]string{ }, "name": "default" }, - "output": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SensuGoAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SensuGoAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DSensuGoAlert\n", + "output": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SensuGoAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SensuGoAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSensuGoAlert\n", "status": 2 }, "entity": { @@ -2316,10 +2316,10 @@ var expNonEmailNotifications = map[string][]string{ }`, }, "pushover_recv/pushover_test": { - "--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert \r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PushoverAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PushoverAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DPushoverAlert\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n", + "--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert \r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PushoverAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PushoverAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DPushoverAlert\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n", }, "telegram_recv/bot6sh027hs034h": { - "--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TelegramAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TelegramAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DTelegramAlert\n\r\n--abcd--\r\n", + "--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TelegramAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TelegramAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTelegramAlert\n\r\n--abcd--\r\n", }, "googlechat_recv/googlechat_test": { `{ @@ -2335,7 +2335,7 @@ var expNonEmailNotifications = map[string][]string{ "widgets": [ { "textParagraph": { - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = GoogleChatAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_GoogleChatAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DGoogleChatAlert\n" + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = GoogleChatAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_GoogleChatAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DGoogleChatAlert\n" } }, { @@ -2373,7 +2373,7 @@ var expNonEmailNotifications = map[string][]string{ "client": "Grafana", "client_url": "http://localhost:3000/alerting/list", "description": "[FIRING:1] KafkaAlert ", - "details": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = KafkaAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_KafkaAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DKafkaAlert\n", + "details": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = KafkaAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_KafkaAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DKafkaAlert\n", "incident_key": "35c0bdb1715f9162a20d7b2a01cb2e3a4c5b1dc663571701e3f67212b696332f" } } @@ -2381,10 +2381,10 @@ var expNonEmailNotifications = map[string][]string{ }`, }, "line_recv/line_test": { - `message=%5BFIRING%3A1%5D+LineAlert+%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_LineAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253DLineAlert%0A`, + `message=%5BFIRING%3A1%5D+LineAlert+%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_LineAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DLineAlert%0A`, }, "threema_recv/threema_test": { - `from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_ThreemaAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253DThreemaAlert%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`, + `from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_ThreemaAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DThreemaAlert%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`, }, "victorops_recv/victorops_test": { `{ @@ -2393,14 +2393,14 @@ var expNonEmailNotifications = map[string][]string{ "entity_id": "633ae988fa7074bcb51f3d1c5fef2ba1c5c4ccb45b3ecbf681f7d507b078b1ae", "message_type": "CRITICAL", "monitoring_tool": "Grafana v", - "state_message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = VictorOpsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_VictorOpsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DVictorOpsAlert\n", + "state_message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = VictorOpsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_VictorOpsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DVictorOpsAlert\n", "timestamp": %s }`, }, "opsgenie_recv/opsgenie_test": { `{ "alias": "47e92f0f6ef9fe99f3954e0d6155f8d09c4b9a038d8c3105e82c0cee4c62956e", - "description": "[FIRING:1] OpsGenieAlert \nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = OpsGenieAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_OpsGenieAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DOpsGenieAlert\n", + "description": "[FIRING:1] OpsGenieAlert \nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = OpsGenieAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_OpsGenieAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DOpsGenieAlert\n", "details": { "url": "http://localhost:3000/alerting/list" },