springboot logback日志+异常+阿里云日志 aliyun-log-logback-appender

news2024/9/28 15:32:57
  1. 前言

最近有个新项目用了,springboot3.0,以前项目日志保存得方式是阿里云云服务自动读取日志文件,最近项目部署得方式可能有变化,所以新项目用logback+aliyun-log-logback-appender得方式保存到阿里云日志服务。用logback得原因主要是懒,spring默认就是这个,其他还要各种配置和兼容。

重点

  1. 通过配置MDC控制保存到阿里云的数据,logback-spring.xml要配置对应的mdcFields

  1. 通过ContentCachingRequestWrapper和ContentCachingResponseWrapper取入参和返回数据,这两个不需要太多代码

  1. RestControllerAdvice+ExceptionHandler全局捕获异常并处理

  1. gradle

    implementation 'com.google.protobuf:protobuf-java:2.5.0'
    implementation 'com.aliyun.openservices:aliyun-log-logback-appender:0.1.18'
    implementation 'org.slf4j:slf4j-nop:1.7.25'

阿里云日志应该只需要这么多得引入。

  1. logback-spring.xml 日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <!-- 项目名称 -->
    <property name="PROJECT_NAME" value="apiv3"/>

    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight([%-5level] [%thread] %logger{50} - %msg%n)</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
    <shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>

    <appender name="apiv3Log" class="com.aliyun.openservices.log.logback.LoghubAppender">
        <!--必选项-->
        <!-- 账号及网络配置 -->
        <endpoint>cn-beijing.log.aliyuncs.com</endpoint>
        <accessKeyId>####</accessKeyId>
        <accessKeySecret>###</accessKeySecret>

        <!-- sls 项目配置 -->
        <project>#####</project>
        <!--开发环境区分 -->
        <springProfile name="dev">
            <logStore>####dev</logStore>
        </springProfile>
         <!--开发环境区分 -->
        <springProfile name="test">
            <logStore>###test</logStore>
        </springProfile>
        <!--必选项 (end)-->

        <!-- 可选项 -->
        <!--        <topic>your topic</topic>-->
        <!--        <source>your source</source>-->

        <!-- 可选项 详见 '参数说明'-->
        <totalSizeInBytes>104857600</totalSizeInBytes>
        <maxBlockMs>0</maxBlockMs>
        <ioThreadCount>8</ioThreadCount>
        <batchSizeThresholdInBytes>524288</batchSizeThresholdInBytes>
        <batchCountThreshold>4096</batchCountThreshold>
        <lingerMs>2000</lingerMs>
        <retries>10</retries>
        <baseRetryBackoffMs>100</baseRetryBackoffMs>
        <maxRetryBackoffMs>50000</maxRetryBackoffMs>

        <!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式 -->
<!--        <encoder>-->
<!--            <pattern>%d %-5level [%thread] %X{traceId} %logger{0}: %msg</pattern>&ndash;&gt;-->
<!--            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight([%-5level] [%thread] %logger{50} - %msg%n)</pattern>-->
<!--            <charset>UTF-8</charset>-->
<!--        </encoder>-->

        <!-- 可选项 设置 time 字段呈现的格式 -->
        <timeFormat>yyyy-MM-dd'T'HH:mmZ</timeFormat>
        <!-- 可选项 设置 time 字段呈现的时区 -->
        <timeZone>UTC+8</timeZone>
        <mdcFields>
            TraceId,#####
        </mdcFields>
    </appender>
    <!--TRACE < DEBUG < INFO < WARN < ERROR < FATAL-->
    <!-- 开发环境下的日志配置 -->
    <springProfile name="dev">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="apiv3Log"/>
        </root>
    </springProfile>
    <springProfile name="test">
        <root level="INFO">
            <!--            <appender-ref ref="CONSOLE"/>-->
            <appender-ref ref="apiv3Log"/>
        </root>
    </springProfile>

    <!-- 生产环境下的日志配置 -->
    <springProfile name="prod">
        <root level="INFO">
           。。。。。。
        </root>
    </springProfile>
</configuration>

这是logback-spring.xml 基本配置。放在resources目录中。

###基本都是阿里云中得一些参数信息,熟悉阿里云日志的,都明白。

mdcFields也是我最近才发现,它会把MDC中的中对应不为null的字段保存在阿里云中。

  1. HttpFilter

因为在日志中要记录请求的参数和返回值,aop实现有点负责,所以用了HttpFilter

@Component
@WebFilter(filterName = "accessLogFilter", urlPatterns = "/*")
@Order(-9999)        // 保证最先执行
public class AccessLogFilter extends HttpFilter {

    private static final Logger logger = LoggerFactory.getLogger(AccessLogAspect.class);

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        ContentCachingRequestWrapper cachingRequestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper cachingResponseWrapper = new ContentCachingResponseWrapper(response);

        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("user-agent"));
        // 接收到请求,记录请求内容````
        MDC.put("TraceId", TraceIDUtils.getTraceId());
        //客户端类型
        MDC.put("ClientType", userAgent.getOperatingSystem().getDeviceType().getName());
        //客户端操作系统类型
        MDC.put("OsType", userAgent.getOperatingSystem().getName());
        MDC.put("ClientMethod", request.getMethod());
        MDC.put("ClientIp", IpUtil.getIpAddress(request));
        MDC.put("ClientParam", request.getQueryString());
        MDC.put("ClientUrl", request.getRequestURL().toString());
        MDC.put("ClientUri", request.getRequestURI());
        //记录请求开始时间
        long start = RequestTimeUtil.getStart();
        MDC.put("RequestStart", String.valueOf(start));
        //请求头参数
        Map<Object, Object> headerMap = new HashMap<>();
        Enumeration<String> enumeration = cachingRequestWrapper.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            String value = request.getHeader(name);
            headerMap.put(name, value);
        }
        MDC.put("RequestHeader", JSON.toJSONString(headerMap));
        MDC.put("LogType", LogTypEnum.requestStart.name());
        MDC.put("LogTime", String.valueOf(System.currentTimeMillis()));
        //日志输出
        logger.info("--------------request start--------------");
        //这里清理掉MDC 防止变成下一个log输出的垃圾数据
        MDC.remove("LogTime");
        MDC.remove("ClientType");
        MDC.remove("OsType");
        MDC.remove("ClientMethod");
        MDC.remove("ClientIp");
        MDC.remove("ClientParam");
        MDC.remove("ClientUrl");
        MDC.remove("ClientUri");
        MDC.remove("RequestStart");
        MDC.remove("LogType");
        MDC.remove("RequestHeader");
        super.doFilter(cachingRequestWrapper, cachingResponseWrapper, chain);
        String responseBody = new String(cachingResponseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);

        MDC.put("LogType", LogTypEnum.requestEnd.name());
        MDC.put("RequestParamMap", JSON.toJSONString(cachingRequestWrapper.getParameterMap()));
        String requestBodyParam = new String(cachingRequestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
        MDC.put("RequestBodyParam", requestBodyParam);
        MDC.put("ReturnData", responseBody);
        long end = RequestTimeUtil.getEnd();
        MDC.put("RequestEnd", String.valueOf(end));
        MDC.put("RequestProcessTime", String.valueOf(end - start));
        MDC.put("LogTime", String.valueOf(System.currentTimeMillis()));
        logger.info("--------------request end--------------");
        // 这一步很重要,把缓存的响应内容,输出到客户端
        cachingResponseWrapper.copyBodyToResponse();
        MDC.clear();
        RequestTimeUtil.remove();
        TraceIDUtils.remove();
    }

找了很久才找到ContentCachingRequestWrapper和ContentCachingResponseWrapper这两个可以取入参和返回数据的类,不用动其他代码就可以实现了。

其实到这里最基础的请求日志,已经实现了。

大概就是这样子的。

  1. 其他aop的日志也是差不多的方式去处理。

  1. 异常处理

RestControllerAdvice+ExceptionHandler全局捕获异常,ResultError是自定义异常,主要处理一些assert抛出的错误。


@RestControllerAdvice
public class ExceptionControllerAdvice {

    private static final Logger logger = LoggerFactory.getLogger(AccessLogAspect.class);

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 然后提取错误提示信息进行返回
        return Result.fail(GlobalResultEnum.fail.getCode(), objectError.getDefaultMessage());
    }


    @ExceptionHandler(ResultError.class)
    public Result<Object> APIExceptionHandler(ResultError e) {
        MDC.put("ResponseStatus", String.valueOf(e.getResultEnum().getCode()));
        MDC.put("ResponseGlobalStatus", String.valueOf(e.getResultEnum().getGlobalStatus()));
        MDC.put("ResponseMsg", e.getResultEnum().getMsg());
        MDC.put("LogType", LogTypEnum.resultError.name());
        MDC.put("LogTime", String.valueOf(System.currentTimeMillis()));
        logger.info(JSON.toJSONString(e.getErrorMessage()));
        MDC.remove("LogTime");
        MDC.remove("ResponseStatus");
        MDC.remove("ResponseGlobalStatus");
        MDC.remove("ResponseMsg");
        MDC.remove("LogType");
        return Result.of(e);
    }

}
public class ResultError extends RuntimeException {

    @Getter
    private ResultEnum resultEnum;
    @Getter
    private Object errorMessage;

    public ResultError(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.resultEnum = resultEnum;
    }

    public ResultError(ResultEnum resultEnum, Object errorMessage) {
        super(resultEnum.getMsg());
        this.resultEnum = resultEnum;
        this.errorMessage = errorMessage;
    }

}
public interface ResultEnum {

    /**
     * 状态码
     * @return int
     */
    int getCode();

    /**
     * 全局状态码
     * @return int
     */
    int getGlobalStatus();

    /**
     * 描述信息
     * @return String
     */
    String getMsg();

}

ResultEnum 是全局状态接口,所有的状态都可以继承这个类,实现类似这样。

public enum GlobalResultEnum implements ResultEnum {
    success(100, "success"),
    fail(99, "fail");

    private final int code;
    private final int globalStatus = 0;
    private final String msg;

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


    /**
     * 状态码
     *
     * @return int
     */
    @Override
    public int getCode() {
        return code;
    }

    /**
     * 全局状态码
     *
     * @return int
     */
    @Override
    public int getGlobalStatus() {
        return globalStatus + code;
    }

    /**
     * 描述信息
     *
     * @return String
     */
    @Override
    public String getMsg() {
        return msg;
    }
}

全局定义唯一状态码(globalStatus),定义全局状态接口(ResultEnum)

也挺麻烦,还需要优化。

写的不是很清晰,欢迎讨论。

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

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

相关文章

《OpenGL宝典》--统一变量

统一变量 [layout (location 0)] uniform float f 1.0f;若设置layout&#xff0c;则不需要使用glGetUniformLocation来获取统一变量的位置 使用glUniform*传递值&#xff0c;glUniformMatrix*()设置矩阵统一变量。 glUseProgram(myShader); glUniform1f(0,45.2f);//0为loc…

思迅软锁安装配置说明

思迅软锁安装配置说明 一、软锁安装、申请及配置流程 1.软件安装环境要求 2.软件安装配置流程 步骤1: 在平台下载软锁程序并安装。在安装了总部数据库的服务器上&#xff0c;运行“思迅软锁服务.exe”程序&#xff0c;按照指引进行安装&#xff0c;安装完成后将在电脑的桌面上…

微服务之Gateway服务网关

&#x1f3e0;个人主页&#xff1a;阿杰的博客 &#x1f4aa;个人简介&#xff1a;大家好&#xff0c;我是阿杰&#xff0c;一个正在努力让自己变得更好的男人&#x1f468; 目前状况&#x1f389;&#xff1a;24届毕业生&#xff0c;奋斗在找实习的路上&#x1f31f; &#x1…

「亲测」0成本考证填报个税纳税额减免3600

「亲测」0成本考证填报个税纳税额减免3600 今天开始2022综合所得的年度汇算就开始办理了&#xff0c;刚刚步入工作的同学&#xff0c;对个税的填报有些苦恼&#xff0c;好像除了房租就没有能减税的政策了。 别急别急&#xff0c;其实个⼈所得税⾥⾯包含⼀个叫“专项附加扣除”的…

C++ 认识和了解C++

1.在使用C语言写代码的时候开头要用到的是&#xff1a; #include<iostream> using namespace std;不可以写成这样&#xff1a; #include iostream.h&#xff08;1&#xff09;iostream是输入输出流类&#xff0c; istream输入流类 cin >> ostream输出流类 cout &…

40系笔记本(可不联网激活)深度学习生产力(环境配置和简单训练测试)

40系笔记本深度学习、转码生产力&#xff08;环境配置和简单训练测试&#xff09;这里写自定义目录标题深度学习环境准备CUDA、CUDNN版本问题torch版本问题其他软件版本的安装命令训练测试代码地址关于Linux还是Windows的问题结果博主首发购买了枪神7超竞4080的版本&#xff0c…

git开发流程

分支介绍 dev&#xff1a;开发环境&#xff0c;从feature去mr test: 测试环境&#xff0c;从feature去mr pre&#xff1a; 预生产环境&#xff0c;从master去mr&#xff0c;为了验证master代码 master: 生产环境&#xff0c;从feature去mr feature&#xff1a; 开发分支----小…

4种方法教你如何隐藏电脑磁盘分区?

磁盘分区是电脑的重要组成部分&#xff0c;我们能够在电脑中保存众多数据&#xff0c;就离不开它。那么你知道该如何隐藏磁盘分区吗&#xff1f;下面小编就教你4个方法隐藏电脑磁盘分区。方法一&#xff1a;使用磁盘管理隐藏硬盘分区1、按下“WinR”键&#xff0c;输入“diskmg…

Python3-元组

Python3 元组 Python 的元组与列表类似&#xff0c;不同之处在于元组的元素不能修改。 元组使用小括号 ( )&#xff0c;列表使用方括号 [ ]。 元组创建很简单&#xff0c;只需要在括号中添加元素&#xff0c;并使用逗号隔开即可。 >>> tup1 (Google, Runoob, 19…

4-1 SpringCloud快速开发入门:RestTemplate类详细解读

RestTemplate类详细解读 RestTemplate 的 GET 请求 Get 请求可以有两种方式&#xff1a; 第一种&#xff1a;getForEntity 该方法返回一个 ResponseEntity对象&#xff0c;ResponseEntity是 Spring 对 HTTP 请求响应的封装&#xff0c;包括了几个重要的元素&#xff0c;比如响…

Python基础篇(十五)-- Pygame游戏编程

1 初识Pygame Pygame是一个开源的Python模块&#xff0c;专门用于多媒体应用&#xff08;如电子游戏&#xff09;的开发&#xff0c;其中包含对图像、声音、视频、事件、碰撞等的支持。Pygame建立在SDL的基础上&#xff0c;SDL是一套跨平台的多媒体开发库&#xff0c;用C语言实…

39万字完整版智能矿山项目建设整体解决方案

本资料来源网络&#xff0c;仅做知识分享&#xff0c;请勿商用。完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 1.1 总体技术要求 1.1.1 核心业务架构 智能矿山业务架构是在统一的标准与规范及安全运维保障体系下&#xff0c;按分层设计模式&#xff0c;分为设备层、…

QML鼠标事件

QML中常用的事件有&#xff1a; 鼠标事件键盘事件拖拽事件定时器MouseArea&#xff08;鼠标区域&#xff09; MouseArea是一个不可见的项目&#xff0c;同、通常用来和一个可见的项目配合使用来为其提供鼠标处理。鼠标处理的逻辑可以包含在MouseArea项目中 常用的属性&#xff…

【设计模式】原型模式与建造者模式

原型模式 原型模式是指通过原型实例指定创建对象的种类&#xff0c;然后通过拷贝的方式创建新的对象。属于创建型模式 原型模式的核心在于拷贝原型对象&#xff0c;主要用于对对象的复制。当你需要通过一大段get/set方法去构建对象的时候&#xff0c;就可以考虑使用原型模式了…

手牵手教Docker部署Springboot+vue ,全过程十分详细,轻松完成项目部署(简单,高效,通用)

手把手教Docker部署Springbootvue &#xff0c;详细全过程&#xff0c;轻松完成项目部署&#xff08;简单&#xff0c;高效&#xff09; 上线前准备 腾讯云的服务器&#xff0c;服务器安装好docker 和docker-compose 最好事先了解技术 nginxdocker-compose 整体编排 后端部…

【C++】Visual Studio C++ 配置并使用gtest(不好用你捶我)

文章目录 相信大家都能感受到Visual Studio C 编辑器链接 lib 或 dll文件是一件非常头疼的事情。配置gooleTest的过程也不例外。 市面上很多教程&#xff0c;要么就不全&#xff0c;要么就缺少一些细节&#xff0c;导致我自己再配置的过程中&#xff0c;踩了很多坑。今天就记录…

用不同的思路实现括号匹配(java)

给定一个只包括 (&#xff0c;) 的字符串&#xff0c;判断字符串是否有效。 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 以上就是题目要求 利用替换的思想 ()()()()()()()(())例如需要判断例子中的括号是否有效&#xff0c;用替换的思想具体就是 将字…

力扣sql简单篇练习(二十一)

力扣sql简单篇练习(二十一) 1 使用唯一标识符替换员工ID 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 # Write your MySQL query statement below SELECT e2.unique_id,e1.name FROM Employees e1 LEFT JOIN EmployeeUNI e2 ON e1.ide2.id1.3 运行…

使用netlify实现自动化部署前端项目(无服务器版本)

介绍 本文以 github仓库进行介绍关联netlify的无服务前端自动化部署。用途&#xff1a;个人网站设计、小游戏等当然这只是让你入门~具体细节等待你自己去探索 实现 打开官方网站 如果没有注册过的账户&#xff0c;你需要使用 github 去进行登录。注册完成后会自动给你提示填…

有关Windows域信任关系的一系列知识

简单的总结了一下来自这篇文章的知识点 https://www.kroll.com/-/media/kroll/pdfs/publications/rootedcon2019-pentesting-active-directory-forests-carlos-garcia.pdf 视频录像为 https://www.youtube.com/watch?v6aV5tZlQ2EQ&t10s&themeRefresh1 森林 域是由树和…