Loki: Default to /labels API with query param instead of /series API (#97935)

* feat(loki-labels-api): add feature toggle

* feat(loki-labels-api): default to `/labels` API
This commit is contained in:
Sven Grossmann 2024-12-13 15:31:41 +01:00 committed by GitHub
parent 4550cfb5b7
commit 5ac7443fce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 84 additions and 17 deletions

View File

@ -84,6 +84,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `zipkinBackendMigration` | Enables querying Zipkin data source without the proxy | Yes |
| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes |
| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes |
| `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes |
## Public preview feature toggles

View File

@ -244,4 +244,5 @@ export interface FeatureToggles {
feedbackButton?: boolean;
elasticsearchCrossClusterSearch?: boolean;
unifiedHistory?: boolean;
lokiLabelNamesQueryApi?: boolean;
}

View File

@ -1689,6 +1689,13 @@ var (
Owner: grafanaFrontendPlatformSquad,
FrontendOnly: true,
},
{
Name: "lokiLabelNamesQueryApi",
Description: "Defaults to using the Loki `/labels` API instead of `/series`",
Stage: FeatureStageGeneralAvailability,
Owner: grafanaObservabilityLogsSquad,
Expression: "true",
},
}
)

View File

@ -225,3 +225,4 @@ alertingNotificationsStepMode,experimental,@grafana/alerting-squad,false,false,t
feedbackButton,experimental,@grafana/grafana-operator-experience-squad,false,false,false
elasticsearchCrossClusterSearch,preview,@grafana/aws-datasources,false,false,false
unifiedHistory,experimental,@grafana/grafana-frontend-platform,false,false,true
lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
225 feedbackButton experimental @grafana/grafana-operator-experience-squad false false false
226 elasticsearchCrossClusterSearch preview @grafana/aws-datasources false false false
227 unifiedHistory experimental @grafana/grafana-frontend-platform false false true
228 lokiLabelNamesQueryApi GA @grafana/observability-logs false false false

View File

@ -910,4 +910,8 @@ const (
// FlagUnifiedHistory
// Displays the navigation history so the user can navigate back to previous pages
FlagUnifiedHistory = "unifiedHistory"
// FlagLokiLabelNamesQueryApi
// Defaults to using the Loki `/labels` API instead of `/series`
FlagLokiLabelNamesQueryApi = "lokiLabelNamesQueryApi"
)

View File

@ -2095,6 +2095,19 @@
"codeowner": "@grafana/observability-logs"
}
},
{
"metadata": {
"name": "lokiLabelNamesQueryApi",
"resourceVersion": "1734096677730",
"creationTimestamp": "2024-12-13T13:31:17Z"
},
"spec": {
"description": "Defaults to using the Loki `/labels` API instead of `/series`",
"stage": "GA",
"codeowner": "@grafana/observability-logs",
"expression": "true"
}
},
{
"metadata": {
"name": "lokiLogsDataplane",

View File

@ -428,17 +428,51 @@ describe('Language completion provider', () => {
expect(instance.request).toHaveBeenCalledWith('labels', datasourceWithLabels.getTimeRangeParams(mockTimeRange));
});
it('should use series endpoint for request with stream selector', async () => {
const datasourceWithLabels = setup({});
datasourceWithLabels.languageProvider.request = jest.fn();
describe('without labelNames feature toggle', () => {
const lokiLabelNamesQueryApi = config.featureToggles.lokiLabelNamesQueryApi;
beforeAll(() => {
config.featureToggles.lokiLabelNamesQueryApi = false;
});
afterAll(() => {
config.featureToggles.lokiLabelNamesQueryApi = lokiLabelNamesQueryApi;
});
const instance = new LanguageProvider(datasourceWithLabels);
instance.request = jest.fn();
await instance.fetchLabels({ streamSelector: '{foo="bar"}' });
expect(instance.request).toHaveBeenCalledWith('series', {
end: 1560163909000,
'match[]': '{foo="bar"}',
start: 1560153109000,
it('should use series endpoint for request with stream selector', async () => {
const datasourceWithLabels = setup({});
datasourceWithLabels.languageProvider.request = jest.fn();
const instance = new LanguageProvider(datasourceWithLabels);
instance.request = jest.fn();
await instance.fetchLabels({ streamSelector: '{foo="bar"}' });
expect(instance.request).toHaveBeenCalledWith('series', {
end: 1560163909000,
'match[]': '{foo="bar"}',
start: 1560153109000,
});
});
});
describe('with labelNames feature toggle', () => {
const lokiLabelNamesQueryApi = config.featureToggles.lokiLabelNamesQueryApi;
beforeAll(() => {
config.featureToggles.lokiLabelNamesQueryApi = true;
});
afterAll(() => {
config.featureToggles.lokiLabelNamesQueryApi = lokiLabelNamesQueryApi;
});
it('should use series endpoint for request with stream selector', async () => {
const datasourceWithLabels = setup({});
datasourceWithLabels.languageProvider.request = jest.fn();
const instance = new LanguageProvider(datasourceWithLabels);
instance.request = jest.fn();
await instance.fetchLabels({ streamSelector: '{foo="bar"}' });
expect(instance.request).toHaveBeenCalledWith('labels', {
end: 1560163909000,
query: '{foo="bar"}',
start: 1560153109000,
});
});
});

View File

@ -147,8 +147,8 @@ export default class LokiLanguageProvider extends LanguageProvider {
* @throws An error if the fetch operation fails.
*/
async fetchLabels(options?: { streamSelector?: string; timeRange?: TimeRange }): Promise<string[]> {
// If there is no stream selector - use /labels endpoint (https://github.com/grafana/loki/pull/11982)
if (!options || !options.streamSelector) {
// We'll default to use `/labels`. If the flag is disabled, and there's a streamSelector, we'll use the series endpoint.
if (config.featureToggles.lokiLabelNamesQueryApi || !options?.streamSelector) {
return this.fetchLabelsByLabelsEndpoint(options);
} else {
const data = await this.fetchSeriesLabels(options.streamSelector, { timeRange: options.timeRange });
@ -166,14 +166,20 @@ export default class LokiLanguageProvider extends LanguageProvider {
* @returns A promise containing an array of label keys.
* @throws An error if the fetch operation fails.
*/
private async fetchLabelsByLabelsEndpoint(options?: { timeRange?: TimeRange }): Promise<string[]> {
private async fetchLabelsByLabelsEndpoint(options?: {
streamSelector?: string;
timeRange?: TimeRange;
}): Promise<string[]> {
const url = 'labels';
const range = options?.timeRange ?? this.getDefaultTimeRange();
const timeRange = this.datasource.getTimeRangeParams(range);
const res = await this.request(url, timeRange);
const { start, end } = this.datasource.getTimeRangeParams(range);
const params: Record<string, string | number> = { start, end };
if (options?.streamSelector) {
params['query'] = options.streamSelector;
}
const res = await this.request(url, params);
if (Array.isArray(res)) {
const labels = res
const labels = Array.from(new Set(res))
.slice()
.sort()
.filter((label: string) => label.startsWith('__') === false);