对于最新的稳定版本,请使用 Spring Framework 6.2.4! |
可观测性支持
Micrometer 定义了一个 Observation 概念,该概念在应用程序中同时启用 Metrics 和 Traces。 Metrics Support 提供了一种创建计时器、仪表或计数器的方法,用于收集有关应用程序运行时行为的统计信息。 指标可以帮助您跟踪错误率、使用模式、性能等。 跟踪提供了整个系统的整体视图,跨越了应用程序边界;您可以放大特定用户请求,并跟踪它们在应用程序中的整个完成情况。
Spring 框架检测其自身代码库的各个部分以发布观察结果,如果ObservationRegistry
已配置。
您可以了解有关在 Spring Boot 中配置可观测性基础设施的更多信息。
生成的 Observation 列表
Spring Framework 检测各种功能以实现可观察性。 如本节开头所述,观察可以生成计时器指标和/或跟踪,具体取决于配置。
观察项名称 | 描述 |
---|---|
HTTP 客户端交换所花费的时间 |
|
框架级别的 HTTP 服务器交换的处理时间 |
|
消息生成者将 JMS 消息发送到目标所花费的时间。 |
|
消息使用者以前收到的 JMS 消息的处理时间。 |
|
执行 |
观测使用 Micrometer 的官方命名约定,但 Metrics 名称将自动转换为监控系统后端首选的格式(Prometheus、Atlas、Graphite、InfluxDB 等)。 |
千分尺观察概念
如果您不熟悉千分尺观察,以下是您应该了解的概念的快速总结。
-
Observation
是应用程序中发生的事情的实际记录。这由ObservationHandler
实现来生成指标或跟踪。 -
每个观察结果都有一个对应的
ObservationContext
实现;此类型包含用于提取其元数据的所有相关信息。 对于 HTTP 服务器观察,上下文实现可以保存 HTTP 请求、HTTP 响应、处理过程中引发的任何异常等。 -
每
Observation
保留KeyValues
元数据。对于 HTTP 服务器观察,这可能是 HTTP 请求方法、HTTP 响应状态等。 此元数据由ObservationConvention
应该声明ObservationContext
他们支持。 -
KeyValues
如果KeyValue
tuple (HTTP 方法就是一个很好的例子)。 低基数值仅贡献给量度。 相反,“高基数”值是无限的(例如,HTTP 请求 URI),并且仅参与跟踪。 -
一
ObservationDocumentation
记录特定域中的所有观察结果,列出预期的键名称及其含义。
配置观测
全局配置选项位于ObservationRegistry#observationConfig()
水平。
每个 instrumented 组件将提供两个扩展点:
-
设置
ObservationRegistry
;如果未设置,则不会记录观测值,并且将为 NO-OPS -
提供自定义
ObservationConvention
更改默认观测项名称和提取的KeyValues
使用自定义 Observation 约定
让我们以 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());
}
}
If you want full control, you can implement the entire convention contract for the observation you’re interested in:
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());
}
}
You can also achieve similar goals using a custom ObservationFilter
– adding or removing key values for an observation.
Filters do not replace the default convention and are used as a post-processing component.
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;
}
}
You can configure ObservationFilter
instances on the ObservationRegistry
.
@Scheduled tasks instrumentation
An Observation is created for each execution of an @Scheduled
task.
Applications need to configure the ObservationRegistry
on the ScheduledTaskRegistrar
to enable the recording of observations.
This can be done by declaring a SchedulingConfigurer
bean that sets the observation registry:
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);
}
}
It is using the org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention
by default, backed by the ScheduledTaskObservationContext
.
You can configure a custom implementation on the ObservationRegistry
directly.
During the execution of the scheduled method, the current observation is restored in the ThreadLocal
context or the Reactor context (if the scheduled method returns a Mono
or Flux
type).
By default, the following KeyValues
are created:
Table 2. Low cardinality Keys
Name
Description
code.function
(required)
Name of Java Method
that is scheduled for execution.
code.namespace
(required)
Canonical name of the class of the bean instance that holds the scheduled method, or "ANONYMOUS"
for anonymous classes.
error
(required)
Class name of the exception thrown during the execution, or "none"
if no exception happened.
exception
(deprecated)
Duplicates the error
key and might be removed in the future.
outcome
(required)
Outcome of the method execution. Can be "SUCCESS"
, "ERROR"
or "UNKNOWN"
(if for example the operation was cancelled during execution).
JMS messaging instrumentation
Spring Framework uses the Jakarta JMS instrumentation provided by Micrometer if the io.micrometer:micrometer-jakarta9
dependency is on the classpath.
The io.micrometer.jakarta9.instrument.jms.JmsInstrumentation
instruments jakarta.jms.Session
and records the relevant observations.
This instrumentation will create 2 types of observations:
-
"jms.message.publish"
when a JMS message is sent to the broker, typically with JmsTemplate
.
-
"jms.message.process"
when a JMS message is processed by the application, typically with a MessageListener
or a @JmsListener
annotated method.
currently there is no instrumentation for "jms.message.receive"
observations as there is little value in measuring the time spent waiting for the reception of a message.
Such an integration would typically instrument MessageConsumer#receive
method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application.
By default, both observations share the same set of possible KeyValues
:
Table 3. Low cardinality Keys
Name
Description
error
Class name of the exception thrown during the messaging operation (or "none").
exception
(deprecated)
Duplicates the error
key and might be removed in the future.
messaging.destination.temporary
(required)
Whether the destination is a TemporaryQueue
or TemporaryTopic
(values: "true"
or "false"
).
messaging.operation
(required)
Name of JMS operation being performed (values: "publish"
or "process"
).
Table 4. High cardinality Keys
Name
Description
messaging.message.conversation_id
The correlation ID of the JMS message.
messaging.destination.name
The name of destination the current message was sent to.
messaging.message.id
Value used by the messaging system as an identifier for the message.
JMS message Publication instrumentation
"jms.message.publish"
observations are recorded when a JMS message is sent to the broker.
They measure the time spent sending the message and propagate the tracing information with outgoing JMS message headers.
You will need to configure the ObservationRegistry
on the JmsTemplate
to enable observations:
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");
}
}
It uses the io.micrometer.jakarta9.instrument.jms.DefaultJmsPublishObservationConvention
by default, backed by the io.micrometer.jakarta9.instrument.jms.JmsPublishObservationContext
.
Similar observations are recorded with @JmsListener
annotated methods when response messages are returned from the listener method.
JMS message Processing instrumentation
"jms.message.process"
observations are recorded when a JMS message is processed by the application.
They measure the time spent processing the message and propagate the tracing context with incoming JMS message headers.
Most applications will use the @JmsListener
annotated methods mechanism to process incoming messages.
You will need to ensure that the ObservationRegistry
is configured on the dedicated JmsListenerContainerFactory
:
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;
}
}
A default container factory is required to enable the annotation support,
but note that @JmsListener
annotations can refer to specific container factory beans for specific purposes.
In all cases, Observations are only recorded if the observation registry is configured on the container factory.
Similar observations are recorded with JmsTemplate
when messages are processed by a MessageListener
.
Such listeners are set on a MessageConsumer
within a session callback (see JmsTemplate.execute(SessionCallback<T>)
).
This observation uses the io.micrometer.jakarta9.instrument.jms.DefaultJmsProcessObservationConvention
by default, backed by the io.micrometer.jakarta9.instrument.jms.JmsProcessObservationContext
.
HTTP Server instrumentation
HTTP server exchange observations are created with the name "http.server.requests"
for Servlet and Reactive applications.
Servlet applications
Applications need to configure the org.springframework.web.filter.ServerHttpObservationFilter
Servlet filter in their application.
It uses the org.springframework.http.server.observation.DefaultServerRequestObservationConvention
by default, backed by the ServerRequestObservationContext
.
This will only record an observation as an error if the Exception
has not been handled by the web framework and has bubbled up to the Servlet filter.
Typically, all exceptions handled by Spring MVC’s @ExceptionHandler
and ProblemDetail
support will not be recorded with the observation.
You can, at any point during request processing, set the error field on the ObservationContext
yourself:
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 {
}
}
Because the instrumentation is done at the Servlet Filter level, the observation scope only covers the filters ordered after this one as well as the handling of the request.
Typically, Servlet container error handling is performed at a lower level and won’t have any active observation or span.
For this use case, a container-specific implementation is required, such as a org.apache.catalina.Valve
for Tomcat; this is outside the scope of this project.
By default, the following KeyValues
are created:
Table 5. Low cardinality Keys
Name
Description
error
(required)
Class name of the exception thrown during the exchange, or "none"
if no exception happened.
exception
(deprecated)
Duplicates the error
key and might be removed in the future.
method
(required)
Name of HTTP request method or "none"
if not a well-known method.
outcome
(required)
Outcome of the HTTP server exchange.
status
(required)
HTTP response raw status code, or "UNKNOWN"
if no response was created.
uri
(required)
URI pattern for the matching handler if available, falling back to REDIRECTION
for 3xx responses, NOT_FOUND
for 404 responses, root
for requests with no path info, and UNKNOWN
for all other requests.
Table 6. High cardinality Keys
Name
Description
http.url
(required)
HTTP request URI.
Reactive applications
Applications need to configure the WebHttpHandlerBuilder
with a MeterRegistry
to enable server instrumentation.
This can be done on the WebHttpHandlerBuilder
, as follows:
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();
}
}
It is using the org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention
by default, backed by the ServerRequestObservationContext
.
This will only record an observation as an error if the Exception
has not been handled by an application Controller.
Typically, all exceptions handled by Spring WebFlux’s @ExceptionHandler
and ProblemDetail
support will not be recorded with the observation.
You can, at any point during request processing, set the error field on the ObservationContext
yourself:
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 {
}
}
By default, the following KeyValues
are created:
Table 7. Low cardinality Keys
Name
Description
error
(required)
Class name of the exception thrown during the exchange, or "none"
if no exception happened.
exception
(deprecated)
Duplicates the error
key and might be removed in the future.
method
(required)
Name of HTTP request method or "none"
if not a well-known method.
outcome
(required)
Outcome of the HTTP server exchange.
status
(required)
HTTP response raw status code, or "UNKNOWN"
if no response was created.
uri
(required)
URI pattern for the matching handler if available, falling back to REDIRECTION
for 3xx responses, NOT_FOUND
for 404 responses, root
for requests with no path info, and UNKNOWN
for all other requests.
Table 8. High cardinality Keys
Name
Description
http.url
(required)
HTTP request URI.
HTTP Client Instrumentation
HTTP client exchange observations are created with the name "http.client.requests"
for blocking and reactive clients.
Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an ObservationRegistry
on the client.
RestTemplate
Applications must configure an ObservationRegistry
on RestTemplate
instances to enable the instrumentation; without that, observations are "no-ops".
Spring Boot will auto-configure RestTemplateBuilder
beans with the observation registry already set.
Instrumentation uses the org.springframework.http.client.observation.ClientRequestObservationConvention
by default, backed by the ClientRequestObservationContext
.
Table 9. Low cardinality Keys
Name
Description
method
(required)
Name of HTTP request method or "none"
if not a well-known method.
uri
(required)
URI template used for HTTP request, or "none"
if none was provided. Only the path part of the URI is considered.
client.name
(required)
Client name derived from the request URI host.
status
(required)
HTTP response raw status code, or "IO_ERROR"
in case of IOException
, or "CLIENT_ERROR"
if no response was received.
outcome
(required)
Outcome of the HTTP client exchange.
error
(required)
Class name of the exception thrown during the exchange, or "none"
if no exception happened.
exception
(deprecated)
Duplicates the error
key and might be removed in the future.
Table 10. High cardinality Keys
Name
Description
http.url
(required)
HTTP request URI.
RestClient
Applications must configure an ObservationRegistry
on the RestClient.Builder
to enable the instrumentation; without that, observations are "no-ops".
Instrumentation uses the org.springframework.http.client.observation.ClientRequestObservationConvention
by default, backed by the ClientRequestObservationContext
.
Table 11. Low cardinality Keys
Name
Description
method
(required)
Name of HTTP request method or "none"
if the request could not be created.
uri
(required)
URI template used for HTTP request, or "none"
if none was provided. Only the path part of the URI is considered.
client.name
(required)
Client name derived from the request URI host.
status
(required)
HTTP response raw status code, or "IO_ERROR"
in case of IOException
, or "CLIENT_ERROR"
if no response was received.
outcome
(required)
Outcome of the HTTP client exchange.
error
(required)
Class name of the exception thrown during the exchange, or "none"
if no exception happened.
exception
(deprecated)
Duplicates the error
key and might be removed in the future.
Table 12. High cardinality Keys
Name
Description
http.url
(required)
HTTP request URI.
WebClient
Applications must configure an ObservationRegistry
on the WebClient
builder to enable the instrumentation; without that, observations are "no-ops".
Spring Boot will auto-configure WebClient.Builder
beans with the observation registry already set.
Instrumentation uses the org.springframework.web.reactive.function.client.ClientRequestObservationConvention
by default, backed by the ClientRequestObservationContext
.
Table 13. Low cardinality Keys
Name
Description
method
(required)
Name of HTTP request method or "none"
if not a well-known method.
uri
(required)
URI template used for HTTP request, or "none"
if none was provided. Only the path part of the URI is considered.
client.name
(required)
Client name derived from the request URI host.
status
(required)
HTTP response raw status code, or "IO_ERROR"
in case of IOException
, or "CLIENT_ERROR"
if no response was received.
outcome
(required)
Outcome of the HTTP client exchange.
error
(required)
Class name of the exception thrown during the exchange, or "none"
if no exception happened.
exception
(deprecated)
Duplicates the error
key and might be removed in the future.
Table 14. High cardinality Keys
Name
Description
http.url
(required)
HTTP request URI.
Application Events and @EventListener
Spring Framework does not contribute Observations for @EventListener
calls,
as they don’t have the right semantics for such instrumentation.
By default, event publication and processing are done synchronously and on the same thread.
This means that during the execution of that task, the ThreadLocals and logging context will be the same as the event publisher.
If the application globally configures a custom ApplicationEventMulticaster
with a strategy that schedules event processing on different threads, this is no longer true.
All @EventListener
methods will be processed on a different thread, outside the main event publication thread.
In these cases, the Micrometer Context Propagation library can help propagate such values and better correlate the processing of the events.
The application can configure the chosen TaskExecutor
to use a ContextPropagatingTaskDecorator
that decorates tasks and propagates context.
For this to work, the io.micrometer:context-propagation
library must be present on the classpath:
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;
}
}
Similarly, if that asynchronous choice is made locally for each @EventListener
annotated method, by adding @Async
to it,
you can choose a TaskExecutor
that propagates context by referring to it by its qualifier.
Given the following TaskExecutor
bean definition, configured with the dedicated task decorator:
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;
}
}
Annotating event listeners with @Async
and the relevant qualifier will achieve similar context propagation results:
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");
}
}