From 43d4c1e7394ab3eef3753751d4f71b76c6c4dea3 Mon Sep 17 00:00:00 2001 From: Zexi Li Date: Tue, 9 Jun 2026 15:14:47 +0800 Subject: [PATCH] feat(monitor): add tag name/id mapping in metric measurement API (#25000) Return structured MetricMeasurementOutput with TagNameIdMap and TagNameIdValueMap so callers can resolve resource names to IDs. --- pkg/apis/monitor/metric.go | 27 ++-- pkg/apis/monitor/unifiedmonitor_query.go | 9 ++ pkg/apis/monitor/unifiedmonitor_query_test.go | 39 ++++++ pkg/monitor/models/datasource.go | 85 +++++++++++-- pkg/monitor/models/datasource_test.go | 118 ++++++++++++++++++ pkg/monitor/models/unifiedmonitor.go | 24 +++- 6 files changed, 279 insertions(+), 23 deletions(-) create mode 100644 pkg/apis/monitor/unifiedmonitor_query_test.go create mode 100644 pkg/monitor/models/datasource_test.go diff --git a/pkg/apis/monitor/metric.go b/pkg/apis/monitor/metric.go index eb07d27d3c..03b7f320b0 100644 --- a/pkg/apis/monitor/metric.go +++ b/pkg/apis/monitor/metric.go @@ -186,14 +186,21 @@ type MetricFieldDetail struct { type InfluxMeasurement struct { apis.Meta - Database string `json:"database"` - Measurement string `json:"measurement"` - MeasurementDisplayName string `json:"measurement_display_name"` - ResType string `json:"res_type"` - Score int `json:"score"` - TagKey []string `json:"tag_key"` - TagValue map[string][]string `json:"tag_value"` - FieldKey []string `json:"field_key"` - FieldDescriptions map[string]MetricFieldDetail `json:"field_descriptions"` - Unit []string `json:"unit"` + Database string + Measurement string + MeasurementDisplayName string + ResType string + Score int + TagKey []string + TagValue map[string][]string + TagNameIdMap map[string]string `json:"tag_name_id_map,omitempty"` + TagNameIdValueMap map[string]map[string]string `json:"tag_name_id_value_map,omitempty"` + FieldKey []string + FieldDescriptions map[string]MetricFieldDetail + Unit []string +} + +type MetricMeasurementOutput struct { + InfluxMeasurement + Func *MetricFunc } diff --git a/pkg/apis/monitor/unifiedmonitor_query.go b/pkg/apis/monitor/unifiedmonitor_query.go index 44d576677a..c563497a0a 100644 --- a/pkg/apis/monitor/unifiedmonitor_query.go +++ b/pkg/apis/monitor/unifiedmonitor_query.go @@ -71,6 +71,15 @@ func GetMeasurementTagIdKeyByResType(resType string) string { return MEASUREMENT_TAG_ID[resType] } +func GetMeasurementTagNameIdMapByResType(resType string) map[string]string { + nameTag := MEASUREMENT_TAG_KEYWORD[resType] + idTag := MEASUREMENT_TAG_ID[resType] + if nameTag == "" || idTag == "" { + return nil + } + return map[string]string{nameTag: idTag} +} + func GetMeasurementTagIdKeyByResTypeWithDefault(resType string) string { tagId := GetMeasurementTagIdKeyByResType(resType) if len(tagId) == 0 { diff --git a/pkg/apis/monitor/unifiedmonitor_query_test.go b/pkg/apis/monitor/unifiedmonitor_query_test.go new file mode 100644 index 0000000000..bf369f056a --- /dev/null +++ b/pkg/apis/monitor/unifiedmonitor_query_test.go @@ -0,0 +1,39 @@ +package monitor + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMeasurementTagNameIdMapByResType(t *testing.T) { + cases := []struct { + resType string + want map[string]string + }{ + { + resType: METRIC_RES_TYPE_GUEST, + want: map[string]string{"vm_name": "vm_id"}, + }, + { + resType: METRIC_RES_TYPE_AGENT, + want: map[string]string{"vm_name": "vm_id"}, + }, + { + resType: METRIC_RES_TYPE_HOST, + want: map[string]string{"host": "host_id"}, + }, + { + resType: METRIC_RES_TYPE_REDIS, + want: map[string]string{"redis_name": "redis_id"}, + }, + { + resType: "unknown", + want: nil, + }, + } + for _, tc := range cases { + got := GetMeasurementTagNameIdMapByResType(tc.resType) + assert.Equal(t, tc.want, got, "resType=%s", tc.resType) + } +} diff --git a/pkg/monitor/models/datasource.go b/pkg/monitor/models/datasource.go index dad6e1173c..b9fbf34ab1 100644 --- a/pkg/monitor/models/datasource.go +++ b/pkg/monitor/models/datasource.go @@ -444,22 +444,22 @@ func renderTimeFilter(from, to string) string { } -func (m *SDataSourceManager) GetMetricMeasurement(userCred mcclient.TokenCredential, query jsonutils.JSONObject, tagFilter *monitor.MetricQueryTag) (jsonutils.JSONObject, error) { +func (m *SDataSourceManager) GetMetricMeasurement(userCred mcclient.TokenCredential, query jsonutils.JSONObject, tagFilter *monitor.MetricQueryTag) (*monitor.InfluxMeasurement, error) { database, _ := query.GetString("database") if database == "" { - return jsonutils.JSONNull, merrors.NewArgIsEmptyErr("database") + return nil, merrors.NewArgIsEmptyErr("database") } measurement, _ := query.GetString("measurement") if measurement == "" { - return jsonutils.JSONNull, merrors.NewArgIsEmptyErr("measurement") + return nil, merrors.NewArgIsEmptyErr("measurement") } field, _ := query.GetString("field") if field == "" { - return jsonutils.JSONNull, merrors.NewArgIsEmptyErr("field") + return nil, merrors.NewArgIsEmptyErr("field") } from, _ := query.GetString("from") if len(from) == 0 { - return jsonutils.JSONNull, merrors.NewArgIsEmptyErr("from") + return nil, merrors.NewArgIsEmptyErr("from") } timeF, err := m.getFromAndToFromParam(query) if err != nil { @@ -472,19 +472,25 @@ func (m *SDataSourceManager) GetMetricMeasurement(userCred mcclient.TokenCredent output.Measurement = measurement output.Database = database output.TagValue = make(map[string][]string, 0) + output.TagNameIdValueMap = make(map[string]map[string]string) + + if measureDes, ok := MetricMeasurementManager.GetCache().Get(measurement); ok && len(measureDes.ResType) != 0 { + output.ResType = measureDes.ResType + } + nameTag := monitor.MEASUREMENT_TAG_KEYWORD[output.ResType] + idTag := monitor.MEASUREMENT_TAG_ID[output.ResType] output.FieldKey = []string{field} // 只查询过去 30m 的指标 if timeF.To == "now" { timeF.From = "30m" } - if err := getTagValues(userCred, output, timeF, tagFilter, true); err != nil { - return jsonutils.JSONNull, errors.Wrap(err, "getTagValues error") + if err := getTagValues(userCred, output, timeF, tagFilter, true, nameTag, idTag); err != nil { + return nil, errors.Wrap(err, "getTagValues error") } m.filterRtnTags(output) - return jsonutils.Marshal(output), nil - + return output, nil } func (m *SDataSourceManager) filterRtnTags(output *monitor.InfluxMeasurement) { @@ -677,7 +683,54 @@ func (m *SDataSourceManager) DropSubscription(subscription InfluxdbSubscription) return nil }*/ -func getTagValues(userCred mcclient.TokenCredential, output *monitor.InfluxMeasurement, timeF timeFilter, tagFilter *monitor.MetricQueryTag, skipCheckSeries bool) error { +func inferNameIdTagsFromSeries(series monitor.TimeSeriesSlice) (nameTag, idTag string) { + if len(series) == 0 { + return "", "" + } + tagKeys := sets.NewString() + for _, s := range series { + for k := range s.Tags { + tagKeys.Insert(k) + } + } + for resType, nameKey := range monitor.MEASUREMENT_TAG_KEYWORD { + idKey := monitor.MEASUREMENT_TAG_ID[resType] + if nameKey != "" && idKey != "" && tagKeys.Has(nameKey) && tagKeys.Has(idKey) { + return nameKey, idKey + } + } + return "", "" +} + +func buildTagNameIdValueMap(series monitor.TimeSeriesSlice, nameTag, idTag string) map[string]map[string]string { + if nameTag == "" || idTag == "" { + nameTag, idTag = inferNameIdTagsFromSeries(series) + } + if nameTag == "" || idTag == "" { + return nil + } + result := make(map[string]map[string]string) + for _, s := range series { + nameVal := renderTagVal(s.Tags[nameTag]) + idVal := renderTagVal(s.Tags[idTag]) + // id 值通常为 UUID,不能用 filterTagValue 过滤 + if len(nameVal) == 0 || len(idVal) == 0 || nameVal == "null" || idVal == "null" || filterTagValue(nameVal) { + continue + } + if result[nameTag] == nil { + result[nameTag] = make(map[string]string) + } + if _, exists := result[nameTag][nameVal]; !exists { + result[nameTag][nameVal] = idVal + } + } + if len(result) == 0 { + return nil + } + return result +} + +func getTagValues(userCred mcclient.TokenCredential, output *monitor.InfluxMeasurement, timeF timeFilter, tagFilter *monitor.MetricQueryTag, skipCheckSeries bool, nameTag, idTag string) error { mq := monitor.MetricQuery{ Database: output.Database, Measurement: output.Measurement, @@ -736,6 +789,17 @@ func getTagValues(userCred mcclient.TokenCredential, output *monitor.InfluxMeasu if len(ret.Series) == 0 { return nil } + if nameTag == "" || idTag == "" { + nameTag, idTag = inferNameIdTagsFromSeries(ret.Series) + } + if len(output.ResType) == 0 && nameTag != "" && idTag != "" { + for resType, keyword := range monitor.MEASUREMENT_TAG_KEYWORD { + if keyword == nameTag && monitor.MEASUREMENT_TAG_ID[resType] == idTag { + output.ResType = resType + break + } + } + } for _, s := range ret.Series { tagMap := s.Tags @@ -760,6 +824,7 @@ func getTagValues(userCred mcclient.TokenCredential, output *monitor.InfluxMeasu output.TagValue = tagValMap sort.Strings(tagKeys) output.TagKey = tagKeys + output.TagNameIdValueMap = buildTagNameIdValueMap(ret.Series, nameTag, idTag) return nil } diff --git a/pkg/monitor/models/datasource_test.go b/pkg/monitor/models/datasource_test.go new file mode 100644 index 0000000000..4dcea7f912 --- /dev/null +++ b/pkg/monitor/models/datasource_test.go @@ -0,0 +1,118 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "yunion.io/x/onecloud/pkg/apis/monitor" +) + +func TestBuildTagNameIdValueMap(t *testing.T) { + cases := []struct { + name string + series monitor.TimeSeriesSlice + nameTag string + idTag string + expected map[string]map[string]string + }{ + { + name: "basic vm mapping", + series: monitor.TimeSeriesSlice{ + {Tags: map[string]string{"vm_name": "vm-a", "vm_id": "uuid-1"}}, + {Tags: map[string]string{"vm_name": "vm-b", "vm_id": "uuid-2"}}, + }, + nameTag: "vm_name", + idTag: "vm_id", + expected: map[string]map[string]string{ + "vm_name": { + "vm-a": "uuid-1", + "vm-b": "uuid-2", + }, + }, + }, + { + name: "uuid id values are kept", + series: monitor.TimeSeriesSlice{ + {Tags: map[string]string{"vm_name": "vm-a", "vm_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}, + }, + nameTag: "vm_name", + idTag: "vm_id", + expected: map[string]map[string]string{ + "vm_name": { + "vm-a": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }, + }, + }, + { + name: "infer tags from series when tag keys empty", + series: monitor.TimeSeriesSlice{ + {Tags: map[string]string{"host": "host-a", "host_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}, + }, + nameTag: "", + idTag: "", + expected: map[string]map[string]string{ + "host": { + "host-a": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }, + }, + }, + { + name: "rename scenario same id different names", + series: monitor.TimeSeriesSlice{ + {Tags: map[string]string{"vm_name": "old-name", "vm_id": "uuid-1"}}, + {Tags: map[string]string{"vm_name": "new-name", "vm_id": "uuid-1"}}, + }, + nameTag: "vm_name", + idTag: "vm_id", + expected: map[string]map[string]string{ + "vm_name": { + "old-name": "uuid-1", + "new-name": "uuid-1", + }, + }, + }, + { + name: "conflicting id for same name keeps first", + series: monitor.TimeSeriesSlice{ + {Tags: map[string]string{"vm_name": "vm-a", "vm_id": "uuid-1"}}, + {Tags: map[string]string{"vm_name": "vm-a", "vm_id": "uuid-2"}}, + }, + nameTag: "vm_name", + idTag: "vm_id", + expected: map[string]map[string]string{ + "vm_name": { + "vm-a": "uuid-1", + }, + }, + }, + { + name: "empty tags", + series: monitor.TimeSeriesSlice{ + {Tags: map[string]string{"vm_name": "vm-a"}}, + }, + nameTag: "vm_name", + idTag: "vm_id", + expected: nil, + }, + { + name: "infer when only partial tag keys provided", + series: monitor.TimeSeriesSlice{ + {Tags: map[string]string{"vm_name": "vm-a", "vm_id": "uuid-1"}}, + }, + nameTag: "", + idTag: "vm_id", + expected: map[string]map[string]string{ + "vm_name": { + "vm-a": "uuid-1", + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := buildTagNameIdValueMap(tc.series, tc.nameTag, tc.idTag) + assert.Equal(t, tc.expected, got) + }) + } +} diff --git a/pkg/monitor/models/unifiedmonitor.go b/pkg/monitor/models/unifiedmonitor.go index c06b9373da..8593cab31a 100644 --- a/pkg/monitor/models/unifiedmonitor.go +++ b/pkg/monitor/models/unifiedmonitor.go @@ -192,12 +192,30 @@ func (self *SUnifiedMonitorManager) GetPropertyMetricMeasurement(ctx context.Con if err != nil { return nil, errors.Wrapf(err, "getTagFilterByRequestQuery %s", query.String()) } - rtn, err := DataSourceManager.GetMetricMeasurement(userCred, query, filter) + ret, err := DataSourceManager.GetMetricMeasurement(userCred, query, filter) if err != nil { return nil, errors.Wrapf(err, "GetMetricMeasurement by query %s, filter %s", query.String(), filter) } - rtn.(*jsonutils.JSONDict).Add(jsonutils.Marshal(&metricFunc), "func") - return rtn, nil + tagNameIdMap := monitor.GetMeasurementTagNameIdMapByResType(ret.ResType) + if tagNameIdMap == nil && len(ret.TagNameIdValueMap) > 0 { + for nameTag := range ret.TagNameIdValueMap { + for resType, keyword := range monitor.MEASUREMENT_TAG_KEYWORD { + if keyword == nameTag { + if idTag := monitor.MEASUREMENT_TAG_ID[resType]; idTag != "" { + tagNameIdMap = map[string]string{nameTag: idTag} + break + } + } + } + break + } + } + ret.TagNameIdMap = tagNameIdMap + output := &monitor.MetricMeasurementOutput{ + InfluxMeasurement: *ret, + Func: &metricFunc, + } + return jsonutils.Marshal(output), nil } func (self *SUnifiedMonitorManager) SetHandlerProcessTimeout(info *appsrv.SHandlerInfo, r *http.Request) time.Duration {