Skip to content

Commit

Permalink
Merge pull request #39 from axiomhq/create-logs-subscriber
Browse files Browse the repository at this point in the history
add logs subscriber
  • Loading branch information
SollyzDev authored Feb 16, 2024
2 parents 4b56e03 + 90918f3 commit a60c2cd
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ jobs:
mkdir build
yq ".Resources.LogsLambda.Properties.Code.ZipFile = \"$(sed 's/\"/\\\"/g' ./handler.py)\"" axiom-cloudwatch-lambda-cloudformation-stack.template.yaml > build/axiom-cloudwatch-lambda-cloudformation-stack.yaml
yq ".Resources.BackfillerLambda.Properties.Code.ZipFile = \"$(sed 's/\"/\\\"/g' ./backfill.py)\"" axiom-cloudwatch-backfiller-lambda-cloudformation-stack.template.yaml > build/axiom-cloudwatch-backfiller-lambda-cloudformation-stack.yaml
yq ".Resources.AxiomCloudWatchLogsSubscriber.Properties.Code.ZipFile = \"$(sed 's/\"/\\\"/g' ./logs_subscriber.py)\"" axiom-cloudwatch-logs-subscriber-cloudformation-stack.template.yaml > build/axiom-cloudwatch-logs-subscriber-cloudformation-stack.yaml
- run: cat build/*
1 change: 1 addition & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
mkdir build
yq ".Resources.LogsLambda.Properties.Code.ZipFile = \"$(sed 's/\"/\\\"/g' ./handler.py)\"" axiom-cloudwatch-lambda-cloudformation-stack.template.yaml > build/axiom-cloudwatch-lambda-cloudformation-stack.yaml
yq ".Resources.BackfillerLambda.Properties.Code.ZipFile = \"$(sed 's/\"/\\\"/g' ./backfill.py)\"" axiom-cloudwatch-backfiller-lambda-cloudformation-stack.template.yaml > build/axiom-cloudwatch-backfiller-lambda-cloudformation-stack.yaml
yq ".Resources.AxiomCloudWatchLogsSubscriber.Properties.Code.ZipFile = \"$(sed 's/\"/\\\"/g' ./logs_subscriber.py)\"" axiom-cloudwatch-logs-subscriber-cloudformation-stack.template.yaml > build/axiom-cloudwatch-logs-subscriber-cloudformation-stack.yaml
- uses: jakejarvis/s3-sync-action@v0.5.1
env:
SOURCE_DIR: build
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ logs from your CloudWatch to [Axiom](https://axiom.co).
2. Create a dataset and an API token with ingest permission for that dataset
3. Launch the stack: [![Launch Stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?#/stacks/new?stackName=CloudWatch-Axiom&templateURL=https://axiom-cloudformation-stacks.s3.amazonaws.com/axiom-cloudwatch-lambda-cloudformation-stack.yaml)
4. Subscribe to more LogGroups: [![Launch Stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?#/stacks/new?stackName=CloudWatch-Backfiller-Axiom&templateURL=https://axiom-cloudformation-stacks.s3.amazonaws.com/axiom-cloudwatch-backfiller-lambda-cloudformation-stack.yaml)
5. Automatically Subscribe to new LogGroups: [![Launch Stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?#/stacks/new?stackName=Axiom-CloudWatch-LogsSubscriber&templateURL=https://axiom-cloudformation-stacks.s3.amazonaws.com/axiom-cloudwatch-logs-subscriber-cloudformation-stack.yaml)


# Logs Subscriber architecture

- Creates an S3 bucket for Cloudtrail
- Creates a Trail to capture creation of new LogGroups
- Creates an Event Rule to pass those creation events to event bus
- EventBridge sends an event to a Lambda function when a new LogGroup is created
- Lambda function creates a subscription filter for the new LogGroup
163 changes: 163 additions & 0 deletions axiom-cloudwatch-logs-subscriber-cloudformation-stack.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
Description: A lambda function to auto subscribe Axiom Forwarder to new CloudWatch Log Groups.
Parameters:
LambdaFunctionName:
Type: String
Description: Name of the AWS Lambda Function.
Default: axiom-cloudwatch-logs-subscriber-lambda
AllowedPattern: ".+" # required
AxiomCloudWatchLambdaIngesterARN:
Type: String
Description: The ARN of the AWS Lambda Function that is used to ingest data to axiom.
AllowedPattern: ".+" # required
CloudWatchLogGroupsPrefix:
Type: String
Description: The Prefix of cloudwatch log groups to subscribe to the AWS Lambda ingester.
Default: "" # all
AxiomLambdaLogRetention:
Type: "Number"
Description: "The number of days to retain CloudWatch logs for the created lambda functions."
Default: 1
Resources:
AxiomCloudWatchLogsSubscriberS3Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: BucketOwnerFullControl
BucketName: !Join ["-", [!Ref AWS::StackName, "axiom", "cloudtrail"]]
AxiomCloudWatchLogsSubscriberS3BucketPolicy:
Type: AWS::S3::BucketPolicy
DependsOn: AxiomCloudWatchLogsSubscriberS3Bucket
Properties:
Bucket: !Ref AxiomCloudWatchLogsSubscriberS3Bucket
PolicyDocument:
{
"Version": "2012-10-17",
"Statement":
[
{
"Sid": "AWSCloudTrailAclCheck20150319",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:GetBucketAcl",
"Resource":
!GetAtt ["AxiomCloudWatchLogsSubscriberS3Bucket", "Arn"],
},
{
"Sid": "AWSCloudTrailWrite20150319",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:PutObject",
"Resource":
!Join [
"",
[
!GetAtt ["AxiomCloudWatchLogsSubscriberS3Bucket", "Arn"],
"/AWSLogs/",
{ "Ref": "AWS::AccountId" },
"/*",
],
],
"Condition":
{
"StringEquals":
{ "s3:x-amz-acl": "bucket-owner-full-control" },
},
},
],
}
AxiomLogsSubscriberCloudTrail:
Type: AWS::CloudTrail::Trail
DependsOn: AxiomCloudWatchLogsSubscriberS3BucketPolicy
Properties:
EnableLogFileValidation: false
IncludeGlobalServiceEvents: true
IsMultiRegionTrail: true
IsLogging: true
S3BucketName: !Join ["-", [!Ref AWS::StackName, "axiom", "cloudtrail"]]
TrailName:
!Join ["-", [!Ref AWS::StackName, "axiom", { "Ref": "AWS::AccountId" }]]
AxiomLogsSubscriberEventRule:
DependsOn: AxiomCloudWatchLogsSubscriber
Type: AWS::Events::Rule
Properties:
Description: Axiom log group auto subscription event rule.,
EventPattern:
source: ["aws.logs"]
detail-type: ["AWS API Call via CloudTrail"]
detail:
eventSource: ["logs.amazonaws.com"]
eventName: ["CreateLogGroup"]
Name:
"Fn::Join":
["-", [{ "Ref": "AWS::StackName" }, "axiom-auto-subscription-rule"]]
Targets:
- Id:
!Join ["-", [!Ref "AWS::StackName", "axiom-auto-subscription-rule"]]
Arn: !GetAtt ["AxiomCloudWatchLogsSubscriber", "Arn"]
AxiomCloudWatchLogsSubscriberPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action:
- logs:DeleteSubscriptionFilter
- logs:PutSubscriptionFilter
- logs:DescribeLogGroups
- lambda:AddPermission
- lambda:RemovePermission
Effect: Allow
Resource: "*"
PolicyName: axiom-cloudwatch-logs-subscriber-lambda-policy
Roles:
- !Ref "AxiomCloudWatchLogsSubscriberRole"
AxiomCloudWatchLogsSubscriberRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
AxiomCloudWatchLogsSubscriber:
Type: AWS::Lambda::Function
DependsOn:
- AxiomCloudWatchLogsSubscriberRole
Properties:
Runtime: python3.9
Handler: index.lambda_handler
Code:
ZipFile: |
# DO NOT EDIT
# CI will replace these comments with the code from ./logs_subscriber.py
Role: !GetAtt
- AxiomCloudWatchLogsSubscriberRole
- Arn
Description: Axiom CloudWatch Automatic Logs Subscriber Lambda
Environment:
Variables:
AXIOM_CLOUDWATCH_LAMBDA_INGESTER_ARN: !Ref "AxiomCloudWatchLambdaIngesterARN"
LOG_GROUP_PREFIX: !Ref "CloudWatchLogGroupsPrefix"
AxiomCloudWatchLogsSubscriberPermission:
Type: AWS::Lambda::Permission
Properties:
Action: "lambda:InvokeFunction"
FunctionName: { "Fn::GetAtt": ["AxiomCloudWatchLogsSubscriber", "Arn"] }
Principal: "events.amazonaws.com"
SourceAccount:
Ref: AWS::AccountId
SourceArn: !GetAtt ["AxiomLogsSubscriberEventRule", "Arn"]
AxiomCloudWatchLogsSubscriberLogGroup:
DependsOn: ["AxiomCloudWatchLogsSubscriberRole"]
Type: AWS::Logs::LogGroup
Properties:
LogGroupName:
!Join [
"",
["/aws/lambda/", { "Ref": "AxiomCloudWatchLogsSubscriber" }],
]
RetentionInDays:
Ref: "AxiomLambdaLogRetention"
2 changes: 1 addition & 1 deletion backfill.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def create_subscription_filter(log_group_arn, lambda_arn):

def lambda_handler(event: dict, context=None):
if axiom_cloudwatch_lambda_ingester_arn is None:
raise Exception("AXIOM_AXIOM_CLOUDWATCH_LAMBDA_INGESTER_ARNTOKEN is not set")
raise Exception("AXIOM_CLOUDWATCH_LAMBDA_INGESTER_ARN is not set")

def log_groups(token=None):
groups_response = get_log_groups(token)
Expand Down
79 changes: 79 additions & 0 deletions logs_subscriber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# subscribe the Axiom ingester to newly created log groups
import boto3
import os
import logging

# Set environment variables.
axiom_cloudwatch_lambda_ingester_arn = os.getenv("AXIOM_CLOUDWATCH_LAMBDA_INGESTER_ARN")
log_group_prefix = os.getenv("LOG_GROUP_PREFIX", "")

# set logger
level = os.getenv("log_level", "INFO")
logging.basicConfig(level=level)
logger = logging.getLogger()
logger.setLevel(level)

# Set up CloudWatch Logs client.
log_client = boto3.client("logs")
lambda_client = boto3.client("lambda")


def lambda_handler(event, context):
"""
Subscribes log ingester to log group from event.
:param event: Event data from CloudWatch Logs.
:type event: dict
:param context: Lambda context object.
:type context: obj
:return: None
"""
if not "detail" in event:
return
# Grab the log group name from incoming event.
aws_account_id = event["account"]
aws_region = event["detail"]["awsRegion"]
log_group_name = event["detail"]["requestParameters"]["logGroupName"]
log_group_arn = (
f"arn:aws:logs:{aws_region}:{aws_account_id}:log-group:{log_group_name}:*"
)

# Check whether the prefix is set - the prefix is used to determine which logs we want.
# or whether the log group's name starts with the set prefix.
if not log_group_prefix or log_group_name.startswith(log_group_prefix):
create_subscription_filter(
log_group_name, log_group_arn, axiom_cloudwatch_lambda_ingester_arn
)

else:
print(
f"log group ({log_group_name}) did not match the prefix ({log_group_prefix})"
)


def create_subscription_filter(log_group_name, log_group_arn, lambda_arn):
try:
logger.info(f"Creating subscription filter for {log_group_name}...")
lambda_client.add_permission(
FunctionName=lambda_arn,
StatementId="%s-axiom" % log_group_name.replace("/", "-"),
Action="lambda:InvokeFunction",
Principal=f"logs.amazonaws.com",
SourceArn=log_group_arn,
)

log_client.put_subscription_filter(
logGroupName=log_group_name,
filterName="%s-axiom" % log_group_name,
filterPattern="",
destinationArn=lambda_arn,
distribution="ByLogStream",
)
logger.info(
f"{log_group_name} subscription filter has been created successfully."
)
except Exception as e:
logger.error(f"Error create Subscription filter: {e}")
raise e

0 comments on commit a60c2cd

Please sign in to comment.