github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/modules/loki/loki_test.go (about)

     1  package loki_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"runtime"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	log "github.com/sirupsen/logrus"
    18  	"github.com/stretchr/testify/assert"
    19  	tomb "gopkg.in/tomb.v2"
    20  
    21  	"github.com/crowdsecurity/go-cs-lib/cstest"
    22  
    23  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    24  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/loki"
    25  	"github.com/crowdsecurity/crowdsec/pkg/types"
    26  )
    27  
    28  func TestConfiguration(t *testing.T) {
    29  	log.Infof("Test 'TestConfigure'")
    30  
    31  	tests := []struct {
    32  		config       string
    33  		expectedErr  string
    34  		password     string
    35  		waitForReady time.Duration
    36  		delayFor     time.Duration
    37  		testName     string
    38  	}{
    39  		{
    40  			config:      `foobar: asd`,
    41  			expectedErr: "line 1: field foobar not found in type loki.LokiConfiguration",
    42  			testName:    "Unknown field",
    43  		},
    44  		{
    45  			config: `
    46  mode: tail
    47  source: loki`,
    48  			expectedErr: "loki query is mandatory",
    49  			testName:    "Missing url",
    50  		},
    51  		{
    52  			config: `
    53  mode: tail
    54  source: loki
    55  url: http://localhost:3100/
    56  `,
    57  			expectedErr: "loki query is mandatory",
    58  			testName:    "Missing query",
    59  		},
    60  		{
    61  			config: `
    62  mode: tail
    63  source: loki
    64  url: http://localhost:3100/
    65  query: >
    66          {server="demo"}
    67  `,
    68  			expectedErr: "",
    69  			testName:    "Correct config",
    70  		},
    71  		{
    72  			config: `
    73  mode: tail
    74  source: loki
    75  url: http://localhost:3100/
    76  wait_for_ready: 5s
    77  query: >
    78          {server="demo"}
    79  `,
    80  			expectedErr:  "",
    81  			testName:     "Correct config with wait_for_ready",
    82  			waitForReady: 5 * time.Second,
    83  		},
    84  		{
    85  			config: `
    86  mode: tail
    87  source: loki
    88  url: http://localhost:3100/
    89  delay_for: 1s
    90  query: >
    91          {server="demo"}
    92  `,
    93  			expectedErr: "",
    94  			testName:    "Correct config with delay_for",
    95  			delayFor:    1 * time.Second,
    96  		},
    97  		{
    98  
    99  			config: `
   100  mode: tail
   101  source: loki
   102  url: http://localhost:3100/
   103  auth:
   104    username: foo
   105    password: bar
   106  query: >
   107          {server="demo"}
   108  `,
   109  			expectedErr: "",
   110  			password:    "bar",
   111  			testName:    "Correct config with password",
   112  		},
   113  		{
   114  
   115  			config: `
   116  mode: tail
   117  source: loki
   118  url: http://localhost:3100/
   119  delay_for: 10s
   120  query: >
   121          {server="demo"}
   122  `,
   123  			expectedErr: "delay_for should be a value between 1s and 5s",
   124  			testName:    "Invalid DelayFor",
   125  		},
   126  	}
   127  	subLogger := log.WithFields(log.Fields{
   128  		"type": "loki",
   129  	})
   130  
   131  	for _, test := range tests {
   132  		t.Run(test.testName, func(t *testing.T) {
   133  			lokiSource := loki.LokiSource{}
   134  			err := lokiSource.Configure([]byte(test.config), subLogger, configuration.METRICS_NONE)
   135  			cstest.AssertErrorContains(t, err, test.expectedErr)
   136  
   137  			if test.password != "" {
   138  				p := lokiSource.Config.Auth.Password
   139  				if test.password != p {
   140  					t.Fatalf("Password mismatch : %s != %s", test.password, p)
   141  				}
   142  			}
   143  
   144  			if test.waitForReady != 0 {
   145  				if lokiSource.Config.WaitForReady != test.waitForReady {
   146  					t.Fatalf("Wrong WaitForReady %v != %v", lokiSource.Config.WaitForReady, test.waitForReady)
   147  				}
   148  			}
   149  
   150  			if test.delayFor != 0 {
   151  				if lokiSource.Config.DelayFor != test.delayFor {
   152  					t.Fatalf("Wrong DelayFor %v != %v", lokiSource.Config.DelayFor, test.delayFor)
   153  				}
   154  			}
   155  		})
   156  	}
   157  }
   158  
   159  func TestConfigureDSN(t *testing.T) {
   160  	log.Infof("Test 'TestConfigureDSN'")
   161  
   162  	tests := []struct {
   163  		name         string
   164  		dsn          string
   165  		expectedErr  string
   166  		since        time.Time
   167  		password     string
   168  		scheme       string
   169  		waitForReady time.Duration
   170  		delayFor     time.Duration
   171  	}{
   172  		{
   173  			name:        "Wrong scheme",
   174  			dsn:         "wrong://",
   175  			expectedErr: "invalid DSN wrong:// for loki source, must start with loki://",
   176  		},
   177  		{
   178  			name:        "Correct DSN",
   179  			dsn:         `loki://localhost:3100/?query={server="demo"}`,
   180  			expectedErr: "",
   181  		},
   182  		{
   183  			name:        "Empty host",
   184  			dsn:         "loki://",
   185  			expectedErr: "empty loki host",
   186  		},
   187  		{
   188  			name:        "Invalid DSN",
   189  			dsn:         "loki",
   190  			expectedErr: "invalid DSN loki for loki source, must start with loki://",
   191  		},
   192  		{
   193  			name:        "Invalid Delay",
   194  			dsn:         `loki://localhost:3100/?query={server="demo"}&delay_for=10s`,
   195  			expectedErr: "delay_for should be a value between 1s and 5s",
   196  		},
   197  		{
   198  			name:  "Bad since param",
   199  			dsn:   `loki://127.0.0.1:3100/?since=3h&query={server="demo"}`,
   200  			since: time.Now().Add(-3 * time.Hour),
   201  		},
   202  		{
   203  			name:     "Basic Auth",
   204  			dsn:      `loki://login:password@localhost:3102/?query={server="demo"}`,
   205  			password: "password",
   206  		},
   207  		{
   208  			name:         "Correct DSN",
   209  			dsn:          `loki://localhost:3100/?query={server="demo"}&wait_for_ready=5s&delay_for=1s`,
   210  			expectedErr:  "",
   211  			waitForReady: 5 * time.Second,
   212  			delayFor:     1 * time.Second,
   213  		},
   214  		{
   215  			name:   "SSL DSN",
   216  			dsn:    `loki://localhost:3100/?ssl=true`,
   217  			scheme: "https",
   218  		},
   219  	}
   220  
   221  	for _, test := range tests {
   222  		subLogger := log.WithFields(log.Fields{
   223  			"type": "loki",
   224  			"name": test.name,
   225  		})
   226  
   227  		t.Logf("Test : %s", test.name)
   228  
   229  		lokiSource := &loki.LokiSource{}
   230  		err := lokiSource.ConfigureByDSN(test.dsn, map[string]string{"type": "testtype"}, subLogger, "")
   231  		cstest.AssertErrorContains(t, err, test.expectedErr)
   232  
   233  		noDuration, _ := time.ParseDuration("0s")
   234  		if lokiSource.Config.Since != noDuration && lokiSource.Config.Since.Round(time.Second) != time.Since(test.since).Round(time.Second) {
   235  			t.Fatalf("Invalid since %v", lokiSource.Config.Since)
   236  		}
   237  
   238  		if test.password != "" {
   239  			p := lokiSource.Config.Auth.Password
   240  			if test.password != p {
   241  				t.Fatalf("Password mismatch : %s != %s", test.password, p)
   242  			}
   243  		}
   244  
   245  		if test.scheme != "" {
   246  			url, _ := url.Parse(lokiSource.Config.URL)
   247  			if test.scheme != url.Scheme {
   248  				t.Fatalf("Schema mismatch : %s != %s", test.scheme, url.Scheme)
   249  			}
   250  		}
   251  
   252  		if test.waitForReady != 0 {
   253  			if lokiSource.Config.WaitForReady != test.waitForReady {
   254  				t.Fatalf("Wrong WaitForReady %v != %v", lokiSource.Config.WaitForReady, test.waitForReady)
   255  			}
   256  		}
   257  
   258  		if test.delayFor != 0 {
   259  			if lokiSource.Config.DelayFor != test.delayFor {
   260  				t.Fatalf("Wrong DelayFor %v != %v", lokiSource.Config.DelayFor, test.delayFor)
   261  			}
   262  		}
   263  	}
   264  }
   265  
   266  func feedLoki(logger *log.Entry, n int, title string) error {
   267  	streams := LogStreams{
   268  		Streams: []LogStream{
   269  			{
   270  				Stream: map[string]string{
   271  					"server": "demo",
   272  					"domain": "cw.example.com",
   273  					"key":    title,
   274  				},
   275  				Values: make([]LogValue, n),
   276  			},
   277  		},
   278  	}
   279  	for i := 0; i < n; i++ {
   280  		streams.Streams[0].Values[i] = LogValue{
   281  			Time: time.Now(),
   282  			Line: fmt.Sprintf("Log line #%d %v", i, title),
   283  		}
   284  	}
   285  
   286  	buff, err := json.Marshal(streams)
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	req, err := http.NewRequest(http.MethodPost, "http://127.0.0.1:3100/loki/api/v1/push", bytes.NewBuffer(buff))
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	req.Header.Set("Content-Type", "application/json")
   297  	req.Header.Set("X-Scope-OrgID", "1234")
   298  
   299  	resp, err := http.DefaultClient.Do(req)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	defer resp.Body.Close()
   305  
   306  	if resp.StatusCode != http.StatusNoContent {
   307  		b, _ := io.ReadAll(resp.Body)
   308  		logger.Error(string(b))
   309  
   310  		return fmt.Errorf("Bad post status %d", resp.StatusCode)
   311  	}
   312  
   313  	logger.Info(n, " Events sent")
   314  
   315  	return nil
   316  }
   317  
   318  func TestOneShotAcquisition(t *testing.T) {
   319  	if runtime.GOOS == "windows" {
   320  		t.Skip("Skipping test on windows")
   321  	}
   322  
   323  	log.SetOutput(os.Stdout)
   324  	log.SetLevel(log.InfoLevel)
   325  	log.Info("Test 'TestStreamingAcquisition'")
   326  
   327  	title := time.Now().String() // Loki will be messy, with a lot of stuff, lets use a unique key
   328  	tests := []struct {
   329  		config string
   330  	}{
   331  		{
   332  			config: fmt.Sprintf(`
   333  mode: cat
   334  source: loki
   335  url: http://127.0.0.1:3100
   336  query: '{server="demo",key="%s"}'
   337  headers:
   338   x-scope-orgid: "1234"
   339  since: 1h
   340  `, title),
   341  		},
   342  	}
   343  
   344  	for _, ts := range tests {
   345  		logger := log.New()
   346  		subLogger := logger.WithFields(log.Fields{
   347  			"type": "loki",
   348  		})
   349  		lokiSource := loki.LokiSource{}
   350  		err := lokiSource.Configure([]byte(ts.config), subLogger, configuration.METRICS_NONE)
   351  
   352  		if err != nil {
   353  			t.Fatalf("Unexpected error : %s", err)
   354  		}
   355  
   356  		err = feedLoki(subLogger, 20, title)
   357  		if err != nil {
   358  			t.Fatalf("Unexpected error : %s", err)
   359  		}
   360  
   361  		out := make(chan types.Event)
   362  		read := 0
   363  
   364  		go func() {
   365  			for {
   366  				<-out
   367  
   368  				read++
   369  			}
   370  		}()
   371  
   372  		lokiTomb := tomb.Tomb{}
   373  
   374  		err = lokiSource.OneShotAcquisition(out, &lokiTomb)
   375  		if err != nil {
   376  			t.Fatalf("Unexpected error : %s", err)
   377  		}
   378  
   379  		assert.Equal(t, 20, read)
   380  	}
   381  }
   382  
   383  func TestStreamingAcquisition(t *testing.T) {
   384  	if runtime.GOOS == "windows" {
   385  		t.Skip("Skipping test on windows")
   386  	}
   387  
   388  	log.SetOutput(os.Stdout)
   389  	log.SetLevel(log.InfoLevel)
   390  	log.Info("Test 'TestStreamingAcquisition'")
   391  
   392  	title := time.Now().String()
   393  	tests := []struct {
   394  		name          string
   395  		config        string
   396  		expectedErr   string
   397  		streamErr     string
   398  		expectedLines int
   399  	}{
   400  		{
   401  			name: "Bad port",
   402  			config: `mode: tail
   403  source: loki
   404  url: "http://127.0.0.1:3101"
   405  headers:
   406    x-scope-orgid: "1234"
   407  query: >
   408    {server="demo"}`, // No Loki server here
   409  			expectedErr:   "",
   410  			streamErr:     `loki is not ready: context deadline exceeded`,
   411  			expectedLines: 0,
   412  		},
   413  		{
   414  			name: "ok",
   415  			config: `mode: tail
   416  source: loki
   417  url: "http://127.0.0.1:3100"
   418  headers:
   419    x-scope-orgid: "1234"
   420  query: >
   421    {server="demo"}`,
   422  			expectedErr:   "",
   423  			streamErr:     "",
   424  			expectedLines: 20,
   425  		},
   426  	}
   427  
   428  	for _, ts := range tests {
   429  		t.Run(ts.name, func(t *testing.T) {
   430  			logger := log.New()
   431  			subLogger := logger.WithFields(log.Fields{
   432  				"type": "loki",
   433  				"name": ts.name,
   434  			})
   435  
   436  			out := make(chan types.Event)
   437  			lokiTomb := tomb.Tomb{}
   438  			lokiSource := loki.LokiSource{}
   439  
   440  			err := lokiSource.Configure([]byte(ts.config), subLogger, configuration.METRICS_NONE)
   441  			if err != nil {
   442  				t.Fatalf("Unexpected error : %s", err)
   443  			}
   444  
   445  			err = lokiSource.StreamingAcquisition(out, &lokiTomb)
   446  			cstest.AssertErrorContains(t, err, ts.streamErr)
   447  
   448  			if ts.streamErr != "" {
   449  				return
   450  			}
   451  
   452  			time.Sleep(time.Second * 2) // We need to give time to start reading from the WS
   453  
   454  			readTomb := tomb.Tomb{}
   455  			readCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   456  			count := 0
   457  
   458  			readTomb.Go(func() error {
   459  				defer cancel()
   460  
   461  				for {
   462  					select {
   463  					case <-readCtx.Done():
   464  						return readCtx.Err()
   465  					case evt := <-out:
   466  						count++
   467  
   468  						if !strings.HasSuffix(evt.Line.Raw, title) {
   469  							return fmt.Errorf("Incorrect suffix : %s", evt.Line.Raw)
   470  						}
   471  
   472  						if count == ts.expectedLines {
   473  							return nil
   474  						}
   475  					}
   476  				}
   477  			})
   478  
   479  			err = feedLoki(subLogger, ts.expectedLines, title)
   480  			if err != nil {
   481  				t.Fatalf("Unexpected error : %s", err)
   482  			}
   483  
   484  			err = readTomb.Wait()
   485  
   486  			cancel()
   487  
   488  			if err != nil {
   489  				t.Fatalf("Unexpected error : %s", err)
   490  			}
   491  
   492  			assert.Equal(t, ts.expectedLines, count)
   493  		})
   494  	}
   495  }
   496  
   497  func TestStopStreaming(t *testing.T) {
   498  	if runtime.GOOS == "windows" {
   499  		t.Skip("Skipping test on windows")
   500  	}
   501  
   502  	config := `
   503  mode: tail
   504  source: loki
   505  url: http://127.0.0.1:3100
   506  headers:
   507    x-scope-orgid: "1234"
   508  query: >
   509    {server="demo"}
   510  `
   511  	logger := log.New()
   512  	subLogger := logger.WithFields(log.Fields{
   513  		"type": "loki",
   514  	})
   515  	title := time.Now().String()
   516  	lokiSource := loki.LokiSource{}
   517  
   518  	err := lokiSource.Configure([]byte(config), subLogger, configuration.METRICS_NONE)
   519  	if err != nil {
   520  		t.Fatalf("Unexpected error : %s", err)
   521  	}
   522  
   523  	out := make(chan types.Event)
   524  
   525  	lokiTomb := &tomb.Tomb{}
   526  
   527  	err = lokiSource.StreamingAcquisition(out, lokiTomb)
   528  	if err != nil {
   529  		t.Fatalf("Unexpected error : %s", err)
   530  	}
   531  
   532  	time.Sleep(time.Second * 2)
   533  
   534  	err = feedLoki(subLogger, 1, title)
   535  	if err != nil {
   536  		t.Fatalf("Unexpected error : %s", err)
   537  	}
   538  
   539  	lokiTomb.Kill(nil)
   540  
   541  	err = lokiTomb.Wait()
   542  	if err != nil {
   543  		t.Fatalf("Unexpected error : %s", err)
   544  	}
   545  }
   546  
   547  type LogStreams struct {
   548  	Streams []LogStream `json:"streams"`
   549  }
   550  
   551  type LogStream struct {
   552  	Stream map[string]string `json:"stream"`
   553  	Values []LogValue        `json:"values"`
   554  }
   555  
   556  type LogValue struct {
   557  	Time time.Time
   558  	Line string
   559  }
   560  
   561  func (l *LogValue) MarshalJSON() ([]byte, error) {
   562  	line, err := json.Marshal(l.Line)
   563  	if err != nil {
   564  		return nil, err
   565  	}
   566  
   567  	return []byte(fmt.Sprintf(`["%d",%s]`, l.Time.UnixNano(), string(line))), nil
   568  }