From cb6bbd7a47b36a8ebe8555fd635c3ec12224b283 Mon Sep 17 00:00:00 2001 From: Charlie Le Date: Fri, 27 Feb 2026 14:07:38 -0800 Subject: [PATCH 1/2] Add per-tenant Grafana Explore URL format for alert GeneratorURL Add support for tenants to configure alert GeneratorURL to use Grafana Explore format instead of the default Prometheus /graph format. This is controlled by three new per-tenant settings: ruler_alert_generator_url_format, ruler_grafana_datasource_uid, and ruler_grafana_org_id. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Charlie Le --- docs/configuration/config-file-reference.md | 16 +++ pkg/ruler/compat.go | 21 +++- pkg/ruler/external_url.go | 56 +++++++++++ pkg/ruler/external_url_test.go | 67 +++++++++++++ pkg/ruler/manager.go | 13 ++- pkg/ruler/ruler.go | 34 ++++++- pkg/ruler/ruler_test.go | 103 +++++++++++++++++--- pkg/util/validation/exporter_test.go | 1 + pkg/util/validation/limits.go | 22 ++++- schemas/cortex-config-schema.json | 17 ++++ 10 files changed, 326 insertions(+), 24 deletions(-) create mode 100644 pkg/ruler/external_url.go create mode 100644 pkg/ruler/external_url_test.go diff --git a/docs/configuration/config-file-reference.md b/docs/configuration/config-file-reference.md index 81b85fb018f..968ca55bb04 100644 --- a/docs/configuration/config-file-reference.md +++ b/docs/configuration/config-file-reference.md @@ -4329,6 +4329,22 @@ query_rejection: # external labels for alerting rules [ruler_external_labels: | default = []] +# Per-tenant external URL for the ruler. If set, it overrides the global +# -ruler.external.url for this tenant's alert notifications. +[ruler_external_url: | default = ""] + +# Format for alert generator URLs. Supported values: prometheus (default), +# grafana-explore. +[ruler_alert_generator_url_format: | default = ""] + +# Grafana datasource UID for alert generator URLs when format is +# grafana-explore. +[ruler_grafana_datasource_uid: | default = ""] + +# Grafana organization ID for alert generator URLs when format is +# grafana-explore. +[ruler_grafana_org_id: | default = 1] + # Enable to allow rules to be evaluated with data from a single zone, if other # zones are not available. [rules_partial_data: | default = false] diff --git a/pkg/ruler/compat.go b/pkg/ruler/compat.go index 0dc5c0210eb..2e471ef7f0f 100644 --- a/pkg/ruler/compat.go +++ b/pkg/ruler/compat.go @@ -19,6 +19,7 @@ import ( "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/rules" "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/util/strutil" "github.com/weaveworks/common/httpgrpc" "github.com/weaveworks/common/user" @@ -164,6 +165,10 @@ type RulesLimits interface { RulerQueryOffset(userID string) time.Duration DisabledRuleGroups(userID string) validation.DisabledRuleGroups RulerExternalLabels(userID string) labels.Labels + RulerExternalURL(userID string) string + RulerAlertGeneratorURLFormat(userID string) string + RulerGrafanaDatasourceUID(userID string) string + RulerGrafanaOrgID(userID string) int64 } type QueryExecutor func(ctx context.Context, qs string, t time.Time) (promql.Vector, error) @@ -373,7 +378,21 @@ func DefaultTenantManagerFactory(cfg Config, p Pusher, q storage.Queryable, engi QueryFunc: queryFunc, Context: prometheusContext, ExternalURL: cfg.ExternalURL.URL, - NotifyFunc: SendAlerts(notifier, cfg.ExternalURL.URL.String()), + NotifyFunc: SendAlerts(notifier, func(expr string) string { + externalURL := cfg.ExternalURL.String() + if tenantURL := overrides.RulerExternalURL(userID); tenantURL != "" { + externalURL = tenantURL + } + if overrides.RulerAlertGeneratorURLFormat(userID) == "grafana-explore" { + datasourceUID := overrides.RulerGrafanaDatasourceUID(userID) + orgID := overrides.RulerGrafanaOrgID(userID) + if orgID == 0 { + orgID = 1 + } + return grafanaExploreLink(externalURL, expr, datasourceUID, orgID) + } + return externalURL + strutil.TableLinkForExpression(expr) + }), Logger: util_log.GoKitLogToSlog(log.With(logger, "user", userID)), Registerer: reg, OutageTolerance: cfg.OutageTolerance, diff --git a/pkg/ruler/external_url.go b/pkg/ruler/external_url.go new file mode 100644 index 00000000000..0928413a889 --- /dev/null +++ b/pkg/ruler/external_url.go @@ -0,0 +1,56 @@ +package ruler + +import ( + "sync" +) + +// userExternalURL tracks per-user resolved external URLs and detects changes. +type userExternalURL struct { + global string + limits RulesLimits + + mtx sync.Mutex + users map[string]string +} + +func newUserExternalURL(global string, limits RulesLimits) *userExternalURL { + return &userExternalURL{ + global: global, + limits: limits, + + mtx: sync.Mutex{}, + users: map[string]string{}, + } +} + +func (e *userExternalURL) update(userID string) (string, bool) { + tenantURL := e.limits.RulerExternalURL(userID) + resolved := e.global + if tenantURL != "" { + resolved = tenantURL + } + + e.mtx.Lock() + defer e.mtx.Unlock() + + if prev, ok := e.users[userID]; ok && prev == resolved { + return resolved, false + } + + e.users[userID] = resolved + return resolved, true +} + +func (e *userExternalURL) remove(user string) { + e.mtx.Lock() + defer e.mtx.Unlock() + delete(e.users, user) +} + +func (e *userExternalURL) cleanup() { + e.mtx.Lock() + defer e.mtx.Unlock() + for user := range e.users { + delete(e.users, user) + } +} diff --git a/pkg/ruler/external_url_test.go b/pkg/ruler/external_url_test.go new file mode 100644 index 00000000000..50b88563e8e --- /dev/null +++ b/pkg/ruler/external_url_test.go @@ -0,0 +1,67 @@ +package ruler + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUserExternalURL(t *testing.T) { + limits := ruleLimits{} + e := newUserExternalURL("http://global:9090", &limits) + + const userID = "test-user" + + t.Run("global URL used when no per-tenant override", func(t *testing.T) { + e.remove(userID) + url, changed := e.update(userID) + require.True(t, changed) + require.Equal(t, "http://global:9090", url) + }) + + t.Run("no change on second update", func(t *testing.T) { + url, changed := e.update(userID) + require.False(t, changed) + require.Equal(t, "http://global:9090", url) + }) + + t.Run("per-tenant URL overrides global", func(t *testing.T) { + limits.mtx.Lock() + limits.externalURL = "http://tenant:3000" + limits.mtx.Unlock() + + url, changed := e.update(userID) + require.True(t, changed) + require.Equal(t, "http://tenant:3000", url) + }) + + t.Run("no change when per-tenant URL is the same", func(t *testing.T) { + url, changed := e.update(userID) + require.False(t, changed) + require.Equal(t, "http://tenant:3000", url) + }) + + t.Run("revert to global when per-tenant override removed", func(t *testing.T) { + limits.mtx.Lock() + limits.externalURL = "" + limits.mtx.Unlock() + + url, changed := e.update(userID) + require.True(t, changed) + require.Equal(t, "http://global:9090", url) + }) + + t.Run("remove and cleanup lifecycle", func(t *testing.T) { + e.remove(userID) + // After remove, next update should report changed + url, changed := e.update(userID) + require.True(t, changed) + require.Equal(t, "http://global:9090", url) + + e.cleanup() + // After cleanup, next update should report changed + url, changed = e.update(userID) + require.True(t, changed) + require.Equal(t, "http://global:9090", url) + }) +} diff --git a/pkg/ruler/manager.go b/pkg/ruler/manager.go index d44a0d95829..86611201899 100644 --- a/pkg/ruler/manager.go +++ b/pkg/ruler/manager.go @@ -53,6 +53,9 @@ type DefaultMultiTenantManager struct { // Per-user externalLabels. userExternalLabels *userExternalLabels + // Per-user externalURL. + userExternalURL *userExternalURL + // rules backup rulesBackupManager *rulesBackupManager @@ -101,6 +104,7 @@ func NewDefaultMultiTenantManager(cfg Config, limits RulesLimits, managerFactory ruleEvalMetrics: evalMetrics, notifiers: map[string]*rulerNotifier{}, userExternalLabels: newUserExternalLabels(cfg.ExternalLabels, limits), + userExternalURL: newUserExternalURL(cfg.ExternalURL.String(), limits), notifiersDiscoveryMetrics: notifiersDiscoveryMetrics, mapper: newMapper(cfg.RulePath, logger), userManagers: map[string]RulesManager{}, @@ -166,6 +170,7 @@ func (r *DefaultMultiTenantManager) SyncRuleGroups(ctx context.Context, ruleGrou r.removeNotifier(userID) r.mapper.cleanupUser(userID) r.userExternalLabels.remove(userID) + r.userExternalURL.remove(userID) r.lastReloadSuccessful.DeleteLabelValues(userID) r.lastReloadSuccessfulTimestamp.DeleteLabelValues(userID) r.configUpdatesTotal.DeleteLabelValues(userID) @@ -210,6 +215,7 @@ func (r *DefaultMultiTenantManager) syncRulesToManager(ctx context.Context, user return } externalLabels, externalLabelsUpdated := r.userExternalLabels.update(user) + externalURL, externalURLUpdated := r.userExternalURL.update(user) existing := true manager := r.getRulesManager(user, ctx) @@ -222,13 +228,13 @@ func (r *DefaultMultiTenantManager) syncRulesToManager(ctx context.Context, user return } - if !existing || rulesUpdated || externalLabelsUpdated { + if !existing || rulesUpdated || externalLabelsUpdated || externalURLUpdated { level.Debug(r.logger).Log("msg", "updating rules", "user", user) r.configUpdatesTotal.WithLabelValues(user).Inc() - if (rulesUpdated || externalLabelsUpdated) && existing { + if (rulesUpdated || externalLabelsUpdated || externalURLUpdated) && existing { r.updateRuleCache(user, manager.RuleGroups()) } - err = manager.Update(r.cfg.EvaluationInterval, files, externalLabels, r.cfg.ExternalURL.String(), r.ruleGroupIterationFunc) + err = manager.Update(r.cfg.EvaluationInterval, files, externalLabels, externalURL, r.ruleGroupIterationFunc) r.deleteRuleCache(user) if err != nil { r.lastReloadSuccessful.WithLabelValues(user).Set(0) @@ -443,6 +449,7 @@ func (r *DefaultMultiTenantManager) Stop() { // cleanup user rules directories r.mapper.cleanup() r.userExternalLabels.cleanup() + r.userExternalURL.cleanup() } func (m *DefaultMultiTenantManager) ValidateRuleGroup(g rulefmt.RuleGroup) []error { diff --git a/pkg/ruler/ruler.go b/pkg/ruler/ruler.go index 97da8166239..b6db6ec886a 100644 --- a/pkg/ruler/ruler.go +++ b/pkg/ruler/ruler.go @@ -2,6 +2,7 @@ package ruler import ( "context" + "encoding/json" "flag" "fmt" "hash/fnv" @@ -26,7 +27,6 @@ import ( "github.com/prometheus/prometheus/notifier" "github.com/prometheus/prometheus/promql/parser" promRules "github.com/prometheus/prometheus/rules" - "github.com/prometheus/prometheus/util/strutil" "github.com/weaveworks/common/user" "golang.org/x/sync/errgroup" @@ -506,7 +506,7 @@ type sender interface { // It filters any non-firing alerts from the input. // // Copied from Prometheus's main.go. -func SendAlerts(n sender, externalURL string) promRules.NotifyFunc { +func SendAlerts(n sender, generatorURLFn func(expr string) string) promRules.NotifyFunc { return func(ctx context.Context, expr string, alerts ...*promRules.Alert) { var res []*notifier.Alert @@ -515,7 +515,7 @@ func SendAlerts(n sender, externalURL string) promRules.NotifyFunc { StartsAt: alert.FiredAt, Labels: alert.Labels, Annotations: alert.Annotations, - GeneratorURL: externalURL + strutil.TableLinkForExpression(expr), + GeneratorURL: generatorURLFn(expr), } if !alert.ResolvedAt.IsZero() { a.EndsAt = alert.ResolvedAt @@ -531,6 +531,34 @@ func SendAlerts(n sender, externalURL string) promRules.NotifyFunc { } } +// grafanaExploreLink builds a Grafana Explore URL for the given expression. +func grafanaExploreLink(baseURL, expr, datasourceUID string, orgID int64) string { + panes := map[string]any{ + "default": map[string]any{ + "datasource": datasourceUID, + "queries": []map[string]any{ + { + "refId": "A", + "expr": expr, + "datasource": map[string]string{"uid": datasourceUID, "type": "prometheus"}, + "editorMode": "code", + }, + }, + "range": map[string]string{ + "from": "now-1h", + "to": "now", + }, + }, + } + panesJSON, _ := json.Marshal(panes) + + return fmt.Sprintf("%s/explore?schemaVersion=1&panes=%s&orgId=%d", + strings.TrimRight(baseURL, "/"), + url.QueryEscape(string(panesJSON)), + orgID, + ) +} + func ruleGroupDisabled(ruleGroup *rulespb.RuleGroupDesc, disabledRuleGroupsForUser validation.DisabledRuleGroups) bool { for _, disabledRuleGroupForUser := range disabledRuleGroupsForUser { if ruleGroup.Namespace == disabledRuleGroupForUser.Namespace && diff --git a/pkg/ruler/ruler_test.go b/pkg/ruler/ruler_test.go index e5738945cb4..6d7bb861920 100644 --- a/pkg/ruler/ruler_test.go +++ b/pkg/ruler/ruler_test.go @@ -35,6 +35,7 @@ import ( promRules "github.com/prometheus/prometheus/rules" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/util/annotations" + "github.com/prometheus/prometheus/util/strutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -89,14 +90,18 @@ func defaultRulerConfig(t testing.TB) Config { } type ruleLimits struct { - mtx sync.RWMutex - tenantShard float64 - maxRulesPerRuleGroup int - maxRuleGroups int - disabledRuleGroups validation.DisabledRuleGroups - maxQueryLength time.Duration - queryOffset time.Duration - externalLabels labels.Labels + mtx sync.RWMutex + tenantShard float64 + maxRulesPerRuleGroup int + maxRuleGroups int + disabledRuleGroups validation.DisabledRuleGroups + maxQueryLength time.Duration + queryOffset time.Duration + externalLabels labels.Labels + externalURL string + alertGeneratorURLFormat string + grafanaDatasourceUID string + grafanaOrgID int64 } func (r *ruleLimits) setRulerExternalLabels(lset labels.Labels) { @@ -147,6 +152,30 @@ func (r *ruleLimits) RulerExternalLabels(_ string) labels.Labels { return r.externalLabels } +func (r *ruleLimits) RulerExternalURL(_ string) string { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.externalURL +} + +func (r *ruleLimits) RulerAlertGeneratorURLFormat(_ string) string { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.alertGeneratorURLFormat +} + +func (r *ruleLimits) RulerGrafanaDatasourceUID(_ string) string { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.grafanaDatasourceUID +} + +func (r *ruleLimits) RulerGrafanaOrgID(_ string) int64 { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.grafanaOrgID +} + func newEmptyQueryable() storage.Queryable { return storage.QueryableFunc(func(mint, maxt int64) (storage.Querier, error) { return emptyQuerier{}, nil @@ -2684,10 +2713,13 @@ func (s senderFunc) Send(alerts ...*notifier.Alert) { func TestSendAlerts(t *testing.T) { testCases := []struct { - in []*promRules.Alert - exp []*notifier.Alert + name string + in []*promRules.Alert + exp []*notifier.Alert + generatorURLFn func(expr string) string }{ { + name: "prometheus format with valid until", in: []*promRules.Alert{ { Labels: labels.FromStrings("l1", "v1"), @@ -2706,8 +2738,12 @@ func TestSendAlerts(t *testing.T) { GeneratorURL: "http://localhost:9090/graph?g0.expr=up&g0.tab=1", }, }, + generatorURLFn: func(expr string) string { + return "http://localhost:9090" + strutil.TableLinkForExpression(expr) + }, }, { + name: "prometheus format with resolved at", in: []*promRules.Alert{ { Labels: labels.FromStrings("l1", "v1"), @@ -2726,21 +2762,56 @@ func TestSendAlerts(t *testing.T) { GeneratorURL: "http://localhost:9090/graph?g0.expr=up&g0.tab=1", }, }, + generatorURLFn: func(expr string) string { + return "http://localhost:9090" + strutil.TableLinkForExpression(expr) + }, }, { - in: []*promRules.Alert{}, + name: "empty alerts", + in: []*promRules.Alert{}, + generatorURLFn: func(expr string) string { + return "http://localhost:9090" + strutil.TableLinkForExpression(expr) + }, + }, + { + name: "grafana explore format", + in: []*promRules.Alert{ + { + Labels: labels.FromStrings("l1", "v1"), + Annotations: labels.FromStrings("a2", "v2"), + ActiveAt: time.Unix(1, 0), + FiredAt: time.Unix(2, 0), + ValidUntil: time.Unix(3, 0), + }, + }, + generatorURLFn: func(expr string) string { + return grafanaExploreLink("http://grafana.example.com", expr, "my-datasource", 1) + }, }, } - for i, tc := range testCases { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - senderFunc := senderFunc(func(alerts ...*notifier.Alert) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var received []*notifier.Alert + sf := senderFunc(func(alerts ...*notifier.Alert) { if len(tc.in) == 0 { t.Fatalf("sender called with 0 alert") } - require.Equal(t, tc.exp, alerts) + received = alerts + if tc.exp != nil { + require.Equal(t, tc.exp, alerts) + } }) - SendAlerts(senderFunc, "http://localhost:9090")(context.TODO(), "up", tc.in...) + SendAlerts(sf, tc.generatorURLFn)(context.TODO(), "up", tc.in...) + + // Additional checks for grafana explore format + if tc.name == "grafana explore format" { + require.Len(t, received, 1) + require.Contains(t, received[0].GeneratorURL, "/explore?schemaVersion=1&panes=") + require.Contains(t, received[0].GeneratorURL, "orgId=1") + require.Contains(t, received[0].GeneratorURL, "my-datasource") + require.Contains(t, received[0].GeneratorURL, "up") + } }) } } diff --git a/pkg/util/validation/exporter_test.go b/pkg/util/validation/exporter_test.go index 01f96b92750..5e14e37f9ef 100644 --- a/pkg/util/validation/exporter_test.go +++ b/pkg/util/validation/exporter_test.go @@ -107,6 +107,7 @@ func TestOverridesExporter_withConfig(t *testing.T) { cortex_overrides{limit_name="reject_old_samples",user="tenant-a"} 0 cortex_overrides{limit_name="reject_old_samples_max_age",user="tenant-a"} 1.2096e+06 cortex_overrides{limit_name="ruler_evaluation_delay_duration",user="tenant-a"} 0 + cortex_overrides{limit_name="ruler_grafana_org_id",user="tenant-a"} 0 cortex_overrides{limit_name="ruler_max_rule_groups_per_tenant",user="tenant-a"} 0 cortex_overrides{limit_name="ruler_max_rules_per_rule_group",user="tenant-a"} 0 cortex_overrides{limit_name="ruler_query_offset",user="tenant-a"} 0 diff --git a/pkg/util/validation/limits.go b/pkg/util/validation/limits.go index 2f14b5cab8c..3c8a464671d 100644 --- a/pkg/util/validation/limits.go +++ b/pkg/util/validation/limits.go @@ -214,7 +214,11 @@ type Limits struct { RulerMaxRuleGroupsPerTenant int `yaml:"ruler_max_rule_groups_per_tenant" json:"ruler_max_rule_groups_per_tenant"` 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"` - 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"` + 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."` + RulerAlertGeneratorURLFormat string `yaml:"ruler_alert_generator_url_format" json:"ruler_alert_generator_url_format" doc:"nocli|description=Format for alert generator URLs. Supported values: prometheus (default), grafana-explore."` + RulerGrafanaDatasourceUID string `yaml:"ruler_grafana_datasource_uid" json:"ruler_grafana_datasource_uid" doc:"nocli|description=Grafana datasource UID for alert generator URLs when format is grafana-explore."` + RulerGrafanaOrgID int64 `yaml:"ruler_grafana_org_id" json:"ruler_grafana_org_id" doc:"nocli|description=Grafana organization ID for alert generator URLs when format is grafana-explore.|default=1"` + 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. StoreGatewayTenantShardSize float64 `yaml:"store_gateway_tenant_shard_size" json:"store_gateway_tenant_shard_size"` @@ -1144,6 +1148,22 @@ func (o *Overrides) RulerExternalLabels(userID string) labels.Labels { return o.GetOverridesForUser(userID).RulerExternalLabels } +func (o *Overrides) RulerExternalURL(userID string) string { + return o.GetOverridesForUser(userID).RulerExternalURL +} + +func (o *Overrides) RulerAlertGeneratorURLFormat(userID string) string { + return o.GetOverridesForUser(userID).RulerAlertGeneratorURLFormat +} + +func (o *Overrides) RulerGrafanaDatasourceUID(userID string) string { + return o.GetOverridesForUser(userID).RulerGrafanaDatasourceUID +} + +func (o *Overrides) RulerGrafanaOrgID(userID string) int64 { + return o.GetOverridesForUser(userID).RulerGrafanaOrgID +} + // MaxRegexPatternLength returns the maximum length of an unoptimized regex pattern. // This is only used in Ingester. func (o *Overrides) MaxRegexPatternLength(userID string) int { diff --git a/schemas/cortex-config-schema.json b/schemas/cortex-config-schema.json index dfbd85f685c..82c5933f89f 100644 --- a/schemas/cortex-config-schema.json +++ b/schemas/cortex-config-schema.json @@ -5475,6 +5475,10 @@ "x-cli-flag": "validation.reject-old-samples.max-age", "x-format": "duration" }, + "ruler_alert_generator_url_format": { + "description": "Format for alert generator URLs. Supported values: prometheus (default), grafana-explore.", + "type": "string" + }, "ruler_evaluation_delay_duration": { "default": "0s", "description": "Deprecated(use ruler.query-offset instead) and will be removed in v1.19.0: Duration to delay the evaluation of rules to ensure the underlying metrics have been pushed to Cortex.", @@ -5488,6 +5492,19 @@ "description": "external labels for alerting rules", "type": "object" }, + "ruler_external_url": { + "description": "Per-tenant external URL for the ruler. If set, it overrides the global -ruler.external.url for this tenant's alert notifications.", + "type": "string" + }, + "ruler_grafana_datasource_uid": { + "description": "Grafana datasource UID for alert generator URLs when format is grafana-explore.", + "type": "string" + }, + "ruler_grafana_org_id": { + "default": 1, + "description": "Grafana organization ID for alert generator URLs when format is grafana-explore.", + "type": "number" + }, "ruler_max_rule_groups_per_tenant": { "default": 0, "description": "Maximum number of rule groups per-tenant. 0 to disable.", From 52b69c6afc0628a9c6f45ac6b7a3b70174834c94 Mon Sep 17 00:00:00 2001 From: Charlie Le Date: Fri, 27 Feb 2026 16:15:51 -0800 Subject: [PATCH 2/2] Fix gofmt alignment in ruler and validation packages Co-Authored-By: Claude Opus 4.6 Signed-off-by: Charlie Le --- pkg/ruler/compat.go | 8 ++++---- pkg/ruler/ruler_test.go | 24 ++++++++++++------------ pkg/util/validation/limits.go | 12 ++++++------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/ruler/compat.go b/pkg/ruler/compat.go index 2e471ef7f0f..69b4fa0b9eb 100644 --- a/pkg/ruler/compat.go +++ b/pkg/ruler/compat.go @@ -374,10 +374,10 @@ func DefaultTenantManagerFactory(cfg Config, p Pusher, q storage.Queryable, engi Appendable: NewPusherAppendable(p, userID, overrides, evalMetrics.TotalWritesVec.WithLabelValues(userID), evalMetrics.FailedWritesVec.WithLabelValues(userID)), - Queryable: q, - QueryFunc: queryFunc, - Context: prometheusContext, - ExternalURL: cfg.ExternalURL.URL, + Queryable: q, + QueryFunc: queryFunc, + Context: prometheusContext, + ExternalURL: cfg.ExternalURL.URL, NotifyFunc: SendAlerts(notifier, func(expr string) string { externalURL := cfg.ExternalURL.String() if tenantURL := overrides.RulerExternalURL(userID); tenantURL != "" { diff --git a/pkg/ruler/ruler_test.go b/pkg/ruler/ruler_test.go index 6d7bb861920..b725008fe1f 100644 --- a/pkg/ruler/ruler_test.go +++ b/pkg/ruler/ruler_test.go @@ -90,18 +90,18 @@ func defaultRulerConfig(t testing.TB) Config { } type ruleLimits struct { - mtx sync.RWMutex - tenantShard float64 - maxRulesPerRuleGroup int - maxRuleGroups int - disabledRuleGroups validation.DisabledRuleGroups - maxQueryLength time.Duration - queryOffset time.Duration - externalLabels labels.Labels - externalURL string - alertGeneratorURLFormat string - grafanaDatasourceUID string - grafanaOrgID int64 + mtx sync.RWMutex + tenantShard float64 + maxRulesPerRuleGroup int + maxRuleGroups int + disabledRuleGroups validation.DisabledRuleGroups + maxQueryLength time.Duration + queryOffset time.Duration + externalLabels labels.Labels + externalURL string + alertGeneratorURLFormat string + grafanaDatasourceUID string + grafanaOrgID int64 } func (r *ruleLimits) setRulerExternalLabels(lset labels.Labels) { diff --git a/pkg/util/validation/limits.go b/pkg/util/validation/limits.go index 3c8a464671d..3aaca4169f4 100644 --- a/pkg/util/validation/limits.go +++ b/pkg/util/validation/limits.go @@ -208,12 +208,12 @@ type Limits struct { QueryRejection QueryRejection `yaml:"query_rejection" json:"query_rejection" doc:"nocli|description=Configuration for query rejection."` // Ruler defaults and limits. - RulerEvaluationDelay model.Duration `yaml:"ruler_evaluation_delay_duration" json:"ruler_evaluation_delay_duration"` - RulerTenantShardSize float64 `yaml:"ruler_tenant_shard_size" json:"ruler_tenant_shard_size"` - RulerMaxRulesPerRuleGroup int `yaml:"ruler_max_rules_per_rule_group" json:"ruler_max_rules_per_rule_group"` - RulerMaxRuleGroupsPerTenant int `yaml:"ruler_max_rule_groups_per_tenant" json:"ruler_max_rule_groups_per_tenant"` - 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"` + RulerEvaluationDelay model.Duration `yaml:"ruler_evaluation_delay_duration" json:"ruler_evaluation_delay_duration"` + RulerTenantShardSize float64 `yaml:"ruler_tenant_shard_size" json:"ruler_tenant_shard_size"` + RulerMaxRulesPerRuleGroup int `yaml:"ruler_max_rules_per_rule_group" json:"ruler_max_rules_per_rule_group"` + RulerMaxRuleGroupsPerTenant int `yaml:"ruler_max_rule_groups_per_tenant" json:"ruler_max_rule_groups_per_tenant"` + 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."` RulerAlertGeneratorURLFormat string `yaml:"ruler_alert_generator_url_format" json:"ruler_alert_generator_url_format" doc:"nocli|description=Format for alert generator URLs. Supported values: prometheus (default), grafana-explore."` RulerGrafanaDatasourceUID string `yaml:"ruler_grafana_datasource_uid" json:"ruler_grafana_datasource_uid" doc:"nocli|description=Grafana datasource UID for alert generator URLs when format is grafana-explore."`