Sanic学习——框架封装
俗话说,学东西就要多用,Sanic也在不停的把玩,不过在玩的过程中,有些不是很方便。
发现问题
1、服务启动问题
既然是一个框架,需要保持服务的运行,所以在服务开发好后,需要通过nohup等方式后台执行
nohup python3 server.py >> service.log 2>&1 &
每次通过这种方式启动是不是优点麻烦想通过脚本或者其他方式封装一下,简化我们启动服务的难度。
2、log日志输出问题
sanic有相应的日志记录,可以查看 sanic/log.py,从源代码可以看到,sanic有三种类型的日志
logger = logging.getLogger("sanic.root")
error_logger = logging.getLogger("sanic.error")
access_logger = logging.getLogger("sanic.access")
不过这三种日志默认都是console,打印到命令行的,当然我们可以通过nohup来按天分割日志,不过使用起来也不是很方便,不过我们可以通过在初始化的时候传递log_conf来修改日志的配置。
app = Sanic(log_config=log_config)
遇到这两个问题,我就在想是否可以封装一下,是sanic框架更好用点。当然这只是我的想法,如果你觉得这样挺好,也是ok的。
解决问题
1、通过bash脚本进行服务的启动
2、封装Sanic初始化工作
3、初始化时,解决log配置问题
那么开始解决问题吧
1、首先解决log配置问题
新建utils/logUtil.py文件
import logging
import sys
logger = logging.getLogger("sanic.access")
# 获取配置
def _make_log_file(filename, count=30):
return dict(
version=1,
disable_existing_loggers=False,
loggers={
"sanic.access": {
"level": "DEBUG", # log日志等级
"handlers": ["access_file_handler"], # log使用的handler
"propagate": False, #不将日志记录传递给祖先
"qualname": "sanic.access",
},
"sanic.root": {"level": "INFO", "handlers": ["root_console_handler"]},
},
handlers={
"access_file_handler": {
"class": "logging.handlers.TimedRotatingFileHandler", #hander名称
"formatter": "access", # 格式化formater名称
"filename": filename, # 输出到的文件名
"when": "midnight", # 分割日志时间
"backupCount": count, #日志保留次数,目前这样配置可以保留30天
"encoding": "utf-8" #日志编码
},
"root_console_handler": {
"class": "logging.StreamHandler",
"formatter": "generic",
"stream": sys.stdout,
},
},
formatters={
"generic": {
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
"class": "logging.Formatter",
},
"access": {
"format": "%(asctime)s [%(levelname)s][%(process)d]: (%(filename)s:%(lineno)d) %(message)s", #日志输出格式
"class": "logging.Formatter",
},
},
)
# 根据参数获取存储的文件名
def get_log_filename(logfile, index):
if logfile.find("{}") >= 0:
return True, logfile.format(index)
if logfile.find("%") >= 0:
return True, logfile % index
return False, logfile
# 设置logging配置以及等级
def set_logger(param=None, filename=None, level=logging.DEBUG, count=30):
if filename:
param = _make_log_file(filename, count)
if param and isinstance(param, (dict, )):
logging.config.dictConfig(param)
level and logger.setLevel(level)
看了这一段代码,可能有些不知所云。
Sanic在源代码中使用了access_log,需要通过设置access_log=True打开,目前可以使用Sanic的access_log进行配置,方便后续进行debug调试。
logging的配置主要包含三个部分,1、logger(定义日志)2、handler(定义日志输出形式)3、formatter(定义日志输出格式)官网文档
目前使用access log,通过文件形式,在24点进行日志切分,保留日志30天。
2、其次开始封装Sanic初始化
新建run_server.py
from argparse import ArgumentParser
from importlib import import_module
from sanic import Sanic
from utils.logUtil import get_log_filename, set_logger, logger
if __name__ == '__main__':
parser = ArgumentParser(prog="sanic")
parser.add_argument("--id", dest="id", type=int, default=0)
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
parser.add_argument("--port", dest="port", type=int, default=8000)
parser.add_argument("--workers", dest="workers", type=int, default=1)
parser.add_argument("--debug", dest="debug", action="store_true")
parser.add_argument("--module", dest="module", help="importable path of app")
parser.add_argument("--logfile", dest="logfile", type=str, default="service.%d.log")
parser.add_argument("--loglevel", dest="loglevel", type=str, default="INFO")
parser.add_argument("--logcount", dest="logcount", type=int, default=30)
parser.add_argument("--accesslog", dest="accesslog", action="store_true")
parser.add_argument("--env", dest="env", type=str, default="prod|qa|uat etc")
parser.add_argument("--cert", dest="cert", type=str, help="location of certificate for SSL")
parser.add_argument("--key", dest="key", type=str, help="location of keyfile for SSL.")
args = parser.parse_args()
try:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name)
app = getattr(module, app_name, None)
if not isinstance(app, Sanic):
raise ValueError(
f"{module_name}.{app_name} is {type(app).__name__} not a Sanic app,"
)
dummy, fn = get_log_filename(args.logfile, args.id)
set_logger(filename=fn, level=args.loglevel, count=args.logcount)
app.cmd_args = args
ssl = None
if args.cert and args.key:
ssl = {"cert": args.cert, "key": args.key}
logger.info(f"Module {module_name}.{app_name} is ready...")
app.run(
host=args.host,
port=args.port,
workers=args.workers,
debug=args.debug,
access_log=args.accesslog,
ssl=None
)
logger.info(f"Module {module_name}.{app_name} is terminated.")
except Exception as ex:
logger.exception(ex)
logger.info(f"Failed to run app: {ex}")
看这一段代码是不是又晕了,主要包含4个步骤
1、接受输入参数
2、通过module参数获取到Sanic对象
3、修改日志配置
4、启动sanic服务
那么为什么要有1,2两个步骤呢?主要是因为,想要尽可能的将输入参数通过脚本传递过来,部分参数是参考sanic.__main__.py,如host,port,workers,debug,module,cert, key等。
id:在启动服务中,可能我们会启动多个进程来运行服务,id用来记录当前是哪一个进程,方便日志输出到对应的文件中。
logfile,loglevel,logcount,accesslog:这几个参数都是日志的相关配置,这里我就不过多赘述了。
env:对应的是当前我们使用的是什么环境,以便我们可以从sanic实例中拿到env环境变量进行对应的设置。
3、通过脚本启动服务
#!/bin/bash
# 定义参数
SANICMODULE=main.app
HOST=127.0.0.1
BASEPORT=12000
WORKERS=1
DEBUG=0
ENV=prod
LOGLEVEL=INFO
LOGCOUNT=30
ACCESSLOG=1
export path=/usr/local/bin:/usr/local/sbin:$PATH
PROJPATH=$(cd `dirname ${0}` && pwd)
cd "${PROJPATH}"
proc_id=0
#定义获取进程号方法
SANICARG="--env=$ENV --module $SANICMODULE --id=$proc_id"
pid=
pidnum=
function get_pid() {
pid=`ps -ef | grep -E " $SANICARG " | grep -v "\<grep\>" | awk '{print $2}'`
pidnum=`echo $pid | wc | awk '{print $2}'`
}
#定义停止服务方法
function stop_service() {
get_pid
if [ $pidnum -ne 0 ] ; then
kill -15 $pid >/dev/null 2>&1
if [ $? -ne 0 ] ; then
echo "Cannot kill, stop ${SANICMODULE} failed."
exit 1
fi
echo -n "${SANICMODULE} is stopping pid=$pid."
while sleep 1
get_pid
[ $pidnum -ne 0 ]
do
echo -n "."
done
echo "stop done "
else
echo "${SANICMODULE} is not running, nothing to do!"
fi
}
#定义开启服务方法
function start_service() {
get_pid
if [ $pidnum -ne 0 ] ; then
echo "${SANICMODULE} is always runing, nothing done!"
else
ulimit -c 409600 >/dev/null 2>&1
ulimit -n 65535 >/dev/null 2>&1
mkdir -p "${PROJPATH}/logs"
chmod +rw "${PROJPATH}/logs" -R >/dev/null 2>&1
let "port=${BASEPORT}+${proc_id}"
nohup python3 $PROJPATH/run_server.py $SANICARG $DEBUG $ACCESSLOG $LOGCOUNT \
--host=$HOST \
--port=$port \
--loglevel=$LOGLEVEL \
--logfile="logs/app.$ENV.$SANICMODULE.%d.log" \
--workers=$WORKERS >> $PROJPATH/logs/console.$ENV.$SANICMODULE.$proc_id.log 2>&1 &
echo -n "$SANICMODULE is starting"
for ((i=0; i<3; i++)); do
sleep 1
get_pid
if [ $pidnum -eq 0 ]; then
echo -n "failed."
exit 1
fi
echo -n "."
done
echo "start ok (pid=$pid)."
exit 0
fi
}
# 通过判断输入命令来进行相应的操作,目前支持三个命令
# start,stop和restart,可以通过传入不同的id来启动不同的进程
if [ $# -lt 1 ] ; then
echo "usage: $0 [start|stop|restart] [id]"
exit 1;
else
if [ $# -ge 2 ]; then
proc_id=$2
fi
if [ "$1" == 'stop' ] ; then
stop_service
elif [ "$1" == 'start' ]; then
start_service
elif [ "$1" == 'restart' ]; then
stop_service
start_service
fi
fi
启动脚本也大致三个部分
1、定义参数
2、定义方法,如获取进程号,启动服务和停止服务
3、通过启动脚本命令运行不同的方法,达到启动和停止服务的效果
到此为止,封装也差不多了,赶紧来测试一下吧。
# main.py
from sanic import Sanic
from sanic.response import json
from utils.logUtil import logger
app = Sanic()
@app.route("/")
async def test(request):
logger.info("1111")
return json({"hello": "world"})
通过调用
./run_server.sh start
有没有发现服务已经启动了呢?