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:26 UTC
(causeway) 32/41: CAUSEWAY-4065: proposes application-defined spans
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 59405c8febb91f1f215efd698c72f1fd21ca1c62
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 24 13:53:52 2026 +0100
CAUSEWAY-4065: proposes application-defined spans
---
.../add-application-defined-spans/.openspec.yaml | 2 +
.../add-application-defined-spans/design.md | 106 ++++++++++++++++
.../add-application-defined-spans/proposal.md | 30 +++++
.../specs/application-defined-spans/spec.md | 134 +++++++++++++++++++++
.../changes/add-application-defined-spans/tasks.md | 28 +++++
5 files changed, 300 insertions(+)
diff --git a/openspec/changes/add-application-defined-spans/.openspec.yaml b/openspec/changes/add-application-defined-spans/.openspec.yaml
new file mode 100644
index 00000000000..265da3d94ed
--- /dev/null
+++ b/openspec/changes/add-application-defined-spans/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-23
diff --git a/openspec/changes/add-application-defined-spans/design.md b/openspec/changes/add-application-defined-spans/design.md
new file mode 100644
index 00000000000..25fe8058dbb
--- /dev/null
+++ b/openspec/changes/add-application-defined-spans/design.md
@@ -0,0 +1,106 @@
+## Context
+
+Causeway currently creates framework-owned Micrometer observations through `CausewayObservationIntegration`, whose dedicated registry is connected to the OpenTelemetry Java agent only when the `observation` profile is active.
+Applications can technically depend on that internal class or its qualified registry, but there is no supported applib contract for dividing long-running domain actions into application-meaningful regions.
+
+An action invocation exposes its logical identity through `InteractionService.currentInteraction()`, the current `Execution`, and `Execution.getLogicalMemberIdentifier()`.
+Declared actions already carry the required domain-facing identifier, but contributed mixin actions currently construct `ActionInvocation` with the mixin implementation identifier, such as an identity ending in `#act`, rather than the mixed-in action identifier.
+The change must correct that execution identity at its construction boundary before the application span service can use it without reconstructing identity from implementation details.
+The exported tracing span name is selected from Micrometer's contextual observation name and is bounded to 50 characters by `CausewayTracingObservationHandler`.
+Application-defined region names therefore need a deterministic suffix-preserving compaction policy rather than ordinary right-side truncation.
+
+The API must remain independent of Micrometer and OpenTelemetry so applications do not acquire an SDK or exporter and so the Java agent remains the sole telemetry owner.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Provide an injectable applib service that wraps synchronous application work in an observation span.
+- Support both value-returning callables and non-returning throwing runnables.
+- Ensure declared and contributed mixin action executions expose their domain-facing logical member identifier.
+- Derive a child span's contextual name from the current execution's canonical `<logical-type-name>#<member-logical-name>` identity and an application-provided static suffix.
+- Preserve suffixes of at most 46 characters exactly in every accepted contextual name while keeping the name within 50 characters.
+- Keep the full logical member identifier available as canonical span metadata when its display form is compacted or truncated.
+- Preserve current observation parentage, error reporting, return values, and exception behavior.
+- Degrade to normal unobserved execution when framework observation is inactive.
+
+**Non-Goals:**
+
+- Expose Micrometer `Observation`, OpenTelemetry `Span`, scopes, registries, SDKs, samplers, or exporters through applib.
+- Support asynchronous context propagation or spans whose lifecycle outlives a single service call.
+- Accept dynamic instance data, action arguments, user identities, bookmarks, or record identifiers as names or attributes.
+- Replace Causeway's action-invocation span or change its existing naming requirements.
+- Add configurable sampling or export behavior.
+
+## Decisions
+
+### Add an `ApplicationSpanService` applib contract
+
+Add `org.apache.causeway.applib.services.span.ApplicationSpanService` with synchronous `call(String suffix, Callable<T>)` and `run(String suffix, ThrowingRunnable)` operations.
+The methods follow the established `InteractionService` convention: they do not declare checked exceptions, but an original checked exception, runtime exception, or error escapes unchanged and is not wrapped.
+The service owns the complete span lifecycle so application code cannot leak a scope or forget to stop a span.
+
+The alternative of exposing a span handle implementing `AutoCloseable` was rejected because it makes incorrect lifecycle ordering and missing error recording easier.
+The alternative of exposing Micrometer directly was rejected because it would add telemetry-specific API surface and dependencies to applib.
+
+### Use one stable observation category and a contextual display name
+
+Every application-defined span uses stable observation name `causeway.application.span`.
+Its exported contextual name is formed as `<logical-member-identifier> <suffix>` using one plain-space separator when a current execution and logical member identifier are available.
+The canonical logical member identifier is formatted as `<logical-type-name>#<member-logical-name>` without action parentheses or parameter type signatures.
+The implementation obtains the identifier components through `InteractionService.currentInteraction()`, then the current `Execution`, rather than reconstructing them from the target class or Java method.
+
+Before adding the service, correct `MemberExecutorServiceDefault` so a contributed mixin `ActionInvocation` is constructed with the domain-facing action `Identifier` exposed by its `ActionInteractionHead`, while retaining the implementation action identifier for invocation internals that still require it.
+This makes the execution API truthful for mixins and lets nested wrapper invocations naturally expose whichever execution is current.
+
+The full canonical identifier is attached as low-cardinality attribute `causeway.member.id`, and the supplied suffix is attached as `causeway.application.span.suffix`.
+The API documentation requires the suffix to be a static operation or phase identifier rather than instance-specific data.
+
+Using the suffix as the stable observation name was rejected because it would create an unbounded observation-name vocabulary and would lose the stable category used for filtering.
+Using only the logical identifier as the contextual name was rejected because sibling regions inside one action would be indistinguishable.
+
+### Preserve the suffix when bounding contextual names
+
+The contextual-name builder first tries the full logical member identifier followed by one space and the suffix.
+If that exceeds 50 characters, it retries with the logical type namespace removed, following existing semantic naming behavior.
+If the compact form still exceeds 50 characters, it truncates only the logical-member portion to fit while preserving the separator and complete suffix.
+
+A suffix longer than 46 characters is rejected with `IllegalArgumentException` before the callable or runnable executes.
+Null and blank suffixes are also rejected.
+The context-independent 46-character limit reserves four characters for the fallback prefix `app `, so every accepted suffix fits whether or not an execution identity is available.
+This makes the suffix-preservation guarantee explicit instead of silently producing an ambiguous or altered region name.
+
+When no current execution identity is available, the contextual name is `app <suffix>` and no `causeway.member.id` attribute is attached.
+The same validation and 50-character bound apply to this fallback.
+This allows use from anonymous interactions and tests without making telemetry context a prerequisite for application behavior.
+
+The alternative of applying the existing generic right-side truncation was rejected because it can remove the application-provided region suffix, which is the information that distinguishes sibling spans.
+
+### Implement in runtime services over the existing observation integration
+
+Place the default implementation in core runtime services and inject `InteractionService` plus `CausewayObservationIntegration`.
+Create each observation from the existing dedicated registry, add module and bean metadata consistently with framework observations, set its contextual name and canonical attributes, and execute work through Micrometer's lifecycle helper.
+
+This preserves nesting beneath the active action observation and beneath an enclosing application span.
+When the registry is `ObservationRegistry.NOOP`, the implementation still invokes the supplied work exactly once and returns or throws exactly as it would with observation active.
+No application-defined `ObservationRegistry`, SDK, or exporter participates.
+
+## Risks / Trade-offs
+
+- [Risk] A dynamic suffix can create high-cardinality names and attributes even though the API describes it as static. → Document the restriction prominently and test examples using fixed phase identifiers; the service cannot reliably infer whether arbitrary text is dynamic.
+- [Risk] Rejecting an overlong suffix can make telemetry naming affect application execution. → Define and document the context-independent 46-character maximum at compile-facing API entry points and validate it before invoking work.
+- [Risk] Correcting mixin `ActionInvocation` identity could affect consumers that accidentally depend on the implementation `#act` identifier. → Treat the current value as inconsistent with the execution API's logical-identity contract and add focused DTO, publishing, and nested-execution regressions.
+- [Risk] `Interaction.getCurrentExecution()` could change during wrapper-mediated nested invocations. → Resolve the logical identity when each span starts so the span describes the execution that actually requested it.
+- [Risk] Callable exception semantics can be accidentally changed by the implementation. → Follow the existing `InteractionService` sneaky-throw convention and add tests for checked exceptions, runtime exceptions, errors, return values, and exactly-once invocation.
+- [Trade-off] The synchronous closure API cannot represent asynchronous regions. → Keep asynchronous context propagation out of scope rather than exposing unsafe span handles in this maintenance branch.
+- [Trade-off] Fallback names outside a member execution omit canonical member identity. → Prefer useful standalone spans and safe execution over failing solely because no Causeway execution is current.
+
+## Migration Plan
+
+The API and implementation are additive and require no application migration.
+Applications may replace direct use of `CausewayObservationIntegration`, the qualified registry, or OpenTelemetry span lifecycle code with `ApplicationSpanService` incrementally.
+Rollback consists of removing those service calls or deploying the previous framework version; no persisted data or exporter configuration changes are involved.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/add-application-defined-spans/proposal.md b/openspec/changes/add-application-defined-spans/proposal.md
new file mode 100644
index 00000000000..88f2f96e8eb
--- /dev/null
+++ b/openspec/changes/add-application-defined-spans/proposal.md
@@ -0,0 +1,30 @@
+## Why
+
+Applications with long-running Causeway actions need a supported way to divide an action invocation into meaningful child spans without depending on Causeway's internal observation classes or owning another telemetry SDK.
+A public application-facing API can preserve the enclosing action's logical identity while allowing the application to identify each region clearly.
+
+## What Changes
+
+- Add an application-facing `ApplicationSpanService` with synchronous `call` and `run` operations for executing work within an application-defined observation span.
+- Ensure action executions, including contributed mixin actions, expose their domain-facing logical member identity to the service.
+- Derive each span's contextual display name from the current interaction's canonical `<logical-type-name>#<member-logical-name>` identity, one plain-space separator, and an application-provided suffix.
+- Preserve the application-provided suffix when enforcing the 50-character contextual-name limit, rejecting suffixes longer than 46 characters and truncating or compacting the logical member identifier first.
+- Retain the full logical member identifier and suffix as bounded, non-instance-specific observation attributes so truncated display names remain diagnosable.
+- Parent application-defined spans beneath the currently active action or nested application span and propagate failures through the existing observation lifecycle.
+- Make the service harmless when observation is inactive, without requiring applications to use Micrometer or OpenTelemetry APIs directly.
+
+## Capabilities
+
+### New Capabilities
+
+- `application-defined-spans`: Public API, naming, parentage, metadata, error handling, and inactive-profile behavior for application-defined spans within Causeway interactions.
+
+### Modified Capabilities
+
+- None.
+
+## Impact
+
+The change adds a public service contract to `causeway-applib` and a runtime implementation backed by the existing `CausewayObservationIntegration` and dedicated observation registry.
+It corrects the logical identifier stored by mixin `ActionInvocation` executions and affects observation naming utilities, runtime service composition, tests, and tracing documentation.
+It introduces no new telemetry SDK, exporter, or third-party dependency and preserves the Java agent as the telemetry owner.
diff --git a/openspec/changes/add-application-defined-spans/specs/application-defined-spans/spec.md b/openspec/changes/add-application-defined-spans/specs/application-defined-spans/spec.md
new file mode 100644
index 00000000000..4d0d769672b
--- /dev/null
+++ b/openspec/changes/add-application-defined-spans/specs/application-defined-spans/spec.md
@@ -0,0 +1,134 @@
+## ADDED Requirements
+
+### Requirement: Application-defined span service
+Causeway SHALL provide `org.apache.causeway.applib.services.span.ApplicationSpanService` as an injectable applib service that executes synchronous application work within an application-defined observation span without exposing Micrometer or OpenTelemetry types through its public contract.
+The service SHALL provide a value-returning `call(String suffix, Callable<T>)` operation and a non-returning `run(String suffix, ThrowingRunnable)` operation.
+
+#### Scenario: Value-returning region completes
+- **WHEN** application code calls the service with a valid suffix and a callable that returns a value
+- **THEN** Causeway executes the callable exactly once within an application-defined span
+- **AND** returns the callable's value unchanged
+
+#### Scenario: Non-returning region completes
+- **WHEN** application code calls the service with a valid suffix and a throwing runnable that completes normally
+- **THEN** Causeway executes the runnable exactly once within an application-defined span
+- **AND** closes and stops the span after the runnable completes
+
+### Requirement: Current logical member identity
+Causeway SHALL ensure that declared and contributed mixin action executions expose their domain-facing `Identifier` as the current execution's logical member identifier.
+Causeway SHALL resolve the domain-facing logical member identifier for an application-defined span from the current interaction's current execution when one is available.
+Causeway SHALL format the canonical identifier as `<logical-type-name>#<member-logical-name>` without action parentheses or parameter type signatures.
+Causeway SHALL NOT reconstruct that identity from the callable class, target implementation class, or Java method name.
+
+#### Scenario: Region executes within a declared action
+- **WHEN** application code starts a region while a declared action execution is current
+- **THEN** the region's contextual name incorporates that action's logical member identifier
+- **AND** the full logical member identifier is retained as canonical span metadata
+
+#### Scenario: Region executes within a mixin action
+- **WHEN** application code starts a region from a mixin action
+- **THEN** the current `ActionInvocation` and the region use the domain-facing mixed-in action identity
+- **AND** neither uses the mixin implementation identity ending in `#act`
+
+#### Scenario: Region executes within a nested wrapper invocation
+- **WHEN** a wrapper-invoked action starts an application-defined region while its execution is current
+- **THEN** the region uses the nested current execution's logical member identifier rather than the outer command's identifier
+
+### Requirement: Stable and contextual naming
+Causeway SHALL use `causeway.application.span` as the stable observation name for every application-defined span.
+When a logical member identifier is available, Causeway SHALL form the contextual display name from that identifier followed by one plain-space separator and the application-provided suffix.
+Causeway SHALL attach the full canonical logical member identifier as `causeway.member.id` and the suffix as `causeway.application.span.suffix`.
+
+#### Scenario: Two regions divide one action
+- **WHEN** one action invokes application-defined regions with suffixes `load` and `process`
+- **THEN** both observations have stable name `causeway.application.span`
+- **AND** their contextual names end with `load` and `process` respectively
+- **AND** their canonical member attributes identify the same enclosing action
+
+#### Scenario: No current execution identity is available
+- **WHEN** application code starts a region without a current execution logical member identifier
+- **THEN** Causeway uses contextual name `app <suffix>`
+- **AND** does not attach a `causeway.member.id` attribute
+- **AND** still executes the supplied work normally
+
+### Requirement: Suffix-preserving bounded names
+Causeway SHALL limit application-defined contextual names to 50 characters and SHALL reject suffixes longer than 46 characters before invoking application work.
+Causeway SHALL preserve every accepted application-provided suffix completely and SHALL compact or truncate only the logical-member portion when the combined name exceeds the limit.
+Causeway SHALL preserve the full logical member identifier in canonical metadata when its display form is compacted or truncated.
+
+#### Scenario: Full combined name fits
+- **WHEN** the full logical member identifier, separator, and suffix total no more than 50 characters
+- **THEN** the contextual name contains the full logical member identifier and complete suffix without truncation
+
+#### Scenario: Removing the logical type namespace is sufficient
+- **WHEN** the full combined name exceeds 50 characters but the namespace-free logical member identifier and suffix fit
+- **THEN** Causeway removes the logical type namespace from the contextual name
+- **AND** preserves the complete suffix
+- **AND** retains the full logical member identifier in `causeway.member.id`
+
+#### Scenario: Compact logical identity remains too long
+- **WHEN** the namespace-free logical member identifier, separator, and suffix still exceed 50 characters
+- **THEN** Causeway truncates only the displayed logical-member portion
+- **AND** the contextual name is exactly at most 50 characters
+- **AND** the complete suffix remains at the end of the contextual name
+- **AND** `causeway.member.id` retains the complete logical member identifier
+
+#### Scenario: Suffix exceeds the context-independent limit
+- **WHEN** a suffix contains more than 46 characters
+- **THEN** the service rejects the suffix with `IllegalArgumentException`
+- **AND** does not invoke the supplied callable or runnable
+
+#### Scenario: Longest accepted suffix has no current execution
+- **WHEN** a 46-character suffix is supplied without a current execution identity
+- **THEN** contextual name `app <suffix>` is exactly 50 characters
+- **AND** preserves the complete suffix
+
+#### Scenario: Suffix is absent or blank
+- **WHEN** application code supplies a null or blank suffix
+- **THEN** the service rejects the suffix before invoking the supplied work
+
+### Requirement: Span parentage and nesting
+Causeway SHALL make an application-defined span a child of the observation that is current when the service operation begins.
+A nested application-defined span SHALL therefore be a child of its enclosing application-defined span.
+
+#### Scenario: Region executes within an observed action
+- **WHEN** an action invocation span is current and application code starts a region
+- **THEN** the application-defined span is exported in the same trace beneath the action span
+
+#### Scenario: Application regions are nested
+- **WHEN** application code starts one region while another application-defined region is current
+- **THEN** the inner region is a child of the outer region
+- **AND** the previous current observation is restored when the inner region closes
+
+### Requirement: Failure and cleanup semantics
+Causeway SHALL report a throwable from application work to the active observation, close its scope, stop its span, and preserve the original return or throw behavior.
+Cleanup SHALL occur exactly once even when application work fails.
+
+#### Scenario: Runtime failure escapes a region
+- **WHEN** application work throws a runtime exception or error
+- **THEN** the span records that throwable as an observation error
+- **AND** Causeway closes and stops the span
+- **AND** the original throwable escapes unchanged
+
+#### Scenario: Callable throws a checked exception
+- **WHEN** a callable throws a checked exception
+- **THEN** Causeway records the exception and lets the original exception escape unchanged and unwrapped using the established `InteractionService` convention
+- **AND** the `call` operation does not declare a checked exception or invoke the callable again
+
+### Requirement: Safe inactive behavior
+When Causeway observation is inactive, the application-defined span service SHALL execute supplied work without exporting an application-defined span and without requiring an application-owned telemetry registry, SDK, or exporter.
+Activation or deactivation of observation SHALL NOT alter the work's result, exception behavior, or invocation count.
+
+#### Scenario: Observation profile is inactive
+- **WHEN** application code invokes the service while Causeway uses its no-op observation registry
+- **THEN** the supplied work executes exactly once
+- **AND** no application-defined span is exported
+- **AND** the application does not need conditional tracing code
+
+### Requirement: Bounded static application metadata
+The application-defined span API SHALL document that suffixes identify static operations or phases and MUST NOT contain domain-object identifiers, titles, bookmarks, argument values, record values, user identities, tenancy identifiers, or other instance-specific data.
+
+#### Scenario: Application chooses region suffixes
+- **WHEN** an application defines suffixes for repeated executions of one action phase
+- **THEN** it uses the same bounded suffix for each execution of that phase
+- **AND** per-invocation values are absent from the contextual name and span attributes
diff --git a/openspec/changes/add-application-defined-spans/tasks.md b/openspec/changes/add-application-defined-spans/tasks.md
new file mode 100644
index 00000000000..bf94bf028f0
--- /dev/null
+++ b/openspec/changes/add-application-defined-spans/tasks.md
@@ -0,0 +1,28 @@
+## 1. Public API
+
+- [ ] 1.1 Add `org.apache.causeway.applib.services.span.ApplicationSpanService` with synchronous `call(String, Callable<T>)` and `run(String, ThrowingRunnable)` operations that do not expose telemetry implementation types or declare checked exceptions.
+- [ ] 1.2 Document the 46-character suffix limit, static low-cardinality naming expectations, unchanged sneaky-throw exception semantics, inactive behavior, and the exclusion of asynchronous span lifecycles.
+- [ ] 1.3 Add the new service package to applib documentation and module exports where required by the existing module structure.
+
+## 2. Naming and Runtime Implementation
+
+- [ ] 2.1 Correct `ActionInvocation` construction so declared and contributed mixin actions expose their domain-facing `Identifier`, while retaining the implementation identifier for invocation internals.
+- [ ] 2.2 Extend observation naming support with a 50-character application-span builder that formats canonical identities without action signatures, tries the full identity, then its namespace-free form, and finally truncates only the identity while preserving the complete suffix.
+- [ ] 2.3 Validate null, blank, and longer-than-46-character suffixes before supplied application work is invoked.
+- [ ] 2.4 Implement the runtime service using `InteractionService.currentInteraction()`, the current execution's domain-facing logical member identifier, and the existing `CausewayObservationIntegration` registry.
+- [ ] 2.5 Configure each application observation with stable name `causeway.application.span`, the suffix-preserving contextual name using one plain-space separator, `causeway.member.id` when available, and `causeway.application.span.suffix`.
+- [ ] 2.6 Register the implementation in runtime service composition and ensure nested spans use the current observation scope without adding an SDK, registry, sampler, or exporter.
+
+## 3. Unit and Integration Tests
+
+- [ ] 3.1 Add naming tests for signature-free canonical identifiers, namespace compaction, suffix-preserving identifier truncation, fallback `app <suffix>` names, 46-character suffixes, exact 50-character boundaries, casing, and invalid suffixes.
+- [ ] 3.2 Add service tests proving callable return values, exactly-once runnable and callable execution, unchanged checked and unchecked failure behavior, error reporting, scope cleanup, and no-op registry behavior.
+- [ ] 3.3 Add interaction tests proving domain-facing `ActionInvocation` identity for declared and mixin actions, DTO and publishing compatibility, nested wrapper invocations, absent current executions, canonical metadata, and nested application-span parentage.
+- [ ] 3.4 Extend the tracing compatibility regression to assert that an exported application-defined span is a child of its action span, carries the expected stable metadata, and preserves its suffix.
+- [ ] 3.5 Verify the no-agent tracing regression still starts successfully and does not create an application-owned OpenTelemetry SDK or exporter.
+
+## 4. Documentation and Verification
+
+- [ ] 4.1 Update the tracing operations guide with injection and usage examples, naming examples, profile behavior, cardinality guidance, and the prohibition on instance-specific suffixes.
+- [ ] 4.2 Run focused applib, core configuration, runtime services, and tracing compatibility tests on the supported Java and Maven toolchain.
+- [ ] 4.3 Run strict OpenSpec validation and confirm all application-defined-span scenarios are covered by automated tests or explicit documentation checks.