Skip to content

Commit 0c5aab3

Browse files
authored
Merge pull request #2515 from grafana/hotfix/0.38.0/thresholds-url-grouping-parsing
Fix the parsing of threshold name tags containing tokens
2 parents e4b75cd + 652f7be commit 0c5aab3

File tree

3 files changed

+45
-19
lines changed

3 files changed

+45
-19
lines changed

metrics/metric.go

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ var ErrMetricNameParsing = errors.New("parsing metric name failed")
133133
// of "key:value" strings. On failure, it returns an error containing the `ErrMetricNameParsing` in its chain.
134134
func ParseMetricName(name string) (string, []string, error) {
135135
openingTokenPos := strings.IndexByte(name, '{')
136-
closingTokenPos := strings.IndexByte(name, '}')
136+
closingTokenPos := strings.LastIndexByte(name, '}')
137137
containsOpeningToken := openingTokenPos != -1
138138
containsClosingToken := closingTokenPos != -1
139139

@@ -147,37 +147,44 @@ func ParseMetricName(name string) (string, []string, error) {
147147
// its counterpart, the expression is malformed.
148148
if (containsOpeningToken && !containsClosingToken) ||
149149
(!containsOpeningToken && containsClosingToken) {
150-
return "", nil, fmt.Errorf("%w; reason: unmatched opening/close curly brace", ErrMetricNameParsing)
150+
return "", nil, fmt.Errorf(
151+
"%w, metric %q has unmatched opening/close curly brace",
152+
ErrMetricNameParsing, name,
153+
)
151154
}
152155

156+
// If the closing brace token appears before the opening one,
157+
// the expression is malformed
153158
if closingTokenPos < openingTokenPos {
154-
return "", nil, fmt.Errorf("%w; reason: closing curly brace appears before opening one", ErrMetricNameParsing)
159+
return "", nil, fmt.Errorf("%w, metric %q closing curly brace appears before opening one", ErrMetricNameParsing, name)
155160
}
156161

157-
parserFn := func(c rune) bool {
158-
return c == '{' || c == '}'
162+
// If the last character is not a closing brace token,
163+
// the expression is malformed.
164+
if closingTokenPos != (len(name) - 1) {
165+
err := fmt.Errorf(
166+
"%w, metric %q lacks a closing curly brace in its last position",
167+
ErrMetricNameParsing,
168+
name,
169+
)
170+
return "", nil, err
159171
}
160172

161-
// Split the metric_name{tag_key:tag_value,...} expression
162-
// into two "metric_name" and "tag_key:tag_value,..." strings.
163-
parts := strings.FieldsFunc(name, parserFn)
164-
if len(parts) == 0 || len(parts) > 2 {
165-
return "", nil, ErrMetricNameParsing
166-
}
167-
168-
// Split the tag key values
169-
tags := strings.Split(parts[1], ",")
173+
// We already know the position of the opening and closing curly brace
174+
// tokens. Thus, we extract the string in between them, and split its
175+
// content to obtain the tags key values.
176+
tags := strings.Split(name[openingTokenPos+1:closingTokenPos], ",")
170177

171-
// For each tag definition, ensure
178+
// For each tag definition, ensure it is correctly formed
172179
for i, t := range tags {
173180
keyValue := strings.SplitN(t, ":", 2)
174181

175182
if len(keyValue) != 2 || keyValue[1] == "" {
176-
return "", nil, fmt.Errorf("%w; reason: malformed tag expression %q", ErrMetricNameParsing, t)
183+
return "", nil, fmt.Errorf("%w, metric %q tag expression is malformed", ErrMetricNameParsing, t)
177184
}
178185

179186
tags[i] = strings.TrimSpace(t)
180187
}
181188

182-
return parts[0], tags, nil
189+
return name[0:openingTokenPos], tags, nil
183190
}

metrics/metric_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ func TestParseMetricName(t *testing.T) {
111111
wantTags: []string{"group:::mygroup"},
112112
wantErr: false,
113113
},
114+
{
115+
name: "metric name with valid name and repeated curly braces tokens in tags definition",
116+
metricNameExpression: "http_req_duration{name:http://${}.com}",
117+
wantMetricName: "http_req_duration",
118+
wantTags: []string{"name:http://${}.com"},
119+
wantErr: false,
120+
},
121+
{
122+
name: "metric name with valid name and repeated curly braces and colon tokens in tags definition",
123+
metricNameExpression: "http_req_duration{name:http://${}.com,url:ssh://github.com:grafana/k6}",
124+
wantMetricName: "http_req_duration",
125+
wantTags: []string{"name:http://${}.com", "url:ssh://github.com:grafana/k6"},
126+
wantErr: false,
127+
},
114128
{
115129
name: "metric name with tag definition missing `:value`",
116130
metricNameExpression: "test_metric{easyas}",
@@ -146,6 +160,11 @@ func TestParseMetricName(t *testing.T) {
146160
metricNameExpression: "test_metric}abc{bar",
147161
wantErr: true,
148162
},
163+
{
164+
name: "metric name with valid name and trailing characters after closing curly brace in tags definition",
165+
metricNameExpression: "test_metric{foo:ba}r",
166+
wantErr: true,
167+
},
149168
}
150169
for _, tt := range tests {
151170
tt := tt

metrics/thresholds.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,8 @@ var ErrInvalidThreshold = errors.New("invalid threshold")
260260
func (ts *Thresholds) Validate(metricName string, r *Registry) error {
261261
parsedMetricName, _, err := ParseMetricName(metricName)
262262
if err != nil {
263-
err := fmt.Errorf("unable to validate threshold expressions: %w", ErrMetricNameParsing)
264-
return errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
263+
parseErr := fmt.Errorf("unable to validate threshold expressions; reason: %w", err)
264+
return errext.WithExitCodeIfNone(parseErr, exitcodes.InvalidConfig)
265265
}
266266

267267
// Obtain the metric the thresholds apply to from the registry.

0 commit comments

Comments
 (0)