|
|
|
@ -592,8 +592,14 @@ func (ev *evaluator) eval(expr Expr) Value {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case lt == ExprVector && rt == ExprVector:
|
|
|
|
|
switch e.Op {
|
|
|
|
|
case itemLAND:
|
|
|
|
|
return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching)
|
|
|
|
|
case itemLOR:
|
|
|
|
|
return ev.vectorOr(lhs.(Vector), rhs.(Vector), e.VectorMatching)
|
|
|
|
|
default:
|
|
|
|
|
return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
case lt == ExprVector && rt == ExprScalar:
|
|
|
|
|
return ev.vectorScalarBinop(e.Op, lhs.(Vector), rhs.(*Scalar), false)
|
|
|
|
|
|
|
|
|
@ -698,109 +704,171 @@ func (ev *evaluator) matrixSelectorBounds(node *MatrixSelector) Matrix {
|
|
|
|
|
return Matrix(sampleStreams)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// vectorBinop evaluates a binary operation between two vector values.
|
|
|
|
|
func (ev *evaluator) vectorAnd(lhs, rhs Vector, matching *VectorMatching) Vector {
|
|
|
|
|
if matching.Card != CardManyToMany {
|
|
|
|
|
panic("logical operations must always be many-to-many matching")
|
|
|
|
|
}
|
|
|
|
|
// If no matching labels are specified, match by all labels.
|
|
|
|
|
sigf := signatureFunc(matching.On...)
|
|
|
|
|
|
|
|
|
|
var result Vector
|
|
|
|
|
// The set of signatures for the right-hand side vector.
|
|
|
|
|
rightSigs := map[uint64]struct{}{}
|
|
|
|
|
// Add all rhs samples to a map so we can easily find matches later.
|
|
|
|
|
for _, rs := range rhs {
|
|
|
|
|
rightSigs[sigf(rs.Metric)] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, ls := range lhs {
|
|
|
|
|
// If there's a matching entry in the right-hand side vector, add the sample.
|
|
|
|
|
if _, ok := rightSigs[sigf(ls.Metric)]; ok {
|
|
|
|
|
result = append(result, ls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ev *evaluator) vectorOr(lhs, rhs Vector, matching *VectorMatching) Vector {
|
|
|
|
|
if matching.Card != CardManyToMany {
|
|
|
|
|
panic("logical operations must always be many-to-many matching")
|
|
|
|
|
}
|
|
|
|
|
sigf := signatureFunc(matching.On...)
|
|
|
|
|
|
|
|
|
|
var result Vector
|
|
|
|
|
leftSigs := map[uint64]struct{}{}
|
|
|
|
|
// Add everything from the left-hand-side vector.
|
|
|
|
|
for _, ls := range lhs {
|
|
|
|
|
leftSigs[sigf(ls.Metric)] = struct{}{}
|
|
|
|
|
result = append(result, ls)
|
|
|
|
|
}
|
|
|
|
|
// Add all right-hand side elements which have not been added from the left-hand side.
|
|
|
|
|
for _, rs := range rhs {
|
|
|
|
|
if _, ok := leftSigs[sigf(rs.Metric)]; !ok {
|
|
|
|
|
result = append(result, rs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// vectorBinop evaluates a binary operation between two vector, excluding AND and OR.
|
|
|
|
|
func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorMatching) Vector {
|
|
|
|
|
result := make(Vector, 0, len(rhs))
|
|
|
|
|
if matching.Card == CardManyToMany {
|
|
|
|
|
panic("many-to-many only allowed for AND and OR")
|
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
result = Vector{}
|
|
|
|
|
sigf = signatureFunc(matching.On...)
|
|
|
|
|
resultLabels = append(matching.On, matching.Include...)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// The control flow below handles one-to-one or many-to-one matching.
|
|
|
|
|
// For one-to-many, swap sidedness and account for the swap when calculating
|
|
|
|
|
// values.
|
|
|
|
|
if matching.Card == CardOneToMany {
|
|
|
|
|
lhs, rhs = rhs, lhs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All samples from the rhs hashed by the matching label/values.
|
|
|
|
|
rm := map[uint64]*Sample{}
|
|
|
|
|
// Maps the hash of the label values used for matching to the hashes of the label
|
|
|
|
|
// values of the include labels (if any). It is used to keep track of already
|
|
|
|
|
// inserted samples.
|
|
|
|
|
added := map[uint64][]uint64{}
|
|
|
|
|
rightSigs := map[uint64]*Sample{}
|
|
|
|
|
|
|
|
|
|
// Add all rhs samples to a map so we can easily find matches later.
|
|
|
|
|
for _, rs := range rhs {
|
|
|
|
|
hash := hashForMetric(rs.Metric.Metric, matching.On)
|
|
|
|
|
sig := sigf(rs.Metric)
|
|
|
|
|
// The rhs is guaranteed to be the 'one' side. Having multiple samples
|
|
|
|
|
// with the same hash means that the matching is many-to-many,
|
|
|
|
|
// which is not supported.
|
|
|
|
|
if _, found := rm[hash]; matching.Card != CardManyToMany && found {
|
|
|
|
|
// with the same signature means that the matching is many-to-many.
|
|
|
|
|
if _, found := rightSigs[sig]; found {
|
|
|
|
|
// Many-to-many matching not allowed.
|
|
|
|
|
ev.errorf("many-to-many matching not allowed")
|
|
|
|
|
ev.errorf("many-to-many matching not allowed: matching labels must be unique on one side")
|
|
|
|
|
}
|
|
|
|
|
// In many-to-many matching the entry is simply overwritten. It can thus only
|
|
|
|
|
// be used to check whether any matching rhs entry exists but not retrieve them all.
|
|
|
|
|
rm[hash] = rs
|
|
|
|
|
rightSigs[sig] = rs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tracks the match-signature. For one-to-one operations the value is nil. For many-to-one
|
|
|
|
|
// the value is a set of signatures to detect duplicated result elements.
|
|
|
|
|
matchedSigs := map[uint64]map[uint64]struct{}{}
|
|
|
|
|
|
|
|
|
|
// For all lhs samples find a respective rhs sample and perform
|
|
|
|
|
// the binary operation.
|
|
|
|
|
for _, ls := range lhs {
|
|
|
|
|
hash := hashForMetric(ls.Metric.Metric, matching.On)
|
|
|
|
|
// Any lhs sample we encounter in an OR operation belongs to the result.
|
|
|
|
|
if op == itemLOR {
|
|
|
|
|
ls.Metric = resultMetric(op, ls, nil, matching)
|
|
|
|
|
result = append(result, ls)
|
|
|
|
|
added[hash] = nil // Ensure matching rhs sample is not added later.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
sig := sigf(ls.Metric)
|
|
|
|
|
|
|
|
|
|
rs, found := rm[hash] // Look for a match in the rhs vector.
|
|
|
|
|
rs, found := rightSigs[sig] // Look for a match in the rhs vector.
|
|
|
|
|
if !found {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
var value clientmodel.SampleValue
|
|
|
|
|
var keep bool
|
|
|
|
|
|
|
|
|
|
if op == itemLAND {
|
|
|
|
|
value = ls.Value
|
|
|
|
|
keep = true
|
|
|
|
|
} else {
|
|
|
|
|
if _, exists := added[hash]; matching.Card == CardOneToOne && exists {
|
|
|
|
|
// Many-to-one matching must be explicit.
|
|
|
|
|
ev.errorf("many-to-one matching must be explicit")
|
|
|
|
|
}
|
|
|
|
|
// Account for potentially swapped sidedness.
|
|
|
|
|
vl, vr := ls.Value, rs.Value
|
|
|
|
|
if matching.Card == CardOneToMany {
|
|
|
|
|
vl, vr = vr, vl
|
|
|
|
|
}
|
|
|
|
|
value, keep = vectorElemBinop(op, vl, vr)
|
|
|
|
|
value, keep := vectorElemBinop(op, vl, vr)
|
|
|
|
|
if !keep {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
metric := resultMetric(ls.Metric, op, resultLabels...)
|
|
|
|
|
|
|
|
|
|
if keep {
|
|
|
|
|
metric := resultMetric(op, ls, rs, matching)
|
|
|
|
|
// Check if the same label set has been added for a many-to-one matching before.
|
|
|
|
|
if matching.Card == CardManyToOne || matching.Card == CardOneToMany {
|
|
|
|
|
insHash := clientmodel.SignatureForLabels(metric.Metric, matching.Include)
|
|
|
|
|
if ihs, exists := added[hash]; exists {
|
|
|
|
|
for _, ih := range ihs {
|
|
|
|
|
if ih == insHash {
|
|
|
|
|
ev.errorf("metric with label set has already been matched")
|
|
|
|
|
}
|
|
|
|
|
insertedSigs, exists := matchedSigs[sig]
|
|
|
|
|
if matching.Card == CardOneToOne {
|
|
|
|
|
if exists {
|
|
|
|
|
ev.errorf("multiple matches for labels: many-to-one matching must be explicit (group_left/group_right)")
|
|
|
|
|
}
|
|
|
|
|
added[hash] = append(ihs, insHash)
|
|
|
|
|
matchedSigs[sig] = nil // Set existance to true.
|
|
|
|
|
} else {
|
|
|
|
|
added[hash] = []uint64{insHash}
|
|
|
|
|
// In many-to-one matching the grouping labels have to ensure a unique metric
|
|
|
|
|
// for the result vector. Check whether those labels have already been added for
|
|
|
|
|
// the same matching labels.
|
|
|
|
|
insertSig := clientmodel.SignatureForLabels(metric.Metric, matching.Include)
|
|
|
|
|
if !exists {
|
|
|
|
|
insertedSigs = map[uint64]struct{}{}
|
|
|
|
|
matchedSigs[sig] = insertedSigs
|
|
|
|
|
} else if _, duplicate := insertedSigs[insertSig]; duplicate {
|
|
|
|
|
ev.errorf("multiple matches for labels: grouping labels must ensure unique matches")
|
|
|
|
|
}
|
|
|
|
|
insertedSigs[insertSig] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
ns := &Sample{
|
|
|
|
|
|
|
|
|
|
result = append(result, &Sample{
|
|
|
|
|
Metric: metric,
|
|
|
|
|
Value: value,
|
|
|
|
|
Timestamp: ev.Timestamp,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// signatureFunc returns a function that calculates the signature for a metric
|
|
|
|
|
// based on the provided labels.
|
|
|
|
|
func signatureFunc(labels ...clientmodel.LabelName) func(m clientmodel.COWMetric) uint64 {
|
|
|
|
|
if len(labels) == 0 {
|
|
|
|
|
return func(m clientmodel.COWMetric) uint64 {
|
|
|
|
|
m.Delete(clientmodel.MetricNameLabel)
|
|
|
|
|
return uint64(m.Metric.Fingerprint())
|
|
|
|
|
}
|
|
|
|
|
result = append(result, ns)
|
|
|
|
|
added[hash] = added[hash] // Set existance to true.
|
|
|
|
|
}
|
|
|
|
|
return func(m clientmodel.COWMetric) uint64 {
|
|
|
|
|
return clientmodel.SignatureForLabels(m.Metric, labels)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add all remaining samples in the rhs in an OR operation if they
|
|
|
|
|
// have not been matched up with a lhs sample.
|
|
|
|
|
if op == itemLOR {
|
|
|
|
|
for hash, rs := range rm {
|
|
|
|
|
if _, exists := added[hash]; !exists {
|
|
|
|
|
rs.Metric = resultMetric(op, rs, nil, matching)
|
|
|
|
|
result = append(result, rs)
|
|
|
|
|
// resultMetric returns the metric for the given sample(s) based on the vector
|
|
|
|
|
// binary operation and the matching options.
|
|
|
|
|
func resultMetric(met clientmodel.COWMetric, op itemType, labels ...clientmodel.LabelName) clientmodel.COWMetric {
|
|
|
|
|
if len(labels) == 0 {
|
|
|
|
|
if shouldDropMetricName(op) {
|
|
|
|
|
met.Delete(clientmodel.MetricNameLabel)
|
|
|
|
|
}
|
|
|
|
|
return met
|
|
|
|
|
}
|
|
|
|
|
// As we definitly write, creating a new metric is the easiest solution.
|
|
|
|
|
m := clientmodel.Metric{}
|
|
|
|
|
for _, ln := range labels {
|
|
|
|
|
// Included labels from the `group_x` modifier are taken from the "many"-side.
|
|
|
|
|
if v, ok := met.Metric[ln]; ok {
|
|
|
|
|
m[ln] = v
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
return clientmodel.COWMetric{Metric: m, Copied: false}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// vectorScalarBinop evaluates a binary operation between a vector and a scalar.
|
|
|
|
@ -1018,64 +1086,6 @@ func shouldDropMetricName(op itemType) bool {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resultMetric returns the metric for the given sample(s) based on the vector
|
|
|
|
|
// binary operation and the matching options.
|
|
|
|
|
func resultMetric(op itemType, ls, rs *Sample, matching *VectorMatching) clientmodel.COWMetric {
|
|
|
|
|
if len(matching.On) == 0 || op == itemLOR || op == itemLAND {
|
|
|
|
|
if shouldDropMetricName(op) {
|
|
|
|
|
ls.Metric.Delete(clientmodel.MetricNameLabel)
|
|
|
|
|
}
|
|
|
|
|
return ls.Metric
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m := clientmodel.Metric{}
|
|
|
|
|
for _, ln := range matching.On {
|
|
|
|
|
m[ln] = ls.Metric.Metric[ln]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, ln := range matching.Include {
|
|
|
|
|
// Included labels from the `group_x` modifier are taken from the "many"-side.
|
|
|
|
|
v, ok := ls.Metric.Metric[ln]
|
|
|
|
|
if ok {
|
|
|
|
|
m[ln] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return clientmodel.COWMetric{false, m}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hashForMetric calculates a hash value for the given metric based on the matching
|
|
|
|
|
// options for the binary operation.
|
|
|
|
|
func hashForMetric(metric clientmodel.Metric, withLabels clientmodel.LabelNames) uint64 {
|
|
|
|
|
var labels clientmodel.LabelNames
|
|
|
|
|
|
|
|
|
|
if len(withLabels) > 0 {
|
|
|
|
|
var match bool
|
|
|
|
|
for _, ln := range withLabels {
|
|
|
|
|
if _, match = metric[ln]; !match {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If the metric does not contain the labels to match on, build the hash
|
|
|
|
|
// over the whole metric to give it a unique hash.
|
|
|
|
|
if !match {
|
|
|
|
|
labels = make(clientmodel.LabelNames, 0, len(metric))
|
|
|
|
|
for ln := range metric {
|
|
|
|
|
labels = append(labels, ln)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
labels = withLabels
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
labels = make(clientmodel.LabelNames, 0, len(metric))
|
|
|
|
|
for ln := range metric {
|
|
|
|
|
if ln != clientmodel.MetricNameLabel {
|
|
|
|
|
labels = append(labels, ln)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return clientmodel.SignatureForLabels(metric, labels)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// chooseClosestSample chooses the closest sample of a list of samples
|
|
|
|
|
// surrounding a given target time. If samples are found both before and after
|
|
|
|
|
// the target time, the sample value is interpolated between these. Otherwise,
|
|
|
|
|