Loki: Add the ability to prettify logql queries (#64337)

* pushed to get help of a genius

* fix: error response is not json

* feat: make request on click

* refactor: remove print statement

* refactor: remove unnecessary code

* feat: convert grafana variables to value for API request

* use the parser to interpolate and recover the original query (#64591)

* Prettify query: use the parser to interpolate and recover the original query

* Fix typo

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>

* Fix typo

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>

* fix: reverse transformation not working

---------

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Co-authored-by: Gareth Dawson <gwdawson.work@gmail.com>

* fix: bugs created from merge

* refactor: move prettify code out of monaco editor

* fix: variables with the same value get converted back to the incorect variable

* refactor

* use consistent styling with bigquery

* fix: only allow text/plain and application/json

* fix: only make the request if the query is valid

* endpoint now returns application/json

* prettify from js

* WIP: not all cases are handles, code still needs cleaning up

* WIP

* large refactor, finished support for all pipeline expressions

* add tests for all format functions

* idk why these files changed

* add support for range aggregation expr & refactor

* add support for vector aggregation expressions

* add support for bin op expression

* add support for literal and vector expressions

* add tests and fix some bugs

* add support for distinct and decolorize

* feat: update variable replace and return

* fix: lezer throws an errow when using a range variable

* remove api implementation

* remove api implementation

* remove type assertions

* add feature flag

* update naming

* fix: bug incorrectly formatting unwrap with labelfilter

* support label replace expr

* remove duplicate code (after migration)

* add more tests

* validate query before formatting

* move tests to lezer repo

* add feature tracking

* populate feature tracking with some data

* upgrade lezer version to 0.1.7

* bump lezer to 0.1.8

* add tests

---------

Co-authored-by: Matias Chomicki <matyax@gmail.com>
Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
This commit is contained in:
Gareth Dawson
2023-07-21 13:03:56 +01:00
committed by GitHub
parent c84d689ce0
commit 4e42f9b619
11 changed files with 133 additions and 9 deletions

View File

@@ -111,6 +111,7 @@ Experimental features might be changed or removed without prior notice.
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
| `dashboardEmbed` | Allow embedding dashboard for external use in Code editors |
| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) |
| `lokiFormatQuery` | Enables the ability to format Loki queries |
| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries |
| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore |
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries |

View File

@@ -265,7 +265,7 @@
"@grafana/faro-core": "1.1.0",
"@grafana/faro-web-sdk": "1.1.0",
"@grafana/google-sdk": "0.1.1",
"@grafana/lezer-logql": "0.1.5",
"@grafana/lezer-logql": "0.1.8",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "0.22.0",

View File

@@ -98,6 +98,7 @@ export interface FeatureToggles {
dashboardEmbed?: boolean;
frontendSandboxMonitorOnly?: boolean;
sqlDatasourceDatabaseSelection?: boolean;
lokiFormatQuery?: boolean;
cloudWatchLogsMonacoEditor?: boolean;
exploreScrollableLogsContainer?: boolean;
recordedQueriesMulti?: boolean;

View File

@@ -546,6 +546,13 @@ var (
Stage: FeatureStagePublicPreview,
Owner: grafanaBiSquad,
},
{
Name: "lokiFormatQuery",
Description: "Enables the ability to format Loki queries",
FrontendOnly: true,
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityLogsSquad,
},
{
Name: "cloudWatchLogsMonacoEditor",
Description: "Enables the Monaco editor for CloudWatch Logs queries",

View File

@@ -79,6 +79,7 @@ pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,fals
dashboardEmbed,experimental,@grafana/grafana-as-code,false,false,false,true
frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,false,true
sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,false,true
lokiFormatQuery,experimental,@grafana/observability-logs,false,false,false,true
cloudWatchLogsMonacoEditor,experimental,@grafana/aws-datasources,false,false,false,true
exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true
recordedQueriesMulti,experimental,@grafana/observability-metrics,false,false,false,false
1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
79 dashboardEmbed experimental @grafana/grafana-as-code false false false true
80 frontendSandboxMonitorOnly experimental @grafana/plugins-platform-backend false false false true
81 sqlDatasourceDatabaseSelection preview @grafana/grafana-bi-squad false false false true
82 lokiFormatQuery experimental @grafana/observability-logs false false false true
83 cloudWatchLogsMonacoEditor experimental @grafana/aws-datasources false false false true
84 exploreScrollableLogsContainer experimental @grafana/observability-logs false false false true
85 recordedQueriesMulti experimental @grafana/observability-metrics false false false false

View File

@@ -327,6 +327,10 @@ const (
// Enables previous SQL data source dataset dropdown behavior
FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection"
// FlagLokiFormatQuery
// Enables the ability to format Loki queries
FlagLokiFormatQuery = "lokiFormatQuery"
// FlagCloudWatchLogsMonacoEditor
// Enables the Monaco editor for CloudWatch Logs queries
FlagCloudWatchLogsMonacoEditor = "cloudWatchLogsMonacoEditor"

View File

@@ -75,7 +75,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
data-testid={this.props['data-testid']}
>
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15">
<div className="gf-form--grow flex-shrink-1 min-width-15">
<MonacoQueryFieldWrapper
datasource={datasource}
history={history ?? []}

View File

@@ -1,5 +1,6 @@
import { String } from '@grafana/lezer-logql';
import { createLokiDatasource } from './mocks';
import {
getHighlighterExpressionsFromQuery,
getLokiQueryType,
@@ -17,6 +18,7 @@ import {
getLogQueryFromMetricsQuery,
getNormalizedLokiQuery,
getNodePositionsFromQuery,
formatLogqlQuery,
} from './queryUtils';
import { LokiQuery, LokiQueryType } from './types';
@@ -440,3 +442,27 @@ describe('getNodePositionsFromQuery', () => {
expect(nodePositions.length).toBe(0);
});
});
describe('formatLogqlQuery', () => {
const ds = createLokiDatasource();
it('formats a logs query', () => {
expect(formatLogqlQuery('{job="grafana"}', ds)).toBe('{job="grafana"}');
});
it('formats a metrics query', () => {
expect(formatLogqlQuery('count_over_time({job="grafana"}[1m])', ds)).toBe(
'count_over_time(\n {job="grafana"}\n [1m]\n)'
);
});
it('formats a metrics query with variables', () => {
// mock the interpolateString return value so it passes the isValid check
ds.interpolateString = jest.fn(() => 'rate({job="grafana"}[1s])');
expect(formatLogqlQuery('rate({job="grafana"}[$__range])', ds)).toBe('rate(\n {job="grafana"}\n [$__range]\n)');
expect(formatLogqlQuery('rate({job="grafana"}[$__interval])', ds)).toBe(
'rate(\n {job="grafana"}\n [$__interval]\n)'
);
});
});

View File

@@ -19,11 +19,15 @@ import {
Identifier,
Distinct,
Range,
formatLokiQuery,
} from '@grafana/lezer-logql';
import { reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { ErrorId } from '../prometheus/querybuilder/shared/parsingUtils';
import { ErrorId, replaceVariables, returnVariables } from '../prometheus/querybuilder/shared/parsingUtils';
import { placeHolderScopedVars } from './components/monaco-query-field/monaco-completion-provider/validation';
import { LokiDatasource } from './datasource';
import { getStreamSelectorPositions, NodePosition } from './modifyQuery';
import { LokiQuery, LokiQueryType } from './types';
@@ -293,3 +297,39 @@ export const getLokiQueryFromDataQuery = (query?: DataQuery): LokiQuery | undefi
return query;
};
export function formatLogqlQuery(query: string, datasource: LokiDatasource) {
const isInvalid = isQueryWithError(datasource.interpolateString(query, placeHolderScopedVars));
reportInteraction('grafana_loki_format_query_clicked', {
is_invalid: isInvalid,
query_type: isLogsQuery(query) ? 'logs' : 'metric',
});
if (isInvalid) {
return query;
}
let transformedQuery = replaceVariables(query);
const transformationMatches = [];
const tree = parser.parse(transformedQuery);
// Variables are considered errors inside of the parser, so we need to remove them before formatting
// We replace all variables with [0s] and keep track of the replaced variables
// After formatting we replace [0s] with the original variable
if (tree.topNode.firstChild?.firstChild?.type.id === MetricExpr) {
const pattern = /\[__V_[0-2]__\w+__V__\]/g;
transformationMatches.push(...transformedQuery.matchAll(pattern));
transformedQuery = transformedQuery.replace(pattern, '[0s]');
}
let formatted = formatLokiQuery(transformedQuery);
if (tree.topNode.firstChild?.firstChild?.type.id === MetricExpr) {
transformationMatches.forEach((match) => {
formatted = formatted.replace('[0s]', match[0]);
});
}
return returnVariables(formatted);
}

View File

@@ -2,12 +2,15 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { config } from '@grafana/runtime';
import { useStyles2, HorizontalGroup, IconButton, Tooltip, Icon } from '@grafana/ui';
import { getModKey } from 'app/core/utils/browser';
import { testIds } from '../../components/LokiQueryEditor';
import { LokiQueryField } from '../../components/LokiQueryField';
import { getStats } from '../../components/stats';
import { LokiQueryEditorProps } from '../../components/types';
import { formatLogqlQuery } from '../../queryUtils';
import { QueryStats } from '../../types';
import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained';
@@ -31,6 +34,9 @@ export function LokiQueryCodeEditor({
}: Props) {
const styles = useStyles2(getStyles);
const lokiFormatQuery = config.featureToggles.lokiFormatQuery;
const onClickFormatQueryButton = async () => onChange({ ...query, expr: formatLogqlQuery(query.expr, datasource) });
return (
<div className={styles.wrapper}>
<LokiQueryField
@@ -47,6 +53,27 @@ export function LokiQueryCodeEditor({
const stats = await getStats(datasource, query);
setQueryStats(stats);
}}
ExtraFieldElement={
<>
{lokiFormatQuery && (
<div className={styles.buttonGroup}>
<div>
<HorizontalGroup spacing="sm">
<IconButton
onClick={onClickFormatQueryButton}
name="brackets-curly"
size="xs"
tooltip="Format query"
/>
<Tooltip content={`Use ${getModKey()}+z to undo`}>
<Icon className={styles.hint} name="keyboard" />
</Tooltip>
</HorizontalGroup>
</div>
</div>
)}
</>
}
/>
{showExplain && <LokiQueryBuilderExplained query={query.expr} />}
</div>
@@ -61,5 +88,20 @@ const getStyles = (theme: GrafanaTheme2) => {
margin-bottom: 0.5;
}
`,
buttonGroup: css`
border: 1px solid ${theme.colors.border.medium};
border-top: none;
padding: ${theme.spacing(0.5, 0.5, 0.5, 0.5)};
margin-bottom: ${theme.spacing(0.5)};
display: flex;
flex-grow: 1;
justify-content: end;
font-size: ${theme.typography.bodySmall.fontSize};
`,
hint: css`
color: ${theme.colors.text.disabled};
white-space: nowrap;
cursor: help;
`,
};
};

View File

@@ -3959,12 +3959,14 @@ __metadata:
languageName: node
linkType: hard
"@grafana/lezer-logql@npm:0.1.5":
version: 0.1.5
resolution: "@grafana/lezer-logql@npm:0.1.5"
"@grafana/lezer-logql@npm:0.1.8":
version: 0.1.8
resolution: "@grafana/lezer-logql@npm:0.1.8"
dependencies:
lodash: ^4.17.21
peerDependencies:
"@lezer/lr": ^1.0.0
checksum: ac0a842c06add812b583e35f0cd4a6a0272adc1a34b681ceb94fbde3e63ed93db72ef4ee9f6531a781c380530b98b2a178736160baa251262a240d11f3afb211
checksum: f0f301b6d4fbd2d79563b5b4e34303257be0ea995b2b9fa1f012648654b4afaa9cea91642bc59eddb70e9fa24ec8804489c161f7065b41eef49db68d3a2ca561
languageName: node
linkType: hard
@@ -19270,7 +19272,7 @@ __metadata:
"@grafana/faro-core": 1.1.0
"@grafana/faro-web-sdk": 1.1.0
"@grafana/google-sdk": 0.1.1
"@grafana/lezer-logql": 0.1.5
"@grafana/lezer-logql": 0.1.8
"@grafana/monaco-logql": ^0.0.7
"@grafana/runtime": "workspace:*"
"@grafana/scenes": 0.22.0