本篇和大家分享的是自定義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類,程式碼上我們要做的僅僅重寫如下方法即可:
本期咋們實現的步驟是:
- activateOptions方法獲取自定義配置資訊(es連線串,寫es的日誌索引名等)
- append方法獲取並記錄logger.xx()等級的日誌
- 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來說沒啥用處,不做格式化處理所以可以直接忽略;
- log4j.rootLogger:log4根節點配置,根節點配置debug其他子節點不重新定義的話使用繼承模式;esAppend是隨意定義append名稱
- log4j.appender.esAppend:這裡的esAppend對應rootLogger節點上隨意定義的名稱;log.EsAppend是隻對應append的程式碼實現類
- log4j.appender.esAppend.confName:自定義es配置節點,程式碼中get獲取即可(注意:activateOptions方法)
下面列出擴充套件append時需要注意的地方:
- 如果log4j.properties檔案中有自定義屬性,那麼activateOptions方法是必須的,不然通過屬性get是獲取不了log4j.properties檔案中自定義屬性的值
- 因為是使用執行緒池來操作寫es,所以順序方面不能保證,因此最好插入時間列
- 對應用程式而言,es沒法主動區分請求處理伺服器是哪臺,所以需要插入日誌時最好帶上伺服器ip或者唯一標識
- 時間格式: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介面搜尋相關日誌資訊: