mirror of https://github.com/grafana/grafana.git
				
				
				
			Logs: Add toggle behavior support for "filter for" and "filter out" label within Logs Details (#70091)
* Datasource test: fix describe nesting * Parsing: export handleQuotes function * Modify query: add functions to detect the presence of a label and remove it * Loki: add support to toggle filters if already present * Datasource test: fix describe nesting * Loki: add support to toggle filter out if present * Remove label: handle escaped values * Datasource: add test case for escaped label values * Loki: remove = filter when applying != * Remove selector: add support for Selector node being far from Matcher * Modify query: add unit tests * Elasticsearch: create modifyQuery for elastic * Elastic modify query: implement functions * Elasticsearch: implement modifyQuery functions in datasource * Elasticsearch: update datasource test * Loki modify query: check for streamSelectorPositions length * Elasticsearch query has filter: escape filter value in regex * Remove unused type * Modify query: use query modeller instance from module
This commit is contained in:
		
							parent
							
								
									4ff0abd0d1
								
							
						
					
					
						commit
						4c4bd69eb6
					
				|  | @ -1198,12 +1198,24 @@ describe('modifyQuery', () => { | |||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should toggle the filter', () => { | ||||
|       query.query = 'foo:"bar"'; | ||||
|       expect(ds.modifyQuery(query, { type: 'ADD_FILTER', options: { key: 'foo', value: 'bar' } }).query).toBe(''); | ||||
|     }); | ||||
| 
 | ||||
|     it('should add the negative filter', () => { | ||||
|       expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', options: { key: 'foo', value: 'bar' } }).query).toBe( | ||||
|         '-foo:"bar"' | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should remove a positive filter to add a negative filter', () => { | ||||
|       query.query = 'foo:"bar"'; | ||||
|       expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', options: { key: 'foo', value: 'bar' } }).query).toBe( | ||||
|         '-foo:"bar"' | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should do nothing on unknown type', () => { | ||||
|       expect(ds.modifyQuery(query, { type: 'unknown', options: { key: 'foo', value: 'bar' } }).query).toBe(query.query); | ||||
|     }); | ||||
|  |  | |||
|  | @ -52,6 +52,7 @@ import { | |||
| } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; | ||||
| import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; | ||||
| import { isMetricAggregationWithMeta } from './guards'; | ||||
| import { addFilterToQuery, queryHasFilter, removeFilterFromQuery } from './modifyQuery'; | ||||
| import { trackAnnotationQuery, trackQuery } from './tracking'; | ||||
| import { | ||||
|   Logs, | ||||
|  | @ -898,17 +899,22 @@ export class ElasticDatasource | |||
|     let expression = query.query ?? ''; | ||||
|     switch (action.type) { | ||||
|       case 'ADD_FILTER': { | ||||
|         if (expression.length > 0) { | ||||
|           expression += ' AND '; | ||||
|         } | ||||
|         expression += `${action.options.key}:"${action.options.value}"`; | ||||
|         // This gives the user the ability to toggle a filter on and off.
 | ||||
|         expression = queryHasFilter(expression, action.options.key, action.options.value) | ||||
|           ? removeFilterFromQuery(expression, action.options.key, action.options.value) | ||||
|           : addFilterToQuery(expression, action.options.key, action.options.value); | ||||
|         break; | ||||
|       } | ||||
|       case 'ADD_FILTER_OUT': { | ||||
|         if (expression.length > 0) { | ||||
|           expression += ' AND '; | ||||
|         /** | ||||
|          * If there is a filter with the same key and value, remove it. | ||||
|          * This prevents the user from seeing no changes in the query when they apply | ||||
|          * this filter. | ||||
|          */ | ||||
|         if (queryHasFilter(expression, action.options.key, action.options.value)) { | ||||
|           expression = removeFilterFromQuery(expression, action.options.key, action.options.value); | ||||
|         } | ||||
|         expression += `-${action.options.key}:"${action.options.value}"`; | ||||
|         expression = addFilterToQuery(expression, action.options.key, action.options.value, '-'); | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|  |  | |||
|  | @ -0,0 +1,74 @@ | |||
| import { queryHasFilter, removeFilterFromQuery, addFilterToQuery } from './modifyQuery'; | ||||
| 
 | ||||
| describe('queryHasFilter', () => { | ||||
|   it('should return true if the query contains the positive filter', () => { | ||||
|     expect(queryHasFilter('label:"value"', 'label', 'value')).toBe(true); | ||||
|     expect(queryHasFilter('label: "value"', 'label', 'value')).toBe(true); | ||||
|     expect(queryHasFilter('label : "value"', 'label', 'value')).toBe(true); | ||||
|     expect(queryHasFilter('label:value', 'label', 'value')).toBe(true); | ||||
|     expect(queryHasFilter('this:"that" AND label:value', 'label', 'value')).toBe(true); | ||||
|     expect( | ||||
|       queryHasFilter( | ||||
|         'message:"Jun 20 17:19:47 Xtorm syslogd[348]: ASL Sender Statistics"', | ||||
|         'message', | ||||
|         'Jun 20 17:19:47 Xtorm syslogd[348]: ASL Sender Statistics' | ||||
|       ) | ||||
|     ).toBe(true); | ||||
|   }); | ||||
|   it('should return false if the query does not contain the positive filter', () => { | ||||
|     expect(queryHasFilter('label:"value"', 'label', 'otherValue')).toBe(false); | ||||
|     expect(queryHasFilter('-label:"value"', 'label', 'value')).toBe(false); | ||||
|   }); | ||||
|   it('should return true if the query contains the negative filter', () => { | ||||
|     expect(queryHasFilter('-label:"value"', 'label', 'value', '-')).toBe(true); | ||||
|     expect(queryHasFilter('-label: "value"', 'label', 'value', '-')).toBe(true); | ||||
|     expect(queryHasFilter('-label : "value"', 'label', 'value', '-')).toBe(true); | ||||
|     expect(queryHasFilter('-label:value', 'label', 'value', '-')).toBe(true); | ||||
|     expect(queryHasFilter('this:"that" AND -label:value', 'label', 'value', '-')).toBe(true); | ||||
|   }); | ||||
|   it('should return false if the query does not contain the negative filter', () => { | ||||
|     expect(queryHasFilter('label:"value"', 'label', 'otherValue', '-')).toBe(false); | ||||
|     expect(queryHasFilter('label:"value"', 'label', 'value', '-')).toBe(false); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('addFilterToQuery', () => { | ||||
|   it('should add a positive filter to the query', () => { | ||||
|     expect(addFilterToQuery('', 'label', 'value')).toBe('label:"value"'); | ||||
|   }); | ||||
|   it('should add a positive filter to the query with other filters', () => { | ||||
|     expect(addFilterToQuery('label2:"value2"', 'label', 'value')).toBe('label2:"value2" AND label:"value"'); | ||||
|   }); | ||||
|   it('should add a negative filter to the query', () => { | ||||
|     expect(addFilterToQuery('', 'label', 'value', '-')).toBe('-label:"value"'); | ||||
|   }); | ||||
|   it('should add a negative filter to the query with other filters', () => { | ||||
|     expect(addFilterToQuery('label2:"value2"', 'label', 'value', '-')).toBe('label2:"value2" AND -label:"value"'); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('removeFilterFromQuery', () => { | ||||
|   it('should remove filter from query', () => { | ||||
|     const query = 'label:"value"'; | ||||
|     expect(removeFilterFromQuery(query, 'label', 'value')).toBe(''); | ||||
|   }); | ||||
|   it('should remove filter from query with other filters', () => { | ||||
|     expect(removeFilterFromQuery('label:"value" AND label2:"value2"', 'label', 'value')).toBe('label2:"value2"'); | ||||
|     expect(removeFilterFromQuery('label:value AND label2:"value2"', 'label', 'value')).toBe('label2:"value2"'); | ||||
|     expect(removeFilterFromQuery('label : "value" OR label2:"value2"', 'label', 'value')).toBe('label2:"value2"'); | ||||
|     expect(removeFilterFromQuery('test="test" OR label:"value" AND label2:"value2"', 'label', 'value')).toBe( | ||||
|       'test="test" AND label2:"value2"' | ||||
|     ); | ||||
|   }); | ||||
|   it('should not remove the wrong filter', () => { | ||||
|     expect(removeFilterFromQuery('-label:"value" AND label2:"value2"', 'label', 'value')).toBe( | ||||
|       '-label:"value" AND label2:"value2"' | ||||
|     ); | ||||
|     expect(removeFilterFromQuery('label2:"value2" OR -label:value', 'label', 'value')).toBe( | ||||
|       'label2:"value2" OR -label:value' | ||||
|     ); | ||||
|     expect(removeFilterFromQuery('-label : "value" OR label2:"value2"', 'label', 'value')).toBe( | ||||
|       '-label : "value" OR label2:"value2"' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,57 @@ | |||
| import { escapeRegex } from '@grafana/data'; | ||||
| 
 | ||||
| type ModifierType = '' | '-'; | ||||
| 
 | ||||
| /** | ||||
|  * Checks for the presence of a given label:"value" filter in the query. | ||||
|  */ | ||||
| export function queryHasFilter(query: string, key: string, value: string, modifier: ModifierType = ''): boolean { | ||||
|   const regex = getFilterRegex(key, value); | ||||
|   const matches = query.matchAll(regex); | ||||
|   for (const match of matches) { | ||||
|     if (modifier === '-' && match[0].startsWith(modifier)) { | ||||
|       return true; | ||||
|     } | ||||
|     if (modifier === '' && !match[0].startsWith('-')) { | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Adds a label:"value" expression to the query. | ||||
|  */ | ||||
| export function addFilterToQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string { | ||||
|   if (queryHasFilter(query, key, value, modifier)) { | ||||
|     return query; | ||||
|   } | ||||
| 
 | ||||
|   const filter = `${modifier}${key}:"${value}"`; | ||||
| 
 | ||||
|   return query === '' ? filter : `${query} AND ${filter}`; | ||||
| } | ||||
| 
 | ||||
| function getFilterRegex(key: string, value: string) { | ||||
|   return new RegExp(`[-]{0,1}\\s*${key}\\s*:\\s*["']{0,1}${escapeRegex(value)}["']{0,1}`, 'ig'); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Removes a label:"value" expression from the query. | ||||
|  */ | ||||
| export function removeFilterFromQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string { | ||||
|   const regex = getFilterRegex(key, value); | ||||
|   const matches = query.matchAll(regex); | ||||
|   const opRegex = new RegExp(`\\s+(?:AND|OR)\\s*$|^\\s*(?:AND|OR)\\s+`, 'ig'); | ||||
|   for (const match of matches) { | ||||
|     if (modifier === '-' && match[0].startsWith(modifier)) { | ||||
|       query = query.replace(regex, '').replace(opRegex, ''); | ||||
|     } | ||||
|     if (modifier === '' && !match[0].startsWith('-')) { | ||||
|       query = query.replace(regex, '').replace(opRegex, ''); | ||||
|     } | ||||
|   } | ||||
|   query = query.replace(/AND\s+OR/gi, 'OR'); | ||||
|   query = query.replace(/OR\s+AND/gi, 'AND'); | ||||
|   return query; | ||||
| } | ||||
|  | @ -637,6 +637,28 @@ describe('LokiDatasource', () => { | |||
|           expect(result.refId).toEqual('A'); | ||||
|           expect(result.expr).toEqual('rate({bar="baz", job="grafana"}[5m])'); | ||||
|         }); | ||||
| 
 | ||||
|         describe('and the filter is already present', () => { | ||||
|           it('then it should remove the filter', () => { | ||||
|             const query: LokiQuery = { refId: 'A', expr: '{bar="baz", job="grafana"}' }; | ||||
|             const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER' }; | ||||
|             const result = ds.modifyQuery(query, action); | ||||
| 
 | ||||
|             expect(result.refId).toEqual('A'); | ||||
|             expect(result.expr).toEqual('{bar="baz"}'); | ||||
|           }); | ||||
| 
 | ||||
|           it('then it should remove the filter with escaped value', () => { | ||||
|             const query: LokiQuery = { refId: 'A', expr: '{place="luna", job="\\"grafana/data\\""}' }; | ||||
|             const action = { options: { key: 'job', value: '"grafana/data"' }, type: 'ADD_FILTER' }; | ||||
|             const result = ds.modifyQuery(query, action); | ||||
| 
 | ||||
|             expect(result.refId).toEqual('A'); | ||||
|             expect(result.expr).toEqual('{place="luna"}'); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('and query has parser', () => { | ||||
|         it('then the correct label should be added for logs query', () => { | ||||
|           const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt' }; | ||||
|  | @ -646,6 +668,7 @@ describe('LokiDatasource', () => { | |||
|           expect(result.refId).toEqual('A'); | ||||
|           expect(result.expr).toEqual('{bar="baz"} | logfmt | job=`grafana`'); | ||||
|         }); | ||||
| 
 | ||||
|         it('then the correct label should be added for metrics query', () => { | ||||
|           const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' }; | ||||
|           const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER' }; | ||||
|  | @ -654,6 +677,16 @@ describe('LokiDatasource', () => { | |||
|           expect(result.refId).toEqual('A'); | ||||
|           expect(result.expr).toEqual('rate({bar="baz"} | logfmt | job=`grafana` [5m])'); | ||||
|         }); | ||||
| 
 | ||||
|         describe('and the filter is already present', () => { | ||||
|           it('then it should remove the filter', () => { | ||||
|             const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt | job="grafana"' }; | ||||
|             const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER' }; | ||||
|             const result = ds.modifyQuery(query, action); | ||||
| 
 | ||||
|             expect(result.refId).toEqual('A'); | ||||
|             expect(result.expr).toEqual('{bar="baz"} | logfmt'); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|  | @ -691,6 +724,19 @@ describe('LokiDatasource', () => { | |||
|           expect(result.refId).toEqual('A'); | ||||
|           expect(result.expr).toEqual('rate({bar="baz", job!="grafana"}[5m])'); | ||||
|         }); | ||||
| 
 | ||||
|         describe('and the opposite filter is present', () => { | ||||
|           it('then it should remove the filter', () => { | ||||
|             const query: LokiQuery = { refId: 'A', expr: '{bar="baz", job="grafana"}' }; | ||||
|             const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER_OUT' }; | ||||
|             const result = ds.modifyQuery(query, action); | ||||
| 
 | ||||
|             expect(result.refId).toEqual('A'); | ||||
|             expect(result.expr).toEqual('{bar="baz", job!="grafana"}'); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('and query has parser', () => { | ||||
|         let ds: LokiDatasource; | ||||
|         beforeEach(() => { | ||||
|  | @ -705,6 +751,7 @@ describe('LokiDatasource', () => { | |||
|           expect(result.refId).toEqual('A'); | ||||
|           expect(result.expr).toEqual('{bar="baz"} | logfmt | job!=`grafana`'); | ||||
|         }); | ||||
| 
 | ||||
|         it('then the correct label should be added for metrics query', () => { | ||||
|           const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' }; | ||||
|           const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER_OUT' }; | ||||
|  | @ -713,6 +760,16 @@ describe('LokiDatasource', () => { | |||
|           expect(result.refId).toEqual('A'); | ||||
|           expect(result.expr).toEqual('rate({bar="baz"} | logfmt | job!=`grafana` [5m])'); | ||||
|         }); | ||||
| 
 | ||||
|         describe('and the filter is already present', () => { | ||||
|           it('then it should remove the filter', () => { | ||||
|             const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt | job="grafana"' }; | ||||
|             const action = { options: { key: 'job', value: 'grafana' }, type: 'ADD_FILTER_OUT' }; | ||||
|             const result = ds.modifyQuery(query, action); | ||||
| 
 | ||||
|             expect(result.refId).toEqual('A'); | ||||
|             expect(result.expr).toEqual('{bar="baz"} | logfmt | job!=`grafana`'); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|  |  | |||
|  | @ -65,6 +65,8 @@ import { | |||
|   addLineFilter, | ||||
|   findLastPosition, | ||||
|   getLabelFilterPositions, | ||||
|   queryHasFilter, | ||||
|   removeLabelFromQuery, | ||||
| } from './modifyQuery'; | ||||
| import { getQueryHints } from './queryHints'; | ||||
| import { runSplitQuery } from './querySplitting'; | ||||
|  | @ -614,13 +616,27 @@ export class LokiDatasource | |||
|       case 'ADD_FILTER': { | ||||
|         if (action.options?.key && action.options?.value) { | ||||
|           const value = escapeLabelValueInSelector(action.options.value); | ||||
|           expression = addLabelToQuery(expression, action.options.key, '=', value); | ||||
| 
 | ||||
|           // This gives the user the ability to toggle a filter on and off.
 | ||||
|           expression = queryHasFilter(expression, action.options.key, '=', value) | ||||
|             ? removeLabelFromQuery(expression, action.options.key, '=', value) | ||||
|             : addLabelToQuery(expression, action.options.key, '=', value); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|       case 'ADD_FILTER_OUT': { | ||||
|         if (action.options?.key && action.options?.value) { | ||||
|           const value = escapeLabelValueInSelector(action.options.value); | ||||
| 
 | ||||
|           /** | ||||
|            * If there is a filter with the same key and value, remove it. | ||||
|            * This prevents the user from seeing no changes in the query when they apply | ||||
|            * this filter. | ||||
|            */ | ||||
|           if (queryHasFilter(expression, action.options.key, '=', value)) { | ||||
|             expression = removeLabelFromQuery(expression, action.options.key, '=', value); | ||||
|           } | ||||
| 
 | ||||
|           expression = addLabelToQuery(expression, action.options.key, '!=', value); | ||||
|         } | ||||
|         break; | ||||
|  |  | |||
|  | @ -6,7 +6,9 @@ import { | |||
|   addNoPipelineErrorToQuery, | ||||
|   addParserToQuery, | ||||
|   NodePosition, | ||||
|   queryHasFilter, | ||||
|   removeCommentsFromQuery, | ||||
|   removeLabelFromQuery, | ||||
| } from './modifyQuery'; | ||||
| 
 | ||||
| describe('addLabelToQuery()', () => { | ||||
|  | @ -246,3 +248,47 @@ describe('NodePosition', () => { | |||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('queryHasFilter', () => { | ||||
|   it.each([ | ||||
|     ['{job="grafana"}', 'grafana'], | ||||
|     ['{job="grafana", foo="bar"}', 'grafana'], | ||||
|     ['{foo="bar", job="grafana"}', 'grafana'], | ||||
|     ['{job="\\"grafana\\""}', '"grafana"'], | ||||
|     ['{foo="bar"} | logfmt | job=`grafana`', 'grafana'], | ||||
|   ])('should return true if query has a positive filter', (query: string, value: string) => { | ||||
|     expect(queryHasFilter(query, 'job', '=', value)).toBe(true); | ||||
|   }); | ||||
| 
 | ||||
|   it.each([ | ||||
|     ['{job!="grafana"}', 'grafana'], | ||||
|     ['{job!="grafana", foo="bar"}', 'grafana'], | ||||
|     ['{foo="bar", job!="grafana"}', 'grafana'], | ||||
|     ['{job!="\\"grafana\\""}', '"grafana"'], | ||||
|     ['{foo="bar"} | logfmt | job!=`grafana`', 'grafana'], | ||||
|   ])('should return true if query has a negative filter', (query: string, value: string) => { | ||||
|     expect(queryHasFilter(query, 'job', '!=', value)).toBe(true); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('removeLabelFromQuery', () => { | ||||
|   it.each([ | ||||
|     ['{job="grafana"}', 'grafana', '{}'], | ||||
|     ['{job="grafana", foo="bar"}', 'grafana', '{foo="bar"}'], | ||||
|     ['{foo="bar", job="grafana"}', 'grafana', '{foo="bar"}'], | ||||
|     ['{job="\\"grafana\\""}', '"grafana"', '{}'], | ||||
|     ['{foo="bar"} | logfmt | job=`grafana`', 'grafana', '{foo="bar"} | logfmt'], | ||||
|   ])('should remove a positive label matcher from the query', (query: string, value: string, expected: string) => { | ||||
|     expect(removeLabelFromQuery(query, 'job', '=', value)).toBe(expected); | ||||
|   }); | ||||
| 
 | ||||
|   it.each([ | ||||
|     ['{job!="grafana"}', 'grafana', '{}'], | ||||
|     ['{job!="grafana", foo="bar"}', 'grafana', '{foo="bar"}'], | ||||
|     ['{foo="bar", job!="grafana"}', 'grafana', '{foo="bar"}'], | ||||
|     ['{job!="\\"grafana\\""}', '"grafana"', '{}'], | ||||
|     ['{foo="bar"} | logfmt | job!=`grafana`', 'grafana', '{foo="bar"} | logfmt'], | ||||
|   ])('should remove a negative label matcher from the query', (query: string, value: string, expected: string) => { | ||||
|     expect(removeLabelFromQuery(query, 'job', '!=', value)).toBe(expected); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { NodeType, SyntaxNode } from '@lezer/common'; | |||
| import { sortBy } from 'lodash'; | ||||
| 
 | ||||
| import { | ||||
|   Identifier, | ||||
|   JsonExpressionParser, | ||||
|   LabelFilter, | ||||
|   LabelParser, | ||||
|  | @ -14,13 +15,14 @@ import { | |||
|   PipelineExpr, | ||||
|   Selector, | ||||
|   UnwrapExpr, | ||||
|   String, | ||||
| } from '@grafana/lezer-logql'; | ||||
| 
 | ||||
| import { QueryBuilderLabelFilter } from '../prometheus/querybuilder/shared/types'; | ||||
| 
 | ||||
| import { unescapeLabelValue } from './languageUtils'; | ||||
| import { LokiQueryModeller } from './querybuilder/LokiQueryModeller'; | ||||
| import { buildVisualQueryFromString } from './querybuilder/parsing'; | ||||
| import { lokiQueryModeller as modeller } from './querybuilder/LokiQueryModeller'; | ||||
| import { buildVisualQueryFromString, handleQuotes } from './querybuilder/parsing'; | ||||
| 
 | ||||
| export class NodePosition { | ||||
|   from: number; | ||||
|  | @ -45,6 +47,88 @@ export class NodePosition { | |||
|     return query.substring(this.from, this.to); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Checks for the presence of a given label=value filter in any Matcher expression in the query. | ||||
|  */ | ||||
| export function queryHasFilter(query: string, key: string, operator: string, value: string): boolean { | ||||
|   const matchers = getMatchersWithFilter(query, key, operator, value); | ||||
|   return matchers.length > 0; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Removes a label=value Matcher expression from the query. | ||||
|  */ | ||||
| export function removeLabelFromQuery(query: string, key: string, operator: string, value: string): string { | ||||
|   const matchers = getMatchersWithFilter(query, key, operator, value); | ||||
|   for (const matcher of matchers) { | ||||
|     query = | ||||
|       matcher.parent?.type.id === LabelFilter ? removeLabelFilter(query, matcher) : removeSelector(query, matcher); | ||||
|   } | ||||
|   return query; | ||||
| } | ||||
| 
 | ||||
| function removeLabelFilter(query: string, matcher: SyntaxNode): string { | ||||
|   const pipelineStage = matcher.parent?.parent; | ||||
|   if (!pipelineStage) { | ||||
|     return query; | ||||
|   } | ||||
|   return (query.substring(0, pipelineStage.from) + query.substring(pipelineStage.to)).trim(); | ||||
| } | ||||
| 
 | ||||
| function removeSelector(query: string, matcher: SyntaxNode): string { | ||||
|   let selector: SyntaxNode | null = matcher; | ||||
|   do { | ||||
|     selector = selector.parent; | ||||
|   } while (selector && selector.type.id !== Selector); | ||||
|   const label = matcher.getChild(Identifier); | ||||
|   if (!selector || !label) { | ||||
|     return query; | ||||
|   } | ||||
|   const labelName = query.substring(label.from, label.to); | ||||
| 
 | ||||
|   const prefix = query.substring(0, selector.from); | ||||
|   const suffix = query.substring(selector.to); | ||||
| 
 | ||||
|   const matchVisQuery = buildVisualQueryFromString(query.substring(selector.from, selector.to)); | ||||
|   matchVisQuery.query.labels = matchVisQuery.query.labels.filter((label) => label.label !== labelName); | ||||
| 
 | ||||
|   return prefix + modeller.renderQuery(matchVisQuery.query) + suffix; | ||||
| } | ||||
| 
 | ||||
| function getMatchersWithFilter(query: string, key: string, operator: string, value: string): SyntaxNode[] { | ||||
|   const tree = parser.parse(query); | ||||
|   const matchers: SyntaxNode[] = []; | ||||
|   tree.iterate({ | ||||
|     enter: ({ type, node }): void => { | ||||
|       if (type.id === Matcher) { | ||||
|         matchers.push(node); | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|   return matchers.filter((matcher) => { | ||||
|     const labelNode = matcher.getChild(Identifier); | ||||
|     const opNode = labelNode?.nextSibling; | ||||
|     const valueNode = matcher.getChild(String); | ||||
|     if (!labelNode || !opNode || !valueNode) { | ||||
|       return false; | ||||
|     } | ||||
|     const labelName = query.substring(labelNode.from, labelNode.to); | ||||
|     if (labelName !== key) { | ||||
|       return false; | ||||
|     } | ||||
|     const labelValue = query.substring(valueNode.from, valueNode.to); | ||||
|     if (handleQuotes(labelValue) !== unescapeLabelValue(value)) { | ||||
|       return false; | ||||
|     } | ||||
|     const labelOperator = query.substring(opNode.from, opNode.to); | ||||
|     if (labelOperator !== operator) { | ||||
|       return false; | ||||
|     } | ||||
|     return true; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Adds label filter to existing query. Useful for query modification for example for ad hoc filters. | ||||
|  * | ||||
|  | @ -65,6 +149,10 @@ export function addLabelToQuery(query: string, key: string, operator: string, va | |||
|   } | ||||
| 
 | ||||
|   const streamSelectorPositions = getStreamSelectorPositions(query); | ||||
|   if (!streamSelectorPositions.length) { | ||||
|     return query; | ||||
|   } | ||||
| 
 | ||||
|   const hasStreamSelectorMatchers = getMatcherInStreamPositions(query); | ||||
|   const everyStreamSelectorHasMatcher = streamSelectorPositions.every((streamSelectorPosition) => | ||||
|     hasStreamSelectorMatchers.some( | ||||
|  | @ -74,9 +162,6 @@ export function addLabelToQuery(query: string, key: string, operator: string, va | |||
|   ); | ||||
|   const parserPositions = getParserPositions(query); | ||||
|   const labelFilterPositions = getLabelFilterPositions(query); | ||||
|   if (!streamSelectorPositions.length) { | ||||
|     return query; | ||||
|   } | ||||
| 
 | ||||
|   const filter = toLabelFilter(key, value, operator); | ||||
|   // If we have non-empty stream selector and parser/label filter, we want to add a new label filter after the last one.
 | ||||
|  | @ -103,6 +188,9 @@ export function addParserToQuery(query: string, parser: string): string { | |||
|     return addParser(query, lineFilterPositions, parser); | ||||
|   } else { | ||||
|     const streamSelectorPositions = getStreamSelectorPositions(query); | ||||
|     if (!streamSelectorPositions.length) { | ||||
|       return query; | ||||
|     } | ||||
|     return addParser(query, streamSelectorPositions, parser); | ||||
|   } | ||||
| } | ||||
|  | @ -302,13 +390,11 @@ function addFilterToStreamSelector( | |||
|   vectorSelectorPositions: NodePosition[], | ||||
|   filter: QueryBuilderLabelFilter | ||||
| ): string { | ||||
|   const modeller = new LokiQueryModeller(); | ||||
|   let newQuery = ''; | ||||
|   let prev = 0; | ||||
| 
 | ||||
|   for (let i = 0; i < vectorSelectorPositions.length; i++) { | ||||
|     // This is basically just doing splice on a string for each matched vector selector.
 | ||||
| 
 | ||||
|     const match = vectorSelectorPositions[i]; | ||||
|     const isLast = i === vectorSelectorPositions.length - 1; | ||||
| 
 | ||||
|  | @ -421,6 +507,9 @@ function addLabelFormat( | |||
| 
 | ||||
| export function addLineFilter(query: string): string { | ||||
|   const streamSelectorPositions = getStreamSelectorPositions(query); | ||||
|   if (!streamSelectorPositions.length) { | ||||
|     return query; | ||||
|   } | ||||
|   const streamSelectorEnd = streamSelectorPositions[0].to; | ||||
| 
 | ||||
|   const newQueryExpr = query.slice(0, streamSelectorEnd) + ' |= ``' + query.slice(streamSelectorEnd); | ||||
|  |  | |||
|  | @ -597,7 +597,7 @@ function isIntervalVariableError(node: SyntaxNode) { | |||
|   return node?.parent?.type.id === Range; | ||||
| } | ||||
| 
 | ||||
| function handleQuotes(string: string) { | ||||
| export function handleQuotes(string: string) { | ||||
|   if (string[0] === `"` && string[string.length - 1] === `"`) { | ||||
|     return string | ||||
|       .substring(1, string.length - 1) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue