本篇和大家分享的是自定义 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 的集群名称, 通过如下命令进入容器中改了配置文件 (当然可直接通过命令启动时传递参数):
docker exec -it eae7731bb6a1 /bin/bash
然后进入到 /usr/share/Elasticsearch/config 并打开 Elasticsearch.YAML 配置文件修改:
- # 集群名称
- cluster.name: "shenniu_elasticsearch"
- # 本节点名称
- node.name: master
- # 是否 master 节点
- node.master: true
- # 是否存储数据
- node.data: true
- #head 插件设置
- http.cors.enabled: true
- http.cors.allow-origin: "*"
- http.port: 9200
- transport.tcp.port: 9300
- # 可以访问的 ip
- 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 地址:
- docker exec -it 67a0ef871ef7 /bin/bash
- cd etc/
- cd kibana/
VIM kibana.YAML
配置内容修改如:
- server.host: '0.0.0.0'
- 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 信息:
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- <version>1.2.17</version>
- </dependency>
- <dependency>
- <groupId>org.Elasticsearch.client</groupId>
- <artifactId>Elasticsearch-REST-high-level-client</artifactId>
- <version>5.6.16</version>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.56</version>
- <scope>compile</scope>
- </dependency>
首先要明确用代码操作 es(或其他第三方服务), 往往都需 ip(域名)+ 端口, 这里我的配置信息:
- #es 连接串 ','分割
- es.links=http://192.168.181.7:9200,http://localhost:9200
- es.indexName=eslog_shenniu003
然后有如下封装代码:
- public class EsRestHighLevelClient {
- /**
- * new HttpHost("192.168.181.44", 9200, "http")
- */
- private HttpHost[] hosts;
- private String index;
- private String type;
- private String id;
- public EsRestHighLevelClient(String index, String type, String id, HttpHost[] hosts) {
- this.hosts = hosts;
- this.index = index;
- this.type = type;
- this.id = id;
- }
- /**
- * @param index
- * @param type
- * @param hosts
- */
- public EsRestHighLevelClient(String index, String type, String... hosts) {
- this.hosts = IpHelper.getHostArrByStr(hosts);
- this.index = index;
- this.type = type;
- }
- public RestHighLevelClient client() {
- Assert.requireNonEmpty(this.hosts, "无效的 es 连接");
- RestHighLevelClient client = new RestHighLevelClient(
- RestClient.builder(this.hosts).build()
- );
- return client;
- }
- public IndexRequest indexRequest() {
- return new IndexRequest(this.index, this.type, this.id);
- }
- public RestStatus createIndex(Map<String, Object> map) throws IOException {
- return client().
- index(this.indexRequest().source(map)).
- status();
- }
- }
这里还涉及到了一个 IpHelper 辅助类, 主要用来拆分多个 ip 信息参数, 里面涉及到正则匹配方式:
- public class IpHelper {
- private static final String strHosts = "(?<h>[^:]+)://(?<ip>[^:]+):(?<port>[^/|,]+)";
- private static final Pattern hostPattern = Pattern.compile(strHosts);
- public static Optional<String> getHostIp() {
- try {
- return Optional.ofNullable(InetAddress.getLocalHost().getHostAddress());
- } catch (UnknownHostException e) {
- e.printStackTrace();
- }
- return Optional.empty();
- }
- public static Optional<String> getHostName() {
- try {
- return Optional.ofNullable(InetAddress.getLocalHost().getHostName());
- } catch (UnknownHostException e) {
- e.printStackTrace();
- }
- return Optional.empty();
- }
- /**
- * strHosts:"http://192.168.0.1:9200","http://192.168.0.1:9200","http://192.168.0.1:9200"
- *
- * @return
- */
- public static List<HttpHost> getHostsByStr(String... strHosts) {
- List<HttpHost> hosts = new ArrayList<>();
- for (int i = 0; i <strHosts.length; i++) {
- String[] hostArr = strHosts[i].split(",");
- for (String strHost : hostArr) {
- Matcher matcher = hostPattern.matcher(strHost);
- if (matcher.find()) {
- String http = matcher.group("h");
- String ip = matcher.group("ip");
- String port = matcher.group("port");
- if (Strings.isEmpty(http) || Strings.isEmpty(ip) || Strings.isEmpty(port)) {
- continue;
- }
- hosts.add(new HttpHost(ip, Integer.valueOf(port), http));
- }
- }
- }
- return hosts;
- }
- public static HttpHost[] getHostArrByStr(String... strHosts) {
- List<HttpHost> list = getHostsByStr(strHosts);
- return Arrays.copyOf(list.toArray(), list.size(), HttpHost[].class);
- }
- }
自定义 log4j 的 appender
对于日志来说 log4j 是大众化的, 有很多语言也在用这种方式来记录, 使用它相当于一种共识; 它提供了很好的扩展, 很方便达到把日志记录到数据库, 文本获取其他自定义代码方式中; 定义一个 EsAppend 类, 继承 AppenderSkeleton 类, 代码上我们要做的仅仅重写如下方法即可:
本期咋们实现的步骤是:
activateOptions 方法获取自定义配置信息 (es 连接串, 写 es 的日志索引名等)
append 方法获取并记录 logger.xx() 等级的日志
ExecutorService 线程池类操作多个线程执行 execute 提交日志到 es
具体实现代码如下, 可按照上面步骤分析:
- public class EsAppend extends AppenderSkeleton {
- //es 客户端
- private static EsRestHighLevelClient esClient;
- //es 配置文件名
- private String confName;
- private ExecutorService executorService = Executors.newFixedThreadPool(10);
- protected void append(LoggingEvent loggingEvent) {
- if (this.isAsSevereAsThreshold(loggingEvent.getLevel())) {
- executorService.execute(new EsAppendTask(loggingEvent, this.layout));
- // new EsAppendTask(loggingEvent, this.layout).run();
- }
- }
- public void close() {
- this.closed = true;
- }
- public boolean requiresLayout() {
- return false;
- }
- @Override
- public void activateOptions() {
- super.activateOptions();
- try {
- System.out.println("初始化 - EsAppend...");
- if (this.getConfName() == null || this.getConfName().isEmpty()) {
- this.setConfName("eslog.properties");
- }
- PropertiesHelper propertiesHelper = new PropertiesHelper(this.getConfName());
- //es hosts
- String strHosts = propertiesHelper.getProperty("es.links", "http://127.0.0.1:9200");
- //es 日志索引
- String esLogIndex = propertiesHelper.getProperty("es.indexName", "eslog");
- esClient = new EsRestHighLevelClient(esLogIndex, "docs", strHosts);
- System.out.println("初始化完成 - EsAppend");
- } catch (Exception ex) {
- System.out.println("初始化失败 - EsAppend");
- ex.printStackTrace();
- }
- }
- public String getConfName() {
- return confName;
- }
- public void setConfName(String confName) {
- this.confName = confName;
- }
- /**
- * runable 写 es
- */
- class EsAppendTask implements Runnable {
- private HashMap<String, Object> map;
- public EsAppendTask(LoggingEvent loggingEvent, Layout layout) {
- SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSSZ");
- map = new HashMap<String, Object>() {
- {
- put("timeStamp",df.format(new Date()));
- put("serverIp", IpHelper.getHostIp().get());
- put("hostname", IpHelper.getHostName().get());
- put("level", loggingEvent.getLevel().toString());
- put("className", loggingEvent.getLocationInformation().getClassName());
- put("methodName", loggingEvent.getLocationInformation().getMethodName());
- put("data", loggingEvent.getMessage());
- if (loggingEvent.getThrowableInformation() != null && !CollectionUtils.isEmpty(loggingEvent.getThrowableInformation().getThrowableStrRep())) {
- put("exception", String.join(";", loggingEvent.getThrowableInformation().getThrowableStrRep()));
- } else {
- put("exception", "");
- }
- }
- };
- }
- @Override
- public void run() {
- try {
- EsAppend.esClient.createIndex(map);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
如上代码有一些自定义属性如 confName, 这个对应 log4j.properties 文件中自定义的 confName 属性, 也就是说代码中 confName 和配置文件中的节点对应, 可以直接 get 获取值; 如下 log4j 配置信息:
- # Set root logger level to DEBUG and its only appender to A1.
- log4j.rootLogger=DEBUG,esAppend
- # A1 is set to be a ConsoleAppender.
- log4j.appender.esAppend=log.EsAppend
- # 自定义 es 配置文件
- log4j.appender.esAppend.confName=eslog.properties
- # A1 uses PatternLayout.
- #log4j.appender.esAppend.layout=org.apache.log4j.PatternLayout
- #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 基础使用
有了上面步骤后, 我们来到测试环节, 建一个测试接口, 并且请求插入一些数据:
- static Logger logger = Logger.getLogger(TestController.class);
- @GetMapping("/hello/{nickname}")
- public String getHello(@PathVariable String nickname) {
- String str = String.format("你好,%s", nickname);
- logger.debug(str);
- logger.info(str);
- logger.error(str);
- return str;
- }
当我们请求接口 http://localhost:4020/hello/ 神牛 003 一次后, 通过 es header 查看内容如下:
这种方式不怎么直观, 可以通过 kibana 来查看, 如下先配置 kibana 使用的索引:
最后通过 Discover 界面搜索相关日志信息:
来源: https://yq.aliyun.com/articles/702871