Observability with Spring Boot 3

Engineering | Marcin Grzejszczak | October 12, 2022 | ...

The 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.

How Does Micrometer Observation Work?

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.

Building Your First Observed Application

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.

WebMvc Server Setup

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

    • For Micrometer metrics with Prometheus, we need to add the 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.

      • The client application that we will create for this demo will use another tracer library to show an interop between tracers.
    • 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

    • Since we have Micrometer Tracing on the classpath, the logs are automatically correlated (that is, they contain a unique trace identifier). Now we need to ship the logs. For this demo, we ship them to Grafana Loki. We can achieve that by adding the 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>

WebMvc Server Code

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.

RestTemplate Client Application Setup

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

    • For Micrometer metrics with Prometheus, we need to add the 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

    • As previously, we add the 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>

RestTemplate Application Client Code

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

        };
    }
}

Running It All Together

We have prepared a Docker setup of the whole observability infrastructure under this link. Follow these steps to run the infrastructure and both applications.

Running the samples

To run the samples:

  1. 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
    
  2. Run the server side application (this will block your current terminal window).

    $ ./mvnw spring-boot:run -pl :server
    
  3. 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]
    
  4. 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.

    logs metrics traces

    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.

    exemplar

  5. 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.

trace view

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.

span tags

This is how the correlated logs view would look in Loki.

correlated logs

If you want to see the @Observed annotated method metrics, you can go to the Prometheus view and find the user_name Timer.

annotation metric

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.

my observation

Running It All Together with AOT Support

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.

Building

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

Running

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!

Native Support Limitations

On the client side, we need to provide the reflect-config.js configuration manually. For more information, see this PR.

Summary

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.

Acknowledgments

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.

Next Steps

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.

Get the Spring newsletter

Thank you for your interest. Someone will get back to you shortly.

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring Runtime offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all