Explore: Adds support for new loki 'start' and 'end' params for labels endpoint (#17512)

* Explore: Adds support for new loki 'start' and 'end' params for labels endpoint
Also initializes absoluteRange when explore is initialized
Closes #16788

* Explore: Dispatches updateTimeRangeAction instead of passing absoluteRange when initializing
Also removes dependency on sinon from loki language provider test

* Loki: Refactors transformation of absolute time range to URL params into small utility function

* Makes use of rangeToParams() util function in loki language provider test
Also updates LanguageProvider.request() interface so that url should be type string, and adds optional params argument
This commit is contained in:
kay delaney 2019-07-08 16:14:48 +01:00 committed by GitHub
parent 8f5df80161
commit 246358344c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 163 additions and 55 deletions

View File

@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
// Types // Types
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { TimeRange } from '@grafana/data'; import { TimeRange, AbsoluteTimeRange } from '@grafana/data';
import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui'; import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui';
import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore'; import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
@ -38,6 +38,7 @@ interface QueryRowProps extends PropsFromParent {
query: DataQuery; query: DataQuery;
modifyQueries: typeof modifyQueries; modifyQueries: typeof modifyQueries;
range: TimeRange; range: TimeRange;
absoluteRange: AbsoluteTimeRange;
removeQueryRowAction: typeof removeQueryRowAction; removeQueryRowAction: typeof removeQueryRowAction;
runQueries: typeof runQueries; runQueries: typeof runQueries;
queryResponse: PanelData; queryResponse: PanelData;
@ -116,6 +117,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
query, query,
exploreEvents, exploreEvents,
range, range,
absoluteRange,
datasourceStatus, datasourceStatus,
queryResponse, queryResponse,
latency, latency,
@ -148,6 +150,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
onChange={this.onChange} onChange={this.onChange}
panelData={null} panelData={null}
queryResponse={queryResponse} queryResponse={queryResponse}
absoluteRange={absoluteRange}
/> />
) : ( ) : (
<QueryEditor <QueryEditor
@ -202,6 +205,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
history, history,
queries, queries,
range, range,
absoluteRange,
datasourceError, datasourceError,
graphResult, graphResult,
loadingState, loadingState,
@ -224,6 +228,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
history, history,
query, query,
range, range,
absoluteRange,
datasourceStatus, datasourceStatus,
queryResponse, queryResponse,
latency, latency,

View File

@ -250,6 +250,7 @@ export function initializeExplore(
ui, ui,
}) })
); );
dispatch(updateTimeRangeAction({ exploreId }));
}; };
} }

View File

@ -9,7 +9,8 @@ const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
}) => { }) => {
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax( const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
datasource.languageProvider, datasource.languageProvider,
datasourceStatus datasourceStatus,
otherProps.absoluteRange
); );
return ( return (

View File

@ -17,6 +17,7 @@ import BracesPlugin from 'app/features/explore/slate-plugins/braces';
import { LokiQuery } from '../types'; import { LokiQuery } from '../types';
import { TypeaheadOutput, HistoryItem } from 'app/types/explore'; import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui'; import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
import { AbsoluteTimeRange } from '@grafana/data';
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) { function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
if (datasourceStatus === DataSourceStatus.Disconnected) { if (datasourceStatus === DataSourceStatus.Disconnected) {
@ -70,6 +71,7 @@ export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<DataSour
syntax: any; syntax: any;
logLabelOptions: any[]; logLabelOptions: any[];
syntaxLoaded: any; syntaxLoaded: any;
absoluteRange: AbsoluteTimeRange;
onLoadOptions: (selectedOptions: CascaderOption[]) => void; onLoadOptions: (selectedOptions: CascaderOption[]) => void;
onLabelsRefresh?: () => void; onLabelsRefresh?: () => void;
} }
@ -123,7 +125,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
return { suggestions: [] }; return { suggestions: [] };
} }
const { history } = this.props; const { history, absoluteRange } = this.props;
const { prefix, text, value, wrapperNode } = typeahead; const { prefix, text, value, wrapperNode } = typeahead;
// Get DOM-dependent context // Get DOM-dependent context
@ -134,7 +136,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
const result = datasource.languageProvider.provideCompletionItems( const result = datasource.languageProvider.provideCompletionItems(
{ text, value, prefix, wrapperClasses, labelKey }, { text, value, prefix, wrapperClasses, labelKey },
{ history } { history, absoluteRange }
); );
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context); console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);

View File

@ -2,6 +2,7 @@ import { renderHook, act } from 'react-hooks-testing-library';
import LanguageProvider from 'app/plugins/datasource/loki/language_provider'; import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { useLokiLabels } from './useLokiLabels'; import { useLokiLabels } from './useLokiLabels';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource'; import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
describe('useLokiLabels hook', () => { describe('useLokiLabels hook', () => {
it('should refresh labels', async () => { it('should refresh labels', async () => {
@ -10,6 +11,10 @@ describe('useLokiLabels hook', () => {
}; };
const languageProvider = new LanguageProvider(datasource); const languageProvider = new LanguageProvider(datasource);
const logLabelOptionsMock = ['Holy mock!']; const logLabelOptionsMock = ['Holy mock!'];
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560153109000,
};
languageProvider.refreshLogLabels = () => { languageProvider.refreshLogLabels = () => {
languageProvider.logLabelOptions = logLabelOptionsMock; languageProvider.logLabelOptions = logLabelOptionsMock;
@ -17,7 +22,7 @@ describe('useLokiLabels hook', () => {
}; };
const { result, waitForNextUpdate } = renderHook(() => const { result, waitForNextUpdate } = renderHook(() =>
useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Connected) useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Connected)
); );
act(() => result.current.refreshLabels()); act(() => result.current.refreshLabels());
expect(result.current.logLabelOptions).toEqual([]); expect(result.current.logLabelOptions).toEqual([]);
@ -29,26 +34,38 @@ describe('useLokiLabels hook', () => {
const datasource = { const datasource = {
metadataRequest: () => ({ data: { data: [] as any[] } }), metadataRequest: () => ({ data: { data: [] as any[] } }),
}; };
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560153109000,
};
const languageProvider = new LanguageProvider(datasource); const languageProvider = new LanguageProvider(datasource);
languageProvider.refreshLogLabels = jest.fn(); languageProvider.refreshLogLabels = jest.fn();
renderHook(() => renderHook(() =>
useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Disconnected) useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Disconnected)
); );
expect(languageProvider.refreshLogLabels).toBeCalledTimes(1); expect(languageProvider.refreshLogLabels).toBeCalledTimes(1);
expect(languageProvider.refreshLogLabels).toBeCalledWith(true); expect(languageProvider.refreshLogLabels).toBeCalledWith(rangeMock, true);
}); });
it('should not force refresh labels after a connect', () => { it('should not force refresh labels after a connect', () => {
const datasource = { const datasource = {
metadataRequest: () => ({ data: { data: [] as any[] } }), metadataRequest: () => ({ data: { data: [] as any[] } }),
}; };
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560153109000,
};
const languageProvider = new LanguageProvider(datasource); const languageProvider = new LanguageProvider(datasource);
languageProvider.refreshLogLabels = jest.fn(); languageProvider.refreshLogLabels = jest.fn();
renderHook(() => renderHook(() =>
useLokiLabels(languageProvider, true, [], DataSourceStatus.Disconnected, DataSourceStatus.Connected) useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Disconnected, DataSourceStatus.Connected)
); );
expect(languageProvider.refreshLogLabels).not.toBeCalled(); expect(languageProvider.refreshLogLabels).not.toBeCalled();

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource'; import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider'; import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm'; import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
@ -17,6 +18,7 @@ export const useLokiLabels = (
languageProvider: LokiLanguageProvider, languageProvider: LokiLanguageProvider,
languageProviderInitialised: boolean, languageProviderInitialised: boolean,
activeOption: CascaderOption[], activeOption: CascaderOption[],
absoluteRange: AbsoluteTimeRange,
datasourceStatus: DataSourceStatus, datasourceStatus: DataSourceStatus,
initialDatasourceStatus?: DataSourceStatus // used for test purposes initialDatasourceStatus?: DataSourceStatus // used for test purposes
) => { ) => {
@ -32,14 +34,14 @@ export const useLokiLabels = (
// Async // Async
const fetchOptionValues = async (option: string) => { const fetchOptionValues = async (option: string) => {
await languageProvider.fetchLabelValues(option); await languageProvider.fetchLabelValues(option, absoluteRange);
if (mounted.current) { if (mounted.current) {
setLogLabelOptions(languageProvider.logLabelOptions); setLogLabelOptions(languageProvider.logLabelOptions);
} }
}; };
const tryLabelsRefresh = async () => { const tryLabelsRefresh = async () => {
await languageProvider.refreshLogLabels(shouldForceRefreshLabels); await languageProvider.refreshLogLabels(absoluteRange, shouldForceRefreshLabels);
if (mounted.current) { if (mounted.current) {
setRefreshLabels(false); setRefreshLabels(false);

View File

@ -1,5 +1,6 @@
import { renderHook, act } from 'react-hooks-testing-library'; import { renderHook, act } from 'react-hooks-testing-library';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource'; import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
import LanguageProvider from 'app/plugins/datasource/loki/language_provider'; import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { useLokiSyntax } from './useLokiSyntax'; import { useLokiSyntax } from './useLokiSyntax';
@ -14,6 +15,11 @@ describe('useLokiSyntax hook', () => {
const logLabelOptionsMock2 = ['Mock the hell?!']; const logLabelOptionsMock2 = ['Mock the hell?!'];
const logLabelOptionsMock3 = ['Oh my mock!']; const logLabelOptionsMock3 = ['Oh my mock!'];
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560163909000,
};
languageProvider.refreshLogLabels = () => { languageProvider.refreshLogLabels = () => {
languageProvider.logLabelOptions = logLabelOptionsMock; languageProvider.logLabelOptions = logLabelOptionsMock;
return Promise.resolve(); return Promise.resolve();
@ -30,7 +36,9 @@ describe('useLokiSyntax hook', () => {
}; };
it('should provide Loki syntax when used', async () => { it('should provide Loki syntax when used', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected)); const { result, waitForNextUpdate } = renderHook(() =>
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
);
expect(result.current.syntax).toEqual(null); expect(result.current.syntax).toEqual(null);
await waitForNextUpdate(); await waitForNextUpdate();
@ -39,7 +47,9 @@ describe('useLokiSyntax hook', () => {
}); });
it('should fetch labels on first call', async () => { it('should fetch labels on first call', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected)); const { result, waitForNextUpdate } = renderHook(() =>
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
);
expect(result.current.isSyntaxReady).toBeFalsy(); expect(result.current.isSyntaxReady).toBeFalsy();
expect(result.current.logLabelOptions).toEqual([]); expect(result.current.logLabelOptions).toEqual([]);
@ -50,7 +60,9 @@ describe('useLokiSyntax hook', () => {
}); });
it('should try to fetch missing options when active option changes', async () => { it('should try to fetch missing options when active option changes', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected)); const { result, waitForNextUpdate } = renderHook(() =>
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
);
await waitForNextUpdate(); await waitForNextUpdate();
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2); expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
// @ts-ignore // @ts-ignore
import Prism from 'prismjs'; import Prism from 'prismjs';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource'; import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider'; import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels'; import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm'; import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
@ -15,7 +15,11 @@ const PRISM_SYNTAX = 'promql';
* @param languageProvider * @param languageProvider
* @description Initializes given language provider, exposes Loki syntax and enables loading label option values * @description Initializes given language provider, exposes Loki syntax and enables loading label option values
*/ */
export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasourceStatus: DataSourceStatus) => { export const useLokiSyntax = (
languageProvider: LokiLanguageProvider,
datasourceStatus: DataSourceStatus,
absoluteRange: AbsoluteTimeRange
) => {
const mounted = useRefMounted(); const mounted = useRefMounted();
// State // State
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false); const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
@ -32,11 +36,13 @@ export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasource
languageProvider, languageProvider,
languageProviderInitialized, languageProviderInitialized,
activeOption, activeOption,
absoluteRange,
datasourceStatus datasourceStatus
); );
// Async // Async
const initializeLanguageProvider = async () => { const initializeLanguageProvider = async () => {
languageProvider.initialRange = absoluteRange;
await languageProvider.start(); await languageProvider.start();
Prism.languages[PRISM_SYNTAX] = languageProvider.getSyntax(); Prism.languages[PRISM_SYNTAX] = languageProvider.getSyntax();
if (mounted.current) { if (mounted.current) {

View File

@ -78,6 +78,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
...options, ...options,
url, url,
}; };
return this.backendSrv.datasourceRequest(req); return this.backendSrv.datasourceRequest(req);
} }
@ -254,10 +255,10 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return this.languageProvider.importQueries(queries, originMeta.id); return this.languageProvider.importQueries(queries, originMeta.id);
} }
metadataRequest(url: string) { metadataRequest(url: string, params?: any) {
// HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField // HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField
const apiUrl = url.replace('v1', 'prom'); const apiUrl = url.replace('v1', 'prom');
return this._request(apiUrl, { silent: true }).then((res: DataQueryResponse) => { return this._request(apiUrl, params, { silent: true }).then((res: DataQueryResponse) => {
const data: any = { data: { data: res.data.values || [] } }; const data: any = { data: { data: res.data.values || [] } };
return data; return data;
}); });

View File

@ -1,7 +1,8 @@
// @ts-ignore // @ts-ignore
import Plain from 'slate-plain-serializer'; import Plain from 'slate-plain-serializer';
import LanguageProvider, { LABEL_REFRESH_INTERVAL } from './language_provider'; import LanguageProvider, { LABEL_REFRESH_INTERVAL, rangeToParams } from './language_provider';
import { AbsoluteTimeRange } from '@grafana/data';
import { advanceTo, clear, advanceBy } from 'jest-date-mock'; import { advanceTo, clear, advanceBy } from 'jest-date-mock';
import { beforeEach } from 'test/lib/common'; import { beforeEach } from 'test/lib/common';
import { DataQueryResponseData } from '@grafana/ui'; import { DataQueryResponseData } from '@grafana/ui';
@ -11,8 +12,13 @@ describe('Language completion provider', () => {
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }), metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
}; };
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560163909000,
};
describe('empty query suggestions', () => { describe('empty query suggestions', () => {
it('returns no suggestions on emtpty context', () => { it('returns no suggestions on empty context', () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
const value = Plain.deserialize(''); const value = Plain.deserialize('');
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
@ -21,7 +27,7 @@ describe('Language completion provider', () => {
expect(result.suggestions.length).toEqual(0); expect(result.suggestions.length).toEqual(0);
}); });
it('returns default suggestions with history on emtpty context when history was provided', () => { it('returns default suggestions with history on empty context when history was provided', () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
const value = Plain.deserialize(''); const value = Plain.deserialize('');
const history = [ const history = [
@ -29,7 +35,10 @@ describe('Language completion provider', () => {
query: { refId: '1', expr: '{app="foo"}' }, query: { refId: '1', expr: '{app="foo"}' },
}, },
]; ];
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history }); const result = instance.provideCompletionItems(
{ text: '', prefix: '', value, wrapperClasses: [] },
{ history, absoluteRange: rangeMock }
);
expect(result.context).toBeUndefined(); expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined(); expect(result.refresher).toBeUndefined();
expect(result.suggestions).toMatchObject([ expect(result.suggestions).toMatchObject([
@ -79,64 +88,102 @@ describe('Language completion provider', () => {
anchorOffset: 1, anchorOffset: 1,
}); });
const valueWithSelection = value.change().select(range).value; const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({ const result = instance.provideCompletionItems(
text: '', {
prefix: '', text: '',
wrapperClasses: ['context-labels'], prefix: '',
value: valueWithSelection, wrapperClasses: ['context-labels'],
}); value: valueWithSelection,
},
{ absoluteRange: rangeMock }
);
expect(result.context).toBe('context-labels'); expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]); expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]);
}); });
}); });
}); });
describe('Request URL', () => {
it('should contain range params', async () => {
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560163909000,
};
const datasourceWithLabels = {
metadataRequest: url => {
if (url.slice(0, 15) === '/api/prom/label') {
return { data: { data: ['other'] } };
} else {
return { data: { data: [] } };
}
},
};
const datasourceSpy = jest.spyOn(datasourceWithLabels, 'metadataRequest');
const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
await instance.refreshLogLabels(rangeMock, true);
const expectedUrl = '/api/prom/label';
expect(datasourceSpy).toHaveBeenCalledWith(expectedUrl, rangeToParams(rangeMock));
});
});
describe('Query imports', () => { describe('Query imports', () => {
const datasource = { const datasource = {
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }), metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
}; };
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560163909000,
};
it('returns empty queries for unknown origin datasource', async () => { it('returns empty queries for unknown origin datasource', async () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource, { initialRange: rangeMock });
const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown'); const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown');
expect(result).toEqual([{ refId: 'bar', expr: '' }]); expect(result).toEqual([{ refId: 'bar', expr: '' }]);
}); });
describe('prometheus query imports', () => { describe('prometheus query imports', () => {
it('returns empty query from metric-only query', async () => { it('returns empty query from metric-only query', async () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource, { initialRange: rangeMock });
const result = await instance.importPrometheusQuery('foo'); const result = await instance.importPrometheusQuery('foo');
expect(result).toEqual(''); expect(result).toEqual('');
}); });
it('returns empty query from selector query if label is not available', async () => { it('returns empty query from selector query if label is not available', async () => {
const datasourceWithLabels = { const datasourceWithLabels = {
metadataRequest: (url: string) => metadataRequest: url =>
url === '/api/prom/label' ? { data: { data: ['other'] } } : { data: { data: [] as DataQueryResponseData[] } }, url.slice(0, 15) === '/api/prom/label'
? { data: { data: ['other'] } }
: { data: { data: [] as DataQueryResponseData[] } },
}; };
const instance = new LanguageProvider(datasourceWithLabels); const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
const result = await instance.importPrometheusQuery('{foo="bar"}'); const result = await instance.importPrometheusQuery('{foo="bar"}');
expect(result).toEqual('{}'); expect(result).toEqual('{}');
}); });
it('returns selector query from selector query with common labels', async () => { it('returns selector query from selector query with common labels', async () => {
const datasourceWithLabels = { const datasourceWithLabels = {
metadataRequest: (url: string) => metadataRequest: url =>
url === '/api/prom/label' ? { data: { data: ['foo'] } } : { data: { data: [] as DataQueryResponseData[] } }, url.slice(0, 15) === '/api/prom/label'
? { data: { data: ['foo'] } }
: { data: { data: [] as DataQueryResponseData[] } },
}; };
const instance = new LanguageProvider(datasourceWithLabels); const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}'); const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
expect(result).toEqual('{foo="bar"}'); expect(result).toEqual('{foo="bar"}');
}); });
it('returns selector query from selector query with all labels if logging label list is empty', async () => { it('returns selector query from selector query with all labels if logging label list is empty', async () => {
const datasourceWithLabels = { const datasourceWithLabels = {
metadataRequest: (url: string) => metadataRequest: url =>
url === '/api/prom/label' url.slice(0, 15) === '/api/prom/label'
? { data: { data: [] as DataQueryResponseData[] } } ? { data: { data: [] as DataQueryResponseData[] } }
: { data: { data: [] as DataQueryResponseData[] } }, : { data: { data: [] as DataQueryResponseData[] } },
}; };
const instance = new LanguageProvider(datasourceWithLabels); const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}'); const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
expect(result).toEqual('{baz="42",foo="bar"}'); expect(result).toEqual('{baz="42",foo="bar"}');
}); });
@ -149,6 +196,11 @@ describe('Labels refresh', () => {
}; };
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560163909000,
};
beforeEach(() => { beforeEach(() => {
instance.fetchLogLabels = jest.fn(); instance.fetchLogLabels = jest.fn();
}); });
@ -157,18 +209,20 @@ describe('Labels refresh', () => {
jest.clearAllMocks(); jest.clearAllMocks();
clear(); clear();
}); });
it("should not refresh labels if refresh interval hasn't passed", () => { it("should not refresh labels if refresh interval hasn't passed", () => {
advanceTo(new Date(2019, 1, 1, 0, 0, 0)); advanceTo(new Date(2019, 1, 1, 0, 0, 0));
instance.logLabelFetchTs = Date.now(); instance.logLabelFetchTs = Date.now();
advanceBy(LABEL_REFRESH_INTERVAL / 2); advanceBy(LABEL_REFRESH_INTERVAL / 2);
instance.refreshLogLabels(); instance.refreshLogLabels(rangeMock);
expect(instance.fetchLogLabels).not.toBeCalled(); expect(instance.fetchLogLabels).not.toBeCalled();
}); });
it('should refresh labels if refresh interval passed', () => { it('should refresh labels if refresh interval passed', () => {
advanceTo(new Date(2019, 1, 1, 0, 0, 0)); advanceTo(new Date(2019, 1, 1, 0, 0, 0));
instance.logLabelFetchTs = Date.now(); instance.logLabelFetchTs = Date.now();
advanceBy(LABEL_REFRESH_INTERVAL + 1); advanceBy(LABEL_REFRESH_INTERVAL + 1);
instance.refreshLogLabels(); instance.refreshLogLabels(rangeMock);
expect(instance.fetchLogLabels).toBeCalled(); expect(instance.fetchLogLabels).toBeCalled();
}); });
}); });

View File

@ -15,16 +15,18 @@ import {
HistoryItem, HistoryItem,
} from 'app/types/explore'; } from 'app/types/explore';
import { LokiQuery } from './types'; import { LokiQuery } from './types';
import { dateTime } from '@grafana/data'; import { dateTime, AbsoluteTimeRange } from '@grafana/data';
import { PromQuery } from '../prometheus/types'; import { PromQuery } from '../prometheus/types';
const DEFAULT_KEYS = ['job', 'namespace']; const DEFAULT_KEYS = ['job', 'namespace'];
const EMPTY_SELECTOR = '{}'; const EMPTY_SELECTOR = '{}';
const HISTORY_ITEM_COUNT = 10; const HISTORY_ITEM_COUNT = 10;
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
const NS_IN_MS = 1_000_000;
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
const wrapLabel = (label: string) => ({ label }); const wrapLabel = (label: string) => ({ label });
export const rangeToParams = (range: AbsoluteTimeRange) => ({ start: range.from * NS_IN_MS, end: range.to * NS_IN_MS });
type LokiHistoryItem = HistoryItem<LokiQuery>; type LokiHistoryItem = HistoryItem<LokiQuery>;
@ -50,6 +52,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
logLabelOptions: any[]; logLabelOptions: any[];
logLabelFetchTs?: number; logLabelFetchTs?: number;
started: boolean; started: boolean;
initialRange: AbsoluteTimeRange;
constructor(datasource: any, initialValues?: any) { constructor(datasource: any, initialValues?: any) {
super(); super();
@ -67,13 +70,13 @@ export default class LokiLanguageProvider extends LanguageProvider {
return syntax; return syntax;
} }
request = (url: string) => { request = (url: string, params?: any) => {
return this.datasource.metadataRequest(url); return this.datasource.metadataRequest(url, params);
}; };
start = () => { start = () => {
if (!this.startTask) { if (!this.startTask) {
this.startTask = this.fetchLogLabels(); this.startTask = this.fetchLogLabels(this.initialRange);
} }
return this.startTask; return this.startTask;
}; };
@ -120,7 +123,10 @@ export default class LokiLanguageProvider extends LanguageProvider {
return { suggestions }; return { suggestions };
} }
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput { getLabelCompletionItems(
{ text, wrapperClasses, labelKey, value }: TypeaheadInput,
{ absoluteRange }: any
): TypeaheadOutput {
let context: string; let context: string;
let refresher: Promise<any> = null; let refresher: Promise<any> = null;
const suggestions: CompletionItemGroup[] = []; const suggestions: CompletionItemGroup[] = [];
@ -146,7 +152,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
items: labelValues.map(wrapLabel), items: labelValues.map(wrapLabel),
}); });
} else { } else {
refresher = this.fetchLabelValues(labelKey); refresher = this.fetchLabelValues(labelKey, absoluteRange);
} }
} }
} else { } else {
@ -206,7 +212,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
if (existingKeys && existingKeys.length > 0) { if (existingKeys && existingKeys.length > 0) {
// Check for common labels // Check for common labels
for (const key in labels) { for (const key in labels) {
if (existingKeys && existingKeys.indexOf(key) > -1) { if (existingKeys && existingKeys.includes(key)) {
// Should we check for label value equality here? // Should we check for label value equality here?
labelsToKeep[key] = labels[key]; labelsToKeep[key] = labels[key];
} }
@ -227,11 +233,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
return ''; return '';
} }
async fetchLogLabels(): Promise<any> { async fetchLogLabels(absoluteRange: AbsoluteTimeRange): Promise<any> {
const url = '/api/prom/label'; const url = '/api/prom/label';
try { try {
this.logLabelFetchTs = Date.now(); this.logLabelFetchTs = Date.now();
const res = await this.request(url);
const res = await this.request(url, rangeToParams(absoluteRange));
const body = await (res.data || res.json()); const body = await (res.data || res.json());
const labelKeys = body.data.slice().sort(); const labelKeys = body.data.slice().sort();
this.labelKeys = { this.labelKeys = {
@ -244,7 +251,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
return Promise.all( return Promise.all(
labelKeys labelKeys
.filter((key: string) => DEFAULT_KEYS.indexOf(key) > -1) .filter((key: string) => DEFAULT_KEYS.indexOf(key) > -1)
.map((key: string) => this.fetchLabelValues(key)) .map((key: string) => this.fetchLabelValues(key, absoluteRange))
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -252,16 +259,16 @@ export default class LokiLanguageProvider extends LanguageProvider {
return []; return [];
} }
async refreshLogLabels(forceRefresh?: boolean) { async refreshLogLabels(absoluteRange: AbsoluteTimeRange, forceRefresh?: boolean) {
if ((this.labelKeys && Date.now() - this.logLabelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) { if ((this.labelKeys && Date.now() - this.logLabelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) {
await this.fetchLogLabels(); await this.fetchLogLabels(absoluteRange);
} }
} }
async fetchLabelValues(key: string) { async fetchLabelValues(key: string, absoluteRange: AbsoluteTimeRange) {
const url = `/api/prom/label/${key}/values`; const url = `/api/prom/label/${key}/values`;
try { try {
const res = await this.request(url); const res = await this.request(url, rangeToParams(absoluteRange));
const body = await (res.data || res.json()); const body = await (res.data || res.json());
const values = body.data.slice().sort(); const values = body.data.slice().sort();

View File

@ -286,7 +286,7 @@ export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
export abstract class LanguageProvider { export abstract class LanguageProvider {
datasource: any; datasource: any;
request: (url: any) => Promise<any>; request: (url: string, params?: any) => Promise<any>;
/** /**
* Returns startTask that resolves with a task list when main syntax is loaded. * Returns startTask that resolves with a task list when main syntax is loaded.
* Task list consists of secondary promises that load more detailed language features. * Task list consists of secondary promises that load more detailed language features.