Artikel singkat ini akan fokus pada bagaimana Anda dapat menggunakan MDC dalam proyek Musim Semi. Alasan untuk menulis artikel itu adalah artikel terbaru lainnya di Habré .
Kami adalah tim kecil pengembang backend, termasuk saya sendiri, yang mengerjakan proyek server aplikasi seluler untuk sebuah organisasi. Aplikasi hanya digunakan oleh karyawannya dan kami tidak memiliki beban tinggi yang signifikan. Oleh karena itu, kami memilih stack yang paling familiar untuk server: Java dan Spring Boot pada kontainer 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));
}
}
Dan, ya, kami menulis alternatif kami sendiri untuk 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);
}
}
Kelas ini dapat digunakan secara terpisah dalam kode aplikasi untuk menyetel beberapa bidang tambahan yang akan membantu di masa mendatang untuk mencari log di agregator, misalnya:
// ... - ...
try (var ignored = LoggingUtils.mdcCloseable(MdcKeys.SOME_EXT_SVC_URL, url) {
/*
MdcKeys.SOME_EXT_SVC_URL url. */
}
// ... - ...
Semua hal di atas bisa menjadi rekayasa berlebihan yang paling liar.
Saya tidak terlalu familiar dengan Reactor dan WebFlux, jadi saya pikir akan sedikit lebih sulit untuk menerapkan pendekatan serupa dengan mereka.
Lombok ; var
, Map.of
, Set.of
Dan fitur lain dari versi baru dari Java; StreamEx adalah api.
Jangan hanya mengalahkan artikel pertama tentang sumber daya.