针对CloudWatch上日志的优化

# 第一章 概述

本规范所说的日志包含:为跟踪应用软件本身运行状况的应用运行日志和为跟踪业务运行情况的业务日志,不包含应用内部专用的日志(如:行为数据采集日志)。

通过日志规范,达到以下目标:

* 运维和二线支持人员可以方便的跟踪应用系统运行情况,排查应用系统运行异常;

* 通过业务流水日志跟踪日志路径和流程,发现和分析业务运行状况;

原则上所有应用系统应记录日志,重要业务系统应尽可能详细地记录日志,日志记录方式包含日志文件或数据库表。

# 第二章 日志类型

## 应用日志类型

应用日志根据记录的信息和用途不同,可分为运行日志、摘要日志、错误日志三类,概要信息如下:

## 运行日志

### 概述

应用的运行日志,包含应用运行过程中关键的处理逻辑和各种预期内或预期外的运行结果等信息,原则上所有应用系统均应记录应用运行日志,主要用于问题排查或运行情况分析。业务日志本身可以用逗号分隔,例如:

### 日志级别

DEBUG 级别和 INFO 级别,可以通过动态配置方式动态修改日志级别。

### 日志格式

日志内容应以待定分隔符分隔,不强制要求格式模板,但建议每一条日志,都应包括服务器时间戳、线程号、traceID、业务跟踪号、系统跟踪号、关键业务流水号、关键入参、关键过程结果等信息。

### 摘要日志

### 概述

摘要日志指对应用系统运行时的关键信息按照统一的简明扼要的格式打印形成的日志文件,根据用途的不同,摘要日志可分为服务被调用日志、服务调用日志、报文输入输出日志、数据访问日志以及其他需要的日志,每种摘要日志可输出到不同的日志文件。

摘要日志因为其精简的特性,主要用于监控和信息的快速搜索。

### 实现方式

公共SDK提供LogAnnotation注解,在服务管控平台定义的RPC接口会带该注解。在调用时,通过 Filter 拦截调用请求,并记录调用摘要日志。也就是说通过LogAnnotation注解即可记录摘要日志,各应用系统无需额外开发。摘要日志是否记录详情可以通过动态配置进行设置,如使用logDetailFlag配置是否记录调用的入参、出参等详细信息。

### 日志内容

摘要日志包含接口签名信息、请求报文头信息、返回报文头信息,以及接口入出参。

## 错误日志

### 概述

错误日志是一种特殊的 应用运行日志,对于应用系统运行过程中预期外的异常,比如程序空指针、远程调用失败、数据库连接耗尽等,属于需要及时关注并介入处理的范畴,因此这类信息建议专门打印在一个日志文件中,以便在监控系统中配置相应监控并报警。

例如:

应用错误日志位置一般在:/home/admin/logs/${appName}/common-error.log

### 日志内容

日志内容应以特定分隔符分隔,建议包含时间戳、异常信息、异常堆栈的信息。

# 第三章 日志监控分析

## 业务日志监控

业务监控基于应用日志提供完整的业务实时分析及预警能力。

日志内容样例:

注:[日志格式]:日志打印时间 日志级别 线程[traceId, spanId], bizNo, 业务中心域,业务子域,方法名,业务操作返回结果(msg,信息)

依据业务日志监控规范优化的后的格式:

模块名,日志时间,日志级别,线程,

%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss})%highlight(%-5level) %green([%thread])

目前日志的xml文件的配置格式:%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level)

koneview-api- 2023-01-19 11:16:11 [org.springframework.kafka.KafkaListenerEndpointContainer#0-1-C-1] WARN

o.apache.kafka.clients.NetworkClient - [Consumer clientId=consumer-3, groupId=flushLocalCache-kafka-sit_new-0] Connection to node 2 could not be established. Broker may not be available.

<?xml version="1.0" encoding="UTF-8"?>
<!-- 从高到地低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
<!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 -->
<!-- 以下 每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志 -->
<!-- 属性描述 scan:性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration>
    <property name="encoding" value="UTF-8"/>
    <contextName>external-websocket-api</contextName>
    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 对日志进行格式化 -->
        <encoder>
            <!--本地dev | sit| uat 环境下日志的打印格式-->
            <springProfile name="devlocal | sitlocal | uatlocal">
                <pattern>%red(%d{yyyy-MM-dd HH:mm:ss}) %black(%contextName-) %green([%thread]) %highlight(%-5level)
                    %boldMagenta(%logger{80}) %red(%X{traceId}) %green(%X{spanId}) - %gray(%msg%n)
                </pattern>
            </springProfile>
            <!--非本地dev | sit| uat 环境下日志的打印格式即打印在云上的日志-->
            <springProfile name="!(devlocal | sitlocal | uatlocal)">
                <pattern>
                    <![CDATA[
                    %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{80}  %X{traceId} %X{spanId} - %msg%n
                ]]>
                </pattern>
            </springProfile>
            <charset>utf-8</charset>
        </encoder>
    </appender>
    <!--监控sql日志输出 -->
    <logger name="jdbc.sqlonly" level="OFF" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="jdbc.resultset" level="OFF" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <!--  如想看到表格数据,将OFF改为INFO  -->
    <logger name="jdbc.resultsettable" level="OFF" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="jdbc.connection" level="OFF" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="jdbc.sqltiming" level="OFF" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="jdbc.audit" level="OFF" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <!--  非prd环境下控制某指定包下日誌的打印級別  -->
    <springProfile name="!prd">
        <logger name="com.accenture.listener.kafka" level="error"/>
        <logger name="org.apache.kafka" level="error"/>
        <logger name="org.springframework.kafka" level="error"/>
    </springProfile>
    <!-- 本地dev sit uat 环境 -->
    <springProfile name="devlocal | sitlocal | uatlocal">
        <!--info 级别的日志保存的文件名-->
        <appender name="INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <!-- INFO级别日志 -->
                <level>INFO</level>
            </filter>
            <file>${logs}/kone.log</file>
            <Append>true</Append>
            <!--info日志保存到本地文件的格式 -->
            <encoder>
                <pattern>%d [%thread] %-5level %logger{80} %X{traceId} %X{spanId} - %msg%n</pattern>
                <charset class="java.nio.charset.Charset">UTF-8</charset>
            </encoder>
            <!-- 最常用的滚动策略,它根据时间来制定滚动策略.既负责滚动也负责出发滚动 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <!--日志输出位置 可相对、和绝对路径 -->
                <FileNamePattern>${logs}/kone.log.%d{yyyy-MM-dd}.%i</FileNamePattern>
                <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件假设设置每个月滚动,且<maxHistory>是7, 则只保存最近6天的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除 -->
                <maxFileSize>64MB</maxFileSize>
                <maxHistory>7</maxHistory>
                <totalSizeCap>12GB</totalSizeCap>
            </rollingPolicy>
        </appender>
        <!--error 级别的日志保存的文件名  name="ERROR_LOG"-->
        <appender name="ERROR_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>ERROR</level>
            </filter>
            <file>${logs}/kone-error.log</file>
            <Append>true</Append>
            <encoder>
                <pattern>%d [%thread] %-5level %logger{80} %X{traceId} %X{spanId} [%file : %line] - %msg%n</pattern>
                <charset class="java.nio.charset.Charset">UTF-8</charset>
            </encoder>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <FileNamePattern>${logs}/kone-error.log.%d{yyyy-MM-dd}.%i</FileNamePattern>
                <maxFileSize>64MB</maxFileSize>
                <maxHistory>7</maxHistory>
            </rollingPolicy>
        </appender>
    </springProfile>
    <!-- 全局root级别为INFO -->
    <root level="INFO">
        <!--打印到控制台-->
        <appender-ref ref="CONSOLE"/>
        <springProfile name="devlocal | sitlocal | uatlocal">
            <!-- 文件输出 -->
            <appender-ref ref="INFO_LOG"/>
            <appender-ref ref="ERROR_LOG"/>
        </springProfile>
    </root>
</configuration>

# 第四章 日志记录规范

## 日志通用规范

1、【强制】应用中不可直接使用日志系统(Log4J,Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

2、【强制】应用中的扩展日志(如:打点、临时监控和访问日志等)命名方式:

appName_logType_logName.log

logType:日志类型,如:stats/monitor/access/error 等

logName:日志描述

这种命名的好处:通过文件名就可以指导日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。说明:推荐对日志进行分类,如将错误日志和正常日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。

正例:force-web应用中单独监控异常错误,如:forceweb_error.log

3、【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。说明:因为 String 字符串的拼接会使用 StringBuilder 的 append() 方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。

logger.debug("No of Orders " + noOfOrder + " for client: " + client);//不好

logger.debug("No of Orders {} for client: {}", noOfOrder, client);

占位符的两种使用方式,上面用{}作为占位符使用的是void error(String var1, Object var2, Object var3)格式

下面一种是void error(String var1, Throwable var2);因为e会打印出异常的堆栈信息,如果不是用log.error(String.format("keycloak update user failed.userId:%s", resources.getId()) , e)表示,需要将第一个参数转化为string类型,第二个参数是Throwable var2 算抛出异常,含有堆栈信息,如果是用{},则打印的e就是一个常量;

在字符串中使用%s表示字符串的占位符,%f表示float数的占位符;

4、【强制】对于 trace/debug 级别的日志输出,必须进行日志级别的开发判断。说明:虽然在debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT) 为真时(SLF4J 的常见实现 Log4j 和

Logback)就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName()) 这种参数内有 getName() 方法调用,无谓浪费方法调用的开销。

正例:// 如果判断为真,那么可以输出 trace 和 debug 级别的日志

if (logger.isDebugEnabled()) {

......

}

```

5、【强制】避免重复打印日志,浪费磁盘空间,设置 additivity = false, 在日志配置文件如:logback-spring.xml 中

<logger name="com.taobao.dubbo.config" additivity="false'>

  1. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。

logger.error(各类参数或对象 toString(), e );

7、【推荐】谨慎的记录日志。生产环境禁止输出 debug 日志;有选择的输出 info 日志;如果使用 warn 来记录刚上线的业务行为信息,一定要注意日志输出量的我呢提,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。说明:大量的输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带去帮助?

8、【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。说明:注意日志输出的级别,error 级别之记录系统逻辑出错、异常或重要的错误信息。

9、【推荐】尽量使用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚使用中文描述即可,否则容易产生歧义。用英文描述可以避免分词问题。

10、【推荐】从便于监控以及搜索的角度出发,不建议按大小对日志文件进行滚动。如果某日志文件内容过大,可以考虑从一下两方面进行处理:

精简文件内容:看看文件中是否有一些无意义或对排查、定位问题没有帮助的信息,尽可能的删减;如果文件精简后仍然过大,们可以在按日滚动的基础上,按小时滚动,在文件后缀上再加上“_HH” 的后缀。

11、将日志级别调整成可以动态调整,必要时可不重启应用但改变日志打印级别。

**12、**【推荐】**日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。**

**13、**【推荐】**不要忘了记录线程的名称和完整的JAVA类名,因为如果有多个线程同时在执行这段代码,你可能根本找不出事件序列**

**14、**【强制】**除测试代码外严禁使用System.out.println()、System.gc()**

## 日志安全策略

应用日志不能记录密码、密钥、券码、PIN 码、生物特征、令牌等用于身份认证核对的安全类应用信息。

## 日志脱敏

对于关键信息需要脱敏处理,例如:email、银行卡、手机号、密码等,可以实现统一的工具类进行日志脱敏处理。

## 日志动态配置

可以使用动态配置对于日志级别和是否日志脱敏和脱敏规则进行动态配置。

## 附: logback-spring.xml 配置

优化cloudwatch 上的日志量,优化方式

第一步,删除云上大量出现的日志(依据生产库中出现的日志量,看哪些日志重复且大量出现,优先优化这些日志,评判这些日志是否有存在的比要,对于增删改涉及到数据库的操作需要保留日志,关于数据库的连接网络也需要保持日志,因为可能网络异常导致报错,一般查询功能不需要日志就可以删除,其他规则日志是否保留依据上述规则,比如对应的查询日志和本地测试的日志后提交到远程,for循环中不必要存在的日志,先去删掉这些日志,删除或者日志xml文件中控制日志级别的打印;)

第二部;优化日志的格式,用占位符替换+号拼接;

应用中不可直接使用日志系统(Log4J,Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一,可以有两个方式

第一个引入SLF4J的依赖,用@Slf4j注解

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <artifactId>kone-pmc-webtool-server</artifactId>
        <groupId>com.accenture</groupId>
        <version>1.0</version>
    </parent>
    <groupId>com.accenture</groupId>
    <artifactId>kone-pmc-webtool-websocket-api</artifactId>
    <version>1.0</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.accenture</groupId>
            <artifactId>kone-pmc-webtool-websocket</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>com.accenture</groupId>
            <artifactId>kone-pmc-webtool-internal-rest</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>com.accenture</groupId>
            <artifactId>kone-pmc-webtool-quartz</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>log4j-to-slf4j</artifactId>
                    <groupId>org.apache.logging.log4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>it.ozimov</groupId>
            <artifactId>embedded-redis</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-messaging</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--Mysql依赖包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.webjars/stomp-websocket -->
<!--        <dependency>-->
<!--            <groupId>org.webjars</groupId>-->
<!--            <artifactId>stomp-websocket</artifactId>-->
<!--        </dependency>-->

        <!-- https://mvnrepository.com/artifact/org.glassfish.tyrus.bundles/tyrus-standalone-client -->
<!--        <dependency>-->
<!--            <groupId>org.glassfish.tyrus.bundles</groupId>-->
<!--            <artifactId>tyrus-standalone-client</artifactId>-->
<!--        </dependency>-->
        <!-- https://mvnrepository.com/artifact/org.webjars/sockjs-client -->
<!--        <dependency>-->
<!--            <groupId>org.webjars</groupId>-->
<!--            <artifactId>sockjs-client</artifactId>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-core</artifactId>
        </dependency>
        <!-- spring cache -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <profiles>
        <profile>
            <id>prod</id>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-actuator</artifactId>
                </dependency>
                <dependency>
                    <groupId>com.taobao.arthas</groupId>
                    <artifactId>arthas-spring-boot-starter</artifactId>
                </dependency>
            </dependencies>
        </profile>
        <profile>
            <id>test</id>
            <dependencies>
                <dependency>
                    <groupId>com.accenture</groupId>
                    <artifactId>kone-pmc-webtool-swagger</artifactId>
                    <version>1.0</version>
                </dependency>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-actuator</artifactId>
                </dependency>
                <dependency>
                    <groupId>com.taobao.arthas</groupId>
                    <artifactId>arthas-spring-boot-starter</artifactId>
                </dependency>
                <dependency>
                    <groupId>net.logstash.logback</groupId>
                    <artifactId>logstash-logback-encoder</artifactId>
                    <version>4.9</version>
                </dependency>
            </dependencies>
        </profile>
    </profiles>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

方式二就是

第三步:中文替换为英文;

第四步:docker文件上配置环境变量用$PRINT_JVM_LOG

表示-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintStringDeduplicationStatistics 文件配置到

需要找到对应的环境,找到对应的实例,最后再配置相关的参数,修改的而代码也需要发布到对应的环境上,需要点击对应的实例去发布到对应的环境上,云上的代码才会更新到最新修改的而代码;

开关PRINT_JVM_LOG控制-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintStringDeduplicationStatistics是否在dockerfile文件上展示,-XX:+PrintGCDetails中+号表示展示,-XX:-PrintGCDetails的第二个减号表示不展示

还可以kafka_level 一个开关控制几个变量

yaml文件上关于日志级别的打印控制,在dockerfile上有响应的变化,也需要在云服务器上配置开关

KAFKA_LEVEL error

KAFKA_LEVEL 右边的value可以填写日志的各个级别,看自己需要打印的情况

更新后保存确认创建实例,最后更新到最新的标签;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值