Skip to content

Commit

Permalink
add filter by labels
Browse files Browse the repository at this point in the history
  • Loading branch information
Loori-R committed Feb 13, 2024
1 parent 8d1863b commit 1011e16
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 52 deletions.
16 changes: 7 additions & 9 deletions src/backendResultTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DataQueryResponse, DataFrame, isDataFrame, FieldType, QueryResultMeta,
import { getDerivedFields } from './getDerivedFields';
import { makeTableFrames } from './makeTableFrames';
import { getHighlighterExpressionsFromQuery } from './queryUtils';
import { dataFrameHasLokiError } from './responseUtils';
import { dataFrameHasError } from './responseUtils';
import { DerivedFieldConfig, Query, QueryType } from './types';

function isMetricFrame(frame: DataFrame): boolean {
Expand All @@ -28,11 +28,9 @@ function processStreamFrame(
): DataFrame {
const custom: Record<string, string> = {
...frame.meta?.custom, // keep the original meta.custom
// used by logsModel
lokiQueryStatKey: 'Summary: total bytes processed',
};

if (dataFrameHasLokiError(frame)) {
if (dataFrameHasError(frame)) {
custom.error = 'Error when parsing some of the logs';
}

Expand Down Expand Up @@ -102,8 +100,6 @@ function groupFrames(
}

function improveError(error: DataQueryError | undefined, queryMap: Map<string, Query>): DataQueryError | undefined {
// many things are optional in an error-object, we need an error-message to exist,
// and we need to find the loki-query, based on the refId in the error-object.
if (error === undefined) {
return error;
}
Expand All @@ -121,7 +117,7 @@ function improveError(error: DataQueryError | undefined, queryMap: Map<string, Q
if (message.includes('escape') && query.expr.includes('\\')) {
return {
...error,
message: `${message}. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://grafana.com/docs/loki/latest/logql/.`,
message: `${message}. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://docs.victoriametrics.com/victorialogs/logsql/.`,
};
}

Expand All @@ -133,7 +129,7 @@ export function transformBackendResult(
queries: Query[],
derivedFieldConfigs: DerivedFieldConfig[]
): DataQueryResponse {
const { data, error, ...rest } = response;
const { data, errors, ...rest } = response;

// in the typescript type, data is an array of basically anything.
// we do know that they have to be dataframes, so we make a quick check,
Expand All @@ -149,9 +145,11 @@ export function transformBackendResult(

const { streamsFrames, metricInstantFrames, metricRangeFrames } = groupFrames(dataFrames, queryMap);

const improvedErrors = errors && errors.map((error) => improveError(error, queryMap)).filter((e) => e !== undefined);

return {
...rest,
error: improveError(error, queryMap),
errors: improvedErrors as DataQueryError[],
data: [
...processMetricRangeFrames(metricRangeFrames),
...processMetricInstantFrames(metricInstantFrames),
Expand Down
29 changes: 21 additions & 8 deletions src/components/QueryEditor/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { css } from "@emotion/css";
import { isEqual } from 'lodash';
import React, { useEffect, useState } from 'react';

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

import { Query, VictoriaLogsQueryEditorProps } from "../../types";

import QueryCodeEditor from "./QueryCodeEditor";
import { getQueryWithDefaults } from "./state";

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

const { onChange, onRunQuery, data, app, queries } = props;
const [dataIsStale, setDataIsStale] = useState(false);

Expand All @@ -27,7 +30,10 @@ const QueryEditor = React.memo<VictoriaLogsQueryEditorProps>((props) => {
};

return (
<>
<div className={styles.wrapper}>
<div className="flex-grow-1">
<QueryCodeEditor {...props} query={query} onChange={onChangeInternal} showExplain={true}/>
</div>
<div>
{app !== CoreApp.Explore && app !== CoreApp.Correlations && (
<Button
Expand All @@ -41,12 +47,19 @@ const QueryEditor = React.memo<VictoriaLogsQueryEditorProps>((props) => {
</Button>
)}
</div>
<div>
<QueryCodeEditor {...props} query={query} onChange={onChangeInternal} showExplain={true} />
</div>
</>
</div>
);
});

QueryEditor.displayName = 'LokiQueryEditor';
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
display: flex;
align-items: flex-start;
gap: ${theme.spacing(1)};
`
};
};

QueryEditor.displayName = 'QueryEditor';
export default QueryEditor
6 changes: 3 additions & 3 deletions src/components/QueryEditor/QueryField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { VictoriaLogsDatasource } from "../../datasource";
import { Options, Query } from "../../types";
import { MonacoQueryFieldWrapper } from "../monaco-query-field/MonacoQueryFieldWrapper";

export interface LokiQueryFieldProps extends QueryEditorProps<VictoriaLogsDatasource, Query, Options> {
export interface QueryFieldProps extends QueryEditorProps<VictoriaLogsDatasource, Query, Options> {
ExtraFieldElement?: React.ReactNode;
'data-testid'?: string;
}

const QueryField: React.FC<LokiQueryFieldProps> = (props) => {
const QueryField: React.FC<QueryFieldProps> = (props) => {
const {
ExtraFieldElement,
query,
Expand Down Expand Up @@ -50,7 +50,7 @@ const QueryField: React.FC<LokiQueryFieldProps> = (props) => {
onChange={onChangeQuery}
onRunQuery={onRunQuery}
initialValue={query.expr ?? ''}
placeholder="Enter a MetricsQL query…"
placeholder="Enter a LogsQL query…"
/>
</div>
</div>
Expand Down
14 changes: 5 additions & 9 deletions src/configuration/HelpfulLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ import React from "react";
const tips = [
{
title: "Datasource",
url: "https://github.com/VictoriaMetrics/grafana-datasource#victoriametrics-data-source-for-grafana",
url: "https://github.com/VictoriaMetrics/grafana-logs-datasource",
},
{
title: "Cluster VM",
url: "https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#url-format",
title: "VictoriaLogs",
url: "https://docs.victoriametrics.com/victorialogs/",
},
{
title: "Grafana setup",
url: "https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#grafana-setup",
},
{
title: "MetricsQL",
url: "https://docs.victoriametrics.com/MetricsQL.html",
title: "LogsQL",
url: "https://docs.victoriametrics.com/victorialogs/logsql/",
},
{
title: "VictoriaMetrics",
Expand Down
4 changes: 1 addition & 3 deletions src/configuration/QuerySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,17 @@ export const QuerySettings = (props: Props) => {
<div>
<InlineField
label="Maximum lines"
htmlFor="loki_config_maxLines"
labelWidth={22}
tooltip={
<>
Loki queries must contain a limit of the maximum number of lines returned (default: 1000). Increase this
VictoriaLogs queries must contain a limit of the maximum number of lines returned (default: 1000). Increase this
limit to have a bigger result set for ad-hoc analysis. Decrease this limit if your browser becomes sluggish
when displaying the log results.
</>
}
>
<Input
type="number"
id="loki_config_maxLines"
value={maxLines}
onChange={(event: React.FormEvent<HTMLInputElement>) => onMaxLinedChange(event.currentTarget.value)}
width={16}
Expand Down
51 changes: 41 additions & 10 deletions src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,35 @@ import { DataSourceWithBackend } from '@grafana/runtime';

import { transformBackendResult } from "./backendResultTransformer";
import QueryEditor from "./components/QueryEditor/QueryEditor";
import { Query, Options } from './types';


import { escapeLabelValueInSelector } from "./languageUtils";
import { addLabelToQuery, queryHasFilter, removeLabelFromQuery } from "./modifyQuery";
import { Query, Options, ToggleFilterAction, QueryFilterOptions, FilterActionType } from './types';

export class VictoriaLogsDatasource
extends DataSourceWithBackend<Query, Options> {
maxLines: number;

constructor(
private instanceSettings: DataSourceInstanceSettings<Options>,
// private readonly templateSrv: TemplateSrv = getTemplateSrv()
instanceSettings: DataSourceInstanceSettings<Options>,
) {
super(instanceSettings);

// this.languageProvider = new LanguageProvider(this);
const settingsData = instanceSettings.jsonData || {};
this.maxLines = parseInt(settingsData.maxLines ?? '0', 10) || 10;
this.annotations = {
QueryEditor: QueryEditor,
};
// this.variables = new LokiVariableSupport(this);
// this.logContextProvider = new LogContextProvider(this);
}

query(request: DataQueryRequest<Query>): Observable<DataQueryResponse> {
const queries = request.targets
.map((q) => ({ ...q, maxLines: q.maxLines ?? this.maxLines }));
const queries = request.targets.filter(q => q.expr).map((q) => {
// include time range in query if not already present
if (!/_time/.test(q.expr)) {
const timerange = `_time:[${request.range.from.toISOString()}, ${request.range.to.toISOString()}]`
q.expr = `${timerange} AND ${q.expr}`;
}
return { ...q, maxLines: q.maxLines ?? this.maxLines }
});

const fixedRequest: DataQueryRequest<Query> = {
...request,
Expand All @@ -51,4 +53,33 @@ export class VictoriaLogsDatasource
)
);
}

toggleQueryFilter(query: Query, filter: ToggleFilterAction): Query {
let expression = query.expr ?? '';

if (!filter.options?.key || !filter.options?.value) {
return { ...query, expr: expression };
}

const isFilterFor = filter.type === FilterActionType.FILTER_FOR;
const isFilterOut = filter.type === FilterActionType.FILTER_OUT;
const value = escapeLabelValueInSelector(filter.options.value);
const hasFilter = queryHasFilter(expression, filter.options.key, value)
const operator = filter.type === FilterActionType.FILTER_FOR ? 'AND' : 'NOT';

if (hasFilter) {
expression = removeLabelFromQuery(expression, filter.options.key, value);
}

if ((isFilterFor && !hasFilter) || isFilterOut) {
expression = addLabelToQuery(expression, filter.options.key, value, operator);
}

return { ...query, expr: expression };
}

queryHasFilter(query: Query, filter: QueryFilterOptions): boolean {
let expression = query.expr ?? '';
return queryHasFilter(expression, filter.key, filter.value);
}
}
27 changes: 27 additions & 0 deletions src/languageUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const REG_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g;

export function unescapeLabelValue(labelValue: string): string {
return labelValue.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
}

export function isRegexSelector(selector?: string) {
return !!(selector && (selector.includes('=~') || selector.includes('!~')));
}

function escapeMetaRegexp(value: string): string {
return value.replace(REG_METACHARACTERS, '\\$&');
}

export function escapeLabelValueInExactSelector(labelValue: string): string {
return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
}

export function escapeLabelValueInRegexSelector(labelValue: string): string {
return escapeLabelValueInExactSelector(escapeMetaRegexp(labelValue));
}

export function escapeLabelValueInSelector(labelValue: string, selector?: string): string {
return isRegexSelector(selector)
? escapeLabelValueInRegexSelector(labelValue)
: escapeLabelValueInExactSelector(labelValue);
}
28 changes: 28 additions & 0 deletions src/modifyQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function queryHasFilter(query: string, key: string, value: string): boolean {
return query.includes(`${key}:${value}`)
}

export const removeLabelFromQuery = (query: string, key: string, value: string): string => {
const parts = query.split(' ')
const index = parts.findIndex((part) => part.includes(`${key}:${value}`))
const newParts = removeAtIndexAndBefore(parts, index)
return newParts.join(' ')
}

export const addLabelToQuery = (query: string, key: string, value: string, operator: string): string => {
return `${query} ${operator} ${key}:${value}`
}

const removeAtIndexAndBefore = (arr: string[], index: number): string[] => {
if (index < 0 || index >= arr.length) {
return arr;
}

if (index === 0) {
arr.splice(index, 1);
} else {
arr.splice(index - 1, 2);
}

return arr;
}
9 changes: 9 additions & 0 deletions src/parsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function handleQuotes(string: string) {
if (string[0] === `"` && string[string.length - 1] === `"`) {
return string
.substring(1, string.length - 1)
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\');
}
return string.replace(/`/g, '');
}
4 changes: 0 additions & 4 deletions src/queryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { escapeRegExp } from 'lodash';

import { Filter, FilterOp, LineFilter, OrFilter, parser, PipeExact, PipeMatch, String } from "@grafana/lezer-logql"



export function getNodesFromQuery(query: string, nodeTypes?: number[]): SyntaxNode[] {
const nodes: SyntaxNode[] = [];
const tree = parser.parse(query);
Expand Down Expand Up @@ -59,8 +57,6 @@ export function getHighlighterExpressionsFromQuery(input = ''): string[] {

// Only filter expressions with |~ operator are treated as regular expressions
if (pipeMatch) {
// When using backticks, Loki doesn't require to escape special characters and we can just push regular expression to highlights array
// When using quotes, we have extra backslash escaping and we need to replace \\ with \
resultTerm = backtickedTerm ? unwrappedFilterTerm : unwrappedFilterTerm.replace(/\\\\/g, '\\');
} else {
// We need to escape this string so it is not matched as regular expression
Expand Down
2 changes: 1 addition & 1 deletion src/responseUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DataFrame, Labels } from "@grafana/data";

export function dataFrameHasLokiError(frame: DataFrame): boolean {
export function dataFrameHasError(frame: DataFrame): boolean {
const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values ?? [];
return labelSets.some((labels) => labels.__error__ !== undefined);
}
20 changes: 15 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataSourceJsonData, QueryEditorProps } from '@grafana/data';
import { DataFrame, DataSourceJsonData, KeyValue, QueryEditorProps } from '@grafana/data';
import { DataQuery } from '@grafana/schema';

import { VictoriaLogsDatasource } from "./datasource";
Expand All @@ -22,10 +22,6 @@ export enum SupportingQueryType {
LogsVolume = 'logsVolume',
}

export enum QueryEditorMode {
Code = 'code',
}

export enum QueryType {
Instant = 'instant',
Range = 'range',
Expand Down Expand Up @@ -56,3 +52,17 @@ export type DerivedFieldConfig = {
matcherType?: 'label' | 'regex';
};

export interface QueryFilterOptions extends KeyValue<string> {
}

export enum FilterActionType {
FILTER_FOR = 'FILTER_FOR',
FILTER_OUT = 'FILTER_OUT',
}

export interface ToggleFilterAction {
type: FilterActionType;
options: QueryFilterOptions;
frame?: DataFrame;
}

0 comments on commit 1011e16

Please sign in to comment.