feat(aws_kinesis_streams sink): add optional record aggregation - #44
Closed
smolaon wants to merge 1 commit into
Closed
feat(aws_kinesis_streams sink): add optional record aggregation#44smolaon wants to merge 1 commit into
smolaon wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds optional record aggregation to the
aws_kinesis_streamssink: many events are packed into one Kinesis record as newline-delimited JSON and compressed as a single unit, instead of one event per record each compressed independently.Two things drive it. Kinesis bills every record rounded up to 1 KB, so small records waste most of what they are billed. And a per-record compression frame restarts the compressor every time, so redundancy across events — which is enormous in security event streams — is never exploited.
Measured on 400 real CloudTrail events (3,680 B/event raw):
Off by default. With
aggregation.enabled = falsethe sink takes its original code path unchanged.Design
The existing builder is
RequestBuilder<KinesisProcessedEvent>withtype Events = Event, andRequestBuilder::encode_eventsconstructs a freshCompressorper call — which is exactly why every record is its own frame today. TakingVec<Event>is the whole mechanism; it reuses the upstreamimpl Encoder<Vec<Event>> for (Transformer, Encoder<Framer>)thataws_s3already relies on.Everything downstream of the request builder is untouched: one aggregate is one
KinesisRequest, so the index-based partial-failure retry inKinesisRetryLogickeeps working, and finalizers are merged viaVec::take_finalizersso an ack or nack applies to every event in the record.New module under
streams/aggregation/with 1-3 line registration edits elsewhere, plus aKinesisRequest::newconstructor sometadatastays private.Deliberate choices
encoding.codecmust bejsonornative_json; the build errors otherwise. Newline framing is only sound if no event can contain a literal newline. JSON escapes them as the two characters\n, so a raw 0x0A never appears inside a serialized event — buttextorraw_messagepasses bytes through, and one embedded newline would silently split one event into two on the consumer.partition_key_fieldis ignored when aggregation is on, with awarn!rather than an error. Events in one record may disagree on the field, so a random UUID is generated per record — which keeps the MD5 hash distribution, and therefore shard distribution, uniform. It is a warning rather than a hard failure because the option has no correctness impact here, and erroring would crash-loop a sink on config that was previously valid.aggregation.max_bytesdefaults to 256 KiB and is capped at 900,000. The limit bounds the uncompressed input, because the compressed size is not known until the batch closes andRequestBuilderemits exactly one request per batch. Staying well under the 1 MB record limit means even wholly incompressible input still fits. Diminishing returns justify the default: 50 events/record already captures 31% of billed bytes against 30% at 100 and 28% at 400, so a larger record buys single digits while concentrating bytes against the 1 MB/s/shard write cap.Its own config table, not
batch.batchis hard-capped to thePutRecordslimits and its units are records per API call; these are events per record. Reusing it would silently reinterpret 500/5 MB.Verification
cargo checkandcargo check --testsclean, including withaws-kinesis-streams-integration-testsenabled — notesrc/lib.rshas#![deny(warnings)], so that is a warning-free build.cargo test ... aws_kinesis: 4 passed, includinggenerate_config, so the new field does not break config-schema generation.End to end against a locally built binary and a fake Kinesis endpoint, 60 events in:
Event bytes are 29,657 in both cases and the events are byte-identical in frame order once the
stdinsource's per-runhostandtimestampare excluded. The decoded delta is exactly +60 for 60 events — pure newline framing, no content change. So the encoding transformer still applies per event, as it must.Consumer requirement
A consumer must decompress and then split on newlines. An unaggregated record is a single-line payload, so a splitting consumer handles both formats and a stream can carry both during a rollout — but not the reverse: an old consumer receiving a multi-line record fails to parse it, and depending on its error handling that can stall rather than skip. Deploy the consumer change everywhere before enabling this on any producer.