对于一个系统来说,监控、链路追踪、日志的这三者需求都是必然存在的,而有的时候我们会搞不清楚这三者相互之间是什么关系。我之前在做系统设计的时候也考虑过,是不是有必要引入那么多组件,毕竟如果这三者完全分开每一个一项的话,就有三个组件了(事实上就是:Prometheus+Grafana、Jaeger、ELK)。
因此想做个笔记尝试举例来梳理下。
外部链接:
Monitoring(监控)举例来说就是:定期体检。使用监控系统把需要关注的指标采集起来,形成报告,并对需要关注的异常数据进行分析形成告警。
特点是:
这也是Prometheus的架构做得非常简单的原因,Monitoring的需求并没有包含非常高的并发量和通讯量。反过来说:高并发、大数据量的需求并不适用于Monitoring这个点。
Tracing(链路追踪)举例来说就是:对某一项工作的定期汇报。某个工作开始做了A,制作A事件的报告,收集起来,然后这个工作还有B、C、D等条目,一个个处理,然后都汇总进报告,最终的结果就是一个Tracing。
特点是:
因为Tracing是针对某一个事件(一般来说就是一个API),而这个API可能会和很多组件进行沟通,后续的所有的组件沟通无论是内部还是外部的IO,都算作这个API调用的Tracing的一部分。可以想见在一个业务繁忙的系统中,API调用的数量已经是天文数字,而其衍生出来的Tracing记录更是不得了的量。其特点就是高频、巨量,一个API会衍生出大量的子调用。
也因此适合用来做Monitoring的系统就不一定适合做Tracing了,用Prometheus这样的系统来做Tracing肯定完蛋(Prometheus只有拉模式,全部都是HTTP请求,高并发直接挂掉)。一般来说Tracing系统都会在本地磁盘IO上做日志(最高效、也是最低的Cost),然后再通过本地Agent慢慢把文本日志数据发送到聚合服务器上,甚至可能在聚合服务器和本地的Agent之间还需要做消息队列,让聚合服务器慢慢消化巨量的消息。
Tracing在现在的业界是有标准的:OpenTracing,因此它不是很随意的日志/事件聚合,而是有格式要求的日志/事件聚合,这就是Tracing和Logging最大的不同。
Logging(日志)举例来说就是:废品回收站。各种各样的物品都会汇总进入到配品回收站里,然后经过分门别类归纳整理,成为各种可回收资源分别回收到商家那里。一般来说我们在大型系统中提到Logging说的都不是简单的日志,而是日志聚合系统。
从本质上来说,Monitoring和Tracing都是Logging,Logging是这三者中覆盖面最大的超集,而前两者则是其一部分的子集。Logging最麻烦的是,开发者也不会完全知道最后记录进入到日志系统里的一共会有哪些东西,只有在使用(检索)的时候才可能需要汇总查询总量中的一部分。
要在一般的Loggin系统中进行Monitoring也是可以的,直接把聚合进来的日志数据提取出来,定期形成数据报告,就是监控了。Tracing也是一样,只要聚合进了Logging系统,有了原始数据,后面要做都是可以做的。因此Logging系统最为通用,其特点和Tracing基本一致,也是需要处理高频并发和巨大的数据量。
这样一看就很清楚了,每个组件都有其存在的必要性:
随着分布式应用的普及,现在的一些应用系统不再像以前,所有的文件(前后端程序)都打包在一个包中,现在的很多应用都是模块化开发,开发的团队也是不一样,服务与服务之间的调用也比较多,在这种情况下,系统的日志就显得尤其的重要,然而,在多数情况下,当我们的系统出现了异常,需要查看日志时,就会很抓狂。为了避免这种情况,我们需要把同一次的业务调用链上的日志串联起来。
本次通过一个简单的SpringBoot应用来总结,我们如何将日志串联起来,以下截图是最终的实现效果
使用idea创建一个SpringBoot项目的详细步骤,本文不介绍了,具体的步骤,网上有很多例子可以参考
1.1. 我的SpringBoot项目名称是:springboot-track,以下是工程pom.xml文件中所需要的必要依赖
xml
复制代码
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
1.2. 在项目的resource目录下,创建日志框架整合配置文件:logback-spring.xml文件内容配置如下:
xml
复制代码
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--日志存储路径--> <property name="log" value="/Users/username/Downloads"/> <!-- 控制台输出 --> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--输出格式化--> <pattern>[%X{TRACE_ID}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!-- 按天生成日志文件 --> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日志文件名--> <FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern> <!--保留天数--> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>[%X{TRACE_ID}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> <!--日志文件最大的大小--> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender> <!-- 日志输出级别 --> <root level="INFO"> <appender-ref ref="console"/> <appender-ref ref="file"/> </root> </configuration>
1.3. 在项目的resource目录下的主配置文件(application.yml)中添加日志整合配置信息,添加内容如下:
需要注意的是:使用idea创建的SpringBoot项目,application文件的默认后缀是.properties,本人比较喜欢.yml文件,所以将文件后缀名修改了一下
yaml
复制代码
server: port: 8080 logging: config: classpath:logback-springboot.xml pattern: dateformat: MM-dd HH:mm:ss
1.4. 自定义日志拦截器:LogInterceptor.java
java
复制代码
public class LogInterceptor implements HandlerInterceptor { private static final String TRACE_ID="TRACE_ID"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 使用UUID自动生成链路ID String tid=UUID.randomUUID().toString().replace("-", ""); // 客户端可以传入链路ID,需要唯一性 String traceId=request.getHeader(TRACE_ID); if (!StringUtils.isEmpty(traceId)) { tid=request.getHeader(TRACE_ID); } // MDC(Mapped Diagnostic Context)诊断上下文映射,是@Slf4j提供的一个支持动态打印日志信息的工具 MDC.put(TRACE_ID, tid); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { MDC.remove(TRACE_ID); } }
自定义的拦截器需要实现HandlerInterceptor.java接口,然后重写preHandle方法;MDC(Mapped Diagnostic Context)诊断上下文映射,@Slf4j提供的动态打印日志工具。
1.5. 添加拦截器:WebConfigurerAdapter.java
typescript
复制代码
@Configuration public class WebConfigurerAdapter implements WebMvcConfigurer { @Bean public LogInterceptor logInterceptor() { return new LogInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logInterceptor()) // 自定义需要拦截的和不需要拦截的 .addPathPatterns("/**") .excludePathPatterns("/test***.html"); } }
经过上述的几个步骤,基本上就可以简单的将同一次的业务调用链上的日志串联起来了。
简单的写一个测试类:TestController.java
less
复制代码
@RestController @Slf4j public class TestController { @Resource(name="userService") private IUserService userService; @PostMapping("/test") public String testTrace01(@RequestParam("name") final String name) { log.info("入参 name={}", name); testTrace02(); log.info("调用结束name={}", name); return "Hello," + name; } }
使用Postman调用接口:http://localhost:8080/test?name=张三
控制台的输出如下:
至此,一个最简单的日志串联就做好了
作者:Ethan
链接:https://juejin.cn/post/7283418392957796389
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
者:东东 yasking
来源:https://blog.yasking.org/a/python-logbook.html
Python 本身有logging日志记录模块,之前发现了logbook这个包,介绍说是替代logging,索性整理一下,方便之后使用
>>> from logbook import Logger, StreamHandler
>>> import sys
>>> StreamHandler(sys.stdout).push_application
>>> log=Logger('Logbook')
>>> log.info('Hello, World!')
[2015-10-05 18:55:56.937141] INFO: Logbook: Hello, World!
上边这是文档中给出的例子,它定义了许多的Handler,可以把日志记录到标准输出,文件,E-MAIL,甚至Twitter
使用 StreamHandler
记录的日志会以流输出,这里指定sys.stdout
也就是记录到标准输出,与print
一样
(一)可以使用 with
来在一定作用域内记录日志
# -*- coding: utf-8 -*-
from logbook import Logger, StreamHandler
import logbook
import sys
handler=StreamHandler(sys.stdout)
log=Logger('test')
def main:
log.info('something logging')
if __name__=='__main__':
with handler.applicationbound:
main
(二)也可以指定作用于整个应用
# -*- coding: utf-8 -*-
from logbook import Logger, StreamHandler
import logbook
import sys
handler=StreamHandler(sys.stdout)
handler.push_application
log=Logger('test')
def main:
log.info('something logging')
if __name__=='__main__':
main
使用 FileHandler
可以把日志记录到文件,这也是最常见的方式
# -*- coding: utf-8 -*-
from logbook import Logger, FileHandler
import logbook
import sys
handler=FileHandler('app.log')
handler.push_application
log=Logger('test')
def main:
log.info('something logging')
if __name__=='__main__':
main
日志就写到了 app.log
文件
同时把记录输出到多个地方可以方便查阅和记录,初始化 Handler
的时候设置bubble
参数就可以使得其它Handler
也可以接收到记录
from logbook import Logger, StreamHandler, FileHandler
import logbook
import sys
'''
记录日志到文件和STDOUT
'''
StreamHandler(sys.stdout, level='DEBUG').push_application
FileHandler('app.log', bubble=True, level='INFO').push_application
log=Logger('test')
def main:
log.info('hello world')
if __name__=='__main__':
main
另外,通过 level
可以设置日志级别,级别如下,从下到上级别越来越高,如level
设置为INFO
, 则除了DEBUG
外都会记录,设置不同的级别,搭配各种Handler
可以让日志的记录更加灵活,上边使用的log.info
可以使用不同的记录级别
级别 | 说明 |
---|---|
critical | 严重错误,需要退出程序 |
error | 错误,但在可控范围内 |
warning | 警告 |
notice | 大多数情况下希望看到的记录 |
info | 大多数情况不希望看到的记录 |
debug | 调试程序的时候详细输出的记录 |
和日志文件同样重要的就是 MailHandler
了,当出现了比较严重错误的时候就要发送邮寄进行通知
详细的文档见:
分别使用了 163
和qq
邮箱发送邮件测试,使用的邮箱需要开启SMTP权限,代码如下(163和qq发送参数稍有不同)
163 Mail
# -*- coding: utf-8 -*-
from logbook import Logger, MailHandler
import logbook
import sys
sender='Logger'
recipients=['dongdong@qq.com']
email_user='dongdong@163.com'
email_pass='password'
mail_handler=MailHandler(sender, recipients,
server_addr='smtp.163.com',
starttls=True,
secure=False,
credentials=(email_user, email_pass),
format_string=u'''
Subject: {record.level_name} on My Application
Message type: {record.level_name}
Location: {record.filename}:{record.lineno}
Module: {record.module}
Function: {record.func_name}
Time: {record.time:%Y-%m-%d %H:%M:%S}
Remote IP: {record.extra[ip]}
Request: {record.extra[url]} [{record.extra[method]}]
Message: {record.message}
''',
bubble=True)
log=Logger('test')
def main:
log.info('something logging')
if __name__=='__main__':
with mail_handler.threadbound:
main
QQ Mail
# -*- coding: utf-8 -*-
from logbook import Logger, MailHandler
import logbook
import sys
sender='Logger'
recipients=['dongdong@163.com']
email_user='dongdong@qq.com'
email_pass='password'
mail_handler=MailHandler(sender, recipients,
server_addr='smtp.qq.com',
starttls=False,
secure=True,
credentials=(email_user, email_pass),
format_string=u'''
Subject: {record.level_name} on My Application
Message type: {record.level_name}
Location: {record.filename}:{record.lineno}
Module: {record.module}
Function: {record.func_name}
Time: {record.time:%Y-%m-%d %H:%M:%S}
Remote IP: {record.extra[ip]}
Request: {record.extra[url]} [{record.extra[method]}]
Message: {record.message}
''',
bubble=True)
log=Logger('test')
def main:
log.info('something logging')
if __name__=='__main__':
with mail_handler.threadbound:
main
内容 format_string
中的用大括号的会进行数值替换,Subject
字段上边的``和下边需要空一行,这样解析参数收到的邮件才会正确的显示标题
上边发送邮件的例子,参数里面有一个 record.extra[url]
,这个参数是可以自己指定的,比如编写WSGI的程序,处理URL,每一条记录都希望记录到访问者的IP,可以这样做:
# -*- coding: utf-8 -*-
from logbook import Logger, StreamHandler, Processor
import logbook
import sys
handler=StreamHandler(sys.stdout)
handler.format_string='[{{record.time:%Y-%m-%d %H:%M:%S}}] IP:{record.extra[ip]} {record.level_name}: {record.channel}: {record.message}'
handler.formatter
log=Logger('test')
def inject_ip(record):
record.extra['ip']='127.0.0.1'
with handler.applicationbound:
with Processor(inject_ip).applicationbound:
log.error('something error')
使用自定义参数,需要重新设置 format_string
,才能进行记录,record类可以在这里找到,详细参数见logbook.LogRecord
Output:
[2016-12-13 12:20:46] IP:127.0.0.1 ERROR: test: something error
上边在介绍的自定义日志格式的时候使用的时间是虽然指定了格式但是是 UTC
格式,跟北京时间是差了8个小时的。所以需要设置让它记录本地的时间
在刚才的例子前面加上如下代码即可
logbook.set_datetime_format('local') # <=新加入行
handler=StreamHandler(sys.stdout)
handler.format_string='[{record.time:%Y-%m-%d %H:%M:%S}] IP:{record.extra[ip]} {record.level_name}: {record.channel}: {record.message}'
handler.formatter
更过日期格式化的设置参看:api/utilities - logbook.set_datetime_format(datetime_format)
从最开始的例子来看,可以使用两种方式来记录日志,一种是在最开始使用 push_*
来激活,另一种是使用的时候用with
构造上下文,现在进行一些补充
push | with | pop |
---|---|---|
push_application | applicationbound | pop_application |
push_thread | threadbound | pop_threadbound |
push_greenlet | greenletbound | pop_greenlet |
使用 pop_*
可以取消记录的上下文
application作用于整个应用,thread只针对当前线程
handler=MyHandler
handler.push_application
# all here goes to that handler
handler.pop_application
使用多个Handler的时候,使用 push_
方式启动的上下文不会形成嵌套,但是使用with
启动的上下文会形成嵌套,可以使用nested handler
import os
from logbook import NestedSetup, Handler, FileHandler,
MailHandler, Processor
def inject_information(record):
record.extra['cwd']=os.getcwd
setup=NestedSetup([
# Handler避免stderr接受消息
Handler(),
FileHandler('application.log', level='WARNING'),
MailHandler('servererrors@example.com', ['admin@example.com'],
level='ERROR', bubble=True),
Processor(inject_information)
])
使用的时候就只需要一个 with
来启动
with setup.threadbound:
log.info('something logging')
logbook
是个不错的包,记录日志灵活方便,比自己包装发送邮件方便了不少,整理了一些基本用法,还有不少值得学习的功能,暂时能用到的基本上就这么多,之后用到高级的功能再继续研究补充。
题图:pexels,CC0 授权。
*请认真填写需求信息,我们会在24小时内与您取得联系。