【问题标题】:Best way to write custom json messages using log4j2使用 log4j2 编写自定义 json 消息的最佳方法
【发布时间】:2016-09-22 19:59:41
【问题描述】:

我一直在将 log4j 用于不同类型的项目,并且对 log4j2 有一些经验。所有实现都使用默认的附加程序和布局。目前我需要编写一个以 json 格式写入的应用程序。所以我通过设置一个非常简单的 log4j2 记录器尝试了 log4j2 JSONLayout 布局。

public class JSONLogger {

    private static final Logger LOGGER = LogManager.getLogger();

    public static void main(String[] args) {
        JSONLogger jsonlogger = new JSONLogger() ;
    }

    public JSONLogger() {
        LOGGER.log(Level.FATAL, "hi mum!") ;

         int val1 = 10, val2 = 11, val3 = 12;

         LOGGER.log(Level.FATAL,"val1={}, val2={}, val3={}", val1, val2, val3);
    }

}

jsonLoggerProperties.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="DEBUG">
    <Properties>
        <Property name="log-path">/Users/petervannes/NetBeansProjects/JSONLogger/logfiles</Property>
    </Properties>

    <Appenders>
        <RollingFile name="json_file_appender" fileName="${log-path}/jsonlogger.json"
                     filePattern="${log-path}/%d{yyyyMMdd}_jsonlogger-%i.json" >
            <JSONLayout complete="true" compact="false"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="1 KB" />
            </Policies>
            <DefaultRolloverStrategy max="4"/>
        </RollingFile>

    </Appenders>


    <Loggers>
        <root level="debug" additivity="false">
            <AppenderRef ref="json_file_appender"/>
        </root>
    </Loggers>
</Configuration>

导致类似的日志条目;

, {
  "timeMillis" : 1474573600359,
  "thread" : "main",
  "level" : "FATAL",
  "loggerName" : "JSONLogger",
  "message" : "val1=10, val2=11, val3=12",
  "contextStack" : [ "fieldName2" ],
  "endOfBatch" : false,
  "loggerFqcn" : "org.apache.logging.log4j.spi.AbstractLogger",
  "threadId" : 1,
  "threadPriority" : 5
}

我需要的是记录成这样的 JSON 格式;

, {
  "DateTime" : "08/01/2016 21:33:22.334",
  "level" : "FATAL",
  "Summary" : "Something has gone wrong",
  "ChainManager" : "Manager A",
  "Optionals" : { "Key_1": "Value1",
                  "Key_2": "Value2" }
}

这可能与 log4j2 JSONLayout 或是否有任何其他布局我可以使用来获得这种格式?

【问题讨论】:

    标签: json log4j2


    【解决方案1】:

    问题是关于使用 log4j2 编写自定义 json 消息。

    从 log4j2 版本的 2.11 版本开始这是可能的:

    https://issues.apache.org/jira/browse/LOG4J2-2190

    JSONLayout的新参数被调用

    objectMessageAsJsonObject

    。 示例项目文件;

    log4j2.properties

    status = error
    
    
    appender.ana_whitespace.type = RollingFile
    appender.ana_whitespace.name = ana_whitespace
    appender.ana_whitespace.fileName = ${sys:es.logs.base_path:-target}${sys:file.separator}ana_whitespace.log
    appender.ana_whitespace.layout.type = JsonLayout
    appender.ana_whitespace.layout.propertiesAsList = false
    appender.ana_whitespace.layout.compact = false
    appender.ana_whitespace.layout.eventEol = true
    appender.ana_whitespace.layout.objectMessageAsJsonObject = true
    appender.ana_whitespace.layout.complete= true
    appender.ana_whitespace.layout.properties= true
    appender.ana_whitespace.filePattern = ${sys:es.logs.base_path:-target}${sys:file.separator}ana_whitespace-%d{yyyy-MM-dd}.log
    appender.ana_whitespace.filter.1.type = MarkerFilter
    appender.ana_whitespace.filter.1.onMismatch=DENY
    appender.ana_whitespace.filter.1.onMatch=ACCEPT
    appender.ana_whitespace.filter.1.marker=ANA_WHITESPACE
    appender.ana_whitespace.policies.type = Policies
    appender.ana_whitespace.policies.time.type = TimeBasedTriggeringPolicy
    appender.ana_whitespace.policies.time.interval = 1
    appender.ana_whitespace.policies.time.modulate = true
    appender.ana_whitespace.policies.size.type = SizeBasedTriggeringPolicy
    appender.ana_whitespace.policies.size.size = 10 MB
    
    rootLogger.level = info
    rootLogger.appenderRef.ana_whitespace.ref = ana_whitespace
    

    Java 代码示例

    package de.es.stemmer;
    
    import java.io.IOException;
    import java.util.Map;
    import java.util.TreeMap;
    
    import org.apache.http.client.ClientProtocolException;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.apache.logging.log4j.Marker;
    import org.apache.logging.log4j.MarkerManager;
    import org.apache.logging.log4j.ThreadContext;
    import org.apache.logging.log4j.core.LoggerContext;
    import org.apache.logging.log4j.message.ObjectMessage;
    
    public class JsonLoggerTest {
        final static Logger log = LogManager.getLogger(JsonLoggerTest.class);
        final static Marker MARKER_WHITESPACE = MarkerManager.getMarker("ANA_WHITESPACE");
    
        public static void main(String[] args) throws ClientProtocolException, IOException {
            System.setProperty("es.logs.base_path", "target");
            System.setProperty("es.logs.cluster_name", "_cluster");
            LoggerContext.getContext().reconfigure();
            ThreadContext.put("orig", "MDC_origValue");
            ThreadContext.put("source", "MDC_sourceSnippet");
            Map<String, String> map = new TreeMap<>();
            map.put("orig", "msg_origValue");
            map.put("source", "msg_sourceSnippet");
            ObjectMessage msg = new ObjectMessage(map);
            log.info(MARKER_WHITESPACE, msg);
            ThreadContext.remove("orig");
            ThreadContext.remove("source");
        }
    
    }
    

    JSON 日志条目

    [
    {
      "thread" : "main",
      "level" : "INFO",
      "loggerName" : "de.es.stemmer.JsonLoggerTest",
      "marker" : {
        "name" : "ANA_WHITESPACE"
      },
      "message" : {
        "orig" : "msg_origValue",
        "source" : "msg_sourceSnippet"
      },
      "endOfBatch" : false,
      "loggerFqcn" : "org.apache.logging.log4j.spi.AbstractLogger",
      "instant" : {
        "epochSecond" : 1526576578,
        "nanoOfSecond" : 184000000
      },
      "contextMap" : {
        "orig" : "MDC_origValue",
        "source" : "MDC_sourceSnippet"
      },
      "threadId" : 1,
      "threadPriority" : 5
    }
    
    ]
    

    【讨论】:

    • 虽然这很有帮助 - 您可以在消息中放入自定义结构化数据 - 但它并不能真正解决不希望大量噪音和/或未格式化以满足的字段的问题具体需求。在这种情况下,噪音肯定是 loggerFqcn、endOfBatch、即时而不是人类可读的时间戳,并且可能还包括 threadPriority 和 threadId(取决于您的用例)以及可能的其他字段。所以在我看来根本不是真正的定制。
    【解决方案2】:

    如果您正在寻找一种方法来生成自定义 JSON 日志消息,而不会添加任何由 log4j2 添加的“噪音”,您可以创建Message 接口的实现并使用不同的布局 - 例如PatternLayout。下面是这种方法的一个例子:

    首先一个类来实现Message inferface

    import java.util.HashMap;
    import java.util.Map;
    
    import org.apache.logging.log4j.message.Message;
    
    import com.google.gson.Gson;
    import com.google.gson.GsonBuilder;
    
    public class JSONMessage implements Message {
    
        private static final long serialVersionUID = 538439258494853324L;
        private String messageString;
        private static final Gson GSON = new GsonBuilder()
                .setPrettyPrinting()
                .create();
    
        public JSONMessage(){
            this(null);
        }
    
        public JSONMessage(Object msgObj){
            parseMessageAsJson(msgObj);
        }
    
        public JSONMessage(String msgStr){
            Map<String,String> msgObj = new HashMap<>();
            msgObj.put("message", msgStr);
            parseMessageAsJson(msgObj);
        }
    
        private void parseMessageAsJson(Object msgObj){
            messageString = GSON.toJson(msgObj);
        }
    
        @Override
        public String getFormattedMessage() {
            return messageString;
        }
    
        @Override
        public String getFormat() {
            return messageString;
        }
    
        @Override
        public Object[] getParameters() {
            return null;
        }
    
        @Override
        public Throwable getThrowable() {
            return null;
        }
    
    }
    

    接下来是log4j2.xml配置:

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN" name="App">
        <Appenders>
            <Console name="Console" target="SYSTEM_OUT">
                    <PatternLayout pattern="%m%n" />
            </Console>
        </Appenders>
    
        <Loggers>
            <Root level="trace">
                <AppenderRef ref="Console" />
            </Root>
        </Loggers>
    </Configuration>
    

    现在是一个简单的应用程序类来生成日志事件

    import java.util.HashMap;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    
    public class App {
    
        private static final Logger logger = LogManager.getLogger();
    
        public static void main( String[] args )
        {
            HashMap<String,Object> msgMap = new HashMap<>();
            msgMap.put("someInt", 123);
            msgMap.put("note", "Maybe you put a message here");
    
            HashMap<String,Object> anotherMap = new HashMap<>();
            anotherMap.put("key1", "value1");
            anotherMap.put("key2", "value2");
            msgMap.put("map", anotherMap);
            logger.info(new JSONMessage(msgMap));
        }
    }
    

    这是一些示例输出:

    {
      "note": "Maybe you put a message here",
      "map": {
        "key1": "value1",
        "key2": "value2"
      },
      "someInt": 123
    }
    

    请注意,我使用Gson 创建 JSON 输出,但您可以使用任何您想要的库。另请注意,此代码不会生成“完整” JSON,因为它不会在日志的开头和结尾添加方括号或消息对象之间的逗号。

    【讨论】:

      【解决方案3】:

      我找到了适合我的解决方案; slf4j-json-logger。 它是一个 slf4j 框架,所以应该包含在 pom.xml 中。 示例项目文件;

      pom.xml

        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
           <modelVersion>4.0.0</modelVersion>
           <groupId>com.reddipped</groupId>
           <artifactId>JSONLogger_2</artifactId>
           <version>1.0-SNAPSHOT</version>
           <packaging>jar</packaging>
           <properties>
      
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
              <maven.compiler.source>1.6</maven.compiler.source>
              <maven.compiler.target>1.6</maven.compiler.target>
      
              <mainClass>com.reddipped.jsonlogger_2.Test</mainClass>
      
              <slf4j.version>1.7.21</slf4j.version>
              <!-- current log4j 2 release -->
              <log4j.version>2.6.2</log4j.version> 
      
           </properties>
      
           <dependencies>
              <dependency>
                 <groupId>org.slf4j</groupId>
                 <artifactId>slf4j-api</artifactId>
                 <version>${slf4j.version}</version>
              </dependency>
              <!-- Binding for Log4J -->
              <dependency>
                 <groupId>org.apache.logging.log4j</groupId>
                 <artifactId>log4j-slf4j-impl</artifactId>
                 <version>${log4j.version}</version>
              </dependency>
              <!-- Log4j API and Core implementation required for binding -->
              <dependency>
                 <groupId>org.apache.logging.log4j</groupId>
                 <artifactId>log4j-api</artifactId>
                 <version>${log4j.version}</version>
              </dependency>
              <dependency>
                 <groupId>org.apache.logging.log4j</groupId>
                 <artifactId>log4j-core</artifactId>
                 <version>${log4j.version}</version>
              </dependency>
              <!-- Logger slf4j-json-logger -->
              <dependency>
                 <groupId>com.savoirtech.logging</groupId>
                 <artifactId>slf4j-json-logger</artifactId>
                 <version>2.0.2</version>
              </dependency>
           </dependencies> 
        </project>
      

      log4j2.xml

        <?xml version="1.0" encoding="UTF-8"?>
      
        <!--
      
        Use java property log4j.configurationFile to specify log4j2.xml location
        if not available in classpath
      
        -    Dlog4j.configurationFile="/Users/petervannes/NetBeansProjects/JSONLogger_2/src/mann/java/resources/log4j2.xml"
      
        -->
        <configuration status="trace">
           <Properties>
              <Property name="log-path">/Users/petervannes/NetBeansProjects/JSONLogger_2/logfiles</Property>
           </Properties>
           <appenders>
              <RollingFile name="RollingFile" fileName="${log-path}/jsonlogger.json"
                        filePattern="${log-path}/%d{yyyyMMdd}_jsonlogger-%i.json" >
                 <PatternLayout>
                    <pattern>%m%n</pattern>
                 </PatternLayout> 
                 <Policies>
                    <TimeBasedTriggeringPolicy />
                    <SizeBasedTriggeringPolicy size="1 KB" />
                 </Policies>
                 <DefaultRolloverStrategy max="4"/>
              </RollingFile>
           </appenders>
           <Loggers>
              <Logger name="JSONLogger" level="debug" additivity="false">
                 <AppenderRef ref="RollingFile" />
              </Logger>
              <Root level="debug">
                 <AppenderRef ref="RollingFile" />
              </Root>
           </Loggers>
        </configuration>
      

      Java 代码示例

        package com.reddipped.jsonlogger_2;
      
        import com.savoirtech.logging.slf4j.json.LoggerFactory;
        import java.util.HashMap;
        import java.util.Map;
      
        /**
         *
         * @author petervannes
         */
        public class Test {
            public static void main(String[] args) {   
              Test t = new Test() ;
           }
      
      
           public Test() {
      
              LoggerFactory.setIncludeLoggerName(false);
              LoggerFactory.setDateFormatString("yyyy-MM-dd HH:mm:ss.SSS");
      
               com.savoirtech.logging.slf4j.json.logger.Logger LOGGER =  LoggerFactory.getLogger("JSONLogger");
      
           Map<String, String> optionalFields = new HashMap();
           optionalFields.put("CaseNumber", "C12.12343");
           optionalFields.put("Step","Assignment") ;
           optionalFields.put("Department","BPM") ;
      
           String LOB = "Business Administration" ;
           String Service = "DocumentService" ;
           String Process = "AddAttachements" ;
           String Reason = "Technical" ; 
      
           LOGGER.error().message("Conversion error 'incompatible PDF document'")
                 .field("LOB",LOB)
                 .field("Service", Service)
                 .field("Process",Process)
                 .field("Reason", Reason)
                 .map("OptionalFields", optionalFields).log() ;
           }
      
        }
      

      JSON 日志条目

        {
          "message": "Conversion error  'incompatible PDF document'",
          "LOB": "Business Administration",
          "Service": "DocumentService",
          "Process": "AddAttachements",
          "Reason": "Technical",
          "OptionalFields": {
           "Step": "Assignment",
           "Department": "BPM",
           "CaseNumber": "C12.12343"
          },
          "level": "ERROR",
          "thread_name": "main",
          "class": "com.reddipped.jsonlogger_2.Test",
          "@timestamp": "2016-09-23 10:18:06.623"
        }
      

      【讨论】:

      • 很好,但我希望我能在没有 sl4j 的情况下实现这一点。 log4j2.8 还是有支持的
      • 你可以,我不得不寻找替代路径,因为 slF4j 正在干扰。
      • 在我的评论中,我的意思是说log4j 2.8 still does not have json logs support。它以 json 格式打印,但不是我添加的消息。
      • 我也有同样的需求,所以我为 Log4j2 构建了一个名为“extended-jsonlayout”的小插件。你可以在这里找到它 - github.com/savantly-net/log4j2-extended-jsonlayout
      • @pcvnes 有没有办法删除“class”、“thread_name”和“@timestamp” JSON 输出的属性?
      【解决方案4】:

      我知道这是一个老问题,但我认为有更好的方法。

      您应该使用JSON Template Layout

      然后您就可以使用这样的模板来配置您的 JsonLayout:

      {
        "mdc": {
          "$resolver": "mdc"
        },
        "exception": {
          "exception_class": {
            "$resolver": "exception",
            "field": "className"
          },
          "exception_message": {
            "$resolver": "exception",
            "field": "message"
          },
          "stacktrace": {
            "$resolver": "exception",
            "field": "stackTrace",
            "stackTrace": {
              "stringified": true
            }
          }
        },
        "line_number": {
          "$resolver": "source",
          "field": "lineNumber"
        },
        "class": {
          "$resolver": "source",
          "field": "className"
        },
        "@version": 1,
        "source_host": "${hostName}",
        "message": {
          "$resolver": "message",
          "stringified": true
        },
        "thread_name": {
          "$resolver": "thread",
          "field": "name"
        },
        "@timestamp": {
          "$resolver": "timestamp"
        },
        "level": {
          "$resolver": "level",
          "field": "name"
        },
        "file": {
          "$resolver": "source",
          "field": "fileName"
        },
        "method": {
          "$resolver": "source",
          "field": "methodName"
        },
        "logger_name": {
          "$resolver": "logger",
          "field": "name"
        }
      }
      

      有许多使用模板可用的配置。

      在此处查看更多信息:

      https://logging.apache.org/log4j/2.x/manual/json-template-layout.html

      【讨论】:

      • 知道如何向其中添加自定义字段吗?
      • @AmitK 是什么意思?此模板布局已创建,因此您可以设置所需的任何自定义字段。你需要什么?例如,此“@version”是一个名为“@version”的自定义字段。你可以设置任何你想要的字段。
      • 如果我添加一个“数据”字段,代码如何动态向该字段添加数据?
      • 我不确定你想要什么。你可以创建一个“数据”字段,但你需要使用一个可用的解析器。如果你澄清你想要什么,我可以提供帮助。
      • @LucasSoares 我认为问题是从 java log.info() 中,它不允许您添加其他属性或传递它们。预期的能力是 log.info(" message").addField("额外字段", data);或类似的东西。提供的示例是添加格式或解析器,在添加字段之类的数据时,在每个日志行上都这样做。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多