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

     1  package cloudwatchacquisition
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/url"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/aws/aws-sdk-go/aws"
    14  	"github.com/aws/aws-sdk-go/aws/session"
    15  	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	log "github.com/sirupsen/logrus"
    18  	"gopkg.in/tomb.v2"
    19  	"gopkg.in/yaml.v2"
    20  
    21  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    22  	"github.com/crowdsecurity/crowdsec/pkg/parser"
    23  	"github.com/crowdsecurity/crowdsec/pkg/types"
    24  )
    25  
    26  var openedStreams = prometheus.NewGaugeVec(
    27  	prometheus.GaugeOpts{
    28  		Name: "cs_cloudwatch_openstreams_total",
    29  		Help: "Number of opened stream within group.",
    30  	},
    31  	[]string{"group"},
    32  )
    33  
    34  var streamIndexMutex = sync.Mutex{}
    35  
    36  var linesRead = prometheus.NewCounterVec(
    37  	prometheus.CounterOpts{
    38  		Name: "cs_cloudwatch_stream_hits_total",
    39  		Help: "Number of event read from stream.",
    40  	},
    41  	[]string{"group", "stream"},
    42  )
    43  
    44  // CloudwatchSource is the runtime instance keeping track of N streams within 1 cloudwatch group
    45  type CloudwatchSource struct {
    46  	metricsLevel int
    47  	Config       CloudwatchSourceConfiguration
    48  	/*runtime stuff*/
    49  	logger           *log.Entry
    50  	t                *tomb.Tomb
    51  	cwClient         *cloudwatchlogs.CloudWatchLogs
    52  	monitoredStreams []*LogStreamTailConfig
    53  	streamIndexes    map[string]string
    54  }
    55  
    56  // CloudwatchSourceConfiguration allows user to define one or more streams to monitor within a cloudwatch log group
    57  type CloudwatchSourceConfiguration struct {
    58  	configuration.DataSourceCommonCfg `yaml:",inline"`
    59  	GroupName                         string         `yaml:"group_name"`              //the group name to be monitored
    60  	StreamRegexp                      *string        `yaml:"stream_regexp,omitempty"` //allow to filter specific streams
    61  	StreamName                        *string        `yaml:"stream_name,omitempty"`
    62  	StartTime, EndTime                *time.Time     `yaml:"-"`
    63  	DescribeLogStreamsLimit           *int64         `yaml:"describelogstreams_limit,omitempty"` //batch size for DescribeLogStreamsPagesWithContext
    64  	GetLogEventsPagesLimit            *int64         `yaml:"getlogeventspages_limit,omitempty"`
    65  	PollNewStreamInterval             *time.Duration `yaml:"poll_new_stream_interval,omitempty"` //frequency at which we poll for new streams within the log group
    66  	MaxStreamAge                      *time.Duration `yaml:"max_stream_age,omitempty"`           //monitor only streams that have been updated within $duration
    67  	PollStreamInterval                *time.Duration `yaml:"poll_stream_interval,omitempty"`     //frequency at which we poll each stream
    68  	StreamReadTimeout                 *time.Duration `yaml:"stream_read_timeout,omitempty"`      //stop monitoring streams that haven't been updated within $duration, might be reopened later tho
    69  	AwsApiCallTimeout                 *time.Duration `yaml:"aws_api_timeout,omitempty"`
    70  	AwsProfile                        *string        `yaml:"aws_profile,omitempty"`
    71  	PrependCloudwatchTimestamp        *bool          `yaml:"prepend_cloudwatch_timestamp,omitempty"`
    72  	AwsConfigDir                      *string        `yaml:"aws_config_dir,omitempty"`
    73  	AwsRegion                         *string        `yaml:"aws_region,omitempty"`
    74  }
    75  
    76  // LogStreamTailConfig is the configuration for one given stream within one group
    77  type LogStreamTailConfig struct {
    78  	GroupName                  string
    79  	StreamName                 string
    80  	GetLogEventsPagesLimit     int64
    81  	PollStreamInterval         time.Duration
    82  	StreamReadTimeout          time.Duration
    83  	PrependCloudwatchTimestamp *bool
    84  	Labels                     map[string]string
    85  	logger                     *log.Entry
    86  	ExpectMode                 int
    87  	t                          tomb.Tomb
    88  	StartTime, EndTime         time.Time //only used for CatMode
    89  }
    90  
    91  var (
    92  	def_DescribeLogStreamsLimit = int64(50)
    93  	def_PollNewStreamInterval   = 10 * time.Second
    94  	def_MaxStreamAge            = 5 * time.Minute
    95  	def_PollStreamInterval      = 10 * time.Second
    96  	def_AwsApiCallTimeout       = 10 * time.Second
    97  	def_StreamReadTimeout       = 10 * time.Minute
    98  	def_PollDeadStreamInterval  = 10 * time.Second
    99  	def_GetLogEventsPagesLimit  = int64(1000)
   100  	def_AwsConfigDir            = ""
   101  )
   102  
   103  func (cw *CloudwatchSource) GetUuid() string {
   104  	return cw.Config.UniqueId
   105  }
   106  
   107  func (cw *CloudwatchSource) UnmarshalConfig(yamlConfig []byte) error {
   108  	cw.Config = CloudwatchSourceConfiguration{}
   109  	if err := yaml.UnmarshalStrict(yamlConfig, &cw.Config); err != nil {
   110  		return fmt.Errorf("cannot parse CloudwatchSource configuration: %w", err)
   111  	}
   112  
   113  	if len(cw.Config.GroupName) == 0 {
   114  		return fmt.Errorf("group_name is mandatory for CloudwatchSource")
   115  	}
   116  
   117  	if cw.Config.Mode == "" {
   118  		cw.Config.Mode = configuration.TAIL_MODE
   119  	}
   120  
   121  	if cw.Config.DescribeLogStreamsLimit == nil {
   122  		cw.Config.DescribeLogStreamsLimit = &def_DescribeLogStreamsLimit
   123  	}
   124  
   125  	if cw.Config.PollNewStreamInterval == nil {
   126  		cw.Config.PollNewStreamInterval = &def_PollNewStreamInterval
   127  	}
   128  
   129  	if cw.Config.MaxStreamAge == nil {
   130  		cw.Config.MaxStreamAge = &def_MaxStreamAge
   131  	}
   132  
   133  	if cw.Config.PollStreamInterval == nil {
   134  		cw.Config.PollStreamInterval = &def_PollStreamInterval
   135  	}
   136  
   137  	if cw.Config.StreamReadTimeout == nil {
   138  		cw.Config.StreamReadTimeout = &def_StreamReadTimeout
   139  	}
   140  
   141  	if cw.Config.GetLogEventsPagesLimit == nil {
   142  		cw.Config.GetLogEventsPagesLimit = &def_GetLogEventsPagesLimit
   143  	}
   144  
   145  	if cw.Config.AwsApiCallTimeout == nil {
   146  		cw.Config.AwsApiCallTimeout = &def_AwsApiCallTimeout
   147  	}
   148  
   149  	if cw.Config.AwsConfigDir == nil {
   150  		cw.Config.AwsConfigDir = &def_AwsConfigDir
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  func (cw *CloudwatchSource) Configure(yamlConfig []byte, logger *log.Entry, MetricsLevel int) error {
   157  	err := cw.UnmarshalConfig(yamlConfig)
   158  	if err != nil {
   159  		return err
   160  	}
   161  	cw.metricsLevel = MetricsLevel
   162  
   163  	cw.logger = logger.WithField("group", cw.Config.GroupName)
   164  
   165  	cw.logger.Debugf("Starting configuration for Cloudwatch group %s", cw.Config.GroupName)
   166  	cw.logger.Tracef("describelogstreams_limit set to %d", *cw.Config.DescribeLogStreamsLimit)
   167  	cw.logger.Tracef("poll_new_stream_interval set to %v", *cw.Config.PollNewStreamInterval)
   168  	cw.logger.Tracef("max_stream_age set to %v", *cw.Config.MaxStreamAge)
   169  	cw.logger.Tracef("poll_stream_interval set to %v", *cw.Config.PollStreamInterval)
   170  	cw.logger.Tracef("stream_read_timeout set to %v", *cw.Config.StreamReadTimeout)
   171  	cw.logger.Tracef("getlogeventspages_limit set to %v", *cw.Config.GetLogEventsPagesLimit)
   172  	cw.logger.Tracef("aws_api_timeout set to %v", *cw.Config.AwsApiCallTimeout)
   173  
   174  	if *cw.Config.MaxStreamAge > *cw.Config.StreamReadTimeout {
   175  		cw.logger.Warningf("max_stream_age > stream_read_timeout, stream might keep being opened/closed")
   176  	}
   177  	cw.logger.Tracef("aws_config_dir set to %s", *cw.Config.AwsConfigDir)
   178  
   179  	if *cw.Config.AwsConfigDir != "" {
   180  		_, err := os.Stat(*cw.Config.AwsConfigDir)
   181  		if err != nil {
   182  			cw.logger.Errorf("can't read aws_config_dir '%s' got err %s", *cw.Config.AwsConfigDir, err)
   183  			return fmt.Errorf("can't read aws_config_dir %s got err %s ", *cw.Config.AwsConfigDir, err)
   184  		}
   185  		os.Setenv("AWS_SDK_LOAD_CONFIG", "1")
   186  		//as aws sdk relies on $HOME, let's allow the user to override it :)
   187  		os.Setenv("AWS_CONFIG_FILE", fmt.Sprintf("%s/config", *cw.Config.AwsConfigDir))
   188  		os.Setenv("AWS_SHARED_CREDENTIALS_FILE", fmt.Sprintf("%s/credentials", *cw.Config.AwsConfigDir))
   189  	} else {
   190  		if cw.Config.AwsRegion == nil {
   191  			cw.logger.Errorf("aws_region is not specified, specify it or aws_config_dir")
   192  			return fmt.Errorf("aws_region is not specified, specify it or aws_config_dir")
   193  		}
   194  		os.Setenv("AWS_REGION", *cw.Config.AwsRegion)
   195  	}
   196  
   197  	if err := cw.newClient(); err != nil {
   198  		return err
   199  	}
   200  	cw.streamIndexes = make(map[string]string)
   201  
   202  	targetStream := "*"
   203  	if cw.Config.StreamRegexp != nil {
   204  		if _, err := regexp.Compile(*cw.Config.StreamRegexp); err != nil {
   205  			return fmt.Errorf("while compiling regexp '%s': %w", *cw.Config.StreamRegexp, err)
   206  		}
   207  		targetStream = *cw.Config.StreamRegexp
   208  	} else if cw.Config.StreamName != nil {
   209  		targetStream = *cw.Config.StreamName
   210  	}
   211  
   212  	cw.logger.Infof("Adding cloudwatch group '%s' (stream:%s) to datasources", cw.Config.GroupName, targetStream)
   213  	return nil
   214  }
   215  
   216  func (cw *CloudwatchSource) newClient() error {
   217  	var sess *session.Session
   218  
   219  	if cw.Config.AwsProfile != nil {
   220  		sess = session.Must(session.NewSessionWithOptions(session.Options{
   221  			SharedConfigState: session.SharedConfigEnable,
   222  			Profile:           *cw.Config.AwsProfile,
   223  		}))
   224  	} else {
   225  		sess = session.Must(session.NewSessionWithOptions(session.Options{
   226  			SharedConfigState: session.SharedConfigEnable,
   227  		}))
   228  	}
   229  
   230  	if sess == nil {
   231  		return fmt.Errorf("failed to create aws session")
   232  	}
   233  	if v := os.Getenv("AWS_ENDPOINT_FORCE"); v != "" {
   234  		cw.logger.Debugf("[testing] overloading endpoint with %s", v)
   235  		cw.cwClient = cloudwatchlogs.New(sess, aws.NewConfig().WithEndpoint(v))
   236  	} else {
   237  		cw.cwClient = cloudwatchlogs.New(sess)
   238  	}
   239  	if cw.cwClient == nil {
   240  		return fmt.Errorf("failed to create cloudwatch client")
   241  	}
   242  	return nil
   243  }
   244  
   245  func (cw *CloudwatchSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
   246  	cw.t = t
   247  	monitChan := make(chan LogStreamTailConfig)
   248  	t.Go(func() error {
   249  		return cw.LogStreamManager(monitChan, out)
   250  	})
   251  	return cw.WatchLogGroupForStreams(monitChan)
   252  }
   253  
   254  func (cw *CloudwatchSource) GetMetrics() []prometheus.Collector {
   255  	return []prometheus.Collector{linesRead, openedStreams}
   256  }
   257  
   258  func (cw *CloudwatchSource) GetAggregMetrics() []prometheus.Collector {
   259  	return []prometheus.Collector{linesRead, openedStreams}
   260  }
   261  
   262  func (cw *CloudwatchSource) GetMode() string {
   263  	return cw.Config.Mode
   264  }
   265  
   266  func (cw *CloudwatchSource) GetName() string {
   267  	return "cloudwatch"
   268  }
   269  
   270  func (cw *CloudwatchSource) CanRun() error {
   271  	return nil
   272  }
   273  
   274  func (cw *CloudwatchSource) Dump() interface{} {
   275  	return cw
   276  }
   277  
   278  func (cw *CloudwatchSource) WatchLogGroupForStreams(out chan LogStreamTailConfig) error {
   279  	cw.logger.Debugf("Starting to watch group (interval:%s)", cw.Config.PollNewStreamInterval)
   280  	ticker := time.NewTicker(*cw.Config.PollNewStreamInterval)
   281  	var startFrom *string
   282  
   283  	for {
   284  		select {
   285  		case <-cw.t.Dying():
   286  			cw.logger.Infof("stopping group watch")
   287  			return nil
   288  		case <-ticker.C:
   289  			hasMoreStreams := true
   290  			startFrom = nil
   291  			for hasMoreStreams {
   292  				cw.logger.Tracef("doing the call to DescribeLogStreamsPagesWithContext")
   293  
   294  				ctx := context.Background()
   295  				//there can be a lot of streams in a group, and we're only interested in those recently written to, so we sort by LastEventTime
   296  				err := cw.cwClient.DescribeLogStreamsPagesWithContext(
   297  					ctx,
   298  					&cloudwatchlogs.DescribeLogStreamsInput{
   299  						LogGroupName: aws.String(cw.Config.GroupName),
   300  						Descending:   aws.Bool(true),
   301  						NextToken:    startFrom,
   302  						OrderBy:      aws.String(cloudwatchlogs.OrderByLastEventTime),
   303  						Limit:        cw.Config.DescribeLogStreamsLimit,
   304  					},
   305  					func(page *cloudwatchlogs.DescribeLogStreamsOutput, lastPage bool) bool {
   306  						cw.logger.Tracef("in helper of DescribeLogStreamsPagesWithContext")
   307  						for _, event := range page.LogStreams {
   308  							startFrom = page.NextToken
   309  							//we check if the stream has been written to recently enough to be monitored
   310  							if event.LastIngestionTime != nil {
   311  								//aws uses millisecond since the epoch
   312  								oldest := time.Now().UTC().Add(-*cw.Config.MaxStreamAge)
   313  								//TBD : verify that this is correct : Unix 2nd arg expects Nanoseconds, and have a code that is more explicit.
   314  								LastIngestionTime := time.Unix(0, *event.LastIngestionTime*int64(time.Millisecond))
   315  								if LastIngestionTime.Before(oldest) {
   316  									cw.logger.Tracef("stop iteration, %s reached oldest age, stop (%s < %s)", *event.LogStreamName, LastIngestionTime, time.Now().UTC().Add(-*cw.Config.MaxStreamAge))
   317  									hasMoreStreams = false
   318  									return false
   319  								}
   320  								cw.logger.Tracef("stream %s is elligible for monitoring", *event.LogStreamName)
   321  								//the stream has been updated recently, check if we should monitor it
   322  								var expectMode int
   323  								if !cw.Config.UseTimeMachine {
   324  									expectMode = types.LIVE
   325  								} else {
   326  									expectMode = types.TIMEMACHINE
   327  								}
   328  								monitorStream := LogStreamTailConfig{
   329  									GroupName:                  cw.Config.GroupName,
   330  									StreamName:                 *event.LogStreamName,
   331  									GetLogEventsPagesLimit:     *cw.Config.GetLogEventsPagesLimit,
   332  									PollStreamInterval:         *cw.Config.PollStreamInterval,
   333  									StreamReadTimeout:          *cw.Config.StreamReadTimeout,
   334  									PrependCloudwatchTimestamp: cw.Config.PrependCloudwatchTimestamp,
   335  									ExpectMode:                 expectMode,
   336  									Labels:                     cw.Config.Labels,
   337  								}
   338  								out <- monitorStream
   339  							}
   340  						}
   341  						if lastPage {
   342  							cw.logger.Tracef("reached last page")
   343  							hasMoreStreams = false
   344  						}
   345  						return true
   346  					},
   347  				)
   348  				if err != nil {
   349  					return fmt.Errorf("while describing group %s: %w", cw.Config.GroupName, err)
   350  				}
   351  				cw.logger.Tracef("after DescribeLogStreamsPagesWithContext")
   352  			}
   353  		}
   354  	}
   355  }
   356  
   357  // LogStreamManager receives the potential streams to monitor, and starts a go routine when needed
   358  func (cw *CloudwatchSource) LogStreamManager(in chan LogStreamTailConfig, outChan chan types.Event) error {
   359  
   360  	cw.logger.Debugf("starting to monitor streams for %s", cw.Config.GroupName)
   361  	pollDeadStreamInterval := time.NewTicker(def_PollDeadStreamInterval)
   362  
   363  	for {
   364  		select {
   365  		case newStream := <-in: //nolint:govet // copylocks won't matter if the tomb is not initialized
   366  			shouldCreate := true
   367  			cw.logger.Tracef("received new streams to monitor : %s/%s", newStream.GroupName, newStream.StreamName)
   368  
   369  			if cw.Config.StreamName != nil && newStream.StreamName != *cw.Config.StreamName {
   370  				cw.logger.Tracef("stream %s != %s", newStream.StreamName, *cw.Config.StreamName)
   371  				continue
   372  			}
   373  
   374  			if cw.Config.StreamRegexp != nil {
   375  				match, err := regexp.MatchString(*cw.Config.StreamRegexp, newStream.StreamName)
   376  				if err != nil {
   377  					cw.logger.Warningf("invalid regexp : %s", err)
   378  				} else if !match {
   379  					cw.logger.Tracef("stream %s doesn't match %s", newStream.StreamName, *cw.Config.StreamRegexp)
   380  					continue
   381  				}
   382  			}
   383  
   384  			for idx, stream := range cw.monitoredStreams {
   385  				if newStream.GroupName == stream.GroupName && newStream.StreamName == stream.StreamName {
   386  					//stream exists, but is dead, remove it from list
   387  					if !stream.t.Alive() {
   388  						cw.logger.Debugf("stream %s already exists, but is dead", newStream.StreamName)
   389  						cw.monitoredStreams = append(cw.monitoredStreams[:idx], cw.monitoredStreams[idx+1:]...)
   390  						if cw.metricsLevel != configuration.METRICS_NONE {
   391  							openedStreams.With(prometheus.Labels{"group": newStream.GroupName}).Dec()
   392  						}
   393  						break
   394  					}
   395  					shouldCreate = false
   396  					break
   397  				}
   398  			}
   399  
   400  			//let's start watching this stream
   401  			if shouldCreate {
   402  				if cw.metricsLevel != configuration.METRICS_NONE {
   403  					openedStreams.With(prometheus.Labels{"group": newStream.GroupName}).Inc()
   404  				}
   405  				newStream.t = tomb.Tomb{}
   406  				newStream.logger = cw.logger.WithFields(log.Fields{"stream": newStream.StreamName})
   407  				cw.logger.Debugf("starting tail of stream %s", newStream.StreamName)
   408  				newStream.t.Go(func() error {
   409  					return cw.TailLogStream(&newStream, outChan)
   410  				})
   411  				cw.monitoredStreams = append(cw.monitoredStreams, &newStream)
   412  			}
   413  		case <-pollDeadStreamInterval.C:
   414  			newMonitoredStreams := cw.monitoredStreams[:0]
   415  			for idx, stream := range cw.monitoredStreams {
   416  				if !cw.monitoredStreams[idx].t.Alive() {
   417  					cw.logger.Debugf("remove dead stream %s", stream.StreamName)
   418  					if cw.metricsLevel != configuration.METRICS_NONE {
   419  						openedStreams.With(prometheus.Labels{"group": cw.monitoredStreams[idx].GroupName}).Dec()
   420  					}
   421  				} else {
   422  					newMonitoredStreams = append(newMonitoredStreams, stream)
   423  				}
   424  			}
   425  			cw.monitoredStreams = newMonitoredStreams
   426  		case <-cw.t.Dying():
   427  			cw.logger.Infof("LogStreamManager for %s is dying, %d alive streams", cw.Config.GroupName, len(cw.monitoredStreams))
   428  			for idx, stream := range cw.monitoredStreams {
   429  				if cw.monitoredStreams[idx].t.Alive() {
   430  					cw.logger.Debugf("killing stream %s", stream.StreamName)
   431  					cw.monitoredStreams[idx].t.Kill(nil)
   432  					if err := cw.monitoredStreams[idx].t.Wait(); err != nil {
   433  						cw.logger.Debugf("error while waiting for death of %s : %s", stream.StreamName, err)
   434  					}
   435  				}
   436  			}
   437  			cw.monitoredStreams = nil
   438  			cw.logger.Debugf("routine cleanup done, return")
   439  			return nil
   440  		}
   441  	}
   442  }
   443  
   444  func (cw *CloudwatchSource) TailLogStream(cfg *LogStreamTailConfig, outChan chan types.Event) error {
   445  	var startFrom *string
   446  	lastReadMessage := time.Now().UTC()
   447  	ticker := time.NewTicker(cfg.PollStreamInterval)
   448  	//resume at existing index if we already had
   449  	streamIndexMutex.Lock()
   450  	v := cw.streamIndexes[cfg.GroupName+"+"+cfg.StreamName]
   451  	streamIndexMutex.Unlock()
   452  	if v != "" {
   453  		cfg.logger.Debugf("restarting on index %s", v)
   454  		startFrom = &v
   455  	}
   456  	/*during first run, we want to avoid reading any message, but just get a token.
   457  	if we don't, we might end up sending the same item several times. hence the 'startup' hack */
   458  	for {
   459  		select {
   460  		case <-ticker.C:
   461  			cfg.logger.Tracef("entering loop")
   462  			hasMorePages := true
   463  			for hasMorePages {
   464  				/*for the first call, we only consume the last item*/
   465  				cfg.logger.Tracef("calling GetLogEventsPagesWithContext")
   466  				ctx := context.Background()
   467  				err := cw.cwClient.GetLogEventsPagesWithContext(ctx,
   468  					&cloudwatchlogs.GetLogEventsInput{
   469  						Limit:         aws.Int64(cfg.GetLogEventsPagesLimit),
   470  						LogGroupName:  aws.String(cfg.GroupName),
   471  						LogStreamName: aws.String(cfg.StreamName),
   472  						NextToken:     startFrom,
   473  						StartFromHead: aws.Bool(true),
   474  					},
   475  					func(page *cloudwatchlogs.GetLogEventsOutput, lastPage bool) bool {
   476  						cfg.logger.Tracef("%d results, last:%t", len(page.Events), lastPage)
   477  						startFrom = page.NextForwardToken
   478  						if page.NextForwardToken != nil {
   479  							streamIndexMutex.Lock()
   480  							cw.streamIndexes[cfg.GroupName+"+"+cfg.StreamName] = *page.NextForwardToken
   481  							streamIndexMutex.Unlock()
   482  						}
   483  						if lastPage { /*wait another ticker to check on new log availability*/
   484  							cfg.logger.Tracef("last page")
   485  							hasMorePages = false
   486  						}
   487  						if len(page.Events) > 0 {
   488  							lastReadMessage = time.Now().UTC()
   489  						}
   490  						for _, event := range page.Events {
   491  							evt, err := cwLogToEvent(event, cfg)
   492  							if err != nil {
   493  								cfg.logger.Warningf("cwLogToEvent error, discarded event : %s", err)
   494  							} else {
   495  								cfg.logger.Debugf("pushing message : %s", evt.Line.Raw)
   496  								if cw.metricsLevel != configuration.METRICS_NONE {
   497  									linesRead.With(prometheus.Labels{"group": cfg.GroupName, "stream": cfg.StreamName}).Inc()
   498  								}
   499  								outChan <- evt
   500  							}
   501  						}
   502  						return true
   503  					},
   504  				)
   505  				if err != nil {
   506  					newerr := fmt.Errorf("while reading %s/%s: %w", cfg.GroupName, cfg.StreamName, err)
   507  					cfg.logger.Warningf("err : %s", newerr)
   508  					return newerr
   509  				}
   510  				cfg.logger.Tracef("done reading GetLogEventsPagesWithContext")
   511  				if time.Since(lastReadMessage) > cfg.StreamReadTimeout {
   512  					cfg.logger.Infof("%s/%s reached timeout (%s) (last message was %s)", cfg.GroupName, cfg.StreamName, time.Since(lastReadMessage),
   513  						lastReadMessage)
   514  					return nil
   515  				}
   516  			}
   517  		case <-cfg.t.Dying():
   518  			cfg.logger.Infof("logstream tail stopping")
   519  			return fmt.Errorf("killed")
   520  		}
   521  	}
   522  }
   523  
   524  func (cw *CloudwatchSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error {
   525  	cw.logger = logger
   526  
   527  	dsn = strings.TrimPrefix(dsn, cw.GetName()+"://")
   528  	args := strings.Split(dsn, "?")
   529  	if len(args) != 2 {
   530  		return fmt.Errorf("query is mandatory (at least start_date and end_date or backlog)")
   531  	}
   532  	frags := strings.Split(args[0], ":")
   533  	if len(frags) != 2 {
   534  		return fmt.Errorf("cloudwatch path must contain group and stream : /my/group/name:stream/name")
   535  	}
   536  	cw.Config.GroupName = frags[0]
   537  	cw.Config.StreamName = &frags[1]
   538  	cw.Config.Labels = labels
   539  	cw.Config.UniqueId = uuid
   540  
   541  	u, err := url.ParseQuery(args[1])
   542  	if err != nil {
   543  		return fmt.Errorf("while parsing %s: %w", dsn, err)
   544  	}
   545  
   546  	for k, v := range u {
   547  		switch k {
   548  		case "log_level":
   549  			if len(v) != 1 {
   550  				return fmt.Errorf("expected zero or one value for 'log_level'")
   551  			}
   552  			lvl, err := log.ParseLevel(v[0])
   553  			if err != nil {
   554  				return fmt.Errorf("unknown level %s: %w", v[0], err)
   555  			}
   556  			cw.logger.Logger.SetLevel(lvl)
   557  
   558  		case "profile":
   559  			if len(v) != 1 {
   560  				return fmt.Errorf("expected zero or one value for 'profile'")
   561  			}
   562  			awsprof := v[0]
   563  			cw.Config.AwsProfile = &awsprof
   564  			cw.logger.Debugf("profile set to '%s'", *cw.Config.AwsProfile)
   565  		case "start_date":
   566  			if len(v) != 1 {
   567  				return fmt.Errorf("expected zero or one argument for 'start_date'")
   568  			}
   569  			//let's reuse our parser helper so that a ton of date formats are supported
   570  			strdate, startDate := parser.GenDateParse(v[0])
   571  			cw.logger.Debugf("parsed '%s' as '%s'", v[0], strdate)
   572  			cw.Config.StartTime = &startDate
   573  		case "end_date":
   574  			if len(v) != 1 {
   575  				return fmt.Errorf("expected zero or one argument for 'end_date'")
   576  			}
   577  			//let's reuse our parser helper so that a ton of date formats are supported
   578  			strdate, endDate := parser.GenDateParse(v[0])
   579  			cw.logger.Debugf("parsed '%s' as '%s'", v[0], strdate)
   580  			cw.Config.EndTime = &endDate
   581  		case "backlog":
   582  			if len(v) != 1 {
   583  				return fmt.Errorf("expected zero or one argument for 'backlog'")
   584  			}
   585  			//let's reuse our parser helper so that a ton of date formats are supported
   586  			duration, err := time.ParseDuration(v[0])
   587  			if err != nil {
   588  				return fmt.Errorf("unable to parse '%s' as duration: %w", v[0], err)
   589  			}
   590  			cw.logger.Debugf("parsed '%s' as '%s'", v[0], duration)
   591  			start := time.Now().UTC().Add(-duration)
   592  			cw.Config.StartTime = &start
   593  			end := time.Now().UTC()
   594  			cw.Config.EndTime = &end
   595  		default:
   596  			return fmt.Errorf("unexpected argument %s", k)
   597  		}
   598  	}
   599  	cw.logger.Tracef("host=%s", cw.Config.GroupName)
   600  	cw.logger.Tracef("stream=%s", *cw.Config.StreamName)
   601  	cw.Config.GetLogEventsPagesLimit = &def_GetLogEventsPagesLimit
   602  
   603  	if err := cw.newClient(); err != nil {
   604  		return err
   605  	}
   606  
   607  	if cw.Config.StreamName == nil || cw.Config.GroupName == "" {
   608  		return fmt.Errorf("missing stream or group name")
   609  	}
   610  	if cw.Config.StartTime == nil || cw.Config.EndTime == nil {
   611  		return fmt.Errorf("start_date and end_date or backlog are mandatory in one-shot mode")
   612  	}
   613  
   614  	cw.Config.Mode = configuration.CAT_MODE
   615  	cw.streamIndexes = make(map[string]string)
   616  	cw.t = &tomb.Tomb{}
   617  	return nil
   618  }
   619  
   620  func (cw *CloudwatchSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error {
   621  	//StreamName string, Start time.Time, End time.Time
   622  	config := LogStreamTailConfig{
   623  		GroupName:              cw.Config.GroupName,
   624  		StreamName:             *cw.Config.StreamName,
   625  		StartTime:              *cw.Config.StartTime,
   626  		EndTime:                *cw.Config.EndTime,
   627  		GetLogEventsPagesLimit: *cw.Config.GetLogEventsPagesLimit,
   628  		logger: cw.logger.WithFields(log.Fields{
   629  			"group":  cw.Config.GroupName,
   630  			"stream": *cw.Config.StreamName,
   631  		}),
   632  		Labels:     cw.Config.Labels,
   633  		ExpectMode: types.TIMEMACHINE,
   634  	}
   635  	return cw.CatLogStream(&config, out)
   636  }
   637  
   638  func (cw *CloudwatchSource) CatLogStream(cfg *LogStreamTailConfig, outChan chan types.Event) error {
   639  	var startFrom *string
   640  	var head = true
   641  	/*convert the times*/
   642  	startTime := cfg.StartTime.UTC().Unix() * 1000
   643  	endTime := cfg.EndTime.UTC().Unix() * 1000
   644  	hasMoreEvents := true
   645  	for hasMoreEvents {
   646  		select {
   647  		default:
   648  			cfg.logger.Tracef("Calling GetLogEventsPagesWithContext(%s, %s), startTime:%d / endTime:%d",
   649  				cfg.GroupName, cfg.StreamName, startTime, endTime)
   650  			cfg.logger.Tracef("startTime:%s / endTime:%s", cfg.StartTime, cfg.EndTime)
   651  			if startFrom != nil {
   652  				cfg.logger.Tracef("next_token: %s", *startFrom)
   653  			}
   654  			ctx := context.Background()
   655  			err := cw.cwClient.GetLogEventsPagesWithContext(ctx,
   656  				&cloudwatchlogs.GetLogEventsInput{
   657  					Limit:         aws.Int64(10),
   658  					LogGroupName:  aws.String(cfg.GroupName),
   659  					LogStreamName: aws.String(cfg.StreamName),
   660  					StartTime:     aws.Int64(startTime),
   661  					EndTime:       aws.Int64(endTime),
   662  					StartFromHead: &head,
   663  					NextToken:     startFrom,
   664  				},
   665  				func(page *cloudwatchlogs.GetLogEventsOutput, lastPage bool) bool {
   666  					cfg.logger.Tracef("in GetLogEventsPagesWithContext handker (%d events) (last:%t)", len(page.Events), lastPage)
   667  					for _, event := range page.Events {
   668  						evt, err := cwLogToEvent(event, cfg)
   669  						if err != nil {
   670  							cfg.logger.Warningf("discard event : %s", err)
   671  						}
   672  						cfg.logger.Debugf("pushing message : %s", evt.Line.Raw)
   673  						outChan <- evt
   674  					}
   675  					if startFrom != nil && *page.NextForwardToken == *startFrom {
   676  						cfg.logger.Debugf("reached end of available events")
   677  						hasMoreEvents = false
   678  						return false
   679  					}
   680  					startFrom = page.NextForwardToken
   681  					return true
   682  				},
   683  			)
   684  			if err != nil {
   685  				return fmt.Errorf("while reading logs from %s/%s: %w", cfg.GroupName, cfg.StreamName, err)
   686  			}
   687  			cfg.logger.Tracef("after GetLogEventsPagesWithContext")
   688  		case <-cw.t.Dying():
   689  			cfg.logger.Warningf("cat stream killed")
   690  			return nil
   691  		}
   692  	}
   693  	cfg.logger.Tracef("CatLogStream out")
   694  
   695  	return nil
   696  }
   697  
   698  func cwLogToEvent(log *cloudwatchlogs.OutputLogEvent, cfg *LogStreamTailConfig) (types.Event, error) {
   699  	l := types.Line{}
   700  	evt := types.Event{}
   701  	if log.Message == nil {
   702  		return evt, fmt.Errorf("nil message")
   703  	}
   704  	msg := *log.Message
   705  	if cfg.PrependCloudwatchTimestamp != nil && *cfg.PrependCloudwatchTimestamp {
   706  		eventTimestamp := time.Unix(0, *log.Timestamp*int64(time.Millisecond))
   707  		msg = eventTimestamp.String() + " " + msg
   708  	}
   709  	l.Raw = msg
   710  	l.Labels = cfg.Labels
   711  	l.Time = time.Now().UTC()
   712  	l.Src = fmt.Sprintf("%s/%s", cfg.GroupName, cfg.StreamName)
   713  	l.Process = true
   714  	l.Module = "cloudwatch"
   715  	evt.Line = l
   716  	evt.Process = true
   717  	evt.Type = types.LOG
   718  	evt.ExpectMode = cfg.ExpectMode
   719  	cfg.logger.Debugf("returned event labels : %+v", evt.Line.Labels)
   720  	return evt, nil
   721  }