diff --git a/cmd/climc/shell/monitor/alertrecord.go b/cmd/climc/shell/monitor/alertrecord.go index 9a196d4e28..9a6664d249 100644 --- a/cmd/climc/shell/monitor/alertrecord.go +++ b/cmd/climc/shell/monitor/alertrecord.go @@ -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)) } diff --git a/pkg/apis/monitor/alertrecord.go b/pkg/apis/monitor/alertrecord.go index 9daaab15db..7da5d64590 100644 --- a/pkg/apis/monitor/alertrecord.go +++ b/pkg/apis/monitor/alertrecord.go @@ -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"` +} diff --git a/pkg/apis/monitor/commalert.go b/pkg/apis/monitor/commalert.go index c84b3b084b..ca7a263409 100644 --- a/pkg/apis/monitor/commalert.go +++ b/pkg/apis/monitor/commalert.go @@ -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 { diff --git a/pkg/apis/monitor/monitor_resource.go b/pkg/apis/monitor/monitor_resource.go index a65af14b3b..1129bbb117 100644 --- a/pkg/apis/monitor/monitor_resource.go +++ b/pkg/apis/monitor/monitor_resource.go @@ -45,6 +45,8 @@ type MonitorResourceListInput struct { AlertStates []string `json:"alert_states"` ResName string `json:"res_name"` + // Top 查询参数(用于统计报警数量最多的资源) + TopQueryInput } type MonitorResourceDetails struct { diff --git a/pkg/apis/monitor/monitor_resource_alert.go b/pkg/apis/monitor/monitor_resource_alert.go index b0ee49f55d..12331707ec 100644 --- a/pkg/apis/monitor/monitor_resource_alert.go +++ b/pkg/apis/monitor/monitor_resource_alert.go @@ -38,6 +38,8 @@ type MonitorResourceJointListInput struct { Level string `json:"level"` // 查询所有状态 AllState bool `json:"all_state"` + // Top 查询参数(用于统计各监控指标报警资源最多的资源) + TopQueryInput } type MonitorResourceJointCreateInput struct { diff --git a/pkg/mcclient/options/monitor/alertrecord.go b/pkg/mcclient/options/monitor/alertrecord.go index 8f3e645607..3d417b3b50 100644 --- a/pkg/mcclient/options/monitor/alertrecord.go +++ b/pkg/mcclient/options/monitor/alertrecord.go @@ -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 diff --git a/pkg/mcclient/options/monitor/commonalert.go b/pkg/mcclient/options/monitor/commonalert.go index 0eac9487af..19e3c548c9 100644 --- a/pkg/mcclient/options/monitor/commonalert.go +++ b/pkg/mcclient/options/monitor/commonalert.go @@ -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) { diff --git a/pkg/mcclient/options/monitor/monitor_resource.go b/pkg/mcclient/options/monitor/monitor_resource.go index 484bd30997..675c09197b 100644 --- a/pkg/mcclient/options/monitor/monitor_resource.go +++ b/pkg/mcclient/options/monitor/monitor_resource.go @@ -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) { diff --git a/pkg/mcclient/options/monitor/monitor_resource_alert.go b/pkg/mcclient/options/monitor/monitor_resource_alert.go index 42a9905c58..c32db577f6 100644 --- a/pkg/mcclient/options/monitor/monitor_resource_alert.go +++ b/pkg/mcclient/options/monitor/monitor_resource_alert.go @@ -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 { diff --git a/pkg/monitor/models/alertrecord.go b/pkg/monitor/models/alertrecord.go index 3eb322fd02..8d06197b8a 100644 --- a/pkg/monitor/models/alertrecord.go +++ b/pkg/monitor/models/alertrecord.go @@ -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 +} diff --git a/pkg/monitor/models/commonalert.go b/pkg/monitor/models/commonalert.go index 936fc68ade..a5862aea61 100644 --- a/pkg/monitor/models/commonalert.go +++ b/pkg/monitor/models/commonalert.go @@ -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") { diff --git a/pkg/monitor/models/monitor_resource.go b/pkg/monitor/models/monitor_resource.go index d0b1e3ddae..25c993d16a 100644 --- a/pkg/monitor/models/monitor_resource.go +++ b/pkg/monitor/models/monitor_resource.go @@ -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 } diff --git a/pkg/monitor/models/monitor_resource_alert.go b/pkg/monitor/models/monitor_resource_alert.go index 7f60aeaac7..29ec167cf3 100644 --- a/pkg/monitor/models/monitor_resource_alert.go +++ b/pkg/monitor/models/monitor_resource_alert.go @@ -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,