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/21 22:00:38 UTC
(causeway) 07/14: CAUSEWAY-3975: adds Boot 2.7 observation substrate
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-3975
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 9b8b66d5b7be700fd847f7c1d0191f304edcb385
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon Sep 21 19:47:29 2026 +0100
CAUSEWAY-3975: adds Boot 2.7 observation substrate
---
bom/pom.xml | 25 +++
core/config/pom.xml | 43 +++++
core/config/src/main/java/module-info.java | 8 +-
.../org/apache/causeway/OrgApacheCauseway.java | 34 ++++
.../core/config/CausewayModuleCoreConfig.java | 2 +
.../CausewayObservationConfiguration.java | 96 +++++++++++
.../CausewayObservationIntegration.java | 85 ++++++++++
.../config/observation/ObservationClosure.java | 76 +++++++++
.../CausewayObservationConfigurationTest.java | 118 +++++++++++++
...fecycleRecordsThrowableAndCleansUp.approved.txt | 17 ++
.../config/observation/ObservationClosureTest.java | 183 +++++++++++++++++++++
....partialAndRepeatedCleanupAreNoops.approved.txt | 13 ++
...ycleClosesScopeAndStopsExactlyOnce.approved.txt | 17 ++
.../add-boot27-observation-substrate/design.md | 8 +
.../add-boot27-observation-substrate/tasks.md | 34 ++--
regressiontests/tracing-compatibility/pom.xml | 46 +-----
.../tracing/MicrometerTracingAgentFixture.java | 59 +++----
.../MicrometerTracingCompatibilityTest.java | 6 +
18 files changed, 774 insertions(+), 96 deletions(-)
diff --git a/bom/pom.xml b/bom/pom.xml
index 95dccb94096..1483cb582ce 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -178,6 +178,8 @@ identified
<lombok.version>1.18.44</lombok.version> <!-- overriding Spring -->
<maven-release-plugin.version>3.0.1</maven-release-plugin.version>
+ <micrometer-observation.version>1.10.13</micrometer-observation.version>
+ <micrometer-tracing.version>1.0.12</micrometer-tracing.version>
<momentjs.version>2.29.4</momentjs.version> <!-- keep in sync:
org.apache.causeway.viewer.wicket.ui.components.scalars.datepicker.MomentJsReference -->
@@ -187,6 +189,7 @@ identified
<npm.version>8.19.2</npm.version>
<ognl.version>3.4.3</ognl.version>
+ <opentelemetry-api.version>1.19.0</opentelemetry-api.version>
<osgi.version>8.0.0</osgi.version>
<picocontainer.version>2.15</picocontainer.version>
@@ -1814,6 +1817,28 @@ identified
<version>${momentjs.version}</version>
</dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-observation</artifactId>
+ <version>${micrometer-observation.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-tracing</artifactId>
+ <version>${micrometer-tracing.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-tracing-bridge-otel</artifactId>
+ <version>${micrometer-tracing.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-api</artifactId>
+ <version>${opentelemetry-api.version}</version>
+ </dependency>
+
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core</artifactId>
diff --git a/core/config/pom.xml b/core/config/pom.xml
index 8cf663d8f57..2af5a9b09b2 100644
--- a/core/config/pom.xml
+++ b/core/config/pom.xml
@@ -93,6 +93,38 @@
<artifactId>causeway-applib</artifactId>
</dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-observation</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-tracing</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-tracing-bridge-otel</artifactId>
+ <exclusions>
+ <!-- The Java agent owns the SDK; Causeway needs only the bridge and OTel API. -->
+ <exclusion>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-sdk</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-sdk-common</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-sdk-trace</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-api</artifactId>
+ </dependency>
+
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
@@ -136,6 +168,17 @@
<artifactId>causeway-core-internaltestsupport</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.approvaltests</groupId>
+ <artifactId>approvaltests</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <!-- required by ApprovalTests (version managed by Spring Boot) -->
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
diff --git a/core/config/src/main/java/module-info.java b/core/config/src/main/java/module-info.java
index 0d733bb2dd0..10622b9961c 100644
--- a/core/config/src/main/java/module-info.java
+++ b/core/config/src/main/java/module-info.java
@@ -28,6 +28,7 @@
exports org.apache.causeway.core.config.metamodel.facets;
exports org.apache.causeway.core.config.metamodel.services;
exports org.apache.causeway.core.config.metamodel.specloader;
+ exports org.apache.causeway.core.config.observation;
exports org.apache.causeway.core.config.presets;
exports org.apache.causeway.core.config.progmodel;
exports org.apache.causeway.core.config.util;
@@ -45,6 +46,10 @@
requires java.validation;
requires java.ws.rs;
requires java.inject;
+ requires transitive micrometer.observation;
+ requires micrometer.tracing;
+ requires micrometer.tracing.bridge.otel;
+ requires io.opentelemetry.api;
requires org.apache.logging.log4j;
requires org.eclipse.persistence.core;
requires org.hibernate.validator;
@@ -59,4 +64,5 @@
opens org.apache.causeway.core.config to spring.core, org.hibernate.validator;
opens org.apache.causeway.core.config.environment to spring.core;
-}
\ No newline at end of file
+ opens org.apache.causeway.core.config.observation to spring.core;
+}
diff --git a/core/config/src/main/java/org/apache/causeway/OrgApacheCauseway.java b/core/config/src/main/java/org/apache/causeway/OrgApacheCauseway.java
new file mode 100644
index 00000000000..02ad8d15559
--- /dev/null
+++ b/core/config/src/main/java/org/apache/causeway/OrgApacheCauseway.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import lombok.experimental.UtilityClass;
+
+/**
+ * Namespace anchor for identifiers that represent Apache Causeway as a whole.
+ *
+ * <p>Infrastructure integrations can derive the stable {@code org.apache.causeway}
+ * namespace from this class rather than from an implementation class whose package
+ * may change during refactoring.</p>
+ *
+ * @since 2.2
+ */
+@UtilityClass
+public class OrgApacheCauseway {
+}
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java
index 1c029f18f3f..0ec5b679878 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java
@@ -30,6 +30,7 @@
import org.apache.causeway.core.config.environment.CausewayLocaleInitializer;
import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
import org.apache.causeway.core.config.environment.CausewayTimeZoneInitializer;
+import org.apache.causeway.core.config.observation.CausewayObservationConfiguration;
import org.apache.causeway.core.config.validators.PatternOptionalStringConstraintValidator;
import org.apache.causeway.core.config.viewer.web.WebAppContextPath;
@@ -41,6 +42,7 @@
CausewayBeanFactoryPostProcessorForSpring.class,
CausewayLocaleInitializer.class,
CausewayTimeZoneInitializer.class,
+ CausewayObservationConfiguration.class,
PatternOptionalStringConstraintValidator.class,
RestfulPathProvider.class,
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationConfiguration.java
new file mode 100644
index 00000000000..f71d1d7362d
--- /dev/null
+++ b/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationConfiguration.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.config.observation;
+
+import java.util.Collections;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Profile;
+
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.handler.DefaultTracingObservationHandler;
+import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
+import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+
+import org.apache.causeway.OrgApacheCauseway;
+
+/**
+ * Boot 2.7-specific observation wiring for Causeway framework instrumentation.
+ *
+ * <p>The {@code observation} Spring profile opts into a registry backed by the
+ * OpenTelemetry Java agent's {@link GlobalOpenTelemetry} context. Without the
+ * profile, framework observations use {@link ObservationRegistry#NOOP}.</p>
+ *
+ * @since 2.2
+ */
+@Configuration(proxyBeanMethods = false)
+@Import({
+ CausewayObservationConfiguration.Inactive.class,
+ CausewayObservationConfiguration.Active.class,
+})
+public class CausewayObservationConfiguration {
+
+ public static final String REGISTRY_BEAN_NAME = "causewayObservationRegistry";
+
+ @Bean
+ public CausewayObservationIntegration causewayObservationIntegration(
+ @Qualifier(REGISTRY_BEAN_NAME) final ObservationRegistry observationRegistry) {
+ return new CausewayObservationIntegration(observationRegistry);
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @Profile("!observation")
+ public static class Inactive {
+
+ @Bean(name = REGISTRY_BEAN_NAME)
+ public ObservationRegistry causewayObservationRegistry() {
+ return ObservationRegistry.NOOP;
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @Profile("observation")
+ public static class Active {
+
+ @Bean(name = REGISTRY_BEAN_NAME)
+ public ObservationRegistry causewayObservationRegistry() {
+ final OtelCurrentTraceContext currentTraceContext = new OtelCurrentTraceContext();
+ final OtelBaggageManager baggageManager = new OtelBaggageManager(
+ currentTraceContext,
+ Collections.emptyList(),
+ Collections.emptyList());
+ final Tracer tracer = new OtelTracer(
+ GlobalOpenTelemetry.getTracer(OrgApacheCauseway.class.getPackageName()),
+ currentTraceContext,
+ event -> { },
+ baggageManager);
+
+ final ObservationRegistry observationRegistry = ObservationRegistry.create();
+ observationRegistry.observationConfig()
+ .observationHandler(new DefaultTracingObservationHandler(tracer));
+ return observationRegistry;
+ }
+ }
+}
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationIntegration.java b/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationIntegration.java
new file mode 100644
index 00000000000..70c485514d7
--- /dev/null
+++ b/core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationIntegration.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.config.observation;
+
+import java.util.Objects;
+import java.util.function.UnaryOperator;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+/**
+ * Internal entry point for Causeway framework observations.
+ *
+ * <p>Framework components use this integration rather than depending on an
+ * OpenTelemetry implementation or performing optional registry lookup.</p>
+ *
+ * @since 2.2
+ */
+public final class CausewayObservationIntegration {
+
+ private final ObservationRegistry observationRegistry;
+
+ public CausewayObservationIntegration(final ObservationRegistry observationRegistry) {
+ this.observationRegistry = observationRegistry != null
+ ? observationRegistry
+ : ObservationRegistry.NOOP;
+ }
+
+ public ObservationRegistry observationRegistry() {
+ return observationRegistry;
+ }
+
+ public boolean isNoop() {
+ return observationRegistry.isNoop();
+ }
+
+ public Observation createNotStarted(final Class<?> beanType, final String name) {
+ Objects.requireNonNull(beanType, "beanType");
+ Objects.requireNonNull(name, "name");
+ return Observation.createNotStarted(name, observationRegistry)
+ .lowCardinalityKeyValue("causeway.bean", beanType.getSimpleName());
+ }
+
+ public ObservationProvider provider(final Class<?> beanType) {
+ return name -> createNotStarted(beanType, name);
+ }
+
+ public ObservationProvider provider(
+ final Class<?> beanType,
+ final UnaryOperator<Observation> customizer) {
+ Objects.requireNonNull(customizer, "customizer");
+ return name -> customizer.apply(createNotStarted(beanType, name));
+ }
+
+ public static UnaryOperator<Observation> withModuleName(final String moduleName) {
+ Objects.requireNonNull(moduleName, "moduleName");
+ final String normalizedModuleName = moduleName.startsWith("causeway.")
+ || moduleName.startsWith("causeway-")
+ ? moduleName.substring(9)
+ : moduleName;
+ return observation -> observation.lowCardinalityKeyValue(
+ "causeway.module", normalizedModuleName);
+ }
+
+ @FunctionalInterface
+ public interface ObservationProvider {
+ Observation get(String name);
+ }
+}
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/observation/ObservationClosure.java b/core/config/src/main/java/org/apache/causeway/core/config/observation/ObservationClosure.java
new file mode 100644
index 00000000000..0f9cbc01e36
--- /dev/null
+++ b/core/config/src/main/java/org/apache/causeway/core/config/observation/ObservationClosure.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.core.config.observation;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.Observation.Scope;
+
+/**
+ * Owns an observation whose start and stop occur in different framework callbacks.
+ *
+ * @since 2.2
+ */
+public final class ObservationClosure implements AutoCloseable {
+
+ private Observation observation;
+ private Scope scope;
+
+ public synchronized ObservationClosure startAndOpenScope(final Observation observation) {
+ if (observation == null) {
+ return this;
+ }
+ if (this.observation != null || this.scope != null) {
+ throw new IllegalStateException("Observation lifecycle is already active");
+ }
+
+ this.observation = observation.start();
+ try {
+ this.scope = this.observation.openScope();
+ } catch (RuntimeException | Error ex) {
+ this.observation.error(ex);
+ close();
+ throw ex;
+ }
+ return this;
+ }
+
+ public synchronized void onError(final Throwable failure) {
+ if (observation != null && failure != null) {
+ observation.error(failure);
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ final Scope scopeToClose = scope;
+ final Observation observationToStop = observation;
+ scope = null;
+ observation = null;
+
+ try {
+ if (scopeToClose != null) {
+ scopeToClose.close();
+ }
+ } finally {
+ if (observationToStop != null) {
+ observationToStop.stop();
+ }
+ }
+ }
+}
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/observation/CausewayObservationConfigurationTest.java b/core/config/src/test/java/org/apache/causeway/core/config/observation/CausewayObservationConfigurationTest.java
new file mode 100644
index 00000000000..f64884ae8fb
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/observation/CausewayObservationConfigurationTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.config.observation;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+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.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CausewayObservationConfigurationTest {
+
+ @Test
+ void inactiveProfileUsesCausewayNoopRegistryAndIgnoresApplicationRegistry() {
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ final ObservationRegistry applicationRegistry = ObservationRegistry.create();
+ context.registerBean(
+ "applicationObservationRegistry",
+ ObservationRegistry.class,
+ () -> applicationRegistry);
+ context.register(CausewayObservationConfiguration.class);
+ context.refresh();
+
+ final CausewayObservationIntegration integration = context.getBean(
+ CausewayObservationIntegration.class);
+ assertTrue(integration.isNoop());
+ assertSame(ObservationRegistry.NOOP, integration.observationRegistry());
+ assertNotSame(applicationRegistry, integration.observationRegistry());
+
+ integration.createNotStarted(getClass(), "causeway.test.inactive")
+ .observe(() -> { });
+ assertNull(integration.observationRegistry().getCurrentObservation());
+ }
+ }
+
+ @Test
+ void providerAddsStableCausewayMetadataAndAppliesCustomization() {
+ final ObservationRegistry registry = ObservationRegistry.create();
+ registry.observationConfig().observationHandler(new ObservationHandler<Observation.Context>() {
+ @Override
+ public boolean supportsContext(final Observation.Context context) {
+ return true;
+ }
+ });
+ final CausewayObservationIntegration integration = new CausewayObservationIntegration(registry);
+
+ final Observation observation = integration.provider(
+ getClass(),
+ CausewayObservationIntegration.withModuleName("causeway-core-config"))
+ .get("causeway.test.metadata");
+
+ observation.start();
+ try {
+ assertEquals("causeway.test.metadata", observation.getContext().getName());
+ assertEquals(
+ getClass().getSimpleName(),
+ observation.getContext().getLowCardinalityKeyValue("causeway.bean").getValue());
+ assertEquals(
+ "core-config",
+ observation.getContext().getLowCardinalityKeyValue("causeway.module").getValue());
+ } finally {
+ observation.stop();
+ }
+ }
+
+ @Test
+ void activeProfileUsesRealRegistryAndStartsWithoutJavaAgent() {
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ final ObservationRegistry applicationRegistry = ObservationRegistry.create();
+ context.getEnvironment().setActiveProfiles("observation");
+ context.registerBean(
+ "applicationObservationRegistry",
+ ObservationRegistry.class,
+ () -> applicationRegistry);
+ context.register(CausewayObservationConfiguration.class);
+ context.refresh();
+
+ final CausewayObservationIntegration integration = context.getBean(
+ CausewayObservationIntegration.class);
+ assertFalse(integration.isNoop());
+ assertNotSame(applicationRegistry, integration.observationRegistry());
+
+ final Observation observation = integration.createNotStarted(
+ getClass(), "causeway.test.active");
+ observation.start();
+ try (Observation.Scope ignored = observation.openScope()) {
+ assertSame(observation, integration.observationRegistry().getCurrentObservation());
+ } finally {
+ observation.stop();
+ }
+ assertNull(integration.observationRegistry().getCurrentObservation());
+ }
+ }
+}
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.failingLifecycleRecordsThrowableAndCleansUp.approved.txt b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.failingLifecycleRecordsThrowableAndCleansUp.approved.txt
new file mode 100644
index 00000000000..86f2c696bbe
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.failingLifecycleRecordsThrowableAndCleansUp.approved.txt
@@ -0,0 +1,17 @@
+Scenario: Failing observation lifecycle
+
+This approval checks:
+- The reported Throwable reaches the observation handler with its type and message.
+- The error callback occurs before scope closure and observation stop.
+- Cleanup closes the scope before stopping the observation.
+- No current observation remains after cleanup.
+
+Observed state:
+- current observation after close: <none>
+
+Observed callback order:
+1. onStart(name=causeway.test.failure)
+2. onScopeOpened(name=causeway.test.failure)
+3. onError(type=AssertionError, message=expected)
+4. onScopeClosed(name=causeway.test.failure)
+5. onStop(name=causeway.test.failure)
\ No newline at end of file
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.java b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.java
new file mode 100644
index 00000000000..b7a52b11530
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.config.observation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.approvaltests.Approvals;
+import org.junit.jupiter.api.Test;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationHandler;
+import io.micrometer.observation.ObservationRegistry;
+
+class ObservationClosureTest {
+
+ @Test
+ void successfulLifecycleClosesScopeAndStopsExactlyOnce() {
+ final RecordingHandler handler = new RecordingHandler();
+ final ObservationRegistry registry = registryWith(handler);
+ final Observation observation = Observation.createNotStarted("causeway.test.success", registry);
+ final ObservationClosure closure = new ObservationClosure();
+
+ closure.startAndOpenScope(observation);
+ final String currentAfterStart = currentObservationName(registry);
+ closure.close();
+ closure.close();
+
+ Approvals.verify(report(
+ "Successful observation lifecycle",
+ List.of(
+ "Starting opens the observation scope and makes the observation current.",
+ "The first close closes the scope before stopping the observation.",
+ "The second close produces no additional callbacks.",
+ "No current observation remains after cleanup."),
+ List.of(
+ "current observation after start: " + currentAfterStart,
+ "current observation after two closes: " + currentObservationName(registry)),
+ handler));
+ }
+
+ @Test
+ void failingLifecycleRecordsThrowableAndCleansUp() {
+ final RecordingHandler handler = new RecordingHandler();
+ final ObservationRegistry registry = registryWith(handler);
+ final Observation observation = Observation.createNotStarted("causeway.test.failure", registry);
+ final ObservationClosure closure = new ObservationClosure();
+
+ closure.startAndOpenScope(observation);
+ closure.onError(new AssertionError("expected"));
+ closure.close();
+
+ Approvals.verify(report(
+ "Failing observation lifecycle",
+ List.of(
+ "The reported Throwable reaches the observation handler with its type and message.",
+ "The error callback occurs before scope closure and observation stop.",
+ "Cleanup closes the scope before stopping the observation.",
+ "No current observation remains after cleanup."),
+ List.of("current observation after close: " + currentObservationName(registry)),
+ handler));
+ }
+
+ @Test
+ void partialAndRepeatedCleanupAreNoops() {
+ final List<String> completedOperations = new ArrayList<>();
+ final ObservationClosure closure = new ObservationClosure();
+
+ closure.startAndOpenScope(null);
+ completedOperations.add("start with null observation: completed");
+ closure.onError(new IllegalStateException("ignored"));
+ completedOperations.add("error before start: completed");
+ closure.close();
+ completedOperations.add("first close: completed");
+ closure.close();
+ completedOperations.add("second close: completed");
+
+ final List<String> report = new ArrayList<>(List.of(
+ "Scenario: Defensive cleanup without an active observation",
+ "",
+ "This approval checks:",
+ "- A null observation is accepted as a no-op.",
+ "- Reporting an error before start is accepted as a no-op.",
+ "- Closing before start is accepted as a no-op.",
+ "- Repeated close is accepted as a no-op.",
+ "",
+ "Observed operations:"));
+ completedOperations.forEach(operation -> report.add("- " + operation));
+ Approvals.verify(String.join("\n", report));
+ }
+
+ private static String report(
+ final String scenario,
+ final List<String> checks,
+ final List<String> observedState,
+ final RecordingHandler handler) {
+ final List<String> lines = new ArrayList<>();
+ lines.add("Scenario: " + scenario);
+ lines.add("");
+ lines.add("This approval checks:");
+ checks.forEach(check -> lines.add("- " + check));
+ lines.add("");
+ lines.add("Observed state:");
+ observedState.forEach(state -> lines.add("- " + state));
+ lines.add("");
+ lines.add("Observed callback order:");
+ lines.addAll(handler.numberedEvents());
+ return String.join("\n", lines);
+ }
+
+ private static String currentObservationName(final ObservationRegistry registry) {
+ final Observation current = registry.getCurrentObservation();
+ return current != null ? current.getContext().getName() : "<none>";
+ }
+
+ private static ObservationRegistry registryWith(final RecordingHandler handler) {
+ final ObservationRegistry registry = ObservationRegistry.create();
+ registry.observationConfig().observationHandler(handler);
+ return registry;
+ }
+
+ private static final class RecordingHandler
+ implements ObservationHandler<Observation.Context> {
+
+ private final List<String> events = new ArrayList<>();
+
+ @Override
+ public void onStart(final Observation.Context context) {
+ events.add("onStart(name=" + context.getName() + ")");
+ }
+
+ @Override
+ public void onScopeOpened(final Observation.Context context) {
+ events.add("onScopeOpened(name=" + context.getName() + ")");
+ }
+
+ @Override
+ public void onScopeClosed(final Observation.Context context) {
+ events.add("onScopeClosed(name=" + context.getName() + ")");
+ }
+
+ @Override
+ public void onError(final Observation.Context context) {
+ final Throwable failure = context.getError();
+ events.add("onError(type=" + failure.getClass().getSimpleName()
+ + ", message=" + failure.getMessage() + ")");
+ }
+
+ @Override
+ public void onStop(final Observation.Context context) {
+ events.add("onStop(name=" + context.getName() + ")");
+ }
+
+ @Override
+ public boolean supportsContext(final Observation.Context context) {
+ return true;
+ }
+
+ private List<String> numberedEvents() {
+ final List<String> numberedEvents = new ArrayList<>();
+ for (int i = 0; i < events.size(); i++) {
+ numberedEvents.add((i + 1) + ". " + events.get(i));
+ }
+ return numberedEvents;
+ }
+ }
+}
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.partialAndRepeatedCleanupAreNoops.approved.txt b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.partialAndRepeatedCleanupAreNoops.approved.txt
new file mode 100644
index 00000000000..b12e6442613
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.partialAndRepeatedCleanupAreNoops.approved.txt
@@ -0,0 +1,13 @@
+Scenario: Defensive cleanup without an active observation
+
+This approval checks:
+- A null observation is accepted as a no-op.
+- Reporting an error before start is accepted as a no-op.
+- Closing before start is accepted as a no-op.
+- Repeated close is accepted as a no-op.
+
+Observed operations:
+- start with null observation: completed
+- error before start: completed
+- first close: completed
+- second close: completed
\ No newline at end of file
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.successfulLifecycleClosesScopeAndStopsExactlyOnce.approved.txt b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.successfulLifecycleClosesScopeAndStopsExactlyOnce.approved.txt
new file mode 100644
index 00000000000..6c1b696d245
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/observation/ObservationClosureTest.successfulLifecycleClosesScopeAndStopsExactlyOnce.approved.txt
@@ -0,0 +1,17 @@
+Scenario: Successful observation lifecycle
+
+This approval checks:
+- Starting opens the observation scope and makes the observation current.
+- The first close closes the scope before stopping the observation.
+- The second close produces no additional callbacks.
+- No current observation remains after cleanup.
+
+Observed state:
+- current observation after start: causeway.test.success
+- current observation after two closes: <none>
+
+Observed callback order:
+1. onStart(name=causeway.test.success)
+2. onScopeOpened(name=causeway.test.success)
+3. onScopeClosed(name=causeway.test.success)
+4. onStop(name=causeway.test.success)
\ No newline at end of file
diff --git a/openspec/changes/add-boot27-observation-substrate/design.md b/openspec/changes/add-boot27-observation-substrate/design.md
index ecf5a07615e..181e1d97c49 100644
--- a/openspec/changes/add-boot27-observation-substrate/design.md
+++ b/openspec/changes/add-boot27-observation-substrate/design.md
@@ -102,6 +102,14 @@ Its real-agent OTLP assertion will continue to prove that the production substra
This avoids creating a second agent harness and turns the Phase 1 proof into a regression guard for the production integration.
+## Validated Phase 3 Integration Recipe
+
+Later core instrumentation should inject `CausewayObservationIntegration`, create an `ObservationProvider` for the concrete framework bean type, and optionally compose `withModuleName(...)` for stable low-cardinality module metadata.
+At the beginning of framework work, it should obtain an unstarted observation from that provider and pass it to a new `ObservationClosure` through `startAndOpenScope(...)`.
+A catch path should pass any `Throwable` to `onError(...)` and rethrow it, while a `finally` path should always invoke `close()`.
+Consumers should not inject the qualified registry directly, look up optional beans, or call OpenTelemetry APIs.
+This recipe behaves identically at the call site when the profile is inactive, active without an agent, or active with an agent.
+
## Risks / Trade-offs
- **[Risk] Adding bridge dependencies to core configuration increases every application's runtime classpath even when observation is inactive.** → Keep initialization profile-gated, exclude SDK artifacts, and accept the small classpath cost in preference to a new maintenance-only abstraction layer.
diff --git a/openspec/changes/add-boot27-observation-substrate/tasks.md b/openspec/changes/add-boot27-observation-substrate/tasks.md
index 1478b35de00..a69317a80ce 100644
--- a/openspec/changes/add-boot27-observation-substrate/tasks.md
+++ b/openspec/changes/add-boot27-observation-substrate/tasks.md
@@ -1,28 +1,28 @@
## 1. Add the Validated Production Dependencies
-- [ ] 1.1 Add centrally managed properties and dependency entries for Micrometer Observation 1.10.13, Micrometer Tracing and OpenTelemetry bridge 1.0.12, and OpenTelemetry API 1.19.0 without overriding `micrometer.version`.
-- [ ] 1.2 Add the required dependencies to `core/config`, excluding bridge-transitive OpenTelemetry SDK artifacts and keeping the Java agent and OTLP collector dependencies test-only.
-- [ ] 1.3 Update Java module declarations for the new production API usage and verify that Micrometer Core remains at Spring Boot 2.7.18's managed 1.9.17 version with dependency convergence passing.
+- [x] 1.1 Add centrally managed properties and dependency entries for Micrometer Observation 1.10.13, Micrometer Tracing and OpenTelemetry bridge 1.0.12, and OpenTelemetry API 1.19.0 without overriding `micrometer.version`.
+- [x] 1.2 Add the required dependencies to `core/config`, excluding bridge-transitive OpenTelemetry SDK artifacts and keeping the Java agent and OTLP collector dependencies test-only.
+- [x] 1.3 Update Java module declarations for the new production API usage and verify that Micrometer Core remains at Spring Boot 2.7.18's managed 1.9.17 version with dependency convergence passing.
## 2. Implement the Observation Substrate
-- [ ] 2.1 Add profile-specific core configuration that supplies a qualified `ObservationRegistry.NOOP` when `observation` is inactive and a qualified real registry when it is active.
-- [ ] 2.2 Configure the active registry with `OtelCurrentTraceContext`, `OtelBaggageManager`, an `OtelTracer` obtained from `GlobalOpenTelemetry.getTracer(...)`, and `DefaultTracingObservationHandler` without constructing an SDK or exporter.
-- [ ] 2.3 Add the internal Causeway observation integration that exposes no-op status and consistent creation of unstarted observations or observation providers from the qualified registry.
-- [ ] 2.4 Add Java 11-compatible lifecycle support for start, scope, `Throwable` recording, idempotent cleanup, and clearing retained state.
-- [ ] 2.5 Import the observation configuration from `CausewayModuleCoreConfig` and document the internal contract for later framework instrumentation.
+- [x] 2.1 Add profile-specific core configuration that supplies a qualified `ObservationRegistry.NOOP` when `observation` is inactive and a qualified real registry when it is active.
+- [x] 2.2 Configure the active registry with `OtelCurrentTraceContext`, `OtelBaggageManager`, an `OtelTracer` obtained from `GlobalOpenTelemetry.getTracer(...)`, and `DefaultTracingObservationHandler` without constructing an SDK or exporter.
+- [x] 2.3 Add the internal Causeway observation integration that exposes no-op status and consistent creation of unstarted observations or observation providers from the qualified registry.
+- [x] 2.4 Add Java 11-compatible lifecycle support for start, scope, `Throwable` recording, idempotent cleanup, and clearing retained state.
+- [x] 2.5 Import the observation configuration from `CausewayModuleCoreConfig` and document the internal contract for later framework instrumentation.
## 3. Verify Profile and Lifecycle Behavior
-- [ ] 3.1 Add focused tests proving that the inactive profile selects the Causeway no-op registry, creates no tracing bridge, and ignores unrelated application registries.
-- [ ] 3.2 Add focused tests proving that the active profile creates the real Causeway registry and starts safely without the Java agent.
-- [ ] 3.3 Add lifecycle tests covering successful completion, recorded failures, partial initialization, repeated cleanup, and removal of the current observation from the registry.
-- [ ] 3.4 Update the tracing compatibility child fixture to obtain and use the production Causeway integration instead of constructing duplicate Micrometer bridge wiring.
-- [ ] 3.5 Run the real-agent compatibility test and verify that the production-substrate observation remains the parent or expected semantic ancestor of the agent-generated JDBC span.
+- [x] 3.1 Add focused tests proving that the inactive profile selects the Causeway no-op registry, creates no tracing bridge, and ignores unrelated application registries.
+- [x] 3.2 Add focused tests proving that the active profile creates the real Causeway registry and starts safely without the Java agent.
+- [x] 3.3 Add lifecycle tests covering successful completion, recorded failures, partial initialization, repeated cleanup, and removal of the current observation from the registry.
+- [x] 3.4 Update the tracing compatibility child fixture to obtain and use the production Causeway integration instead of constructing duplicate Micrometer bridge wiring.
+- [x] 3.5 Run the real-agent compatibility test and verify that the production-substrate observation remains the parent or expected semantic ancestor of the agent-generated JDBC span.
## 4. Validate the Production Integration
-- [ ] 4.1 Run targeted `core/config` tests and representative application-context startup tests with and without the `observation` profile.
-- [ ] 4.2 Run the tracing compatibility module in the regression-test reactor on the Java 11 compilation target and capture the resolved dependency graph and span-parentage evidence.
-- [ ] 4.3 Confirm that production source contains no `OpenTelemetrySdk` construction, exporter configuration, semantic framework spans, or public applib API changes.
-- [ ] 4.4 Record the validated internal integration recipe for Phase 3 and validate the completed OpenSpec change.
+- [x] 4.1 Run targeted `core/config` tests and representative application-context startup tests with and without the `observation` profile.
+- [x] 4.2 Run the tracing compatibility module in the regression-test reactor on the Java 11 compilation target and capture the resolved dependency graph and span-parentage evidence.
+- [x] 4.3 Confirm that production source contains no `OpenTelemetrySdk` construction, exporter configuration, semantic framework spans, or public applib API changes.
+- [x] 4.4 Record the validated internal integration recipe for Phase 3 and validate the completed OpenSpec change.
diff --git a/regressiontests/tracing-compatibility/pom.xml b/regressiontests/tracing-compatibility/pom.xml
index 704f1c8d92b..e96f20c78d0 100644
--- a/regressiontests/tracing-compatibility/pom.xml
+++ b/regressiontests/tracing-compatibility/pom.xml
@@ -37,9 +37,6 @@
<maven.install.skip>true</maven.install.skip>
<maven.deploy.skip>true</maven.deploy.skip>
- <micrometer-observation.version>1.10.13</micrometer-observation.version>
- <micrometer-tracing.version>1.0.12</micrometer-tracing.version>
- <opentelemetry-api.version>1.19.0</opentelemetry-api.version>
<opentelemetry-javaagent.version>1.31.0</opentelemetry-javaagent.version>
<opentelemetry-proto.version>1.0.0-alpha</opentelemetry-proto.version>
@@ -54,52 +51,19 @@
<scope>test</scope>
</dependency>
- <!-- Keep this version under Spring Boot 2.7.18 dependency management. -->
<dependency>
- <groupId>io.micrometer</groupId>
- <artifactId>micrometer-core</artifactId>
+ <groupId>org.apache.causeway.core</groupId>
+ <artifactId>causeway-core-config</artifactId>
<scope>test</scope>
</dependency>
+ <!-- Keep this version under Spring Boot 2.7.18 dependency management. -->
<dependency>
<groupId>io.micrometer</groupId>
- <artifactId>micrometer-observation</artifactId>
- <version>${micrometer-observation.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>io.micrometer</groupId>
- <artifactId>micrometer-tracing</artifactId>
- <version>${micrometer-tracing.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>io.micrometer</groupId>
- <artifactId>micrometer-tracing-bridge-otel</artifactId>
- <version>${micrometer-tracing.version}</version>
- <scope>test</scope>
- <exclusions>
- <!-- The Java agent owns the SDK; this fixture needs only the bridge and OTel API. -->
- <exclusion>
- <groupId>io.opentelemetry</groupId>
- <artifactId>opentelemetry-sdk</artifactId>
- </exclusion>
- <exclusion>
- <groupId>io.opentelemetry</groupId>
- <artifactId>opentelemetry-sdk-common</artifactId>
- </exclusion>
- <exclusion>
- <groupId>io.opentelemetry</groupId>
- <artifactId>opentelemetry-sdk-trace</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>io.opentelemetry</groupId>
- <artifactId>opentelemetry-api</artifactId>
- <version>${opentelemetry-api.version}</version>
+ <artifactId>micrometer-core</artifactId>
<scope>test</scope>
</dependency>
+
<dependency>
<groupId>io.opentelemetry.javaagent</groupId>
<artifactId>opentelemetry-javaagent</artifactId>
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 8384908ae8c..b5d38152fb3 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
@@ -22,27 +22,27 @@
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
-import java.util.Collections;
-import io.micrometer.observation.Observation;
-import io.micrometer.observation.ObservationRegistry;
-import io.micrometer.tracing.Tracer;
-import io.micrometer.tracing.handler.DefaultTracingObservationHandler;
-import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
-import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
-import io.micrometer.tracing.otel.bridge.OtelTracer;
-import io.opentelemetry.api.GlobalOpenTelemetry;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Import;
+
+import org.apache.causeway.core.config.observation.CausewayObservationConfiguration;
+import org.apache.causeway.core.config.observation.CausewayObservationIntegration;
+import org.apache.causeway.core.config.observation.ObservationClosure;
/**
- * Runs in a child JVM so the compatibility test can attach the real OpenTelemetry Java agent.
+ * Child-process application launched by {@link MicrometerTracingCompatibilityTest}.
+ *
+ * <p>The fixture needs a {@code main} method because the automated JUnit test starts a new JVM
+ * with the real OpenTelemetry {@code -javaagent} attached before application classes load.</p>
*/
@SpringBootConfiguration(proxyBeanMethods = false)
@EnableAutoConfiguration
+@Import(CausewayObservationConfiguration.class)
public final class MicrometerTracingAgentFixture {
static final String OBSERVATION_NAME = "causeway.compatibility.jdbc";
@@ -52,48 +52,33 @@ private MicrometerTracingAgentFixture() {
}
public static void main(final String[] args) throws Exception {
- try (ConfigurableApplicationContext ignored = new SpringApplicationBuilder(
+ try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
MicrometerTracingAgentFixture.class)
.web(WebApplicationType.NONE)
+ .profiles("observation")
.logStartupInfo(false)
.properties("spring.main.banner-mode=off")
.run(args)) {
- final ObservationRegistry observationRegistry = observationRegistry();
- final Observation observation = Observation.createNotStarted(
- OBSERVATION_NAME, observationRegistry);
+ final CausewayObservationIntegration observationIntegration = context.getBean(
+ CausewayObservationIntegration.class);
+ final ObservationClosure observationClosure = new ObservationClosure()
+ .startAndOpenScope(observationIntegration.createNotStarted(
+ MicrometerTracingAgentFixture.class,
+ OBSERVATION_NAME));
- observation.start();
- try (Observation.Scope scope = observation.openScope()) {
+ try {
executeJdbcWork();
- } catch (Exception ex) {
- observation.error(ex);
+ } catch (Exception | Error ex) {
+ observationClosure.onError(ex);
throw ex;
} finally {
- observation.stop();
+ observationClosure.close();
}
System.out.println(SUCCESS_MARKER);
}
}
- private static ObservationRegistry observationRegistry() {
- final OtelCurrentTraceContext currentTraceContext = new OtelCurrentTraceContext();
- final OtelBaggageManager baggageManager = new OtelBaggageManager(
- currentTraceContext,
- Collections.emptyList(),
- Collections.emptyList());
- final Tracer tracer = new OtelTracer(
- GlobalOpenTelemetry.getTracer("org.apache.causeway.compatibility"),
- currentTraceContext,
- event -> { },
- baggageManager);
-
- final ObservationRegistry observationRegistry = ObservationRegistry.create();
- observationRegistry.observationConfig()
- .observationHandler(new DefaultTracingObservationHandler(tracer));
- return observationRegistry;
- }
-
private static void executeJdbcWork() throws Exception {
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 a08188020fc..dd2329e068a 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
@@ -47,6 +47,12 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+/**
+ * Automated JUnit coverage for the production observation substrate with and without the agent.
+ *
+ * <p>Each test launches {@link MicrometerTracingAgentFixture} in a child JVM because Java agents
+ * must be attached during JVM startup.</p>
+ */
class MicrometerTracingCompatibilityTest {
private static final Duration PROCESS_TIMEOUT = Duration.ofSeconds(45);