You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/09/24 16:22:14 UTC

(causeway) 20/41: CAUSEWAY-4059: proposes audit trail tracing

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch CAUSEWAY-4066
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 34da7fc3a047794b4b5f7403e11f43c3294e0522
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 24 08:46:12 2026 +0100

    CAUSEWAY-4059: proposes audit trail tracing
---
 .../add-audit-trail-write-span/.openspec.yaml      |  2 +
 .../changes/add-audit-trail-write-span/design.md   | 92 ++++++++++++++++++++++
 .../changes/add-audit-trail-write-span/proposal.md | 29 +++++++
 .../specs/audit-trail-write-observation/spec.md    | 75 ++++++++++++++++++
 .../changes/add-audit-trail-write-span/tasks.md    | 24 ++++++
 5 files changed, 222 insertions(+)

diff --git a/openspec/changes/add-audit-trail-write-span/.openspec.yaml b/openspec/changes/add-audit-trail-write-span/.openspec.yaml
new file mode 100644
index 00000000000..75289e4b73f
--- /dev/null
+++ b/openspec/changes/add-audit-trail-write-span/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-24
diff --git a/openspec/changes/add-audit-trail-write-span/design.md b/openspec/changes/add-audit-trail-write-span/design.md
new file mode 100644
index 00000000000..96f080ba4ae
--- /dev/null
+++ b/openspec/changes/add-audit-trail-write-span/design.md
@@ -0,0 +1,92 @@
+## Context
+
+The audit-trail extension receives enlisted `EntityPropertyChange` records through `EntityPropertyChangeSubscriberForAuditTrail` and persists an `AuditTrailEntry` for each change.
+Its bulk callback delegates to `AuditTrailEntryRepository.createFor(Can<...>)`, which uses `RepositoryService.execInBulk(...)` while each entry is persisted and flushed.
+The OpenTelemetry Java agent already creates JDBC spans for the resulting reads and inserts, but no semantic observation identifies the enclosing operation as audit-trail writing.
+Consequently, a large audited action presents many database spans directly beneath the action and obscures their aggregate audit cost.
+
+The existing `CausewayObservationIntegration` supplies profile-controlled Micrometer observations backed by the Java agent's current OpenTelemetry context.
+It is a no-op without the `observation` profile and already supplies static `causeway.bean` and `causeway.module` metadata.
+The Java agent must continue to own HTTP and JDBC instrumentation, propagation, sampling, and export.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Create one semantic parent around each audit-trail subscriber persistence callback.
+- Preserve natural parentage beneath the currently active action, interaction, request, or background trace.
+- Make all automatic JDBC work initiated by the callback descendants of the audit observation.
+- Aggregate a bulk callback without adding one framework span per audit entry.
+- Preserve audit configuration, persistence, transaction, exception, and ordering behavior.
+- Keep names and metadata static, bounded, and free of audit payload or identity data.
+- Preserve no-op behavior when observation is inactive.
+
+**Non-Goals:**
+
+- Trace generic entity-property-change publication or custom subscribers as audit work.
+- Add a span for every audit entry within a bulk callback.
+- Expose changed property identities, targets, bookmarks, old or new values, users, tenants, interaction IDs, sequences, or entry counts.
+- Change the publisher's single-versus-bulk threshold or callback ordering.
+- Change audit persistence batching, flush behavior, schema, or retention.
+- Add an OpenTelemetry SDK, exporter, sampler, or dependency.
+
+## Decisions
+
+### Observe the audit-trail subscriber boundary
+
+`EntityPropertyChangeSubscriberForAuditTrail` will open `causeway.audittrail.write` immediately before delegating to `AuditTrailEntryRepository` and close it after the repository callback completes.
+The contextual name will be the static phrase `write audit trail`.
+The enabled check will remain outside the observation so a disabled audit extension creates neither entries nor spans.
+
+This boundary precisely identifies extension-owned audit work and includes detached-entry creation, initialization, repository persistence, flushing, and automatic JDBC calls.
+Instrumenting `EntityPropertyChangePublisherDefault.publishChangedProperties()` was rejected because that boundary includes change extraction and every application-defined subscriber, not only audit persistence.
+Instrumenting the JDO or JPA repository implementations was rejected because the common subscriber already covers both persistence technologies and any application-provided `AuditTrailEntryRepository` implementation.
+
+### Aggregate according to the existing callback shape
+
+The bulk `onChanging(Can<EntityPropertyChange>)` callback will create one observation around the complete repository bulk operation.
+The individual `onChanging(EntityPropertyChange)` callback will create one observation around that individual repository operation.
+The bulk implementation will not delegate through an observed individual callback, so one bulk publication does not create nested or per-entry framework spans.
+
+This preserves the existing publisher threshold and callback contract.
+For a small publication that the core publisher delivers as individual callbacks, repeated audit observations are accepted because there is no safe subscriber-local lifecycle boundary spanning all sibling callbacks.
+Changing publisher loop ordering or retaining an observation across callbacks was rejected because it would alter subscriber semantics or risk overlapping scopes.
+
+### Use the current trace context without explicit reparenting
+
+The observation will use `CausewayObservationIntegration` and Micrometer's current context.
+When audit persistence runs before an action observation closes, `write audit trail` will naturally be its child and Java-agent JDBC spans will naturally be children of the audit observation.
+When publication occurs at another transaction or interaction boundary, the observation will remain under whatever request, interaction, or background context is current rather than fabricating an action parent.
+
+Explicit span links, stored span contexts, and interaction-ID-based reparenting were rejected because they complicate lifecycle handling and can create invalid or cross-trace parentage.
+
+### Attach static framework metadata only
+
+The observation will rely on the existing static bean and module metadata and will add no audit payload attributes.
+In particular, it will not attach the callback size because entry counts vary per interaction and are not required to identify the operation.
+The stable and contextual names do not vary between single and bulk callbacks.
+
+### Preserve failure and inactive behavior
+
+The observation wrapper will record a thrown repository failure and rethrow the original exception unchanged.
+The existing no-op registry will execute the same repository callback without exporting a span when observation is inactive.
+No catch-and-suppress behavior or audit-specific sampling will be introduced.
+
+## Risks / Trade-offs
+
+- [Small publications can produce multiple sibling audit spans] → Preserve the existing callback contract and avoid unsafe cross-callback scopes; bulk publications still produce one aggregate span.
+- [The current context is not always an action] → Retain truthful natural parentage under the active interaction, request, or background trace rather than forcing a stale action parent.
+- [Audit SQL is already visible and the new span adds telemetry volume] → Add only one span per existing subscriber callback and no per-entry framework spans.
+- [Observation code could alter persistence failures] → Use the standard observation wrapper and test that the original exception instance or type is rethrown.
+- [Payload attributes could disclose sensitive audit data] → Emit only static framework metadata and test the absence of target, value, user, tenant, identifier, sequence, and count attributes.
+
+## Migration Plan
+
+Applications using the audit-trail extension receive the new observation only when the existing `observation` profile is active.
+No configuration, schema, or data migration is required.
+Rollback consists of removing the subscriber observation wrapper; audit entries and Java-agent JDBC spans remain unchanged.
+
+## Open Questions
+
+- Confirm during implementation whether the audit-trail applib's existing provided runtime dependency exposes `CausewayObservationIntegration` directly or whether an explicit existing-module dependency declaration is required.
+- Confirm with the Java-agent compatibility fixture that repository JDBC spans retain the audit observation as their immediate semantic ancestor for both successful and failing persistence paths.
diff --git a/openspec/changes/add-audit-trail-write-span/proposal.md b/openspec/changes/add-audit-trail-write-span/proposal.md
new file mode 100644
index 00000000000..07b3a6fc885
--- /dev/null
+++ b/openspec/changes/add-audit-trail-write-span/proposal.md
@@ -0,0 +1,29 @@
+## Why
+
+Audit-trail persistence currently appears as a large sequence of Java-agent JDBC spans directly beneath an action or interaction, so operators cannot identify their aggregate cost as audit work.
+A bounded semantic parent span is needed to distinguish audit-trail writing from domain SQL while preserving the existing automatic JDBC detail.
+
+## What Changes
+
+- Add a `causeway.audittrail.write` observation around enabled audit-trail persistence with contextual name `write audit trail`.
+- Make the observation inherit the current action, interaction, or request context so audit queries and inserts become its automatic descendants.
+- Use one aggregate observation for a bulk property-change callback and one observation for an individual callback, without adding per-entry child observations.
+- Record audit persistence failures on the observation while preserving the existing exception and transaction behavior.
+- Keep audit writing unchanged and telemetry inactive when the `observation` profile is absent or the audit-trail extension is disabled.
+- Attach only static framework metadata and exclude property values, pre/post values, targets, bookmarks, interaction identifiers, sequence numbers, usernames, tenants, and entry counts.
+- Add focused lifecycle, parentage, failure, no-op, and persistence compatibility tests and document the resulting trace hierarchy.
+
+## Capabilities
+
+### New Capabilities
+
+- `audit-trail-write-observation`: Define a bounded semantic observation around audit-trail persistence and its automatic JDBC descendants.
+
+### Modified Capabilities
+
+None.
+
+## Impact
+
+The change affects the audit-trail extension's entity-property-change subscriber, its observation dependency wiring, focused extension tests, Java-agent compatibility evidence, and tracing operations documentation.
+It adds no exporter, sampler, SDK ownership, database schema, persistence behavior, configuration property, or third-party dependency.
diff --git a/openspec/changes/add-audit-trail-write-span/specs/audit-trail-write-observation/spec.md b/openspec/changes/add-audit-trail-write-span/specs/audit-trail-write-observation/spec.md
new file mode 100644
index 00000000000..42a7c719552
--- /dev/null
+++ b/openspec/changes/add-audit-trail-write-span/specs/audit-trail-write-observation/spec.md
@@ -0,0 +1,75 @@
+## ADDED Requirements
+
+### Requirement: Audit-trail write observation
+When observation is active and audit persistence is enabled, the audit-trail extension SHALL create a `causeway.audittrail.write` observation around each `EntityPropertyChangeSubscriberForAuditTrail` persistence callback.
+The observation SHALL use contextual name `write audit trail` and SHALL inherit the trace context current when the subscriber callback begins.
+
+#### Scenario: Bulk property changes are audited
+- **WHEN** the audit subscriber receives a bulk collection of entity property changes
+- **THEN** one `causeway.audittrail.write` observation encloses the complete bulk repository operation
+- **AND** the extension creates no additional framework observation for each audit entry in that bulk callback
+
+#### Scenario: An individual property change is audited
+- **WHEN** the audit subscriber receives one entity property change through its individual callback
+- **THEN** one `causeway.audittrail.write` observation encloses that repository operation
+- **AND** the observation uses the same stable and contextual names as a bulk callback
+
+#### Scenario: Audit writing occurs beneath an action
+- **WHEN** audit persistence executes while an action observation is current
+- **THEN** `write audit trail` is a child of that action observation
+- **AND** the audit extension does not store or reconstruct an action context explicitly
+
+#### Scenario: Audit writing occurs at another interaction boundary
+- **WHEN** audit persistence executes without a current action but with another request, interaction, or background trace context
+- **THEN** the audit observation inherits that current context
+- **AND** it does not fabricate an action parent
+
+### Requirement: Audit persistence child operations and lifecycle
+The audit observation SHALL cover detached audit-entry creation, entry initialization, repository persistence, flushing, and other synchronous work initiated by the subscriber's repository callback.
+Automatic JDBC spans initiated by that work SHALL inherit the audit observation as an ancestor while remaining owned by the Java agent.
+The observation SHALL close after success and SHALL record and rethrow a persistence failure without changing transaction behavior.
+
+#### Scenario: Audit entries produce JDBC operations
+- **WHEN** audit persistence executes database reads or writes with Java-agent JDBC instrumentation active
+- **THEN** the resulting JDBC spans are descendants of `causeway.audittrail.write`
+- **AND** Causeway does not replace or duplicate those JDBC spans
+
+#### Scenario: Audit persistence succeeds
+- **WHEN** the audit repository callback completes normally
+- **THEN** the audit observation closes after all synchronous audit persistence work
+- **AND** no audit observation remains current afterward
+
+#### Scenario: Audit persistence fails
+- **WHEN** detached-entry creation, initialization, persistence, flushing, or another repository operation throws
+- **THEN** the audit observation records the failure and closes
+- **AND** the original failure is rethrown for the existing transaction handling
+- **AND** no audit observation scope leaks into subsequent work
+
+### Requirement: Bounded and private audit telemetry
+The audit observation SHALL attach only static framework metadata needed to identify the operation and module.
+It MUST NOT attach changed property identifiers, targets, bookmarks, pre-values, post-values, interaction identifiers, transaction identifiers, sequence numbers, usernames, tenancy identifiers, entry counts, or other audit payload data.
+
+#### Scenario: A bulk callback contains many changes
+- **WHEN** a bulk audit callback contains changes for multiple objects, properties, or users
+- **THEN** its observation name and attributes remain identical in shape to those for an individual callback
+- **AND** neither the callback size nor any entry identity or value appears in telemetry
+
+#### Scenario: Canonical framework metadata is emitted
+- **WHEN** an audit observation is exported
+- **THEN** its stable name, contextual name, bean metadata, and module metadata identify the bounded framework operation
+- **AND** no application-instance data is required to filter or aggregate it
+
+### Requirement: Audit behavior without active observation
+Observation SHALL NOT change audit-trail enablement, callback selection, persistence ordering, batching, flush behavior, or results.
+When the audit extension is disabled, the subscriber SHALL create neither audit entries nor audit observations.
+When observation is inactive, enabled audit persistence SHALL remain unchanged and no Causeway audit span SHALL be exported.
+
+#### Scenario: Audit persistence is disabled
+- **WHEN** the audit-trail persistence setting disables the subscriber
+- **THEN** the subscriber returns according to its existing behavior without invoking the repository
+- **AND** it creates no `causeway.audittrail.write` observation
+
+#### Scenario: Observation profile is inactive
+- **WHEN** audit persistence is enabled without the `observation` profile
+- **THEN** the same audit entries are created with the same transaction behavior
+- **AND** no `causeway.audittrail.write` span is exported
diff --git a/openspec/changes/add-audit-trail-write-span/tasks.md b/openspec/changes/add-audit-trail-write-span/tasks.md
new file mode 100644
index 00000000000..76d88daaea1
--- /dev/null
+++ b/openspec/changes/add-audit-trail-write-span/tasks.md
@@ -0,0 +1,24 @@
+## 1. Audit Observation Boundary
+
+- [ ] 1.1 Confirm the audit-trail applib's compile-time access to `CausewayObservationIntegration` and add only an existing-module dependency declaration if the current provided runtime dependency is insufficient.
+- [ ] 1.2 Add stable `causeway.audittrail.write` and contextual `write audit trail` observation construction using the audit-trail extension's static module metadata.
+- [ ] 1.3 Wrap enabled individual subscriber persistence in one observation without changing repository invocation, transaction, or exception behavior.
+- [ ] 1.4 Wrap enabled bulk subscriber persistence in one aggregate observation without creating nested or per-entry framework observations.
+- [ ] 1.5 Keep the enablement check outside the observation and preserve inactive-profile execution through the existing no-op integration.
+
+## 2. Lifecycle, Privacy, and Persistence Tests
+
+- [ ] 2.1 Add focused subscriber tests proving that individual and bulk repository callbacks execute with `causeway.audittrail.write` current and that a bulk callback produces exactly one observation.
+- [ ] 2.2 Add parentage tests proving the audit observation inherits an existing action or interaction observation and synchronous simulated database work inherits the audit observation.
+- [ ] 2.3 Add success and failure lifecycle tests proving scopes close, the original failure is rethrown, and no observation remains current.
+- [ ] 2.4 Add disabled-extension and no-op-registry tests proving repository behavior and exported telemetry remain unchanged in inactive modes.
+- [ ] 2.5 Verify exported names and attributes contain only static bean and module metadata and no audit target, property, value, identity, user, tenant, sequence, transaction, or count data.
+- [ ] 2.6 Run the existing JDO and JPA audit-trail persistence tests to confirm entry contents, ordering, bulk behavior, and transaction semantics remain unchanged.
+
+## 3. Java-Agent Compatibility and Documentation
+
+- [ ] 3.1 Extend the Java-agent compatibility fixture with representative audit persistence and assert `action -> write audit trail -> JDBC` ancestry while HTTP and JDBC spans remain agent-owned and unduplicated.
+- [ ] 3.2 Verify a bulk fixture creates one audit observation around multiple automatic JDBC operations rather than one framework observation per entry.
+- [ ] 3.3 Update the tracing operations guide with the audit hierarchy, stable and contextual names, single-versus-bulk behavior, natural non-action parentage, activation requirements, and privacy exclusions.
+- [ ] 3.4 Record representative trace evidence showing audit queries and inserts grouped beneath `write audit trail` and separated from domain SQL.
+- [ ] 3.5 Run focused extension and compatibility test suites, run `git diff --check`, validate the OpenSpec change strictly, and record the commands and results in `validation.md`.