Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 11 additions & 9 deletions .github/workflows/pr-test-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,21 @@ jobs:
? `**Source:** Fork (\`${{ needs.check-permissions.outputs.pr-repo }}\`)`
: `**Source:** Branch (\`${{ needs.check-permissions.outputs.pr-ref }}\`)`;

const comment = `
## 🚀 Build Started

**PR:** #${{ needs.check-permissions.outputs.pr-number }}
${sourceInfo}
**Triggered by:** @${{ github.actor }}

[⏳ View build progress](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
`;

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ needs.check-permissions.outputs.pr-number }},
body: `
## 🚀 Build Started

**PR:** #${{ needs.check-permissions.outputs.pr-number }}
${sourceInfo}
**Triggered by:** @${{ github.actor }}

[⏳ View build progress](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
`
body: comment
});

- name: Check out PR code
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

* BUGFIX: fix setting `extra_stream_filters` param to `Custom query parameters`. See [this comment](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/405#issuecomment-3420418177).
* BUGFIX: add `sort by (_time) asc/desc` pipe if logs are sorted in asc/desc order. In versions of grafana below `12.x.x`, you need to manually run the query if the sorting has been changed. See [#379](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/379).
* fix the issue of overwriting the `level` label. Set the calculated level into a `detected_level` label, which is supported only in Grafana version 11.0.8 and above.
* BUGFIX: fix the issue of overwriting the `level` label. Set the calculated level into a `detected_level` label, which is supported only in Grafana version 11.0.8 and above. See [#425](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/425).
* BUGFIX: fix interpolation of multi-value query variables by mapping `filterName:$filterVar` and `filterName:=$filterVar` to the `filerName:in("v1", ..., "vN")`. Support negative operators and stream tags(`{tag = $var}` to `{tag in($var)}`). Disallow interpolation in regexp with variables (e.g., `field:~$var`). See [#238](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/238).

## v0.21.1

Expand Down
83 changes: 83 additions & 0 deletions src/LogsQL/regExpOperator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getQueryExprVariableRegExp } from './regExpOperator';

describe('getQueryExprVariableRegExp', () => {
it('should fing fieldName:~$var', () => {
const result = getQueryExprVariableRegExp(' fieldName:~$var ');
expect(result?.[0]).toEqual(' fieldName:~$var');
});

it('should find fieldName:~$var without spaces', () => {
const result = getQueryExprVariableRegExp('fieldName:~$var');
expect(result?.[0]).toEqual('fieldName:~$var');
});

it('should find fieldName:~$var at the start of string', () => {
const result = getQueryExprVariableRegExp('fieldName:~$var and more');
expect(result?.[0]).toEqual('fieldName:~$var');
});

it('should find fieldName:~$var at the end of string', () => {
const result = getQueryExprVariableRegExp('some query fieldName:~$var');
expect(result?.[0]).toEqual(' fieldName:~$var');
});

it('should find variable with underscores', () => {
const result = getQueryExprVariableRegExp('fieldName:~$my_var_name');
expect(result?.[0]).toEqual('fieldName:~$my_var_name');
});

it('should find variable with numbers', () => {
const result = getQueryExprVariableRegExp('fieldName:~$var123');
expect(result?.[0]).toEqual('fieldName:~$var123');
});

it('should find complex field name with variable', () => {
const result = getQueryExprVariableRegExp('complex_field_name:~$variable');
expect(result?.[0]).toEqual('complex_field_name:~$variable');
});

it('should return null for query without variables', () => {
const result = getQueryExprVariableRegExp('fieldName:value');
expect(result).toBeNull();
});

it('should return null for empty string', () => {
const result = getQueryExprVariableRegExp('');
expect(result).toBeNull();
});

it('should return null for string with only spaces', () => {
const result = getQueryExprVariableRegExp(' ');
expect(result).toBeNull();
});

it('should find first variable when multiple variables exist', () => {
const result = getQueryExprVariableRegExp('field1:~$var1 field2:~$var2');
expect(result?.[0]).toEqual('field1:~$var1');
});

it('should find variable with special characters in field name', () => {
const result = getQueryExprVariableRegExp('field-name:~$var');
expect(result?.[0]).toEqual('field-name:~$var');
});

it('should not handle variable without field name prefix', () => {
const result = getQueryExprVariableRegExp('~$var');
expect(result).toBeNull();
});

it('should handle multiple spaces around variable', () => {
const result = getQueryExprVariableRegExp(' fieldName:~$var ');
expect(result?.[0]).toEqual(' fieldName:~$var');
});

it('should find variable with mixed case field name', () => {
const result = getQueryExprVariableRegExp('fieldName:~$MyVariable');
expect(result?.[0]).toEqual('fieldName:~$MyVariable');
});

it('should find variable with dot separated field name', () => {
const result = getQueryExprVariableRegExp('someField: Some value | field.name.with.some.dot.separated:~$MyVariable');
expect(result?.[0]).toEqual(' field.name.with.some.dot.separated:~$MyVariable');
});
});
8 changes: 8 additions & 0 deletions src/LogsQL/regExpOperator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Regular expression for matching variables in a query expression -> filterName:~"$VariableName"
* */
const variableRegExp = /(^|\||\s)([^\s:~]+)\s*:\s*~\s*(?:"\$([A-Za-z0-9_.-]+)"|\$([A-Za-z0-9_.-]+)|"([^"]+)"|([^\s|]+))(?=\s|\||$)/;

export function getQueryExprVariableRegExp(queryExpr: string) {
return variableRegExp.exec(queryExpr);
}
19 changes: 17 additions & 2 deletions src/components/QueryEditor/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { css } from "@emotion/css";
import { isEqual } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';

import { CoreApp, GrafanaTheme2, LoadingState } from '@grafana/data';
import { Button, ConfirmModal, useStyles2 } from '@grafana/ui';

import { getQueryExprVariableRegExp } from "../../LogsQL/regExpOperator";
import { isExprHasStatsPipeFunctions } from "../../LogsQL/statsPipeFunctions";
import { storeKeys } from "../../store/constants";
import store from "../../store/store";
Expand All @@ -16,21 +17,26 @@ import { QueryBuilderContainer } from "./QueryBuilder/QueryBuilderContainer";
import { QueryEditorModeToggle } from "./QueryBuilder/QueryEditorModeToggle";
import { buildVisualQueryFromString } from "./QueryBuilder/utils/parseFromString";
import QueryCodeEditor from "./QueryCodeEditor";
import { QueryEditorHelp } from "./QueryEditorHelp";
import { QueryEditorOptions } from "./QueryEditorOptions";
import QueryEditorVariableRegexpError from "./QueryEditorVariableRegexpError";
import VmuiLink from "./VmuiLink";
import { changeEditorMode, getQueryWithDefaults } from "./state";

const QueryEditor = React.memo<VictoriaLogsQueryEditorProps>((props) => {
const styles = useStyles2(getStyles);

const { onChange, onRunQuery, data, app, queries, datasource, range: timeRange } = props;
const { onChange, onRunQuery: runQuery, data, app, queries, datasource, range: timeRange } = props;
const [dataIsStale, setDataIsStale] = useState(false);
const [parseModalOpen, setParseModalOpen] = useState(false);

const query = getQueryWithDefaults(props.query, app, data?.request?.panelPluginId);
const editorMode = query.editorMode!;
const isStatsQuery = query.queryType === QueryType.Stats || query.queryType === QueryType.StatsRange;
const showStatsWarn = isStatsQuery && !isExprHasStatsPipeFunctions(query.expr || '');
const varRegExp= useMemo(() => {
return getQueryExprVariableRegExp(query.expr)?.[0] || null;
}, [query.expr]);

const onEditorModeChange = useCallback((newEditorMode: QueryEditorMode) => {
if (newEditorMode === QueryEditorMode.Builder) {
Expand All @@ -56,6 +62,13 @@ const QueryEditor = React.memo<VictoriaLogsQueryEditorProps>((props) => {
onChange(query);
};

const onRunQuery = useCallback(() => {
if(varRegExp) {
return;
}
runQuery();
}, [runQuery, varRegExp]);

useEffect(() => {
// grafana with a version below 12 doesn't support subscribe function on store
if ('subscribe' in store) {
Expand All @@ -81,6 +94,7 @@ const QueryEditor = React.memo<VictoriaLogsQueryEditorProps>((props) => {
<div className={styles.wrapper}>
<EditorHeader>
{showStatsWarn && (<QueryEditorStatsWarn queryType={query.queryType}/>)}
<QueryEditorHelp />
<VmuiLink
query={query}
panelData={data}
Expand Down Expand Up @@ -111,6 +125,7 @@ const QueryEditor = React.memo<VictoriaLogsQueryEditorProps>((props) => {
) : (
<QueryCodeEditor {...props} query={query} onChange={onChangeInternal} showExplain={true}/>
)}
{varRegExp && (<QueryEditorVariableRegexpError regExp={varRegExp} />)}
<QueryEditorOptions
query={query}
onChange={onChange}
Expand Down
39 changes: 39 additions & 0 deletions src/components/QueryEditor/QueryEditorHelp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMemo } from "react";
import * as React from "react";

import { Icon, Tooltip } from "@grafana/ui";

export const QueryEditorHelp = () => {
const helpTooltipContent = useMemo(() => {
const helpText = `Variable Interpolation Rules:

1. Field Value Variables:
• field:$var -> field:in("v1", ..., "vN")
• field:=$var -> field:in("v1", ..., "vN")
• Values are quoted and escaped
• Empty values or "All" expand to in(*)

2. Function Contexts:
• in($var) and contains_any($var) expand to quoted lists
• Values maintain proper quoting within function calls

3. Nequality Operators in stream filters:
• field:!$var -> !field in("v1", ..., "vN")
• field:!=$var -> !field in("v1", ..., "vN")

4. Stream Filters:
• {tag=$var} -> {tag in(...)}
• {field!=$var} -> {tag not_in(...)}

5. Restrictions:
• Interpolation is NOT allowed in regexp (e.g., field:~$var)
• Invalid usage will show an error message`;
return <pre>{helpText}</pre>
}, []);

return (
<Tooltip placement="top" content={helpTooltipContent} theme="info">
<Icon name="info-circle" size="sm" width={16} height={16} />
</Tooltip>
)
}
44 changes: 44 additions & 0 deletions src/components/QueryEditor/QueryEditorVariableRegexpError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { css } from "@emotion/css";
import React from 'react';

import { GrafanaTheme2 } from "@grafana/data";
import { Badge, useTheme2 } from "@grafana/ui";


interface Props {
regExp: string;
}

const QueryEditorVariableRegexpError = ({ regExp }: Props) => {
const theme = useTheme2();
const styles = getStyles(theme);

const text = (
<div>
Regexp operator (~) cannot be used with variables in {regExp}. Use exact match operator (:) or use non variable in regexp instead.
</div>
)

return (
<div className={styles.root}>
<Badge
icon={"exclamation-triangle"}
color={"red"}
text={text}
/>
</div>
)
}

export default QueryEditorVariableRegexpError;

const getStyles = (theme: GrafanaTheme2) => {
return {
root: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
flexGrow: 1,
}),
};
};
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const VARIABLE_ALL_VALUE = "$__all";
Loading