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.
This commit is contained in:
Zexi Li
2026-06-09 15:14:47 +08:00
committed by GitHub
parent b876405c97
commit 43d4c1e739
6 changed files with 279 additions and 23 deletions
+17 -10
View File
@@ -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
}
+9
View File
@@ -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 {
@@ -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)
}
}
+75 -10
View File
@@ -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
}
+118
View File
@@ -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)
})
}
}
+21 -3
View File
@@ -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 {