From 40249f907cdcf0362b1a280155a53930fcb7f021 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr <40685729+ueberfuhr@users.noreply.github.com> Date: Wed, 28 Dec 2022 21:47:49 +0100 Subject: [PATCH] BAEL-5867: Micrometer Observability API with Spring Boot 3 (#13180) * BAEL-5867: Create project and create first usage main method * BAEL-5867: Use Observation API in Boot * BAEL-5867: Refactoring and testing * BAEL-5867: Add tracing * BAEL-5867: Remove notes and add project as a module to the aggregator * BAEL-5867: Fix pmd rules violation --- pom.xml | 2 + .../spring-boot-3-observation/pom.xml | 66 +++++++++++++++++++ .../baeldung/samples/GreetingApplication.java | 13 ++++ .../samples/SimpleObservationApplication.java | 66 +++++++++++++++++++ .../samples/boundary/GreetingController.java | 26 ++++++++ .../ObservationFilterConfiguration.java | 22 +++++++ .../config/ObservationHandlerLogger.java | 28 ++++++++ ...ObservationTextPublisherConfiguration.java | 21 ++++++ .../config/ObservedAspectConfiguration.java | 20 ++++++ .../samples/config/SimpleLoggingHandler.java | 59 +++++++++++++++++ .../samples/domain/GreetingService.java | 14 ++++ .../src/main/resources/application.yml | 6 ++ .../config/SimpleLoggingHandlerUnitTest.java | 17 +++++ .../samples/domain/EnableTestObservation.java | 44 +++++++++++++ ...tingServiceObservationIntegrationTest.java | 35 ++++++++++ ...GreetingServiceTracingIntegrationTest.java | 42 ++++++++++++ 16 files changed, 481 insertions(+) create mode 100644 spring-boot-modules/spring-boot-3-observation/pom.xml create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/GreetingApplication.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/SimpleObservationApplication.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/GreetingController.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/boundary/ObservationFilterConfiguration.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationHandlerLogger.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservationTextPublisherConfiguration.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/ObservedAspectConfiguration.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/config/SimpleLoggingHandler.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/java/com/baeldung/samples/domain/GreetingService.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/main/resources/application.yml create mode 100644 spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/config/SimpleLoggingHandlerUnitTest.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/EnableTestObservation.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceObservationIntegrationTest.java create mode 100644 spring-boot-modules/spring-boot-3-observation/src/test/java/com/baeldung/samples/domain/GreetingServiceTracingIntegrationTest.java 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(); + } + +}