自定義log4j的appender寫es日誌

神牛003發表於2019-05-18

本篇和大家分享的是自定義log4j的appender,用es來記錄日誌並且通過kibana瀏覽es記錄;就目前網際網路或者一些中大型公司通常會用到第三方組合elk,其主要用寫資料到es中,然後通過視覺化工具kibana來做直觀資料檢視和統計;本篇內容節點如下:

  • docker快速搭建es,es header,kibana 環境
  • 封裝寫es工具類
  • 自定義log4j的appender
  • kibana基礎使用

docker快速搭建es,kibana,es header 環境

對於愛研究第三方服務的程式設計師來說docker是很好的助手,能夠快速搭建一套簡易的使用環境;docker啟動es映象具體不多說了看這裡docker快速搭建幾個常用的第三方服務,值得注意的是這裡我定義了es的叢集名稱,通過如下命令進入容器中改了配置檔案(當然可直接通過命令啟動時傳遞引數):

1 docker exec -it eae7731bb6a1 /bin/bash

然後進入到 /usr/share/elasticsearch/config 並開啟elasticsearch.yml配置檔案修改:

 1 #叢集名稱
 2 cluster.name: "shenniu_elasticsearch"
 3 #本節點名稱
 4 node.name: master
 5 #是否master節點
 6 node.master: true
 7 #是否儲存資料
 8 node.data: true
 9 #head外掛設定
10 http.cors.enabled: true
11 http.cors.allow-origin: "*"
12 http.port: 9200
13 transport.tcp.port: 9300
14 #可以訪問的ip
15 network.bind_host: 0.0.0.0

這裡定義叢集名為:shenniu_elasticsearch

如上啟動了es後,我們為了直觀的看到es中資訊,這裡用到了es header工具(當然不必須);只要docker啟動其映象後,我們能夠在上面輸入咋們的es地址,以此來檢測es叢集是否開啟並瀏覽相關索引資訊,es header預設埠9100:

通常搭配es的是kibana(視覺化工具),用來檢視es的資料和做一些統計(如數量統計,按列聚合統計等),這裡通過docker run啟動kibana映象後,我們還需要讓其關聯上es才行,同樣通過docker exec去修改裡面配置資訊,主要在裡面配置es地址:

1 docker exec -it 67a0ef871ef7 /bin/bash
2 cd etc/
3 cd kibana/
4 vim kibana.yml

配置內容修改如:

1 server.host: '0.0.0.0'
2 elasticsearch.url: 'http://192.168.181.7:9200'  #es地址

如上操作完後,開啟kibana地址 http://192.168.181.7:5601/app/kibana ,能夠看到讓咋們配置es索引查詢規則的介面,如果es地址down掉或者配置不對,kibana會停留在red介面,讓我們正確配置:

封裝寫es工具類

java往es中寫資料,可以使用官網推薦的 org.elasticsearch.client 包(注意版本問題),我這裡es是5.6版本對應的rest-high-leve-client最好也引入5.6版本的,如下pom資訊:

 1         <dependency>
 2             <groupId>log4j</groupId>
 3             <artifactId>log4j</artifactId>
 4             <version>1.2.17</version>
 5         </dependency>
 6         <dependency>
 7             <groupId>org.elasticsearch.client</groupId>
 8             <artifactId>elasticsearch-rest-high-level-client</artifactId>
 9             <version>5.6.16</version>
10         </dependency>
11         <dependency>
12             <groupId>com.alibaba</groupId>
13             <artifactId>fastjson</artifactId>
14             <version>1.2.56</version>
15             <scope>compile</scope>
16         </dependency>

首先要明確用程式碼操作es(或其他第三方服務),往往都需ip(域名)+埠,這裡我的配置資訊:

1 #es連線串 ','分割
2 es.links=http://192.168.181.7:9200,http://localhost:9200
3 es.indexName=eslog_shenniu003

然後有如下封裝程式碼:

 1 public class EsRestHighLevelClient {
 2 
 3     /**
 4      * new HttpHost("192.168.181.44", 9200, "http")
 5      */
 6     private HttpHost[] hosts;
 7     private String index;
 8     private String type;
 9     private String id;
10 
11     public EsRestHighLevelClient(String index, String type, String id, HttpHost[] hosts) {
12         this.hosts = hosts;
13         this.index = index;
14         this.type = type;
15         this.id = id;
16     }
17 
18     /**
19      * @param index
20      * @param type
21      * @param hosts
22      */
23     public EsRestHighLevelClient(String index, String type, String... hosts) {
24         this.hosts = IpHelper.getHostArrByStr(hosts);
25         this.index = index;
26         this.type = type;
27     }
28 
29     public RestHighLevelClient client() {
30         Assert.requireNonEmpty(this.hosts, "無效的es連線");
31 
32         RestHighLevelClient client = new RestHighLevelClient(
33                 RestClient.builder(this.hosts).build()
34         );
35         return client;
36     }
37 
38     public IndexRequest indexRequest() {
39         return new IndexRequest(this.index, this.type, this.id);
40     }
41 
42     public RestStatus createIndex(Map<String, Object> map) throws IOException {
43         return client().
44                 index(this.indexRequest().source(map)).
45                 status();
46     }
47 }

這裡還涉及到了一個IpHelper輔助類,主要用來拆分多個ip資訊引數,裡面涉及到正則匹配方式:

 1 public class IpHelper {
 2 
 3     private static final String strHosts = "(?<h>[^:]+)://(?<ip>[^:]+):(?<port>[^/|,]+)";
 4     private static final Pattern hostPattern = Pattern.compile(strHosts);
 5 
 6     public static Optional<String> getHostIp() {
 7         try {
 8             return Optional.ofNullable(InetAddress.getLocalHost().getHostAddress());
 9         } catch (UnknownHostException e) {
10             e.printStackTrace();
11         }
12         return Optional.empty();
13     }
14 
15     public static Optional<String> getHostName() {
16         try {
17             return Optional.ofNullable(InetAddress.getLocalHost().getHostName());
18         } catch (UnknownHostException e) {
19             e.printStackTrace();
20         }
21         return Optional.empty();
22     }
23 
24     /**
25      * strHosts:"http://192.168.0.1:9200","http://192.168.0.1:9200","http://192.168.0.1:9200"
26      *
27      * @return
28      */
29     public static List<HttpHost> getHostsByStr(String... strHosts) {
30         List<HttpHost> hosts = new ArrayList<>();
31         for (int i = 0; i < strHosts.length; i++) {
32             String[] hostArr = strHosts[i].split(",");
33             for (String strHost : hostArr) {
34                 Matcher matcher = hostPattern.matcher(strHost);
35                 if (matcher.find()) {
36                     String http = matcher.group("h");
37                     String ip = matcher.group("ip");
38                     String port = matcher.group("port");
39 
40                     if (Strings.isEmpty(http) || Strings.isEmpty(ip) || Strings.isEmpty(port)) {
41                         continue;
42                     }
43                     hosts.add(new HttpHost(ip, Integer.valueOf(port), http));
44                 }
45             }
46         }
47         return hosts;
48     }
49 
50     public static HttpHost[] getHostArrByStr(String... strHosts) {
51         List<HttpHost> list = getHostsByStr(strHosts);
52         return Arrays.copyOf(list.toArray(), list.size(), HttpHost[].class);
53     }
54 }

自定義log4j的appender

對於日誌來說log4j是大眾化的,有很多語言也在用這種方式來記錄,使用它相當於一種共識;它提供了很好的擴充套件,很方便達到把日誌記錄到資料庫,文字獲取其他自定義程式碼方式中;定義一個EsAppend類,繼承AppenderSkeleton類,程式碼上我們要做的僅僅重寫如下方法即可:

本期咋們實現的步驟是:

  1. activateOptions方法獲取自定義配置資訊(es連線串,寫es的日誌索引名等)
  2. append方法獲取並記錄logger.xx()等級的日誌
  3. ExecutorService執行緒池類操作多個執行緒執行execute提交日誌到es

具體實現程式碼如下,可按照上面步驟分析:

 1 public class EsAppend extends AppenderSkeleton {
 2 
 3     //es客戶端
 4     private static EsRestHighLevelClient esClient;
 5     //es配置檔名
 6     private String confName;
 7 
 8     private ExecutorService executorService = Executors.newFixedThreadPool(10);
 9 
10     protected void append(LoggingEvent loggingEvent) {
11         if (this.isAsSevereAsThreshold(loggingEvent.getLevel())) {
12             executorService.execute(new EsAppendTask(loggingEvent, this.layout));
13 //            new EsAppendTask(loggingEvent, this.layout).run();
14         }
15     }
16 
17     public void close() {
18         this.closed = true;
19     }
20 
21     public boolean requiresLayout() {
22         return false;
23     }
24 
25     @Override
26     public void activateOptions() {
27         super.activateOptions();
28         try {
29             System.out.println("初始化 - EsAppend...");
30 
31             if (this.getConfName() == null || this.getConfName().isEmpty()) {
32                 this.setConfName("eslog.properties");
33             }
34             PropertiesHelper propertiesHelper = new PropertiesHelper(this.getConfName());
35             //es hosts
36             String strHosts = propertiesHelper.getProperty("es.links", "http://127.0.0.1:9200");
37             //es日誌索引
38             String esLogIndex = propertiesHelper.getProperty("es.indexName", "eslog");
39             esClient = new EsRestHighLevelClient(esLogIndex, "docs", strHosts);
40 
41             System.out.println("初始化完成 - EsAppend");
42         } catch (Exception ex) {
43             System.out.println("初始化失敗- EsAppend");
44             ex.printStackTrace();
45         }
46     }
47 
48     public String getConfName() {
49         return confName;
50     }
51 
52     public void setConfName(String confName) {
53         this.confName = confName;
54     }
55 
56     /**
57      * runable寫es
58      */
59     class EsAppendTask implements Runnable {
60         private HashMap<String, Object> map;
61 
62         public EsAppendTask(LoggingEvent loggingEvent, Layout layout) {
63             SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSSZ");
64             map = new HashMap<String, Object>() {
65                 {
66                     put("timeStamp",df.format(new Date()));
67                     put("serverIp", IpHelper.getHostIp().get());
68                     put("hostname", IpHelper.getHostName().get());
69                     put("level", loggingEvent.getLevel().toString());
70 
71                     put("className", loggingEvent.getLocationInformation().getClassName());
72                     put("methodName", loggingEvent.getLocationInformation().getMethodName());
73                     put("data", loggingEvent.getMessage());
74 
75                     if (loggingEvent.getThrowableInformation() != null && !CollectionUtils.isEmpty(loggingEvent.getThrowableInformation().getThrowableStrRep())) {
76                         put("exception", String.join(";", loggingEvent.getThrowableInformation().getThrowableStrRep()));
77                     } else {
78                         put("exception", "");
79                     }
80                 }
81             };
82         }
83 
84         @Override
85         public void run() {
86             try {
87                 EsAppend.esClient.createIndex(map);
88             } catch (IOException e) {
89                 e.printStackTrace();
90             }
91         }
92     }
93 }

如上程式碼有一些自定義屬性如confName,這個對應log4j.properties檔案中自定義的confName屬性,也就是說程式碼中confName和配置檔案中的節點對應,可以直接get獲取值;如下log4j配置資訊:

 1 # Set root logger level to DEBUG and its only appender to A1.
 2 log4j.rootLogger=DEBUG,esAppend
 3 # A1 is set to be a ConsoleAppender.
 4 log4j.appender.esAppend=log.EsAppend
 5 #自定義es配置檔案
 6 log4j.appender.esAppend.confName=eslog.properties
 7 
 8 # A1 uses PatternLayout.
 9 #log4j.appender.esAppend.layout=org.apache.log4j.PatternLayout
10 #log4j.appender.esAppend.layout

上面PatternLayout配置是註釋的,因為對於我寫es來說沒啥用處,不做格式化處理所以可以直接忽略;

  1. log4j.rootLogger:log4根節點配置,根節點配置debug其他子節點不重新定義的話使用繼承模式;esAppend是隨意定義append名稱
  2. log4j.appender.esAppend:這裡的esAppend對應rootLogger節點上隨意定義的名稱;log.EsAppend是隻對應append的程式碼實現類
  3. log4j.appender.esAppend.confName:自定義es配置節點,程式碼中get獲取即可(注意:activateOptions方法)

下面列出擴充套件append時需要注意的地方:

  1. 如果log4j.properties檔案中有自定義屬性,那麼activateOptions方法是必須的,不然通過屬性get是獲取不了log4j.properties檔案中自定義屬性的值
  2. 因為是使用執行緒池來操作寫es,所以順序方面不能保證,因此最好插入時間列
  3. 對應用程式而言,es沒法主動區分請求處理伺服器是哪臺,所以需要插入日誌時最好帶上伺服器ip或者唯一標識
  4. 時間格式:yyyy-MM-dd'T'HH:mm:ss.SSSZ ,目前kibana搜尋預設支援的時間格式

kibana基礎使用

有了上面步驟後,我們來到測試環節,建一個測試介面,並且請求插入一些資料:

 1     static Logger logger = Logger.getLogger(TestController.class);
 2 
 3     @GetMapping("/hello/{nickname}")
 4     public String getHello(@PathVariable String nickname) {
 5         String str = String.format("你好,%s", nickname);
 6         logger.debug(str);
 7         logger.info(str);
 8         logger.error(str);
 9         return str;
10     }

當我們請求介面 http://localhost:4020/hello/神牛003 一次後,通過es header檢視內容如下:

這種方式不怎麼直觀,可以通過kibana來檢視,如下先配置kibana使用的索引:

最後通過Discover介面搜尋相關日誌資訊:

相關文章