@@ -2651,48 +2651,92 @@ getMetricGroupTermsFromAgg = function (agg, terms) {
26512651} ;
26522652exports . getMetricGroupTermsFromAgg = getMetricGroupTermsFromAgg ;
26532653
2654+ // --------------------------------------------------------------------------------------------------------------
2655+ // Parse a breakout entry from either structured object or legacy string format.
2656+ // Returns { name, values (array|null), regex (string|null), aggregate (bool) }
2657+ function parseBreakoutEntry ( entry ) {
2658+ if ( typeof entry === 'object' && entry !== null && entry . name ) {
2659+ return {
2660+ name : entry . name ,
2661+ values : entry . values || null ,
2662+ regex : entry . regex || null ,
2663+ aggregate : ! ! entry . aggregate
2664+ } ;
2665+ }
2666+ var parsed = { name : String ( entry ) , values : null , regex : null , aggregate : false } ;
2667+ var eqMatch = / ^ ( [ ^ = ] + ) = ( .+ ) $ / . exec ( entry ) ;
2668+ if ( eqMatch ) {
2669+ parsed . name = eqMatch [ 1 ] ;
2670+ var val = eqMatch [ 2 ] ;
2671+ var regexMatch = / ^ ( [ r R ] ) ( .) ( .+ ) \2$ / . exec ( val ) ;
2672+ if ( regexMatch ) {
2673+ parsed . regex = regexMatch [ 3 ] ;
2674+ parsed . aggregate = regexMatch [ 1 ] === 'R' ;
2675+ } else {
2676+ parsed . values = val . split ( '+' ) ;
2677+ }
2678+ }
2679+ return parsed ;
2680+ }
2681+ exports . parseBreakoutEntry = parseBreakoutEntry ;
2682+
2683+ function buildAggregateLabel ( bp , maxLen ) {
2684+ maxLen = maxLen || 30 ;
2685+ if ( bp . values && bp . values . length > 0 ) {
2686+ var vals = bp . values . slice ( ) . sort ( function ( a , b ) {
2687+ var na = Number ( a ) , nb = Number ( b ) ;
2688+ if ( ! isNaN ( na ) && ! isNaN ( nb ) ) return na - nb ;
2689+ return a < b ? - 1 : a > b ? 1 : 0 ;
2690+ } ) ;
2691+ var allNumeric = vals . every ( function ( v ) { return ! isNaN ( Number ( v ) ) ; } ) ;
2692+ if ( allNumeric ) {
2693+ var nums = vals . map ( Number ) ;
2694+ var ranges = [ ] ;
2695+ var start = nums [ 0 ] , end = nums [ 0 ] ;
2696+ for ( var i = 1 ; i < nums . length ; i ++ ) {
2697+ if ( nums [ i ] === end + 1 ) {
2698+ end = nums [ i ] ;
2699+ } else {
2700+ ranges . push ( start === end ? String ( start ) : start + '-' + end ) ;
2701+ start = end = nums [ i ] ;
2702+ }
2703+ }
2704+ ranges . push ( start === end ? String ( start ) : start + '-' + end ) ;
2705+ var rangeStr = ranges . join ( ',' ) ;
2706+ if ( rangeStr . length <= maxLen ) return rangeStr ;
2707+ }
2708+ var joined = vals . join ( ',' ) ;
2709+ if ( joined . length <= maxLen ) return joined ;
2710+ }
2711+ if ( bp . regex ) {
2712+ if ( bp . regex . length <= maxLen ) return '/' + bp . regex + '/' ;
2713+ }
2714+ var count = bp . values ? bp . values . length : '?' ;
2715+ return count + ' values' ;
2716+ }
2717+
26542718// --------------------------------------------------------------------------------------------------------------
26552719getBreakoutAggregation = function ( source , type , breakout ) {
26562720 var agg_str = '{' ;
26572721 agg_str += '"metric_desc.source": { "terms": { "field": "metric_desc.source"}' ;
26582722 agg_str += ',"aggs": { "metric_desc.type": { "terms": { "field": "metric_desc.type"}' ;
2659- // More nested aggregations are added, one per field found in the broeakout
2723+ // More nested aggregations are added, one per field found in the breakout
26602724 var field_count = 0 ;
2661- var regExp = / ( [ ^ \= ] + ) \= ( [ ^ \= ] + ) / ;
2662- //var matches = regExp.exec("");
26632725
26642726 if ( Array . isArray ( breakout ) ) {
2665- breakout . forEach ( ( field ) => {
2666- //if (/([^\=]+)\=([^\=]+)/.exec(field)) {
2667- var matches = regExp . exec ( field ) ;
2668- var shouldAggregate = true ; // default: include in aggregation
2669-
2670- if ( matches ) {
2671- //field = $1;
2672- var fieldName = matches [ 1 ] ;
2673- var value = matches [ 2 ] ;
2674-
2675- // Check if this is an aggregated regex pattern (R/pattern/)
2676- // If uppercase R, we should NOT add this field to the aggregation
2677- // (all matches will be combined into a single metric)
2678- if ( / ^ R ./ . test ( value ) ) {
2679- shouldAggregate = false ;
2680- }
2681-
2682- field = fieldName ;
2683- }
2727+ breakout . forEach ( ( entry ) => {
2728+ var bp = parseBreakoutEntry ( entry ) ;
26842729
2685- // Only add to aggregation if shouldAggregate is true
2686- if ( shouldAggregate ) {
2730+ if ( ! bp . aggregate ) {
26872731 agg_str +=
26882732 ',"aggs": { "metric_desc.names.' +
2689- field +
2733+ bp . name +
26902734 '": { "terms": ' +
26912735 '{ "show_term_doc_count_error": true, "size": ' +
26922736 bigQuerySize +
26932737 ',' +
26942738 '"field": "metric_desc.names.' +
2695- field +
2739+ bp . name +
26962740 '" }' ;
26972741 field_count ++ ;
26982742 }
@@ -2871,48 +2915,22 @@ getMetricGroupsFromBreakouts = async function (instance, sets, yearDotMonth) {
28712915 if ( set . run != null ) {
28722916 q . query . bool . filter . push ( JSON . parse ( '{"term": {"run.run-uuid": "' + set . run + '"}}' ) ) ;
28732917 }
2874- // If the breakout contains a match requirement (something like "host=myhost"), then we must add a term filter for it.
2875- // Multiple values can be specified with commas: "host=a,b,c" which will match any of those values.
2876- // Regex patterns can be specified with r/pattern/ (separate metrics) or R/pattern/ (aggregated metric).
2877- var regExp = / ( [ ^ \= ] + ) \= ( [ ^ \= ] + ) / ;
2878- set . breakout . forEach ( ( field ) => {
2879- var matches = regExp . exec ( field ) ;
2880- if ( matches ) {
2881- field = matches [ 1 ] ;
2882- value = matches [ 2 ] ;
2883-
2884- // Check if it's a regex pattern: r/pattern/ or R/pattern/
2885- // Group 1: r or R (lowercase = separate metrics, uppercase = aggregated)
2886- // Group 2: delimiter character (usually /, but can be any char)
2887- // Group 3: the actual regex pattern
2888- // \2: backreference to ensure matching closing delimiter
2889- var regexMatch = / ^ ( [ r R ] ) ( .) ( .+ ) \2$ / . exec ( value ) ;
2890-
2891- if ( regexMatch ) {
2892- // It's a regex pattern
2893- var isAggregated = regexMatch [ 1 ] === 'R' ;
2894- var delimiter = regexMatch [ 2 ] ;
2895- var pattern = regexMatch [ 3 ] ;
2896-
2897- // Add regexp filter to OpenSearch query
2898- // Both r/pattern/ and R/pattern/ use the same filter,
2899- // the difference is in the aggregation (handled in getBreakoutAggregation)
2918+ // Add filters for breakout entries that specify values or regex patterns
2919+ set . breakout . forEach ( ( entry ) => {
2920+ var bp = parseBreakoutEntry ( entry ) ;
2921+ if ( bp . regex ) {
2922+ q . query . bool . filter . push (
2923+ JSON . parse ( '{"regexp": {"metric_desc.names.' + bp . name + '": ' + JSON . stringify ( bp . regex ) + '}}' )
2924+ ) ;
2925+ } else if ( bp . values ) {
2926+ if ( bp . values . length > 1 ) {
29002927 q . query . bool . filter . push (
2901- JSON . parse ( '{"regexp ": {"metric_desc.names.' + field + '": ' + JSON . stringify ( pattern ) + '}}' )
2928+ JSON . parse ( '{"terms ": {"metric_desc.names.' + bp . name + '": ' + JSON . stringify ( bp . values ) + '}}' )
29022929 ) ;
29032930 } else {
2904- // Not a regex pattern, handle as literal value(s)
2905- // Multiple values are separated by '+': field=value1+value2
2906- var values = value . split ( '+' ) ;
2907- if ( values . length > 1 ) {
2908- // Multiple values: use "terms" query (note the plural)
2909- q . query . bool . filter . push (
2910- JSON . parse ( '{"terms": {"metric_desc.names.' + field + '": ' + JSON . stringify ( values ) + '}}' )
2911- ) ;
2912- } else {
2913- // Single value: use "term" query (singular)
2914- q . query . bool . filter . push ( JSON . parse ( '{"term": {"metric_desc.names.' + field + '": "' + value + '"}}' ) ) ;
2915- }
2931+ q . query . bool . filter . push (
2932+ JSON . parse ( '{"term": {"metric_desc.names.' + bp . name + '": "' + bp . values [ 0 ] + '"}}' )
2933+ ) ;
29162934 }
29172935 }
29182936 } ) ;
@@ -2938,26 +2956,34 @@ getMetricGroupsFromBreakouts = async function (instance, sets, yearDotMonth) {
29382956 // Derive the label from each group and organize into a dict, key = label, value = the filter terms
29392957 var metricGroupTermsByLabel = getMetricGroupTermsByLabel ( metricGroupTerms ) ;
29402958
2941- // Extract regexp filters that were excluded from aggregation (R/pattern/)
2942- // These need to be preserved when querying for metric IDs
2959+ // For aggregated breakouts, insert a synthetic label segment at the correct position
29432960 var regexpFilters = [ ] ;
2944- var regExp = / ( [ ^ \= ] + ) \= ( [ ^ \= ] + ) / ;
2945- sets [ idx ] . breakout . forEach ( ( field ) => {
2946- var matches = regExp . exec ( field ) ;
2947- if ( matches ) {
2948- var fieldName = matches [ 1 ] ;
2949- var value = matches [ 2 ] ;
2950- var regexMatch = / ^ ( [ r R ] ) ( .) ( .+ ) \2$ / . exec ( value ) ;
2951- if ( regexMatch ) {
2952- var isAggregated = regexMatch [ 1 ] === 'R' ;
2953- var pattern = regexMatch [ 3 ] ;
2954- if ( isAggregated ) {
2955- // This field was excluded from aggregation, need to preserve the regexp filter
2956- regexpFilters . push ( { field : fieldName , pattern : pattern } ) ;
2957- }
2958- }
2959- }
2961+ var aggregatedPositions = [ ] ;
2962+ sets [ idx ] . breakout . forEach ( ( entry , bpIdx ) => {
2963+ var bp = parseBreakoutEntry ( entry ) ;
2964+ if ( ! bp . aggregate ) return ;
2965+ if ( bp . regex ) {
2966+ regexpFilters . push ( { field : bp . name , pattern : bp . regex } ) ;
2967+ }
2968+ aggregatedPositions . push ( { position : bpIdx , segment : '<' + buildAggregateLabel ( bp ) + '>' } ) ;
29602969 } ) ;
2970+ if ( aggregatedPositions . length > 0 ) {
2971+ var oldLabels = Object . keys ( metricGroupTermsByLabel ) ;
2972+ if ( oldLabels . length === 0 ) {
2973+ var synLabel = aggregatedPositions . map ( function ( ap ) { return ap . segment ; } ) . join ( '-' ) ;
2974+ metricGroupTermsByLabel [ synLabel ] = '' ;
2975+ } else {
2976+ var updated = { } ;
2977+ oldLabels . forEach ( function ( oldLabel ) {
2978+ var segments = oldLabel . match ( / < [ ^ > ] * > / g) || [ ] ;
2979+ aggregatedPositions . forEach ( function ( ap ) {
2980+ segments . splice ( ap . position , 0 , ap . segment ) ;
2981+ } ) ;
2982+ updated [ segments . join ( '-' ) ] = metricGroupTermsByLabel [ oldLabel ] ;
2983+ } ) ;
2984+ metricGroupTermsByLabel = updated ;
2985+ }
2986+ }
29612987
29622988 var thisLabelSet = {
29632989 run : sets [ idx ] . run ,
@@ -3519,14 +3545,7 @@ getMetricDataSets = async function (instance, sets, yearDotMonth) {
35193545 for ( var i = 0 ; i < sets . length ; i ++ ) {
35203546 if ( sets [ i ] . breakout != 'undefined' ) {
35213547 for ( var j = 0 ; j < sets [ i ] . breakout . length ; j ++ ) {
3522- var breakout = sets [ i ] . breakout [ j ] ;
3523- // The breakout requested might have a match included, for example, csid=1. We only
3524- // want the string before the '='
3525- var regExp = / ( [ ^ \= ] + ) \= ( [ ^ \= ] + ) / ;
3526- var matches = regExp . exec ( breakout ) ;
3527- if ( matches ) {
3528- breakout = matches [ 1 ] ;
3529- }
3548+ var breakout = parseBreakoutEntry ( sets [ i ] . breakout [ j ] ) . name ;
35303549 if ( ! setBreakouts [ i ] . includes ( breakout ) ) {
35313550 retMsg +=
35323551 'ERROR: the breakout [' +
@@ -3561,18 +3580,14 @@ getMetricDataSets = async function (instance, sets, yearDotMonth) {
35613580 // Check if any regex filters resulted in zero matches
35623581 for ( var idx = 0 ; idx < metricGroupIdsByLabelSets . length ; idx ++ ) {
35633582 if ( Object . keys ( metricGroupIdsByLabelSets [ idx ] ) . length === 0 ) {
3564- // This set has no metric groups - check if it was due to a regex filter
3583+ // This set has no metric groups - check if it was due to a regex or value filter
35653584 var regexFilters = [ ] ;
3566- var regExp = / ( [ ^ \= ] + ) \= ( [ ^ \= ] + ) / ;
3567- sets [ idx ] . breakout . forEach ( ( field ) => {
3568- var matches = regExp . exec ( field ) ;
3569- if ( matches ) {
3570- var fieldName = matches [ 1 ] ;
3571- var value = matches [ 2 ] ;
3572- // Check if it's a regex pattern
3573- if ( / ^ [ r R ] ./ . test ( value ) ) {
3574- regexFilters . push ( { field : fieldName , pattern : value } ) ;
3575- }
3585+ sets [ idx ] . breakout . forEach ( ( entry ) => {
3586+ var bp = parseBreakoutEntry ( entry ) ;
3587+ if ( bp . regex ) {
3588+ regexFilters . push ( { field : bp . name , pattern : bp . regex } ) ;
3589+ } else if ( bp . values ) {
3590+ regexFilters . push ( { field : bp . name , pattern : bp . values . join ( '+' ) } ) ;
35763591 }
35773592 } ) ;
35783593
@@ -3622,19 +3637,14 @@ getMetricDataSets = async function (instance, sets, yearDotMonth) {
36223637 // Build the label-decoder and the remaining breakouts
36233638 dataSets [ i ] . usedBreakouts = sets [ i ] . breakout ;
36243639 dataSets [ i ] . valueSeriesLabelDecoder = '' ;
3625- var regExp = / ( [ ^ \= ] + ) \= ( [ ^ \= ] + ) / ;
3626- dataSets [ i ] . usedBreakouts . forEach ( ( field ) => {
3627- var matches = regExp . exec ( field ) ;
3628- if ( matches ) {
3629- field = matches [ 1 ] ;
3630- value = matches [ 2 ] ;
3631- }
3632- dataSets [ i ] . valueSeriesLabelDecoder += '-' + '<' + field + '>' ;
3633- //TODO: validate if user's breakouts are available by checking against data.breakouts
3640+ var usedNames = [ ] ;
3641+ dataSets [ i ] . usedBreakouts . forEach ( ( entry ) => {
3642+ var bp = parseBreakoutEntry ( entry ) ;
3643+ usedNames . push ( bp . name ) ;
3644+ dataSets [ i ] . valueSeriesLabelDecoder += '-' + '<' + bp . name + '>' ;
36343645 } ) ;
36353646 dataSets [ i ] . valueSeriesLabelDecoder = dataSets [ i ] . valueSeriesLabelDecoder . replace ( '-' , '' ) ;
3636- // Breakouts already used should not show up in the list of avauilable breakouts
3637- dataSets [ i ] . remainingBreakouts = setBreakouts [ i ] . filter ( ( n ) => ! dataSets [ i ] . usedBreakouts . includes ( n ) ) ;
3647+ dataSets [ i ] . remainingBreakouts = setBreakouts [ i ] . filter ( ( n ) => ! usedNames . includes ( n ) ) ;
36383648 }
36393649
36403650 for ( var i = 0 ; i < sets . length ; i ++ ) {
0 commit comments