python微服务sanic 使用异步zipkin(2) - 一步步创建Sanic插件: sanic-zipin

参考:python微服务sanic 使用异步zipkin(1)

关键字:python sanic 微服务 异步 zipkin sanic-plugin 插件 Sanic-Plugins-Framewor Pypi发布
所需环境:python3.7, Docker, Linux or WSL

image.png

Sanic插件(Plugin/Extension) - sanic-zipkin已经ready,你可以轻松用pip安装啦:
喜欢的话,github点个赞吧:https://github.com/kevinqqnj/sanic-zipkin

pip install sanic-zipkin
# app.py
from sanic_zipkin import SanicZipkin, logger, sz_rpc
sz = SanicZipkin(app)

上一篇已经学会了如何在Sanic app里引入aiozipkin,来做分布式系统追踪。本篇,来讨论下,如何创建一个完整的Sanic插件,以方便自己或者分享给他人。

先来看看插件是怎么用的:

功能

  • adding "Request span" by default
  • if Request is from another micro-service endpoint, span will be attached (Inject/Extract) to that endpoint
  • use "logger" decorator to create span for "methods" calls
  • use "sz_rpc" method to create sub-span for RPC calls, attaching to parent span

使用例子

  1. run examples/servic_a.py and examples/service_b.py
  2. use Docker to run zipkin or jaeger:
    docker run -d -p9411:9411 openzipkin/zipkin:latest
    or
    docker run -d -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp -p5778:5778 -p16686:16686 -p14268:14268 -p9411:9411 jaegertracing/all-in-one
  3. access the endpoint:
  • 最简应用:
    curl localhost:8000/ to see plugin's basic usage
from sanic_zipkin import SanicZipkin, logger, sz_rpc

app = Sanic(__name__)

# initilize plugin, default parameters:
#        zipkin_address = 'http://127.0.0.1:9411/api/v2/spans'
#        service = __name__
#        host = '127.0.0.1'
#        port = 8000
sz = SanicZipkin(app, service='service-a')

@app.route("/")
async def index(request):
    return response.json({"hello": "from index"})

This "/" endpoint will add trace span to zipkin automatically

  • 使用装饰器装饰方法,以及链式trace
    curl localhost:8000/2 to see how to decorate methods and chain-calls
@logger()
async def db_access(context, data):
    await asyncio.sleep(0.1)
    print(f'db_access done. data: {data}')
    return

@sz.route("/2")
async def method_call(request, context):
    await db_access(context, 'this is method_call data')
    return response.json({"hello": 'method_call'})

Use "@logger" decorator to generate span for methods.
Note: in this case, you need to use "@sz.route" decorator, and pass contextparameter to method calls.

  • 微服务之间通过PRC访问:
    curl localhost:8000/3 to see how RPC calls working, both GET/POST is supported
@logger()
async def decorate_demo(context, data):
    await db_access(context, data)
    data = {'payload': 'rpc call data of decorate_demo'}
    rsp = await sz_rpc(context, backend_service2, data, method='GET')
    print(rsp.status, await rsp.text())
    return

@sz.route("/3")
async def rpc_call(request, context):
    await decorate_demo(context, 'this is index4 data')
    data = {'payload': 'rpc call data of rpc_call'}
    rsp = await sz_rpc(context, backend_service1, data) # default method='POST'
    print(rsp.status, await rsp.text())
    return response.json({"hello": 'rpc_call'})

method sz_rpc just wrapper span injection to RPC POST/GET calls. In peer server, span-context will be automatically extracted and generate a chain-view in zipkin.

  1. 在Zipkin/Jaeger UI里查看Trace:


    image.png

Sanic插件开发过程

使用Sanic-Plugins-Framework开发,省时省力,而且充分利用Sanic异步框架的威力。

1. 插件初始化

  • 用户可自定义的初始化变量
    继承Sanic-Plugins-Framework(SPF) 的Contextualize类型,在on_before_registered方法引用时,加载用户自定义的初始变量。
    目前支持:
    • zipkin server地址
    • 微服务名称service
    • 微服务IP, port
from spf.plugins.contextualize import Contextualize

class SanicZipkin(Contextualize):
    def __init__(self, *args, **kwargs):
        super(SanicZipkin, self).__init__(*args, **kwargs)
        self.zipkin_address = None
        self.service = None
        self.host = None
        self.port = None

    def on_before_registered(self, context, *args, **kwargs):
        self.zipkin_address = kwargs.get('zipkin_address', 'http://127.0.0.1:9411/api/v2/spans')
        self.service = kwargs.get('service', __name__)
        self.host = kwargs.get('host', '127.0.0.1')
        self.port = kwargs.get('port', 8000)
        _logger.info(f'SanicZipkin: before registered: service={self.service}')
  • 创建aiozipkin服务
    实例化sanic_zipkin,然后调用Sanic 'before_server_start'方法,初始化context.tracercontext.aio_session
    context是SPF全局可以访问的上下文变量,可以存储任何你想要共享的数据。
sanic_zipkin = instance = SanicZipkin()

@sanic_zipkin.listener('before_server_start')
async def setup_zipkin(app, loop, context):
    endpoint = az.create_endpoint(sanic_zipkin.service, ipv4=sanic_zipkin.host,
                                port=sanic_zipkin.port)
    context.tracer = await az.create(sanic_zipkin.zipkin_address, endpoint, 
                                sample_rate=1.0)
    trace_config = az.make_trace_config(context.tracer)
    context.aio_session = aiohttp.ClientSession(trace_configs=[trace_config])
    context.span = []
    context.zipkin_headers = []

这里context.span设计成数组,模拟堆栈FILO(先进后出),是因为考虑到链式调用时,tracer需要以parent span为基础,创建child span。同理Inject/Extract用到的context.zipkin_headers也设成数组。

2. 创建middleware,给Request GET/POST自动添加span

  • Requst: 先用middleware装饰器来监听request消息,然后调用自定义方法request_span(request, context)来创建span。
@sanic_zipkin.middleware(priority=2, with_context=True)
def mw1(request, context):
    context.log(DEBUG, f'mw-request: add span and headers before request')
    span = request_span(request, context)
    context.span.append(span)
    context.zipkin_headers.append(span.context.make_headers())

这里,要考虑到,当前span是new span,还是child span。
span.append()压入堆栈。

  • 自定义方法request_span(),通过读取request里是否有zipkin_headers信息,来判断当前是其它微服务的RPC call,还是用户发起的http访问。
def request_span(request, context):
    context.log(DEBUG, f'REQUEST json: {request.json}, args: {request.args}')
    headers = request.parsed_json.get('zipkin_headers', None) if request.json else \
                request.args.get('zipkin_headers', None)
  • Response: 在一次http访问结束,返回response时,此次访问的context.spancontext.zipkin_headers弹出堆栈,以免污染到其它http访问的trace:span.pop()
@sanic_zipkin.middleware(priority=8, attach_to='response', relative='post',
                      with_context=True)
def mw2(request, response, context):
    context.span.pop()
    context.zipkin_headers.pop()
    context.log(DEBUG, 'mw-response: clear span/zipkin_headers after Response')

3. 创建zipkin_headers,用于RPC Inject/Extract

如果当前堆栈里有zipkin_headers,则方法request_span()创建上下文:

def request_span(request, context):
    context.log(DEBUG, f'REQUEST json: {request.json}, args: {request.args}')
    headers = request.parsed_json.get('zipkin_headers', None) if request.json else \
                request.args.get('zipkin_headers', None)
    if headers:
        span_context = az.make_context(headers)
        with context.tracer.new_child(span_context) as span:
            ...

如果无,则创建新的span:

        with context.tracer.new_trace() as span:
            span.name(f'{request.method} {request.path}')
            ...

4. methods函数,添加@logger装饰器

对于非http request的方法函数,因为没有middleware可以监听,则需要创建新的装饰器了。
使用@logger时,默认context作为第一个参数:

def logger(type=None, category=None, detail=None, description=None,
               tracing=True, level=logging.INFO, *args, **kwargs):
        def decorator(fn=None):
            @functools.wraps(fn)
            async def _decorator(*args, **kwargs):
                # print('_decorator args: ', args, kwargs)
                context = args[0] if len(args) > 0 and isinstance(args[0], ContextDict) else None
                ...

然后创建新的new span 或 child span。
同时,添加(Inject)新的上下文zipkin_headers

                span = gen_span(fn.__name__, context)
                context.zipkin_headers.append(span.context.make_headers())

执行装饰器所装饰的函数fn()
之后,必须清除(弹出最上面)本次装饰器新增的临时span和zipkin_headers。因为当前函数外部的其它引用函数(如果有)所需要的数据,还在堆栈下面。

                try:
                    exce = False
                    res = await fn(*args, **kwargs)
                    return res
                except Exception as e:
                    exce = True
                    raise e
                finally:
                    ...
                    # clean up tmp vars for this wrapper
                    context.span.pop()
                    context.zipkin_headers.pop()

5. 创建帮助函数sz_rpc(),简化RPC 访问其它微服务时的Inject操作

这个很简单的helper,负责把zipkin_headers,inject到POST/GET访问的data里,这样,对方收到http request时,就可以顺利的Extract,得到span上下文了。

async def sz_rpc(context, url, data, method='POST'):
    data.update({'zipkin_headers': json.dumps(context.zipkin_headers[-1])})
    if method.upper() == 'POST':
        return await context.aio_session.post(url, json=data)
    else: 
        return await context.aio_session.get(url, params=data)

6. 发布插件到Pypi

插件写好了,下面就是发布了。

  • 先注册一个Pypi账号:https://pypi.org
  • 修改当前目录的结构,以及添加一些必要的标注文件:
kevinqq@CN-00009841:/c/Users/xxx/git/sanic-zipkin$ tree -L 2
├── CHANGES.txt  # 版本信息,必须
├── LICENSE         # 必须
├── MANIFEST.in
├── README.md
├── dist                  # 发布时自动打的包
│   └── sanic-zipkin-0.1.2.tar.gz
├── examples
│   ├── requirements.txt
│   ├── service_a.py
│   └── service_b.py
├── requirements-dev.txt
├── sanic_zipkin     # 包的目录
│   ├── __init__.py  # 必须。含版本信息和可引用的对象
│   └── sanic_zipkin.py    # 主文件
├── sanic_zipkin.egg-info  # 发布时自动生成
└── setup.py  # 发布用的程序
  • 发布前检查:
    python3 setup.py check

  • 发布到Pypi:
    python3 setup.py sdist upload
    此时,会让你输入Pypi的密码。
    如果收到200,则上传成功。

  • 检查是否已经可用:
    pip install sanic-zipkin

Quetions?
Github: https://github.com/kevinqqnj/sanic-zipkin

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

推荐阅读更多精彩内容