move all cache related stuff in caching.ts

This commit is contained in:
ismail simsek
2025-05-25 14:47:22 +02:00
parent eb85cc4ef8
commit d8b9221768
9 changed files with 264 additions and 96 deletions
@@ -0,0 +1,143 @@
import { getCacheDurationInMinutes, getDaysToCacheMetadata, getDebounceTimeInMilliseconds, buildCacheHeaders, getDefaultCacheHeaders } from './caching';
import { PrometheusCacheLevel } from './types';
describe('caching', () => {
describe('getDebounceTimeInMilliseconds', () => {
it('should return 600ms for Medium cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.Medium)).toBe(600);
});
it('should return 1200ms for High cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.High)).toBe(1200);
});
it('should return 350ms for Low cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.Low)).toBe(350);
});
it('should return 350ms for None cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.None)).toBe(350);
});
it('should return default value (350ms) for unknown cache level', () => {
expect(getDebounceTimeInMilliseconds('invalid' as PrometheusCacheLevel)).toBe(350);
});
});
describe('getDaysToCacheMetadata', () => {
it('should return 7 days for Medium cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.Medium)).toBe(7);
});
it('should return 30 days for High cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.High)).toBe(30);
});
it('should return 1 day for Low cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.Low)).toBe(1);
});
it('should return 1 day for None cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.None)).toBe(1);
});
it('should return default value (1 day) for unknown cache level', () => {
expect(getDaysToCacheMetadata('invalid' as PrometheusCacheLevel)).toBe(1);
});
});
describe('getCacheDurationInMinutes', () => {
it('should return 10 minutes for Medium cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.Medium)).toBe(10);
});
it('should return 60 minutes for High cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.High)).toBe(60);
});
it('should return 1 minute for Low cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.Low)).toBe(1);
});
it('should return 1 minute for None cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.None)).toBe(1);
});
it('should return default value (1 minute) for unknown cache level', () => {
expect(getCacheDurationInMinutes('invalid' as PrometheusCacheLevel)).toBe(1);
});
});
describe('buildCacheHeaders', () => {
it('should build cache headers with provided duration in seconds', () => {
const result = buildCacheHeaders(300);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=300',
},
});
});
it('should handle zero duration', () => {
const result = buildCacheHeaders(0);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=0',
},
});
});
it('should handle large duration values', () => {
const oneDayInSeconds = 86400;
const result = buildCacheHeaders(oneDayInSeconds);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=86400',
},
});
});
});
describe('getDefaultCacheHeaders', () => {
it('should return cache headers for Medium cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.Medium);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=600', // 10 minutes in seconds
},
});
});
it('should return cache headers for High cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.High);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=3600', // 60 minutes in seconds
},
});
});
it('should return cache headers for Low cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.Low);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=60', // 1 minute in seconds
},
});
});
it('should return undefined for None cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.None);
expect(result).toBeUndefined();
});
it('should handle unknown cache level as default (1 minute)', () => {
const result = getDefaultCacheHeaders('invalid' as PrometheusCacheLevel);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=60', // 1 minute in seconds
},
});
});
});
});
+101
View File
@@ -0,0 +1,101 @@
import { PrometheusCacheLevel } from './types';
/**
* Returns the debounce time in milliseconds based on the cache level.
* Used to control the frequency of API requests.
*
* @param {PrometheusCacheLevel} cacheLevel - The cache level (None, Low, Medium, High)
* @returns {number} Debounce time in milliseconds:
* - Medium: 600ms
* - High: 1200ms
* - Default (None/Low): 350ms
*/
export const getDebounceTimeInMilliseconds = (cacheLevel: PrometheusCacheLevel): number => {
switch (cacheLevel) {
case PrometheusCacheLevel.Medium:
return 600;
case PrometheusCacheLevel.High:
return 1200;
default:
return 350;
}
};
/**
* Returns the number of days to cache metadata based on the cache level.
* Used for caching Prometheus metric metadata.
*
* @param {PrometheusCacheLevel} cacheLevel - The cache level (None, Low, Medium, High)
* @returns {number} Number of days to cache:
* - Medium: 7 days
* - High: 30 days
* - Default (None/Low): 1 day
*/
export const getDaysToCacheMetadata = (cacheLevel: PrometheusCacheLevel): number => {
switch (cacheLevel) {
case PrometheusCacheLevel.Medium:
return 7;
case PrometheusCacheLevel.High:
return 30;
default:
return 1;
}
};
/**
* Returns the cache duration in minutes based on the cache level.
* Used for general API response caching.
*
* @param {PrometheusCacheLevel} cacheLevel - The cache level (None, Low, Medium, High)
* @returns {number} Cache duration in minutes:
* - Medium: 10 minutes
* - High: 60 minutes
* - Default (None/Low): 1 minute
*/
export function getCacheDurationInMinutes(cacheLevel: PrometheusCacheLevel) {
switch (cacheLevel) {
case PrometheusCacheLevel.Medium:
return 10;
case PrometheusCacheLevel.High:
return 60;
default:
return 1;
}
}
/**
* Builds cache headers for Prometheus API requests.
* Creates a standard cache control header with private scope and max-age directive.
*
* @param {number} durationInSeconds - Cache duration in seconds
* @returns {object} Object containing headers with cache control directives:
* - X-Grafana-Cache: private, max-age=<duration>
* @example
* // Returns { headers: { 'X-Grafana-Cache': 'private, max-age=300' } }
* buildCacheHeaders(300)
*/
export const buildCacheHeaders = (durationInSeconds: number) => {
return {
headers: {
'X-Grafana-Cache': `private, max-age=${durationInSeconds}`,
},
};
};
/**
* Gets appropriate cache headers based on the configured cache level.
* Converts cache duration from minutes to seconds and builds the headers.
* Returns undefined if caching is disabled (None level).
*
* @param {PrometheusCacheLevel} cacheLevel - Cache level (None, Low, Medium, High)
* @returns {object|undefined} Cache headers object or undefined if caching is disabled
* @example
* // For Medium level, returns { headers: { 'X-Grafana-Cache': 'private, max-age=600' } }
* getDefaultCacheHeaders(PrometheusCacheLevel.Medium)
*/
export const getDefaultCacheHeaders = (cacheLevel: PrometheusCacheLevel) => {
if (cacheLevel !== PrometheusCacheLevel.None) {
return buildCacheHeaders(getCacheDurationInMinutes(cacheLevel) * 60);
}
return;
};
@@ -143,7 +143,6 @@ describe('PromVariableQueryEditor', () => {
getLabelValues: jest.fn().mockImplementation(() => ['that']),
fetchLabelsWithMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })),
} as Partial<PrometheusLanguageProvider> as PrometheusLanguageProvider,
getDebounceTimeInMilliseconds: jest.fn(),
getTagKeys: jest
.fn()
.mockImplementation(() => Promise.resolve([{ text: 'this', value: 'this', label: 'this' }])),
+1 -32
View File
@@ -47,12 +47,7 @@ import PrometheusLanguageProvider, {
importFromAbstractQuery,
SUGGESTIONS_LIMIT,
} from './language_provider';
import {
expandRecordingRules,
getClientCacheDurationInMinutes,
getPrometheusTime,
getRangeSnapInterval,
} from './language_utils';
import { expandRecordingRules, getPrometheusTime, getRangeSnapInterval } from './language_utils';
import { PrometheusMetricFindQuery } from './metric_find_query';
import { getQueryHints } from './query_hints';
import { promQueryModeller } from './querybuilder/PromQueryModeller';
@@ -866,32 +861,6 @@ export class PrometheusDatasource
return range.raw.from.includes('now') || range.raw.to.includes('now');
}
getDebounceTimeInMilliseconds(): number {
switch (this.cacheLevel) {
case PrometheusCacheLevel.Medium:
return 600;
case PrometheusCacheLevel.High:
return 1200;
default:
return 350;
}
}
getDaysToCacheMetadata(): number {
switch (this.cacheLevel) {
case PrometheusCacheLevel.Medium:
return 7;
case PrometheusCacheLevel.High:
return 30;
default:
return 1;
}
}
getCacheDurationInMinutes(): number {
return getClientCacheDurationInMinutes(this.cacheLevel);
}
getDefaultQuery(app: CoreApp): PromQuery {
const defaults = {
refId: 'A',
@@ -1,6 +1,7 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.test.ts
import { AbstractLabelOperator, dateTime, TimeRange } from '@grafana/data';
import { getCacheDurationInMinutes } from './caching';
import { DEFAULT_SERIES_LIMIT } from './components/metrics-browser/types';
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
import { PrometheusDatasource } from './datasource';
@@ -10,7 +11,7 @@ import LanguageProvider, {
processSeries,
removeQuotesIfExist,
} from './language_provider';
import { getClientCacheDurationInMinutes, getPrometheusTime, getRangeSnapInterval } from './language_utils';
import { getPrometheusTime, getRangeSnapInterval } from './language_utils';
import { PrometheusCacheLevel, PromQuery } from './types';
const now = new Date(1681300293392).getTime();
@@ -176,14 +177,13 @@ describe('Prometheus Language Provider', () => {
});
it('should call labels endpoint with quantized time parameters when cache level is set', () => {
const timeSnapMinutes = getClientCacheDurationInMinutes(PrometheusCacheLevel.Low);
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Low);
const languageProvider = new LanguageProvider({
...defaultDatasource,
hasLabelsMatchAPISupport: () => true,
cacheLevel: PrometheusCacheLevel.Low,
getAdjustedInterval: (timeRange: TimeRange) =>
getRangeSnapInterval(PrometheusCacheLevel.Low, getMockQuantizedTimeRangeParams()),
getCacheDurationInMinutes: () => timeSnapMinutes,
} as PrometheusDatasource);
const getSeriesLabels = languageProvider.getSeriesLabels;
const requestSpy = jest.spyOn(languageProvider, 'request');
@@ -568,11 +568,10 @@ describe('Prometheus Language Provider', () => {
});
it('should include cache headers for requests when cacheLevel is set', () => {
const timeSnapMinutes = getClientCacheDurationInMinutes(PrometheusCacheLevel.Medium);
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium);
const languageProvider = new LanguageProvider({
...defaultDatasource,
cacheLevel: PrometheusCacheLevel.Medium,
getCacheDurationInMinutes: () => timeSnapMinutes,
} as PrometheusDatasource);
const fetchLabelValues = languageProvider.fetchLabelValues;
const requestSpy = jest.spyOn(languageProvider, 'request');
@@ -708,7 +707,7 @@ describe('Prometheus Language Provider', () => {
});
it('should include cache headers when cacheLevel is set', async () => {
const timeSnapMinutes = getClientCacheDurationInMinutes(PrometheusCacheLevel.Medium);
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium);
const languageProvider = new LanguageProvider({
...defaultDatasource,
cacheLevel: PrometheusCacheLevel.Medium,
@@ -16,13 +16,13 @@ import {
} from '@grafana/data';
import { BackendSrvRequest } from '@grafana/runtime';
import { buildCacheHeaders, getDaysToCacheMetadata, getDefaultCacheHeaders } from './caching';
import { DEFAULT_SERIES_LIMIT, REMOVE_SERIES_LIMIT } from './components/metrics-browser/types';
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
import { PrometheusDatasource } from './datasource';
import {
extractLabelMatchers,
fixSummariesMetadata,
getClientCacheDurationInMinutes,
getRangeSnapInterval,
processHistogramMetrics,
processLabels,
@@ -30,7 +30,7 @@ import {
} from './language_utils';
import PromqlSyntax from './promql';
import { buildVisualQueryFromString } from './querybuilder/parsing';
import { PrometheusCacheLevel, PromMetricsMetadata, PromQuery } from './types';
import { PromMetricsMetadata, PromQuery } from './types';
import { escapeForUtf8Support, isValidLegacyName } from './utf8_support';
const DEFAULT_KEYS = ['job', 'instance'];
@@ -55,34 +55,6 @@ type UrlParamsType = {
limit?: string;
};
/**
* Builds cache headers for Prometheus API requests.
*
* @param {number} durationInSeconds - Cache duration in seconds
* @returns {object} Object with headers property containing cache headers
*/
const buildCacheHeaders = (durationInSeconds: number) => {
return {
headers: {
'X-Grafana-Cache': `private, max-age=${durationInSeconds}`,
},
};
};
/**
* Gets appropriate cache headers based on the configured cache level.
* Returns undefined if caching is disabled.
*
* @param {PrometheusCacheLevel} cacheLevel - Cache level (None, Low, Medium, High)
* @returns {object|undefined} Cache headers object or undefined if caching is disabled
*/
const getDefaultCacheHeaders = (cacheLevel: PrometheusCacheLevel) => {
if (cacheLevel !== PrometheusCacheLevel.None) {
return buildCacheHeaders(getClientCacheDurationInMinutes(cacheLevel) * 60);
}
return;
};
export default class PromQlLanguageProvider extends LanguageProvider {
declare startTask: Promise<any>;
declare labelFetchTs: number;
@@ -150,7 +122,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
*/
private _fetchMetadata = async () => {
const secondsInDay = 86400;
const headers = buildCacheHeaders(this.datasource.getDaysToCacheMetadata() * secondsInDay);
const headers = buildCacheHeaders(getDaysToCacheMetadata(this.datasource.cacheLevel) * secondsInDay);
const metadata = await this.request(
API_V1.METADATA,
{},
@@ -276,7 +248,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
*/
async loadMetricsMetadata() {
const secondsInDay = 86400;
const headers = buildCacheHeaders(this.datasource.getDaysToCacheMetadata() * secondsInDay);
const headers = buildCacheHeaders(getDaysToCacheMetadata(this.datasource.cacheLevel) * secondsInDay);
this.metricsMetadata = fixSummariesMetadata(
await this.request(
API_V1.METADATA,
@@ -14,6 +14,7 @@ import {
} from '@grafana/data';
import { addLabelToQuery } from './add_label_to_query';
import { getCacheDurationInMinutes } from './caching';
import { SUGGESTIONS_LIMIT } from './language_provider';
import { PrometheusCacheLevel, PromMetricsMetadata, PromMetricsMetadataItem, RecordingRuleIdentifier } from './types';
@@ -492,16 +493,15 @@ export function getRangeSnapInterval(
}
// Otherwise round down to the nearest nth minute for the start time
const startTime = getPrometheusTime(range.from, false);
// const startTimeQuantizedSeconds = roundSecToLastMin(startTime, getClientCacheDurationInMinutes(cacheLevel)) * 60;
const startTimeQuantizedSeconds = incrRoundDn(startTime, getClientCacheDurationInMinutes(cacheLevel) * 60);
const startTimeQuantizedSeconds = incrRoundDn(startTime, getCacheDurationInMinutes(cacheLevel) * 60);
// And round up to the nearest nth minute for the end time
const endTime = getPrometheusTime(range.to, true);
const endTimeQuantizedSeconds = roundSecToNextMin(endTime, getClientCacheDurationInMinutes(cacheLevel)) * 60;
const endTimeQuantizedSeconds = roundSecToNextMin(endTime, getCacheDurationInMinutes(cacheLevel)) * 60;
// If the interval was too short, we could have rounded both start and end to the same time, if so let's add one step to the end
if (startTimeQuantizedSeconds === endTimeQuantizedSeconds) {
const endTimePlusOneStep = endTimeQuantizedSeconds + getClientCacheDurationInMinutes(cacheLevel) * 60;
const endTimePlusOneStep = endTimeQuantizedSeconds + getCacheDurationInMinutes(cacheLevel) * 60;
return { start: startTimeQuantizedSeconds.toString(), end: endTimePlusOneStep.toString() };
}
@@ -511,17 +511,6 @@ export function getRangeSnapInterval(
return { start, end };
}
export function getClientCacheDurationInMinutes(cacheLevel: PrometheusCacheLevel) {
switch (cacheLevel) {
case PrometheusCacheLevel.Medium:
return 10;
case PrometheusCacheLevel.High:
return 60;
default:
return 1;
}
}
export function getPrometheusTime(date: string | DateTime, roundUp: boolean) {
if (typeof date === 'string') {
date = dateMath.parse(date, roundUp)!;
@@ -3,6 +3,7 @@ import { useCallback } from 'react';
import { SelectableValue, TimeRange } from '@grafana/data';
import { getDebounceTimeInMilliseconds } from '../../caching';
import { PrometheusDatasource } from '../../datasource';
import { truncateResult } from '../../language_utils';
import { PromMetricsMetadata } from '../../types';
@@ -207,7 +208,7 @@ export function MetricsLabelsSection({
variableEditor={variableEditor}
/>
<LabelFilters
debounceDuration={datasource.getDebounceTimeInMilliseconds()}
debounceDuration={getDebounceTimeInMilliseconds(datasource.cacheLevel)}
getLabelValuesAutofillSuggestions={getLabelValuesAutocompleteSuggestions}
labelsFilters={query.labels}
onChange={onChangeLabels}
@@ -1,6 +1,6 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/MetricsModal.tsx
import { cx } from '@emotion/css';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import debounce from 'debounce-promise';
import { useCallback, useEffect, useMemo, useReducer } from 'react';
@@ -19,6 +19,7 @@ import {
useTheme2,
} from '@grafana/ui';
import { getDebounceTimeInMilliseconds } from '../../../caching';
import { PrometheusDatasource } from '../../../datasource';
import { PromVisualQuery } from '../../types';
@@ -35,13 +36,7 @@ import {
setMetrics,
tracking,
} from './state/helpers';
import {
DEFAULT_RESULTS_PER_PAGE,
initialState,
MAXIMUM_RESULTS_PER_PAGE,
MetricsModalMetadata,
// stateSlice,
} from './state/state';
import { DEFAULT_RESULTS_PER_PAGE, initialState, MAXIMUM_RESULTS_PER_PAGE, MetricsModalMetadata } from './state/state';
import { getStyles } from './styles';
import { MetricsData, PromFilterOption } from './types';
import { debouncedFuzzySearch } from './uFuzzy';
@@ -117,7 +112,7 @@ export const MetricsModal = (props: MetricsModalProps) => {
isLoading: false,
})
);
}, datasource.getDebounceTimeInMilliseconds()),
}, getDebounceTimeInMilliseconds(datasource.cacheLevel)),
[datasource, query]
);