對于一個系統(tǒng)來說,監(jiān)控、鏈路追蹤、日志的這三者需求都是必然存在的,而有的時候我們會搞不清楚這三者相互之間是什么關系。我之前在做系統(tǒng)設計的時候也考慮過,是不是有必要引入那么多組件,畢竟如果這三者完全分開每一個一項的話,就有三個組件了(事實上就是:Prometheus+Grafana、Jaeger、ELK)。
因此想做個筆記嘗試舉例來梳理下。
外部鏈接:
Monitoring(監(jiān)控)舉例來說就是:定期體檢。使用監(jiān)控系統(tǒng)把需要關注的指標采集起來,形成報告,并對需要關注的異常數(shù)據(jù)進行分析形成告警。
特點是:
這也是Prometheus的架構做得非常簡單的原因,Monitoring的需求并沒有包含非常高的并發(fā)量和通訊量。反過來說:高并發(fā)、大數(shù)據(jù)量的需求并不適用于Monitoring這個點。
Tracing(鏈路追蹤)舉例來說就是:對某一項工作的定期匯報。某個工作開始做了A,制作A事件的報告,收集起來,然后這個工作還有B、C、D等條目,一個個處理,然后都匯總進報告,最終的結果就是一個Tracing。
特點是:
因為Tracing是針對某一個事件(一般來說就是一個API),而這個API可能會和很多組件進行溝通,后續(xù)的所有的組件溝通無論是內部還是外部的IO,都算作這個API調用的Tracing的一部分。可以想見在一個業(yè)務繁忙的系統(tǒng)中,API調用的數(shù)量已經是天文數(shù)字,而其衍生出來的Tracing記錄更是不得了的量。其特點就是高頻、巨量,一個API會衍生出大量的子調用。
也因此適合用來做Monitoring的系統(tǒng)就不一定適合做Tracing了,用Prometheus這樣的系統(tǒng)來做Tracing肯定完蛋(Prometheus只有拉模式,全部都是HTTP請求,高并發(fā)直接掛掉)。一般來說Tracing系統(tǒng)都會在本地磁盤IO上做日志(最高效、也是最低的Cost),然后再通過本地Agent慢慢把文本日志數(shù)據(jù)發(fā)送到聚合服務器上,甚至可能在聚合服務器和本地的Agent之間還需要做消息隊列,讓聚合服務器慢慢消化巨量的消息。
Tracing在現(xiàn)在的業(yè)界是有標準的:OpenTracing,因此它不是很隨意的日志/事件聚合,而是有格式要求的日志/事件聚合,這就是Tracing和Logging最大的不同。
Logging(日志)舉例來說就是:廢品回收站。各種各樣的物品都會匯總進入到配品回收站里,然后經過分門別類歸納整理,成為各種可回收資源分別回收到商家那里。一般來說我們在大型系統(tǒng)中提到Logging說的都不是簡單的日志,而是日志聚合系統(tǒng)。
從本質上來說,Monitoring和Tracing都是Logging,Logging是這三者中覆蓋面最大的超集,而前兩者則是其一部分的子集。Logging最麻煩的是,開發(fā)者也不會完全知道最后記錄進入到日志系統(tǒng)里的一共會有哪些東西,只有在使用(檢索)的時候才可能需要匯總查詢總量中的一部分。
要在一般的Loggin系統(tǒng)中進行Monitoring也是可以的,直接把聚合進來的日志數(shù)據(jù)提取出來,定期形成數(shù)據(jù)報告,就是監(jiān)控了。Tracing也是一樣,只要聚合進了Logging系統(tǒng),有了原始數(shù)據(jù),后面要做都是可以做的。因此Logging系統(tǒng)最為通用,其特點和Tracing基本一致,也是需要處理高頻并發(fā)和巨大的數(shù)據(jù)量。
這樣一看就很清楚了,每個組件都有其存在的必要性:
隨著分布式應用的普及,現(xiàn)在的一些應用系統(tǒng)不再像以前,所有的文件(前后端程序)都打包在一個包中,現(xiàn)在的很多應用都是模塊化開發(fā),開發(fā)的團隊也是不一樣,服務與服務之間的調用也比較多,在這種情況下,系統(tǒng)的日志就顯得尤其的重要,然而,在多數(shù)情況下,當我們的系統(tǒng)出現(xiàn)了異常,需要查看日志時,就會很抓狂。為了避免這種情況,我們需要把同一次的業(yè)務調用鏈上的日志串聯(lián)起來。
本次通過一個簡單的SpringBoot應用來總結,我們如何將日志串聯(lián)起來,以下截圖是最終的實現(xiàn)效果
使用idea創(chuàng)建一個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目錄下,創(chuàng)建日志框架整合配置文件: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> <!--保留天數(shù)--> <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創(chuàng)建的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提供的一個支持動態(tài)打印日志信息的工具 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); } }
自定義的攔截器需要實現(xiàn)HandlerInterceptor.java接口,然后重寫preHandle方法;MDC(Mapped Diagnostic Context)診斷上下文映射,@Slf4j提供的動態(tài)打印日志工具。
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"); } }
經過上述的幾個步驟,基本上就可以簡單的將同一次的業(yè)務調用鏈上的日志串聯(lián)起來了。
簡單的寫一個測試類: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=張三
控制臺的輸出如下:
至此,一個最簡單的日志串聯(lián)就做好了
作者:Ethan
鏈接:https://juejin.cn/post/7283418392957796389
來源:稀土掘金
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
者:東東 yasking
來源:https://blog.yasking.org/a/python-logbook.html
Python 本身有l(wèi)ogging日志記錄模塊,之前發(fā)現(xiàn)了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
參數(shù)就可以使得其它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 | 大多數(shù)情況下希望看到的記錄 |
info | 大多數(shù)情況不希望看到的記錄 |
debug | 調試程序的時候詳細輸出的記錄 |
和日志文件同樣重要的就是 MailHandler
了,當出現(xiàn)了比較嚴重錯誤的時候就要發(fā)送郵寄進行通知
詳細的文檔見:
分別使用了 163
和qq
郵箱發(fā)送郵件測試,使用的郵箱需要開啟SMTP權限,代碼如下(163和qq發(fā)送參數(shù)稍有不同)
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
中的用大括號的會進行數(shù)值替換,Subject
字段上邊的``和下邊需要空一行,這樣解析參數(shù)收到的郵件才會正確的顯示標題
上邊發(fā)送郵件的例子,參數(shù)里面有一個 record.extra[url]
,這個參數(shù)是可以自己指定的,比如編寫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')
使用自定義參數(shù),需要重新設置 format_string
,才能進行記錄,record類可以在這里找到,詳細參數(shù)見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
構造上下文,現(xiàn)在進行一些補充
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
是個不錯的包,記錄日志靈活方便,比自己包裝發(fā)送郵件方便了不少,整理了一些基本用法,還有不少值得學習的功能,暫時能用到的基本上就這么多,之后用到高級的功能再繼續(xù)研究補充。
題圖:pexels,CC0 授權。
*請認真填寫需求信息,我們會在24小時內與您取得聯(lián)系。