The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/explore/utils/supplementaryQueries.test.ts

361 lines
12 KiB

import { flatten } from 'lodash';
import { from, Observable } from 'rxjs';
import {
DataFrame,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceWithSupplementaryQueriesSupport,
FieldType,
LoadingState,
LogLevel,
LogsVolumeType,
MutableDataFrame,
SupplementaryQueryType,
SupplementaryQueryOptions,
toDataFrame,
} from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query';
import { ExplorePanelData } from '../../../types';
import { mockExplorePanelData } from '../__mocks__/data';
import { getSupplementaryQueryProvider } from './supplementaryQueries';
class MockDataSourceWithSupplementaryQuerySupport
extends MockDataSourceApi
implements DataSourceWithSupplementaryQueriesSupport<DataQuery>
{
private supplementaryQueriesResults: Record<SupplementaryQueryType, DataFrame[] | undefined> = {
[SupplementaryQueryType.LogsVolume]: undefined,
[SupplementaryQueryType.LogsSample]: undefined,
};
withSupplementaryQuerySupport(type: SupplementaryQueryType, data: DataFrame[]) {
this.supplementaryQueriesResults[type] = data;
return this;
}
getDataProvider(
type: SupplementaryQueryType,
request: DataQueryRequest<DataQuery>
): Observable<DataQueryResponse> | undefined {
const data = this.supplementaryQueriesResults[type];
if (data) {
return from([
{ state: LoadingState.Loading, data: [] },
{ state: LoadingState.Done, data },
]);
}
return undefined;
}
getSupplementaryQuery(options: SupplementaryQueryOptions, query: DataQuery): DataQuery | undefined {
return query;
}
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] {
return Object.values(SupplementaryQueryType).filter((type) => this.supplementaryQueriesResults[type]);
}
}
const createSupplementaryQueryResponse = (type: SupplementaryQueryType, id: string) => {
return [
toDataFrame({
refId: `1-${type}-${id}`,
fields: [{ name: 'value', type: FieldType.string, values: [1] }],
meta: {
custom: {
logsVolumeType: LogsVolumeType.FullRange,
},
},
}),
toDataFrame({
refId: `2-${type}-${id}`,
fields: [{ name: 'value', type: FieldType.string, values: [2] }],
meta: {
custom: {
logsVolumeType: LogsVolumeType.FullRange,
},
},
}),
];
};
const mockRow = (refId: string) => {
return {
rowIndex: 0,
entryFieldIndex: 0,
dataFrame: new MutableDataFrame({ refId, fields: [{ name: 'A', values: [] }] }),
entry: '',
hasAnsi: false,
hasUnescapedContent: false,
labels: {},
logLevel: LogLevel.info,
raw: '',
timeEpochMs: 0,
timeEpochNs: '0',
timeFromNow: '',
timeLocal: '',
timeUtc: '',
uid: '1',
};
};
const mockExploreDataWithLogs = () =>
mockExplorePanelData({
logsResult: {
rows: [mockRow('0'), mockRow('1')],
visibleRange: { from: 0, to: 1 },
bucketSize: 1000,
},
});
const datasources: DataSourceApi[] = [
new MockDataSourceWithSupplementaryQuerySupport('logs-volume-a').withSupplementaryQuerySupport(
SupplementaryQueryType.LogsVolume,
createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-a')
),
new MockDataSourceWithSupplementaryQuerySupport('logs-volume-b').withSupplementaryQuerySupport(
SupplementaryQueryType.LogsVolume,
createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-b')
),
new MockDataSourceWithSupplementaryQuerySupport('logs-sample-a').withSupplementaryQuerySupport(
SupplementaryQueryType.LogsSample,
createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-a')
),
new MockDataSourceWithSupplementaryQuerySupport('logs-sample-b').withSupplementaryQuerySupport(
SupplementaryQueryType.LogsSample,
createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-b')
),
new MockDataSourceApi('no-data-providers'),
new MockDataSourceApi('no-data-providers-2'),
];
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => {
return {
get: async ({ uid }: { uid: string }) => datasources.find((ds) => ds.name === uid) || undefined,
};
},
}));
const setup = async (targetSources: string[], type: SupplementaryQueryType) => {
const requestMock = new MockDataQueryRequest({
targets: targetSources.map((source, i) => new MockQuery(`${i}`, 'a', { uid: source })),
});
const explorePanelDataMock: Observable<ExplorePanelData> = mockExploreDataWithLogs();
const datasources = await Promise.all(
targetSources.map(async (source, i) => {
const datasource = await getDataSourceSrv().get({ uid: source });
return {
datasource,
targets: [new MockQuery(`${i}`, 'a', { uid: source })],
};
})
);
return getSupplementaryQueryProvider(datasources, type, requestMock, explorePanelDataMock);
};
const assertDataFrom = (type: SupplementaryQueryType, ...datasources: string[]) => {
return flatten(
datasources.map((name: string) => {
return [{ refId: `1-${type}-${name}` }, { refId: `2-${type}-${name}` }];
})
);
};
const assertDataFromLogsResults = () => {
return [{ meta: { custom: { logsVolumeType: LogsVolumeType.Limited } } }];
};
describe('SupplementaryQueries utils', function () {
describe('Non-mixed data source', function () {
it('Returns result from the provider', async () => {
const testProvider = await setup(['logs-volume-a'], SupplementaryQueryType.LogsVolume);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{ data: [], state: LoadingState.Loading },
{
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
state: LoadingState.Done,
},
]);
});
});
it('Uses fallback for logs volume', async () => {
const testProvider = await setup(['no-data-providers'], SupplementaryQueryType.LogsVolume);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{
data: assertDataFromLogsResults(),
state: LoadingState.Done,
},
]);
});
});
it('Returns undefined for logs sample', async () => {
const testProvider = await setup(['no-data-providers'], SupplementaryQueryType.LogsSample);
await expect(testProvider).toBe(undefined);
});
it('Creates single fallback result', async () => {
const testProvider = await setup(['no-data-providers', 'no-data-providers-2'], SupplementaryQueryType.LogsVolume);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{
data: assertDataFromLogsResults(),
state: LoadingState.Done,
},
{
data: [...assertDataFromLogsResults(), ...assertDataFromLogsResults()],
state: LoadingState.Done,
},
]);
});
});
});
describe('Mixed data source', function () {
describe('Logs volume', function () {
describe('All data sources support full range logs volume', function () {
it('Merges all data frames into a single response', async () => {
const testProvider = await setup(['logs-volume-a', 'logs-volume-b'], SupplementaryQueryType.LogsVolume);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{ data: [], state: LoadingState.Loading },
{
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
state: LoadingState.Done,
},
{
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a', 'logs-volume-b'),
state: LoadingState.Done,
},
]);
});
});
});
describe('All data sources do not support full range logs volume', function () {
it('Creates single fallback result', async () => {
const testProvider = await setup(
['no-data-providers', 'no-data-providers-2'],
SupplementaryQueryType.LogsVolume
);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{
data: assertDataFromLogsResults(),
state: LoadingState.Done,
},
{
data: [...assertDataFromLogsResults(), ...assertDataFromLogsResults()],
state: LoadingState.Done,
},
]);
});
});
});
describe('Some data sources support full range logs volume, while others do not', function () {
it('Creates merged result containing full range and limited logs volume', async () => {
const testProvider = await setup(
['logs-volume-a', 'no-data-providers', 'logs-volume-b', 'no-data-providers-2'],
SupplementaryQueryType.LogsVolume
);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{
data: [],
state: LoadingState.Loading,
},
{
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
state: LoadingState.Done,
},
{
data: [
...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
...assertDataFromLogsResults(),
],
state: LoadingState.Done,
},
{
data: [
...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
...assertDataFromLogsResults(),
...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-b'),
],
state: LoadingState.Done,
},
]);
});
});
});
});
describe('Logs sample', function () {
describe('All data sources support logs sample', function () {
it('Merges all responses into single result', async () => {
const testProvider = await setup(['logs-sample-a', 'logs-sample-b'], SupplementaryQueryType.LogsSample);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{ data: [], state: LoadingState.Loading },
{
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'),
state: LoadingState.Done,
},
{
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'),
state: LoadingState.Done,
},
]);
});
});
});
describe('All data sources do not support full range logs volume', function () {
it('Does not provide fallback result', async () => {
const testProvider = await setup(
['no-data-providers', 'no-data-providers-2'],
SupplementaryQueryType.LogsSample
);
await expect(testProvider).toBeUndefined();
});
});
describe('Some data sources support full range logs volume, while others do not', function () {
it('Returns results only for data sources supporting logs sample', async () => {
const testProvider = await setup(
['logs-sample-a', 'no-data-providers', 'logs-sample-b', 'no-data-providers-2'],
SupplementaryQueryType.LogsSample
);
await expect(testProvider).toEmitValuesWith((received) => {
expect(received).toMatchObject([
{ data: [], state: LoadingState.Loading },
{
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'),
state: LoadingState.Done,
},
{
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'),
state: LoadingState.Done,
},
]);
});
});
});
});
});
});