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  }