Skip to content

Commit 54b29df

Browse files
atheurerclaude
andcommitted
Add regex pattern support for breakout filters
Implemented regex pattern matching in breakout filters with two modes: - r/pattern/ (lowercase): Returns separate metrics for each matching value - R/pattern/ (uppercase): Returns single aggregated metric for all matches Features: - Custom delimiter support: use any character after r/R as delimiter (e.g., r/pattern/, r|pattern|, r#pattern#) - Consistent syntax with literal values (r vs R parallels , vs +) - OpenSearch regexp query integration for efficient pattern matching Examples: - --breakout hostname=r/^worker-.*/ (separate metrics per worker) - --breakout hostname=R/^client-.*/ (aggregated metric for all clients) - --breakout dev=r|/dev/sd.*| (custom delimiter for patterns with /) Implementation: - Modified getBreakoutAggregation() to exclude fields with R/pattern/ - Updated getMetricGroupsFromBreakouts() to detect and apply regexp filters - Added comprehensive documentation with examples and use cases Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c879aea commit 54b29df

2 files changed

Lines changed: 103 additions & 20 deletions

File tree

queries/cdmq/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,50 @@ This feature is particularly useful when:
406406

407407
**Note**: Each comma-separated value in a breakout filter (e.g., `csid=1,2`) will produce separate metrics in the output, not an aggregated metric. Future enhancements may support aggregation using a different syntax (e.g., `csid=1+2`).
408408

409+
#### Using Regular Expressions in Breakouts
410+
411+
In addition to specifying exact values or lists of values, you can use regular expressions to match multiple values dynamically. This is particularly useful when you want to match values that follow a pattern without knowing all possible values in advance.
412+
413+
**Syntax**: Use `r/pattern/` for separate metrics (one per match) or `R/pattern/` for aggregated metrics (all matches combined).
414+
415+
- **Lowercase `r`**: Each value matching the pattern gets its own metric (similar to `hostname=a,b,c`)
416+
- **Uppercase `R`**: All values matching the pattern are aggregated into a single metric (similar to future `hostname=a+b+c`)
417+
418+
**Examples:**
419+
420+
```bash
421+
# Get separate metrics for all worker nodes matching the pattern
422+
node ./get-metric-data.js --period <UUID> --source mpstat --type Busy-CPU --breakout hostname=r/^worker-.*/
423+
424+
# Get a single aggregated metric for all client nodes
425+
node ./get-metric-data.js --period <UUID> --source sar-net --type L2-Gbps --breakout hostname=R/^client-.*/
426+
427+
# Mix regex with other filters
428+
node ./get-metric-data.js --period <UUID> --source mpstat --type Busy-CPU --breakout hostname=r/worker-[0-9]+/,cstype=physical
429+
430+
# Use different delimiter if pattern contains slashes
431+
node ./get-metric-data.js --period <UUID> --source iostat --type kB-sec --breakout dev=r|/dev/sd.*|
432+
```
433+
434+
**Custom Delimiter**: The character immediately after `r` or `R` is used as the delimiter. While `/` is conventional, you can use any character (like `|`, `#`, `@`, `~`) if your pattern contains forward slashes.
435+
436+
**Regular Expression Syntax**: The patterns use OpenSearch regex syntax, which is similar to standard regex but with some differences. Common patterns include:
437+
- `.*` - Match any characters (zero or more)
438+
- `.+` - Match any characters (one or more)
439+
- `^` - Match start of string
440+
- `$` - Match end of string
441+
- `[0-9]` - Match any digit
442+
- `[a-z]` - Match any lowercase letter
443+
- `(a|b)` - Match 'a' or 'b'
444+
445+
**Use Cases:**
446+
- Match all nodes of a certain type: `hostname=r/^worker-.*/`
447+
- Match numbered resources: `cpu=r/[0-9]+/`
448+
- Match specific patterns: `device=r/^eth[0-9]/`
449+
- Exclude certain patterns: Use regex negative lookahead if needed
450+
451+
**Performance Note**: Regex patterns are evaluated by OpenSearch and may be slower than exact value matches for very large datasets. Use them when the flexibility is needed.
452+
409453
So far all of the metrics have been represented as a single value for a specific time period. When `--period` is used, the script finds the begin and end times for this period, which in most cases, has a duration equal to the measurement time in the benchmark itself (around 90 seconds in these examples). One can also specify `--run`, `--begin`, and `--end` instead of `--period`, should they need to focus on a different period of time. However, for benchmark metrics (such as uperf), it is important to limit the begin and end to within the actual measurement period for that sample. Conversely, tool metrics can use a begin and end spanning any time period within the run, as the tool collection tends to run continuously for any particular run. Whatever time period is used, one can also use `--resolution` to divide this time period into multiple data-samples, in order to generate things like line graphs:
410454

411455
# node ./get-metric-data.js --period 4F1014D6-AD33-11EC-94E3-ADE96E3275F7 --source sar-net --type L2-Gbps --breakout csid=1,cstype=worker,type=physical,direction=tx,dev --filter gt:0.01 --resolution 10

queries/cdmq/cdm.js

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,21 +2425,37 @@ getBreakoutAggregation = function (source, type, breakout) {
24252425
breakout.forEach((field) => {
24262426
//if (/([^\=]+)\=([^\=]+)/.exec(field)) {
24272427
var matches = regExp.exec(field);
2428+
var shouldAggregate = true; // default: include in aggregation
2429+
24282430
if (matches) {
24292431
//field = $1;
2430-
field = matches[1];
2432+
var fieldName = matches[1];
2433+
var value = matches[2];
2434+
2435+
// Check if this is an aggregated regex pattern (R/pattern/)
2436+
// If uppercase R, we should NOT add this field to the aggregation
2437+
// (all matches will be combined into a single metric)
2438+
if (/^R./.test(value)) {
2439+
shouldAggregate = false;
2440+
}
2441+
2442+
field = fieldName;
2443+
}
2444+
2445+
// Only add to aggregation if shouldAggregate is true
2446+
if (shouldAggregate) {
2447+
agg_str +=
2448+
',"aggs": { "metric_desc.names.' +
2449+
field +
2450+
'": { "terms": ' +
2451+
'{ "show_term_doc_count_error": true, "size": ' +
2452+
bigQuerySize +
2453+
',' +
2454+
'"field": "metric_desc.names.' +
2455+
field +
2456+
'" }';
2457+
field_count++;
24312458
}
2432-
agg_str +=
2433-
',"aggs": { "metric_desc.names.' +
2434-
field +
2435-
'": { "terms": ' +
2436-
'{ "show_term_doc_count_error": true, "size": ' +
2437-
bigQuerySize +
2438-
',' +
2439-
'"field": "metric_desc.names.' +
2440-
field +
2441-
'" }';
2442-
field_count++;
24432459
});
24442460
while (field_count > 0) {
24452461
agg_str += '}}';
@@ -2610,23 +2626,46 @@ getMetricGroupsFromBreakouts = async function (instance, sets, yearDotMonth) {
26102626
}
26112627
// If the breakout contains a match requirement (something like "host=myhost"), then we must add a term filter for it.
26122628
// Multiple values can be specified with commas: "host=a,b,c" which will match any of those values.
2613-
// Eventually it would be nice to have something other than a match, like a regex: host=/^client/.
2629+
// Regex patterns can be specified with r/pattern/ (separate metrics) or R/pattern/ (aggregated metric).
26142630
var regExp = /([^\=]+)\=([^\=]+)/;
26152631
set.breakout.forEach((field) => {
26162632
var matches = regExp.exec(field);
26172633
if (matches) {
26182634
field = matches[1];
26192635
value = matches[2];
2620-
// Check if the value contains multiple comma-separated values
2621-
var values = value.split(',');
2622-
if (values.length > 1) {
2623-
// Multiple values: use "terms" query (note the plural)
2636+
2637+
// Check if it's a regex pattern: r/pattern/ or R/pattern/
2638+
// Group 1: r or R (lowercase = separate metrics, uppercase = aggregated)
2639+
// Group 2: delimiter character (usually /, but can be any char)
2640+
// Group 3: the actual regex pattern
2641+
// \2: backreference to ensure matching closing delimiter
2642+
var regexMatch = /^([rR])(.)(.+)\2$/.exec(value);
2643+
2644+
if (regexMatch) {
2645+
// It's a regex pattern
2646+
var isAggregated = regexMatch[1] === 'R';
2647+
var delimiter = regexMatch[2];
2648+
var pattern = regexMatch[3];
2649+
2650+
// Add regexp filter to OpenSearch query
2651+
// Both r/pattern/ and R/pattern/ use the same filter,
2652+
// the difference is in the aggregation (handled in getBreakoutAggregation)
26242653
q.query.bool.filter.push(
2625-
JSON.parse('{"terms": {"metric_desc.names.' + field + '": ' + JSON.stringify(values) + '}}')
2654+
JSON.parse('{"regexp": {"metric_desc.names.' + field + '": ' + JSON.stringify(pattern) + '}}')
26262655
);
26272656
} else {
2628-
// Single value: use "term" query (singular)
2629-
q.query.bool.filter.push(JSON.parse('{"term": {"metric_desc.names.' + field + '": "' + value + '"}}'));
2657+
// Not a regex pattern, handle as literal value(s)
2658+
// Check if the value contains multiple comma-separated values
2659+
var values = value.split(',');
2660+
if (values.length > 1) {
2661+
// Multiple values: use "terms" query (note the plural)
2662+
q.query.bool.filter.push(
2663+
JSON.parse('{"terms": {"metric_desc.names.' + field + '": ' + JSON.stringify(values) + '}}')
2664+
);
2665+
} else {
2666+
// Single value: use "term" query (singular)
2667+
q.query.bool.filter.push(JSON.parse('{"term": {"metric_desc.names.' + field + '": "' + value + '"}}'));
2668+
}
26302669
}
26312670
}
26322671
});

0 commit comments

Comments
 (0)