Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Literal Expressions in LogQL #1677

Merged
merged 26 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e208772
binops in ast
owen-d Feb 7, 2020
d55c587
bin op associativity & precedence
owen-d Feb 7, 2020
519d226
binOpEvaluator work
owen-d Feb 7, 2020
05809bd
defers close only if constructed without error
owen-d Feb 7, 2020
d6c2217
tests binary ops
owen-d Feb 7, 2020
7ea39b9
more binops
owen-d Feb 7, 2020
3f19385
updates docs
owen-d Feb 7, 2020
0ec42b0
changelog
owen-d Feb 7, 2020
cee6860
number literals in ast
owen-d Feb 8, 2020
ce6ce4e
[wip] literalExpr parsing
owen-d Feb 10, 2020
bfc70b4
number parsing
owen-d Feb 10, 2020
1db3bd4
signed literals
owen-d Feb 10, 2020
178be5f
propagates evaluator close, handles healthchecks
owen-d Feb 10, 2020
92c9516
literal evaluator works on non commutative operations
owen-d Feb 10, 2020
642d590
Merge remote-tracking branch 'upstream/master' into feature/ast-literals
owen-d Feb 11, 2020
c2ceabb
literalExprs cannot be used as legs of logical/set binops
owen-d Feb 11, 2020
6495411
removes comment
owen-d Feb 11, 2020
acaeaf2
single literalExpr tests
owen-d Feb 11, 2020
706023d
reduces binops with literals in ast construction where possible
owen-d Feb 11, 2020
e33ec36
doc updates
owen-d Feb 11, 2020
99bf381
scalar datatypes in logql
owen-d Feb 13, 2020
9100ca1
Merge remote-tracking branch 'upstream/master' into feature/ast-literals
owen-d Feb 13, 2020
cb4626f
scalar serialization type
owen-d Feb 13, 2020
e689e88
increases safety and reduces complexity in ast evaluator
owen-d Feb 14, 2020
d4780b1
recursive literal binop reduction parse test, improves parse errors o…
owen-d Feb 14, 2020
3fd28bb
vector + literal test
owen-d Feb 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

* [1677](https://github.com/grafana/loki/pull/1677) **owen-d**: Introduces numeric literals in LogQL
* [1662](https://github.com/grafana/loki/pull/1662) **owen-d**: Introduces binary operators in LogQL
* [1572](https://github.com/grafana/loki/pull/1572) **owen-d**: Introduces the `querier.query-ingesters-within` flag and associated yaml config. When enabled, queries for a time range that do not overlap this lookback interval will not be sent to the ingesters.
* [1558](https://github.com/grafana/loki/pull/1558) **owen-d**: Introduces `ingester.max-chunk-age` which specifies the maximum chunk age before it's cut.
Expand Down
16 changes: 14 additions & 2 deletions docs/logql.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,24 @@ The following binary arithmetic operators exist in Loki:
- `%` (modulo)
- `^` (power/exponentiation)

Binary arithmetic operators are defined only between two vectors.
Binary arithmetic operators are defined between two literals (scalars), a literal and a vector, and two vectors.

Between two instant vectors, a binary arithmetic operator is applied to each entry in the left-hand side vector and its matching element in the right-hand vector. The result is propagated into the result vector with the grouping labels becoming the output label set. Entries for which no matching entry in the right-hand vector can be found are not part of the result.
Between two literals, the behavior is obvious: they evaluate to another literal that is the result of the operator applied to both scalar operands (1 + 1 = 2).

Between a vector and a literal, the operator is applied to the value of every data sample in the vector. E.g. if a time series vector is multiplied by 2, the result is another vector in which every sample value of the original vector is multiplied by 2.

Between two vectors, a binary arithmetic operator is applied to each entry in the left-hand side vector and its matching element in the right-hand vector. The result is propagated into the result vector with the grouping labels becoming the output label set. Entries for which no matching entry in the right-hand vector can be found are not part of the result.

##### Examples

Implement a health check with a simple query:

> `1 + 1`
Double the rate of a a log stream's entries:

> `sum(rate({app="foo"})) * 2`
Get proportion of warning logs to error logs for the `foo` app

> `sum(rate({app="foo", level="warn"}[1m])) / sum(rate({app="foo", level="error"}[1m]))`
Expand Down
93 changes: 86 additions & 7 deletions pkg/logql/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,18 +228,25 @@ const (
OpTypeCountOverTime = "count_over_time"
OpTypeRate = "rate"

// binops
// binops - logical/set
OpTypeOr = "or"
OpTypeAnd = "and"
OpTypeUnless = "unless"
OpTypeAdd = "+"
OpTypeSub = "-"
OpTypeMul = "*"
OpTypeDiv = "/"
OpTypeMod = "%"
OpTypePow = "^"

// binops - operations
OpTypeAdd = "+"
OpTypeSub = "-"
OpTypeMul = "*"
OpTypeDiv = "/"
OpTypeMod = "%"
OpTypePow = "^"
)

// IsLogicalBinOp tests whether an operation is a logical/set binary operation
func IsLogicalBinOp(op string) bool {
return op == OpTypeOr || op == OpTypeAnd || op == OpTypeUnless
}

// SampleExpr is a LogQL expression filtering logs and returning metric samples.
type SampleExpr interface {
// Selector is the LogQL selector to apply when retrieving logs.
Expand Down Expand Up @@ -348,6 +355,7 @@ func mustNewVectorAggregationExpr(left SampleExpr, operation string, gr *groupin
if p, err = strconv.Atoi(*params); err != nil {
panic(newParseError(fmt.Sprintf("invalid parameter %s(%s,", operation, *params), 0, 0))
}

default:
if params != nil {
panic(newParseError(fmt.Sprintf("unsupported parameter for operation %s(%s,", operation, *params), 0, 0))
Expand Down Expand Up @@ -409,13 +417,84 @@ func mustNewBinOpExpr(op string, lhs, rhs Expr) SampleExpr {
rhs,
), 0, 0))
}

leftLit, lOk := left.(*literalExpr)
rightLit, rOk := right.(*literalExpr)

if IsLogicalBinOp(op) {
if lOk {
panic(newParseError(fmt.Sprintf(
"unexpected literal for left leg of logical/set binary operation (%s): %f",
op,
leftLit.value,
), 0, 0))
}

if rOk {
panic(newParseError(fmt.Sprintf(
"unexpected literal for right leg of logical/set binary operation (%s): %f",
op,
rightLit.value,
), 0, 0))
}
}

// map expr like (1+1) -> 2
if lOk && rOk {
return reduceBinOp(op, leftLit, rightLit)
}

return &binOpExpr{
SampleExpr: left,
RHS: right,
op: op,
}
}

// Reduces a binary operation expression. A binop is reducable if both of its legs are literal expressions.
// This is because literals need match all labels, which is currently difficult to encode into StepEvaluators.
// Therefore, we ensure a binop can be reduced/simplified, maintaining the invariant that it does not have two literal legs.
func reduceBinOp(op string, left, right *literalExpr) *literalExpr {
merged := (&defaultEvaluator{}).mergeBinOp(
op,
&promql.Sample{Point: promql.Point{V: left.value}},
&promql.Sample{Point: promql.Point{V: right.value}},
)
return &literalExpr{value: merged.V}
}

type literalExpr struct {
value float64
}

func mustNewLiteralExpr(s string, invert bool) *literalExpr {
n, err := strconv.ParseFloat(s, 64)
if err != nil {
panic(err)
owen-d marked this conversation as resolved.
Show resolved Hide resolved
}

if invert {
n = -n
}

return &literalExpr{
value: n,
}
}

func (e *literalExpr) logQLExpr() {}

func (e *literalExpr) String() string {
return fmt.Sprintf("%f", e.value)
}

// literlExpr impls SampleExpr & LogSelectorExpr mainly to reduce the need for more complicated typings
// to facilitate sum types. We'll be type switching when evaluating them anyways
// and they will only be present in binary operation legs.
func (e *literalExpr) Selector() LogSelectorExpr { return e }
func (e *literalExpr) Filter() (Filter, error) { return nil, nil }
func (e *literalExpr) Matchers() []*labels.Matcher { return nil }

// helper used to impl Stringer for vector and range aggregations
// nolint:interfacer
func formatOperation(op string, grouping *grouping, params ...string) string {
Expand Down
9 changes: 1 addition & 8 deletions pkg/logql/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,6 @@ func (ng *engine) exec(ctx context.Context, q *query) (promql.Value, error) {
defer cancel()

qs := q.String()
// This is a legacy query used for health checking. Not the best practice, but it works.
if qs == "1+1" {
if GetRangeType(q) == InstantType {
return promql.Vector{}, nil
}
return promql.Matrix{}, nil
}

expr, err := ParseExpr(qs)
if err != nil {
Expand All @@ -211,7 +204,6 @@ func (ng *engine) exec(ctx context.Context, q *query) (promql.Value, error) {

// evalSample evaluate a sampleExpr
func (ng *engine) evalSample(ctx context.Context, expr SampleExpr, q *query) (promql.Value, error) {

stepEvaluator, err := ng.evaluator.Evaluator(ctx, expr, q)
if err != nil {
return nil, err
Expand All @@ -225,6 +217,7 @@ func (ng *engine) evalSample(ctx context.Context, expr SampleExpr, q *query) (pr
sort.Slice(vec, func(i, j int) bool { return labels.Compare(vec[i].Metric, vec[j].Metric) < 0 })
return vec, nil
}

for next {
for _, p := range vec {
var (
Expand Down
99 changes: 96 additions & 3 deletions pkg/logql/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,25 @@ func TestEngine_NewInstantQuery(t *testing.T) {
promql.Sample{Point: promql.Point{T: 60 * 1000, V: 0.2}, Metric: labels.Labels{labels.Label{Name: "app", Value: "fuzz"}}},
},
},
{
// healthcheck
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test for Literal + Vector ?

`1+1`, time.Unix(60, 0), logproto.FORWARD, 100,
[][]*logproto.Stream{},
[]SelectParams{},
promql.Vector{
promql.Sample{Point: promql.Point{T: 60 * 1000, V: 2}},
},
},
{
// single literal
`2`,
time.Unix(60, 0), logproto.FORWARD, 100,
[][]*logproto.Stream{},
[]SelectParams{},
promql.Vector{
promql.Sample{Point: promql.Point{T: 60 * 1000, V: 2}},
},
},
} {
test := test
t.Run(fmt.Sprintf("%s %s", test.qs, test.direction), func(t *testing.T) {
Expand Down Expand Up @@ -945,9 +964,72 @@ func TestEngine_NewRangeQuery(t *testing.T) {
},
},
{
`
count_over_time({app="bar"}[1m]) ^ count_over_time({app="bar"}[1m])
`,
`1+1--1`,
time.Unix(60, 0), time.Unix(180, 0), 30 * time.Second, logproto.FORWARD, 100,
[][]*logproto.Stream{},
[]SelectParams{},
promql.Matrix{
promql.Series{
Points: []promql.Point{{T: 60 * 1000, V: 3}},
},
},
},
{
`rate({app="bar"}[1m]) - 1`,
time.Unix(60, 0), time.Unix(180, 0), 30 * time.Second, logproto.FORWARD, 100,
[][]*logproto.Stream{
{
newStream(testSize, factor(5, identity), `{app="bar"}`),
},
},
[]SelectParams{
{&logproto.QueryRequest{Direction: logproto.FORWARD, Start: time.Unix(0, 0), End: time.Unix(180, 0), Limit: 0, Selector: `{app="bar"}`}},
},
promql.Matrix{
promql.Series{
Metric: labels.Labels{{Name: "app", Value: "bar"}},
Points: []promql.Point{{T: 60 * 1000, V: -0.8}, {T: 90 * 1000, V: -0.8}, {T: 120 * 1000, V: -0.8}, {T: 150 * 1000, V: -0.8}, {T: 180 * 1000, V: -0.8}},
},
},
},
{
`1 - rate({app="bar"}[1m])`,
time.Unix(60, 0), time.Unix(180, 0), 30 * time.Second, logproto.FORWARD, 100,
[][]*logproto.Stream{
{
newStream(testSize, factor(5, identity), `{app="bar"}`),
},
},
[]SelectParams{
{&logproto.QueryRequest{Direction: logproto.FORWARD, Start: time.Unix(0, 0), End: time.Unix(180, 0), Limit: 0, Selector: `{app="bar"}`}},
},
promql.Matrix{
promql.Series{
Metric: labels.Labels{{Name: "app", Value: "bar"}},
Points: []promql.Point{{T: 60 * 1000, V: 0.8}, {T: 90 * 1000, V: 0.8}, {T: 120 * 1000, V: 0.8}, {T: 150 * 1000, V: 0.8}, {T: 180 * 1000, V: 0.8}},
},
},
},
{
`rate({app="bar"}[1m]) - 1 / 2`,
time.Unix(60, 0), time.Unix(180, 0), 30 * time.Second, logproto.FORWARD, 100,
[][]*logproto.Stream{
{
newStream(testSize, factor(5, identity), `{app="bar"}`),
},
},
[]SelectParams{
{&logproto.QueryRequest{Direction: logproto.FORWARD, Start: time.Unix(0, 0), End: time.Unix(180, 0), Limit: 0, Selector: `{app="bar"}`}},
},
promql.Matrix{
promql.Series{
Metric: labels.Labels{{Name: "app", Value: "bar"}},
Points: []promql.Point{{T: 60 * 1000, V: -0.3}, {T: 90 * 1000, V: -0.3}, {T: 120 * 1000, V: -0.3}, {T: 150 * 1000, V: -0.3}, {T: 180 * 1000, V: -0.3}},
},
},
},
{
`count_over_time({app="bar"}[1m]) ^ count_over_time({app="bar"}[1m])`,
time.Unix(60, 0), time.Unix(180, 0), 30 * time.Second, logproto.FORWARD, 100,
[][]*logproto.Stream{
{
Expand All @@ -964,6 +1046,17 @@ func TestEngine_NewRangeQuery(t *testing.T) {
},
},
},
{
`2`,
time.Unix(60, 0), time.Unix(180, 0), 30 * time.Second, logproto.FORWARD, 100,
[][]*logproto.Stream{},
[]SelectParams{},
promql.Matrix{
promql.Series{
Points: []promql.Point{{T: 60 * 1000, V: 2}},
},
},
},
} {
test := test
t.Run(fmt.Sprintf("%s %s", test.qs, test.direction), func(t *testing.T) {
Expand Down
Loading