diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md index bf4e0cc90fe..898b2ae98b5 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md @@ -33,6 +33,11 @@ refs: destination: /docs/grafana//alerting/configure-notifications/manage-contact-points/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/ + notification-templates-namespaced-functions: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/reference/#namespaced-functions + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference/#namespaced-functions --- # Configure webhook notifications @@ -70,6 +75,7 @@ For more details on contact points, including how to test them and enable notifi | Basic Authentication Password | Password for HTTP Basic Authentication. | | Authentication Header Scheme | Scheme for the `Authorization` Request Header. Default is `Bearer`. | | Authentication Header Credentials | Credentials for the `Authorization` Request header. | +| Extra Headers | Additional HTTP headers to include in the request. | | Max Alerts | Maximum number of alerts to include in a notification. Any alerts exceeding this limit are ignored. `0` means no limit. | | TLS | TLS configuration options, including CA certificate, client certificate, and client key. | | HMAC Signature | HMAC signature configuration options. | @@ -115,18 +121,11 @@ To validate incoming webhook requests from Grafana, follow these steps: Use the following settings to include custom data within the [JSON payload](#body). Both options support using [notification templates](ref:notification-templates). -| Option | Description | -| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| Title | Sends the value as a string in the `title` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | -| Message | Sends the value as a string in the `message` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | - -{{< admonition type="note" >}} -You can customize the `title` and `message` options to include custom messages and notification data using notification templates. These fields are always sent as strings in the JSON payload. - -However, you cannot customize the webhook data structure, such as adding or changing other JSON fields and HTTP headers, or sending data in a different format like XML. - -If you need to format these fields as JSON or modify other webhook request options, consider sending webhook notifications to a proxy server that adjusts the webhook request before forwarding it to the final destination. -{{< /admonition >}} +| Option | Description | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Title | Sends the value as a string in the `title` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | +| Message | Sends the value as a string in the `message` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | +| [Custom Payload](#custom-payload) | Optionally override the default payload format with a custom template. | #### Optional notification settings @@ -134,7 +133,7 @@ If you need to format these fields as JSON or modify other webhook request optio | ------------------------ | ------------------------------------------------------------------- | | Disable resolved message | Enable this option to prevent notifications when an alert resolves. | -## JSON payload +## Default JSON payload The following example shows the payload of a webhook notification containing information about two firing alerts: @@ -252,3 +251,71 @@ The Alert object represents an alert included in the notification group, as prov | `dashboardURL` | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation. | | `panelURL` | string | A link to the panel if the alert has a Panel ID annotation. | | `imageURL` | string | URL of a screenshot of a panel assigned to the rule that created this notification. | + +## Custom Payload + +The `Custom Payload` option allows you to completely customize the webhook payload using templates. This gives you full control over the structure and content of the webhook request. + +| Option | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------- | +| Payload Template | Template string that defines the structure of the webhook payload. | +| Payload Variables | Key-value pairs that define additional variables available in the template under `.Vars.`. | + +Example of a custom payload template that includes variables: + +``` +{ + "alert_name": "{{ .CommonLabels.alertname }}", + "status": "{{ .Status }}", + "environment": "{{ .Vars.environment }}", + "custom_field": "{{ .Vars.custom_field }}" +} +``` + +{{< admonition type="note" >}} +When using Custom Payload, the Title and Message fields are ignored as the entire payload structure is determined by your template. +{{< /admonition >}} + +### JSON Template Functions + +When creating custom payloads, several template functions are available to help generate valid JSON structures. These include functions for creating dictionaries (`coll.Dict`), arrays (`coll.Slice`, `coll.Append`), and converting between JSON strings and objects (`data.ToJSON`, `data.JSON`). + +For detailed information about these and other template functions, refer to [notification template functions](ref:notification-templates-namespaced-functions). + +Example using JSON helper functions: + +``` +{{ define "webhook.custom.payload" -}} + {{ coll.Dict + "receiver" .Receiver + "status" .Status + "alerts" (tmpl.Exec "webhook.custom.simple_alerts" .Alerts | data.JSON) + "groupLabels" .GroupLabels + "commonLabels" .CommonLabels + "commonAnnotations" .CommonAnnotations + "externalURL" .ExternalURL + "version" "1" + "orgId" (index .Alerts 0).OrgID + "truncatedAlerts" .TruncatedAlerts + "groupKey" .GroupKey + "state" (tmpl.Inline "{{ if eq .Status \"resolved\" }}ok{{ else }}alerting{{ end }}" . ) + "allVariables" .Vars + "title" (tmpl.Exec "default.title" . ) + "message" (tmpl.Exec "default.message" . ) + | data.ToJSONPretty " "}} +{{- end }} + +{{- /* Embed json templates in other json templates. */ -}} +{{ define "webhook.custom.simple_alerts" -}} + {{- $alerts := coll.Slice -}} + {{- range . -}} + {{ $alerts = coll.Append (coll.Dict + "status" .Status + "labels" .Labels + "startsAt" .StartsAt + "endsAt" .EndsAt + ) $alerts}} + {{- end -}} + {{- $alerts | data.ToJSON -}} +{{- end }} +``` diff --git a/docs/sources/alerting/configure-notifications/template-notifications/reference.md b/docs/sources/alerting/configure-notifications/template-notifications/reference.md index 905ad86e264..665e15857c9 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/reference.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/reference.md @@ -56,17 +56,19 @@ This documentation lists the data available for use in notification templates. In notification templates, dot (`.`) is initialized with the following data: -| Name | Type | Description | -| ------------------- | ----------------- | ----------------------------------------------------------------------------------------------------- | -| `Receiver` | string | The name of the contact point sending the notification | -| `Status` | string | The status is `firing` if at least one alert is firing, otherwise `resolved`. | -| `Alerts` | [][Alert](#alert) | List of all firing and resolved alerts in this notification. | -| `Alerts.Firing` | [][Alert](#alert) | List of all firing alerts in this notification. | -| `Alerts.Resolved` | [][Alert](#alert) | List of all resolved alerts in this notification. | -| `GroupLabels` | [KV](#kv) | The labels that group these alerts in this notification based on the `Group by` option. | -| `CommonLabels` | [KV](#kv) | The labels common to all alerts in this notification. | -| `CommonAnnotations` | [KV](#kv) | The annotations common to all alerts in this notification. | -| `ExternalURL` | string | A link to Grafana, or the Alertmanager that sent this notification if using an external Alertmanager. | +| Name | Type | Description | +| ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- | +| `Receiver` | string | The name of the contact point sending the notification | +| `Status` | string | The status is `firing` if at least one alert is firing, otherwise `resolved`. | +| `Alerts` | [][Alert](#alert) | List of all firing and resolved alerts in this notification. | +| `Alerts.Firing` | [][Alert](#alert) | List of all firing alerts in this notification. | +| `Alerts.Resolved` | [][Alert](#alert) | List of all resolved alerts in this notification. | +| `GroupLabels` | [KV](#kv) | The labels that group these alerts in this notification based on the `Group by` option. | +| `CommonLabels` | [KV](#kv) | The labels common to all alerts in this notification. | +| `CommonAnnotations` | [KV](#kv) | The annotations common to all alerts in this notification. | +| `ExternalURL` | string | A link to Grafana, or the Alertmanager that sent this notification if using an external Alertmanager. | +| `GroupKey` | string | The key used to identify this alert group. | +| `TruncatedAlerts` | integer | The number of alerts, if any, that were truncated in the notification. Supported by Webhook and OnCall. | It's important to remember that [a single notification can group multiple alerts](ref:alert-grouping) to reduce the number of alerts you receive. `Alerts` is an array that includes all the alerts in the notification. @@ -115,6 +117,7 @@ Grafana-managed alerts include these additional properties: | `SilenceURL` | string | A link to silence the alert. | | `Values` | [KV](#kv) | The values of expressions used to evaluate the alert condition. Only relevant values are included. | | `ValueString` | string | A string that contains the labels and value of each reduced expression in the alert. | +| `OrgID` | integer | The ID of the organization that owns the alert. | This example iterates over the list of firing and resolved alerts (`.Alerts`) in the notification and prints the data for each alert: @@ -264,6 +267,176 @@ You can then use `tz` to change the timezone from UTC to local time, such as `Eu 21:01:45 CET ``` +## Namespaced Functions + +In addition to the top-level functions, the following namespaced functions are also available: + +### Collection Functions + +| Name | Arguments | Returns | Description | +| ------------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `coll.Dict` | key string, value any, ... | map | Creates a map with string keys from key/value pairs. All keys are converted to strings. If an odd number of arguments is provided, the last key will have an empty string value. | +| `coll.Slice` | ...any | []any | Creates a slice (array/list) from the provided arguments. Useful for creating lists that can be iterated over with `range`. | +| `coll.Append` | value any, list []any | []any | Creates a new list by appending a value to the end of an existing list. Does not modify the original list. | + +Example using collection functions: + +```go +{{ define "collection.example" }} +{{- /* Create a dictionary of alert metadata */ -}} +{{- $metadata := coll.Dict + "severity" "critical" + "team" "infrastructure" + "environment" "production" +-}} + +{{- /* Create a slice of affected services */ -}} +{{- $services := coll.Slice "database" "cache" "api" -}} + +{{- /* Append a new service to the list */ -}} +{{- $services = coll.Append "web" $services -}} + +{{- /* Use the collections in a template */ -}} +Affected Services: {{ range $services }}{{ . }},{{ end }} + +Alert Metadata: +{{- range $k, $v := $metadata }} + {{ $k }}: {{ $v }} +{{- end }} +{{ end }} +``` + +Output: + +``` +Affected Services: database,cache,api,web, + +Alert Metadata: + environment: production + severity: critical + team: infrastructure +``` + +### Data Functions + +| Name | Arguments | Returns | Description | +| ------------------- | ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `data.JSON` | jsonString string | any | Parses a JSON string into an object that can be manipulated in the template. Works with both JSON objects and arrays. | +| `data.ToJSON` | obj any | string | Serializes any object (maps, arrays, etc.) into a JSON string. Useful for creating webhook payloads. | +| `data.ToJSONPretty` | indent string, obj any | string | Creates an indented JSON string representation of an object. The first argument specifies the indentation string (e.g., spaces). | + +Example using data functions: + +```go +{{ define "data.example" }} +{{- /* First, let's create some alert data as a JSON string */ -}} +{{ $jsonString := `{ + "service": { + "name": "payment-api", + "environment": "production", + "thresholds": { + "error_rate": 5, + "latency_ms": 100 + } + } +}` }} + +{{- /* Parse the JSON string into an object we can work with */ -}} +{{ $config := $jsonString | data.JSON }} + +{{- /* Create a new alert payload */ -}} +{{ $payload := coll.Dict + "service" $config.service.name + "environment" $config.service.environment + "status" .Status + "errorThreshold" $config.service.thresholds.error_rate +}} + +{{- /* Output the payload in different JSON formats */ -}} +Compact JSON: {{ $payload | data.ToJSON }} + +Pretty JSON with 2-space indent: +{{ $payload | data.ToJSONPretty " " }} +{{ end }} +``` + +Output: + +``` +Compact JSON: {"environment":"production","errorThreshold":5,"service":"payment-api","status":"resolved"} + +Pretty JSON with 2-space indent: +{ + "environment": "production", + "errorThreshold": 5, + "service": "payment-api", + "status": "resolved" +} +``` + +### Template Functions + +| Name | Arguments | Returns | Description | +| ------------- | ---------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `tmpl.Exec` | name string, [context any] | string | Executes a named template and returns the result as a string. Similar to the `template` action but allows for post-processing of the result. | +| `tmpl.Inline` | template string, context any | string | Renders a string as a template. | + +```go +{{ define "template.example" -}} +{{ coll.Dict + "info" (tmpl.Exec `info` . | data.JSON) + "severity" (tmpl.Inline `{{ print "critical" | toUpper }}` . ) + | data.ToJSONPretty " "}} +{{- end }} + +{{- /* Define a sub-template */ -}} +{{ define "info" -}} +{{coll.Dict + "team" "infrastructure" + "environment" "production" | data.ToJSON }} +{{- end }} +``` + +Output: + +```json +{ + "info": { + "environment": "production", + "team": "infrastructure" + }, + "severity": "CRITICAL" +} +``` + +### Time Functions + +| Name | Arguments | Returns | Description | +| ---------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------ | +| `time.Now` | | Time | Returns the current local time as a time.Time object. Can be formatted using Go's time formatting functions. | + +Example using time functions: + +```go +{{ define "time.example" }} +{{- /* Get current time in different formats */ -}} +Current Time (UTC): {{ (time.Now).UTC.Format "2006-01-02 15:04:05 MST" }} +Current Time (Local): {{ (time.Now).Format "Monday, January 2, 2006 at 15:04:05" }} + +{{- /* Compare alert time with current time */ -}} +{{ $timeAgo := (time.Now).Sub .StartsAt }} +Alert fired: {{ $timeAgo }} ago +{{ end }} +``` + +Output: + +``` +Current Time (UTC): 2025-03-08 18:14:27 UTC +Current Time (Local): Saturday, March 8, 2025 at 14:14:27 +Alert fired: 25h49m32.78574723s ago +``` + ## Differences with annotation and label templates In the alert rule, you can also template annotations and labels to include additional information. For example, you might add a `summary` annotation that displays the query value triggering the alert. diff --git a/go.mod b/go.mod index c8bca87d601..1f1bbd67d1c 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,7 @@ require ( github.com/googleapis/go-sql-spanner v1.11.1 // @grafana/grafana-search-and-storage github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 808c51502de..be1f64f196d 100644 --- a/go.sum +++ b/go.sum @@ -1565,8 +1565,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 h1:5mOChD6fbkxeafK1iuy/iO/qKLyHeougMTtRJSgvAUM= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 h1:qT0D7AIV0GRu8JUrSJYuyzj86kqLgksKQjwD++DqyOM= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d h1:TDVZemfYeJHPyXeYCnqL7BQqsa+mpaZYth/Qm3TKaT8= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us= github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= diff --git a/go.work.sum b/go.work.sum index 22028db7e3a..c5f245e6b72 100644 --- a/go.work.sum +++ b/go.work.sum @@ -575,6 +575,7 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go index 3c1b227c3e5..0e8c319113b 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go @@ -5,7 +5,7 @@ import ( "regexp" "strings" - "github.com/prometheus/alertmanager/template" + "github.com/grafana/alerting/templates" "gopkg.in/yaml.v3" ) @@ -34,7 +34,7 @@ func (t *NotificationTemplate) Validate() error { // Validate template contents. We try to stick as close to what will actually happen when the templates are parsed // by the alertmanager as possible. - tmpl, err := template.New() + tmpl, err := templates.NewTemplate() if err != nil { return fmt.Errorf("failed to create template: %w", err) } diff --git a/pkg/services/ngalert/api/tooling/definitions/contact_points.go b/pkg/services/ngalert/api/tooling/definitions/contact_points.go index 597694e6d60..9acbe144e88 100644 --- a/pkg/services/ngalert/api/tooling/definitions/contact_points.go +++ b/pkg/services/ngalert/api/tooling/definitions/contact_points.go @@ -313,16 +313,24 @@ type WebhookIntegration struct { URL string `json:"url" yaml:"url" hcl:"url"` - HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"` - MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"` - AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"` - AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"` - User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"` - Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"` - Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` - Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` - TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tlsConfig,block"` - HMACConfig *HMACConfig `json:"hmacConfig,omitempty" yaml:"hmacConfig,omitempty" hcl:"hmacConfig,block"` + HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"` + MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"` + AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"` + AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"` + User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"` + Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"` + ExtraHeaders *map[string]string `json:"headers,omitempty" yaml:"headers,omitempty" hcl:"headers"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tlsConfig,block"` + HMACConfig *HMACConfig `json:"hmacConfig,omitempty" yaml:"hmacConfig,omitempty" hcl:"hmacConfig,block"` + + Payload *CustomPayload `json:"payload,omitempty" yaml:"payload,omitempty" hcl:"payload,block"` +} + +type CustomPayload struct { + Template *string `json:"template,omitempty" yaml:"template,omitempty" hcl:"template"` + Vars *map[string]string `json:"vars,omitempty" yaml:"vars,omitempty" hcl:"vars"` } type HMACConfig struct { diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index 6d3429a7359..5b70d7f58fa 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -952,6 +952,13 @@ func GetAvailableNotifiers() []*NotifierPlugin { PropertyName: "authorization_credentials", Secure: true, }, + { // New in 12.0. + Label: "Extra Headers", + Description: "Optionally provide extra headers to be used in the request.", + Element: ElementTypeKeyValueMap, + InputType: InputTypeText, + PropertyName: "headers", + }, { // New in 8.0. TODO: How to enforce only numbers? 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.", @@ -974,6 +981,30 @@ func GetAvailableNotifiers() []*NotifierPlugin { PropertyName: "message", Placeholder: alertingTemplates.DefaultMessageEmbed, }, + { // New in 12.0. + Label: "Custom Payload", + Description: "Optionally provide a templated payload. Overrides 'Message' and 'Title' field.", + Element: ElementTypeSubform, + PropertyName: "payload", + SubformOptions: []NotifierOption{ + { + Label: "Payload Template", + Description: "Custom payload template.", + Element: ElementTypeTextArea, + PropertyName: "template", + Placeholder: `{{ template "webhook.default.payload" . }}`, + Required: true, + }, + { + Label: "Payload Variables", + Description: "Optionally provide a variables to be used in the payload template. They will be available in the template as `.Vars.`.", + Element: ElementTypeKeyValueMap, + InputType: InputTypeText, + PropertyName: "vars", + }, + }, + }, + { Label: "TLS", PropertyName: "tlsConfig", diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 8c61381d1ae..9c644bf99cd 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -202,7 +202,7 @@ require ( github.com/googleapis/go-sql-spanner v1.11.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 // indirect + github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 // indirect github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 8e289866fa2..a6bc7f5bc4b 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -1249,8 +1249,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 h1:5mOChD6fbkxeafK1iuy/iO/qKLyHeougMTtRJSgvAUM= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 h1:qT0D7AIV0GRu8JUrSJYuyzj86kqLgksKQjwD++DqyOM= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d h1:TDVZemfYeJHPyXeYCnqL7BQqsa+mpaZYth/Qm3TKaT8= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us= github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index b3442b3ed6f..6727b533738 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -29,6 +29,7 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/infra/db" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -2536,6 +2537,7 @@ var expEmailNotifications = []*notifications.SendEmailCommandSync{ SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%3DUID_EmailAlert&orgId=1", DashboardURL: "", PanelURL: "", + OrgID: util.Pointer(int64(1)), Values: map[string]float64{"A": 1}, ValueString: "[ var='A' labels={} value=1 ]", }, @@ -2699,7 +2701,8 @@ var expNonEmailNotifications = map[string][]string{ "fingerprint": "15c59b0a380bd9f1", "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%%3DUID_WebhookAlert&orgId=1", "dashboardURL": "", - "panelURL": "" + "panelURL": "", + "orgId": 1 } ], "groupLabels": { diff --git a/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts b/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts index a53bb621b15..c82ea2f74aa 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts +++ b/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts @@ -145,4 +145,43 @@ Alert annotations: {{ len .Annotations.SortedPairs }} - RunbookURL: {{ .Annotations.runbook_url}} {{ end -}}`, }, + { + description: 'Create JSON payload for webhook contact point', + example: `{{- /* Example displaying a custom JSON payload for a webhook contact point.*/ -}} +{{- /* Edit the template name and template content as needed. */ -}} +{{- /* Variables defined in the webhook contact point can be accessed in .Vars but will not be previewable. */ -}} +{{ define "webhook.custom.payload" -}} + {{ coll.Dict + "receiver" .Receiver + "status" .Status + "alerts" (tmpl.Exec "webhook.custom.simple_alerts" .Alerts | data.JSON) + "groupLabels" .GroupLabels + "commonLabels" .CommonLabels + "commonAnnotations" .CommonAnnotations + "externalURL" .ExternalURL + "version" "1" + "orgId" (index .Alerts 0).OrgID + "truncatedAlerts" .TruncatedAlerts + "groupKey" .GroupKey + "state" (tmpl.Inline "{{ if eq .Status \\"resolved\\" }}ok{{ else }}alerting{{ end }}" . ) + "allVariables" .Vars + "title" (tmpl.Exec "default.title" . ) + "message" (tmpl.Exec "default.message" . ) + | data.ToJSONPretty " "}} +{{- end }} + +{{- /* Example showcasing embedding json templates in other json templates. */ -}} +{{ define "webhook.custom.simple_alerts" -}} + {{- $alerts := coll.Slice -}} + {{- range . -}} + {{ $alerts = coll.Append (coll.Dict + "status" .Status + "labels" .Labels + "startsAt" .StartsAt + "endsAt" .EndsAt + ) $alerts}} + {{- end -}} + {{- $alerts | data.ToJSON -}} +{{- end }}`, + }, ]; diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx index 184a787a8b0..fd38b545945 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx @@ -184,3 +184,36 @@ describe('TemplatePreview component', () => { expect(within(previewContent).getByTestId('mockeditor')).toHaveValue('This is the template result bla bla bla'); }); }); + +it('Should render preview type , if response contains valid json ', async () => { + const response: TemplatePreviewResponse = { + results: [ + { name: 'template_text', text: 'This is the template result bla bla bla' }, + { name: 'template_valid', text: '{"test":"value","test2":"value2"}' }, + { name: 'template_invalid', text: '{"test":"value","test2":"value2",}' }, + ], + }; + mockPreviewTemplateResponse(server, response); + render( + , + { wrapper: getProviderWraper() } + ); + + const previews = ui.resultItems.getAll; + await waitFor(() => { + expect(previews()).toHaveLength(3); + }); + const previewItems = previews(); + expect(within(previewItems[0]).getByRole('banner')).toHaveTextContent('template_text'); + expect(within(previewItems[0]).getByRole('banner')).toHaveTextContent('plaintext'); + expect(within(previewItems[1]).getByRole('banner')).toHaveTextContent('template_valid'); + expect(within(previewItems[1]).getByRole('banner')).toHaveTextContent('json'); + expect(within(previewItems[2]).getByRole('banner')).toHaveTextContent('template_invalid'); + expect(within(previewItems[2]).getByRole('banner')).toHaveTextContent('plaintext'); +}); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx index 6f743447ff1..d9e86c083e7 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx @@ -69,15 +69,30 @@ function PreviewResultViewer({ previews }: { previews: TemplatePreviewResult[] } // If there is only one template, we don't need to show the name const singleTemplate = previews.length === 1; + const isValidJson = (text: string) => { + try { + JSON.parse(text); + return true; + } catch { + return false; + } + }; + return (
    {previews.map((preview) => { + const language = isValidJson(preview.text) ? 'json' : 'plaintext'; return (
  • - {singleTemplate ? null :
    {preview.name}
    } + {singleTemplate ? null : ( +
    + {preview.name} +
    {language}
    +
    + )} ({ height: 'inherit', }), header: css({ + display: 'flex', + justifyContent: 'space-between', fontSize: theme.typography.bodySmall.fontSize, padding: theme.spacing(1, 2), borderBottom: `1px solid ${theme.colors.border.medium}`, backgroundColor: theme.colors.background.secondary, }), + language: css({ + marginLeft: 'auto', + fontStyle: 'italic', + }), errorText: css({ color: theme.colors.error.text, }), diff --git a/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts b/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts index 4a217a10cac..c92b4f3419c 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts @@ -1,6 +1,6 @@ import type { Monaco } from '@grafana/ui'; -import { AlertmanagerTemplateFunction } from './language'; +import { AlertmanagerTemplateFunction, GomplateFunctions } from './language'; import { SuggestionDefinition } from './suggestionDefinition'; export function getAlertManagerSuggestions(monaco: Monaco): SuggestionDefinition[] { @@ -50,3 +50,15 @@ export function getAlertManagerSuggestions(monaco: Monaco): SuggestionDefinition }, ]; } + +export function getGomplateSuggestions(monaco: Monaco): SuggestionDefinition[] { + const kind = monaco.languages.CompletionItemKind.Function; + return Object.values(GomplateFunctions).flatMap((functionList) => + functionList.map((func) => ({ + label: func.keyword, + detail: func.usage, + documentation: `${func.definition}\n\n${func.example}`, + kind, + })) + ); +} diff --git a/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts b/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts index 29439cb5a5f..e41b91db5ac 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts @@ -3,7 +3,7 @@ import type { IDisposable, IRange, Position, editor, languages } from 'monaco-ed import type { Monaco } from '@grafana/ui'; -import { getAlertManagerSuggestions } from './alertManagerSuggestions'; +import { getAlertManagerSuggestions, getGomplateSuggestions } from './alertManagerSuggestions'; import { SuggestionDefinition } from './suggestionDefinition'; import { getAlertSuggestions, @@ -72,7 +72,10 @@ export class CompletionProvider { }; getFunctionsSuggestions = (): languages.ProviderResult => { - return this.getCompletionsFromDefinitions(getAlertManagerSuggestions(this.monaco)); + return this.getCompletionsFromDefinitions( + getAlertManagerSuggestions(this.monaco), + getGomplateSuggestions(this.monaco) + ); }; getTemplateDataSuggestions = (wordContext: string): languages.ProviderResult => { diff --git a/public/app/features/alerting/unified/components/receivers/editor/language.ts b/public/app/features/alerting/unified/components/receivers/editor/language.ts index c62e9df4eaa..ec1a5d237ee 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/language.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/language.ts @@ -26,7 +26,83 @@ export enum AlertmanagerTemplateFunction { stringSlice = 'stringSlice', } -export const availableAlertManagerFunctions = Object.values(AlertmanagerTemplateFunction); +// list of available Gomplate functions in Alertmanager templates +// see https://github.com/hairyhenderson/gomplate +export const GomplateFunctions = { + coll: [ + { + keyword: 'coll.Dict', + definition: + 'Creates a map with string keys from key/value pairs. All keys are converted to strings. If an odd number of arguments is provided, the last is used as the key with an empty string value.', + usage: 'function(key string, val any, ...)', + example: `{{ coll.Dict "name" "Frank" "age" 42 | data.ToYAML }}`, + }, + { + keyword: 'coll.Slice', + definition: 'Creates a slice (like an array or list). Useful when needing to range over a bunch of variables.', + usage: 'function(in ...any)', + example: `{{ range coll.Slice "Bart" "Lisa" "Maggie" }}Hello, {{ . }}{{ end }}`, + }, + { + keyword: 'coll.Append', + definition: 'Appends a value to the end of a list. Creates a new list rather than modifying the input.', + usage: 'function(value any, list []any)', + example: `{{ coll.Slice 1 1 2 3 | append 5 }}`, + }, + ], + + data: [ + { + keyword: 'data.JSON', + definition: 'Converts a JSON string into an object. Works for JSON Objects and Arrays.', + usage: 'function(json string)', + example: `{{ ($json | data.JSON).hello }}`, + }, + { + keyword: 'data.ToJSON', + definition: 'Converts an object to a JSON document.', + usage: 'function(obj any)', + example: `{{ (\`{"foo":{"hello":"world"}}\` | json).foo | data.ToJSON }}`, + }, + { + keyword: 'data.ToJSONPretty', + definition: 'Converts an object to a pretty-printed (indented) JSON document.', + usage: 'function(indent string, obj any)', + example: `{{ \`{"hello":"world"}\` | data.JSON | data.ToJSONPretty " " }}`, + }, + ], + + tmpl: [ + { + keyword: 'tmpl.Exec', + definition: + 'Execute (render) the named template. This is equivalent to using the `template` action, except the result is returned as a string. This allows for post-processing of templates.', + usage: 'function(name string, [context any])', + example: `{{ tmpl.Exec "T1" | strings.ToUpper }}`, + }, + { + keyword: 'tmpl.Inline', + definition: + 'Render the given string as a template, just like a nested template. If the template is given a name, it can be re-used later with the `template` keyword. A context can be provided, otherwise the default gomplate context will be used.', + usage: 'function(partial string, context any)', + example: `{{ tmpl.Inline "{{print \`hello world\`}}" }}`, + }, + ], + + time: [ + { + keyword: 'time.Now', + definition: "Returns the current local time, as a time.Time. This wraps Go's time.Now.", + usage: 'function()', + example: `{{ (time.Now).UTC.Format "Day 2 of month 1 in year 2006 (timezone MST)" }}`, + }, + ], +}; + +export const availableAlertManagerFunctions = [ + ...Object.values(AlertmanagerTemplateFunction), + ...Object.keys(GomplateFunctions).map((namespace) => namespace), +]; // boolean functions const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge']; diff --git a/public/app/features/alerting/unified/components/receivers/editor/snippets.ts b/public/app/features/alerting/unified/components/receivers/editor/snippets.ts index 2230a126977..f6d29dfaa35 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/snippets.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/snippets.ts @@ -28,6 +28,14 @@ Annotations: {{ end }} `; +export const jsonSnippet = ` +{{ coll.Dict + "receiver" .Receiver + "status" .Status + "alerts" ( len .Alerts ) +| data.ToJSONPretty " " }} +`; + export const groupLabelsLoopSnippet = getKeyValueTemplate('GroupLabels.SortedPairs'); export const commonLabelsLoopSnippet = getKeyValueTemplate('CommonLabels.SortedPairs'); export const commonAnnotationsLoopSnippet = getKeyValueTemplate('CommonAnnotations.SortedPairs'); diff --git a/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts b/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts index 6dedd8f9fe3..27c42e5507e 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts @@ -7,6 +7,7 @@ import { commonAnnotationsLoopSnippet, commonLabelsLoopSnippet, groupLabelsLoopSnippet, + jsonSnippet, labelsLoopSnippet, } from './snippets'; import { SuggestionDefinition } from './suggestionDefinition'; @@ -28,6 +29,8 @@ export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] { { label: 'CommonLabels', kind, detail: '[]KeyValue' }, { label: 'CommonAnnotations', kind, detail: '[]KeyValue' }, { label: 'ExternalURL', kind, detail: 'string' }, + { label: 'GroupKey', kind, detail: 'string' }, + { label: 'TruncatedAlerts', kind, detail: 'integer' }, ]; } @@ -104,6 +107,12 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] { detail: 'string', documentation: 'String that contains labels and values of each reduced expression in the alert.', }, + { + label: { label: 'OrgID', detail: '(Alert)' }, + kind, + detail: 'integer', + documentation: 'The ID of the organization that owns the alert.', + }, ]; } @@ -169,6 +178,11 @@ export const snippets = { description: 'Renders a loop through annotations', snippet: annotationsLoopSnippet, }, + json: { + label: 'json', + description: 'Renders a JSON object', + snippet: jsonSnippet, + }, }; // Snippets @@ -176,7 +190,7 @@ export function getSnippetsSuggestions(monaco: Monaco): SuggestionDefinition[] { const snippetKind = monaco.languages.CompletionItemKind.Snippet; const snippetInsertRule = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; - const { alerts, alertDetails, groupLabels, commonLabels, commonAnnotations, labels, annotations } = snippets; + const { alerts, alertDetails, groupLabels, commonLabels, commonAnnotations, labels, annotations, json } = snippets; return [ { @@ -231,5 +245,12 @@ export function getSnippetsSuggestions(monaco: Monaco): SuggestionDefinition[] { insertText: annotations.snippet, insertTextRules: snippetInsertRule, }, + { + label: json.label, + documentation: json.description, + kind: snippetKind, + insertText: json.snippet, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.KeepWhitespace, + }, ]; }