Skip to content

Commit f299346

Browse files
authored
Support unnamed groups in carbon receiver regex parser (#39137)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Adding a feature - support for unnamed groups in carbon receiver regex parser. This makes it possible to parse a wider range of metrics, e.g. optional metric prefix or optional metric tags. <!--Describe what testing was performed and which tests were added.--> #### Testing Added a test for a rule with an optional prefix: `"(prefix\.)?..."` Added a test for a rule with an optional metric label: `"(job=(?P<key_job>[^.]+)\.)?..."` <!--Describe the documentation added.--> #### Documentation Added an example with an optional prefix and an optional metric label
1 parent 94fb481 commit f299346

File tree

5 files changed

+192
-4
lines changed

5 files changed

+192
-4
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: carbonreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Support unnamed groups in carbon receiver regex parser
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [39137]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

receiver/carbonreceiver/config_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ func TestLoadConfig(t *testing.T) {
6868
},
6969
MetricType: "cumulative",
7070
},
71+
{
72+
Regexp: `(optional_prefix\.)?(?P<key_just>test)\.(?P<key_match>.*)`,
73+
},
74+
{
75+
Regexp: `(experiment(?P<key_experiment>[0-9]+)\.)?(?P<key_just>test)\.(?P<key_match>.*)`,
76+
},
7177
{
7278
Regexp: `(?P<key_just>test)\.(?P<key_match>.*)`,
7379
},

receiver/carbonreceiver/protocol/regex_parser.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,19 @@ func (rpp *regexPathParser) ParsePath(path string, parsedPath *ParsedPath) error
168168
attributes := pcommon.NewMap()
169169

170170
for i := 1; i < len(ms); i++ {
171-
if strings.HasPrefix(nms[i], metricNameCapturePrefix) {
172-
metricNameLookup[nms[i]] = ms[i]
171+
groupName, groupValue := nms[i], ms[i]
172+
if groupName == "" {
173+
// Skip unnamed groups.
174+
continue
175+
}
176+
if groupValue == "" {
177+
// Skip unmatched groups.
178+
continue
179+
}
180+
if strings.HasPrefix(groupName, metricNameCapturePrefix) {
181+
metricNameLookup[groupName] = groupValue
173182
} else {
174-
attributes.PutStr(nms[i][len(keyCapturePrefix):], ms[i])
183+
attributes.PutStr(groupName[len(keyCapturePrefix):], groupValue)
175184
}
176185
}
177186

receiver/carbonreceiver/protocol/regex_parser_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,148 @@ func Test_regexParser_parsePath(t *testing.T) {
176176
}
177177
}
178178

179+
func Test_regexParser_parsePath_simple_unnamed_group(t *testing.T) {
180+
config := RegexParserConfig{
181+
Rules: []*RegexRule{
182+
{
183+
Regexp: `(prefix\.)?(?P<key_svc>[^.]+)\.(?P<key_host>[^.]+)\.cpu\.seconds`,
184+
NamePrefix: "cpu_seconds",
185+
},
186+
},
187+
}
188+
189+
require.NoError(t, compileRegexRules(config.Rules))
190+
rp := &regexPathParser{
191+
rules: config.Rules,
192+
}
193+
194+
tests := []struct {
195+
name string
196+
path string
197+
wantName string
198+
wantAttributes pcommon.Map
199+
wantMetricType TargetMetricType
200+
wantErr bool
201+
}{
202+
{
203+
name: "no_rule_match",
204+
path: "service_name.host01.rpc.duration.seconds",
205+
wantName: "service_name.host01.rpc.duration.seconds",
206+
wantAttributes: pcommon.NewMap(),
207+
},
208+
{
209+
name: "match_no_prefix",
210+
path: "service_name.host00.cpu.seconds",
211+
wantName: "cpu_seconds",
212+
wantAttributes: func() pcommon.Map {
213+
m := pcommon.NewMap()
214+
m.PutStr("svc", "service_name")
215+
m.PutStr("host", "host00")
216+
return m
217+
}(),
218+
},
219+
{
220+
name: "match_optional_prefix",
221+
path: "prefix.service_name.host00.cpu.seconds",
222+
wantName: "cpu_seconds",
223+
wantAttributes: func() pcommon.Map {
224+
m := pcommon.NewMap()
225+
m.PutStr("svc", "service_name")
226+
m.PutStr("host", "host00")
227+
return m
228+
}(),
229+
},
230+
}
231+
232+
for _, tt := range tests {
233+
t.Run(tt.name, func(t *testing.T) {
234+
got := ParsedPath{}
235+
err := rp.ParsePath(tt.path, &got)
236+
if tt.wantErr {
237+
assert.Error(t, err)
238+
return
239+
}
240+
241+
assert.Equal(t, tt.wantName, got.MetricName)
242+
assert.Equal(t, tt.wantAttributes, got.Attributes)
243+
assert.Equal(t, tt.wantMetricType, got.MetricType)
244+
})
245+
}
246+
}
247+
248+
func Test_regexParser_parsePath_key_inside_unnamed_group(t *testing.T) {
249+
config := RegexParserConfig{
250+
Rules: []*RegexRule{
251+
{
252+
Regexp: `(job=(?P<key_job>[^.]+).)?(?P<key_svc>[^.]+)\.(?P<key_host>[^.]+)\.cpu\.seconds`,
253+
NamePrefix: "cpu_seconds",
254+
MetricType: string(GaugeMetricType),
255+
},
256+
},
257+
}
258+
259+
require.NoError(t, compileRegexRules(config.Rules))
260+
rp := &regexPathParser{
261+
rules: config.Rules,
262+
}
263+
264+
tests := []struct {
265+
name string
266+
path string
267+
wantName string
268+
wantAttributes pcommon.Map
269+
wantMetricType TargetMetricType
270+
wantErr bool
271+
}{
272+
{
273+
name: "no_rule_match",
274+
path: "service_name.host01.rpc.duration.seconds",
275+
wantName: "service_name.host01.rpc.duration.seconds",
276+
wantAttributes: pcommon.NewMap(),
277+
},
278+
{
279+
name: "match_missing_optional_key",
280+
path: "service_name.host00.cpu.seconds",
281+
wantName: "cpu_seconds",
282+
wantAttributes: func() pcommon.Map {
283+
m := pcommon.NewMap()
284+
m.PutStr("svc", "service_name")
285+
m.PutStr("host", "host00")
286+
return m
287+
}(),
288+
wantMetricType: GaugeMetricType,
289+
},
290+
{
291+
name: "match_present_optional_key",
292+
path: "job=71972c09-de94-4a4e-a8a7-ad3de050a141.service_name.host00.cpu.seconds",
293+
wantName: "cpu_seconds",
294+
wantAttributes: func() pcommon.Map {
295+
m := pcommon.NewMap()
296+
m.PutStr("job", "71972c09-de94-4a4e-a8a7-ad3de050a141")
297+
m.PutStr("svc", "service_name")
298+
m.PutStr("host", "host00")
299+
return m
300+
}(),
301+
wantMetricType: GaugeMetricType,
302+
},
303+
}
304+
305+
for _, tt := range tests {
306+
t.Run(tt.name, func(t *testing.T) {
307+
got := ParsedPath{}
308+
err := rp.ParsePath(tt.path, &got)
309+
if tt.wantErr {
310+
assert.Error(t, err)
311+
return
312+
}
313+
314+
assert.Equal(t, tt.wantName, got.MetricName)
315+
assert.Equal(t, tt.wantAttributes, got.Attributes)
316+
assert.Equal(t, tt.wantMetricType, got.MetricType)
317+
})
318+
}
319+
}
320+
179321
var res struct {
180322
name string
181323
attributes pcommon.Map

receiver/carbonreceiver/testdata/config.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ carbon/regex:
4848
# type is used to select the metric type to be set, the default is
4949
# "gauge", the other alternative is "cumulative".
5050
type: cumulative
51-
# The second rule for this "regex" parser.
51+
# The second rule matches metric with or without a prefix.
52+
- regexp: "(optional_prefix\\.)?(?P<key_just>test)\\.(?P<key_match>.*)"
53+
# The third rule parses an optional dimension with key "experiment".
54+
- regexp: "(experiment(?P<key_experiment>[0-9]+)\\.)?(?P<key_just>test)\\.(?P<key_match>.*)"
55+
# The forth rule for this "regex" parser.
5256
- regexp: "(?P<key_just>test)\\.(?P<key_match>.*)"
5357
# Name separator is used when concatenating named regular expression
5458
# captures prefixed with "name_"

0 commit comments

Comments
 (0)