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 }