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

     1  package loki
     2  
     3  /*
     4  https://grafana.com/docs/loki/latest/api/#get-lokiapiv1tail
     5  */
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"net/url"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	log "github.com/sirupsen/logrus"
    18  	tomb "gopkg.in/tomb.v2"
    19  	yaml "gopkg.in/yaml.v2"
    20  
    21  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    22  	lokiclient "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/loki/internal/lokiclient"
    23  	"github.com/crowdsecurity/crowdsec/pkg/types"
    24  )
    25  
    26  const (
    27  	readyTimeout time.Duration = 3 * time.Second
    28  	readyLoop    int           = 3
    29  	readySleep   time.Duration = 10 * time.Second
    30  	lokiLimit    int           = 100
    31  )
    32  
    33  var linesRead = prometheus.NewCounterVec(
    34  	prometheus.CounterOpts{
    35  		Name: "cs_lokisource_hits_total",
    36  		Help: "Total lines that were read.",
    37  	},
    38  	[]string{"source"})
    39  
    40  type LokiAuthConfiguration struct {
    41  	Username string `yaml:"username"`
    42  	Password string `yaml:"password"`
    43  }
    44  
    45  type LokiConfiguration struct {
    46  	URL                               string                `yaml:"url"`    // Loki url
    47  	Prefix                            string                `yaml:"prefix"` // Loki prefix
    48  	Query                             string                `yaml:"query"`  // LogQL query
    49  	Limit                             int                   `yaml:"limit"`  // Limit of logs to read
    50  	DelayFor                          time.Duration         `yaml:"delay_for"`
    51  	Since                             time.Duration         `yaml:"since"`
    52  	Headers                           map[string]string     `yaml:"headers"`        // HTTP headers for talking to Loki
    53  	WaitForReady                      time.Duration         `yaml:"wait_for_ready"` // Retry interval, default is 10 seconds
    54  	Auth                              LokiAuthConfiguration `yaml:"auth"`
    55  	MaxFailureDuration                time.Duration         `yaml:"max_failure_duration"` // Max duration of failure before stopping the source
    56  	configuration.DataSourceCommonCfg `yaml:",inline"`
    57  }
    58  
    59  type LokiSource struct {
    60  	metricsLevel int
    61  	Config       LokiConfiguration
    62  
    63  	Client *lokiclient.LokiClient
    64  
    65  	logger        *log.Entry
    66  	lokiWebsocket string
    67  }
    68  
    69  func (l *LokiSource) GetMetrics() []prometheus.Collector {
    70  	return []prometheus.Collector{linesRead}
    71  }
    72  
    73  func (l *LokiSource) GetAggregMetrics() []prometheus.Collector {
    74  	return []prometheus.Collector{linesRead}
    75  }
    76  
    77  func (l *LokiSource) UnmarshalConfig(yamlConfig []byte) error {
    78  	err := yaml.UnmarshalStrict(yamlConfig, &l.Config)
    79  	if err != nil {
    80  		return fmt.Errorf("cannot parse loki acquisition configuration: %w", err)
    81  	}
    82  
    83  	if l.Config.Query == "" {
    84  		return errors.New("loki query is mandatory")
    85  	}
    86  
    87  	if l.Config.WaitForReady == 0 {
    88  		l.Config.WaitForReady = 10 * time.Second
    89  	}
    90  
    91  	if l.Config.DelayFor < 0*time.Second || l.Config.DelayFor > 5*time.Second {
    92  		return errors.New("delay_for should be a value between 1s and 5s")
    93  	}
    94  
    95  	if l.Config.Mode == "" {
    96  		l.Config.Mode = configuration.TAIL_MODE
    97  	}
    98  	if l.Config.Prefix == "" {
    99  		l.Config.Prefix = "/"
   100  	}
   101  
   102  	if !strings.HasSuffix(l.Config.Prefix, "/") {
   103  		l.Config.Prefix += "/"
   104  	}
   105  
   106  	if l.Config.Limit == 0 {
   107  		l.Config.Limit = lokiLimit
   108  	}
   109  
   110  	if l.Config.Mode == configuration.TAIL_MODE {
   111  		l.logger.Infof("Resetting since")
   112  		l.Config.Since = 0
   113  	}
   114  
   115  	if l.Config.MaxFailureDuration == 0 {
   116  		l.Config.MaxFailureDuration = 30 * time.Second
   117  	}
   118  
   119  	return nil
   120  }
   121  
   122  func (l *LokiSource) Configure(config []byte, logger *log.Entry, MetricsLevel int) error {
   123  	l.Config = LokiConfiguration{}
   124  	l.logger = logger
   125  	l.metricsLevel = MetricsLevel
   126  	err := l.UnmarshalConfig(config)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	l.logger.Infof("Since value: %s", l.Config.Since.String())
   132  
   133  	clientConfig := lokiclient.Config{
   134  		LokiURL:         l.Config.URL,
   135  		Headers:         l.Config.Headers,
   136  		Limit:           l.Config.Limit,
   137  		Query:           l.Config.Query,
   138  		Since:           l.Config.Since,
   139  		Username:        l.Config.Auth.Username,
   140  		Password:        l.Config.Auth.Password,
   141  		FailMaxDuration: l.Config.MaxFailureDuration,
   142  	}
   143  
   144  	l.Client = lokiclient.NewLokiClient(clientConfig)
   145  	l.Client.Logger = logger.WithFields(log.Fields{"component": "lokiclient", "source": l.Config.URL})
   146  	return nil
   147  }
   148  
   149  func (l *LokiSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error {
   150  	l.logger = logger
   151  	l.Config = LokiConfiguration{}
   152  	l.Config.Mode = configuration.CAT_MODE
   153  	l.Config.Labels = labels
   154  	l.Config.UniqueId = uuid
   155  
   156  	u, err := url.Parse(dsn)
   157  	if err != nil {
   158  		return fmt.Errorf("while parsing dsn '%s': %w", dsn, err)
   159  	}
   160  	if u.Scheme != "loki" {
   161  		return fmt.Errorf("invalid DSN %s for loki source, must start with loki://", dsn)
   162  	}
   163  	if u.Host == "" {
   164  		return errors.New("empty loki host")
   165  	}
   166  	scheme := "http"
   167  
   168  	params := u.Query()
   169  	if q := params.Get("ssl"); q != "" {
   170  		scheme = "https"
   171  	}
   172  	if q := params.Get("query"); q != "" {
   173  		l.Config.Query = q
   174  	}
   175  	if w := params.Get("wait_for_ready"); w != "" {
   176  		l.Config.WaitForReady, err = time.ParseDuration(w)
   177  		if err != nil {
   178  			return err
   179  		}
   180  	} else {
   181  		l.Config.WaitForReady = 10 * time.Second
   182  	}
   183  
   184  	if d := params.Get("delay_for"); d != "" {
   185  		l.Config.DelayFor, err = time.ParseDuration(d)
   186  		if err != nil {
   187  			return fmt.Errorf("invalid duration: %w", err)
   188  		}
   189  		if l.Config.DelayFor < 0*time.Second || l.Config.DelayFor > 5*time.Second {
   190  			return errors.New("delay_for should be a value between 1s and 5s")
   191  		}
   192  	} else {
   193  		l.Config.DelayFor = 0 * time.Second
   194  	}
   195  
   196  	if s := params.Get("since"); s != "" {
   197  		l.Config.Since, err = time.ParseDuration(s)
   198  		if err != nil {
   199  			return fmt.Errorf("invalid since in dsn: %w", err)
   200  		}
   201  	}
   202  
   203  	if max_failure_duration := params.Get("max_failure_duration"); max_failure_duration != "" {
   204  		duration, err := time.ParseDuration(max_failure_duration)
   205  		if err != nil {
   206  			return fmt.Errorf("invalid max_failure_duration in dsn: %w", err)
   207  		}
   208  		l.Config.MaxFailureDuration = duration
   209  	} else {
   210  		l.Config.MaxFailureDuration = 5 * time.Second // for OneShot mode it doesn't make sense to have longer duration
   211  	}
   212  
   213  	if limit := params.Get("limit"); limit != "" {
   214  		limit, err := strconv.Atoi(limit)
   215  		if err != nil {
   216  			return fmt.Errorf("invalid limit in dsn: %w", err)
   217  		}
   218  		l.Config.Limit = limit
   219  	} else {
   220  		l.Config.Limit = 5000 // max limit allowed by loki
   221  	}
   222  
   223  	if logLevel := params.Get("log_level"); logLevel != "" {
   224  		level, err := log.ParseLevel(logLevel)
   225  		if err != nil {
   226  			return fmt.Errorf("invalid log_level in dsn: %w", err)
   227  		}
   228  		l.Config.LogLevel = &level
   229  		l.logger.Logger.SetLevel(level)
   230  	}
   231  
   232  	l.Config.URL = fmt.Sprintf("%s://%s", scheme, u.Host)
   233  	if u.User != nil {
   234  		l.Config.Auth.Username = u.User.Username()
   235  		l.Config.Auth.Password, _ = u.User.Password()
   236  	}
   237  
   238  	clientConfig := lokiclient.Config{
   239  		LokiURL:  l.Config.URL,
   240  		Headers:  l.Config.Headers,
   241  		Limit:    l.Config.Limit,
   242  		Query:    l.Config.Query,
   243  		Since:    l.Config.Since,
   244  		Username: l.Config.Auth.Username,
   245  		Password: l.Config.Auth.Password,
   246  		DelayFor: int(l.Config.DelayFor / time.Second),
   247  	}
   248  
   249  	l.Client = lokiclient.NewLokiClient(clientConfig)
   250  	l.Client.Logger = logger.WithFields(log.Fields{"component": "lokiclient", "source": l.Config.URL})
   251  
   252  	return nil
   253  }
   254  
   255  func (l *LokiSource) GetMode() string {
   256  	return l.Config.Mode
   257  }
   258  
   259  func (l *LokiSource) GetName() string {
   260  	return "loki"
   261  }
   262  
   263  // OneShotAcquisition reads a set of file and returns when done
   264  func (l *LokiSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error {
   265  	l.logger.Debug("Loki one shot acquisition")
   266  	l.Client.SetTomb(t)
   267  	readyCtx, cancel := context.WithTimeout(context.Background(), l.Config.WaitForReady)
   268  	defer cancel()
   269  	err := l.Client.Ready(readyCtx)
   270  	if err != nil {
   271  		return fmt.Errorf("loki is not ready: %w", err)
   272  	}
   273  
   274  	ctx, cancel := context.WithCancel(context.Background())
   275  	c := l.Client.QueryRange(ctx, false)
   276  
   277  	for {
   278  		select {
   279  		case <-t.Dying():
   280  			l.logger.Debug("Loki one shot acquisition stopped")
   281  			cancel()
   282  			return nil
   283  		case resp, ok := <-c:
   284  			if !ok {
   285  				l.logger.Info("Loki acquisition done, chan closed")
   286  				cancel()
   287  				return nil
   288  			}
   289  			for _, stream := range resp.Data.Result {
   290  				for _, entry := range stream.Entries {
   291  					l.readOneEntry(entry, l.Config.Labels, out)
   292  				}
   293  			}
   294  		}
   295  	}
   296  }
   297  
   298  func (l *LokiSource) readOneEntry(entry lokiclient.Entry, labels map[string]string, out chan types.Event) {
   299  	ll := types.Line{}
   300  	ll.Raw = entry.Line
   301  	ll.Time = entry.Timestamp
   302  	ll.Src = l.Config.URL
   303  	ll.Labels = labels
   304  	ll.Process = true
   305  	ll.Module = l.GetName()
   306  
   307  	if l.metricsLevel != configuration.METRICS_NONE {
   308  		linesRead.With(prometheus.Labels{"source": l.Config.URL}).Inc()
   309  	}
   310  	expectMode := types.LIVE
   311  	if l.Config.UseTimeMachine {
   312  		expectMode = types.TIMEMACHINE
   313  	}
   314  	out <- types.Event{
   315  		Line:       ll,
   316  		Process:    true,
   317  		Type:       types.LOG,
   318  		ExpectMode: expectMode,
   319  	}
   320  }
   321  
   322  func (l *LokiSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
   323  	l.Client.SetTomb(t)
   324  	readyCtx, cancel := context.WithTimeout(context.Background(), l.Config.WaitForReady)
   325  	defer cancel()
   326  	err := l.Client.Ready(readyCtx)
   327  	if err != nil {
   328  		return fmt.Errorf("loki is not ready: %w", err)
   329  	}
   330  	ll := l.logger.WithField("websocket_url", l.lokiWebsocket)
   331  	t.Go(func() error {
   332  		ctx, cancel := context.WithCancel(context.Background())
   333  		defer cancel()
   334  		respChan := l.Client.QueryRange(ctx, true)
   335  		if err != nil {
   336  			ll.Errorf("could not start loki tail: %s", err)
   337  			return fmt.Errorf("while starting loki tail: %w", err)
   338  		}
   339  		for {
   340  			select {
   341  			case resp, ok := <-respChan:
   342  				if !ok {
   343  					ll.Warnf("loki channel closed")
   344  					return err
   345  				}
   346  				for _, stream := range resp.Data.Result {
   347  					for _, entry := range stream.Entries {
   348  						l.readOneEntry(entry, l.Config.Labels, out)
   349  					}
   350  				}
   351  			case <-t.Dying():
   352  				return nil
   353  			}
   354  		}
   355  	})
   356  	return nil
   357  }
   358  
   359  func (l *LokiSource) CanRun() error {
   360  	return nil
   361  }
   362  
   363  func (l *LokiSource) GetUuid() string {
   364  	return l.Config.UniqueId
   365  }
   366  
   367  func (l *LokiSource) Dump() interface{} {
   368  	return l
   369  }
   370  
   371  // SupportedModes returns the supported modes by the acquisition module
   372  func (l *LokiSource) SupportedModes() []string {
   373  	return []string{configuration.TAIL_MODE, configuration.CAT_MODE}
   374  }