mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Add feature to open log sample in split view (#62097)
* Add tests * Implement split open to see logs functionality * Fix imports in test * Update packages/grafana-data/src/types/logs.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * Update packages/grafana-data/src/types/logs.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * Update default scneario to throw error * Exit early in getSupplementaryQuery * Update public/app/features/explore/LogsSamplePanel.tsx Co-authored-by: Matias Chomicki <matyax@gmail.com>
This commit is contained in:
@@ -195,11 +195,23 @@ export enum SupplementaryQueryType {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export interface DataSourceWithSupplementaryQueriesSupport<TQuery extends DataQuery> {
|
export interface DataSourceWithSupplementaryQueriesSupport<TQuery extends DataQuery> {
|
||||||
|
/**
|
||||||
|
* Returns an observable that will be used to fetch supplementary data based on the provided
|
||||||
|
* supplementary query type and original request.
|
||||||
|
*/
|
||||||
getDataProvider(
|
getDataProvider(
|
||||||
type: SupplementaryQueryType,
|
type: SupplementaryQueryType,
|
||||||
request: DataQueryRequest<TQuery>
|
request: DataQueryRequest<TQuery>
|
||||||
): Observable<DataQueryResponse> | undefined;
|
): Observable<DataQueryResponse> | undefined;
|
||||||
|
/**
|
||||||
|
* Returns supplementary query types that data source supports.
|
||||||
|
*/
|
||||||
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[];
|
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[];
|
||||||
|
/**
|
||||||
|
* Returns a supplementary query to be used to fetch supplementary data based on the provided type and original query.
|
||||||
|
* If provided query is not suitable for provided supplementary query type, undefined should be returned.
|
||||||
|
*/
|
||||||
|
getSupplementaryQuery(type: SupplementaryQueryType, query: TQuery): TQuery | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasSupplementaryQuerySupport = <TQuery extends DataQuery>(
|
export const hasSupplementaryQuerySupport = <TQuery extends DataQuery>(
|
||||||
@@ -214,6 +226,7 @@ export const hasSupplementaryQuerySupport = <TQuery extends DataQuery>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
withSupplementaryQueriesSupport.getDataProvider !== undefined &&
|
withSupplementaryQueriesSupport.getDataProvider !== undefined &&
|
||||||
|
withSupplementaryQueriesSupport.getSupplementaryQuery !== undefined &&
|
||||||
withSupplementaryQueriesSupport.getSupportedSupplementaryQueryTypes().includes(type)
|
withSupplementaryQueriesSupport.getSupportedSupplementaryQueryTypes().includes(type)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -367,15 +367,17 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLogsSamplePanel() {
|
renderLogsSamplePanel() {
|
||||||
const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance } = this.props;
|
const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance, queries } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogsSamplePanel
|
<LogsSamplePanel
|
||||||
queryResponse={logsSample.data}
|
queryResponse={logsSample.data}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
enabled={logsSample.enabled}
|
enabled={logsSample.enabled}
|
||||||
|
queries={queries}
|
||||||
datasourceInstance={datasourceInstance}
|
datasourceInstance={datasourceInstance}
|
||||||
setLogsSampleEnabled={(enabled) =>
|
splitOpen={this.onSplitOpen('logsSample')}
|
||||||
|
setLogsSampleEnabled={(enabled: boolean) =>
|
||||||
setSupplementaryQueryEnabled(exploreId, enabled, SupplementaryQueryType.LogsSample)
|
setSupplementaryQueryEnabled(exploreId, enabled, SupplementaryQueryType.LogsSample)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
|
|
||||||
import { ArrayVector, FieldType, LoadingState, MutableDataFrame } from '@grafana/data';
|
import {
|
||||||
|
ArrayVector,
|
||||||
|
FieldType,
|
||||||
|
LoadingState,
|
||||||
|
MutableDataFrame,
|
||||||
|
SupplementaryQueryType,
|
||||||
|
DataSourceApi,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
|
||||||
import { LogsSamplePanel } from './LogsSamplePanel';
|
import { LogsSamplePanel } from './LogsSamplePanel';
|
||||||
|
|
||||||
@@ -20,6 +28,8 @@ const createProps = (propOverrides?: Partial<ComponentProps<typeof LogsSamplePan
|
|||||||
timeZone: 'timeZone',
|
timeZone: 'timeZone',
|
||||||
datasourceInstance: undefined,
|
datasourceInstance: undefined,
|
||||||
setLogsSampleEnabled: jest.fn(),
|
setLogsSampleEnabled: jest.fn(),
|
||||||
|
queries: [],
|
||||||
|
splitOpen: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...props, ...propOverrides };
|
return { ...props, ...propOverrides };
|
||||||
@@ -100,4 +110,32 @@ describe('LogsSamplePanel', () => {
|
|||||||
expect(screen.getByText('Failed to load logs sample for this query')).toBeInTheDocument();
|
expect(screen.getByText('Failed to load logs sample for this query')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Test error message')).toBeInTheDocument();
|
expect(screen.getByText('Test error message')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
it('has split open button functionality', async () => {
|
||||||
|
const datasourceInstance = {
|
||||||
|
uid: 'test_uid',
|
||||||
|
getDataProvider: jest.fn(),
|
||||||
|
getSupportedSupplementaryQueryTypes: jest.fn().mockImplementation(() => [SupplementaryQueryType.LogsSample]),
|
||||||
|
getSupplementaryQuery: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
refId: 'test_refid',
|
||||||
|
} as DataQuery;
|
||||||
|
}),
|
||||||
|
} as unknown as DataSourceApi;
|
||||||
|
const splitOpen = jest.fn();
|
||||||
|
render(
|
||||||
|
<LogsSamplePanel
|
||||||
|
{...createProps({
|
||||||
|
queries: [{ refId: 'test_refid' }],
|
||||||
|
queryResponse: { data: [sampleDataFrame], state: LoadingState.Done },
|
||||||
|
splitOpen,
|
||||||
|
datasourceInstance,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const splitButton = screen.getByText('Open logs in split view');
|
||||||
|
expect(splitButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(splitButton);
|
||||||
|
expect(splitOpen).toHaveBeenCalledWith({ datasourceUid: 'test_uid', query: { refId: 'test_refid' } });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataQueryResponse, DataSourceApi, LoadingState, LogsDedupStrategy } from '@grafana/data';
|
import {
|
||||||
|
DataQueryResponse,
|
||||||
|
DataSourceApi,
|
||||||
|
GrafanaTheme2,
|
||||||
|
hasSupplementaryQuerySupport,
|
||||||
|
LoadingState,
|
||||||
|
LogsDedupStrategy,
|
||||||
|
SplitOpen,
|
||||||
|
SupplementaryQueryType,
|
||||||
|
} from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone, DataQuery } from '@grafana/schema';
|
||||||
import { Collapse } from '@grafana/ui';
|
import { Button, Collapse, useStyles2 } from '@grafana/ui';
|
||||||
import { dataFrameToLogsModel } from 'app/core/logsModel';
|
import { dataFrameToLogsModel } from 'app/core/logsModel';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
|
|
||||||
@@ -16,13 +26,16 @@ type Props = {
|
|||||||
queryResponse: DataQueryResponse | undefined;
|
queryResponse: DataQueryResponse | undefined;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
queries: DataQuery[];
|
||||||
datasourceInstance: DataSourceApi | null | undefined;
|
datasourceInstance: DataSourceApi | null | undefined;
|
||||||
|
splitOpen: SplitOpen;
|
||||||
setLogsSampleEnabled: (enabled: boolean) => void;
|
setLogsSampleEnabled: (enabled: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LogsSamplePanel(props: Props) {
|
export function LogsSamplePanel(props: Props) {
|
||||||
const { queryResponse, timeZone, enabled, setLogsSampleEnabled, datasourceInstance } = props;
|
const { queryResponse, timeZone, enabled, setLogsSampleEnabled, datasourceInstance, queries, splitOpen } = props;
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
const onToggleLogsSampleCollapse = (isOpen: boolean) => {
|
const onToggleLogsSampleCollapse = (isOpen: boolean) => {
|
||||||
setLogsSampleEnabled(isOpen);
|
setLogsSampleEnabled(isOpen);
|
||||||
reportInteraction('grafana_explore_logs_sample_toggle_clicked', {
|
reportInteraction('grafana_explore_logs_sample_toggle_clicked', {
|
||||||
@@ -31,6 +44,32 @@ export function LogsSamplePanel(props: Props) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const OpenInSplitViewButton = () => {
|
||||||
|
if (!hasSupplementaryQuerySupport(datasourceInstance, SupplementaryQueryType.LogsSample)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logSampleQueries = queries
|
||||||
|
.map((query) => datasourceInstance.getSupplementaryQuery(SupplementaryQueryType.LogsSample, query))
|
||||||
|
.filter((query): query is DataQuery => !!query);
|
||||||
|
|
||||||
|
if (!logSampleQueries.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={styles.logSamplesButton}
|
||||||
|
// TODO: support multiple queries (#62107)
|
||||||
|
// This currently works only for the first query as splitOpen supports only 1 query
|
||||||
|
onClick={() => splitOpen({ query: logSampleQueries[0], datasourceUid: datasourceInstance.uid })}
|
||||||
|
>
|
||||||
|
Open logs in split view
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let LogsSamplePanelContent: JSX.Element | null;
|
let LogsSamplePanelContent: JSX.Element | null;
|
||||||
|
|
||||||
if (queryResponse === undefined) {
|
if (queryResponse === undefined) {
|
||||||
@@ -46,6 +85,8 @@ export function LogsSamplePanel(props: Props) {
|
|||||||
} else {
|
} else {
|
||||||
const logs = dataFrameToLogsModel(queryResponse.data);
|
const logs = dataFrameToLogsModel(queryResponse.data);
|
||||||
LogsSamplePanelContent = (
|
LogsSamplePanelContent = (
|
||||||
|
<>
|
||||||
|
<OpenInSplitViewButton />
|
||||||
<LogRows
|
<LogRows
|
||||||
logRows={logs.rows}
|
logRows={logs.rows}
|
||||||
dedupStrategy={LogsDedupStrategy.none}
|
dedupStrategy={LogsDedupStrategy.none}
|
||||||
@@ -56,6 +97,7 @@ export function LogsSamplePanel(props: Props) {
|
|||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
enableLogDetails={true}
|
enableLogDetails={true}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,3 +107,11 @@ export function LogsSamplePanel(props: Props) {
|
|||||||
</Collapse>
|
</Collapse>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
logSamplesButton: css`
|
||||||
|
position: absolute;
|
||||||
|
top: ${theme.spacing(1)};
|
||||||
|
right: ${theme.spacing(1)}; ;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const createDefaultInitialState = () => {
|
|||||||
getSupportedSupplementaryQueryTypes: jest
|
getSupportedSupplementaryQueryTypes: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(() => [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]),
|
.mockImplementation(() => [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]),
|
||||||
|
getSupplementaryQuery: jest.fn(),
|
||||||
meta: {
|
meta: {
|
||||||
id: 'something',
|
id: 'something',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ describe('reducer', () => {
|
|||||||
SupplementaryQueryType.LogsVolume,
|
SupplementaryQueryType.LogsVolume,
|
||||||
SupplementaryQueryType.LogsSample,
|
SupplementaryQueryType.LogsSample,
|
||||||
],
|
],
|
||||||
|
getSupplementaryQuery: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
FieldType,
|
FieldType,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
|
SupplementaryQueryType,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
toUtc,
|
toUtc,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
@@ -924,6 +925,56 @@ describe('ElasticDatasource', () => {
|
|||||||
|
|
||||||
expect((interpolatedQuery.bucketAggs![0] as Filters).settings!.filters![0].query).toBe('*');
|
expect((interpolatedQuery.bucketAggs![0] as Filters).settings!.filters![0].query).toBe('*');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getSupplementaryQuery', () => {
|
||||||
|
let ds: ElasticDatasource;
|
||||||
|
beforeEach(() => {
|
||||||
|
ds = getTestContext().ds;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return logs volume query for metric query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, {
|
||||||
|
refId: 'A',
|
||||||
|
metrics: [{ type: 'count', id: '1' }],
|
||||||
|
bucketAggs: [{ type: 'filters', settings: { filters: [{ query: 'foo', label: '' }] }, id: '1' }],
|
||||||
|
query: 'foo="bar"',
|
||||||
|
})
|
||||||
|
).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns logs volume query for log query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, {
|
||||||
|
refId: 'A',
|
||||||
|
metrics: [{ type: 'logs', id: '1' }],
|
||||||
|
query: 'foo="bar"',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
bucketAggs: [
|
||||||
|
{
|
||||||
|
field: '',
|
||||||
|
id: '3',
|
||||||
|
settings: {
|
||||||
|
interval: 'auto',
|
||||||
|
min_doc_count: '0',
|
||||||
|
trimEdges: '0',
|
||||||
|
},
|
||||||
|
type: 'date_histogram',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'count',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
query: 'foo="bar"',
|
||||||
|
refId: 'log-volume-A',
|
||||||
|
timeField: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getMultiSearchUrl', () => {
|
describe('getMultiSearchUrl', () => {
|
||||||
|
|||||||
@@ -600,15 +600,20 @@ export class ElasticDatasource
|
|||||||
return [SupplementaryQueryType.LogsVolume];
|
return [SupplementaryQueryType.LogsVolume];
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogsVolumeDataProvider(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> | undefined {
|
getSupplementaryQuery(type: SupplementaryQueryType, query: ElasticsearchQuery): ElasticsearchQuery | undefined {
|
||||||
const isLogsVolumeAvailable = request.targets.some((target) => {
|
if (!this.getSupportedSupplementaryQueryTypes().includes(type)) {
|
||||||
return target.metrics?.length === 1 && target.metrics[0].type === 'logs';
|
return undefined;
|
||||||
});
|
}
|
||||||
if (!isLogsVolumeAvailable) {
|
|
||||||
|
let isQuerySuitable = false;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case SupplementaryQueryType.LogsVolume:
|
||||||
|
// it has to be a logs-producing range-query
|
||||||
|
isQuerySuitable = !!(query.metrics?.length === 1 && query.metrics[0].type === 'logs');
|
||||||
|
if (!isQuerySuitable) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const logsVolumeRequest = cloneDeep(request);
|
|
||||||
logsVolumeRequest.targets = logsVolumeRequest.targets.map((target) => {
|
|
||||||
const bucketAggs: BucketAggregation[] = [];
|
const bucketAggs: BucketAggregation[] = [];
|
||||||
const timeField = this.timeField ?? '@timestamp';
|
const timeField = this.timeField ?? '@timestamp';
|
||||||
|
|
||||||
@@ -637,21 +642,38 @@ export class ElasticDatasource
|
|||||||
field: timeField,
|
field: timeField,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logsVolumeQuery: ElasticsearchQuery = {
|
return {
|
||||||
refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`,
|
refId: `${REF_ID_STARTER_LOG_VOLUME}${query.refId}`,
|
||||||
query: target.query,
|
query: query.query,
|
||||||
metrics: [{ type: 'count', id: '1' }],
|
metrics: [{ type: 'count', id: '1' }],
|
||||||
timeField,
|
timeField,
|
||||||
bucketAggs,
|
bucketAggs,
|
||||||
};
|
};
|
||||||
return logsVolumeQuery;
|
|
||||||
});
|
|
||||||
|
|
||||||
return queryLogsVolume(this, logsVolumeRequest, {
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogsVolumeDataProvider(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> | undefined {
|
||||||
|
const logsVolumeRequest = cloneDeep(request);
|
||||||
|
const targets = logsVolumeRequest.targets
|
||||||
|
.map((target) => this.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, target))
|
||||||
|
.filter((query): query is ElasticsearchQuery => !!query);
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryLogsVolume(
|
||||||
|
this,
|
||||||
|
{ ...logsVolumeRequest, targets },
|
||||||
|
{
|
||||||
range: request.range,
|
range: request.range,
|
||||||
targets: request.targets,
|
targets: request.targets,
|
||||||
extractLevel: (dataFrame) => getLogLevelFromKey(dataFrame.name || ''),
|
extractLevel: (dataFrame) => getLogLevelFromKey(dataFrame.name || ''),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
query(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> {
|
query(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> {
|
||||||
|
|||||||
@@ -894,7 +894,7 @@ describe('LokiDatasource', () => {
|
|||||||
|
|
||||||
it('creates provider for logs query', () => {
|
it('creates provider for logs query', () => {
|
||||||
const options = getQueryOptions<LokiQuery>({
|
const options = getQueryOptions<LokiQuery>({
|
||||||
targets: [{ expr: '{label=value}', refId: 'A' }],
|
targets: [{ expr: '{label=value}', refId: 'A', queryType: LokiQueryType.Range }],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ds.getDataProvider(SupplementaryQueryType.LogsVolume, options)).toBeDefined();
|
expect(ds.getDataProvider(SupplementaryQueryType.LogsVolume, options)).toBeDefined();
|
||||||
@@ -911,8 +911,8 @@ describe('LokiDatasource', () => {
|
|||||||
it('creates provider if at least one query is a logs query', () => {
|
it('creates provider if at least one query is a logs query', () => {
|
||||||
const options = getQueryOptions<LokiQuery>({
|
const options = getQueryOptions<LokiQuery>({
|
||||||
targets: [
|
targets: [
|
||||||
{ expr: 'rate({label=value}[1m])', refId: 'A' },
|
{ expr: 'rate({label=value}[1m])', queryType: LokiQueryType.Range, refId: 'A' },
|
||||||
{ expr: '{label=value}', refId: 'B' },
|
{ expr: '{label=value}', queryType: LokiQueryType.Range, refId: 'B' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -962,6 +962,93 @@ describe('LokiDatasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getSupplementaryQuery', () => {
|
||||||
|
let ds: LokiDatasource;
|
||||||
|
beforeEach(() => {
|
||||||
|
ds = createLokiDatasource(templateSrvStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logs volume', () => {
|
||||||
|
it('returns logs volume query for range log query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, {
|
||||||
|
expr: '{label=value}',
|
||||||
|
queryType: LokiQueryType.Range,
|
||||||
|
refId: 'A',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
expr: 'sum by (level) (count_over_time({label=value}[$__interval]))',
|
||||||
|
instant: false,
|
||||||
|
queryType: 'range',
|
||||||
|
refId: 'log-volume-A',
|
||||||
|
volumeQuery: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return logs volume query for instant log query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, {
|
||||||
|
expr: '{label=value}',
|
||||||
|
queryType: LokiQueryType.Instant,
|
||||||
|
refId: 'A',
|
||||||
|
})
|
||||||
|
).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return logs volume query for metric query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, {
|
||||||
|
expr: 'rate({label=value}[5m]',
|
||||||
|
queryType: LokiQueryType.Range,
|
||||||
|
refId: 'A',
|
||||||
|
})
|
||||||
|
).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logs sample', () => {
|
||||||
|
it('returns logs sample query for range metric query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsSample, {
|
||||||
|
expr: 'rate({label=value}[5m]',
|
||||||
|
queryType: LokiQueryType.Range,
|
||||||
|
refId: 'A',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
expr: '{label=value}',
|
||||||
|
queryType: 'range',
|
||||||
|
refId: 'log-sample-A',
|
||||||
|
maxLines: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns logs sample query for instant metric query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsSample, {
|
||||||
|
expr: 'rate({label=value}[5m]',
|
||||||
|
queryType: LokiQueryType.Instant,
|
||||||
|
refId: 'A',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
expr: '{label=value}',
|
||||||
|
queryType: 'instant',
|
||||||
|
refId: 'log-sample-A',
|
||||||
|
maxLines: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return logs sample query for log query query', () => {
|
||||||
|
expect(
|
||||||
|
ds.getSupplementaryQuery(SupplementaryQueryType.LogsSample, {
|
||||||
|
expr: '{label=value}',
|
||||||
|
queryType: LokiQueryType.Range,
|
||||||
|
refId: 'A',
|
||||||
|
})
|
||||||
|
).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('importing queries', () => {
|
describe('importing queries', () => {
|
||||||
let ds: LokiDatasource;
|
let ds: LokiDatasource;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -161,62 +161,80 @@ export class LokiDatasource
|
|||||||
return [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample];
|
return [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample];
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogsVolumeDataProvider(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> | undefined {
|
getSupplementaryQuery(type: SupplementaryQueryType, query: LokiQuery): LokiQuery | undefined {
|
||||||
const isQuerySuitable = (query: LokiQuery) => {
|
if (!this.getSupportedSupplementaryQueryTypes().includes(type)) {
|
||||||
const normalized = getNormalizedLokiQuery(query);
|
return undefined;
|
||||||
const { expr } = normalized;
|
}
|
||||||
// it has to be a logs-producing range-query
|
|
||||||
return expr && isLogsQuery(expr) && normalized.queryType === LokiQueryType.Range;
|
const normalizedQuery = getNormalizedLokiQuery(query);
|
||||||
};
|
const expr = removeCommentsFromQuery(normalizedQuery.expr);
|
||||||
|
let isQuerySuitable = false;
|
||||||
const isLogsVolumeAvailable = request.targets.some(isQuerySuitable);
|
|
||||||
|
switch (type) {
|
||||||
if (!isLogsVolumeAvailable) {
|
case SupplementaryQueryType.LogsVolume:
|
||||||
|
// it has to be a logs-producing range-query
|
||||||
|
isQuerySuitable = !!(query.expr && isLogsQuery(query.expr) && query.queryType === LokiQueryType.Range);
|
||||||
|
if (!isQuerySuitable) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logsVolumeRequest = cloneDeep(request);
|
|
||||||
logsVolumeRequest.targets = logsVolumeRequest.targets.filter(isQuerySuitable).map((target) => {
|
|
||||||
const query = removeCommentsFromQuery(target.expr);
|
|
||||||
return {
|
return {
|
||||||
...target,
|
...normalizedQuery,
|
||||||
refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`,
|
refId: `${REF_ID_STARTER_LOG_VOLUME}${normalizedQuery.refId}`,
|
||||||
instant: false,
|
instant: false,
|
||||||
volumeQuery: true,
|
volumeQuery: true,
|
||||||
expr: `sum by (level) (count_over_time(${query}[$__interval]))`,
|
expr: `sum by (level) (count_over_time(${expr}[$__interval]))`,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
return queryLogsVolume(this, logsVolumeRequest, {
|
case SupplementaryQueryType.LogsSample:
|
||||||
|
// it has to be a metric query
|
||||||
|
isQuerySuitable = !!(query.expr && !isLogsQuery(query.expr));
|
||||||
|
if (!isQuerySuitable) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...normalizedQuery,
|
||||||
|
refId: `${REF_ID_STARTER_LOG_SAMPLE}${normalizedQuery.refId}`,
|
||||||
|
expr: getLogQueryFromMetricsQuery(expr),
|
||||||
|
maxLines: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogsVolumeDataProvider(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> | undefined {
|
||||||
|
const logsVolumeRequest = cloneDeep(request);
|
||||||
|
const targets = logsVolumeRequest.targets
|
||||||
|
.map((query) => this.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, query))
|
||||||
|
.filter((query): query is LokiQuery => !!query);
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryLogsVolume(
|
||||||
|
this,
|
||||||
|
{ ...logsVolumeRequest, targets },
|
||||||
|
{
|
||||||
extractLevel,
|
extractLevel,
|
||||||
range: request.range,
|
range: request.range,
|
||||||
targets: request.targets,
|
targets: request.targets,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogsSampleDataProvider(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> | undefined {
|
getLogsSampleDataProvider(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> | undefined {
|
||||||
const isQuerySuitable = (query: LokiQuery) => {
|
const logsSampleRequest = cloneDeep(request);
|
||||||
return query.expr && !isLogsQuery(query.expr);
|
const targets = logsSampleRequest.targets
|
||||||
};
|
.map((query) => this.getSupplementaryQuery(SupplementaryQueryType.LogsSample, query))
|
||||||
|
.filter((query): query is LokiQuery => !!query);
|
||||||
|
|
||||||
const isLogsSampleAvailable = request.targets.some(isQuerySuitable);
|
if (!targets.length) {
|
||||||
|
|
||||||
if (!isLogsSampleAvailable) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
return queryLogsSample(this, { ...logsSampleRequest, targets });
|
||||||
const logsSampleRequest = cloneDeep(request);
|
|
||||||
logsSampleRequest.targets = logsSampleRequest.targets.filter(isQuerySuitable).map((target) => {
|
|
||||||
const query = removeCommentsFromQuery(target.expr);
|
|
||||||
return {
|
|
||||||
...target,
|
|
||||||
refId: `${REF_ID_STARTER_LOG_SAMPLE}${target.refId}`,
|
|
||||||
expr: getLogQueryFromMetricsQuery(query),
|
|
||||||
maxLines: 100,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return queryLogsSample(this, logsSampleRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
|
query(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
|
||||||
|
|||||||
Reference in New Issue
Block a user