自定义log4j的appender写es日志

本文涉及的产品
Elasticsearch Serverless检索通用型,资源抵扣包 100CU*H
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本篇和大家分享的是自定义log4j的appender,用es来记录日志并且通过kibana浏览es记录;就目前互联网或者一些中大型公司通常会用到第三方组合elk,其主要用写数据到es中,然后通过可视化工具kibana来做直观数据查看和统计;本篇内容节点如下:docker快速搭建es,es he...

本篇和大家分享的是自定义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.yml配置文件修改:

#集群名称
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:
image


通常搭配es的是kibana(可视化工具),用来查看es的数据和做一些统计(如数量统计,按列聚合统计等),这里通过docker run启动kibana镜像后,我们还需要让其关联上es才行,同样通过docker exec去修改里面配置信息,主要在里面配置es地址:

docker exec -it 67a0ef871ef7 /bin/bash
cd etc/
cd kibana/
vim kibana.yml

配置内容修改如:

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界面,让我们正确配置:
image

封装写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类,代码上我们要做的仅仅重写如下方法即可:
image

本期咋们实现的步骤是:

  1. activateOptions方法获取自定义配置信息(es连接串,写es的日志索引名等)
  2. append方法获取并记录logger.xx()等级的日志
  3. 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来说没啥用处,不做格式化处理所以可以直接忽略;

  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基础使用

有了上面步骤后,我们来到测试环节,建一个测试接口,并且请求插入一些数据:

    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查看内容如下:
image

这种方式不怎么直观,可以通过kibana来查看,如下先配置kibana使用的索引:
image

最后通过Discover界面搜索相关日志信息:
image
image

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
消息中间件 运维 监控
智能运维,由你定义:SAE自定义日志与监控解决方案
通过引入 Sidecar 容器的技术,SAE 为用户提供了更强大的自定义日志与监控解决方案,帮助用户轻松实现日志采集、监控指标收集等功能。未来,SAE 将会支持 istio 多租场景,帮助用户更高效地部署和管理服务网格。
288 51
|
5月前
|
监控 安全 Apache
什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球广泛使用的Web服务器软件,支持超过30%的活跃网站。它通过接收和处理HTTP请求,与后端服务器通信,返回响应并记录日志,确保网页请求的快速准确处理。Apache日志分为访问日志和错误日志,对提升用户体验、保障安全及优化性能至关重要。EventLog Analyzer等工具可有效管理和分析这些日志,增强Web服务的安全性和可靠性。
148 9
|
3月前
|
存储 SQL 关系型数据库
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log、原理、写入过程;binlog与redolog区别、update语句的执行流程、两阶段提交、主从复制、三种日志的使用场景;查询日志、慢查询日志、错误日志等其他几类日志
247 35
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
|
2月前
|
监控 Java 应用服务中间件
Tomcat log日志解析
理解和解析Tomcat日志文件对于诊断和解决Web应用中的问题至关重要。通过分析 `catalina.out`、`localhost.log`、`localhost_access_log.*.txt`、`manager.log`和 `host-manager.log`等日志文件,可以快速定位和解决问题,确保Tomcat服务器的稳定运行。掌握这些日志解析技巧,可以显著提高运维和开发效率。
150 13
|
2月前
|
缓存 Java 编译器
|
3月前
|
存储 缓存 关系型数据库
图解MySQL【日志】——Redo Log
Redo Log(重做日志)是数据库中用于记录数据页修改的物理日志,确保事务的持久性和一致性。其主要作用包括崩溃恢复、提高性能和保证事务一致性。Redo Log 通过先写日志的方式,在内存中缓存修改操作,并在适当时候刷入磁盘,减少随机写入带来的性能损耗。WAL(Write-Ahead Logging)技术的核心思想是先将修改操作记录到日志文件中,再择机写入磁盘,从而实现高效且安全的数据持久化。Redo Log 的持久化过程涉及 Redo Log Buffer 和不同刷盘时机的控制参数(如 `innodb_flush_log_at_trx_commit`),以平衡性能与数据安全性。
123 5
图解MySQL【日志】——Redo Log
|
2月前
|
消息中间件 运维 监控
智能运维,由你定义:SAE自定义日志与监控解决方案
SAE(Serverless应用引擎)是阿里云推出的全托管PaaS平台,致力于简化微服务应用开发与管理。为满足用户对可观测性和运维能力的更高需求,SAE引入Sidecar容器技术,实现日志采集、监控指标收集等功能扩展,且无需修改主应用代码。通过共享资源模式和独立资源模式,SAE平衡了资源灵活性与隔离性。同时,提供全链路运维能力,确保应用稳定性。未来,SAE将持续优化,支持更多场景,助力用户高效用云。
172 2
|
2月前
|
SQL 存储 关系型数据库
简单聊聊MySQL的三大日志(Redo Log、Binlog和Undo Log)各有什么区别
在MySQL数据库管理中,理解Redo Log(重做日志)、Binlog(二进制日志)和Undo Log(回滚日志)至关重要。Redo Log确保数据持久性和崩溃恢复;Binlog用于主从复制和数据恢复,记录逻辑操作;Undo Log支持事务的原子性和隔离性,实现回滚与MVCC。三者协同工作,保障事务ACID特性。文章还详细解析了日志写入流程及可能的异常情况,帮助深入理解数据库日志机制。
222 0
|
4月前
|
SQL 关系型数据库 MySQL
MySQL事务日志-Undo Log工作原理分析
事务的持久性是交由Redo Log来保证,原子性则是交由Undo Log来保证。如果事务中的SQL执行到一半出现错误,需要把前面已经执行过的SQL撤销以达到原子性的目的,这个过程也叫做"回滚",所以Undo Log也叫回滚日志。
174 7
MySQL事务日志-Undo Log工作原理分析
|
3月前
|
存储 关系型数据库 MySQL
图解MySQL【日志】——Undo Log
Undo Log(回滚日志)是 MySQL 中用于实现事务原子性和一致性的关键机制。在默认的自动提交模式下,MySQL 隐式开启事务,每条增删改语句都会记录到 Undo Log 中。其主要作用包括:
123 0

热门文章

最新文章