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

ahuber pushed a commit to branch 3975-telemetry
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 54f2aa935ad8b9796bc47a07b4e245d14111142d
Author: andi-huber <[email protected]>
AuthorDate: Fri Mar 20 08:16:00 2026 +0100

    CAUSEWAY-3975: Open Telemetry Integration
    
    initial commit
    
    Task-Url: https://issues.apache.org/jira/browse/CAUSEWAY-3975
---
 bom/pom.xml                                        | 12 ++++
 commons/src/main/java/module-info.java             |  2 +
 .../observation/CausewayObservationInternal.java   | 76 ++++++++++++++++++++++
 .../metamodel/CausewayModuleCoreMetamodel.java     | 14 +++-
 .../core/runtime/CausewayModuleCoreRuntime.java    |  4 +-
 ...entService.java => XrayInitializerService.java} | 34 +++-------
 .../CausewayModuleCoreRuntimeServices.java         | 11 ++++
 .../session/InteractionServiceDefault.java         | 50 +++++++++-----
 .../model/CausewayModuleViewerWicketModel.java     | 14 +++-
 9 files changed, 170 insertions(+), 47 deletions(-)

diff --git a/bom/pom.xml b/bom/pom.xml
index 700f2bdcd1e..a9b0aa3ff71 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -1803,6 +1803,18 @@ identified
                 <version>${jdom.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-opentelemetry</artifactId>
+                <version>${spring-boot.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.jetbrains</groupId>
+                        <artifactId>annotations</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-quartz</artifactId>
diff --git a/commons/src/main/java/module-info.java 
b/commons/src/main/java/module-info.java
index 398b9432e33..a5d7963e405 100644
--- a/commons/src/main/java/module-info.java
+++ b/commons/src/main/java/module-info.java
@@ -50,6 +50,7 @@
     exports org.apache.causeway.commons.internal.html;
     exports org.apache.causeway.commons.internal.image;
     exports org.apache.causeway.commons.internal.ioc;
+    exports org.apache.causeway.commons.internal.observation;
     exports org.apache.causeway.commons.internal.os;
     exports org.apache.causeway.commons.internal.primitives;
     exports org.apache.causeway.commons.internal.proxy;
@@ -67,6 +68,7 @@
     requires transitive tools.jackson.core;
     requires transitive tools.jackson.databind;
     requires transitive tools.jackson.module.jakarta.xmlbind;
+    requires transitive micrometer.observation;
     requires transitive org.jdom2;
     requires transitive org.jspecify;
     requires transitive org.jsoup;
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java
new file mode 100644
index 00000000000..f4387ed04d9
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java
@@ -0,0 +1,76 @@
+/*
+ *  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.commons.internal.observation;
+
+import java.util.Optional;
+
+import org.springframework.util.StringUtils;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+/**
+ * Holder of {@link ObservationRegistry} which comes as a dependency of 
<i>spring-context</i>.
+ *
+ * @apiNote each Causeway module can have its own, using qualifiers and bean 
factory methods, e.g.:
+ * <pre>
+ * @Bean("causeway-metamodel")
+ * public CausewayObservationInternal causewayObservationInternal(
+ *   Optional<ObservationRegistry> observationRegistryOpt) {
+ *   return new CausewayObservationInternal(observationRegistryOpt, 
"causeway-metamodel");
+ * }
+ *  </pre>
+ */
+public record CausewayObservationInternal(
+        ObservationRegistry observationRegistry,
+        String module) {
+
+    public CausewayObservationInternal(
+            final Optional<ObservationRegistry> observationRegistryOpt,
+            final String module) {
+        this(observationRegistryOpt.orElse(ObservationRegistry.NOOP), module);
+    }
+
+    public CausewayObservationInternal {
+        observationRegistry = observationRegistry!=null
+                ? observationRegistry
+                : ObservationRegistry.NOOP;
+        module = StringUtils.hasText(module) ? module : "unknown_module";
+    }
+
+    public boolean isNoop() {
+        return observationRegistry.isNoop();
+    }
+
+    public Observation createNotStarted(final Class<?> bean, final String 
name) {
+        return Observation.createNotStarted(name, observationRegistry)
+                .lowCardinalityKeyValue("module", module)
+                .highCardinalityKeyValue("bean", bean.getSimpleName());
+    }
+
+    @FunctionalInterface
+    public interface ObservationProvider {
+        Observation get(String name);
+    }
+
+    public ObservationProvider provider(final Class<?> bean) {
+        return name->createNotStarted(bean, name);
+    }
+
+}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
index f0b1049c17e..4f6b1a6b210 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
@@ -20,6 +20,7 @@
 
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import jakarta.inject.Provider;
@@ -42,6 +43,7 @@
 import org.apache.causeway.commons.functional.Either;
 import org.apache.causeway.commons.functional.Railway;
 import org.apache.causeway.commons.functional.Try;
+import 
org.apache.causeway.commons.internal.observation.CausewayObservationInternal;
 import org.apache.causeway.commons.semantics.CollectionSemantics;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.config.CausewayModuleCoreConfig;
@@ -116,7 +118,9 @@
 import 
org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault;
 import org.apache.causeway.core.security.CausewayModuleCoreSecurity;
 
-@Configuration
+import io.micrometer.observation.ObservationRegistry;
+
+@Configuration(proxyBeanMethods = false)
 @Import({
         // Modules
         CausewayModuleApplib.class,
@@ -200,7 +204,7 @@
 
         // standalone validators
         LogicalTypeMalformedValidator.class,
-        
+
         // menubar contributions
         MetamodelInspectMenu.class
 })
@@ -261,4 +265,10 @@ public ValueCodec valueCodec(
         return new ValueCodec(bookmarkService, valueSemanticsResolverProvider);
     }
 
+    @Bean("causeway-metamodel")
+    public CausewayObservationInternal causewayObservationInternal(
+            final Optional<ObservationRegistry> observationRegistryOpt) {
+        return new CausewayObservationInternal(observationRegistryOpt, 
"causeway-metamodel");
+    }
+
 }
diff --git 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
index 442e0008e44..220ff51c8c3 100644
--- 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
+++ 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
@@ -23,7 +23,7 @@
 
 import org.apache.causeway.core.interaction.CausewayModuleCoreInteraction;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
-import org.apache.causeway.core.runtime.events.MetamodelEventService;
+import org.apache.causeway.core.runtime.events.XrayInitializerService;
 import org.apache.causeway.core.transaction.CausewayModuleCoreTransaction;
 
 @Configuration
@@ -34,7 +34,7 @@
         CausewayModuleCoreTransaction.class,
 
         // @Service's
-        MetamodelEventService.class,
+        XrayInitializerService.class,
 })
 public class CausewayModuleCoreRuntime {
 
diff --git 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/MetamodelEventService.java
 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/XrayInitializerService.java
similarity index 58%
rename from 
core/runtime/src/main/java/org/apache/causeway/core/runtime/events/MetamodelEventService.java
rename to 
core/runtime/src/main/java/org/apache/causeway/core/runtime/events/XrayInitializerService.java
index d02a988f772..4320e218ece 100644
--- 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/MetamodelEventService.java
+++ 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/XrayInitializerService.java
@@ -18,49 +18,31 @@
  */
 package org.apache.causeway.core.runtime.events;
 
-import jakarta.annotation.Priority;
-import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.events.metamodel.MetamodelEvent;
+import org.apache.causeway.applib.events.metamodel.MetamodelListener;
 import org.apache.causeway.applib.services.confview.ConfigurationViewService;
-import org.apache.causeway.applib.services.eventbus.EventBusService;
 import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime;
 
-/**
- *
- * @since 2.0
- * @implNote Listeners to runtime events can only reliably receive these after 
the
- * post-construct phase has finished and before the pre-destroy phase has 
begun.
- */
 @Service
-@Named(CausewayModuleCoreRuntime.NAMESPACE + ".MetamodelEventService")
-@Priority(PriorityPrecedence.MIDPOINT)
-@Qualifier("Default")
-public class MetamodelEventService {
-
-    @Inject
-    private EventBusService eventBusService;
+@Named(CausewayModuleCoreRuntime.NAMESPACE + ".XrayInitializerService")
+public class XrayInitializerService implements MetamodelListener {
 
     @Autowired(required = false)
     private ConfigurationViewService configurationService;
 
-    public void fireBeforeMetamodelLoading() {
-
+    @Override
+    public void onMetamodelAboutToBeLoaded() {
         if(configurationService!=null) {
             _Xray.addConfiguration(configurationService);
         }
-
-        eventBusService.post(MetamodelEvent.BEFORE_METAMODEL_LOADING);
     }
 
-    public void fireAfterMetamodelLoaded() {
-        eventBusService.post(MetamodelEvent.AFTER_METAMODEL_LOADED);
+    @Override
+    public void onMetamodelLoaded() {
+        // no-op
     }
-
 }
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
index bee6e2db2ce..ae003667556 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.core.runtimeservices;
 
+import java.util.Optional;
+
 import org.springframework.boot.autoconfigure.AutoConfigureOrder;
 import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
@@ -29,6 +31,7 @@
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import 
org.apache.causeway.commons.internal.observation.CausewayObservationInternal;
 import 
org.apache.causeway.core.codegen.bytebuddy.CausewayModuleCoreCodegenByteBuddy;
 import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime;
 import 
org.apache.causeway.core.runtimeservices.bookmarks.BookmarkServiceDefault;
@@ -73,6 +76,8 @@
 import org.apache.causeway.core.runtimeservices.xml.XmlServiceDefault;
 import 
org.apache.causeway.core.runtimeservices.xmlsnapshot.XmlSnapshotServiceDefault;
 
+import io.micrometer.observation.ObservationRegistry;
+
 @Configuration(proxyBeanMethods = false)
 @Import({
         // Modules
@@ -151,4 +156,10 @@ public HmacAuthority fallbackHmacAuthority() {
         }
     }
 
+    @Bean("causeway-runtimeservices")
+    public CausewayObservationInternal causewayObservationInternal(
+            final Optional<ObservationRegistry> observationRegistryOpt) {
+        return new CausewayObservationInternal(observationRegistryOpt, 
"causeway-runtimeservices");
+    }
+
 }
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
index 810a262b682..10c70fe6869 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
@@ -30,6 +30,8 @@
 import jakarta.inject.Named;
 import jakarta.inject.Provider;
 
+import org.jspecify.annotations.NonNull;
+
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.config.ConfigurableBeanFactory;
 import org.springframework.context.event.ContextRefreshedEvent;
@@ -38,8 +40,10 @@
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.annotation.Programmatic;
+import org.apache.causeway.applib.events.metamodel.MetamodelEvent;
 import org.apache.causeway.applib.services.clock.ClockService;
 import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.applib.services.eventbus.EventBusService;
 import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
 import org.apache.causeway.applib.services.iactnlayer.InteractionLayer;
@@ -57,17 +61,17 @@
 import org.apache.causeway.commons.internal.debug._Probe;
 import org.apache.causeway.commons.internal.debug.xray.XrayUi;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import 
org.apache.causeway.commons.internal.observation.CausewayObservationInternal;
+import 
org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider;
 import 
org.apache.causeway.core.interaction.scope.InteractionScopeBeanFactoryPostProcessor;
 import 
org.apache.causeway.core.interaction.scope.InteractionScopeLifecycleHandler;
 import org.apache.causeway.core.interaction.session.CausewayInteraction;
 import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
 import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
-import org.apache.causeway.core.runtime.events.MetamodelEventService;
 import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
 import 
org.apache.causeway.core.runtimeservices.transaction.TransactionServiceSpring;
 import 
org.apache.causeway.core.security.authentication.InteractionContextFactory;
 
-import org.jspecify.annotations.NonNull;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 
@@ -93,7 +97,8 @@ public class InteractionServiceDefault
     //  ThreadLocal would be considered bad practice and instead should be 
managed using the TransactionSynchronization mechanism.
     final ThreadLocal<Stack<InteractionLayer>> interactionLayerStack = 
ThreadLocal.withInitial(Stack::new);
 
-    final MetamodelEventService runtimeEventService;
+    final EventBusService eventBusService;
+    final ObservationProvider observationProvider;
     final Provider<SpecificationLoader> specificationLoaderProvider;
     final ServiceInjector serviceInjector;
 
@@ -108,7 +113,9 @@ public class InteractionServiceDefault
 
     @Inject
     public InteractionServiceDefault(
-            final MetamodelEventService runtimeEventService,
+            final EventBusService eventBusService,
+            @Qualifier("causeway-runtimeservices")
+            final CausewayObservationInternal observation,
             final Provider<SpecificationLoader> specificationLoaderProvider,
             final ServiceInjector serviceInjector,
             final TransactionServiceSpring transactionServiceSpring,
@@ -116,7 +123,8 @@ public InteractionServiceDefault(
             final Provider<CommandPublisher> commandPublisherProvider,
             final ConfigurableBeanFactory beanFactory,
             final InteractionIdGenerator interactionIdGenerator) {
-        this.runtimeEventService = runtimeEventService;
+        this.eventBusService = eventBusService;
+        this.observationProvider = observation.provider(getClass());
         this.specificationLoaderProvider = specificationLoaderProvider;
         this.serviceInjector = serviceInjector;
         this.transactionServiceSpring = transactionServiceSpring;
@@ -124,19 +132,34 @@ public InteractionServiceDefault(
         this.commandPublisherProvider = commandPublisherProvider;
         this.beanFactory = beanFactory;
         this.interactionIdGenerator = interactionIdGenerator;
-
         this.interactionScopeLifecycleHandler = 
InteractionScopeBeanFactoryPostProcessor.lookupScope(beanFactory);
     }
 
     @EventListener
     public void init(final ContextRefreshedEvent event) {
-
         log.info("Initialising Causeway System");
         log.info("working directory: {}", new File(".").getAbsolutePath());
 
-        runtimeEventService.fireBeforeMetamodelLoading();
+        observationProvider.get("Initialising Causeway System")
+        .observe(()->{
+            observationProvider.get("Notify BEFORE_METAMODEL_LOADING 
Listeners")
+            .observe(()->{
+                eventBusService.post(MetamodelEvent.BEFORE_METAMODEL_LOADING);
+            });
+
+            observationProvider.get("Initialising Causeway Metamodel")
+            .observe(()->{
+                initMetamodel(specificationLoaderProvider.get());
+            });
+
+            observationProvider.get("Notify AFTER_METAMODEL_LOADED Listeners")
+            .observe(()->{
+                eventBusService.post(MetamodelEvent.AFTER_METAMODEL_LOADED);
+            });
+        });
+    }
 
-        var specificationLoader = specificationLoaderProvider.get();
+    private void initMetamodel(final SpecificationLoader specificationLoader) {
 
         var taskList = 
_ConcurrentTaskList.named("CausewayInteractionFactoryDefault Init")
                 .addRunnable("SpecificationLoader::createMetaModel", 
specificationLoader::createMetaModel)
@@ -161,9 +184,6 @@ public void init(final ContextRefreshedEvent event) {
                 //throw _Exceptions.unrecoverable("Validation FAILED");
             }
         }
-
-        runtimeEventService.fireAfterMetamodelLoaded();
-
     }
 
     @Override
@@ -191,10 +211,9 @@ public InteractionLayer openInteraction(
                 .map(currentInteractionContext -> 
Objects.equals(currentInteractionContext, interactionContextToUse))
                 .orElse(false);
 
-        if(reuseCurrentLayer) {
+        if(reuseCurrentLayer)
             // we are done, just return the stack's top
             return interactionLayerStack.get().peek();
-        }
 
         var interactionLayer = new InteractionLayer(causewayInteraction, 
interactionContextToUse);
 
@@ -465,9 +484,8 @@ private void 
closeInteractionLayerStackDownToStackSize(final int downToStackSize
 
     private CausewayInteraction getInternalInteractionElseFail() {
         var interaction = currentInteractionElseFail();
-        if(interaction instanceof CausewayInteraction) {
+        if(interaction instanceof CausewayInteraction)
             return (CausewayInteraction) interaction;
-        }
         throw _Exceptions.unrecoverable("the framework does not recognize "
                 + "this implementation of an Interaction: %s", 
interaction.getClass().getName());
     }
diff --git 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java
 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java
index 6db06646573..7e3bca0fbd0 100644
--- 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java
+++ 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java
@@ -18,18 +18,30 @@
  */
 package org.apache.causeway.viewer.wicket.model;
 
+import java.util.Optional;
+
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
+import 
org.apache.causeway.commons.internal.observation.CausewayObservationInternal;
 import org.apache.causeway.core.webapp.CausewayModuleCoreWebapp;
 
+import io.micrometer.observation.ObservationRegistry;
+
 /**
  * @since 1.x {@index}
  */
-@Configuration
+@Configuration(proxyBeanMethods = false)
 @Import({
         // Modules
         CausewayModuleCoreWebapp.class,
 })
 public class CausewayModuleViewerWicketModel {
+
+    @Bean("causeway-wicketviewer")
+    public CausewayObservationInternal causewayObservationInternal(
+            final Optional<ObservationRegistry> observationRegistryOpt) {
+        return new CausewayObservationInternal(observationRegistryOpt, 
"causeway-wicketviewer");
+    }
 }

Reply via email to