自定义log4j的appender写es日志
本篇和大家分享的是自定义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界面搜索相关日志信息: