Description: Continuously dump all matching CloudWatch Log groups to a bucket in a
  central account for long-term storage (by CloudSnorkel)
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Target
        Parameters:
          - LogDestination
      - Label:
          default: CloudWatch Logs
        Parameters:
          - SubscribeSchedule
          - LogGroupNamePrefix
    ParameterLabels:
      LogDestination:
        default: Log Destination
      LogGroupNamePrefix:
        default: Required Log Group Name Prefix
      SubscribeSchedule:
        default: Look for New Logs Schedule
  AWS::ServerlessRepo::Application:
    Author: CloudSnorkel
    Description: Logging source for CloudWatch2S3 from a separate AWS account. Deploy
      CloudWatch2S3 to your main account first.
    HomePageUrl: https://github.com/CloudSnorkel/CloudWatch2S3
    Labels:
      - cloudwatch
      - s3
      - export
    LicenseUrl: LICENSE
    Name: CloudWatch2S3-additional-account
    ReadmeUrl: README.md
    SemanticVersion: 1.0.0
    SourceCodeUrl: https://github.com/CloudSnorkel/CloudWatch2S3
    SpdxLicenseId: MIT
Parameters:
  LogDestination:
    AllowedPattern: arn:[a-z\-]+:logs:[a-z1-9\-]+:[0-9]+:destination:.*
    Description: Log destination ARN from the outputs of the main template
    Type: String
  LogGroupNamePrefix:
    Default: ''
    Description: Prefix to match against log group that should be exported (leave
      empty to export all log groups)
    Type: String
  SubscribeSchedule:
    Default: rate(1 hour)
    Description: Schedule to look for new log groups for export (in case CloudTrail
      missed something)
    Type: String
Resources:
  LogSubscriberFunction:
    Properties:
      Code:
        ZipFile:
          Fn::Sub: |
            import traceback

            import boto3
            import botocore.exceptions
            import cfnresponse

            logs_client = boto3.client("logs")


            def subscribe(log_group_name):
                print("Subscribe ", log_group_name)

                if log_group_name.startswith("/aws/lambda/${AWS::StackName}") \
                        or log_group_name.startswith("/aws/kinesisfirehose/${AWS::StackName}"):
                    print("Skipping our log groups to avoid endless recursion")
                    return

                try:
                    logs_client.put_subscription_filter(
                        logGroupName=log_group_name,
                        filterName="BucketBackupFilter",
                        filterPattern="",
                        destinationArn="${LogDestination}",
                    )
                except logs_client.exceptions.LimitExceededException:
                    print(f"ERROR: Unable to subscribe to {log_group_name} as it already has an active subscription")


            def matched_log_groups(prefix):
                print(f"Finding all log groups with prefix '{prefix}'")

                log_group_paginator = logs_client.get_paginator("describe_log_groups")

                paginate_params = {}
                if prefix:
                    paginate_params["logGroupNamePrefix"] = prefix

                for log_group_page in log_group_paginator.paginate(**paginate_params):
                    for log_group in log_group_page["logGroups"]:
                        yield log_group["logGroupName"]


            def subscribe_all():
                for log_group_name in matched_log_groups("${LogGroupNamePrefix}"):
                    subscribe(log_group_name)


            def unsubscribe_all():
                for log_group_name in matched_log_groups(""):
                    print("Unsubscribe ", log_group_name)

                    try:
                        logs_client.delete_subscription_filter(
                            logGroupName=log_group_name,
                            filterName="BucketBackupFilter",
                        )
                    except botocore.exceptions.ClientError:
                        pass


            def handler(event, context):
                print('event:', event)

                if "ResponseURL" in event and "RequestType" in event:
                    # custom resource callback
                    try:
                        if event["RequestType"] in ["Create", "Update"]:
                            print("Subscribe to all new log groups on resource", event["RequestType"])
                            subscribe_all()

                        elif event["RequestType"] == "Delete":
                            print("Unsubscribe all on resource", event["RequestType"])
                            unsubscribe_all()

                        cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, "ok")

                    except Exception as e:
                        try:
                            traceback.print_last()
                        except ValueError:
                            print("Caught exception but unable to print stack trace")
                            print(e)
                        cfnresponse.send(event, context, cfnresponse.FAILED, {}, "fail")

                else:
                    # other call
                    detail_type = event.get("detail-type")

                    if detail_type == "AWS API Call via CloudTrail":
                        print("Subscribe to specific new log group from CloudTrail")

                        request_parameters = event['detail']['requestParameters']

                        if request_parameters:
                            log_group_name = request_parameters['logGroupName']

                            if log_group_name.startswith("${LogGroupNamePrefix}"):
                                subscribe(log_group_name)
                            else:
                                print(log_group_name, "doesn't match required prefix '${LogGroupNamePrefix}'")

                        else:
                            print("Bad parameters")

                    elif detail_type == "Scheduled Event":
                        print("Subscribe to all new log groups on schedule")

                        subscribe_all()

                    else:
                        print("Subscribe to all new log groups")

                        subscribe_all()
      Handler: index.handler
      Role:
        Fn::GetAtt:
          - LogSubscriberRole
          - Arn
      Runtime: python3.9
      Timeout: 300
    Type: AWS::Lambda::Function
  LogSubscriberPermission:
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Fn::GetAtt:
          - LogSubscriberFunction
          - Arn
      Principal:
        Fn::Sub: events.${AWS::URLSuffix}
      SourceArn:
        Fn::GetAtt:
          - LogSubscriberRule
          - Arn
    Type: AWS::Lambda::Permission
  LogSubscriberRole:
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - Fn::Sub: lambda.${AWS::URLSuffix}
        Version: '2012-10-17'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - logs:DeleteSubscriptionFilter
                  - logs:DescribeLogGroups
                  - logs:PutSubscriptionFilter
                Effect: Allow
                Resource: '*'
                Sid: Logs
            Version: '2012-10-17'
          PolicyName: Logs
    Type: AWS::IAM::Role
  LogSubscriberRule:
    Properties:
      EventPattern:
        detail:
          eventName:
            - CreateLogGroup
          eventSource:
            - Fn::Sub: logs.${AWS::URLSuffix}
        detail-type:
          - AWS API Call via CloudTrail
        source:
          - aws.logs
      ScheduleExpression:
        Ref: SubscribeSchedule
      Targets:
        - Arn:
            Fn::GetAtt:
              - LogSubscriberFunction
              - Arn
          Id: LogSubscriberLambda
    Type: AWS::Events::Rule
  Subscriber:
    DependsOn:
      - LogSubscriberFunction
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - LogSubscriberFunction
          - Arn
    Type: Custom::Subscriber
Transform: AWS::Serverless-2016-10-31