mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
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.
491 lines
16 KiB
491 lines
16 KiB
![]()
2 years ago
|
import moment from 'moment';
|
||
|
|
||
|
import { DataFrame, DataQueryRequest, DateTime, dateTime, TimeRange } from '@grafana/data/src';
|
||
|
|
||
|
import { QueryEditorMode } from '../querybuilder/shared/types';
|
||
|
import { PromQuery } from '../types';
|
||
|
|
||
|
import { getTargSig, QueryCache } from './QueryCache';
|
||
|
import { IncrementalStorageDataFrameScenarios } from './QueryCacheTestData';
|
||
|
|
||
|
const mockRequest = (request?: Partial<DataQueryRequest<PromQuery>>): DataQueryRequest<PromQuery> => {
|
||
|
// Histogram
|
||
|
const defaultRequest: DataQueryRequest<PromQuery> = {
|
||
|
app: 'undefined',
|
||
|
requestId: '',
|
||
|
timezone: '',
|
||
|
range: {
|
||
|
from: moment('2023-01-30T19:33:01.332Z') as DateTime,
|
||
|
to: moment('2023-01-30T20:33:01.332Z') as DateTime,
|
||
|
raw: { from: 'now-1h', to: 'now' },
|
||
|
},
|
||
|
interval: '15s',
|
||
|
intervalMs: 15000,
|
||
|
targets: [
|
||
|
{
|
||
|
datasource: { type: 'prometheus', uid: 'OPQv8Kc4z' },
|
||
|
editorMode: QueryEditorMode.Code,
|
||
|
exemplar: false,
|
||
|
expr: 'sum by(le) (rate(cortex_request_duration_seconds_bucket{cluster="dev-us-central-0", job="cortex-dev-01/cortex-gw-internal", namespace="cortex-dev-01"}[$__rate_interval]))',
|
||
|
format: 'heatmap',
|
||
|
legendFormat: '{{le}}',
|
||
|
range: true,
|
||
|
refId: 'A',
|
||
|
utcOffsetSec: -21600,
|
||
|
},
|
||
|
],
|
||
|
maxDataPoints: 871,
|
||
|
scopedVars: {
|
||
|
__interval: { text: '15s', value: '15s' },
|
||
|
__interval_ms: { text: '15000', value: 15000 },
|
||
|
},
|
||
|
startTime: 1675110781332,
|
||
|
rangeRaw: { from: 'now-1h', to: 'now' },
|
||
|
};
|
||
|
return {
|
||
|
...defaultRequest,
|
||
|
...request,
|
||
|
};
|
||
|
};
|
||
|
|
||
|
describe('QueryCache', function () {
|
||
|
it('instantiates', () => {
|
||
|
const storage = new QueryCache();
|
||
|
expect(storage).toBeInstanceOf(QueryCache);
|
||
|
});
|
||
|
|
||
|
it('will not modify or crash with empty response', () => {
|
||
|
const storage = new QueryCache();
|
||
|
const firstFrames: DataFrame[] = [];
|
||
|
const secondFrames: DataFrame[] = [];
|
||
|
|
||
|
const cache = new Map<string, string>();
|
||
|
|
||
|
// start time of scenario
|
||
|
const firstFrom = dateTime(new Date(1675262550000));
|
||
|
// End time of scenario
|
||
|
const firstTo = dateTime(new Date(1675262550000)).add(6, 'hours');
|
||
|
|
||
|
const firstRange: TimeRange = {
|
||
|
from: firstFrom,
|
||
|
to: firstTo,
|
||
|
raw: {
|
||
|
from: 'now-6h',
|
||
|
to: 'now',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// Same query 2 minutes later
|
||
|
const numberOfSamplesLater = 4;
|
||
|
const interval = 30000;
|
||
|
|
||
|
const secondFrom = dateTime(new Date(1675262550000 + interval * numberOfSamplesLater));
|
||
|
const secondTo = dateTime(new Date(1675262550000 + interval * numberOfSamplesLater)).add(6, 'hours');
|
||
|
|
||
|
const secondRange: TimeRange = {
|
||
|
from: secondFrom,
|
||
|
to: secondTo,
|
||
|
raw: {
|
||
|
from: 'now-6h',
|
||
|
to: 'now',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
const targetSignature = `'1=1'|${interval}|${JSON.stringify(secondRange.raw)}`;
|
||
|
const dashboardId = `dashid`;
|
||
|
const panelId = 2;
|
||
|
const targetIdentity = `${dashboardId}|${panelId}|A`;
|
||
|
|
||
|
cache.set(targetIdentity, targetSignature);
|
||
|
|
||
|
const firstStoredFrames = storage.procFrames(
|
||
|
mockRequest({
|
||
|
range: firstRange,
|
||
|
dashboardUID: dashboardId,
|
||
|
panelId: panelId,
|
||
|
}),
|
||
|
{
|
||
|
requests: [], // unused
|
||
|
targSigs: cache,
|
||
|
shouldCache: true,
|
||
|
},
|
||
|
firstFrames
|
||
|
);
|
||
|
|
||
|
const cached = storage.cache.get(targetIdentity);
|
||
|
|
||
|
expect(cached?.frames[0].fields[0].values.length).toEqual(firstFrames[0]?.fields[0]?.values?.length);
|
||
|
expect(firstStoredFrames[0]?.fields[0].values.length).toEqual(firstFrames[0]?.fields[0]?.values?.length);
|
||
|
|
||
|
// Should return the request frames unaltered
|
||
|
expect(firstStoredFrames).toEqual(firstFrames);
|
||
|
|
||
|
const secondRequest = mockRequest({
|
||
|
range: secondRange,
|
||
|
dashboardUID: dashboardId,
|
||
|
panelId: panelId,
|
||
|
});
|
||
|
|
||
|
const secondStoredFrames = storage.procFrames(
|
||
|
secondRequest,
|
||
|
{
|
||
|
requests: [], // unused
|
||
|
targSigs: cache,
|
||
|
shouldCache: true,
|
||
|
},
|
||
|
secondFrames
|
||
|
);
|
||
|
|
||
|
const storageLengthAfterSubsequentQuery = storage.cache.get(targetIdentity);
|
||
|
|
||
|
expect(secondStoredFrames).toEqual([]);
|
||
|
|
||
|
storageLengthAfterSubsequentQuery?.frames.forEach((dataFrame, index) => {
|
||
|
const secondFramesLength = secondFrames[index].fields[0].values.length;
|
||
|
const firstFramesLength = firstFrames[index].fields[0].values.length;
|
||
|
|
||
|
const cacheLength = dataFrame.fields[0].values.length;
|
||
|
|
||
|
// Cache can contain more, but never less
|
||
|
expect(cacheLength).toBeGreaterThanOrEqual(secondFramesLength + firstFramesLength - (20 + numberOfSamplesLater));
|
||
|
|
||
|
// Fewer results are sent in incremental result
|
||
|
expect(firstFramesLength).toBeGreaterThan(secondFramesLength);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('Merges incremental queries in storage', () => {
|
||
|
const scenarios = [
|
||
|
IncrementalStorageDataFrameScenarios.histogram.getSeriesWithGapAtEnd(),
|
||
|
IncrementalStorageDataFrameScenarios.histogram.getSeriesWithGapInMiddle(),
|
||
|
IncrementalStorageDataFrameScenarios.histogram.getSeriesWithGapAtStart(),
|
||
|
];
|
||
|
scenarios.forEach((scenario, index) => {
|
||
|
const storage = new QueryCache();
|
||
|
const firstFrames = scenario.first.dataFrames as unknown as DataFrame[];
|
||
|
const secondFrames = scenario.second.dataFrames as unknown as DataFrame[];
|
||
|
|
||
|
const targetSignatures = new Map<string, string>();
|
||
|
|
||
|
// start time of scenario
|
||
|
const firstFrom = dateTime(new Date(1675262550000));
|
||
|
// End time of scenario
|
||
|
const firstTo = dateTime(new Date(1675262550000)).add(6, 'hours');
|
||
|
|
||
|
const firstRange: TimeRange = {
|
||
|
from: firstFrom,
|
||
|
to: firstTo,
|
||
|
raw: {
|
||
|
from: 'now-6h',
|
||
|
to: 'now',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// Same query 2 minutes later
|
||
|
const numberOfSamplesLater = 4;
|
||
|
const interval = 30000;
|
||
|
|
||
|
const secondFrom = dateTime(new Date(1675262550000 + interval * numberOfSamplesLater));
|
||
|
const secondTo = dateTime(new Date(1675262550000 + interval * numberOfSamplesLater)).add(6, 'hours');
|
||
|
|
||
|
const secondRange: TimeRange = {
|
||
|
from: secondFrom,
|
||
|
to: secondTo,
|
||
|
raw: {
|
||
|
from: 'now-6h',
|
||
|
to: 'now',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
const dashboardId = `dashid--${index}`;
|
||
|
const panelId = 2 + index;
|
||
|
|
||
|
// This can't change
|
||
|
const targetIdentity = `${dashboardId}|${panelId}|A`;
|
||
|
|
||
|
const request = mockRequest({
|
||
|
range: firstRange,
|
||
|
dashboardUID: dashboardId,
|
||
|
panelId: panelId,
|
||
|
});
|
||
|
|
||
|
// But the signature can, and we should clean up any non-matching signatures
|
||
|
const targetSignature = getTargSig(request.targets[0].expr, request, request.targets[0]);
|
||
|
|
||
|
targetSignatures.set(targetIdentity, targetSignature);
|
||
|
|
||
|
const firstStoredFrames = storage.procFrames(
|
||
|
request,
|
||
|
{
|
||
|
requests: [], // unused
|
||
|
targSigs: targetSignatures,
|
||
|
shouldCache: true,
|
||
|
},
|
||
|
firstFrames
|
||
|
);
|
||
|
|
||
|
const cached = storage.cache.get(targetIdentity);
|
||
|
|
||
|
// I would expect that the number of values received from the API should be the same as the cached values?
|
||
|
expect(cached?.frames[0].fields[0].values.length).toEqual(firstFrames[0].fields[0].values.length);
|
||
|
|
||
|
// Should return the request frames unaltered
|
||
|
expect(firstStoredFrames).toEqual(firstFrames);
|
||
|
|
||
|
const secondRequest = mockRequest({
|
||
|
range: secondRange,
|
||
|
dashboardUID: dashboardId,
|
||
|
panelId: panelId,
|
||
|
});
|
||
|
|
||
|
const secondStoredFrames = storage.procFrames(
|
||
|
secondRequest,
|
||
|
{
|
||
|
requests: [], // unused
|
||
|
targSigs: targetSignatures,
|
||
|
shouldCache: true,
|
||
|
},
|
||
|
secondFrames
|
||
|
);
|
||
|
|
||
|
const storageLengthAfterSubsequentQuery = storage.cache.get(targetIdentity);
|
||
|
|
||
|
storageLengthAfterSubsequentQuery?.frames.forEach((dataFrame, index) => {
|
||
|
const secondFramesLength = secondFrames[index].fields[0].values.length;
|
||
|
const firstFramesLength = firstFrames[index].fields[0].values.length;
|
||
|
|
||
|
const cacheLength = dataFrame.fields[0].values.length;
|
||
|
|
||
|
// Cache can contain more, but never less
|
||
|
expect(cacheLength).toBeGreaterThanOrEqual(
|
||
|
secondFramesLength + firstFramesLength - (20 + numberOfSamplesLater)
|
||
|
);
|
||
|
|
||
|
// Fewer results are sent in incremental result
|
||
|
expect(firstFramesLength).toBeGreaterThan(secondFramesLength);
|
||
|
});
|
||
|
|
||
|
// All of the new values should be the ones that were stored, this is overkill
|
||
|
secondFrames.forEach((frame, frameIdx) => {
|
||
|
frame.fields.forEach((field, fieldIdx) => {
|
||
|
secondFrames[frameIdx].fields[fieldIdx].values.toArray().forEach((value) => {
|
||
|
expect(secondStoredFrames[frameIdx].fields[fieldIdx].values).toContain(value);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
const interpolateString = (s: string) => {
|
||
|
return s;
|
||
|
};
|
||
|
const secondRequestModified = {
|
||
|
...secondRequest,
|
||
|
range: {
|
||
|
...secondRequest.range,
|
||
|
to: dateTime(secondRequest.range.to.valueOf() + 30000),
|
||
|
},
|
||
|
};
|
||
|
const cacheRequest = storage.requestInfo(secondRequestModified, interpolateString);
|
||
|
expect(cacheRequest.requests[0].targets).toEqual(secondRequestModified.targets);
|
||
|
expect(cacheRequest.requests[0].range.to).toEqual(secondRequestModified.range.to);
|
||
|
expect(cacheRequest.requests[0].range.raw).toEqual(secondRequestModified.range.raw);
|
||
|
expect(cacheRequest.requests[0].range.from.valueOf() - 21000000).toEqual(
|
||
|
secondRequestModified.range.from.valueOf()
|
||
|
);
|
||
|
expect(cacheRequest.shouldCache).toBe(true);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('Will evict old dataframes, and use stored data when user shortens query window', () => {
|
||
|
const storage = new QueryCache();
|
||
|
|
||
|
// Initial request with all data for time range
|
||
|
const firstFrames = IncrementalStorageDataFrameScenarios.histogram.evictionRequests.first
|
||
|
.dataFrames as unknown as DataFrame[];
|
||
|
|
||
|
// Shortened request 30s later
|
||
|
const secondFrames = IncrementalStorageDataFrameScenarios.histogram.evictionRequests.second
|
||
|
.dataFrames as unknown as DataFrame[];
|
||
|
|
||
|
// Now the user waits a minute and changes the query duration to just the last 5 minutes, luckily the interval hasn't changed, so we can still use the data in storage except for the latest minute
|
||
|
const thirdFrames = IncrementalStorageDataFrameScenarios.histogram.evictionRequests.second
|
||
|
.dataFrames as unknown as DataFrame[];
|
||
|
|
||
|
const cache = new Map<string, string>();
|
||
|
const interval = 15000;
|
||
|
|
||
|
// start time of scenario
|
||
|
const firstFrom = dateTime(new Date(1675107180000));
|
||
|
const firstTo = dateTime(new Date(1675107180000)).add(1, 'hours');
|
||
|
const firstRange: TimeRange = {
|
||
|
from: firstFrom,
|
||
|
to: firstTo,
|
||
|
raw: {
|
||
|
from: 'now-1h',
|
||
|
to: 'now',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// 30 seconds later
|
||
|
const secondNumberOfSamplesLater = 2;
|
||
|
const secondFrom = dateTime(new Date(1675107180000 + interval * secondNumberOfSamplesLater));
|
||
|
const secondTo = dateTime(new Date(1675107180000 + interval * secondNumberOfSamplesLater)).add(1, 'hours');
|
||
|
const secondRange: TimeRange = {
|
||
|
from: secondFrom,
|
||
|
to: secondTo,
|
||
|
raw: {
|
||
|
from: 'now-1h',
|
||
|
to: 'now',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// 1 minute + 30 seconds later, but 5 minute viewing window
|
||
|
const thirdNumberOfSamplesLater = 6;
|
||
|
const thirdFrom = dateTime(new Date(1675107180000 + interval * thirdNumberOfSamplesLater));
|
||
|
const thirdTo = dateTime(new Date(1675107180000 + interval * thirdNumberOfSamplesLater)).add(5, 'minutes');
|
||
|
const thirdRange: TimeRange = {
|
||
|
from: thirdFrom,
|
||
|
to: thirdTo,
|
||
|
raw: {
|
||
|
from: 'now-5m',
|
||
|
to: 'now',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// Signifier definition
|
||
|
|
||
|
const dashboardId = `dashid`;
|
||
|
const panelId = 200;
|
||
|
|
||
|
const targetIdentity = `${dashboardId}|${panelId}|A`;
|
||
|
|
||
|
const request = mockRequest({
|
||
|
range: firstRange,
|
||
|
dashboardUID: dashboardId,
|
||
|
panelId: panelId,
|
||
|
});
|
||
|
|
||
|
const requestInfo = {
|
||
|
requests: [], // unused
|
||
|
targSigs: cache,
|
||
|
shouldCache: true,
|
||
|
};
|
||
|
const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`;
|
||
|
cache.set(targetIdentity, targetSignature);
|
||
|
|
||
|
const firstQueryResult = storage.procFrames(request, requestInfo, firstFrames);
|
||
|
|
||
|
const firstMergedLength = firstQueryResult[0].fields[0].values.length;
|
||
|
|
||
|
const secondQueryResult = storage.procFrames(
|
||
|
mockRequest({
|
||
|
range: secondRange,
|
||
|
dashboardUID: dashboardId,
|
||
|
panelId: panelId,
|
||
|
}),
|
||
|
{
|
||
|
requests: [], // unused
|
||
|
targSigs: cache,
|
||
|
shouldCache: true,
|
||
|
},
|
||
|
secondFrames
|
||
|
);
|
||
|
|
||
|
const secondMergedLength = secondQueryResult[0].fields[0].values.length;
|
||
|
|
||
|
// Since the step is 15s, and the request was 30 seconds later, we should have 2 extra frames, but we should evict the first two, so we should get the same length
|
||
|
expect(firstMergedLength).toEqual(secondMergedLength);
|
||
|
expect(firstQueryResult[0].fields[0].values.toArray()[2]).toEqual(
|
||
|
secondQueryResult[0].fields[0].values.toArray()[0]
|
||
|
);
|
||
|
expect(firstQueryResult[0].fields[0].values.toArray()[0] + 30000).toEqual(
|
||
|
secondQueryResult[0].fields[0].values.toArray()[0]
|
||
|
);
|
||
|
|
||
|
cache.set(targetIdentity, `'1=1'|${interval}|${JSON.stringify(thirdRange.raw)}`);
|
||
|
|
||
|
storage.procFrames(
|
||
|
mockRequest({
|
||
|
range: thirdRange,
|
||
|
dashboardUID: dashboardId,
|
||
|
panelId: panelId,
|
||
|
}),
|
||
|
{
|
||
|
requests: [], // unused
|
||
|
targSigs: cache,
|
||
|
shouldCache: true,
|
||
|
},
|
||
|
thirdFrames
|
||
|
);
|
||
|
|
||
|
const cachedAfterThird = storage.cache.get(targetIdentity);
|
||
|
const storageLengthAfterThirdQuery = cachedAfterThird?.frames[0].fields[0].values.toArray().length;
|
||
|
expect(storageLengthAfterThirdQuery).toEqual(20);
|
||
|
});
|
||
|
|
||
|
it('Will build signature using target overrides', () => {
|
||
|
const targetInterval = '30s';
|
||
|
const requestInterval = '15s';
|
||
|
|
||
|
const target: PromQuery = {
|
||
|
datasource: { type: 'prometheus', uid: 'OPQv8Kc4z' },
|
||
|
editorMode: QueryEditorMode.Code,
|
||
|
exemplar: false,
|
||
|
expr: 'sum by(le) (rate(cortex_request_duration_seconds_bucket{cluster="dev-us-central-0", job="cortex-dev-01/cortex-gw-internal", namespace="cortex-dev-01"}[$__rate_interval]))',
|
||
|
format: 'heatmap',
|
||
|
interval: targetInterval,
|
||
|
legendFormat: '{{le}}',
|
||
|
range: true,
|
||
|
refId: 'A',
|
||
|
utcOffsetSec: -21600,
|
||
|
};
|
||
|
|
||
|
const request = mockRequest({
|
||
|
interval: requestInterval,
|
||
|
targets: [target],
|
||
|
});
|
||
|
const targSig = getTargSig('__EXPR__', request, target);
|
||
|
expect(targSig).toContain(targetInterval);
|
||
|
expect(targSig.includes(requestInterval)).toBeFalsy();
|
||
|
});
|
||
|
|
||
|
it('will not modify request with absolute duration', () => {
|
||
|
const request = mockRequest({
|
||
|
range: {
|
||
|
from: moment('2023-01-30T19:33:01.332Z') as DateTime,
|
||
|
to: moment('2023-01-30T20:33:01.332Z') as DateTime,
|
||
|
raw: { from: '2023-01-30T19:33:01.332Z', to: '2023-01-30T20:33:01.332Z' },
|
||
|
},
|
||
|
rangeRaw: { from: '2023-01-30T19:33:01.332Z', to: '2023-01-30T20:33:01.332Z' },
|
||
|
});
|
||
|
const storage = new QueryCache();
|
||
|
const interpolateString = (s: string) => {
|
||
|
return s;
|
||
|
};
|
||
|
const cacheRequest = storage.requestInfo(request, interpolateString);
|
||
|
expect(cacheRequest.requests[0]).toBe(request);
|
||
|
expect(cacheRequest.shouldCache).toBe(false);
|
||
|
});
|
||
|
|
||
|
it('mark request as shouldCache', () => {
|
||
|
const request = mockRequest();
|
||
|
const storage = new QueryCache();
|
||
|
const interpolateString = (s: string) => {
|
||
|
return s;
|
||
|
};
|
||
|
const cacheRequest = storage.requestInfo(request, interpolateString);
|
||
|
expect(cacheRequest.requests[0]).toBe(request);
|
||
|
expect(cacheRequest.shouldCache).toBe(true);
|
||
|
});
|
||
|
|
||
|
it('Should modify request', () => {
|
||
|
const request = mockRequest();
|
||
|
const storage = new QueryCache();
|
||
|
const interpolateString = (s: string) => {
|
||
|
return s;
|
||
|
};
|
||
|
const cacheRequest = storage.requestInfo(request, interpolateString);
|
||
|
expect(cacheRequest.requests[0]).toBe(request);
|
||
|
expect(cacheRequest.shouldCache).toBe(true);
|
||
|
});
|
||
|
});
|