Este breve artículo se centrará en cómo puede utilizar MDC en un proyecto de Spring. La razón para escribir el artículo fue otro artículo reciente sobre Habré .
Somos un pequeño equipo de desarrolladores de backend, incluido yo mismo, que trabajamos en un proyecto de servidor de aplicaciones móviles para una organización. Las aplicaciones son utilizadas solo por sus empleados y no tenemos una gran carga significativa. Por lo tanto, elegimos la pila más familiar para el servidor: Java y Spring Boot en contenedores de servlets.
- 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));
}
}
Y sí, escribimos nuestra propia alternativa a 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);
}
}
Esta clase se puede usar por separado en el código de la aplicación para establecer algunos campos adicionales que ayudarán en el futuro a buscar registros en el agregador, por ejemplo:
// ... - ...
try (var ignored = LoggingUtils.mdcCloseable(MdcKeys.SOME_EXT_SVC_URL, url) {
/*
MdcKeys.SOME_EXT_SVC_URL url. */
}
// ... - ...
Todo lo anterior puede resultar ser la sobreingeniería más salvaje.
No estoy muy familiarizado con Reactor y WebFlux, por lo que creo que sería un poco más difícil aplicar un enfoque similar con ellos.
Lombok ; var
, Map.of
, Set.of
Y otras características de las nuevas versiones de Java; StreamEx es todo fuego.
No supere estrictamente el primer artículo sobre el recurso.