mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
7e51eac740
commit
7e505ea49c
@ -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…
Reference in New Issue
Block a user