Loki: Add lokiQueryHints feature flag (#78953)
* add `lokiQueryHints` feature flag * language provider tests * fix unwrap tests * add feature toggle
This commit is contained in:
@@ -53,6 +53,7 @@ Some features are enabled by default. You can disable these feature by setting t
|
|||||||
| `alertingInsights` | Show the new alerting insights landing page | Yes |
|
| `alertingInsights` | Show the new alerting insights landing page | Yes |
|
||||||
| `cloudWatchWildCardDimensionValues` | Fetches dimension values from CloudWatch to correctly label wildcard dimensions | Yes |
|
| `cloudWatchWildCardDimensionValues` | Fetches dimension values from CloudWatch to correctly label wildcard dimensions | Yes |
|
||||||
| `displayAnonymousStats` | Enables anonymous stats to be shown in the UI for Grafana | Yes |
|
| `displayAnonymousStats` | Enables anonymous stats to be shown in the UI for Grafana | Yes |
|
||||||
|
| `lokiQueryHints` | Enables query hints for Loki | Yes |
|
||||||
|
|
||||||
## Preview feature toggles
|
## Preview feature toggles
|
||||||
|
|
||||||
|
|||||||
@@ -169,4 +169,5 @@ export interface FeatureToggles {
|
|||||||
regressionTransformation?: boolean;
|
regressionTransformation?: boolean;
|
||||||
displayAnonymousStats?: boolean;
|
displayAnonymousStats?: boolean;
|
||||||
alertStateHistoryAnnotationsFromLoki?: boolean;
|
alertStateHistoryAnnotationsFromLoki?: boolean;
|
||||||
|
lokiQueryHints?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1270,5 +1270,16 @@ var (
|
|||||||
RequiresRestart: true,
|
RequiresRestart: true,
|
||||||
Created: time.Date(2023, time.November, 30, 12, 0, 0, 0, time.UTC),
|
Created: time.Date(2023, time.November, 30, 12, 0, 0, 0, time.UTC),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// this is mainly used a a way to quickly disable query hints as a safe guard for our infrastructure
|
||||||
|
Name: "lokiQueryHints",
|
||||||
|
Description: "Enables query hints for Loki",
|
||||||
|
Stage: FeatureStageGeneralAvailability,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Expression: "true",
|
||||||
|
Owner: grafanaObservabilityLogsSquad,
|
||||||
|
AllowSelfServe: false,
|
||||||
|
Created: time.Date(2023, time.December, 18, 12, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -150,3 +150,4 @@ tableSharedCrosshair,experimental,@grafana/grafana-bi-squad,2023-12-12,false,fal
|
|||||||
regressionTransformation,privatePreview,@grafana/grafana-bi-squad,2023-11-24,false,false,false,true
|
regressionTransformation,privatePreview,@grafana/grafana-bi-squad,2023-11-24,false,false,false,true
|
||||||
displayAnonymousStats,GA,@grafana/identity-access-team,2023-11-29,false,false,false,true
|
displayAnonymousStats,GA,@grafana/identity-access-team,2023-11-29,false,false,false,true
|
||||||
alertStateHistoryAnnotationsFromLoki,experimental,@grafana/alerting-squad,2023-11-30,false,false,true,false
|
alertStateHistoryAnnotationsFromLoki,experimental,@grafana/alerting-squad,2023-11-30,false,false,true,false
|
||||||
|
lokiQueryHints,GA,@grafana/observability-logs,2023-12-18,false,false,false,true
|
||||||
|
|||||||
|
@@ -610,4 +610,8 @@ const (
|
|||||||
// FlagAlertStateHistoryAnnotationsFromLoki
|
// FlagAlertStateHistoryAnnotationsFromLoki
|
||||||
// Enable using Loki as the source for panel annotations generated by alert rules
|
// Enable using Loki as the source for panel annotations generated by alert rules
|
||||||
FlagAlertStateHistoryAnnotationsFromLoki = "alertStateHistoryAnnotationsFromLoki"
|
FlagAlertStateHistoryAnnotationsFromLoki = "alertStateHistoryAnnotationsFromLoki"
|
||||||
|
|
||||||
|
// FlagLokiQueryHints
|
||||||
|
// Enables query hints for Loki
|
||||||
|
FlagLokiQueryHints = "lokiQueryHints"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, getDefaultTimeRange } from '@grafana/data';
|
import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, getDefaultTimeRange } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import LanguageProvider from './LanguageProvider';
|
import LanguageProvider from './LanguageProvider';
|
||||||
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
||||||
@@ -328,6 +329,14 @@ describe('Query imports', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getParserAndLabelKeys()', () => {
|
describe('getParserAndLabelKeys()', () => {
|
||||||
|
const queryHintsFeatureToggle = config.featureToggles.lokiQueryHints;
|
||||||
|
beforeAll(() => {
|
||||||
|
config.featureToggles.lokiQueryHints = true;
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
config.featureToggles.lokiQueryHints = queryHintsFeatureToggle;
|
||||||
|
});
|
||||||
|
|
||||||
let datasource: LokiDatasource, languageProvider: LanguageProvider;
|
let datasource: LokiDatasource, languageProvider: LanguageProvider;
|
||||||
const extractLogParserFromDataFrameMock = jest.mocked(extractLogParserFromDataFrame);
|
const extractLogParserFromDataFrameMock = jest.mocked(extractLogParserFromDataFrame);
|
||||||
const extractedLabelKeys = ['extracted', 'label'];
|
const extractedLabelKeys = ['extracted', 'label'];
|
||||||
@@ -452,6 +461,12 @@ describe('Query imports', () => {
|
|||||||
mockTimeRange
|
mockTimeRange
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('does not call dataSample with feature toggle disabled', async () => {
|
||||||
|
config.featureToggles.lokiQueryHints = false;
|
||||||
|
jest.spyOn(datasource, 'getDataSamples');
|
||||||
|
languageProvider.getParserAndLabelKeys('{place="luna"}', { timeRange: mockTimeRange });
|
||||||
|
expect(datasource.getDataSamples).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { LRUCache } from 'lru-cache';
|
|||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
|
|
||||||
import { LanguageProvider, AbstractQuery, KeyValue, getDefaultTimeRange, TimeRange } from '@grafana/data';
|
import { LanguageProvider, AbstractQuery, KeyValue, getDefaultTimeRange, TimeRange } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { extractLabelMatchers, processLabels, toPromLikeExpr } from 'app/plugins/datasource/prometheus/language_utils';
|
import { extractLabelMatchers, processLabels, toPromLikeExpr } from 'app/plugins/datasource/prometheus/language_utils';
|
||||||
|
|
||||||
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
||||||
@@ -261,6 +262,18 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
streamSelector: string,
|
streamSelector: string,
|
||||||
options?: { maxLines?: number; timeRange?: TimeRange }
|
options?: { maxLines?: number; timeRange?: TimeRange }
|
||||||
): Promise<ParserAndLabelKeysResult> {
|
): Promise<ParserAndLabelKeysResult> {
|
||||||
|
const empty = {
|
||||||
|
extractedLabelKeys: [],
|
||||||
|
structuredMetadataKeys: [],
|
||||||
|
unwrapLabelKeys: [],
|
||||||
|
hasJSON: false,
|
||||||
|
hasLogfmt: false,
|
||||||
|
hasPack: false,
|
||||||
|
};
|
||||||
|
if (!config.featureToggles.lokiQueryHints) {
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
const series = await this.datasource.getDataSamples(
|
const series = await this.datasource.getDataSamples(
|
||||||
{
|
{
|
||||||
expr: streamSelector,
|
expr: streamSelector,
|
||||||
@@ -271,14 +284,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!series.length) {
|
if (!series.length) {
|
||||||
return {
|
return empty;
|
||||||
extractedLabelKeys: [],
|
|
||||||
structuredMetadataKeys: [],
|
|
||||||
unwrapLabelKeys: [],
|
|
||||||
hasJSON: false,
|
|
||||||
hasLogfmt: false,
|
|
||||||
hasPack: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasLogfmt, hasJSON, hasPack } = extractLogParserFromDataFrame(series[0]);
|
const { hasLogfmt, hasJSON, hasPack } = extractLogParserFromDataFrame(series[0]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue, TimeRange } from '@grafana/data';
|
import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue, TimeRange } from '@grafana/data';
|
||||||
import { EditorRow } from '@grafana/experimental';
|
import { EditorRow } from '@grafana/experimental';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters';
|
import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters';
|
||||||
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
|
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
|
||||||
import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList';
|
import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList';
|
||||||
@@ -105,7 +106,9 @@ export const LokiQueryBuilder = React.memo<Props>(
|
|||||||
setSampleData(sampleData);
|
setSampleData(sampleData);
|
||||||
};
|
};
|
||||||
|
|
||||||
onGetSampleData().catch(console.error);
|
if (config.featureToggles.lokiQueryHints) {
|
||||||
|
onGetSampleData().catch(console.error);
|
||||||
|
}
|
||||||
}, [datasource, query, timeRange]);
|
}, [datasource, query, timeRange]);
|
||||||
|
|
||||||
const lang = { grammar: logqlGrammar, name: 'logql' };
|
const lang = { grammar: logqlGrammar, name: 'logql' };
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
|
|
||||||
import { DataFrame, DataSourceApi, FieldType, toDataFrame } from '@grafana/data';
|
import { DataFrame, DataSourceApi, FieldType, toDataFrame } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
QueryBuilderOperation,
|
QueryBuilderOperation,
|
||||||
QueryBuilderOperationParamDef,
|
QueryBuilderOperationParamDef,
|
||||||
@@ -15,6 +16,14 @@ import { LokiOperationId } from '../types';
|
|||||||
import { UnwrapParamEditor } from './UnwrapParamEditor';
|
import { UnwrapParamEditor } from './UnwrapParamEditor';
|
||||||
|
|
||||||
describe('UnwrapParamEditor', () => {
|
describe('UnwrapParamEditor', () => {
|
||||||
|
const queryHintsFeatureToggle = config.featureToggles.lokiQueryHints;
|
||||||
|
beforeAll(() => {
|
||||||
|
config.featureToggles.lokiQueryHints = true;
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
config.featureToggles.lokiQueryHints = queryHintsFeatureToggle;
|
||||||
|
});
|
||||||
|
|
||||||
it('shows value if value present', () => {
|
it('shows value if value present', () => {
|
||||||
const props = createProps({ value: 'unique' });
|
const props = createProps({ value: 'unique' });
|
||||||
render(<UnwrapParamEditor {...props} />);
|
render(<UnwrapParamEditor {...props} />);
|
||||||
@@ -53,6 +62,16 @@ describe('UnwrapParamEditor', () => {
|
|||||||
expect(await screen.findByText('status')).toBeInTheDocument();
|
expect(await screen.findByText('status')).toBeInTheDocument();
|
||||||
expect(await screen.findByText('duration')).toBeInTheDocument();
|
expect(await screen.findByText('duration')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show labels with unwrap-friendly values when feature is disabled', async () => {
|
||||||
|
config.featureToggles.lokiQueryHints = false;
|
||||||
|
const props = createProps({}, frames);
|
||||||
|
render(<UnwrapParamEditor {...props} />);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
await userEvent.click(input);
|
||||||
|
expect(screen.queryByText('status')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('duration')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const createProps = (
|
const createProps = (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue, getDefaultTimeRange, toOption } from '@grafana/data';
|
import { SelectableValue, getDefaultTimeRange, toOption } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { Select } from '@grafana/ui';
|
import { Select } from '@grafana/ui';
|
||||||
|
|
||||||
import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types';
|
import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types';
|
||||||
@@ -31,7 +32,7 @@ export function UnwrapParamEditor({
|
|||||||
inputId={getOperationParamId(operationId, index)}
|
inputId={getOperationParamId(operationId, index)}
|
||||||
onOpenMenu={async () => {
|
onOpenMenu={async () => {
|
||||||
// This check is always true, we do it to make typescript happy
|
// This check is always true, we do it to make typescript happy
|
||||||
if (datasource instanceof LokiDatasource) {
|
if (datasource instanceof LokiDatasource && config.featureToggles.lokiQueryHints) {
|
||||||
setState({ isLoading: true });
|
setState({ isLoading: true });
|
||||||
const options = await loadUnwrapOptions(query, datasource, timeRange);
|
const options = await loadUnwrapOptions(query, datasource, timeRange);
|
||||||
setState({ options, isLoading: undefined });
|
setState({ options, isLoading: undefined });
|
||||||
|
|||||||
Reference in New Issue
Block a user