優(yōu)雅的實現(xiàn)打印requestId

前言

對于如何優(yōu)雅的實現(xiàn)打印requestId這事情上,google了半天都沒有找到一個滿足我的需求的實現(xiàn)饶辙。唯有自己去實現(xiàn)一個轴总,整個思路我覺得還挺有趣的,值得分享出來怔檩,所以就有了這文章。

需求

我負(fù)責(zé)的子系統(tǒng)有:IP管理系統(tǒng)蓄诽,爬蟲管理系統(tǒng)薛训,爬蟲系統(tǒng)。這個只是整個系統(tǒng)的其中一部分仑氛。這個時候乙埃,如果某個請求或者某個服務(wù)出錯了,需要定位問題的原因就比較復(fù)雜了调衰。為了快速定位問題膊爪,很核心的一個點日志規(guī)范,而日志規(guī)范中最最最重要的一點就是要有個requestId來跟蹤整個整一個請求或服務(wù)的調(diào)用鏈路嚎莉。需求來了米酬,為每一條日志都加上requestId這個參數(shù)。這個需求再抽象一下趋箩,就是為每一個日志都增加一個前綴赃额。這個前綴可以有requestId, server_name等等信息。這樣抽象之后呢叫确,如果還需要增加其他參數(shù)也方便跳芳。ps:requestId的生成可以簡單粗暴的uuid來,也可以讓它帶有信息(IP竹勉,服務(wù)名等等)飞盆,這個不是本文的重點,最好就由一個專門的函數(shù)來生成次乓,以后有變化改起來也方便吓歇。正如某名言說的那樣,用一個穩(wěn)定的東西來包裝一個不穩(wěn)定的東西以提供一個穩(wěn)定的服務(wù)票腰。

實現(xiàn)思路

不用動腦子想到的實現(xiàn)方式

  1. 類的初始化參數(shù)&函數(shù)參數(shù)都提供一個參數(shù)專門來傳遞log前綴
  2. 做一個裝飾器給需要打印業(yè)務(wù)log的方法或函數(shù)使用

對于方式1城看,這個實現(xiàn)起來巨惡心,入侵過大杏慰,偶合過大我的內(nèi)心是拒絕的测柠。對于方式2稍微好了一點,傳說當(dāng)中的切面編程缘滥,對代碼沒有入侵性轰胁,但是粒度太大,函數(shù)或者方法內(nèi)的log無法加上前綴朝扼,肯定是不行的软吐,所以只能放棄。所以此時又把目光放到方式1上吟税,此時大腦冒出了一個大膽的想法凹耙,給整個系統(tǒng)的類或函數(shù)打補(bǔ)丁。補(bǔ)丁的作用就是為類的初始化方法添加上log前綴的可選參數(shù)肠仪,函數(shù)參數(shù)添加上log前綴的可選參數(shù)肖抱。然后還居然真的有相關(guān)的模塊可以獲取方法或者類的參數(shù)信息,并修改异旧,這個高級功能只存在python3之中意述。準(zhǔn)備擼起袖子就開干的時候,還是被惡心到了吮蛹,即便實現(xiàn)了荤崇,還是需要將log前綴層層傳遞,代碼將會變得很難看潮针。前面說的層層傳遞术荤,抽象一下就是在每個調(diào)用間共享一個變量,此時大腦想起了threading模塊中的local類每篷,但是但是我們的服務(wù)部署一般是gevent的模式瓣戚,threading的local模塊并不是一個協(xié)程安全的東西。萬能的google再次拯救我焦读,在werkzeug實現(xiàn)了一個協(xié)程安全Local類子库,celery當(dāng)中也實現(xiàn)了,實現(xiàn)方式都差不多矗晃,連注釋都一毛一樣仑嗅。

最終的實現(xiàn)方式

暫時想到的最優(yōu)的實現(xiàn)方式:在服務(wù)的開頭出在local對象中設(shè)置上log的前綴,出口處刪掉张症,每條log都從Local對象處獲取前綴打印仓技。這都是由框架來做,具體的業(yè)務(wù)代碼是無感知的吠冤。這樣做的好處就是對代碼幾乎是無入侵的浑彰,擴(kuò)展性強(qiáng),大家都開心的方式拯辙。在細(xì)化一下具體實現(xiàn)方式郭变,本質(zhì)上還是添加中間層來實現(xiàn)。對沒錯涯保,任何計算機(jī)問題均可以通過增加一個中間層來解決诉濒。

  • http請求:在header處添加上requestID這參數(shù),然后中間件來做統(tǒng)一處理
  • celery異步任務(wù):任務(wù)消息帶上log前綴夕春,任務(wù)開始前做添加上前綴未荒,任務(wù)返回后刪掉

實現(xiàn)核心過程

一點說明,代碼中有部分配置信息及志,這個我賴得改直接就粘貼復(fù)制過來了片排,不是什么重點內(nèi)容 寨腔,忽略就好。另外的率寡,必要的一下基礎(chǔ)知識 迫卢,我并沒有進(jìn)行講解,一來嘛冶共,不是重點乾蛤,二來嘛,我感覺我會寫得沒別人好捅僵,但會給出有幫助的文章鏈接家卖。

django 中間件

給application添加Local對象清理的中間件

有參考價值的文章: 關(guān)于werzeug模塊中對的Local對象的使用的中文文章

文件wsgi.py

from werkzeug.local import Local, LocalManager
local = Local()
local_manager = LocalManager([local])

# 請求結(jié)束后對當(dāng)前線程或者協(xié)程中的local對象中的變量進(jìn)行清除
application = local_manager.make_middleware(application)

自定義django的中間件

將django中間件怎么做的文章: django中間件。代碼沒啥好說的庙楚,核心點就是設(shè)置給Local對象設(shè)置上一個dict對象上荡,里面包含了需要的前綴信息。

from xxx import local

class LogPrexiMiddleware(MiddlewareMixin):
    def process_request(self, request):
        log_data = self._get_log_data(request)
        attr_name = getattr(settings, LOCAL_LOG_ATTR_NAME, DEFAULT_LOCAL_LOG_ATTR_NAME)
        
        setattr(local, attr_name, log_data)

    def _get_log_data(self, request):
        log_header_key = getattr(settings, LOG_HEADER_KEY_SETTING, None)

        log_data = {}
        if log_header_key and log_header_key in request.META:
            log_data = {"requestId": request.META.get(log_header_key)}

        if not log_data and getattr(settings, GENERATE_REQUEST_ID_IF_NOT_IN_HEADER_SETTING, False):
            
            # 通過一個函數(shù)生成request_id
            request_id = gen_request_id()
            log_data = {"requestId": request_id}

        return log_data

log的filter

關(guān)于python log處理模塊的文章:python log基礎(chǔ)知識醋奠。

from xxx import local


class PrefixFilter(Filter):
    def filter(self, record):
        log_data = getattr(local, settings.LOCAL_LOG_ATTR_NAME, {})
        log_prefix = "".join(["[{}={}]".format(key, value) for key, value in log_data.items()])
        
        # log format: [%(log_prefix)s][%(asctime)s,%(msecs)d][%(levelname)s]
        record.log_prefix = log_prefix
        return True

celery異步任務(wù)

這個是整個實現(xiàn)的難點所在榛臼,也是值得借鑒的地方,這部分必須嘮叨的解釋一下窜司,因為這部分網(wǎng)上沒啥好的文章給我貼出來沛善,而且有的部分被迫自己實現(xiàn)的,說來都是淚啊塞祈,差點弄我一個通宵金刁。前面所說的,任務(wù)消息帶上log前綴议薪,任務(wù)開始前做添加上前綴尤蛮,任務(wù)返回后刪掉難點是在于如何優(yōu)雅的做到不入侵原先的代碼斯议。

任務(wù)消息帶上log前綴

馬上想到的方式在異步任務(wù)的參數(shù)上帶上产捞,但是感覺不太優(yōu)雅,直覺上celery框架應(yīng)該有提供攜帶額外信息的參數(shù)哼御。翻了一下celery官網(wǎng)文檔坯临,有一個headers的參數(shù)放出刺眼的光芒引起了我的注意,測試了一下還真的可以恋昼,這樣僅僅需要將log前綴信息放在headers中即可看靠。ps:測試的時候還順便更加理解amqp這協(xié)議,celery的一下額外的控制參數(shù)例如task_id都是存放在消息的headers中的液肌,然后在接收到消息后做統(tǒng)一的處理挟炬。

任務(wù)開始前做添加上前綴,任務(wù)返回前刪掉

直覺上celery框架是有留出有相關(guān)的接口做這事,還是去翻文檔谤祖。翻到有一個after_return的handler, 但是但是沒有一個pre_execute的handler婿滓。只找到一個task_prerun的信號,這貨是個全局性的東西泊脐,感覺上粒度太大空幻,我不好控制。這時候冒出大膽的想法容客,去翻源碼,看看怎么添加上pre_execute的handler约郁,再利用monkey_patch給celery這個包打補(bǔ)丁缩挑。直接翻源碼還是挺痛苦的,有點暈鬓梅,此時想到了pycharm是可以斷點調(diào)試的供置,斷點觀察整個調(diào)用過程+全局關(guān)鍵字搜索來找添加的入口。最后在celery/app/trace.py文件中找到了build_tracer這個函數(shù)绽快,一個任務(wù)執(zhí)行所有的流程控制都在里面芥丧。真的是牛逼啊,用一個流程控制的方法來控制整個流程坊罢,思路很好续担,值得借鑒,大神們的軟件架構(gòu)思路無比膜拜活孩。所要做的就很簡單物遇,按照里面的規(guī)則照葫蘆畫瓢的添加一個pre_execute的處理,然后mokey_patch搞起來即可憾儒。

具體的一下代碼

from xxx import local, local_manager

class BaseTask(app.Task):
    def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
                    link=None, link_error=None, shadow=None, **options):
        """
        自動根據(jù)local變量中的log相關(guān)的信息询兴,設(shè)置去message的headers中,讓異步任務(wù)根據(jù)設(shè)置的log信息設(shè)置log
        """
        
        # 自動把log prefix 信息添加到消息的headers中
        log_data = getattr(local, settings.LOCAL_LOG_ATTR_NAME, {})
        headers = options.get("headers", {})
        headers.update({settings.LOCAL_LOG_ATTR_NAME: json.dumps(log_data)})
        options["headers"] = headers

        super(BaseTask, self).apply_async(args=args, kwargs=kwargs, task_id=task_id, producer=producer,
                    link=link, link_error=link_error, shadow=shadow, **options)

     def pre_execute(self, status, retval, task_id, args, kwargs, einfo):
        # 任務(wù)開始前將headers中的log前綴信息起趾,給添加到local變量里面
        log_data = {}
        if hasattr(self.request, settings.LOCAL_LOG_ATTR_NAME):
            try:
                log_data = json.loads(getattr(self.request, settings.LOCAL_LOG_ATTR_NAME))
            except ValueError as why:
                sm_log.debug("異步任務(wù)設(shè)置log的出錯", exc_info=True)

        setattr(local, settings.LOCAL_LOG_ATTR_NAME, log_data)
    
    def after_return(self, status, retval, task_id, args, kwargs, einfo):
        # 清理變量诗舰,其實不清理也沒什么影響,畢竟部署都是進(jìn)程池或者是協(xié)程池的训裆,不會有很多眶根。
        super(BaseTask, self).after_return(status, retval, task_id, args, kwargs, einfo)
        local_manager.cleanup()
        

# 使用說明
方式一:粒度可控的
@app.task(bind=True, base=BaseTask, max_retries=3)
def debug_t(self, a):
    pass
    
方式二:全局的
app.Task = BaseTask 

最后的嘮叨

人上了年紀(jì),容易發(fā)出一些感慨缭保。這個東西實現(xiàn)后汛闸,總結(jié)的體會是:1. 越來越意識到抽象是一樣多么重要的事情,如果沒有去抽象出共性艺骂,我可能就被迫用最惡心的方式1來實現(xiàn)了诸老。2. 編程這工作雖然說是偏文科,以知識和經(jīng)驗為主,但是良好的分析思路卻能很好的彌補(bǔ)上知識和經(jīng)驗上的不足别伏。在分析能力的提高這部分還是要經(jīng)過有意識的訓(xùn)練才行蹄衷,這里立個flag,那天我覺得自己分析能力有了一個臺階的上升后厘肮,擼一篇如果分析的文章愧口。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市类茂,隨后出現(xiàn)的幾起案子耍属,更是在濱河造成了極大的恐慌,老刑警劉巖巩检,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厚骗,死亡現(xiàn)場離奇詭異,居然都是意外死亡兢哭,警方通過查閱死者的電腦和手機(jī)领舰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來迟螺,“玉大人冲秽,你說我怎么就攤上這事【馗福” “怎么了锉桑?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長浙垫。 經(jīng)常有香客問我刨仑,道長,這世上最難降的妖魔是什么夹姥? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任杉武,我火速辦了婚禮,結(jié)果婚禮上辙售,老公的妹妹穿的比我還像新娘轻抱。我一直安慰自己,他們只是感情好旦部,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布祈搜。 她就那樣靜靜地躺著,像睡著了一般士八。 火紅的嫁衣襯著肌膚如雪容燕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天婚度,我揣著相機(jī)與錄音蘸秘,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛醋虏,可吹牛的內(nèi)容都是我干的寻咒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼颈嚼,長吁一口氣:“原來是場噩夢啊……” “哼毛秘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起阻课,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叫挟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后限煞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霞揉,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年晰骑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绊序。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡硕舆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出骤公,到底是詐尸還是另有隱情抚官,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布阶捆,位于F島的核電站凌节,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏洒试。R本人自食惡果不足惜倍奢,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垒棋。 院中可真熱鬧卒煞,春花似錦、人聲如沸叼架。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乖订。三九已至扮饶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乍构,已是汗流浹背甜无。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人毫蚓。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓占键,卻偏偏與公主長得像,于是被迫代替她去往敵國和親元潘。 傳聞我的和親對象是個殘疾皇子畔乙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容