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

     1  package lokiclient
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"strconv"
    12  	"time"
    13  
    14  	"github.com/crowdsecurity/crowdsec/pkg/cwversion"
    15  	"github.com/gorilla/websocket"
    16  	"github.com/pkg/errors"
    17  	log "github.com/sirupsen/logrus"
    18  	"gopkg.in/tomb.v2"
    19  )
    20  
    21  type LokiClient struct {
    22  	Logger *log.Entry
    23  
    24  	config                Config
    25  	t                     *tomb.Tomb
    26  	fail_start            time.Time
    27  	currentTickerInterval time.Duration
    28  	requestHeaders        map[string]string
    29  }
    30  
    31  type Config struct {
    32  	LokiURL    string
    33  	LokiPrefix string
    34  	Query      string
    35  	Headers    map[string]string
    36  
    37  	Username string
    38  	Password string
    39  
    40  	Since time.Duration
    41  	Until time.Duration
    42  
    43  	FailMaxDuration time.Duration
    44  
    45  	DelayFor int
    46  	Limit    int
    47  }
    48  
    49  func updateURI(uri string, lq LokiQueryRangeResponse, infinite bool) string {
    50  	u, _ := url.Parse(uri)
    51  	queryParams := u.Query()
    52  
    53  	if len(lq.Data.Result) > 0 {
    54  		lastTs := lq.Data.Result[0].Entries[len(lq.Data.Result[0].Entries)-1].Timestamp
    55  		// +1 the last timestamp to avoid getting the same result again.
    56  		queryParams.Set("start", strconv.Itoa(int(lastTs.UnixNano()+1)))
    57  	}
    58  
    59  	if infinite {
    60  		queryParams.Set("end", strconv.Itoa(int(time.Now().UnixNano())))
    61  	}
    62  
    63  	u.RawQuery = queryParams.Encode()
    64  	return u.String()
    65  }
    66  
    67  func (lc *LokiClient) SetTomb(t *tomb.Tomb) {
    68  	lc.t = t
    69  }
    70  
    71  func (lc *LokiClient) resetFailStart() {
    72  	if !lc.fail_start.IsZero() {
    73  		log.Infof("loki is back after %s", time.Since(lc.fail_start))
    74  	}
    75  	lc.fail_start = time.Time{}
    76  }
    77  func (lc *LokiClient) shouldRetry() bool {
    78  	if lc.fail_start.IsZero() {
    79  		lc.Logger.Warningf("loki is not available, will retry for %s", lc.config.FailMaxDuration)
    80  		lc.fail_start = time.Now()
    81  		return true
    82  	}
    83  	if time.Since(lc.fail_start) > lc.config.FailMaxDuration {
    84  		lc.Logger.Errorf("loki didn't manage to recover after %s, giving up", lc.config.FailMaxDuration)
    85  		return false
    86  	}
    87  	return true
    88  }
    89  
    90  func (lc *LokiClient) increaseTicker(ticker *time.Ticker) {
    91  	maxTicker := 10 * time.Second
    92  	if lc.currentTickerInterval < maxTicker {
    93  		lc.currentTickerInterval *= 2
    94  		if lc.currentTickerInterval > maxTicker {
    95  			lc.currentTickerInterval = maxTicker
    96  		}
    97  		ticker.Reset(lc.currentTickerInterval)
    98  	}
    99  }
   100  
   101  func (lc *LokiClient) decreaseTicker(ticker *time.Ticker) {
   102  	minTicker := 100 * time.Millisecond
   103  	if lc.currentTickerInterval != minTicker {
   104  		lc.currentTickerInterval = minTicker
   105  		ticker.Reset(lc.currentTickerInterval)
   106  	}
   107  }
   108  
   109  func (lc *LokiClient) queryRange(uri string, ctx context.Context, c chan *LokiQueryRangeResponse, infinite bool) error {
   110  	lc.currentTickerInterval = 100 * time.Millisecond
   111  	ticker := time.NewTicker(lc.currentTickerInterval)
   112  	defer ticker.Stop()
   113  	for {
   114  		select {
   115  		case <-ctx.Done():
   116  			return ctx.Err()
   117  		case <-lc.t.Dying():
   118  			return lc.t.Err()
   119  		case <-ticker.C:
   120  			resp, err := lc.Get(uri)
   121  			if err != nil {
   122  				if ok := lc.shouldRetry(); !ok {
   123  					return errors.Wrapf(err, "error querying range")
   124  				} else {
   125  					lc.increaseTicker(ticker)
   126  					continue
   127  				}
   128  			}
   129  
   130  			if resp.StatusCode != http.StatusOK {
   131  				lc.Logger.Warnf("bad HTTP response code for query range: %d", resp.StatusCode)
   132  				body, _ := io.ReadAll(resp.Body)
   133  				resp.Body.Close()
   134  				if ok := lc.shouldRetry(); !ok {
   135  					return errors.Wrapf(err, "bad HTTP response code: %d: %s", resp.StatusCode, string(body))
   136  				} else {
   137  					lc.increaseTicker(ticker)
   138  					continue
   139  				}
   140  			}
   141  
   142  			var lq LokiQueryRangeResponse
   143  			if err := json.NewDecoder(resp.Body).Decode(&lq); err != nil {
   144  				resp.Body.Close()
   145  				if ok := lc.shouldRetry(); !ok {
   146  					return errors.Wrapf(err, "error decoding Loki response")
   147  				} else {
   148  					lc.increaseTicker(ticker)
   149  					continue
   150  				}
   151  			}
   152  			resp.Body.Close()
   153  			lc.Logger.Tracef("Got response: %+v", lq)
   154  			c <- &lq
   155  			lc.resetFailStart()
   156  			if !infinite && (len(lq.Data.Result) == 0 || len(lq.Data.Result[0].Entries) < lc.config.Limit) {
   157  				lc.Logger.Infof("Got less than %d results (%d), stopping", lc.config.Limit, len(lq.Data.Result))
   158  				close(c)
   159  				return nil
   160  			}
   161  			if len(lq.Data.Result) > 0 {
   162  				lc.Logger.Debugf("(timer:%v) %d results / %d entries result[0] (uri:%s)", lc.currentTickerInterval, len(lq.Data.Result), len(lq.Data.Result[0].Entries), uri)
   163  			} else {
   164  				lc.Logger.Debugf("(timer:%v) no results (uri:%s)", lc.currentTickerInterval, uri)
   165  			}
   166  			if infinite {
   167  				if len(lq.Data.Result) > 0 { //as long as we get results, we keep lowest ticker
   168  					lc.decreaseTicker(ticker)
   169  				} else {
   170  					lc.increaseTicker(ticker)
   171  				}
   172  			}
   173  
   174  			uri = updateURI(uri, lq, infinite)
   175  		}
   176  	}
   177  }
   178  
   179  func (lc *LokiClient) getURLFor(endpoint string, params map[string]string) string {
   180  	u, err := url.Parse(lc.config.LokiURL)
   181  	if err != nil {
   182  		return ""
   183  	}
   184  	queryParams := u.Query()
   185  	for k, v := range params {
   186  		queryParams.Set(k, v)
   187  	}
   188  	u.RawQuery = queryParams.Encode()
   189  
   190  	u.Path, err = url.JoinPath(lc.config.LokiPrefix, u.Path, endpoint)
   191  
   192  	if err != nil {
   193  		return ""
   194  	}
   195  
   196  	if endpoint == "loki/api/v1/tail" {
   197  		if u.Scheme == "http" {
   198  			u.Scheme = "ws"
   199  		} else {
   200  			u.Scheme = "wss"
   201  		}
   202  	}
   203  
   204  	return u.String()
   205  }
   206  
   207  func (lc *LokiClient) Ready(ctx context.Context) error {
   208  	tick := time.NewTicker(500 * time.Millisecond)
   209  	url := lc.getURLFor("ready", nil)
   210  	for {
   211  		select {
   212  		case <-ctx.Done():
   213  			tick.Stop()
   214  			return ctx.Err()
   215  		case <-lc.t.Dying():
   216  			tick.Stop()
   217  			return lc.t.Err()
   218  		case <-tick.C:
   219  			lc.Logger.Debug("Checking if Loki is ready")
   220  			resp, err := lc.Get(url)
   221  			if err != nil {
   222  				lc.Logger.Warnf("Error checking if Loki is ready: %s", err)
   223  				continue
   224  			}
   225  			_ = resp.Body.Close()
   226  			if resp.StatusCode != http.StatusOK {
   227  				lc.Logger.Debugf("Loki is not ready, status code: %d", resp.StatusCode)
   228  				continue
   229  			}
   230  			lc.Logger.Info("Loki is ready")
   231  			return nil
   232  		}
   233  	}
   234  }
   235  
   236  func (lc *LokiClient) Tail(ctx context.Context) (chan *LokiResponse, error) {
   237  	responseChan := make(chan *LokiResponse)
   238  	dialer := &websocket.Dialer{}
   239  	u := lc.getURLFor("loki/api/v1/tail", map[string]string{
   240  		"limit":     strconv.Itoa(lc.config.Limit),
   241  		"start":     strconv.Itoa(int(time.Now().Add(-lc.config.Since).UnixNano())),
   242  		"query":     lc.config.Query,
   243  		"delay_for": strconv.Itoa(lc.config.DelayFor),
   244  	})
   245  
   246  	lc.Logger.Debugf("Since: %s (%s)", lc.config.Since, time.Now().Add(-lc.config.Since))
   247  
   248  	if lc.config.Username != "" || lc.config.Password != "" {
   249  		dialer.Proxy = func(req *http.Request) (*url.URL, error) {
   250  			req.SetBasicAuth(lc.config.Username, lc.config.Password)
   251  			return nil, nil
   252  		}
   253  	}
   254  
   255  	requestHeader := http.Header{}
   256  	for k, v := range lc.requestHeaders {
   257  		requestHeader.Add(k, v)
   258  	}
   259  	lc.Logger.Infof("Connecting to %s", u)
   260  	conn, _, err := dialer.Dial(u, requestHeader)
   261  
   262  	if err != nil {
   263  		lc.Logger.Errorf("Error connecting to websocket, err: %s", err)
   264  		return responseChan, fmt.Errorf("error connecting to websocket")
   265  	}
   266  
   267  	lc.t.Go(func() error {
   268  		for {
   269  			jsonResponse := &LokiResponse{}
   270  			err = conn.ReadJSON(jsonResponse)
   271  
   272  			if err != nil {
   273  				lc.Logger.Errorf("Error reading from websocket: %s", err)
   274  				return fmt.Errorf("websocket error: %w", err)
   275  			}
   276  
   277  			responseChan <- jsonResponse
   278  		}
   279  	})
   280  
   281  	return responseChan, nil
   282  }
   283  
   284  func (lc *LokiClient) QueryRange(ctx context.Context, infinite bool) chan *LokiQueryRangeResponse {
   285  	url := lc.getURLFor("loki/api/v1/query_range", map[string]string{
   286  		"query":     lc.config.Query,
   287  		"start":     strconv.Itoa(int(time.Now().Add(-lc.config.Since).UnixNano())),
   288  		"end":       strconv.Itoa(int(time.Now().UnixNano())),
   289  		"limit":     strconv.Itoa(lc.config.Limit),
   290  		"direction": "forward",
   291  	})
   292  
   293  	c := make(chan *LokiQueryRangeResponse)
   294  
   295  	lc.Logger.Debugf("Since: %s (%s)", lc.config.Since, time.Now().Add(-lc.config.Since))
   296  
   297  	lc.Logger.Infof("Connecting to %s", url)
   298  	lc.t.Go(func() error {
   299  		return lc.queryRange(url, ctx, c, infinite)
   300  	})
   301  	return c
   302  }
   303  
   304  // Create a wrapper for http.Get to be able to set headers and auth
   305  func (lc *LokiClient) Get(url string) (*http.Response, error) {
   306  	request, err := http.NewRequest(http.MethodGet, url, nil)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	for k, v := range lc.requestHeaders {
   311  		request.Header.Add(k, v)
   312  	}
   313  	return http.DefaultClient.Do(request)
   314  }
   315  
   316  func NewLokiClient(config Config) *LokiClient {
   317  	headers := make(map[string]string)
   318  	for k, v := range config.Headers {
   319  		headers[k] = v
   320  	}
   321  	if config.Username != "" || config.Password != "" {
   322  		headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(config.Username+":"+config.Password))
   323  	}
   324  	headers["User-Agent"] = "Crowdsec " + cwversion.VersionStr()
   325  	return &LokiClient{Logger: log.WithField("component", "lokiclient"), config: config, requestHeaders: headers}
   326  }