Loki Autocomplete: Suggest only possible labels for unwrap (#61411)

* feat(loki-autocomplete): filter valid labels for unwrap autocomplete suggestions

* refactor(unwrap): reuse new function in builder
pull/61274/head
Matias Chomicki 2 years ago committed by GitHub
parent 7e51eac740
commit 7e505ea49c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      public/app/plugins/datasource/loki/LanguageProvider.test.ts
  2. 17
      public/app/plugins/datasource/loki/LanguageProvider.ts
  3. 1
      public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts
  4. 19
      public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts
  5. 4
      public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts
  6. 30
      public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx
  7. 14
      public/app/plugins/datasource/loki/responseUtils.test.ts
  8. 26
      public/app/plugins/datasource/loki/responseUtils.ts

@ -6,7 +6,11 @@ import { TypeaheadInput } from '@grafana/ui';
import LanguageProvider, { LokiHistoryItem } from './LanguageProvider'; import LanguageProvider, { LokiHistoryItem } from './LanguageProvider';
import { LokiDatasource } from './datasource'; import { LokiDatasource } from './datasource';
import { createLokiDatasource, createMetadataRequest } from './mocks'; import { createLokiDatasource, createMetadataRequest } from './mocks';
import { extractLogParserFromDataFrame, extractLabelKeysFromDataFrame } from './responseUtils'; import {
extractLogParserFromDataFrame,
extractLabelKeysFromDataFrame,
extractUnwrapLabelKeysFromDataFrame,
} from './responseUtils';
import { LokiQueryType } from './types'; import { LokiQueryType } from './types';
jest.mock('./responseUtils'); jest.mock('./responseUtils');
@ -304,11 +308,13 @@ describe('Query imports', () => {
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'];
const unwrapLabelKeys = ['unwrap', 'labels'];
beforeEach(() => { beforeEach(() => {
datasource = createLokiDatasource(); datasource = createLokiDatasource();
languageProvider = new LanguageProvider(datasource); languageProvider = new LanguageProvider(datasource);
jest.mocked(extractLabelKeysFromDataFrame).mockReturnValue(extractedLabelKeys); jest.mocked(extractLabelKeysFromDataFrame).mockReturnValue(extractedLabelKeys);
jest.mocked(extractUnwrapLabelKeysFromDataFrame).mockReturnValue(unwrapLabelKeys);
}); });
it('identifies selectors with JSON parser data', async () => { it('identifies selectors with JSON parser data', async () => {
@ -317,6 +323,7 @@ describe('Query imports', () => {
expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({ expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({
extractedLabelKeys, extractedLabelKeys,
unwrapLabelKeys,
hasJSON: true, hasJSON: true,
hasLogfmt: false, hasLogfmt: false,
}); });
@ -328,6 +335,7 @@ describe('Query imports', () => {
expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({ expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({
extractedLabelKeys, extractedLabelKeys,
unwrapLabelKeys,
hasJSON: false, hasJSON: false,
hasLogfmt: true, hasLogfmt: true,
}); });
@ -339,6 +347,7 @@ describe('Query imports', () => {
expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({ expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({
extractedLabelKeys: [], extractedLabelKeys: [],
unwrapLabelKeys: [],
hasJSON: false, hasJSON: false,
hasLogfmt: false, hasLogfmt: false,
}); });

@ -12,7 +12,11 @@ import {
} from 'app/plugins/datasource/prometheus/language_utils'; } from 'app/plugins/datasource/prometheus/language_utils';
import { LokiDatasource } from './datasource'; import { LokiDatasource } from './datasource';
import { extractLabelKeysFromDataFrame, extractLogParserFromDataFrame } from './responseUtils'; import {
extractLabelKeysFromDataFrame,
extractLogParserFromDataFrame,
extractUnwrapLabelKeysFromDataFrame,
} from './responseUtils';
import syntax, { FUNCTIONS, PIPE_PARSERS, PIPE_OPERATORS } from './syntax'; import syntax, { FUNCTIONS, PIPE_PARSERS, PIPE_OPERATORS } from './syntax';
import { LokiQuery, LokiQueryType } from './types'; import { LokiQuery, LokiQueryType } from './types';
@ -465,15 +469,20 @@ export default class LokiLanguageProvider extends LanguageProvider {
async getParserAndLabelKeys( async getParserAndLabelKeys(
selector: string selector: string
): Promise<{ extractedLabelKeys: string[]; hasJSON: boolean; hasLogfmt: boolean }> { ): Promise<{ extractedLabelKeys: string[]; hasJSON: boolean; hasLogfmt: boolean; unwrapLabelKeys: string[] }> {
const series = await this.datasource.getDataSamples({ expr: selector, refId: 'data-samples' }); const series = await this.datasource.getDataSamples({ expr: selector, refId: 'data-samples' });
if (!series.length) { if (!series.length) {
return { extractedLabelKeys: [], hasJSON: false, hasLogfmt: false }; return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false };
} }
const { hasLogfmt, hasJSON } = extractLogParserFromDataFrame(series[0]); const { hasLogfmt, hasJSON } = extractLogParserFromDataFrame(series[0]);
return { extractedLabelKeys: extractLabelKeysFromDataFrame(series[0]), hasJSON, hasLogfmt }; return {
extractedLabelKeys: extractLabelKeysFromDataFrame(series[0]),
unwrapLabelKeys: extractUnwrapLabelKeysFromDataFrame(series[0]),
hasJSON,
hasLogfmt,
};
} }
} }

@ -50,6 +50,7 @@ const otherLabels: Label[] = [
const seriesLabels = { place: ['series', 'labels'], source: [], other: [] }; const seriesLabels = { place: ['series', 'labels'], source: [], other: [] };
const parserAndLabelKeys = { const parserAndLabelKeys = {
extractedLabelKeys: ['extracted', 'label', 'keys'], extractedLabelKeys: ['extracted', 'label', 'keys'],
unwrapLabelKeys: ['unwrap', 'labels'],
hasJSON: true, hasJSON: true,
hasLogfmt: false, hasLogfmt: false,
}; };

@ -31,6 +31,7 @@ const labelNames = ['place', 'source'];
const labelValues = ['moon', 'luna', 'server\\1']; const labelValues = ['moon', 'luna', 'server\\1'];
// Source is duplicated to test handling duplicated labels // Source is duplicated to test handling duplicated labels
const extractedLabelKeys = ['extracted', 'place', 'source']; const extractedLabelKeys = ['extracted', 'place', 'source'];
const unwrapLabelKeys = ['unwrap', 'labels'];
const otherLabels: Label[] = [ const otherLabels: Label[] = [
{ {
name: 'place', name: 'place',
@ -195,6 +196,7 @@ describe('getCompletions', () => {
jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues); jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues);
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys, extractedLabelKeys,
unwrapLabelKeys,
hasJSON: false, hasJSON: false,
hasLogfmt: false, hasLogfmt: false,
}); });
@ -327,6 +329,7 @@ describe('getCompletions', () => {
async (afterPipe: boolean, hasSpace: boolean) => { async (afterPipe: boolean, hasSpace: boolean) => {
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys, extractedLabelKeys,
unwrapLabelKeys,
hasJSON: true, hasJSON: true,
hasLogfmt: false, hasLogfmt: false,
}); });
@ -343,6 +346,7 @@ describe('getCompletions', () => {
async (afterPipe: boolean) => { async (afterPipe: boolean) => {
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys, extractedLabelKeys,
unwrapLabelKeys,
hasJSON: false, hasJSON: false,
hasLogfmt: true, hasLogfmt: true,
}); });
@ -368,7 +372,20 @@ describe('getCompletions', () => {
const extractedCompletions = completions.filter((completion) => completion.type === 'LABEL_NAME'); const extractedCompletions = completions.filter((completion) => completion.type === 'LABEL_NAME');
const functionCompletions = completions.filter((completion) => completion.type === 'FUNCTION'); const functionCompletions = completions.filter((completion) => completion.type === 'FUNCTION');
expect(extractedCompletions).toHaveLength(3); expect(extractedCompletions).toEqual([
{
insertText: 'unwrap',
label: 'unwrap',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
{
insertText: 'labels',
label: 'labels',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
]);
expect(functionCompletions).toHaveLength(3); expect(functionCompletions).toHaveLength(3);
}); });
}); });

@ -273,9 +273,9 @@ async function getAfterUnwrapCompletions(
logQuery: string, logQuery: string,
dataProvider: CompletionDataProvider dataProvider: CompletionDataProvider
): Promise<Completion[]> { ): Promise<Completion[]> {
const { extractedLabelKeys } = await dataProvider.getParserAndLabelKeys(logQuery); const { unwrapLabelKeys } = await dataProvider.getParserAndLabelKeys(logQuery);
const labelCompletions: Completion[] = extractedLabelKeys.map((label) => ({ const labelCompletions: Completion[] = unwrapLabelKeys.map((label) => ({
type: 'LABEL_NAME', type: 'LABEL_NAME',
label, label,
insertText: label, insertText: label,

@ -1,14 +1,13 @@
import { isNaN } from 'lodash';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; import { SelectableValue, toOption } from '@grafana/data';
import { Select } from '@grafana/ui'; import { Select } from '@grafana/ui';
import { getOperationParamId } from '../../../prometheus/querybuilder/shared/operationUtils'; import { getOperationParamId } from '../../../prometheus/querybuilder/shared/operationUtils';
import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types'; import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types';
import { LokiDatasource } from '../../datasource'; import { LokiDatasource } from '../../datasource';
import { isBytesString } from '../../languageUtils';
import { getLogQueryFromMetricsQuery, isValidQuery } from '../../queryUtils'; import { getLogQueryFromMetricsQuery, isValidQuery } from '../../queryUtils';
import { extractUnwrapLabelKeysFromDataFrame } from '../../responseUtils';
import { lokiQueryModeller } from '../LokiQueryModeller'; import { lokiQueryModeller } from '../LokiQueryModeller';
import { LokiVisualQuery } from '../types'; import { LokiVisualQuery } from '../types';
@ -62,30 +61,7 @@ async function loadUnwrapOptions(
} }
const samples = await datasource.getDataSamples({ expr: logExpr, refId: 'unwrap_samples' }); const samples = await datasource.getDataSamples({ expr: logExpr, refId: 'unwrap_samples' });
const labelsArray: Array<{ [key: string]: string }> | undefined = const unwrapLabels = extractUnwrapLabelKeysFromDataFrame(samples[0]);
samples[0]?.fields?.find((field) => field.name === 'labels')?.values.toArray() ?? [];
if (!labelsArray || labelsArray.length === 0) {
return [];
}
// We do this only for first label object, because we want to consider only labels that are present in all log lines
// possibleUnwrapLabels are labels with 1. number value OR 2. value that is valid go duration OR 3. bytes string value
const possibleUnwrapLabels = Object.keys(labelsArray[0]).filter((key) => {
const value = labelsArray[0][key];
if (!value) {
return false;
}
return !isNaN(Number(value)) || isValidGoDuration(value) || isBytesString(value);
});
const unwrapLabels: string[] = [];
for (const label of possibleUnwrapLabels) {
// Add only labels that are present in every line to unwrapLabels
if (labelsArray.every((obj) => obj[label])) {
unwrapLabels.push(label);
}
}
const labelOptions = unwrapLabels.map((label) => ({ const labelOptions = unwrapLabels.map((label) => ({
label, label,

@ -8,6 +8,7 @@ import {
extractLevelLikeLabelFromDataFrame, extractLevelLikeLabelFromDataFrame,
extractLogParserFromDataFrame, extractLogParserFromDataFrame,
extractLabelKeysFromDataFrame, extractLabelKeysFromDataFrame,
extractUnwrapLabelKeysFromDataFrame,
} from './responseUtils'; } from './responseUtils';
const frame: DataFrame = { const frame: DataFrame = {
@ -105,3 +106,16 @@ describe('extractLabelKeysFromDataFrame', () => {
expect(extractLabelKeysFromDataFrame(input)).toEqual(['level']); expect(extractLabelKeysFromDataFrame(input)).toEqual(['level']);
}); });
}); });
describe('extractUnwrapLabelKeysFromDataFrame', () => {
it('returns empty by default', () => {
const input = cloneDeep(frame);
input.fields[1].values = new ArrayVector([]);
expect(extractUnwrapLabelKeysFromDataFrame(input)).toEqual([]);
});
it('extracts possible unwrap label keys', () => {
const input = cloneDeep(frame);
input.fields[1].values = new ArrayVector([{ number: 13 }]);
expect(extractUnwrapLabelKeysFromDataFrame(input)).toEqual(['number']);
});
});

@ -1,7 +1,9 @@
import { DataFrame, FieldType, Labels } from '@grafana/data'; import { DataFrame, FieldType, isValidGoDuration, Labels } from '@grafana/data';
import { isLogLineJSON, isLogLineLogfmt } from './lineParser'; import { isLogLineJSON, isLogLineLogfmt } from './lineParser';
import { isBytesString } from './languageUtils';
export function dataFrameHasLokiError(frame: DataFrame): boolean { export function dataFrameHasLokiError(frame: DataFrame): boolean {
const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values.toArray() ?? []; const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values.toArray() ?? [];
return labelSets.some((labels) => labels.__error__ !== undefined); return labelSets.some((labels) => labels.__error__ !== undefined);
@ -46,6 +48,28 @@ export function extractLabelKeysFromDataFrame(frame: DataFrame): string[] {
return Object.keys(labelsArray[0]); return Object.keys(labelsArray[0]);
} }
export function extractUnwrapLabelKeysFromDataFrame(frame: DataFrame): string[] {
const labelsArray: Array<{ [key: string]: string }> | undefined =
frame?.fields?.find((field) => field.name === 'labels')?.values.toArray() ?? [];
if (!labelsArray?.length) {
return [];
}
// We do this only for first label object, because we want to consider only labels that are present in all log lines
// possibleUnwrapLabels are labels with 1. number value OR 2. value that is valid go duration OR 3. bytes string value
const possibleUnwrapLabels = Object.keys(labelsArray[0]).filter((key) => {
const value = labelsArray[0][key];
if (!value) {
return false;
}
return !isNaN(Number(value)) || isValidGoDuration(value) || isBytesString(value);
});
// Add only labels that are present in every line to unwrapLabels
return possibleUnwrapLabels.filter((label) => labelsArray.every((obj) => obj[label]));
}
export function extractHasErrorLabelFromDataFrame(frame: DataFrame): boolean { export function extractHasErrorLabelFromDataFrame(frame: DataFrame): boolean {
const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other); const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other);
if (labelField == null) { if (labelField == null) {

Loading…
Cancel
Save