I am facing an issue on propagating slf4 MDC in play framework. I actually require to pass an X-Request-Id from request through multiple threads for the purpose of slf4 logging.
I found a solution here, https://yanns.github.io/blog/2014/05/04/slf4j-mapped-diagnostic-context-mdc-with-play-framework/ for passing MDC in an asynchronous environment. But unfortunately it's not working. After putting MDC in a filter I am getting null value in controller for that MDC. Here is my code snippts.
LoggingFilters.java
@Component
public class LoggingFilters extends Filter {
private final Executor exec;
public static final String REQUEST_ID_STRING = "X-Request-Id";
private static final Logger log = LoggerFactory.getLogger(LoggingFilters.class);
@Inject
public LoggingFilters(final Materializer mat, final Executor exec) {
super(mat);
this.exec = exec;
}
@Override
public CompletionStage<Result> apply(
final Function<Http.RequestHeader, CompletionStage<Result>> nextFilter,
final Http.RequestHeader requestHeader) {
final String requestId = requestHeader.header(REQUEST_ID_STRING).orElse(UUID.randomUUID().toString());
MDC.put(REQUEST_ID_STRING, requestId);
return nextFilter
.apply(requestHeader)
.thenApplyAsync(
result -> {
MDC.remove(REQUEST_ID_STRING);
return result;
}, exec);
}
}
- Filters.java
public class Filters implements HttpFilters {
private final List<EssentialFilter> filters;
@Inject
public Filters(final LoggingFilters loggingFilter) {
filters = Lists.newArrayList(
loggingFilter.asJava());
}
@Override
public List<EssentialFilter> getFilters() {
return filters;
}
}
MDCPropagatingDispatcherConfigurator.scala
package topl.util
import akka.dispatch._
import com.typesafe.config.Config
import org.slf4j.MDC
import java.util.concurrent.TimeUnit
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.{Duration, FiniteDuration}
class MDCPropagatingDispatcherConfigurator(config: Config, prerequisites: DispatcherPrerequisites)
extends MessageDispatcherConfigurator(config, prerequisites) {
private val instance = new MDCPropagatingDispatcher(
this,
config.getString("id"),
config.getInt("throughput"),
FiniteDuration(config.getDuration("throughput-deadline-time", TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS),
configureExecutor(),
FiniteDuration(config.getDuration("shutdown-timeout", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS))
override def dispatcher(): MessageDispatcher = instance
}
class MDCPropagatingDispatcher(_configurator: MessageDispatcherConfigurator,
id: String,
throughput: Int,
throughputDeadlineTime: Duration,
executorServiceFactoryProvider: ExecutorServiceFactoryProvider,
shutdownTimeout: FiniteDuration)
extends Dispatcher(_configurator, id, throughput, throughputDeadlineTime, executorServiceFactoryProvider, shutdownTimeout) {
self =>
override def prepare(): ExecutionContext = new ExecutionContext {
// capture the MDC
val mdcContext = MDC.getCopyOfContextMap
def execute(r: Runnable) = self.execute(new Runnable {
def run() = {
// backup the callee MDC context
val oldMDCContext = MDC.getCopyOfContextMap
// Run the runnable with the captured context
setContextMap(mdcContext)
try {
r.run()
} finally {
// restore the callee MDC context
setContextMap(oldMDCContext)
}
}
})
def reportFailure(t: Throwable) = self.reportFailure(t)
}
private[this] def setContextMap(context: java.util.Map[String, String]) {
if (context == null) {
MDC.clear()
} else {
MDC.setContextMap(context)
}
}
}
Application.conf
akka {
actor {
default-dispatcher {
type = "topl.util.MDCPropagatingDispatcherConfigurator"
}
}
}
I have also some custom Execution Context for serving different purposes. I am also passing MDC context there too. For example: MediumExecutionContext
-
MediumExecutionContext.java@Component public class MediumExecutionContext extends CustomExecutionContext { public MediumExecutionContext(final ActorSystem actorSystem) { // uses a custom thread pool defined in application-default.conf super(actorSystem, "medium-dispatcher"); } @Override public ExecutionContext prepare() { final Map<String, String> mdcContext = MDC.getCopyOfContextMap(); return new ExecutionContext() { @Override public void execute(final Runnable r) { final Map<String, String> oldMDCContext = MDC.getCopyOfContextMap(); setContextMap(mdcContext); try { r.run(); } finally { setContextMap(oldMDCContext); } } @Override public ExecutionContext prepare() { return this; } @Override public void reportFailure(final Throwable t) { play.Logger.info("error occured in dispatcher"); } }; } private void setContextMap(final Map<String, String> context) { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } } } DispatchHelper.javapublic Executor getMediumExecutor() { return HttpExecution.fromThread((Executor)mediumExecutionContext); }
-
logback.xml<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %mdc{X-Request-Id:--} - %msg%n </pattern> </encoder> </appender>Controller@Slf4j @RequiredArgsConstructor @Controller @GeneralOpenApiResource public class FetchDirectoryApi extends NewAuthenticatedInterceptorStack { public CompletionStage<Result> getRootDirectories(final Request request,final String category) { log.info("Test 1"); //I am not getting the MDC context here and it'snot logging the x-request-id } }
So, could you please help me to make this work? Or, is there any different approach that I can follow to log the request_id from some nested threads?
I have got a Fix!
Solution:
I am sharing the solution here so that it helps other.
Method Interception: Instead of Filter it's better to use a Method interceptor to store the MDC. I have followed this link to extend Action.Simple for intercepting the request in play framework.
public class DefaultAction extends Action.Simple {
@Override public CompletionStage<Result> call(final Request request) { try { MDC.put("TestKey", "Test Value") return delegate.call(request); } finally { // Clean up the MDC MDC.remove("TestKey") } }}
Change for Custom Dispatchers: To propagate MDC context to different Dispatchers (which runs in different threads) I override the execute() method as follows.
public class MediumExecutionContext extends CustomExecutionContext {
public MediumExecutionContext(final ActorSystem actorSystem) { // uses a custom thread pool defined in application.conf super(actorSystem, "medium-dispatcher"); } @Override public void execute(final Runnable command) { final Map<String, String> mdcContext = MDC.getCopyOfContextMap(); final Runnable runnable = () -> { final Map<String, String> oldMdcContext = MDC.getCopyOfContextMap(); MDC.setContextMap(mdcContext); try { command.run(); } finally { MDC.setContextMap(oldMdcContext); } }; runnable.run(); }}
Now it should propagate the MDC value through the dispatchers (in different thread).