框架之日志管理

1 Butterfly 日志

1.1 Butterfly 日志概览

  • 程序标准输出

    • __stdout

  • 框架日志

    • logs/acc.log: access 日志

    • logs/err.log: error 日志

    • logs/init.log: 启动信息日志

  • 自定义日志

    • logs/common.log

    • logs/common.log.wf

    • logs/common_bf.log

    • logs/common_bf.log.wf

1.2 访问日志

logs/acc.log

1.2.1 不同请求时的访问日志

curl "http://127.0.0.1:8585/demo_api/ping" -------------------------- 正常无参数请求

2021-02-09 10:18:02	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	75DA11E45E4644EF	GET	/demo_api/ping	cost:0.000455	stat:OK	user:-	talk:	params:x=1	error_msg:	res:

curl "http://127.0.0.1:8585/demo_api/hello?str_info=meetbill" ------- 正常有参数请求

2021-02-09 10:19:51	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	549D3CA17B4182EE	GET	/demo_api/hello	cost:0.001822	stat:OK	user:-	talk:	params:str_info=meetbill	error_msg:	res:

curl "http://127.0.0.1:8585/demo_api/hello?str_infox=meetbill" ------ 接口存在,但参数错误

2021-02-09 10:21:08	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	4E44BDEBD770F599	GET	/demo_api/hello	cost:0.000263	stat:ERR_BAD_PARAMS	user:-	talk:	params:str_infox=meetbill	error_msg:Param check failed	res:

curl -v "http://127.0.0.1:8585/demo_api/hellox?str_info=meetbill" -- 接口不存在

2021-02-09 10:22:10	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	8A348A0C7D9699D2	GET	/demo_api/hellox	cost:0.002694	stat:400	user:-	talk:	params:	error_msg:API Not Found	res:

1.2.2 字段解析

curl "http://127.0.0.1:8585/demo_api/hello?str_info=meetbill" ------- 正常有参数请求

2021-02-08 21:35:30	91640	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	1FA15BDE4B6CD7A0	GET	/demo_api/hello	cost:0.000162	stat:OK	user:-	talk:ceshi1=0.098;ceshi2=0.002	params:str_info=hello	error_msg:	res:ceshi2=5.4;ceshi1=5.3

以正常有参数请求日志为例

  • DATE: 2021-02-08

  • TIME: 21:35:30

  • PID : 91640

  • CODE_INFO: /meetbill/butterfly/xlib/httpgateway.py:259

  • IP: 127.0.0.1

  • REQID: 1FA15BDE4B6CD7A0 (请求 ID)

  • METHOD: GET

  • PATH: /demo_api/hello

  • COST: cost:0.000162 (整体耗时,单位是秒)

  • STAT: stat:OK (执行结果状态, 若自定义 HTTP 方式时,则此处会是 HTTP 状态码)

  • USER: user:- (用户名)

  • TALK: talk:ceshi1=0.098;ceshi2=0.002 (代码片段耗时,比如一个 SQL 语句,请求 Redis 的耗时等)

  • PARAMS: params:str_info=hello (请求参数)

  • ERROR_MSG: error_msg:(错误信息)

  • RES: res:ceshi2=5.4;ceshi1=5.3 (需要记录的结果数据信息)

1.2.3 时间说明

厂内 GDP 的时间

+--------+------+---------+-------+-------------------+------+--------+-------+
|        |      |         |       |                   |      |        |       |
| ...... | pack | connect | write | server processing | read | unpack | ..... |
|        |      |         |       |                   |      |        |       |
+--------+----------------+-------+-------------------+---------------+-------+
|               |                                            |                |
|               | <----------------- talk -----------------> |                |
|                                                                             |
|                                                                             |
| <--------------------------------- cost ----------------------------------> |
|                                                                             |
  • talk[7.561] 本次交互耗时, 单位ms

  • connect[3.673] 链接耗时, 单位ms

  • write[0.241] 写耗时, 单位ms

  • read[0.100] 读耗时, 单位ms

  • pack[0] 打包的耗时, 单位ms

  • unpack[0.179] 解包的耗时,单位ms

1.3 自定义日志 logging

logging 中的几个概念:

Logger   :日志记录器,是应用程序中可以直接使用的接口。
Handler  :日志处理器,用以表明将日志保存到什么地方以及保存多久。
Formatter:格式化,用以配置日志的输出格式。

上述三者的关系是:一个 Logger 使用多个 Handler,一个 Handler 使用一个 Formatter。

    +----------------------------------------+
    |                 Logger                 |
    +----------------------------------------+
         |              |              |
         V              V              V
    +----------+   +----------+   +----------+
    | Handler1 |   | Handler2 |   | Handler3 |
    +----------+   +----------+   +----------+
         |              |              |
         V              V              V
    +----------+   +----------+   +----------+
    |Formatter1|   |Formatter2|   |Formatter3|
    +----------+   +----------+   +----------+

配置

def init_log(log_path, level=logging.INFO, when="D", backup=7,
             format="%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d * %(thread)d %(message)s",
             datefmt="%m-%d %H:%M:%S"):
    formatter = logging.Formatter(format, datefmt)
    logger = logging.getLogger()

    handler = logging.handlers.TimedRotatingFileHandler(log_path, when=when, backupCount=backup)
    handler.setLevel(level)
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    handler = logging.handlers.TimedRotatingFileHandler(log_path + ".wf", when=when, backupCount=backup)
    handler.setLevel(logging.WARNING)
    handler.setFormatter(formatter)
    logger.addHandler(handler)

1.3.1 logging 模块常用 format 格式说明

%(name)s            : Logger 的名字             # 也就是其中的 .getLogger 里的路径,或者我们用他的文件名看我们填什么
%(levelno)s         : 数字形式的日志级别        # 日志里面的打印的对象的级别
%(levelname)s       : 文本形式的日志级别        # 级别的名称
%(pathname)s        : 调用日志输出函数的模块的完整路径名,可能没有
%(filename)s        : 调用日志输出函数的模块的文件名
%(module)s          : 调用日志输出函数的模块名
%(funcName)s        : 调用日志输出函数的函数名
%(lineno)d          : 调用日志输出函数的语句所在的代码行
%(created)f         : 当前时间,用 UNIX 标准的表示时间的浮 点数表示
%(relativeCreated)d : 输出日志信息时的,自 Logger 创建以 来的毫秒数
%(asctime)s         : 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒
%(thread)d          : 线程 ID。可能没有
%(threadName)s      : 线程名。可能没有
%(process)d         : 进程 ID。可能没有
%(message)s         : 用户输出的消息

common 日志 format

format="%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d * %(thread)d %(message)s",

1.3.2 logging 记录日志时默认添加 reqid 信息

除了传递给日志记录函数的参数外,有时候我们还想在日志输出中包含一些额外的上下文信息。

比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如:请求 id

这里我们来介绍以下几种实现方式,详细查看传送门:

  • 通过向日志记录函数传递一个 extra 参数引入上下文信息

  • 使用 LoggerAdapters 引入上下文信息

  • 使用 Filters 引入上下文信息

实现

本框架使用 Filters 实现对要打印的日志默认添加 reqid 信息

主线程中设置:logging 的 filters 模块添加 reqid 信息,reqid 从 butterfly_local 中获取

butterfly_local = threading.local()


class RequestLogFilter(logging.Filter):
    """
    日志过滤器,将当前请求线程的 reqid 信息保存到日志的 record 上下文

    Filter 提供的是比 log level 更精确的过滤控制,只有当 Filter.filter 函数返回 True 的时候,这条日志才会被输出。

    https://docs.python.org/zh-cn/3/howto/logging-cookbook.html#using-loggeradapters-to-impart-contextual-information
    """

    def filter(self, record):
        record.reqid = getattr(butterfly_local, 'reqid', "XXXXXXXXXXXXXXXX")
        return True

线程中更新 butterfly_local 的属性

import xlib.logger
...
xlib.logger.butterfly_local.reqid = reqid
...

todo

以上方式在使用 logging 记录日志时不需要额外传参,但每次写日志都会查询全局作用域,所以会消耗一些时间, 后续可以再优化下

变量作用域

Python 中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。

变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。Python 的作用域一共有 4 种,分别是:

L (Local) 局部作用域
E (Enclosing) 闭包函数外的函数中
G (Global) 全局作用域
B (Built-in) 内建作用域

以 L –> E –> G –>B 的规则查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找

作用域例子

a = int(3.14)       # int 函数在内建作用域中

NAME='John'         # NAME 在全局作用域中

def fun():
    name='July'     # 闭包函数外的函数域
    def fun2():
        name='Jack' # 局部作用域
    print(name)

fun()

2 Butterfly 日志相关实现

2.1 获取当前运行函数文件名和代码行号

对象的属性

dir(对象) 获取实例的属性名,以列表形式返回

函数 (function) 的属性一般为 func_*
方法 (method) 的属性一般为 im_* (instance_method)
生成器 (generator) 的属性一般为 gi_*
代码块 (code) 的属性一般为 co_*
栈帧 (frame) 的属性一般为 f_*, 函数调用时才会产生
追踪 (traceback) 的属性为 tb_*, 异常时才会构建生成

栈帧 (frame)

栈帧表示程序运行时函数调用栈中的某一帧。函数没有属性可以获取它,因为它在函数调用时才会产生,而生成器则是由函数调用返回的,所以有属性指向栈帧。想要获得某个函数相关的栈帧,则必须在调用这个函数且这个函数尚未返回时获取。你可以使用 sys 模块的_getframe() 函数、或 inspect 模块的 currentframe() 函数获取当前栈帧。这里列出来的属性全部是只读的。

f_back: 调用栈的前一帧。
f_code: 栈帧对应的 code 对象。
f_locals: 用在当前栈帧时与内建函数 locals() 相同,但你可以先获取其他帧然后使用这个属性获取那个帧的 locals()。
f_globals: 用在当前栈帧时与内建函数 globals() 相同,但你可以先获取其他帧……。
f_lineno: 代码在哪一行

代码块 (code)

代码块可以由类源代码、函数源代码或是一个简单的语句代码编译得到。这里我们只考虑它指代一个函数时的情况;

co_argcount: 普通参数的总数,不包括*参数和**参数。
co_names: 所有的参数名(包括*参数和**参数)和局部变量名的元组。
co_varnames: 所有的局部变量名的元组。
co_filename: 源代码所在的文件名。

代码

def get_cur_info()
    func = inspect.currentframe().f_back
    filename = os.path.basename(func.f_code.co_filename)
    lineno = func.f_lineno
    cur_info = "{filename}:{lineno}".format(filename=filename, lineno=lineno)

2.2 使用 inspect.stack() 获取调用栈

2.2.1 demo

#!/usr/bin/python
# coding=utf8
import inspect

def get_stack():
    """
    return call stack info

    # stack[0] 是调用 stack() 处的 frame_record, 丢弃(即本函数中的 stack = inspect.stack())
    # stack[1] 是调用 get_stack() 处的 frame_record, 丢弃
    """
    stack = inspect.stack()
    msg="[call stack]:"
    for i in range(2, len(stack) ):
        filename = stack[i][1]
        lineno = stack[i][2]
        code = stack[i][4][0]
        # code[0:-1] 是为了去掉后面的 "\n"
        msg=msg +"\n" + '\t\t<frame %d>:%s, line:%d  func:%s' % (i - 1, filename, lineno, code[0:-1] )

    return msg

def v2():
    w = get_stack()
    print w

def v1():
    v2()

def main():
    v1()

if __name__ == "__main__":
    main()

输出

[call stack]:
		<frame 1>:test_inspect.py, line:29  func:    v2()
		<frame 2>:test_inspect.py, line:32  func:    v1()
		<frame 3>:test_inspect.py, line:35  func:    main()

inspect.stack() 信息

[
    (<frame object at 0x7fbe78f1ab20>, 'test_inspect.py', 12, 'get_stack', ['    stack = inspect.stack()\n'], 0),
    (<frame object at 0x10a6bc200>, 'test_inspect.py', 25, 'v2', ['    w = get_stack()\n'], 0),
    (<frame object at 0x10a617548>, 'test_inspect.py', 29, 'v1', ['    v2()\n'], 0),
    (<frame object at 0x10a6173a0>, 'test_inspect.py', 32, 'main', ['    v1()\n'], 0),
    (<frame object at 0x10a4d4c20>, 'test_inspect.py', 35, '<module>', ['    main()\n'], 0)
]

2.2.2 封装为库

import inspect

def get_stack():
    """
    return call stack info

    # stack[0] 是调用 stack() 处的 frame_record, 丢弃(即本函数中的 stack = inspect.stack())
    # stack[1] 是调用 get_stack() 处的 frame_record, 丢弃
    """
    stack = inspect.stack()
    msg="[call stack]:"
    for i in range(2, len(stack) ):
        filename = stack[i][1]
        lineno = stack[i][2]
        code = stack[i][4][0]
        # code[0:-1] 是为了去掉后面的 "\n"
        msg=msg +"\n" + '\t\t<frame %d>:%s, line:%d  func:%s' % (i - 1, filename, lineno, code[0:-1] )

    return msg

即在需要打印日志的时候,可以通过调用 get_stack() 获取调用栈信息

3 报表

报表统计需要分析日志的字段并进行统计

3.1 使用 re 进行日志中的字段预处理

3.1.1 re 匹配日志

re 分组

r"/index/(?P<num>\d*)/(?P<nid>\d*)"

(?P<num>\d*) 是将后面匹配的 \d* 数据,取一个组名,这个组名必须是唯一的,不重复的,没有特殊符号,函数可以获得这个组名下的数据

re 基础知识

3.1.2 twemproxy 解析日志 demo

# [2020-02-23 12:30:35.273] nc_response.c:646 SLOW_LOG_1 : cost_time(110614us) recv_client_time(1582432235.162875s) send_server_time(1582432235.163468s) recv_server_time(1582432235.164527s) send_client_time(1582432235.273489s)
# 此处的时间格式和常态表示方法不同
# 此处的 1582432235.162875s 小数点前是秒数,小数点后是微妙数,当 1582432235.875s 时表示 875 微秒,而不是毫秒
parts = [
        r'.*SLOW_LOG_1\s+:',                                                                        # [2020-02-23 12:30:35.273] nc_response.c:646 SLOW_LOG_1 :
        r'cost_time\((?P<cost_time>[0-9]+)us\)',                                                    # cost_time(110614us)
        r'recv_client_time\((?P<recv_client_time_s>[0-9]+)\.(?P<recv_client_time_us>[0-9]+)s\)',    # recv_client_time(1582432235.162875s)
        r'send_server_time\((?P<send_server_time_s>[0-9]+)\.(?P<send_server_time_us>[0-9]+)s\)',    # send_server_time(1582432235.163468s)
        r'recv_server_time\((?P<recv_server_time_s>[0-9]+)\.(?P<recv_server_time_us>[0-9]+)s\)',    # recv_server_time(1582432235.164527s)
        r'send_client_time\((?P<send_client_time_s>[0-9]+)\.(?P<send_client_time_us>[0-9]+)s\)',    # send_client_time(1582432235.273489s)
        r'(?P<EXTRA>.*)',
    ]
pattern = re.compile(r'\s+'.join(parts)+r'\s*')

m = pattern.match(line)
if m:
    print m.groupdict()

3.1.2 GDP 解析日志 demo

GDP 日志是以 key1[value1] key2[value2] 方式输出,可以通过如下方式获取到每个 field 的值

parts_auto = [
        r'.*apply_bcc_resource.go:[0-9]+',
        r'worker_id\[(?P<worker_id>.*)\]',
        r'logid\[(?P<logid>.*)\]',
        r'task_id\[(?P<task_id>.*)\]',
        r'task_batch_id\[(?P<task_batch_id>.*)\]',
        r'unit_id\[(?P<unit_id>.*)\]',
        r'entity\[(?P<entity>.*)\]',
        r'processor\[(?P<processor>.*)\]',
        r'user_orderid\[(?P<user_orderid>.*)\]',
        r'deployset\[(?P<deployset>.*)\]',
        r'count\[(?P<count>.*)\]',
        r'reserve\[(?P<reserve>.*)\]',
        r'user_id\[(?P<user_id>.*)\]',
        r'bcc_orderid\[(?P<bcc_orderid>.*)\]',
        r'exe_step\[(?P<exe_step>.*)\]',
        r'error\[(?P<error>.*)\]',
        r'message\[(?P<message>.*)\]',
        r'.*',
    ]
pattern_auto = re.compile(r'\s+'.join(parts_auto)+r'\s*')

3.2 将日志转换为 dict

import re
def line2dict(line):
    #Snippet, thanks to http://www.seehuhn.de/blog/52
    parts = [
        r'(?P<DATE>\S+)',               # date      eg.:2019-08-12
        r'(?P<TIME>\S+)',               # time      eg.:09:22:47
        r'(?P<PID>\S+)',                # pid       eg.:41442
        r'(?P<CODE_INFO>\S+)',          # code_info eg.:httpgateway.py:185
        r'(?P<HOST>\S+)',               # host      eg.:127.0.0.1
        r'(?P<REQID>\S+)',              # reqid     eg.:CACE332C8F5E39F8
        r'(?P<METHOD>\S+)',             # method    eg.:GET
        r'(?P<PATH>\S+)',               # path      eg.:/x/ping
        r'cost:(?P<COST>\S+)',          # cost time eg.:0.002147
        r'stat:(?P<STAT>\S+)',          # status    eg.:200(careful, can be 'OK'/'ERR')
        r'user:(?P<USER>\S+)',          # username  eg.:meetbill (or -)
        r'talk:(?P<TALK>\S*)',          # talk      eg.:ceshi1=404.443;ceshi2=101.594
        r'params:(?P<PARAMS>\S*)',      # params    eg.:str_info=hello
        r'error_msg:(?P<ERROR_MSG>.*)',  # error_msg eg.:API Processing Exception
        r'res:(?P<RES>.*)',             # result    eg.:ceshi2=5.4;ceshi1=5.3
    ]
    pattern = re.compile(r'\s+'.join(parts)+r'\s*\Z')
    m = pattern.match(line)
    res = m.groupdict()
    return res

with open("./acc.log") as f:
    for line in f.readlines():
        print line2dict(line)

eg.

源日志:
2021-02-09 10:18:02	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	75DA11E45E4644EF	GET	/demo_api/ping	cost:0.000455	stat:OK	user:-	talk:	params:x=1	error_msg:	res:
2021-02-09 10:19:51	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	549D3CA17B4182EE	GET	/demo_api/hello	cost:0.001822	stat:OK	user:-	talk:	params:str_info=meetbill	error_msg:	res:
2021-02-09 10:21:08	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	4E44BDEBD770F599	GET	/demo_api/hello	cost:0.000263	stat:ERR_BAD_PARAMS	user:-	talk:	params:str_infox=meetbill	error_msg:Param check failed	res:
2021-02-09 10:22:10	97082	/meetbill/butterfly/xlib/httpgateway.py:259	127.0.0.1	8A348A0C7D9699D2	GET	/demo_api/hellox	cost:0.002694	stat:400	user:-	talk:	params:	error_msg:API Not Found	res:

转换后:
{'STAT': 'OK', 'RES': '', 'TIME': '10:18:02', 'PID': '97082', 'HOST': '127.0.0.1', 'METHOD': 'GET', 'REQID': '75DA11E45E4644EF', 'COST': '0.000455', 'PARAMS': 'x=1', 'USER': '-', 'CODE_INFO': '/meetbill/butterfly/xlib/httpgateway.py:259', 'DATE': '2021-02-09', 'PATH': '/demo_api/ping', 'ERROR_MSG': '', 'TALK': ''}
{'STAT': 'OK', 'RES': '', 'TIME': '10:19:51', 'PID': '97082', 'HOST': '127.0.0.1', 'METHOD': 'GET', 'REQID': '549D3CA17B4182EE', 'COST': '0.001822', 'PARAMS': 'str_info=meetbill', 'USER': '-', 'CODE_INFO': '/meetbill/butterfly/xlib/httpgateway.py:259', 'DATE': '2021-02-09', 'PATH': '/demo_api/hello', 'ERROR_MSG': '', 'TALK': ''}
{'STAT': 'ERR_BAD_PARAMS', 'RES': '', 'TIME': '10:21:08', 'PID': '97082', 'HOST': '127.0.0.1', 'METHOD': 'GET', 'REQID': '4E44BDEBD770F599', 'COST': '0.000263', 'PARAMS': 'str_infox=meetbill', 'USER': '-', 'CODE_INFO': '/meetbill/butterfly/xlib/httpgateway.py:259', 'DATE': '2021-02-09', 'PATH': '/demo_api/hello', 'ERROR_MSG': 'Param check failed', 'TALK': ''}
{'STAT': '400', 'RES': '', 'TIME': '10:22:10', 'PID': '97082', 'HOST': '127.0.0.1', 'METHOD': 'GET', 'REQID': '8A348A0C7D9699D2', 'COST': '0.002694', 'PARAMS': '', 'USER': '-', 'CODE_INFO': '/meetbill/butterfly/xlib/httpgateway.py:259', 'DATE': '2021-02-09', 'PATH': '/demo_api/hellox', 'ERROR_MSG': 'API Not Found', 'TALK': ''}

3.3 输出例子

reportlog

访问方式

http://{IP}:{PORT}/demo_template/log

4 tips

4.1 如何匹配 acc.log 对应的列数据

acc.log 的各列间使用 TAB 间隔

假设要获取 400 状态码的日志,需要加 —P 来匹配 TAB

cat acc.log  | grep -P "\tstat:400\t"

4.2 如果查看大日志文件

比如一行日志就有 200MB

方法1: 只查看某些列数据
awk '{for(i=1;i<15;i++) printf("%s ",$i); printf("\n")}' common_bf.log

方法2: 将日志进行切割(按字节大小分割)
split -b 10m large_file.log new_file_prefix

5 在线调整日志级别

  • 在线调整日志级别

  • 获取缓存命中率等信息

5.1 logging 日志级别调整

DEBUG(10) < INFO(20) < WARNING(30) < ERROR(40) < CRITICAL(50)

目前调整时只能调整为启动时的级别及对应级别以上的级别
当前 Butterfly 启动时的级别是 INFO 级别
@funcattr.api
def loglevel_get(req):
    """
    获取 logging logger 日志级别

    DEBUG(10) < INFO(20) < WARNING(30) < ERROR(40) < CRITICAL(50)

    Args:
        req     : Request
    Returns:
        json_status, Content, headers
    """
    isinstance(req, Request)
    data = {}
    root_level = root_logger.level
    butterfly_level = butterfly_logger.level
    data["root"] = root_level
    data["butterfly"] = butterfly_level
    return retstat.OK, {"data": data}, [(__info, __version)]


@funcattr.api
def loglevel_set(req, name, level):
    """
    调整 logging logger 日志级别

    DEBUG(10) < INFO(20) < WARNING(30) < ERROR(40) < CRITICAL(50)

    Args:
        req     : Request
        name    : logger name
        level   : 日志级别,应该为数字,并在 [10, 20, 30, 40, 50] 中
    Returns:
        json_status, Content, headers
        "ERR_LEVEL_NOT_NUM": level 不是数字
        "ERR_LEVEL_INVALID": level 不在 [10, 20, 30, 40, 50] 中
        "ERR_NAME_INVALID" : name 不为 root 或 butterfly
    """
    isinstance(req, Request)
    try:
        level_num = int(level)
    except BaseException:
        return ERR_LEVEL_NOT_NUM, {}, [(__info, __version)]

    if level_num not in [10, 20, 30, 40, 50]:
        return ERR_LEVEL_INVALID, {}, [(__info, __version)]

    if name == "root":
        root_logger.setLevel(level_num)
        return retstat.OK, {}, [(__info, __version)]
    elif name == "butterfly":
        butterfly_logger.setLevel(level_num)
        return retstat.OK, {}, [(__info, __version)]
    else:
        return ERR_NAME_INVALID, {}, [(__info, __version)]

参考传送门

Last updated