From e99d7da667a10e4a00be80eaebb767d03569dbcf Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Tue, 13 Jan 2026 17:53:02 +0000 Subject: [PATCH] Use target as name for aliased queries --- pkg/tsdb/graphite/query.go | 7 +- pkg/tsdb/graphite/query_test.go | 113 ++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/graphite/query.go b/pkg/tsdb/graphite/query.go index 1a3d9b1beb6..b1acde68751 100644 --- a/pkg/tsdb/graphite/query.go +++ b/pkg/tsdb/graphite/query.go @@ -200,8 +200,11 @@ func (s *Service) toDataFrames(response *http.Response, refId string, fromAlert return nil, err } + aliasRegex := regexp.MustCompile(`(alias\(|aliasByMetric|aliasByNode|aliasByTags|aliasQuery|aliasSub)`) + frames = data.Frames{} for _, series := range responseData { + aliasMatch := aliasRegex.MatchString(series.Target) timeVector := make([]time.Time, 0, len(series.DataPoints)) values := make([]*float64, 0, len(series.DataPoints)) @@ -217,7 +220,9 @@ func (s *Service) toDataFrames(response *http.Response, refId string, fromAlert tags := make(map[string]string) for name, value := range series.Tags { if name == "name" { - if fromAlert { + // Queries with aliases should use the target as the name + // to ensure multi-dimensional queries are distinguishable from each other + if fromAlert || aliasMatch { value = series.Target } } diff --git a/pkg/tsdb/graphite/query_test.go b/pkg/tsdb/graphite/query_test.go index 761e62112ba..03dfcfd41d4 100644 --- a/pkg/tsdb/graphite/query_test.go +++ b/pkg/tsdb/graphite/query_test.go @@ -738,6 +738,119 @@ Error: Target not found } } +func TestAliasMatching(t *testing.T) { + service := &Service{ + logger: backend.Logger, + } + + testCases := []struct { + name string + target string + tagsName string + fromAlert bool + expectedLabelName string + }{ + { + name: "alias() function sets name tag to target", + target: "alias(stats.counters.web.hits, 'Web Hits')", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "alias(stats.counters.web.hits, 'Web Hits')", + }, + { + name: "aliasByNode() function sets name tag to target", + target: "aliasByNode(stats.counters.web.hits, 2)", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "aliasByNode(stats.counters.web.hits, 2)", + }, + { + name: "aliasByMetric() function sets name tag to target", + target: "aliasByMetric(stats.counters.web.hits)", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "aliasByMetric(stats.counters.web.hits)", + }, + { + name: "aliasByTags() function sets name tag to target", + target: "aliasByTags(stats.counters.web.hits, 'host')", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "aliasByTags(stats.counters.web.hits, 'host')", + }, + { + name: "aliasSub() function sets name tag to target", + target: "aliasSub(stats.counters.web.hits, 'stats', 'metrics')", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "aliasSub(stats.counters.web.hits, 'stats', 'metrics')", + }, + { + name: "aliasQuery() function sets name tag to target", + target: "aliasQuery(stats.counters.web.hits, 'SELECT name FROM hosts')", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "aliasQuery(stats.counters.web.hits, 'SELECT name FROM hosts')", + }, + { + name: "no alias function keeps original name tag", + target: "stats.counters.web.hits", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "stats.counters.web.hits", + }, + { + name: "fromAlert overrides name tag even without alias", + target: "stats.counters.web.hits", + tagsName: "original.name", + fromAlert: true, + expectedLabelName: "stats.counters.web.hits", + }, + { + name: "nested alias function matches", + target: "sumSeries(alias(stats.counters.*.hits, 'Hits'))", + tagsName: "stats.counters.web.hits", + fromAlert: false, + expectedLabelName: "sumSeries(alias(stats.counters.*.hits, 'Hits'))", + }, + { + name: "alias in metric path should not match", + target: "stats.alias.web.hits", + tagsName: "stats.alias.web.hits", + fromAlert: false, + expectedLabelName: "stats.alias.web.hits", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + body := fmt.Sprintf(`[ + { + "target": %q, + "tags": { "name": %q, "host": "server1" }, + "datapoints": [[100, 1609459200]] + } + ]`, tc.target, tc.tagsName) + + httpResponse := &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body))} + dataFrames, err := service.toDataFrames(httpResponse, "A", tc.fromAlert) + + require.NoError(t, err) + require.Len(t, dataFrames, 1) + + frame := dataFrames[0] + require.GreaterOrEqual(t, len(frame.Fields), 2) + + valueField := frame.Fields[1] + require.NotNil(t, valueField.Labels) + + actualName, ok := valueField.Labels["name"] + require.True(t, ok, "name label should exist") + assert.Equal(t, tc.expectedLabelName, actualName, "name label should match expected value") + }) + } +} + func TestParseGraphiteError(t *testing.T) { tests := []struct { name string