Get ahead
VMware offers training and certification to turbo-charge your progress.
Learn moreThe Spring Observability Team has been working on adding observability support for Spring Applications for quite some time, and we are pleased to inform you that this feature will be generally available with Spring Framework 6 and Spring Boot 3!
What is observability? In our understanding, it is "how well you can understand the internals of your system by examining its outputs". We believe that the interconnection between metrics, logging, and distributed tracing gives you the ability to reason about the state of your system in order to debug exceptions and latency in your applications. You can watch more about what we think observability is in this episode of Enlightning with Jonatan Ivanov.
The upcoming Spring Boot 3.0.0-RC1
release will contain numerous autoconfigurations for improved metrics with Micrometer and new distributed tracing support with Micrometer Tracing (formerly Spring Cloud Sleuth). The most notable changes are that it will contain built-in support for log correlation, W3C context propagation will be the default propagation type, and we will support automatic propagation of metadata to be used by the tracing infrastructure (called "remote baggage") that helps to label the observations.
We have been changing the Micrometer API a lot over the course of this year. The most important change is that we have introduced a new API: the Observation API.
The idea of its founding was that we want the users to instrument their code once using a single API and have multiple benefits out of it (e.g. metrics, tracing, logging).
This blog post details what you need to know to about that API and how you can use it to provide more insights into your application.
For any observation to happen, you need to register ObservationHandler
objects through an ObservationRegistry
. An ObservationHandler
reacts only to supported implementations of an Observation.Context
and can create, for example, timers, spans, and logs by reacting to the lifecycle events of an observation, such as:
start
- Observation has been started. Happens when the Observation#start()
method gets called.
stop
- Observation has been stopped. Happens when the Observation#stop()
method gets called.
error
- An error occurred while observing. Happens when the Observation#error(exception)
method gets called.
event
- An event happened when observing. Happens when the Observation#event(event)
method gets called.
scope started
- Observation opens a scope. The scope must be closed when no longer used. Handlers can create thread local variables on start that are cleared upon closing of the scope. Happens when the Observation#openScope()
method gets called.
scope stopped
- Observation stops a scope. Happens when the Observation.Scope#close()
method gets called.
Whenever any of these methods is called, an ObservationHandler
method (such as onStart(T extends Observation.Context ctx)
, onStop(T extends Observation.Context ctx)
, and so on) are called. To pass state between the handler methods, you can use the Observation.Context
.
The observation state diagram looks like this:
Observation Observation
Context Context
Created ----------> Started ----------> Stopped
The observation Scope state diagram looks like this:
Observation
Context
Scope Started ----------> Scope Closed
To make it possible to debug production problems, an observation needs additional metadata, such as key-value pairs (also known as tags). You can then query your metrics or distributed tracing backend by using those tags to find the required data. Tags can be of either high or low cardinality.
This is an example of the Micrometer Observation API.
// Create an ObservationRegistry
ObservationRegistry registry = ObservationRegistry.create();
// Register an ObservationHandler
registry.observationConfig().observationHandler(new MyHandler());
// Create an Observation and observe your code!
Observation.createNotStarted("user.name", registry)
.contextualName("getting-user-name")
.lowCardinalityKeyValue("userType", "userType1") // let's assume that you can have 3 user types
.highCardinalityKeyValue("userId", "1234") // let's assume that this is an arbitrary number
.observe(() -> log.info("Hello")); // this is a shortcut for starting an observation, opening a scope, running user's code, closing the scope and stopping the observation
Important
High cardinality means that a pair will have an unbounded number of possible values. An HTTP URL is a good example of such a key value (for example, /user/user1234
, /user/user2345
, and so on). Low cardinality means that a key value will have a bounded number of possible values. A templated HTTP URL (such as /user/{userId}
) is a good example of such a key value.
To separate observation lifecycle operations from an observation configuration (such as names and low and high cardinality tags), you can use the ObservationConvention
that provides an easy way of overriding the default naming conventions.
The easiest way to get started is to create a new project from https://start.spring.io. Make sure to select Spring Boot 3.0.0-SNAPSHOT (you can switch to RC1 once it’s available) and your favorite build tool.
We will build a Spring WebMvc server application and a client to call the server using RestTemplate. We start with the server side.
Since we want to start an HTTP server, we have to pick the org.springframework.boot:spring-boot-starter-web
dependency.
To create observations by using the @Observed
aspect, we need to add the org.springframework.boot:spring-boot-starter-aop
dependency.
To add observation features to your application, choose spring-boot-starter-actuator
(to add Micrometer to the classpath).
Now it is time to add observability related features!
Metrics
io.micrometer:micrometer-registry-prometheus
dependency.Tracing
For Tracing Context Propagation with Micrometer Tracing, we need to pick a tracer bridge (tracer is a library that is used to handle the lifecycle of a span). We pick Zipkin Brave by adding the io.micrometer:micrometer-tracing-bridge-brave
.
For Latency Visualization, we need to send the finished spans in some format to a server. In our case, we produce an Zipkin-compliant span. To achieve that, we need to add the io.zipkin.reporter2:zipkin-reporter-brave
dependency.
Logs
com.github.loki4j:loki-logback-appender
dependency (check this link for the latest release version)Important
If you are new to tracing, we need to quickly define a couple of basic terms. You can wrap any operation in a span
. It has a unique span id
and contains timing information and some additional metadata (key-value pairs). Because you can produce child spans from spans, the whole tree of spans forms a trace
that shares the same trace id
(that is, a correlation identifier).
Now we need to add some configuration. We set up actuator
and metrics
to publish percentiles histograms, and we redefine the logging pattern to include the trace and span identifiers. We set the sampling probability to 1.0
to send all traces to latency analysis tool.
/src/main/resources/application.properties
server.port=7654
spring.application.name=server
# All traces should be sent to latency analysis tool
management.tracing.sampling.probability=1.0
management.endpoints.web.exposure.include=prometheus
# For Exemplars to work we need histogram buckets
management.metrics.distribution.percentiles-histogram.http.server.requests=true
# traceID and spanId are predefined MDC keys - we want the logs to include them
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
Since we are running the Grafana stack with Loki and Tempo locally, we configure the loki-logback-appender
to send logs to the local instance of Loki.
/src/main/resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml" />
<springProperty scope="context" name="appName" source="spring.application.name"/>
<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
<http>
<url>http://localhost:3100/loki/api/v1/push</url>
</http>
<format>
<label>
<pattern>app=${appName},host=${HOSTNAME},traceID=%X{traceId:-NONE},level=%level</pattern>
</label>
<message>
<pattern>${FILE_LOG_PATTERN}</pattern>
</message>
<sortByTime>true</sortByTime>
</format>
</appender>
<root level="INFO">
<appender-ref ref="LOKI"/>
</root>
</configuration>
Time to write some server-side code! We want to achieve full observability of our application, including metrics, tracing, and additional logging.
To begin with, we write a controller that logs a message to the console and delegate work to a service.
MyController.java
@RestController
class MyController {
private static final Logger log = LoggerFactory.getLogger(MyController.class);
private final MyUserService myUserService;
MyController(MyUserService myUserService) {
this.myUserService = myUserService;
}
@GetMapping("/user/{userId}")
String userName(@PathVariable("userId") String userId) {
log.info("Got a request");
return myUserService.userName(userId);
}
}
We want to have some detailed observation of the MyUserService#userName
method. Thanks to having added AOP support, we can use the @Observed
annotation. To do so, we can register a ObservedAspect
bean.
MyConfiguration.java
@Configuration(proxyBeanMethods = false)
class MyConfiguration {
// To have the @Observed support we need to register this aspect
@Bean
ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
return new ObservedAspect(observationRegistry);
}
}
MyUserService.java
@Service
class MyUserService {
private static final Logger log = LoggerFactory.getLogger(MyUserService.class);
private final Random random = new Random();
// Example of using an annotation to observe methods
// <user.name> will be used as a metric name
// <getting-user-name> will be used as a span name
// <userType=userType2> will be set as a tag for both metric & span
@Observed(name = "user.name",
contextualName = "getting-user-name",
lowCardinalityKeyValues = {"userType", "userType2"})
String userName(String userId) {
log.info("Getting user name for user with id <{}>", userId);
try {
Thread.sleep(random.nextLong(200L)); // simulates latency
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "foo";
}
}
With metrics and tracing on the classpath, having this annotation leads to the creation of a timer
, a long task timer
, and a span
. The timer would be named user.name
, the long task timer would be named user.name.active
, and the span would be named getting-user-name
.
What about logs? We do not want to write the logging statements manually whenever an observation takes place. What we can do is to create a dedicated handler that logs some text for each observation.
MyHandler.java
// Example of plugging in a custom handler that in this case will print a statement before and after all observations take place
@Component
class MyHandler implements ObservationHandler<Observation.Context> {
private static final Logger log = LoggerFactory.getLogger(MyHandler.class);
@Override
public void onStart(Observation.Context context) {
log.info("Before running the observation for context [{}], userType [{}]", context.getName(), getUserTypeFromContext(context));
}
@Override
public void onStop(Observation.Context context) {
log.info("After running the observation for context [{}], userType [{}]", context.getName(), getUserTypeFromContext(context));
}
@Override
public boolean supportsContext(Observation.Context context) {
return true;
}
private String getUserTypeFromContext(Observation.Context context) {
return StreamSupport.stream(context.getLowCardinalityKeyValues().spliterator(), false)
.filter(keyValue -> "userType".equals(keyValue.getKey()))
.map(KeyValue::getValue)
.findFirst()
.orElse("UNKNOWN");
}
}
That is it! Time for the client side.
As before, we add the spring-boot-starter-web
and spring-boot-starter-actuator
dependencies to have a web server running and Micrometer support added.
Time to add observability related features!
Metrics
io.micrometer:micrometer-registry-prometheus
dependency.Tracing
For Tracing Context Propagation with Micrometer Tracing, we need to pick a tracer bridge. We pick OpenTelemetry by adding the io.micrometer:micrometer-tracing-bridge-otel
.
For Latency Visualization, we need to send the finished spans in some format to a server. In our case, we produce an OpenZipkin compliant span. To achieve that, we need to add the io.opentelemetry:opentelemetry-exporter-zipkin
dependency.
Logs
com.github.loki4j:loki-logback-appender
dependency (check this link for the latest release version) to ship logs to Loki.Now we need to add some configuration. We add almost identical configuration as we did on the server side.
/src/main/resources/application.properties
server.port=6543
spring.application.name=client
# All traces should be sent to latency analysis tool
management.tracing.sampling.probability=1.0
management.endpoints.web.exposure.include=prometheus
# traceID and spanId are predefined MDC keys - we want the logs to include them
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
The Loki Appender configuration looks exactly the same.
/src/main/resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml" />
<springProperty scope="context" name="appName" source="spring.application.name"/>
<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
<http>
<url>http://localhost:3100/loki/api/v1/push</url>
</http>
<format>
<label>
<pattern>app=${appName},host=${HOSTNAME},traceID=%X{traceId:-NONE},level=%level</pattern>
</label>
<message>
<pattern>${FILE_LOG_PATTERN}</pattern>
</message>
<sortByTime>true</sortByTime>
</format>
</appender>
<root level="INFO">
<appender-ref ref="LOKI"/>
</root>
</configuration>
Now it is time to write some client-side code! We send a request with RestTemplate
to the server side, and we want to achieve the full observability of our application, including metrics and tracing.
To begin, we need a RestTemplate
bean that is automatically instrumented by Spring Boot. Remember to inject the RestTemplateBuilder
and to construct a RestTemplate
instance from the builder.
MyConfiguration.java
@Configuration(proxyBeanMethods = false)
class MyConfiguration {
// IMPORTANT! To instrument RestTemplate you must inject the RestTemplateBuilder
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
Now we can write a CommandLineRunner
bean that is wrapped by using the Observation API and that sends a request to the server side. All parts of the API are described in more detail in the following snippet.
MyConfiguration.java
@Configuration(proxyBeanMethods = false)
class MyConfiguration {
@Bean
CommandLineRunner myCommandLineRunner(ObservationRegistry registry, RestTemplate restTemplate) {
Random highCardinalityValues = new Random(); // Simulates potentially large number of values
List<String> lowCardinalityValues = Arrays.asList("userType1", "userType2", "userType3"); // Simulates low number of values
return args -> {
String highCardinalityUserId = String.valueOf(highCardinalityValues.nextLong(100_000));
// Example of using the Observability API manually
// <my.observation> is a "technical" name that does not depend on the context. It will be used to name e.g. Metrics
Observation.createNotStarted("my.observation", registry)
// Low cardinality means that the number of potential values won't be big. Low cardinality entries will end up in e.g. Metrics
.lowCardinalityKeyValue("userType", randomUserTypePicker(lowCardinalityValues))
// High cardinality means that the number of potential values can be large. High cardinality entries will end up in e.g. Spans
.highCardinalityKeyValue("userId", highCardinalityUserId)
// <command-line-runner> is a "contextual" name that gives more details within the provided context. It will be used to name e.g. Spans
.contextualName("command-line-runner")
// The following lambda will be executed with an observation scope (e.g. all the MDC entries will be populated with tracing information). Also the observation will be started, stopped and if an error occurred it will be recorded on the observation
.observe(() -> {
log.info("Will send a request to the server"); // Since we're in an observation scope - this log line will contain tracing MDC entries ...
String response = restTemplate.getForObject("http://localhost:7654/user/{userId}", String.class, highCardinalityUserId); // Boot's RestTemplate instrumentation creates a child span here
log.info("Got response [{}]", response); // ... so will this line
});
};
}
}
We have prepared a Docker setup of the whole observability infrastructure under this link. Follow these steps to run the infrastructure and both applications.
To run the samples:
Start up the observability stack (for demonstration purposes, you can use the provided Grafana, Tempo, and Loki stack) and wait for it to start.
$ docker compose up
To access Prometheus go to http://localhost:9090/
To access Grafana go to http://localhost:3000/
Run the server side application (this will block your current terminal window).
$ ./mvnw spring-boot:run -pl :server
Run the client side application (this will block your current terminal window)
$ ./mvnw spring-boot:run -pl :client
You should see log statements similar to these:
2022-10-04T15:04:55.345+02:00 INFO [client,bbe3aea006077640b66d40f3e62f04b9,93b7a150b7e293ef] 92556 --- [ main] com.example.client.ClientApplication : Will send a request to the server
2022-10-04T15:04:55.385+02:00 INFO [client,bbe3aea006077640b66d40f3e62f04b9,93b7a150b7e293ef] 92556 --- [ main] com.example.client.ClientApplication : Got response [foo]
Go to Grafana, go to dashboards, and click on the Logs, Traces, Metrics
dashboard. There you can pick a trace ID value (for example, bbe3aea006077640b66d40f3e62f04b9
) to find all logs and traces from both applications that correspond to that trace ID. You should see a following correlated view of logs and traces related to the same trace identifier, and you will see metrics taking place at the same time range. The metrics are related to HTTP request processing latency. These come from the automated Spring Boot WebMvc instrumentation that uses the Micrometer API.
Notice a diamond shape in the metrics. These are Exemplars
. Those are “specific trace representative of measurement taken in a given time interval”. If you click on the shape, you can jump to the trace ID view to see the corresponding trace.
Either click on the trace ID to Query it with Tempo
or go to Tempo and pick the trace identifier yourself. You will see the following screen.
Each bar represents a span
. You can see how much time it took for each operation to complete. If you click on a given span, you can see tags (key-value metadata) and timing information related to that particular operation.
This is how the correlated logs view would look in Loki.
If you want to see the @Observed
annotated method metrics, you can go to the Prometheus
view and find the user_name
Timer.
If you want to see the metrics from your Observation that you have manually created, go to the Prometheus
view and find the my_observation
Timer.
To better understand how Spring Boot supports Native, please read this excellent blog post. We reuse that knowledge to run the previously created applications using Spring Native.
To build the applications, you need GraalVM on your path. If you use SDKMan
, invoke the following:
sdk install java 22.3.r17.ea-nik
See also GraalVM Quickstart.
To build the application with Maven, you need to enable the native
profile:
$ ./mvnw native:compile -Pnative
Run the server side application first
$ ./server/target/server
Next, run the client side application.
$ ./client/target/client
You should get output similar to this:
Client side logs
2022-10-10T12:57:17.712+02:00 INFO \[client,,\] 82009 --- \[ main\] com.example.client.ClientApplication : Starting ClientApplication using Java 17.0.4 on marcin-precision5560 with PID 82009 (/home/marcin/repo/observability/blogs/bootRc1/client/target/client started by marcin in /home/marcin/repo/observability/blogs/bootRc1)
2022-10-10T12:57:17.712+02:00 INFO \[client,,\] 82009 --- \[ main\] com.example.client.ClientApplication : No active profile set, falling back to 1 default profile: "default"
2022-10-10T12:57:17.723+02:00 INFO \[client,,\] 82009 --- \[ main\] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 6543 (http)
2022-10-10T12:57:17.723+02:00 INFO \[client,,\] 82009 --- \[ main\] o.apache.catalina.core.StandardService : Starting service \[Tomcat\]
2022-10-10T12:57:17.723+02:00 INFO \[client,,\] 82009 --- \[ main\] o.apache.catalina.core.StandardEngine : Starting Servlet engine: \[Apache Tomcat/10.0.23\]
2022-10-10T12:57:17.727+02:00 INFO \[client,,\] 82009 --- \[ main\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\] : Initializing Spring embedded WebApplicationContext
2022-10-10T12:57:17.727+02:00 INFO \[client,,\] 82009 --- \[ main\] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 15 ms
2022-10-10T12:57:17.731+02:00 WARN \[client,,\] 82009 --- \[ main\] i.m.c.i.binder.jvm.JvmGcMetrics : GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM
2022-10-10T12:57:17.781+02:00 INFO \[client,,\] 82009 --- \[ main\] o.s.b.a.e.web.EndpointLinksResolver : Exposing 15 endpoint(s) beneath base path '/actuator'
2022-10-10T12:57:17.783+02:00 INFO \[client,,\] 82009 --- \[ main\] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 6543 (http) with context path ''
2022-10-10T12:57:17.783+02:00 INFO \[client,,\] 82009 --- \[ main\] com.example.client.ClientApplication : Started ClientApplication in 0.077 seconds (process running for 0.079)
2022-10-10T12:57:17.784+02:00 INFO \[client,27c1113e4276c4173daec3675f536bf4,e0f2db8b983607d8\] 82009 --- \[ main\] com.example.client.ClientApplication : Will send a request to the server
2022-10-10T12:57:17.820+02:00 INFO \[client,27c1113e4276c4173daec3675f536bf4,e0f2db8b983607d8\] 82009 --- \[ main\] com.example.client.ClientApplication : Got response \[foo\]
2022-10-10T12:57:18.966+02:00 INFO \[client,,\] 82009 --- \[nio-6543-exec-1\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-10-10T12:57:18.966+02:00 INFO \[client,,\] 82009 --- \[nio-6543-exec-1\] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-10-10T12:57:18.966+02:00 INFO \[client,,\] 82009 --- \[nio-6543-exec-1\] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
Server side logs
2022-10-10T12:57:07.200+02:00 INFO \[server,,\] 81760 --- \[ main\] com.example.server.ServerApplication : Starting ServerApplication using Java 17.0.4 on marcin-precision5560 with PID 81760 (/home/marcin/repo/observability/blogs/bootRc1/server/target/server started by marcin in /home/marcin/repo/observability/blogs/bootRc1)
2022-10-10T12:57:07.201+02:00 INFO \[server,,\] 81760 --- \[ main\] com.example.server.ServerApplication : No active profile set, falling back to 1 default profile: "default"
2022-10-10T12:57:07.213+02:00 INFO \[server,,\] 81760 --- \[ main\] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 7654 (http)
2022-10-10T12:57:07.213+02:00 INFO \[server,,\] 81760 --- \[ main\] o.apache.catalina.core.StandardService : Starting service \[Tomcat\]
2022-10-10T12:57:07.213+02:00 INFO \[server,,\] 81760 --- \[ main\] o.apache.catalina.core.StandardEngine : Starting Servlet engine: \[Apache Tomcat/10.0.23\]
2022-10-10T12:57:07.217+02:00 INFO \[server,,\] 81760 --- \[ main\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\] : Initializing Spring embedded WebApplicationContext
2022-10-10T12:57:07.217+02:00 INFO \[server,,\] 81760 --- \[ main\] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 16 ms
2022-10-10T12:57:07.222+02:00 WARN \[server,,\] 81760 --- \[ main\] i.m.c.i.binder.jvm.JvmGcMetrics : GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM
2022-10-10T12:57:07.278+02:00 INFO \[server,,\] 81760 --- \[ main\] o.s.b.a.e.web.EndpointLinksResolver : Exposing 15 endpoint(s) beneath base path '/actuator'
2022-10-10T12:57:07.280+02:00 INFO \[server,,\] 81760 --- \[ main\] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 7654 (http) with context path ''
2022-10-10T12:57:07.281+02:00 INFO \[server,,\] 81760 --- \[ main\] com.example.server.ServerApplication : Started ServerApplication in 0.086 seconds (process running for 0.088)
2022-10-10T12:57:07.639+02:00 INFO \[server,,\] 81760 --- \[nio-7654-exec-1\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-10-10T12:57:07.639+02:00 INFO \[server,,\] 81760 --- \[nio-7654-exec-1\] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-10-10T12:57:07.640+02:00 INFO \[server,,\] 81760 --- \[nio-7654-exec-1\] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2022-10-10T12:57:17.785+02:00 INFO \[server,,\] 81760 --- \[nio-7654-exec-8\] com.example.server.MyHandler : Before running the observation for context \[http.server.requests\]
2022-10-10T12:57:17.785+02:00 INFO \[server,27c1113e4276c4173daec3675f536bf4,9affba5698490e2d\] 81760 --- \[nio-7654-exec-8\] com.example.server.MyController : Got a request
2022-10-10T12:57:17.820+02:00 INFO \[server,,\] 81760 --- \[nio-7654-exec-8\] com.example.server.MyHandler : After running the observation for context \[http.server.requests\]
You can check Grafana for metrics, traces and logs!
On the client side, we need to provide the reflect-config.js
configuration manually. For more information, see this PR.
In this blog post, we have managed to give you an introduction of the main concepts behind the Micrometer Observability API. We have also shown you how you can create observations by using the Observation API and annotations. You can also visualize the latency, see the correlated logs, and check the metrics that come from your Spring Boot applications.
You could also observe your applications by using native images with Spring Native.
Work on the Micrometer Observability would not be possible without the extensive support of the whole Spring team, Tadaya Tsuyukubo, Johnny Lim, and all the other contributors and reviewers.
Based on community feedback, we will continue to improve our Observability story. We intend to go GA in November this year.
This is an exciting time for us. We would again like to thank everyone who has already contributed and reported feedback, and we look forward to further feedback! Check out Spring Boot’s latest snapshots! Check out the documentation of our projects: Micrometer Context Propagation, Micrometer, Micrometer Observation, Micrometer Tracing and Micrometer Docs Generator! Click here to see the code used for this blog post.