日志埋点功能

前言

开发中经常会有日志埋点需求, 用于统计接口的请求量、处理速度等等,为此本篇幅从一下几个维度进行分析,从零到有搭建。

技术架构解析

实现日志埋点功能,从字面意思就可以想到功能大致分为两个方向:
1、 埋点功能(logback + 封装通用SDK方法 + 共享文件夹(如果是多台机子部署服务器))
2、 解析日志(解析logback中约定的文件地址 + 逐个文件解析每行(记录状态)

埋点功能

利用原生的logback,配置好xml即可实现日志保存,不需要考虑批量刷盘等问题,而封装好通用的SDK是为了方便后续系统使用 。 共享文件夹是为了后续不同微服务部署在不同机器上时,设置的绝对路径文件地址能被后续定时任务解析到文件。

解析日志

解析日志就是文件记录+文件解析的过程,可以建立两张表。一张记录有哪些文件,这些文件是否被解析过,状态如何,另一张表即日志文件表,保存每一次埋点的数据。解析过程会严格安装之前的目录进行,涉及到服务器名、时间等需要特别留意。

埋点功能实现

logback.xml

前置知识需要用到logback,不懂的配置logback.xml的同学可以看下下面的链接:logback 从入门到精通 超详细配置说明

多的不说,少的不唠,直接贴上logback的配置(这里只贴埋点功能涉及的配置)

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <contextName>front-oms</contextName>
    <jmxConfigurator />
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{smy.requestFlowNo}|%X{smy.consumerIp}] %logger{56}.%method\(\):%L - %msg%n</pattern>
        </layout>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ALL</level>
        </filter>
    </appender>

    <!--埋点相关配置如下-->
    <!--系统编码(填写相应的系统编码)-->
    <substitutionProperty name="sys" value="td-b2b-front" />

    <appender name="file.biz.log" class="ch.qos.logback.classic.sift.SiftingAppender">
        <discriminator class="ch.qos.logback.classic.sift.MDCBasedDiscriminator">
            <key>biz.log.file.name</key>
            <defaultValue>default</defaultValue>
        </discriminator>
        <sift>
            <appender name="file.biz.info-{biz.log.file.name}" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <file>/bizdata/jss/businessdata/monitor/logs/${sys}/${biz.log.file.name}_${datetime}.txt</file>
                <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                    <!-- 定义文件滚动时的文件名的格式 -->
                    <fileNamePattern>/bizdata/jss/businessdata/monitor/logs/${sys}/${biz.log.file.name}_%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
                    <!-- 日期大小分割 -->
                    <maxFileSize>20MB</maxFileSize>
                    <maxHistory>60</maxHistory>
                    <totalSizeCap>20GB</totalSizeCap>
                </rollingPolicy>
                <encoder>
                    <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%thread|%X{smy.requestFlowNo}|%X{smy.consumerIp}|%X{class.method.line} - %msg%n
                    </pattern>
                </encoder>
            </appender>
        </sift>
    </appender>

    <logger name="biz.log" additivity="false">
        <level value="info"/>
        <appender-ref ref="file.biz.log" />
    </logger>

</configuration>

logback.xml文件主要就干几件事:
1、配置一个<appender>标签 ,确认埋点的日志文件存在什么目录,目录结构是什么样的,有几层目录
2、每个文件的大小多大,如何打包,日志的显示样式如何
3、配置一个<logger> 标签指定我只解析biz.log 名字的,在后面SDK中体现,LoggerFactory.getLogger(“biz.log”)。
注意
1)<appender>标签中的biz.log.file.name 就是文件名,在后面的SDK通用方法里会存,这里就能读的到
2)目录层级有多少层,后面解析的日志的时候,会从层级里拿sys名字,文件日期等

通用SDK

通用sdk就是如果你有好多个项目要用这个功能,就可以打包成本地仓库,以后maven引一下就可以直接用了

少的不说,多的不唠,这里也直接贴上SDK代码

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

/**
 * @description 耗时业务日志输出
 * @Date 2023/9/10 16:50
 **/
public class MonitorBizLog {
   
    private static final Logger bizlogger = LoggerFactory.getLogger("biz.log");

    /**
     * mdc-key:类名+方法名+行
     */
    private final static String CLASS_METHOD_LINE = "class.method.line";

    /**
     * mdc-key:日志输出文件名
     */
    private final static String FILE_NAME = "biz.log.file.name";

    private final static String CUR_DATETIME = "datetime";
	 /**
     * DEFAULT :租户,有用到就用,每用到就default
     */
    private final static String DEFAULT = "default";

    /**
     * 获取本机ip
     */
    private final static String ethIpAddress = HostUtil.getEthIpAddress();

    /**
     * 记录业务日志
     *
     * @param subBizNo    业务码 (必穿)
     * @param logUniqueId 日志唯一标识 (必穿)
     * @param projectNo   项目号
     * @param orderNo     订单号
     * @param transField  透传字段
     */
    public static void info(String subBizNo, String logUniqueId, String projectNo, String orderNo, String transField) {
   
        try {
   
            String curDate = DateUtil.getCurDate("yyyy-MM-dd");
            //获取租户编号,获取不到则默认为default
            String fbAccessNo = getFbAccessNo();
            if (StringUtils.isEmpty(fbAccessNo)) {
   
                fbAccessNo = DEFAULT;
            }
            //设置mdc值,供logback.xml使用
            //日志输出使用,因日志由此工具类输出,日志输出的“类.方法.行”为本方法,此处获取调用方“类.方法.行”
            System.setProperty(CUR_DATETIME, curDate);
            MDC.put(CLASS_METHOD_LINE, getCallingClassMethodName());
            //日志输出目录:文件名
            MDC.put(FILE_NAME, curDate + "/" + fbAccessNo + "_" + ethIpAddress);

            //日志打印
            bizlogger.info("业务耗时监控|{}|{}|{}|{}|{}", subBizNo, logUniqueId, projectNo, orderNo, transField);
        } catch (Exception e) {
   
            bizlogger.info("业务耗时异常 info subBizNo:{} logUniqueId:{} projectNo:{} orderNo:{} transField:{}", subBizNo, logUniqueId, projectNo, orderNo, transField, e);
        } finally {
   
            //清空mdc值
            MDC.remove(FILE_NAME);
            MDC.remove(CLASS_METHOD_LINE);
        }
    }


    /**
     * 获取调调用类名+方法+行号
     *
     * @return 类名+方法+行号
     */
    private static String getCallingClassMethodName() {
   
        String name = null;
        try {
   
            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();

            if (stackTrace.length >= 4) {
   
                StackTraceElement caller = stackTrace[3];
                name = caller.getClassName() + "." + caller.getMethodName() + ":" + caller.getLineNumber();
            }
        } catch (Exception e) {
   
            bizlogger.info("业务耗时异常 getCallingClassMethodName", e);
        }
        return name;
    }
}


import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;

/**
 * @Date 2022-5-31 17:51:43
 */
public class HostUtil {
   

    public static void main(String[] args) {
   
        String ethIpAddress = getEthIpAddress();
        System.err.println(ethIpAddress);
    }

    public static String getEthIpAddress() {
   
        try {
   
            // 获取本地主机网络接口列表
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();

            // 遍历所有网络接口
            while (interfaces.hasMoreElements()) {
   
                NetworkInterface ni = interfaces.nextElement();

                // 只处理物理网卡
                if (!ni.isVirtual() && !ni.isLoopback() && !ni.isPointToPoint() && ni.isUp()) {
   
                    // 获取该接口的所有IP地址
                    Enumeration<InetAddress> addresses = ni.getInetAddresses();

                    // 遍历IP地址列表
                    while (addresses.hasMoreElements()) {
   
                        InetAddress addr = addresses.nextElement();

                        // 只处理IPv4地址 && eth 網卡地址
                        if (addr instanceof Inet4Address && ni.getName().startsWith("eth")) {
   
                            // 输出IP地址和子网掩码
                            return addr.getHostAddress();
                        }
                    }
                }
            }
        } catch (SocketException e) {
   
            e.printStackTrace();
        }
        return null;
    }
}

SDK主要干这几件事
1、确认subBizNo业务号和logUniqueId, 这会在后面生产日志表的时候使用,其他字段都是选填
2、进一步确定日期文件目录和名称, 按照当前配置最终目录:/bizdata/jss/businessdata/monitor/logs/td-b2b-front/2024-04-01/default_192.168.0.1_2024-04-01.txt
3、设置好动态参数,datetime时间、biz.log.file.name文件名、class.method.line调用类名+方法+行号
注意
这里引入了租户、ip、系统名、日期时间等概念,都应用到了目录命名中,方便后续解析的时候能清楚定位, 如不使用全部设置成defult也行。


解析日志功能实现

大体用到两张表,一张t_log_parse解析日志记录表、t_log_print_detail日志详情表

CREATE TABLE `t_log_parse` (
  `id` varchar(50) NOT NULL COMMENT 'id',
  `application_name` varchar(50) DEFAULT NULL COMMENT '应用名称',
  `ip` varchar(50) NOT NULL COMMENT '所属ip地址',
  `file_name` varchar(100) DEFAULT NULL COMMENT '文件名称',
  `file_path` varchar(255) DEFAULT NULL COMMENT '文件路径',
  `file_line` int(11) DEFAULT NULL COMMENT '文件解析行号',
  `status` varchar(50) DEFAULT NULL COMMENT '解析状态 init初始化、progressing解析中、finish解析完成\n',
  `create_datetime` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_datetime` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='解析日志记录表';

CREATE TABLE `t_log_print_detail` (
  `id` varchar(50) NOT NULL COMMENT 'id',
  `log_record_id` varchar(50) DEFAULT NULL COMMENT '关联解析文件记录id',
  `parent_biz_no` varchar(50) DEFAULT NULL COMMENT '父业务码',
  `sub_biz_no` varchar(50) DEFAULT NULL COMMENT '子业务码',
  `unique_id` varchar(50) DEFAULT NULL COMMENT '日志唯一标识',
  `project_no` varchar(50) DEFAULT NULL COMMENT '项目编号',
  `order_no` varchar(50) DEFAULT NULL COMMENT '订单编号',
  `trans_field` varchar(50) DEFAULT NULL COMMENT '透传字段',
  `thread_id` varchar(100) DEFAULT NULL COMMENT '线程id',
  `type` varchar(4) NOT NULL DEFAULT '01' COMMENT '01 正常 02 卡件',
  `start_time` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间',
  `end_time` datetime(3) DEFAULT NULL COMMENT '完成时间',
  `start_datetime` bigint(13) DEFAULT NULL COMMENT '开始时间',
  `end_datetime` bigint(13) DEFAULT NULL COMMENT '结束时间',
  `create_datetime` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_datetime` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_sub_biz_no_unique_id` (`sub_biz_no`,`unique_id`) USING BTREE COMMENT '业务码+日志唯一id',
  KEY `idx_sub_biz_no_start_datetime_end_datetime` (`sub_biz_no`,`start_datetime`,`end_datetime`) USING BTREE COMMENT '业务码+开始时间+结束时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='耗时日志详情表';

解析日志记录

起个定时调度, 定时扫描先前约定路径,做好日志文件初始化。
前置准备工作:添加好枚举类、常量、文件Util、HostUtil,定时调度自行选择框架,用Spring原生也行。

前置工作

枚举

public enum LogStatusEnum implements LogEnums{
   
    /**
     * 解析状态
     */
    INIT("INIT", "初始化"),
    PROGRESSING("PROGRESSING", "解析中"),
    FINISH("FINISH", "解析完成");


    private final String code;
    private final String msg;

    LogStatusEnum(String code, String msg) {
   
        this.code = code;
        this.msg = msg;
    }

    public static LogStatusEnum judgeValue(String code) {
   
        LogStatusEnum dataType = null;
        for (LogStatusEnum t : LogStatusEnum.values()) {
   
            if (StringUtils.equals(code, String.valueOf
  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java Agent是一种Java应用程序的扩展方式,通过在应用程序启动时加载Agent,可以在不修改原始代码的情况下实现对应用程序的监控和修改。Agent可以通过字节码注入的方式,在运行时动态修改应用程序的字节码,实现埋点操作。 埋点是指在应用程序中插入一些代码,用于记录关键的业务逻辑和性能指标。通过在Java Agent中实现埋点功能,可以方便地获取应用程序的执行过程和性能数据,以及关键业务逻辑的执行情况。 Java Agent埋点的实现方式通常涉及对类加载机制的hook和字节码编译技术。Agent可以通过在类加载之前修改字节码,将需要埋点的代码动态插入到应用程序中。埋点代码可以是用于记录日志、收集性能数据、统计方法执行时间等。 与传统的静态埋点相比,Java Agent埋点的优势在于不需要修改原始代码,且可以在运行时动态修改应用程序的行为。这种方式不会对原始代码产生任何影响,也不会增加部署的复杂性。同时,Java Agent还可以提供更加细粒度的监控和修改能力,可以对特定的方法、类和类加载器进行监控和修改。 总之,Java Agent埋点是一种非侵入式的监控和修改应用程序的方式。通过加载Agent并在运行时修改字节码,可以方便地实现埋点操作,用于记录关键的业务逻辑和性能指标,从而提供更好的应用程序监控和调优能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值