Skip to content
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

feat: get user alerts and alerts count #1626

Merged
merged 20 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions apiserver/paasng/paasng/infras/bkmonitorv3/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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:
Expand Down Expand Up @@ -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:
"""查询告警策略

Expand All @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions apiserver/paasng/paasng/infras/bkmonitorv3/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ class QueryAlertsParams:
:param keyword: 告警名称包含的关键字. 可选
"""

app_code: str
start_time: datetime
end_time: datetime
app_code: Optional[str] = None
xidons marked this conversation as resolved.
Show resolved Hide resolved
bk_biz_ids: Optional[List[str]] = None
environment: Optional[str] = None
alert_code: Optional[str] = None
status: Optional[str] = None
Expand All @@ -58,13 +59,18 @@ 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))],
"bk_biz_ids": [],
xidons marked this conversation as resolved.
Show resolved Hide resolved
"page": 1,
"page_size": 500,
# 按照 ID 降序
"ordering": ["-id"],
}

if self.bk_biz_ids:
d["bk_biz_ids"] = [int(id) for id in self.bk_biz_ids]
elif self.app_code:
d["bk_biz_ids"] = [int(get_bk_biz_id(self.app_code))]

if self.status:
d["status"] = [self.status]

Expand Down
22 changes: 21 additions & 1 deletion apiserver/paasng/paasng/misc/monitoring/monitor/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ApplicationSLZWithLogo
from paasng.platform.engine.constants import AppEnvName
from paasng.utils.serializers import HumanizeTimestampField

Expand Down Expand Up @@ -71,12 +72,20 @@ 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(**data)
if self.context.get("app_code"):
params.app_code = self.context.get("app_code")
if self.context.get("bk_biz_ids"):
params.bk_biz_ids = self.context.get("bk_biz_ids")
return params

def validate(self, data: QueryAlertsParams):
if data.start_time > data.end_time:
raise serializers.ValidationError("end_time must be greater than start_time")

if not data.app_code and not data.bk_biz_ids:
raise serializers.ValidationError("at least one of app_code or bk_biz_ids is required")

return data


Expand Down Expand Up @@ -121,6 +130,17 @@ def get_env(self, instance) -> Optional[str]:
return None


class AlertListByUserSLZ(serializers.Serializer):
xidons marked this conversation as resolved.
Show resolved Hide resolved
application = ApplicationSLZWithLogo(read_only=True)
count = serializers.IntegerField(help_text="应用告警数")
alerts = serializers.ListSerializer(help_text="应用告警", child=AlertSLZ())


class AlertListByUserRespSLZ(serializers.Serializer):
count = serializers.IntegerField(help_text="用户告警总数")
alerts = serializers.ListSerializer(help_text="各个应用的告警", child=AlertListByUserSLZ())


class ListAlarmStrategiesSLZ(serializers.Serializer):
alert_code = serializers.CharField(required=False)
environment = serializers.ChoiceField(choices=AppEnvName.get_choices(), required=False)
Expand Down
1 change: 1 addition & 0 deletions apiserver/paasng/paasng/misc/monitoring/monitor/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
),
path("api/monitor/supported_alert_rules/", views.AlertRulesView.as_view({"get": "list_supported_alert_rules"})),
path("api/monitor/applications/<slug:code>/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/<slug:code>/alarm_strategies/",
views.ListAlarmStrategiesView.as_view({"post": "list"}),
Expand Down
48 changes: 48 additions & 0 deletions apiserver/paasng/paasng/misc/monitoring/monitor/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.db.models import Q
Expand Down Expand Up @@ -53,6 +54,7 @@
)
from .serializers import (
AlarmStrategySLZ,
AlertListByUserRespSLZ,
AlertRuleSLZ,
AlertSLZ,
ListAlarmStrategiesSLZ,
Expand Down Expand Up @@ -269,6 +271,52 @@ def list(self, request, code):
serializer = AlertSLZ(alerts, many=True)
return Response(serializer.data)

@swagger_auto_schema(request_body=ListAlertsSLZ, responses={200: AlertListByUserRespSLZ()})
def list_alerts_by_user(self, request):
"""查询用户各应用告警及数量"""
app_codes = UserApplicationFilter(request.user).filter().values_list("code", flat=True)
if not app_codes:
return Response(AlertListByUserRespSLZ({"count": 0, "alerts": None}).data)

bk_monitor_client = make_bk_monitor_client()
# 查询应用的监控空间
monitor_spaces = bk_monitor_client.query_space_biz_id(app_codes=app_codes)
bk_biz_ids = [space["bk_biz_id"] for space in monitor_spaces]
if not bk_biz_ids:
return Response(AlertListByUserRespSLZ({"count": 0, "alerts": None}).data)
bizid_app_map = {space["bk_biz_id"]: space["application"] for space in monitor_spaces}

serializer = ListAlertsSLZ(data=request.data, context={"bk_biz_ids": bk_biz_ids})
serializer.is_valid(raise_exception=True)

try:
alerts = bk_monitor_client.query_alerts(serializer.validated_data)
except BkMonitorSpaceDoesNotExist:
# 用户所有应用 BkMonitorSpace 不存在(应用未部署或配置监控)时,返回空列表
return Response(AlertListByUserRespSLZ({"count": 0, "alerts": None}).data)
except BkMonitorGatewayServiceError as e:
raise error_codes.QUERY_ALERTS_FAILED.f(str(e))
if not alerts:
return Response(AlertListByUserRespSLZ({"count": 0, "alerts": None}).data)
xidons marked this conversation as resolved.
Show resolved Hide resolved

biz_grouped_alerts = defaultdict(list)
for alert in alerts:
bk_biz_id = alert["bk_biz_id"]
biz_grouped_alerts[bk_biz_id].append(alert)

app_grouped_alerts = []
for bizid, app_alerts in biz_grouped_alerts.items():
app_grouped_alerts.append(
{
"application": bizid_app_map[bizid],
"count": len(app_alerts),
"alerts": app_alerts,
}
)

serializer = AlertListByUserRespSLZ({"count": len(alerts), "alerts": app_grouped_alerts})
return Response(serializer.data)


class ListAlarmStrategiesView(ViewSet, ApplicationCodeInPathMixin):
permission_classes = [IsAuthenticated, application_perm_class(AppAction.VIEW_BASIC_INFO)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
ApplicationRelationSLZ,
ApplicationSLZ,
ApplicationSLZ4Record,
ApplicationSLZWithLogo,
ApplicationWithDeployInfoSLZ,
ApplicationWithMarketMinimalSLZ,
ApplicationWithMarketSLZ,
Expand Down Expand Up @@ -60,6 +61,7 @@
"ApplicationListMinimalSLZ",
"ApplicationLogoSLZ",
"ApplicationMarkedSLZ",
"ApplicationSLZWithLogo",
"ApplicationMinimalSLZ",
"ApplicationRelationSLZ",
"ApplicationSLZ",
Expand Down
11 changes: 11 additions & 0 deletions apiserver/paasng/paasng/platform/applications/serializers/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,17 @@ class Meta:
fields = ["id", "type", "code", "name", "logo_url", "config_info"]


class ApplicationSLZWithLogo(serializers.ModelSerializer):
xidons marked this conversation as resolved.
Show resolved Hide resolved
"""用于带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()

Expand Down
17 changes: 16 additions & 1 deletion apiserver/paasng/tests/api/test_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ def bk_monitor_space(bk_app):
)


@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/",
Expand All @@ -57,6 +57,21 @@ 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 resp.data["count"] == 3
assert len(resp.data["alerts"]) == 1
assert resp.data["alerts"][0]["count"] == 3
assert resp.data["alerts"][0]["application"]["id"] == "1"


class TestAlarmStrategiesView:
@mock.patch("paasng.infras.bkmonitorv3.client.BkMonitorClient", new=StubBKMonitorClient)
Expand Down
34 changes: 34 additions & 0 deletions apiserver/paasng/tests/paasng/misc/monitoring/test_alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# to the current version of the project delivered to anyone in the future.

import atexit
import random
from datetime import datetime
from functools import partial
from pathlib import Path
Expand Down Expand Up @@ -106,6 +107,39 @@ def test_to_dict(self, query_params, expected_query_string, bk_monitor_space):
if query_string := result.get("query_string"):
assert query_string == expected_query_string

@pytest.mark.parametrize(
("query_params", "expected_query_string"),
[
(
AppQueryAlertsParams(bk_biz_ids=[random.randint(-5000000, -4000000)]),
None,
),
(
AppQueryAlertsParams(environment="stag", bk_biz_ids=[random.randint(-5000000, -4000000)]),
"labels:(stag)",
),
(
AppQueryAlertsParams(
environment="stag", alert_code="high_cpu_usage", bk_biz_ids=[random.randint(-5000000, -4000000)]
),
"labels:(stag AND high_cpu_usage)",
),
(
AppQueryAlertsParams(alert_code="high_cpu_usage", bk_biz_ids=[random.randint(-5000000, -4000000)]),
"labels:(high_cpu_usage)",
),
(
AppQueryAlertsParams(keyword=SEARCH_KEYWORD, bk_biz_ids=[random.randint(-5000000, -4000000)]),
f"alert_name:({SEARCH_KEYWORD} OR *{SEARCH_KEYWORD}*)",
),
],
)
def test_to_dict_with_bk_biz_id(self, query_params, expected_query_string, bk_monitor_space):
result = query_params.to_dict()
assert result["bk_biz_ids"] == query_params.bk_biz_ids
if query_string := result.get("query_string"):
assert query_string == expected_query_string


class TestQueryAlarmStrategiesParams:
@pytest.mark.parametrize(
Expand Down
23 changes: 22 additions & 1 deletion apiserver/paasng/tests/utils/mocks/bkmonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@

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


def get_fake_alerts(start_time: int, end_time: int) -> List:
alerts = [
{
"id": generate_random_string(6),
"bk_biz_id": random.randint(-5000000, -4000000),
"bk_biz_id": -5000000,
"alert_name": generate_random_string(6),
"status": random.choice(["ABNORMAL", "CLOSED", "RECOVERED"]),
"description": generate_random_string(),
Expand Down Expand Up @@ -81,13 +83,32 @@ 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": -5000000,
}
]


class StubBKMonitorClient(BkMonitorClient):
"""蓝鲸监控提供的API,仅供单元测试使用"""

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()