AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template for groundcover CloudWatch logs ingestion via Firehose with multiple log groups support'

Parameters:
  GroundcoverEndpoint:
    Type: String
    Description: 'groundcover Firehose logs endpoint URL'
    Default: 'https://ingest.groundcover.com/v1/firehose/logs'
  
  GroundcoverApiToken:
    Type: String
    Description: 'groundcover API token for authentication'
    NoEcho: true
  
  EnvironmentName:
    Type: String
    Description: 'Environment name that will show up in groundcover application'
    Default: 'production'
  
  LogGroupNames:
    Type: CommaDelimitedList
    Description: 'Comma-separated list of CloudWatch log group names to monitor (e.g., /aws/lambda/func1,/aws/lambda/func2,/aws/ec2/instance1)'
    Default: '/aws/lambda/example-function'
  
  FirehoseStreamName:
    Type: String
    Description: 'Name for the Firehose delivery stream'
    Default: 'PUT-Groundcover-logs'
  
  BackupBucketName:
    Type: String
    Description: 'S3 bucket name for Firehose backup (will be created if not exists)'
    Default: 'groundcover-firehose-backup'

Resources:
  # S3 Bucket for Firehose backup
  BackupBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${BackupBucketName}-${AWS::AccountId}'
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  # IAM Role for CloudWatch Logs to assume
  CloudWatchLogsRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-cloudwatch-logs-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: Statement1
            Effect: Allow
            Principal:
              Service: logs.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: FirehosePutPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - firehose:PutRecord
                  - firehose:PutRecordBatch
                Resource: !GetAtt FirehoseDeliveryStream.Arn

  # IAM Role for Firehose delivery stream
  FirehoseDeliveryRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-firehose-delivery-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: firehose.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: S3BackupPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:AbortMultipartUpload
                  - s3:GetBucketLocation
                  - s3:GetObject
                  - s3:ListBucket
                  - s3:ListBucketMultipartUploads
                  - s3:PutObject
                Resource:
                  - !GetAtt BackupBucket.Arn
                  - !Sub '${BackupBucket.Arn}/*'

  # Firehose Delivery Stream
  FirehoseDeliveryStream:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties:
      DeliveryStreamName: !Ref FirehoseStreamName
      DeliveryStreamType: DirectPut
      HttpEndpointDestinationConfiguration:
        EndpointConfiguration:
          Url: !Ref GroundcoverEndpoint
          Name: groundcover-endpoint
          AccessKey: !Ref GroundcoverApiToken
        RequestConfiguration:
          ContentEncoding: GZIP
          CommonAttributes:
            - AttributeName: env_name
              AttributeValue: !Ref EnvironmentName
        S3BackupMode: FailedDataOnly
        S3Configuration:
          BucketARN: !GetAtt BackupBucket.Arn
          BufferingHints:
            IntervalInSeconds: 60
            SizeInMBs: 5
          CompressionFormat: GZIP
          Prefix: 'failed-logs/'
          RoleARN: !GetAtt FirehoseDeliveryRole.Arn
        RetryOptions:
          DurationInSeconds: 300
        BufferingHints:
          IntervalInSeconds: 60
          SizeInMBs: 5

  # CloudWatch Log Group for Firehose
  FirehoseLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/kinesisfirehose/${FirehoseStreamName}'
      RetentionInDays: 14

  # Create subscription filters for each log group
  # This uses a custom resource to create multiple subscription filters
  # DependsOn ensures this runs after all other resources are ready
  SubscriptionFilterManager:
    Type: AWS::CloudFormation::CustomResource
    DependsOn: 
      - FirehoseDeliveryStream
      - CloudWatchLogsRole
      - SubscriptionFilterFunction
    Properties:
      ServiceToken: !GetAtt SubscriptionFilterFunction.Arn
      LogGroupNames: !Ref LogGroupNames
      FirehoseArn: !GetAtt FirehoseDeliveryStream.Arn
      RoleArn: !GetAtt CloudWatchLogsRole.Arn
      StackName: !Ref AWS::StackName

  # Lambda function to create subscription filters
  SubscriptionFilterFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-subscription-filter-manager'
      Runtime: python3.9
      Handler: index.lambda_handler
      Role: !GetAtt SubscriptionFilterRole.Arn
      Timeout: 300
      MemorySize: 256
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse
          import time
          
          def lambda_handler(event, context):
              logs_client = boto3.client('logs')
              
              try:
                  if event['RequestType'] == 'Delete':
                      # On delete, remove all subscription filters
                      log_group_names = event['ResourceProperties']['LogGroupNames']
                      for log_group_name in log_group_names:
                          try:
                              # List and delete subscription filters
                              response = logs_client.describe_subscription_filters(
                                  logGroupName=log_group_name
                              )
                              for filter_info in response['subscriptionFilters']:
                                  if event['ResourceProperties']['StackName'] in filter_info['filterName']:
                                      logs_client.delete_subscription_filter(
                                          logGroupName=log_group_name,
                                          filterName=filter_info['filterName']
                                      )
                          except logs_client.exceptions.ResourceNotFoundException:
                              pass  # Log group doesn't exist
                      
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                      return
                  
                  # Create subscription filters
                  log_group_names = event['ResourceProperties']['LogGroupNames']
                  firehose_arn = event['ResourceProperties']['FirehoseArn']
                  role_arn = event['ResourceProperties']['RoleArn']
                  stack_name = event['ResourceProperties']['StackName']
                  
                  created_filters = []
                  for log_group_name in log_group_names:
                      try:
                          filter_name = f"{stack_name}-filter-{log_group_name.replace('/', '-').replace('*', 'wildcard')}"
                          
                          # Check if log group exists first (with retry and exponential backoff)
                          log_group_exists = False
                          max_attempts = 5
                          base_delay = 2
                          
                          for attempt in range(max_attempts):
                              try:
                                  response = logs_client.describe_log_groups(logGroupNamePrefix=log_group_name)
                                  if any(lg['logGroupName'] == log_group_name for lg in response['logGroups']):
                                      log_group_exists = True
                                      print(f"Log group {log_group_name} found on attempt {attempt + 1}")
                                      break
                              except Exception as e:
                                  print(f"Attempt {attempt + 1}: Log group {log_group_name} check failed: {str(e)}")
                                  if attempt < max_attempts - 1:
                                      delay = base_delay * (2 ** attempt)  # Exponential backoff: 2, 4, 8, 16 seconds
                                      print(f"Waiting {delay} seconds before retry...")
                                      time.sleep(delay)
                              
                          if not log_group_exists:
                              print(f"Log group {log_group_name} does not exist after {max_attempts} attempts, skipping")
                              continue
                          
                          # Check if filter already exists
                          try:
                              existing_filters = logs_client.describe_subscription_filters(
                                  logGroupName=log_group_name
                              )
                              for existing_filter in existing_filters['subscriptionFilters']:
                                  if existing_filter['filterName'] == filter_name:
                                      print(f"Filter {filter_name} already exists for {log_group_name}")
                                      created_filters.append(log_group_name)
                                      continue
                          except logs_client.exceptions.ResourceNotFoundException:
                              pass
                          
                          # Create subscription filter
                          logs_client.put_subscription_filter(
                              logGroupName=log_group_name,
                              filterName=filter_name,
                              filterPattern='',  # Empty pattern means all logs
                              destinationArn=firehose_arn,
                              roleArn=role_arn
                          )
                          created_filters.append(log_group_name)
                          print(f"Created subscription filter for {log_group_name}")
                          
                      except Exception as e:
                          print(f"Error creating filter for {log_group_name}: {str(e)}")
                          # Continue with other log groups even if one fails
                  
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {
                      'CreatedFilters': len(created_filters),
                      'LogGroups': created_filters
                  })
                  
              except Exception as e:
                  print(f"Error: {str(e)}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})

  # IAM Role for Subscription Filter Lambda
  SubscriptionFilterRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-subscription-filter-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: SubscriptionFilterPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:DescribeLogGroups
                  - logs:DescribeSubscriptionFilters
                  - logs:PutSubscriptionFilter
                  - logs:DeleteSubscriptionFilter
                Resource: '*'
              - Effect: Allow
                Action:
                  - iam:PassRole
                Resource: !GetAtt CloudWatchLogsRole.Arn

Outputs:
  FirehoseStreamArn:
    Description: 'ARN of the Firehose delivery stream'
    Value: !GetAtt FirehoseDeliveryStream.Arn
    Export:
      Name: !Sub '${AWS::StackName}-FirehoseStreamArn'
  
  CloudWatchLogsRoleArn:
    Description: 'ARN of the IAM role for CloudWatch Logs'
    Value: !GetAtt CloudWatchLogsRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-CloudWatchLogsRoleArn'
  
  BackupBucketName:
    Description: 'Name of the S3 backup bucket'
    Value: !Ref BackupBucket
    Export:
      Name: !Sub '${AWS::StackName}-BackupBucketName'
  
  LogGroupNames:
    Description: 'Log groups being monitored'
    Value: !Join [', ', !Ref LogGroupNames]
    Export:
      Name: !Sub '${AWS::StackName}-LogGroupNames'
  
  Instructions:
    Description: 'Instructions for adding more log groups'
    Value: !Sub |
      To add more log groups, update the LogGroupNames parameter and update the stack.
      Current log groups: See LogGroupNames output
      Firehose stream: ${FirehoseStreamName}
