diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index 62f2ed7dc..d5e0980cf 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -17,6 +17,7 @@ package v1 import ( "context" + gojson "encoding/json" "errors" "fmt" "math" @@ -745,13 +746,13 @@ type Stat struct { func (rg *RuleGroup) UnmarshalJSON(b []byte) error { v := struct { - Name string `json:"name"` - File string `json:"file"` - Interval float64 `json:"interval"` - Rules []json.RawMessage `json:"rules"` + Name string `json:"name"` + File string `json:"file"` + Interval float64 `json:"interval"` + Rules []gojson.RawMessage `json:"rules"` }{} - if err := json.Unmarshal(b, &v); err != nil { + if err := gojson.Unmarshal(b, &v); err != nil { return err } @@ -760,102 +761,78 @@ func (rg *RuleGroup) UnmarshalJSON(b []byte) error { rg.Interval = v.Interval for _, rule := range v.Rules { - alertingRule := AlertingRule{} - if err := json.Unmarshal(rule, &alertingRule); err == nil { - rg.Rules = append(rg.Rules, alertingRule) - continue + ruleType, err := unmarshalRuleType(rule) + if err != nil { + return err } - recordingRule := RecordingRule{} - if err := json.Unmarshal(rule, &recordingRule); err == nil { + switch ruleType { + case RuleTypeAlerting: + alertingRule := AlertingRule{} + if err := alertingRule.unmarshalTypeCheckedJSON(rule); err != nil { + return err + } + rg.Rules = append(rg.Rules, alertingRule) + case RuleTypeRecording: + recordingRule := RecordingRule{} + if err := recordingRule.unmarshalTypeCheckedJSON(rule); err != nil { + return err + } rg.Rules = append(rg.Rules, recordingRule) - continue + default: + return errors.New("failed to decode JSON into an alerting or recording rule") } - return errors.New("failed to decode JSON into an alerting or recording rule") } return nil } -func (r *AlertingRule) UnmarshalJSON(b []byte) error { +func unmarshalRuleType(b []byte) (RuleType, error) { v := struct { Type string `json:"type"` }{} - if err := json.Unmarshal(b, &v); err != nil { - return err + if err := gojson.Unmarshal(b, &v); err != nil { + return RuleType(""), err } if v.Type == "" { - return errors.New("type field not present in rule") - } - if v.Type != string(RuleTypeAlerting) { - return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeAlerting), v.Type) - } - - rule := struct { - Name string `json:"name"` - Query string `json:"query"` - Duration float64 `json:"duration"` - Labels model.LabelSet `json:"labels"` - Annotations model.LabelSet `json:"annotations"` - Alerts []*Alert `json:"alerts"` - Health RuleHealth `json:"health"` - LastError string `json:"lastError,omitempty"` - EvaluationTime float64 `json:"evaluationTime"` - LastEvaluation time.Time `json:"lastEvaluation"` - State string `json:"state"` - }{} - if err := json.Unmarshal(b, &rule); err != nil { + return RuleType(""), errors.New("type field not present in rule") + } + return RuleType(v.Type), nil +} + +func (r *AlertingRule) UnmarshalJSON(b []byte) error { + ruleType, err := unmarshalRuleType(b) + if err != nil { return err } - r.Health = rule.Health - r.Annotations = rule.Annotations - r.Name = rule.Name - r.Query = rule.Query - r.Alerts = rule.Alerts - r.Duration = rule.Duration - r.Labels = rule.Labels - r.LastError = rule.LastError - r.EvaluationTime = rule.EvaluationTime - r.LastEvaluation = rule.LastEvaluation - r.State = rule.State + if ruleType != RuleTypeAlerting { + return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeAlerting), ruleType) + } + return r.unmarshalTypeCheckedJSON(b) +} - return nil +type alertingRuleInternal AlertingRule + +// unmarshalTypeCheckedJSON unmarshals json with the type field already verified to be RuleTypeAlerting +func (r *AlertingRule) unmarshalTypeCheckedJSON(b []byte) error { + return gojson.Unmarshal(b, (*alertingRuleInternal)(r)) } func (r *RecordingRule) UnmarshalJSON(b []byte) error { - v := struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(b, &v); err != nil { + ruleType, err := unmarshalRuleType(b) + if err != nil { return err } - if v.Type == "" { - return errors.New("type field not present in rule") - } - if v.Type != string(RuleTypeRecording) { - return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeRecording), v.Type) + if ruleType != RuleTypeRecording { + return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeRecording), ruleType) } + return r.unmarshalTypeCheckedJSON(b) +} - rule := struct { - Name string `json:"name"` - Query string `json:"query"` - Labels model.LabelSet `json:"labels,omitempty"` - Health RuleHealth `json:"health"` - LastError string `json:"lastError,omitempty"` - EvaluationTime float64 `json:"evaluationTime"` - LastEvaluation time.Time `json:"lastEvaluation"` - }{} - if err := json.Unmarshal(b, &rule); err != nil { - return err - } - r.Health = rule.Health - r.Labels = rule.Labels - r.Name = rule.Name - r.LastError = rule.LastError - r.Query = rule.Query - r.EvaluationTime = rule.EvaluationTime - r.LastEvaluation = rule.LastEvaluation +type recordingRuleInternal RecordingRule - return nil +// unmarshalTypeCheckedJSON unmarshals json with the type field already verified to be RuleTypeRecording +func (r *RecordingRule) unmarshalTypeCheckedJSON(b []byte) error { + return gojson.Unmarshal(b, (*recordingRuleInternal)(r)) } func (qr *queryResult) UnmarshalJSON(b []byte) error { @@ -1296,7 +1273,7 @@ func (h *httpAPI) Rules(ctx context.Context, matches []string) (RulesResult, err } var res RulesResult - err = json.Unmarshal(body, &res) + err = gojson.Unmarshal(body, &res) return res, err } diff --git a/api/prometheus/v1/api_bench_test.go b/api/prometheus/v1/api_bench_test.go index 97f502e56..1fe56fea3 100644 --- a/api/prometheus/v1/api_bench_test.go +++ b/api/prometheus/v1/api_bench_test.go @@ -13,8 +13,10 @@ package v1 import ( + "bytes" "encoding/json" "strconv" + "strings" "testing" "time" @@ -197,3 +199,75 @@ func BenchmarkSamplesJsonSerialization(b *testing.B) { }) } } + +func BenchmarkRuleGroup(b *testing.B) { + alertingRuleJSON, err := json.Marshal(struct { + Type RuleType `json:"type"` + AlertingRule `json:""` + }{ + Type: RuleTypeAlerting, + AlertingRule: AlertingRule{ + Name: "HighRequestLatency", + Query: "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5", + Duration: 600, + Labels: model.LabelSet{"severity": "page"}, + Annotations: model.LabelSet{"summary": "High request latency"}, + Alerts: []*Alert{{ + ActiveAt: time.Now().UTC(), + Annotations: model.LabelSet{"summary": "High request latency"}, + Labels: model.LabelSet{"alertname": "HighRequestLatency", "severity": "page"}, + State: AlertStateFiring, + Value: "1e+00", + }}, + Health: RuleHealthGood, + LastError: "Unknown", + EvaluationTime: 1, + LastEvaluation: time.Now().Round(time.Millisecond).UTC(), + State: "state", + }, + }) + if err != nil { + b.Fatal(err) + } + b.Log("alerting:", string(alertingRuleJSON)) + + recordingRuleJSON, err := json.Marshal(struct { + Type RuleType `json:"type"` + RecordingRule `json:""` + }{ + Type: RuleTypeRecording, + RecordingRule: RecordingRule{ + Name: "job:http_inprogress_requests:sum", + Query: "sum(http_inprogress_requests) by (job)", + Labels: model.LabelSet{"severity": "page"}, + Health: RuleHealthGood, + LastError: "Unknown", + EvaluationTime: 1, + LastEvaluation: time.Now().Round(time.Millisecond).UTC(), + }, + }) + if err != nil { + b.Fatal(err) + } + b.Log("recording:", string(recordingRuleJSON)) + + data := []byte(`{ +"name":"myname","file":"myfile","interval":0.0000005,"rules":[` + + string(alertingRuleJSON) + strings.Repeat(","+string(alertingRuleJSON), 100) + strings.Repeat(","+string(recordingRuleJSON), 100) + + `]}`) + + b.Run("streaming", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if err := json.NewDecoder(bytes.NewReader(data)).Decode(&RuleGroup{}); err != nil { + b.Fatal(err) + } + } + }) + b.Run("unmarshal", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if err := json.Unmarshal(data, &RuleGroup{}); err != nil { + b.Fatal(err) + } + } + }) +}