Skip to content

Add clientUserAgent and snapshotSummary to the table audit event - #639

Open
cbb330 wants to merge 1 commit into
mainfrom
chbush/table-audit-client-version
Open

Add clientUserAgent and snapshotSummary to the table audit event#639
cbb330 wants to merge 1 commit into
mainfrom
chbush/table-audit-client-version

Conversation

@cbb330

@cbb330 cbb330 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds two purely-additive fields to TableAuditEvent for client/commit observability, populated server-side in TableAuditAspect (both are observability only — neither gates a request):

  • clientUserAgent — the raw User-Agent request header, stored verbatim, so the client/runtime version can be derived at query time (e.g. openhouse-java-client/<version>). Set in the shared buildAndSendEvent, read from the bound servlet request via RequestContextHolder (mirroring ServiceAuditAspect); best-effort and never disrupts the audited operation.
  • snapshotSummary — the full Iceberg summary map of the snapshot the main ref points to (the same snapshot as currentSnapshotId), emitted verbatim (operation, spark.app.id/trino_query_id, engine/iceberg versions, and the core-computed file-delta counts). Emitted unaggregated so queries derive operation and write-mode signals downstream without a lossy server-side classification — there is no per-commit field that cleanly declares copy-on-write vs merge-on-read across engines. Snapshot.summary() omits the operation key (Iceberg parses it out into a separate field), so it is merged back to mirror the on-disk summary. It is hooked onto the snapshot extractSnapshotInfo already resolves for currentSnapshotTimestampMs; null when the request carries no main ref (branch-only commits), consistent with currentSnapshotId.

Changes

  • Internal API Changes
  • New Features
  • Tests

TableAuditEvent gains clientUserAgent (String) and snapshotSummary (Map<String,String>). snapshotSummary is emitted unbounded by design (unlike the allowlist-filtered auditedTableProperties) — a snapshot summary is naturally a small, fixed set of keys; clientUserAgent is captured on every audited event, not just commits.

Testing Done

  • Added new tests for the changes made.
  • Updated existing tests to reflect the changes made.

IcebergSnapshotsApiHandlerAuditTest:

  • testPutIcebergSnapshotsCapturesClientUserAgent — sets a User-Agent header and asserts verbatim capture.
  • testPutIcebergSnapshotsEmitsRewriteSummaryVerbatim / testPutIcebergSnapshotsEmitsDeleteFileSummaryVerbatim — assert the raw summary counters are emitted for a copy-on-write overwrite and a merge-on-read delete.
  • Extended testPutIcebergSnapshotsContainsSnapshotInfo and the failed-path test to assert the summary is captured (including on failure), and the branch-only test to assert it is null when there is no main ref.

snapshotSummary is excluded from the ReflectionEquals expected constants and asserted explicitly. Full audit test suite passes (IcebergSnapshotsApiHandlerAuditTest 16, plus TablesApiHandlerAuditTest / DatabasesApiHandlerAuditTest).

Additional Information

  • Large PR broken into smaller PRs, and PR plan linked in the description.

Part of a sliced rollout of server-side commit-audit observability. A follow-up will add failureType (the exception class on failed commits) plus isolation of the audit emission so it can never mask the operation being audited.

@cbb330
cbb330 force-pushed the chbush/table-audit-client-version branch from 66b9ff1 to ab32236 Compare June 23, 2026 23:54
@cbb330 cbb330 changed the title Add snapshotSummary and clientUserAgent to the table audit event Capture client User-Agent in the table audit event Jun 23, 2026
@cbb330
cbb330 changed the base branch from chbush/branch-ref-audit-event to main June 23, 2026 23:55
Two purely-additive fields on TableAuditEvent for client/commit observability
(both observability only — neither gates a request):

- clientUserAgent: the raw User-Agent request header, stored verbatim, so the
  client/runtime version can be derived at query time (e.g.
  openhouse-java-client/<version>). Set in the shared buildAndSendEvent, read
  from the bound servlet request via RequestContextHolder (mirroring
  ServiceAuditAspect); best-effort and never disrupts the audited operation.

- snapshotSummary: the full Iceberg summary map of the snapshot the main ref
  points to (the same snapshot as currentSnapshotId), emitted verbatim
  (operation, spark.app.id/trino_query_id, engine/iceberg versions, and the
  core-computed file-delta counts). Emitted unaggregated so queries derive
  operation and write-mode signals downstream without a lossy server-side
  classification. Snapshot.summary() omits the operation key (Iceberg parses it
  out separately), so it is merged back to mirror the on-disk summary.
@cbb330
cbb330 force-pushed the chbush/table-audit-client-version branch from ab32236 to 352e9f7 Compare June 24, 2026 00:24
@cbb330 cbb330 changed the title Capture client User-Agent in the table audit event Add clientUserAgent and snapshotSummary to the table audit event Jun 24, 2026
} catch (Exception e) {
log.warn("Failed to read User-Agent header for audit event", e);
}
return null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit. Optional vs null.

*/
private static Map<String, String> buildSnapshotSummary(Snapshot snapshot) {
Map<String, String> summary = new HashMap<>();
if (snapshot.summary() != null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

under what conditions is the summary / operation expected to be null? is a partial version of this considered valid?

private static Map<String, String> buildSnapshotSummary(Snapshot snapshot) {
Map<String, String> summary = new HashMap<>();
if (snapshot.summary() != null) {
summary.putAll(snapshot.summary());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems fine. The contents do not contain column statistics that would subject this to privacy risks.

summary.putAll(snapshot.summary());
}
if (snapshot.operation() != null) {
summary.put(SNAPSHOT_OPERATION_KEY, snapshot.operation());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is empty operation / summary tested? I want to understand the expectation when one or both is actually null given the code.

Snapshot snapshot = SnapshotParser.fromJson(snapshotJson);
if (snapshot.snapshotId() == mainSnapshotId) {
eventBuilder.currentSnapshotTimestampMs(snapshot.timestampMillis());
eventBuilder.snapshotSummary(buildSnapshotSummary(snapshot));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I assume this is fast, <1ms because its a pure function and we are already emitting the data.

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.

2 participants