/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package provider import ( "bytes" "errors" "fmt" "strings" "text/template" "k8s.io/apimachinery/pkg/selection" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" ) // QueryBuilder provides functions for generating Prometheus queries. type QueryBuilder interface { BuildSelector(seriesName string, groupBy string, groupBySlice []string, queryParts []queryPart) (prom.Selector, error) } type queryBuilder struct { metricsQueryTemplate *template.Template } // NewQueryBuilder creates a QueryBuilder. func NewQueryBuilder(metricsQuery string) (QueryBuilder, error) { metricsQueryTemplate, err := template.New("metrics-query").Delims("<<", ">>").Parse(metricsQuery) if err != nil { return nil, fmt.Errorf("unable to parse metrics query template %q: %v", metricsQuery, err) } return &queryBuilder{ metricsQueryTemplate: metricsQueryTemplate, }, nil } func (n *queryBuilder) BuildSelector(seriesName string, groupBy string, groupBySlice []string, queryParts []queryPart) (prom.Selector, error) { // Convert our query parts into the types we need for our template. exprs, valuesByName, err := n.processQueryParts(queryParts) if err != nil { return "", err } args := queryTemplateArgs{ Series: seriesName, LabelMatchers: strings.Join(exprs, ","), LabelValuesByName: valuesByName, GroupBy: groupBy, GroupBySlice: groupBySlice, } selector, err := n.createSelectorFromTemplateArgs(args) if err != nil { return "", err } return selector, nil } func (n *queryBuilder) createSelectorFromTemplateArgs(args queryTemplateArgs) (prom.Selector, error) { //Turn our template arguments into a Selector. queryBuff := new(bytes.Buffer) if err := n.metricsQueryTemplate.Execute(queryBuff, args); err != nil { return "", err } if queryBuff.Len() == 0 { return "", fmt.Errorf("empty query produced by metrics query template") } return prom.Selector(queryBuff.String()), nil } func (n *queryBuilder) processQueryParts(queryParts []queryPart) ([]string, map[string][]string, error) { // We've take the approach here that if we can't perfectly map their query into a Prometheus // query that we should abandon the effort completely. // The concern is that if we don't get a perfect match on their query parameters, the query result // might contain unexpected data that would cause them to take an erroneous action based on the result. // Contains the expressions that we want to include as part of the query to Prometheus. // e.g. "namespace=my-namespace" // e.g. "some_label=some-value" var exprs []string // Contains the list of label values we're targeting, by namespace. // e.g. "some_label" => ["value-one", "value-two"] valuesByName := map[string][]string{} // Convert our query parts into template arguments. for _, qPart := range queryParts { // Be resilient against bad inputs. // We obviously can't generate label filters for these cases. fmt.Println("This is queryPart", qPart.labelName, qPart.operator, qPart.values) if qPart.labelName == "" { return nil, nil, ErrLabelNotSpecified } if !n.operatorIsSupported(qPart.operator) { return nil, nil, ErrUnsupportedOperator } matcher, err := n.selectMatcher(qPart.operator, qPart.values) if err != nil { return nil, nil, err } targetValue, err := n.selectTargetValue(qPart.operator, qPart.values) if err != nil { return nil, nil, err } expression := matcher(qPart.labelName, targetValue) exprs = append(exprs, expression) valuesByName[qPart.labelName] = qPart.values } return exprs, valuesByName, nil } func (n *queryBuilder) selectMatcher(operator selection.Operator, values []string) (func(string, string) string, error) { numValues := len(values) if numValues == 0 { switch operator { case selection.Exists: return prom.LabelNeq, nil case selection.DoesNotExist: return prom.LabelEq, nil case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn: return nil, ErrMalformedQuery } } else if numValues == 1 { switch operator { case selection.Equals, selection.DoubleEquals: return prom.LabelEq, nil case selection.NotEquals: return prom.LabelNeq, nil case selection.In, selection.Exists: return prom.LabelMatches, nil case selection.DoesNotExist, selection.NotIn: return prom.LabelNotMatches, nil } } else { // Since labels can only have one value, providing multiple // values results in a regex match, even if that's not what the user // asked for. switch operator { case selection.Equals, selection.DoubleEquals, selection.In, selection.Exists: return prom.LabelMatches, nil case selection.NotEquals, selection.DoesNotExist, selection.NotIn: return prom.LabelNotMatches, nil } } return nil, errors.New("operator not supported by query builder") } func (n *queryBuilder) selectTargetValue(operator selection.Operator, values []string) (string, error) { numValues := len(values) if numValues == 0 { switch operator { case selection.Exists, selection.DoesNotExist: // Return an empty string when values are equal to 0 // When the operator is LabelNotMatches this will select series without the label // or with the label but a value of "". // When the operator is LabelMatches this will select series with the label // whose value is NOT "". return "", nil case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn: return "", ErrMalformedQuery } } else if numValues == 1 { switch operator { case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn: // Pass the value through as-is. // It's somewhat strange to do this for both the regex and equality // operators, but if we do it this way it gives the user a little more control. // They might choose to send an "IN" request and give a list of static values // or they could send a single value that's a regex, giving them a passthrough // for their label selector. return values[0], nil case selection.Exists, selection.DoesNotExist: return "", errors.New("operator does not support values") } } else { switch operator { case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn: // Pass the value through as-is. // It's somewhat strange to do this for both the regex and equality // operators, but if we do it this way it gives the user a little more control. // They might choose to send an "IN" request and give a list of static values // or they could send a single value that's a regex, giving them a passthrough // for their label selector. return strings.Join(values, "|"), nil case selection.Exists, selection.DoesNotExist: return "", ErrQueryUnsupportedValues } } return "", errors.New("operator not supported by query builder") } func (n *queryBuilder) operatorIsSupported(operator selection.Operator) bool { return operator != selection.GreaterThan && operator != selection.LessThan }