优雅的实现打印requestId

前言

对于如何优雅的实现打印requestId这事情上,google了半天都没有找到一个满足我的需求的实现。唯有自己去实现一个,整个思路我觉得还挺有趣的,值得分享出来,所以就有了这文章。

需求

我负责的子系统有:IP管理系统,爬虫管理系统,爬虫系统。这个只是整个系统的其中一部分。这个时候,如果某个请求或者某个服务出错了,需要定位问题的原因就比较复杂了。为了快速定位问题,很核心的一个点日志规范,而日志规范中最最最重要的一点就是要有个requestId来跟踪整个整一个请求或服务的调用链路。需求来了,为每一条日志都加上requestId这个参数。这个需求再抽象一下,就是为每一个日志都增加一个前缀。这个前缀可以有requestId, server_name等等信息。这样抽象之后呢,如果还需要增加其他参数也方便。ps:requestId的生成可以简单粗暴的uuid来,也可以让它带有信息(IP,服务名等等),这个不是本文的重点,最好就由一个专门的函数来生成,以后有变化改起来也方便。正如某名言说的那样,用一个稳定的东西来包装一个不稳定的东西以提供一个稳定的服务

实现思路

不用动脑子想到的实现方式

  1. 类的初始化参数&函数参数都提供一个参数专门来传递log前缀
  2. 做一个装饰器给需要打印业务log的方法或函数使用

对于方式1,这个实现起来巨恶心,入侵过大,偶合过大我的内心是拒绝的。对于方式2稍微好了一点,传说当中的切面编程,对代码没有入侵性,但是粒度太大,函数或者方法内的log无法加上前缀,肯定是不行的,所以只能放弃。所以此时又把目光放到方式1上,此时大脑冒出了一个大胆的想法,给整个系统的类或函数打补丁。补丁的作用就是为类的初始化方法添加上log前缀的可选参数,函数参数添加上log前缀的可选参数。然后还居然真的有相关的模块可以获取方法或者类的参数信息,并修改,这个高级功能只存在python3之中。准备撸起袖子就开干的时候,还是被恶心到了,即便实现了,还是需要将log前缀层层传递,代码将会变得很难看。前面说的层层传递,抽象一下就是在每个调用间共享一个变量,此时大脑想起了threading模块中的local类,但是但是我们的服务部署一般是gevent的模式,threading的local模块并不是一个协程安全的东西。万能的google再次拯救我,在werkzeug实现了一个协程安全Local类,celery当中也实现了,实现方式都差不多,连注释都一毛一样。

最终的实现方式

暂时想到的最优的实现方式:在服务的开头出在local对象中设置上log的前缀,出口处删掉,每条log都从Local对象处获取前缀打印。这都是由框架来做,具体的业务代码是无感知的。这样做的好处就是对代码几乎是无入侵的,扩展性强,大家都开心的方式。在细化一下具体实现方式,本质上还是添加中间层来实现。对没错,任何计算机问题均可以通过增加一个中间层来解决

  • http请求:在header处添加上requestID这参数,然后中间件来做统一处理
  • celery异步任务:任务消息带上log前缀,任务开始前做添加上前缀,任务返回后删掉

实现核心过程

一点说明,代码中有部分配置信息,这个我赖得改直接就粘贴复制过来了,不是什么重点内容 ,忽略就好。另外的,必要的一下基础知识 ,我并没有进行讲解,一来嘛,不是重点,二来嘛,我感觉我会写得没别人好,但会给出有帮助的文章链接。

django 中间件

给application添加Local对象清理的中间件

有参考价值的文章: 关于werzeug模块中对的Local对象的使用的中文文章

文件wsgi.py

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

# 请求结束后对当前线程或者协程中的local对象中的变量进行清除
application = local_manager.make_middleware(application)

自定义django的中间件

将django中间件怎么做的文章: django中间件。代码没啥好说的,核心点就是设置给Local对象设置上一个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):
            
            # 通过一个函数生成request_id
            request_id = gen_request_id()
            log_data = {"requestId": request_id}

        return log_data

log的filter

关于python log处理模块的文章:python log基础知识

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异步任务

这个是整个实现的难点所在,也是值得借鉴的地方,这部分必须唠叨的解释一下,因为这部分网上没啥好的文章给我贴出来,而且有的部分被迫自己实现的,说来都是泪啊,差点弄我一个通宵。前面所说的,任务消息带上log前缀,任务开始前做添加上前缀,任务返回后删掉难点是在于如何优雅的做到不入侵原先的代码。

任务消息带上log前缀

马上想到的方式在异步任务的参数上带上,但是感觉不太优雅,直觉上celery框架应该有提供携带额外信息的参数。翻了一下celery官网文档,有一个headers的参数放出刺眼的光芒引起了我的注意,测试了一下还真的可以,这样仅仅需要将log前缀信息放在headers中即可。ps:测试的时候还顺便更加理解amqp这协议,celery的一下额外的控制参数例如task_id都是存放在消息的headers中的,然后在接收到消息后做统一的处理。

任务开始前做添加上前缀,任务返回前删掉

直觉上celery框架是有留出有相关的接口做这事,还是去翻文档。翻到有一个after_return的handler, 但是但是没有一个pre_execute的handler。只找到一个task_prerun的信号,这货是个全局性的东西,感觉上粒度太大,我不好控制。这时候冒出大胆的想法,去翻源码,看看怎么添加上pre_execute的handler,再利用monkey_patch给celery这个包打补丁。直接翻源码还是挺痛苦的,有点晕,此时想到了pycharm是可以断点调试的,断点观察整个调用过程+全局关键字搜索来找添加的入口。最后在celery/app/trace.py文件中找到了build_tracer这个函数,一个任务执行所有的流程控制都在里面。真的是牛逼啊,用一个流程控制的方法来控制整个流程,思路很好,值得借鉴,大神们的软件架构思路无比膜拜。所要做的就很简单,按照里面的规则照葫芦画瓢的添加一个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):
        """
        自动根据local变量中的log相关的信息,设置去message的headers中,让异步任务根据设置的log信息设置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):
        # 任务开始前将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("异步任务设置log的出错", exc_info=True)

        setattr(local, settings.LOCAL_LOG_ATTR_NAME, log_data)
    
    def after_return(self, status, retval, task_id, args, kwargs, einfo):
        # 清理变量,其实不清理也没什么影响,毕竟部署都是进程池或者是协程池的,不会有很多。
        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 

最后的唠叨

人上了年纪,容易发出一些感慨。这个东西实现后,总结的体会是:1. 越来越意识到抽象是一样多么重要的事情,如果没有去抽象出共性,我可能就被迫用最恶心的方式1来实现了。2. 编程这工作虽然说是偏文科,以知识和经验为主,但是良好的分析思路却能很好的弥补上知识和经验上的不足。在分析能力的提高这部分还是要经过有意识的训练才行,这里立个flag,那天我觉得自己分析能力有了一个台阶的上升后,撸一篇如果分析的文章。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容