Alerting: Webhook Improvements - Templateable Payloads (#103818)

* Template editor syntax highlighting when preview is json-like

* Add new template editor language examples, snippets, and functions

* Use updated NewTemplate function

* Add new fields to webhook notifier

- CustomPayload
- ExtraHeaders

* Documentation

* Update grafana/alerting to in-progress PR (needs updating after merge)

* Fix integration test

* Remove docs reference to .Extra template context

No longer exists, was part of a previous iteration

* make update-workspace

* Update grafana/alerting to actual merged commit
pull/104489/head
Matthew Jacobson 3 months ago committed by GitHub
parent c5760eb147
commit 9e933882ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 93
      docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md
  2. 195
      docs/sources/alerting/configure-notifications/template-notifications/reference.md
  3. 2
      go.mod
  4. 4
      go.sum
  5. 1
      go.work.sum
  6. 4
      pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go
  7. 28
      pkg/services/ngalert/api/tooling/definitions/contact_points.go
  8. 31
      pkg/services/ngalert/notifier/channels_config/available_channels.go
  9. 2
      pkg/storage/unified/apistore/go.mod
  10. 4
      pkg/storage/unified/apistore/go.sum
  11. 5
      pkg/tests/api/alerting/api_notification_channel_test.go
  12. 39
      public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts
  13. 33
      public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx
  14. 25
      public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx
  15. 14
      public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts
  16. 7
      public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts
  17. 78
      public/app/features/alerting/unified/components/receivers/editor/language.ts
  18. 8
      public/app/features/alerting/unified/components/receivers/editor/snippets.ts
  19. 23
      public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts

@ -33,6 +33,11 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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.<variable_name>`. |
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 }}
```

@ -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.

@ -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

@ -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=

@ -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=

@ -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)
}

@ -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 {

@ -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.<variable_name>`.",
Element: ElementTypeKeyValueMap,
InputType: InputTypeText,
PropertyName: "vars",
},
},
},
{
Label: "TLS",
PropertyName: "tlsConfig",

@ -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

@ -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=

@ -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": {

@ -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 }}`,
},
];

@ -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(
<TemplatePreview
payload={'[{"a":"b"}]'}
templateName="potato"
templateContent={`{{ define "potato" }}{{ . }}{{ end }}`}
payloadFormatError={null}
setPayloadFormatError={jest.fn()}
/>,
{ 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');
});

@ -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 (
<ul className={styles.viewer.container} data-testid="template-preview">
{previews.map((preview) => {
const language = isValidJson(preview.text) ? 'json' : 'plaintext';
return (
<li className={styles.viewer.box} key={preview.name}>
{singleTemplate ? null : <header className={styles.viewer.header}>{preview.name}</header>}
{singleTemplate ? null : (
<header className={styles.viewer.header}>
{preview.name}
<div className={styles.viewer.language}>{language}</div>
</header>
)}
<CodeEditor
containerStyles={styles.editorContainer}
language={'plaintext'}
language={language}
showLineNumbers={false}
showMiniMap={false}
value={preview.text}
@ -133,11 +148,17 @@ const getStyles = (theme: GrafanaTheme2) => ({
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,
}),

@ -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,
}))
);
}

@ -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<languages.CompletionList> => {
return this.getCompletionsFromDefinitions(getAlertManagerSuggestions(this.monaco));
return this.getCompletionsFromDefinitions(
getAlertManagerSuggestions(this.monaco),
getGomplateSuggestions(this.monaco)
);
};
getTemplateDataSuggestions = (wordContext: string): languages.ProviderResult<languages.CompletionList> => {

@ -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'];

@ -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');

@ -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,
},
];
}

Loading…
Cancel
Save