Case of using Mapping Diagnostic Context and @Async

This short article will focus on how you can use MDC in a Spring project. The reason to write the article was another recent article on Habrรฉ .






We're a small team of backend developers, myself included, working on a mobile app server project for an organization. Applications are used only by its employees and we do not have a significant highload. Therefore, we chose the most familiar stack for the server: Java and Spring Boot on servlet containers.





- MDC. MDC? , , Kubernetes, (Graylog). logback- appender, , MDC :





logback-graylog.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <springProperty name="graylog_environment"
                    scope="context"
                    source="logging.graylog.environment"
                    defaultValue="local"/>
    <springProperty name="graylog_host"
                    scope="context"
                    source="logging.graylog.host"
                    defaultValue="127.0.0.1"/>
    <springProperty name="graylog_port"
                    scope="context"
                    source="logging.graylog.port"
                    defaultValue="12201"/>
    <springProperty name="graylog_microservice"
                    scope="context"
                    source="logging.graylog.microservice"
                    defaultValue=""/>

    <appender name="UDP_GELF"
              class="biz.paluch.logging.gelf.logback.GelfLogbackAppender">
        <host>${graylog_host}</host>
        <port>${graylog_port}</port>

        <version>1.1</version>

        <extractStackTrace>true</extractStackTrace>
        <filterStackTrace>true</filterStackTrace>

        <includeFullMdc>true</includeFullMdc>
        <additionalFields>environment=${graylog_environment},microservice=${graylog_microservice}</additionalFields>
        <additionalFieldTypes>environment=String,microservice=String</additionalFieldTypes>

        <timestampPattern>yyyy-MM-dd HH:mm:ss,SSS</timestampPattern>
        <maximumMessageSize>8192</maximumMessageSize>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="UDP_GELF"/>
        <appender-ref ref="CONSOLE"/>
    </root>

</configuration>
      
      







โ€” ( ), , Spring Cloud Sleuth (traceId spanId), , Graylog- - . ELK, , .





Security-, . MDC , . , :





@UtilityClass
public class MdcKeys {

    /**
     *  HTTP- User-Agent   .
     */
    public final String MDC_USER_AGENT = "user-agent";

    /**
     *   Authorization   .
     */
    public final String MDC_USER_TOKEN = "authorization";

    /**
     *  ,     .
     */
    public final String MDC_USER_LOGIN = "login";

    /**
     * URL,     .
     */
    public final String MDC_API_URL = "apiUrl";

    // ...    ...
}
      
      



MDC#put



, : , AuthenticationManager-. , , servlet- , "" . โ€” try-catch finally.





, , @Async



. , , , MDC , - . Spring Security. , :





/**
 * TaskExecutor,     .
 */
@Bean
@Qualifier("taskExecutor")
TaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    // ...   taskExecutor-  ...

    taskExecutor.setTaskDecorator(new AsyncTaskCustomDecorator());
    return taskExecutor;
}
      
      



, :





private static class AsyncTaskCustomDecorator implements TaskDecorator {

    @Override
    @NonNull
    public Runnable decorate(@NonNull Runnable runnable) {
        var runnableWithRestoredMDC = LoggingUtils.decorateMdcCopying(runnable);
        return new DelegatingSecurityContextRunnable(runnableWithRestoredMDC);
    }
}
      
      



, : (LoggingUtils#decorateMdcCopying) Spring Security ( SecurityContextHolder-). " " , . :





@UtilityClass
public class LoggingUtils {

    private final Set<String> COPYABLE_MDC_FIELDS = Set.of(
            MdcKeys.MDC_USER_AGENT,
            MdcKeys.MDC_USER_TOKEN,
            MdcKeys.MDC_USER_LOGIN,
            MdcKeys.MDC_API_URL,
            MdcKeys.MDC_MOBILE_FEATURE);

    /**
     *  Runnable  ,       
     *       MDC 
     *  .
     */
    public Runnable decorateMdcCopying(Runnable runnable) {
        //  ,      MDC.
        Map<String, String> mdcMap = getMdcMeaningfulMap();
        return () -> {
            //     MDC  -.
            try (var ignored = mdcCloseable(mdcMap)) {
                //   .
                runnable.run();
            }
        };
    }

    private Map<String, String> getMdcMeaningfulMap() {
        return StreamEx.of(COPYABLE_MDC_FIELDS)
                .mapToEntry(MDC::get)
                .nonNullValues()
                .toMap();
    }
  
    public MdcCloseable mdcCloseable(Map<String, String> values) {
        //   ,     singleton.
        if (MapUtils.isEmpty(values)) {
            return MdcCloseable.EMPTY;
        }

        // ,         MDC.
        var mdcMap = MapUtils.emptyIfNull(MDC.getCopyOfContextMap());
        if (MapUtils.isEmpty(mdcMap)) {
            return new MdcCloseable(values, Collections.emptyMap());
        }

        // ,      MDC
        // (     ).
        Map<String, String> original = EntryStream.of(mdcMap)
                .nonNullValues()
                .filterKeys(values::containsKey)
                .filterKeyValue((k, v) -> Objects.equals(v, mdcMap.get(k)))
                .toMap();
        return new MdcCloseable(values, original);
    }

    public MdcCloseable mdcCloseable(String key, String value) {
        return mdcCloseable(Map.of(key, value));
    }
}
      
      



And, yes, we wrote our own alternative to MDC.MDCCloseable:





/**
 *     org.slf4j.MDC.MDCCloseable :
 * <ol>
 *     <li>   ,</li>
 *     <li>   .</li>
 * </ol>
 */
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class MdcCloseable implements Closeable {

    public static final MdcCloseable EMPTY = new MdcCloseable(
            Collections.emptySet(),
            Collections.emptyMap());

    private final Set<String> values;
    private final Map<String, String> original;

    MdcCloseable(Map<String, String> values, Map<String, String> original) {
        this(values.keySet(), original);
        values.forEach(MDC::put);
    }

    @Override
    public void close() {
        //      .
        values.forEach(MDC::remove);
        //    .
        original.forEach(MDC::put);
    }
}

      
      



This class can be used separately in the application code to set some additional fields that will help in the future to search for logs in the aggregator, for example:





// ... -  ...
try (var ignored = LoggingUtils.mdcCloseable(MdcKeys.SOME_EXT_SVC_URL, url) {
    /*          
        MdcKeys.SOME_EXT_SVC_URL   url. */
}
// ... -  ...
      
      




All of the above can turn out to be the wildest over-engineering.





I'm not very familiar with Reactor and WebFlux, so I think it would be a little more difficult to apply a similar approach with them.





Lombok ; var



, Map.of



, Set.of



And other features of new versions of Java; StreamEx is all fire.





Do not beat strictly for the first article on the resource.








All Articles