🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Log系统 ## log系统中类图 在cts的log包中。 ![](https://box.kancloud.cn/2016-01-09_56911ddd5cd21.jpg) ## log系统的入口 入口类为CLog。采用的是代理模式,被代理的类是DDM内部的Log类。 ### CLog ~~~ public static class CLog { /** * The shim version of {@link Log#v(String, String)}. * * @param message The {@code String} to log */ public static void v(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.v(getClassName(2), message); } /** * The shim version of {@link Log#v(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void v(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.v(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#d(String, String)}. * * @param message The {@code String} to log */ public static void d(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.d(getClassName(2), message); } /** * The shim version of {@link Log#d(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void d(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.d(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#i(String, String)}. * * @param message The {@code String} to log */ public static void i(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.i(getClassName(2), message); } /** * The shim version of {@link Log#i(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void i(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.i(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#w(String, String)}. * * @param message The {@code String} to log */ public static void w(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.w(getClassName(2), message); } /** * The shim version of {@link Log#w(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void w(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.w(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#e(String, String)}. * * @param message The {@code String} to log */ public static void e(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.e(getClassName(2), message); } /** * The shim version of {@link Log#e(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void e(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.e(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#e(String, Throwable)}. * * @param t the {@link Throwable} to output. */ public static void e(Throwable t) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.e(getClassName(2), t); } /** * The shim version of {@link Log#logAndDisplay(LogLevel, String, String)}. * * @param logLevel the {@link LogLevel} * @param format A format string for the message to log * @param args The format string arguments */ public static void logAndDisplay(LogLevel logLevel, String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.logAndDisplay(logLevel, getClassName(2), String.format(format, args)); } /** * Return the simple classname from the {@code frame}th stack frame in the call path. * Note: this method does <emph>not</emph> check array bounds for the stack trace length. * * @param frame The index of the stack trace frame to inspect for the class name * @return The simple class name (or full-qualified if an error occurs getting a ref to the * class) for the given element of the stack trace. */ public static String getClassName(int frame) { StackTraceElement[] frames = (new Throwable()).getStackTrace(); String fullName = frames[frame].getClassName(); @SuppressWarnings("rawtypes") Class klass = null; try { klass = Class.forName(fullName); } catch (ClassNotFoundException e) { // oops; not much we can do. // Intentionally fall through so we hit the null check below } if (klass == null) { return fullName; } else { return klass.getSimpleName(); } } } ~~~ 所以CLog里的方法也是直接调用Log相应的方法。 仔细看看Log类中相应的log方法,比如Log.i()、Log.e()。他们都不约而同的转向了println方法。 ~~~ private static void println(LogLevel logLevel, String tag, String message) { if (logLevel.getPriority() >= mLevel.getPriority()) { if (sLogOutput != null) { sLogOutput.printLog(logLevel, tag, message); } else { printLog(logLevel, tag, message); } } } ~~~ 其中sLogOutput为Log类放出的接口对象,接口定义如下所示。一般情况下如果你不设置该sLogOutput值的话,它为null。Log信息的打印会直接转到System.out.println,直接在控制台输出信息。但是如果我们实现该接口,将该属性附上值,我们就可重定向log的输出,转而到我们自己定义的输出设备中。所以Cts的log系统就是利用这一点做的。 ### ILogOutput ~~~ public interface ILogOutput { /** * Sent when a log message needs to be printed. * @param logLevel The {@link LogLevel} enum representing the priority of the message. * @param tag The tag associated with the message. * @param message The message to display. */ public void printLog(LogLevel logLevel, String tag, String message); /** * Sent when a log message needs to be printed, and, if possible, displayed to the user * in a dialog box. * @param logLevel The {@link LogLevel} enum representing the priority of the message. * @param tag The tag associated with the message. * @param message The message to display. */ public void printAndPromptLog(LogLevel logLevel, String tag, String message); } private static ILogOutput sLogOutput; ~~~ 上面的ILogOutput接口只有2个方法,Cts扩展了该接口,重新定义了一个接口,继承ILogOutput接口,取名为ILeveledLogOutput。该接口主要定义输出设备类要使用的方法。 ### ILeveledLogOutput ~~~ public interface ILeveledLogOutput extends ILogOutput { /** * Initialize the log, creating any required IO resources. */ public void init() throws IOException; /** * Gets the minimum log level to display. * * @return the current {@link LogLevel} */ public LogLevel getLogLevel(); /** * Sets the minimum log level to display. * * @param logLevel the {@link LogLevel} to display */ public void setLogLevel(LogLevel logLevel); /** * Grabs a snapshot stream of the log data. * <p/> * Must not be called after {@link ILeveledLogOutput#closeLog()}. * <p/> * The returned stream is not guaranteed to have optimal performance. Callers may wish to * wrap result in a {@link BufferedInputStream}. * * @return a {@link InputStreamSource} of the log data * @throws IllegalStateException if called when log has been closed. */ public InputStreamSource getLog(); /** * Closes the log and performs any cleanup before closing, as necessary. */ public void closeLog(); /** * @return a {@link ILeveledLogOutput} */ public ILeveledLogOutput clone(); } ~~~ ILeveledLogOutput拥有2个实现类 ### FileLogger ~~~ public class FileLogger implements ILeveledLogOutput { private static final String TEMP_FILE_PREFIX = "tradefed_log_"; private static final String TEMP_FILE_SUFFIX = ".txt"; @Option(name = "log-level", description = "the minimum log level to log.") private LogLevel mLogLevel = LogLevel.DEBUG; @Option(name = "log-level-display", shortName = 'l', description = "the minimum log level to display on stdout.", importance = Importance.ALWAYS) private LogLevel mLogLevelDisplay = LogLevel.ERROR; @Option(name = "log-tag-display", description = "Always display given tags logs on stdout") private Collection<String> mLogTagsDisplay = new HashSet<String>(); @Option(name = "max-log-size", description = "maximum allowable size of tmp log data in mB.") private long mMaxLogSizeMbytes = 20; private SizeLimitedOutputStream mLogStream; /** * Adds tags to the log-tag-display list * * @param tags collection of tags to add */ void addLogTagsDisplay(Collection<String> tags) { mLogTagsDisplay.addAll(tags); } public FileLogger() { } /** * {@inheritDoc} */ @Override public void init() throws IOException { mLogStream = new SizeLimitedOutputStream(mMaxLogSizeMbytes * 1024 * 1024, TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX); } /** * Creates a new {@link FileLogger} with the same log level settings as the current object. * <p/> * Does not copy underlying log file content (ie the clone's log data will be written to a new * file.) */ @Override public ILeveledLogOutput clone() { FileLogger logger = new FileLogger(); logger.setLogLevelDisplay(mLogLevelDisplay); logger.setLogLevel(mLogLevel); logger.addLogTagsDisplay(mLogTagsDisplay); return logger; } /** * {@inheritDoc} */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { internalPrintLog(logLevel, tag, message, true /* force print to stdout */); } /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { internalPrintLog(logLevel, tag, message, false /* don't force stdout */); } /** * A version of printLog(...) which can be forced to print to stdout, even if the log level * isn't above the urgency threshold. */ private void internalPrintLog(LogLevel logLevel, String tag, String message, boolean forceStdout) { String outMessage = LogUtil.getLogFormatString(logLevel, tag, message); if (forceStdout || logLevel.getPriority() >= mLogLevelDisplay.getPriority() || mLogTagsDisplay.contains(tag)) { System.out.print(outMessage); } try { writeToLog(outMessage); } catch (IOException e) { e.printStackTrace(); } } /** * Writes given message to log. * <p/> * Exposed for unit testing. * * @param outMessage the entry to write to log * @throws IOException */ void writeToLog(String outMessage) throws IOException { if (mLogStream != null) { mLogStream.write(outMessage.getBytes()); } } /** * {@inheritDoc} */ @Override public LogLevel getLogLevel() { return mLogLevel; } /** * {@inheritDoc} */ @Override public void setLogLevel(LogLevel logLevel) { mLogLevel = logLevel; } /** * Sets the log level filtering for stdout. * * @param logLevel the minimum {@link LogLevel} to display */ void setLogLevelDisplay(LogLevel logLevel) { mLogLevelDisplay = logLevel; } /** * Gets the log level filtering for stdout. * * @return the current {@link LogLevel} */ LogLevel getLogLevelDisplay() { return mLogLevelDisplay; } /** * {@inheritDoc} */ @Override public InputStreamSource getLog() { if (mLogStream != null) { try { // create a InputStream from log file mLogStream.flush(); return new SnapshotInputStreamSource(mLogStream.getData()); } catch (IOException e) { System.err.println("Failed to get log"); e.printStackTrace(); } } return new ByteArrayInputStreamSource(new byte[0]); } /** * {@inheritDoc} */ @Override public void closeLog() { try { doCloseLog(); } catch (IOException e) { e.printStackTrace(); } } /** * Flushes stream and closes log file. * <p/> * Exposed for unit testing. * * @throws IOException */ void doCloseLog() throws IOException { SizeLimitedOutputStream stream = mLogStream; mLogStream = null; if (stream != null) { try { stream.flush(); stream.close(); } finally { stream.delete(); } } } /** * Dump the contents of the input stream to this log * * @param createInputStream * @throws IOException */ void dumpToLog(InputStream inputStream) throws IOException { if (mLogStream != null) { StreamUtil.copyStreams(inputStream, mLogStream); } } } ~~~ ### StdoutLogger ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.log; import com.android.ddmlib.Log.LogLevel; import com.android.tradefed.config.Option; import com.android.tradefed.config.Option.Importance; import com.android.tradefed.config.OptionClass; import com.android.tradefed.result.ByteArrayInputStreamSource; import com.android.tradefed.result.InputStreamSource; import java.io.IOException; /** * A {@link ILeveledLogOutput} that directs log messages to stdout. */ @OptionClass(alias = "stdout") public class StdoutLogger implements ILeveledLogOutput { @Option(name="log-level", description="minimum log level to display.", importance = Importance.ALWAYS) private LogLevel mLogLevel = LogLevel.INFO; //StdoutLogger(){} /** * {@inheritDoc} * * */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { printLog(logLevel, tag, message); } /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { LogUtil.printLog(logLevel, tag, message); } /** * {@inheritDoc} */ @Override public void setLogLevel(LogLevel logLevel) { mLogLevel = logLevel; } /** * {@inheritDoc} */ @Override public LogLevel getLogLevel() { return mLogLevel; } /** * {@inheritDoc} */ @Override public void closeLog() { // ignore } /** * {@inheritDoc} */ @Override public InputStreamSource getLog() { // not supported - return empty stream return new ByteArrayInputStreamSource(new byte[0]); } @Override public ILeveledLogOutput clone() { return new StdoutLogger(); } @Override public void init() throws IOException { // ignore } } ~~~ 又定义了一个Log注册器的接口,叫ILogRegistry。该接口也是继承ILogOutput,但是该接口中的方法明显不是一个log输出系统,而是一个注册log所要用到的方法。 ### ILogRegistry ~~~ public interface ILogRegistry extends ILogOutput { /** * Set the log level display for the global log * * @param logLevel the {@link LogLevel} to use */ public void setGlobalLogDisplayLevel(LogLevel logLevel); /** * Set the log tags to display for the global log */ public void setGlobalLogTagDisplay(Collection<String> logTagsDisplay); /** * Returns current log level display for the global log * * @return logLevel the {@link LogLevel} to use */ public LogLevel getGlobalLogDisplayLevel(); /** * Registers the logger as the instance to use for the current thread. */ public void registerLogger(ILeveledLogOutput log); /** * Unregisters the current logger in effect for the current thread. */ public void unregisterLogger(); /** * Dumps the entire contents of a {@link ILeveledLogOutput} logger to the global log. * <p/> * This is useful in scenarios where you know the logger's output won't be saved permanently, * yet you want the contents to be saved somewhere and not lost. * * @param log */ public void dumpToGlobalLog(ILeveledLogOutput log); /** * Closes and removes all logs being managed by this LogRegistry. */ public void closeAndRemoveAllLogs(); /** * Diagnosis method to dump all logs to files. */ public void dumpLogs(); } ~~~ ILogRegistry实现类,该类采用的是单例模式 ### LogRegistry ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.log; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.tradefed.config.Option; import com.android.tradefed.config.OptionClass; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.InputStreamSource; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.StreamUtil; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; /** * A {@link ILogRegistry} implementation that multiplexes and manages different loggers, * using the appropriate one based on the {@link ThreadGroup} of the thread making the call. * <p/> * Note that the registry hashes on the ThreadGroup in which a thread belongs. If a thread is * spawned with its own explicitly-supplied ThreadGroup, it will not inherit the parent thread's * logger, and thus will need to register its own logger with the LogRegistry if it wants to log * output. */ @OptionClass(alias="logreg") public class LogRegistry implements ILogRegistry { private static final String LOG_TAG = "LogRegistry"; private static LogRegistry mLogRegistry = null; private Map<ThreadGroup, ILeveledLogOutput> mLogTable = new Hashtable<ThreadGroup, ILeveledLogOutput>(); private FileLogger mGlobalLogger; /** * Package-private constructor; callers should use {@link #getLogRegistry} to get an instance of * the {@link LogRegistry}. */ LogRegistry() { try { mGlobalLogger = new FileLogger(); mGlobalLogger.init(); } catch (IOException e) { System.err.println("Failed to create global logger"); throw new IllegalStateException(e); } } /** * Get the {@link LogRegistry} instance * <p/> * * @return a {@link LogRegistry} that can be used to register, get, write to, and close logs */ public static ILogRegistry getLogRegistry() { if (mLogRegistry == null) { mLogRegistry = new LogRegistry(); } return mLogRegistry; } /** * {@inheritDoc} */ @Override public void setGlobalLogDisplayLevel(LogLevel logLevel) { mGlobalLogger.setLogLevelDisplay(logLevel); } /** * {@inheritDoc} */ @Override public void setGlobalLogTagDisplay(Collection<String> logTagsDisplay) { mGlobalLogger.addLogTagsDisplay(logTagsDisplay); } /** * {@inheritDoc} */ @Override public LogLevel getGlobalLogDisplayLevel() { return mGlobalLogger.getLogLevelDisplay(); } /** * {@inheritDoc} */ @Override public void registerLogger(ILeveledLogOutput log) { ILeveledLogOutput oldValue = mLogTable.put(getCurrentThreadGroup(), log); if (oldValue != null) { Log.e(LOG_TAG, "Registering a new logger when one already exists for this thread!"); oldValue.closeLog(); } } /** * {@inheritDoc} */ @Override public void unregisterLogger() { ThreadGroup currentThreadGroup = getCurrentThreadGroup(); if (currentThreadGroup != null) { mLogTable.remove(currentThreadGroup); } else { printLog(LogLevel.ERROR, LOG_TAG, "Unregistering when thread has no logger registered."); } } /** * {@inheritDoc} */ @Override public void dumpToGlobalLog(ILeveledLogOutput log) { InputStreamSource source = log.getLog(); try { InputStream stream = source.createInputStream(); mGlobalLogger.dumpToLog(stream); StreamUtil.close(stream); } catch (IOException e) { System.err.println("Failed to dump log"); e.printStackTrace(); } finally { source.cancel(); } } /** * Gets the current thread Group. * <p/> * Exposed so unit tests can mock * * @return the ThreadGroup that the current thread belongs to */ ThreadGroup getCurrentThreadGroup() { return Thread.currentThread().getThreadGroup(); } /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { ILeveledLogOutput log = getLogger(); LogLevel currentLogLevel = log.getLogLevel(); if (logLevel.getPriority() >= currentLogLevel.getPriority()) { log.printLog(logLevel, tag, message); } } /** * {@inheritDoc} */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { getLogger().printAndPromptLog(logLevel, tag, message); } /** * Gets the underlying logger associated with this thread. * * @return the logger for this thread, or null if one has not been registered. */ ILeveledLogOutput getLogger() { ILeveledLogOutput log = mLogTable.get(getCurrentThreadGroup()); if (log == null) { // If there's no logger set for this thread, use global logger log = mGlobalLogger; } return log; } /** * {@inheritDoc} */ @Override public void closeAndRemoveAllLogs() { Collection<ILeveledLogOutput> allLogs = mLogTable.values(); Iterator<ILeveledLogOutput> iter = allLogs.iterator(); while (iter.hasNext()) { ILeveledLogOutput log = iter.next(); log.closeLog(); iter.remove(); } saveGlobalLog(); mGlobalLogger.closeLog(); } /** * {@inheritDoc} */ private void saveGlobalLog() { InputStreamSource globalLog = mGlobalLogger.getLog(); saveLog("tradefed_global_log_", globalLog); globalLog.cancel(); } /** * Save log data to a temporary file * * @param filePrefix the file name prefix * @param logData the textual log data */ private void saveLog(String filePrefix, InputStreamSource logData) { try { File tradefedLog = FileUtil.createTempFile(filePrefix, ".txt"); FileUtil.writeToFile(logData.createInputStream(), tradefedLog); CLog.logAndDisplay(LogLevel.INFO, String.format("Saved log to %s", tradefedLog.getAbsolutePath())); } catch (IOException e) { // ignore } } /** * {@inheritDoc} */ @Override public void dumpLogs() { for (Map.Entry<ThreadGroup, ILeveledLogOutput> logEntry : mLogTable.entrySet()) { // use thread group name as file name - assume its descriptive String filePrefix = String.format("%s_log_", logEntry.getKey().getName()); InputStreamSource logSource = logEntry.getValue().getLog(); saveLog(filePrefix, logSource); logSource.cancel(); } // save global log last saveGlobalLog(); } } ~~~ ## Log系统的运行过程 当cts系统开始运行的时候,我们会将LogRegistry对象注册为Log类中sLogOutput,将DDM内部的Log输出重定向到CTS自己的Log系统中,该对象就是LogRegistry对象,但是该对象不是直接输出lgo的,它照样采用代理模式或者也可以说是装饰者模式,负责调配、管理所有的Log设备。因为cts中的允许多任务同时运行,所以管理好每一个线程的log信息,很有必要。那具体是怎么做的呢,当Cts启动的时候,将Log注册器设置为DDM内部的log重定向设备,代码如下: ![](https://box.kancloud.cn/2016-01-09_56911ddd8355d.jpg) 在initLogging()方法中,调用Log.setLogOutput方法将我们的log注册器设置成sLogOutput属性。 ~~~ public static void setLogOutput(ILogOutput logOutput) { sLogOutput = logOutput; } ~~~ 那么以后所有调用CLog打印的信息的操作都会转到我们的LogRegistry对象中相应的方法中。那么就有必要来看看LogRegistry中对ILogOutput方法的实现 ~~~ /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { ILeveledLogOutput log = getLogger(); LogLevel currentLogLevel = log.getLogLevel(); if (logLevel.getPriority() >= currentLogLevel.getPriority()) { log.printLog(logLevel, tag, message); } } /** * {@inheritDoc} */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { getLogger().printAndPromptLog(logLevel, tag, message); } ~~~ 这两个方法中,首先第一步都是要调用getLogger方法得到具体的输出设备。 ~~~ ILeveledLogOutput getLogger() { ILeveledLogOutput log = mLogTable.get(getCurrentThreadGroup()); if (log == null) { // If there's no logger set for this thread, use global logger log = mGlobalLogger; } return log; } ~~~ 由上面的代码可以看出,如果我们在Map对象中找不到log输出设备,我们就会使用全局log设备。那么就要先看看全局log器是啥,如果能从map中得到log器,那这个log器又是啥。 首先来看全局log器 ~~~ private FileLogger mGlobalLogger; ~~~ 它的定义是一个FileLogger对象:该对象是一个文件log系统,它会把CLog传入的信息在打印的同时也会保存到文件中。那么我们可以说这是个log文件。 那么如果Map不为空,我们能得到啥log器。追踪源码可以发现向map放入log器的代码存在于registerLogger方法中。那么该方法为何会将当前线程所在的线程组作为关键字放到map中。那我们就要看看谁调用了registerLogger方法。 ~~~ @Override public void registerLogger(ILeveledLogOutput log) { ILeveledLogOutput oldValue = mLogTable.put(getCurrentThreadGroup(), log); if (oldValue != null) { Log.e(LOG_TAG, "Registering a new logger when one already exists for this thread!"); oldValue.closeLog(); } } ~~~ 原来在TestInvocation类中的invoke方法中调用这个方法 ![](https://box.kancloud.cn/2016-01-09_56911dddcc2b4.jpg) 如果你看过我之前的关于cts框架分析的文章,你应该会了解这个invoke在何时被调用,在每个任务启动的时候,会创建一个单独的线程来执行这个任务,这个时候invoke会被调用。那们我们也就明白为什么会用线程所在的线程组来当做Map的关键字啦。这样以后我们就可以通过调用者所在线程组得到器log输出设备。这样就区别了每个线程的log。