github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/logql/log/metrics_extraction_test.go (about) 1 package log 2 3 import ( 4 "sort" 5 "testing" 6 "time" 7 8 "github.com/prometheus/prometheus/model/labels" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 ) 12 13 func Test_labelSampleExtractor_Extract(t *testing.T) { 14 tests := []struct { 15 name string 16 ex SampleExtractor 17 in labels.Labels 18 want float64 19 wantLbs labels.Labels 20 wantOk bool 21 }{ 22 { 23 "convert float", 24 mustSampleExtractor(LabelExtractorWithStages( 25 "foo", ConvertFloat, nil, false, false, nil, NoopStage, 26 )), 27 labels.Labels{labels.Label{Name: "foo", Value: "15.0"}}, 28 15, 29 labels.Labels{}, 30 true, 31 }, 32 { 33 "convert float as vector with no grouping", 34 mustSampleExtractor(LabelExtractorWithStages( 35 "foo", ConvertFloat, nil, false, true, nil, NoopStage, 36 )), 37 labels.Labels{labels.Label{Name: "foo", Value: "15.0"}, labels.Label{Name: "bar", Value: "buzz"}}, 38 15, 39 labels.Labels{}, 40 true, 41 }, 42 { 43 "convert float without", 44 mustSampleExtractor(LabelExtractorWithStages( 45 "foo", ConvertFloat, []string{"bar", "buzz"}, true, false, nil, NoopStage, 46 )), 47 labels.Labels{ 48 {Name: "foo", Value: "10"}, 49 {Name: "bar", Value: "foo"}, 50 {Name: "buzz", Value: "blip"}, 51 {Name: "namespace", Value: "dev"}, 52 }, 53 10, 54 labels.Labels{ 55 {Name: "namespace", Value: "dev"}, 56 }, 57 true, 58 }, 59 { 60 "convert float with", 61 mustSampleExtractor(LabelExtractorWithStages( 62 "foo", ConvertFloat, []string{"bar", "buzz"}, false, false, nil, NoopStage, 63 )), 64 labels.Labels{ 65 {Name: "foo", Value: "0.6"}, 66 {Name: "bar", Value: "foo"}, 67 {Name: "buzz", Value: "blip"}, 68 {Name: "namespace", Value: "dev"}, 69 }, 70 0.6, 71 labels.Labels{ 72 {Name: "bar", Value: "foo"}, 73 {Name: "buzz", Value: "blip"}, 74 }, 75 true, 76 }, 77 { 78 "convert duration with", 79 mustSampleExtractor(LabelExtractorWithStages( 80 "foo", ConvertDuration, []string{"bar", "buzz"}, false, false, nil, NoopStage, 81 )), 82 labels.Labels{ 83 {Name: "foo", Value: "500ms"}, 84 {Name: "bar", Value: "foo"}, 85 {Name: "buzz", Value: "blip"}, 86 {Name: "namespace", Value: "dev"}, 87 }, 88 0.5, 89 labels.Labels{ 90 {Name: "bar", Value: "foo"}, 91 {Name: "buzz", Value: "blip"}, 92 }, 93 true, 94 }, 95 { 96 "convert bytes", 97 mustSampleExtractor(LabelExtractorWithStages( 98 "foo", ConvertBytes, []string{"bar", "buzz"}, false, false, nil, NoopStage, 99 )), 100 labels.Labels{ 101 {Name: "foo", Value: "13 MiB"}, 102 {Name: "bar", Value: "foo"}, 103 {Name: "buzz", Value: "blip"}, 104 {Name: "namespace", Value: "dev"}, 105 }, 106 13 * 1024 * 1024, 107 labels.Labels{ 108 {Name: "bar", Value: "foo"}, 109 {Name: "buzz", Value: "blip"}, 110 }, 111 true, 112 }, 113 { 114 "not convertable", 115 mustSampleExtractor(LabelExtractorWithStages( 116 "foo", ConvertFloat, []string{"bar", "buzz"}, false, false, nil, NoopStage, 117 )), 118 labels.Labels{ 119 {Name: "foo", Value: "not_a_number"}, 120 {Name: "bar", Value: "foo"}, 121 }, 122 0, 123 labels.Labels{ 124 {Name: "__error__", Value: "SampleExtractionErr"}, 125 {Name: "__error_details__", Value: "strconv.ParseFloat: parsing \"not_a_number\": invalid syntax"}, 126 {Name: "bar", Value: "foo"}, 127 {Name: "foo", Value: "not_a_number"}, 128 }, 129 true, 130 }, 131 } 132 for _, tt := range tests { 133 t.Run(tt.name, func(t *testing.T) { 134 sort.Sort(tt.in) 135 136 outval, outlbs, ok := tt.ex.ForStream(tt.in).Process(0, []byte("")) 137 require.Equal(t, tt.wantOk, ok) 138 require.Equal(t, tt.want, outval) 139 require.Equal(t, tt.wantLbs, outlbs.Labels()) 140 141 outval, outlbs, ok = tt.ex.ForStream(tt.in).ProcessString(0, "") 142 require.Equal(t, tt.wantOk, ok) 143 require.Equal(t, tt.want, outval) 144 require.Equal(t, tt.wantLbs, outlbs.Labels()) 145 }) 146 } 147 } 148 149 func Test_Extract_ExpectedLabels(t *testing.T) { 150 ex := mustSampleExtractor(LabelExtractorWithStages("duration", ConvertDuration, []string{"foo"}, false, false, []Stage{NewJSONParser()}, NoopStage)) 151 152 f, lbs, ok := ex.ForStream(labels.Labels{{Name: "bar", Value: "foo"}}).ProcessString(0, `{"duration":"20ms","foo":"json"}`) 153 require.True(t, ok) 154 require.Equal(t, (20 * time.Millisecond).Seconds(), f) 155 require.Equal(t, labels.Labels{{Name: "foo", Value: "json"}}, lbs.Labels()) 156 157 } 158 func TestLabelExtractorWithStages(t *testing.T) { 159 160 // A helper type to check if particular logline should be skipped 161 // during `ProcessLine` or got correct sample value extracted. 162 type checkLine struct { 163 logLine string 164 skip bool 165 sample float64 166 } 167 168 tests := []struct { 169 name string 170 extractor SampleExtractor 171 checkLines []checkLine 172 shouldFail bool 173 }{ 174 { 175 name: "with just logfmt and stringlabelfilter", 176 // {foo="bar"} | logfmt | subqueries != "0" (note: "0", a stringlabelfilter) 177 extractor: mustSampleExtractor( 178 LabelExtractorWithStages("subqueries", ConvertFloat, []string{"foo"}, false, false, []Stage{NewLogfmtParser(), NewStringLabelFilter(labels.MustNewMatcher(labels.MatchNotEqual, "subqueries", "0"))}, NoopStage), 179 ), 180 checkLines: []checkLine{ 181 {logLine: "msg=hello subqueries=5", skip: false, sample: 5}, 182 {logLine: "msg=hello subqueries=0", skip: true}, 183 {logLine: "msg=hello ", skip: true}, // log lines doesn't contain the `subqueries` label 184 }, 185 }, 186 { 187 name: "with just logfmt and numeric labelfilter", 188 // {foo="bar"} | logfmt | subqueries != 0 (note: "0", a numericLabelFilter) 189 extractor: mustSampleExtractor( 190 LabelExtractorWithStages("subqueries", ConvertFloat, []string{"foo"}, false, false, []Stage{NewLogfmtParser(), NewNumericLabelFilter(LabelFilterNotEqual, "subqueries", 0)}, NoopStage), 191 ), 192 checkLines: []checkLine{ 193 {logLine: "msg=hello subqueries=5", skip: false, sample: 5}, 194 {logLine: "msg=hello subqueries=0", skip: true}, 195 {logLine: "msg=hello ", skip: true}, // log lines doesn't contain the `subqueries` label 196 }, 197 }, 198 } 199 200 for _, tc := range tests { 201 t.Run(tc.name, func(t *testing.T) { 202 for _, line := range tc.checkLines { 203 v, lbs, ok := tc.extractor.ForStream(labels.Labels{{Name: "bar", Value: "foo"}}).ProcessString(0, line.logLine) 204 skipped := !ok 205 assert.Equal(t, line.skip, skipped, "line", line.logLine) 206 if !skipped { 207 assert.Equal(t, line.sample, v) 208 209 // lbs shouldn't have __error__ = SampleExtractionError 210 assert.Empty(t, lbs.Labels()) 211 return 212 } 213 214 // if line is skipped, `lbs` will be nil. 215 assert.Nil(t, lbs, "line", line.logLine) 216 } 217 }) 218 } 219 } 220 221 func mustSampleExtractor(ex SampleExtractor, err error) SampleExtractor { 222 if err != nil { 223 panic(err) 224 } 225 return ex 226 } 227 228 func TestNewLineSampleExtractor(t *testing.T) { 229 se, err := NewLineSampleExtractor(CountExtractor, nil, nil, false, false) 230 require.NoError(t, err) 231 232 lbs := labels.Labels{ 233 {Name: "namespace", Value: "dev"}, 234 {Name: "cluster", Value: "us-central1"}, 235 } 236 sort.Sort(lbs) 237 238 sse := se.ForStream(lbs) 239 f, l, ok := sse.Process(0, []byte(`foo`)) 240 require.True(t, ok) 241 require.Equal(t, 1., f) 242 assertLabelResult(t, lbs, l) 243 244 f, l, ok = sse.ProcessString(0, `foo`) 245 require.True(t, ok) 246 require.Equal(t, 1., f) 247 assertLabelResult(t, lbs, l) 248 249 stage := mustFilter(NewFilter("foo", labels.MatchEqual)).ToStage() 250 se, err = NewLineSampleExtractor(BytesExtractor, []Stage{stage}, []string{"namespace"}, false, false) 251 require.NoError(t, err) 252 253 sse = se.ForStream(lbs) 254 f, l, ok = sse.Process(0, []byte(`foo`)) 255 require.True(t, ok) 256 require.Equal(t, 3., f) 257 assertLabelResult(t, labels.Labels{labels.Label{Name: "namespace", Value: "dev"}}, l) 258 259 sse = se.ForStream(lbs) 260 _, _, ok = sse.Process(0, []byte(`nope`)) 261 require.False(t, ok) 262 } 263 264 func TestFilteringSampleExtractor(t *testing.T) { 265 se := NewFilteringSampleExtractor([]PipelineFilter{ 266 newPipelineFilter(2, 4, labels.Labels{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, "e"), 267 newPipelineFilter(3, 5, labels.Labels{{Name: "baz", Value: "foo"}}, "e"), 268 }, newStubExtractor()) 269 270 tt := []struct { 271 name string 272 ts int64 273 line string 274 labels labels.Labels 275 ok bool 276 }{ 277 {"it is after the timerange", 6, "line", labels.Labels{{Name: "baz", Value: "foo"}}, true}, 278 {"it is before the timerange", 1, "line", labels.Labels{{Name: "baz", Value: "foo"}}, true}, 279 {"it doesn't match the filter", 3, "all good", labels.Labels{{Name: "baz", Value: "foo"}}, true}, 280 {"it doesn't match all the selectors", 3, "line", labels.Labels{{Name: "foo", Value: "bar"}}, true}, 281 {"it doesn't match any selectors", 3, "line", labels.Labels{{Name: "beep", Value: "boop"}}, true}, 282 {"it matches all selectors", 3, "line", labels.Labels{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, false}, 283 {"it tries all the filters", 5, "line", labels.Labels{{Name: "baz", Value: "foo"}}, false}, 284 } 285 286 for _, test := range tt { 287 t.Run(test.name, func(t *testing.T) { 288 _, _, ok := se.ForStream(test.labels).Process(test.ts, []byte(test.line)) 289 require.Equal(t, test.ok, ok) 290 291 _, _, ok = se.ForStream(test.labels).ProcessString(test.ts, test.line) 292 require.Equal(t, test.ok, ok) 293 }) 294 } 295 } 296 297 func newStubExtractor() *stubExtractor { 298 return &stubExtractor{ 299 sp: &stubStreamExtractor{}, 300 } 301 } 302 303 // A stub always returns the same data 304 type stubExtractor struct { 305 sp *stubStreamExtractor 306 } 307 308 func (p *stubExtractor) ForStream(labels labels.Labels) StreamSampleExtractor { 309 return p.sp 310 } 311 312 // A stub always returns the same data 313 type stubStreamExtractor struct{} 314 315 func (p *stubStreamExtractor) BaseLabels() LabelsResult { 316 return nil 317 } 318 319 func (p *stubStreamExtractor) Process(ts int64, line []byte) (float64, LabelsResult, bool) { 320 return 0, nil, true 321 } 322 323 func (p *stubStreamExtractor) ProcessString(ts int64, line string) (float64, LabelsResult, bool) { 324 return 0, nil, true 325 }