Skip to content

Sends billing.events log lines to S3 - #2522

Open
Hazel-Datastax wants to merge 66 commits into
mainfrom
hazel/billing-to-s3
Open

Sends billing.events log lines to S3#2522
Hazel-Datastax wants to merge 66 commits into
mainfrom
hazel/billing-to-s3

Conversation

@Hazel-Datastax

@Hazel-Datastax Hazel-Datastax commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What this PR does:

Sends billing.events log lines to S3 as batched NDJSON objects

How it works:

  • BillingS3HandlerInstaller wires it up: attaches BillingS3LogHandler to the billing.events JUL logger at startup, detaches/closes it at shutdown.
  • BillingS3LogHandler is the orchestrator. publish() pushes each line into BillingQueue; the handler then decides when to ship — batch sealed (count/bytes), age tick, or close() drain — gates upload concurrency, calls the uploader, and records outcomes via BillingMetrics. Delivery is at-most-once by design: publish never blocks on S3, a full queue drops events, shutdown drains best-effort within a timeout.
  • BillingQueue owns the batching policy (seal by max events/bytes) and hands out drained batches.
  • S3BatchUploader is the only S3-aware piece (AsyncBatchUploader impl) — encodes NDJSON, PUTs to a time-partitioned key.
  • BillingMetrics tracks offered/flushed/failed/dropped counts plus a delivery heartbeat.

Tests

  • BillingS3LogHandlerTest is the main coverage: per-method unit tests (flush triggers, failure containment, close draining), plus concurrency tests (multi-producer no-loss/no-dup, close racing with in-flight publish) and pipeline tests (concurrency gate, auto-chaining the next batch on settle).
  • BillingS3ExportIntegrationTest covers the real path end-to-end against S3Mock, including that a dead S3 endpoint never breaks the API.
  • Remaining unit tests (queue/uploader/metrics/installer) cover their components individually.

Which issue(s) this PR fixes:
Fixes #

Checklist

  • Changes manually tested
  • Automated Tests added/updated
  • Documentation added/updated
  • CLA Signed: DataStax CLA

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

📉 Unit Test Coverage Delta vs Main Branch

Metric Value
Main Branch 53.40%
This PR 53.34%
Delta 🔴 -0.07%
⚠️ Coverage decreased

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Unit Test Coverage Report

Overall Project 53.34% -0.78% 🍏
Files changed 49.12%

File Coverage
Billing.java 100% 🍏
BillingEventType.java 97.58% 🍏
DefaultBilling.java 95.63% 🍏
AsyncBatchedLogUploader.java 90% -10% 🍏
BatchedLogBuffer.java 89.39% -10.61% 🍏
BillingUploadingLogHandler.java 83.24% -16.76% 🍏
BillingEvent.java 61.24% 🍏
RequestContext.java 19.76% 🍏
S3BatchedLogUploader.java 0%
BillingS3HandlerInstaller.java 0%
MetricsBase.java 0%
BatchedLogBufferMetrics.java 0%
BatchedLogUploaderMetrics.java 0%

@erichare
erichare self-requested a review September 1, 2026 17:46
Comment thread src/test/java/io/stargate/sgv2/jsonapi/service/billing/BatchedLogBufferTest.java Outdated
Comment thread src/main/java/io/stargate/sgv2/jsonapi/service/billing/BatchedLogBuffer.java Outdated
Comment thread src/main/java/io/stargate/sgv2/jsonapi/service/billing/BatchedLogBuffer.java Outdated
Comment thread src/test/java/io/stargate/sgv2/jsonapi/service/billing/BatchedLogBufferTest.java Outdated
@erichare erichare mentioned this pull request Sep 1, 2026
4 tasks
amorton and others added 2 commits September 4, 2026 15:50
still needs decisions on what to do when upload fails.

Installer setup to do basic starting of the uploader.
Map<String, String> props = new HashMap<>();
props.put("stargate.jsonapi.billing.s3.enabled", "true");
props.put("stargate.jsonapi.billing.s3.bucket", BUCKET);
props.put("stargate.jsonapi.billing.s3.bucket-region", BUCKET_REGION);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
props.put("stargate.jsonapi.billing.s3.bucket-region", BUCKET_REGION);
props.put("stargate.jsonapi.billing.s3.region", BUCKET_REGION);

props.put("stargate.jsonapi.billing.s3.bucket-region", BUCKET_REGION);
props.put("stargate.jsonapi.billing.s3.endpoint-override", httpEndpoint);
// Small thresholds so the export flushes promptly: count seal at 5, age sweep every 2s.
props.put("stargate.jsonapi.billing.s3.max-events", "5");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
props.put("stargate.jsonapi.billing.s3.max-events", "5");
props.put("stargate.jsonapi.billing.s3.max-batch-size", "5");

props.put("stargate.jsonapi.billing.s3.endpoint-override", httpEndpoint);
// Small thresholds so the export flushes promptly: count seal at 5, age sweep every 2s.
props.put("stargate.jsonapi.billing.s3.max-events", "5");
props.put("stargate.jsonapi.billing.s3.max-age", "PT2S");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
props.put("stargate.jsonapi.billing.s3.max-age", "PT2S");
props.put("stargate.jsonapi.billing.s3.max-batch-age", "PT2S");

// Small thresholds so the export flushes promptly: count seal at 5, age sweep every 2s.
props.put("stargate.jsonapi.billing.s3.max-events", "5");
props.put("stargate.jsonapi.billing.s3.max-age", "PT2S");
props.put("stargate.jsonapi.billing.s3.shutdown-timeout", "PT5S");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
props.put("stargate.jsonapi.billing.s3.shutdown-timeout", "PT5S");
props.put("stargate.jsonapi.billing.s3.upload-shutdown-deadline", "PT5S");

.atMost(Duration.ofSeconds(10))
.untilAsserted(
() ->
assertThat(metricTotal("billing_s3_events_flushed_total"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think BatchedLogUploaderMetrics now registers billing.s3.uploaded.events, which Prometheus exposes as billing_s3_uploaded_events_total... so that should be what we check here. Correct me if im wrong

.atMost(Duration.ofSeconds(60))
.pollInterval(Duration.ofSeconds(2))
.untilAsserted(
() -> assertThat(metricTotal("billing_s3_batches_failed_total")).isGreaterThan(0.0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as above, i think this should be billing_s3_failed_batches_total

props.put("stargate.jsonapi.billing.s3.bucket", BUCKET);
props.put("stargate.jsonapi.billing.s3.bucket-region", BUCKET_REGION);
props.put("stargate.jsonapi.billing.s3.endpoint-override", httpEndpoint);
// Small thresholds so the export flushes promptly: count seal at 5, age sweep every 2s.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we set stargate.jsonapi.billing.s3.upload-sleep-duration to something short around here?

* billing.events} JUL logger, the disabled path, and fail-loud startup on bad config. Delivery
* through an installed handler is covered by {@code BillingS3ExportIntegrationTest}.
*/
class BillingS3HandlerInstallerTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To re-enable soon?

* flush()/notify() on an object lands before the upload thread is in wait() - if we used
* Object.notify() and .wait()
*/
private final Semaphore wakeupPermit = new Semaphore(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is smart. I like it

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.

3 participants