自定义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地址 ,能够看到让咋们配置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 }
当我们请求接口 神牛003 一次后,通过es header查看内容如下:
这种方式不怎么直观,可以通过kibana来查看,如下先配置kibana使用的索引:
最后通过discover界面搜索相关日志信息:
上一篇: Linux C 实现一个简单的线程池
下一篇: 去年我生日