Automated cherry pick of #23878: feat(monitor): 支持指定过去时间段返回报警资源最多的top5监控策略 (#23880)

* feat(monitor): 支持指定过去时间段返回报警资源最多的top5监控策略

* feat(monitor): 支持指定过去时间段返回报警数量最多的top5资源

* feat(monitor): 支持指定过去时间段某监控策略下各监控指标报警资源最多的top5资源

* feat(monitor): 支持获取过去时间段各项目下报警资源数量
This commit is contained in:
Zexi Li
2025-12-02 15:48:01 +08:00
committed by GitHub
parent 201a1e33f1
commit 0971950307
13 changed files with 672 additions and 14 deletions
+1
View File
@@ -25,4 +25,5 @@ func init() {
cmd.Show(new(options.AlertRecordShowOptions))
cmd.GetProperty(new(options.AlertRecordTotalOptions))
cmd.GetProperty(new(options.AlertRecordHistoryAlertOptions))
cmd.GetProperty(new(options.AlertRecordProjectAlertResourceCountOptions))
}
+24
View File
@@ -111,3 +111,27 @@ func (self AlertRecordHistoryAlertData) GetMetricTags() map[string]string {
type AlertRecordHistoryAlert struct {
Data []AlertRecordHistoryAlertData `json:"data"`
}
// ProjectAlertResourceCountData 报警资源统计数据(按 scope 分类)
type ProjectAlertResourceCountData struct {
Scope string `json:"scope"` // system/domain/project
DomainId string `json:"domain_id"` // 域ID(domain/project scope 时有效)
Domain string `json:"domain"` // 域名称(domain/project scope 时有效)
ProjectId string `json:"project_id"` // 项目ID(project scope 时有效)
Project string `json:"project"` // 项目名称(project scope 时有效)
ResCount int64 `json:"res_count"` // 报警资源数量
}
// ProjectAlertResourceCount 报警资源统计结果
type ProjectAlertResourceCount struct {
Data []ProjectAlertResourceCountData `json:"data"`
}
// ProjectAlertResourceCountInput 项目报警资源统计查询输入
type ProjectAlertResourceCountInput struct {
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
ResType string `json:"res_type"`
AlertId string `json:"alert_id"`
Scope string `json:"scope"`
}
+16
View File
@@ -14,6 +14,10 @@
package monitor
import (
time "time"
)
const (
ALERT_STATUS_READY = "ready"
ALERT_STATUS_DELETE = "start_delete"
@@ -113,6 +117,16 @@ type CommonAlertQuery struct {
Operator string `json:"operator"`
}
// TopQueryInput 用于 top 查询的通用时间段和 top 参数
type TopQueryInput struct {
// 查询时间段开始时间
StartTime time.Time `json:"start_time"`
// 查询时间段结束时间
EndTime time.Time `json:"end_time"`
// 返回 top N(默认 5)
Top *int `json:"top"`
}
type CommonAlertListInput struct {
AlertListInput
//V1AlertListInput
@@ -125,6 +139,8 @@ type CommonAlertListInput struct {
ResType []string `json:"res_type"`
UsedBy string `json:"used_by"`
Name string `json:"name"`
// Top 查询参数(用于统计报警资源最多的监控策略)
TopQueryInput
}
type CommonAlertUpdateInput struct {
+2
View File
@@ -45,6 +45,8 @@ type MonitorResourceListInput struct {
AlertStates []string `json:"alert_states"`
ResName string `json:"res_name"`
// Top 查询参数(用于统计报警数量最多的资源)
TopQueryInput
}
type MonitorResourceDetails struct {
@@ -38,6 +38,8 @@ type MonitorResourceJointListInput struct {
Level string `json:"level"`
// 查询所有状态
AllState bool `json:"all_state"`
// Top 查询参数(用于统计各监控指标报警资源最多的资源)
TopQueryInput
}
type MonitorResourceJointCreateInput struct {
@@ -77,6 +77,22 @@ func (o *AlertRecordHistoryAlertOptions) Property() string {
return "history-alert"
}
type AlertRecordProjectAlertResourceCountOptions struct {
StartTime time.Time `help:"start time (RFC3339 format)" json:"start_time" default:"2025-01-01 00:00:00"`
EndTime time.Time `help:"end time (RFC3339 format)" json:"end_time" default:"2025-01-01 00:00:00"`
ResType string `help:"resource type" json:"res_type"`
AlertId string `help:"alert id" json:"alert_id"`
Scope string `help:"scope" json:"scope" choices:"system|domain|project"`
}
func (o *AlertRecordProjectAlertResourceCountOptions) Params() (jsonutils.JSONObject, error) {
return options.StructToParams(o)
}
func (o *AlertRecordProjectAlertResourceCountOptions) Property() string {
return "project-alert-resource-count"
}
type AlertRecordShieldListOptions struct {
options.BaseListOptions
+8 -3
View File
@@ -15,6 +15,8 @@
package monitor
import (
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
@@ -27,9 +29,12 @@ import (
type CommonAlertListOptions struct {
options.BaseListOptions
// 报警类型
AlertType string `help:"common alert type" choices:"normal|system"`
Level string `help:"common alert notify level" choices:"normal|important|fatal"`
MonitorResourceId []string `help:"monitor resource id"`
AlertType string `help:"common alert type" choices:"normal|system"`
Level string `help:"common alert notify level" choices:"normal|important|fatal"`
MonitorResourceId []string `help:"monitor resource id"`
StartTime time.Time `help:"start time, format: 2025-01-01 00:00:00" json:"start_time"`
EndTime time.Time `help:"end time, format: 2025-01-01 00:00:00" json:"end_time"`
Top int `help:"top" json:"top"`
}
func (o *CommonAlertListOptions) Params() (jsonutils.JSONObject, error) {
@@ -15,6 +15,8 @@
package monitor
import (
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient/options"
@@ -33,10 +35,13 @@ func (o *MonitorResourceJointAlertOptions) Property() string {
type MonitorResourceListOptions struct {
options.BaseListOptions
ResType string `help:"filter by resource type" json:"res_type"`
ResId []string `help:"filter by resource id" json:"res_id"`
ResName string `help:"filter by resource name" json:"res_name"`
AlertStates []string `help:"filter by alert state" json:"alert_states"`
ResType string `help:"filter by resource type" json:"res_type"`
ResId []string `help:"filter by resource id" json:"res_id"`
ResName string `help:"filter by resource name" json:"res_name"`
AlertStates []string `help:"filter by alert state" json:"alert_states"`
StartTime time.Time `help:"start time for top query, format: 2025-01-01 00:00:00" json:"start_time"`
EndTime time.Time `help:"end time for top query, format: 2025-01-01 00:00:00" json:"end_time"`
Top int `help:"return top N resources by alert count (default: 5)" json:"top"`
}
func (o *MonitorResourceListOptions) Params() (jsonutils.JSONObject, error) {
@@ -15,6 +15,8 @@
package monitor
import (
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient/options"
@@ -22,12 +24,15 @@ import (
type MonitorResourceAlertListOptions struct {
options.BaseListOptions
MonitorResourceId string `help:"ID of monitor resource" json:"monitor_resource_id"`
AlertId string `help:"ID of alert" json:"alert_id"`
Alerting bool `help:"search alerting resource" json:"alerting"`
SendState string `json:"send_state"`
AllState bool `help:"Show all state" json:"all_state"`
Ip string `help:"IP address" json:"ip"`
MonitorResourceId string `help:"ID of monitor resource" json:"monitor_resource_id"`
AlertId string `help:"ID of alert" json:"alert_id"`
Alerting bool `help:"search alerting resource" json:"alerting"`
SendState string `json:"send_state"`
AllState bool `help:"Show all state" json:"all_state"`
Ip string `help:"IP address" json:"ip"`
StartTime time.Time `help:"start time for top query, format: 2025-01-01 00:00:00" json:"start_time"`
EndTime time.Time `help:"end time for top query, format: 2025-01-01 00:00:00" json:"end_time"`
Top int `help:"return top N resources by alert count (default: 5)" json:"top"`
}
func (o *MonitorResourceAlertListOptions) GetMasterOpt() string {
+170 -1
View File
@@ -330,10 +330,16 @@ func (record *SAlertRecord) CustomizeCreate(
query jsonutils.JSONObject,
data jsonutils.JSONObject,
) error {
err := record.SMonitorScopedResource.CustomizeCreate(ctx, userCred, ownerId, query, data)
/*err := record.SMonitorScopedResource.CustomizeCreate(ctx, userCred, ownerId, query, data)
if err != nil {
return err
}*/
alert, err := AlertManager.GetAlert(record.AlertId)
if err != nil {
return errors.Wrapf(err, "GetAlert %s", record.AlertId)
}
record.DomainId = alert.GetDomainId()
record.ProjectId = alert.GetProjectId()
obj, err := db.NewModelObject(AlertRecordManager)
if err != nil {
return errors.Wrapf(err, "NewModelObject %s", AlertRecordManager.Keyword())
@@ -532,3 +538,166 @@ func (manager *SAlertRecordManager) GetPropertyHistoryAlert(
}
return result, nil
}
// GetPropertyProjectAlertResourceCount 获取指定时间段内各项目下的报警资源数量
func (manager *SAlertRecordManager) GetPropertyProjectAlertResourceCount(
ctx context.Context,
userCred mcclient.TokenCredential,
input monitor.ProjectAlertResourceCountInput,
) (*monitor.ProjectAlertResourceCount, error) {
// 验证时间段参数
if input.StartTime.IsZero() || input.EndTime.IsZero() {
return nil, httperrors.NewInputParameterError("start_time and end_time must be specified")
}
if input.StartTime.After(input.EndTime) {
return nil, httperrors.NewInputParameterError("start_time must be before end_time")
}
// 构建查询
q := manager.Query()
q = q.GE("created_at", input.StartTime).LE("created_at", input.EndTime)
q = q.IsNotEmpty("res_ids")
// 应用权限过滤
scope := rbacscope.ScopeSystem
if input.Scope != "" {
scope = rbacscope.TRbacScope(input.Scope)
}
q = manager.SMonitorScopedResourceManager.FilterByOwner(ctx, q, manager, userCred, userCred, scope)
// 如果指定了 ResType,添加过滤条件
if input.ResType != "" {
q = q.Equals("res_type", input.ResType)
}
// 如果指定了 AlertId,添加过滤条件
if input.AlertId != "" {
q = q.Equals("alert_id", input.AlertId)
}
// 执行查询获取所有记录
alerts := make([]SAlertRecord, 0)
err := q.All(&alerts)
if err != nil {
return nil, errors.Wrap(err, "query alert records")
}
// 按 scope 分组统计唯一资源数量
// systemResourceSet = set of resource IDs (system scope)
// domainResourceSet[domainId] = set of resource IDs (domain scope)
// projectResourceSet[domainId][projectId] = set of resource IDs (project scope)
systemResourceSet := sets.NewString()
domainResourceSet := make(map[string]sets.String)
projectResourceSet := make(map[string]map[string]sets.String)
domainIds := sets.NewString()
projectIds := sets.NewString()
for _, alert := range alerts {
if len(alert.ResIds) == 0 {
continue
}
domainId := alert.DomainId
projectId := alert.ProjectId
// 解析 res_ids(逗号分隔)
resIds := strings.Split(alert.ResIds, ",")
for _, resId := range resIds {
resId = strings.TrimSpace(resId)
if len(resId) == 0 {
continue
}
// 根据 domainId 和 projectId 判断 scope
if domainId == "" && projectId == "" {
// system scope
systemResourceSet.Insert(resId)
} else if domainId != "" && projectId == "" {
// domain scope
domainIds.Insert(domainId)
if domainResourceSet[domainId] == nil {
domainResourceSet[domainId] = sets.NewString()
}
domainResourceSet[domainId].Insert(resId)
} else if domainId != "" && projectId != "" {
// project scope
domainIds.Insert(domainId)
projectIds.Insert(projectId)
if projectResourceSet[domainId] == nil {
projectResourceSet[domainId] = make(map[string]sets.String)
}
if projectResourceSet[domainId][projectId] == nil {
projectResourceSet[domainId][projectId] = sets.NewString()
}
projectResourceSet[domainId][projectId].Insert(resId)
}
}
}
// 获取项目和域的名称
domainMap := make(map[string]string)
if domainIds.Len() > 0 {
domains := []db.STenant{}
err = db.TenantCacheManager.GetDomainQuery().In("id", domainIds.List()).All(&domains)
if err != nil {
return nil, errors.Wrap(err, "GetDomainQuery.In.All")
}
for _, domain := range domains {
domainMap[domain.Id] = domain.Name
}
}
projectMap := make(map[string]string)
if projectIds.Len() > 0 {
projects := []db.STenant{}
err = db.TenantCacheManager.GetTenantQuery().In("id", projectIds.List()).All(&projects)
if err != nil {
return nil, errors.Wrap(err, "GetTenantQuery.In.All")
}
for _, project := range projects {
projectMap[project.Id] = project.Name
}
}
// 构建返回结果
result := &monitor.ProjectAlertResourceCount{
Data: make([]monitor.ProjectAlertResourceCountData, 0),
}
// system scope
if systemResourceSet.Len() > 0 {
result.Data = append(result.Data, monitor.ProjectAlertResourceCountData{
Scope: string(rbacscope.ScopeSystem),
ResCount: int64(systemResourceSet.Len()),
})
}
// domain scope
for domainId, resourceSet := range domainResourceSet {
if resourceSet.Len() > 0 {
result.Data = append(result.Data, monitor.ProjectAlertResourceCountData{
Scope: string(rbacscope.ScopeDomain),
DomainId: domainId,
Domain: domainMap[domainId],
ResCount: int64(resourceSet.Len()),
})
}
}
// project scope
for domainId, projects := range projectResourceSet {
for projectId, resourceSet := range projects {
if resourceSet.Len() > 0 {
result.Data = append(result.Data, monitor.ProjectAlertResourceCountData{
Scope: string(rbacscope.ScopeProject),
DomainId: domainId,
Domain: domainMap[domainId],
ProjectId: projectId,
Project: projectMap[projectId],
ResCount: int64(resourceSet.Len()),
})
}
}
}
return result, nil
}
+105
View File
@@ -481,6 +481,11 @@ func (man *SCommonAlertManager) ListItemFilter(
userCred mcclient.TokenCredential,
query monitor.CommonAlertListInput,
) (*sqlchemy.SQuery, error) {
// 如果指定了时间段和 top 参数,执行特殊的 top 查询
if query.Top != nil {
return man.getTopAlertsByResourceCount(ctx, q, userCred, query)
}
q, err := man.SAlertManager.ListItemFilter(ctx, q, userCred, query.AlertListInput)
if err != nil {
return nil, err
@@ -508,6 +513,106 @@ func (man *SCommonAlertManager) FieldListFilter(q *sqlchemy.SQuery, input monito
}
}
// getTopAlertsByResourceCount 查询指定时间段内报警资源最多的 top N 监控策略
func (man *SCommonAlertManager) getTopAlertsByResourceCount(
ctx context.Context,
q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
query monitor.CommonAlertListInput,
) (*sqlchemy.SQuery, error) {
// 验证时间段和 top 参数
startTime, endTime, top, err := validateTopQueryInput(query.TopQueryInput)
if err != nil {
return nil, err
}
// 查询指定时间段内的 AlertRecord
recordQuery := AlertRecordManager.Query("alert_id", "res_ids")
recordQuery = recordQuery.GE("created_at", startTime).LE("created_at", endTime)
recordQuery = recordQuery.IsNotNull("res_type").IsNotEmpty("res_type")
recordQuery = recordQuery.IsNotEmpty("res_ids")
// 应用权限过滤
recordQuery, err = AlertRecordManager.SScopedResourceBaseManager.ListItemFilter(
ctx, recordQuery, userCred, query.ScopedResourceBaseListInput)
if err != nil {
return nil, errors.Wrap(err, "AlertRecordManager.ListItemFilter")
}
// 执行查询获取所有记录
type RecordRow struct {
AlertId string
ResIds string
}
rows := make([]RecordRow, 0)
err = recordQuery.All(&rows)
if err != nil {
return nil, errors.Wrap(err, "query alert records")
}
// 统计每个 alert_id 的唯一资源数量
alertResourceCount := make(map[string]sets.String)
for _, row := range rows {
if len(row.ResIds) == 0 {
continue
}
// 解析 res_ids(逗号分隔)
resIds := strings.Split(row.ResIds, ",")
if alertResourceCount[row.AlertId] == nil {
alertResourceCount[row.AlertId] = sets.NewString()
}
for _, resId := range resIds {
resId = strings.TrimSpace(resId)
if len(resId) > 0 {
alertResourceCount[row.AlertId].Insert(resId)
}
}
}
// 转换为切片并按资源数量排序
type AlertCount struct {
AlertId string
Count int
}
alertCounts := make([]AlertCount, 0, len(alertResourceCount))
for alertId, resSet := range alertResourceCount {
alertCounts = append(alertCounts, AlertCount{
AlertId: alertId,
Count: resSet.Len(),
})
}
// 按资源数量降序排序
for i := 0; i < len(alertCounts)-1; i++ {
for j := i + 1; j < len(alertCounts); j++ {
if alertCounts[i].Count < alertCounts[j].Count {
alertCounts[i], alertCounts[j] = alertCounts[j], alertCounts[i]
}
}
}
// 获取 top N 的 alert_id
topAlertIds := make([]string, 0, top)
for i := 0; i < top && i < len(alertCounts); i++ {
topAlertIds = append(topAlertIds, alertCounts[i].AlertId)
}
if len(topAlertIds) == 0 {
// 如果没有找到任何记录,返回空查询
return q.FilterByFalse(), nil
}
// 用 top alert_id 过滤 CommonAlert 查询
q, err = man.SAlertManager.ListItemFilter(ctx, q, userCred, query.AlertListInput)
if err != nil {
return nil, err
}
man.FieldListFilter(q, query)
q = q.In("id", topAlertIds)
return q, nil
}
func (manager *SCommonAlertManager) GetExportExtraKeys(ctx context.Context, keys stringutils2.SSortedStrings, rowMap map[string]string) *jsonutils.JSONDict {
res := manager.SResourceBaseManager.GetExportExtraKeys(ctx, keys, rowMap)
if keys.Contains("tenant") {
+135
View File
@@ -17,6 +17,7 @@ package models
import (
"context"
"reflect"
"strings"
"sync"
"time"
@@ -40,6 +41,23 @@ var (
MonitorResourceManager *SMonitorResourceManager
)
// validateTopQueryInput 验证 TopQueryInput 参数并返回解析后的值
func validateTopQueryInput(input monitor.TopQueryInput) (startTime time.Time, endTime time.Time, top int, err error) {
startTime = input.StartTime
endTime = input.EndTime
if startTime.IsZero() || endTime.IsZero() {
return time.Time{}, time.Time{}, 0, httperrors.NewInputParameterError("start_time and end_time must be specified")
}
if startTime.After(endTime) {
return time.Time{}, time.Time{}, 0, httperrors.NewInputParameterError("start_time must be before end_time")
}
top = *input.Top
if top <= 0 {
top = 5 // 默认返回 top 5
}
return startTime, endTime, top, nil
}
type IMonitorResourceCache interface {
Get(resId string) (jsonutils.JSONObject, bool)
}
@@ -190,6 +208,11 @@ func (manager *SMonitorResourceManager) ListItemFilter(
userCred mcclient.TokenCredential,
query monitor.MonitorResourceListInput,
) (*sqlchemy.SQuery, error) {
// 如果指定了时间段和 top 参数,执行特殊的 top 查询
if query.Top != nil {
return manager.getTopResourcesByAlertCount(ctx, q, userCred, query)
}
var err error
q, err = manager.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput)
if err != nil {
@@ -235,6 +258,118 @@ func (man *SMonitorResourceManager) OrderByExtraFields(
return q, nil
}
// getTopResourcesByAlertCount 查询指定时间段内报警数量最多的 top N 资源
func (man *SMonitorResourceManager) getTopResourcesByAlertCount(
ctx context.Context,
q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
query monitor.MonitorResourceListInput,
) (*sqlchemy.SQuery, error) {
// 验证时间段和 top 参数
startTime, endTime, top, err := validateTopQueryInput(query.TopQueryInput)
if err != nil {
return nil, err
}
// 查询指定时间段内的 AlertRecord
recordQuery := AlertRecordManager.Query("res_ids", "res_type")
recordQuery = recordQuery.GE("created_at", startTime).LE("created_at", endTime)
recordQuery = recordQuery.IsNotNull("res_type").IsNotEmpty("res_type")
recordQuery = recordQuery.IsNotEmpty("res_ids")
// 如果指定了 ResType,添加过滤条件
if len(query.ResType) > 0 {
recordQuery = recordQuery.Equals("res_type", query.ResType)
}
// 应用权限过滤 - 使用 FilterByOwner 方法
// 从 query 中获取 scope,如果没有则使用默认值
scope := rbacscope.ScopeSystem
if len(query.VirtualResourceListInput.Scope) > 0 {
scope = rbacscope.TRbacScope(query.VirtualResourceListInput.Scope)
}
recordQuery = AlertRecordManager.SMonitorScopedResourceManager.FilterByOwner(
ctx, recordQuery, AlertRecordManager, userCred, userCred, scope)
// 执行查询获取所有记录
type RecordRow struct {
ResIds string
ResType string
}
rows := make([]RecordRow, 0)
err = recordQuery.All(&rows)
if err != nil {
return nil, errors.Wrap(err, "query alert records")
}
// 统计每个资源的报警数量
resourceAlertCount := make(map[string]int)
for _, row := range rows {
if len(row.ResIds) == 0 {
continue
}
// 解析 res_ids(逗号分隔)
resIds := strings.Split(row.ResIds, ",")
for _, resId := range resIds {
resId = strings.TrimSpace(resId)
if len(resId) > 0 {
// 如果指定了 ResType,需要匹配 res_type
if len(query.ResType) > 0 && row.ResType != query.ResType {
continue
}
resourceAlertCount[resId]++
}
}
}
// 转换为切片并按报警数量排序
type ResourceCount struct {
ResId string
Count int
}
resourceCounts := make([]ResourceCount, 0, len(resourceAlertCount))
for resId, count := range resourceAlertCount {
resourceCounts = append(resourceCounts, ResourceCount{
ResId: resId,
Count: count,
})
}
// 按报警数量降序排序
for i := 0; i < len(resourceCounts)-1; i++ {
for j := i + 1; j < len(resourceCounts); j++ {
if resourceCounts[i].Count < resourceCounts[j].Count {
resourceCounts[i], resourceCounts[j] = resourceCounts[j], resourceCounts[i]
}
}
}
// 获取 top N 的资源 ID
topResIds := make([]string, 0, top)
for i := 0; i < top && i < len(resourceCounts); i++ {
topResIds = append(topResIds, resourceCounts[i].ResId)
}
if len(topResIds) == 0 {
// 如果没有找到任何记录,返回空查询
return q.FilterByFalse(), nil
}
// 用 top res_id 过滤 MonitorResource 查询
q, err = man.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput)
if err != nil {
return nil, err
}
q, err = man.SEnabledResourceBaseManager.ListItemFilter(ctx, q, userCred, query.EnabledResourceBaseListInput)
if err != nil {
return nil, err
}
q = man.FieldListFilter(q, query)
q = q.In("res_id", topResIds)
return q, nil
}
func (man *SMonitorResourceManager) HasName() bool {
return false
}
@@ -27,6 +27,7 @@ import (
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
@@ -203,6 +204,25 @@ func (m *SMonitorResourceAlertManager) GetNowAlertingAlerts(ctx context.Context,
}
func (m *SMonitorResourceAlertManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input *monitor.MonitorResourceJointListInput) (*sqlchemy.SQuery, error) {
// 如果指定了时间段、top 和 alert_id 参数,执行特殊的 top 查询
// 使用 RawQuery 以包含 deleted 的数据(已恢复的资源)
if input.Top != nil {
// 加上 top 的参数校验
if input.Top == nil || *input.Top <= 0 {
return nil, httperrors.NewInputParameterError("top must be specified and greater than 0")
}
if input.StartTime.IsZero() || input.EndTime.IsZero() {
return nil, httperrors.NewInputParameterError("start_time and end_time must be specified")
}
if input.StartTime.After(input.EndTime) {
return nil, httperrors.NewInputParameterError("start_time must be before end_time")
}
if len(input.AlertId) == 0 {
return nil, httperrors.NewInputParameterError("alert_id must be specified")
}
return m.getTopResourcesByMetricAndAlertCount(ctx, q, userCred, input)
}
var err error
q, err = m.SJointResourceBaseManager.ListItemFilter(ctx, q, userCred, input.JointResourceBaseListInput)
if err != nil {
@@ -282,6 +302,159 @@ func (m *SMonitorResourceAlertManager) CustomizeFilterList(ctx context.Context,
return filters, nil
}
// getTopResourcesByMetricAndAlertCount 查询指定时间段内,某个监控策略下各监控指标报警资源最多的 top N 资源
// 使用 RawQuery 查询包含 deleted 的数据,以包含已恢复的资源
func (m *SMonitorResourceAlertManager) getTopResourcesByMetricAndAlertCount(
ctx context.Context,
q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
input *monitor.MonitorResourceJointListInput,
) (*sqlchemy.SQuery, error) {
// 验证时间段和 top 参数
startTime, endTime, top, err := validateTopQueryInput(input.TopQueryInput)
if err != nil {
return nil, err
}
// 查询指定时间段内的 AlertRecord,过滤 alert_id
recordQuery := AlertRecordManager.Query("id", "alert_rule", "res_ids", "res_type")
recordQuery = recordQuery.Equals("alert_id", input.AlertId)
recordQuery = recordQuery.GE("created_at", startTime).LE("created_at", endTime)
recordQuery = recordQuery.IsNotEmpty("res_ids")
// 如果指定了 ResType,添加过滤条件
if len(input.ResType) > 0 {
recordQuery = recordQuery.Equals("res_type", input.ResType)
}
// 执行查询获取所有记录
type RecordRow struct {
Id string
AlertRule jsonutils.JSONObject
ResIds string
ResType string
}
rows := make([]RecordRow, 0)
err = recordQuery.All(&rows)
if err != nil {
return nil, errors.Wrap(err, "query alert records")
}
// 按 metric 分组统计,然后合并所有 metric 的统计结果
// metricResourceCount[metric][resId] = count
metricResourceCount := make(map[string]map[string]int)
for _, row := range rows {
if len(row.ResIds) == 0 {
continue
}
// 从 AlertRule 中解析 metric
var alertRules []*monitor.AlertRecordRule
if row.AlertRule != nil {
if err := row.AlertRule.Unmarshal(&alertRules); err != nil {
log.Warningf("unmarshal alert_rule error: %v", err)
continue
}
}
if len(alertRules) == 0 {
continue
}
// 解析 res_ids(逗号分隔)
resIds := strings.Split(row.ResIds, ",")
for _, resId := range resIds {
resId = strings.TrimSpace(resId)
if len(resId) == 0 {
continue
}
// 如果指定了 ResType,需要匹配 res_type
if len(input.ResType) > 0 && row.ResType != input.ResType {
continue
}
// 对于每个 metric,统计资源数量
for _, rule := range alertRules {
if len(rule.Metric) == 0 {
continue
}
if metricResourceCount[rule.Metric] == nil {
metricResourceCount[rule.Metric] = make(map[string]int)
}
metricResourceCount[rule.Metric][resId]++
}
}
}
// 合并所有 metric 的统计结果,计算每个资源的总报警数
resourceCount := make(map[string]int)
for _, resourceCountByMetric := range metricResourceCount {
for resId, count := range resourceCountByMetric {
resourceCount[resId] += count
}
}
log.Infof("=======resourceCount: %#v", resourceCount)
// 转换为切片并按报警数量排序
type ResourceCount struct {
ResId string
Count int
}
resourceCounts := make([]ResourceCount, 0, len(resourceCount))
for resId, count := range resourceCount {
resourceCounts = append(resourceCounts, ResourceCount{
ResId: resId,
Count: count,
})
}
// 按报警数量降序排序
for i := 0; i < len(resourceCounts)-1; i++ {
for j := i + 1; j < len(resourceCounts); j++ {
if resourceCounts[i].Count < resourceCounts[j].Count {
resourceCounts[i], resourceCounts[j] = resourceCounts[j], resourceCounts[i]
}
}
}
// 获取全局 top N 的资源 ID
topResIds := make([]string, 0, top)
for i := 0; i < min(top, len(resourceCounts)); i++ {
topResIds = append(topResIds, resourceCounts[i].ResId)
}
log.Infof("top %d resources: %v", top, resourceCounts[:min(top, len(resourceCounts))])
log.Infof("====topResIds: %#v", topResIds)
if len(topResIds) == 0 {
// 如果没有找到任何记录,返回空查询
return q.FilterByFalse(), nil
}
q = m.RawQuery()
q = q.Equals("alert_id", input.AlertId)
q = q.Filter(sqlchemy.In(q.Field("monitor_resource_id"), topResIds))
// 应用其他过滤条件
if len(input.AlertState) > 0 {
q = q.Equals("alert_state", input.AlertState)
}
if len(input.SendState) != 0 {
q = q.Equals("send_state", input.SendState)
}
if len(input.ResType) != 0 {
q = q.Equals("res_type", input.ResType)
}
if len(input.Metric) != 0 {
q = q.Equals("metric", input.Metric)
}
return q, nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func (man *SMonitorResourceAlertManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,