feat(monitor): add batch resource metrics query API (#24598)

Add unifiedmonitor class action `resource-metrics` with client options/CLI command.
Implement host/guest metric drivers to query cpu/mem/disk/net rates and attach alert state.
This commit is contained in:
Zexi Li
2026-03-31 17:45:32 +08:00
committed by GitHub
parent 65bf21a163
commit d4ce3799a9
6 changed files with 325 additions and 0 deletions
+13
View File
@@ -41,4 +41,17 @@ func init() {
printObject(resp)
return nil
})
R(new(options.ResourceMetricsOptions), "monitor-resource-metrics", "Query batch resource metrics (cpu/mem/disk/net) and alert state", func(s *mcclient.ClientSession, opts *options.ResourceMetricsOptions) error {
input, err := opts.GetInput()
if err != nil {
return err
}
resp, err := monitor.UnifiedMonitorManager.PerformResourceMetrics(s, input)
if err != nil {
return errors.Wrap(err, "PerformResourceMetrics")
}
printObject(resp)
return nil
})
}
+39
View File
@@ -313,3 +313,42 @@ func (c CdfQueryDataSet) Less(i, j int) bool {
type CdfQueryOutput struct {
Data CdfQueryDataSet `json:"data"`
}
// ResourceMetricsQueryInput 批量查询资源监控数据的输入参数
type ResourceMetricsQueryInput struct {
// 资源类型: "host" 或 "guest"
ResType string `json:"res_type"`
// 资源 ID 列表
ResIds []string `json:"res_ids"`
// 查询开始时间(默认: 1小时前)
StartTime time.Time `json:"start_time"`
// 查询结束时间(默认: now)
EndTime time.Time `json:"end_time"`
// 查询间隔(默认: 5m)
Interval string `json:"interval"`
}
// ResourceMetricValues 单个资源的监控数据
type ResourceMetricValues struct {
// CPU 使用率 (%)
CpuUsage *float64 `json:"cpu_usage"`
// 内存使用率 (%)
MemUsage *float64 `json:"mem_usage"`
// 磁盘使用率 (%)
DiskUsage *float64 `json:"disk_usage"`
// 磁盘读速率 (Bps)
DiskReadRate *float64 `json:"disk_read_rate"`
// 磁盘写速率 (Bps)
DiskWriteRate *float64 `json:"disk_write_rate"`
// 网络入流量 (bps)
NetInRate *float64 `json:"net_in_rate"`
// 网络出流量 (bps)
NetOutRate *float64 `json:"net_out_rate"`
// 告警状态: init/attach/alerting
AlertState string `json:"alert_state"`
}
// ResourceMetricsQueryOutput 批量查询资源监控数据的输出
type ResourceMetricsQueryOutput struct {
ResourceMetrics map[string]ResourceMetricValues `json:"resource_metrics"`
}
@@ -52,3 +52,7 @@ func NewUnifiedMonitorManager() *SUnifiedMonitorManager {
func (m *SUnifiedMonitorManager) PerformQuery(s *mcclient.ClientSession, input *monitor.MetricQueryInput) (jsonutils.JSONObject, error) {
return m.PerformClassAction(s, "query", jsonutils.Marshal(input))
}
func (m *SUnifiedMonitorManager) PerformResourceMetrics(s *mcclient.ClientSession, input *monitor.ResourceMetricsQueryInput) (jsonutils.JSONObject, error) {
return m.PerformClassAction(s, "resource-metrics", jsonutils.Marshal(input))
}
@@ -26,6 +26,37 @@ import (
"yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
)
type ResourceMetricsOptions struct {
RES_TYPE string `help:"resource type: host or guest" choices:"host|guest"`
ResIds []string `help:"resource IDs" json:"res_ids" nargs:"+"`
StartTime string `help:"start time (RFC3339). e.g.: 2023-12-06T21:54:42Z" json:"-"`
EndTime string `help:"end time (RFC3339). e.g.: 2023-12-18T21:54:42Z" json:"-"`
Interval string `help:"query interval. e.g.: 5m, 1h" json:"interval"`
}
func (o *ResourceMetricsOptions) GetInput() (*api.ResourceMetricsQueryInput, error) {
input := &api.ResourceMetricsQueryInput{
ResType: o.RES_TYPE,
ResIds: o.ResIds,
Interval: o.Interval,
}
if o.StartTime != "" {
t, err := time.Parse(time.RFC3339, o.StartTime)
if err != nil {
return nil, errors.Wrapf(err, "invalid start_time %q", o.StartTime)
}
input.StartTime = t
}
if o.EndTime != "" {
t, err := time.Parse(time.RFC3339, o.EndTime)
if err != nil {
return nil, errors.Wrapf(err, "invalid end_time %q", o.EndTime)
}
input.EndTime = t
}
return input, nil
}
type MeasurementsQueryOptions struct {
Scope string `json:"scope"`
ProjectDomainId string `json:"project_domin_id"`
@@ -0,0 +1,115 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"yunion.io/x/onecloud/pkg/apis/monitor"
)
// IResourceMetricDriver 定义资源监控指标的驱动接口
type IResourceMetricDriver interface {
// GetResType 返回资源类型标识
GetResType() string
// GetTagKey 返回 InfluxDB 中用于标识资源 ID 的 tag key
GetTagKey() string
// GetMetricSpecs 返回该资源类型的所有指标查询规格
GetMetricSpecs() []ResourceMetricSpec
}
// ResourceMetricSpec 描述一个 measurement 下需要查询的字段及其输出映射
type ResourceMetricSpec struct {
Measurement string
Fields []string
OutputKeys []string
}
var resourceMetricDrivers = make(map[string]IResourceMetricDriver)
// RegisterResourceMetricDriver 注册资源监控指标驱动
func RegisterResourceMetricDriver(drv IResourceMetricDriver) {
resourceMetricDrivers[drv.GetResType()] = drv
}
// GetResourceMetricDriver 根据资源类型获取对应驱动
func GetResourceMetricDriver(resType string) IResourceMetricDriver {
return resourceMetricDrivers[resType]
}
// SetResourceMetricValue 将查询结果赋值到 ResourceMetricValues 对应字段
func SetResourceMetricValue(rv *monitor.ResourceMetricValues, key string, val float64) {
switch key {
case "cpu_usage":
rv.CpuUsage = &val
case "mem_usage":
rv.MemUsage = &val
case "disk_usage":
rv.DiskUsage = &val
case "disk_read_rate":
rv.DiskReadRate = &val
case "disk_write_rate":
rv.DiskWriteRate = &val
case "net_in_rate":
rv.NetInRate = &val
case "net_out_rate":
rv.NetOutRate = &val
}
}
// sHostMetricDriver 宿主机监控指标驱动
type sHostMetricDriver struct{}
func (d *sHostMetricDriver) GetResType() string {
return monitor.METRIC_RES_TYPE_HOST
}
func (d *sHostMetricDriver) GetTagKey() string {
return monitor.MEASUREMENT_TAG_ID[monitor.METRIC_RES_TYPE_HOST]
}
func (d *sHostMetricDriver) GetMetricSpecs() []ResourceMetricSpec {
return []ResourceMetricSpec{
{"cpu", []string{"usage_active"}, []string{"cpu_usage"}},
{"mem", []string{"used_percent"}, []string{"mem_usage"}},
{"disk", []string{"used_percent"}, []string{"disk_usage"}},
{"diskio", []string{"read_bps", "write_bps"}, []string{"disk_read_rate", "disk_write_rate"}},
{"net", []string{"bps_recv", "bps_sent"}, []string{"net_in_rate", "net_out_rate"}},
}
}
// sGuestMetricDriver 虚拟机监控指标驱动
type sGuestMetricDriver struct{}
func (d *sGuestMetricDriver) GetResType() string {
return monitor.METRIC_RES_TYPE_GUEST
}
func (d *sGuestMetricDriver) GetTagKey() string {
return monitor.MEASUREMENT_TAG_ID[monitor.METRIC_RES_TYPE_GUEST]
}
func (d *sGuestMetricDriver) GetMetricSpecs() []ResourceMetricSpec {
return []ResourceMetricSpec{
{"vm_cpu", []string{"usage_active"}, []string{"cpu_usage"}},
{"vm_mem", []string{"used_percent"}, []string{"mem_usage"}},
{"vm_disk", []string{"used_percent"}, []string{"disk_usage"}},
{"vm_diskio", []string{"read_bps", "write_bps"}, []string{"disk_read_rate", "disk_write_rate"}},
{"vm_netio", []string{"bps_recv", "bps_sent"}, []string{"net_in_rate", "net_out_rate"}},
}
}
func init() {
RegisterResourceMetricDriver(&sHostMetricDriver{})
RegisterResourceMetricDriver(&sGuestMetricDriver{})
}
+123
View File
@@ -703,3 +703,126 @@ func (self *SUnifiedMonitorManager) GetPropertyCdfQuery(ctx context.Context, use
}
return result, nil
}
func (self *SUnifiedMonitorManager) PerformResourceMetrics(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
data jsonutils.JSONObject,
) (jsonutils.JSONObject, error) {
input := new(monitor.ResourceMetricsQueryInput)
if err := data.Unmarshal(input); err != nil {
return nil, httperrors.NewInputParameterError("unmarshal input: %v", err)
}
drv := GetResourceMetricDriver(input.ResType)
if drv == nil {
return nil, httperrors.NewInputParameterError("unsupported res_type %q", input.ResType)
}
if len(input.ResIds) == 0 {
return nil, httperrors.NewMissingParameterError("res_ids")
}
if input.EndTime.IsZero() {
input.EndTime = time.Now()
}
if input.StartTime.IsZero() {
input.StartTime = input.EndTime.Add(-1 * time.Hour)
}
if len(input.Interval) == 0 {
input.Interval = "5m"
}
tagKey := drv.GetTagKey()
specs := drv.GetMetricSpecs()
result := make(map[string]*monitor.ResourceMetricValues, len(input.ResIds))
for _, id := range input.ResIds {
result[id] = &monitor.ResourceMetricValues{}
}
for _, spec := range specs {
qInput := mod.NewMetricQueryInputWithDB(TELEGRAF_DATABASE, spec.Measurement).SkipCheckSeries(true)
sel := qInput.Selects()
for _, f := range spec.Fields {
sel.Select(f).MEAN()
}
where := qInput.Where()
where.IN(tagKey, input.ResIds)
qInput.GroupBy().TAG(tagKey)
qInput.From(input.StartTime).To(input.EndTime).Interval(input.Interval)
queryData := qInput.ToQueryData()
dbRtn, err := self.performQuery(ctx, userCred, queryData)
if err != nil {
log.Warningf("query %s metrics error: %v", spec.Measurement, err)
continue
}
for _, series := range dbRtn.Series {
resId := series.Tags[tagKey]
if resId == "" {
continue
}
rv, ok := result[resId]
if !ok {
continue
}
// 取最后一个有效数据点
for pi := len(series.Points) - 1; pi >= 0; pi-- {
point := series.Points[pi]
if len(point) < 2 {
continue
}
allValid := true
for fi := 0; fi < len(spec.Fields) && fi < len(point)-1; fi++ {
if point[fi] == nil {
allValid = false
break
}
if fval, ok := point[fi].(*float64); !ok || fval == nil {
allValid = false
break
}
}
if !allValid {
continue
}
for fi, outputKey := range spec.OutputKeys {
if fi < len(point)-1 {
val := 0.0
if fval, ok := point[fi].(*float64); ok && fval != nil {
val = *fval
} else if fval, ok := point[fi].(float64); ok {
val = fval
}
SetResourceMetricValue(rv, outputKey, val)
}
}
break
}
}
}
// 查询告警状态
monResources, err := MonitorResourceManager.GetMonitorResources(monitor.MonitorResourceListInput{
ResId: input.ResIds,
})
if err != nil {
log.Warningf("GetMonitorResources error: %v", err)
} else {
for _, mr := range monResources {
if rv, ok := result[mr.ResId]; ok {
rv.AlertState = mr.AlertState
}
}
}
output := monitor.ResourceMetricsQueryOutput{
ResourceMetrics: make(map[string]monitor.ResourceMetricValues, len(result)),
}
for id, rv := range result {
output.ResourceMetrics[id] = *rv
}
return jsonutils.Marshal(output), nil
}