Commit 24635834 by kay delaney Committed by GitHub

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
parent 8f5df801
...@@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act ...@@ -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 { ...@@ -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> { ...@@ -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> { ...@@ -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) ...@@ -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) ...@@ -224,6 +228,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
history, history,
query, query,
range, range,
absoluteRange,
datasourceStatus, datasourceStatus,
queryResponse, queryResponse,
latency, latency,
......
...@@ -250,6 +250,7 @@ export function initializeExplore( ...@@ -250,6 +250,7 @@ export function initializeExplore(
ui, ui,
}) })
); );
dispatch(updateTimeRangeAction({ exploreId }));
}; };
} }
......
...@@ -9,7 +9,8 @@ const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({ ...@@ -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 (
......
...@@ -17,6 +17,7 @@ import BracesPlugin from 'app/features/explore/slate-plugins/braces'; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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);
......
...@@ -2,6 +2,7 @@ import { renderHook, act } from 'react-hooks-testing-library'; ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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();
......
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 = ( ...@@ -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 = ( ...@@ -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);
......
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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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);
......
...@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; ...@@ -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'; ...@@ -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 ...@@ -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) {
......
...@@ -78,6 +78,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { ...@@ -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> { ...@@ -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;
}); });
......
// @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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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();
}); });
}); });
...@@ -15,16 +15,18 @@ import { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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();
......
...@@ -286,7 +286,7 @@ export interface HistoryItem<TQuery extends DataQuery = DataQuery> { ...@@ -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.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment