1 服务管理
1.1 配置管理
1.1.1 配置全览
conf/config.py(APP 配置)
Copy # coding:utf8
"""
Butterfly config
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SERVER_LISTEN_ADDR = ("0.0.0.0", 8585)
SERVER_THREAD_NUM = 16
SERVER_NAME = "Butterfly_app"
# Reqid prefix, two characters
SERVER_ID = "BF"
SERVER_IDC = "default"
# static
STATIC_PATH = "static"
STATIC_PREFIX = "static"
# DB
"""
# wuxing/ruqi use default database
"""
DATABASES = {
"default": "mysql+retrypool://root:password@127.0.0.1:3306/test?max_connections=300&stale_timeout=300",
}
# Redis
"""
# eg1:"redis://[[username]:[password]]@localhost:6379/0"
# eg2:"redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=1&retry_on_timeout=true"
"""
CACHES = {
"default": "redis://@localhost:6379/0",
# for cache: connect_timeout 100ms, read_timeout 200ms
# "wuxing": "redis://@localhost:6379/0?socket_timeout=0.2&socket_connect_timeout=0.1&retry_on_timeout=false",
# for mq: connect_timeout 500ms, read_timeout 2000ms
# "baichuan": "redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=0.5&retry_on_timeout=false",
}
# Local data or cache
LOCALDATA_DIR = os.path.join(BASE_DIR, "data")
# Auth
SECRET_KEY = None # If it is None, a key will be randomly generated each time butterfly is started
JWT_TOKEN_TTL = 28800 # default 8 hours
HEADER_USERNAME = "HTTP_X_USERNAME"
# Scheduler
scheduler_store = "memory" # ("mysql"/"memory")
# Adaptor
"""
# {"stat":"ERR", "data":{}} --> {"stat":"ERR", "success":False, "message":"ERR", "data":{}}
STAT_ADAPTOR={"status_name":"success", "status_map":{"OK":True,"default":False}, "message_name":"message"}
"""
STAT_ADAPTOR = None
config/logger_conf.py(日志配置)
Copy # coding:utf8
"""
初始化配置日志
acclog : Butterfly 访问日志
errlog : Butterfly 错误日志
initlog: Butterfly 启动相关信息
"""
import os
import logging
from xlib import logger
from conf import config
# --------------------------LOG PATH-----------------------------
LOG_SIZE_LIMIT = 1024 * 1024 * 2
LOG_BATCH_WRITE = 0
PATH_INIT_LOG = os.path.join(config.BASE_DIR, "logs/init.log")
PATH_ACC_LOG = os.path.join(config.BASE_DIR, "logs/acc.log")
PATH_INFO_LOG = os.path.join(config.BASE_DIR, "logs/info.log")
PATH_WARNING_LOG = os.path.join(config.BASE_DIR, "logs/warning.log")
PATH_ERR_LOG = os.path.join(config.BASE_DIR, "logs/err.log")
PATH_CRIT_LOG = os.path.join(config.BASE_DIR, "logs/crit.log")
PATH_COMMON_LOG = os.path.join(config.BASE_DIR, "logs/common.log")
PATH_SCHEDULER_LOG = os.path.join(config.BASE_DIR, "logs/scheduler/scheduler.log")
PATH_SCHEDULER_WORKER_LOG = os.path.join(config.BASE_DIR, "logs/scheduler/scheduler-worker.log")
PATH_SERVICE_LOG = os.path.join(config.BASE_DIR, "logs/service.log")
PATH_RAL_LOG = os.path.join(config.BASE_DIR, "logs/ral/ral-worker.log")
# --------------------------LOG logger---------------------------
# 通用 logging 初始化( root logger ), 此日志没有 reqid
logger.init_log(PATH_COMMON_LOG)
# Butterfly scheduler 日志初始化, 此日志没有 reqid
logger.init_log(PATH_SCHEDULER_LOG, logger_name="apscheduler", level=logging.INFO)
logger.init_log(PATH_SCHEDULER_WORKER_LOG, logger_name="apscheduler_worker", level=logging.INFO)
# Butterfly service 日志初始化, 此日志含有 reqid
logger.init_bf_log(PATH_SERVICE_LOG, logger_name="service")
# Butterfly ral 日志初始化(Resource Access Layer), 此日志含有 reqid
logger.init_bf_log(PATH_RAL_LOG, logger_name="ral", level=logging.DEBUG)
critlog = logger.LoggerBase(PATH_CRIT_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
errlog = logger.LoggerBase(PATH_ERR_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
warninglog = logger.LoggerBase(PATH_WARNING_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
infolog = logger.LoggerBase(PATH_INFO_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
acclog = logger.LoggerBase(PATH_ACC_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
initlog = logger.LoggerBase(PATH_INIT_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
1.1.2 SERVER
Copy SERVER_LISTEN_ADDR = ("0.0.0.0", 8585) # 监听端口
SERVER_THREAD_NUM = 16 # 线程数
SERVER_NAME = "Butterfly_app" # 服务名标识,用于百川 worker
1.1.3 LOG
Copy LOG_SIZE_LIMIT = 1024 * 1024 * 2 # 日志大小,大于此值时,日志文件将会被清空
LOG_BATCH_WRITE = 0
PATH_INIT_LOG = os.path.join(BASE_DIR, "logs/init.log") # 启动日志,记录 handler 信息等
PATH_ACC_LOG = os.path.join(BASE_DIR, "logs/acc.log") # 访问日志
PATH_INFO_LOG = os.path.join(BASE_DIR, "logs/info.log")
PATH_WARNING_LOG = os.path.join(BASE_DIR, "logs/warning.log")
PATH_ERR_LOG = os.path.join(BASE_DIR, "logs/err.log") # 错误日志
PATH_CRIT_LOG = os.path.join(BASE_DIR, "logs/crit.log")
PATH_COMMON_LOG = os.path.join(BASE_DIR, "logs/common.log") # logging 日志
PATH_COMMON_BF_LOG = os.path.join(BASE_DIR, "logs/common_bf.log") # logging 日志
1.1.4 STATIC
Copy STATIC_PATH = "static" # 静态文件路径,默认为 {butterfly_project}/static/{file_path}
STATIC_PREFIX = "static" # 静态文件前缀,判断请求 URL 是否为请求静态文件
1.1.5 DB
Copy """
# wuxing/ruqi use default database
"""
DATABASES = {
"default": "mysql+retrypool://root:password@127.0.0.1:3306/test?max_connections=300&stale_timeout=300",
}
1.1.6 Redis
Copy """
# eg1:"redis://[[username]:[password]]@localhost:6379/0"
# eg2:"redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=1&retry_on_timeout=true"
"""
CACHES = {
"default": "redis://@localhost:6379/0",
# for cache: connect_timeout 100ms, read_timeout 200ms
# "wuxing": "redis://@localhost:6379/0?socket_timeout=0.2&socket_connect_timeout=0.1&retry_on_timeout=false",
# for mq: connect_timeout 500ms, read_timeout 2000ms
# "baichuan": "redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=0.5&retry_on_timeout=false",
}
1.1.7 LOCALDATA
Copy LOCALDATA_DIR = os.path.join(BASE_DIR, "data")
存储本地数据目录
1.1.8 AUTH
Copy SECRET_KEY = None # If it is None, a key will be randomly generated each time butterfly is started
JWT_TOKEN_TTL = 28800 # default 8 hours
HEADER_USERNAME = "HTTP_X_USERNAME"
SECRET_KEY : jwt SECRET_KEY
JWT_TOKEN_TTL : jwt token ttl
HEADER_USERNAME : 用户名 handler
请求方法:若此项配置的 "HTTP_X_USERNAME" 则请求时可传 -H "x_username:meetbill"
header
1.1.9 Scheduler
Copy scheduler_store = "memory" # ("mysql"/"memory")
主要用于【如期】使用
1.1.10 Adaptor
Copy """
# {"stat":"ERR", "data":{}} --> {"stat":"ERR", "success":False, "message":"ERR", "data":{}}
STAT_ADAPTOR={"status_name":"success", "status_map":{"OK":True,"default":False}, "message_name":"message"}
"""
STAT_ADAPTOR = None
用于扩展 API 接口的状态字段
STAT_ADAPTOR 需要有三个信息
status_map: 状态映射,默认值为 "default" value, 若可以匹配到映射中的值,则以映射值为值
1.2 启动管理
Copy Usage: run.sh {start|stop|restart|status}
start Start butterfly processes.
docker_start Start butterfly processes for docker.
stop Kill all butterfly processes.
restart Kill all butterfly processes and start again.
status Show butterfly processes status.
备注:bash run.sh docker_start
用于打 docker 镜像时使用
如何修改启动项标识
在日常使用中可能会在同一台机器上启动多个 butterfly,不同的 butterfly 提供着不同的服务,可能是测试服务,可能是做配置管理服务使用的,等等,当然这些 butterfly 的端口是不一样的,那如何根据进程看出是什么服务尼?
只需要在服务启动前修改下,run.sh 的 PROC_SIG="954216e9"
修改为想要使用的用途即可,比如 PROC_SIG="test_dev"
Copy 注:此项修改需要在服务启动前修改,因为 run.sh 对进程的管理,也会对 PROC_SIG 进行匹配
1.3 日志管理
1.3.1 启动日志
启动相关信息日志会记录在 __stdout
如果启动失败,可以在这个日志文件中找到原因
1.3.2 butterfly 框架日志
1.3.2.1 acc 访问日志
acc 日志路径在 logs/acc.log
日志内容
Copy 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
备注:
(1) access 日志中各 field 使用 '\t' 进行分割
(2) COST,STAT,USER,TALK,PARAMS,ERROR_MSG,RES field 会有 field 前缀,使用 `:` 进行分割
field 中多个 item 使用 `;` 进行分割,每个 item 使用 `=` 进行分割 item_name 和 item_value
即:{field_prefix}:{item_name1}={item_value1};{item_name2}={item_name2}
如:talk:ceshi1=0.098;ceshi2=0.002
CODE_INFO: /meetbill/butterfly/xlib/httpgateway.py:259
REQID: 1FA15BDE4B6CD7A0 (请求 ID)
COST: cost:0.000162 (整体耗时,单位是秒)
STAT: stat:OK (执行结果状态,若自定义 HTTP 方式时,则此处会是 HTTP 状态码)
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 (需要记录的结果数据信息)
在 handler 逻辑中可以通过如下方式写入日志到 logs/acc.log
记录额外参数
Copy req.log_params["x"] = 1
记录程序运行耗时
Copy # req.start_timming() 为可选项,若不加此语句,则默认以本次请求开始记录耗时
req.start_timming()
...
req.timming("ceshi1") # 会记录 req.start_timming() 到此行的耗时
...
req.timming("ceshi2") # 会记录 req.timming("ceshi1") 到 req.timming("ceshi2") 间的耗时
将会打印日志如下:
ceshi1=0.001;ceshi2=0.003
记录错误信息(推荐)
Copy req.error_str = err_msg
使用此方式,错误日志不但可以记录到 acc.log , 而且响应头中也会返回给客户端此信息,如下:
Copy reply: 'HTTP/1.1 200 OK\r\n'
header: ruqi: 1.0.1
header: Content-Length: 15
header: butterfly: 1.1.4
header: x-reqid: 3B2BBCF470912A79
header: x-cost: 7.732843
header: x-reason: u'Job identifier (test1) conflicts with an existing job'
header: Connection: close
header: Date: Sat, 02 Jan 2021 01:18:47 GMT
header: Server: XXXXXXXXXXXXXXXX
{u'stat': u'ERR'}
记录 response 信息
req.log_res:(set)
Copy 代码:
req.log_res.add("test1=info")
日志:
... res:test1=info
1.3.2.2 err 异常日志
err 日志路径在 logs/err.log
例子(不常用,主要用于 butterfly 框架捕获未处理的异常逻辑使用)
Copy req.log(self._errlog, "Json dump failed\n%s" % traceback.format_exc())
req.log 用于记录异常日志以及其他类型日志,日志中会记录 reqid, 需要传入 logger 和日志信息
1.3.2.3 init 日志
init 日志路径在 logs/init.log, 日志中会记录如下信息
日志中会打印自动加载的 handler 的日志信息
如果开启了 scheduler, 则打印 scheduler 是否启动成功
1.3.3 自定义日志输出
acc.log 和 err.log 均为框架本身产生的日志,用于接口统计以及服务异常排查问题等等
这里的自定义日志内容有两种方式
使用 logging 模块『推荐』,将日志写入 common.log 和 common.log.wf
这个来说明下如何使用 logging 将日志写入 common.log 和 common.log.wf
Copy from xlib import retstat
from xlib.middleware import funcattr
import logging
logger = logging.getLogger("service")
__info = "test_api"
__version = "1.0.1"
@funcattr.api
def hello(req, str_info):
isinstance(req, Request)
# 打印 info log
logger.info("test info log")
# 打印 error log
logger.error("test error log")
return retstat.OK, {"data": {"str_info": str_info}}, [(__info, __version)]
备注:
Copy 如果使用了 logging 日志记录方式
(1) 默认 info 级别以上的 logging 日志都会打印到 config.py 设置的 PATH_COMMON_LOG = "logs/common.log" 日志中
(2) 默认 error 级别以上的 logging 日志都会打印到 logs/common.log.wf 中
(3) 如果是用户发起的请求 (handler 应用程序),logging 日志中会默认加上 reqid, 如果没有获取到 reqid(比如 Main 线程) 则会打印 "XXXXXXXXXXXXXXXX"
2 接口开发
2.1 编写函数(根据返回值,区分为两种方式)
2.1.1 推荐方式
(1) 函数文件
Copy handlers/demo_api/__init__.py
例子
Copy @funcattr.api
def hello(req, str_info):
"""
带参数请求例子
Args:
req : Request
str_info: (str)
Returns:
json_status, Content, headers
"""
isinstance(req, Request)
return retstat.OK, {"str_info": str_info}, [(__info, __version)]
返回值分别为:状态信息字段 ,数据信息字段 , 自定义 HTTP 报文头信息列表
2.1.2 自定义 HTTP 码返回方式
(1) 函数文件
Copy handlers/demo_httpapi/__init__.py
(2) 编写对应方法
返回值 (httpstatus, [content], [headers])
content: (str/dict) 非必须(当返回值为 2 个的时候,第 2 个返回值为 Content)
当 content 为 dict 时,会自动转为 json ,并且设置 header("Content-Type","application/json")
当 content 为其他时,会自动设置为 ("Content-Type","text/html")
headers: 非必须(当返回值为 3 个的时候,第 3 个返回值为 headers)
如:
Copy def hello(req, str_info):
isinstance(req, Request)
return retstat.HTTP_OK, {"stat":"OK","str_info": str_info}, [(__info__, __version__)]
返回值分别为:HTTP 返回码 ,状态信息字段 + 数据信息字段 , 自定义 HTTP 报文头信息列表
2.2 路由注册
(1) 函数文件
handlers package 和 handlers 下的 package 都会默认注册到路由中
2.3 访问接口函数
get 请求
Request:
Copy curl -v "http://ip:port/demo_api/hello?str_info=happy"
如:
curl -v "http://127.0.0.1:8585/demo_api/hello?str_info=happy"
同时可以使用 -v 查看额外 headers 信息,如下:
Response:
Copy * Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8585 (#0)
> GET /demo_api/hello?str_info=happy HTTP/1.1
> Host: 127.0.0.1:8585
> User-Agent: curl/7.54.0
> Accept: **/**
>
< HTTP/1.1 200 OK
< meetbill: 1.0.1 --------------------------------- 函数中返回的 headers 信息
< Content-Length: 35 ------------------------------- Content 的长度
< butterfly: 1.1.3 --------------------------------- Butterfly 版本信息
< x-reqid: C31582ED1FBBFA2D ------------------------ 请求 id,此 id 会打印到日志文件中
< x-cost: 0.003114 --------------------------------- 耗时 (time.time() UNIX 时间戳
< 格林尼治时间 1970 年 01 月 01 日 00 时 00 分 00 秒 起至现在的总秒数之差)
< Date: Mon, 04 Mar 2019 09:33:26 GMT
< Server: XXXXXXXXXXXXXXXX
<
* Connection #0 to host 127.0.0.1 left intact
{"stat": "OK", "str_info": "happy"}%
post 请求
request:
Copy curl -v -d '{"str_info":"happy"}' http://127.0.0.1:8585/demo_api/hello
Response:
Copy * Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8585 (#0)
> POST /demo_api/hello HTTP/1.1
> Host: 127.0.0.1:8585
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Length: 20
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 20 out of 20 bytes
< HTTP/1.1 200 OK
< meetbill: 1.0.1
< Content-Length: 35
< butterfly: 1.1.3
< x-reqid: 1A805D21CC791F3E
< x-cost: 0.000142
< Date: Mon, 04 Mar 2019 11:48:04 GMT
< Server: XXXXXXXXXXXXXXXX
<
* Connection #0 to host 127.0.0.1 left intact
{"stat": "OK", "str_info": "happy"}
备注 1 :JSON 格式数据属性必须用双引号包裹
Copy var json = '{"name":"imooc"}'; // 这个是正确的 JSON 格式
var json = "{\"name\":\"imooc\"}"; // 这个也是正确的 JSON 格式
var json = '{name:"imooc"}'; // 这个是错误的 JSON 格式,因为属性名没有用双引号包裹
var json = "{'name':'imooc'}";// 这个也是错误的 JSON 格式,属性名用双引号包裹,而它用了单引号
备注 2 :可传多级 json
Copy # Request:
curl -d '{"str_info":{"field1": "value1", "field2": "value2"}}' http://127.0.0.1:8585/demo_api/hello
# Response:
{"stat": "OK", "str_info": {"field2": "value2", "field1": "value1"}}
python post 方式提交 json 字符串
Copy import urllib2
import json
data = {"str_info":"hello info"}
headers = {'Content-Type':'application/json'}
request = urllib2.Request(url='http://127.0.0.1:8585/demo_api/hello',headers=headers,data=json.dumps(data))
response = urllib2.urlopen(request)
print response.read()
2.4 handler 访问本目录文件
通过 cur_path 与文件进行拼接
Copy cur_path = os.path.split(os.path.realpath(__file__))[0]
3 手动测试 handler
测试方法
Copy $ python test_handler.py
Last updated 6 months ago