Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Line filters: Allow backticks #992

Merged
merged 88 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
a4aad04
chore: add case sensitive line filter state to local storage
gtk-grafana Dec 12, 2024
516929b
chore: cleanup
gtk-grafana Dec 12, 2024
4239206
chore: cleanuop
gtk-grafana Dec 12, 2024
91e2ac0
chore: wip
gtk-grafana Dec 12, 2024
0bb19d0
chore: dont debounce case sensitive toggle
gtk-grafana Dec 12, 2024
e381752
chore: rename local storage
gtk-grafana Dec 12, 2024
e28fa96
chore: wip
gtk-grafana Dec 12, 2024
6c8bb6c
chore: spellcheck
gtk-grafana Dec 12, 2024
3a6c628
Merge branch 'gtk-grafana/issues/952/line-filter-ui-updates__case-sen…
gtk-grafana Dec 12, 2024
073c242
feat: add regex line filter button
gtk-grafana Dec 12, 2024
d60261d
chore: clean up
gtk-grafana Dec 12, 2024
4c2ef58
test: add unit test coverage
gtk-grafana Dec 12, 2024
656c1b9
chore: remove uncessary runtime check
gtk-grafana Dec 13, 2024
2d7540d
Merge branch 'gtk-grafana/issues/952/line-filter-ui-updates__case-sen…
gtk-grafana Dec 13, 2024
e442ad0
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 13, 2024
607fcc2
chore: remove uncessary runtime typecheck
gtk-grafana Dec 13, 2024
1cd6621
chore: rename, make buttons
gtk-grafana Dec 13, 2024
3c144b4
refactor: clean up
gtk-grafana Dec 13, 2024
77f7975
feat: migrate line filter variable to new ad-hoc-variable - PoC - WIP
gtk-grafana Dec 13, 2024
9a435c9
chore: todo list
gtk-grafana Dec 13, 2024
ae042d8
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 13, 2024
86a7e91
wip
gtk-grafana Dec 16, 2024
0854122
test: fix e2e, unit tests
gtk-grafana Dec 16, 2024
e8daca4
chore: prevent duplicate queries by extending AdHocFiltersVariable
gtk-grafana Dec 16, 2024
a32694c
test: update tests
gtk-grafana Dec 16, 2024
6c9c120
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 17, 2024
9545fd4
test: fix test assertions
gtk-grafana Dec 17, 2024
a694d91
chore: fix bug generating logQL, add unit test coverage
gtk-grafana Dec 17, 2024
57f4168
chore: add negative line filter option to logs panel
gtk-grafana Dec 17, 2024
8171385
chore: add tooltips, tweak copy
gtk-grafana Dec 17, 2024
2562fbe
chore: copy tweak
gtk-grafana Dec 18, 2024
7035bea
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 18, 2024
160cdc7
chore: functional wip state
gtk-grafana Dec 19, 2024
10850fc
chore: remove only
gtk-grafana Dec 19, 2024
b172211
chore: hide variable
gtk-grafana Dec 19, 2024
3892b7b
chore: clear pending line filter on nav
gtk-grafana Dec 19, 2024
066ebe3
chore: clean up CustomAdHocFiltersVariable
gtk-grafana Dec 19, 2024
363a07a
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 19, 2024
f36015f
chore: fix clear
gtk-grafana Dec 19, 2024
406a7f9
chore: fix bad interpolation
gtk-grafana Dec 19, 2024
456641c
chore: ui review updates, fix breakdown queries
gtk-grafana Dec 19, 2024
4ad627d
chore: clean up
gtk-grafana Dec 19, 2024
4934aaf
chore: add loading state while debouncing
gtk-grafana Dec 19, 2024
4b2e826
chore: facepalm
gtk-grafana Dec 19, 2024
79f338a
chore: fix debounce bugs, style tweaks, submit on enter
gtk-grafana Dec 20, 2024
fa4aa56
chore: mock debounce cancel
gtk-grafana Dec 20, 2024
b1d7f28
chore: update variable layout to match logQL query
gtk-grafana Dec 20, 2024
2bf483e
chore: clean up variable layout styles
gtk-grafana Dec 20, 2024
8c042a0
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 20, 2024
18258c6
chore: upgrade scenes, fix duplicate query bug, fix layout
gtk-grafana Dec 20, 2024
2c93275
fix: flush debounce on submit
gtk-grafana Dec 20, 2024
cfcf1af
chore: fix migration, add e2e tests
gtk-grafana Dec 20, 2024
cb33cb3
chore: remove stale comment
gtk-grafana Dec 20, 2024
839374d
chore: cancel ongoing debounce updates on clear
gtk-grafana Dec 20, 2024
9993038
chore: clean up & document
gtk-grafana Jan 2, 2025
636cce5
chore: refactor line filters, new directory, split out react component
gtk-grafana Jan 2, 2025
32e9d5c
chore: move RegexIconButton
gtk-grafana Jan 2, 2025
2fe440f
chore: unify sorting
gtk-grafana Jan 2, 2025
482c6d8
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 2, 2025
38b27f0
chore: rename case sensitivity button
gtk-grafana Jan 2, 2025
63117c7
chore: update button active state
gtk-grafana Jan 2, 2025
922b53a
chore: fix spacing between toggle buttons and close button
gtk-grafana Jan 2, 2025
7afc9c6
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 6, 2025
9dbd5af
chore: change copy
gtk-grafana Jan 6, 2025
d93210a
chore: remove only
gtk-grafana Jan 6, 2025
5391f89
test: update copy
gtk-grafana Jan 6, 2025
2588618
Merge branch 'main' into gtk-grafana/issues/952/line-filter-ui-update…
gtk-grafana Jan 6, 2025
613d810
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 8, 2025
0055aad
chore: clean up
gtk-grafana Jan 8, 2025
c1d9c5e
chore: add log panel filters to top level var
gtk-grafana Jan 8, 2025
67ff076
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 8, 2025
db09f1a
chore: remove only
gtk-grafana Jan 8, 2025
0c1b45d
fix: remove onChange behavior, only run queries on button submit
gtk-grafana Jan 8, 2025
4178c04
chore: remove include/exclude dropdown, add buttons
gtk-grafana Jan 8, 2025
fbf1b77
Update src/Components/ServiceScene/LogsListScene.tsx
gtk-grafana Jan 9, 2025
511c2eb
chore: run detected_fields on line filters change
gtk-grafana Jan 9, 2025
1d87371
feat: validate backticks in filter expr
gtk-grafana Jan 9, 2025
7391318
chore: remove commented line
gtk-grafana Jan 9, 2025
bf6db6a
feat: allow backticks in line-filters
gtk-grafana Jan 9, 2025
f7b4e0a
chore: spellcheck
gtk-grafana Jan 9, 2025
caa73cb
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/98…
gtk-grafana Jan 9, 2025
891f68c
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/98…
gtk-grafana Jan 9, 2025
b919ae7
chore: remove console.oog
gtk-grafana Jan 9, 2025
0279271
Update src/services/query.ts
gtk-grafana Jan 10, 2025
2208d36
Merge branch 'gtk-grafana/issues/989/line-filters-validation' of http…
gtk-grafana Jan 10, 2025
97f6502
chore: remove unused import
gtk-grafana Jan 10, 2025
bf715a8
Update src/services/query.ts
gtk-grafana Jan 10, 2025
5c800eb
chore: always wrap line filters in double quotes
gtk-grafana Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,4 @@ pagedown
linefilter
queryfrontend
timesync
openmetrics
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ export function LineFilterEditor({
const getStyles = (theme: GrafanaTheme2) => ({
inputNoBorderRight: css({
input: {
borderRight: 'none',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
Expand All @@ -131,6 +130,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
justifyContent: 'center',
}),
includeButton: css({
borderLeft: 'none',
borderRadius: 0,
borderRight: 'none',
'&[disabled]': {
Expand Down
86 changes: 76 additions & 10 deletions src/services/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe('renderLogQLFieldFilters', () => {
);
});
});
describe('renderLogQLLineFilter', () => {
describe('renderLogQLLineFilter not containing backticks', () => {
// REGEXP ops
test('Renders positive case-insensitive regex', () => {
const filters: AdHocVariableFilter[] = [
Expand All @@ -164,7 +164,18 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('|~ `(?i).(search`');
expect(renderLogQLLineFilter(filters)).toEqual('|~ "(?i).(search"');
});
test('Renders positive case-insensitive regex with newline', () => {
const filters: AdHocVariableFilter[] = [
{
key: LineFilterCaseSensitive.caseInsensitive,
operator: LineFilterOp.regex,
value: '\nThe "key" field',
},
];

expect(renderLogQLLineFilter(filters)).toEqual('|~ "(?i)\\nThe \\"key\\" field"');
});
test('Renders positive case-sensitive regex', () => {
const filters: AdHocVariableFilter[] = [
Expand All @@ -175,7 +186,7 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('|~ `\\w+`');
expect(renderLogQLLineFilter(filters)).toEqual('|~ "\\\\w+"');
});
test('Renders negative case-sensitive regex', () => {
const filters: AdHocVariableFilter[] = [
Expand All @@ -186,7 +197,7 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('!~ `\\w+`');
expect(renderLogQLLineFilter(filters)).toEqual('!~ "\\\\w+"');
});
test('Renders negative case-insensitive regex', () => {
const filters: AdHocVariableFilter[] = [
Expand All @@ -197,7 +208,7 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('!~ `(?i)\\w+`');
expect(renderLogQLLineFilter(filters)).toEqual('!~ "(?i)\\\\w+"');
});

// String contains ops
Expand All @@ -210,7 +221,7 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('|~ `(?i)\\.\\(search`');
expect(renderLogQLLineFilter(filters)).toEqual('|~ "(?i)\\\\.\\\\(search"');
});
test('Renders positive case-sensitive string compare', () => {
const filters: AdHocVariableFilter[] = [
Expand All @@ -221,7 +232,7 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('|= `.(search`');
expect(renderLogQLLineFilter(filters)).toEqual('|= ".(search"');
});
test('Renders negative case-insensitive string compare', () => {
const filters: AdHocVariableFilter[] = [
Expand All @@ -232,7 +243,7 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('!~ `(?i)\\.\\(search`');
expect(renderLogQLLineFilter(filters)).toEqual('!~ "(?i)\\\\.\\\\(search"');
});
test('Renders negative case-sensitive string compare', () => {
const filters: AdHocVariableFilter[] = [
Expand All @@ -243,10 +254,65 @@ describe('renderLogQLLineFilter', () => {
},
];

expect(renderLogQLLineFilter(filters)).toEqual('!= `.(search`');
expect(renderLogQLLineFilter(filters)).toEqual('!= ".(search"');
});
});
describe('renderLogQLLineFilter containing backticks', () => {
// Keep in mind we see twice as many escape chars in the test code as we do IRL
test('Renders positive case-insensitive regex with newline', () => {
const filters: AdHocVariableFilter[] = [
{
key: LineFilterCaseSensitive.caseInsensitive,
operator: LineFilterOp.regex,
// If a log line contains a newline as a string, they will need to escape the escape char and type "\\n" in the field input, otherwise loki will match actual newlines with regex searches
value: '\\\\nThe `key` field', // the user enters: \\nThe `key` field
},
];
expect(renderLogQLLineFilter(filters)).toEqual('|~ "(?i)\\\\\\\\nThe `key` field"');
});
test('Renders positive case-sensitive regex with newline', () => {
const filters: AdHocVariableFilter[] = [
{
key: LineFilterCaseSensitive.caseSensitive,
operator: LineFilterOp.regex,
value: '\\\\nThe `key` field', // the user enters: \\nThe `key` field
},
];
expect(renderLogQLLineFilter(filters)).toEqual('|~ "\\\\\\\\nThe `key` field"');
});
test('Renders positive case-insensitive match with newline', () => {
const filters: AdHocVariableFilter[] = [
{
key: LineFilterCaseSensitive.caseInsensitive,
operator: LineFilterOp.match,
value: '\\nThe `key` field', // the user enters: \nThe `key` field
},
];
expect(renderLogQLLineFilter(filters)).toEqual(`|~ "(?i)\\\\\\\\nThe \`key\` field"`);
});
test('Renders positive case-sensitive match with newline', () => {
const filters: AdHocVariableFilter[] = [
{
key: LineFilterCaseSensitive.caseSensitive,
operator: LineFilterOp.match,
value: '\\nThe `key` field', // the user enters: \nThe `key` field
},
];
expect(renderLogQLLineFilter(filters)).toEqual('|= "\\\\nThe `key` field"');
});
test('Renders positive case-insensitive regex', () => {
const filters: AdHocVariableFilter[] = [
{
key: LineFilterCaseSensitive.caseInsensitive,
operator: LineFilterOp.regex,
value: `^level=[error|warning].+((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}:\\d{5}"$|\``, // the user enters ^level=[error|warning].+((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}:\d{5}"$|`
},
];
expect(renderLogQLLineFilter(filters)).toEqual(
'|~ "(?i)^level=[error|warning].+((25[0-5]|(2[0-4]|1\\\\d|[1-9]|)\\\\d)\\\\.?\\\\b){4}:\\\\d{5}\\"$|`"'
);
});
});

describe('renderLogQLLabelFilters', () => {
test('Renders positive filters', () => {
const filters: AdHocVariableFilter[] = [
Expand Down
70 changes: 53 additions & 17 deletions src/services/query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AdHocVariableFilter } from '@grafana/data';
import { AppliedPattern, numericOperatorArray } from 'Components/IndexScene/IndexScene';
import { EMPTY_VARIABLE_VALUE, VAR_DATASOURCE_EXPR } from './variables';
import { escapeRegExp, groupBy, trim } from 'lodash';
import { groupBy, trim } from 'lodash';
import { getValueFromFieldsFilter } from './variableGetters';
import { LokiQuery } from './lokiQuery';
import { SceneDataQueryResourceRequest, SceneDataQueryResourceRequestOptions } from './datasourceTypes';
Expand Down Expand Up @@ -120,34 +120,48 @@ export function renderLogQLFieldFilters(filters: AdHocVariableFilter[]) {
return `${positiveFilters} ${negativeFilters} ${numericFilters}`.trim();
}

export function escapeDoubleQuotedLineFilter(filter: AdHocFilterWithLabels) {
gtk-grafana marked this conversation as resolved.
Show resolved Hide resolved
// Is not regex
if (filter.operator === LineFilterOp.match || filter.operator === LineFilterOp.negativeMatch) {
if (filter.key === LineFilterCaseSensitive.caseInsensitive) {
return escapeLabelValueInRegexSelector(filter.value);
} else {
return escapeLabelValueInExactSelector(filter.value);
}
} else {
return escapeLabelValueInExactSelector(filter.value);
}
}

function buildLogQlLineFilter(filter: AdHocFilterWithLabels, value: string) {
// Change operator if needed and insert caseInsensitive flag
if (filter.key === LineFilterCaseSensitive.caseInsensitive) {
if (filter.operator === LineFilterOp.negativeRegex || filter.operator === LineFilterOp.negativeMatch) {
return `${LineFilterOp.negativeRegex} "(?i)${value}"`;
}
return `${LineFilterOp.regex} "(?i)${value}"`;
}

return `${filter.operator} "${value}"`;
}

/**
* Converts line filter ad-hoc filters to LogQL
*
* the filter key is LineFilterCaseSensitive
* the filter operator is LineFilterOp
* the value is the user in put
* the value is the user input
*/
export function renderLogQLLineFilter(filters: AdHocFilterWithLabels[]) {
sortLineFilters(filters);
return filters
.map((f) => {
if (f.value === '') {
.map((filter) => {
if (filter.value === '') {
return '';
}
const value =
(f.operator === LineFilterOp.match || f.operator === LineFilterOp.negativeMatch) &&
f.key === LineFilterCaseSensitive.caseInsensitive
? escapeRegExp(f.value)
: f.value;

if (f.key === LineFilterCaseSensitive.caseInsensitive) {
if (f.operator === LineFilterOp.negativeRegex || f.operator === LineFilterOp.negativeMatch) {
return `${LineFilterOp.negativeRegex} \`(?i)${value}\``;
}
return `${LineFilterOp.regex} \`(?i)${value}\``;
}

return `${f.operator} \`${value}\``;
const value = escapeDoubleQuotedLineFilter(filter);
return buildLogQlLineFilter(filter, value);
})
.join(' ');
}
Expand Down Expand Up @@ -264,3 +278,25 @@ export function sanitizeStreamSelector(expression: string) {

// default line limit; each data source can define it's own line limit too
export const LINE_LIMIT = 1000;

// Taken from /grafana/grafana/public/app/plugins/datasource/loki/languageUtils.ts

// 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, '\\"');
}

// Loki 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 meta characters is: *+?()|\.[]{}^$
// we make a javascript regular expression that matches those characters:
const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g;
function escapeLokiRegexp(value: string): string {
return value.replace(RE2_METACHARACTERS, '\\$&');
}
export function escapeLabelValueInRegexSelector(labelValue: string): string {
return escapeLabelValueInExactSelector(escapeLokiRegexp(labelValue));
}
Loading