Skip to content

Commit 042d624

Browse files
author
Kaoru
committed
fix: query_backtest_logs/query_component_logs level 双向 lower 归一
ClickHouse level 列混大小写存储,== 大小写敏感。query_backtest_logs / query_component_logs 的单边 level.upper() 传 "ERROR" 匹配不到小写 "error" 存储,按级别过滤漏数据。 对齐 search_logs(已修)范式:func.lower(model.level) == level.lower()。 TDD: 3 测试 RED->GREEN(session mock 捕获 query 对象,编译 WHERE clause, 三断言交叉锁区分单边 .upper() 与双向 func.lower)。 三层冒烟:L1 py_compile / L2 启动路径 / L3 log_service 回归 39 passed 零回归。
1 parent c9b807d commit 042d624

2 files changed

Lines changed: 99 additions & 2 deletions

File tree

src/ginkgo/services/logging/log_service.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ def query_backtest_logs(
113113
if strategy_id:
114114
conditions.append(MBacktestLog.strategy_id == strategy_id)
115115
if level:
116-
conditions.append(MBacktestLog.level == level.upper())
116+
# ClickHouse level 列混大小写存储(arch_log_level_lowercase_storage),
117+
# == 大小写敏感。双向 func.lower 归一,对齐 search_logs。
118+
conditions.append(func.lower(MBacktestLog.level) == level.lower())
117119
if trace_id:
118120
conditions.append(MBacktestLog.trace_id == trace_id)
119121
if event_type:
@@ -181,7 +183,9 @@ def query_component_logs(
181183
if component_name:
182184
conditions.append(MComponentLog.component_name == component_name)
183185
if level:
184-
conditions.append(MComponentLog.level == level.upper())
186+
# ClickHouse level 列混大小写存储(arch_log_level_lowercase_storage),
187+
# 双向 func.lower 归一,对齐 search_logs / query_backtest_logs。
188+
conditions.append(func.lower(MComponentLog.level) == level.lower())
185189
if start_time:
186190
conditions.append(MComponentLog.timestamp >= start_time)
187191
if end_time:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Upstream: src/ginkgo/services/logging/log_service.py
2+
# Context: [[arch_log_level_lowercase_storage]] — ClickHouse level 列混大小写存储,
3+
# `==` 大小写敏感。search_logs 已用 func.lower() 双向归一修复(#6232),
4+
# 但 query_backtest_logs / query_component_logs 仍用单边 level.upper(),
5+
# 传 "ERROR" 匹配不到小写 "error" → CLI/portfolio_service 按级别过滤漏数据。
6+
from unittest.mock import MagicMock
7+
8+
from ginkgo.services.logging.log_service import LogService
9+
10+
11+
def _make_service_with_capturing_session():
12+
"""装配 LogService(object.__new__ 避开 __init__ 的 get_db_connection 重依赖),
13+
mock session 捕获 execute 收到的 query,供断言 where clause 的 level 归一。"""
14+
svc = object.__new__(LogService)
15+
svc._logger = MagicMock()
16+
# _log_operation_start/end 继承自 BaseService,mock 成 no-op
17+
svc._log_operation_start = lambda *a, **kw: None
18+
svc._log_operation_end = lambda *a, **kw: None
19+
20+
captured = {"query": None}
21+
22+
mock_session = MagicMock()
23+
24+
def _execute(q):
25+
captured["query"] = q
26+
result = MagicMock()
27+
result.scalars.return_value.all.return_value = []
28+
return result
29+
30+
mock_session.execute.side_effect = _execute
31+
mock_engine = MagicMock()
32+
mock_engine.get_session.return_value.__enter__.return_value = mock_session
33+
mock_engine.get_session.return_value.__exit__.return_value = False
34+
svc._engine = mock_engine
35+
return svc, captured
36+
37+
38+
def _compiled_sql(query) -> str:
39+
"""编译 query 为含字面量的 SQL(保留原始大小写)。
40+
41+
保留大小写是关键:bug 代码参数='ERROR'(大写),修复后='error'(小写),
42+
若整体 .lower() 会掩盖该差异致假绿。
43+
"""
44+
return str(query.compile(compile_kwargs={"literal_binds": True}))
45+
46+
47+
class TestQueryLogLevelCaseInsensitive:
48+
"""[[arch_log_level_lowercase_storage]] — query_*_logs level 过滤须双向 lower 归一。"""
49+
50+
def test_query_backtest_logs_uses_double_lower_for_level(self):
51+
"""query_backtest_logs level 过滤应双向 lower(列 + 参数),对齐 search_logs。
52+
53+
三断言共同区分 bug(单边 .upper())与修复(双向 func.lower):
54+
- lower( 函数名存在(列侧归一)
55+
- 参数小写 'error'(参数侧归一)
56+
- 参数非大写 'ERROR'(反向锁,单边 .upper() 会留大写)
57+
"""
58+
svc, captured = _make_service_with_capturing_session()
59+
svc.query_backtest_logs(level="ErRoR")
60+
61+
assert captured["query"] is not None, "query 未被 session.execute 捕获"
62+
sql = _compiled_sql(captured["query"])
63+
assert "lower(" in sql.lower(), (
64+
f"列侧未用 func.lower 归一(单边 .upper() 漏小写存储): {sql}"
65+
)
66+
assert "'error'" in sql, f"参数未归一小写(应为 'error'): {sql}"
67+
assert "'ERROR'" not in sql, (
68+
f"参数仍为大写 'ERROR'(单边 .upper() 未双向归一): {sql}"
69+
)
70+
71+
def test_query_component_logs_uses_double_lower_for_level(self):
72+
"""query_component_logs 同源 bug,同样须双向 lower。"""
73+
svc, captured = _make_service_with_capturing_session()
74+
svc.query_component_logs(level="WARNING")
75+
76+
assert captured["query"] is not None, "query 未被 session.execute 捕获"
77+
sql = _compiled_sql(captured["query"])
78+
assert "lower(" in sql.lower(), f"列侧未用 func.lower 归一: {sql}"
79+
assert "'warning'" in sql, f"参数未归一小写: {sql}"
80+
assert "'WARNING'" not in sql, (
81+
f"参数仍为大写 'WARNING'(单边 .upper() 未双向归一): {sql}"
82+
)
83+
84+
def test_query_backtest_logs_no_level_does_not_filter(self):
85+
"""回归锁:level=None(默认)时不加 level 条件(if level 守卫)。"""
86+
svc, captured = _make_service_with_capturing_session()
87+
svc.query_backtest_logs() # 无 level
88+
89+
assert captured["query"] is not None
90+
sql = _compiled_sql(captured["query"])
91+
assert "'error'" not in sql and "'ERROR'" not in sql, (
92+
f"level=None 时不应注入级别过滤: {sql}"
93+
)

0 commit comments

Comments
 (0)