Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/configuration/config-file-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -4394,8 +4394,9 @@ query_rejection:

# Go text/template for alert generator URLs. Available variables: .ExternalURL
# (resolved external URL) and .Expression (PromQL expression). Built-in
# functions like urlquery are available. If empty, uses default Prometheus
# /graph format.
# functions like urlquery are available. A jsonEscape function is also provided
# for embedding expressions inside JSON-encoded URL parameters. If empty, uses
# default Prometheus /graph format.
[ruler_alert_generator_url_template: <string> | default = ""]

# Enable to allow rules to be evaluated with data from a single zone, if other
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/runtime-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ overrides:
tenant-a:
ruler_external_url: "http://localhost:3000"
ruler_alert_generator_url_template: >-
{{ .ExternalURL }}/explore?schemaVersion=1&panes=%7B%22default%22:%7B%22datasource%22:%22tenant-a%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22{{ urlquery .Expression }}%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1
{{ .ExternalURL }}/explore?schemaVersion=1&panes=%7B%22default%22:%7B%22datasource%22:%22tenant-a%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22{{ urlquery (jsonEscape .Expression) }}%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1

# Tenant using Perses for alert generator URLs.
# Clicking "Source" on an alert opens Perses explore view with
# the PromQL expression pre-filled and the TenantB datasource selected.
tenant-b:
ruler_external_url: http://localhost:8080
ruler_alert_generator_url_template: >-
{{ .ExternalURL }}/explore?explorer=Prometheus-PrometheusExplorer&data=%7B%22tab%22%3A%22graph%22%2C%22queries%22%3A%5B%7B%22kind%22%3A%22TimeSeriesQuery%22%2C%22spec%22%3A%7B%22plugin%22%3A%7B%22kind%22%3A%22PrometheusTimeSeriesQuery%22%2C%22spec%22%3A%7B%22datasource%22%3A%7B%22kind%22%3A%22PrometheusDatasource%22%2C%22name%22%3A%22tenantb%22%7D%2C%22query%22%3A%22{{ urlquery .Expression }}%22%7D%7D%7D%7D%5D%7D
{{ .ExternalURL }}/explore?explorer=Prometheus-PrometheusExplorer&data=%7B%22tab%22%3A%22graph%22%2C%22queries%22%3A%5B%7B%22kind%22%3A%22TimeSeriesQuery%22%2C%22spec%22%3A%7B%22plugin%22%3A%7B%22kind%22%3A%22PrometheusTimeSeriesQuery%22%2C%22spec%22%3A%7B%22datasource%22%3A%7B%22kind%22%3A%22PrometheusDatasource%22%2C%22name%22%3A%22tenantb%22%7D%2C%22query%22%3A%22{{ urlquery (jsonEscape .Expression) }}%22%7D%7D%7D%7D%5D%7D

# Tenants without overrides use the global ruler.external.url
# and the default Prometheus /graph format.
25 changes: 23 additions & 2 deletions docs/getting-started/single-binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,22 @@ The `ruler_alert_generator_url_template` field accepts a Go template with two va
- `{{ .ExternalURL }}` — the resolved external URL for this tenant (set via `ruler_external_url`)
- `{{ .Expression }}` — the PromQL expression that triggered the alert

Built-in Go template functions like `urlquery` are available for URL encoding.
Built-in Go template functions like `urlquery` are available for URL encoding. Cortex also provides a `jsonEscape` function that escapes a string for embedding inside a JSON string value (e.g., `"` → `\"`). Use `jsonEscape` when the expression is placed inside a JSON-encoded URL parameter, such as Grafana's `panes`.

Example for Grafana Explore:
Example for Grafana Explore (simple query parameter):
```yaml
ruler_external_url: "http://localhost:3000"
ruler_alert_generator_url_template: >-
{{ .ExternalURL }}/explore?expr={{ urlquery .Expression }}
```

Example for Grafana Explore (JSON-encoded `panes` parameter — use `jsonEscape` to properly escape quotes in expressions):
```yaml
ruler_external_url: "http://localhost:3000"
ruler_alert_generator_url_template: >-
{{ .ExternalURL }}/explore?schemaVersion=1&panes=%7B%22default%22:%7B%22datasource%22:%22my-datasource%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22{{ urlquery (jsonEscape .Expression) }}%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1
```

### Try It Out

1. **Load alertmanager configs** for tenant-a and tenant-b:
Expand Down Expand Up @@ -296,6 +303,13 @@ rules:
severity: critical
annotations:
summary: "Error rate exceeds 5%"
- alert: AlwaysFiringWithQuotes
expr: count(up{job!="nonexistent"} or vector(1))
for: 0m
labels:
severity: info
annotations:
summary: "Demo alert with quotes in expression"
EOF

# Alert rules for tenant-b
Expand All @@ -320,6 +334,13 @@ rules:
severity: warning
annotations:
summary: "P99 latency exceeds 2s"
- alert: AlwaysFiringWithQuotes
expr: count(up{job!="nonexistent"} or vector(1))
for: 0m
labels:
severity: info
annotations:
summary: "Demo alert with quotes in expression"
EOF
```

Expand Down
17 changes: 16 additions & 1 deletion pkg/ruler/ruler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ruler
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"hash/fnv"
Expand Down Expand Up @@ -539,6 +540,20 @@ type generatorURLTemplateData struct {
Expression string
}

// generatorURLTemplateFuncMap contains custom functions available in generator URL templates.
// - jsonEscape: escapes a string for embedding inside a JSON string value (e.g., " → \", \ → \\).
// Useful when the expression is placed inside a JSON-encoded URL parameter like Grafana's panes.
var generatorURLTemplateFuncMap = template.FuncMap{
"jsonEscape": func(s string) string {
b, err := json.Marshal(s)
if err != nil {
return s
}
// json.Marshal wraps the string in quotes; strip them to get just the escaped content.
return string(b[1 : len(b)-1])
},
}

// generatorURLTemplateCache caches a parsed text/template keyed on the template string.
// If the template string changes (e.g., via runtime config), the cache is invalidated.
type generatorURLTemplateCache struct {
Expand All @@ -552,7 +567,7 @@ func (c *generatorURLTemplateCache) getOrParse(tmplStr string) (*template.Templa
if c.tmpl != nil && c.tmplStr == tmplStr {
return c.tmpl, nil
}
tmpl, err := template.New("generator_url").Parse(tmplStr)
tmpl, err := template.New("generator_url").Funcs(generatorURLTemplateFuncMap).Parse(tmplStr)
if err != nil {
return nil, err
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/ruler/ruler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2833,12 +2833,19 @@ func TestExecuteGeneratorURLTemplate(t *testing.T) {
expectErr: true,
},
{
name: "template with multiple variables",
tmplStr: "{{ .ExternalURL }}/explore?left=%7B%22queries%22:%5B%7B%22expr%22:%22{{ urlquery .Expression }}%22%7D%5D%7D",
name: "template with JSON-encoded panes parameter",
tmplStr: "{{ .ExternalURL }}/explore?left=%7B%22queries%22:%5B%7B%22expr%22:%22{{ urlquery (jsonEscape .Expression) }}%22%7D%5D%7D",
externalURL: "http://grafana:3000",
expr: "up",
expected: "http://grafana:3000/explore?left=%7B%22queries%22:%5B%7B%22expr%22:%22up%22%7D%5D%7D",
},
{
name: "grafana explore template with expression containing double quotes",
tmplStr: `{{ .ExternalURL }}/explore?schemaVersion=1&panes=%7B%22default%22:%7B%22datasource%22:%22tenant-a%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22{{ urlquery (jsonEscape .Expression) }}%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1`,
externalURL: "http://localhost:3000",
expr: `count(up{job!="nonexistent"} or vector(1))`,
expected: `http://localhost:3000/explore?schemaVersion=1&panes=%7B%22default%22:%7B%22datasource%22:%22tenant-a%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22count%28up%7Bjob%21%3D%5C%22nonexistent%5C%22%7D+or+vector%281%29%29%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1`,
},
{
name: "javascript URI scheme is rejected",
tmplStr: "javascript://alert('xss')",
Expand Down
10 changes: 8 additions & 2 deletions pkg/util/validation/limits.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ type Limits struct {
RulerQueryOffset model.Duration `yaml:"ruler_query_offset" json:"ruler_query_offset"`
RulerExternalLabels labels.Labels `yaml:"ruler_external_labels" json:"ruler_external_labels" doc:"nocli|description=external labels for alerting rules"`
RulerExternalURL string `yaml:"ruler_external_url" json:"ruler_external_url" doc:"nocli|description=Per-tenant external URL for the ruler. If set, it overrides the global -ruler.external.url for this tenant's alert notifications."`
RulerAlertGeneratorURLTemplate string `yaml:"ruler_alert_generator_url_template" json:"ruler_alert_generator_url_template" doc:"nocli|description=Go text/template for alert generator URLs. Available variables: .ExternalURL (resolved external URL) and .Expression (PromQL expression). Built-in functions like urlquery are available. If empty, uses default Prometheus /graph format."`
RulerAlertGeneratorURLTemplate string `yaml:"ruler_alert_generator_url_template" json:"ruler_alert_generator_url_template" doc:"nocli|description=Go text/template for alert generator URLs. Available variables: .ExternalURL (resolved external URL) and .Expression (PromQL expression). Built-in functions like urlquery are available. A jsonEscape function is also provided for embedding expressions inside JSON-encoded URL parameters. If empty, uses default Prometheus /graph format."`
RulesPartialData bool `yaml:"rules_partial_data" json:"rules_partial_data" doc:"nocli|description=Enable to allow rules to be evaluated with data from a single zone, if other zones are not available.|default=false"`

// Store-gateway.
Expand Down Expand Up @@ -439,7 +439,13 @@ func (l *Limits) Validate(nameValidationScheme model.ValidationScheme, shardByAl
}

if l.RulerAlertGeneratorURLTemplate != "" {
if _, err := template.New("").Parse(l.RulerAlertGeneratorURLTemplate); err != nil {
// Register custom functions so that templates using them pass validation.
// The actual implementations are in the ruler package; these stubs just
// allow the parser to accept the function names.
funcMap := template.FuncMap{
"jsonEscape": func(s string) string { return s },
}
if _, err := template.New("").Funcs(funcMap).Parse(l.RulerAlertGeneratorURLTemplate); err != nil {
return fmt.Errorf("invalid ruler_alert_generator_url_template: %w", err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion schemas/cortex-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5522,7 +5522,7 @@
"x-format": "duration"
},
"ruler_alert_generator_url_template": {
"description": "Go text/template for alert generator URLs. Available variables: .ExternalURL (resolved external URL) and .Expression (PromQL expression). Built-in functions like urlquery are available. If empty, uses default Prometheus /graph format.",
"description": "Go text/template for alert generator URLs. Available variables: .ExternalURL (resolved external URL) and .Expression (PromQL expression). Built-in functions like urlquery are available. A jsonEscape function is also provided for embedding expressions inside JSON-encoded URL parameters. If empty, uses default Prometheus /graph format.",
"type": "string"
},
"ruler_evaluation_delay_duration": {
Expand Down
Loading