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:
parent
5e1dc22f88
commit
ea1fcbb866
@ -195,11 +195,23 @@ export enum SupplementaryQueryType {
|
||||
* @internal
|
||||
*/
|
||||
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(
|
||||
type: SupplementaryQueryType,
|
||||
request: DataQueryRequest<TQuery>
|
||||
): Observable<DataQueryResponse> | undefined;
|
||||
/**
|
||||
* Returns supplementary query types that data source supports.
|
||||
*/
|
||||
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>(
|
||||
@ -214,6 +226,7 @@ export const hasSupplementaryQuerySupport = <TQuery extends DataQuery>(
|
||||
|
||||
return (
|
||||
withSupplementaryQueriesSupport.getDataProvider !== undefined &&
|
||||
withSupplementaryQueriesSupport.getSupplementaryQuery !== undefined &&
|
||||
withSupplementaryQueriesSupport.getSupportedSupplementaryQueryTypes().includes(type)
|
||||
);
|
||||
};
|
||||
|
@ -367,15 +367,17 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
}
|
||||
|
||||
renderLogsSamplePanel() {
|
||||
const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance } = this.props;
|
||||
const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance, queries } = this.props;
|
||||
|
||||
return (
|
||||
<LogsSamplePanel
|
||||
queryResponse={logsSample.data}
|
||||
timeZone={timeZone}
|
||||
enabled={logsSample.enabled}
|
||||
queries={queries}
|
||||
datasourceInstance={datasourceInstance}
|
||||
setLogsSampleEnabled={(enabled) =>
|
||||
splitOpen={this.onSplitOpen('logsSample')}
|
||||
setLogsSampleEnabled={(enabled: boolean) =>
|
||||
setSupplementaryQueryEnabled(exploreId, enabled, SupplementaryQueryType.LogsSample)
|
||||
}
|
||||
/>
|
||||
|
@ -2,7 +2,15 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
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';
|
||||
|
||||
@ -20,6 +28,8 @@ const createProps = (propOverrides?: Partial<ComponentProps<typeof LogsSamplePan
|
||||
timeZone: 'timeZone',
|
||||
datasourceInstance: undefined,
|
||||
setLogsSampleEnabled: jest.fn(),
|
||||
queries: [],
|
||||
splitOpen: jest.fn(),
|
||||
};
|
||||
|
||||
return { ...props, ...propOverrides };
|
||||
@ -100,4 +110,32 @@ describe('LogsSamplePanel', () => {
|
||||
expect(screen.getByText('Failed to load logs sample for this query')).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 { 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 { TimeZone } from '@grafana/schema';
|
||||
import { Collapse } from '@grafana/ui';
|
||||
import { TimeZone, DataQuery } from '@grafana/schema';
|
||||
import { Button, Collapse, useStyles2 } from '@grafana/ui';
|
||||
import { dataFrameToLogsModel } from 'app/core/logsModel';
|
||||
import store from 'app/core/store';
|
||||
|
||||
@ -16,13 +26,16 @@ type Props = {
|
||||
queryResponse: DataQueryResponse | undefined;
|
||||
enabled: boolean;
|
||||
timeZone: TimeZone;
|
||||
queries: DataQuery[];
|
||||
datasourceInstance: DataSourceApi | null | undefined;
|
||||
splitOpen: SplitOpen;
|
||||
setLogsSampleEnabled: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
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) => {
|
||||
setLogsSampleEnabled(isOpen);
|
||||
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;
|
||||
|
||||
if (queryResponse === undefined) {
|
||||
@ -46,16 +85,19 @@ export function LogsSamplePanel(props: Props) {
|
||||
} else {
|
||||
const logs = dataFrameToLogsModel(queryResponse.data);
|
||||
LogsSamplePanelContent = (
|
||||
<LogRows
|
||||
logRows={logs.rows}
|
||||
dedupStrategy={LogsDedupStrategy.none}
|
||||
showLabels={store.getBool(SETTINGS_KEYS.showLabels, false)}
|
||||
showTime={store.getBool(SETTINGS_KEYS.showTime, true)}
|
||||
wrapLogMessage={store.getBool(SETTINGS_KEYS.wrapLogMessage, true)}
|
||||
prettifyLogMessage={store.getBool(SETTINGS_KEYS.prettifyLogMessage, false)}
|
||||
timeZone={timeZone}
|
||||
enableLogDetails={true}
|
||||
/>
|
||||
<>
|
||||
<OpenInSplitViewButton />
|
||||
<LogRows
|
||||
logRows={logs.rows}
|
||||
dedupStrategy={LogsDedupStrategy.none}
|
||||
showLabels={store.getBool(SETTINGS_KEYS.showLabels, false)}
|
||||
showTime={store.getBool(SETTINGS_KEYS.showTime, true)}
|
||||
wrapLogMessage={store.getBool(SETTINGS_KEYS.wrapLogMessage, true)}
|
||||
prettifyLogMessage={store.getBool(SETTINGS_KEYS.prettifyLogMessage, false)}
|
||||
timeZone={timeZone}
|
||||
enableLogDetails={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,3 +107,11 @@ export function LogsSamplePanel(props: Props) {
|
||||
</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
|
||||
.fn()
|
||||
.mockImplementation(() => [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]),
|
||||
getSupplementaryQuery: jest.fn(),
|
||||
meta: {
|
||||
id: 'something',
|
||||
},
|
||||
|
@ -413,6 +413,7 @@ describe('reducer', () => {
|
||||
SupplementaryQueryType.LogsVolume,
|
||||
SupplementaryQueryType.LogsSample,
|
||||
],
|
||||
getSupplementaryQuery: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
FieldType,
|
||||
MutableDataFrame,
|
||||
RawTimeRange,
|
||||
SupplementaryQueryType,
|
||||
TimeRange,
|
||||
toUtc,
|
||||
} from '@grafana/data';
|
||||
@ -924,6 +925,56 @@ describe('ElasticDatasource', () => {
|
||||
|
||||
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', () => {
|
||||
|
@ -600,58 +600,80 @@ export class ElasticDatasource
|
||||
return [SupplementaryQueryType.LogsVolume];
|
||||
}
|
||||
|
||||
getLogsVolumeDataProvider(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> | undefined {
|
||||
const isLogsVolumeAvailable = request.targets.some((target) => {
|
||||
return target.metrics?.length === 1 && target.metrics[0].type === 'logs';
|
||||
});
|
||||
if (!isLogsVolumeAvailable) {
|
||||
getSupplementaryQuery(type: SupplementaryQueryType, query: ElasticsearchQuery): ElasticsearchQuery | undefined {
|
||||
if (!this.getSupportedSupplementaryQueryTypes().includes(type)) {
|
||||
return undefined;
|
||||
}
|
||||
const logsVolumeRequest = cloneDeep(request);
|
||||
logsVolumeRequest.targets = logsVolumeRequest.targets.map((target) => {
|
||||
const bucketAggs: BucketAggregation[] = [];
|
||||
const timeField = this.timeField ?? '@timestamp';
|
||||
|
||||
if (this.logLevelField) {
|
||||
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;
|
||||
}
|
||||
const bucketAggs: BucketAggregation[] = [];
|
||||
const timeField = this.timeField ?? '@timestamp';
|
||||
|
||||
if (this.logLevelField) {
|
||||
bucketAggs.push({
|
||||
id: '2',
|
||||
type: 'terms',
|
||||
settings: {
|
||||
min_doc_count: '0',
|
||||
size: '0',
|
||||
order: 'desc',
|
||||
orderBy: '_count',
|
||||
missing: LogLevel.unknown,
|
||||
},
|
||||
field: this.logLevelField,
|
||||
});
|
||||
}
|
||||
bucketAggs.push({
|
||||
id: '2',
|
||||
type: 'terms',
|
||||
id: '3',
|
||||
type: 'date_histogram',
|
||||
settings: {
|
||||
interval: 'auto',
|
||||
min_doc_count: '0',
|
||||
size: '0',
|
||||
order: 'desc',
|
||||
orderBy: '_count',
|
||||
missing: LogLevel.unknown,
|
||||
trimEdges: '0',
|
||||
},
|
||||
field: this.logLevelField,
|
||||
field: timeField,
|
||||
});
|
||||
|
||||
return {
|
||||
refId: `${REF_ID_STARTER_LOG_VOLUME}${query.refId}`,
|
||||
query: query.query,
|
||||
metrics: [{ type: 'count', id: '1' }],
|
||||
timeField,
|
||||
bucketAggs,
|
||||
};
|
||||
|
||||
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,
|
||||
targets: request.targets,
|
||||
extractLevel: (dataFrame) => getLogLevelFromKey(dataFrame.name || ''),
|
||||
}
|
||||
bucketAggs.push({
|
||||
id: '3',
|
||||
type: 'date_histogram',
|
||||
settings: {
|
||||
interval: 'auto',
|
||||
min_doc_count: '0',
|
||||
trimEdges: '0',
|
||||
},
|
||||
field: timeField,
|
||||
});
|
||||
|
||||
const logsVolumeQuery: ElasticsearchQuery = {
|
||||
refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`,
|
||||
query: target.query,
|
||||
metrics: [{ type: 'count', id: '1' }],
|
||||
timeField,
|
||||
bucketAggs,
|
||||
};
|
||||
return logsVolumeQuery;
|
||||
});
|
||||
|
||||
return queryLogsVolume(this, logsVolumeRequest, {
|
||||
range: request.range,
|
||||
targets: request.targets,
|
||||
extractLevel: (dataFrame) => getLogLevelFromKey(dataFrame.name || ''),
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
query(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> {
|
||||
|
@ -894,7 +894,7 @@ describe('LokiDatasource', () => {
|
||||
|
||||
it('creates provider for logs query', () => {
|
||||
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();
|
||||
@ -911,8 +911,8 @@ describe('LokiDatasource', () => {
|
||||
it('creates provider if at least one query is a logs query', () => {
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [
|
||||
{ expr: 'rate({label=value}[1m])', refId: 'A' },
|
||||
{ expr: '{label=value}', refId: 'B' },
|
||||
{ expr: 'rate({label=value}[1m])', queryType: LokiQueryType.Range, refId: 'A' },
|
||||
{ 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', () => {
|
||||
let ds: LokiDatasource;
|
||||
beforeEach(() => {
|
||||
|
@ -161,62 +161,80 @@ export class LokiDatasource
|
||||
return [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample];
|
||||
}
|
||||
|
||||
getLogsVolumeDataProvider(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> | undefined {
|
||||
const isQuerySuitable = (query: LokiQuery) => {
|
||||
const normalized = getNormalizedLokiQuery(query);
|
||||
const { expr } = normalized;
|
||||
// it has to be a logs-producing range-query
|
||||
return expr && isLogsQuery(expr) && normalized.queryType === LokiQueryType.Range;
|
||||
};
|
||||
|
||||
const isLogsVolumeAvailable = request.targets.some(isQuerySuitable);
|
||||
|
||||
if (!isLogsVolumeAvailable) {
|
||||
getSupplementaryQuery(type: SupplementaryQueryType, query: LokiQuery): LokiQuery | undefined {
|
||||
if (!this.getSupportedSupplementaryQueryTypes().includes(type)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const logsVolumeRequest = cloneDeep(request);
|
||||
logsVolumeRequest.targets = logsVolumeRequest.targets.filter(isQuerySuitable).map((target) => {
|
||||
const query = removeCommentsFromQuery(target.expr);
|
||||
return {
|
||||
...target,
|
||||
refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`,
|
||||
instant: false,
|
||||
volumeQuery: true,
|
||||
expr: `sum by (level) (count_over_time(${query}[$__interval]))`,
|
||||
};
|
||||
});
|
||||
const normalizedQuery = getNormalizedLokiQuery(query);
|
||||
const expr = removeCommentsFromQuery(normalizedQuery.expr);
|
||||
let isQuerySuitable = false;
|
||||
|
||||
return queryLogsVolume(this, logsVolumeRequest, {
|
||||
extractLevel,
|
||||
range: request.range,
|
||||
targets: request.targets,
|
||||
});
|
||||
switch (type) {
|
||||
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 {
|
||||
...normalizedQuery,
|
||||
refId: `${REF_ID_STARTER_LOG_VOLUME}${normalizedQuery.refId}`,
|
||||
instant: false,
|
||||
volumeQuery: true,
|
||||
expr: `sum by (level) (count_over_time(${expr}[$__interval]))`,
|
||||
};
|
||||
|
||||
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,
|
||||
range: request.range,
|
||||
targets: request.targets,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getLogsSampleDataProvider(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> | undefined {
|
||||
const isQuerySuitable = (query: LokiQuery) => {
|
||||
return query.expr && !isLogsQuery(query.expr);
|
||||
};
|
||||
const logsSampleRequest = cloneDeep(request);
|
||||
const targets = logsSampleRequest.targets
|
||||
.map((query) => this.getSupplementaryQuery(SupplementaryQueryType.LogsSample, query))
|
||||
.filter((query): query is LokiQuery => !!query);
|
||||
|
||||
const isLogsSampleAvailable = request.targets.some(isQuerySuitable);
|
||||
|
||||
if (!isLogsSampleAvailable) {
|
||||
if (!targets.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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);
|
||||
return queryLogsSample(this, { ...logsSampleRequest, targets });
|
||||
}
|
||||
|
||||
query(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
|
||||
|
Loading…
Reference in New Issue
Block a user