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

fix(logql): Make LabelSampleExtractor work as expected when label doesn't exist in logline #6766

Merged
merged 10 commits into from
Aug 3, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* [6395](https://github.com/grafana/loki/pull/6395) **DylanGuedes**: Add encoding support

##### Fixes
* [6766](https://github.com/grafana/loki/pull/6766) **kavirajk**: fix(logql): Make `StringLabelfilter` work as expected

##### Changes

Expand Down
19 changes: 18 additions & 1 deletion pkg/logql/log/label_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,24 @@ func (s *StringLabelFilter) Process(_ int64, line []byte, lbs *LabelsBuilder) ([
if s.Name == logqlmodel.ErrorLabel {
return line, s.Matches(lbs.GetErr())
}
v, _ := lbs.Get(s.Name)
v, ok := lbs.Get(s.Name)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is the real fix.


// NOTE(kavi): whether the `label` exists in logline matters when doing `!=` or `!~`.
// Because, consider following long lines
// ```
// 1.msg="hello" subqueries=5
// 2.msg="hello" subqueries=0
// 3.msg="hello"
// 4.msg="hello" subqueries=""
//```

// Now when quering like `{...}|logfmt|subqueries=""` should consider lines 3 and 4.
Copy link
Contributor

@cyriltovena cyriltovena Jul 28, 2022

Choose a reason for hiding this comment

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

Not sure about this, it doesn't really make it easier to understand for the end user as this make it more obscure:

  • when using equality we consider non existent label empty
  • but for inequality we don't.

Honestly in #6713 I think the expectations were wrong.

sum(sum_over_time({job="loki-ops/query-frontend"}
  |= "metrics.go"
  | logfmt
  | subqueries!="0"
  | unwrap subqueries
  [5m]
))

When subqueries doesn't exist then subqueries!="0" is true.

What is weird was that empty labels does transform into a 0 integer.

Copy link
Contributor Author

@kavirajk kavirajk Jul 28, 2022

Choose a reason for hiding this comment

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

When subqueries doesn't exist then subqueries!="0" is true.

So you saying subqueries!=0 and subqueries!="0" ok to return different log lines?. But that's unexpected behaviour to end-user IMHO. Both should return exactly same results IMO.

What is weird was that empty labels does transform into a 0 integer.

That's because, we don't mutate the log line if label is not present. So treat it as if label="" during label filter, But still log line doesn't contain actual label, which breaks when doing unwrap on same label during metrics extraction.

Copy link
Member

Choose a reason for hiding this comment

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

What is weird was that empty labels does transform into a 0 integer.

I agree with @cyriltovena. This is the part I think we need to address.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

May be we discard empty values ? If that is possible.

Yes that's possible. We can skip empty values during metrics extraction when using unwrap without changing any of the existing label filter behavior.

Also the more I think about it, subqueries!="0" and subqueries!=0 are two different label filters and should be treated as such. And former can indeed match log lines that doesn't have subqueries label at all (we are assuming it has default empty string substring=" when doesn't exist).

I have change the PR like it's mentioned here.

// But when doing queries like `{...}|logfmt|subqueries!="0"` (also with !~), then line 3 shouldn't be considered.
// https://github.com/grafana/loki/pull/6766
// https://github.com/grafana/loki/issues/6713
if !ok && (s.Type == labels.MatchNotEqual || s.Type == labels.MatchNotRegexp) {
return line, false
}
return line, s.Matches(v)
}

Expand Down
68 changes: 68 additions & 0 deletions pkg/logql/log/label_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/prometheus/prometheus/model/labels"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/grafana/loki/pkg/logqlmodel"
Expand Down Expand Up @@ -350,3 +351,70 @@ func TestReduceAndLabelFilter(t *testing.T) {
})
}
}

func TestStringLabelFilter(t *testing.T) {
// NOTE: https://github.com/grafana/loki/issues/6713

tests := []struct {
name string
filter *StringLabelFilter
labels labels.Labels
shouldMatch bool
}{
{
name: `logfmt|subqueries!="0" (without label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchNotEqual, "subqueries", "0")),
labels: labels.Labels{{Name: "msg", Value: "hello"}}, // no label `subqueries`
shouldMatch: false,
},
kavirajk marked this conversation as resolved.
Show resolved Hide resolved
{
name: `logfmt|subqueries!="0" (with label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchNotEqual, "subqueries", "0")),
labels: labels.Labels{{Name: "msg", Value: "hello"}, {Name: "subqueries", Value: "2"}}, // label `subqueries` exist
shouldMatch: true,
},
{
name: `logfmt|subqueries!~"0" (without label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchNotRegexp, "subqueries", "0")),
labels: labels.Labels{{Name: "msg", Value: "hello"}}, // no label `subqueries`
shouldMatch: false,
},
{
name: `logfmt|subqueries!~"0" (with label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchNotRegexp, "subqueries", "0")),
labels: labels.Labels{{Name: "msg", Value: "hello"}, {Name: "subqueries", Value: "2"}}, // label `subqueries` exist
shouldMatch: true,
},
{
name: `logfmt|subqueries="0" (without label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchEqual, "subqueries", "")),
labels: labels.Labels{{Name: "msg", Value: "hello"}}, // no label `subqueries`
shouldMatch: true,
},
{
name: `logfmt|subqueries="0" (with label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchEqual, "subqueries", "")),
labels: labels.Labels{{Name: "msg", Value: "hello"}, {Name: "subqueries", Value: ""}}, // label `subqueries` exist
shouldMatch: true,
},
{
name: `logfmt|subqueries=~"0" (without label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchRegexp, "subqueries", "")),
labels: labels.Labels{{Name: "msg", Value: "hello"}}, // no label `subqueries`
shouldMatch: true,
},
{
name: `logfmt|subqueries=~"0" (with label)`,
filter: NewStringLabelFilter(labels.MustNewMatcher(labels.MatchRegexp, "subqueries", "")),
labels: labels.Labels{{Name: "msg", Value: "hello"}, {Name: "subqueries", Value: ""}}, // label `subqueries` exist
shouldMatch: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, ok := tc.filter.Process(0, []byte("sample log line"), NewBaseLabelsBuilder().ForLabels(tc.labels, tc.labels.Hash()))
assert.Equal(t, tc.shouldMatch, ok)
})
}
}
64 changes: 64 additions & 0 deletions pkg/logql/log/metrics_extraction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/prometheus/prometheus/model/labels"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -152,6 +153,69 @@ func Test_Extract_ExpectedLabels(t *testing.T) {
require.True(t, ok)
require.Equal(t, (20 * time.Millisecond).Seconds(), f)
require.Equal(t, labels.Labels{{Name: "foo", Value: "json"}}, lbs.Labels())

}
func TestLabelExtractorWithStages(t *testing.T) {

// A helper type to check if particular logline should be skipped
// during `ProcessLine` or got correct sample value extracted.
type checkLine struct {
logLine string
skip bool
sample float64
}

tests := []struct {
name string
extractor SampleExtractor
checkLines []checkLine
shouldFail bool
}{
{
name: "with just logfmt and stringlabelfilter",
// {foo="bar"} | logfmt | subqueries != "0" (note: "0", a stringlabelfilter)
extractor: mustSampleExtractor(
LabelExtractorWithStages("subqueries", ConvertFloat, []string{"foo"}, false, false, []Stage{NewLogfmtParser(), NewStringLabelFilter(labels.MustNewMatcher(labels.MatchNotEqual, "subqueries", "0"))}, NoopStage),
ssncferreira marked this conversation as resolved.
Show resolved Hide resolved
),
checkLines: []checkLine{
{logLine: "msg=hello subqueries=5", skip: false, sample: 5},
{logLine: "msg=hello subqueries=0", skip: true},
{logLine: "msg=hello ", skip: true}, // log lines doesn't contain the `subqueries` label
},
},
{
name: "with just logfmt and numeric labelfilter",
// {foo="bar"} | logfmt | subqueries != 0 (note: "0", a numericLabelFilter)
extractor: mustSampleExtractor(
LabelExtractorWithStages("subqueries", ConvertFloat, []string{"foo"}, false, false, []Stage{NewLogfmtParser(), NewNumericLabelFilter(LabelFilterNotEqual, "subqueries", 0)}, NoopStage),
),
checkLines: []checkLine{
{logLine: "msg=hello subqueries=5", skip: false, sample: 5},
{logLine: "msg=hello subqueries=0", skip: true},
{logLine: "msg=hello ", skip: true}, // log lines doesn't contain the `subqueries` label
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
for _, line := range tc.checkLines {
v, lbs, ok := tc.extractor.ForStream(labels.Labels{{Name: "bar", Value: "foo"}}).ProcessString(0, line.logLine)
skipped := !ok
assert.Equal(t, line.skip, skipped, "line", line.logLine)
if !skipped {
assert.Equal(t, line.sample, v)

// lbs shouldn't have __error__ = SampleExtractionError
assert.Empty(t, lbs.Labels())
return
}

// if line is skipped, `lbs` will be nil.
assert.Nil(t, lbs, "line", line.logLine)
}
})
}
}

func mustSampleExtractor(ex SampleExtractor, err error) SampleExtractor {
Expand Down