Skip to content

Commit 528c911

Browse files
authored
Merge branch 'main' into helm-query-frontend-lb-optional
2 parents 6a02841 + 01bf61e commit 528c911

File tree

2 files changed

+112
-5
lines changed

2 files changed

+112
-5
lines changed

pkg/distributor/field_detection.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,25 @@ func (l *FieldDetector) shouldDiscoverGenericFields() bool {
7979
}
8080

8181
func (l *FieldDetector) extractLogLevel(labels labels.Labels, structuredMetadata labels.Labels, entry logproto.Entry) (logproto.LabelAdapter, bool) {
82-
// If the level is already set in the structured metadata, we don't need to do anything.
83-
if structuredMetadata.Has(constants.LevelLabel) {
84-
return logproto.LabelAdapter{}, false
82+
// Check if detected_level is already present in entry.StructuredMetadata and normalize it
83+
for i, sm := range entry.StructuredMetadata {
84+
if sm.Name == constants.LevelLabel {
85+
normalizedLevel := normalizeLogLevel(sm.Value)
86+
if sm.Value != normalizedLevel {
87+
// Update the value in-place with the normalized version
88+
entry.StructuredMetadata[i].Value = normalizedLevel
89+
}
90+
// Level already exists and has been normalized if needed
91+
return logproto.LabelAdapter{}, false
92+
}
8593
}
8694

8795
levelFromLabel, hasLevelLabel := labelsContainAny(labels, l.allowedLevelLabels)
8896
var logLevel string
8997
if hasLevelLabel {
90-
logLevel = levelFromLabel
98+
logLevel = normalizeLogLevel(levelFromLabel)
9199
} else if levelFromMetadata, ok := labelsContainAny(structuredMetadata, l.allowedLevelLabels); ok {
92-
logLevel = levelFromMetadata
100+
logLevel = normalizeLogLevel(levelFromMetadata)
93101
} else {
94102
logLevel = l.detectLogLevelFromLogEntry(entry, structuredMetadata)
95103
}
@@ -129,6 +137,30 @@ func labelsContainAny(labels labels.Labels, names []string) (string, bool) {
129137
return "", false
130138
}
131139

140+
// normalizeLogLevel normalizes log level strings to lowercase standard values
141+
func normalizeLogLevel(level string) string {
142+
levelBytes := unsafe.Slice(unsafe.StringData(level), len(level)) // #nosec G103 -- we know the string is not mutated -- nosemgrep: use-of-unsafe-block
143+
switch {
144+
case bytes.EqualFold(levelBytes, traceBytes), bytes.EqualFold(levelBytes, traceAbbrv):
145+
return constants.LogLevelTrace
146+
case bytes.EqualFold(levelBytes, debug), bytes.EqualFold(levelBytes, debugAbbrv):
147+
return constants.LogLevelDebug
148+
case bytes.EqualFold(levelBytes, info), bytes.EqualFold(levelBytes, infoAbbrv), bytes.EqualFold(levelBytes, infoFull):
149+
return constants.LogLevelInfo
150+
case bytes.EqualFold(levelBytes, warn), bytes.EqualFold(levelBytes, warnAbbrv), bytes.EqualFold(levelBytes, warning):
151+
return constants.LogLevelWarn
152+
case bytes.EqualFold(levelBytes, errorStr), bytes.EqualFold(levelBytes, errorAbbrv):
153+
return constants.LogLevelError
154+
case bytes.EqualFold(levelBytes, critical):
155+
return constants.LogLevelCritical
156+
case bytes.EqualFold(levelBytes, fatal):
157+
return constants.LogLevelFatal
158+
default:
159+
// Return the original value if it doesn't match any known level
160+
return level
161+
}
162+
}
163+
132164
func (l *FieldDetector) detectLogLevelFromLogEntry(entry logproto.Entry, structuredMetadata labels.Labels) string {
133165
// otlp logs have a severity number, using which we are defining the log levels.
134166
// Significance of severity number is explained in otel docs here https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber

pkg/distributor/field_detection_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,81 @@ func Test_DetectLogLevels(t *testing.T) {
168168
})
169169
})
170170

171+
t.Run("detected_level with mixed case value gets normalized to lowercase", func(t *testing.T) {
172+
// Test various mixed case values
173+
testCases := []struct {
174+
input string
175+
expected string
176+
}{
177+
{"WaRn", constants.LogLevelWarn},
178+
{"InFo", constants.LogLevelInfo},
179+
{"CRITICAL", constants.LogLevelCritical},
180+
{"Debug", constants.LogLevelDebug},
181+
{"FaTaL", constants.LogLevelFatal},
182+
{"tRaCe", constants.LogLevelTrace},
183+
{"ERROR", constants.LogLevelError},
184+
}
185+
186+
for _, tc := range testCases {
187+
// Create a fresh setup for each test case
188+
limits, ingester := setup(true)
189+
distributors, _ := prepare(t, 1, 5, limits, func(_ string) (ring_client.PoolClient, error) { return ingester, nil })
190+
191+
writeReq := makeWriteRequestWithLabels(1, 10, []string{`{foo="bar"}`}, false, false, false)
192+
writeReq.Streams[0].Entries[0].Line = `test log`
193+
writeReq.Streams[0].Entries[0].StructuredMetadata = push.LabelsAdapter{
194+
{
195+
Name: constants.LevelLabel,
196+
Value: tc.input,
197+
},
198+
}
199+
200+
_, err := distributors[0].Push(ctx, writeReq)
201+
require.NoError(t, err)
202+
topVal := ingester.Peek()
203+
204+
sm := topVal.Streams[0].Entries[0].StructuredMetadata
205+
require.Len(t, sm, 1)
206+
require.Equal(t, constants.LevelLabel, sm[0].Name)
207+
require.Equal(t, tc.expected, sm[0].Value, "Input %q should normalize to %q", tc.input, tc.expected)
208+
}
209+
})
210+
211+
t.Run("level from stream labels gets normalized to lowercase in detected_level", func(t *testing.T) {
212+
// Test various mixed case values in stream labels
213+
testCases := []struct {
214+
streamLabel string
215+
expected string
216+
}{
217+
{`{foo="bar", level="ERROR"}`, constants.LogLevelError},
218+
{`{foo="bar", level="WaRn"}`, constants.LogLevelWarn},
219+
{`{foo="bar", level="InFo"}`, constants.LogLevelInfo},
220+
{`{foo="bar", level="CRITICAL"}`, constants.LogLevelCritical},
221+
{`{foo="bar", level="Debug"}`, constants.LogLevelDebug},
222+
{`{foo="bar", level="FaTaL"}`, constants.LogLevelFatal},
223+
{`{foo="bar", level="tRaCe"}`, constants.LogLevelTrace},
224+
}
225+
226+
for _, tc := range testCases {
227+
// Create a fresh setup for each test case
228+
limits, ingester := setup(true)
229+
distributors, _ := prepare(t, 1, 5, limits, func(_ string) (ring_client.PoolClient, error) { return ingester, nil })
230+
231+
writeReq := makeWriteRequestWithLabels(1, 10, []string{tc.streamLabel}, false, false, false)
232+
writeReq.Streams[0].Entries[0].Line = `log message without level`
233+
234+
_, err := distributors[0].Push(ctx, writeReq)
235+
require.NoError(t, err)
236+
topVal := ingester.Peek()
237+
238+
// Verify that detected_level is normalized to lowercase
239+
sm := topVal.Streams[0].Entries[0].StructuredMetadata
240+
require.Len(t, sm, 1, "Expected detected_level in structured metadata for stream label %s", tc.streamLabel)
241+
require.Equal(t, constants.LevelLabel, sm[0].Name)
242+
require.Equal(t, tc.expected, sm[0].Value, "Stream label %q should normalize to %q in detected_level", tc.streamLabel, tc.expected)
243+
}
244+
})
245+
171246
t.Run("indexed OTEL severity takes precedence over structured metadata or log line", func(t *testing.T) {
172247
limits, ingester := setup(true)
173248
distributors, _ := prepare(t, 1, 5, limits, func(_ string) (ring_client.PoolClient, error) { return ingester, nil })

0 commit comments

Comments
 (0)