为什么要规范日志
规范的日志是养成良好编程习惯的开始,也是关键时刻解决严重BUG的救命稻草。程序员开发的过程中可以打印debug日志,在复杂业务中提供日志来排查问题,也可以在出现生产问题的时候快速问题,及时处理。无论如何了解和学习日志的规范是程序员必备的基本功。
日志作用
- 线上问题定位。日志主要的作用,核心业务必须要具备完整的日志以便于问题排查。
- debug日志调试。在开发和测试中可以通过debug日志调试,在关键部分添加debug日志有利于测试的准确性,开发也可以借助Debug日志进行自测。
- 用户日志行为。主要是记录一些用户敏感操作,用于监控或者运营团队反馈客户问题使用,这些行为一半具备一定的产品规范。
- 扯皮。主要是第三方对接的时候,如果出现类似对面突然改返回参数赖账的情况下可以拿日志作为证据。或者运营误操作也可以用日志讲道理。
简单案例
直接看一些较为优秀的开源框架,或者阅读一些JDK源码的异常处理是不错的方式,这里简单介绍一些例子:
根据具体的异常信息捕获而日志打印:
try {
File defaultAclFile = new File(fileName);
if (!defaultAclFile.exists()) {
defaultAclFile.createNewFile();
}
} catch (IOException e) {
// 进行具体的异常信息捕获而日志打印
log.warn("create default acl file has exception when update accessConfig. ", e);
}
使用String.format 代替 + 拼接以及自定义异常的定义和抛出:
try {
byte[] signature = sign(data.getBytes(charset), key.getBytes(charset), algorithm);
return new String(Base64.encodeBase64(signature), DEFAULT_CHARSET);
} catch (Exception e) {
// 使用String.format 代替 + 拼接
String message = String.format(CAL_SIGNATURE_FAILED_MSG, CAL_SIGNATURE_FAILED, e.getMessage());
log.error(message, e);
// 自定义异常
throw new AclException("CAL_SIGNATURE_FAILED", CAL_SIGNATURE_FAILED, message, e);
}
有时候异常处理有着意想不到行为,我认为这种处理看上去不错但是实际上很容易“埋雷”,如果作者没有在Doc中进行相关介绍,会是十分危险的行为。
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
// 我认为这种处理看上去不错但是实际上很容易“埋雷”
return new BigDecimal(value).longValue();
}
发展历史
JAVA的日志框架可以说是一坨麻线,并且这坨麻线归根结底都是都是一个人之手。相对应的如果我们使用lombok工具,会发现又有@Log4j、@Slf4j、@Log4j2这些注解,这些日志不仅本身名字难记,还长的差不多,用到的时候更是不知道用那个,甚至也不知道为什么用这个。这里先普及一下发展到目前整个框架的成果图:
下面开始了解这个图是怎么出来的=-=。
System.out以及System.err
算是最为古老的JAVA打印日志的方式,这个打印有点是简单快速,缺点是不能进行任何格式配置,也没输出问题,效率极低。
Log4j
1996年诞生自欧洲电子安全市场的项目决定自己开发出一套日志跟踪API,后续这套API独立成为了Log4j这个项目。
Ceki Gülcü
作为Log4j的主要开发,为日志开发的基础架构提供了很多参考。Log4j 至今依然有大量的公司采用,影响十分深远。
JUL(Java Util Logging)
Sun公司对于Log4j十分眼红,在拒绝Log4j融入Java中的请求之后,自己开发了日志框架,当然基本可以看作是照搬。但是因为Log4j出来很多年了,所以市场反馈不理想,大家还是用Log4j。
JCL(Jakarta Commons Logging)
比较关键点来了,Apach和Sun开始争夺日志标准,Apach后续开发出JCL,意图用JCL翘掉Sun开发的日志工具。Jakarta Commons Logging
在发布之后还提供了统一的日志接口,其实就是一个简单的日志打印抽象层。
但是想法很美好现实很骨感,JCL 和 JUL 的下场也是类似,也没有受到很好的市场反馈,后面被Apach雪藏。
Slf4j(Simple Logging Facade for Java)
在这样的背景之下,Ceki Gülcü
因为个人原因从Apach离职,并且企图想要靠自己制定一套取代JCL的规范,最终命名为Sfl4j,从结果来看确实要比JCL成功许多,但是我们要注意Slf4j和JCL一样只是一套标准,于是后面Ceki Gülcü
又开发了桥接包兼容 JCL,Log4j,顺带把JUC也一起集成进去。自己一个人把所有事情干完了,真的是god一样的人物。
到目前为止我们可以大致整理上面提到的关系,蓝色代表这具体的实体工具或者产品,而黄色部分代表规范。
我们再加入桥接包(红色标识桥接包),又出现了下面的变化,Slf4j规范+兼容包的成果如下:
但是有时候具体使用框架的时候会出现另一种情况,虽然产品都是使用Slf4j实现但是实际的日志打印细节都是各自托管的,因为Slf4j只是简单了统一入口,并没有做任何日志统一的操作。比如这里我们使用SpringBoot的框架,假设它使用log4j,而我们自己的业务代码使用slf4j,就会出现两种不同日志格式的出现。
所以Slf4j后续又做了兼容处理,目的就是不仅可以接入各种日志,还可以接管其他日志框架的所有工作,没错这里还是使用桥接。不过桥接的是相当于把所有兼容日志框架的日志打印按照sfl4j的日志格式进行兼容管理。
到这里Slf4j把所有的其他产品兼容了一个遍,也确实做到了一个框架代替其他日志框架的效果。
Logback
SLf4j 一串三把几个框架全串起来了,但是Sl4j毕竟还是规范,不是具体的产品,于是Ceki Gülcü
又撸了一个Logback出来,这个LogBack可以看作是Log4j的升级,不仅性能可以比Log4j快十倍,同时也完美替代Slf4j。为了简单记忆这里简单概括为 Logback = Slf4j + Log4j(改良)
。
Log4j2
Ceki Gülcü
的老东家的Apach看到以前的员工蹬鼻子上脸自然不乐意,为了对标Logback开发出Log4j 2,毫不意外的,它涵盖了基本上所有的Logback的特性,同时还搞出分离的操作,没错就是在Slf4j的基础上做了一点改进而已,把兼容其他日志框架的接口作为一个包(Api),把日志产品本身又形成一个包(Core)。
最后画出来这个图有点可怕,搭建可以保存下来多看几遍(为了方便观看,这里把辅助线去掉了)
整个Java的日志框架发展出4框架和三个接口,主要使用的框架是:Log4j2、Slf4j、Logback、JUL(Sun开发),接口是一大堆桥接包,用于把其他框架的规范和接口全部桥接到自己的产品中,
注意:其实这里可以看到一个比较有意思的细节,那就是JCL(Jakarta Commons Logging)
在无声无息中退出历史舞台,因为在过去确实非常坑,Apach开发的Log4j2也只接入了JUL而不是自己过去开发的JCL。
那么在系统在选择日志方案的时候,如何抉择呢?
• 显然第一点是使用日志接口的API而不是直接使用日志产品的API这一条也是必须的,也是符合依赖倒置原则的,我们应该依赖日志的抽象,而不是日志的实现。
• 日志产品的依赖只添加一个,如果依赖多个日志产品只会让自己的应用处理日志显得更复杂,不可统一控制。
• 把日志产品的依赖设置为Optional和runtime scope其中Optional是为了依赖不会被传递,比如别的人引用你这个jar,就会被迫使用不想用的日志依赖。
比如防止依赖传递可以使用下面的方式:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
<optional>true</optional>
</dependency>
而scope设置为runtime,是可以保证日志的产品的依赖只有在运行时需要,编译的时候不需要,同样可以保证开发人员误用API:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<scope>runtime</scope>
</dependency>
以上就是Java的日志烂摊子历史,希望读者可以从中吸收教训而不是经验。
什么时候记录日志?
记录日志主要查看下面几个点:
- 初始化参数:初始化参数在各种框架里面可以看到一些内容,而在自己开发的业务中则使用打印业务参数阅读相关内容。
- 编程语言提示异常:主要和业务开发人员对于异常的处理以及异常的容忍度,这里根据不同的日志级别打印相关日志。
- 业务流程预期不符:分支条件进入到预期之外的情况下需要打印相关日志。
- 系统核心角色,组件关键动作:主要是核心业务的触发动作,但是需要避免非常高频率的打印,同时对于日志进行提炼。
- 第三方服务远程调用:微服务中的第三方组件可信度都不高,记录日志是必要的,有时候面对突发的问题比较可以排查出比较关键的信息。
日志实践
我们可以参考下面的格式进行日志输出格式化:
2019-12-01 00:00:00.000|pid|log-level|[svc-name,trace-id,span-id,user-id,biz-id]|thread-name|package-name.class-name : log message
- 时间
- pid,pid
- log-level,日志级别
- svc-name,应用名称
- trace-id,调用链标识
- span-id,调用层级标识
- user-id,用户标识
- biz-id,业务标识
- thread-name,线程名称
- package-name.class-name,日志记录器名称
- log message,日志消息体
以Logback为例,我们可以使用下面的格式进行标准化打印:
%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}|${PID:- }|%level|${LOG_LEVEL_PATTERN:-%5p}|%t|%-40.40logger{39}: %msg%n
logback.xml的设置如下:
<configuration><property name="LOG_PATH"
value="${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}"/>
<springProperty scope="context" name="APP_NAME"
source="spring.application.name" defaultValue="spring-boot-fusion"/>
<!-- 全局统一 pattern -->
<property name="LOG_PATTERN"
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}|${PID:- }|%level|${LOG_LEVEL_PATTERN:-%5p}|%t|%-40.40logger{39}: %msg%n"/>
<!-- 输出模式 file,滚动记录文件,先将日志文件指定到文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="fileInfo" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
<file>${LOG_PATH}/${APP_NAME}-info.log</file>
<!--滚动策略 基于时间的分包策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- yyyy-MM-dd 时间策略则为一天一个文件 -->
<FileNamePattern>${LOG_PATH}/${APP_NAME}-info.%d{yyyy-MM-dd-HH}.%i.log</FileNamePattern>
<!--日志文件保留小时数-->
<MaxHistory>48</MaxHistory>
<maxFileSize>1GB</maxFileSize>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<!-- layout 负责把事件转换成字符串,格式化的日志信息的输出 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<!--级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据onMath 和 onMismatch接收或拒绝日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--设置过滤级别-->
<level>INFO</level>
<!--用于配置符合过滤条件的操作-->
<onMatch>ACCEPT</onMatch>
<!--用于配置不符合过滤条件的操作-->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
</configuration>
这里有几个重点进行介绍:
- 时间点具体到毫秒,HH:mm:ss.SSS 精确到具体的时间更有利于排查问题。
- 不同级别的日志进行分类存储,可以使用additivity="false"避免日志的重复打印。
- 推荐使用
@lombok.extern.slf4j.Slf4j
+logback的方案,logback本身也是slft4j的作者,log4j的作者做出来的东西,算是最终的集大成产品。
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
- 禁用 System.out.println 和 System.err.println,同时对于要直接打印的对象内容,为对象设置
@ToString
注解或者使用IDE自动生成皆可。 - 打印日志不建议使用
+
而是更建议使用{}
占位符的方式进行打印:
// 正确示例,必须使用参数化信息的方式
log.debug("order is paying with userId:[{}] and orderId : [{}]",userId, orderId);
// 错误示例,不要进行字符串拼接,那样会产生很多 String 对象,占用空间,影响性能。及日志级别高于此级别也会进行字符串拼接逻辑。
log.debug("order is paying with userId: " + userId + " and orderId: " + orderId);
外置解决方案
外置解决方案比较常见的就是自己搭一套ELK日志采集系统采集各个系统模块的日志,Kibana提供了界面分析工具分析相关的内容,方便又好用。
ELK
ELK日志搜集服务是一种比较常见的日志搜集过滤框架,ELK 是 Elasticsearch、Logstash、Kibana 三大开源框架首字母大写简称。
- Elasticsearch 是一个基于 Lucene、分布式、通过 Restful 方式进行交互的近实时搜索平台框架,
- Logstash 是 ELK 的中央数据流引擎,用于从不同目标(文件/数据存储/MQ)收集的不同格式数据,经过过滤后支持输出到不同目的地(文件/ MQ / Redis / Elasticsearch / Kafka 等)。
- Kibana 可以将 Elasticsearch 的数据通过友好的页面展示出来,提供实时分析的功能
收集的日志内容参考案例如下:
2019-11-26 15:01:03.332|1543|INFO|[example-server-book-service,28f019d57b8336ab,630697c7f34ca4fa,105,45982043|XNIO-1 task-42]|c.p.f.w.logging.AccessLoggingFilter :
> url: http://liweichao.com:10011/actuator/health
> http-method: GET
> request-header: [Accept:"text/plain, text/*, */*", Connection:"close", User-Agent:"Consul Health Check", Host:"liweichao.com:10011", Accept-Encoding:"gzip"]
> request-time: 2019-11-26 15:01:03.309
> querystring: -
> payload: -
> extra-param: -
< response-time: 2019-11-26 15:01:03.332
< take-time: 23
< http-status: 200
< response-header: [content-type:"application/vnd.spring-boot.actuator.v2+json;charset=UTF-8", content-size:"15"]
< response-data: {"status":"UP"}
SLS 阿里云日志服务
阿里云日志服务(简称 SLS)是针对日志类数据的一站式服务。无需开发就可以进行日志数据采集、消费以及查询分析功能。
主要分为下面的功能:
Project:项目管理的基础单元,日志服务建议一个环境一个Project,日志需要根据环境服务调用产生。
logstore:不同的日志格式可以设置不同的警告格式。日志库(Logstore)是日志服务中日志数据的采集、存储和查询单元。每个日志库隶属于一个项目,且每个项目可以创建多个日志库。
分区:Logstore读写日志必定保存在某一个分区(Shard)上。每个日志库(Logstore)分若干个分区。
更多内容可以搜索SLS阿里云日志服务了解,这里就不过多展开介绍了。
使用建议
如果允许,可以使用lombok加入注解的方式使用日志变量实例。
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
使用的地方如下:
import lombok.extern.slf4j.Slf4j;
@Slf4j public class LogTest
{
public static void main(String[] args) {
log.info("this is log test");
}
}
日志级别选择
日志主要的级别如下,日志等级从小到大分别如下:
- DEBUG:DEBUG日志主要是开发是阶段使用,使用场景通常是开发和测试阶段对于一些关键操作是否执行的输出,开发人员可以把各种内容详细记录到Debug信息,尽可能的在开阶段发现和排查问题。
- INFO:INFO日志包含了关键的日志信息,主要作用是保留工作期间的信息,开发人员可以保留关键日志便于运维提取关键逻辑的执行日志信息,因为INFO日志会在线上日志控制台实时打印,所以需要保留最为关键的信息,建议在完成之后本地调整为INFO级别测试。
- WARN:警告信息和ERROR并不是很好区分,然而实际上只要把WARN和ERROR的日志级别考虑为有影响但是对于业务流程影响不是特别大的行为进行警告区分即可,比如参数存在异常的情况,需要进行后续日志分析。
- ERROR:遇到严重影响业务执行的场景就需要打印Error日志,如果影响不是特别大,只是需要关注问题的情况则打印WARN 级别日志。
DEBUG / INFO 的选择
DEBUG 级别本身比INFO级别低,并且线上通常开启INFO级别日志,DEBUG日志用来本地和测试最为合适,而INFO则是给运维或者反馈给运营的有力证据,INFO级别日志不能输出无意义或者无价值的信息,一定是关键信息才会输出INFO日志。
- 如果代码为核心代码,执行频率非常高,务必推敲日志设计是否合理。
- 日志的可读性,自己review代码。
- 注意日志公有化在多线程环境下的打印会互相打断。
WARN / ERROR 的选择
和上文的描述类似,当遇到用户的敏感操作或者出现意外结果但是不产生事故的情况可以使用WARN进行警告,如果存疑可以后续查看WARN日志排查。而ERROR是需要技术上线排查问题的比较严重的情况使用,所以开发过程需要谨慎考虑ERROR的打印位置。
ERROR的核心要点是下面几个:
- 发生了什么问题,哪些功能受到影响
- 获取帮助信息:直接帮助信息或帮助信息的存储位置
- 通过报警知道解决方案或者找何人解决
规范建议
1. 恰当的日志级别
- error:比较严重的问题,影响正常业务运行
- warn:对业务影响不大,但是需要开发注意
- info:用于日常排查问题的关键信息,接口入参和出参等等
- trace:详细信息,日志文件级别
- debug:仅仅用于开发或者测试查看重要的内部逻辑细节,但是和线上的业务关系不是特别密切
2. 日志打印出参入参
凡是和接口有关的日志,以及关键方法的入参和返回值都建议加上日志。
3. 合适的格式格式
参考模板:
"%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n"
4. 多分支条件建议分支首行打印
if...else...或者switch等如果分支条件比较多的情况下建议在进入分支之前打印一下,当然自己调试的时候也可以用这个法子判断走的是哪个分支:
String type = ???;
if(log.isDebugEnbale()){
log.debug("当前分支类型为:{}", type);
}
if(type == "xxx"){
}else if(type == "aaa"){
}
// 或者
switch(type){
case "xxx":
// ....
break;
case "xxx":
// ....
break;
}
5. 日志级别判断
这一条针对debug和trace这种低级别日志,同时为了减少线上调用日志打印没有日志浪费的情况:
User user = new User(666L, "xxxx", "xxxx");
if (log.isDebugEnabled()) {
log.debug("userId is: {}", user.getId());
}
6. 使用日志框架SLF4J中的API
人家lombok都给了一个@Slf4j
的注解,所以用起来把。
实际上是因为slf4j 也是log4j 的作者写的并且做了门面兼容广受好评,然后.....然后 apach 就学过去了,适配加门面是吧,我也会!最后结果是 Java 的日志系统开源组件极度混乱,并且烂的和一坨shit一样。从这一情况也可以看出定标准是非常重要的。
7. 占位符而不是+号
和java编译为class的时候会使用StringBuffer 做字符串拼接操作。发现不管是大小项目,甚至到了框架也时常看见+号拼接的情况,虽然高版本的JDK这种编译优化下的影响实际上已经很小了,但是个人还是不太喜欢这种+号拼接的写法,不够优雅。
正确用法
logger.info("Processing trade with id: {} and symbol : {} ", id, symbol);
使用+
操作符进行字符串的拼接,有一定的性能损耗
logger.info("Processing trade with id: " + id + " and symbol: " + symbol);
8. 建议使用异步的方式来输出日志
- 日志最终会输出到文件或者其它输出流中的,如果是IO性能会有要求的建议使用异步,可以显著提升IO性能。
- 使用异步的方式来输出日志。以logback为例,要配置异步,使用 AsyncAppender
<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ASYNC"/>
</appender>
9. 不要使用e.printStackTrace()
不要使用的理由:
e.printStackTrace()
打印出的堆栈日志跟业务代码日志是交错混合在一起的,通常排查异常日志不太方便。e.printStackTrace()
语句产生的字符串记录的是堆栈信息,如果信息太长太多,字符串常量池所在的内存块没有空间了,即内存满了,那么,用户的请求就卡住啦~
应该使用如下的正确用法:
try{
// 业务代码处理
}catch(Exception e){
log.error("你的程序有异常啦",e);
}
10. 异常日志不要只打一半
比如下面的日志就没有任何价值:
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦');
}
其实处理方式很简单:
try{
// 业务代码处理
}catch(Exception e){
log.error("你的程序有异常啦",e);
}
另外需要注意,e.getMessage()
不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。此外,如果使用 Hutool 工具,里面有一个异常信息提取的工具类比较方便。
11. 不要囫囵吞枣
这一条注意事项很简单,但是很关键,尽量避免使用 Exception 全盘接收,而是需要考虑针对具体的异常给出更多有用的日志信息,这样可以减少线上问题的排查时间。
12. 禁止在线上环境开启 debug
不仅仅是系统会有很多debug日志出现,还存在框架的debug日志输出问题,线上开启debug很容易打满磁盘,并且容易造成CPU的磁盘IO等待,久而久之会直接影响系统服务。
13. 不要嵌套异常
嵌套异常是最容易吞噬异常的场景,很多时候方法代码块层层嵌套会忘记里面捕获异常,外层又捕获异常但是实际根本拿不到异常,如果异常捕获和处理混乱,那么本身就会大大增加问题排查难度。
本条的建议是在编写设计方法或者类之前,需要提前考虑异常如何处理,完成整个调用之后需要及时的回顾代码。
下面是对应反例:
try{
// 业务代码处理
try{
// 业务代码处理
}catch(Exception e){
log.error("你的程序有异常啦",e);
}
}catch(Exception e){
log.error("你的程序有异常啦",e);
}
14. 不要记录异常又抛出
记录之后抛出异常是非常危险的操作,因为外层可能会因为内层捕获异常之后不会再次处理,如果是自定义异常更是难以排查问题,此外这样做法会导致堆栈二次打印,非常浪费系统性能,
反例如下:
try{
// 业务代码处理
}catch(Exception e){
log.error("IO exception", e);
throw new MyException(e);
}
15. 日志文件分离
可以把不同类型的日志分离出去,比如access.log
,或者error级别error.log
,都可以单独打印到一个文件里面。根据业务模块拆分也是一种办法,这样各自负责的模块能清晰看到日志。
16. 避免重复打印日志
如果日志可以用一行表示,那就尽量用一行表达含义。
log.info("该用户是会员,Id:{}",user,getUserId());
//冗余,可以跟前面的日志合并一起
log.info("开始处理会员逻辑,id:{}",user,getUserId());
17. 核心功能模块日志
如果是核心功能模块的日志,其实多打印一些内容是可以接受的,但是需要注意打印的日志必须要第一时间可以定位到问题所在。
规范建议参考
下面是规范的日志打印:
2018-05-22 15:35:53.850 TRACE TDWZLog [0x00001b10] <36> <TDWZProtocol::Init>,TDWZProtocol::Init
2018-05-22 15:35:53.850 TRACE TDWZLog [0x00001b10] <89> <TDWZProtocol::Init>,End in processing TDWZProtocol::Init
2018-05-22 15:35:53.853 TRACE TDWZLog [0x00001b10] <142> <TDWZProtocol::Connect>,Connect Execute finish
2018-05-22 15:35:53.853 TRACE TDWZLog [0x00002f10] <149> <GetAlarmEventPro>,Enter GetAlarmEventPro func
2018-05-22 15:39:36.382 WARN TrackLog [0x000029fc] - [ internal WARN htrace_server_convert_msgstring_to_contextintls(493) ] detect input id error, trace_id span_id,this chain may not be tracked.
2018-05-22 15:39:36.383 WARN TrackLog [0x000029fc] - [ internal WARN htrace_server_receive(195) ] can not detect trace_id in context, this chain may not be tracked.
2018-05-22 15:39:36.383 TRACE TDWZLog [0x000029fc] <231> <TDWZProtocol::DisConnect>,TDWZProtocol::DisConnect
2018-05-22 15:39:37.502 TRACE TDWZLog [0x00002f10] <225> <GetAlarmEventPro>,End Get AlarmEventPro Func
2018-05-22 15:39:37.503 TRACE TDWZLog [0x000029fc] <241> <TDWZProtocol::DisConnect>,close socket
2018-05-22 15:39:37.503 TRACE TDWZLog [0x000029fc] <242> <TDWZProtocol::DisConnect>,Execute DisConnect function succeed.
TRACE 日志记录示例
DRV_LOG_TRACE("Connect Execute start");
DRV_LOG_TRACE("Connect Execute finish");
DRV_LOG_TRACE("DisConnect func");
DRV_LOG_TRACE("Execute DisConnect function succeed.");
DRV_LOG_TRACE("Enter UploadEvent Func");
DRV_LOG_TRACE("extInfo = %s", Extension);
DRV_LOG_TRACE("Send a Msg ");
DRV_LOG_TRACE("- Connect Execute start");
DRV_LOG_TRACE("- Connect Execute finish");
DRV_LOG_TRACE("- Enter GetAlarmEventPro func");
DRV_LOG_TRACE("- Receive an info");
DRV_LOG_TRACE("- End Get AlarmEventPro Func");
DRV_LOG_TRACE("- DisConnect func");
DRV_LOG_TRACE("- Execute DisConnect function succeed.");
DRV_LOG_TRACE("- Enter UploadEvent Func");
DRV_LOG_TRACE("- Leave UploadEvent Func");
DRV_LOG_TRACE("- ============电网报警触发");
DRV_LOG_TRACE("- ============开始发送电流电压值");
DRV_LOG_TRACE("- ============间隔超过分钟再次发送电流电压值");
INFO 日志记录
DRV_LOG_INFO("- UpdataEvent nchal= %d,EventID = %d.",iChannelNo,nEventType);
DRV_LOG_INFO("- do not support doControl");
DRV_LOG_INFO("- channelId = %s, nStatusType = %d", channelId.c_str(), nStatusType)
DEBUG 日志记录
DRV_LOG_DEBUG("- 输出报警情况:电网编号:%d,报警数量:%d,报警内容:%s.",datas.data1.chn,datas.data1.alarm_num,datas.data1.alarms);
DRV_LOG_DEBUG("- 输出报警情况:电网编号:%d,报警数量:%d,报警内容:%s.",datas.data2.chn,datas.data2.alarm_num,datas.data2.alarms);
DRV_LOG_DEBUG("- 输出报警情况:电网编号:%d,报警数量:%d,报警内容:%s.",datas.data3.chn,datas.data3.alarm_num,datas.data3.alarms);
DRV_LOG_DEBUG("- 输出报警情况:电网编号:%d,报警数量:%d,报警内容:%s.",datas.data4.chn,datas.data4.alarm_num,datas.data4.alarms);
DRV_LOG_DEBUG("- ============datas.data1.huab = %d",datas.data1.huab);
DRV_LOG_DEBUG("- ============datas.data1.hiab = %d",datas.data1.hiab);
DRV_LOG_DEBUG("- ============datas.data2.huab = %d",datas.data2.huab);
DRV_LOG_DEBUG("- ============datas.data2.hiab = %d",datas.data2.hiab);
DRV_LOG_DEBUG("- ============datas.data3.huab = %d",datas.data3.huab);
DRV_LOG_DEBUG("- ============datas.data3.hiab = %d",datas.data3.hiab);
DRV_LOG_DEBUG("- ============datas.data4.huab = %d",datas.data4.huab);
DRV_LOG_DEBUG("- ============datas.data4.hiab = %d",datas.data4.hiab);
DRV_LOG_DEBUG("- Alarm is : %s",szEvent.c_str());
DRV_LOG_DEBUG("- GetChannelExtInfo channelId=%s", channelId.c_str());
DRV_LOG_DEBUG("- nChan = %d, szInfo = %s", nChan, szInfo);
WARN 日志记录
DRV_LOG_WARN("[0x%08x] - invaild event msg,discard it", DRV_INVALID_ARG);
DRV_LOG_WARN("[0x%08x] - Can't find channel by channelId");
DRV_LOG_WARN("[0x%08x] - [DWSdk.errorcode=0x%08x]Connect device failed", DRV_CONNECT_FAILED, sdkErrCode);
DRV_LOG_WARN("[0x%08x] - [DWSdk.errorcode=0x%08x]dw_start_receive failed", DRV_ERROR, sdkErrCode);
DRV_LOG_WARN("[0x%08x] - [DWSdk.errorcode=0x%08x]Communicate failed, socket recv error", DRV_ERROR, DW_SOCKET_RECV_ERROR);
DRV_LOG_WARN("[0x%08x] - [DWSdk.errorcode=0x%08x>other error", DRV_ERROR, iGetResult);
DRV_LOG_WARN("[0x%08x] - [DWSdk.errorcode=0x%08x>other error", DRV_ERROR, iGetResult);
DRV_LOG_WARN("[0x%08x] - SetEventCallBack should be called first", DRV_ERROR);
ERROR 日志记录
DRV_LOG_ERROR("Init DwSDK filded;<errCode=%d>", initRet);
DRV_LOG_ERROR("Connect device failed");
DRV_LOG_ERROR("Create thread failed");
DRV_LOG_ERROR("dw_start_receive failed");
DRV_LOG_ERROR("Communicate failed, socket recv error");
DRV_LOG_ERROR("other error<errCode=%d>", iGetResult);
DRV_LOG_ERROR("SetEventCallBack should be called first");
DRV_LOG_ERROR("[0x%08x] - [DWSdk.errorcode=0x%08x]Init DwSDK filded", DRV_INIT_FAILED, initRet);
DRV_LOG_ERROR("- [HPR.errorcode=0x%08x]Create thread failed", HPR_GetLastError());
[0x%08x] 的含义可以参考例子,printf("0x%08x", 0x1234);
,格式化之后会变为0x00001234
。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。