Skip to content

Commit 4e42f9b

Browse files
itsgarethmatyaxivanahuckova
authored
Loki: Add the ability to prettify logql queries (grafana#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 (grafana#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>
1 parent c84d689 commit 4e42f9b

File tree

11 files changed

+133
-9
lines changed

11 files changed

+133
-9
lines changed

docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ Experimental features might be changed or removed without prior notice.
111111
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
112112
| `dashboardEmbed` | Allow embedding dashboard for external use in Code editors |
113113
| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) |
114+
| `lokiFormatQuery` | Enables the ability to format Loki queries |
114115
| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries |
115116
| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore |
116117
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@
265265
"@grafana/faro-core": "1.1.0",
266266
"@grafana/faro-web-sdk": "1.1.0",
267267
"@grafana/google-sdk": "0.1.1",
268-
"@grafana/lezer-logql": "0.1.5",
268+
"@grafana/lezer-logql": "0.1.8",
269269
"@grafana/monaco-logql": "^0.0.7",
270270
"@grafana/runtime": "workspace:*",
271271
"@grafana/scenes": "0.22.0",

packages/grafana-data/src/types/featureToggles.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export interface FeatureToggles {
9898
dashboardEmbed?: boolean;
9999
frontendSandboxMonitorOnly?: boolean;
100100
sqlDatasourceDatabaseSelection?: boolean;
101+
lokiFormatQuery?: boolean;
101102
cloudWatchLogsMonacoEditor?: boolean;
102103
exploreScrollableLogsContainer?: boolean;
103104
recordedQueriesMulti?: boolean;

pkg/services/featuremgmt/registry.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,13 @@ var (
546546
Stage: FeatureStagePublicPreview,
547547
Owner: grafanaBiSquad,
548548
},
549+
{
550+
Name: "lokiFormatQuery",
551+
Description: "Enables the ability to format Loki queries",
552+
FrontendOnly: true,
553+
Stage: FeatureStageExperimental,
554+
Owner: grafanaObservabilityLogsSquad,
555+
},
549556
{
550557
Name: "cloudWatchLogsMonacoEditor",
551558
Description: "Enables the Monaco editor for CloudWatch Logs queries",

pkg/services/featuremgmt/toggles_gen.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,fals
7979
dashboardEmbed,experimental,@grafana/grafana-as-code,false,false,false,true
8080
frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,false,true
8181
sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,false,true
82+
lokiFormatQuery,experimental,@grafana/observability-logs,false,false,false,true
8283
cloudWatchLogsMonacoEditor,experimental,@grafana/aws-datasources,false,false,false,true
8384
exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true
8485
recordedQueriesMulti,experimental,@grafana/observability-metrics,false,false,false,false

pkg/services/featuremgmt/toggles_gen.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,10 @@ const (
327327
// Enables previous SQL data source dataset dropdown behavior
328328
FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection"
329329

330+
// FlagLokiFormatQuery
331+
// Enables the ability to format Loki queries
332+
FlagLokiFormatQuery = "lokiFormatQuery"
333+
330334
// FlagCloudWatchLogsMonacoEditor
331335
// Enables the Monaco editor for CloudWatch Logs queries
332336
FlagCloudWatchLogsMonacoEditor = "cloudWatchLogsMonacoEditor"

public/app/plugins/datasource/loki/components/LokiQueryField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
7575
className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
7676
data-testid={this.props['data-testid']}
7777
>
78-
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15">
78+
<div className="gf-form--grow flex-shrink-1 min-width-15">
7979
<MonacoQueryFieldWrapper
8080
datasource={datasource}
8181
history={history ?? []}

public/app/plugins/datasource/loki/queryUtils.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { String } from '@grafana/lezer-logql';
22

3+
import { createLokiDatasource } from './mocks';
34
import {
45
getHighlighterExpressionsFromQuery,
56
getLokiQueryType,
@@ -17,6 +18,7 @@ import {
1718
getLogQueryFromMetricsQuery,
1819
getNormalizedLokiQuery,
1920
getNodePositionsFromQuery,
21+
formatLogqlQuery,
2022
} from './queryUtils';
2123
import { LokiQuery, LokiQueryType } from './types';
2224

@@ -440,3 +442,27 @@ describe('getNodePositionsFromQuery', () => {
440442
expect(nodePositions.length).toBe(0);
441443
});
442444
});
445+
446+
describe('formatLogqlQuery', () => {
447+
const ds = createLokiDatasource();
448+
449+
it('formats a logs query', () => {
450+
expect(formatLogqlQuery('{job="grafana"}', ds)).toBe('{job="grafana"}');
451+
});
452+
453+
it('formats a metrics query', () => {
454+
expect(formatLogqlQuery('count_over_time({job="grafana"}[1m])', ds)).toBe(
455+
'count_over_time(\n {job="grafana"}\n [1m]\n)'
456+
);
457+
});
458+
459+
it('formats a metrics query with variables', () => {
460+
// mock the interpolateString return value so it passes the isValid check
461+
ds.interpolateString = jest.fn(() => 'rate({job="grafana"}[1s])');
462+
463+
expect(formatLogqlQuery('rate({job="grafana"}[$__range])', ds)).toBe('rate(\n {job="grafana"}\n [$__range]\n)');
464+
expect(formatLogqlQuery('rate({job="grafana"}[$__interval])', ds)).toBe(
465+
'rate(\n {job="grafana"}\n [$__interval]\n)'
466+
);
467+
});
468+
});

public/app/plugins/datasource/loki/queryUtils.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ import {
1919
Identifier,
2020
Distinct,
2121
Range,
22+
formatLokiQuery,
2223
} from '@grafana/lezer-logql';
24+
import { reportInteraction } from '@grafana/runtime';
2325
import { DataQuery } from '@grafana/schema';
2426

25-
import { ErrorId } from '../prometheus/querybuilder/shared/parsingUtils';
27+
import { ErrorId, replaceVariables, returnVariables } from '../prometheus/querybuilder/shared/parsingUtils';
2628

29+
import { placeHolderScopedVars } from './components/monaco-query-field/monaco-completion-provider/validation';
30+
import { LokiDatasource } from './datasource';
2731
import { getStreamSelectorPositions, NodePosition } from './modifyQuery';
2832
import { LokiQuery, LokiQueryType } from './types';
2933

@@ -293,3 +297,39 @@ export const getLokiQueryFromDataQuery = (query?: DataQuery): LokiQuery | undefi
293297

294298
return query;
295299
};
300+
301+
export function formatLogqlQuery(query: string, datasource: LokiDatasource) {
302+
const isInvalid = isQueryWithError(datasource.interpolateString(query, placeHolderScopedVars));
303+
304+
reportInteraction('grafana_loki_format_query_clicked', {
305+
is_invalid: isInvalid,
306+
query_type: isLogsQuery(query) ? 'logs' : 'metric',
307+
});
308+
309+
if (isInvalid) {
310+
return query;
311+
}
312+
313+
let transformedQuery = replaceVariables(query);
314+
const transformationMatches = [];
315+
const tree = parser.parse(transformedQuery);
316+
317+
// Variables are considered errors inside of the parser, so we need to remove them before formatting
318+
// We replace all variables with [0s] and keep track of the replaced variables
319+
// After formatting we replace [0s] with the original variable
320+
if (tree.topNode.firstChild?.firstChild?.type.id === MetricExpr) {
321+
const pattern = /\[__V_[0-2]__\w+__V__\]/g;
322+
transformationMatches.push(...transformedQuery.matchAll(pattern));
323+
transformedQuery = transformedQuery.replace(pattern, '[0s]');
324+
}
325+
326+
let formatted = formatLokiQuery(transformedQuery);
327+
328+
if (tree.topNode.firstChild?.firstChild?.type.id === MetricExpr) {
329+
transformationMatches.forEach((match) => {
330+
formatted = formatted.replace('[0s]', match[0]);
331+
});
332+
}
333+
334+
return returnVariables(formatted);
335+
}

public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { css } from '@emotion/css';
22
import React from 'react';
33

44
import { GrafanaTheme2 } from '@grafana/data';
5-
import { useStyles2 } from '@grafana/ui';
5+
import { config } from '@grafana/runtime';
6+
import { useStyles2, HorizontalGroup, IconButton, Tooltip, Icon } from '@grafana/ui';
7+
import { getModKey } from 'app/core/utils/browser';
68

79
import { testIds } from '../../components/LokiQueryEditor';
810
import { LokiQueryField } from '../../components/LokiQueryField';
911
import { getStats } from '../../components/stats';
1012
import { LokiQueryEditorProps } from '../../components/types';
13+
import { formatLogqlQuery } from '../../queryUtils';
1114
import { QueryStats } from '../../types';
1215

1316
import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained';
@@ -31,6 +34,9 @@ export function LokiQueryCodeEditor({
3134
}: Props) {
3235
const styles = useStyles2(getStyles);
3336

37+
const lokiFormatQuery = config.featureToggles.lokiFormatQuery;
38+
const onClickFormatQueryButton = async () => onChange({ ...query, expr: formatLogqlQuery(query.expr, datasource) });
39+
3440
return (
3541
<div className={styles.wrapper}>
3642
<LokiQueryField
@@ -47,6 +53,27 @@ export function LokiQueryCodeEditor({
4753
const stats = await getStats(datasource, query);
4854
setQueryStats(stats);
4955
}}
56+
ExtraFieldElement={
57+
<>
58+
{lokiFormatQuery && (
59+
<div className={styles.buttonGroup}>
60+
<div>
61+
<HorizontalGroup spacing="sm">
62+
<IconButton
63+
onClick={onClickFormatQueryButton}
64+
name="brackets-curly"
65+
size="xs"
66+
tooltip="Format query"
67+
/>
68+
<Tooltip content={`Use ${getModKey()}+z to undo`}>
69+
<Icon className={styles.hint} name="keyboard" />
70+
</Tooltip>
71+
</HorizontalGroup>
72+
</div>
73+
</div>
74+
)}
75+
</>
76+
}
5077
/>
5178
{showExplain && <LokiQueryBuilderExplained query={query.expr} />}
5279
</div>
@@ -61,5 +88,20 @@ const getStyles = (theme: GrafanaTheme2) => {
6188
margin-bottom: 0.5;
6289
}
6390
`,
91+
buttonGroup: css`
92+
border: 1px solid ${theme.colors.border.medium};
93+
border-top: none;
94+
padding: ${theme.spacing(0.5, 0.5, 0.5, 0.5)};
95+
margin-bottom: ${theme.spacing(0.5)};
96+
display: flex;
97+
flex-grow: 1;
98+
justify-content: end;
99+
font-size: ${theme.typography.bodySmall.fontSize};
100+
`,
101+
hint: css`
102+
color: ${theme.colors.text.disabled};
103+
white-space: nowrap;
104+
cursor: help;
105+
`,
64106
};
65107
};

0 commit comments

Comments
 (0)