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:34 UTC

(causeway) 40/41: CAUSEWAY-4066: observes application primer callbacks

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 473454f2447093df5565739b93a71dc4a0fc9d89
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 24 16:50:40 2026 +0100

    CAUSEWAY-4066: observes application primer callbacks
---
 adoc/micrometer-tracing-operations.adoc            |  45 +-
 .../services/priming/PrimingRegistryDefault.java   | 106 ++++-
 .../PrimingRegistryDefaultObservationTest.java     | 487 +++++++++++++++++++++
 .../observe-application-primers/proposal.md        |   2 +-
 .../specs/application-primer-observation/spec.md   |   2 +-
 .../changes/observe-application-primers/tasks.md   |  38 +-
 .../observe-application-primers/validation.md      |  56 +++
 .../tracing/MicrometerTracingAgentFixture.java     | 107 ++++-
 .../MicrometerTracingCompatibilityTest.java        |  51 ++-
 9 files changed, 848 insertions(+), 46 deletions(-)

diff --git a/adoc/micrometer-tracing-operations.adoc b/adoc/micrometer-tracing-operations.adoc
index 7bf870d0117..a30b27b2566 100644
--- a/adoc/micrometer-tracing-operations.adoc
+++ b/adoc/micrometer-tracing-operations.adoc
@@ -322,6 +322,7 @@ With the `observation` profile active, Causeway adds these core semantic observa
 * `causeway.application.span` covers an application-defined synchronous operation and uses a display name such as `demo.Customer#updateName load`;
 * `causeway.root.interaction` covers one top-level Causeway interaction and retains that stable span display name;
 * `causeway.action.invocation` covers an action invocation and uses a display name such as `act demo.Customer#updateName`;
+* `causeway.priming.action` and `causeway.priming.view` cover application primer callbacks and use display names such as `prime action demo.Customer#updateName` and `prime view demo.Customer`;
 * `causeway.property.access` and `causeway.collection.access` cover evaluation of mixed-in associations and use display names such as `prop demo.Customer#calculatedTotal` and `coll demo.Customer#recentOrders`.
 
 The stable observation name remains `causeway.action.invocation` even though the contextual name becomes the exported tracing span display name.
@@ -333,9 +334,51 @@ Contextual names preserve the declared casing of static logical type, member, an
 Causeway limits each semantic contextual name to 50 characters.
 For page, invocation, and prompt names, Causeway first tries the full logical identifier, then omits the logical-type namespace if necessary, and finally truncates at 50 characters.
 For example, `act isisExtSecMan.ApplicationUser#updateEmailAddress` falls back to `act ApplicationUser#updateEmailAddress`.
-The same policy applies to `prop`, `coll`, `prepare`, `prompt`, and `render` contextual names.
+The same policy applies to `prop`, `coll`, `prime`, `prepare`, `prompt`, and `render` contextual names.
 Causeway does not add action arguments, return values, target instance identifiers, object titles, bookmarks, user names, tenancy tokens, or other instance-specific application data to names or attributes on these semantic spans.
 
+=== Application Primer Callbacks
+
+When an application registers `ActionPrimer` or `ViewPrimer` callbacks and the `observation` profile is active, Causeway creates one semantic observation around every matching callback that is actually invoked.
+Action primers use stable name `causeway.priming.action` and contextual name `prime action <logical-member-identifier>`.
+View primers use stable name `causeway.priming.view` and contextual name `prime view <logical-object-type>`.
+The contextual names use the same case-preserving namespace fallback and deterministic 50-character bound as other semantic names.
+
+An action primer runs within the current action invocation immediately before the action method, so its JDBC preload work appears beneath that primer:
+
+[source,text]
+----
+act demo.Order#submit
+├── prime action demo.Order#submit
+│   ├── SELECT Customer
+│   └── SELECT OrderLine
+└── application action work
+----
+
+A view primer runs before Wicket page preparation begins, so it is a sibling of later preparation and rendering beneath the naturally current request or interaction context:
+
+[source,text]
+----
+HTTP
+└── causeway.root.interaction
+    ├── prime view demo.Order
+    │   └── SELECT OrderLine
+    ├── prepare demo.Order
+    └── render demo.Order
+----
+
+When several primers match the same action or logical type, Causeway creates separate sequential sibling observations with the same semantic name and canonical attributes.
+It does not add an aggregate priming span or identify callbacks by implementation class, generated lambda name, or registration sequence.
+Separate durations and Java-agent JDBC children show the work performed by each callback.
+
+Action-primer observations carry complete `causeway.object.type` and `causeway.action.id` attributes.
+View-primer observations carry the complete `causeway.object.type` attribute.
+They do not attach targets, arguments, values, bookmarks, titles, users, tenants, transaction or interaction identifiers, or Wicket component paths.
+A primer failure is recorded on its observation, its scope closes, and the original exception or error propagates unchanged with the existing fail-fast callback behavior.
+No observation is created when no primer matches, a repeated Wicket visit is excluded by the existing guard, or observation is inactive.
+The Java agent continues to own JDBC spans, propagation, sampling, and export.
+Primer observations are core semantic observations and do not consume the Causeway Wicket per-request observation budget.
+
 === Entity Property-Change Evaluation
 
 When entity property-change publication has enlisted candidate records and the `observation` profile is active, Causeway adds `causeway.entitychange.evaluate` around their transaction-completion evaluation.
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/priming/PrimingRegistryDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/priming/PrimingRegistryDefault.java
index 28090fdbb9f..b57156f0be9 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/priming/PrimingRegistryDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/priming/PrimingRegistryDefault.java
@@ -41,6 +41,10 @@
 import org.apache.causeway.applib.services.priming.ViewPrimer;
 import org.apache.causeway.applib.services.registry.ServiceRegistry;
 import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.core.config.observation.CausewayObservationIntegration;
+import org.apache.causeway.core.config.observation.CausewayObservationIntegration.ObservationProvider;
+import org.apache.causeway.core.config.observation.CausewayObservationNaming;
+import org.apache.causeway.core.config.observation.ObservationClosure;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
@@ -60,6 +64,11 @@
 public final class PrimingRegistryDefault
 implements PrimingRegistry, PrimingService, MetamodelListener {
 
+    static final String ACTION_OBSERVATION_NAME = "causeway.priming.action";
+    static final String VIEW_OBSERVATION_NAME = "causeway.priming.view";
+    static final String OBJECT_TYPE_TAG = "causeway.object.type";
+    static final String ACTION_ID_TAG = "causeway.action.id";
+
     private final SpecificationLoader specificationLoader;
     private final ServiceRegistry serviceRegistry;
 
@@ -123,9 +132,15 @@ public synchronized <T> void actions(
         final ObjectSpecification specification = exactSpecification(domainType);
         for (String actionLogicalName : actionLogicalNames) {
             validateLocalActionName(specification, domainType, actionLogicalName);
-            final String key = actionKey(specification, actionLogicalName);
-            mutableActionPrimers().computeIfAbsent(key, __ -> new ArrayList<>())
-                    .add(new RegisteredActionPrimer<>(domainType, primer));
+            final String logicalMemberIdentifier = actionKey(
+                    specification, actionLogicalName);
+            mutableActionPrimers().computeIfAbsent(
+                    logicalMemberIdentifier, __ -> new ArrayList<>())
+                    .add(new RegisteredActionPrimer<>(
+                            domainType,
+                            primer,
+                            specification.logicalTypeName(),
+                            logicalMemberIdentifier));
         }
     }
 
@@ -137,8 +152,9 @@ public synchronized <T> void view(
         ensureAcceptingRegistrations();
         Objects.requireNonNull(primer, "primer");
         final ObjectSpecification specification = exactSpecification(domainType);
-        mutableViewPrimers().computeIfAbsent(specification.logicalTypeName(), __ -> new ArrayList<>())
-                .add(new RegisteredViewPrimer<>(domainType, primer));
+        final String logicalTypeName = specification.logicalTypeName();
+        mutableViewPrimers().computeIfAbsent(logicalTypeName, __ -> new ArrayList<>())
+                .add(new RegisteredViewPrimer<>(domainType, primer, logicalTypeName));
     }
 
     @Override
@@ -154,7 +170,9 @@ public void primeAction(
             return;
         }
         final ActionArguments actionArguments = ActionArguments.of(arguments);
-        matching.forEach(registered -> registered.prime(target, actionArguments));
+        final ObservationProvider observationProvider = observationProvider();
+        matching.forEach(registered -> registered.prime(
+                target, actionArguments, observationProvider));
     }
 
     @Override
@@ -167,7 +185,18 @@ public void primeView(
         if(matching == null) {
             return;
         }
-        matching.forEach(registered -> registered.prime(target));
+        final ObservationProvider observationProvider = observationProvider();
+        matching.forEach(registered -> registered.prime(target, observationProvider));
+    }
+
+    private ObservationProvider observationProvider() {
+        return serviceRegistry.lookupService(CausewayObservationIntegration.class)
+                .filter(integration -> !integration.isNoop())
+                .map(integration -> integration.provider(
+                        getClass(),
+                        CausewayObservationIntegration.withModuleName(
+                                CausewayModuleCoreMetamodel.NAMESPACE)))
+                .orElse(null);
     }
 
     private <T> ObjectSpecification exactSpecification(final Class<T> domainType) {
@@ -248,32 +277,83 @@ private static <T> Map<String, List<T>> immutableCopy(final Map<String, List<T>>
     private static final class RegisteredActionPrimer<T> {
         private final Class<T> domainType;
         private final ActionPrimer<? super T> primer;
+        private final String logicalTypeName;
+        private final String logicalMemberIdentifier;
 
         private RegisteredActionPrimer(
                 final Class<T> domainType,
-                final ActionPrimer<? super T> primer) {
+                final ActionPrimer<? super T> primer,
+                final String logicalTypeName,
+                final String logicalMemberIdentifier) {
             this.domainType = domainType;
             this.primer = primer;
+            this.logicalTypeName = logicalTypeName;
+            this.logicalMemberIdentifier = logicalMemberIdentifier;
         }
 
-        private void prime(final Object target, final ActionArguments arguments) {
-            primer.prime(domainType.cast(target), arguments);
+        private void prime(
+                final Object target,
+                final ActionArguments arguments,
+                final ObservationProvider observationProvider) {
+            final Runnable callback = () ->
+                    primer.prime(domainType.cast(target), arguments);
+            if(observationProvider == null) {
+                callback.run();
+                return;
+            }
+            final ObservationClosure observation = new ObservationClosure()
+                    .startAndOpenScope(observationProvider.get(ACTION_OBSERVATION_NAME)
+                            .contextualName(CausewayObservationNaming.forLogicalMember(
+                                    "prime action", logicalMemberIdentifier))
+                            .lowCardinalityKeyValue(OBJECT_TYPE_TAG, logicalTypeName)
+                            .lowCardinalityKeyValue(
+                                    ACTION_ID_TAG, logicalMemberIdentifier + "()"));
+            try {
+                callback.run();
+            } catch (RuntimeException | Error ex) {
+                observation.onError(ex);
+                throw ex;
+            } finally {
+                observation.close();
+            }
         }
     }
 
     private static final class RegisteredViewPrimer<T> {
         private final Class<T> domainType;
         private final ViewPrimer<? super T> primer;
+        private final String logicalTypeName;
 
         private RegisteredViewPrimer(
                 final Class<T> domainType,
-                final ViewPrimer<? super T> primer) {
+                final ViewPrimer<? super T> primer,
+                final String logicalTypeName) {
             this.domainType = domainType;
             this.primer = primer;
+            this.logicalTypeName = logicalTypeName;
         }
 
-        private void prime(final Object target) {
-            primer.prime(domainType.cast(target));
+        private void prime(
+                final Object target,
+                final ObservationProvider observationProvider) {
+            final Runnable callback = () -> primer.prime(domainType.cast(target));
+            if(observationProvider == null) {
+                callback.run();
+                return;
+            }
+            final ObservationClosure observation = new ObservationClosure()
+                    .startAndOpenScope(observationProvider.get(VIEW_OBSERVATION_NAME)
+                            .contextualName(CausewayObservationNaming.forType(
+                                    "prime view", logicalTypeName))
+                            .lowCardinalityKeyValue(OBJECT_TYPE_TAG, logicalTypeName));
+            try {
+                callback.run();
+            } catch (RuntimeException | Error ex) {
+                observation.onError(ex);
+                throw ex;
+            } finally {
+                observation.close();
+            }
         }
     }
 }
diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/services/priming/PrimingRegistryDefaultObservationTest.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/services/priming/PrimingRegistryDefaultObservationTest.java
new file mode 100644
index 00000000000..f3971fbbde8
--- /dev/null
+++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/services/priming/PrimingRegistryDefaultObservationTest.java
@@ -0,0 +1,487 @@
+/*
+ *  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.metamodel.services.priming;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationHandler;
+import io.micrometer.observation.ObservationRegistry;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.services.priming.PrimingRegistrar;
+import org.apache.causeway.applib.services.registry.ServiceRegistry;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.config.observation.CausewayObservationIntegration;
+import org.apache.causeway.core.config.observation.CausewayObservationNaming;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+
+class PrimingRegistryDefaultObservationTest {
+
+    static class Invoice {}
+
+    private SpecificationLoader specificationLoader;
+    private ServiceRegistry serviceRegistry;
+    private ObjectSpecification invoiceSpecification;
+
+    @BeforeEach
+    void setUp() {
+        specificationLoader = mock(SpecificationLoader.class);
+        serviceRegistry = mock(ServiceRegistry.class);
+        invoiceSpecification = specification(
+                Invoice.class, "test.Invoice", "approve", "recalculate");
+        when(specificationLoader.snapshotSpecifications())
+                .thenReturn(Can.of(invoiceSpecification));
+    }
+
+    @Test
+    void actionPrimerHasSemanticNameCanonicalMetadataAndNaturalParent() {
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final List<String> calls = new ArrayList<>();
+        final PrimingRegistrar registrar = registry -> registry.action(
+                Invoice.class,
+                "approve",
+                (invoice, arguments) -> calls.add("primer"));
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+        final Observation parent = Observation.createNotStarted(
+                "causeway.action.invocation", observationRegistry).start();
+
+        try (Observation.Scope ignored = parent.openScope()) {
+            registry.primeAction(
+                    invoiceSpecification,
+                    "approve",
+                    new Invoice(),
+                    Collections.emptyList());
+        } finally {
+            parent.stop();
+        }
+
+        assertEquals(Collections.singletonList("primer"), calls);
+        final Snapshot primer = handler.single(
+                PrimingRegistryDefault.ACTION_OBSERVATION_NAME);
+        assertEquals("prime action test.Invoice#approve", primer.contextualName);
+        assertEquals("causeway.action.invocation", primer.parentName);
+        assertEquals("test.Invoice", primer.lowCardinality.get(
+                PrimingRegistryDefault.OBJECT_TYPE_TAG));
+        assertEquals("test.Invoice#approve()", primer.lowCardinality.get(
+                PrimingRegistryDefault.ACTION_ID_TAG));
+        assertEquals("PrimingRegistryDefault", primer.lowCardinality.get("causeway.bean"));
+        assertEquals("metamodel", primer.lowCardinality.get("causeway.module"));
+        assertFalse(primer.lowCardinality.keySet().stream().anyMatch(key ->
+                key.contains("argument")
+                || key.contains("target")
+                || key.contains("primer.class")
+                || key.contains("sequence")));
+        assertNull(observationRegistry.getCurrentObservation());
+    }
+
+    @Test
+    void everyMatchingActionPrimerGetsASeparateSiblingObservation() {
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final List<String> calls = new ArrayList<>();
+        final PrimingRegistrar registrar = registry -> {
+            registry.action(Invoice.class, "approve",
+                    (invoice, arguments) -> calls.add("first"));
+            registry.action(Invoice.class, "approve",
+                    (invoice, arguments) -> calls.add("second"));
+        };
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+        final Observation parent = Observation.createNotStarted(
+                "causeway.action.invocation", observationRegistry).start();
+
+        try (Observation.Scope ignored = parent.openScope()) {
+            registry.primeAction(
+                    invoiceSpecification,
+                    "approve",
+                    new Invoice(),
+                    Collections.emptyList());
+        } finally {
+            parent.stop();
+        }
+
+        assertEquals(List.of("first", "second"), calls);
+        final List<Snapshot> primers = handler.named(
+                PrimingRegistryDefault.ACTION_OBSERVATION_NAME);
+        assertEquals(2, primers.size());
+        assertTrue(primers.stream().allMatch(primer ->
+                "causeway.action.invocation".equals(primer.parentName)));
+        assertFalse(handler.names().contains("causeway.priming"));
+    }
+
+    @Test
+    void everyMatchingViewPrimerGetsASeparateSiblingObservation() {
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final List<String> calls = new ArrayList<>();
+        final PrimingRegistrar registrar = registry -> {
+            registry.view(Invoice.class, invoice -> calls.add("first"));
+            registry.view(Invoice.class, invoice -> calls.add("second"));
+        };
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+        final Observation request = Observation.createNotStarted(
+                "http.request", observationRegistry).start();
+
+        try (Observation.Scope ignored = request.openScope()) {
+            registry.primeView(invoiceSpecification, new Invoice());
+        } finally {
+            request.stop();
+        }
+
+        assertEquals(List.of("first", "second"), calls);
+        final List<Snapshot> primers = handler.named(
+                PrimingRegistryDefault.VIEW_OBSERVATION_NAME);
+        assertEquals(2, primers.size());
+        assertTrue(primers.stream().allMatch(primer ->
+                "http.request".equals(primer.parentName)));
+        assertFalse(handler.names().contains("causeway.priming"));
+    }
+
+    @Test
+    void viewPrimerIsARequestLevelSiblingOfLaterPagePreparation() {
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final PrimingRegistrar registrar = registry -> registry.view(
+                Invoice.class, invoice -> {});
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+        final Observation request = Observation.createNotStarted(
+                "http.request", observationRegistry).start();
+
+        try (Observation.Scope ignored = request.openScope()) {
+            registry.primeView(invoiceSpecification, new Invoice());
+            Observation.createNotStarted(
+                    "causeway.wicket.page.prepare", observationRegistry)
+                    .contextualName("prepare test.Invoice")
+                    .observe(() -> {});
+        } finally {
+            request.stop();
+        }
+
+        final Snapshot primer = handler.single(
+                PrimingRegistryDefault.VIEW_OBSERVATION_NAME);
+        assertEquals("prime view test.Invoice", primer.contextualName);
+        assertEquals("http.request", primer.parentName);
+        assertEquals("test.Invoice", primer.lowCardinality.get(
+                PrimingRegistryDefault.OBJECT_TYPE_TAG));
+        assertNull(primer.lowCardinality.get(PrimingRegistryDefault.ACTION_ID_TAG));
+        assertEquals("http.request", handler.single(
+                "causeway.wicket.page.prepare").parentName);
+    }
+
+    @Test
+    void contributedActionUsesRegisteredTargetIdentityRatherThanMixinIdentity() {
+        invoiceSpecification = specification(Invoice.class, "test.Invoice");
+        final ObjectAction contributedAction = mock(ObjectAction.class);
+        when(contributedAction.getFeatureIdentifier()).thenReturn(
+                Identifier.actionIdentifier(
+                        LogicalType.eager(
+                                Invoice.class, "implementation.Invoice_approveMixin"),
+                        "approve"));
+        when(invoiceSpecification.getAction("approve", MixedIn.INCLUDED))
+                .thenReturn(Optional.of(contributedAction));
+        when(specificationLoader.snapshotSpecifications())
+                .thenReturn(Can.of(invoiceSpecification));
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final PrimingRegistrar registrar = registry -> registry.action(
+                Invoice.class, "approve", (invoice, arguments) -> {});
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+
+        registry.primeAction(
+                invoiceSpecification,
+                "approve",
+                new Invoice(),
+                Collections.emptyList());
+
+        final Snapshot primer = handler.single(
+                PrimingRegistryDefault.ACTION_OBSERVATION_NAME);
+        assertEquals("prime action test.Invoice#approve", primer.contextualName);
+        assertEquals("test.Invoice#approve()", primer.lowCardinality.get(
+                PrimingRegistryDefault.ACTION_ID_TAG));
+        assertFalse(primer.contextualName.contains("approveMixin"));
+        assertFalse(primer.lowCardinality.values().stream().anyMatch(value ->
+                value.contains("approveMixin")));
+    }
+
+    @Test
+    void namesUseExistingCasePreservingBoundedPolicyAndKeepCompleteAttributes() {
+        final String logicalTypeName =
+                "org.example.application.verylongnamespace.MixedCaseInvoiceAggregate";
+        invoiceSpecification = specification(
+                Invoice.class, logicalTypeName, "RecalculateOutstandingBalance");
+        when(specificationLoader.snapshotSpecifications())
+                .thenReturn(Can.of(invoiceSpecification));
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final PrimingRegistrar registrar = registry -> registry.action(
+                Invoice.class,
+                "RecalculateOutstandingBalance",
+                (invoice, arguments) -> {});
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+
+        registry.primeAction(
+                invoiceSpecification,
+                "RecalculateOutstandingBalance",
+                new Invoice(),
+                Collections.emptyList());
+
+        final Snapshot primer = handler.single(
+                PrimingRegistryDefault.ACTION_OBSERVATION_NAME);
+        assertEquals(CausewayObservationNaming.forLogicalMember(
+                "prime action",
+                logicalTypeName + "#RecalculateOutstandingBalance"),
+                primer.contextualName);
+        assertTrue(primer.contextualName.length()
+                <= CausewayObservationNaming.MAX_CONTEXTUAL_NAME_LENGTH);
+        assertTrue(primer.contextualName.contains("MixedCaseInvoiceAggregate"));
+        assertEquals(logicalTypeName, primer.lowCardinality.get(
+                PrimingRegistryDefault.OBJECT_TYPE_TAG));
+        assertEquals(logicalTypeName + "#RecalculateOutstandingBalance()",
+                primer.lowCardinality.get(PrimingRegistryDefault.ACTION_ID_TAG));
+    }
+
+    @Test
+    void failureIsRecordedPropagatesUnchangedAndPreventsLaterPrimer() {
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final IllegalStateException expected = new IllegalStateException("primer failed");
+        final List<String> calls = new ArrayList<>();
+        final PrimingRegistrar registrar = registry -> {
+            registry.view(Invoice.class, invoice -> {
+                calls.add("first");
+                throw expected;
+            });
+            registry.view(Invoice.class, invoice -> calls.add("second"));
+        };
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+
+        final IllegalStateException actual = assertThrows(
+                IllegalStateException.class,
+                () -> registry.primeView(invoiceSpecification, new Invoice()));
+
+        assertSame(expected, actual);
+        assertEquals(Collections.singletonList("first"), calls);
+        assertEquals(Collections.singletonList(expected), handler.errors);
+        assertEquals(1, handler.named(PrimingRegistryDefault.VIEW_OBSERVATION_NAME).size());
+        assertNull(observationRegistry.getCurrentObservation());
+    }
+
+    @Test
+    void errorIsRecordedAndPropagatesUnchanged() {
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final AssertionError expected = new AssertionError("primer error");
+        final PrimingRegistrar registrar = registry -> registry.action(
+                Invoice.class, "approve", (invoice, arguments) -> {
+                    throw expected;
+                });
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+
+        final AssertionError actual = assertThrows(
+                AssertionError.class,
+                () -> registry.primeAction(
+                        invoiceSpecification,
+                        "approve",
+                        new Invoice(),
+                        Collections.emptyList()));
+
+        assertSame(expected, actual);
+        assertEquals(Collections.singletonList(expected), handler.errors);
+        assertNull(observationRegistry.getCurrentObservation());
+    }
+
+    @Test
+    void absentOrNoopObservationIntegrationDoesNotChangeCallbacks() {
+        final List<String> calls = new ArrayList<>();
+        final PrimingRegistrar registrar = registry -> {
+            registry.action(Invoice.class, "approve",
+                    (invoice, arguments) -> calls.add("action"));
+            registry.view(Invoice.class, invoice -> calls.add("view"));
+        };
+        final PrimingRegistryDefault absent = initializedRegistry(registrar, null);
+
+        absent.primeAction(invoiceSpecification, "approve", new Invoice(), List.of());
+        absent.primeView(invoiceSpecification, new Invoice());
+
+        final PrimingRegistryDefault noop = initializedRegistry(
+                registrar,
+                new CausewayObservationIntegration(ObservationRegistry.NOOP));
+        noop.primeAction(invoiceSpecification, "approve", new Invoice(), List.of());
+        noop.primeView(invoiceSpecification, new Invoice());
+
+        assertEquals(List.of("action", "view", "action", "view"), calls);
+    }
+
+    @Test
+    void noMatchingPrimerCreatesNoObservation() {
+        final RecordingHandler handler = new RecordingHandler();
+        final ObservationRegistry observationRegistry = registryWith(handler);
+        final PrimingRegistrar registrar = registry -> registry.action(
+                Invoice.class, "approve", (invoice, arguments) -> {});
+        final PrimingRegistryDefault registry = initializedRegistry(
+                registrar, new CausewayObservationIntegration(observationRegistry));
+
+        registry.primeAction(
+                invoiceSpecification,
+                "recalculate",
+                new Invoice(),
+                Collections.emptyList());
+        registry.primeView(invoiceSpecification, new Invoice());
+
+        assertTrue(handler.snapshots.isEmpty());
+    }
+
+    private PrimingRegistryDefault initializedRegistry(
+            final PrimingRegistrar registrar,
+            final CausewayObservationIntegration integration) {
+        when(serviceRegistry.select(PrimingRegistrar.class)).thenReturn(Can.of(registrar));
+        if(integration != null) {
+            doReturn(Optional.of(integration)).when(serviceRegistry)
+                    .lookupService(CausewayObservationIntegration.class);
+        } else {
+            doReturn(Optional.empty()).when(serviceRegistry)
+                    .lookupService(CausewayObservationIntegration.class);
+        }
+        final PrimingRegistryDefault registry = new PrimingRegistryDefault(
+                specificationLoader, serviceRegistry);
+        registry.onMetamodelAboutToBeLoaded();
+        registry.onMetamodelLoaded();
+        return registry;
+    }
+
+    private static ObjectSpecification specification(
+            final Class<?> correspondingClass,
+            final String logicalTypeName,
+            final String... actionNames) {
+        final ObjectSpecification specification = mock(ObjectSpecification.class);
+        doReturn(correspondingClass).when(specification).getCorrespondingClass();
+        when(specification.logicalTypeName()).thenReturn(logicalTypeName);
+        for (String actionName : actionNames) {
+            final ObjectAction action = mock(ObjectAction.class);
+            final Identifier identifier = mock(Identifier.class);
+            when(identifier.memberLogicalName()).thenReturn(actionName);
+            when(action.getFeatureIdentifier()).thenReturn(identifier);
+            when(specification.getAction(actionName, MixedIn.INCLUDED))
+                    .thenReturn(Optional.of(action));
+        }
+        return specification;
+    }
+
+    private static ObservationRegistry registryWith(final RecordingHandler handler) {
+        final ObservationRegistry registry = ObservationRegistry.create();
+        registry.observationConfig().observationHandler(handler);
+        return registry;
+    }
+
+    private static final class Snapshot {
+        private final String name;
+        private final String contextualName;
+        private final String parentName;
+        private final Map<String, String> lowCardinality;
+
+        private Snapshot(final Observation.Context context) {
+            this.name = context.getName();
+            this.contextualName = context.getContextualName();
+            this.parentName = context.getParentObservation() != null
+                    ? context.getParentObservation().getContextView().getName()
+                    : null;
+            this.lowCardinality = new LinkedHashMap<>();
+            for (KeyValue keyValue : context.getLowCardinalityKeyValues()) {
+                lowCardinality.put(keyValue.getKey(), keyValue.getValue());
+            }
+        }
+    }
+
+    private static final class RecordingHandler
+    implements ObservationHandler<Observation.Context> {
+        private final List<Snapshot> snapshots = new ArrayList<>();
+        private final List<Throwable> errors = new ArrayList<>();
+
+        @Override
+        public boolean supportsContext(final Observation.Context context) {
+            return true;
+        }
+
+        @Override
+        public void onStart(final Observation.Context context) {
+            snapshots.add(new Snapshot(context));
+        }
+
+        @Override
+        public void onError(final Observation.Context context) {
+            errors.add(context.getError());
+        }
+
+        private List<Snapshot> named(final String name) {
+            final List<Snapshot> matches = new ArrayList<>();
+            for (Snapshot snapshot : snapshots) {
+                if(name.equals(snapshot.name)) {
+                    matches.add(snapshot);
+                }
+            }
+            return matches;
+        }
+
+        private Snapshot single(final String name) {
+            final List<Snapshot> matches = named(name);
+            assertEquals(1, matches.size(), () -> "Expected one " + name);
+            return matches.get(0);
+        }
+
+        private List<String> names() {
+            final List<String> names = new ArrayList<>();
+            for (Snapshot snapshot : snapshots) {
+                names.add(snapshot.name);
+            }
+            return names;
+        }
+    }
+}
diff --git a/openspec/changes/observe-application-primers/proposal.md b/openspec/changes/observe-application-primers/proposal.md
index d6e55cb8c1a..e3eba218fd9 100644
--- a/openspec/changes/observe-application-primers/proposal.md
+++ b/openspec/changes/observe-application-primers/proposal.md
@@ -10,7 +10,7 @@ A semantic observation around every invoked primer will expose its duration and
 - Use stable observation names, bounded domain-facing contextual names, and complete canonical action or object-type attributes.
 - Preserve natural action, interaction, HTTP, and Wicket parentage without creating observations when no primer matches.
 - Record callback failures and propagate them unchanged while preserving inactive-observation behavior.
-- Keep primer targets, arguments, values, bookmarks, users, tenants, implementation class names, registration order, and other instance data out of observation names and attributes.
+- Keep primer targets, arguments, values, bookmarks, users, tenants, primer implementation class names, registration order, and other instance data out of observation names and attributes.
 - Preserve Java-agent ownership of JDBC spans, propagation, sampling, and export.
 
 ## Capabilities
diff --git a/openspec/changes/observe-application-primers/specs/application-primer-observation/spec.md b/openspec/changes/observe-application-primers/specs/application-primer-observation/spec.md
index 66594abfd9a..ebb1d1755f3 100644
--- a/openspec/changes/observe-application-primers/specs/application-primer-observation/spec.md
+++ b/openspec/changes/observe-application-primers/specs/application-primer-observation/spec.md
@@ -79,7 +79,7 @@ Contextual names SHALL preserve declared casing, reduce the logical-type namespa
 
 Each action-primer observation SHALL carry the complete domain-facing `causeway.object.type` and `causeway.action.id` attributes.
 Each view-primer observation SHALL carry the complete domain-facing `causeway.object.type` attribute.
-Primer observations MUST NOT attach implementation class names, generated lambda names, registration order, target identity, arguments, values, bookmarks, object titles, users, tenants, transaction identifiers, interaction identifiers, or generated component paths.
+Primer observations MUST NOT attach primer implementation class names, generated lambda names, registration order, target identity, arguments, values, bookmarks, object titles, users, tenants, transaction identifiers, interaction identifiers, or generated component paths.
 
 #### Scenario: Action-primer metadata is exported
 
diff --git a/openspec/changes/observe-application-primers/tasks.md b/openspec/changes/observe-application-primers/tasks.md
index 015a23de7b6..0557ea7209b 100644
--- a/openspec/changes/observe-application-primers/tasks.md
+++ b/openspec/changes/observe-application-primers/tasks.md
@@ -1,30 +1,30 @@
 ## 1. Primer Observation Model
 
-- [ ] 1.1 Add stable action-primer and view-primer observation names and bounded contextual-name construction using the existing semantic naming policy.
-- [ ] 1.2 Define canonical action-primer metadata for complete domain-facing object type and action identifiers.
-- [ ] 1.3 Define canonical view-primer metadata for the complete domain-facing object type.
-- [ ] 1.4 Retain the validated domain-facing action identity with each registered action primer without exposing primer implementation identity.
+- [x] 1.1 Add stable action-primer and view-primer observation names and bounded contextual-name construction using the existing semantic naming policy.
+- [x] 1.2 Define canonical action-primer metadata for complete domain-facing object type and action identifiers.
+- [x] 1.3 Define canonical view-primer metadata for the complete domain-facing object type.
+- [x] 1.4 Retain the validated domain-facing action identity with each registered action primer without exposing primer implementation identity.
 
 ## 2. Per-Callback Observation Dispatch
 
-- [ ] 2.1 Resolve the optional Causeway observation integration at the `PrimingRegistryDefault` dispatch boundary without changing the public priming SPI.
-- [ ] 2.2 Wrap every matching action-primer callback in its own `causeway.priming.action` observation.
-- [ ] 2.3 Wrap every matching view-primer callback in its own `causeway.priming.view` observation.
-- [ ] 2.4 Preserve deterministic callback order, fail-fast behavior, original exception propagation, and observation scope cleanup.
-- [ ] 2.5 Preserve no-match, excluded association-access, repeated Wicket visit, absent-integration, and no-op behavior without creating synthetic or aggregate observations.
-- [ ] 2.6 Verify that action primers remain children of action invocation and view primers remain natural request-level siblings of later Wicket preparation and rendering.
+- [x] 2.1 Resolve the optional Causeway observation integration at the `PrimingRegistryDefault` dispatch boundary without changing the public priming SPI.
+- [x] 2.2 Wrap every matching action-primer callback in its own `causeway.priming.action` observation.
+- [x] 2.3 Wrap every matching view-primer callback in its own `causeway.priming.view` observation.
+- [x] 2.4 Preserve deterministic callback order, fail-fast behavior, original exception propagation, and observation scope cleanup.
+- [x] 2.5 Preserve no-match, excluded association-access, repeated Wicket visit, absent-integration, and no-op behavior without creating synthetic or aggregate observations.
+- [x] 2.6 Verify that action primers remain children of action invocation and view primers remain natural request-level siblings of later Wicket preparation and rendering.
 
 ## 3. Focused Tests
 
-- [ ] 3.1 Test zero, one, and multiple matching action primers, including one sibling observation per invoked callback and no aggregate parent.
-- [ ] 3.2 Test zero, one, and multiple matching view primers together with the existing once-per-request page visit guard.
-- [ ] 3.3 Test ordinary and contributed action naming, complete canonical attributes, namespace fallback, deterministic truncation, and case preservation.
-- [ ] 3.4 Test callback success, runtime exception, error, fail-fast ordering, scope cleanup, and unchanged exception identity.
-- [ ] 3.5 Test inactive and absent observation integration paths invoke every applicable callback exactly once without exporting observations.
-- [ ] 3.6 Test metadata privacy, including absence of implementation classes, lambda names, registration order, targets, arguments, values, bookmarks, titles, users, tenants, and generated component paths.
+- [x] 3.1 Test zero, one, and multiple matching action primers, including one sibling observation per invoked callback and no aggregate parent.
+- [x] 3.2 Test zero, one, and multiple matching view primers together with the existing once-per-request page visit guard.
+- [x] 3.3 Test ordinary and contributed action naming, complete canonical attributes, namespace fallback, deterministic truncation, and case preservation.
+- [x] 3.4 Test callback success, runtime exception, error, fail-fast ordering, scope cleanup, and unchanged exception identity.
+- [x] 3.5 Test inactive and absent observation integration paths invoke every applicable callback exactly once without exporting observations.
+- [x] 3.6 Test metadata privacy, including absence of implementation classes, lambda names, registration order, targets, arguments, values, bookmarks, titles, users, tenants, and generated component paths.
 
 ## 4. Compatibility and Operations Evidence
 
-- [ ] 4.1 Extend Java-agent compatibility coverage to verify action-primer and view-primer parentage and Java-agent-owned JDBC descendants without duplicate HTTP or JDBC spans.
-- [ ] 4.2 Update the tracing operations guide with stable names, contextual names, hierarchy examples, multiple-primer behavior, privacy, and view-primer placement before page preparation.
-- [ ] 4.3 Run focused metamodel, Wicket, and Java-agent compatibility tests, run `git diff --check`, validate the OpenSpec change strictly, and record commands and results in `validation.md`.
+- [x] 4.1 Extend Java-agent compatibility coverage to verify action-primer and view-primer parentage and Java-agent-owned JDBC descendants without duplicate HTTP or JDBC spans.
+- [x] 4.2 Update the tracing operations guide with stable names, contextual names, hierarchy examples, multiple-primer behavior, privacy, and view-primer placement before page preparation.
+- [x] 4.3 Run focused metamodel, Wicket, and Java-agent compatibility tests, run `git diff --check`, validate the OpenSpec change strictly, and record commands and results in `validation.md`.
diff --git a/openspec/changes/observe-application-primers/validation.md b/openspec/changes/observe-application-primers/validation.md
new file mode 100644
index 00000000000..5a9c557f0b0
--- /dev/null
+++ b/openspec/changes/observe-application-primers/validation.md
@@ -0,0 +1,56 @@
+# Validation
+
+## Implementation evidence
+
+`PrimingRegistryDefault` creates one `causeway.priming.action` observation around every matching action-primer callback and one `causeway.priming.view` observation around every matching view-primer callback.
+The registry performs observation lookup only after a matching list is found and bypasses observation creation when integration is absent or no-op.
+Each matching registration is observed separately, so multiple callbacks produce sequential siblings without an aggregate priming parent.
+Action-primer observations use contextual name `prime action <logical-member-identifier>` and complete `causeway.object.type` and `causeway.action.id` attributes.
+View-primer observations use contextual name `prime view <logical-object-type>` and the complete `causeway.object.type` attribute.
+Names use the established case-preserving namespace fallback and deterministic 50-character bound.
+Validated registration keys retain domain-facing target identity and do not expose a contributed-action implementation type.
+Callback runtime exceptions and errors are recorded, observation scopes close, the original failure identity propagates, and later callbacks remain uninvoked under existing fail-fast behavior.
+The public priming SPI, registration matching, callback order, Wicket request guard, and Java-agent ownership remain unchanged.
+
+## Focused test evidence
+
+`PrimingRegistryDefaultObservationTest` ran 10 tests with no failures.
+Coverage includes zero, one, and multiple matching action and view primers, sibling parentage, action and view metadata, contributed-action identity, bounded names, runtime exceptions, errors, fail-fast ordering, absent integration, no-op integration, and privacy.
+Existing `PrimingRegistryDefaultTest`, `ActionExecutorPrimingTest`, and `EntityPagePrimingTest` continue to cover registration order, action dispatch, association exclusion, and once-per-request Wicket view priming.
+
+[source,bash]
+----
+JAVA_HOME=$HOME/.sdkman/candidates/java/25.0.3-tem mvn -pl viewers/wicket/ui-test -am -Dtest=PrimingRegistryDefaultTest,PrimingRegistryDefaultObservationTest,ActionExecutorPrimingTest,EntityPagePrimingTest -Dsurefire.failIfNoSpecifiedTests=false -Drevision=2.2.0-SNAPSHOT -Dmaven.compiler.release=11 -Dmaven.compiler.proc=full -Dmaven.source.skip=true -T 1 test
+----
+
+The complete Wicket UI reactor and its upstream metamodel tests passed.
+The assertion-error log lines in that suite are expected output from tests that deliberately exercise assertion failures; Maven reported `BUILD SUCCESS`.
+
+[source,bash]
+----
+JAVA_HOME=$HOME/.sdkman/candidates/java/25.0.3-tem mvn -pl viewers/wicket/ui-test -am -Drevision=2.2.0-SNAPSHOT -Dmaven.compiler.release=11 -Dmaven.compiler.proc=full -Dmaven.source.skip=true -T 1 test
+----
+
+## Java-agent compatibility evidence
+
+The Java-agent compatibility suite ran 2 tests with no failures using the Java 25 reactor and Java 11 child fixture.
+The fixture invokes the real priming registry for both action and view callbacks.
+It verifies that the action primer is a child of action invocation, the view primer is a request-interaction child and sibling of later page preparation, and each primer owns its Java-agent-created JDBC descendant.
+It also verifies canonical primer attributes, absence of primer implementation metadata, one HTTP entry span, and no duplicate JDBC instrumentation.
+
+[source,bash]
+----
+JAVA_HOME=$HOME/.sdkman/candidates/java/25.0.3-tem mvn -Dmodule-regressiontests -pl :causeway-regressiontests-tracing-compatibility -am -Dtest=MicrometerTracingCompatibilityTest -Dsurefire.failIfNoSpecifiedTests=false -Dcauseway.tracing.fixture.java.home=$HOME/.sdkman/candidates/java/11.0.29-tem -Drevision=2.2.0-SNAPSHOT -Dmaven.compiler.release=11 -Dmaven.compiler.proc=full -Dmaven.source.skip=true -T 1 test
+----
+
+## Final checks
+
+`git diff --check` passed.
+Strict OpenSpec validation passed with all four required artifacts complete and all 19 implementation tasks complete.
+
+[source,bash]
+----
+git diff --check
+openspec validate observe-application-primers --strict
+openspec status --change observe-application-primers
+----
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 a822ba05231..406a32c381c 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
@@ -24,6 +24,8 @@
 import java.sql.DriverManager;
 import java.sql.ResultSet;
 import java.sql.Statement;
+import java.util.Collections;
+import java.util.Optional;
 import java.util.UUID;
 
 import javax.inject.Provider;
@@ -47,6 +49,8 @@
 import org.apache.causeway.applib.services.clock.ClockService;
 import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
 import org.apache.causeway.applib.services.inject.ServiceInjector;
+import org.apache.causeway.applib.services.priming.PrimingRegistrar;
+import org.apache.causeway.applib.services.registry.ServiceRegistry;
 import org.apache.causeway.applib.services.span.ApplicationSpanService;
 import org.apache.causeway.applib.services.xactn.TransactionState;
 import org.apache.causeway.commons.collections.Can;
@@ -65,8 +69,10 @@
 import org.apache.causeway.core.metamodel.interactions.InteractionHead;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
+import org.apache.causeway.core.metamodel.services.priming.PrimingRegistryDefault;
 import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
 import org.apache.causeway.core.runtime.events.MetamodelEventService;
@@ -78,6 +84,7 @@
 import org.apache.causeway.core.webapp.modules.observation.CausewayForegroundTraceFilter;
 import org.apache.causeway.viewer.wicket.ui.observation.WicketRenderObservationDescriptor;
 
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
@@ -102,6 +109,9 @@ public final class MicrometerTracingAgentFixture {
             "act causeway.TracingFixture#executeJdbc";
     static final String ENTITY_CHANGE_EVALUATION_NAME = "evaluate property changes";
     static final String AUDIT_TRAIL_WRITE_NAME = "write audit trail";
+    static final String ACTION_PRIMER_NAME =
+            "prime action causeway.TracingFixture#executeJdbc";
+    static final String VIEW_PRIMER_NAME = "prime view causeway.TracingFixture";
     static final String PAGE_PREPARATION_NAME = "prepare causeway.TracingFixture";
     static final String COLLECTION_PREPARATION_NAME = "prepare collection roles";
     static final String ROW_PREPARATION_NAME = "prepare row causeway.TracingRole";
@@ -180,6 +190,8 @@ public static final class SemanticTracingController {
         private final MemberExecutorServiceDefault memberExecutorService;
         private final ApplicationSpanService applicationSpanService;
         private final ActionExecutor actionExecutor;
+        private final JdbcAction actionTarget;
+        private final PrimerFixture primerFixture;
 
         public SemanticTracingController(
                 final CausewayObservationIntegration observationIntegration) throws Exception {
@@ -188,9 +200,13 @@ public SemanticTracingController(
             this.memberExecutorService = memberExecutorService(observationIntegration);
             this.applicationSpanService = new ApplicationSpanServiceDefault(
                     interactionService, observationIntegration);
-            this.actionExecutor = actionExecutor(
+            this.primerFixture = primerFixture(observationIntegration);
+            this.actionTarget = new JdbcAction(
                     this::completeEntityChanges,
-                    applicationSpanService);
+                    applicationSpanService,
+                    primerFixture);
+            this.actionExecutor = actionExecutor(
+                    actionTarget, primerFixture.specification);
         }
 
         @GetMapping("/trace")
@@ -268,6 +284,8 @@ public void failure() {
         }
 
         private void renderPageAndPrompt() {
+            primerFixture.registry.primeView(
+                    primerFixture.specification, actionTarget);
             final WicketRenderObservationDescriptor preparation =
                     WicketRenderObservationDescriptor.pagePreparation(OBJECT_TYPE);
             final WicketRenderObservationDescriptor collectionPreparation =
@@ -474,15 +492,14 @@ private static MemberExecutorServiceDefault memberExecutorService(
     }
 
     private static ActionExecutor actionExecutor(
-            final Runnable auditTrailWrite,
-            final ApplicationSpanService applicationSpanService) throws Exception {
-        final JdbcAction targetPojo = new JdbcAction(
-                auditTrailWrite,
-                applicationSpanService);
+            final JdbcAction targetPojo,
+            final ObjectSpecification specification) throws Exception {
         final ManagedObject target = mock(ManagedObject.class);
         when(target.getPojo()).thenReturn(targetPojo);
+        when(target.objSpec()).thenReturn(specification);
         final InteractionHead head = mock(InteractionHead.class);
         when(head.getTarget()).thenReturn(target);
+        when(head.getOwner()).thenReturn(target);
 
         final ManagedObject adaptedResult = mock(ManagedObject.class);
         final ObjectManager objectManager = mock(ObjectManager.class);
@@ -490,8 +507,7 @@ private static ActionExecutor actionExecutor(
         final FacetHolder facetHolder = mock(FacetHolder.class);
         when(facetHolder.getObjectManager()).thenReturn(objectManager);
 
-        final ObjectSpecification declaringType = mock(ObjectSpecification.class);
-        when(declaringType.logicalTypeName()).thenReturn(OBJECT_TYPE);
+        final ObjectSpecification declaringType = specification;
         final ObjectAction owningAction = mock(ObjectAction.class);
         when(owningAction.getDeclaringType()).thenReturn(declaringType);
         when(owningAction.getFeatureIdentifier()).thenReturn(Identifier.actionIdentifier(
@@ -509,19 +525,90 @@ private static ActionExecutor actionExecutor(
                 mock(ActionInvocationFacetAbstract.class));
     }
 
+    private static PrimerFixture primerFixture(
+            final CausewayObservationIntegration observationIntegration) {
+        final SpecificationLoader specificationLoader = mock(SpecificationLoader.class);
+        final ServiceRegistry serviceRegistry = mock(ServiceRegistry.class);
+        final ObjectSpecification specification = mock(ObjectSpecification.class);
+        doReturn(JdbcAction.class).when(specification).getCorrespondingClass();
+        when(specification.logicalTypeName()).thenReturn(OBJECT_TYPE);
+        final ObjectAction action = mock(ObjectAction.class);
+        when(action.getFeatureIdentifier()).thenReturn(Identifier.actionIdentifier(
+                LogicalType.eager(JdbcAction.class, OBJECT_TYPE),
+                "executeJdbc"));
+        when(specification.getAction("executeJdbc", MixedIn.INCLUDED))
+                .thenReturn(Optional.of(action));
+        when(specificationLoader.snapshotSpecifications())
+                .thenReturn(Can.of(specification));
+
+        final PrimingRegistrar registrar = registry -> {
+            registry.action(
+                    JdbcAction.class,
+                    "executeJdbc",
+                    (target, arguments) -> executePrimerJdbc());
+            registry.view(JdbcAction.class, target -> executePrimerJdbc());
+        };
+        when(serviceRegistry.select(PrimingRegistrar.class))
+                .thenReturn(Can.of(registrar));
+        doReturn(Optional.of(observationIntegration)).when(serviceRegistry)
+                .lookupService(CausewayObservationIntegration.class);
+
+        final PrimingRegistryDefault registry = new PrimingRegistryDefault(
+                specificationLoader, serviceRegistry);
+        registry.onMetamodelAboutToBeLoaded();
+        registry.onMetamodelLoaded();
+        return new PrimerFixture(registry, specification);
+    }
+
+    private static void executePrimerJdbc() {
+        try {
+            Class.forName("org.h2.Driver");
+            try (Connection connection = DriverManager.getConnection(
+                    "jdbc:h2:mem:causeway-tracing;DB_CLOSE_DELAY=-1");
+                    Statement statement = connection.createStatement();
+                    ResultSet resultSet = statement.executeQuery("select 1")) {
+                if(!resultSet.next() || resultSet.getInt(1) != 1) {
+                    throw new IllegalStateException("Unexpected primer JDBC probe result");
+                }
+            }
+        } catch (Exception ex) {
+            throw new IllegalStateException("Primer JDBC probe failed", ex);
+        }
+    }
+
+    private static final class PrimerFixture {
+        private final PrimingRegistryDefault registry;
+        private final ObjectSpecification specification;
+
+        private PrimerFixture(
+                final PrimingRegistryDefault registry,
+                final ObjectSpecification specification) {
+            this.registry = registry;
+            this.specification = specification;
+        }
+    }
+
     public static final class JdbcAction {
 
         private final Runnable auditTrailWrite;
         private final ApplicationSpanService applicationSpanService;
+        private final PrimerFixture primerFixture;
 
         private JdbcAction(
                 final Runnable auditTrailWrite,
-                final ApplicationSpanService applicationSpanService) {
+                final ApplicationSpanService applicationSpanService,
+                final PrimerFixture primerFixture) {
             this.auditTrailWrite = auditTrailWrite;
             this.applicationSpanService = applicationSpanService;
+            this.primerFixture = primerFixture;
         }
 
         public String executeJdbc() {
+            primerFixture.registry.primeAction(
+                    primerFixture.specification,
+                    "executeJdbc",
+                    this,
+                    Collections.emptyList());
             final String result = applicationSpanService.call("jdbcWork", () -> {
                 Class.forName("org.h2.Driver");
                 try (Connection connection = DriverManager.getConnection(
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 321d04d55d8..60ab76cd1e8 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
@@ -84,6 +84,30 @@ void javaAgentExportsHttpRootInteractionActionAndJdbcAncestry() throws Exception
                     .findFirst()
                     .orElseGet(() -> fail("Action span is not a child of the root interaction.\n"
                             + describe(spans) + "\n" + result.output));
+            final ExportedSpan actionPrimerSpan = childSpanNamed(
+                    spans,
+                    actionSpan,
+                    MicrometerTracingAgentFixture.ACTION_PRIMER_NAME,
+                    result.output);
+            final ExportedSpan actionPrimerJdbcSpan = spans.stream()
+                    .filter(span -> actionPrimerSpan.traceId.equals(span.traceId))
+                    .filter(span -> actionPrimerSpan.spanId.equals(span.parentSpanId))
+                    .filter(span -> "h2".equals(span.attributes.get("db.system")))
+                    .findFirst()
+                    .orElseGet(() -> fail("Primer JDBC span is not a child of action primer.\n"
+                            + describe(spans) + "\n" + result.output));
+            final ExportedSpan viewPrimerSpan = childSpanNamed(
+                    spans,
+                    rootSpan,
+                    MicrometerTracingAgentFixture.VIEW_PRIMER_NAME,
+                    result.output);
+            final ExportedSpan viewPrimerJdbcSpan = spans.stream()
+                    .filter(span -> viewPrimerSpan.traceId.equals(span.traceId))
+                    .filter(span -> viewPrimerSpan.spanId.equals(span.parentSpanId))
+                    .filter(span -> "h2".equals(span.attributes.get("db.system")))
+                    .findFirst()
+                    .orElseGet(() -> fail("Primer JDBC span is not a child of view primer.\n"
+                            + describe(spans) + "\n" + result.output));
             final ExportedSpan preparationSpan = spans.stream()
                     .filter(span -> MicrometerTracingAgentFixture.PAGE_PREPARATION_NAME.equals(span.name))
                     .filter(span -> rootSpan.traceId.equals(span.traceId))
@@ -277,6 +301,27 @@ void javaAgentExportsHttpRootInteractionActionAndJdbcAncestry() throws Exception
             assertFalse(auditTrailWriteSpan.attributes.containsKey("causeway.user"));
             assertFalse(auditTrailWriteSpan.attributes.containsKey("causeway.tenant"));
             assertFalse(auditTrailWriteSpan.attributes.containsKey("causeway.audit.entry.count"));
+            assertEquals(1L, spans.stream()
+                    .filter(span -> actionSpan.traceId.equals(span.traceId))
+                    .filter(span -> MicrometerTracingAgentFixture.ACTION_PRIMER_NAME
+                            .equals(span.name))
+                    .count());
+            assertEquals(1L, spans.stream()
+                    .filter(span -> rootSpan.traceId.equals(span.traceId))
+                    .filter(span -> MicrometerTracingAgentFixture.VIEW_PRIMER_NAME
+                            .equals(span.name))
+                    .count());
+            assertEquals(MicrometerTracingAgentFixture.OBJECT_TYPE,
+                    actionPrimerSpan.attributes.get("causeway.object.type"));
+            assertEquals(MicrometerTracingAgentFixture.ACTION_ID,
+                    actionPrimerSpan.attributes.get("causeway.action.id"));
+            assertEquals("h2", actionPrimerJdbcSpan.attributes.get("db.system"));
+            assertEquals(MicrometerTracingAgentFixture.OBJECT_TYPE,
+                    viewPrimerSpan.attributes.get("causeway.object.type"));
+            assertFalse(viewPrimerSpan.attributes.containsKey("causeway.action.id"));
+            assertEquals("h2", viewPrimerJdbcSpan.attributes.get("db.system"));
+            assertFalse(actionPrimerSpan.attributes.containsKey("causeway.primer.class"));
+            assertFalse(viewPrimerSpan.attributes.containsKey("causeway.primer.class"));
             assertEquals(MicrometerTracingAgentFixture.OBJECT_TYPE,
                     preparationSpan.attributes.get("causeway.object.type"));
             assertEquals("h2", initializationJdbcSpan.attributes.get("db.system"));
@@ -390,10 +435,12 @@ void javaAgentExportsHttpRootInteractionActionAndJdbcAncestry() throws Exception
             assertEquals("500", failedEntrySpan.attributes.get("http.status_code"));
             assertEquals("STATUS_CODE_ERROR", failedEntrySpan.statusCode);
             System.out.printf(
-                    "CAUSEWAY_TRACING_EVIDENCE traceId=%s http=%s rootSpanId=%s prepare=%s initializationJdbc=%s collectionPrepare=%s rowPrepare=%s preparationJdbc=%s page=%s collection=%s table=%s tableHeader=%s tableBody=%s tableFooter=%s row=%s prompt=%s action=%s entityChange=%s entityChangeJdbc=%s audit=%s auditInserts=%s jdbcSpanId=%s%n",
+                    "CAUSEWAY_TRACING_EVIDENCE traceId=%s http=%s rootSpanId=%s viewPrimer=%s viewPrimerJdbc=%s prepare=%s initializationJdbc=%s collectionPrepare=%s rowPrepare=%s preparationJdbc=%s page=%s collection=%s table=%s tableHeader=%s tableBody=%s tableFooter=%s row=%s prompt=%s action=%s actionPrimer=%s actionPrimerJdbc=%s entityChange=%s entityChangeJdbc=%s audit=%s auditInserts=%s jdbcSpanId=%s%n",
                     rootSpan.traceId,
                     httpSpan.name,
                     rootSpan.spanId,
+                    viewPrimerSpan.name,
+                    viewPrimerJdbcSpan.name,
                     preparationSpan.name,
                     initializationJdbcSpan.name,
                     collectionPreparationSpan.name,
@@ -408,6 +455,8 @@ void javaAgentExportsHttpRootInteractionActionAndJdbcAncestry() throws Exception
                     rowSpan.name,
                     promptSpan.name,
                     actionSpan.name,
+                    actionPrimerSpan.name,
+                    actionPrimerJdbcSpan.name,
                     entityChangeEvaluationSpan.name,
                     entityChangeJdbcSpanCount,
                     auditTrailWriteSpan.name,