Browse Source

Feature: Case insensitive Loki search (#15948)

* Case insensitive Loki search
* Make Loki case insensitivity work with highlighting

Signed-off-by: Steven Sheehy <ssheehy@firescope.com>
Steven Sheehy 6 years ago
parent
commit
0bc314a47b

+ 34 - 1
public/app/core/utils/text.test.ts

@@ -1,4 +1,4 @@
-import { findMatchesInText } from './text';
+import { findMatchesInText, parseFlags } from './text';
 
 
 describe('findMatchesInText()', () => {
 describe('findMatchesInText()', () => {
   it('gets no matches for when search and or line are empty', () => {
   it('gets no matches for when search and or line are empty', () => {
@@ -32,4 +32,37 @@ describe('findMatchesInText()', () => {
     expect(findMatchesInText('foo foo bar', '(')).toEqual([]);
     expect(findMatchesInText('foo foo bar', '(')).toEqual([]);
     expect(findMatchesInText('foo foo bar', '(foo|')).toEqual([]);
     expect(findMatchesInText('foo foo bar', '(foo|')).toEqual([]);
   });
   });
+
+  test('should parse and use flags', () => {
+    expect(findMatchesInText(' foo FOO bar ', '(?i)foo')).toEqual([
+      { length: 3, start: 1, text: 'foo', end: 4 },
+      { length: 3, start: 5, text: 'FOO', end: 8 },
+    ]);
+    expect(findMatchesInText(' foo FOO bar ', '(?i)(?-i)foo')).toEqual([{ length: 3, start: 1, text: 'foo', end: 4 }]);
+    expect(findMatchesInText('FOO\nfoobar\nbar', '(?ims)^foo.')).toEqual([
+      { length: 4, start: 0, text: 'FOO\n', end: 4 },
+      { length: 4, start: 4, text: 'foob', end: 8 },
+    ]);
+    expect(findMatchesInText('FOO\nfoobar\nbar', '(?ims)(?-smi)^foo.')).toEqual([]);
+  });
+});
+
+describe('parseFlags()', () => {
+  it('when no flags or text', () => {
+    expect(parseFlags('')).toEqual({ cleaned: '', flags: 'g' });
+    expect(parseFlags('(?is)')).toEqual({ cleaned: '', flags: 'gis' });
+    expect(parseFlags('foo')).toEqual({ cleaned: 'foo', flags: 'g' });
+  });
+
+  it('when flags present', () => {
+    expect(parseFlags('(?i)foo')).toEqual({ cleaned: 'foo', flags: 'gi' });
+    expect(parseFlags('(?ims)foo')).toEqual({ cleaned: 'foo', flags: 'gims' });
+  });
+
+  it('when flags cancel each other', () => {
+    expect(parseFlags('(?i)(?-i)foo')).toEqual({ cleaned: 'foo', flags: 'g' });
+    expect(parseFlags('(?i-i)foo')).toEqual({ cleaned: 'foo', flags: 'g' });
+    expect(parseFlags('(?is)(?-ims)foo')).toEqual({ cleaned: 'foo', flags: 'g' });
+    expect(parseFlags('(?i)(?-i)(?i)foo')).toEqual({ cleaned: 'foo', flags: 'gi' });
+  });
 });
 });

+ 31 - 2
public/app/core/utils/text.ts

@@ -22,10 +22,10 @@ export function findMatchesInText(haystack: string, needle: string): TextMatch[]
     return [];
     return [];
   }
   }
   const matches = [];
   const matches = [];
-  const cleaned = cleanNeedle(needle);
+  const { cleaned, flags } = parseFlags(cleanNeedle(needle));
   let regexp: RegExp;
   let regexp: RegExp;
   try {
   try {
-    regexp = new RegExp(`(?:${cleaned})`, 'g');
+    regexp = new RegExp(`(?:${cleaned})`, flags);
   } catch (error) {
   } catch (error) {
     return matches;
     return matches;
   }
   }
@@ -44,6 +44,35 @@ export function findMatchesInText(haystack: string, needle: string): TextMatch[]
   return matches;
   return matches;
 }
 }
 
 
+const CLEAR_FLAG = '-';
+const FLAGS_REGEXP = /\(\?([ims-]+)\)/g;
+
+/**
+ * Converts any mode modifers in the text to the Javascript equivalent flag
+ */
+export function parseFlags(text: string): { cleaned: string; flags: string } {
+  const flags: Set<string> = new Set(['g']);
+
+  const cleaned = text.replace(FLAGS_REGEXP, (str, group) => {
+    const clearAll = group.startsWith(CLEAR_FLAG);
+
+    for (let i = 0; i < group.length; ++i) {
+      const flag = group.charAt(i);
+      if (clearAll || group.charAt(i - 1) === CLEAR_FLAG) {
+        flags.delete(flag);
+      } else if (flag !== CLEAR_FLAG) {
+        flags.add(flag);
+      }
+    }
+    return ''; // Remove flag from text
+  });
+
+  return {
+    cleaned: cleaned,
+    flags: Array.from(flags).join(''),
+  };
+}
+
 const XSSWL = Object.keys(xss.whiteList).reduce((acc, element) => {
 const XSSWL = Object.keys(xss.whiteList).reduce((acc, element) => {
   acc[element] = xss.whiteList[element].concat(['class', 'style']);
   acc[element] = xss.whiteList[element].concat(['class', 'style']);
   return acc;
   return acc;

+ 5 - 5
public/app/plugins/datasource/loki/query_utils.test.ts

@@ -11,7 +11,7 @@ describe('parseQuery', () => {
   it('returns regexp for strings without query', () => {
   it('returns regexp for strings without query', () => {
     expect(parseQuery('test')).toEqual({
     expect(parseQuery('test')).toEqual({
       query: '',
       query: '',
-      regexp: 'test',
+      regexp: '(?i)test',
     });
     });
   });
   });
 
 
@@ -25,14 +25,14 @@ describe('parseQuery', () => {
   it('returns query for strings with query and search string', () => {
   it('returns query for strings with query and search string', () => {
     expect(parseQuery('x {foo="bar"}')).toEqual({
     expect(parseQuery('x {foo="bar"}')).toEqual({
       query: '{foo="bar"}',
       query: '{foo="bar"}',
-      regexp: 'x',
+      regexp: '(?i)x',
     });
     });
   });
   });
 
 
   it('returns query for strings with query and regexp', () => {
   it('returns query for strings with query and regexp', () => {
     expect(parseQuery('{foo="bar"} x|y')).toEqual({
     expect(parseQuery('{foo="bar"} x|y')).toEqual({
       query: '{foo="bar"}',
       query: '{foo="bar"}',
-      regexp: 'x|y',
+      regexp: '(?i)x|y',
     });
     });
   });
   });
 
 
@@ -46,11 +46,11 @@ describe('parseQuery', () => {
   it('returns query and regexp with quantifiers', () => {
   it('returns query and regexp with quantifiers', () => {
     expect(parseQuery('{foo="bar"} \\.java:[0-9]{1,5}')).toEqual({
     expect(parseQuery('{foo="bar"} \\.java:[0-9]{1,5}')).toEqual({
       query: '{foo="bar"}',
       query: '{foo="bar"}',
-      regexp: '\\.java:[0-9]{1,5}',
+      regexp: '(?i)\\.java:[0-9]{1,5}',
     });
     });
     expect(parseQuery('\\.java:[0-9]{1,5} {foo="bar"}')).toEqual({
     expect(parseQuery('\\.java:[0-9]{1,5} {foo="bar"}')).toEqual({
       query: '{foo="bar"}',
       query: '{foo="bar"}',
-      regexp: '\\.java:[0-9]{1,5}',
+      regexp: '(?i)\\.java:[0-9]{1,5}',
     });
     });
   });
   });
 });
 });

+ 4 - 0
public/app/plugins/datasource/loki/query_utils.ts

@@ -1,4 +1,5 @@
 const selectorRegexp = /(?:^|\s){[^{]*}/g;
 const selectorRegexp = /(?:^|\s){[^{]*}/g;
+const caseInsensitive = '(?i)'; // Golang mode modifier for Loki, doesn't work in JavaScript
 export function parseQuery(input: string) {
 export function parseQuery(input: string) {
   input = input || '';
   input = input || '';
   const match = input.match(selectorRegexp);
   const match = input.match(selectorRegexp);
@@ -10,6 +11,9 @@ export function parseQuery(input: string) {
     regexp = input.replace(selectorRegexp, '').trim();
     regexp = input.replace(selectorRegexp, '').trim();
   }
   }
 
 
+  if (regexp) {
+    regexp = caseInsensitive + regexp;
+  }
   return { query, regexp };
   return { query, regexp };
 }
 }