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/10/09 16:39:19 UTC
(causeway) branch CAUSEWAY-4062-v4 created (now 09b33b0d5b0)
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a change to branch CAUSEWAY-4062-v4
in repository https://gitbox.apache.org/repos/asf/causeway.git
at 09b33b0d5b0 CAUSEWAY-4062: add semantic member span names
This branch includes the following new commits:
new 41dfa69342c CAUSEWAY-4062: propose semantic member span names
new 09b33b0d5b0 CAUSEWAY-4062: add semantic member span names
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(causeway) 02/02: CAUSEWAY-4062: add semantic member span names
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-4062-v4
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 09b33b0d5b0fdfb0523a7ff8c073d9e955cc0199
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Oct 9 17:38:58 2026 +0100
CAUSEWAY-4062: add semantic member span names
---
.../ROOT/pages/2026/4.0.0-M3/otel-howto.adoc | 15 ++-
.../modules/ROOT/pages/observability.adoc | 21 +++-
.../observation/CausewayObservationNaming.java | 108 +++++++++++++++++++++
.../observation/CausewayObservationNamingTest.java | 45 +++++++++
.../core/metamodel/execution/ActionExecutor.java | 19 ++++
.../executor/MemberExecutorServiceDefault.java | 37 +++++--
.../executor/MemberObservationPolicyTest.java | 72 +++++++++++++-
.../changes/port-semantic-member-names/tasks.md | 16 +--
.../port-semantic-member-names/validation.md | 25 +++++
.../planned-changes/otel-forward-port-roadmap.md | 4 +-
.../tracing/MicrometerTracingAgentFixture.java | 50 +++++++++-
.../MicrometerTracingCompatibilityTest.java | 44 ++++++++-
12 files changed, 432 insertions(+), 24 deletions(-)
diff --git a/antora/components/relnotes/modules/ROOT/pages/2026/4.0.0-M3/otel-howto.adoc b/antora/components/relnotes/modules/ROOT/pages/2026/4.0.0-M3/otel-howto.adoc
index 0445485ab8e..f1573ce2617 100644
--- a/antora/components/relnotes/modules/ROOT/pages/2026/4.0.0-M3/otel-howto.adoc
+++ b/antora/components/relnotes/modules/ROOT/pages/2026/4.0.0-M3/otel-howto.adoc
@@ -114,9 +114,20 @@ HTMX domain operations use `/graphql`; Wicket requests use `/wicket/` paths.
* Expand the span tree and inspect the spans
+
-Span display names are contextual, for example `Action <member>` or `Persist <entity type>`.
+Span display names describe domain work, for example `act petclinic.PetOwner#updateName` or `Persist <entity type>`.
+Mixed-in actions use the contributed action name rather than the mixin implementation's `act` method.
+Mixed-in properties and collections use `prop <logical-type>#<member>` and `coll <logical-type>#<member>`; opening an owner can show `coll petclinic.PetOwner#visits`.
+Regular property updates retain their `Property Update` spans.
+
-NOTE: instance identifiers, object titles or argument values and _not_ included in the spans.
+Select a member span to inspect its full identifier: `causeway.action.id`, `causeway.property.id` or `causeway.collection.id`.
+Action identifiers include declared parameter types, for example `petclinic.PetOwner#updateName(java.lang.String)`.
+In Jaeger's *Attributes* search field, use `causeway.action.id="petclinic.PetOwner#updateName(java.lang.String)"` or `causeway.collection.id="petclinic.PetOwner#visits"` with *All Span Names* selected.
++
+Display names retain identifier casing and are limited to 50 characters: long names lose their namespace first, then are truncated if still too long.
+The full identifier attribute remains intact, so use it to distinguish compact names that look the same.
+These names and attributes work in both Boot-managed and agent-managed tracing; they require no extra configuration.
++
+NOTE: instance identifiers, object titles or argument values are _not_ included in the spans.
* on the top-level "entry" span, inspect its *attributes*:
diff --git a/antora/components/userguide/modules/ROOT/pages/observability.adoc b/antora/components/userguide/modules/ROOT/pages/observability.adoc
index d86d505accb..8ea6385bfea 100644
--- a/antora/components/userguide/modules/ROOT/pages/observability.adoc
+++ b/antora/components/userguide/modules/ROOT/pages/observability.adoc
@@ -78,6 +78,25 @@ The former `causeway.observation.jpa-duration-threshold` property has been remov
Filtering a JPA parent independently could leave its JDBC children without an exported parent.
Normal sampling, exporter failures and application-defined policies still determine which spans reach the backend.
+== Semantic member spans
+
+Action observations use the fixed Micrometer operation `causeway.member.action` and contextual name `act <logical-type>#<member>`.
+For mixed-in actions, the name identifies the contributed domain action rather than the mixin's implementation method.
+Mixed-in property and collection accesses use `causeway.property.access` / `causeway.collection.access`, with contextual prefixes `prop` / `coll`.
+Regular property updates remain `causeway.member.property-update` observations.
+
+Inspect `causeway.action.id`, `causeway.property.id` or `causeway.collection.id` for the complete canonical domain identifier.
+Action identifiers retain declared parameter signatures.
+The existing generic `causeway.member.id` retains the physical invoked member's metadata; use the category-specific keys for domain-facing searches.
+
+Contextual names retain casing and use a 50-character display budget.
+Names that exceed the budget first use a namespace-free type name, then truncate if still necessary; complete identifier attributes remain unchanged.
+Names contain static model identifiers, not instance titles, bookmarks or argument values.
+Unresolved mixed-in association metadata falls back to an action observation.
+
+Both Boot and agent modes use this naming without extra settings.
+The root interaction and HTTP entry names are unchanged; Wicket rendering regions and semantic entry-span names are separate capabilities.
+
== Observation names and metadata migration (CAUSEWAY-4096)
Micrometer's operation name identifies an operation category and its meters.
@@ -96,7 +115,7 @@ Bookmarks, instance identifiers, titles, query descriptions and query arguments
|`Refresh (type=...)` |`causeway.jpa.refresh` |`Refresh <entity type>`
|`Remove (type=...)` |`causeway.jpa.remove` |`Remove <entity type>`
|`Detach (type=...)` |`causeway.jpa.detach` |`Detach <entity type>`
-|`Action Invocation (...)` |`causeway.member.action` |`Action <member identifier>`
+|`Action Invocation (...)` |`causeway.member.action` |`act <domain logical type>#<member>`
|`Property Update (...)` |`causeway.member.property-update` |`Property Update <member identifier>`
|`Execution Publishing (subscribers=N)` |`causeway.execution.publish` |`Execution Publishing`
|===
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationNaming.java b/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationNaming.java
new file mode 100644
index 00000000000..94b4a713d82
--- /dev/null
+++ b/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationNaming.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.config.observation;
+
+import java.util.Objects;
+
+/**
+ * Creates compact contextual names with a bounded display length for semantic observations.
+ *
+ * @since 4.0.0
+ */
+public final class CausewayObservationNaming {
+
+ public static final int MAX_CONTEXTUAL_NAME_LENGTH = 50;
+
+ private CausewayObservationNaming() {}
+
+ public static String forType(
+ final String operation,
+ final String logicalTypeName) {
+ final String prefix = requireText(operation, "operation") + " ";
+ final String typeName = requireText(logicalTypeName, "logicalTypeName");
+ final String fullName = prefix + typeName;
+ return fullName.length() <= MAX_CONTEXTUAL_NAME_LENGTH
+ ? fullName
+ : bounded(prefix + simpleTypeName(typeName));
+ }
+
+ public static String forMember(
+ final String operation,
+ final String logicalTypeName,
+ final String memberLogicalName) {
+ final String typeName = requireText(logicalTypeName, "logicalTypeName");
+ final String memberName = requireText(memberLogicalName, "memberLogicalName");
+ return forLogicalMember(operation, typeName + "#" + memberName);
+ }
+
+ public static String forLogicalMember(
+ final String operation,
+ final String logicalMemberIdentifier) {
+ final String prefix = requireText(operation, "operation") + " ";
+ final String identifier = requireText(
+ logicalMemberIdentifier, "logicalMemberIdentifier");
+ final String fullName = prefix + identifier;
+ if(fullName.length() <= MAX_CONTEXTUAL_NAME_LENGTH) {
+ return fullName;
+ }
+
+ final int memberSeparator = identifier.indexOf('#');
+ if(memberSeparator < 1 || memberSeparator + 1 >= identifier.length()) {
+ return bounded(fullName);
+ }
+ final String typeName = identifier.substring(0, memberSeparator);
+ final String memberName = identifier.substring(memberSeparator + 1);
+ return bounded(prefix + simpleTypeName(typeName) + "#" + memberName);
+ }
+
+ public static String bounded(final String contextualName) {
+ final String name = requireText(contextualName, "contextualName");
+ return name.length() <= MAX_CONTEXTUAL_NAME_LENGTH
+ ? name
+ : name.substring(0, MAX_CONTEXTUAL_NAME_LENGTH);
+ }
+
+ private static String simpleTypeName(final String logicalTypeName) {
+ int separator = -1;
+ for (int i = 0; i < logicalTypeName.length(); i++) {
+ switch (logicalTypeName.charAt(i)) {
+ case '.':
+ case '/':
+ case '\\':
+ case ':':
+ case '$':
+ separator = i;
+ break;
+ default:
+ break;
+ }
+ }
+ return separator >= 0 && separator + 1 < logicalTypeName.length()
+ ? logicalTypeName.substring(separator + 1)
+ : logicalTypeName;
+ }
+
+ private static String requireText(final String value, final String name) {
+ Objects.requireNonNull(value, name);
+ if(value.trim().isEmpty()) {
+ throw new IllegalArgumentException(name + " must not be blank");
+ }
+ return value;
+ }
+}
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/observation/CausewayObservationNamingTest.java b/core/config/src/test/java/org/apache/causeway/core/config/observation/CausewayObservationNamingTest.java
new file mode 100644
index 00000000000..c30b26541e3
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/observation/CausewayObservationNamingTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.config.observation;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+class CausewayObservationNamingTest {
+ @Test void fullNamesAndCasingAreRetainedWhenTheyFit() {
+ assertEquals("act petclinic.PetOwner#updateName", CausewayObservationNaming.forMember("act", "petclinic.PetOwner", "updateName"));
+ assertEquals("prop PetOwner#address", CausewayObservationNaming.forMember("prop", "PetOwner", "address"));
+ assertEquals("coll PetOwner#pets", CausewayObservationNaming.forMember("coll", "PetOwner", "pets"));
+ }
+ @Test void namespaceIsShortenedBeforeMemberIsTruncated() {
+ assertEquals("act PetOwner#updateName", CausewayObservationNaming.forMember("act", "very.long.namespace.with.many.components.PetOwner", "updateName"));
+ var compact = CausewayObservationNaming.forMember("act", "a.b.PetOwner", "x".repeat(100));
+ assertEquals(50, compact.length());
+ assertTrue(compact.startsWith("act PetOwner#"));
+ }
+ @Test void namespaceCompactionMayCollide() {
+ assertEquals(CausewayObservationNaming.forMember("act", "long.namespace.one.with.many.components.Owner", "name"),
+ CausewayObservationNaming.forMember("act", "long.namespace.two.with.many.components.Owner", "name"));
+ }
+ @Test void exactBudgetAndInvalidInputs() {
+ assertEquals(50, CausewayObservationNaming.forMember("act", "Type", "x".repeat(41)).length());
+ assertThrows(NullPointerException.class, () -> CausewayObservationNaming.forMember("act", null, "name"));
+ assertThrows(IllegalArgumentException.class, () -> CausewayObservationNaming.forMember("act", "Type", " "));
+ }
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/ActionExecutor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/ActionExecutor.java
index cbb7fcabfcc..f8ee544fc33 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/ActionExecutor.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/ActionExecutor.java
@@ -21,6 +21,7 @@
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import org.jspecify.annotations.NonNull;
@@ -42,12 +43,16 @@
import org.apache.causeway.core.metamodel.execution.MemberExecutorService.MemberExecutor;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetAbstract;
+import org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetForMixedInPropertyOrCollection;
import org.apache.causeway.core.metamodel.facets.actions.semantics.ActionSemanticsFacet;
import org.apache.causeway.core.metamodel.interactions.InteractionHead;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.core.metamodel.object.MmUnwrapUtils;
+import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+import org.apache.causeway.core.metamodel.spec.feature.MixedInMember;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
import org.apache.causeway.schema.ixn.v2.ActionInvocationDto;
@@ -110,6 +115,20 @@ public Object executeWithExecutingEvents(
return result;
}
+ /** Resolves the domain association implemented by this pseudo-getter, if available. */
+ public Optional<ObjectAssociation> mixedInAssociation() {
+ if (!(actionInvocationFacetAbstract instanceof
+ ActionInvocationFacetForMixedInPropertyOrCollection)) {
+ return Optional.empty();
+ }
+ return head.owner().objSpec()
+ .streamAssociations(MixedIn.INCLUDED)
+ .filter(MixedInMember.class::isInstance)
+ .filter(association -> ((MixedInMember) association)
+ .hasMixinAction(owningAction))
+ .findFirst();
+ }
+
@SneakyThrows
private Object doExecuteWithExecutingEvents(
final int executionSequence,
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
index 2ddc9bf5514..ef2216eec22 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
@@ -47,6 +47,7 @@
import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade;
import org.apache.causeway.core.config.CausewayConfiguration;
import org.apache.causeway.core.config.observation.CausewayObservationIntegration;
+import org.apache.causeway.core.config.observation.CausewayObservationNaming;
import org.apache.causeway.core.config.observation.CausewayObservationIntegration.ObservationProvider;
import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MessageTemplate;
import org.apache.causeway.core.metamodel.commons.CanonicalInvoker;
@@ -76,10 +77,11 @@
import static org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet.isPublishingEnabled;
+import io.micrometer.observation.Observation;
+
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
-import io.micrometer.observation.Observation;
/**
* Default implementation of {@link MemberExecutorService}.
@@ -136,11 +138,7 @@ private Observation memberObservation(final String operation,
public ManagedObject invokeAction(
final @NonNull ActionExecutor actionExecutor) {
- var executionResult = memberObservation("causeway.member.action", "Action",
- actionExecutor.owningAction().getFeatureIdentifier())
- .lowCardinalityKeyValue("causeway.execution.initiatedBy", actionExecutor.interactionInitiatedBy().name())
- //could also add action's args as tags (but potentially sensitive)
- //(we do this with Xray, but that is local for debugging only)
+ var executionResult = semanticMemberObservation(actionExecutor)
.observe(()->
actionExecutor.interactionInitiatedBy().isPassThrough()
? Try.call(()->
@@ -152,6 +150,33 @@ public ManagedObject invokeAction(
.valueAsNullableElseFail();
}
+ private Observation semanticMemberObservation(final ActionExecutor executor) {
+ var invokedId = executor.owningAction().getFeatureIdentifier();
+ var association = executor.mixedInAssociation();
+ var domainId = association.map(member -> member.getFeatureIdentifier())
+ .orElseGet(() -> invokedId == null ? null : domainFacingActionIdentifier(executor.head(), executor.owningAction()));
+ var property = association.map(member -> member.isSingular()).orElse(false);
+ var operation = association.isEmpty() ? "causeway.member.action"
+ : property ? "causeway.property.access" : "causeway.collection.access";
+ var prefix = association.isEmpty() ? "act" : property ? "prop" : "coll";
+ var identityKey = association.isEmpty() ? "causeway.action.id"
+ : property ? "causeway.property.id" : "causeway.collection.id";
+ var observation = observationProvider.get(operation).contextualName(prefix)
+ .lowCardinalityKeyValue("causeway.execution.initiatedBy", executor.interactionInitiatedBy().name());
+ // Generic metadata retains the physical identity for existing consumers.
+ // The semantic key carries the complete domain identity, independently
+ // of the compact display name and without runtime values or arguments.
+ if (invokedId != null) {
+ observation.lowCardinalityKeyValue("causeway.member.id", invokedId.toString());
+ }
+ if (domainId != null) {
+ observation.contextualName(CausewayObservationNaming.forMember(
+ prefix, domainId.logicalTypeName(), domainId.memberLogicalName()))
+ .lowCardinalityKeyValue(identityKey, domainId.getLogicalIdentityString("#"));
+ }
+ return observation;
+ }
+
private ManagedObject invokeActionInternally(
final ActionExecutor actionExecutor) {
diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberObservationPolicyTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberObservationPolicyTest.java
index 50be6f2e901..497dc60cb09 100644
--- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberObservationPolicyTest.java
+++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberObservationPolicyTest.java
@@ -95,11 +95,81 @@ public static class Target {
verify(manager).adapt("sentinel-result");
var context = stopped.get(stopped.size() - 1);
assertEquals("causeway.member.action", context.getName());
- assertEquals("Action " + id, context.getContextualName());
+ assertEquals(org.apache.causeway.core.config.observation.CausewayObservationNaming.forMember(
+ "act", id.logicalTypeName(), id.memberLogicalName()), context.getContextualName());
+ assertEquals(id.getLogicalIdentityString("#"), context.getLowCardinalityKeyValue("causeway.action.id").getValue());
assertEquals(id.toString(), context.getLowCardinalityKeyValue("causeway.member.id").getValue());
assertFalse(context.getAllKeyValues().toString().contains("sentinel"));
}
}
+ @Test void mixedInAssociationResolutionSelectsActualContribution() throws Exception {
+ var spec = mock(org.apache.causeway.core.metamodel.spec.ObjectSpecification.class);
+ var loader = mock(org.apache.causeway.core.metamodel.specloader.SpecificationLoader.class);
+ when(spec.getSpecificationLoader()).thenReturn(loader);
+ when(loader.specForType(Target.class)).thenReturn(java.util.Optional.of(spec));
+ var owner = ManagedObject.other(spec, new Target());
+ var head = mock(org.apache.causeway.core.metamodel.interactions.InteractionHead.class);
+ when(head.owner()).thenReturn(owner);
+ var action = mock(org.apache.causeway.core.metamodel.spec.feature.ObjectAction.class);
+ var property = association(action, true);
+ var unrelated = association(mock(org.apache.causeway.core.metamodel.spec.feature.ObjectAction.class), false);
+ var facet = mock(org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetForMixedInPropertyOrCollection.class);
+ var executor = new org.apache.causeway.core.metamodel.execution.ActionExecutor(null, null,
+ InteractionInitiatedBy.PASS_THROUGH, action, null, head, null, facet, null);
+ when(spec.streamAssociations(org.apache.causeway.core.metamodel.spec.feature.MixedIn.INCLUDED))
+ .thenAnswer(__ -> java.util.stream.Stream.of(unrelated, property));
+ assertSame(property, executor.mixedInAssociation().orElseThrow());
+ when(spec.streamAssociations(org.apache.causeway.core.metamodel.spec.feature.MixedIn.INCLUDED))
+ .thenAnswer(__ -> java.util.stream.Stream.of(unrelated));
+ assertTrue(executor.mixedInAssociation().isEmpty());
+ var ordinary = new org.apache.causeway.core.metamodel.execution.ActionExecutor(null, null,
+ InteractionInitiatedBy.PASS_THROUGH, action, null, head, null,
+ mock(org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetAbstract.class), null);
+ assertTrue(ordinary.mixedInAssociation().isEmpty());
+ }
+
+ @Test void associationCategoriesAndFallbackKeepWorkObservable() throws Exception {
+ for (int kind = 0; kind < 3; kind++) {
+ var stopped = new ArrayList<Observation.Context>();
+ var executor = mock(org.apache.causeway.core.metamodel.execution.ActionExecutor.class, RETURNS_DEEP_STUBS);
+ var invokedId = Identifier.actionIdentifier(LogicalType.fqcn(Target.class), "call", String.class);
+ when(executor.owningAction().getFeatureIdentifier()).thenReturn(invokedId);
+ when(executor.interactionInitiatedBy()).thenReturn(InteractionInitiatedBy.PASS_THROUGH);
+ when(executor.arguments()).thenReturn(org.apache.causeway.commons.collections.Can.empty());
+ var spec = mock(org.apache.causeway.core.metamodel.spec.ObjectSpecification.class);
+ var loader = mock(org.apache.causeway.core.metamodel.specloader.SpecificationLoader.class);
+ when(spec.getSpecificationLoader()).thenReturn(loader);
+ when(loader.specForType(Target.class)).thenReturn(java.util.Optional.of(spec));
+ var head = executor.head();
+ doReturn(ManagedObject.other(spec, new Target())).when(head).target();
+ doReturn(org.apache.causeway.commons.internal.reflection._MethodFacades.testing.regular(Target.class.getDeclaredMethod("call")))
+ .when(executor).method();
+ var manager = executor.facetHolder().getObjectManager();
+ doReturn(ManagedObject.unspecified()).when(manager).adapt("sentinel-result");
+ var association = association(executor.owningAction(), kind == 0);
+ var associationId = Identifier.propertyIdentifier(LogicalType.eager(Target.class, "domain.Owner"), kind == 0 ? "name" : "items");
+ when(association.getFeatureIdentifier()).thenReturn(associationId);
+ when(executor.mixedInAssociation()).thenReturn(kind < 2 ? java.util.Optional.of(association) : java.util.Optional.empty());
+ new MemberExecutorServiceDefault(null, null, null, null, null, null, null, integration(stopped)).invokeAction(executor);
+ var context = stopped.get(stopped.size() - 1);
+ var key = kind == 0 ? "causeway.property.id" : kind == 1 ? "causeway.collection.id" : "causeway.action.id";
+ assertEquals(kind == 0 ? "causeway.property.access" : kind == 1 ? "causeway.collection.access" : "causeway.member.action", context.getName());
+ assertEquals((kind < 2 ? associationId : invokedId).getLogicalIdentityString("#"), context.getLowCardinalityKeyValue(key).getValue());
+ if (kind < 2) assertNull(context.getLowCardinalityKeyValue("causeway.action.id"));
+ assertEquals(invokedId.toString(), context.getLowCardinalityKeyValue("causeway.member.id").getValue());
+ assertFalse(context.getAllKeyValues().toString().contains("sentinel"));
+ }
+ }
+
+ private org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation association(
+ org.apache.causeway.core.metamodel.spec.feature.ObjectAction action, boolean singular) {
+ var association = mock(org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation.class,
+ withSettings().extraInterfaces(org.apache.causeway.core.metamodel.spec.feature.MixedInMember.class));
+ when(((org.apache.causeway.core.metamodel.spec.feature.MixedInMember) association).hasMixinAction(action)).thenReturn(true);
+ when(association.isSingular()).thenReturn(singular);
+ return association;
+ }
+
@Test void subscriberCountsDoNotChangeNamesOrMeterDimensions() {
var stopped = new ArrayList<Observation.Context>();
var integration = integration(stopped);
diff --git a/openspec/changes/port-semantic-member-names/tasks.md b/openspec/changes/port-semantic-member-names/tasks.md
index bb1282c2198..006663011e3 100644
--- a/openspec/changes/port-semantic-member-names/tasks.md
+++ b/openspec/changes/port-semantic-member-names/tasks.md
@@ -1,16 +1,16 @@
## 1. Core semantic metadata
-- [ ] 1.1 Add the core member-name formatter and tests for casing, full-name retention, namespace shortening, truncation and canonical-name collisions; omit Wicket-only helpers.
-- [ ] 1.2 Resolve mixed-in associations from invocation facet and actual metamodel linkage in ActionExecutor; cover property, collection, multiple contributions and missing lookup.
-- [ ] 1.3 Select semantic observation category/context and full category-specific identifier in MemberExecutorServiceDefault, reusing domain action identity while retaining main action operation and existing generic metadata.
+- [x] 1.1 Add the core member-name formatter and tests for casing, full-name retention, namespace shortening, truncation and canonical-name collisions; omit Wicket-only helpers.
+- [x] 1.2 Resolve mixed-in associations from invocation facet and actual metamodel linkage in ActionExecutor; cover property, collection, multiple contributions and missing lookup.
+- [x] 1.3 Select semantic observation category/context and full category-specific identifier in MemberExecutorServiceDefault, reusing domain action identity while retaining main action operation and existing generic metadata.
## 2. Behavior and export validation
-- [ ] 2.1 Add runtime tests for ordinary/mixed-in actions and associations, complete action parameter signatures, fallback, unchanged invocation/DTO behavior and error cleanup.
-- [ ] 2.2 Extend real exported Boot/agent fixtures for act/prop/coll names, exact identifier attributes, uppercase and long/colliding names, one span per invocation and HTTP/framework/JDBC ancestry.
-- [ ] 2.3 Verify observation inactive, missing exporter, failure-followed-by-success and custom registry/tracer behavior; confirm existing handlers preserve naming before considering any scoped correction.
+- [x] 2.1 Add runtime tests for ordinary/mixed-in actions and associations, complete action parameter signatures, fallback, unchanged invocation/DTO behavior and error cleanup.
+- [x] 2.2 Extend real exported Boot/agent fixtures for act/prop/coll names, exact identifier attributes, uppercase and long/colliding names, one span per invocation and HTTP/framework/JDBC ancestry.
+- [x] 2.3 Verify observation inactive, missing exporter, failure-followed-by-success and custom registry/tracer behavior; confirm existing handlers preserve naming before considering any scoped correction.
## 3. Operator guidance and delivery checks
-- [ ] 3.1 Merge the new M3 capability into the existing otel-howto and observability guide with verified examples, full-identifier Jaeger searches and operation/context distinctions; omit before/after comparisons and migration tables, retaining the Boot-first then agent structure.
-- [ ] 3.2 Run focused naming/metamodel/runtime checks, relevant interact regressions, exported-trace compatibility suite and documentation rendering; record commands, baseline versions, results and remaining limits in validation.md.
+- [x] 3.1 Merge the new M3 capability into the existing otel-howto and observability guide with verified examples, full-identifier Jaeger searches and operation/context distinctions; omit before/after comparisons and migration tables, retaining the Boot-first then agent structure.
+- [x] 3.2 Run focused naming/metamodel/runtime checks, relevant interact regressions, exported-trace compatibility suite and documentation rendering; record commands, baseline versions, results and remaining limits in validation.md.
diff --git a/openspec/changes/port-semantic-member-names/validation.md b/openspec/changes/port-semantic-member-names/validation.md
new file mode 100644
index 00000000000..289220d9787
--- /dev/null
+++ b/openspec/changes/port-semantic-member-names/validation.md
@@ -0,0 +1,25 @@
+# Semantic member naming validation
+
+Validated 2026-10-09 on CAUSEWAY-4062-v4, following planning commit `41dfa69342c` and current-action archive `c04587a7bcc`.
+
+## Implementation
+
+Core member display names use act/prop/coll, preserve casing and have a 50-character display budget. Association categories come from the actual mixed-in association linkage, not method spelling; missing linkage falls back to action instrumentation. Full domain-facing category-specific identifiers retain declared parameter signatures. Existing generic member metadata retains physical invocation identity.
+
+The existing invocation body, interaction DTO and command paths remain unchanged. No applib APIs, tracing handler, registry/SDK replacement or configuration flags were added. Wicket naming remains a later roadmap item.
+
+## Checks
+
+Java 25.0.4-tem and Maven 3.9.13; dependencies use Boot 4.2.0-M1, Micrometer 1.18.0-M1, Micrometer Tracing 1.8.0-M1, OpenTelemetry 1.64.0 and Java agent 2.31.1.
+
+- `mvn -o -pl core/config,core/runtimeservices -am -Dtest=CausewayObservationNamingTest,MemberObservationPolicyTest,MemberExecutorServiceDefaultTest,InteractionServiceObservationTest,CausewayObservationAutoConfigurationTest,CausewayAgentObservationAutoConfigurationTest -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.source.skip=true -Dmaven.javadoc.skip=true install`: passed 43 tests. Covers display compaction/collisions, actual association selection, category/fallback metadata, executio [...]
+- `mvn -o -f regressiontests/pom.xml -pl interact -am -Dmaven.source.skip=true -Dmaven.javadoc.skip=true test`: passed 77 executed tests, with 2 existing disabled tests. Includes current-action restoration, nested failures, mixin receiver/domain identity and wrapper execution/DTO behavior.
+- `mvn -o -f regressiontests/tracing-compatibility/pom.xml test`: passed 10 tests (8 compatibility and 2 JPA policy tests). Real exported Boot/agent spans cover ordinary/mixed-in actions, property/collection access, unresolved association fallback, canonical action signatures, uppercase, colliding compact names, one member span per invocation, root/framework/JPA/JDBC ancestry, inactive observation, missing exporter and a failed request followed by success on the same worker.
+- Both modified AsciiDoc pages converted successfully with Antora's installed Asciidoctor core. Petclinic's logical type `petclinic.PetOwner`, `updateName(String)` and mixed-in visits collection were checked against sample source.
+- `git diff --check` and strict OpenSpec validation passed.
+
+Existing standard handlers preserve the names and attributes in both tracing modes; no maintenance-specific handler workaround is required.
+
+## Limits
+
+The export fixture uses controlled metamodel collaborators and actual runtime invocation, JPA observation and SQL work. It verifies declared identifier signatures independently of runtime argument values. This does not replace interactive Petclinic verification; no browser session or full reactor build was performed. Automatic HTTP/JDBC names and Wicket region naming are outside this change.
diff --git a/openspec/planned-changes/otel-forward-port-roadmap.md b/openspec/planned-changes/otel-forward-port-roadmap.md
index 541f3b402b8..9e27c6cf94a 100644
--- a/openspec/planned-changes/otel-forward-port-roadmap.md
+++ b/openspec/planned-changes/otel-forward-port-roadmap.md
@@ -14,7 +14,7 @@ Main already contains CAUSEWAY-3975, including instrumentation absent from the s
| 1b | CAUSEWAY-4096: standardize observation names, metadata and duration-filtering policy | **Archived (2026-10-05):** [standardize observation policy](../changes/archive/2026-10-05-standardize-observation-policy/proposal.md) on branch `CAUSEWAY-4096`. Follow-up to 3975: stable operation names vs contextual names; review bookmark/query-description names, identity attributes, hard-coded duration threshold and missing exported parents. Set policy before adding new semantic names. | Main [...]
| 2 | CAUSEWAY-4068 (3975/4068 provenance): classification and correlation | **Archived (2026-10-07):** [classify and correlate observations](../changes/archive/2026-10-07-classify-and-correlate-observations/proposal.md). Foreground/background classification, root interaction ID, fixed execution-mode attribute key. Depends on foundation; adapt to main's interaction layers. | `024bd33128d`, `0325232a3c7`, `3083a51eabd` |
| 3 | CAUSEWAY-4058: current action context | **Archived (2026-10-09):** [port current action context](../changes/archive/2026-10-09-port-current-action-context/proposal.md). Exact-current-action queries and rule-checking state; nested restoration and physical mixin receiver semantics. Can proceed independently of most telemetry work. | `6d037db6609`; account for `f66b8f17a89` |
-| 4 | CAUSEWAY-4062: semantic member names | **Proposed:** [port semantic member names](../changes/port-semantic-member-names/proposal.md). Bounded operation categories with domain-facing contextual names; mixed-in property/collection access distinguished from actions. Depends on foundation and naming policy. | `a2ae5877c99`, `d5ecd49a1f3`, `45e01ff6f42` |
+| 4 | CAUSEWAY-4062: semantic member names | **Implemented, awaiting review/archive:** [port semantic member names](../changes/port-semantic-member-names/proposal.md). Bounded operation categories with domain-facing contextual names; mixed-in property/collection access distinguished from actions. Depends on foundation and naming policy. | `a2ae5877c99`, `d5ecd49a1f3`, `45e01ff6f42` |
| 5 | 4059 + relevant 4062: Wicket regions/preparation | Planned. Entity-page preparation, region rendering and action prompts; Ajax ancestry, serialization and failure cleanup. The 4062 source touches Wicket as well as core: extract the relevant portions here rather than blindly applying step 4 commits. | `a8264fd1f9b`, relevant parts of `a2ae5877c99` / `45e01ff6f42` |
| 6 | 4062 + 4067: collection detail and volume | Planned. Table phases, rows, cells, row actions; deliver detail levels, request budgets and collection aggregates with the new detail. Depends on Wicket regions. | `8e5eaab6145`, `036a28a23c2`, `5a1dc2dd17b` |
| 7 | 4063: semantic trace display names | Planned. Foreground entry-span naming from prioritized action/prompt/view nominations. Depends on classification, semantic naming and Wicket nominations for Wicket support. | `e17c3088ac2` |
@@ -140,3 +140,5 @@ Current action context archived on 2026-10-09 after rebase onto main `ce0c8352de
Step 4 is promoted into [port-semantic-member-names](../changes/port-semantic-member-names/proposal.md), with design, requirements and implementation tasks. This extracts core naming from `a2ae5877c99`, `d5ecd49a1f3` and `45e01ff6f42`: domain-facing act/prop/coll displays, distinct mixed-in association categories, full canonical semantic attributes and bounded case-preserving display compaction. Main's existing `causeway.member.action` operation is preserved for ordinary actions. Wicket [...]
Planning baseline is local `c04587a7bcc` on CAUSEWAY-4058, following current-action context archival. Refresh main and select a CAUSEWAY-4062 implementation branch before apply. The M3 how-to and observability guide are implementation tasks; no runtime validation or implementation is claimed by this proposal.
+
+Semantic member naming implemented on `CAUSEWAY-4062-v4` after planning commit `41dfa69342c`. All eight tasks are complete; [validation evidence](../changes/port-semantic-member-names/validation.md) records 43 focused tests, 77 executed interact regressions (2 existing disabled tests), 10 tracing/JPA compatibility tests and successful documentation rendering. Standard Boot and agent handlers preserve casing without a custom handler. Implementation is recorded in the checkpoint commit con [...]
diff --git a/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingAgentFixture.java b/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingAgentFixture.java
index 79b25dd545f..263e5e0ac59 100644
--- a/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingAgentFixture.java
+++ b/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingAgentFixture.java
@@ -87,6 +87,8 @@ public static void main(String[] args) throws Exception {
var members = (MemberExecutorServiceDefault) constructor.newInstance(
interactions, null, null, null, null, null, null, integration);
var action = action(executionContext, integration, context.getEnvironment().acceptsProfiles(org.springframework.core.env.Profiles.of("agent")));
+ boolean semantic = context.getEnvironment().getProperty("fixture.semantic", Boolean.class, false);
+ var semanticActions = semantic ? semanticActions(action) : List.<ActionExecutor>of();
var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
// One worker proves that subsequent requests cannot inherit stale thread-local scopes.
var executor = Executors.newSingleThreadExecutor();
@@ -99,7 +101,9 @@ public static void main(String[] args) throws Exception {
interactions.call(org.apache.causeway.applib.services.iactn.InteractionContext.ofUserWithSystemDefaults(
org.apache.causeway.applib.services.user.UserMemento.ofName("sentinel-user")
.withMultiTenancyToken("sentinel-tenant")), () -> {
- members.invokeAction(action);
+ if (semantic) {
+ for (var semanticAction : semanticActions) members.invokeAction(semanticAction);
+ } else members.invokeAction(action);
if (exchange.getRequestURI().getQuery() != null) throw new IllegalStateException("fixture failure");
return null;
});
@@ -140,6 +144,50 @@ private static ActionExecutor action(ExecutionContext executionContext, Causeway
_MethodFacades.testing.regular(JdbcAction.class.getDeclaredMethod("executeJdbc")), head, Can.empty(),
mock(ActionInvocationFacetAbstract.class), integration.provider(ActionExecutor.class));
}
+ private static List<ActionExecutor> semanticActions(ActionExecutor base) {
+ var result = new java.util.ArrayList<ActionExecutor>();
+ var target = base.head().target();
+ for (int kind = 0; kind < 7; kind++) {
+ var action = mock(ObjectAction.class);
+ var ownerSpec = mock(org.apache.causeway.core.metamodel.spec.ObjectSpecification.class);
+ var logicalName = kind >= 5 ? "very.long.namespace." + (kind == 5 ? "one" : "two")
+ + ".with.many.components.UpperCaseOwner" : "domain.UpperCaseOwner";
+ var ownerType = LogicalType.eager(JdbcAction.class, logicalName);
+ when(ownerSpec.logicalType()).thenReturn(ownerType);
+ var ownerLoader = mock(org.apache.causeway.core.metamodel.specloader.SpecificationLoader.class);
+ when(ownerSpec.getSpecificationLoader()).thenReturn(ownerLoader);
+ when(ownerLoader.specForType(JdbcAction.class)).thenReturn(java.util.Optional.of(ownerSpec));
+ var head = mock(InteractionHead.class);
+ when(head.target()).thenReturn(target);
+ var owner = ManagedObject.other(ownerSpec, target.getPojo());
+ when(head.owner()).thenReturn(owner);
+ var invokedId = Identifier.actionIdentifier(kind == 0 || kind >= 5 ? ownerType
+ : LogicalType.eager(JdbcAction.class, "implementation.Owner_updateName"), "executeJdbc", String.class);
+ when(action.getFeatureIdentifier()).thenReturn(invokedId);
+ if (kind > 0 && kind < 5) {
+ when(action.isDeclaredOnMixin()).thenReturn(true);
+ var model = mock(org.apache.causeway.core.metamodel.progmodel.ProgrammingModel.class, RETURNS_DEEP_STUBS);
+ when(action.getProgrammingModel()).thenReturn(model);
+ when(model.mixinNamingStrategy().memberId(JdbcAction.class)).thenReturn("updateName");
+ }
+ org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetAbstract facet;
+ if (kind >= 2 && kind <= 4) {
+ facet = mock(org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetForMixedInPropertyOrCollection.class);
+ var association = mock(org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation.class,
+ withSettings().extraInterfaces(org.apache.causeway.core.metamodel.spec.feature.MixedInMember.class));
+ when(((org.apache.causeway.core.metamodel.spec.feature.MixedInMember) association).hasMixinAction(action)).thenReturn(true);
+ when(association.isSingular()).thenReturn(kind == 2);
+ when(association.getFeatureIdentifier()).thenReturn(Identifier.propertyIdentifier(ownerType, kind == 2 ? "name" : "items"));
+ boolean missing = kind == 4;
+ when(ownerSpec.streamAssociations(org.apache.causeway.core.metamodel.spec.feature.MixedIn.INCLUDED))
+ .thenAnswer(__ -> missing ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(association));
+ } else facet = mock(ActionInvocationFacetAbstract.class);
+ result.add(new ActionExecutor(base.executionContext(), base.facetHolder(), base.interactionInitiatedBy(),
+ action, base.method(), head, base.arguments(), facet, base.observationProvider()));
+ }
+ return result;
+ }
+
public static class JdbcAction {
private final org.apache.causeway.core.metamodel.facets.object.entity.EntityFacet jpa;
public JdbcAction(CausewayObservationIntegration integration, boolean agent) {
diff --git a/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingCompatibilityTest.java b/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingCompatibilityTest.java
index cf3e841a456..6d3de25aba8 100644
--- a/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingCompatibilityTest.java
+++ b/regressiontests/tracing-compatibility/src/test/java/org/apache/causeway/regressiontests/tracing/MicrometerTracingCompatibilityTest.java
@@ -51,7 +51,7 @@ class MicrometerTracingCompatibilityTest {
var http = spans.stream().filter(s -> s.getSpanId().equals(root.getParentSpanId())).findFirst().orElseThrow(() -> new AssertionError(names(spans)));
assertEquals(Span.SpanKind.SPAN_KIND_SERVER, http.getKind());
assertEquals(http.getTraceId(), root.getTraceId());
- var actions = spans.stream().filter(s -> s.getParentSpanId().equals(root.getSpanId()) && s.getName().startsWith("Action ")).toList();
+ var actions = spans.stream().filter(s -> s.getParentSpanId().equals(root.getSpanId()) && s.getName().startsWith("act ")).toList();
assertEquals(1, actions.size(), names(spans));
var action = actions.get(0);
assertEquals(root.getTraceId(), action.getTraceId());
@@ -64,7 +64,7 @@ class MicrometerTracingCompatibilityTest {
for (String mode : List.of("boot", "agent")) {
try (var collector = new Collector()) {
launch(mode, collector, "--causeway.observation.jpa-duration-threshold=1d");
- var actions = collector.spans.stream().filter(s -> s.getName().startsWith("Action ")).toList();
+ var actions = collector.spans.stream().filter(s -> s.getName().startsWith("act ")).toList();
assertEquals(2, actions.size(), names(collector.spans));
for (var action : actions) assertJpaAncestry(collector.spans, action, mode.equals("agent"));
assertFalse(collector.spans.stream().anyMatch(s -> attribute(s, "causeway.discard") != null));
@@ -74,7 +74,7 @@ class MicrometerTracingCompatibilityTest {
@Test void bootDefaultsRetainJpaParentsAndIncludeIdentity() throws Exception {
try (var collector = new Collector()) {
launch("boot", collector);
- var actions = collector.spans.stream().filter(s -> s.getName().startsWith("Action ")).toList();
+ var actions = collector.spans.stream().filter(s -> s.getName().startsWith("act ")).toList();
assertEquals(2, actions.size(), names(collector.spans));
for (var action : actions) assertJpaAncestry(collector.spans, action, false);
collector.spans.stream().filter(s -> s.getName().equals("Causeway Root Interaction"))
@@ -91,6 +91,42 @@ class MicrometerTracingCompatibilityTest {
}
}
}
+ @Test void semanticMembersExportDomainIdentityAndCompactNamesInBothModes() throws Exception {
+ for (String mode : List.of("boot", "agent")) {
+ try (var collector = new Collector()) {
+ launch(mode, collector, "--fixture.semantic=true");
+ var members = collector.spans.stream().filter(span -> attribute(span, "causeway.member.id") != null).toList();
+ assertEquals(14, members.size(), names(collector.spans)); // seven operations, two requests
+ for (String key : List.of("causeway.action.id", "causeway.property.id", "causeway.collection.id")) {
+ var selected = members.stream().filter(span -> attribute(span, key) != null).toList();
+ assertEquals(key.equals("causeway.action.id") ? 10 : 2, selected.size(), names(members));
+ for (var span : selected) {
+ assertTrue(span.getName().startsWith(key.equals("causeway.action.id") ? "act "
+ : key.equals("causeway.property.id") ? "prop " : "coll "), span.getName());
+ assertTrue(span.getName().contains("UpperCaseOwner#"), span.getName());
+ assertTrue(span.getName().length() <= 50);
+ assertFalse(attribute(span, key).contains("implementation."));
+ if (!key.equals("causeway.action.id")) assertNull(attribute(span, "causeway.action.id"));
+ var root = collector.spans.stream().filter(parent -> parent.getSpanId().equals(span.getParentSpanId()))
+ .findFirst().orElseThrow();
+ assertEquals("Causeway Root Interaction", root.getName());
+ assertEquals(root.getTraceId(), span.getTraceId());
+ assertJpaAncestry(collector.spans, span, mode.equals("agent"));
+ }
+ }
+ var longActions = members.stream().filter(span -> {
+ var id = attribute(span, "causeway.action.id");
+ return id != null && id.startsWith("very.long.namespace.");
+ }).toList();
+ assertEquals(4, longActions.size());
+ assertEquals(1, longActions.stream().map(Span::getName).distinct().count());
+ assertEquals(2, longActions.stream().map(span -> attribute(span, "causeway.action.id")).distinct().count());
+ assertTrue(longActions.stream().allMatch(span -> attribute(span, "causeway.action.id").endsWith("(java.lang.String)")));
+ assertTrue(members.stream().anyMatch(span -> "act domain.UpperCaseOwner#updateName".equals(span.getName())));
+ }
+ }
+ }
+
private void assertIdentity(Span span, String userName, String token) {
var attributes = span.getAttributesList().stream().collect(java.util.stream.Collectors.toMap(
a -> a.getKey(), a -> a.getValue().getStringValue()));
@@ -114,7 +150,7 @@ private void assertJpaAncestry(List<Span> spans, Span action, boolean agent) {
try (var collector = new Collector()) {
launch("inactive", collector);
assertTrue(collector.spans.stream().anyMatch(s -> s.getKind() == Span.SpanKind.SPAN_KIND_SERVER), names(collector.spans));
- assertFalse(collector.spans.stream().anyMatch(s -> s.getName().contains("Causeway Root Interaction") || s.getName().startsWith("Action ")), names(collector.spans));
+ assertFalse(collector.spans.stream().anyMatch(s -> s.getName().contains("Causeway Root Interaction") || s.getName().startsWith("act ")), names(collector.spans));
}
}
@Test void activeWithoutAgentOrExporterIsSafe() throws Exception {
(causeway) 01/02: CAUSEWAY-4062: propose semantic member span names
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-4062-v4
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 41dfa69342c1047f255778bb9ffa54c32887e38c
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Oct 9 16:22:26 2026 +0100
CAUSEWAY-4062: propose semantic member span names
---
.../port-semantic-member-names/.openspec.yaml | 2 +
.../changes/port-semantic-member-names/design.md | 51 +++++++++++++++
.../changes/port-semantic-member-names/proposal.md | 28 +++++++++
.../specs/semantic-member-span-naming/spec.md | 73 ++++++++++++++++++++++
.../changes/port-semantic-member-names/tasks.md | 16 +++++
.../planned-changes/otel-forward-port-roadmap.md | 8 ++-
6 files changed, 177 insertions(+), 1 deletion(-)
diff --git a/openspec/changes/port-semantic-member-names/.openspec.yaml b/openspec/changes/port-semantic-member-names/.openspec.yaml
new file mode 100644
index 00000000000..81264324b05
--- /dev/null
+++ b/openspec/changes/port-semantic-member-names/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-10-09
diff --git a/openspec/changes/port-semantic-member-names/design.md b/openspec/changes/port-semantic-member-names/design.md
new file mode 100644
index 00000000000..a650be5d2dc
--- /dev/null
+++ b/openspec/changes/port-semantic-member-names/design.md
@@ -0,0 +1,51 @@
+## Context
+
+Main already separates operation and contextual names under CAUSEWAY-4096. MemberExecutorServiceDefault currently emits `causeway.member.action`, `Action <invoked identifier>` and `causeway.member.id`. CAUSEWAY-4058 supplies separate invoked/domain-facing action identity; main's InteractionHead is a mixee/mixin record, unlike maintenance's managed head hierarchy.
+
+Maintenance's three source commits also modify Wicket and install a custom tracing handler for its older Micrometer baseline. Neither should be copied wholesale. Main has both Boot-managed and framework-supplied agent bridge configurations and supports application-provided telemetry beans.
+
+## Goals / Non-Goals
+
+**Goals:** domain-facing action/property/collection names; fixed meter categories; full canonical search attributes; case-preserving bounded display names; unchanged execution and trace ownership; demonstrated behavior in both modes.
+
+**Non-Goals:** Wicket rendering/preparation/prompts; table detail/budgets; HTTP/root semantic naming; changing JPA or property-update naming; application span service; new applib APIs; duration filtering; modifying automatic agent HTTP/JDBC names.
+
+## Decisions
+
+### Preserve main's operation identity
+
+Keep `causeway.member.action` for ordinary actions instead of maintenance's `causeway.action.invocation`. Add `causeway.property.access` and `causeway.collection.access` only for safely resolved mixed-in associations. This avoids an unnecessary ordinary-action metric migration while introducing useful distinctions. Emit one observation around the existing execution path, never both old and new spans.
+
+### Derive semantic metadata without changing invocation identity
+
+Reuse the CAUSEWAY-4058 domain-facing action identifier helper. Keep physical receiver, invoked identifier, rule state, argument execution, command/DTO publishing and transaction behavior unchanged. For an ActionInvocationFacetForMixedInPropertyOrCollection, resolve its association from the owning specification using MixedInMember.hasMixinAction, then use the association's feature identifier and singular/plural kind. Do not infer kind from a method name such as prop or coll. Unresolved a [...]
+
+The full canonical domain identifier uses Identifier.getLogicalIdentityString("#"), including declared action parameter signatures; do not copy maintenance's synthetic `()` suffix. Add exactly the applicable semantic key (`causeway.action.id`, `causeway.property.id` or `causeway.collection.id`). Preserve existing generic `causeway.member.id` metadata for compatibility, documenting that the new semantic keys are authoritative for domain-facing searches. Generic property-update metadata re [...]
+
+### Share core display compaction
+
+Introduce a narrowly scoped core naming formatter for logical member display names. Try the full logical type/member string; above 50 characters retry with a namespace-free logical type, then truncate to 50 characters. Preserve static identifier casing. Display names may collide; full category-specific identity attributes distinguish them. The 50-character display budget ports maintenance's explicit contract and is not a metric-cardinality policy or a new configuration setting. Do not br [...]
+
+### Respect main's handlers and registry ownership
+
+Use standard contextual-name APIs with existing registries. Verify actual uppercase/casing and compaction in exported Boot and agent spans. Main's earlier evidence already showed preserved casing; do not add maintenance's custom tracing handler unless a focused reproduction proves it necessary. Any necessary correction must be limited to these Causeway observations and preserve application/custom registry ownership, scope, parentage, tags and errors; do not replace the shared handler pipeline.
+
+### Keep scope and operator guidance explicit
+
+Root Interaction and HTTP names remain unchanged. Existing inner execution/domain-event/JPA/transaction boundaries remain. Integrate action/mixin association examples and Jaeger searches on full semantic identifiers into the existing M3 how-to and userguide, explaining when only compact names are visible and when association fallback remains action-shaped. Present this as new M3 functionality, without a before/after comparison or migration table. Preserve the existing document flow. Use [...]
+
+## Risks / Trade-offs
+
+- [Risk] Association lookup selects a different contribution → match the actual implementation action via metamodel linkage; cover multiple contributions and missing lookup.
+- [Risk] Short display names collide or lose namespace/signature → retain full canonical semantic attributes and test collision/overload cases.
+- [Risk] Different tracing integrations alter casing → use exported Boot/agent assertions before considering specialized handling.
+- [Risk] Telemetry consumers depend on old names/categories → describe the resulting operation categories and canonical search attributes within the existing guidance, and avoid duplicate legacy spans.
+- [Risk] Introducing new observations changes failure/transaction behavior → keep the current observe boundary and verify original exception, cleanup and parentage.
+
+## Migration Plan
+
+No Java application API or configuration migration is required. Update trace-name searches to act/prop/coll and use the new semantic identifier keys for exact searches; dashboards counting mixed-in accesses as actions must select the new property/collection categories. No rollback data migration is needed. Refresh target main and ensure CAUSEWAY-4058 is present before implementation; no remote verification or runtime validation was performed during proposal creation.
+
+## Open Questions
+
+None requiring user input. Actual sample examples and export-handler behavior are implementation verification tasks, not assumptions of maintenance compatibility.
diff --git a/openspec/changes/port-semantic-member-names/proposal.md b/openspec/changes/port-semantic-member-names/proposal.md
new file mode 100644
index 00000000000..e6c5a079770
--- /dev/null
+++ b/openspec/changes/port-semantic-member-names/proposal.md
@@ -0,0 +1,28 @@
+## Why
+
+Main's member observations expose generic action names and mixin implementation identities. Operators should see the domain action, property or collection being accessed, with stable metric categories and complete identifiers for searching traces.
+
+## What Changes
+
+- Give action spans contextual names `act <logical-type>#<member>`, using contributed domain identity for mixins while preserving main's `causeway.member.action` operation name.
+- Recognize mixed-in property/collection implementations and instrument them as `causeway.property.access` / `causeway.collection.access`, with `prop` / `coll` contextual names.
+- Add full canonical domain identifiers as `causeway.action.id`, `causeway.property.id` or `causeway.collection.id`, retaining parameter signatures for actions.
+- Port compact naming: retain full logical names when they fit, shorten namespaces when needed, then bound display names to 50 characters. Preserve declared casing and complete identity attributes.
+- **BREAKING (telemetry only):** contextual member names change, and resolved mixed-in association accesses move from action meters to their own categories. Avoid duplicate legacy spans.
+- Extend real Boot/agent trace fixtures and merge examples of the new semantic names and identifier searches into the existing M3 how-to content; no before/after comparison is needed.
+
+## Capabilities
+
+### New Capabilities
+
+- `semantic-member-span-naming`: Domain-facing member display names, category-specific canonical attributes, mixed-in association recognition and safe display compaction.
+
+### Modified Capabilities
+
+None. The existing observation-policy contract remains intact: stable operation categories, static metadata and preserved invocation semantics.
+
+## Impact
+
+Core configuration naming helper; metamodel ActionExecutor association lookup; runtime MemberExecutorServiceDefault observation selection; core and exported-trace tests; M3 how-to and observability guide. No applib API additions, configuration flags, new SDK/exporter or dependency pins.
+
+Sources: maintenance `a2ae5877c99`, `d5ecd49a1f3`, `45e01ff6f42`. Extract only core naming and tests; Wicket preparation/prompts/regions remain roadmap step 5, collection rendering detail step 6 and entry trace naming step 7. Planning baseline: local `c04587a7bcc` on CAUSEWAY-4058; refresh main and select a CAUSEWAY-4062 implementation branch before apply.
diff --git a/openspec/changes/port-semantic-member-names/specs/semantic-member-span-naming/spec.md b/openspec/changes/port-semantic-member-names/specs/semantic-member-span-naming/spec.md
new file mode 100644
index 00000000000..132d915bd58
--- /dev/null
+++ b/openspec/changes/port-semantic-member-names/specs/semantic-member-span-naming/spec.md
@@ -0,0 +1,73 @@
+## ADDED Requirements
+
+### Requirement: Action observations use domain-facing contextual names
+
+Causeway SHALL preserve `causeway.member.action` as the main action operation name and assign contextual names `act <logical-type>#<member>` using domain-facing action identity. It SHALL add `causeway.action.id` containing the complete canonical logical action identifier, including declared parameter signatures. Physical receiver, invoked identifier, rule state and publishing/DTO behavior SHALL remain unchanged.
+
+#### Scenario: Declared action
+- **WHEN** a declared action is observed
+- **THEN** its display name begins with act and identifies the domain type/member, while its operation remains causeway.member.action and its semantic attribute retains the complete signature
+
+#### Scenario: Contributed action
+- **WHEN** a mixin implementation method act contributes Customer#updateName
+- **THEN** the contextual name and causeway.action.id identify Customer#updateName rather than the mixin method, without changing the actual invocation identity
+
+### Requirement: Mixed-in association accesses have distinct semantic categories
+
+Causeway SHALL recognize mixed-in property/collection access by facet and metamodel association linkage. Resolved singular access SHALL use `causeway.property.access`, contextual prefix prop and `causeway.property.id`. Resolved collection access SHALL use `causeway.collection.access`, contextual prefix coll and `causeway.collection.id`. Both SHALL use the full domain-facing association identifier. Only the applicable semantic identifier key SHALL be emitted; association spans SHALL NOT b [...]
+
+#### Scenario: Property access through a mixin
+- **WHEN** a mixin implements a domain property's getter
+- **THEN** one prop span identifies the domain property in its property-access category, not as an action invocation
+
+#### Scenario: Collection access through a mixin
+- **WHEN** a mixin implements a domain collection's getter
+- **THEN** one coll span identifies the domain collection in its collection-access category
+
+#### Scenario: Unresolved association
+- **WHEN** the association facet is present but its domain association cannot be recovered safely
+- **THEN** ordinary action observation remains available without inventing association identity or dropping instrumentation
+
+### Requirement: Compact display names preserve canonical identity and privacy
+
+Contextual names SHALL contain only operation prefixes and static logical model identifiers, preserving declared casing. The formatter SHALL retain a full name when it fits within 50 characters, otherwise retry with a namespace-free type name, then truncate to 50 characters if necessary. The semantic identity attribute SHALL retain its complete identifier independently of compaction. Names and semantic attributes SHALL NOT include instance IDs, bookmarks, titles, argument values, usernam [...]
+
+#### Scenario: Short full name
+- **WHEN** an act, prop or coll name fits the display budget
+- **THEN** its complete logical type/member name and casing are retained
+
+#### Scenario: Long name
+- **WHEN** a name exceeds 50 characters
+- **THEN** its namespace is shortened before truncation, while its full semantic identity attribute is unchanged
+
+#### Scenario: Collision or overload
+- **WHEN** different canonical members have the same compact display name or actions have different declared parameter signatures
+- **THEN** their full semantic attributes distinguish them
+
+#### Scenario: Instance-specific inputs
+- **WHEN** the same logical operation runs for different instances or arguments
+- **THEN** display name and semantic attributes remain identical and contain none of those instance-specific values
+
+### Requirement: Both tracing owners preserve lifecycle and semantic names
+
+Semantic naming SHALL work under supported Boot-managed and agent-managed tracing, preserving casing, parentage, scopes, attributes and errors. It SHALL create no competing registry, SDK/exporter or duplicate observations, and inactive observations SHALL remain inert. Root interaction, HTTP, JDBC and unrelated instrumentation boundaries SHALL remain unchanged by this feature.
+
+#### Scenario: Boot and agent exports
+- **WHEN** representative declared/mixed-in action, property and collection work is collected in each mode
+- **THEN** exported spans show the expected contextual names and canonical attributes with existing ancestry and exactly one member observation per invocation
+
+#### Scenario: Failure followed by success
+- **WHEN** observed member work fails and later work succeeds
+- **THEN** the original failure is preserved and the later execution inherits no stale scope or name
+
+#### Scenario: Observation disabled
+- **WHEN** the observation profile is inactive
+- **THEN** invocation behavior is unchanged and no Causeway semantic spans are emitted
+
+### Requirement: Operators can inspect semantic member spans
+
+The M3 how-to and observability guide SHALL integrate the new capability into their existing content, explaining operation versus display identity, action/property/collection categories, canonical attributes and compact-name collisions. They SHALL present the capability as new M3 functionality without before/after comparisons or migration tables. They SHALL identify the Wicket/root-naming features that remain out of scope and SHALL NOT require new naming configuration flags.
+
+#### Scenario: Reader inspects semantic spans
+- **WHEN** a reader follows the updated guide
+- **THEN** they can recognize representative act/prop/coll spans and search by full semantic identifier rather than relying on potentially shortened display names
diff --git a/openspec/changes/port-semantic-member-names/tasks.md b/openspec/changes/port-semantic-member-names/tasks.md
new file mode 100644
index 00000000000..bb1282c2198
--- /dev/null
+++ b/openspec/changes/port-semantic-member-names/tasks.md
@@ -0,0 +1,16 @@
+## 1. Core semantic metadata
+
+- [ ] 1.1 Add the core member-name formatter and tests for casing, full-name retention, namespace shortening, truncation and canonical-name collisions; omit Wicket-only helpers.
+- [ ] 1.2 Resolve mixed-in associations from invocation facet and actual metamodel linkage in ActionExecutor; cover property, collection, multiple contributions and missing lookup.
+- [ ] 1.3 Select semantic observation category/context and full category-specific identifier in MemberExecutorServiceDefault, reusing domain action identity while retaining main action operation and existing generic metadata.
+
+## 2. Behavior and export validation
+
+- [ ] 2.1 Add runtime tests for ordinary/mixed-in actions and associations, complete action parameter signatures, fallback, unchanged invocation/DTO behavior and error cleanup.
+- [ ] 2.2 Extend real exported Boot/agent fixtures for act/prop/coll names, exact identifier attributes, uppercase and long/colliding names, one span per invocation and HTTP/framework/JDBC ancestry.
+- [ ] 2.3 Verify observation inactive, missing exporter, failure-followed-by-success and custom registry/tracer behavior; confirm existing handlers preserve naming before considering any scoped correction.
+
+## 3. Operator guidance and delivery checks
+
+- [ ] 3.1 Merge the new M3 capability into the existing otel-howto and observability guide with verified examples, full-identifier Jaeger searches and operation/context distinctions; omit before/after comparisons and migration tables, retaining the Boot-first then agent structure.
+- [ ] 3.2 Run focused naming/metamodel/runtime checks, relevant interact regressions, exported-trace compatibility suite and documentation rendering; record commands, baseline versions, results and remaining limits in validation.md.
diff --git a/openspec/planned-changes/otel-forward-port-roadmap.md b/openspec/planned-changes/otel-forward-port-roadmap.md
index 5ba7e2995d0..541f3b402b8 100644
--- a/openspec/planned-changes/otel-forward-port-roadmap.md
+++ b/openspec/planned-changes/otel-forward-port-roadmap.md
@@ -14,7 +14,7 @@ Main already contains CAUSEWAY-3975, including instrumentation absent from the s
| 1b | CAUSEWAY-4096: standardize observation names, metadata and duration-filtering policy | **Archived (2026-10-05):** [standardize observation policy](../changes/archive/2026-10-05-standardize-observation-policy/proposal.md) on branch `CAUSEWAY-4096`. Follow-up to 3975: stable operation names vs contextual names; review bookmark/query-description names, identity attributes, hard-coded duration threshold and missing exported parents. Set policy before adding new semantic names. | Main [...]
| 2 | CAUSEWAY-4068 (3975/4068 provenance): classification and correlation | **Archived (2026-10-07):** [classify and correlate observations](../changes/archive/2026-10-07-classify-and-correlate-observations/proposal.md). Foreground/background classification, root interaction ID, fixed execution-mode attribute key. Depends on foundation; adapt to main's interaction layers. | `024bd33128d`, `0325232a3c7`, `3083a51eabd` |
| 3 | CAUSEWAY-4058: current action context | **Archived (2026-10-09):** [port current action context](../changes/archive/2026-10-09-port-current-action-context/proposal.md). Exact-current-action queries and rule-checking state; nested restoration and physical mixin receiver semantics. Can proceed independently of most telemetry work. | `6d037db6609`; account for `f66b8f17a89` |
-| 4 | 4062: semantic member names | Planned. Bounded operation categories with domain-facing contextual names; mixed-in property/collection access distinguished from actions. Depends on foundation and naming policy. | `a2ae5877c99`, `d5ecd49a1f3`, `45e01ff6f42` |
+| 4 | CAUSEWAY-4062: semantic member names | **Proposed:** [port semantic member names](../changes/port-semantic-member-names/proposal.md). Bounded operation categories with domain-facing contextual names; mixed-in property/collection access distinguished from actions. Depends on foundation and naming policy. | `a2ae5877c99`, `d5ecd49a1f3`, `45e01ff6f42` |
| 5 | 4059 + relevant 4062: Wicket regions/preparation | Planned. Entity-page preparation, region rendering and action prompts; Ajax ancestry, serialization and failure cleanup. The 4062 source touches Wicket as well as core: extract the relevant portions here rather than blindly applying step 4 commits. | `a8264fd1f9b`, relevant parts of `a2ae5877c99` / `45e01ff6f42` |
| 6 | 4062 + 4067: collection detail and volume | Planned. Table phases, rows, cells, row actions; deliver detail levels, request budgets and collection aggregates with the new detail. Depends on Wicket regions. | `8e5eaab6145`, `036a28a23c2`, `5a1dc2dd17b` |
| 7 | 4063: semantic trace display names | Planned. Foreground entry-span naming from prioritized action/prompt/view nominations. Depends on classification, semantic naming and Wicket nominations for Wicket support. | `e17c3088ac2` |
@@ -134,3 +134,9 @@ Planning baseline: `41d8db75a06` on CAUSEWAY-4068-v4. Refresh main and select a
Current action context implemented on CAUSEWAY-4058 after planning commit `9a250065cfa`. All nine tasks completed; [validation evidence](../changes/archive/2026-10-09-port-current-action-context/validation.md) records 29 focused tests and 77 executed interact regressions (2 existing disabled tests), documentation rendering and main-specific mixin identity adaptation. Implementation is recorded in the checkpoint commit containing this update; the change was subsequently rebased and archiv [...]
Current action context archived on 2026-10-09 after rebase onto main `ce0c8352dee`: rebased planning `5c43e44b085`, implementation `be228adf4ac`. Fresh archive checks passed 29 focused tests and 77 interact regressions (2 existing disabled tests), documentation rendering and strict validation. Requirements are synced to the main specs.
+
+## Semantic member naming proposal (2026-10-09)
+
+Step 4 is promoted into [port-semantic-member-names](../changes/port-semantic-member-names/proposal.md), with design, requirements and implementation tasks. This extracts core naming from `a2ae5877c99`, `d5ecd49a1f3` and `45e01ff6f42`: domain-facing act/prop/coll displays, distinct mixed-in association categories, full canonical semantic attributes and bounded case-preserving display compaction. Main's existing `causeway.member.action` operation is preserved for ordinary actions. Wicket [...]
+
+Planning baseline is local `c04587a7bcc` on CAUSEWAY-4058, following current-action context archival. Refresh main and select a CAUSEWAY-4062 implementation branch before apply. The M3 how-to and observability guide are implementation tasks; no runtime validation or implementation is claimed by this proposal.