github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/configs/userconfig/config.go (about) 1 package userconfig 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "time" 7 8 "github.com/pkg/errors" 9 10 "gopkg.in/yaml.v3" 11 12 "github.com/go-kit/log" 13 "github.com/prometheus/common/model" 14 "github.com/prometheus/prometheus/pkg/labels" 15 "github.com/prometheus/prometheus/pkg/rulefmt" 16 "github.com/prometheus/prometheus/promql/parser" 17 "github.com/prometheus/prometheus/rules" 18 19 legacy_promql "github.com/cortexproject/cortex/pkg/configs/legacy_promql" 20 util_log "github.com/cortexproject/cortex/pkg/util/log" 21 ) 22 23 // An ID is the ID of a single users's Cortex configuration. When a 24 // configuration changes, it gets a new ID. 25 type ID int 26 27 // RuleFormatVersion indicates which Prometheus rule format (v1 vs. v2) to use in parsing. 28 type RuleFormatVersion int 29 30 const ( 31 // RuleFormatV1 is the Prometheus 1.x rule format. 32 RuleFormatV1 RuleFormatVersion = iota 33 // RuleFormatV2 is the Prometheus 2.x rule format. 34 RuleFormatV2 RuleFormatVersion = iota 35 ) 36 37 // IsValid returns whether the rules format version is a valid (known) version. 38 func (v RuleFormatVersion) IsValid() bool { 39 switch v { 40 case RuleFormatV1, RuleFormatV2: 41 return true 42 default: 43 return false 44 } 45 } 46 47 // MarshalJSON implements json.Marshaler. 48 func (v RuleFormatVersion) MarshalJSON() ([]byte, error) { 49 switch v { 50 case RuleFormatV1: 51 return json.Marshal("1") 52 case RuleFormatV2: 53 return json.Marshal("2") 54 default: 55 return nil, fmt.Errorf("unknown rule format version %d", v) 56 } 57 } 58 59 // MarshalYAML implements yaml.Marshaler. 60 func (v RuleFormatVersion) MarshalYAML() (interface{}, error) { 61 switch v { 62 case RuleFormatV1: 63 return yaml.Marshal("1") 64 case RuleFormatV2: 65 return yaml.Marshal("2") 66 default: 67 return nil, fmt.Errorf("unknown rule format version %d", v) 68 } 69 } 70 71 // UnmarshalJSON implements json.Unmarshaler. 72 func (v *RuleFormatVersion) UnmarshalJSON(data []byte) error { 73 var s string 74 if err := json.Unmarshal(data, &s); err != nil { 75 return err 76 } 77 switch s { 78 case "1": 79 *v = RuleFormatV1 80 case "2": 81 *v = RuleFormatV2 82 default: 83 return fmt.Errorf("unknown rule format version %q", string(data)) 84 } 85 return nil 86 } 87 88 // UnmarshalYAML implements yaml.Unmarshaler. 89 func (v *RuleFormatVersion) UnmarshalYAML(unmarshal func(interface{}) error) error { 90 var s string 91 if err := unmarshal(&s); err != nil { 92 return err 93 } 94 switch s { 95 case "1": 96 *v = RuleFormatV1 97 case "2": 98 *v = RuleFormatV2 99 default: 100 return fmt.Errorf("unknown rule format version %q", s) 101 } 102 return nil 103 } 104 105 // A Config is a Cortex configuration for a single user. 106 type Config struct { 107 // RulesFiles maps from a rules filename to file contents. 108 RulesConfig RulesConfig 109 TemplateFiles map[string]string 110 AlertmanagerConfig string 111 } 112 113 // configCompat is a compatibility struct to support old JSON config blobs 114 // saved in the config DB that didn't have a rule format version yet and 115 // just had a top-level field for the rule files. 116 type configCompat struct { 117 RulesFiles map[string]string `json:"rules_files" yaml:"rules_files"` 118 RuleFormatVersion RuleFormatVersion `json:"rule_format_version" yaml:"rule_format_version"` 119 TemplateFiles map[string]string `json:"template_files" yaml:"template_files"` 120 AlertmanagerConfig string `json:"alertmanager_config" yaml:"alertmanager_config"` 121 } 122 123 // MarshalJSON implements json.Marshaler. 124 func (c Config) MarshalJSON() ([]byte, error) { 125 compat := &configCompat{ 126 RulesFiles: c.RulesConfig.Files, 127 RuleFormatVersion: c.RulesConfig.FormatVersion, 128 TemplateFiles: c.TemplateFiles, 129 AlertmanagerConfig: c.AlertmanagerConfig, 130 } 131 132 return json.Marshal(compat) 133 } 134 135 // MarshalYAML implements yaml.Marshaler. 136 func (c Config) MarshalYAML() (interface{}, error) { 137 compat := &configCompat{ 138 RulesFiles: c.RulesConfig.Files, 139 RuleFormatVersion: c.RulesConfig.FormatVersion, 140 TemplateFiles: c.TemplateFiles, 141 AlertmanagerConfig: c.AlertmanagerConfig, 142 } 143 144 return yaml.Marshal(compat) 145 } 146 147 // UnmarshalJSON implements json.Unmarshaler. 148 func (c *Config) UnmarshalJSON(data []byte) error { 149 compat := configCompat{} 150 if err := json.Unmarshal(data, &compat); err != nil { 151 return err 152 } 153 *c = Config{ 154 RulesConfig: RulesConfig{ 155 Files: compat.RulesFiles, 156 FormatVersion: compat.RuleFormatVersion, 157 }, 158 TemplateFiles: compat.TemplateFiles, 159 AlertmanagerConfig: compat.AlertmanagerConfig, 160 } 161 return nil 162 } 163 164 // UnmarshalYAML implements yaml.Unmarshaler. 165 func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { 166 compat := configCompat{} 167 if err := unmarshal(&compat); err != nil { 168 return errors.WithStack(err) 169 } 170 *c = Config{ 171 RulesConfig: RulesConfig{ 172 Files: compat.RulesFiles, 173 FormatVersion: compat.RuleFormatVersion, 174 }, 175 TemplateFiles: compat.TemplateFiles, 176 AlertmanagerConfig: compat.AlertmanagerConfig, 177 } 178 return nil 179 } 180 181 // View is what's returned from the Weave Cloud configs service 182 // when we ask for all Cortex configurations. 183 // 184 // The configs service is essentially a JSON blob store that gives each 185 // _version_ of a configuration a unique ID and guarantees that later versions 186 // have greater IDs. 187 type View struct { 188 ID ID `json:"id"` 189 Config Config `json:"config"` 190 DeletedAt time.Time `json:"deleted_at"` 191 } 192 193 // IsDeleted tells you if the config is deleted. 194 func (v View) IsDeleted() bool { 195 return !v.DeletedAt.IsZero() 196 } 197 198 // GetVersionedRulesConfig specializes the view to just the rules config. 199 func (v View) GetVersionedRulesConfig() *VersionedRulesConfig { 200 if v.Config.RulesConfig.Files == nil { 201 return nil 202 } 203 return &VersionedRulesConfig{ 204 ID: v.ID, 205 Config: v.Config.RulesConfig, 206 DeletedAt: v.DeletedAt, 207 } 208 } 209 210 // RulesConfig is the rules configuration for a particular organization. 211 type RulesConfig struct { 212 FormatVersion RuleFormatVersion `json:"format_version"` 213 Files map[string]string `json:"files"` 214 } 215 216 // Equal compares two RulesConfigs for equality. 217 // 218 // instance Eq RulesConfig 219 func (c RulesConfig) Equal(o RulesConfig) bool { 220 if c.FormatVersion != o.FormatVersion { 221 return false 222 } 223 if len(o.Files) != len(c.Files) { 224 return false 225 } 226 for k, v1 := range c.Files { 227 v2, ok := o.Files[k] 228 if !ok || v1 != v2 { 229 return false 230 } 231 } 232 return true 233 } 234 235 // Parse parses and validates the content of the rule files in a RulesConfig 236 // according to the passed rule format version. 237 func (c RulesConfig) Parse() (map[string][]rules.Rule, error) { 238 switch c.FormatVersion { 239 case RuleFormatV1: 240 return c.parseV1() 241 case RuleFormatV2: 242 return c.parseV2() 243 default: 244 return nil, fmt.Errorf("unknown rule format version %v", c.FormatVersion) 245 } 246 } 247 248 // ParseFormatted returns the rulefmt map of a users rules configs. It allows 249 // for rules to be mapped to disk and read by the prometheus rules manager. 250 func (c RulesConfig) ParseFormatted() (map[string]rulefmt.RuleGroups, error) { 251 switch c.FormatVersion { 252 case RuleFormatV1: 253 return c.parseV1Formatted() 254 case RuleFormatV2: 255 return c.parseV2Formatted() 256 default: 257 return nil, fmt.Errorf("unknown rule format version %v", c.FormatVersion) 258 } 259 } 260 261 // parseV2 parses and validates the content of the rule files in a RulesConfig 262 // according to the Prometheus 2.x rule format. 263 func (c RulesConfig) parseV2Formatted() (map[string]rulefmt.RuleGroups, error) { 264 ruleMap := map[string]rulefmt.RuleGroups{} 265 266 for fn, content := range c.Files { 267 rgs, errs := rulefmt.Parse([]byte(content)) 268 for _, err := range errs { // return just the first error, if any 269 return nil, err 270 } 271 ruleMap[fn] = *rgs 272 273 } 274 return ruleMap, nil 275 } 276 277 // parseV1 parses and validates the content of the rule files in a RulesConfig 278 // according to the Prometheus 1.x rule format. 279 func (c RulesConfig) parseV1Formatted() (map[string]rulefmt.RuleGroups, error) { 280 result := map[string]rulefmt.RuleGroups{} 281 for fn, content := range c.Files { 282 stmts, err := legacy_promql.ParseStmts(content) 283 if err != nil { 284 return nil, fmt.Errorf("error parsing %s: %s", fn, err) 285 } 286 287 ra := []rulefmt.RuleNode{} 288 for _, stmt := range stmts { 289 var rule rulefmt.RuleNode 290 switch r := stmt.(type) { 291 case *legacy_promql.AlertStmt: 292 _, err := parser.ParseExpr(r.Expr.String()) 293 if err != nil { 294 return nil, err 295 } 296 297 rule = rulefmt.RuleNode{ 298 Alert: yaml.Node{Value: r.Name}, 299 Expr: yaml.Node{Value: r.Expr.String()}, 300 For: model.Duration(r.Duration), 301 Labels: r.Labels.Map(), 302 Annotations: r.Annotations.Map(), 303 } 304 305 case *legacy_promql.RecordStmt: 306 _, err := parser.ParseExpr(r.Expr.String()) 307 if err != nil { 308 return nil, err 309 } 310 311 rule = rulefmt.RuleNode{ 312 Record: yaml.Node{Value: r.Name}, 313 Expr: yaml.Node{Value: r.Expr.String()}, 314 Labels: r.Labels.Map(), 315 } 316 317 default: 318 return nil, fmt.Errorf("ruler.GetRules: unknown statement type") 319 } 320 ra = append(ra, rule) 321 } 322 result[fn] = rulefmt.RuleGroups{ 323 Groups: []rulefmt.RuleGroup{ 324 { 325 Name: "rg:" + fn, 326 Rules: ra, 327 }, 328 }, 329 } 330 } 331 return result, nil 332 } 333 334 // parseV2 parses and validates the content of the rule files in a RulesConfig 335 // according to the Prometheus 2.x rule format. 336 // 337 // NOTE: On one hand, we cannot return fully-fledged lists of rules.Group 338 // here yet, as creating a rules.Group requires already 339 // passing in rules.ManagerOptions options (which in turn require a 340 // notifier, appender, etc.), which we do not want to create simply 341 // for parsing. On the other hand, we should not return barebones 342 // rulefmt.RuleGroup sets here either, as only a fully-converted rules.Rule 343 // is able to track alert states over multiple rule evaluations. The caller 344 // would otherwise have to ensure to convert the rulefmt.RuleGroup only exactly 345 // once, not for every evaluation (or risk losing alert pending states). So 346 // it's probably better to just return a set of rules.Rule here. 347 func (c RulesConfig) parseV2() (map[string][]rules.Rule, error) { 348 groups := map[string][]rules.Rule{} 349 350 for fn, content := range c.Files { 351 rgs, errs := rulefmt.Parse([]byte(content)) 352 if len(errs) > 0 { 353 return nil, fmt.Errorf("error parsing %s: %v", fn, errs[0]) 354 } 355 356 for _, rg := range rgs.Groups { 357 rls := make([]rules.Rule, 0, len(rg.Rules)) 358 for _, rl := range rg.Rules { 359 expr, err := parser.ParseExpr(rl.Expr.Value) 360 if err != nil { 361 return nil, err 362 } 363 364 if rl.Alert.Value != "" { 365 rls = append(rls, rules.NewAlertingRule( 366 rl.Alert.Value, 367 expr, 368 time.Duration(rl.For), 369 labels.FromMap(rl.Labels), 370 labels.FromMap(rl.Annotations), 371 nil, 372 "", 373 true, 374 log.With(util_log.Logger, "alert", rl.Alert.Value), 375 )) 376 continue 377 } 378 rls = append(rls, rules.NewRecordingRule( 379 rl.Record.Value, 380 expr, 381 labels.FromMap(rl.Labels), 382 )) 383 } 384 385 // Group names have to be unique in Prometheus, but only within one rules file. 386 groups[rg.Name+";"+fn] = rls 387 } 388 } 389 390 return groups, nil 391 } 392 393 // parseV1 parses and validates the content of the rule files in a RulesConfig 394 // according to the Prometheus 1.x rule format. 395 // 396 // The same comment about rule groups as on ParseV2() applies here. 397 func (c RulesConfig) parseV1() (map[string][]rules.Rule, error) { 398 result := map[string][]rules.Rule{} 399 for fn, content := range c.Files { 400 stmts, err := legacy_promql.ParseStmts(content) 401 if err != nil { 402 return nil, fmt.Errorf("error parsing %s: %s", fn, err) 403 } 404 ra := []rules.Rule{} 405 for _, stmt := range stmts { 406 var rule rules.Rule 407 408 switch r := stmt.(type) { 409 case *legacy_promql.AlertStmt: 410 // legacy_promql.ParseStmts has parsed the whole rule for us. 411 // Ideally we'd just use r.Expr and pass that to rules.NewAlertingRule, 412 // but it is of the type legacy_proql.Expr and not promql.Expr. 413 // So we convert it back to a string, and then parse it again with the 414 // upstream parser to get it into the right type. 415 expr, err := parser.ParseExpr(r.Expr.String()) 416 if err != nil { 417 return nil, err 418 } 419 420 rule = rules.NewAlertingRule( 421 r.Name, 422 expr, 423 r.Duration, 424 r.Labels, 425 r.Annotations, 426 nil, 427 "", 428 true, 429 log.With(util_log.Logger, "alert", r.Name), 430 ) 431 432 case *legacy_promql.RecordStmt: 433 expr, err := parser.ParseExpr(r.Expr.String()) 434 if err != nil { 435 return nil, err 436 } 437 438 rule = rules.NewRecordingRule(r.Name, expr, r.Labels) 439 440 default: 441 return nil, fmt.Errorf("ruler.GetRules: unknown statement type") 442 } 443 ra = append(ra, rule) 444 } 445 result[fn] = ra 446 } 447 return result, nil 448 } 449 450 // VersionedRulesConfig is a RulesConfig together with a version. 451 // `data Versioned a = Versioned { id :: ID , config :: a }` 452 type VersionedRulesConfig struct { 453 ID ID `json:"id"` 454 Config RulesConfig `json:"config"` 455 DeletedAt time.Time `json:"deleted_at"` 456 } 457 458 // IsDeleted tells you if the config is deleted. 459 func (vr VersionedRulesConfig) IsDeleted() bool { 460 return !vr.DeletedAt.IsZero() 461 }