From d8b92217683348dc20295cc4cc090cdf6ee07c46 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Sun, 25 May 2025 14:45:01 +0200 Subject: [PATCH] move all cache related stuff in caching.ts --- .../grafana-prometheus/src/caching.test.ts | 143 ++++++++++++++++++ packages/grafana-prometheus/src/caching.ts | 101 +++++++++++++ .../components/VariableQueryEditor.test.tsx | 1 - packages/grafana-prometheus/src/datasource.ts | 33 +--- .../src/language_provider.test.ts | 11 +- .../src/language_provider.ts | 36 +---- .../grafana-prometheus/src/language_utils.ts | 19 +-- .../components/MetricsLabelsSection.tsx | 3 +- .../components/metrics-modal/MetricsModal.tsx | 13 +- 9 files changed, 264 insertions(+), 96 deletions(-) create mode 100644 packages/grafana-prometheus/src/caching.test.ts create mode 100644 packages/grafana-prometheus/src/caching.ts diff --git a/packages/grafana-prometheus/src/caching.test.ts b/packages/grafana-prometheus/src/caching.test.ts new file mode 100644 index 00000000000..f2ccfae176d --- /dev/null +++ b/packages/grafana-prometheus/src/caching.test.ts @@ -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 + }, + }); + }); + }); +}); diff --git a/packages/grafana-prometheus/src/caching.ts b/packages/grafana-prometheus/src/caching.ts new file mode 100644 index 00000000000..6b1f39d0139 --- /dev/null +++ b/packages/grafana-prometheus/src/caching.ts @@ -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= + * @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; +}; diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx index 887a6cef2c4..346ee20f4a5 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx @@ -143,7 +143,6 @@ describe('PromVariableQueryEditor', () => { getLabelValues: jest.fn().mockImplementation(() => ['that']), fetchLabelsWithMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })), } as Partial as PrometheusLanguageProvider, - getDebounceTimeInMilliseconds: jest.fn(), getTagKeys: jest .fn() .mockImplementation(() => Promise.resolve([{ text: 'this', value: 'this', label: 'this' }])), diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 4a1aa48fbd1..9a1110afc0f 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -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', diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index cdbb774ad63..d1e88b6cc47 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -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, diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index f32b30534a9..be6555d163d 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -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; 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, diff --git a/packages/grafana-prometheus/src/language_utils.ts b/packages/grafana-prometheus/src/language_utils.ts index 63426517d88..fed7f7fbcb4 100644 --- a/packages/grafana-prometheus/src/language_utils.ts +++ b/packages/grafana-prometheus/src/language_utils.ts @@ -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)!; diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx index 10ebde457ac..7cd74c3b311 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx @@ -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} /> { isLoading: false, }) ); - }, datasource.getDebounceTimeInMilliseconds()), + }, getDebounceTimeInMilliseconds(datasource.cacheLevel)), [datasource, query] );