diff --git a/pom.xml b/pom.xml
index a5bdd2cf4c..3d2863e1f2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1168,6 +1168,7 @@
spring-boot-modules/spring-boot-camel
spring-boot-modules/spring-boot-3
spring-boot-modules/spring-boot-3-native
+ spring-boot-modules/spring-boot-3-observation
spring-swagger-codegen/custom-validations-opeanpi-codegen
testing-modules/testing-assertions
persistence-modules/fauna
@@ -1251,6 +1252,7 @@
spring-boot-modules/spring-boot-camel
spring-boot-modules/spring-boot-3
spring-boot-modules/spring-boot-3-native
+ spring-boot-modules/spring-boot-3-observation
spring-swagger-codegen/custom-validations-opeanpi-codegen
testing-modules/testing-assertions
persistence-modules/fauna
diff --git a/spring-boot-modules/spring-boot-3-observation/pom.xml b/spring-boot-modules/spring-boot-3-observation/pom.xml
new file mode 100644
index 0000000000..ed613ee98e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+ spring-boot-3-observation
+ 0.0.1-SNAPSHOT
+ spring-boot-3-observation
+ Demo project for Spring Boot 3 Observation
+
+
+ com.baeldung
+ parent-boot-3
+ 0.0.1-SNAPSHOT
+ ../../parent-boot-3
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.micrometer
+ micrometer-tracing
+
+
+ io.micrometer
+ micrometer-tracing-bridge-brave
+
+
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
+
+ io.micrometer
+ micrometer-tracing-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+
+
+
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/GreetingApplication.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/GreetingApplication.java
new file mode 100644
index 0000000000..f5014a8abd
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/GreetingApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.samples;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class GreetingApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(GreetingApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/SimpleObservationApplication.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/SimpleObservationApplication.java
new file mode 100644
index 0000000000..4434535939
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/SimpleObservationApplication.java
@@ -0,0 +1,66 @@
+package com.baeldung.samples;
+
+import io.micrometer.core.instrument.Measurement;
+import io.micrometer.core.instrument.Statistic;
+import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.ObservationTextPublisher;
+
+import java.util.Optional;
+import java.util.stream.StreamSupport;
+
+public class SimpleObservationApplication {
+
+ // we can run this as a simple command line application
+ public static void main(String[] args) {
+ // create registry
+ final var observationRegistry = ObservationRegistry.create();
+ // create meter registry and observation handler
+ final var meterRegistry = new SimpleMeterRegistry();
+ final var meterObservationHandler = new DefaultMeterObservationHandler(meterRegistry);
+ // create simple logging observation handler
+ final var loggingObservationHandler = new ObservationTextPublisher(System.out::println);
+ // register observation handlers
+ observationRegistry
+ .observationConfig()
+ .observationHandler(meterObservationHandler)
+ .observationHandler(loggingObservationHandler);
+ // make an observation
+ Observation.Context context = new Observation.Context();
+ String observationName = "obs1";
+ Observation observation = Observation
+ .createNotStarted(observationName, () -> context, observationRegistry)
+ .lowCardinalityKeyValue("gender", "male")
+ .highCardinalityKeyValue("age", "41");
+
+ for (int i = 0; i < 10; i++) {
+ observation.observe(SimpleObservationApplication::doSomeAction);
+ }
+
+ meterRegistry.getMeters().forEach(m -> {
+ System.out.println(m.getId() + "\n============");
+ m.measure().forEach(ms -> System.out.println(ms.getValue() + " [" + ms.getStatistic() + "]"));
+ System.out.println("----------------------------");
+ });
+ Optional maximumDuration = meterRegistry.getMeters().stream()
+ .filter(m -> "obs1".equals(m.getId().getName()))
+ .flatMap(m -> StreamSupport.stream(m.measure().spliterator(), false))
+ .filter(ms -> ms.getStatistic() == Statistic.MAX)
+ .findFirst()
+ .map(Measurement::getValue);
+
+ System.out.println(maximumDuration);
+ }
+
+ private static void doSomeAction() {
+ try {
+ Thread.sleep(Math.round(Math.random() * 1000));
+ System.out.println("Hello World!");
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/GreetingController.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/GreetingController.java
new file mode 100644
index 0000000000..bc179540f8
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/GreetingController.java
@@ -0,0 +1,26 @@
+package com.baeldung.samples.boundary;
+
+import com.baeldung.samples.domain.GreetingService;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+@Controller
+@RequestMapping("/greet")
+public class GreetingController {
+
+ private final GreetingService service;
+
+ public GreetingController(GreetingService service) {
+ this.service = service;
+ }
+
+ @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
+ @ResponseBody
+ public String sayHello() {
+ return this.service.sayHello();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/ObservationFilterConfiguration.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/ObservationFilterConfiguration.java
new file mode 100644
index 0000000000..c39af961a1
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/ObservationFilterConfiguration.java
@@ -0,0 +1,22 @@
+package com.baeldung.samples.boundary;
+
+import io.micrometer.observation.ObservationRegistry;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.ServerHttpObservationFilter;
+
+@Configuration
+public class ObservationFilterConfiguration {
+
+ // if an ObservationRegistry is already configured
+ @ConditionalOnBean(ObservationRegistry.class)
+ // if we do not use Actuator
+ @ConditionalOnMissingBean(ServerHttpObservationFilter.class)
+ @Bean
+ public ServerHttpObservationFilter observationFilter(ObservationRegistry registry) {
+ return new ServerHttpObservationFilter(registry);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationHandlerLogger.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationHandlerLogger.java
new file mode 100644
index 0000000000..0a1f52f9c1
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationHandlerLogger.java
@@ -0,0 +1,28 @@
+package com.baeldung.samples.config;
+
+import io.micrometer.observation.ObservationHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ObservationHandlerLogger {
+
+ private static final Logger log = LoggerFactory.getLogger(ObservationHandlerLogger.class);
+
+ private static String toString(ObservationHandler> handler) {
+ return handler.getClass().getName() + " [ " + handler + "]";
+ }
+
+ @EventListener(ContextRefreshedEvent.class)
+ public void logObservationHandlers(ContextRefreshedEvent evt) {
+ evt.getApplicationContext().getBeansOfType(ObservationHandler.class)
+ .values()
+ .stream()
+ .map(ObservationHandlerLogger::toString)
+ .forEach(log::info);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationTextPublisherConfiguration.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationTextPublisherConfiguration.java
new file mode 100644
index 0000000000..29637166c9
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationTextPublisherConfiguration.java
@@ -0,0 +1,21 @@
+package com.baeldung.samples.config;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationHandler;
+import io.micrometer.observation.ObservationTextPublisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ObservationTextPublisherConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(ObservationTextPublisherConfiguration.class);
+
+ @Bean
+ public ObservationHandler observationTextPublisher() {
+ return new ObservationTextPublisher(log::info);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservedAspectConfiguration.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservedAspectConfiguration.java
new file mode 100644
index 0000000000..cd475113c7
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservedAspectConfiguration.java
@@ -0,0 +1,20 @@
+package com.baeldung.samples.config;
+
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.aop.ObservedAspect;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+@ConditionalOnClass(ObservedAspect.class)
+public class ObservedAspectConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
+ return new ObservedAspect(observationRegistry);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/SimpleLoggingHandler.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/SimpleLoggingHandler.java
new file mode 100644
index 0000000000..c87aa68085
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/SimpleLoggingHandler.java
@@ -0,0 +1,59 @@
+package com.baeldung.samples.config;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SimpleLoggingHandler implements ObservationHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(SimpleLoggingHandler.class);
+
+ private static String toString(Observation.Context context) {
+ return null == context ? "(no context)" : context.getName()
+ + " (" + context.getClass().getName() + "@" + System.identityHashCode(context) + ")";
+ }
+
+ private static String toString(Observation.Event event) {
+ return null == event ? "(no event)" : event.getName();
+ }
+
+ @Override
+ public boolean supportsContext(Observation.Context context) {
+ return true;
+ }
+
+ @Override
+ public void onStart(Observation.Context context) {
+ log.info("Starting context " + toString(context));
+ }
+
+ @Override
+ public void onError(Observation.Context context) {
+ log.info("Error for context " + toString(context));
+ }
+
+ @Override
+ public void onEvent(Observation.Event event, Observation.Context context) {
+ log.info("Event for context " + toString(context) + " [" + toString(event) + "]");
+ }
+
+ @Override
+ public void onScopeOpened(Observation.Context context) {
+ log.info("Scope opened for context " + toString(context));
+
+ }
+
+ @Override
+ public void onScopeClosed(Observation.Context context) {
+ log.info("Scope closed for context " + toString(context));
+ }
+
+ @Override
+ public void onStop(Observation.Context context) {
+ log.info("Stopping context " + toString(context));
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/domain/GreetingService.java b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/domain/GreetingService.java
new file mode 100644
index 0000000000..ec362dd3cc
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/domain/GreetingService.java
@@ -0,0 +1,14 @@
+package com.baeldung.samples.domain;
+
+import io.micrometer.observation.annotation.Observed;
+import org.springframework.stereotype.Service;
+
+@Observed(name = "greetingService")
+@Service
+public class GreetingService {
+
+ public String sayHello() {
+ return "Hello World!";
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/main/resources/application.yml b/spring-boot-modules/spring-boot-3-observation/src/main/resources/application.yml
new file mode 100644
index 0000000000..9f91e8a03a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/main/resources/application.yml
@@ -0,0 +1,6 @@
+management:
+ endpoints:
+ web:
+ exposure:
+ include: '*'
+ #health,info,beans,metrics,startup
diff --git a/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/config/SimpleLoggingHandlerUnitTest.java b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/config/SimpleLoggingHandlerUnitTest.java
new file mode 100644
index 0000000000..5a6d1bd23f
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/config/SimpleLoggingHandlerUnitTest.java
@@ -0,0 +1,17 @@
+package com.baeldung.samples.config;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationHandler;
+import io.micrometer.observation.tck.AnyContextObservationHandlerCompatibilityKit;
+
+class SimpleLoggingHandlerUnitTest
+ extends AnyContextObservationHandlerCompatibilityKit {
+
+ SimpleLoggingHandler handler = new SimpleLoggingHandler();
+
+ @Override
+ public ObservationHandler handler() {
+ return handler;
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/EnableTestObservation.java b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/EnableTestObservation.java
new file mode 100644
index 0000000000..8e4e2a1da0
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/EnableTestObservation.java
@@ -0,0 +1,44 @@
+package com.baeldung.samples.domain;
+
+import com.baeldung.samples.config.ObservedAspectConfiguration;
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.tracing.test.simple.SimpleTracer;
+import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@AutoConfigureObservability
+@Import({
+ ObservedAspectConfiguration.class,
+ EnableTestObservation.ObservationTestConfiguration.class
+})
+public @interface EnableTestObservation {
+
+ @TestConfiguration
+ class ObservationTestConfiguration {
+
+ @Bean
+ TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ SimpleTracer simpleTracer() {
+ return new SimpleTracer();
+ }
+
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceObservationIntegrationTest.java b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceObservationIntegrationTest.java
new file mode 100644
index 0000000000..98fa175660
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceObservationIntegrationTest.java
@@ -0,0 +1,35 @@
+package com.baeldung.samples.domain;
+
+import io.micrometer.observation.tck.TestObservationRegistry;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat;
+
+@ExtendWith(SpringExtension.class)
+@ComponentScan(basePackageClasses = GreetingService.class)
+@EnableAutoConfiguration
+@EnableTestObservation
+class GreetingServiceObservationIntegrationTest {
+
+ @Autowired
+ GreetingService service;
+ @Autowired
+ TestObservationRegistry registry;
+
+ @Test
+ void testObservation() {
+ // invoke service
+ service.sayHello();
+ assertThat(registry)
+ .hasObservationWithNameEqualTo("greetingService")
+ .that()
+ .hasBeenStarted()
+ .hasBeenStopped();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceTracingIntegrationTest.java b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceTracingIntegrationTest.java
new file mode 100644
index 0000000000..0199c0e7ef
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceTracingIntegrationTest.java
@@ -0,0 +1,42 @@
+package com.baeldung.samples.domain;
+
+import io.micrometer.tracing.test.simple.SimpleTracer;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static io.micrometer.tracing.test.simple.TracerAssert.assertThat;
+
+@ExtendWith(SpringExtension.class)
+@ComponentScan(basePackageClasses = GreetingService.class)
+@EnableAutoConfiguration
+@EnableTestObservation
+class GreetingServiceTracingIntegrationTest {
+
+ @Autowired
+ GreetingService service;
+ @Value("${management.tracing.enabled:true}")
+ boolean tracingEnabled;
+ @Autowired
+ SimpleTracer tracer;
+
+ @Test
+ void testEnabledTracing() {
+ Assertions.assertThat(tracingEnabled).isTrue();
+ }
+
+ @Test
+ void testTracingForGreeting() {
+ service.sayHello();
+ assertThat(tracer)
+ .onlySpan()
+ .hasNameEqualTo("greeting-service#say-hello")
+ .isEnded();
+ }
+
+}