main:采用异步 Redis 客户端优化指标管理模块
变更内容: - 将 `redis` 客户端替换为 `redis.asyncio` 实现。 - 系统中同步方法调整为异步方法,提升事件循环效率。 - 在 `MetricsManager` 中添加异步初始化及关闭逻辑,避免阻塞问题。 - 更新便捷函数以支持异步上下文,并添加同步模式的兼容方法。 - 调整 Worker、JobManager、API 路由等模块,适配异步指标操作。 - 扩展单元测试,覆盖新增的异步方法及 Redis 操作逻辑。 - 简化 Dockerfile,取消开发依赖安装命令。
This commit is contained in:
@@ -1,158 +1,239 @@
|
||||
"""metrics_unified 模块单元测试"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_manager():
|
||||
"""每个测试前后重置管理器"""
|
||||
from functional_scaffold.core.metrics_unified import reset_metrics_manager_sync
|
||||
|
||||
reset_metrics_manager_sync()
|
||||
yield
|
||||
reset_metrics_manager_sync()
|
||||
|
||||
|
||||
class TestMetricsManager:
|
||||
"""MetricsManager 类测试"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redis(self):
|
||||
"""模拟 Redis 客户端"""
|
||||
with patch("redis.Redis") as mock:
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_instance.hincrbyfloat.return_value = 1.0
|
||||
mock_instance.hset.return_value = True
|
||||
mock_instance.hgetall.return_value = {}
|
||||
mock_instance.hget.return_value = "0"
|
||||
mock_instance.keys.return_value = []
|
||||
mock_instance.pipeline.return_value = MagicMock()
|
||||
mock.return_value = mock_instance
|
||||
yield mock_instance
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self, mock_redis):
|
||||
"""创建测试用的 MetricsManager"""
|
||||
from functional_scaffold.core.metrics_unified import (
|
||||
MetricsManager,
|
||||
reset_metrics_manager,
|
||||
)
|
||||
|
||||
reset_metrics_manager()
|
||||
manager = MetricsManager()
|
||||
return manager
|
||||
|
||||
def test_init_loads_default_config(self, manager):
|
||||
def test_init_loads_default_config(self):
|
||||
"""测试初始化加载默认配置"""
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
assert manager.config is not None
|
||||
assert "builtin_metrics" in manager.config or len(manager.metrics_definitions) > 0
|
||||
|
||||
def test_metrics_definitions_registered(self, manager):
|
||||
def test_metrics_definitions_registered(self):
|
||||
"""测试指标定义已注册"""
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
assert "http_requests_total" in manager.metrics_definitions
|
||||
assert "http_request_duration_seconds" in manager.metrics_definitions
|
||||
assert "algorithm_executions_total" in manager.metrics_definitions
|
||||
|
||||
def test_incr_counter(self, manager, mock_redis):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_incr_counter(self, mock_redis_class):
|
||||
"""测试计数器增加"""
|
||||
manager.incr("http_requests_total", {"method": "GET", "endpoint": "/", "status": "success"})
|
||||
mock_redis.hincrbyfloat.assert_called()
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.hincrbyfloat = AsyncMock(return_value=1.0)
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
def test_incr_with_invalid_metric_type(self, manager, mock_redis):
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
await manager.initialize()
|
||||
|
||||
await manager.incr(
|
||||
"http_requests_total", {"method": "GET", "endpoint": "/", "status": "success"}
|
||||
)
|
||||
mock_instance.hincrbyfloat.assert_called()
|
||||
|
||||
def test_incr_with_invalid_metric_type(self):
|
||||
"""测试对非计数器类型调用 incr"""
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
# http_request_duration_seconds 是 histogram 类型
|
||||
manager.incr("http_request_duration_seconds", {})
|
||||
# 不应该调用 Redis(因为类型不匹配)
|
||||
# 验证没有调用 hincrbyfloat(或者调用次数没有增加)
|
||||
# 验证不会抛出异常(因为 Redis 不可用)
|
||||
|
||||
def test_set_gauge(self, manager, mock_redis):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_set_gauge(self, mock_redis_class):
|
||||
"""测试设置仪表盘"""
|
||||
manager.set("http_requests_in_progress", {}, 5)
|
||||
mock_redis.hset.assert_called()
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.hset = AsyncMock(return_value=True)
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
def test_gauge_incr(self, manager, mock_redis):
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
await manager.initialize()
|
||||
|
||||
await manager.set("http_requests_in_progress", {}, 5)
|
||||
mock_instance.hset.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_gauge_incr(self, mock_redis_class):
|
||||
"""测试增加仪表盘"""
|
||||
manager.gauge_incr("http_requests_in_progress", {}, 1)
|
||||
mock_redis.hincrbyfloat.assert_called()
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.hincrbyfloat = AsyncMock(return_value=1.0)
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
def test_gauge_decr(self, manager, mock_redis):
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
await manager.initialize()
|
||||
|
||||
await manager.gauge_incr("http_requests_in_progress", {}, 1)
|
||||
mock_instance.hincrbyfloat.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_gauge_decr(self, mock_redis_class):
|
||||
"""测试减少仪表盘"""
|
||||
manager.gauge_decr("http_requests_in_progress", {}, 1)
|
||||
mock_redis.hincrbyfloat.assert_called()
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.hincrbyfloat = AsyncMock(return_value=1.0)
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
def test_observe_histogram(self, manager, mock_redis):
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
await manager.initialize()
|
||||
|
||||
await manager.gauge_decr("http_requests_in_progress", {}, 1)
|
||||
mock_instance.hincrbyfloat.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_observe_histogram(self, mock_redis_class):
|
||||
"""测试直方图观测"""
|
||||
mock_pipeline = MagicMock()
|
||||
mock_redis.pipeline.return_value = mock_pipeline
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.close = AsyncMock()
|
||||
|
||||
manager.observe("http_request_duration_seconds", {"method": "GET", "endpoint": "/"}, 0.05)
|
||||
mock_pipeline = AsyncMock()
|
||||
mock_pipeline.hincrbyfloat = MagicMock()
|
||||
mock_pipeline.execute = AsyncMock(return_value=[])
|
||||
mock_instance.pipeline = MagicMock(return_value=mock_pipeline)
|
||||
|
||||
mock_redis.pipeline.assert_called()
|
||||
mock_pipeline.execute.assert_called()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
def test_labels_to_key(self, manager):
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
await manager.initialize()
|
||||
|
||||
await manager.observe(
|
||||
"http_request_duration_seconds", {"method": "GET", "endpoint": "/"}, 0.05
|
||||
)
|
||||
mock_instance.pipeline.assert_called()
|
||||
|
||||
def test_labels_to_key(self):
|
||||
"""测试标签转换为 key"""
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
labels = {"method": "GET", "endpoint": "/api"}
|
||||
key = manager._labels_to_key(labels)
|
||||
assert "method=GET" in key
|
||||
assert "endpoint=/api" in key
|
||||
|
||||
def test_labels_to_key_empty(self, manager):
|
||||
def test_labels_to_key_empty(self):
|
||||
"""测试空标签转换"""
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
key = manager._labels_to_key(None)
|
||||
assert key == ""
|
||||
|
||||
key = manager._labels_to_key({})
|
||||
assert key == ""
|
||||
|
||||
def test_is_available(self, manager):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_is_available(self, mock_redis_class):
|
||||
"""测试 Redis 可用性检查"""
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import MetricsManager
|
||||
|
||||
manager = MetricsManager()
|
||||
await manager.initialize()
|
||||
|
||||
assert manager.is_available() is True
|
||||
|
||||
|
||||
class TestConvenienceFunctions:
|
||||
"""便捷函数测试"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""每个测试前重置管理器"""
|
||||
from functional_scaffold.core.metrics_unified import reset_metrics_manager
|
||||
|
||||
reset_metrics_manager()
|
||||
|
||||
@patch("redis.Redis")
|
||||
def test_incr_function(self, mock_redis_class):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_incr_function(self, mock_redis_class):
|
||||
"""测试 incr 便捷函数"""
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.hincrbyfloat = AsyncMock(return_value=1.0)
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import incr, reset_metrics_manager
|
||||
from functional_scaffold.core.metrics_unified import incr
|
||||
|
||||
reset_metrics_manager()
|
||||
incr("http_requests_total", {"method": "GET", "endpoint": "/", "status": "success"})
|
||||
await incr(
|
||||
"http_requests_total", {"method": "GET", "endpoint": "/", "status": "success"}
|
||||
)
|
||||
|
||||
mock_instance.hincrbyfloat.assert_called()
|
||||
|
||||
@patch("redis.Redis")
|
||||
def test_set_function(self, mock_redis_class):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_set_function(self, mock_redis_class):
|
||||
"""测试 set 便捷函数"""
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.hset = AsyncMock(return_value=True)
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import reset_metrics_manager, set
|
||||
from functional_scaffold.core.metrics_unified import set
|
||||
|
||||
reset_metrics_manager()
|
||||
set("http_requests_in_progress", {}, 10)
|
||||
await set("http_requests_in_progress", {}, 10)
|
||||
|
||||
mock_instance.hset.assert_called()
|
||||
|
||||
@patch("redis.Redis")
|
||||
def test_observe_function(self, mock_redis_class):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_observe_function(self, mock_redis_class):
|
||||
"""测试 observe 便捷函数"""
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_pipeline = MagicMock()
|
||||
mock_instance.pipeline.return_value = mock_pipeline
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.close = AsyncMock()
|
||||
|
||||
mock_pipeline = AsyncMock()
|
||||
mock_pipeline.hincrbyfloat = MagicMock()
|
||||
mock_pipeline.execute = AsyncMock(return_value=[])
|
||||
mock_instance.pipeline = MagicMock(return_value=mock_pipeline)
|
||||
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import observe, reset_metrics_manager
|
||||
from functional_scaffold.core.metrics_unified import observe
|
||||
|
||||
reset_metrics_manager()
|
||||
observe("http_request_duration_seconds", {"method": "GET", "endpoint": "/"}, 0.1)
|
||||
await observe("http_request_duration_seconds", {"method": "GET", "endpoint": "/"}, 0.1)
|
||||
|
||||
mock_instance.pipeline.assert_called()
|
||||
|
||||
@@ -160,42 +241,49 @@ class TestConvenienceFunctions:
|
||||
class TestExport:
|
||||
"""导出功能测试"""
|
||||
|
||||
@patch("redis.Redis")
|
||||
def test_export_counter(self, mock_redis_class):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_export_counter(self, mock_redis_class):
|
||||
"""测试导出计数器"""
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_instance.hgetall.return_value = {"method=GET,endpoint=/,status=success": "10"}
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
mock_instance.hgetall = AsyncMock(
|
||||
return_value={"method=GET,endpoint=/,status=success": "10"}
|
||||
)
|
||||
mock_instance.hget = AsyncMock(return_value="0")
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import export, reset_metrics_manager
|
||||
from functional_scaffold.core.metrics_unified import export
|
||||
|
||||
reset_metrics_manager()
|
||||
output = export()
|
||||
output = await export()
|
||||
|
||||
assert "http_requests_total" in output
|
||||
assert "HELP" in output
|
||||
assert "TYPE" in output
|
||||
|
||||
@patch("redis.Redis")
|
||||
def test_export_histogram(self, mock_redis_class):
|
||||
@pytest.mark.asyncio
|
||||
@patch("redis.asyncio.Redis")
|
||||
async def test_export_histogram(self, mock_redis_class):
|
||||
"""测试导出直方图"""
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_instance.hgetall.side_effect = lambda key: (
|
||||
{"method=GET,endpoint=/": "5"}
|
||||
if "count" in key
|
||||
else {"method=GET,endpoint=/": "0.5"}
|
||||
if "sum" in key
|
||||
else {}
|
||||
)
|
||||
mock_instance.hget.return_value = "3"
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.ping = AsyncMock(return_value=True)
|
||||
|
||||
async def mock_hgetall(key):
|
||||
if "count" in key:
|
||||
return {"method=GET,endpoint=/": "5"}
|
||||
elif "sum" in key:
|
||||
return {"method=GET,endpoint=/": "0.5"}
|
||||
return {}
|
||||
|
||||
mock_instance.hgetall = mock_hgetall
|
||||
mock_instance.hget = AsyncMock(return_value="3")
|
||||
mock_instance.close = AsyncMock()
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import export, reset_metrics_manager
|
||||
from functional_scaffold.core.metrics_unified import export
|
||||
|
||||
reset_metrics_manager()
|
||||
output = export()
|
||||
output = await export()
|
||||
|
||||
assert "http_request_duration_seconds" in output
|
||||
|
||||
@@ -226,21 +314,9 @@ class TestEnvVarSubstitution:
|
||||
class TestTrackAlgorithmExecution:
|
||||
"""track_algorithm_execution 装饰器测试"""
|
||||
|
||||
@patch("redis.Redis")
|
||||
def test_decorator_success(self, mock_redis_class):
|
||||
def test_decorator_success(self):
|
||||
"""测试装饰器成功执行"""
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_pipeline = MagicMock()
|
||||
mock_instance.pipeline.return_value = mock_pipeline
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import (
|
||||
reset_metrics_manager,
|
||||
track_algorithm_execution,
|
||||
)
|
||||
|
||||
reset_metrics_manager()
|
||||
from functional_scaffold.core.metrics_unified import track_algorithm_execution
|
||||
|
||||
@track_algorithm_execution("test_algo")
|
||||
def test_func():
|
||||
@@ -249,21 +325,9 @@ class TestTrackAlgorithmExecution:
|
||||
result = test_func()
|
||||
assert result == "result"
|
||||
|
||||
@patch("redis.Redis")
|
||||
def test_decorator_error(self, mock_redis_class):
|
||||
def test_decorator_error(self):
|
||||
"""测试装饰器错误处理"""
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.ping.return_value = True
|
||||
mock_pipeline = MagicMock()
|
||||
mock_instance.pipeline.return_value = mock_pipeline
|
||||
mock_redis_class.return_value = mock_instance
|
||||
|
||||
from functional_scaffold.core.metrics_unified import (
|
||||
reset_metrics_manager,
|
||||
track_algorithm_execution,
|
||||
)
|
||||
|
||||
reset_metrics_manager()
|
||||
from functional_scaffold.core.metrics_unified import track_algorithm_execution
|
||||
|
||||
@track_algorithm_execution("test_algo")
|
||||
def test_func():
|
||||
|
||||
Reference in New Issue
Block a user