mirror of
https://github.com/grafana/grafana.git
synced 2025-09-25 10:13:55 +08:00
Loki/Explore: Add query type selector (#28817)
* Create query type switcher * Add and update tests * Add handling in datasource * Refactor * Update tests, when checking higlighting, suppy logs * Remove both option as redundant * Add tooltip, remove old comments * Remove unused importts * Remove console.log, update width * Update public/app/plugins/datasource/loki/components/LokiExploreExtraField.tsx Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update tests * Prettier fixes * Fix test Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
@ -1,32 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { LokiExploreExtraField, LokiExploreExtraFieldProps } from './LokiExploreExtraField';
|
import { LokiExploreExtraFieldProps, LokiExploreExtraField } from './LokiExploreExtraField';
|
||||||
|
|
||||||
const setup = (propOverrides?: LokiExploreExtraFieldProps) => {
|
const setup = (propOverrides?: LokiExploreExtraFieldProps) => {
|
||||||
const label = 'Loki Explore Extra Field';
|
const queryType = 'range';
|
||||||
const value = '123';
|
const lineLimitValue = '1';
|
||||||
const type = 'number';
|
const onLineLimitChange = jest.fn();
|
||||||
const min = 0;
|
const onQueryTypeChange = jest.fn();
|
||||||
const onChangeFunc = jest.fn();
|
|
||||||
const onKeyDownFunc = jest.fn();
|
const onKeyDownFunc = jest.fn();
|
||||||
|
|
||||||
const props: any = {
|
const props: any = {
|
||||||
label,
|
queryType,
|
||||||
value,
|
lineLimitValue,
|
||||||
type,
|
onLineLimitChange,
|
||||||
min,
|
onQueryTypeChange,
|
||||||
onChangeFunc,
|
|
||||||
onKeyDownFunc,
|
onKeyDownFunc,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
return shallow(<LokiExploreExtraField {...props} />);
|
return render(<LokiExploreExtraField {...props} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('LokiExploreExtraField', () => {
|
describe('LokiExploreExtraField', () => {
|
||||||
it('should render component', () => {
|
it('should render step field', () => {
|
||||||
const wrapper = setup();
|
setup();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(screen.getByTestId('lineLimitField')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render query type field', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId('queryTypeField')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,33 +1,68 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { InlineFormLabel } from '@grafana/ui';
|
import { InlineFormLabel, RadioButtonGroup } from '@grafana/ui';
|
||||||
|
|
||||||
export interface LokiExploreExtraFieldProps {
|
export interface LokiExploreExtraFieldProps {
|
||||||
label: string;
|
lineLimitValue: string;
|
||||||
onChangeFunc: (e: React.SyntheticEvent<HTMLInputElement>) => void;
|
queryType: string;
|
||||||
|
onLineLimitChange: (e: React.SyntheticEvent<HTMLInputElement>) => void;
|
||||||
onKeyDownFunc: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
onKeyDownFunc: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
value: string;
|
onQueryTypeChange: (value: string) => void;
|
||||||
type?: string;
|
|
||||||
min?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) {
|
export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) {
|
||||||
const { label, onChangeFunc, onKeyDownFunc, value, type, min } = props;
|
const { onLineLimitChange, onKeyDownFunc, lineLimitValue, queryType, onQueryTypeChange } = props;
|
||||||
|
|
||||||
|
const rangeOptions = [
|
||||||
|
{ value: 'range', label: 'Range' },
|
||||||
|
{ value: 'instant', label: 'Instant' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-inline">
|
<div aria-label="Loki extra field" className="gf-form-inline">
|
||||||
<div className="gf-form">
|
{/*Query type field*/}
|
||||||
<InlineFormLabel width={5}>{label}</InlineFormLabel>
|
<div
|
||||||
|
data-testid="queryTypeField"
|
||||||
|
className={cx(
|
||||||
|
'gf-form explore-input-margin',
|
||||||
|
css`
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
aria-label="Query type field"
|
||||||
|
>
|
||||||
|
<InlineFormLabel
|
||||||
|
tooltip="Choose the type of query you would like to run. An instant query queries against a single point in time. A range query queries over a range of time."
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
Query type
|
||||||
|
</InlineFormLabel>
|
||||||
|
|
||||||
|
<RadioButtonGroup options={rangeOptions} value={queryType} onChange={onQueryTypeChange} />
|
||||||
|
</div>
|
||||||
|
{/*Line limit field*/}
|
||||||
|
<div
|
||||||
|
data-testid="lineLimitField"
|
||||||
|
className={cx(
|
||||||
|
'gf-form',
|
||||||
|
css`
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
aria-label="Line limit field"
|
||||||
|
>
|
||||||
|
<InlineFormLabel width={5}>Line limit</InlineFormLabel>
|
||||||
<input
|
<input
|
||||||
type={type}
|
type="number"
|
||||||
className="gf-form-input width-4"
|
className="gf-form-input width-4"
|
||||||
placeholder={'auto'}
|
placeholder={'auto'}
|
||||||
onChange={onChangeFunc}
|
min={0}
|
||||||
|
onChange={onLineLimitChange}
|
||||||
onKeyDown={onKeyDownFunc}
|
onKeyDown={onKeyDownFunc}
|
||||||
min={min}
|
value={lineLimitValue}
|
||||||
value={value}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,6 +20,17 @@ export function LokiExploreQueryEditor(props: Props) {
|
|||||||
onChange(nextQuery);
|
onChange(nextQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onQueryTypeChange(value: string) {
|
||||||
|
const { query, onChange } = props;
|
||||||
|
let nextQuery;
|
||||||
|
if (value === 'instant') {
|
||||||
|
nextQuery = { ...query, instant: true, range: false };
|
||||||
|
} else {
|
||||||
|
nextQuery = { ...query, instant: false, range: true };
|
||||||
|
}
|
||||||
|
onChange(nextQuery);
|
||||||
|
}
|
||||||
|
|
||||||
function preprocessMaxLines(value: string): number {
|
function preprocessMaxLines(value: string): number {
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
// empty input - falls back to dataSource.maxLines limit
|
// empty input - falls back to dataSource.maxLines limit
|
||||||
@ -58,12 +69,11 @@ export function LokiExploreQueryEditor(props: Props) {
|
|||||||
range={range}
|
range={range}
|
||||||
ExtraFieldElement={
|
ExtraFieldElement={
|
||||||
<LokiExploreExtraField
|
<LokiExploreExtraField
|
||||||
label={'Line limit'}
|
queryType={query.instant ? 'instant' : 'range'}
|
||||||
onChangeFunc={onMaxLinesChange}
|
lineLimitValue={query?.maxLines?.toString() || ''}
|
||||||
|
onQueryTypeChange={onQueryTypeChange}
|
||||||
|
onLineLimitChange={onMaxLinesChange}
|
||||||
onKeyDownFunc={onReturnKeyDown}
|
onKeyDownFunc={onReturnKeyDown}
|
||||||
value={query?.maxLines?.toString() || ''}
|
|
||||||
type={'number'}
|
|
||||||
min={0}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -150,7 +150,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1">
|
<div className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1">
|
||||||
<div className="gf-form flex-shrink-0">
|
<div className="gf-form flex-shrink-0 min-width-5">
|
||||||
<ButtonCascader
|
<ButtonCascader
|
||||||
options={logLabelOptions || []}
|
options={logLabelOptions || []}
|
||||||
disabled={buttonDisabled}
|
disabled={buttonDisabled}
|
||||||
@ -161,7 +161,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
{chooserText}
|
{chooserText}
|
||||||
</ButtonCascader>
|
</ButtonCascader>
|
||||||
</div>
|
</div>
|
||||||
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15 explore-input-margin">
|
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15">
|
||||||
<QueryField
|
<QueryField
|
||||||
additionalPlugins={this.plugins}
|
additionalPlugins={this.plugins}
|
||||||
cleanText={cleanText}
|
cleanText={cleanText}
|
||||||
@ -176,8 +176,8 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
syntaxLoaded={syntaxLoaded}
|
syntaxLoaded={syntaxLoaded}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ExtraFieldElement}
|
|
||||||
</div>
|
</div>
|
||||||
|
{ExtraFieldElement}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`LokiExploreExtraField should render component 1`] = `
|
|
||||||
<div
|
|
||||||
className="gf-form-inline"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gf-form"
|
|
||||||
>
|
|
||||||
<Component
|
|
||||||
width={5}
|
|
||||||
>
|
|
||||||
Loki Explore Extra Field
|
|
||||||
</Component>
|
|
||||||
<input
|
|
||||||
className="gf-form-input width-4"
|
|
||||||
min={0}
|
|
||||||
onChange={[MockFunction]}
|
|
||||||
onKeyDown={[MockFunction]}
|
|
||||||
placeholder="auto"
|
|
||||||
type="number"
|
|
||||||
value="123"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -4,12 +4,11 @@ exports[`LokiExploreQueryEditor should render component 1`] = `
|
|||||||
<Component
|
<Component
|
||||||
ExtraFieldElement={
|
ExtraFieldElement={
|
||||||
<Memo(LokiExploreExtraField)
|
<Memo(LokiExploreExtraField)
|
||||||
label="Line limit"
|
lineLimitValue="0"
|
||||||
min={0}
|
|
||||||
onChangeFunc={[Function]}
|
|
||||||
onKeyDownFunc={[Function]}
|
onKeyDownFunc={[Function]}
|
||||||
type="number"
|
onLineLimitChange={[Function]}
|
||||||
value="0"
|
onQueryTypeChange={[Function]}
|
||||||
|
queryType="range"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
data={
|
data={
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { of, throwError } from 'rxjs';
|
import { of, throwError } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { omit } from 'lodash';
|
import { AnnotationQueryRequest, CoreApp, DataFrame, dateTime, FieldCache, TimeRange, TimeSeries } from '@grafana/data';
|
||||||
import { AnnotationQueryRequest, CoreApp, DataFrame, dateTime, FieldCache, TimeRange } from '@grafana/data';
|
|
||||||
import { BackendSrvRequest, FetchResponse } from '@grafana/runtime';
|
import { BackendSrvRequest, FetchResponse } from '@grafana/runtime';
|
||||||
|
|
||||||
import LokiDatasource from './datasource';
|
import LokiDatasource from './datasource';
|
||||||
@ -26,7 +25,7 @@ const timeSrvStub = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const testResponse: FetchResponse<LokiResponse> = {
|
const testLogsResponse: FetchResponse<LokiResponse> = {
|
||||||
data: {
|
data: {
|
||||||
data: {
|
data: {
|
||||||
resultType: LokiResultType.Stream,
|
resultType: LokiResultType.Stream,
|
||||||
@ -49,6 +48,29 @@ const testResponse: FetchResponse<LokiResponse> = {
|
|||||||
config: ({} as unknown) as BackendSrvRequest,
|
config: ({} as unknown) as BackendSrvRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testMetricsResponse: FetchResponse<LokiResponse> = {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
resultType: LokiResultType.Matrix,
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
metric: {},
|
||||||
|
values: [[1605715380, '1.1']],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
ok: true,
|
||||||
|
headers: ({} as unknown) as Headers,
|
||||||
|
redirected: false,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
type: 'basic',
|
||||||
|
url: '',
|
||||||
|
config: ({} as unknown) as BackendSrvRequest,
|
||||||
|
};
|
||||||
|
|
||||||
describe('LokiDatasource', () => {
|
describe('LokiDatasource', () => {
|
||||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||||
|
|
||||||
@ -96,7 +118,7 @@ describe('LokiDatasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when querying with limits', () => {
|
describe('when doing logs queries with limits', () => {
|
||||||
const runLimitTest = async ({
|
const runLimitTest = async ({
|
||||||
maxDataPoints = 123,
|
maxDataPoints = 123,
|
||||||
queryMaxLines,
|
queryMaxLines,
|
||||||
@ -121,7 +143,7 @@ describe('LokiDatasource', () => {
|
|||||||
const options = getQueryOptions<LokiQuery>({ targets: [{ expr, refId: 'B', maxLines: queryMaxLines }] });
|
const options = getQueryOptions<LokiQuery>({ targets: [{ expr, refId: 'B', maxLines: queryMaxLines }] });
|
||||||
options.maxDataPoints = maxDataPoints;
|
options.maxDataPoints = maxDataPoints;
|
||||||
|
|
||||||
fetchMock.mockImplementation(() => of(testResponse));
|
fetchMock.mockImplementation(() => of(testLogsResponse));
|
||||||
|
|
||||||
await expect(ds.query(options).pipe(take(1))).toEmitValuesWith(() => {
|
await expect(ds.query(options).pipe(take(1))).toEmitValuesWith(() => {
|
||||||
expect(fetchMock.mock.calls.length).toBe(1);
|
expect(fetchMock.mock.calls.length).toBe(1);
|
||||||
@ -151,10 +173,10 @@ describe('LokiDatasource', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when querying', () => {
|
describe('when querying', () => {
|
||||||
function setup(expr: string, app: CoreApp) {
|
function setup(expr: string, app: CoreApp, instant?: boolean, range?: boolean) {
|
||||||
const ds = createLokiDSForTests();
|
const ds = createLokiDSForTests();
|
||||||
const options = getQueryOptions<LokiQuery>({
|
const options = getQueryOptions<LokiQuery>({
|
||||||
targets: [{ expr, refId: 'B' }],
|
targets: [{ expr, refId: 'B', instant, range }],
|
||||||
app,
|
app,
|
||||||
});
|
});
|
||||||
ds.runInstantQuery = jest.fn(() => of({ data: [] }));
|
ds.runInstantQuery = jest.fn(() => of({ data: [] }));
|
||||||
@ -162,68 +184,95 @@ describe('LokiDatasource', () => {
|
|||||||
return { ds, options };
|
return { ds, options };
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should run range and instant query in Explore if running metric query', async () => {
|
const metricsQuery = 'rate({job="grafana"}[10m])';
|
||||||
const { ds, options } = setup('rate({job="grafana"}[10m])', CoreApp.Explore);
|
const logsQuery = '{job="grafana"} |= "foo"';
|
||||||
|
|
||||||
|
it('should run logs instant if only instant is selected', async () => {
|
||||||
|
const { ds, options } = setup(logsQuery, CoreApp.Explore, true, false);
|
||||||
await ds.query(options).toPromise();
|
await ds.query(options).toPromise();
|
||||||
expect(ds.runInstantQuery).toBeCalled();
|
expect(ds.runInstantQuery).toBeCalled();
|
||||||
expect(ds.runRangeQuery).toBeCalled();
|
expect(ds.runRangeQuery).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run only range query in Explore if running logs query', async () => {
|
it('should run metrics instant if only instant is selected', async () => {
|
||||||
const { ds, options } = setup('{job="grafana"}', CoreApp.Explore);
|
const { ds, options } = setup(metricsQuery, CoreApp.Explore, true, false);
|
||||||
|
await ds.query(options).toPromise();
|
||||||
|
expect(ds.runInstantQuery).toBeCalled();
|
||||||
|
expect(ds.runRangeQuery).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run only logs range query if only range is selected', async () => {
|
||||||
|
const { ds, options } = setup(logsQuery, CoreApp.Explore, false, true);
|
||||||
await ds.query(options).toPromise();
|
await ds.query(options).toPromise();
|
||||||
expect(ds.runInstantQuery).not.toBeCalled();
|
expect(ds.runInstantQuery).not.toBeCalled();
|
||||||
expect(ds.runRangeQuery).toBeCalled();
|
expect(ds.runRangeQuery).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run only range query in Dashboard', async () => {
|
it('should run only metrics range query if only range is selected', async () => {
|
||||||
const { ds, options } = setup('rate({job="grafana"}[10m])', CoreApp.Dashboard);
|
const { ds, options } = setup(metricsQuery, CoreApp.Explore, false, true);
|
||||||
await ds.query(options).toPromise();
|
await ds.query(options).toPromise();
|
||||||
expect(ds.runInstantQuery).not.toBeCalled();
|
expect(ds.runInstantQuery).not.toBeCalled();
|
||||||
expect(ds.runRangeQuery).toBeCalled();
|
expect(ds.runRangeQuery).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return series data for both queries in Explore if metrics query', async () => {
|
it('should run only logs range query if no query type is selected in Explore', async () => {
|
||||||
|
const { ds, options } = setup(logsQuery, CoreApp.Explore);
|
||||||
|
await ds.query(options).toPromise();
|
||||||
|
expect(ds.runInstantQuery).not.toBeCalled();
|
||||||
|
expect(ds.runRangeQuery).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run only metrics range query if no query type is selected in Explore', async () => {
|
||||||
|
const { ds, options } = setup(metricsQuery, CoreApp.Explore);
|
||||||
|
await ds.query(options).toPromise();
|
||||||
|
expect(ds.runInstantQuery).not.toBeCalled();
|
||||||
|
expect(ds.runRangeQuery).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run only logs range query in Dashboard', async () => {
|
||||||
|
const { ds, options } = setup(logsQuery, CoreApp.Dashboard);
|
||||||
|
await ds.query(options).toPromise();
|
||||||
|
expect(ds.runInstantQuery).not.toBeCalled();
|
||||||
|
expect(ds.runRangeQuery).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run only metrics range query in Dashboard', async () => {
|
||||||
|
const { ds, options } = setup(metricsQuery, CoreApp.Dashboard);
|
||||||
|
await ds.query(options).toPromise();
|
||||||
|
expect(ds.runInstantQuery).not.toBeCalled();
|
||||||
|
expect(ds.runRangeQuery).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return series data for metrics range queries', async () => {
|
||||||
const ds = createLokiDSForTests();
|
const ds = createLokiDSForTests();
|
||||||
const options = getQueryOptions<LokiQuery>({
|
const options = getQueryOptions<LokiQuery>({
|
||||||
targets: [{ expr: 'rate({job="grafana"} |= "foo" [10m])', refId: 'B' }],
|
targets: [{ expr: metricsQuery, refId: 'B', range: true }],
|
||||||
app: CoreApp.Explore,
|
app: CoreApp.Explore,
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock
|
fetchMock.mockImplementation(() => of(testMetricsResponse));
|
||||||
.mockImplementationOnce(() => of(testResponse))
|
|
||||||
.mockImplementation(() => of(omit(testResponse, 'data.status')));
|
|
||||||
|
|
||||||
await expect(ds.query(options)).toEmitValuesWith(received => {
|
await expect(ds.query(options)).toEmitValuesWith(received => {
|
||||||
// first result always comes from runInstantQuery
|
const result = received[0];
|
||||||
const firstResult = received[0];
|
const timeSeries = result.data[0] as TimeSeries;
|
||||||
expect(firstResult).toEqual({ data: [], key: 'B_instant' });
|
|
||||||
|
|
||||||
// second result always comes from runRangeQuery
|
expect(timeSeries.meta?.preferredVisualisationType).toBe('graph');
|
||||||
const secondResult = received[1];
|
expect(timeSeries.refId).toBe('B');
|
||||||
const dataFrame = secondResult.data[0] as DataFrame;
|
expect(timeSeries.datapoints[0]).toEqual([1.1, 1605715380000]);
|
||||||
const fieldCache = new FieldCache(dataFrame);
|
|
||||||
|
|
||||||
expect(fieldCache.getFieldByName('line')?.values.get(0)).toBe('hello');
|
|
||||||
expect(dataFrame.meta?.limit).toBe(500);
|
|
||||||
expect(dataFrame.meta?.searchWords).toEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return series data for range query in Dashboard', async () => {
|
it('should return series data for logs range query', async () => {
|
||||||
const ds = createLokiDSForTests();
|
const ds = createLokiDSForTests();
|
||||||
const options = getQueryOptions<LokiQuery>({
|
const options = getQueryOptions<LokiQuery>({
|
||||||
targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }],
|
targets: [{ expr: logsQuery, refId: 'B' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock
|
fetchMock.mockImplementation(() => of(testLogsResponse));
|
||||||
.mockImplementationOnce(() => of(testResponse))
|
|
||||||
.mockImplementation(() => of(omit(testResponse, 'data.status')));
|
|
||||||
|
|
||||||
await expect(ds.query(options)).toEmitValuesWith(received => {
|
await expect(ds.query(options)).toEmitValuesWith(received => {
|
||||||
// first result will come from runRangeQuery
|
const result = received[0];
|
||||||
const firstResult = received[0];
|
const dataFrame = result.data[0] as DataFrame;
|
||||||
const dataFrame = firstResult.data[0] as DataFrame;
|
|
||||||
const fieldCache = new FieldCache(dataFrame);
|
const fieldCache = new FieldCache(dataFrame);
|
||||||
|
|
||||||
expect(fieldCache.getFieldByName('line')?.values.get(0)).toBe('hello');
|
expect(fieldCache.getFieldByName('line')?.values.get(0)).toBe('hello');
|
||||||
|
@ -24,13 +24,17 @@ import {
|
|||||||
QueryResultMeta,
|
QueryResultMeta,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
CoreApp,
|
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getTemplateSrv, TemplateSrv, BackendSrvRequest, FetchError, getBackendSrv } from '@grafana/runtime';
|
import { getTemplateSrv, TemplateSrv, BackendSrvRequest, FetchError, getBackendSrv } from '@grafana/runtime';
|
||||||
import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { convertToWebSocketUrl } from 'app/core/utils/explore';
|
import { convertToWebSocketUrl } from 'app/core/utils/explore';
|
||||||
import { lokiResultsToTableModel, lokiStreamResultToDataFrame, processRangeQueryResponse } from './result_transformer';
|
import {
|
||||||
|
lokiResultsToTableModel,
|
||||||
|
lokiStreamResultToDataFrame,
|
||||||
|
lokiStreamsToDataFrames,
|
||||||
|
processRangeQueryResponse,
|
||||||
|
} from './result_transformer';
|
||||||
import { getHighlighterExpressionsFromQuery } from './query_utils';
|
import { getHighlighterExpressionsFromQuery } from './query_utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -99,13 +103,12 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
for (const target of filteredTargets) {
|
for (const target of filteredTargets) {
|
||||||
// In explore we want to show result of metrics instant query in a table under the graph panel to mimic behaviour of prometheus.
|
if (target.instant) {
|
||||||
// We don't want to do that in dashboards though as user would have to pick the correct data frame.
|
|
||||||
if (options.app === CoreApp.Explore && isMetricsQuery(target.expr)) {
|
|
||||||
subQueries.push(this.runInstantQuery(target, options, filteredTargets.length));
|
subQueries.push(this.runInstantQuery(target, options, filteredTargets.length));
|
||||||
}
|
} else {
|
||||||
subQueries.push(this.runRangeQuery(target, options, filteredTargets.length));
|
subQueries.push(this.runRangeQuery(target, options, filteredTargets.length));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// No valid targets, return the empty result to save a round trip.
|
// No valid targets, return the empty result to save a round trip.
|
||||||
if (isEmpty(subQueries)) {
|
if (isEmpty(subQueries)) {
|
||||||
@ -124,12 +127,14 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
responseListLength: number
|
responseListLength: number
|
||||||
): Observable<DataQueryResponse> => {
|
): Observable<DataQueryResponse> => {
|
||||||
const timeNs = this.getTime(options.range.to, true);
|
const timeNs = this.getTime(options.range.to, true);
|
||||||
|
const queryLimit = isMetricsQuery(target.expr) ? options.maxDataPoints : target.maxLines;
|
||||||
const query = {
|
const query = {
|
||||||
query: target.expr,
|
query: target.expr,
|
||||||
time: `${timeNs + (1e9 - (timeNs % 1e9))}`,
|
time: `${timeNs + (1e9 - (timeNs % 1e9))}`,
|
||||||
limit: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
limit: Math.min(queryLimit || Infinity, this.maxLines),
|
||||||
};
|
};
|
||||||
/** Show results of Loki instant queries only in table */
|
|
||||||
|
/** Used only for results of metrics instant queries */
|
||||||
const meta: QueryResultMeta = {
|
const meta: QueryResultMeta = {
|
||||||
preferredVisualisationType: 'table',
|
preferredVisualisationType: 'table',
|
||||||
};
|
};
|
||||||
@ -138,7 +143,14 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
map((response: { data: LokiResponse }) => {
|
map((response: { data: LokiResponse }) => {
|
||||||
if (response.data.data.resultType === LokiResultType.Stream) {
|
if (response.data.data.resultType === LokiResultType.Stream) {
|
||||||
return {
|
return {
|
||||||
data: [],
|
data: response.data
|
||||||
|
? lokiStreamsToDataFrames(
|
||||||
|
response.data as LokiStreamResponse,
|
||||||
|
target,
|
||||||
|
query.limit,
|
||||||
|
this.instanceSettings.jsonData
|
||||||
|
)
|
||||||
|
: [],
|
||||||
key: `${target.refId}_instant`,
|
key: `${target.refId}_instant`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,10 @@ describe('loki result transformer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('lokiStreamsToDataframes', () => {
|
describe('lokiStreamsToDataFrames', () => {
|
||||||
it('should enhance data frames', () => {
|
it('should enhance data frames', () => {
|
||||||
jest.spyOn(ResultTransformer, 'enhanceDataFrame');
|
jest.spyOn(ResultTransformer, 'enhanceDataFrame');
|
||||||
const dataFrames = ResultTransformer.lokiStreamsToDataframes(lokiResponse, { refId: 'B' }, 500, {
|
const dataFrames = ResultTransformer.lokiStreamsToDataFrames(lokiResponse, { refId: 'B' }, 500, {
|
||||||
derivedFields: [
|
derivedFields: [
|
||||||
{
|
{
|
||||||
matcherRegex: 'trace=(w+)',
|
matcherRegex: 'trace=(w+)',
|
||||||
|
@ -305,7 +305,7 @@ function lokiStatsToMetaStat(stats: LokiStats | undefined): QueryResultMetaStat[
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lokiStreamsToDataframes(
|
export function lokiStreamsToDataFrames(
|
||||||
response: LokiStreamResponse,
|
response: LokiStreamResponse,
|
||||||
target: { refId: string; expr?: string },
|
target: { refId: string; expr?: string },
|
||||||
limit: number,
|
limit: number,
|
||||||
@ -472,7 +472,7 @@ export function processRangeQueryResponse(
|
|||||||
switch (response.data.resultType) {
|
switch (response.data.resultType) {
|
||||||
case LokiResultType.Stream:
|
case LokiResultType.Stream:
|
||||||
return of({
|
return of({
|
||||||
data: lokiStreamsToDataframes(response as LokiStreamResponse, target, limit, config, reverse),
|
data: lokiStreamsToDataFrames(response as LokiStreamResponse, target, limit, config, reverse),
|
||||||
key: `${target.refId}_log`,
|
key: `${target.refId}_log`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,6 +30,8 @@ export interface LokiQuery extends DataQuery {
|
|||||||
legendFormat?: string;
|
legendFormat?: string;
|
||||||
valueWithRefId?: boolean;
|
valueWithRefId?: boolean;
|
||||||
maxLines?: number;
|
maxLines?: number;
|
||||||
|
range?: boolean;
|
||||||
|
instant?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LokiOptions extends DataSourceJsonData {
|
export interface LokiOptions extends DataSourceJsonData {
|
||||||
|
@ -23,7 +23,7 @@ export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-label="Prometheus extra field" className="gf-form-inline">
|
<div aria-label="Prometheus extra field" className="gf-form-inline">
|
||||||
{/*QueryTypeField */}
|
{/*Query type field*/}
|
||||||
<div
|
<div
|
||||||
data-testid="queryTypeField"
|
data-testid="queryTypeField"
|
||||||
className={cx(
|
className={cx(
|
||||||
|
Reference in New Issue
Block a user