mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-01 15:07:17 +08:00
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:
+17
-10
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user