You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2019/08/14 06:31:29 UTC

[skywalking] branch master updated: Enhance agent logger. (#3250)

This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/master by this push:
     new ffe1df5  Enhance agent logger. (#3250)
ffe1df5 is described below

commit ffe1df579f26e3822b3def3e5620eca3f267442e
Author: Alvin <32...@qq.com>
AuthorDate: Wed Aug 14 14:31:24 2019 +0800

    Enhance agent logger. (#3250)
    
    * Add pattern logger to replace the easy logger.
    * Doc update
    * Support console log
---
 .../apm/util/PlaceholderConfigurerSupport.java     |   1 +
 .../skywalking/apm/agent/core/conf/Config.java     |  23 +++
 .../apm/agent/core/logging/api/LogManager.java     |   4 +-
 .../apm/agent/core/logging/core/Converter.java}    |  14 +-
 .../apm/agent/core/logging/core/LogEvent.java      |  72 ++++++++
 .../apm/agent/core/logging/core/LogOutput.java}    |  12 +-
 .../apm/agent/core/logging/core/Parser.java        | 195 +++++++++++++++++++++
 ...asyLogResolver.java => PatternLogResolver.java} |  13 +-
 .../core/{EasyLogger.java => PatternLogger.java}   |  99 +++++++----
 .../apm/agent/core/logging/core/WriterFactory.java |   6 +-
 .../logging/core/coverts/AgentNameConverter.java}  |  18 +-
 .../core/logging/core/coverts/ClassConverter.java} |  19 +-
 .../DateConverter.java}                            |  24 +--
 .../core/logging/core/coverts/LevelConverter.java} |  17 +-
 .../LiteralConverter.java}                         |  24 +--
 .../logging/core/coverts/MessageConverter.java}    |  17 +-
 .../logging/core/coverts/ThreadConverter.java}     |  17 +-
 .../logging/core/coverts/ThrowableConverter.java   |  51 ++++++
 ...solverTest.java => PatternLogResolverTest.java} |  10 +-
 ...{EasyLoggerTest.java => PatternLoggerTest.java} |  57 ++++--
 .../agent/core/logging/core/WriterFactoryTest.java |  69 ++++++++
 docs/en/setup/service-agent/java-agent/README.md   |   2 +
 22 files changed, 627 insertions(+), 137 deletions(-)

diff --git a/apm-commons/apm-util/src/main/java/org/apache/skywalking/apm/util/PlaceholderConfigurerSupport.java b/apm-commons/apm-util/src/main/java/org/apache/skywalking/apm/util/PlaceholderConfigurerSupport.java
index 493d6b8..7f96b25 100644
--- a/apm-commons/apm-util/src/main/java/org/apache/skywalking/apm/util/PlaceholderConfigurerSupport.java
+++ b/apm-commons/apm-util/src/main/java/org/apache/skywalking/apm/util/PlaceholderConfigurerSupport.java
@@ -31,4 +31,5 @@ public class PlaceholderConfigurerSupport {
 
     /** Default value separator: {@value} */
     public static final String DEFAULT_VALUE_SEPARATOR = ":";
+
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java
index 145f1d2..d5cb768 100755
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java
@@ -21,6 +21,7 @@ package org.apache.skywalking.apm.agent.core.conf;
 
 import org.apache.skywalking.apm.agent.core.context.trace.TraceSegment;
 import org.apache.skywalking.apm.agent.core.logging.core.LogLevel;
+import org.apache.skywalking.apm.agent.core.logging.core.LogOutput;
 import org.apache.skywalking.apm.agent.core.logging.core.WriterFactory;
 
 import java.util.HashMap;
@@ -169,6 +170,28 @@ public class Config {
          * The log level. Default is debug.
          */
         public static LogLevel LEVEL = LogLevel.DEBUG;
+
+        /**
+         * The log output. Default is FILE.
+         */
+        public static LogOutput OUTPUT = LogOutput.FILE;
+
+        /**
+         * The log patten. Default is "%level %timestamp %thread %class : %msg %throwable".
+         * Each conversion specifiers starts with a percent sign '%' and fis followed by conversion word.
+         * There are some default conversion specifiers:
+         * %thread = ThreadName
+         * %level = LogLevel  {@link LogLevel}
+         * %timestamp = The now() who format is 'yyyy-MM-dd HH:mm:ss:SSS'
+         * %class = SimpleName of TargetClass
+         * %msg = Message of user input
+         * %throwable = Throwable of user input
+         * %agent_name = ServiceName of Agent {@link Agent#SERVICE_NAME}
+         *
+         * @see org.apache.skywalking.apm.agent.core.logging.core.PatternLogger#DEFAULT_CONVERTER_MAP
+         *
+         */
+        public static String PATTERN = "%level %timestamp %thread %class : %msg %throwable";
     }
 
     public static class Plugin {
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/api/LogManager.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/api/LogManager.java
index 3f3b2e8..a8afb25 100644
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/api/LogManager.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/api/LogManager.java
@@ -19,7 +19,7 @@
 
 package org.apache.skywalking.apm.agent.core.logging.api;
 
-import org.apache.skywalking.apm.agent.core.logging.core.EasyLogResolver;
+import org.apache.skywalking.apm.agent.core.logging.core.PatternLogResolver;
 
 /**
  * LogManager is the {@link LogResolver} implementation manager. By using {@link LogResolver}, {@link
@@ -34,7 +34,7 @@ import org.apache.skywalking.apm.agent.core.logging.core.EasyLogResolver;
  * <p> Created by xin on 2016/11/10.
  */
 public class LogManager {
-    private static LogResolver RESOLVER = new EasyLogResolver();
+    private static LogResolver RESOLVER = new PatternLogResolver();
 
     public static void setLogResolver(LogResolver resolver) {
         LogManager.RESOLVER = resolver;
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/Converter.java
similarity index 75%
copy from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/Converter.java
index 5e4bea4..0561226 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/Converter.java
@@ -19,15 +19,13 @@
 
 package org.apache.skywalking.apm.agent.core.logging.core;
 
-import org.junit.Assert;
-import org.junit.Test;
 
 /**
- * Created by wusheng on 2017/2/28.
+ * The Converter, It is used to convert the LogEvent to the String.
+ * @author alvin
  */
-public class EasyLogResolverTest {
-    @Test
-    public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
-    }
+public interface Converter {
+
+    String convert(LogEvent logEvent);
+
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/LogEvent.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/LogEvent.java
new file mode 100644
index 0000000..7783134
--- /dev/null
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/LogEvent.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.apm.agent.core.logging.core;
+
+/**
+ * The representation of logging events. This instance is pass around to the List of Converter.
+ *
+ * @author alvin
+ */
+public class LogEvent {
+
+    public LogEvent(LogLevel level, String message, Throwable throwable, String targetClass) {
+        this.level = level;
+        this.message = message;
+        this.throwable = throwable;
+        this.targetClass = targetClass;
+    }
+
+    private LogLevel level;
+    private String message;
+    private Throwable throwable;
+    private String targetClass;
+
+    public String getTargetClass() {
+        return targetClass;
+    }
+
+    public void setTargetClass(String targetClass) {
+        this.targetClass = targetClass;
+    }
+
+    public LogLevel getLevel() {
+        return level;
+    }
+
+    public void setLevel(LogLevel level) {
+        this.level = level;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public Throwable getThrowable() {
+        return throwable;
+    }
+
+    public void setThrowable(Throwable throwable) {
+        this.throwable = throwable;
+    }
+}
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/LogOutput.java
similarity index 75%
copy from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/LogOutput.java
index 5e4bea4..556654c 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/LogOutput.java
@@ -19,15 +19,9 @@
 
 package org.apache.skywalking.apm.agent.core.logging.core;
 
-import org.junit.Assert;
-import org.junit.Test;
-
 /**
- * Created by wusheng on 2017/2/28.
+ * @author alvin
  */
-public class EasyLogResolverTest {
-    @Test
-    public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
-    }
+public enum LogOutput {
+    FILE, CONSOLE
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/Parser.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/Parser.java
new file mode 100644
index 0000000..743364e
--- /dev/null
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/Parser.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.apm.agent.core.logging.core;
+
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.LiteralConverter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parser of LogPattern. It is used to parse a pattern to the List of Converter.
+ * @author alvin
+ */
+public class Parser {
+    private final Map<String, Class<? extends Converter>> convertMaps;
+
+    enum State {
+        LITERAL_STATE, KEYWORD_STATE
+    }
+
+
+    public static final char ESCAPE_CHAR = '\\';
+    public static final char PERCENT_CHAR = '%';
+
+    private final String pattern;
+    private final int patternLength;
+    private int pointer = 0;
+    private State state = State.LITERAL_STATE;
+
+
+    public Parser(String pattern, Map<String, Class<? extends Converter>> convertMaps) {
+        if (pattern == null || pattern.length() == 0) {
+            throw new IllegalArgumentException("null or empty pattern string not allowed");
+        }
+        this.convertMaps = convertMaps;
+        this.pattern = pattern;
+        this.patternLength = pattern.length();
+    }
+
+    public List<Converter> parse() {
+        List<Converter> patternConverters = new ArrayList<Converter>();
+        StringBuilder buf = new StringBuilder();
+        while (pointer < patternLength) {
+            char c = pattern.charAt(pointer);
+            pointer++;
+            switch (state) {
+                case LITERAL_STATE:
+                    handleLiteralState(c, buf, patternConverters);
+                    break;
+                case KEYWORD_STATE:
+                    handleKeywordState(c, buf, patternConverters);
+                    break;
+                default:
+            }
+        }
+
+        switch (state) {
+            case LITERAL_STATE:
+                addConverter(buf, patternConverters, LiteralConverter.class);
+                break;
+            case KEYWORD_STATE:
+                addConverterWithKeyword(buf, patternConverters);
+                break;
+            default:
+        }
+        return combineLiteral(patternConverters);
+    }
+
+    private List<Converter> combineLiteral(List<Converter> patternConverters) {
+        List<Converter> converterList = new ArrayList<Converter>();
+        StringBuilder stringBuilder = new StringBuilder();
+        for (Converter patternConverter : patternConverters) {
+            if (patternConverter instanceof LiteralConverter) {
+                stringBuilder.append(patternConverter.convert(null));
+            } else {
+                if (stringBuilder.length() > 0) {
+                    converterList.add(new LiteralConverter(stringBuilder.toString()));
+                    stringBuilder.setLength(0);
+                }
+                converterList.add(patternConverter);
+            }
+        }
+        return converterList;
+    }
+
+    private void handleKeywordState(char c, StringBuilder buf, List<Converter> patternConverters) {
+        if (Character.isJavaIdentifierPart(c)) {
+            buf.append(c);
+        } else if (c == PERCENT_CHAR) {
+            addConverterWithKeyword(buf, patternConverters);
+        } else {
+            addConverterWithKeyword(buf, patternConverters);
+            if (c == ESCAPE_CHAR) {
+                escape("%", buf);
+            } else {
+                buf.append(c);
+            }
+            state = State.LITERAL_STATE;
+        }
+    }
+
+    private void addConverterWithKeyword(StringBuilder buf, List<Converter> patternConverters) {
+        String keyword = buf.toString();
+        if (convertMaps.containsKey(keyword)) {
+            addConverter(buf, patternConverters, convertMaps.get(keyword));
+        } else {
+            buf.insert(0, "%");
+            addConverter(buf, patternConverters, LiteralConverter.class);
+        }
+    }
+
+    private void handleLiteralState(char c, StringBuilder buf, List<Converter> patternConverters) {
+        switch (c) {
+            case ESCAPE_CHAR:
+                escape("%", buf);
+                break;
+            case PERCENT_CHAR:
+                addConverter(buf, patternConverters, LiteralConverter.class);
+                state = State.KEYWORD_STATE;
+                break;
+            default:
+                buf.append(c);
+        }
+
+    }
+
+    private void escape(String escapeChars, StringBuilder buf) {
+        if (pointer < patternLength) {
+            char next = pattern.charAt(pointer++);
+            escape(escapeChars, buf, next);
+        }
+    }
+
+    private void addConverter(StringBuilder buf, List<Converter> patternConverters, Class<? extends Converter> aClass) {
+        if (buf.length() > 0) {
+            String result = buf.toString();
+            if (LiteralConverter.class.equals(aClass)) {
+                patternConverters.add(new LiteralConverter(result));
+            } else {
+                try {
+                    patternConverters.add(aClass.newInstance());
+                } catch (Exception e) {
+                    throw new IllegalStateException("Create Converter error. Class: " + aClass, e);
+                }
+            }
+            buf.setLength(0);
+        }
+    }
+
+    private void escape(String escapeChars, StringBuilder buf, char next) {
+        if (escapeChars.indexOf(next) >= 0) {
+            buf.append(next);
+        } else {
+            switch (next) {
+                case '_':
+                    // the \_ sequence is swallowed
+                    break;
+                case '\\':
+                    buf.append(next);
+                    break;
+                case 't':
+                    buf.append('\t');
+                    break;
+                case 'r':
+                    buf.append('\r');
+                    break;
+                case 'n':
+                    buf.append('\n');
+                    break;
+                default:
+                    throw new IllegalArgumentException("Illegal char " + next + ". It not allowed as escape characters.");
+            }
+        }
+    }
+
+
+}
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogResolver.java
similarity index 77%
copy from apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogResolver.java
index 238cd56..328cc51 100644
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogResolver.java
@@ -19,19 +19,22 @@
 
 package org.apache.skywalking.apm.agent.core.logging.core;
 
+import org.apache.skywalking.apm.agent.core.conf.Config;
 import org.apache.skywalking.apm.agent.core.logging.api.ILog;
 import org.apache.skywalking.apm.agent.core.logging.api.LogResolver;
 
 /**
- * Created by wusheng on 2016/11/26.
+ * @author alvin
  */
-public class EasyLogResolver implements LogResolver {
+public class PatternLogResolver implements LogResolver {
+
     @Override
     public ILog getLogger(Class<?> clazz) {
-        return new EasyLogger(clazz);
+        return new PatternLogger(clazz, Config.Logging.PATTERN);
     }
 
-    @Override public ILog getLogger(String clazz) {
-        return new EasyLogger(clazz);
+    @Override
+    public ILog getLogger(String clazz) {
+        return new PatternLogger(clazz, Config.Logging.PATTERN);
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogger.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogger.java
similarity index 60%
rename from apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogger.java
rename to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogger.java
index c548b9a..a03341c 100644
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogger.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogger.java
@@ -16,36 +16,75 @@
  *
  */
 
-
 package org.apache.skywalking.apm.agent.core.logging.core;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.regex.Matcher;
 import org.apache.skywalking.apm.agent.core.conf.Config;
-import org.apache.skywalking.apm.agent.core.conf.Constants;
 import org.apache.skywalking.apm.agent.core.logging.api.ILog;
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.AgentNameConverter;
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.ClassConverter;
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.DateConverter;
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.LevelConverter;
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.MessageConverter;
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.ThreadConverter;
+import org.apache.skywalking.apm.agent.core.logging.core.coverts.ThrowableConverter;
 import org.apache.skywalking.apm.util.StringUtil;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+
 /**
- * The <code>EasyLogger</code> is a simple implementation of {@link ILog}.
+ * A flexible Logger configurable with pattern string.
+ * This is default implementation of {@link ILog}
+ * This can parse a pattern to the List of converter with Parser.
+ * We package LogEvent with message, level,timestamp ..., passing around to the List of converter to concat actually Log-String.
  *
- * @author wusheng
+ * @author alvin
  */
-public class EasyLogger implements ILog {
+public class PatternLogger implements ILog {
 
+
+    public static final Map<String, Class<? extends Converter>> DEFAULT_CONVERTER_MAP = new HashMap<String, Class<? extends Converter>>();
+
+    static {
+        DEFAULT_CONVERTER_MAP.put("thread", ThreadConverter.class);
+        DEFAULT_CONVERTER_MAP.put("level", LevelConverter.class);
+        DEFAULT_CONVERTER_MAP.put("agent_name", AgentNameConverter.class);
+        DEFAULT_CONVERTER_MAP.put("timestamp", DateConverter.class);
+        DEFAULT_CONVERTER_MAP.put("msg", MessageConverter.class);
+        DEFAULT_CONVERTER_MAP.put("throwable", ThrowableConverter.class);
+        DEFAULT_CONVERTER_MAP.put("class", ClassConverter.class);
+    }
+
+    public static final String DEFAULT_PATTERN = "%level %timestamp %thread %class : %msg %throwable";
+
+    private String pattern;
+    private List<Converter> converters;
     private String targetClass;
 
-    public EasyLogger(Class targetClass) {
-        this.targetClass = targetClass.getSimpleName();
+    public PatternLogger(Class targetClass, String pattern) {
+        this(targetClass.getSimpleName(), pattern);
     }
 
-    public EasyLogger(String targetClass) {
+    public PatternLogger(String targetClass, String pattern) {
         this.targetClass = targetClass;
+        this.setPattern(pattern);
+    }
+
+    public String getPattern() {
+        return pattern;
     }
 
+    public void setPattern(String pattern) {
+        if (StringUtil.isEmpty(pattern)) {
+            pattern = DEFAULT_PATTERN;
+        }
+        this.pattern = pattern;
+        converters = new Parser(pattern, DEFAULT_CONVERTER_MAP).parse();
+    }
+
+
     protected void logger(LogLevel level, String message, Throwable e) {
         WriterFactory.getLogWriter().write(format(level, message, e));
     }
@@ -68,30 +107,6 @@ public class EasyLogger implements ILog {
         return tmpMessage;
     }
 
-    String format(LogLevel level, String message, Throwable t) {
-        return StringUtil.join(' ', level.name(),
-            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()),
-            Thread.currentThread().getName(),
-            targetClass,
-            ": ",
-            message,
-            t == null ? "" : format(t)
-        );
-    }
-
-    String format(Throwable t) {
-        ByteArrayOutputStream buf = new ByteArrayOutputStream();
-        t.printStackTrace(new java.io.PrintWriter(buf, true));
-        String expMessage = buf.toString();
-        try {
-            buf.close();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-
-        return Constants.LINE_SEPARATOR + expMessage;
-    }
-
     @Override
     public void info(String format) {
         if (isInfoEnable())
@@ -168,4 +183,14 @@ public class EasyLogger implements ILog {
             logger(LogLevel.ERROR, format, null);
         }
     }
+
+
+    String format(LogLevel level, String message, Throwable t) {
+        LogEvent logEvent = new LogEvent(level, message, t, targetClass);
+        StringBuilder stringBuilder = new StringBuilder();
+        for (Converter converter : converters) {
+            stringBuilder.append(converter.convert(logEvent));
+        }
+        return stringBuilder.toString();
+    }
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/WriterFactory.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/WriterFactory.java
index 4bc357d..8c9a619 100644
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/WriterFactory.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/WriterFactory.java
@@ -27,7 +27,7 @@ import org.apache.skywalking.apm.util.StringUtil;
 
 public class WriterFactory {
     public static IWriter getLogWriter() {
-        if (SnifferConfigInitializer.isInitCompleted() && AgentPackagePath.isPathFound()) {
+        if (!useConsole() && SnifferConfigInitializer.isInitCompleted() && AgentPackagePath.isPathFound()) {
             if (StringUtil.isEmpty(Config.Logging.DIR)) {
                 try {
                     Config.Logging.DIR = AgentPackagePath.getPath() + "/logs";
@@ -40,4 +40,8 @@ public class WriterFactory {
             return SystemOutWriter.INSTANCE;
         }
     }
+
+    private static boolean useConsole() {
+        return LogOutput.CONSOLE == Config.Logging.OUTPUT;
+    }
 }
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/AgentNameConverter.java
similarity index 65%
copy from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/AgentNameConverter.java
index 5e4bea4..3b47c80 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/AgentNameConverter.java
@@ -17,17 +17,19 @@
  */
 
 
-package org.apache.skywalking.apm.agent.core.logging.core;
+package org.apache.skywalking.apm.agent.core.logging.core.coverts;
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.skywalking.apm.agent.core.conf.Config;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
 
 /**
- * Created by wusheng on 2017/2/28.
+ *
+ * @author alvin
  */
-public class EasyLogResolverTest {
-    @Test
-    public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
+public class AgentNameConverter implements Converter {
+    @Override
+    public String convert(LogEvent logEvent) {
+        return Config.Agent.SERVICE_NAME;
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ClassConverter.java
similarity index 66%
copy from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ClassConverter.java
index 5e4bea4..650b2c4 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ClassConverter.java
@@ -17,17 +17,20 @@
  */
 
 
-package org.apache.skywalking.apm.agent.core.logging.core;
+package org.apache.skywalking.apm.agent.core.logging.core.coverts;
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
 
 /**
- * Created by wusheng on 2017/2/28.
+ * Just return logEvent.getTargetClass().
+ * @author alvin
  */
-public class EasyLogResolverTest {
-    @Test
-    public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
+public class ClassConverter implements Converter {
+
+
+    @Override
+    public String convert(LogEvent logEvent) {
+        return logEvent.getTargetClass();
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/DateConverter.java
similarity index 61%
copy from apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/DateConverter.java
index 238cd56..c4cbc4f 100644
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/DateConverter.java
@@ -17,21 +17,23 @@
  */
 
 
-package org.apache.skywalking.apm.agent.core.logging.core;
+package org.apache.skywalking.apm.agent.core.logging.core.coverts;
 
-import org.apache.skywalking.apm.agent.core.logging.api.ILog;
-import org.apache.skywalking.apm.agent.core.logging.api.LogResolver;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
 
 /**
- * Created by wusheng on 2016/11/26.
+ * The Converter is used to return a now date with format.
+ *
+ * @author alvin
  */
-public class EasyLogResolver implements LogResolver {
-    @Override
-    public ILog getLogger(Class<?> clazz) {
-        return new EasyLogger(clazz);
-    }
+public class DateConverter implements Converter {
 
-    @Override public ILog getLogger(String clazz) {
-        return new EasyLogger(clazz);
+    @Override
+    public String convert(LogEvent logEvent) {
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/LevelConverter.java
similarity index 66%
copy from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/LevelConverter.java
index 5e4bea4..83e57b8 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/LevelConverter.java
@@ -17,17 +17,18 @@
  */
 
 
-package org.apache.skywalking.apm.agent.core.logging.core;
+package org.apache.skywalking.apm.agent.core.logging.core.coverts;
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
 
 /**
- * Created by wusheng on 2017/2/28.
+ * Just return logEvent.getLevel().name()
+ * @author alvin
  */
-public class EasyLogResolverTest {
-    @Test
-    public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
+public class LevelConverter implements Converter {
+    @Override
+    public String convert(LogEvent logEvent) {
+        return logEvent.getLevel().name();
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/LiteralConverter.java
similarity index 62%
rename from apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java
rename to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/LiteralConverter.java
index 238cd56..2fbb413 100644
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolver.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/LiteralConverter.java
@@ -17,21 +17,25 @@
  */
 
 
-package org.apache.skywalking.apm.agent.core.logging.core;
+package org.apache.skywalking.apm.agent.core.logging.core.coverts;
 
-import org.apache.skywalking.apm.agent.core.logging.api.ILog;
-import org.apache.skywalking.apm.agent.core.logging.api.LogResolver;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
 
 /**
- * Created by wusheng on 2016/11/26.
+ * This Converter is used to return the literal.
+ * @author alvin
  */
-public class EasyLogResolver implements LogResolver {
-    @Override
-    public ILog getLogger(Class<?> clazz) {
-        return new EasyLogger(clazz);
+public class LiteralConverter implements Converter {
+
+    private final String literal;
+
+    public LiteralConverter(String literal) {
+        this.literal = literal;
     }
 
-    @Override public ILog getLogger(String clazz) {
-        return new EasyLogger(clazz);
+    @Override
+    public String convert(LogEvent logEvent) {
+        return literal;
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/MessageConverter.java
similarity index 66%
copy from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/MessageConverter.java
index 5e4bea4..4bd34b3 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/MessageConverter.java
@@ -17,17 +17,18 @@
  */
 
 
-package org.apache.skywalking.apm.agent.core.logging.core;
+package org.apache.skywalking.apm.agent.core.logging.core.coverts;
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
 
 /**
- * Created by wusheng on 2017/2/28.
+ * Just return the logEvent.getMessage()
+ * @author alvin
  */
-public class EasyLogResolverTest {
-    @Test
-    public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
+public class MessageConverter implements Converter {
+    @Override
+    public String convert(LogEvent logEvent) {
+        return logEvent.getMessage();
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ThreadConverter.java
similarity index 65%
copy from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
copy to apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ThreadConverter.java
index 5e4bea4..6fc2ba9 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ThreadConverter.java
@@ -17,17 +17,18 @@
  */
 
 
-package org.apache.skywalking.apm.agent.core.logging.core;
+package org.apache.skywalking.apm.agent.core.logging.core.coverts;
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
 
 /**
- * Created by wusheng on 2017/2/28.
+ * Just return the Thread.currentThread().getName()
+ * @author alvin
  */
-public class EasyLogResolverTest {
-    @Test
-    public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
+public class ThreadConverter implements Converter {
+    @Override
+    public String convert(LogEvent logEvent) {
+        return Thread.currentThread().getName();
     }
 }
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ThrowableConverter.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ThrowableConverter.java
new file mode 100644
index 0000000..09797e1
--- /dev/null
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/logging/core/coverts/ThrowableConverter.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.apm.agent.core.logging.core.coverts;
+
+import org.apache.skywalking.apm.agent.core.conf.Constants;
+import org.apache.skywalking.apm.agent.core.logging.core.Converter;
+import org.apache.skywalking.apm.agent.core.logging.core.LogEvent;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Return the StackTrace of String with logEvent.getThrowable()
+ * @author alvin
+ */
+public class ThrowableConverter implements Converter {
+    @Override
+    public String convert(LogEvent logEvent) {
+        Throwable t = logEvent.getThrowable();
+        return t == null ? "" : format(t);
+    }
+
+    public static String format(Throwable t) {
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
+        t.printStackTrace(new java.io.PrintWriter(buf, true));
+        String expMessage = buf.toString();
+        try {
+            buf.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return Constants.LINE_SEPARATOR + expMessage;
+    }
+}
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java b/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogResolverTest.java
similarity index 84%
rename from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
rename to apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogResolverTest.java
index 5e4bea4..3cdb748 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLogResolverTest.java
+++ b/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLogResolverTest.java
@@ -23,11 +23,13 @@ import org.junit.Assert;
 import org.junit.Test;
 
 /**
- * Created by wusheng on 2017/2/28.
+ * @author alvin
  */
-public class EasyLogResolverTest {
+public class PatternLogResolverTest {
+
     @Test
     public void testGetLogger() {
-        Assert.assertTrue(new EasyLogResolver().getLogger(EasyLogResolverTest.class) instanceof EasyLogger);
+        Assert.assertTrue(new PatternLogResolver().getLogger(PatternLoggerTest.class) instanceof PatternLogger);
     }
-}
+
+}
\ No newline at end of file
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLoggerTest.java b/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLoggerTest.java
similarity index 64%
rename from apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLoggerTest.java
rename to apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLoggerTest.java
index e3a7d22..f940b09 100644
--- a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/EasyLoggerTest.java
+++ b/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/PatternLoggerTest.java
@@ -19,22 +19,29 @@
 
 package org.apache.skywalking.apm.agent.core.logging.core;
 
+import com.google.common.collect.Lists;
+import org.apache.skywalking.apm.agent.core.conf.Config;
+import org.apache.skywalking.apm.agent.core.conf.Constants;
+import org.hamcrest.core.StringContains;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.mockito.Mockito;
-import org.apache.skywalking.apm.agent.core.conf.Constants;
 
 import java.io.PrintStream;
+import java.util.List;
 
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.times;
 
 /**
- * Created by wusheng on 2017/2/28.
+ * @author alvin
  */
-public class EasyLoggerTest {
+public class PatternLoggerTest {
+
+    public static final String PATTERN = "%timestamp+0800 %level [%agent_name,,,] [%thread] %class:-1 %msg %throwable";
+
     private static PrintStream OUT_REF;
     private static PrintStream ERR_REF;
 
@@ -42,18 +49,21 @@ public class EasyLoggerTest {
     public static void initAndHoldOut() {
         OUT_REF = System.out;
         ERR_REF = System.err;
+        Config.Agent.SERVICE_NAME = "testAppFromConfig";
     }
 
+
     @Test
     public void testLog() {
         PrintStream output = Mockito.mock(PrintStream.class);
         System.setOut(output);
         PrintStream err = Mockito.mock(PrintStream.class);
         System.setErr(err);
-        EasyLogger logger = new EasyLogger(EasyLoggerTest.class) {
+        PatternLogger logger = new PatternLogger(PatternLoggerTest.class, PATTERN) {
             @Override
             protected void logger(LogLevel level, String message, Throwable e) {
-                SystemOutWriter.INSTANCE.write(format(level, message, e));
+                String r = format(level, message, e);
+                SystemOutWriter.INSTANCE.write(r);
             }
         };
 
@@ -83,7 +93,7 @@ public class EasyLoggerTest {
         System.setOut(output);
         PrintStream err = Mockito.mock(PrintStream.class);
         System.setErr(err);
-        EasyLogger logger = new EasyLogger(EasyLoggerTest.class) {
+        PatternLogger logger = new PatternLogger(PatternLoggerTest.class, PATTERN) {
             @Override
             protected void logger(LogLevel level, String message, Throwable e) {
                 SystemOutWriter.INSTANCE.write(format(level, message, e));
@@ -111,18 +121,45 @@ public class EasyLoggerTest {
     }
 
     @Test
-    public void testFormat() {
+    public void testLogOk_whenPatternHasKeyword() {
+        final List<String> strings = Lists.newArrayList();
+        PatternLogger logger = new PatternLogger(PatternLoggerTest.class, "logmsg: %%%%%%!@#$\\%^&*() %{this is message} \\\\ \\n\\t \t\n %%msg") {
+            @Override
+            protected void logger(LogLevel level, String message, Throwable e) {
+                String r = format(level, message, e);
+                strings.add(r);
+            }
+        };
+        logger.info("msg");
+        Assert.assertThat(strings.get(0), StringContains.containsString("logmsg: %%%%%%!@#$%^&*() %{this is message} \\ \n\t \t\n %msg"));
+    }
+
+    @Test
+    public void testLogFormat() {
+        final List<String> strings = Lists.newArrayList();
+        PatternLogger logger = new PatternLogger(PatternLoggerTest.class, PATTERN) {
+            @Override
+            protected void logger(LogLevel level, String message, Throwable e) {
+                String r = format(level, message, e);
+                strings.add(r);
+            }
+        };
         NullPointerException exception = new NullPointerException();
-        EasyLogger logger = new EasyLogger(EasyLoggerTest.class);
-        String formatLines = logger.format(exception);
+        logger.error("hello world", exception);
+        logger.error("hello world", null);
+        String formatLines = strings.get(0);
         String[] lines = formatLines.split(Constants.LINE_SEPARATOR);
+        Assert.assertThat(lines[0], StringContains.containsString("ERROR [testAppFromConfig,,,] [main] PatternLoggerTest:-1 hello world "));
         Assert.assertEquals("java.lang.NullPointerException", lines[1]);
-        Assert.assertEquals("\tat org.apache.skywalking.apm.agent.core.logging.core.EasyLoggerTest.testFormat(EasyLoggerTest.java:115)", lines[2]);
+        Assert.assertThat(lines[2], StringContains.containsString("PatternLoggerTest.testLogFormat"));
+        Assert.assertEquals(strings.get(1).split(Constants.LINE_SEPARATOR).length, 1);
     }
 
+
     @AfterClass
     public static void reset() {
         System.setOut(OUT_REF);
         System.setErr(ERR_REF);
     }
+
 }
diff --git a/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/WriterFactoryTest.java b/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/WriterFactoryTest.java
new file mode 100644
index 0000000..aca5164
--- /dev/null
+++ b/apm-sniffer/apm-agent-core/src/test/java/org/apache/skywalking/apm/agent/core/logging/core/WriterFactoryTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.apm.agent.core.logging.core;
+
+import org.apache.skywalking.apm.agent.core.boot.AgentPackagePath;
+import org.apache.skywalking.apm.agent.core.conf.Config;
+import org.apache.skywalking.apm.agent.core.conf.SnifferConfigInitializer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.BDDMockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(value = {SnifferConfigInitializer.class, AgentPackagePath.class})
+public class WriterFactoryTest {
+
+    @Test
+    public void alwaysReturnSystemLogWriteWithSetLoggingDir() {
+        Config.Logging.OUTPUT = LogOutput.CONSOLE;
+        PowerMockito.mockStatic(SnifferConfigInitializer.class);
+        PowerMockito.mockStatic(AgentPackagePath.class);
+        BDDMockito.given(SnifferConfigInitializer.isInitCompleted()).willReturn(true);
+        BDDMockito.given(AgentPackagePath.isPathFound()).willReturn(true);
+
+        assertTrue(SnifferConfigInitializer.isInitCompleted());
+        assertTrue(AgentPackagePath.isPathFound());
+
+        IWriter logWriter = WriterFactory.getLogWriter();
+        PowerMockito.verifyStatic();
+        assertTrue(logWriter instanceof SystemOutWriter);
+    }
+
+    @Test
+    public void returnFileWriterWriteWithBlankLoggingDir() {
+        Config.Logging.OUTPUT = LogOutput.FILE;
+        PowerMockito.mockStatic(SnifferConfigInitializer.class);
+        PowerMockito.mockStatic(AgentPackagePath.class);
+        BDDMockito.given(SnifferConfigInitializer.isInitCompleted()).willReturn(true);
+        BDDMockito.given(AgentPackagePath.isPathFound()).willReturn(true);
+
+        assertTrue(SnifferConfigInitializer.isInitCompleted());
+        assertTrue(AgentPackagePath.isPathFound());
+
+        IWriter logWriter = WriterFactory.getLogWriter();
+        PowerMockito.verifyStatic();
+        assertTrue(logWriter instanceof FileWriter);
+    }
+}
\ No newline at end of file
diff --git a/docs/en/setup/service-agent/java-agent/README.md b/docs/en/setup/service-agent/java-agent/README.md
index 4d7e556..3dfb2df 100755
--- a/docs/en/setup/service-agent/java-agent/README.md
+++ b/docs/en/setup/service-agent/java-agent/README.md
@@ -82,7 +82,9 @@ property key | Description | Default |
 `collector.backend_service`|Collector SkyWalking trace receiver service addresses.|`127.0.0.1:11800`|
 `logging.level`|The log level. Default is debug.|`DEBUG`|
 `logging.file_name`|Log file name.|`skywalking-api.log`|
+`logging.output`| Log output. Default is FILE. Use CONSOLE means output to stdout. |`FILE`|
 `logging.dir`|Log files directory. Default is blank string, means, use "system.out" to output logs.|`""`|
+`logging.pattern `|logging format. There are all conversion specifiers: <br>&nbsp;&nbsp;* `%level` means log level. <br>&nbsp;&nbsp;*  `%timestamp` means now of time with format `yyyy-MM-dd HH:mm:ss:SSS`.<br>&nbsp;&nbsp;*   `%thread` means name of current thread.<br>&nbsp;&nbsp;*   `%msg` means some message which user logged. <br>&nbsp;&nbsp;*  `%class` means SimpleName of TargetClass. <br>&nbsp;&nbsp;*  `%throwable` means a throwable which user called. <br>&nbsp;&nbsp;*  `%agent_name` m [...]
 `logging.max_file_size`|The max size of log file. If the size is bigger than this, archive the current file, and write into a new file.|`300 * 1024 * 1024`|
 `jvm.buffer_size`|The buffer size of collected JVM info.|`60 * 10`|
 `buffer.channel_size`|The buffer channel size.|`5`|