mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
8f5df80161
commit
246358344c
@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
|
||||
|
||||
// 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 { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
@ -38,6 +38,7 @@ interface QueryRowProps extends PropsFromParent {
|
||||
query: DataQuery;
|
||||
modifyQueries: typeof modifyQueries;
|
||||
range: TimeRange;
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
removeQueryRowAction: typeof removeQueryRowAction;
|
||||
runQueries: typeof runQueries;
|
||||
queryResponse: PanelData;
|
||||
@ -116,6 +117,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
query,
|
||||
exploreEvents,
|
||||
range,
|
||||
absoluteRange,
|
||||
datasourceStatus,
|
||||
queryResponse,
|
||||
latency,
|
||||
@ -148,6 +150,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
onChange={this.onChange}
|
||||
panelData={null}
|
||||
queryResponse={queryResponse}
|
||||
absoluteRange={absoluteRange}
|
||||
/>
|
||||
) : (
|
||||
<QueryEditor
|
||||
@ -202,6 +205,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
|
||||
history,
|
||||
queries,
|
||||
range,
|
||||
absoluteRange,
|
||||
datasourceError,
|
||||
graphResult,
|
||||
loadingState,
|
||||
@ -224,6 +228,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
|
||||
history,
|
||||
query,
|
||||
range,
|
||||
absoluteRange,
|
||||
datasourceStatus,
|
||||
queryResponse,
|
||||
latency,
|
||||
|
@ -250,6 +250,7 @@ export function initializeExplore(
|
||||
ui,
|
||||
})
|
||||
);
|
||||
dispatch(updateTimeRangeAction({ exploreId }));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,8 @@ const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
|
||||
}) => {
|
||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||
datasource.languageProvider,
|
||||
datasourceStatus
|
||||
datasourceStatus,
|
||||
otherProps.absoluteRange
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -17,6 +17,7 @@ import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||
import { LokiQuery } from '../types';
|
||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||
import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
|
||||
import { AbsoluteTimeRange } from '@grafana/data';
|
||||
|
||||
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
|
||||
if (datasourceStatus === DataSourceStatus.Disconnected) {
|
||||
@ -70,6 +71,7 @@ export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<DataSour
|
||||
syntax: any;
|
||||
logLabelOptions: any[];
|
||||
syntaxLoaded: any;
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
onLoadOptions: (selectedOptions: CascaderOption[]) => void;
|
||||
onLabelsRefresh?: () => void;
|
||||
}
|
||||
@ -123,7 +125,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
||||
return { suggestions: [] };
|
||||
}
|
||||
|
||||
const { history } = this.props;
|
||||
const { history, absoluteRange } = this.props;
|
||||
const { prefix, text, value, wrapperNode } = typeahead;
|
||||
|
||||
// Get DOM-dependent context
|
||||
@ -134,7 +136,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
||||
|
||||
const result = datasource.languageProvider.provideCompletionItems(
|
||||
{ text, value, prefix, wrapperClasses, labelKey },
|
||||
{ history }
|
||||
{ history, absoluteRange }
|
||||
);
|
||||
|
||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
|
||||
|
@ -2,6 +2,7 @@ import { renderHook, act } from 'react-hooks-testing-library';
|
||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||
import { useLokiLabels } from './useLokiLabels';
|
||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||
import { AbsoluteTimeRange } from '@grafana/data';
|
||||
|
||||
describe('useLokiLabels hook', () => {
|
||||
it('should refresh labels', async () => {
|
||||
@ -10,6 +11,10 @@ describe('useLokiLabels hook', () => {
|
||||
};
|
||||
const languageProvider = new LanguageProvider(datasource);
|
||||
const logLabelOptionsMock = ['Holy mock!'];
|
||||
const rangeMock: AbsoluteTimeRange = {
|
||||
from: 1560153109000,
|
||||
to: 1560153109000,
|
||||
};
|
||||
|
||||
languageProvider.refreshLogLabels = () => {
|
||||
languageProvider.logLabelOptions = logLabelOptionsMock;
|
||||
@ -17,7 +22,7 @@ describe('useLokiLabels hook', () => {
|
||||
};
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Connected)
|
||||
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Connected)
|
||||
);
|
||||
act(() => result.current.refreshLabels());
|
||||
expect(result.current.logLabelOptions).toEqual([]);
|
||||
@ -29,26 +34,38 @@ describe('useLokiLabels hook', () => {
|
||||
const datasource = {
|
||||
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||
};
|
||||
|
||||
const rangeMock: AbsoluteTimeRange = {
|
||||
from: 1560153109000,
|
||||
to: 1560153109000,
|
||||
};
|
||||
|
||||
const languageProvider = new LanguageProvider(datasource);
|
||||
languageProvider.refreshLogLabels = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Disconnected)
|
||||
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Disconnected)
|
||||
);
|
||||
|
||||
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', () => {
|
||||
const datasource = {
|
||||
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||
};
|
||||
|
||||
const rangeMock: AbsoluteTimeRange = {
|
||||
from: 1560153109000,
|
||||
to: 1560153109000,
|
||||
};
|
||||
|
||||
const languageProvider = new LanguageProvider(datasource);
|
||||
languageProvider.refreshLogLabels = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useLokiLabels(languageProvider, true, [], DataSourceStatus.Disconnected, DataSourceStatus.Connected)
|
||||
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Disconnected, DataSourceStatus.Connected)
|
||||
);
|
||||
|
||||
expect(languageProvider.refreshLogLabels).not.toBeCalled();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||
import { AbsoluteTimeRange } from '@grafana/data';
|
||||
|
||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
||||
@ -17,6 +18,7 @@ export const useLokiLabels = (
|
||||
languageProvider: LokiLanguageProvider,
|
||||
languageProviderInitialised: boolean,
|
||||
activeOption: CascaderOption[],
|
||||
absoluteRange: AbsoluteTimeRange,
|
||||
datasourceStatus: DataSourceStatus,
|
||||
initialDatasourceStatus?: DataSourceStatus // used for test purposes
|
||||
) => {
|
||||
@ -32,14 +34,14 @@ export const useLokiLabels = (
|
||||
|
||||
// Async
|
||||
const fetchOptionValues = async (option: string) => {
|
||||
await languageProvider.fetchLabelValues(option);
|
||||
await languageProvider.fetchLabelValues(option, absoluteRange);
|
||||
if (mounted.current) {
|
||||
setLogLabelOptions(languageProvider.logLabelOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const tryLabelsRefresh = async () => {
|
||||
await languageProvider.refreshLogLabels(shouldForceRefreshLabels);
|
||||
await languageProvider.refreshLogLabels(absoluteRange, shouldForceRefreshLabels);
|
||||
|
||||
if (mounted.current) {
|
||||
setRefreshLabels(false);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { renderHook, act } from 'react-hooks-testing-library';
|
||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||
import { AbsoluteTimeRange } from '@grafana/data';
|
||||
|
||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||
import { useLokiSyntax } from './useLokiSyntax';
|
||||
@ -14,6 +15,11 @@ describe('useLokiSyntax hook', () => {
|
||||
const logLabelOptionsMock2 = ['Mock the hell?!'];
|
||||
const logLabelOptionsMock3 = ['Oh my mock!'];
|
||||
|
||||
const rangeMock: AbsoluteTimeRange = {
|
||||
from: 1560153109000,
|
||||
to: 1560163909000,
|
||||
};
|
||||
|
||||
languageProvider.refreshLogLabels = () => {
|
||||
languageProvider.logLabelOptions = logLabelOptionsMock;
|
||||
return Promise.resolve();
|
||||
@ -30,7 +36,9 @@ describe('useLokiSyntax hook', () => {
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
await waitForNextUpdate();
|
||||
@ -39,7 +47,9 @@ describe('useLokiSyntax hook', () => {
|
||||
});
|
||||
|
||||
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.logLabelOptions).toEqual([]);
|
||||
|
||||
@ -50,7 +60,9 @@ describe('useLokiSyntax hook', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
// @ts-ignore
|
||||
import Prism from 'prismjs';
|
||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||
|
||||
import { AbsoluteTimeRange } from '@grafana/data';
|
||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
|
||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
||||
@ -15,7 +15,11 @@ const PRISM_SYNTAX = 'promql';
|
||||
* @param languageProvider
|
||||
* @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();
|
||||
// State
|
||||
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
|
||||
@ -32,11 +36,13 @@ export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasource
|
||||
languageProvider,
|
||||
languageProviderInitialized,
|
||||
activeOption,
|
||||
absoluteRange,
|
||||
datasourceStatus
|
||||
);
|
||||
|
||||
// Async
|
||||
const initializeLanguageProvider = async () => {
|
||||
languageProvider.initialRange = absoluteRange;
|
||||
await languageProvider.start();
|
||||
Prism.languages[PRISM_SYNTAX] = languageProvider.getSyntax();
|
||||
if (mounted.current) {
|
||||
|
@ -78,6 +78,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
...options,
|
||||
url,
|
||||
};
|
||||
|
||||
return this.backendSrv.datasourceRequest(req);
|
||||
}
|
||||
|
||||
@ -254,10 +255,10 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
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
|
||||
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 || [] } };
|
||||
return data;
|
||||
});
|
||||
|
@ -1,7 +1,8 @@
|
||||
// @ts-ignore
|
||||
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 { beforeEach } from 'test/lib/common';
|
||||
import { DataQueryResponseData } from '@grafana/ui';
|
||||
@ -11,8 +12,13 @@ describe('Language completion provider', () => {
|
||||
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
|
||||
};
|
||||
|
||||
const rangeMock: AbsoluteTimeRange = {
|
||||
from: 1560153109000,
|
||||
to: 1560163909000,
|
||||
};
|
||||
|
||||
describe('empty query suggestions', () => {
|
||||
it('returns no suggestions on emtpty context', () => {
|
||||
it('returns no suggestions on empty context', () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('');
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||
@ -21,7 +27,7 @@ describe('Language completion provider', () => {
|
||||
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 value = Plain.deserialize('');
|
||||
const history = [
|
||||
@ -29,7 +35,10 @@ describe('Language completion provider', () => {
|
||||
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.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
@ -79,64 +88,102 @@ describe('Language completion provider', () => {
|
||||
anchorOffset: 1,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
value: valueWithSelection,
|
||||
});
|
||||
const result = instance.provideCompletionItems(
|
||||
{
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
value: valueWithSelection,
|
||||
},
|
||||
{ absoluteRange: rangeMock }
|
||||
);
|
||||
expect(result.context).toBe('context-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', () => {
|
||||
const datasource = {
|
||||
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
|
||||
};
|
||||
|
||||
const rangeMock: AbsoluteTimeRange = {
|
||||
from: 1560153109000,
|
||||
to: 1560163909000,
|
||||
};
|
||||
|
||||
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');
|
||||
expect(result).toEqual([{ refId: 'bar', expr: '' }]);
|
||||
});
|
||||
|
||||
describe('prometheus query imports', () => {
|
||||
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');
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
it('returns empty query from selector query if label is not available', async () => {
|
||||
const datasourceWithLabels = {
|
||||
metadataRequest: (url: string) =>
|
||||
url === '/api/prom/label' ? { data: { data: ['other'] } } : { data: { data: [] as DataQueryResponseData[] } },
|
||||
metadataRequest: url =>
|
||||
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"}');
|
||||
expect(result).toEqual('{}');
|
||||
});
|
||||
|
||||
it('returns selector query from selector query with common labels', async () => {
|
||||
const datasourceWithLabels = {
|
||||
metadataRequest: (url: string) =>
|
||||
url === '/api/prom/label' ? { data: { data: ['foo'] } } : { data: { data: [] as DataQueryResponseData[] } },
|
||||
metadataRequest: url =>
|
||||
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"}');
|
||||
expect(result).toEqual('{foo="bar"}');
|
||||
});
|
||||
|
||||
it('returns selector query from selector query with all labels if logging label list is empty', async () => {
|
||||
const datasourceWithLabels = {
|
||||
metadataRequest: (url: string) =>
|
||||
url === '/api/prom/label'
|
||||
metadataRequest: url =>
|
||||
url.slice(0, 15) === '/api/prom/label'
|
||||
? { 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"}');
|
||||
expect(result).toEqual('{baz="42",foo="bar"}');
|
||||
});
|
||||
@ -149,6 +196,11 @@ describe('Labels refresh', () => {
|
||||
};
|
||||
const instance = new LanguageProvider(datasource);
|
||||
|
||||
const rangeMock: AbsoluteTimeRange = {
|
||||
from: 1560153109000,
|
||||
to: 1560163909000,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
instance.fetchLogLabels = jest.fn();
|
||||
});
|
||||
@ -157,18 +209,20 @@ describe('Labels refresh', () => {
|
||||
jest.clearAllMocks();
|
||||
clear();
|
||||
});
|
||||
|
||||
it("should not refresh labels if refresh interval hasn't passed", () => {
|
||||
advanceTo(new Date(2019, 1, 1, 0, 0, 0));
|
||||
instance.logLabelFetchTs = Date.now();
|
||||
advanceBy(LABEL_REFRESH_INTERVAL / 2);
|
||||
instance.refreshLogLabels();
|
||||
instance.refreshLogLabels(rangeMock);
|
||||
expect(instance.fetchLogLabels).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should refresh labels if refresh interval passed', () => {
|
||||
advanceTo(new Date(2019, 1, 1, 0, 0, 0));
|
||||
instance.logLabelFetchTs = Date.now();
|
||||
advanceBy(LABEL_REFRESH_INTERVAL + 1);
|
||||
instance.refreshLogLabels();
|
||||
instance.refreshLogLabels(rangeMock);
|
||||
expect(instance.fetchLogLabels).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
@ -15,16 +15,18 @@ import {
|
||||
HistoryItem,
|
||||
} from 'app/types/explore';
|
||||
import { LokiQuery } from './types';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTime, AbsoluteTimeRange } from '@grafana/data';
|
||||
import { PromQuery } from '../prometheus/types';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'namespace'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
const HISTORY_ITEM_COUNT = 10;
|
||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
const NS_IN_MS = 1_000_000;
|
||||
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
|
||||
|
||||
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>;
|
||||
|
||||
@ -50,6 +52,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
logLabelOptions: any[];
|
||||
logLabelFetchTs?: number;
|
||||
started: boolean;
|
||||
initialRange: AbsoluteTimeRange;
|
||||
|
||||
constructor(datasource: any, initialValues?: any) {
|
||||
super();
|
||||
@ -67,13 +70,13 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
return syntax;
|
||||
}
|
||||
|
||||
request = (url: string) => {
|
||||
return this.datasource.metadataRequest(url);
|
||||
request = (url: string, params?: any) => {
|
||||
return this.datasource.metadataRequest(url, params);
|
||||
};
|
||||
|
||||
start = () => {
|
||||
if (!this.startTask) {
|
||||
this.startTask = this.fetchLogLabels();
|
||||
this.startTask = this.fetchLogLabels(this.initialRange);
|
||||
}
|
||||
return this.startTask;
|
||||
};
|
||||
@ -120,7 +123,10 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
||||
getLabelCompletionItems(
|
||||
{ text, wrapperClasses, labelKey, value }: TypeaheadInput,
|
||||
{ absoluteRange }: any
|
||||
): TypeaheadOutput {
|
||||
let context: string;
|
||||
let refresher: Promise<any> = null;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
@ -146,7 +152,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
items: labelValues.map(wrapLabel),
|
||||
});
|
||||
} else {
|
||||
refresher = this.fetchLabelValues(labelKey);
|
||||
refresher = this.fetchLabelValues(labelKey, absoluteRange);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -206,7 +212,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
if (existingKeys && existingKeys.length > 0) {
|
||||
// Check for common 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?
|
||||
labelsToKeep[key] = labels[key];
|
||||
}
|
||||
@ -227,11 +233,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
return '';
|
||||
}
|
||||
|
||||
async fetchLogLabels(): Promise<any> {
|
||||
async fetchLogLabels(absoluteRange: AbsoluteTimeRange): Promise<any> {
|
||||
const url = '/api/prom/label';
|
||||
try {
|
||||
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 labelKeys = body.data.slice().sort();
|
||||
this.labelKeys = {
|
||||
@ -244,7 +251,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
return Promise.all(
|
||||
labelKeys
|
||||
.filter((key: string) => DEFAULT_KEYS.indexOf(key) > -1)
|
||||
.map((key: string) => this.fetchLabelValues(key))
|
||||
.map((key: string) => this.fetchLabelValues(key, absoluteRange))
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -252,16 +259,16 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
return [];
|
||||
}
|
||||
|
||||
async refreshLogLabels(forceRefresh?: boolean) {
|
||||
async refreshLogLabels(absoluteRange: AbsoluteTimeRange, forceRefresh?: boolean) {
|
||||
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`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const res = await this.request(url, rangeToParams(absoluteRange));
|
||||
const body = await (res.data || res.json());
|
||||
const values = body.data.slice().sort();
|
||||
|
||||
|
@ -286,7 +286,7 @@ export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||
|
||||
export abstract class LanguageProvider {
|
||||
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.
|
||||
* Task list consists of secondary promises that load more detailed language features.
|
||||
|
Loading…
Reference in New Issue
Block a user