Cas d'utilisation de Mapping Diagnostic Context et @Async

Ce court article se concentrera sur la façon dont vous pouvez utiliser MDC dans un projet Spring. La raison d'écrire cet article était un autre article récent sur Habré .






Nous sommes une petite équipe de développeurs backend, moi y compris, travaillant sur un projet de serveur d'applications mobiles pour une organisation. Les applications ne sont utilisées que par ses employés et nous n'avons pas une charge importante. Par conséquent, nous avons choisi la pile la plus familière pour le serveur: Java et Spring Boot sur les conteneurs de servlet.





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



Et, oui, nous avons écrit notre propre alternative à 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);
    }
}

      
      



Cette classe peut être utilisée séparément dans le code de l'application pour définir des champs supplémentaires qui aideront à l'avenir à rechercher des journaux dans l'agrégateur, par exemple:





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




Tout ce qui précède peut s'avérer être la sur-ingénierie la plus folle.





Je ne suis pas très familier avec Reactor et WebFlux, donc je pense qu'il serait un peu plus difficile d'appliquer une approche similaire avec eux.





Lombok ; var



, Map.of



, Set.of



Et d' autres caractéristiques des nouvelles versions de Java; StreamEx est tout le feu.





Ne battez pas strictement pour le premier article sur la ressource.








All Articles