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 }