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"); + } }
