Logback日志异步打印接入指南,输出自定义业务数据

news2024/12/26 11:15:57

背景

随着应用的请求量上升,日志输出量也会成线性比例的上升,给磁盘IO带来压力与性能瓶颈。应用也遇到了线程池满,是因为大量线程卡在输出日志。为了缓解日志同步打印,会采取异步打印日志。这样会引起日志中的追踪id丢失,不能基于追踪id查询相关日志,给问题解决带来新的挑战。

目标

  • 业务数据传递
    • 在日志输出中,业务可以传递用户自定义数据并输出到日志中,并自动构建字段索引,便于快速查询。(包含同步输出)
  • 轻量级接入

技术方案

基于SLF4J日志事件LoggingEvent和映射诊断上下文MDC

  • 在Logback日志事件LoggingEvent implements ILoggingEvent进入日志异步追加器AsyncAppender extends AsyncAppenderBase<ILoggingEvent>的队列blockingQueue之前,把数据状态临时存储到MDC适配器LogbackMDCAdaptermdcPropertyMap线程本地变量副本中。
  • 在组装日志数据前从其取出这些临时的内存数据状态,并组装到最终的日志文本数据中。

具体实现

XxxJsonLayout

package com.xxx.logback;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.contrib.json.classic.JsonLayout;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Map;

import org.apache.skywalking.apm.toolkit.trace.TraceContext;

/**
 * @author hadesy
 */
public class XxxJsonLayout extends JsonLayout {

    /**
     * 零时区 UTC 0
     * 协调世界时(UTC)
     */
    private static final ZoneId ZONE_ID_0 = ZoneId.ofOffset("UTC", ZoneOffset.UTC);
    /**
     * 东八区 UTC+8
     */
    private static final ZoneId ZONE_ID_8 = ZoneId.of("Asia/Shanghai");

    private static final String AT_TIMESTAMP_ATTR_NAME = "@timestamp";

    @Override
    protected void addCustomDataToJsonMap(Map<String, Object> map, ILoggingEvent event) {
        String timestampFormat = Instant.ofEpochMilli(event.getTimeStamp())
                .atZone(ZONE_ID_8)
                .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        map.put(TIMESTAMP_ATTR_NAME, timestampFormat);
        String atTimestampFormat = Instant.ofEpochMilli(event.getTimeStamp())
                .atZone(ZONE_ID_0)
                .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        // ES record create timestamp
        map.put(AT_TIMESTAMP_ATTR_NAME, atTimestampFormat);

        // log async appender print, app data pass by MDC
        // 日志异步打印,应用日志数据从MDC传递
        if (this.isIncludeMDC()) {
            Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
            map.putAll(MdcUtil.applyAsMap(mdcPropertyMap));
        }

        String traceId = TraceContext.traceId();
        // 日志异步打印时,追踪id为空,需要从MDC传递
        if (!isEmptyTraceId(traceId)) {
            map.put(MdcUtil.TRACE_ID_KEY, traceId);
        }
    }

    /**
     * 空的追踪身份
     */
    private static final String EMPTY_TRACE_CONTEXT_ID = "N/A";
    /**
     * 忽略的追踪
     */
    private static final String IGNORE_TRACE = "Ignored_Trace";

    private static boolean isEmptyTraceId(String traceId) {
        return traceId == null || traceId.isEmpty()
                || "N/A".equals(traceId);
    }
}

MdcUtil

package com.xxx.logback;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.slf4j.MDC;

/**
 * Proxy of {@link MDC}.
 *
 * @since 2024/4/13
 */
@Slf4j
public final class MdcUtil {
    /**
     * 追踪身份
     */
    static final String TRACE_ID_KEY = "traceId";

    public static void setTraceId() {
        MDC.put(TRACE_ID_KEY, TraceContext.traceId());
    }

    public static void setTraceId(String traceId) {
        MDC.put(TRACE_ID_KEY, traceId);
    }

    // 业务过程数据

    private static final String USER_ID = "userId";
    private static final String COACH_ID = "coachId";
    private static final String ADMIN_ID = "adminId";
    private static final String RESPONSE_TIME = "rt";
    private static final String RESPONSE_CODE = "code";
    private static final String API = "api";
    private static final String REMOTE_APP = "remoteApp";

    public static void setUserId(Long userId) {
        MDC.put(USER_ID, "" + userId);
    }

    public static void setCoachId(Long coachId) {
        MDC.put(COACH_ID, "" + coachId);
    }

    public static void setAdminId(Long adminId) {
        MDC.put(ADMIN_ID, "" + adminId);
    }

    public static void setResponseTime(long responseTime) {
        MDC.put(RESPONSE_TIME, Long.toString(responseTime));
    }

    public static void setResponseTime(int responseTime) {
        MDC.put(RESPONSE_TIME, Integer.toString(responseTime));
    }

    public static void setResponseCode(int responseCode) {
        MDC.put(RESPONSE_CODE, Integer.toString(responseCode));
    }

    public static void setResponseCode(String responseCode) {
        MDC.put(RESPONSE_CODE, responseCode);
    }

    public static void setApi(String api) {
        MDC.put(API, api);
    }

    public static void setRemoteApp(String remoteApp) {
        MDC.put(REMOTE_APP, remoteApp);
    }

    public static void clear() {
        MDC.clear();
    }

    /**
     * ES long data type
     */
    private static final Set<String> LONG_DATA_KEY_SET = Sets.newHashSet(
            USER_ID, COACH_ID, ADMIN_ID, RESPONSE_TIME
    );

    public static Map<String, Object> applyAsMap(Map<String, String> mdcPropertyMap) {
        Map<String, Object> result = new HashMap<>(mdcPropertyMap.size());

        mdcPropertyMap.forEach((key, value) -> {
            if (LONG_DATA_KEY_SET.contains(key)) {
                result.put(key, toLong(value, Long.MIN_VALUE));
            } else {
                result.put(key, value);
            }
        });

        return result;
    }

    private static long toLong(String str, long defaultValue) {
        if (str == null) {
            return defaultValue;
        } else {
            try {
                return Long.parseLong(str, 10);
            } catch (NumberFormatException e) {
                log.warn("parse string to long error, str={}", str);
                return defaultValue;
            }
        }
    }
}

XxxJsonLayoutEncoder

package com.xxx.logback;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.contrib.jackson.JacksonJsonFormatter;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;

import java.nio.charset.StandardCharsets;

public class XxxJsonLayoutEncoder extends LayoutWrappingEncoder<ILoggingEvent> {
    @Override
    public void start() {
        XxxJsonLayout jsonLayout = new XxxJsonLayout();
        jsonLayout.setContext(context);
        jsonLayout.setIncludeContextName(false);
        jsonLayout.setAppendLineSeparator(true);
        jsonLayout.setJsonFormatter(new JacksonJsonFormatter());
        jsonLayout.start();

        super.setCharset(StandardCharsets.UTF_8);
        super.setLayout(jsonLayout);
        super.start();
    }
}

应用如何接入

xxx-spring-boot-starter升级依赖版本

xxx-spring-boot-starter版本是2.7.18

<properties>
    <xxx-spring-boot.version>2.7.18</xxx-spring-boot.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.spring.boot</groupId>
            <artifactId>xxx-spring-boot-starter</artifactId>
            <version>${xxx-spring-boot.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

Logback日志配置

logback-spring.xml

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

    <springProperty scope="context" name="appName" source="spring.application.name"/>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <property name="STDOUT_PATTERN" value="%d [%t] %5p %c - %m%n"/>
    <property name="log.name" value="${appName}"/>
    <property name="log.path" value="/home/admin/logs"/>

    <springProperty scope="context" name="appName" source="spring.application.name"/>

    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="BIZ_LOG">
        <encoder class="com.xxx.logback.XxxJsonLayoutEncoder"/>
        <file>${log.path}/${log.name}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>${log.path}/${log.name}_%i.log</fileNamePattern>
            <maxIndex>1</maxIndex>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>100MB</maxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- report日志异步打印appender -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志(默认discardingThreshold=queueSize/5,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 默认队列深度,该值会影响性能.默认值256 -->
        <queueSize>256</queueSize>
        <!-- 当队列满了之后,后面阻塞的线程想要打印的日志就直接被丢弃,从而线程不会阻塞,但有可能会丢失日志-->
        <neverBlock>true</neverBlock>
        <appender-ref ref="BIZ_LOG"/>
    </appender>

    <logger name="report" level="info" additivity="false">
        <appender-ref ref="ASYNC"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="ASYNC"/>
    </root>

</configuration>

传递业务自定义数据到日志

使用MdcUtil传递用户id、教练id、优惠券id、商品id、交易订单id、支付订单id、物流订单id、api、responseTime、responseCode、追踪id等,从用户、教练、营销、商品、交易、物流等维度观测用户的实操路径。

以Dubbo Filter举例

@Activate(group = CommonConstants.PROVIDER, order = 1)
public class DubboAccessLogFilter implements Filter {

    private static final Logger REPORT_LOG = LoggerFactory.getLogger("report");

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        JSONObject logInfo = new JSONObject();
        // ...

        try {
            // 在日志输出前设置过程数据到MDC
            // 异步输出日志时,才需要设置
            MdcUtil.setTraceId();
            // 可选-同步/异步
            MdcUtil.setUserId(userId);
            MdcUtil.setCoachId(coachId);
            MdcUtil.setApi(api);
            MdcUtil.setResponseTime(responseTime);
            MdcUtil.setResponseCode(responseCode);

            // ...
            Result result = invoker.invoke(invocation);
            // ...

            return result;
        } finally {
            REPORT_LOG.info(logInfo.toJSONString());
            
            // 资源清理,需要放在日志打印后面
            MdcUtil.clear();
        }
    }
}

使用案例

xxx-class日志异步打印

按追踪维度查询操作日志

按追踪维度查询操作日志

xxx-user日志同步打印

日志同步打印

按api维度查询统计数据

api:"com.xxx.user.client.UserTokenApi/decodeTokenForCoach" and code:"00000"

按api维度查询统计数据

按用户维度查询实操路径

按用户维度查询实操路径

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1931475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记录些MySQL题集(9)

MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析 一、MySQL中的死锁现象 所谓的并发事务&#xff0c;本质上就是MySQL内部多条工作线程并行执行的情况&#xff0c;也正由于MySQL是多线程应用&#xff0c;所以需要具备完善的锁机制来避免线程不安全问题的问题产生&#…

leetcode简单题26 N.118 杨辉三角 rust描述

// 动态规划 pub fn generate(num_rows: i32) -> Vec<Vec<i32>> {let mut triangle: Vec<Vec<i32>> vec![];for i in 0..num_rows {let mut row vec![1; (i 1) as usize];for j in 1..i as usize {row[j] triangle[(i - 1) as usize][(j - 1)]…

代理高并发如何去解决?

代理高并发问题的解决方法涉及多个层面&#xff0c;包括架构设计、资源优化、技术选型等方面。以下是一些具体的解决方案&#xff1a; 1. 架构设计 分布式架构&#xff1a; 微服务架构&#xff1a;将大型应用拆分为多个小型服务&#xff0c;每个服务独立部署、扩展和升级&…

【python虚拟环境管理】【mac m3】 使用pipx安装poetry

文章目录 一. 安装 pipx二. 安装Poetry1. 安装2. advanced 操作 官网文档&#xff1a;https://python-poetry.org/docs/ pipx介绍文档&#xff1a;https://blog.51cto.com/u_15064632/2570626 一. 安装 pipx pipx 用于全局安装 Python 命令行应用程序&#xff0c;同时在虚拟环…

Qt纯代码绘制一个等待提示Ui控件

等待样式控件是我们在做UI时出场率还挺高的控件之一&#xff0c;通常情况下有如下的几种实现方式&#xff1a;1、自定义绘图&#xff0c;然后重写paintEvent函数&#xff0c;在paintEvent中绘制等待图标&#xff0c;通过QTimer更新绘制达到转圈圈的效果。2、 获取一张gif的资源…

GD32 MCU上电跌落导致启动异常如何解决

大家是否碰到过MCU上电过程中存在电源波动或者电压跌落导致MCU启动异常的问题&#xff1f;本视频将会为大家讲解可能的原因以及解决方法&#xff1a; GD32 MCU上下电复位波形如下图所示&#xff0c;上电过程中如果存在吃电的模块&#xff0c;比如wifi模块/4G模块/开启某块电路…

【Python实战因果推断】37_双重差分8

目录 Diff-in-Diff with Covariates Diff-in-Diff with Covariates 您需要学习的 DID 的另一个变量是如何在模型中包含干预前协变量。这在您怀疑平行趋势不成立&#xff0c;但条件平行趋势成立的情况下非常有用&#xff1a; 考虑这种情况&#xff1a;您拥有与之前相同的营销数…

Java面试题--JVM大厂篇之Serial GC在JVM中有哪些优点和局限性

目录 引言: 正文&#xff1a; 一、Serial GC概述 二、Serial GC的优点 三、Serial GC的局限性 结束语: 引言: 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;垃圾收集器&#xff08;Garbage Collector, GC&#xff09;是关键组件之一&#xff0c;负责自动管理内…

深度学习落地实战:手势识别

前言 大家好&#xff0c;我是机长 本专栏将持续收集整理市场上深度学习的相关项目&#xff0c;旨在为准备从事深度学习工作或相关科研活动的伙伴&#xff0c;储备、提升更多的实际开发经验&#xff0c;每个项目实例都可作为实际开发项目写入简历&#xff0c;且都附带完整的代…

部署运维之二:虚拟化

摘要&#xff1a; 在21世纪初的曙光中&#xff0c;虚拟化技术悄然萌芽&#xff0c;标志着计算领域的一次革命性飞跃。这一时期&#xff0c;通过引入虚拟化技术&#xff0c;业界实现了在单一物理服务器之上并行运行多个虚拟机的壮举&#xff0c;每个虚拟机均构筑起一个隔离而独…

【计算机网络】学习指南及导论

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️计算机网络】 文章目录 前言我们为什么要学计算机网络&#xff1f;计算机网络概述计算机网络的分类按交换技术分类按使用者分类按传输介质分类按覆盖网络分类按覆盖网络分类 局域网的连接方式有线连接…

从零实现大模型-BERT微调

The Annotated Transformer注释加量版&#xff1a;复现Transformer&#xff0c;训练翻译模型 The Annotated GPT2注释加量版&#xff1a;GPT2预训练 The Annotated BERT注释加量版&#xff1a;BERT预训练 从零实现大模型-GPT2指令微调&#xff1a;GPT2指令微调 按照顺序&am…

海外媒体发稿:葡萄牙-实现高效媒体软文发稿计划-大舍传媒

一、葡萄牙媒体环境概述 葡萄牙&#xff0c;位于欧洲大陆西南端的国家&#xff0c;拥有丰富的文化和历史。在这个国家&#xff0c;媒体行业也有着相当大的影响力。葡萄牙的媒体环境多元化&#xff0c;包括电视、广播、报纸、杂志和互联网等各个领域。 二、葡萄牙媒体发稿的重…

Win10+Docker配置TensorRT环境

1.Docker下载和安装 Docker下载:Install Docker Desktop on Windows Docker安装: 勾选直接下一步就行,安装完成后需要电脑重启。 重启后,选择Accept—>Continue without signing in—>skip survey. 可以进入下面页面,并且左下角是绿色的,显示e…

前端开发之盒子模型

目录 盒子分类 display属性 盒子内部结构特征 padding填充区 border边框区 margin外边距 盒子width和height边界 盒子分类 块级盒子&#xff08;又叫块级元素、块级标签&#xff09; 特征&#xff1a;独占一行&#xff0c;对宽度高度支持 如&#xff1a;p div ul li h1…

Vue3项目基于Axios封装request请求

在 Vue 3 的项目开发中&#xff0c;使用 Axios 进行 HTTP 请求是非常常见的作法&#xff0c;为了更方便开发者更高效的进行代码编写和项目的维护&#xff0c;可以通过再次封装 Axios 来实现。 在本文中&#xff0c;博主将详细指导你如何在自己的 Vue 3 项目中使用 Axios 二次封…

【Java开发实训】day04——可变参数和递归练习

目录 一、可变参数 1.1定义 1.2注意 1.3示例 二、递归 2.1定义 2.2注意 2.3示例 2.4练习 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&…

CSS3实现提示工具的渐入渐出效果及CSS3动画简介

上一篇文章用CSS3实现了一个提示工具&#xff0c;本文介绍如何利用CSS3实现提示工具以渐入的方式呈现&#xff0c;以渐出的方式消失。 CSS3主要可以通过两个样式来实现动画效果&#xff1a;animation和transition。 其中&#xff0c;animation需要自己定义一组关键帧从而实现…

css实现前端水印

单处水印 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Watermark Example</title>&l…

昇思25天学习打卡营第24天|应用实践之Pix2Pix实现图像转换

基本介绍 今日要实践的模型是Pix2Pix模型&#xff0c;用于图像转换。使用官方的指定数据集&#xff0c;该数据集是已经经过处理的外墙&#xff08;facades&#xff09;数据&#xff0c;可以直接使用mindspore.dataset的方法读取。由于Pix2Pix模型是基于cGAN&#xff08;条件生成…