diff --git a/go.mod b/go.mod index 888054ce48..668c5fc766 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/samber/lo v1.26.0 github.com/spf13/cobra v1.5.0 github.com/spf13/pflag v1.0.5 - github.com/tidwall/gjson v1.14.1 + github.com/tidwall/gjson v1.14.2 github.com/tidwall/sjson v1.2.4 github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 github.com/vbauerster/mpb/v7 v7.4.2 diff --git a/go.sum b/go.sum index 5a0793cd14..0c1ee8a35a 100644 --- a/go.sum +++ b/go.sum @@ -2715,8 +2715,8 @@ github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJH github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/gjson v1.7.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= -github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= diff --git a/pkg/metrics/unmarshal/unmarshal.go b/pkg/metrics/unmarshal/unmarshal.go index 9debb06963..f540f0cf71 100644 --- a/pkg/metrics/unmarshal/unmarshal.go +++ b/pkg/metrics/unmarshal/unmarshal.go @@ -1,15 +1,11 @@ package unmarshal import ( - "bytes" "encoding/json" "fmt" + "github.com/prometheus/common/model" "go.uber.org/zap" - "io" "net/http" - "reflect" - - "github.com/prometheus/common/model" ) // Struct for unmarshalling from github.com/prometheus/common/model @@ -123,23 +119,8 @@ func unmarshallPrometheusWebResponseData(data []byte) (*response, error) { } func UnmarshallPrometheusWebResponse(resp *http.Response, lg *zap.SugaredLogger) (*response, error) { - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - lg.With( - "error", err, - ).Error("failed to close response body") - } - }(resp.Body) - responseBuf := new(bytes.Buffer) - if _, err := io.Copy(responseBuf, resp.Body); err != nil { - lg.With( - "error", err, - ).Error("failed to read response body") - return nil, err - } - - val, err := unmarshallPrometheusWebResponseData(responseBuf.Bytes()) + var val *response + err := json.NewDecoder(resp.Body).Decode(&val) if err != nil { return nil, err } @@ -148,50 +129,3 @@ func UnmarshallPrometheusWebResponse(resp *http.Response, lg *zap.SugaredLogger) } return val, nil } - -func InterfaceToInterfaceSlice(input interface{}) ([]interface{}, error) { - i := reflect.ValueOf(input) - if i.Kind() != reflect.Slice { - return nil, fmt.Errorf("input is not a slice") - } - if i.IsNil() { - return nil, fmt.Errorf("input is nil") - } - output := make([]interface{}, i.Len()) - for j := 0; j < i.Len(); j++ { - output[j] = i.Index(j).Interface() - } - return output, nil -} - -func InterfaceToMap(input interface{}) (map[string]interface{}, error) { - m := reflect.ValueOf(input) - if m.Kind() != reflect.Map { - return nil, fmt.Errorf("input is not a map") - } - if m.IsNil() { - return nil, fmt.Errorf("provided interface is nil") - } - - output := make(map[string]interface{}) - for _, k := range m.MapKeys() { - output[k.String()] = m.MapIndex(k).Interface() - } - - return output, nil -} - -func InterfaceToStringSlice(input interface{}) ([]string, error) { - s := reflect.ValueOf(input) - if s.Kind() != reflect.Slice { - return nil, fmt.Errorf("Provided interface cannot be converted to a slice") - } - if s.IsNil() { - return nil, fmt.Errorf("Provided interface is nil") - } - res := make([]string, s.Len()) - for i := 0; i < s.Len(); i++ { - res[i] = fmt.Sprintf("%v", s.Index(i).Interface()) - } - return res, nil -} diff --git a/plugins/cortex/pkg/cortex/admin.go b/plugins/cortex/pkg/cortex/admin.go index abf27c4bfd..9a184c8021 100644 --- a/plugins/cortex/pkg/cortex/admin.go +++ b/plugins/cortex/pkg/cortex/admin.go @@ -451,13 +451,13 @@ func (p *Plugin) GetSeriesMetrics(ctx context.Context, request *cortexadmin.Seri mapVal, err := parseCortexSeriesMetadata(resp, lg, uniqueMetricName) if err == nil { if metricHelp, ok := mapVal["help"]; ok { - m.Description = fmt.Sprintf("%v", metricHelp) + m.Description = metricHelp.String() } if metricType, ok := mapVal["type"]; ok { - m.Type = fmt.Sprintf("%v", metricType) + m.Type = metricType.String() } - if metricUnit, ok := mapVal["unit"]; ok && metricUnit == "" { - m.Unit = fmt.Sprintf("%v", metricUnit) + if metricUnit, ok := mapVal["unit"]; ok && metricUnit.String() == "" { + m.Unit = metricUnit.String() } } } @@ -487,9 +487,6 @@ func (p *Plugin) GetMetricLabelSets(ctx context.Context, request *cortexadmin.La } labelSets := []*cortexadmin.LabelSet{} // label name -> list of label values for _, labelName := range labelNames { - if labelName == "job" || labelName == "__name__" { //useless labels - continue - } labelResp, err := getCortexLabelValues(p, ctx, request, labelName) if err != nil { return nil, err //FIXME: consider returning partial results diff --git a/plugins/cortex/pkg/cortex/prometheus_util.go b/plugins/cortex/pkg/cortex/prometheus_util.go index 11b5459b36..b548b0daa8 100644 --- a/plugins/cortex/pkg/cortex/prometheus_util.go +++ b/plugins/cortex/pkg/cortex/prometheus_util.go @@ -3,8 +3,8 @@ package cortex import ( "context" "fmt" - "github.com/rancher/opni/pkg/metrics/unmarshal" "github.com/rancher/opni/plugins/cortex/pkg/apis/cortexadmin" + "github.com/tidwall/gjson" "go.uber.org/zap" "io" "net/http" @@ -68,25 +68,17 @@ func enumerateCortexSeries(p *Plugin, lg *zap.SugaredLogger, ctx context.Context } func parseCortexEnumerateSeries(resp *http.Response, lg *zap.SugaredLogger) (set map[string]struct{}, err error) { - val, err := unmarshal.UnmarshallPrometheusWebResponse(resp, lg) - if err != nil { - return nil, err + b, err := io.ReadAll(resp.Body) + if !gjson.Valid(string(b)) { + return nil, fmt.Errorf("invalid json in response") } set = make(map[string]struct{}) - // must convert to slice - interfaceSlice, err := unmarshal.InterfaceToInterfaceSlice(val.Data) - if err != nil { - return nil, err + result := gjson.Get(string(b), "data.#.__name__") + if !result.Exists() { + return nil, fmt.Errorf("Empty series response from cortex") } - // must convert each to a map[string]interface{} - for _, i := range interfaceSlice { - mapStruct, err := unmarshal.InterfaceToMap(i) - if err != nil { - continue - } - if v, ok := mapStruct["__name__"]; ok { - set[fmt.Sprintf("%v", v)] = struct{}{} - } + for _, name := range result.Array() { + set[name.String()] = struct{}{} } return set, nil } @@ -102,30 +94,17 @@ func fetchCortexSeriesMetadata(p *Plugin, lg *zap.SugaredLogger, ctx context.Con return resp, err } -func parseCortexSeriesMetadata(resp *http.Response, lg *zap.SugaredLogger, metricName string) (map[string]interface{}, error) { - val, err := unmarshal.UnmarshallPrometheusWebResponse(resp, lg) - if err != nil { - return nil, err - } - if val.Data == nil { //FIXME currently cortex always returns nil here & its not clear why - return nil, fmt.Errorf("no metadata in response") +func parseCortexSeriesMetadata(resp *http.Response, lg *zap.SugaredLogger, metricName string) (map[string]gjson.Result, error) { + b, err := io.ReadAll(resp.Body) + if !gjson.Valid(string(b)) { + return nil, fmt.Errorf("invalid json in response") } - // otherwise it is a map -> list -> map[string]interface{} - valMap, err := unmarshal.InterfaceToMap(val.Data) - if err != nil { - return nil, err - } - if _, ok := valMap[metricName]; !ok { - return nil, fmt.Errorf(fmt.Sprintf("no metadata for metric %s in response", metricName)) - } - // technically each metric name can have multiple metadata if you set it up in a stupid way - bestResult, err := unmarshal.InterfaceToInterfaceSlice(valMap[metricName]) - if err != nil { - return nil, err + result := gjson.Get(string(b), fmt.Sprintf("data.%s)", metricName)) + if !result.Exists() { + return nil, fmt.Errorf("no metadata in cortex response") } - // first result is not necessarily the best result - res, err := unmarshal.InterfaceToMap(bestResult[0]) - return res, err + metadata := result.Array()[0].Map() + return metadata, err } func getCortexMetricLabels(p *Plugin, lg *zap.SugaredLogger, ctx context.Context, request *cortexadmin.LabelRequest) (*http.Response, error) { @@ -140,13 +119,17 @@ func getCortexMetricLabels(p *Plugin, lg *zap.SugaredLogger, ctx context.Context } func parseCortexMetricLabels(p *Plugin, resp *http.Response) ([]string, error) { - allLabelsStruct, err := unmarshal.UnmarshallPrometheusWebResponse(resp, p.logger) + b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } - labelNames, err := unmarshal.InterfaceToStringSlice(allLabelsStruct.Data) - if err != nil { - return nil, err + labelNames := []string{} + result := gjson.Get(string(b), "data") + for _, name := range result.Array() { + if name.String() == "__name__" || name.String() == "job" { + continue + } + labelNames = append(labelNames, name.String()) } return labelNames, nil } diff --git a/plugins/slo/pkg/slo/simple.go b/plugins/slo/pkg/slo/simple.go index fa7d0b4845..9f24a01a5e 100644 --- a/plugins/slo/pkg/slo/simple.go +++ b/plugins/slo/pkg/slo/simple.go @@ -1,12 +1,14 @@ package slo import ( + "bytes" "fmt" "github.com/google/uuid" prommodel "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/rulefmt" "gopkg.in/yaml.v3" "os" + "text/template" "time" ) @@ -15,14 +17,37 @@ const ( slo_service = "slo_opni_service" slo_name = "slo_opni_name" ratio_rate_query_name = "slo:sli_error:ratio_rate" + RecordingRuleSuffix = "-recording" + MetadataRuleSuffix = "-metadata" + AlertRuleSuffix = "-alerts" ) var ( - RecordingRuleSuffix = "-recording" - MetadataRuleSuffix = "-metadata" - AlertRuleSuffix = "-alerts" + goodQueryTpl = template.Must(template.New("").Parse(` + sum(rate({{.Metric}}\{job=\"{{.JobId }}\"{{.Labels}}\}[{{.Window}}])) + `)) + totalQueryTpl = template.Must(template.New("").Parse(` + sum(rate({{.Metric}}\{job=\"{{.JobId}}\"{{.Labels}}\}[{{"{{.Window}}"}}])) + `)) + rawSliQueryTpl = template.Must(template.New("").Parse(` + 1 - (({{.GoodQuery}})/({{.TotalQuery}})) + `)) ) +// QueryInfo used for filling query Templates +type QueryInfo struct { + Metric string + JodId string + Labels string + Window string +} + +// SliQueryInfo used for filling sli query templates +type SliQueryInfo struct { + GoodQuery string + TotalQuery string +} + type ruleGroupYAMLv2 struct { Name string `yaml:"name"` Interval prommodel.Duration `yaml:"interval,omitempty"` @@ -121,16 +146,35 @@ func (s *SLO) GetName() string { return s.idLabels[slo_name] // let it panic if not found } -func (s *SLO) RawSLIQuery(w string) string { +func (s *SLO) RawSLIQuery(w string) (string, error) { goodConstructedEvents := s.goodEvents.Construct() - simpleQueryGood := fmt.Sprintf("%s{job=\"{%s}\"%s}", s.goodMetric, s.svc, goodConstructedEvents) - aggregateQueryGood := fmt.Sprintf("sum(rate(%s[%s]))", simpleQueryGood, w) - - totalConstructedEvents := s.totalEvents.Construct() - simpleQueryTotal := fmt.Sprintf("%s{job=\"{%s}\"%s}", s.totalMetric, s.svc, totalConstructedEvents) - aggregateQueryTotal := fmt.Sprintf("sum(rate(%s[%s]))", simpleQueryTotal, w) - - return fmt.Sprintf("1 - (%s/%s)", aggregateQueryGood, aggregateQueryTotal) + var bGood bytes.Buffer + q := QueryInfo{ + Metric: string(s.goodMetric), + JodId: string(s.svc), + Labels: goodConstructedEvents, + Window: w, + } + err := goodQueryTpl.Execute(&bGood, q) + if err != nil { + return "", err + } + var bTotal bytes.Buffer + err = totalQueryTpl.Execute(&bTotal, QueryInfo{ + Metric: string(s.totalMetric), + JodId: string(s.svc), + Labels: goodConstructedEvents, + Window: w, + }) + if err != nil { + return "", err + } + var bQuery bytes.Buffer + err = rawSliQueryTpl.Execute(&bQuery, SliQueryInfo{ + GoodQuery: bGood.String(), + TotalQuery: bTotal.String(), + }) + return bQuery.String(), err } func (s *SLO) ConstructCortexRules() (queryStr string) { @@ -151,9 +195,13 @@ func (s *SLO) ConstructCortexRules() (queryStr string) { Interval: interval, } for _, w := range NewWindowRange(s.sloPeriod) { + rawSli, err := s.RawSLIQuery(w) + if err != nil { + panic(err) + } rrecording.Rules = append(rrecording.Rules, rulefmt.Rule{ Record: ratio_rate_query_name + w, - Expr: s.RawSLIQuery(w), + Expr: rawSli, Labels: MergeLabels(s.idLabels, map[string]string{ "slo_window": w, }), @@ -163,7 +211,7 @@ func (s *SLO) ConstructCortexRules() (queryStr string) { rmetadata.Rules = []rulefmt.Rule{ { Record: "slo:objective:ratio", - Expr: fmt.Sprintf("vector(0.%s)", s.objective/100), + Expr: fmt.Sprintf("vector(0.%f)", s.objective/100), }, { Record: "slo:error_budget:ratio", @@ -245,7 +293,6 @@ func (s *SLO) ConstructCortexRules() (queryStr string) { os.WriteFile(s.GetId()+"-metadata.yaml", smetadata, 0644) os.WriteFile(s.GetId()+"-alerts.yaml", salerts, 0644) } - return queryStr }