You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/03/20 07:16:10 UTC
(causeway) 01/01: CAUSEWAY-3975: Open Telemetry Integration
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 <ah...@apache.org>
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");
+ }
}