2021-09-21 15:19:52 +02:00
import { PromMetricsMetadata , PromMetricsMetadataItem } from './types' ;
2020-05-22 15:16:01 +02:00
import { addLabelToQuery } from './add_label_to_query' ;
2021-01-14 15:45:45 +01:00
import { SUGGESTIONS_LIMIT } from './language_provider' ;
2020-05-07 12:02:45 +02:00
2021-10-12 13:16:09 +02:00
export const processHistogramMetrics = ( metrics : string [ ] ) = > {
2020-06-25 08:51:42 -07:00
const resultSet : Set < string > = new Set ( ) ;
2019-04-29 15:42:58 +02:00
const regexp = new RegExp ( '_bucket($|:)' ) ;
2021-10-12 13:16:09 +02:00
for ( let index = 0 ; index < metrics . length ; index ++ ) {
const metric = metrics [ index ] ;
const isHistogramValue = regexp . test ( metric ) ;
2019-04-29 15:42:58 +02:00
if ( isHistogramValue ) {
2021-10-12 13:16:09 +02:00
resultSet . add ( metric ) ;
2019-04-29 15:42:58 +02:00
}
}
2021-10-12 13:16:09 +02:00
return [ . . . resultSet ] ;
2019-04-29 15:42:58 +02:00
} ;
2019-09-23 12:26:05 +01:00
export function processLabels ( labels : Array < { [ key : string ] : string } > , withName = false ) {
2021-01-14 15:45:45 +01:00
// For processing we are going to use sets as they have significantly better performance than arrays
// After we process labels, we will convert sets to arrays and return object with label values in arrays
const valueSet : { [ key : string ] : Set < string > } = { } ;
2021-01-20 07:59:48 +01:00
labels . forEach ( ( label ) = > {
2021-01-14 15:45:45 +01:00
const { __name__ , . . . rest } = label ;
2018-08-06 14:36:02 +02:00
if ( withName ) {
2021-01-14 15:45:45 +01:00
valueSet [ '__name__' ] = valueSet [ '__name__' ] || new Set ( ) ;
if ( ! valueSet [ '__name__' ] . has ( __name__ ) ) {
valueSet [ '__name__' ] . add ( __name__ ) ;
2018-08-06 14:36:02 +02:00
}
}
2021-01-20 07:59:48 +01:00
Object . keys ( rest ) . forEach ( ( key ) = > {
2021-01-14 15:45:45 +01:00
if ( ! valueSet [ key ] ) {
valueSet [ key ] = new Set ( ) ;
2018-04-26 11:58:42 +02:00
}
2021-01-14 15:45:45 +01:00
if ( ! valueSet [ key ] . has ( rest [ key ] ) ) {
valueSet [ key ] . add ( rest [ key ] ) ;
2018-04-26 11:58:42 +02:00
}
} ) ;
} ) ;
2021-01-14 15:45:45 +01:00
// valueArray that we are going to return in the object
const valueArray : { [ key : string ] : string [ ] } = { } ;
2021-01-20 07:59:48 +01:00
limitSuggestions ( Object . keys ( valueSet ) ) . forEach ( ( key ) = > {
2021-01-14 15:45:45 +01:00
valueArray [ key ] = limitSuggestions ( Array . from ( valueSet [ key ] ) ) ;
} ) ;
return { values : valueArray , keys : Object.keys ( valueArray ) } ;
2018-04-26 11:58:42 +02:00
}
2018-08-06 14:36:02 +02:00
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
2021-01-08 18:31:21 +01:00
export const selectorRegexp = /\{[^}]*?(\}|$)/ ;
2018-11-13 15:35:20 +00:00
export const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g ;
2018-08-10 14:45:09 +02:00
export function parseSelector ( query : string , cursorOffset = 1 ) : { labelKeys : any [ ] ; selector : string } {
2018-08-06 14:36:02 +02:00
if ( ! query . match ( selectorRegexp ) ) {
// Special matcher for metrics
2018-09-19 12:01:02 +02:00
if ( query . match ( /^[A-Za-z:][\w:]*$/ ) ) {
2018-08-10 14:45:09 +02:00
return {
selector : ` {__name__=" ${ query } "} ` ,
labelKeys : [ '__name__' ] ,
} ;
2018-08-06 14:36:02 +02:00
}
throw new Error ( 'Query must contain a selector: ' + query ) ;
}
// Check if inside a selector
const prefix = query . slice ( 0 , cursorOffset ) ;
const prefixOpen = prefix . lastIndexOf ( '{' ) ;
const prefixClose = prefix . lastIndexOf ( '}' ) ;
if ( prefixOpen === - 1 ) {
throw new Error ( 'Not inside selector, missing open brace: ' + prefix ) ;
}
if ( prefixClose > - 1 && prefixClose > prefixOpen ) {
throw new Error ( 'Not inside selector, previous selector already closed: ' + prefix ) ;
}
const suffix = query . slice ( cursorOffset ) ;
const suffixCloseIndex = suffix . indexOf ( '}' ) ;
const suffixClose = suffixCloseIndex + cursorOffset ;
const suffixOpenIndex = suffix . indexOf ( '{' ) ;
const suffixOpen = suffixOpenIndex + cursorOffset ;
if ( suffixClose === - 1 ) {
throw new Error ( 'Not inside selector, missing closing brace in suffix: ' + suffix ) ;
}
if ( suffixOpenIndex > - 1 && suffixOpen < suffixClose ) {
throw new Error ( 'Not inside selector, next selector opens before this one closed: ' + suffix ) ;
}
// Extract clean labels to form clean selector, incomplete labels are dropped
const selector = query . slice ( prefixOpen , suffixClose ) ;
2019-04-29 15:42:58 +02:00
const labels : { [ key : string ] : { value : string ; operator : string } } = { } ;
2019-12-31 08:56:57 +01:00
selector . replace ( labelRegexp , ( label , key , operator , value ) = > {
const labelOffset = query . indexOf ( label ) ;
const valueStart = labelOffset + key . length + operator . length + 1 ;
const valueEnd = labelOffset + key . length + operator . length + value . length - 1 ;
// Skip label if cursor is in value
if ( cursorOffset < valueStart || cursorOffset > valueEnd ) {
labels [ key ] = { value , operator } ;
}
2018-08-06 14:36:02 +02:00
return '' ;
} ) ;
// Add metric if there is one before the selector
const metricPrefix = query . slice ( 0 , prefixOpen ) ;
2018-09-19 12:01:02 +02:00
const metricMatch = metricPrefix . match ( /[A-Za-z:][\w:]*$/ ) ;
2018-08-06 14:36:02 +02:00
if ( metricMatch ) {
2018-10-14 10:43:48 +08:00
labels [ '__name__' ] = { value : ` " ${ metricMatch [ 0 ] } " ` , operator : '=' } ;
2018-08-06 14:36:02 +02:00
}
// Build sorted selector
2018-08-10 14:45:09 +02:00
const labelKeys = Object . keys ( labels ) . sort ( ) ;
2021-01-20 07:59:48 +01:00
const cleanSelector = labelKeys . map ( ( key ) = > ` ${ key } ${ labels [ key ] . operator } ${ labels [ key ] . value } ` ) . join ( ',' ) ;
2018-08-06 14:36:02 +02:00
2018-08-10 14:45:09 +02:00
const selectorString = [ '{' , cleanSelector , '}' ] . join ( '' ) ;
return { labelKeys , selector : selectorString } ;
2018-08-06 14:36:02 +02:00
}
2018-11-16 14:31:51 +00:00
export function expandRecordingRules ( query : string , mapping : { [ name : string ] : string } ) : string {
const ruleNames = Object . keys ( mapping ) ;
const rulesRegex = new RegExp ( ` ( \\ s|^)( ${ ruleNames . join ( '|' ) } )( \\ s| $ | \\ (| \\ [| \\ {) ` , 'ig' ) ;
2020-05-22 15:16:01 +02:00
const expandedQuery = query . replace ( rulesRegex , ( match , pre , name , post ) = > ` ${ pre } ${ mapping [ name ] } ${ post } ` ) ;
// Split query into array, so if query uses operators, we can correctly add labels to each individual part.
const queryArray = expandedQuery . split ( /(\+|\-|\*|\/|\%|\^)/ ) ;
2020-10-07 12:29:30 +02:00
// Regex that matches occurrences of ){ or }{ or ]{ which is a sign of incorrecly added labels.
2020-05-22 15:16:01 +02:00
const invalidLabelsRegex = /(\)\{|\}\{|\]\{)/ ;
2021-01-20 07:59:48 +01:00
const correctlyExpandedQueryArray = queryArray . map ( ( query ) = > {
2020-07-06 21:16:27 +02:00
return addLabelsToExpression ( query , invalidLabelsRegex ) ;
2020-05-22 15:16:01 +02:00
} ) ;
return correctlyExpandedQueryArray . join ( '' ) ;
}
function addLabelsToExpression ( expr : string , invalidLabelsRegexp : RegExp ) {
2020-07-06 21:16:27 +02:00
const match = expr . match ( invalidLabelsRegexp ) ;
if ( ! match ) {
return expr ;
}
2020-05-22 15:16:01 +02:00
// Split query into 2 parts - before the invalidLabelsRegex match and after.
2020-07-06 21:16:27 +02:00
const indexOfRegexMatch = match . index ? ? 0 ;
2020-05-22 15:16:01 +02:00
const exprBeforeRegexMatch = expr . substr ( 0 , indexOfRegexMatch + 1 ) ;
const exprAfterRegexMatch = expr . substr ( indexOfRegexMatch + 1 ) ;
// Create arrayOfLabelObjects with label objects that have key, operator and value.
const arrayOfLabelObjects : Array < { key : string ; operator : string ; value : string } > = [ ] ;
exprAfterRegexMatch . replace ( labelRegexp , ( label , key , operator , value ) = > {
arrayOfLabelObjects . push ( { key , operator , value } ) ;
return '' ;
} ) ;
// Loop trough all of the label objects and add them to query.
// As a starting point we have valid query without the labels.
let result = exprBeforeRegexMatch ;
2021-01-20 07:59:48 +01:00
arrayOfLabelObjects . filter ( Boolean ) . forEach ( ( obj ) = > {
2020-05-22 15:16:01 +02:00
// Remove extra set of quotes from obj.value
const value = obj . value . substr ( 1 , obj . value . length - 2 ) ;
result = addLabelToQuery ( result , obj . key , value , obj . operator ) ;
} ) ;
return result ;
2018-11-16 14:31:51 +00:00
}
2020-05-07 12:02:45 +02:00
/ * *
* Adds metadata for synthetic metrics for which the API does not provide metadata .
* See https : //github.com/grafana/grafana/issues/22337 for details.
*
* @param metadata HELP and TYPE metadata from / api / v1 / metadata
* /
2021-09-21 15:19:52 +02:00
export function fixSummariesMetadata ( metadata : { [ metric : string ] : PromMetricsMetadataItem [ ] } ) : PromMetricsMetadata {
2020-05-07 12:02:45 +02:00
if ( ! metadata ) {
return metadata ;
}
2021-09-21 15:19:52 +02:00
const baseMetadata : PromMetricsMetadata = { } ;
2020-05-07 12:02:45 +02:00
const summaryMetadata : PromMetricsMetadata = { } ;
for ( const metric in metadata ) {
2021-09-21 15:19:52 +02:00
// NOTE: based on prometheus-documentation, we can receive
// multiple metadata-entries for the given metric, it seems
// it happens when the same metric is on multiple targets
// and their help-text differs
// (https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-metadata)
// for now we just use the first entry.
2020-05-07 12:02:45 +02:00
const item = metadata [ metric ] [ 0 ] ;
2021-09-21 15:19:52 +02:00
baseMetadata [ metric ] = item ;
2021-05-28 14:51:06 +02:00
if ( item . type === 'histogram' ) {
2021-09-21 15:19:52 +02:00
summaryMetadata [ ` ${ metric } _bucket ` ] = {
type : 'counter' ,
help : ` Cumulative counters for the observation buckets ( ${ item . help } ) ` ,
} ;
summaryMetadata [ ` ${ metric } _count ` ] = {
type : 'counter' ,
help : ` Count of events that have been observed for the histogram metric ( ${ item . help } ) ` ,
} ;
summaryMetadata [ ` ${ metric } _sum ` ] = {
type : 'counter' ,
help : ` Total sum of all observed values for the histogram metric ( ${ item . help } ) ` ,
} ;
2021-05-28 14:51:06 +02:00
}
2020-05-07 12:02:45 +02:00
if ( item . type === 'summary' ) {
2021-09-21 15:19:52 +02:00
summaryMetadata [ ` ${ metric } _count ` ] = {
type : 'counter' ,
help : ` Count of events that have been observed for the base metric ( ${ item . help } ) ` ,
} ;
summaryMetadata [ ` ${ metric } _sum ` ] = {
type : 'counter' ,
help : ` Total sum of all observed values for the base metric ( ${ item . help } ) ` ,
} ;
2020-05-07 12:02:45 +02:00
}
}
2021-05-28 14:51:06 +02:00
// Synthetic series
const syntheticMetadata : PromMetricsMetadata = { } ;
2021-09-21 15:19:52 +02:00
syntheticMetadata [ 'ALERTS' ] = {
type : 'counter' ,
help :
'Time series showing pending and firing alerts. The sample value is set to 1 as long as the alert is in the indicated active (pending or firing) state.' ,
} ;
return { . . . baseMetadata , . . . summaryMetadata , . . . syntheticMetadata } ;
2020-05-07 12:02:45 +02:00
}
2021-01-08 19:19:00 +01:00
export function roundMsToMin ( milliseconds : number ) : number {
return roundSecToMin ( milliseconds / 1000 ) ;
}
export function roundSecToMin ( seconds : number ) : number {
return Math . floor ( seconds / 60 ) ;
}
2021-01-14 15:45:45 +01:00
export function limitSuggestions ( items : string [ ] ) {
return items . slice ( 0 , SUGGESTIONS_LIMIT ) ;
}
export function addLimitInfo ( items : any [ ] | undefined ) : string {
return items && items . length >= SUGGESTIONS_LIMIT ? ` , limited to the first ${ SUGGESTIONS_LIMIT } received items ` : '' ;
}
2021-09-30 15:50:02 +02:00
// NOTE: the following 2 exported functions are very similar to the prometheus*Escape
// functions in datasource.ts, but they are not exactly the same algorithm, and we found
// no way to reuse one in the another or vice versa.
// Prometheus regular-expressions use the RE2 syntax (https://github.com/google/re2/wiki/Syntax),
// so every character that matches something in that list has to be escaped.
// the list of metacharacters is: *+?()|\.[]{}^$
// we make a javascript regular expression that matches those characters:
const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g ;
function escapePrometheusRegexp ( value : string ) : string {
return value . replace ( RE2_METACHARACTERS , '\\$&' ) ;
}
// based on the openmetrics-documentation, the 3 symbols we have to handle are:
// - \n ... the newline character
// - \ ... the backslash character
// - " ... the double-quote character
export function escapeLabelValueInExactSelector ( labelValue : string ) : string {
return labelValue . replace ( /\\/g , '\\\\' ) . replace ( /\n/g , '\\n' ) . replace ( /"/g , '\\"' ) ;
}
export function escapeLabelValueInRegexSelector ( labelValue : string ) : string {
return escapeLabelValueInExactSelector ( escapePrometheusRegexp ( labelValue ) ) ;
}