mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Loki: Add fetchDetectedFields to LanguageProvider (#99394)
* feat: add fetchDetectedFields to loki LanguageProvider
This commit is contained in:
parent
572be19f76
commit
be40f531e6
@ -2,6 +2,7 @@ import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, getDefaultTimeRa
|
|||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import LanguageProvider from './LanguageProvider';
|
import LanguageProvider from './LanguageProvider';
|
||||||
|
import { createDetectedFieldValuesMetadataRequest } from './__mocks__/createDetectedFieldValuesMetadataRequest';
|
||||||
import { createDetectedFieldsMetadataRequest } from './__mocks__/createDetectedFieldsMetadataRequest';
|
import { createDetectedFieldsMetadataRequest } from './__mocks__/createDetectedFieldsMetadataRequest';
|
||||||
import { createLokiDatasource } from './__mocks__/datasource';
|
import { createLokiDatasource } from './__mocks__/datasource';
|
||||||
import { createMetadataRequest } from './__mocks__/metadataRequest';
|
import { createMetadataRequest } from './__mocks__/metadataRequest';
|
||||||
@ -11,7 +12,7 @@ import {
|
|||||||
extractLabelKeysFromDataFrame,
|
extractLabelKeysFromDataFrame,
|
||||||
extractUnwrapLabelKeysFromDataFrame,
|
extractUnwrapLabelKeysFromDataFrame,
|
||||||
} from './responseUtils';
|
} from './responseUtils';
|
||||||
import { LabelType, LokiQueryType } from './types';
|
import { DetectedFieldsResult, LabelType, LokiQueryType } from './types';
|
||||||
|
|
||||||
jest.mock('./responseUtils');
|
jest.mock('./responseUtils');
|
||||||
|
|
||||||
@ -384,6 +385,85 @@ describe('Language completion provider', () => {
|
|||||||
expect(requestSpy2).toHaveBeenCalledTimes(1);
|
expect(requestSpy2).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('fetchDetectedFields', () => {
|
||||||
|
const expectedOptions = {
|
||||||
|
start: 1546372800000,
|
||||||
|
end: 1546380000000,
|
||||||
|
query: '{cluster=~".+"}',
|
||||||
|
limit: 999,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: {
|
||||||
|
expr: string;
|
||||||
|
timeRange?: TimeRange;
|
||||||
|
limit?: number;
|
||||||
|
scopedVars?: ScopedVars;
|
||||||
|
throwError?: boolean;
|
||||||
|
} = {
|
||||||
|
expr: '{cluster=~".+"}',
|
||||||
|
timeRange: mockTimeRange,
|
||||||
|
limit: 999,
|
||||||
|
throwError: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedResponse: DetectedFieldsResult = {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'bytes',
|
||||||
|
type: 'bytes',
|
||||||
|
cardinality: 6,
|
||||||
|
parsers: ['logfmt'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'traceID',
|
||||||
|
type: 'string',
|
||||||
|
cardinality: 50,
|
||||||
|
parsers: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'active_series',
|
||||||
|
type: 'int',
|
||||||
|
cardinality: 8,
|
||||||
|
parsers: ['logfmt'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
limit: 999,
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasource = detectedFieldsSetup(expectedResponse, {
|
||||||
|
end: mockTimeRange.to.valueOf(),
|
||||||
|
start: mockTimeRange.from.valueOf(),
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch detected label values', async () => {
|
||||||
|
const provider = await getLanguageProvider(datasource, false);
|
||||||
|
const requestSpy = jest.spyOn(provider, 'request');
|
||||||
|
const labelValues = await provider.fetchDetectedFields(options);
|
||||||
|
|
||||||
|
expect(requestSpy).toHaveBeenCalledWith(`detected_fields`, expectedOptions, true, undefined);
|
||||||
|
expect(labelValues).toEqual(expectedResponse);
|
||||||
|
});
|
||||||
|
it('should return values', async () => {
|
||||||
|
const provider = await getLanguageProvider(datasource, false);
|
||||||
|
const requestSpy = jest.spyOn(provider, 'request');
|
||||||
|
const labelValues = await provider.fetchDetectedFields(options);
|
||||||
|
|
||||||
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(labelValues).toEqual(expectedResponse);
|
||||||
|
|
||||||
|
const nextLabelValues = await provider.fetchDetectedFields(options);
|
||||||
|
expect(requestSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(requestSpy).toHaveBeenCalledWith(`detected_fields`, expectedOptions, true, undefined);
|
||||||
|
expect(nextLabelValues).toEqual(expectedResponse);
|
||||||
|
});
|
||||||
|
it('should encode special characters', async () => {
|
||||||
|
const provider = await getLanguageProvider(datasource, false);
|
||||||
|
const requestSpy = jest.spyOn(provider, 'request');
|
||||||
|
await provider.fetchDetectedFields(options);
|
||||||
|
|
||||||
|
expect(requestSpy).toHaveBeenCalledWith('detected_fields', expectedOptions, true, undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('fetchLabels', () => {
|
describe('fetchLabels', () => {
|
||||||
it('should return labels', async () => {
|
it('should return labels', async () => {
|
||||||
@ -762,6 +842,18 @@ function setup(
|
|||||||
function detectedLabelValuesSetup(response: string[], rangeMock: { start: number; end: number }): LokiDatasource {
|
function detectedLabelValuesSetup(response: string[], rangeMock: { start: number; end: number }): LokiDatasource {
|
||||||
const datasource = createLokiDatasource();
|
const datasource = createLokiDatasource();
|
||||||
|
|
||||||
|
jest.spyOn(datasource, 'getTimeRangeParams').mockReturnValue(rangeMock);
|
||||||
|
jest.spyOn(datasource, 'metadataRequest').mockImplementation(createDetectedFieldValuesMetadataRequest(response));
|
||||||
|
jest.spyOn(datasource, 'interpolateString').mockImplementation((string: string) => string);
|
||||||
|
|
||||||
|
return datasource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectedFieldsSetup(
|
||||||
|
response: DetectedFieldsResult,
|
||||||
|
rangeMock: { start: number; end: number }
|
||||||
|
): LokiDatasource {
|
||||||
|
const datasource = createLokiDatasource();
|
||||||
jest.spyOn(datasource, 'getTimeRangeParams').mockReturnValue(rangeMock);
|
jest.spyOn(datasource, 'getTimeRangeParams').mockReturnValue(rangeMock);
|
||||||
jest.spyOn(datasource, 'metadataRequest').mockImplementation(createDetectedFieldsMetadataRequest(response));
|
jest.spyOn(datasource, 'metadataRequest').mockImplementation(createDetectedFieldsMetadataRequest(response));
|
||||||
jest.spyOn(datasource, 'interpolateString').mockImplementation((string: string) => string);
|
jest.spyOn(datasource, 'interpolateString').mockImplementation((string: string) => string);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { flatten } from 'lodash';
|
import { flatten } from 'lodash';
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
|
|
||||||
import { LanguageProvider, AbstractQuery, KeyValue, getDefaultTimeRange, TimeRange, ScopedVars } from '@grafana/data';
|
import { AbstractQuery, getDefaultTimeRange, KeyValue, LanguageProvider, ScopedVars, TimeRange } from '@grafana/data';
|
||||||
import { BackendSrvRequest, config } from '@grafana/runtime';
|
import { BackendSrvRequest, config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
||||||
@ -13,7 +13,7 @@ import {
|
|||||||
extractLogParserFromDataFrame,
|
extractLogParserFromDataFrame,
|
||||||
extractUnwrapLabelKeysFromDataFrame,
|
extractUnwrapLabelKeysFromDataFrame,
|
||||||
} from './responseUtils';
|
} from './responseUtils';
|
||||||
import { ParserAndLabelKeysResult, LokiQuery, LokiQueryType, LabelType } from './types';
|
import { DetectedFieldsResult, LabelType, LokiQuery, LokiQueryType, ParserAndLabelKeysResult } from './types';
|
||||||
|
|
||||||
const NS_IN_MS = 1000000;
|
const NS_IN_MS = 1000000;
|
||||||
const EMPTY_SELECTOR = '{}';
|
const EMPTY_SELECTOR = '{}';
|
||||||
@ -256,6 +256,60 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return nanoseconds ? Math.floor(nanoseconds / NS_IN_MS / 1000 / 60 / 5) : 0;
|
return nanoseconds ? Math.floor(nanoseconds / NS_IN_MS / 1000 / 60 / 5) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchDetectedFields(
|
||||||
|
queryOptions: {
|
||||||
|
expr: string;
|
||||||
|
timeRange?: TimeRange;
|
||||||
|
limit?: number;
|
||||||
|
scopedVars?: ScopedVars;
|
||||||
|
},
|
||||||
|
requestOptions?: Partial<BackendSrvRequest>
|
||||||
|
): Promise<DetectedFieldsResult | Error> {
|
||||||
|
const interpolatedExpr =
|
||||||
|
queryOptions.expr && queryOptions.expr !== EMPTY_SELECTOR
|
||||||
|
? this.datasource.interpolateString(queryOptions.expr, queryOptions.scopedVars)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!interpolatedExpr) {
|
||||||
|
throw new Error('fetchDetectedFields requires query expression');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `detected_fields`;
|
||||||
|
const range = queryOptions?.timeRange ?? this.getDefaultTimeRange();
|
||||||
|
const rangeParams = this.datasource.getTimeRangeParams(range);
|
||||||
|
const { start, end } = rangeParams;
|
||||||
|
const params: KeyValue<string | number> = { start, end, limit: queryOptions?.limit ?? 1000 };
|
||||||
|
params.query = interpolatedExpr;
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const data = await this.request(url, params, true, requestOptions);
|
||||||
|
resolve(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchDetectedFieldValues(
|
||||||
|
labelName: string,
|
||||||
|
queryOptions?: {
|
||||||
|
expr?: string;
|
||||||
|
timeRange?: TimeRange;
|
||||||
|
limit?: number;
|
||||||
|
scopedVars?: ScopedVars;
|
||||||
|
throwError?: boolean;
|
||||||
|
},
|
||||||
|
requestOptions?: Partial<BackendSrvRequest>
|
||||||
|
): Promise<string[] | Error> {
|
||||||
|
// This function was named poorly, it's not detected label values, it's detected field values! :facepalm
|
||||||
|
return this.fetchDetectedLabelValues(labelName, queryOptions, requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated: use fetchDetectedFieldValues instead
|
||||||
|
*/
|
||||||
async fetchDetectedLabelValues(
|
async fetchDetectedLabelValues(
|
||||||
labelName: string,
|
labelName: string,
|
||||||
queryOptions?: {
|
queryOptions?: {
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
export function createDetectedFieldValuesMetadataRequest(labelsAndValues: string[]) {
|
||||||
|
const lokiLabelsAndValuesEndpointRegex = /^detected_field\/([%\w]*)\/values/;
|
||||||
|
|
||||||
|
return async function metadataRequestMock(url: string) {
|
||||||
|
const labelsMatch = url.match(lokiLabelsAndValuesEndpointRegex);
|
||||||
|
if (labelsMatch) {
|
||||||
|
return labelsAndValues ?? [];
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unexpected url error, ${url}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
export function createDetectedFieldsMetadataRequest(labelsAndValues: string[]) {
|
import { DetectedFieldsResult } from '../types';
|
||||||
const lokiLabelsAndValuesEndpointRegex = /^detected_field\/([%\w]*)\/values/;
|
|
||||||
|
export function createDetectedFieldsMetadataRequest(response: DetectedFieldsResult) {
|
||||||
|
const lokiLabelsAndValuesEndpointRegex = /^detected_fields/;
|
||||||
|
|
||||||
return async function metadataRequestMock(url: string) {
|
return async function metadataRequestMock(url: string) {
|
||||||
const labelsMatch = url.match(lokiLabelsAndValuesEndpointRegex);
|
const labelsMatch = url.match(lokiLabelsAndValuesEndpointRegex);
|
||||||
if (labelsMatch) {
|
if (labelsMatch) {
|
||||||
return labelsAndValues ?? [];
|
return response ?? {};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unexpected url error, ${url}`);
|
throw new Error(`Unexpected url error, ${url}`);
|
||||||
}
|
}
|
||||||
|
@ -546,6 +546,11 @@ export class LokiDatasource
|
|||||||
if (!res.data && res.values) {
|
if (!res.data && res.values) {
|
||||||
return res.values ?? [];
|
return res.values ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detected_fields has a different return structure then other metadata responses
|
||||||
|
if (!res.data && res.fields) {
|
||||||
|
return res.fields ?? [];
|
||||||
|
}
|
||||||
return res.data ?? [];
|
return res.data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,4 +105,14 @@ export interface ParserAndLabelKeysResult {
|
|||||||
unwrapLabelKeys: string[];
|
unwrapLabelKeys: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DetectedFieldsResult {
|
||||||
|
fields: Array<{
|
||||||
|
label: string;
|
||||||
|
type: 'bytes' | 'float' | 'int' | 'string' | 'duration';
|
||||||
|
cardinality: number;
|
||||||
|
parsers: Array<'logfmt' | 'json'> | null;
|
||||||
|
}>;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type LokiGroupedRequest = { request: DataQueryRequest<LokiQuery>; partition: TimeRange[] };
|
export type LokiGroupedRequest = { request: DataQueryRequest<LokiQuery>; partition: TimeRange[] };
|
||||||
|
Loading…
Reference in New Issue
Block a user