Skip to content

Commit

Permalink
Explore links: add pattern filter support (#1036)
Browse files Browse the repository at this point in the history
* feat: add pattern filter support to explore links
  • Loading branch information
gtk-grafana authored Feb 18, 2025
1 parent acc65e0 commit 965bbdc
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 128 deletions.
7 changes: 2 additions & 5 deletions src/Components/IndexScene/IndexScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
SceneVariableSet,
} from '@grafana/scenes';
import {
AppliedPattern,
AdHocFiltersWithLabelsAndMeta,
EXPLORATION_DS,
MIXED_FORMAT_EXPR,
Expand Down Expand Up @@ -66,7 +67,6 @@ import {
renderLogQLLabelFilters,
renderLogQLLineFilter,
renderLogQLMetadataFilters,
renderPatternFilters,
} from 'services/query';
import { VariableHide } from '@grafana/schema';
import { CustomConstantVariable } from '../../services/CustomConstantVariable';
Expand Down Expand Up @@ -99,12 +99,9 @@ import { areArraysEqual } from '../../services/comparison';
import { isFilterMetadata } from '../../services/filters';
import { getFieldsTagValuesExpression } from '../../services/expressions';
import { isOperatorInclusive } from '../../services/operatorHelpers';
import { renderPatternFilters } from '../../services/renderPatternFilters';

export const showLogsButtonSceneKey = 'showLogsButtonScene';
export interface AppliedPattern {
pattern: string;
type: 'include' | 'exclude';
}

export interface IndexSceneState extends SceneObjectState {
// contentScene is the scene that is displayed in the main body of the index scene - it can be either the service selection or service scene
Expand Down
2 changes: 1 addition & 1 deletion src/Components/IndexScene/PatternControls.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { PatternControls } from './PatternControls';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AppliedPattern } from './IndexScene';
import { AppliedPattern } from '../../services/variables';

const originalWidth = global.window.innerWidth;
beforeAll(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/Components/IndexScene/PatternControls.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { AppliedPattern } from './IndexScene';
import { PatternTag } from './PatternTag';
import { css } from '@emotion/css';
import { useStyles2, Text } from '@grafana/ui';
import { USER_EVENTS_ACTIONS, USER_EVENTS_PAGES, reportAppInteraction } from 'services/analytics';
import { testIds } from 'services/testIds';
import { AppliedPattern } from '../../services/variables';

type Props = {
patterns: AppliedPattern[] | undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/Components/IndexScene/VariableLayoutScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { css, cx } from '@emotion/css';
import { GiveFeedbackButton } from './GiveFeedbackButton';
import { CustomVariableValueSelectors } from './CustomVariableValueSelectors';
import { PatternControls } from './PatternControls';
import { AppliedPattern, IndexScene } from './IndexScene';
import { IndexScene } from './IndexScene';
import { CONTROLS_VARS_DATASOURCE, CONTROLS_VARS_FIELDS_COMBINED, LayoutScene } from './LayoutScene';
import { useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { AppliedPattern } from '../../services/variables';

interface VariableLayoutSceneState extends SceneObjectState {}
export class VariableLayoutScene extends SceneObjectBase<VariableLayoutSceneState> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ import React from 'react';
import { LoadingState } from '@grafana/data';
import { Alert, Button } from '@grafana/ui';
import {
AppliedPattern,
LOG_STREAM_SELECTOR_EXPR,
PATTERNS_SAMPLE_SELECTOR_EXPR,
VAR_PATTERNS_EXPR,
} from '../../../../services/variables';
import { buildDataQuery, renderPatternFilters } from '../../../../services/query';
import { buildDataQuery } from '../../../../services/query';
import { getQueryRunner } from '../../../../services/panel';
import { AppliedPattern } from '../../../IndexScene/IndexScene';
import { PatternsViewTableScene } from './PatternsViewTableScene';
import { emptyStateStyles } from '../FieldsBreakdownScene';
import { getFieldsVariable, getLevelsVariable, getLineFiltersVariable } from '../../../../services/variableGetters';
import { LokiQuery } from '../../../../services/lokiQuery';
import { logger } from '../../../../services/logger';
import { renderPatternFilters } from '../../../../services/renderPatternFilters';

interface PatternsLogsSampleSceneState extends SceneObjectState {
pattern: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@grafana/scenes';
import { PatternFrame } from './PatternsBreakdownScene';
import React from 'react';
import { AppliedPattern, IndexScene } from '../../../IndexScene/IndexScene';
import { IndexScene } from '../../../IndexScene/IndexScene';
import { DataFrame, GrafanaTheme2, LoadingState, PanelData, scaledUnits } from '@grafana/data';
import { AxisPlacement, Column, InteractiveTable, TooltipDisplayMode, useTheme2 } from '@grafana/ui';
import { CellProps } from 'react-table';
Expand All @@ -23,6 +23,7 @@ import { PatternNameLabel } from './PatternNameLabel';
import { getExplorationFor } from 'services/scenes';
import { PatternsTableExpandedRow } from './PatternsTableExpandedRow';
import { LINE_LIMIT } from '../../../../services/query';
import { AppliedPattern } from '../../../../services/variables';

// copied from from grafana repository packages/grafana-data/src/valueFormats/categories.ts
// that is used in Grafana codebase for "short" units
Expand Down
72 changes: 72 additions & 0 deletions src/services/extensions/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,78 @@ describe('contextToLink', () => {
});
});

describe('pattern-filters', () => {
it('should parse pattern filters', () => {
const target = getTestTarget({
expr: '{cluster="eu-west-1"} !> "<_> - - [<_> +0000]" |> "<_> - <_> [<_> +0000]" | json | logfmt | drop __error__, __error_details__',
});
const config = getTestConfig(linkConfigs, target);

const expectedLabelFiltersUrlString = `&var-filters=${encodeFilter(
`cluster|=|${addCustomInputPrefixAndValueLabels('eu-west-1')}`
)}`;

const expectedPatternsVariable = `&var-patterns=${encodeFilter(
'!> "<_> - - [<_> +0000]" |> "<_> - <_> [<_> +0000]"'
)}`;

const pattern = `[{"type":"exclude","pattern":"<_> - - [<_> +0000]"},{"type":"include","pattern":"<_> - <_> [<_> +0000]"}]`;
const expectedPatterns = `&patterns=${encodeFilter(pattern)}`;

expect(config).toEqual({
path: getPath({
slug: 'cluster/eu-west-1',
expectedLabelFiltersUrlString,
expectedPatternsVariable,
expectedPatterns,
}),
});
});
it('should parse multiple pattern filters', () => {
const target = getTestTarget({
expr: '{cluster="eu-west-1"} !> "<_> - - [<_> +0000]" |> "<_> - <_> [<_> +0000]" or "<_> - <_> [<_> +0000] \\"POST <_> <_>\\"" | json | logfmt | drop __error__, __error_details__',
});
const config = getTestConfig(linkConfigs, target);

const expectedLabelFiltersUrlString = `&var-filters=${encodeFilter(
`cluster|=|${addCustomInputPrefixAndValueLabels('eu-west-1')}`
)}`;

const expectedPatternsVariable = `&var-patterns=${encodeFilter(
'!> "<_> - - [<_> +0000]" |> "<_> - <_> [<_> +0000]" or "<_> - <_> [<_> +0000] \\"POST <_> <_>\\""'
)}`;

const pattern = `[{"type":"exclude","pattern":"<_> - - [<_> +0000]"},{"type":"include","pattern":"<_> - <_> [<_> +0000]"},{"type":"include","pattern":"<_> - <_> [<_> +0000] \\"POST <_> <_>\\""}]`;
const expectedPatterns = `&patterns=${encodeFilter(pattern)}`;

expect(config).toEqual({
path: getPath({
slug: 'cluster/eu-west-1',
expectedLabelFiltersUrlString,
expectedPatternsVariable,
expectedPatterns,
}),
});
});
it('should parse empty filters', () => {
const target = getTestTarget({
expr: '{cluster="eu-west-1"} !> "" !> "" |> "" or "" | json | logfmt | drop __error__, __error_details__',
});
const config = getTestConfig(linkConfigs, target);

const expectedLabelFiltersUrlString = `&var-filters=${encodeFilter(
`cluster|=|${addCustomInputPrefixAndValueLabels('eu-west-1')}`
)}`;

expect(config).toEqual({
path: getPath({
slug: 'cluster/eu-west-1',
expectedLabelFiltersUrlString,
}),
});
});
});

describe('fields', () => {
describe('string fields', () => {
it('should parse structured metadata field', () => {
Expand Down
24 changes: 23 additions & 1 deletion src/services/extensions/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PluginExtensionLinkConfig, PluginExtensionPanelContext, PluginExtension
import {
addAdHocFilterUserInputPrefix,
AdHocFieldValue,
AppliedPattern,
LEVEL_VARIABLE_VALUE,
SERVICE_NAME,
stripAdHocFilterUserInputPrefix,
Expand All @@ -13,13 +14,16 @@ import {
VAR_LEVELS,
VAR_LINE_FILTERS,
VAR_METADATA,
VAR_PATTERNS,
} from 'services/variables';
import pluginJson from '../../plugin.json';
import { getMatcherFromQuery } from '../logqlMatchers';
import { LokiQuery } from '../lokiQuery';
import { LabelType } from '../fieldsTypes';

import { isOperatorInclusive } from '../operatorHelpers';
import { PatternFilterOp } from '../filterTypes';
import { renderPatternFilters } from '../renderPatternFilters';

const title = 'Open in Explore Logs';
const description = 'Open current query in the Explore Logs view';
Expand Down Expand Up @@ -88,7 +92,7 @@ function contextToLink<T extends PluginExtensionPanelContext>(context?: T) {
}

const expr = lokiQuery.expr;
const { labelFilters, lineFilters, fields } = getMatcherFromQuery(expr, context, lokiQuery);
const { labelFilters, lineFilters, fields, patternFilters } = getMatcherFromQuery(expr, context, lokiQuery);
const labelSelector = labelFilters.find((selector) => isOperatorInclusive(selector.operator));

// Require at least one inclusive operator to run a valid Loki query
Expand Down Expand Up @@ -163,6 +167,22 @@ function contextToLink<T extends PluginExtensionPanelContext>(context?: T) {
}
}
}
if (patternFilters?.length) {
const patterns: AppliedPattern[] = [];

for (const field of patternFilters) {
patterns.push({
type: field.operator === PatternFilterOp.match ? 'include' : 'exclude',
pattern: stringifyValues(field.value),
});
}

let patternsString = renderPatternFilters(patterns);

params = appendUrlParameter(UrlParameters.Patterns, JSON.stringify(patterns), params);
params = appendUrlParameter(UrlParameters.PatternsVariable, patternsString, params);
}

return {
path: createAppUrl(`/explore/${labelName}/${labelValue}/logs`, params),
};
Expand All @@ -181,6 +201,8 @@ export const UrlParameters = {
Metadata: `var-${VAR_METADATA}`,
Levels: `var-${VAR_LEVELS}`,
LineFilters: `var-${VAR_LINE_FILTERS}`,
Patterns: VAR_PATTERNS,
PatternsVariable: `var-${VAR_PATTERNS}`,
} as const;
export type UrlParameterType = (typeof UrlParameters)[keyof typeof UrlParameters];

Expand Down
13 changes: 13 additions & 0 deletions src/services/extensions/scenesMethods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Warning, this file is included in the main module.tsx bundle, and doesn't contain any imports to keep that bundle size small. Don't add imports to this file!

/**
* Methods copied from scenes that we want in the module (to generate links which cannot be lazy loaded), without including all of scenes.
* See https://github.com/grafana/scenes/issues/1046
*/
// 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, '\\"');
}
8 changes: 6 additions & 2 deletions src/services/extensions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ export function getPath(options: {
expectedLineFiltersUrlString?: string;
expectedFieldsUrlString?: string;
expectedLevelsFilterUrlString?: string;
expectedPatternsVariable?: string;
expectedPatterns?: string;
}) {
return `/a/grafana-lokiexplore-app/explore/${options.slug}/logs?var-ds=123abc&from=1675828800000&to=1675854000000${
options.expectedLabelFiltersUrlString ?? ''
}${options.expectedMetadataString ?? ''}${options.expectedLineFiltersUrlString ?? ''}${
options.expectedFieldsUrlString ?? ''
}${options.expectedLevelsFilterUrlString ?? ''}`;
options.expectedPatterns ?? ''
}${options.expectedPatternsVariable ?? ''}${options.expectedFieldsUrlString ?? ''}${
options.expectedLevelsFilterUrlString ?? ''
}`;
}
10 changes: 10 additions & 0 deletions src/services/filterTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,23 @@ export type LineFilterType = {
value: string;
};

export type PatternFilterType = {
operator: PatternFilterOp;
value: string;
};

export enum LineFilterOp {
match = '|=',
negativeMatch = `!=`,
regex = '|~',
negativeRegex = `!~`,
}

export enum PatternFilterOp {
match = '|>',
negativeMatch = '!>',
}

export enum LineFilterCaseSensitive {
caseSensitive = 'caseSensitive',
caseInsensitive = 'caseInsensitive',
Expand Down
Loading

0 comments on commit 965bbdc

Please sign in to comment.