可观测性支持
Micrometer 定义了一种Observation(观测)概念,可在应用程序中同时支持指标(Metrics)和追踪(Traces)。 指标支持提供了一种创建计时器、仪表或计数器的方法,用于收集有关应用程序运行时行为的统计信息。 指标可以帮助你跟踪错误率、使用模式、性能等。 追踪则提供了跨越应用边界的整个系统的整体视图;你可以聚焦于特定的用户请求,并跟踪其在整个应用系统中的完整执行过程。
Spring Framework 对其自身代码库的多个部分进行了插桩,以便在配置了 ObservationRegistry 时发布观测数据。
您可以进一步了解在 Spring Boot 中配置可观测性基础设施。
已生成的观测列表
Spring Framework 提供了多种用于可观测性的功能。 如本节开头所述,根据配置的不同,观测(observations)可以生成计时器指标(Metrics)和/或追踪(Traces)。
| 观测名称 | 描述 |
|---|---|
HTTP 客户端交换所花费的时间 |
|
在框架级别处理 HTTP 服务器交换的时间 |
|
消息生产者向目标发送 JMS 消息所花费的时间。 |
|
之前由消息消费者接收到的 JMS 消息的处理时间。 |
|
|
| Observations 使用 Micrometer 的官方命名规范,但指标(Metrics)名称将自动转换为 监控系统后端所偏好的格式 (如 Prometheus、Atlas、Graphite、InfluxDB 等)。 |
Micrometer 观测概念
如果你不熟悉 Micrometer Observation,以下是关于你需要了解的概念的简要概述。
-
Observation是对应用程序中发生的某件事情的实际记录。它由ObservationHandler的实现进行处理,以生成指标(metrics)或追踪(traces)。 -
每个观测(observation)都有一个对应的
ObservationContext实现;该类型保存了用于提取其元数据的所有相关信息。 对于 HTTP 服务器观测而言,其上下文实现可能包含 HTTP 请求、HTTP 响应、处理过程中抛出的任何异常等。 -
每个
Observation都包含KeyValues元数据。以 HTTP 服务器的 observation 为例,这些元数据可以是 HTTP 请求方法、HTTP 响应状态等。 这些元数据由ObservationConvention的实现类提供,这些实现类应声明它们所支持的ObservationContext类型。 -
如果
KeyValues元组的可能取值数量较少且有限(例如 HTTP 方法就是一个很好的例子),则称这些KeyValue为“低基数”(low cardinality)。 低基数的值仅用于指标(metrics)。 相反,“高基数”(high cardinality)的值是无界的(例如 HTTP 请求 URI),仅用于追踪(traces)。 -
ObservationDocumentation用于记录特定领域中的所有观测数据,列出预期的键名及其含义。
配置观测功能
全局配置选项可在 ObservationRegistry#observationConfig() 级别进行设置。
每个被监控的组件将提供两个扩展点:
-
设置
ObservationRegistry;如果未设置,观测数据将不会被记录,相关操作将变为无操作(no-op) -
提供一个自定义的
ObservationConvention来更改默认的观测名称和提取的KeyValues
使用自定义观测约定
以 Spring MVC 的 "http.server.requests" 指标监控为例,它使用了 ServerHttpObservationFilter。
该监控使用了一个带有 ServerRequestObservationConvention 的 ServerRequestObservationContext;自定义的约定可以在 Servlet 过滤器上进行配置。
如果您希望自定义监控所产生的元数据,可以根据自身需求继承 DefaultServerRequestObservationConvention:
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
public class ExtendedServerRequestObservationConvention extends DefaultServerRequestObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
// here, we just want to have an additional KeyValue to the observation, keeping the default values
return super.getLowCardinalityKeyValues(context).and(custom(context));
}
private KeyValue custom(ServerRequestObservationContext context) {
return KeyValue.of("custom.method", context.getCarrier().getMethod());
}
}
如果你希望拥有完全的控制权,可以为你所关注的观测实现完整的约定契约:
import java.util.Locale;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.ServerHttpObservationDocumentation;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.http.server.observation.ServerRequestObservationConvention;
public class CustomServerRequestObservationConvention implements ServerRequestObservationConvention {
@Override
public String getName() {
// will be used as the metric name
return "http.server.requests";
}
@Override
public String getContextualName(ServerRequestObservationContext context) {
// will be used for the trace name
return "http " + context.getCarrier().getMethod().toLowerCase(Locale.ROOT);
}
@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
return KeyValues.of(method(context), status(context), exception(context));
}
@Override
public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) {
return KeyValues.of(httpUrl(context));
}
private KeyValue method(ServerRequestObservationContext context) {
// You should reuse as much as possible the corresponding ObservationDocumentation for key names
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod());
}
// status(), exception(), httpUrl()...
private KeyValue status(ServerRequestObservationContext context) {
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, String.valueOf(context.getResponse().getStatus()));
}
private KeyValue exception(ServerRequestObservationContext context) {
String exception = (context.getError() != null ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE);
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, exception);
}
private KeyValue httpUrl(ServerRequestObservationContext context) {
return KeyValue.of(ServerHttpObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, context.getCarrier().getRequestURI());
}
}
你也可以通过使用自定义的 ObservationFilter 来实现类似的目标——为观测(observation)添加或移除键值。
过滤器不会替换默认的约定,而是作为后处理组件使用。
import io.micrometer.common.KeyValue;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationFilter;
import org.springframework.http.server.observation.ServerRequestObservationContext;
public class ServerRequestObservationFilter implements ObservationFilter {
@Override
public Observation.Context map(Observation.Context context) {
if (context instanceof ServerRequestObservationContext serverContext) {
context.setName("custom.observation.name");
context.addLowCardinalityKeyValue(KeyValue.of("project", "spring"));
String customAttribute = (String) serverContext.getCarrier().getAttribute("customAttribute");
context.addLowCardinalityKeyValue(KeyValue.of("custom.attribute", customAttribute));
}
return context;
}
}
你可以在 ObservationFilter 上配置 ObservationRegistry 实例。
@Scheduled 任务的可观测性支持
每次执行 @Scheduled 任务 时,都会创建一个观测记录。
应用程序需要在 ScheduledTaskRegistrar 上配置 ObservationRegistry,以启用观测记录的采集。
这可以通过声明一个设置观测注册表的 SchedulingConfigurer Bean 来实现:
import io.micrometer.observation.ObservationRegistry;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
public class ObservationSchedulingConfigurer implements SchedulingConfigurer {
private final ObservationRegistry observationRegistry;
public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setObservationRegistry(this.observationRegistry);
}
}
它默认使用 org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention,并由 ScheduledTaskObservationContext 提供支持。
您可以直接在 ObservationRegistry 上配置自定义实现。
在调度方法执行期间,当前的观测(observation)会恢复到 ThreadLocal 上下文或 Reactor 上下文中(如果调度方法返回 Mono 或 Flux 类型)。
默认情况下,会创建以下 KeyValues:
姓名 |
描述 |
|
被安排执行的 Java |
|
持有调度方法的 Bean 实例所属类的规范名称,对于匿名类则为 |
|
执行期间抛出的异常的类名,如果没有发生异常,则为 |
|
复制了 |
|
方法执行的结果。可以是 |
JMS 消息传递插桩
如果类路径中包含 io.micrometer:micrometer-jakarta9 依赖,Spring Framework 将使用 Micrometer 提供的 Jakarta JMS 指标监控功能。
io.micrometer.jakarta9.instrument.jms.JmsInstrumentation 会对 jakarta.jms.Session 进行监控,并记录相关的观测数据。
此插桩将创建两种类型的观测:
-
"jms.message.publish"当 JMS 消息发送到代理(broker)时触发,通常通过JmsTemplate发送。 -
"jms.message.process"当应用程序处理 JMS 消息时触发,通常通过MessageListener或带有@JmsListener注解的方法实现。
目前没有针对 "jms.message.receive" 观测的埋点,因为测量等待接收消息所花费的时间几乎没有价值。
此类集成通常会对 MessageConsumer#receive 方法调用进行埋点。但一旦这些方法返回,处理时间就无法被测量,且追踪上下文也无法传播到应用程序中。 |
默认情况下,这两个观测共享同一组可能的KeyValues:
姓名 |
描述 |
|
消息传递操作期间抛出的异常的类名(或“none”)。 |
|
复制了 |
|
目标是否为 |
|
正在执行的 JMS 操作的名称(取值: |
姓名 |
描述 |
|
JMS 消息的关联 ID。 |
|
当前消息所发送到的目标名称。 |
|
消息系统用作消息标识符的值。 |
JMS 消息发布插桩
"jms.message.publish" 观测记录在 JMS 消息发送到代理(broker)时生成。
它们用于测量发送消息所花费的时间,并通过传出的 JMS 消息头传播追踪信息。
您需要在 ObservationRegistry 上配置 JmsTemplate 以启用观测功能:
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.jms.core.JmsTemplate;
public class JmsTemplatePublish {
private final JmsTemplate jmsTemplate;
private final JmsMessagingTemplate jmsMessagingTemplate;
public JmsTemplatePublish(ObservationRegistry observationRegistry, ConnectionFactory connectionFactory) {
this.jmsTemplate = new JmsTemplate(connectionFactory);
// configure the observation registry
this.jmsTemplate.setObservationRegistry(observationRegistry);
// For JmsMessagingTemplate, instantiate it with a JMS template that has a configured registry
this.jmsMessagingTemplate = new JmsMessagingTemplate(this.jmsTemplate);
}
public void sendMessages() {
this.jmsTemplate.convertAndSend("spring.observation.test", "test message");
}
}
默认使用 io.micrometer.jakarta9.instrument.jms.DefaultJmsPublishObservationConvention,其底层由 io.micrometer.jakarta9.instrument.jms.JmsPublishObservationContext 支持。
当监听器方法返回响应消息时,使用 @JmsListener 注解的方法也会记录类似的观察结果。
JMS 消息处理仪器化
"jms.message.process" 观测记录在应用程序处理 JMS 消息时生成。
它们用于测量处理消息所花费的时间,并通过传入的 JMS 消息头传播追踪上下文。
大多数应用程序将使用 @JmsListener 注解方法 机制来处理传入消息。
您需要确保在专用的 JmsListenerContainerFactory 上配置了 ObservationRegistry:
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
@Configuration
@EnableJms
public class JmsConfiguration {
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, ObservationRegistry observationRegistry) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setObservationRegistry(observationRegistry);
return factory;
}
}
需要默认的容器工厂来启用注解支持,
但请注意,@JmsListener 注解可以引用特定的容器工厂 Bean 以实现特定目的。
在所有情况下,只有当容器工厂上配置了观测注册表(observation registry)时,才会记录观测数据(Observations)。
当消息由 JmsTemplate 处理时,MessageListener 会记录类似的观察结果。
此类监听器是在会话回调(参见 MessageConsumer)中设置到 JmsTemplate.execute(SessionCallback<T>) 上的。
此观测(observation)默认使用 io.micrometer.jakarta9.instrument.jms.DefaultJmsProcessObservationConvention,其底层由 io.micrometer.jakarta9.instrument.jms.JmsProcessObservationContext 支持。
HTTP 服务器插桩
对于 Servlet 和响应式(Reactive)应用程序,HTTP 服务器交换观测指标会以名称 "http.server.requests" 创建;
如果使用 OpenTelemetry 规范,则名称为 "http.server.request.duration"。
Servlet 应用程序
应用程序需要在其应用中配置 org.springframework.web.filter.ServerHttpObservationFilter Servlet 过滤器。
仅当 Exception 未被 Web 框架处理并冒泡至 Servlet 过滤器时,才会将此次观测记录为错误。
通常,由 Spring MVC 的 @ExceptionHandler 和 ProblemDetail 支持 处理的所有异常都不会被记录到该观测中。
您可以在请求处理的任何阶段,自行在 ObservationContext 上设置错误字段:
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.filter.ServerHttpObservationFilter;
@Controller
public class UserController {
@ExceptionHandler(MissingUserException.class)
ResponseEntity<Void> handleMissingUser(HttpServletRequest request, MissingUserException exception) {
// We want to record this exception with the observation
ServerHttpObservationFilter.findObservationContext(request)
.ifPresent(context -> context.setError(exception));
return ResponseEntity.notFound().build();
}
static class MissingUserException extends RuntimeException {
}
}
由于该检测(instrumentation)是在 Servlet 过滤器(Filter)级别完成的,因此观测范围(observation scope)仅涵盖排在此过滤器之后的其他过滤器以及请求的处理过程。
通常,Servlet 容器的错误处理是在更低的层级执行的,不会有任何活跃的观测(observation)或跨度(span)。
针对这种使用场景,需要容器特定的实现方式,例如 Tomcat 中的 org.apache.catalina.Valve;这超出了本项目的范围。 |
默认语义约定
默认使用 org.springframework.http.server.observation.DefaultServerRequestObservationConvention,其底层由 ServerRequestObservationContext 支持。
默认情况下,会创建以下 KeyValues:
姓名 |
描述 |
|
交换过程中抛出的异常的类名,如果未发生异常,则为 |
|
复制了 |
|
HTTP 请求方法的名称,如果该方法不是已知的标准方法,则为 |
|
HTTP 服务器交换的结果。 |
|
HTTP 响应的原始状态码,如果未创建响应,则为 |
|
匹配处理器的 URI 模式(如果可用),否则对于 3xx 响应回退为 |
姓名 |
描述 |
|
HTTP 请求 URI。 |
响应式应用程序
应用程序需要使用 WebHttpHandlerBuilder 配置 MeterRegistry 以启用服务器端监控。
这可以通过 WebHttpHandlerBuilder 完成,如下所示:
import io.micrometer.observation.ObservationRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@Configuration(proxyBeanMethods = false)
public class HttpHandlerConfiguration {
private final ApplicationContext applicationContext;
public HttpHandlerConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
public HttpHandler httpHandler(ObservationRegistry registry) {
return WebHttpHandlerBuilder.applicationContext(this.applicationContext)
.observationRegistry(registry)
.build();
}
}
默认使用 org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention,其底层由 ServerRequestObservationContext 支持。
仅当 Exception 未被应用程序 Controller 处理时,才会将此次观测记录为错误。
通常,由 Spring WebFlux 的 @ExceptionHandler 和 ProblemDetail 支持 处理的所有异常都不会被记录到观测中。
您可以在请求处理的任何阶段,自行在 ObservationContext 上设置错误字段:
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ServerWebExchange;
@Controller
public class UserController {
@ExceptionHandler(MissingUserException.class)
ResponseEntity<Void> handleMissingUser(ServerWebExchange exchange, MissingUserException exception) {
// We want to record this exception with the observation
ServerRequestObservationContext.findCurrent(exchange.getAttributes())
.ifPresent(context -> context.setError(exception));
return ResponseEntity.notFound().build();
}
static class MissingUserException extends RuntimeException {
}
}
默认情况下,会创建以下 KeyValues:
姓名 |
描述 |
|
交换过程中抛出的异常的类名,如果未发生异常,则为 |
|
复制了 |
|
HTTP 请求方法的名称,如果该方法不是已知的标准方法,则为 |
|
HTTP 服务器交换的结果。 |
|
HTTP 响应的原始状态码,如果未创建响应,则为 |
|
匹配处理器的 URI 模式(如果可用),否则对于 3xx 响应回退为 |
姓名 |
描述 |
|
HTTP 请求 URI。 |
HTTP 客户端插桩
HTTP 客户端交换观测(observations)会为阻塞式和响应式客户端创建,其名称为 "http.client.requests"。
该观测衡量整个 HTTP 请求/响应交换过程,从建立连接一直到响应体反序列化完成。
与服务器端的对应机制不同,该仪器化(instrumentation)直接在客户端中实现,因此唯一需要的步骤就是在客户端上配置一个 ObservationRegistry。
RestTemplate
应用程序必须在 ObservationRegistry 实例上配置一个 RestTemplate 才能启用仪器化;否则,观测操作将变为“空操作”(no-ops)。
Spring Boot 会自动配置已设置好观测注册表(observation registry)的 RestTemplateBuilder Bean。
Instrumentation 默认使用 org.springframework.http.client.observation.ClientRequestObservationConvention,其底层由 ClientRequestObservationContext 支持。
姓名 |
描述 |
|
HTTP 请求方法的名称,如果该方法不是已知的标准方法,则为 |
|
用于 HTTP 请求的 URI 模板,如果未提供则为 |
|
从请求 URI 的主机名派生的客户端名称。 |
|
HTTP 响应的原始状态码,如果发生 |
|
HTTP 客户端交换的结果。 |
|
交换过程中抛出的异常的类名,如果未发生异常,则为 |
|
复制了 |
姓名 |
描述 |
|
HTTP 请求 URI。 |
REST 客户端
应用程序必须在 ObservationRegistry 上配置一个 RestClient.Builder 以启用监控;否则,监控操作将被视为“空操作”(no-ops)。
Instrumentation 默认使用 org.springframework.http.client.observation.ClientRequestObservationConvention,其底层由 ClientRequestObservationContext 支持。
姓名 |
描述 |
|
HTTP 请求方法的名称,如果请求无法创建,则为 |
|
用于 HTTP 请求的 URI 模板,如果未提供则为 |
|
从请求 URI 的主机名派生的客户端名称。 |
|
HTTP 响应的原始状态码,如果发生 |
|
HTTP 客户端交换的结果。 |
|
交换过程中抛出的异常的类名,如果未发生异常,则为 |
|
复制了 |
姓名 |
描述 |
|
HTTP 请求 URI。 |
WebClient
应用程序必须在 ObservationRegistry 上配置一个 WebClient.Builder 以启用监控功能;否则,监控操作将被视为“空操作”(no-ops)。
Spring Boot 会自动配置已设置好观察注册表(observation registry)的 WebClient.Builder Bean。
Instrumentation 默认使用 org.springframework.web.reactive.function.client.ClientRequestObservationConvention,其底层由 ClientRequestObservationContext 支持。
姓名 |
描述 |
|
HTTP 请求方法的名称,如果该方法不是已知的标准方法,则为 |
|
用于 HTTP 请求的 URI 模板,如果未提供则为 |
|
从请求 URI 的主机名派生的客户端名称。 |
|
HTTP 响应的原始状态码,如果发生 |
|
HTTP 客户端交换的结果。 |
|
交换过程中抛出的异常的类名,如果未发生异常,则为 |
|
复制了 |
姓名 |
描述 |
|
HTTP 请求 URI。 |
应用程序事件和@EventListener
Spring Framework 不会为 @EventListener 调用 提供观测数据,
因为它们不具备此类埋点所需的正确语义。
默认情况下,事件的发布和处理是同步且在同一线程上完成的。
这意味着在该任务执行期间,ThreadLocal 变量和日志上下文将与事件发布者保持一致。
如果应用程序全局配置了一个自定义的 ApplicationEventMulticaster,并且该多播器使用的策略是在不同线程上调度事件处理,那么上述情况就不再成立。
所有 @EventListener 方法都将在不同于主事件发布线程的其他线程上执行。
在这种情况下,Micrometer 上下文传播库 可以帮助传播此类值,并更好地关联事件的处理过程。
应用程序可以配置所选的 TaskExecutor,使其使用一个 ContextPropagatingTaskDecorator 来装饰任务并传播上下文。
要实现此功能,类路径中必须包含 io.micrometer:context-propagation 库:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
@Configuration
public class ApplicationEventsConfiguration {
@Bean(name = "applicationEventMulticaster")
public SimpleApplicationEventMulticaster simpleApplicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
// decorate task execution with a decorator that supports context propagation
taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
eventMulticaster.setTaskExecutor(taskExecutor);
return eventMulticaster;
}
}
同样地,如果为每个使用 @EventListener 注解的方法本地地做出异步选择,即在该方法上添加 @Async 注解,
你可以通过引用其限定符(qualifier)来选择一个能够传播上下文的 TaskExecutor。
假设有如下配置了专用任务装饰器(task decorator)的 TaskExecutor Bean 定义:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
@Configuration
public class EventAsyncExecutionConfiguration {
@Bean(name = "propagatingContextExecutor")
public TaskExecutor propagatingContextExecutor() {
SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
// decorate task execution with a decorator that supports context propagation
taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
return taskExecutor;
}
}
使用 @Async 注解和相应的限定符对事件监听器进行标注,可以实现类似的上下文传播效果:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class EmailNotificationListener {
private final Log logger = LogFactory.getLog(EmailNotificationListener.class);
@EventListener(EmailReceivedEvent.class)
@Async("propagatingContextExecutor")
public void emailReceived(EmailReceivedEvent event) {
// asynchronously process the received event
// this logging statement will contain the expected MDC entries from the propagated context
logger.info("email has been received");
}
}