@@ -13,6 +13,8 @@ import (
13
13
14
14
"github.com/grafana/loki/v3/pkg/loghttp/push"
15
15
"github.com/grafana/loki/v3/pkg/logproto"
16
+ "github.com/grafana/loki/v3/pkg/logql/log"
17
+ "github.com/grafana/loki/v3/pkg/logql/log/jsonexpr"
16
18
"github.com/grafana/loki/v3/pkg/logql/log/logfmt"
17
19
"github.com/grafana/loki/v3/pkg/util/constants"
18
20
)
@@ -31,46 +33,43 @@ var (
31
33
errorAbbrv = []byte ("err" )
32
34
critical = []byte ("critical" )
33
35
fatal = []byte ("fatal" )
36
+
37
+ defaultAllowedLevelFields = []string {"level" , "LEVEL" , "Level" , "severity" , "SEVERITY" , "Severity" , "lvl" , "LVL" , "Lvl" }
34
38
)
35
39
36
- func allowedLabelsForLevel (allowedFields []string ) map [ string ] struct {} {
40
+ func allowedLabelsForLevel (allowedFields []string ) [] string {
37
41
if len (allowedFields ) == 0 {
38
- return map [string ]struct {}{
39
- "level" : {}, "LEVEL" : {}, "Level" : {},
40
- "severity" : {}, "SEVERITY" : {}, "Severity" : {},
41
- "lvl" : {}, "LVL" : {}, "Lvl" : {},
42
- }
43
- }
44
- allowedFieldsMap := make (map [string ]struct {}, len (allowedFields ))
45
- for _ , field := range allowedFields {
46
- allowedFieldsMap [field ] = struct {}{}
42
+ return defaultAllowedLevelFields
47
43
}
48
- return allowedFieldsMap
44
+ return allowedFields
49
45
}
50
46
51
- type LevelDetector struct {
52
- validationContext validationContext
53
- allowedLabels map [ string ] struct {}
47
+ type FieldDetector struct {
48
+ validationContext validationContext
49
+ allowedLevelLabels [] string
54
50
}
55
51
56
- func newLevelDetector (validationContext validationContext ) * LevelDetector {
57
- logLevelFields := validationContext .logLevelFields
58
- return & LevelDetector {
59
- validationContext : validationContext ,
60
- allowedLabels : allowedLabelsForLevel (logLevelFields ),
52
+ func newFieldDetector (validationContext validationContext ) * FieldDetector {
53
+ return & FieldDetector {
54
+ validationContext : validationContext ,
55
+ allowedLevelLabels : allowedLabelsForLevel (validationContext .logLevelFields ),
61
56
}
62
57
}
63
58
64
- func (l * LevelDetector ) shouldDiscoverLogLevels () bool {
59
+ func (l * FieldDetector ) shouldDiscoverLogLevels () bool {
65
60
return l .validationContext .allowStructuredMetadata && l .validationContext .discoverLogLevels
66
61
}
67
62
68
- func (l * LevelDetector ) extractLogLevel (labels labels.Labels , structuredMetadata labels.Labels , entry logproto.Entry ) (logproto.LabelAdapter , bool ) {
69
- levelFromLabel , hasLevelLabel := l .hasAnyLevelLabels (labels )
63
+ func (l * FieldDetector ) shouldDiscoverGenericFields () bool {
64
+ return l .validationContext .allowStructuredMetadata && len (l .validationContext .discoverGenericFields ) > 0
65
+ }
66
+
67
+ func (l * FieldDetector ) extractLogLevel (labels labels.Labels , structuredMetadata labels.Labels , entry logproto.Entry ) (logproto.LabelAdapter , bool ) {
68
+ levelFromLabel , hasLevelLabel := labelsContainAny (labels , l .allowedLevelLabels )
70
69
var logLevel string
71
70
if hasLevelLabel {
72
71
logLevel = levelFromLabel
73
- } else if levelFromMetadata , ok := l . hasAnyLevelLabels (structuredMetadata ); ok {
72
+ } else if levelFromMetadata , ok := labelsContainAny (structuredMetadata , l . allowedLevelLabels ); ok {
74
73
logLevel = levelFromMetadata
75
74
} else {
76
75
logLevel = l .detectLogLevelFromLogEntry (entry , structuredMetadata )
@@ -85,16 +84,33 @@ func (l *LevelDetector) extractLogLevel(labels labels.Labels, structuredMetadata
85
84
}, true
86
85
}
87
86
88
- func (l * LevelDetector ) hasAnyLevelLabels (labels labels.Labels ) (string , bool ) {
89
- for lbl := range l .allowedLabels {
90
- if labels .Has (lbl ) {
91
- return labels .Get (lbl ), true
87
+ func (l * FieldDetector ) extractGenericField (name string , hints []string , labels labels.Labels , structuredMetadata labels.Labels , entry logproto.Entry ) (logproto.LabelAdapter , bool ) {
88
+
89
+ var value string
90
+ if v , ok := labelsContainAny (labels , hints ); ok {
91
+ value = v
92
+ } else if v , ok := labelsContainAny (structuredMetadata , hints ); ok {
93
+ value = v
94
+ } else {
95
+ value = l .detectGenericFieldFromLogEntry (entry , hints )
96
+ }
97
+
98
+ if value == "" {
99
+ return logproto.LabelAdapter {}, false
100
+ }
101
+ return logproto.LabelAdapter {Name : name , Value : value }, true
102
+ }
103
+
104
+ func labelsContainAny (labels labels.Labels , names []string ) (string , bool ) {
105
+ for _ , name := range names {
106
+ if labels .Has (name ) {
107
+ return labels .Get (name ), true
92
108
}
93
109
}
94
110
return "" , false
95
111
}
96
112
97
- func (l * LevelDetector ) detectLogLevelFromLogEntry (entry logproto.Entry , structuredMetadata labels.Labels ) string {
113
+ func (l * FieldDetector ) detectLogLevelFromLogEntry (entry logproto.Entry , structuredMetadata labels.Labels ) string {
98
114
// otlp logs have a severity number, using which we are defining the log levels.
99
115
// Significance of severity number is explained in otel docs here https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
100
116
if otlpSeverityNumberTxt := structuredMetadata .Get (push .OTLPSeverityNumber ); otlpSeverityNumberTxt != "" {
@@ -123,13 +139,24 @@ func (l *LevelDetector) detectLogLevelFromLogEntry(entry logproto.Entry, structu
123
139
return l .extractLogLevelFromLogLine (entry .Line )
124
140
}
125
141
126
- func (l * LevelDetector ) extractLogLevelFromLogLine (log string ) string {
127
- logSlice := unsafe .Slice (unsafe .StringData (log ), len (log ))
142
+ func (l * FieldDetector ) detectGenericFieldFromLogEntry (entry logproto.Entry , hints []string ) string {
143
+ lineBytes := unsafe .Slice (unsafe .StringData (entry .Line ), len (entry .Line ))
144
+ var v []byte
145
+ if isJSON (entry .Line ) {
146
+ v = getValueUsingJSONParser (lineBytes , hints )
147
+ } else if isLogFmt (lineBytes ) {
148
+ v = getValueUsingLogfmtParser (lineBytes , hints )
149
+ }
150
+ return string (v )
151
+ }
152
+
153
+ func (l * FieldDetector ) extractLogLevelFromLogLine (log string ) string {
154
+ lineBytes := unsafe .Slice (unsafe .StringData (log ), len (log ))
128
155
var v []byte
129
156
if isJSON (log ) {
130
- v = l . getValueUsingJSONParser (logSlice )
131
- } else if isLogFmt (logSlice ) {
132
- v = l . getValueUsingLogfmtParser (logSlice )
157
+ v = getValueUsingJSONParser (lineBytes , l . allowedLevelLabels )
158
+ } else if isLogFmt (lineBytes ) {
159
+ v = getValueUsingLogfmtParser (lineBytes , l . allowedLevelLabels )
133
160
} else {
134
161
return detectLevelFromLogLine (log )
135
162
}
@@ -154,24 +181,42 @@ func (l *LevelDetector) extractLogLevelFromLogLine(log string) string {
154
181
}
155
182
}
156
183
157
- func ( l * LevelDetector ) getValueUsingLogfmtParser (line []byte ) []byte {
184
+ func getValueUsingLogfmtParser (line []byte , hints [] string ) []byte {
158
185
d := logfmt .NewDecoder (line )
186
+ // In order to have the same behaviour as the JSON field extraction,
187
+ // the full line needs to be parsed to extract all possible matching fields.
188
+ pos := len (hints ) // the index of the hint that matches
189
+ var res []byte
159
190
for ! d .EOL () && d .ScanKeyval () {
160
- if _ , ok := l .allowedLabels [string (d .Key ())]; ok {
161
- return d .Value ()
191
+ k := unsafe .String (unsafe .SliceData (d .Key ()), len (d .Key ()))
192
+ for x , hint := range hints {
193
+ if strings .EqualFold (k , hint ) && x < pos {
194
+ res , pos = d .Value (), x
195
+ // If there is only a single hint, or the matching hint is the first one,
196
+ // we can stop parsing the rest of the line and return early.
197
+ if x == 0 {
198
+ return res
199
+ }
200
+ }
162
201
}
163
202
}
164
- return nil
203
+ return res
165
204
}
166
205
167
- func (l * LevelDetector ) getValueUsingJSONParser (log []byte ) []byte {
168
- for allowedLabel := range l .allowedLabels {
169
- l , _ , _ , err := jsonparser .Get (log , allowedLabel )
170
- if err == nil {
171
- return l
206
+ func getValueUsingJSONParser (line []byte , hints []string ) []byte {
207
+ var res []byte
208
+ for _ , allowedLabel := range hints {
209
+ parsed , err := jsonexpr .Parse (allowedLabel , false )
210
+ if err != nil {
211
+ continue
212
+ }
213
+ l , _ , _ , err := jsonparser .Get (line , log .JSONPathToStrings (parsed )... )
214
+ if err != nil {
215
+ continue
172
216
}
217
+ return l
173
218
}
174
- return nil
219
+ return res
175
220
}
176
221
177
222
func isLogFmt (line []byte ) bool {
0 commit comments