Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

slf4j MDCAdapter with multi-thread-context 支持 #51

Closed
bwzhang2011 opened this issue Jun 23, 2015 · 20 comments
Closed

slf4j MDCAdapter with multi-thread-context 支持 #51

bwzhang2011 opened this issue Jun 23, 2015 · 20 comments
Assignees
Labels
❓question Further information is requested
Milestone

Comments

@bwzhang2011
Copy link

@oldratlee, 能否进一步提供multi-thread-context针对slf4j MDCAdapter的集成支持。MDCAdapterslf4j spi的定义, logback以及log4j2等均提供了实现。

但个人感觉里面的跨线程上下文传递应该没有multi-thread-context考虑得那么完备(也能否帮忙看一下LOG4J2内部的实现能否满足multi-thread-context实现的那样),鉴于日志场景中许多事基于slf4j的(不想MDC的地方调用了一次,然后又调用一次multi-thread-contextthreadLocal方式),看能否提供一个这样的替代实现(或者通过java instrument api替代掉原有的MDCAdapter实现--可由multi-thread-context实现)。这个实现能缺省提供则有保障(个人只是有这个思路)

@bwzhang2011
Copy link
Author

@oldratlee,是否考虑对multi-thread-context的一些使用增强一下。类似于slf4jMDC类,大部分类似于MAP的操作,不需要关心threadLocal的操作(如创建、初始化等,这个操作由MdcAdapter来完成)。

@oldratlee
Copy link
Member

你说的sl4j是指slf4j 是吧?拼写错误?

鉴于日志场景中许多事基于sl4j的(不想MDC的地方调用了一次,然后又调用一次multi-thread-contextthreadLocal方式),看能否提供一个这样的替代实现(或者通过java instrument api替代掉原有的MDCAdapter实现--可由multi-thread-context实现)

从『MDC的地方调用了一次,然后又调用一次multi-thread-contextthreadLocal』的方式 到 提供 MDCAdapter SPI的实现,是把前一种方式中『两次设置』的逻辑 在MDCAdapter SPI中实现。

提供一个MDCAdapter SPI实现。

可以Run的代码说明在 #49 (comment)

思路OK的。

你了解一下如何开发MDCAdapter SPI,然后给个实现一个不?求贡献代码。如何? 😸 @bwzhang2011

PS: 最近比较忙没有时间去细看 MDCAdapter SPI的用法。

@bwzhang2011
Copy link
Author

@oldratlee sorry,那是笔误。

就使用而言,

//省略
ThreadContext.put(TRACE_ID, TRACE_ID_VALUE);
mtc.get().put(TRACE_ID, TRACE_ID_VALUE);

上述需要你写两步,一步是调用ThreadContext下一步是使用mtc,但都是同样上下文变量的处理。所以这样的集成实际价值不大(也就是要么以MDC的处理方式为主,要么以MTC为主)

MDC中也集成了threadlocal的使用,但不清楚是否达到了MTC的效果。MTC对于跨现场的处理中有restore这样的方式(但貌似MDC的实现,即MDCAdapter关联的部分又没有,就不确定是否能达到等同的传递线程变量的效果了)

@bwzhang2011 bwzhang2011 changed the title sl4j MDCAdapter with multi-thread-context 支持 slf4j MDCAdapter with multi-thread-context 支持 Jun 23, 2015
@oldratlee
Copy link
Member

MDC中也集成了threadlocal的使用,但不清楚是否达到了MTC的效果

ThreadLocal不能解决到Thread Pool线程的Context,MTC解决这个问题,本身就是继承ThreadLocal实现的。

另外,看了一下,slf4jMDC实现类

  static {
    try {
      mdcAdapter = StaticMDCBinder.SINGLETON.getMDCA();
    } catch (NoClassDefFoundError ncde) {
      mdcAdapter = new NOPMDCAdapter();
      String msg = ncde.getMessage();
      if (msg != null && msg.indexOf("StaticMDCBinder") != -1) {
        Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
        Util.report("Defaulting to no-operation MDCAdapter implementation.");
        Util
            .report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
      } else {
        throw ncde;
      }
    } catch (Exception e) {
      // we should never get here
      Util.report("MDC binding unsuccessful.", e);
    }
  }

slf4j加载具体实现StaticMDCBinder类,是通过静态类名加载的,如 slf4-Log4j中包含的StaticMDCBinder类如下:

public class StaticMDCBinder {


  /**
   * The unique instance of this class.
   */
  public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();

  private StaticMDCBinder() {
  }

  /**
   * Currently this method always returns an instance of 
   * {@link StaticMDCBinder}.
   */
  public MDCAdapter getMDCA() {
     return new Log4jMDCAdapter();
  }

  public String  getMDCAdapterClassStr() {
    return Log4jMDCAdapter.class.getName();
  }
}

写死了要加载的Log4jMDCAdapter类,所以 不能换Log4jMDCAdapter的实现,以加上MTC相关的逻辑。目前的slf4jMDC设计是如此。

如果slf4j可以设置Log4jMDCAdapter的实现,则与MTC集成的Log4jMDCAdapter实现如下:

package com.alibaba.mtc;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.slf4j.spi.MDCAdapter;

public class Log4jMDCAdapter implements MDCAdapter {
  // =================== MTC ===========================
  static MtContextThreadLocal<Map<String, String>> mtc = new MtContextThreadLocal<Map<String, String>>() {
    @Override
    protected void beforeExecute() {
      final Map<String, String> log4j2Context = get();
      for (Map.Entry<String, String> entry : log4j2Context.entrySet()) {
        org.apache.log4j.MDC.put(entry.getKey(), entry.getValue());
      }
    }

    @Override
    protected void afterExecute() {
      org.apache.log4j.MDC.clear();
    }

    @Override
    protected Map<String, String> initialValue() {
      return new HashMap<String, String>();
    }
  };

  public void clear() {
    Map map = org.apache.log4j.MDC.getContext();
    if (map != null) {
      map.clear();
    }

    // =================== MTC ===========================
    mtc.get().clear();
  }

  public String get(String key) {
    return (String) org.apache.log4j.MDC.get(key);
  }

  public void put(String key, String val) {
    org.apache.log4j.MDC.put(key, val);

    // =================== MTC ===========================
    mtc.get().put(key, val);
  }

  public void remove(String key) {
    org.apache.log4j.MDC.remove(key);

    // =================== MTC ===========================
    mtc.get().remove(key);
  }

  public Map getCopyOfContextMap() {
    Map old = org.apache.log4j.MDC.getContext();
    if(old != null) {
      return new HashMap(old);
    } else {
      return null;
    }
  }

  public void setContextMap(Map contextMap) {
    Map old = org.apache.log4j.MDC.getContext();
    if(old == null) {
      Iterator entrySetIterator = contextMap.entrySet().iterator();
      while(entrySetIterator.hasNext()) {
        Map.Entry mapEntry = (Map.Entry) entrySetIterator.next();
        org.apache.log4j.MDC.put((String) mapEntry.getKey(), mapEntry.getValue());

        // =================== MTC ===========================
        mtc.get().put((String) mapEntry.getKey(), (String) mapEntry.getValue());
      }
    } else {
      old.clear();
      old.putAll(contextMap);

      // =================== MTC ===========================
      mtc.get().clear();
      mtc.get().putAll(contextMap);
    }
  }
}

@oldratlee
Copy link
Member

写死了要加载的Log4jMDCAdapter类,所以 不能换Log4jMDCAdapter的实现,以加上MTC相关的逻辑。目前的slf4j的MDC设计是如此。

要加强slf4j的设计,以支持 自动定义MDCAdapter,这个估计,比较慢,并且需要和slf4j的同学聊。

如果跨ThreadPool线程传递LogContext,你现在有需要,推荐先按之前 可以Run方案做了,后面再跟进slf4j的问题吧。

PS:

可以hack的方式把 Log4jMDCAdapterorg.slf4j.MDC#mdcAdapter)改成自己写的那个类,如走反射。

@bwzhang2011
Copy link
Author

@oldratlee, 的确。对了,鼎哥能否确定一个事情:就是log4j2的threadContext也有类似的提示:

However, as discussed in the Executors class and in other cases where thread pooling is utilized, the ThreadContext may not always be automatically passed to worker threads. In those cases the pooling mechanism should provide a means for doing so. The getContext() and cloneStack() methods can be used to obtain copies of the Map and Stack respectively.

它上面有一段有暗示但没有提示的话:

In those cases the pooling mechanism should provide a means for doing so.

但是没有说明the pooling merchanisom提供何种手段。

那就等好消息了,我后续花一些时间按照Run的方式来写。日志还是沿用MDC.put但提取数据则是从mtc中提取,这样是否可行。

@bwzhang2011
Copy link
Author

@oldratlee, 我使用log4j2对应的MDCAdapter实现。感觉跨线程(包括executorService)也是可以的,但不清楚什么时候是无效的,譬如上述文档中的may not be always be passed to worker thread. 作为建议还是希望工程自身提供一个近似MDC的帮助类,这样我可以不去管mtc的创建,只需要与其进行操作即可。

@oldratlee
Copy link
Member

鼎哥能否确定一个事情:就是log4j2的threadContext也有类似的提示 ...

我使用log4j2对应的MDCAdapter实现。感觉跨线程(包括executorService)也是可以的,但不清楚什么时候是无效的,譬如上述文档中的may not be always be passed to worker thread.

无效时候,应该就是MTC要解决问题所对应的情况。

PS: 这是不停地给我派活儿啊 😓 😄

@bwzhang2011
Copy link
Author

@oldratlee,这不是我的本意啊,不过你也是大牛了。俗话说得好送佛送到西,我刚才表述的想必你也能理解了。如果能进一步的话,至少 MTC可以作为MDC的非官方的缺省实现,既可以满足日志需求也可以满足上下文传递所需要的其他场景。之所以要用MDC的日志,是因为我们的平台有做日志归集的需求,需要通过类似syslog appender这样的流将本地的日志上传到远端比如FLUME。而改动应用最小的又能现有结合比较好的就只有日志LOGGER(而slf4j则当前用得比较广泛)。以MDC来比较MTC的一些测试类,当然MDC更直观(里面是一些静态方法,直接操作就OK了),想必这也是MTC可以进一步可便利化的地方,拜托了。

@oldratlee
Copy link
Member

oldratlee commented Jun 23, 2015

客气了~

目前slf4j目前不能指定MDCAdapter的实现,一个做法把log4j-slf4j-impl实现中的MDCAdapter实现改了,加上multi-thread-context的逻辑。

这样的解法,使用者 需要 使用改写了的log4j-slf4j-impl的依赖。

另外,从你的说明来看,传递multi-thread-context对应用代码是透明的(不期望应用代码使用MtContextRunnable或是MtContextCallable),需要Java的启动参数加上:

  • -Xbootclasspath/a:/path/to/multithread.context-1.x.x.jar
  • -javaagent:/path/to/multithread.context-1.x.x.jar

这点详见文档使用Java Agent来修饰JDK线程池实现类

这点往往需要定制的容器,这样方便大家统一都加上上面的Java的启动参数。在淘宝应用容器是使用统一定制的。

上面2点是预期的不? @bwzhang2011 现在我也没有想到更多好的方法。

你用的是log4j2,改一个log4j-slf4j-impl的实现给你,如何?

PS:

改一个slf4j实现这样的做法不好 😓 看起来slf4jMDCcallback(就像multi-thread-context提供callback)让业务可以添加自己逻辑是比较正规的做法。

PPS:

涉及统一平台要做的事为了应用使用透明,都很麻烦。

是因为我们的平台有做日志归集的需求,需要通过类似syslog appender这样的流将本地的日志上传到远端比如FLUME

平台是给其它的应用使用的,是吧?

@oldratlee
Copy link
Member

@bwzhang2011 log4j-slf4j-impl实现中的MDCAdapter实现修改了,支持MTC https://github.com/oldratlee/log4j-slf4j-impl-patch-mtc ,可以测试一下。

改了这个实现,后续的跟进 你了解一下实现,自己来吧 😄

PS:

log4j-slf4j-impl-patch-mtc工程的第一个提交的代码来自log4j2的官方库 http://git-wip-us.apache.org/repos/asf/logging-log4j2.git 2.3版本的log4j-slf4j-impl模块。

@bwzhang2011
Copy link
Author

@oldratlee, 非常感谢持续的改进和推动。如有问题或其他建议,再在那个patch上跟进和沟通。

@oldratlee oldratlee added the ❓question Further information is requested label Jun 24, 2015
@oldratlee oldratlee self-assigned this Jun 24, 2015
@oldratlee oldratlee modified the milestone: 1.3.0 Jun 24, 2015
@oldratlee
Copy link
Member

@bwzhang2011 用起来了?可以反馈一下问题解决的如何 :)

@bwzhang2011
Copy link
Author

@oldratlee, 可以用起来。感觉跟直接用MDC差不不大,能否直接替代掉ThreadContext中的ThreadContextMap的实现。

@oldratlee
Copy link
Member

整个过程用起来还是有些麻烦,给力!:+1:

直接替代掉...的实现

直接改log4j2的实现,而不是改slf4j-log4j-impl,是吧?

这样避免直接用log4j的代码 有问题。

@bwzhang2011
Copy link
Author

@oldratlee, 抱歉昨天忘记看回复了。是的,诚如你说的。如果能在实现中替代掉应该是比较直接的方式,可否增加mtc-extension的工程,如果可如此替代的话。以log4j2中依赖 https://github.com/apache/logging-log4j2/blob/master/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java ;可否通过将mtc的threadLocal移植到该了中,然后通过agent类来进行替代。

@bwzhang2011
Copy link
Author

@oldratlee, 不知道上述想法是否可行,还是会存在问题。如果adapter中实现的部分均是靠threadContextMap的threadLocal关联的map来操作,若替代成MTC的threadlocal是否能解决问题。可避免新的工程,不知是否可行。

@bwzhang2011
Copy link
Author

@oldratlee, 我再仔细REVIEW了LOG4J2中的ThreaContext代码。其中初始化的部分:

final String threadContextMapName = managerProps.getStringProperty(THREAD_CONTEXT_KEY);
        final ClassLoader cl = ProviderUtil.findClassLoader();
        if (threadContextMapName != null) {
            try {
                final Class<?> clazz = cl.loadClass(threadContextMapName);
                if (ThreadContextMap.class.isAssignableFrom(clazz)) {
                    contextMap = (ThreadContextMap) clazz.newInstance();
                }
            } catch (final ClassNotFoundException cnfe) {
                LOGGER.error("Unable to locate configured ThreadContextMap {}", threadContextMapName);
            } catch (final Exception ex) {
                LOGGER.error("Unable to create configured ThreadContextMap {}", threadContextMapName, ex);
            }
        }

也就是可以设置一个机遇MTC的ThreadContextMap实现即可——由应用来决定是否(只是面向LOG4J2的)。现在我遇到一个问题就是初始化的部分,即LOG4J2的缺省实现为:

new InheritableThreadLocal<Map<String, String>>() {
                @Override
                protected Map<String, String> childValue(final Map<String, String> parentValue) {
                    return parentValue != null && isMapEnabled //
                            ? Collections.unmodifiableMap(new HashMap<String, String>(parentValue)) //
                            : null;
                }
            };

上述是在childValue中构建一个map。但看过你在patch工程部分中的Log4jMDCAdapter的部分

 MtContextThreadLocal<Map<String, String>> log4j2Context = new MtContextThreadLocal<Map<String, String>>() {
        @Override
        protected void beforeExecute() {
            final Map<String, String> log4j2Context = get();
            for (Map.Entry<String, String> entry : log4j2Context.entrySet()) {
                ThreadContext.put(entry.getKey(), entry.getValue());
            }
        }

        @Override
        protected void afterExecute() {
            ThreadContext.clearAll();
        }

        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<String, String>();
        }
    };

貌似没有去覆盖childValue的部分。我想确认的是如果重新构建一个基于MTC的localMap初始化的部分该如何编写。

@bwzhang2011
Copy link
Author

@oldratlee,我到那个patch上提一个issue了:https://github.com/oldratlee/log4j-slf4j-impl-patch-mtc/issues/1 看能否提供一个你的官方实现。

@oldratlee
Copy link
Member

oldratlee commented Jul 10, 2015

不好意思,现在才回复你。

THREAD_CONTEXT_KEY的路径指向你自己实现的threadContextMap即可,这样就做到了拔插。

按你的思路,实现了 https://github.com/oldratlee/log4j2-mtc-thread-context-map

把这个依赖放到项目中,即可开启 Log4j2MtcThreadContextMap

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
❓question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants