Click the blue “Most Programmer” to follow me!
Add a “Star“, every day at 18:03, let’s learn technology together.
Source: juejin.cn/post/7278498472860581925
-
Pain Points -
Solution -
MDC Principle -
Logback Placeholder

Pain Points
When checking online logs, logs from multiple threads within the same Pod are interleaved, making it difficult to trace the log information corresponding to each request.
After the log collection tool collects logs from multiple Pods into the same database, the situation becomes even more chaotic.
Solution
TraceId + MDC
MDC:
https://logback.qos.ch/manual/mdc.html
-
Each time a request is made from the frontend, add the <span>X-App-Trace-Id</span>request header. The value for<span>X-App-Trace-Id</span>can be generated using 【<span>timestamp + UUID</span>】 to ensure the uniqueness of the traceId. -
The backend retrieves the value of <span>X-App-Trace-Id</span>from the TraceIdFilter:<span>String traceId = httpServletRequest.getHeader(TRACE_ID_HEADER_KEY)</span>. If the request does not carry the<span>X-App-Trace-Id</span>request header, the backend service can generate a traceId using UUID or the Snowflake algorithm. -
Put the traceId into the slf4j MDC: <span>MDC.put(MDC_TRACE_ID_KEY, traceId)</span>, and use the<span>%X{traceId}</span>placeholder in the logback pattern to print the traceId.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String traceId = httpServletRequest.getHeader(TRACE_ID_HEADER_KEY);
if (StrUtil.isBlank(traceId)) {
traceId = UUID.randomUUID().toString();
}
MDC.put(MDC_TRACE_ID_KEY, traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove(MDC_TRACE_ID_KEY);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<property name="pattern" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%thread] %logger %line [%X{traceId}] [%tid] - %msg%n"/>
Integrating Feign
When making service calls, the traceId in the MDC needs to be passed to the called service. In our project, we uniformly use Feign Client to implement HTTP remote calls between services. In the <span>Feign RequestInterceptor</span>, retrieve the traceId from the MDC and put it into the request header:<span>requestTemplate.header(TRACE_ID_HEADER_KEY, MDC.get(MDC_TRACE_ID_KEY));</span>
@Override
public void apply(RequestTemplate template) {
template.header(TRACE_ID_HEADER_KEY, MDC.get(MDC_TRACE_ID_KEY));
}
Multi-threading Adaptation
Please note that MDC as implemented by logback-classic assumes that values are placed into the MDC with moderate frequency. Also note that a child thread does not automatically inherit a copy of the mapped diagnostic context of its parent.
Before executing tasks in child threads, set the MDC content of the parent thread to the MDC of the child thread; after the child thread task is completed, clear the contents of the child thread’s MDC.
Adapting JDK ThreadPoolExecutor:
public class MdcAwareThreadPoolExecutor extends ThreadPoolExecutor {
@Override
public void execute(Runnable command) {
Map<String, String> parentThreadContextMap = MDC.getCopyOfContextMap();
super.execute(MdcTaskUtils.adaptMdcRunnable(command, parentThreadContextMap));
}
}
Adapting Spring TaskDecorator:
@Component
public class MdcAwareTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> parentThreadContextMap = MDC.getCopyOfContextMap();
return MdcTaskUtils.adaptMdcRunnable(runnable, parentThreadContextMap);
}
}
<span>MdcTaskUtils#adaptMdcRunnable()</span>: Using the decorator pattern, decorate the native <span>Runnable runnable</span> object. Before executing the native Runnable object, set the parent thread’s MDC into the child thread, and after the native Runnable object execution ends, clear the contents of the child thread’s MDC.
@Slf4j
publicabstractclass MdcTaskUtils {
public static Runnable adaptMdcRunnable(Runnable runnable, Map<String, String> parentThreadContextMap) {
return () -> {
log.debug("parentThreadContextMap: {}, currentThreadContextMap: {}", parentThreadContextMap,
MDC.getCopyOfContextMap());
if (MapUtils.isEmpty(parentThreadContextMap) || !parentThreadContextMap.containsKey(MDC_TRACE_ID_KEY)) {
log.debug("can not find a parentThreadContextMap, maybe task is fired using async or schedule task.");
MDC.put(MDC_TRACE_ID_KEY, UUID.randomUUID().toString());
} else {
MDC.put(MDC_TRACE_ID_KEY, parentThreadContextMap.get(MDC_TRACE_ID_KEY));
}
try {
runnable.run();
} finally {
MDC.remove(MDC_TRACE_ID_KEY);
}
};
}
}
Integrating Skywalking
The Skywalking official provides adaptation for logback version 1.x:<span>apm-toolkit-logback-1.x</span>, which can print the <span>skywalking traceId</span> in logback. The <span>X-App-Trace-Id</span> and <span>skywalking traceId</span> can be combined to facilitate the investigation of interface business and performance issues.
-
Select the specific implementation class for layout <span>TraceIdPatternLogbackLayout</span>. -
In the <span>logback pattern</span>, use<span>%tid</span>to print the<span>skywalking traceId</span>.
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<property name="pattern" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%thread] %logger %line [%X{traceId}] [%tid] - %msg%n"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${pattern}</pattern>
</layout>
</encoder>
</appender>
<span>TraceIdPatternLogbackLayout</span> class initializes with two <span>PatternConverter</span>:
-
tid: Uses <span>LogbackPatternConverter</span>to convert<span>%tid</span>placeholder to<span>skywalking traceId</span>. -
sw_ctx: Uses <span>LogbackSkyWalkingContextPatternConverter</span>to convert %sw_ctx placeholder to<span>skywalking context</span>.
public class TraceIdPatternLogbackLayout extends PatternLayout {
public TraceIdPatternLogbackLayout() {
}
static {
defaultConverterMap.put("tid", LogbackPatternConverter.class.getName());
defaultConverterMap.put("sw_ctx", LogbackSkyWalkingContextPatternConverter.class.getName());
}
}
<span>LogbackPatternConverter#convert()</span> method returns “<span>TID: N/A</span>“, what is going on?
public class LogbackPatternConverter extends ClassicConverter {
public LogbackPatternConverter() {
}
public String convert(ILoggingEvent iLoggingEvent) {
return "TID: N/A";
}
}
When starting the Java application, specify the java agent startup parameter <span>-javaagent:-javaagent:/opt/tools/skywalking-agent.jar. The skywalking agent</span> will proxy the <span>LogbackPatternConverter</span> class and overwrite the logic of the <span>convert()</span> method.
package org.apache.skywalking.apm.toolkit.log.logback.v1.x;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import java.lang.reflect.Method;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter;
import org.apache.skywalking.apm.toolkit.log.logback.v1.x.LogbackPatternConverter$auxiliary$pJ6Zrqzi;
publicclass LogbackPatternConverter
extends ClassicConverter
implements EnhancedInstance {
privatevolatile Object _$EnhancedClassField_ws;
publicstaticvolatile/* synthetic */ InstMethodsInter delegate$mo3but1;
privatestaticfinal/* synthetic */ Method cachedValue$oeLgRjrq$u5j8qu3;
public String convert(ILoggingEvent iLoggingEvent) {
return (String)delegate$mo3but1.intercept(this, new Object[]{iLoggingEvent}, new LogbackPatternConverter$auxiliary$pJ6Zrqzi(this, iLoggingEvent), cachedValue$oeLgRjrq$u5j8qu3);
}
private/* synthetic */ String convert$original$T8InTdln(ILoggingEvent iLoggingEvent) {
/*34*/ return"TID: N/A";
}
@Override
public void setSkyWalkingDynamicField(Object object) {
this._$EnhancedClassField_ws = object;
}
@Override
public Object getSkyWalkingDynamicField() {
returnthis._$EnhancedClassField_ws;
}
static {
ClassLoader.getSystemClassLoader().loadClass("org.apache.skywalking.apm.dependencies.net.bytebuddy.dynamic.Nexus").getMethod("initialize", Class.class, Integer.TYPE).invoke(null, LogbackPatternConverter.class, -1942176692);
cachedValue$oeLgRjrq$u5j8qu3 = LogbackPatternConverter.class.getMethod("convert", ILoggingEvent.class);
}
final/* synthetic */ String convert$original$T8InTdln$accessor$oeLgRjrq(ILoggingEvent iLoggingEvent) {
returnthis.convert$original$T8InTdln(iLoggingEvent);
}
}
MDC Principle
MDC is in the <span>slf4j-api jar</span> package. MDC is the specification of slf4j, and all operations on MDC will fall to the methods of the <span>MDCAdapter</span> interface.
public class MDC {
staticfinal String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
staticfinal String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
static MDCAdapter mdcAdapter;
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
thrownew IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
thrownew IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.put(key, val);
}
}
<span>MDCAdapter</span> is the MDC adapter interface provided by slf4j, which is the specification of MDC. Any logging framework that wants to use the MDC functionality must comply with the <span>MDCAdapter</span> interface specification and implement the methods in the interface.
// This interface abstracts the service offered by various MDC implementations.
public interface MDCAdapter {
public void put(String key, String val);
public String get(String key);
}
The Logback logging framework provides adaptation for <span>MDCAdapter</span>: <span>LogbackMDCAdapter</span>, which is implemented using ThreadLocal.
public class LogbackMDCAdapter implements MDCAdapter {
// The internal map is copied so as
// We wish to avoid unnecessarily copying of the map. To ensure
// efficient/timely copying, we have a variable keeping track of the last
// operation. A copy is necessary on 'put' or 'remove' but only if the last
// operation was a 'get'. Get operations never necessitate a copy nor
// successive 'put/remove' operations, only a get followed by a 'put/remove'
// requires copying the map.
// See http://jira.qos.ch/browse/LOGBACK-620 for the original discussion.
// We no longer use CopyOnInheritThreadLocal in order to solve LBCLASSIC-183
// Initially the contents of the thread local in parent and child threads
// reference the same map. However, as soon as a thread invokes the put()
// method, the maps diverge as they should.
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
privatestaticfinalint WRITE_OPERATION = 1;
privatestaticfinalint MAP_COPY_OPERATION = 2;
// keeps track of the last operation performed
final ThreadLocal<Integer> lastOperation = new ThreadLocal<Integer>();
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
thrownew IllegalArgumentException("key cannot be null");
}
Map<String, String> oldMap = copyOnThreadLocal.get();
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
} else {
oldMap.put(key, val);
}
}
public String get(String key) {
final Map<String, String> map = copyOnThreadLocal.get();
if ((map != null) && (key != null)) {
return map.get(key);
} else {
returnnull;
}
}
}
Logback Placeholders
<span>PatternLayout</span> class initializes common logback placeholders with corresponding Converters.
public class PatternLayout extends PatternLayoutBase<ILoggingEvent> {
publicstaticfinal Map<String, String> DEFAULT_CONVERTER_MAP = new HashMap<String, String>();
publicstaticfinal Map<String, String> CONVERTER_CLASS_TO_KEY_MAP = new HashMap<String, String>();
/**
* @deprecated replaced by DEFAULT_CONVERTER_MAP
*/
publicstaticfinal Map<String, String> defaultConverterMap = DEFAULT_CONVERTER_MAP;
publicstaticfinal String HEADER_PREFIX = "#logback.classic pattern: ";
static {
DEFAULT_CONVERTER_MAP.putAll(Parser.DEFAULT_COMPOSITE_CONVERTER_MAP);
DEFAULT_CONVERTER_MAP.put("d", DateConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("date", DateConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(DateConverter.class.getName(), "date");
DEFAULT_CONVERTER_MAP.put("r", RelativeTimeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("relative", RelativeTimeConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(RelativeTimeConverter.class.getName(), "relative");
DEFAULT_CONVERTER_MAP.put("level", LevelConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("le", LevelConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("p", LevelConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LevelConverter.class.getName(), "level");
DEFAULT_CONVERTER_MAP.put("t", ThreadConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("thread", ThreadConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(ThreadConverter.class.getName(), "thread");
DEFAULT_CONVERTER_MAP.put("lo", LoggerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("logger", LoggerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("c", LoggerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LoggerConverter.class.getName(), "logger");
DEFAULT_CONVERTER_MAP.put("m", MessageConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("msg", MessageConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("message", MessageConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(MessageConverter.class.getName(), "message");
DEFAULT_CONVERTER_MAP.put("C", ClassOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("class", ClassOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(ClassOfCallerConverter.class.getName(), "class");
DEFAULT_CONVERTER_MAP.put("M", MethodOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("method", MethodOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(MethodOfCallerConverter.class.getName(), "method");
DEFAULT_CONVERTER_MAP.put("L", LineOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("line", LineOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LineOfCallerConverter.class.getName(), "line");
DEFAULT_CONVERTER_MAP.put("F", FileOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("file", FileOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(FileOfCallerConverter.class.getName(), "file");
DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("mdc", MDCConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("ex", ThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("exception", ThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("rEx", RootCauseFirstThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("rootException", RootCauseFirstThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("throwable", ThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("xEx", ExtendedThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("xException", ExtendedThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("xThrowable", ExtendedThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("nopex", NopThrowableInformationConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("nopexception", NopThrowableInformationConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("cn", ContextNameConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("contextName", ContextNameConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(ContextNameConverter.class.getName(), "contextName");
DEFAULT_CONVERTER_MAP.put("caller", CallerDataConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(CallerDataConverter.class.getName(), "caller");
DEFAULT_CONVERTER_MAP.put("marker", MarkerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(MarkerConverter.class.getName(), "marker");
DEFAULT_CONVERTER_MAP.put("property", PropertyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("n", LineSeparatorConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("black", BlackCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("red", RedCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("green", GreenCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("yellow", YellowCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("blue", BlueCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("magenta", MagentaCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("cyan", CyanCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("white", WhiteCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("gray", GrayCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldRed", BoldRedCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldGreen", BoldGreenCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldYellow", BoldYellowCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldBlue", BoldBlueCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldMagenta", BoldMagentaCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldCyan", BoldCyanCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldWhite", BoldWhiteCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("highlight", HighlightingCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("lsn", LocalSequenceNumberConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LocalSequenceNumberConverter.class.getName(), "lsn");
DEFAULT_CONVERTER_MAP.put("prefix", PrefixCompositeConverter.class.getName());
}
}
<span>ThreadConverter</span>: Converts the <span>%thread </span> placeholder to the logger thread.
public class ThreadConverter extends ClassicConverter {
public String convert(ILoggingEvent event) {
return event.getThreadName();
}
)
In the <span>PatternLayout</span>, support for MDC is added by default, allowing <span>%X{key}</span> or <span>%mdc{key}</span> placeholders to be converted to <span>MDC.get(key)</span>.
DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("mdc", MDCConverter.class.getName());
<span>MDCConverter</span> can convert <span>%X{traceId}</span> or <span>%mdc{traceId}</span> placeholders to <span>MDC.get("traceId")</span>?
When the program starts, <span>ch.qos.logback.core.pattern.parser.Compiler#compile()</span> will parse the user-configured pattern expression to obtain the dynamic placeholders that need to be processed, such as <span>%d{yyyy-MM-dd HH:mm:ss.SSS}</span> and <span>%X{traceId}</span>. These dynamic placeholders are passed to the <span>DynamicConverter#optionList</span> field (<span>MDCConverter </span> is essentially a <span>DynamicConverter</span>), and the optionList field contains the names of the placeholders to be processed by the program, such as traceId.

When the program starts, logback initializes Convert and will call the <span>MDCConverter#start()</span> method, setting the member variable <span>private String key</span> to <span>traceId (MDCConverter#getFirstOption()</span> returns the user-configured traceId).

In the <span>MDCConverter#convert()</span> method, the traceId placeholder is converted to <span>MDC.get(key): String value = mdcPropertyMap.get(key)</span>.
public class MDCConverter extends ClassicConverter {
private String key;
private String defaultValue = "";
@Override
public void start() {
String[] keyInfo = extractDefaultReplacement(getFirstOption());
key = keyInfo[0];
if (keyInfo[1] != null) {
defaultValue = keyInfo[1];
}
super.start();
}
@Override
public String convert(ILoggingEvent event) {
Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
if (mdcPropertyMap == null) {
return defaultValue;
}
if (key == null) {
return outputMDCForAllKeys(mdcPropertyMap);
} else {
String value = mdcPropertyMap.get(key);
if (value != null) {
return value;
} else {
return defaultValue;
}
}
}
}
<span>ILoggingEvent</span> interface implementation class <span>LoggingEvent</span> adapts the <span>MDCAdapter</span>:
public class LoggingEvent implements ILoggingEvent {
public Map<String, String> getMDCPropertyMap() {
// populate mdcPropertyMap if null
if (mdcPropertyMap == null) {
MDCAdapter mdc = MDC.getMDCAdapter();
if (mdc instanceof LogbackMDCAdapter)
mdcPropertyMap = ((LogbackMDCAdapter) mdc).getPropertyMap();
else
mdcPropertyMap = mdc.getCopyOfContextMap();
}
// mdcPropertyMap still null, use emptyMap()
if (mdcPropertyMap == null)
mdcPropertyMap = Collections.emptyMap();
return mdcPropertyMap;
}
}
