-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ 添加简单统计功能 #661
Closed
Closed
✨ 添加简单统计功能 #661
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
from datetime import time, datetime | ||
from collections.abc import Callable, Sequence, Awaitable | ||
|
||
from loguru import logger | ||
from nonebot.compat import model_dump | ||
from sqlalchemy.orm import selectinload | ||
from sqlalchemy.exc import IntegrityError | ||
|
@@ -32,9 +33,11 @@ def __init__(self): | |
self.delete_target_hook: list[Callable[[str, T_Target], Awaitable]] = [] | ||
|
||
def register_add_target_hook(self, fun: Callable[[str, T_Target], Awaitable]): | ||
logger.debug(f"register add target hook {fun.__name__}") | ||
self.add_target_hook.append(fun) | ||
|
||
def register_delete_target_hook(self, fun: Callable[[str, T_Target], Awaitable]): | ||
logger.debug(f"register delete target hook {fun.__name__}") | ||
self.delete_target_hook.append(fun) | ||
|
||
async def add_subscribe( | ||
|
@@ -55,8 +58,10 @@ async def add_subscribe( | |
db_target_stmt = select(Target).where(Target.platform_name == platform_name).where(Target.target == target) | ||
db_target: Target | None = await session.scalar(db_target_stmt) | ||
if not db_target: | ||
logger.debug(f"add sub get db_target: {db_target}") | ||
db_target = Target(target=target, platform_name=platform_name, target_name=target_name) | ||
await asyncio.gather(*[hook(platform_name, target) for hook in self.add_target_hook]) | ||
hook_resp = await asyncio.gather(*[hook(platform_name, target) for hook in self.add_target_hook]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个 hook resp 的内容会是什么 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hook func的返回值 虽然目前为空就是了( |
||
logger.debug(f"add hook response: {hook_resp}") | ||
else: | ||
db_target.target_name = target_name | ||
subscribe = Subscribe( | ||
|
@@ -106,7 +111,10 @@ async def del_subscribe(self, user: PlatformTarget, target: str, platform_name: | |
) | ||
if target_count == 0: | ||
# delete empty target | ||
await asyncio.gather(*[hook(platform_name, T_Target(target)) for hook in self.delete_target_hook]) | ||
hook_resp = await asyncio.gather( | ||
*[hook(platform_name, T_Target(target)) for hook in self.delete_target_hook] | ||
) | ||
logger.debug(f"del hook response: {hook_resp}") | ||
await session.commit() | ||
|
||
async def update_subscribe( | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
from functools import wraps | ||
from datetime import datetime | ||
from collections import defaultdict | ||
from collections.abc import Callable, Coroutine | ||
from typing import TYPE_CHECKING, Any, Literal, TypeVar, TypedDict | ||
|
||
from nonebot_bison.utils import dict_to_str | ||
|
||
from ..utils import Site | ||
from ..types import Target | ||
|
||
if TYPE_CHECKING: | ||
from .scheduler import Scheduler, Schedulable | ||
|
||
|
||
TSchd = TypeVar("TSchd", bound="Scheduler") | ||
|
||
|
||
RecordKind = Literal["insert_new", "delete"] | ||
|
||
|
||
class PlatformTargetRecord(TypedDict): | ||
schedule_count: defaultdict[str, defaultdict[str, int]] | ||
insert_new: list[tuple[str, datetime]] | ||
delete: list[tuple[str, datetime]] | ||
post_send: defaultdict[str, int] | ||
|
||
|
||
class RuntimeStatistic: | ||
def __init__(self): | ||
self._record = PlatformTargetRecord( | ||
schedule_count=defaultdict(lambda: defaultdict(int)), insert_new=[], delete=[], post_send=defaultdict(int) | ||
) | ||
|
||
def statistic_schedule_count(self, func: "Callable[[TSchd], Coroutine[Any, Any, Schedulable | None]]"): | ||
@wraps(func) | ||
async def wrapper(*args, **kwargs): | ||
if not (schedulable := await func(*args, **kwargs)): | ||
return | ||
self._record["schedule_count"][schedulable.platform_name][schedulable.target] += 1 | ||
return schedulable | ||
|
||
return wrapper | ||
|
||
def statistic_record(self, name: RecordKind): | ||
record = self._record[name] | ||
|
||
def decorator(func: "Callable[[TSchd, str, Target], None]"): | ||
@wraps(func) | ||
def wrapper(*args, **kwargs): | ||
platform_name, target = args[1], args[2] | ||
record.append((f"{platform_name}-{target}", datetime.now())) | ||
return func(*args, **kwargs) | ||
|
||
return wrapper | ||
|
||
return decorator | ||
|
||
def statistic_post_send(self, platform_name: str): | ||
self._record["post_send"][platform_name] += 1 | ||
|
||
def _generate_stats(self, scheduler_dict: "dict[type[Site], Scheduler]"): | ||
report_dict: dict[str, Any] = { | ||
"新增订阅hook调用记录": [f"{record[0]}: {record[1]}" for record in self._record["insert_new"]], | ||
"删除订阅hook调用记录": [f"{record[0]}: {record[1]}" for record in self._record["delete"]], | ||
} | ||
|
||
platform_dict = {} | ||
for platform, targets in self._record["schedule_count"].items(): | ||
target_dict = [f"{target}: {count} 次" for target, count in targets.items()] | ||
platform_dict[platform] = target_dict | ||
report_dict["调度统计"] = platform_dict | ||
|
||
post_send_dict = {} | ||
for platform, count in self._record["post_send"].items(): | ||
post_send_dict[platform] = f"{count} 次" | ||
|
||
report_dict["发送消息统计"] = post_send_dict | ||
|
||
all_schedulable = {} | ||
for site, scheduler in scheduler_dict.items(): | ||
scheduler_list = [] | ||
for schedulable in scheduler.schedulable_list: | ||
scheduler_list.append( | ||
f"{schedulable.target}: [权重]{schedulable.current_weight} | [批量]{schedulable.use_batch}" | ||
) | ||
all_schedulable[site.name] = scheduler_list | ||
|
||
report_dict["所有调度对象"] = all_schedulable | ||
|
||
return report_dict | ||
|
||
def generate_report(self, scheduler_dict: "dict[type[Site], Scheduler]"): | ||
"""根据 schduler_dict 生成报告""" | ||
report_dict = self._generate_stats(scheduler_dict) | ||
return dict_to_str(report_dict) | ||
|
||
|
||
runtime_statistic = RuntimeStatistic() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
from textwrap import dedent | ||
from datetime import datetime | ||
from typing import TYPE_CHECKING | ||
from collections import defaultdict | ||
from unittest.mock import Mock, AsyncMock, patch | ||
|
||
import pytest | ||
|
||
if TYPE_CHECKING: | ||
from nonebot_bison.scheduler.statistic import RuntimeStatistic | ||
|
||
|
||
@pytest.fixture | ||
def runtime_statistic(): | ||
from nonebot_bison.scheduler.statistic import RuntimeStatistic | ||
|
||
return RuntimeStatistic() | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_statistic_schedule_count(app, runtime_statistic: "RuntimeStatistic"): | ||
mock_func = AsyncMock() | ||
mock_func.return_value = AsyncMock(platform_name="test_platform", target="test_target") | ||
|
||
decorated_func = runtime_statistic.statistic_schedule_count(mock_func) | ||
await decorated_func() | ||
|
||
assert runtime_statistic._record["schedule_count"]["test_platform"]["test_target"] == 1 | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_statistic_schedule_count_no_schedulable(app, runtime_statistic: "RuntimeStatistic"): | ||
mock_func = AsyncMock() | ||
mock_func.return_value = None | ||
|
||
decorated_func = runtime_statistic.statistic_schedule_count(mock_func) | ||
await decorated_func() | ||
|
||
assert "test_platform" not in runtime_statistic._record["schedule_count"] | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_statistic_record_insert_new(app, runtime_statistic: "RuntimeStatistic"): | ||
mock_func = AsyncMock() | ||
mock_self = Mock() | ||
|
||
decorated_func = runtime_statistic.statistic_record("insert_new")(mock_func) | ||
with patch("nonebot_bison.scheduler.statistic.datetime") as mock_datetime: | ||
mock_datetime.now.return_value = datetime(2023, 1, 1) | ||
await decorated_func(mock_self, "test_platform", "test_target") # type: ignore | ||
|
||
assert runtime_statistic._record["insert_new"] == [("test_platform-test_target", datetime(2023, 1, 1))] | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_statistic_record_delete(app, runtime_statistic: "RuntimeStatistic"): | ||
mock_func = AsyncMock() | ||
mock_self = Mock() | ||
|
||
decorated_func = runtime_statistic.statistic_record("delete")(mock_func) | ||
with patch("nonebot_bison.scheduler.statistic.datetime") as mock_datetime: | ||
mock_datetime.now.return_value = datetime(2023, 1, 2) | ||
await decorated_func(mock_self, "test_platform", "test_target") # type: ignore | ||
|
||
assert runtime_statistic._record["delete"] == [("test_platform-test_target", datetime(2023, 1, 2))] | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_statistic_record_report_generate(app, runtime_statistic: "RuntimeStatistic"): | ||
from nonebot_bison.utils import Site | ||
from nonebot_bison.scheduler.scheduler import Scheduler | ||
|
||
class MockSite(Site): | ||
name = "test_site" | ||
schedule_type = "interval" | ||
schedule_setting = {"seconds": 100} | ||
|
||
scheduler_dict: dict[type[Site], Scheduler] = {MockSite: Scheduler(MockSite, [], [])} | ||
_mock_record = defaultdict(int) | ||
_mock_record["test_target"] = 1 | ||
runtime_statistic._record["schedule_count"]["test_platform"] = _mock_record | ||
runtime_statistic._record["insert_new"] = [("test_platform-test_target", datetime(2023, 1, 1))] | ||
runtime_statistic._record["delete"] = [("test_platform-test_target", datetime(2023, 1, 1))] | ||
|
||
report = runtime_statistic._generate_stats(scheduler_dict) | ||
|
||
assert report == { | ||
"新增订阅hook调用记录": [f"test_platform-test_target: {datetime(2023, 1, 1).strftime('%Y-%m-%d %H:%M:%S')}"], | ||
"删除订阅hook调用记录": [f"test_platform-test_target: {datetime(2023, 1, 1).strftime('%Y-%m-%d %H:%M:%S')}"], | ||
"调度统计": {"test_platform": ["test_target: 1 次"]}, | ||
"所有调度对象": {"test_site": []}, | ||
"发送消息统计": {}, | ||
} | ||
|
||
repost_str = runtime_statistic.generate_report(scheduler_dict) | ||
assert ( | ||
repost_str | ||
== dedent( | ||
""" | ||
新增订阅hook调用记录: | ||
test_platform-test_target: 2023-01-01 00:00:00 | ||
删除订阅hook调用记录: | ||
test_platform-test_target: 2023-01-01 00:00:00 | ||
调度统计: | ||
test_platform: | ||
test_target: 1 次 | ||
发送消息统计: | ||
所有调度对象: | ||
test_site: | ||
""" | ||
)[1:] | ||
) # 不要第一个换行符 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
为什么没有用 nb 的 logger