Skip to content

Support for flow logs - #22

Merged
mheffner merged 15 commits into
mainfrom
vpc-logs
Jan 23, 2026
Merged

mheffner merged 15 commits into
mainfrom
vpc-logs

Conversation

@mheffner

@mheffner mheffner commented Jan 21, 2026 •

Copy link
Copy Markdown
Member

This supports forwarding VPC flow logs from CloudWatch log groups. If the forwarder lambda function has permissions to ecs:DescribeFlowLogs, then on startup we query flow logs from EC2. The flow logs define the formatting of the log output, which if we can query, is used to split out the named attribute fields from the log message. If we don't have access then the log is just forwarded as the full string.

The flow log definitions are refreshed periodically and they are cached to S3. The S3 cache provides for faster cold start times and reduces calls to the EC2 APIs to refresh the data, in theory the Ec2 calls are slower than querying S3. If the IAM permissions do not exist for this, we enter a backoff period and avoid repeated calls.

We enrich the resource record with any tags found on the flog logs, allowing it to have more info on the source.

Resolves: #9

@mheffner
mheffner marked this pull request as ready for review January 22, 2026 00:09
@mheffner
mheffner requested a review from rjenkins January 22, 2026 00:10

@rjenkins rjenkins left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, left a couple suggestions/comments and questions for my own understanding.

Comment thread src/flowlogs/cache.rs Outdated
/// The log format string (e.g., "${version} ${account-id} ${interface-id} ...")
pub log_format: String,
/// The destination type (e.g., "cloud-watch-logs", "s3")
pub destination_type: String,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any good reason not to use an enum here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, on inspection there's not really a good reason to save it in this struct. We already filter to only Cloudwatch destinations, so this should never be anything else. Removing entirely.

Comment thread src/flowlogs/ec2.rs

// Get the log group name
let log_group_name = match flow_log.log_group_name() {
Some(name) => name,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could probably just make this a none check? Is the Some(name) => name needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the match block is combining an unwrap with a conditional statement on it being None. We could alternatively do an is_none() check here and then an unwrap() later when we need it (since we know it's not none), but this handles both here. Maybe there's a cleaner way to do this though?

Comment thread src/flowlogs/ec2.rs
}
};

let log_format = match flow_log.log_format().map(|s| s.to_string()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious what does this return if it's not a string already?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AWS API uses &str, so the map of to_string() is converting that to String, which we want for later.

Comment thread src/flowlogs/mod.rs
///
/// Returns information about the current state of the cache including
/// the number of cached entries and whether fetching is currently disabled.
pub fn cache_stats(&self) -> CacheStats {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only used for testing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, I've marked these as cfg(test) now

Comment thread src/s3_cache.rs
put_request = put_request.if_match(etag);
} else {
debug!("Using conditional write with if-none-match");
put_request = put_request.if_none_match("*");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we not want to write if there is an existing file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's probably overkill for the use case, but I wanted to make sure we never overwrite a cache in S3. Instead, if there's any conflict I want to read-merge-update.

It's probably less important for the flow log use case since we save all entries at once. For log group tag caching, which I want to eventually use this module for, we may be updating individual entries in the cache file separately, so the merging case is more important.

@mheffner
mheffner merged commit 2809b5e into main Jan 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support VPC flow logs

2 participants