diff --git a/apiserver/paasng/paasng/infras/bkmonitorv3/client.py b/apiserver/paasng/paasng/infras/bkmonitorv3/client.py index 7d559485df..678329cc7a 100644 --- a/apiserver/paasng/paasng/infras/bkmonitorv3/client.py +++ b/apiserver/paasng/paasng/infras/bkmonitorv3/client.py @@ -30,6 +30,7 @@ BkMonitorGatewayServiceError, BkMonitorSpaceDoesNotExist, ) +from paasng.infras.bkmonitorv3.models import BKMonitorSpace as BKMonitorSpaceModel from paasng.infras.bkmonitorv3.params import QueryAlarmStrategiesParams, QueryAlertsParams logger = logging.getLogger(__name__) @@ -38,26 +39,19 @@ class BkMonitorBackend(Protocol): """Describes protocols of calling API service""" - def metadata_get_space_detail(self, *args, **kwargs) -> Dict: - ... + def metadata_get_space_detail(self, *args, **kwargs) -> Dict: ... - def metadata_create_space(self, *args, **kwargs) -> Dict: - ... + def metadata_create_space(self, *args, **kwargs) -> Dict: ... - def metadata_update_space(self, *args, **kwargs) -> Dict: - ... + def metadata_update_space(self, *args, **kwargs) -> Dict: ... - def search_alert(self, *args, **kwargs) -> Dict: - ... + def search_alert(self, *args, **kwargs) -> Dict: ... - def search_alarm_strategy_v3(self, *args, **kwargs) -> Dict: - ... + def search_alarm_strategy_v3(self, *args, **kwargs) -> Dict: ... - def promql_query(self, *args, **kwargs) -> Dict: - ... + def promql_query(self, *args, **kwargs) -> Dict: ... - def as_code_import_config(self, *args, **kwargs) -> Dict: - ... + def as_code_import_config(self, *args, **kwargs) -> Dict: ... class BKMonitorSpaceManager: @@ -174,6 +168,16 @@ def query_alerts(self, query_params: QueryAlertsParams) -> List: raise BkMonitorApiError(resp["message"]) return resp.get("data", {}).get("alerts", []) + def query_space_biz_id(self, app_codes: List[str]) -> List[Dict]: + """查询应用的蓝鲸监控空间在权限中心的资源 id + + :param app_codes: 查询监控空间的应用 id + """ + monitor_spaces = BKMonitorSpaceModel.objects.filter(application__code__in=app_codes).select_related( + "application" + ) + return [{"application": space.application, "bk_biz_id": space.iam_resource_id} for space in monitor_spaces] + def query_alarm_strategies(self, query_params: QueryAlarmStrategiesParams) -> Dict: """查询告警策略 @@ -189,9 +193,9 @@ def query_alarm_strategies(self, query_params: QueryAlarmStrategiesParams) -> Di if not resp.get("result"): raise BkMonitorApiError(resp["message"]) data = resp.get("data", {}) - data[ - "strategy_config_link" - ] = f"{settings.BK_MONITORV3_URL}/?bizId={query_params_dict['bk_biz_id']}/#/strategy-config/" + data["strategy_config_link"] = ( + f"{settings.BK_MONITORV3_URL}/?bizId={query_params_dict['bk_biz_id']}/#/strategy-config/" + ) return data def promql_query(self, bk_biz_id: Optional[str], promql: str, start: str, end: str, step: str) -> List: diff --git a/apiserver/paasng/paasng/infras/bkmonitorv3/params.py b/apiserver/paasng/paasng/infras/bkmonitorv3/params.py index 0518f4677a..3df18b81e2 100644 --- a/apiserver/paasng/paasng/infras/bkmonitorv3/params.py +++ b/apiserver/paasng/paasng/infras/bkmonitorv3/params.py @@ -36,18 +36,18 @@ class QueryAlertsParams: """ 查询告警的参数 - :param app_code: 应用 code :param start_time: 发生时间. datetime 类型, 其对应的字符串格式 '%Y-%m-%d %H:%M:%S' :param end_time: 结束时间. datetime 类型, 其对应的字符串格式 '%Y-%m-%d %H:%M:%S' + :param bk_biz_ids: 监控空间资源 id 列表 :param environment: 应用部署环境. 可选 :param alert_code: 支持的告警 code, 如 high_cpu_usage. 可选 :param status: 告警状态 (ABNORMAL: 表示未恢复, CLOSED: 已关闭, RECOVERED: 已恢复). 可选 :param keyword: 告警名称包含的关键字. 可选 """ - app_code: str start_time: datetime end_time: datetime + bk_biz_ids: List[str] environment: Optional[str] = None alert_code: Optional[str] = None status: Optional[str] = None @@ -58,7 +58,8 @@ def to_dict(self) -> Dict: d = { "start_time": int(self.start_time.timestamp()), "end_time": int(self.end_time.timestamp()), - "bk_biz_ids": [int(get_bk_biz_id(self.app_code))], + # 监控那边问题,需要转int + "bk_biz_ids": [int(id) for id in self.bk_biz_ids], "page": 1, "page_size": 500, # 按照 ID 降序 @@ -95,6 +96,42 @@ def _build_valid_args(self, *args) -> Optional[str]: return None return " AND ".join(valid_args) + @classmethod + def create_by_app_codes( + cls, + start_time: datetime, + end_time: datetime, + app_codes: List[str], + environment: Optional[str] = None, + alert_code: Optional[str] = None, + status: Optional[str] = None, + keyword: Optional[str] = None, + ) -> "QueryAlertsParams": + """ + 通过 app_codes 创建 QueryAlertsParams 实例 + + :param start_time: 开始时间 + :param end_time: 结束时间 + :param app_codes: 应用代码列表 + :param environment: 应用部署环境. 可选 + :param alert_code: 告警代码. 可选 + :param status: 告警状态. 可选 + :param keyword: 关键字. 可选 + :return: QueryAlertsParams 实例 + """ + # 获取 app code 对应的监控 biz id. 如果没有对应的 BKMonitorSpace,会被忽略 + monitor_spaces = BKMonitorSpace.objects.filter(application__code__in=app_codes) + bk_biz_ids = [space.iam_resource_id for space in monitor_spaces] + return cls( + start_time=start_time, + end_time=end_time, + bk_biz_ids=bk_biz_ids, + environment=environment, + alert_code=alert_code, + status=status, + keyword=keyword, + ) + @define(kw_only=True) class QueryAlarmStrategiesParams: diff --git a/apiserver/paasng/paasng/misc/monitoring/monitor/serializers.py b/apiserver/paasng/paasng/misc/monitoring/monitor/serializers.py index c1a1d983e4..042f5a16d5 100644 --- a/apiserver/paasng/paasng/misc/monitoring/monitor/serializers.py +++ b/apiserver/paasng/paasng/misc/monitoring/monitor/serializers.py @@ -24,6 +24,7 @@ from paasng.infras.bkmonitorv3.params import QueryAlarmStrategiesParams, QueryAlertsParams from paasng.misc.monitoring.monitor.alert_rules.config.constants import RUN_ENVS +from paasng.platform.applications.serializers import ApplicationWithLogoMinimalSLZ from paasng.platform.engine.constants import AppEnvName from paasng.utils.serializers import HumanizeTimestampField @@ -71,7 +72,8 @@ class ListAlertsSLZ(serializers.Serializer): def to_internal_value(self, data) -> QueryAlertsParams: data = super().to_internal_value(data) - return QueryAlertsParams(app_code=self.context["app_code"], **data) + params = QueryAlertsParams.create_by_app_codes(**data, app_codes=self.context.get("app_codes", [])) + return params def validate(self, data: QueryAlertsParams): if data.start_time > data.end_time: @@ -121,6 +123,19 @@ def get_env(self, instance) -> Optional[str]: return None +class AlertListByUserSLZ(serializers.Serializer): + application = ApplicationWithLogoMinimalSLZ(help_text="应用基础信息", read_only=True) + alerts = serializers.ListSerializer(help_text="应用告警", child=AlertSLZ()) + count = serializers.SerializerMethodField(help_text="应用告警数") + slow_query_count = serializers.SerializerMethodField(help_text="应用慢查询数") + + def get_count(self, obj): + return len(obj.get("alerts") or []) + + def get_slow_query_count(self, obj): + return sum(1 for alert in (obj.get("alerts") or []) if "慢查询" in alert.get("alert_name", "")) + + class ListAlarmStrategiesSLZ(serializers.Serializer): alert_code = serializers.CharField(required=False) environment = serializers.ChoiceField(choices=AppEnvName.get_choices(), required=False) diff --git a/apiserver/paasng/paasng/misc/monitoring/monitor/urls.py b/apiserver/paasng/paasng/misc/monitoring/monitor/urls.py index 728ffc1878..09a6e6b97b 100644 --- a/apiserver/paasng/paasng/misc/monitoring/monitor/urls.py +++ b/apiserver/paasng/paasng/misc/monitoring/monitor/urls.py @@ -50,6 +50,7 @@ ), path("api/monitor/supported_alert_rules/", views.AlertRulesView.as_view({"get": "list_supported_alert_rules"})), path("api/monitor/applications//alerts/", views.ListAlertsView.as_view({"post": "list"})), + path("api/monitor/user/alerts/", views.ListAlertsView.as_view({"post": "list_alerts_by_user"})), path( "api/monitor/applications//alarm_strategies/", views.ListAlarmStrategiesView.as_view({"post": "list"}), diff --git a/apiserver/paasng/paasng/misc/monitoring/monitor/views.py b/apiserver/paasng/paasng/misc/monitoring/monitor/views.py index 77b9c439c1..aa96e8d59c 100644 --- a/apiserver/paasng/paasng/misc/monitoring/monitor/views.py +++ b/apiserver/paasng/paasng/misc/monitoring/monitor/views.py @@ -15,6 +15,7 @@ # We undertake not to change the open source license (MIT license) applicable # to the current version of the project delivered to anyone in the future. +from collections import defaultdict from typing import Text from django.conf import settings @@ -31,6 +32,7 @@ from paasng.infras.bkmonitorv3.client import make_bk_monitor_client from paasng.infras.bkmonitorv3.exceptions import BkMonitorGatewayServiceError, BkMonitorSpaceDoesNotExist from paasng.infras.bkmonitorv3.models import BKMonitorSpace +from paasng.infras.bkmonitorv3.params import QueryAlertsParams from paasng.infras.iam.permissions.resources.application import AppAction from paasng.misc.monitoring.monitor.alert_rules.ascode.exceptions import AsCodeAPIError from paasng.misc.monitoring.monitor.alert_rules.config.constants import DEFAULT_RULE_CONFIGS @@ -55,6 +57,7 @@ ) from .serializers import ( AlarmStrategySLZ, + AlertListByUserSLZ, AlertRuleSLZ, AlertSLZ, ListAlarmStrategiesSLZ, @@ -257,20 +260,63 @@ class ListAlertsView(ViewSet, ApplicationCodeInPathMixin): @swagger_auto_schema(request_body=ListAlertsSLZ, responses={200: AlertSLZ(many=True)}) def list(self, request, code): """查询告警""" - serializer = ListAlertsSLZ(data=request.data, context={"app_code": code}) + serializer = ListAlertsSLZ(data=request.data, context={"app_codes": [code]}) serializer.is_valid(raise_exception=True) + query_params: QueryAlertsParams = serializer.validated_data + if not query_params.bk_biz_ids: + # 应用的 BkMonitorSpace 不存在(应用未部署或配置监控)时,返回空列表 + return Response([]) try: - alerts = make_bk_monitor_client().query_alerts(serializer.validated_data) - except BkMonitorSpaceDoesNotExist: - # BkMonitorSpace 不存在(应用未部署)时,返回空列表 - return Response([]) + alerts = make_bk_monitor_client().query_alerts(query_params) except BkMonitorGatewayServiceError as e: raise error_codes.QUERY_ALERTS_FAILED.f(str(e)) serializer = AlertSLZ(alerts, many=True) return Response(serializer.data) + @swagger_auto_schema(request_body=ListAlertsSLZ, responses={200: AlertListByUserSLZ()}) + def list_alerts_by_user(self, request): + """查询用户各应用告警及数量""" + app_codes = UserApplicationFilter(request.user).filter().values_list("code", flat=True) + if not app_codes: + return Response([]) + + serializer = ListAlertsSLZ(data=request.data, context={"app_codes": app_codes}) + serializer.is_valid(raise_exception=True) + query_params: QueryAlertsParams = serializer.validated_data + if not query_params.bk_biz_ids: + # 应用的 BkMonitorSpace 不存在(应用未部署或配置监控)时,返回空列表 + return Response([]) + + # 查询告警 + bk_monitor_client = make_bk_monitor_client() + try: + alerts = bk_monitor_client.query_alerts(query_params) + except BkMonitorGatewayServiceError as e: + raise error_codes.QUERY_ALERTS_FAILED.f(str(e)) + + if not alerts: + return Response([]) + + # 告警按 bk_biz_id 归类 + biz_grouped_alerts = defaultdict(list) + for alert in alerts: + bk_biz_id = str(alert["bk_biz_id"]) + biz_grouped_alerts[bk_biz_id].append(alert) + + # 查询应用的监控空间, 生成 bk_biz_id 对应 application 的 dict + monitor_spaces = bk_monitor_client.query_space_biz_id(app_codes=app_codes) + bizid_app_map = {space["bk_biz_id"]: space["application"] for space in monitor_spaces} + + # 告警按应用归类 + app_alerts = [ + {"application": bizid_app_map[bizid], "alerts": alerts} for bizid, alerts in biz_grouped_alerts.items() + ] + + serializer = AlertListByUserSLZ(app_alerts, many=True) + return Response(serializer.data) + class ListAlarmStrategiesView(ViewSet, ApplicationCodeInPathMixin): permission_classes = [IsAuthenticated, application_perm_class(AppAction.VIEW_BASIC_INFO)] diff --git a/apiserver/paasng/paasng/platform/applications/serializers/__init__.py b/apiserver/paasng/paasng/platform/applications/serializers/__init__.py index 71fbbab2fe..f482fd6af1 100644 --- a/apiserver/paasng/paasng/platform/applications/serializers/__init__.py +++ b/apiserver/paasng/paasng/platform/applications/serializers/__init__.py @@ -28,6 +28,7 @@ ApplicationSLZ, ApplicationSLZ4Record, ApplicationWithDeployInfoSLZ, + ApplicationWithLogoMinimalSLZ, ApplicationWithMarketMinimalSLZ, ApplicationWithMarketSLZ, ApplicationWithMarkMinimalSLZ, @@ -60,6 +61,7 @@ "ApplicationListMinimalSLZ", "ApplicationLogoSLZ", "ApplicationMarkedSLZ", + "ApplicationWithLogoMinimalSLZ", "ApplicationMinimalSLZ", "ApplicationRelationSLZ", "ApplicationSLZ", diff --git a/apiserver/paasng/paasng/platform/applications/serializers/app.py b/apiserver/paasng/paasng/platform/applications/serializers/app.py index d6fdd6631d..e1f2753108 100644 --- a/apiserver/paasng/paasng/platform/applications/serializers/app.py +++ b/apiserver/paasng/paasng/platform/applications/serializers/app.py @@ -334,6 +334,17 @@ class Meta: fields = ["id", "type", "code", "name", "logo_url", "config_info"] +class ApplicationWithLogoMinimalSLZ(serializers.ModelSerializer): + """用于带Logo URL的简化应用列表""" + + name = TranslatedCharField() + logo_url = serializers.CharField(read_only=True, source="get_logo_url", help_text="应用的Logo地址") + + class Meta: + model = Application + fields = ["id", "type", "code", "name", "logo_url"] + + class MarketAppMinimalSLZ(serializers.Serializer): name = serializers.CharField() diff --git a/apiserver/paasng/tests/api/test_monitor.py b/apiserver/paasng/tests/api/test_monitor.py index 6efb57b36f..8da248d526 100644 --- a/apiserver/paasng/tests/api/test_monitor.py +++ b/apiserver/paasng/tests/api/test_monitor.py @@ -32,16 +32,16 @@ def bk_monitor_space(bk_app): return BKMonitorSpace.objects.create( application=bk_app, - id=40000, - space_type_id=SpaceType.SAAS, + id=4000000, + space_type_id=SpaceType.BKCC, space_id="100", space_name="蓝鲸应用-test", extra_info={"test": "test"}, ) +@mock.patch("paasng.infras.bkmonitorv3.client.BkMonitorClient", new=StubBKMonitorClient) class TestListAlertsView: - @mock.patch("paasng.infras.bkmonitorv3.client.BkMonitorClient", new=StubBKMonitorClient) def test_list_alerts(self, api_client, bk_app, bk_monitor_space): resp = api_client.post( f"/api/monitor/applications/{bk_app.code}/alerts/", @@ -57,6 +57,22 @@ def test_list_alerts(self, api_client, bk_app, bk_monitor_space): assert resp.data[0]["env"] in ["stag", "prod"] assert len(resp.data[0]["receivers"]) == 2 + def test_list_alerts_by_user(self, api_client, bk_app, bk_monitor_space): + resp = api_client.post( + "/api/monitor/user/alerts/", + data={ + "start_time": (datetime.now() - timedelta(minutes=random.randint(1, 30))).strftime( + "%Y-%m-%d %H:%M:%S" + ), + "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + }, + ) + assert len(resp.data) == 1 + assert resp.data[0]["count"] == 3 + assert resp.data[0]["slow_query_count"] == 3 + assert len(resp.data[0]["alerts"]) == 3 + assert resp.data[0]["application"]["id"] == "1" + class TestAlarmStrategiesView: @mock.patch("paasng.infras.bkmonitorv3.client.BkMonitorClient", new=StubBKMonitorClient) diff --git a/apiserver/paasng/tests/paasng/misc/monitoring/test_alert.py b/apiserver/paasng/tests/paasng/misc/monitoring/test_alert.py index 72e0da784b..e3f4fa8d3d 100644 --- a/apiserver/paasng/tests/paasng/misc/monitoring/test_alert.py +++ b/apiserver/paasng/tests/paasng/misc/monitoring/test_alert.py @@ -52,7 +52,10 @@ def clear_filelock(): AppQueryAlertsParams = partial( - QueryAlertsParams, app_code=FAKE_APP_CODE, start_time=datetime.now(), end_time=datetime.now() + QueryAlertsParams.create_by_app_codes, + app_codes=[FAKE_APP_CODE], + start_time=datetime.now(), + end_time=datetime.now(), ) AppQueryAlarmStrategiesParams = partial(QueryAlarmStrategiesParams, app_code=FAKE_APP_CODE) @@ -75,33 +78,34 @@ def bk_monitor_space(): class TestQueryAlertsParams: + @pytest.mark.django_db() @pytest.mark.parametrize( - ("query_params", "expected_query_string"), + ("create_query_params", "expected_query_string"), [ ( - AppQueryAlertsParams(), + lambda: AppQueryAlertsParams(), None, ), ( - AppQueryAlertsParams(environment="stag"), + lambda: AppQueryAlertsParams(environment="stag"), "labels:(stag)", ), ( - AppQueryAlertsParams(environment="stag", alert_code="high_cpu_usage"), + lambda: AppQueryAlertsParams(environment="stag", alert_code="high_cpu_usage"), "labels:(stag AND high_cpu_usage)", ), ( - AppQueryAlertsParams(alert_code="high_cpu_usage"), + lambda: AppQueryAlertsParams(alert_code="high_cpu_usage"), "labels:(high_cpu_usage)", ), ( - AppQueryAlertsParams(keyword=SEARCH_KEYWORD), + lambda: AppQueryAlertsParams(keyword=SEARCH_KEYWORD), f"alert_name:({SEARCH_KEYWORD} OR *{SEARCH_KEYWORD}*)", ), ], ) - def test_to_dict(self, query_params, expected_query_string, bk_monitor_space): - result = query_params.to_dict() + def test_to_dict(self, create_query_params, expected_query_string, bk_monitor_space): + result = create_query_params().to_dict() assert result["bk_biz_ids"] == [int(bk_monitor_space.iam_resource_id)] if query_string := result.get("query_string"): assert query_string == expected_query_string diff --git a/apiserver/paasng/tests/utils/mocks/bkmonitor.py b/apiserver/paasng/tests/utils/mocks/bkmonitor.py index e9d8116655..24ab334d6b 100644 --- a/apiserver/paasng/tests/utils/mocks/bkmonitor.py +++ b/apiserver/paasng/tests/utils/mocks/bkmonitor.py @@ -17,9 +17,11 @@ import random from typing import Dict, List +from unittest.mock import Mock from paasng.infras.bkmonitorv3.client import BkMonitorClient from paasng.infras.bkmonitorv3.params import QueryAlarmStrategiesParams, QueryAlertsParams +from paasng.platform.applications.models import Application from tests.utils.helpers import generate_random_string @@ -27,8 +29,8 @@ def get_fake_alerts(start_time: int, end_time: int) -> List: alerts = [ { "id": generate_random_string(6), - "bk_biz_id": random.randint(-5000000, -4000000), - "alert_name": generate_random_string(6), + "bk_biz_id": -4000000, + "alert_name": generate_random_string(6) + "慢查询", "status": random.choice(["ABNORMAL", "CLOSED", "RECOVERED"]), "description": generate_random_string(), "begin_time": start_time, @@ -81,6 +83,22 @@ def get_fake_alarm_strategies() -> Dict: } +def get_fake_space_biz_id(app_codes: List[str]) -> List[Dict]: + mock_application = Mock(spec=Application) + mock_application.id = 1 + mock_application.type = "default" + mock_application.code = "testapp" + mock_application.name = "Test Application" + mock_application.get_logo_url.return_value = "http://logo.jpg" + + return [ + { + "application": mock_application, + "bk_biz_id": "-4000000", + } + ] + + class StubBKMonitorClient(BkMonitorClient): """蓝鲸监控提供的API,仅供单元测试使用""" @@ -88,6 +106,9 @@ def query_alerts(self, query_params: QueryAlertsParams) -> List: query_data = query_params.to_dict() return get_fake_alerts(query_data["start_time"], query_data["end_time"]) + def query_space_biz_id(self, app_codes: List) -> List: + return get_fake_space_biz_id(app_codes) + def query_alarm_strategies(self, query_params: QueryAlarmStrategiesParams) -> Dict: query_params.to_dict() return get_fake_alarm_strategies()