Skip to content

Commit 88beefb

Browse files
authored
fix(logql): Fix inconsistency with parsed field short circuiting (#17104)
PR #8724 originally changed the behaviour of LogQL so that the first extracted field takes precedence over any later extracted field with the same name. However, this PR had two subtle issues due to the caching that parse stages perform for key lookups: * The precedence logic did not always apply to duplicate fields within the same parse stage (notably for logfmt). * The caching behaviour could incorrectly cause stages to permanently ignore fields for future log lines where that stage should otherwise perform extraction. Additionally, once structured metadata was introduced, it was incorrectly being flagged as an "extracted field." Combined with the caching behaviour described above, this means that parsed fields only take precedence over structured metadata if the first log line encountered by the engine doesn't have that field also set as structured metadata. This can be demonstrated with the following scenario: 1. A log line from a stream that uses structured metadata for `trace_id` is encountered first. This (incorrectly) causes the trace_id field to be ignored in all parse stages. 2. All later log lines from streams without structured metadata can no longer extract or filter on a parsed `trace_id` for the lifetime of the query, due to the caching from step 1. The fix is two-fold: * Decouple skipping previously extracted fields from the logic which caches interned keys. * Only mark parsed labels as extracted fields (previously all labels). Because the check for whether a field is already extracted is now called more times, the list of extracted fields has been updated to a map to allow for faster checks.
1 parent 7312ccb commit 88beefb

4 files changed

Lines changed: 118 additions & 30 deletions

File tree

‎pkg/logql/log/labels.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,15 @@ func (b *LabelsBuilder) Set(category LabelCategory, n, v string) *LabelsBuilder
385385
}
386386
b.add[category] = append(b.add[category], labels.Label{Name: n, Value: v})
387387

388-
// Sometimes labels are set and later modified. Only record
389-
// each label once
390-
b.parserKeyHints.RecordExtracted(n)
388+
if category == ParsedLabel {
389+
// We record parsed labels as extracted so that future parse stages can
390+
// quickly bypass any existing extracted fields.
391+
//
392+
// Note that because this is used for bypassing extracted fields, and
393+
// because parsed labels always take precedence over structured metadata
394+
// and stream labels, we must only call RecordExtracted for parsed labels.
395+
b.parserKeyHints.RecordExtracted(n)
396+
}
391397
return b
392398
}
393399

‎pkg/logql/log/parser.go‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func (j *JSONParser) parseLabelValue(key, value []byte, dataType jsonparser.Valu
147147
}
148148
return field, true
149149
})
150-
if !ok {
150+
if !ok || j.lbs.ParserLabelHints().Extracted(sanitizedKey) {
151151
return nil
152152
}
153153
j.lbs.Set(ParsedLabel, sanitizedKey, readValue(value, dataType))
@@ -188,7 +188,7 @@ func (j *JSONParser) parseLabelValue(key, value []byte, dataType jsonparser.Valu
188188

189189
// reset the prefix position
190190
j.prefixBuffer = j.prefixBuffer[:prefixLen]
191-
if !ok {
191+
if !ok || j.lbs.ParserLabelHints().Extracted(keyString) {
192192
return nil
193193
}
194194

@@ -321,7 +321,7 @@ func (r *RegexpParser) Process(_ int64, line []byte, lbs *LabelsBuilder) ([]byte
321321

322322
return sanitize, true
323323
})
324-
if !ok {
324+
if !ok || parserHints.Extracted(key) {
325325
continue
326326
}
327327

@@ -387,7 +387,7 @@ func (l *LogfmtParser) Process(_ int64, line []byte, lbs *LabelsBuilder) ([]byte
387387
}
388388
return sanitized, true
389389
})
390-
if !ok {
390+
if !ok || parserHints.Extracted(key) {
391391
continue
392392
}
393393

@@ -459,7 +459,7 @@ func (l *PatternParser) Process(_ int64, line []byte, lbs *LabelsBuilder) ([]byt
459459
name = name + duplicateSuffix
460460
}
461461

462-
if !parserHints.ShouldExtract(name) {
462+
if parserHints.Extracted(name) || !parserHints.ShouldExtract(name) {
463463
continue
464464
}
465465

@@ -526,6 +526,14 @@ func (l *LogfmtExpressionParser) Process(_ int64, line []byte, lbs *LabelsBuilde
526526
}
527527
}
528528

529+
// alwaysExtract checks whether a key should be extracted regardless of other
530+
// conditions.
531+
alwaysExtract := func(key string) bool {
532+
// Any key in the expression list should always be extracted.
533+
_, ok := keys[key]
534+
return ok
535+
}
536+
529537
l.dec.Reset(line)
530538
var current []byte
531539
for !l.dec.EOL() {
@@ -546,14 +554,12 @@ func (l *LogfmtExpressionParser) Process(_ int64, line []byte, lbs *LabelsBuilde
546554
return "", false
547555
}
548556

549-
_, alwaysExtract := keys[sanitized]
550-
if !alwaysExtract && !lbs.ParserLabelHints().ShouldExtract(sanitized) {
557+
if !alwaysExtract(sanitized) && !lbs.ParserLabelHints().ShouldExtract(sanitized) {
551558
return "", false
552559
}
553560
return sanitized, true
554561
})
555-
556-
if !ok {
562+
if !ok || (!alwaysExtract(key) && lbs.ParserLabelHints().Extracted(key)) {
557563
continue
558564
}
559565

@@ -572,7 +578,7 @@ func (l *LogfmtExpressionParser) Process(_ int64, line []byte, lbs *LabelsBuilde
572578
if _, ok := l.expressions[key]; ok {
573579
if lbs.BaseHas(key) {
574580
key = key + duplicateSuffix
575-
if !lbs.ParserLabelHints().ShouldExtract(key) {
581+
if lbs.ParserLabelHints().Extracted(key) || !lbs.ParserLabelHints().ShouldExtract(key) {
576582
// Don't extract duplicates if we don't have to
577583
break
578584
}
@@ -784,7 +790,7 @@ func (u *UnpackParser) unpack(entry []byte, lbs *LabelsBuilder) ([]byte, error)
784790
}
785791
return field, true
786792
})
787-
if !ok {
793+
if !ok || lbs.ParserLabelHints().Extracted(key) {
788794
return nil
789795
}
790796

‎pkg/logql/log/parser_hints.go‎

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ func NoParserHints() ParserHint {
1919
//
2020
// All we need to extract is the status_code in the json parser.
2121
type ParserHint interface {
22-
// Tells if a label with the given key should be extracted.
22+
// Extracted returns whether a key has already been extracted by a previous
23+
// parse stage. This result must not be cached.
24+
Extracted(key string) bool
25+
26+
// Tells if a label with the given key should be extracted. Parsers may cache
27+
// the result of ShouldExtract, so implementations of houldExtract must
28+
// return consistent results for any log line or parser stage.
2329
ShouldExtract(key string) bool
2430

2531
// Tells if there's any hint that start with the given prefix.
@@ -54,17 +60,20 @@ type Hints struct {
5460
noLabels bool
5561
requiredLabels []string
5662
shouldPreserveError bool
57-
extracted []string
63+
extracted map[string]struct{}
5864
labelFilters []LabelFilterer
5965
labelNames []string
6066
}
6167

68+
func (p *Hints) Extracted(key string) bool {
69+
_, ok := p.extracted[key] // It's safe to read from a nil map.
70+
return ok
71+
}
72+
6273
func (p *Hints) ShouldExtract(key string) bool {
63-
for _, l := range p.extracted {
64-
if l == key {
65-
return false
66-
}
67-
}
74+
// The result of ShouldExtract gets cached in parser stages, so we must
75+
// return consistent results throughout the lifetime of a query; this means
76+
// we can't account for p.extracted here.
6877

6978
for _, l := range p.requiredLabels {
7079
if l == key {
@@ -93,7 +102,10 @@ func (p *Hints) NoLabels() bool {
93102
}
94103

95104
func (p *Hints) RecordExtracted(key string) {
96-
p.extracted = append(p.extracted, key)
105+
if p.extracted == nil {
106+
p.extracted = make(map[string]struct{})
107+
}
108+
p.extracted[key] = struct{}{}
97109
}
98110

99111
func (p *Hints) AllRequiredExtracted() bool {
@@ -103,19 +115,16 @@ func (p *Hints) AllRequiredExtracted() bool {
103115

104116
found := 0
105117
for _, l := range p.requiredLabels {
106-
for _, e := range p.extracted {
107-
if l == e {
108-
found++
109-
break
110-
}
118+
if p.Extracted(l) {
119+
found++
111120
}
112121
}
113122

114123
return len(p.requiredLabels) == found
115124
}
116125

117126
func (p *Hints) Reset() {
118-
p.extracted = p.extracted[:0]
127+
clear(p.extracted)
119128
}
120129

121130
func (p *Hints) PreserveError() bool {
@@ -159,7 +168,7 @@ func NewParserHint(requiredLabelNames, groups []string, without, noLabels bool,
159168
}
160169
}
161170

162-
extracted := make([]string, 0, len(hints))
171+
extracted := make(map[string]struct{}, len(hints))
163172
if noLabels {
164173
if len(hints) > 0 {
165174
return &Hints{requiredLabels: hints, extracted: extracted, shouldPreserveError: containsError(hints), labelFilters: labelFilters, labelNames: labelNames}

‎pkg/logql/log/parser_test.go‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ type fakeParseHints struct {
301301
extractAll bool
302302
}
303303

304+
func (p *fakeParseHints) Extracted(_ string) bool {
305+
return false
306+
}
307+
304308
func (p *fakeParseHints) ShouldExtract(key string) bool {
305309
p.checkCount++
306310
return key == p.label || p.extractAll
@@ -1149,7 +1153,7 @@ func TestLogfmtParser_parse(t *testing.T) {
11491153
[]byte(`foobar="foo bar" foobar=10ms`),
11501154
labels.FromStrings("foo", "bar"),
11511155
labels.FromStrings("foo", "bar",
1152-
"foobar", "10ms",
1156+
"foobar", "foo bar",
11531157
),
11541158
nil,
11551159
NoParserHints(),
@@ -1342,6 +1346,69 @@ func TestLogfmtParser_keepEmpty(t *testing.T) {
13421346
}
13431347
}
13441348

1349+
func TestLogfmtConsistentPrecedence(t *testing.T) {
1350+
line := `app=lowkey level=error ts=2021-02-12T19:18:10.037940878Z msg="hello world"`
1351+
1352+
t.Run("sturctured metadata first", func(t *testing.T) {
1353+
var (
1354+
metadataStream = NewBaseLabelsBuilder().
1355+
ForLabels(labels.FromStrings("foo", "bar"), 0).
1356+
Set(StructuredMetadataLabel, "app", "loki")
1357+
1358+
basicStream = NewBaseLabelsBuilder().
1359+
ForLabels(labels.FromStrings("foo", "baz"), 0)
1360+
)
1361+
1362+
parser := NewLogfmtParser(true, true)
1363+
1364+
_, ok := parser.Process(0, []byte(line), metadataStream)
1365+
require.True(t, ok)
1366+
1367+
_, ok = parser.Process(0, []byte(line), basicStream)
1368+
require.True(t, ok)
1369+
1370+
res, cat, ok := metadataStream.GetWithCategory("app")
1371+
require.Equal(t, "lowkey", res)
1372+
require.Equal(t, ParsedLabel, cat)
1373+
require.True(t, ok)
1374+
1375+
res, cat, ok = basicStream.GetWithCategory("app")
1376+
require.Equal(t, "lowkey", res)
1377+
require.Equal(t, ParsedLabel, cat)
1378+
require.True(t, ok)
1379+
})
1380+
1381+
t.Run("parsed labels first", func(t *testing.T) {
1382+
var (
1383+
metadataStream = NewBaseLabelsBuilder().
1384+
ForLabels(labels.FromStrings("foo", "bar"), 0).
1385+
Set(StructuredMetadataLabel, "app", "loki")
1386+
1387+
basicStream = NewBaseLabelsBuilder().
1388+
ForLabels(labels.FromStrings("foo", "baz"), 0)
1389+
)
1390+
1391+
parser := NewLogfmtParser(true, true)
1392+
1393+
_, ok := parser.Process(0, []byte(line), basicStream)
1394+
require.True(t, ok)
1395+
1396+
_, ok = parser.Process(0, []byte(line), metadataStream)
1397+
require.True(t, ok)
1398+
1399+
res, cat, ok := metadataStream.GetWithCategory("app")
1400+
require.Equal(t, "lowkey", res)
1401+
require.Equal(t, ParsedLabel, cat)
1402+
require.True(t, ok)
1403+
1404+
res, cat, ok = basicStream.GetWithCategory("app")
1405+
require.Equal(t, "lowkey", res)
1406+
require.Equal(t, ParsedLabel, cat)
1407+
require.True(t, ok)
1408+
})
1409+
1410+
}
1411+
13451412
func TestLogfmtExpressionParser(t *testing.T) {
13461413
testLine := []byte(`app=foo level=error spaces="value with ÜFT8👌" ts=2021-02-12T19:18:10.037940878Z`)
13471414

0 commit comments

Comments
 (0)