Skip to content

feat(core): add generic audit transaction recorder - #3816

Open
strantalis wants to merge 3 commits into
mainfrom
codex/pep-5181-audit-recorder
Open

feat(core): add generic audit transaction recorder#3816
strantalis wants to merge 3 commits into
mainfrom
codex/pep-5181-audit-recorder

Conversation

@strantalis

@strantalis strantalis commented Aug 3, 2026

Copy link
Copy Markdown
Member

Proposed Changes

  • add an externally constructible, string-valued RecordedEvent contract for injected services
  • add non-panicking Logger.Record errors for missing, closed, and invalid transactions/events
  • snapshot inputs before transaction locking to prevent caller mutation, reentrant deadlocks, and numeric precision loss
  • close transactions exactly once while preserving the existing AUDIT / msg / audit output contract

This is PR 1 of 2 for PEP-5181. PR #3817 adds finalized-event processing and startup injection on top of this recorder.

Design and compatibility

  • no SaaS-DSP or Audit API types enter upstream
  • legacy LogAuditEvent retains its panic behavior for source compatibility
  • audit.jwt_claim_mappings remains supported
  • ordinary returned Connect errors retain current behavior; changing transaction outcome semantics is follow-up work

Checklist

  • I have added or updated unit tests
  • I have added or updated integration tests (if appropriate)
  • I have added or updated documentation

Testing Instructions

  • cd service && go test -race ./logger/audit ./pkg/config ./pkg/server ./internal/server
  • cd service && golangci-lint run --new-from-rev=origin/main ./logger/audit/... ./logger/... ./pkg/server/...
  • cd sdk && go test -run TestREADMECodeBlocks
  • git diff --check origin/main...HEAD

Repository-wide make lint is blocked locally by an invalid Buf API token. Repository-wide make test reaches environment-dependent Keycloak/Docker suites that are unavailable locally; the focused race suites above pass.

Summary by CodeRabbit

  • New Features
    • Added structured audit event recording with support for resource, action, actor, and client metadata.
    • Added a public recording interface for submitting audit events.
  • Bug Fixes
    • Prevented events from being recorded after a transaction is closed.
    • Improved event isolation and validation to avoid unintended data changes or invalid audit entries.
  • Tests
    • Added coverage for external recording workflows, cancellation handling, validation, serialization, and metadata preservation.

Add an externally constructible audit event contract and a non-panicking recorder for injected services. Snapshot events at record time and reject writes after transaction finalization while preserving the existing audit wire shape.\n\nRefs: PEP-5181

Signed-off-by: strantalis <strantalis@virtru.com>
Serialize external events before taking the transaction lock so custom values cannot panic or reenter while the lifecycle mutex is held. Return ErrInvalidEvent for snapshot failures.\n\nRefs: PEP-5181

Signed-off-by: strantalis <strantalis@virtru.com>
Decode generic audit snapshots with json.Number so large integer metadata remains exact through finalization and emission.\n\nRefs: PEP-5181

Signed-off-by: strantalis <strantalis@virtru.com>
@strantalis
strantalis requested a review from a team as a code owner August 3, 2026 15:50
@github-actions github-actions Bot added the size/m label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Audit event recording

Layer / File(s) Summary
Recorded event model and snapshot conversion
service/logger/audit/recorded_event.go
Adds typed audit event structures, conversion helpers, validation errors, and defensive JSON snapshots.
Logger recording and transaction lifecycle
service/logger/audit/context.go, service/logger/audit/logger.go, service/logger/audit/enrichment.go
Queues recorded event snapshots, rejects closed transactions, prevents duplicate closure, and keeps legacy event conversion.
Public API and lifecycle validation
service/logger/audit/recorded_event_external_test.go, service/logger/audit/recorded_event_test.go
Tests external recording, serialization, typed errors, panic handling, numeric metadata, and cancellation behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: alkalescent

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Logger.Record
  participant RecordedEvent
  participant auditTransaction
  participant buildRecordedLogEntry
  Caller->>Logger.Record: submit verb and RecordedEvent
  Logger.Record->>RecordedEvent: create event snapshot
  Logger.Record->>auditTransaction: enqueue snapshot
  auditTransaction->>buildRecordedLogEntry: normalize queued event during closure
Loading

Poem

A rabbit records each hop,
With typed events in a tidy crop.
Snapshots guard the fields with care,
Closed transactions stop the snare.
Logs emerge when queues are done.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding a generic audit transaction recorder.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/pep-5181-audit-recorder

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@strantalis strantalis changed the title codex/pep 5181 audit recorder feat(audit): add generic transaction recorder Aug 3, 2026
@strantalis strantalis changed the title feat(audit): add generic transaction recorder feat(core): add generic audit transaction recorder Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 195.661494ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 100.651921ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 426.524057ms
Throughput 234.45 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 49.077647854s
Average Latency 488.738476ms
Throughput 101.88 requests/second

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ Govulncheck found vulnerabilities ⚠️

The following modules have known vulnerabilities:

  • examples
  • otdfctl
  • sdk
  • service
  • lib/fixtures
  • tests-bdd

See the workflow run for details.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@service/logger/audit/recorded_event.go`:
- Around line 38-43: Change the Attributes field in RecordedObject from a value
type to a nil-able *RecordedObjectAttributes while preserving its JSON and audit
tags, so omitempty omits unset attributes instead of serializing an empty
object. Update affected construction and access sites to use the pointer
representation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9dbb1f7f-4de5-4694-9ce0-22391b539a8e

📥 Commits

Reviewing files that changed from the base of the PR and between 8915e93 and ac5d8eb.

📒 Files selected for processing (6)
  • service/logger/audit/context.go
  • service/logger/audit/enrichment.go
  • service/logger/audit/logger.go
  • service/logger/audit/recorded_event.go
  • service/logger/audit/recorded_event_external_test.go
  • service/logger/audit/recorded_event_test.go

Comment on lines +38 to +43
type RecordedObject struct {
Type string `json:"type" audit:"reserved"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
Attributes RecordedObjectAttributes `json:"attributes,omitempty"`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Go's encoding/json omitempty tag omit a non-pointer struct field when all its inner fields are zero values?

💡 Result:

No, the encoding/json package's omitempty tag does not omit a non-pointer struct field simply because its inner fields are zero values [1][2][3]. In Go, the omitempty tag option defines an "empty" value specifically as false, 0, a nil pointer, a nil interface value, or any array, slice, map, or string of length zero [4][5][6]. Because a struct is not included in this definition of "empty," the standard library's JSON encoder does not recursively check if a non-pointer struct's inner fields are zero values; it treats the struct itself as a non-empty value [1][2]. Consequently, an initialized non-pointer struct will be marshaled into JSON (often as an empty object {}) even if all of its exported fields would themselves be omitted [2][3]. To achieve the desired behavior of omitting the struct, you can use one of the following approaches: 1. Use a pointer to the struct: By changing the field type to a pointer (*StructType), the field becomes a nil pointer when not initialized, which the omitempty tag correctly identifies as an empty value and omits [2]. 2. Use the omitzero tag (Go 1.24+): The newer omitzero tag option provides more flexible behavior [5][7]. If the struct type implements an IsZero bool method, the encoder will use that method to determine if the struct should be considered zero and therefore omitted [8][5][7]. If no IsZero method is implemented, omitzero follows the same rules as the zero value for that type [5][7]. If you are using an older version of Go, implementing a custom MarshalJSON method or using a pointer is the standard way to handle this requirement [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Locate file"
fd -a 'recorded_event.go' . || true

echo "## Relevant source outline"
ast-grep outline service/logger/audit/recorded_event.go --view expanded || true

echo "## Relevant source snippets"
sed -n '1,260p' service/logger/audit/recorded_event.go | nl -ba | sed -n '1,260p'

echo "## Search for RecordedObjectAttributes definitions/usages and legacy EventObject"
rg -n "type Record Object Attributes|RecordedObjectAttributes|type EventObject|Attributes|snapshotRecordedEvent|cloneRecordedEvent|Record\\(" service/logger/audit -S

echo "## Go tooling availability"
command -v go || true
go version 2>/dev/null || true

Repository: opentdf/platform

Length of output: 3038


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## RecordedEvent definitions and serialization helpers"
sed -n '25,115p' service/logger/audit/recorded_event.go | awk '{printf "%4d\t%s\n", NR+24, $0}'

echo "## Mapping and legacy conversion helpers"
sed -n '107,237p' service/logger/audit/recorded_event.go | awk '{printf "%4d\t%s\n", NR+106, $0}'

echo "## Search for EventObject and legacy attributes"
rg -n "type EventObject|EventObject.*struct|Attributes|recordedObjectAttributesFromMap|recordedEventFromLegacy|snapshotRecordedEvent|json\\.Marshal|json\\.Unmarshal" service/logger/audit service/logger -S

echo "## Go availability and behavior probe"
command -v go || true
go version 2>/dev/null || true
cat > /tmp/json_omit_probe.go <<'GO'
package main

import (
	"encoding/json"
	"fmt"
)

type RecordedObjectAttributes struct {
	Assertions  []string `json:"assertions,omitempty"`
	Attrs       []string `json:"attrs,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
}

type NonPointerRecordedObject struct {
	Type       string                   `json:"type"`
	Attributes RecordedObjectAttributes `json:"attributes,omitempty"`
}

type PointerRecordedObject struct {
	Type       string                    `json:"type"`
	Attributes *RecordedObjectAttributes `json:"attributes,omitempty"`
}

func main() {
	np, _ := json.Marshal(NonPointerRecordedObject{Type: "create"})
	fmt.Println("non-pointer omitted:", string(np))
	p, _ := json.Marshal(PointerRecordedObject{Type: "create"})
	fmt.Println("pointer omitted:", string(p))
}
GO
go run /tmp/json_omit_probe.go 2>&1 || true

Repository: opentdf/platform

Length of output: 21968


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## service/logger/audit/utils.go EventObject definitions"
sed -n '1,105p' service/logger/audit/utils.go | awk '{printf "%4d\t%s\n", NR, $0}'

echo "## service/logger/logger.go EventObject declaration and write path"
sed -n '1,30p' service/logger/logger.go | awk '{printf "%4d\t%s\n", NR, $0}'
sed -n '160,210p' service/logger/logger.go | awk '{printf "%4d\t%s\n", NR, $0}'

echo "## Relevant recorded event JSON test expectations"
sed -n '1,140p' service/logger/audit/recorded_event_test.go | awk '{printf "%4d\t%s\n", NR, $0}'

Repository: opentdf/platform

Length of output: 9252


Make attributes,omitempty omit unset attributes instead of emitting {}. The current struct value is never empty to encoding/json; a nil-able field such as *RecordedObjectAttributes is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/logger/audit/recorded_event.go` around lines 38 - 43, Change the
Attributes field in RecordedObject from a value type to a nil-able
*RecordedObjectAttributes while preserving its JSON and audit tags, so omitempty
omits unset attributes instead of serializing an empty object. Update affected
construction and access sites to use the pointer representation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant