feat: 【RUM】Span 列表页接口 --story=137033151 - #11887
Conversation
97a44da to
3a946d4
Compare
3a946d4 to
92cd884
Compare
ZhuoZhuoCrayon
left a comment
There was a problem hiding this comment.
本轮先评论合入安全和列表协议问题;序列化器继承会在对话里单独给方案,不在这次 review 里展开。
需要改:
generate_query_string与 APM 一样不鉴权,避免缺app_name时 500。view_config经 Factory 按mode分派,返回单个 mode 的核心协议,而不是{span_config, view_config, session_config}。generate_query_string下沉到 Level,Resource 经 Factory 调用。
| application = _get_authorized_application(data["bk_biz_id"], data["app_name"]) | ||
| span_handler = RumLevelHandlerFactory.create(RumQueryMode.SPAN.value, _build_data_sources([application])) | ||
| return { | ||
| "span_config": span_handler.view_config( |
There was a problem hiding this comment.
bkmonitor/packages/rum_web/query/resources.py:80 [P1]
"span_config": span_handler.view_config(
问题:方案约定 mode 选择 Level,Resource 不选择具体 Level 类,view_config 返回当前 mode 的单份配置。当前写死 SPAN,再拼 {span_config, view_config: {}, session_config: {}},工厂没有分派,空 dict 还会泄漏未就绪层级。
建议:按核心协议返回单个 mode 的 view_config:
handler = RumLevelHandlerFactory.create(data["mode"], _build_data_sources([application]))
return handler.view_config(
start_time=data["start_time"],
end_time=data["end_time"],
)
ZhuoZhuoCrayon
left a comment
There was a problem hiding this comment.
本轮先评论未注册 mode 的 500、文档与实现不一致、以及鉴权白名单;view_config 字段组装单独在对话里确认后再评。
| from rum_web.handlers.level.span import SpanLevelHandler | ||
|
|
||
|
|
||
| class UnsupportedRumQueryMode(ValueError): |
There was a problem hiding this comment.
bkmonitor/packages/rum_web/handlers/level/factory.py:17 [P1]
class UnsupportedRumQueryMode(ValueError):
问题:RumQueryMode 已经包含 view / session,序列化器会放行。Factory 首期只注册 span,未注册模式走 ValueError,落到 custom_exception_handler() 的兜底分支:HTTP 500 + ERROR 栈。文档把 view / session 写成合法枚举,联调时踩得到。
建议:把这个异常改成 DRF 能识别的 400。枚举可以保留三档,未注册交给 Factory 拒绝。
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
class UnsupportedRumQueryMode(ValidationError):
"""未注册的 RUM 查询模式"""
def __init__(self, mode: str):
super().__init__(_("不支持的 RUM 查询模式: {}").format(mode))| } | ||
| ``` | ||
|
|
||
| #### 2.1.2 Response |
There was a problem hiding this comment.
bkmonitor/packages/rum_web/docs/api/search.md:71 [P1]
返回包含
total和data字段的分页结构。
问题:实现已经按首轮结论改了,文档没跟上。前端按这份协议接会接错。
| 接口 | 文档仍写 | 当前实现 |
|---|---|---|
list_records |
{total, data: {list}} |
{list: [...]},没有 total |
view_config |
{span_config, view_config, session_config} |
单个 mode 的字段元数据 |
generate_query_string |
只有 filters |
还要 bk_biz_id / app_name / mode |
建议:list_records 响应改成:
{
"list": [
{
"span_id": "29926da51cae17cf",
"attributes.span_type": "resource",
"status.code": 0
}
]
}view_config 示例去掉 span_config 那一层,顶层直接给 default_sort / fields / groups / display_fields。generate_query_string 补上应用上下文。
|
|
||
| INSTANCE_ID = "app_name" | ||
|
|
||
| def get_permissions(self): |
There was a problem hiding this comment.
bkmonitor/packages/rum_web/query/views.py:28 [P2]
def get_permissions(self):
问题:generate_query_string 已经不鉴权,这点对。当前是数据接口白名单,未列入的 action 默认 []。里程碑 4 加上 field_topk 时,漏改这一处就会裸奔。
建议改成「除查询串转换外都鉴权」:
def get_permissions(self):
if self.action == "generate_query_string":
return []
return [
InstanceActionForDataPermission(
self.INSTANCE_ID,
[ActionEnum.VIEW_RUM_APPLICATION],
ResourceEnum.RUM_APPLICATION,
get_instance_id=Application.get_application_id_by_app_name,
)
]
ZhuoZhuoCrayon
left a comment
There was a problem hiding this comment.
view_config 字段组装:fields 写两遍、WEB_VITALS 空分组、display_fields 用了 user.id。
| config_dict = { | ||
| "default_sort": copy.deepcopy(self.query.DEFAULT_SORT), | ||
| "fields": [ | ||
| { |
There was a problem hiding this comment.
bkmonitor/packages/rum_web/handlers/level/span.py:68 [P1]
"fields": [
问题:view_config 现在会交出三份错协议。
fields写了两遍。这里先用列表推导填一遍,后面循环里再append一遍。all_fields有 N 条,fields就有 2N 条;前 N 条还没有unit/option_values。
config_dict = {
"default_sort": copy.deepcopy(self.query.DEFAULT_SORT),
"fields": [
{
"name": field_name,
"alias": field_dict.get("alias_name"),
"type": field_dict.get("field_type"),
"is_searchable": field_dict.get("is_searchable", False),
"is_agg": field_dict.get("is_agg", False),
"is_list": field_dict.get("is_list", True),
"supported_operations": field_dict.get("supported_operations", []),
}
for field_name, field_dict in all_fields.items()
],
...
}
for field_name, field_dict in all_fields.items():
_field_dict = {
"name": field_name,
"alias": field_dict["alias_name"],
"type": field_dict["field_type"],
"is_searchable": field_dict["is_searchable"],
"is_agg": field_dict["is_agg"],
"is_list": field_dict["is_list"],
"supported_operations": field_dict["supported_operations"],
}
if "unit" in field_dict:
_field_dict["unit"] = field_dict["unit"]
if "option_values" in field_dict:
_field_dict["option_values"] = field_dict["option_values"]
field_map[field_name] = _field_dict
config_dict["fields"].append(_field_dict)-
WEB_VITALS分组是空的。白名单写了LCP、CLS、INP、FCP、TTFB,这些名字不在query_fields()的 mapping 里。field_map只来自 mapping,组装分组时找不到就跳过。 -
DISPLAY_FIELDS用了user.id。协议和 USER 分组都是attributes.user.id,默认表头会空列。
建议:删掉第一次列表推导,fields 只保留循环里那一次;虚拟字段在组分组之前补进 field_map;默认列改成 attributes.user.id。
all_fields = self.query.query_fields(start_time, end_time)
field_map: dict[str, dict[str, Any]] = {}
for field_name, field_dict in all_fields.items():
item = {
"name": field_name,
"alias": field_dict["alias_name"],
"type": field_dict["field_type"],
"is_searchable": field_dict["is_searchable"],
"is_agg": field_dict["is_agg"],
"is_list": field_dict["is_list"],
"supported_operations": field_dict["supported_operations"],
}
if "unit" in field_dict:
item["unit"] = field_dict["unit"]
if "option_values" in field_dict:
item["option_values"] = field_dict["option_values"]
field_map[field_name] = item
# mapping 没有的虚拟字段,先补进 field_map,WEB_VITALS 才组得起来
for name, meta in self.VIRTUAL_FIELDS.items():
field_map.setdefault(name, meta)
return {
"default_sort": list(self.query.DEFAULT_SORT),
"fields": list(field_map.values()),
"groups": [
{
"name": group["name"],
"alias": group["alias"],
"fields": [field_map[name] for name in group["field_names"] if name in field_map],
}
for group in RUM_SEARCH_PAGE_GROUPS.get("span", [])
],
"display_fields": list(self.DISPLAY_FIELDS),
}VIRTUAL_FIELDS 放在 SpanLevelHandler 上,把 LCP 等虚拟字段的 name / alias / type / unit / is_list 写死。DISPLAY_FIELDS 把 user.id 改成 attributes.user.id。
| ], | ||
| }, | ||
| { | ||
| "name": "INTERACTION", |
|
|
||
| field_map: dict[str, dict[str, Any]] = {} | ||
| for field_name, field_dict in all_fields.items(): | ||
| _field_dict = { |
There was a problem hiding this comment.
bkmonitor/packages/rum_web/handlers/level/span.py:85 [P1]
for field_name, field_dict in all_fields.items():
问题:BaseQuery._query_fields() 已经给出字段对象,Level 又映射成 name / alias / type / unit。两边命名不一致,每层都要精修一遍。
建议:查询层和接口层用同一套字段名。BaseQuery 把 alias_name 改成 field_alias,unit 改成 field_unit。view_config 沿用:
field_name
field_alias
field_type
field_unit
origin_field
is_searchable
is_agg
is_list
supported_operations
Level 只丢掉查询层私有键,不再手写字段字典:
ignore_keys: list[str] = ["is_case_sensitive", "is_analyzed"]
for key in ignore_keys:
field_dict.pop(key, None)
No description provided.