diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index d9e9fcd11dc..ca6d1247c80 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/package.json b/package.json index 248dbd4d8b2..500b539285b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3bff251e63a..867ee5df994 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -98,6 +98,7 @@ export interface FeatureToggles { dashboardEmbed?: boolean; frontendSandboxMonitorOnly?: boolean; sqlDatasourceDatabaseSelection?: boolean; + lokiFormatQuery?: boolean; cloudWatchLogsMonacoEditor?: boolean; exploreScrollableLogsContainer?: boolean; recordedQueriesMulti?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2a8e800dc54..925d716e8cd 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index dad0520442a..a1caf4da325 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 199ad9d50cb..3840d333f57 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 7a317af5a27..58c2d5137e3 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -75,7 +75,7 @@ export class LokiQueryField extends React.PureComponent -
+
{ 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)' + ); + }); +}); diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index 37ac9cb9df1..717b90e97b6 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -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); +} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx index 07d5ea9f412..9ee1ea8cfe0 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx @@ -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 (
+ {lokiFormatQuery && ( +
+
+ + + + + + +
+
+ )} + + } /> {showExplain && }
@@ -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; + `, }; }; diff --git a/yarn.lock b/yarn.lock index 9e3700d07b2..715d2ec0015 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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