Squashed 'paste-framework/' content from commit 34e8684
git-subtree-dir: paste-framework git-subtree-split: 34e8684c4bc3cebbe177509f42ab4ef5b5425a7a
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
from typing import Optional, Callable, Set, Any
|
||||
|
||||
import psutil
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from paste.core.logging import echo_log
|
||||
|
||||
MAX_BACKGROUND_TASKS = 3000
|
||||
"""
|
||||
最大任务数量,根据服务器内存调整。
|
||||
"""
|
||||
|
||||
aio_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
"""
|
||||
异步循环对象。
|
||||
"""
|
||||
|
||||
aio_runner: Optional[Callable] = None
|
||||
"""
|
||||
异步方法运行器,对应::
|
||||
|
||||
asyncio.events.AbstractEventLoop.run_until_complete()
|
||||
|
||||
方法的返回值 。
|
||||
"""
|
||||
|
||||
global_background_tasks: Set[asyncio.Task] = set()
|
||||
"""
|
||||
全局后台任务池。
|
||||
"""
|
||||
|
||||
|
||||
async def run_background_task(coro, max_tasks: int = MAX_BACKGROUND_TASKS):
|
||||
"""
|
||||
运行后台任务。
|
||||
|
||||
:param coro: 要在后台执行的协程。
|
||||
:param max_tasks: 背压总量
|
||||
"""
|
||||
global MAX_BACKGROUND_TASKS
|
||||
if max_tasks != MAX_BACKGROUND_TASKS:
|
||||
MAX_BACKGROUND_TASKS = max_tasks
|
||||
|
||||
if len(global_background_tasks) >= MAX_BACKGROUND_TASKS:
|
||||
# 增加背压控制
|
||||
await asyncio.wait(global_background_tasks, return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
task = asyncio.create_task(coro)
|
||||
global_background_tasks.add(task)
|
||||
# 任务完成后自动移除
|
||||
task.add_done_callback(global_background_tasks.discard)
|
||||
|
||||
|
||||
def get_aio_loop():
|
||||
"""
|
||||
这里必须采用方法,在适当的时间点创建事件循环对象,否则会导致服务无法启动。
|
||||
主要是测试到 EventLoop 之间的冲突,或异步事件已经在运行,导致无法顺利执行。
|
||||
|
||||
:return: 事件循环对象
|
||||
"""
|
||||
global aio_loop
|
||||
if aio_loop:
|
||||
return aio_loop
|
||||
else:
|
||||
try:
|
||||
# 尝试获取当前运行中的事件循环
|
||||
aio_loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
# 如果没有运行中的循环,才创建新的
|
||||
aio_loop = asyncio.new_event_loop()
|
||||
return aio_loop
|
||||
|
||||
|
||||
def get_aio_runner():
|
||||
"""
|
||||
这里必须采用方法,在适当的时间点创建事件循环对象,否则会导致服务无法启动。
|
||||
主要是测试到 EventLoop 之间的冲突,或异步事件已经在运行,导致无法顺利执行。
|
||||
|
||||
:return: 运行器对象
|
||||
"""
|
||||
global aio_loop, aio_runner
|
||||
if aio_runner:
|
||||
return aio_runner
|
||||
else:
|
||||
aio_runner = get_aio_loop().run_until_complete
|
||||
return aio_runner
|
||||
|
||||
|
||||
def process_info(pid: int):
|
||||
"""
|
||||
若传入的 PID 存在,则返回进程信息,否则返回 None。
|
||||
|
||||
:param pid: 进程 ID
|
||||
:return: 进程信息
|
||||
"""
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
_delta = relativedelta(datetime.datetime.now(), datetime.datetime.fromtimestamp(process.create_time()))
|
||||
_d, _h, _m, _s = _delta.days, _delta.hours, _delta.minutes, _delta.seconds
|
||||
process.cpu_percent()
|
||||
return {
|
||||
'name': process.name(),
|
||||
'cpu_usage': f"{process.cpu_percent()}%",
|
||||
'memory_usage': f"{process.memory_info().rss / (1024 * 1024):.3f}MB",
|
||||
'running_time': f"{_d}天{_h}时{_m}分{_s}秒",
|
||||
}
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
return None
|
||||
|
||||
|
||||
async def gather_with_retry(*coro_constructors: Callable[[], Any],
|
||||
max_retries: int = 3,
|
||||
eba: int = 0.5,
|
||||
return_exceptions: bool = False) -> tuple[Any, ...]:
|
||||
"""
|
||||
封装 asyncio.gather,支持对一组并发任务进行 N 次重试。
|
||||
|
||||
与 asyncio.gather 对齐:
|
||||
- 使用 *args 传参,而非列表
|
||||
- 支持 return_exceptions 参数
|
||||
- 返回 list 类型,顺序一致
|
||||
|
||||
但要求:每个参数必须是一个**无参函数**,调用后返回一个 awaitable。
|
||||
这样才能在每次重试时重新创建任务。
|
||||
|
||||
Args:
|
||||
*coro_constructors: Callable 对象,async 方法要用 lambda 封装
|
||||
max_retries (int): 最大重试次数(总尝试次数 = max_retries + 1)
|
||||
eba (int): 指数退避的起始等待时间
|
||||
return_exceptions (bool): 若为 True,异常作为结果返回,不抛出
|
||||
|
||||
Returns:
|
||||
list: 所有任务结果列表。若 return_exceptions=True,异常也会作为列表元素。
|
||||
|
||||
Raises:
|
||||
Exception: 当 return_exceptions=False 且所有重试都失败时,抛出最后一次尝试中的第一个异常。
|
||||
"""
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
tasks = [ctor() for ctor in coro_constructors]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=return_exceptions)
|
||||
return tuple(results)
|
||||
except Exception as e:
|
||||
if attempt == max_retries:
|
||||
echo_log(f"共执行 {max_retries} 次后全部失败: {str(e)}")
|
||||
else:
|
||||
echo_log(f"执行第 {attempt + 1} 次重试失败.")
|
||||
# 指数退避
|
||||
await asyncio.sleep(eba * (attempt + 1))
|
||||
|
||||
raise RuntimeError("Unreachable")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
读取配置信息的方法集合。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from paste.util import ufile, udict
|
||||
|
||||
GLOBAL_CONFIG = None
|
||||
"""
|
||||
全局单例配置系统。
|
||||
"""
|
||||
|
||||
|
||||
def load_config() -> dict:
|
||||
"""
|
||||
生成配置 JSON 对象。
|
||||
|
||||
:return: JSON 对象
|
||||
"""
|
||||
global GLOBAL_CONFIG
|
||||
if GLOBAL_CONFIG is None:
|
||||
config_file = os.path.abspath(os.path.join(os.path.curdir, 'config.json'))
|
||||
GLOBAL_CONFIG = json.loads(ufile.read_to_buffer(config_file))
|
||||
return GLOBAL_CONFIG
|
||||
|
||||
|
||||
def get_config_by_path(path: str, default=None):
|
||||
"""
|
||||
读取配置参数。若 path 存在则返回值;若 path 不存在,且没有默认值,则抛出异常,否则返回默认值。
|
||||
|
||||
:param path: 字典中的 key 路径,以"."号分隔
|
||||
:param default: 默认值,为 None 时表示未设置,此时若键名不存在,会抛出异常
|
||||
"""
|
||||
config = load_config()
|
||||
_result = udict.get_by_path(config, path, default)
|
||||
if _result is None:
|
||||
if default is None:
|
||||
raise AssertionError('未读取到配置参数: %s' % path)
|
||||
else:
|
||||
return default
|
||||
return _result
|
||||
|
||||
|
||||
def get_config(key: str, default=None):
|
||||
"""
|
||||
读取配置参数。若 key 存在则返回值;若 key 不存在,且没有默认值,则抛出异常,否则返回默认值。
|
||||
|
||||
:param key: 键名,或配置字典中的 path
|
||||
:param default: 默认值,为 None 时表示未设置,此时若键名不存在,会抛出异常
|
||||
"""
|
||||
return get_config_by_path(path=key, default=default)
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
实现对日志文件的配置封装,详细参考 getLogger 方法。
|
||||
输出日志使用 echo_log 方法。
|
||||
输出到日志文件使用 logToFile 方法。
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from logging import handlers
|
||||
from typing import Any, Union, Optional
|
||||
|
||||
from paste.core import config
|
||||
|
||||
logger_config_name = 'logger.default'
|
||||
"""
|
||||
默认配置字段名称,当在 getLogger 方法中设置了不同名称后,该变量会被修改。
|
||||
"""
|
||||
|
||||
paste_logger: Optional[logging.Logger] = None
|
||||
"""
|
||||
全局日志对象,获取日志对象时初始化。
|
||||
"""
|
||||
|
||||
|
||||
def set_logger_config(config_name: str):
|
||||
"""
|
||||
设置新的日志配置名称。
|
||||
|
||||
:param config_name: 日志配置名称
|
||||
"""
|
||||
global logger_config_name
|
||||
if config_name != logger_config_name:
|
||||
logger_config_name = config_name
|
||||
|
||||
|
||||
def get_logger():
|
||||
"""
|
||||
取得日志对象。先根据配置,更新系统日志配置。若配置了额外的日志文件、格式、层级,则增加响应的日志输出。
|
||||
|
||||
注意:除非额外配置,否则都使用与系统日志相同的配置参数。
|
||||
|
||||
配置结构参考::
|
||||
|
||||
{
|
||||
"logger": {
|
||||
"basic": {
|
||||
"filename": "sys_log.log",
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
"level": 20
|
||||
},
|
||||
"filename": "zb_data_log.log",
|
||||
"name": "ZbData"
|
||||
}
|
||||
}
|
||||
"""
|
||||
global paste_logger
|
||||
|
||||
if paste_logger is None:
|
||||
# 系统日志基本配置
|
||||
_log_base_cfg: dict = config.get_config(f'{logger_config_name}.basic', {})
|
||||
# 系统日志文件
|
||||
_base_log_file = _log_base_cfg.get('filename', '')
|
||||
_base_log_format = _log_base_cfg.get('format', '')
|
||||
_base_formatter = logging.Formatter(_base_log_format)
|
||||
_base_level = _log_base_cfg.get('level', logging.INFO)
|
||||
|
||||
# 日志名称,若不配置,则使用名称:SQL_ALEX
|
||||
_log_name = config.get_config(f'{logger_config_name}.name', 'SQL_ALEX')
|
||||
# 日志文件最大值
|
||||
_log_max_bytes = config.get_config(f'{logger_config_name}.max_bytes', 0)
|
||||
# 日志文件备份数量
|
||||
_log_backup_count = config.get_config(f'{logger_config_name}.backup_count', 0)
|
||||
# 日志格式,若不配置,则使用 base 中的配置
|
||||
_log_format = config.get_config(f'{logger_config_name}.format', _base_log_format)
|
||||
_formatter = logging.Formatter(_log_format)
|
||||
# 日志层级,若不配置,则使用 base 中的配置
|
||||
_log_level = config.get_config(f'{logger_config_name}.level', _base_level)
|
||||
# 日志文件
|
||||
_log_file = config.get_config(f'{logger_config_name}.filename', '')
|
||||
|
||||
# 更新系统日志基本配置
|
||||
logging.basicConfig(**_log_base_cfg)
|
||||
# 重新绑定系统日志文件句柄
|
||||
_base_log_file_handler: Optional[handlers.RotatingFileHandler] = None
|
||||
if _base_log_file not in (None, ''):
|
||||
_base_log_file_handler = handlers.RotatingFileHandler(
|
||||
_base_log_file, maxBytes=_log_max_bytes, backupCount=_log_backup_count
|
||||
)
|
||||
_base_log_file_handler.setFormatter(_base_formatter)
|
||||
logging.root.handlers = [_base_log_file_handler]
|
||||
|
||||
# 创建日志对象
|
||||
paste_logger = logging.Logger(name=_log_name, level=_log_level)
|
||||
# 绑定日志文件句柄
|
||||
if _log_file not in ('', None):
|
||||
# 若配置了日志文件,则创建文件句柄
|
||||
_file_handler = handlers.RotatingFileHandler(
|
||||
_log_file, maxBytes=_log_max_bytes, backupCount=_log_backup_count
|
||||
)
|
||||
_file_handler.setFormatter(_formatter)
|
||||
paste_logger.addHandler(_file_handler)
|
||||
else:
|
||||
# 若未配置,则使用系统日志文件
|
||||
if _base_log_file_handler is not None:
|
||||
paste_logger.addHandler(_base_log_file_handler)
|
||||
|
||||
# 绑定控制台输出
|
||||
_console_handler = logging.StreamHandler()
|
||||
_console_handler.setFormatter(_formatter)
|
||||
paste_logger.addHandler(_console_handler)
|
||||
|
||||
return paste_logger
|
||||
|
||||
|
||||
def echo_log(msg: Union[str, Exception], level: int = logging.INFO, is_log_exc: bool = False):
|
||||
"""
|
||||
输出日志文本。默认输出到日志文件,但是可能不便于查询,这里应该考虑支持输出到日志数据库。
|
||||
|
||||
:param msg: 消息内容,当是 Exception 对象时,从 args 中取出第一项作为消息
|
||||
:param level: 消息等级
|
||||
:param is_log_exc: 是否输出异常的 Traceback 信息到日志文件
|
||||
"""
|
||||
_root = logging.root
|
||||
_logging = get_logger()
|
||||
_log_level = level
|
||||
if isinstance(msg, Exception):
|
||||
_log_level = logging.ERROR
|
||||
if len(msg.args) > 0 and isinstance(msg.args[0], str):
|
||||
msg = msg.args[0]
|
||||
else:
|
||||
msg = str(msg)
|
||||
|
||||
_logging.log(level=_log_level, msg=msg)
|
||||
if is_log_exc:
|
||||
exception_to_file()
|
||||
|
||||
|
||||
def exception_to_file():
|
||||
"""
|
||||
自动检测异常,并输出异常的 Traceback 信息到日志文件。
|
||||
"""
|
||||
_, _, tb = sys.exc_info()
|
||||
if tb is not None:
|
||||
_msg_list = ['Traceback: \n\n'] + traceback.format_tb(tb)
|
||||
log_to_file(msg=''.join(_msg_list), level=logging.ERROR)
|
||||
|
||||
|
||||
def log_to_file(msg: Any, level: int = logging.INFO):
|
||||
"""
|
||||
输出消息到日志文件。
|
||||
|
||||
:param msg: 消息
|
||||
:param level: 消息等级
|
||||
"""
|
||||
_logger = get_logger()
|
||||
_record = _logger.makeRecord(name=_logger.name, level=level, fn=__file__, lno=0, args=(), exc_info=None, msg=msg)
|
||||
for hdl in _logger.handlers:
|
||||
if isinstance(hdl, logging.FileHandler):
|
||||
hdl.handle(_record)
|
||||
Reference in New Issue
Block a user