脑洞:字节码加强 (1) 日志收集方案

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 日志收集方案 动态日志level APM方案埋点解析 tomcat访问日志收集 业务问题排查方案 性能测试

背景

需求:
java技术栈,要接入目前所有的项目到日志中心,需求看似比较简单

但是实施的过程中各种问题,项目所属不同部门,使用开发框架不同,人员能力水平不同,

  1. 方案选择:

    • 方案1 各个项目接入logstash,

         各类日志框架都有,log4j logback log4j2(apache.logging.log4j),十分混乱,而且开发人员需要引入jar,有可能会与现有的jar版本冲突,有一定的开发成本.
    • 方案2 基于现有的文件日志,filebeat收集之后,再进行分析,发送到消息队列,然后转储到elastic search.

         可是日志格式不统一,kao
    • 方案3 基于字节码加强,

        重写log4j logback log4j2中关于打印日志的方法,进行拦截.
        加入我们自己的逻辑,将日志以可控的形式进行记录,将消息直接入mq,再由消费端进行消费(可以是logstash或者自研的处理程序),然后将此程序集成至基础docker镜像,JAVA_OPTS参数设定javaagent路径即可
  2. 实施:

字节码加强框架选择:asm bytebuddy jvm-sandbox,前两个坚决不用,原因:我不相信我自己写的代码,烂+懒
所以选jvm-sandbox github地址
还有另一个原因,类隔离,对加强的项目没有影响
项目里面有个比较好理解的demo,拦截异常的, 可以在这里查看

上代码,再解释(log4j)

new EventWatchBuilder(moduleEventWatcher)
                .onClass("org.apache.log4j.Category")
                .onBehavior("callAppenders")
                .onWatch(new AdviceListener() {
                    @Override
                    public void afterReturning(Advice advice) {
                        try {
                            //定义一个错误级别(默认保持与ERROR一致
                            int errorLevel = 40000;
                            //获取event变量
                            Object event = advice.getParameterArray()[0];
                            // 获取event对应的日志级别
                            int level = invokeMethod(invokeMethod(event, "getLevel"), "toInt");
                            // 获取日志的打印时间
                            long timeStamp = invokeField(event, "timeStamp");
                            // 获取日志格式化后的字符串
                            String msg = invokeMethod(event, "getRenderedMessage");
                            // 获取logger name
                            String loggerName = invokeMethod(event, "getLoggerName");
                            // 获取线程名
                            String threadName = invokeMethod(event, "getThreadName");
                            // 如果小于默认的错误级别
                            if (level < errorLevel) {
                                // 将日志信息发送到本地队列,等待(异步)发送
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, null);
                            } else {
                                // 如果是错误级别,定义throwable变量
                                Throwable throwable = null;
                                // 获取ThrowableInformation信息
                                Object throwProxy = invokeMethod(event, "getThrowableInformation");
                                if (throwProxy != null) {
                                    // 从throwable代理类中获取真实错误信息
                                    throwable = invokeMethod(throwProxy, "getThrowable");
                                }
                                // 将带有错误信息的消息发送到本地消息队列,待发送
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, throwable);
                                // 接入点评的CAT,将错误信息输出到CAT大盘,用于报警
                                Cat.logError("[ERROR] " + msg, throwable);
                                // 设定当前的context有错误信息,做后续处理
                                Cat.getManager().setHasError(true);
                            }

                        } catch (Exception ex) {
                            //黑洞
                        }
                    }
                });

org.apache.log4j.Category.callAppenders 这个方法是log4j框架,在write message之前调用的方法,是将符合设置的level的message写入各个配置中定义的appender.我们加强这段代码,相当于增加了一个自定义的 appender,把数据输入进去.
说明一下,里面用了反射,是使用了缓存的反射,是jvm-sandbox的机制,因为classloader的类加载策略,目前只能使用反射,经测试,并不会对性能有明显损失,后续文章会将性能测试贴出.

接下来 logback加强,一样的类似,基本和log4j没有区别

new EventWatchBuilder(moduleEventWatcher)
                .onClass("ch.qos.logback.classic.Logger")
                .onBehavior("callAppenders")
                .onWatch(new AdviceListener() {
                    @Override
                    public void afterReturning(Advice advice) {
                        try {
                            int errorLevel = 40000;
                            Object event = advice.getParameterArray()[0];
                            int level = invokeMethod(invokeMethod(event, "getLevel"), "toInt");
                            long timeStamp = invokeMethod(event, "getTimeStamp");
                            String msg = invokeMethod(event, "getFormattedMessage");
                            String loggerName = invokeMethod(event, "getLoggerName");
                            String threadName = invokeMethod(event, "getThreadName");
                            if (level < errorLevel) {
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, null);
                            } else {
                                Throwable throwable = null;
                                Object throwProxy = invokeMethod(event, "getThrowableProxy");
                                if (throwProxy != null) {
                                    throwable = invokeMethod(throwProxy, "getThrowable");
                                }
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, throwable);
                                Cat.logError("[ERROR] " + msg, throwable);
                                Cat.getManager().setHasError(true);
                            }
                        } catch (Exception ex) {
                            //黑洞
                        }
                    }
                });

不解释 ,接下来 log4j2 ,比较类似 ,区别是,log4j2的level值,和logback log4j不同, 是反过来的,而且值也不同,所以做了一个转换

new EventWatchBuilder(moduleEventWatcher)
                .onClass("org.apache.logging.log4j.core.config.LoggerConfig")
                .onBehavior("callAppenders")
                .onWatch(new AdviceListener() {
                    @Override
                    public void afterReturning(Advice advice) {
                        try {
                            int errorLevel = 40000;
                            Object event = advice.getParameterArray()[0];
                            int level = invokeMethod(invokeMethod(event, "getLevel"), "intLevel");
                            if (level >= 500) {
                                level = 10000;
                            } else if (level >= 400) {
                                level = 20000;
                            } else if (level >= 300) {
                                level = 30000;
                            } else if (level >= 200) {
                                level = 40000;
                            } else if (level >= 100) {
                                level = 40000;
                            } else {
                                level = 40000;
                            }
                            long timeStamp = invokeMethod(event, "getTimeMillis");
                            String msg = invokeMethod(invokeMethod(event, "getMessage"), "getFormattedMessage");
                            String loggerName = invokeMethod(event, "getLoggerName");
                            String threadName = invokeMethod(event, "getThreadName");
                            if (level < errorLevel) {
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, null);
                            } else {
                                Throwable throwable = invokeMethod(event, "getThrown");
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, throwable);
                                Cat.logError("[ERROR] " + msg, throwable);
                                Cat.getManager().setHasError(true);
                            }
                        } catch (Exception ex) {
                            //黑洞
                        }
                    }
                });
ok ,以上就是第三种日志收集方案的核心代码,本系列文章完成之前会开放源码供参考.

脑洞:字节码加强 (2) 动态日志level
脑洞:字节码加强 (3) APM方案埋点解析
脑洞:字节码加强 (4) tomcat访问日志收集
脑洞:字节码加强 (5) 业务问题排查方案
脑洞:字节码加强 (6) 性能测试

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
运维 安全 API
统一接入API赋能开发者:自动高效、灵活编排的云产品日志采集方案
随着企业对网络安全和数据安全防护水平要求的逐步提升,企业管理对企业生产运维过程中所产生的日志数据,在留存处理、权限隔离、跨境合规、数据汇总等方面提出了更高阶的需求。为了满足大客户及一些国际化客户安全合规、简单快速地接入日志、使用日志、操作日志,我们提出了一种新的解决方案——“云产品统一接入API”。统一接入API主要针对阿里云云产品日志类型,以API的方式提供企业或组织用户快速上手,编排灵活的日志采集方案。
|
3月前
|
存储 监控 Serverless
阿里泛日志设计与实践问题之Grafana Loki在日志查询方案中存在哪些设计限制,如何解决
阿里泛日志设计与实践问题之Grafana Loki在日志查询方案中存在哪些设计限制,如何解决
|
8天前
|
消息中间件 存储 监控
微服务日志监控的挑战及应对方案
【10月更文挑战第23天】微服务化带来模块独立与快速扩展,但也使得日志监控复杂。日志作用包括业务记录、异常追踪和性能定位。
|
2月前
|
Kubernetes API Docker
跟着iLogtail学习容器运行时与K8s下日志采集方案
iLogtail 作为开源可观测数据采集器,对 Kubernetes 环境下日志采集有着非常好的支持,本文跟随 iLogtail 的脚步,了解容器运行时与 K8s 下日志数据采集原理。
|
6月前
|
存储 数据采集 Kubernetes
一文详解K8s环境下Job类日志采集方案
本文介绍了K8s中Job和Cronjob控制器用于非常驻容器编排的场景,以及Job容器的特点:增删频率高、生命周期短和突发并发大。文章重点讨论了Job日志采集的关键考虑点,包括容器发现速度、开始采集延时和弹性支持,并对比了5种采集方案:DaemonSet采集、Sidecar采集、ECI采集、同容器采集和独立存储采集。对于短生命周期Job,建议使用Sidecar或ECI采集,通过调整参数确保数据完整性。对于突发大量Job,需要关注服务端资源限制和采集容器的资源调整。文章总结了不同场景下的推荐采集方案,并指出iLogtail和SLS未来可能的优化方向。
|
3月前
|
Java 编译器 数据库
异步日志方案——spdlog
异步日志方案——spdlog
|
3月前
|
存储 Prometheus Kubernetes
在K8S中,如何收集K8S日志?有哪些方案?
在K8S中,如何收集K8S日志?有哪些方案?
|
3月前
|
存储 Kubernetes Java
阿里泛日志设计与实践问题之在写多查少的降本场景下,通过SLS Scan方案降低成本,如何实现
阿里泛日志设计与实践问题之在写多查少的降本场景下,通过SLS Scan方案降低成本,如何实现
|
4月前
|
Java API Apache
通用快照方案问题之Feign对日志的记录如何解决
通用快照方案问题之Feign对日志的记录如何解决
28 0
|
4月前
|
存储 运维 监控
在Spring Boot中集成分布式日志收集方案
在Spring Boot中集成分布式日志收集方案