diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index 0d9ddb0ef08..e7ddbe5573b 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -6,14 +6,12 @@ import { Label } from './components/monaco-query-field/monaco-completion-provide import { PrometheusDatasource } from './datasource'; import LanguageProvider, { exportToAbstractQuery, - getMetadataHelp, - getMetadataString, - getMetadataType, importFromAbstractQuery, + processSeries, removeQuotesIfExist, } from './language_provider'; import { getClientCacheDurationInMinutes, getPrometheusTime, getRangeSnapInterval } from './language_utils'; -import { PrometheusCacheLevel, PromMetricsMetadata, PromQuery } from './types'; +import { PrometheusCacheLevel, PromQuery } from './types'; const now = new Date(1681300293392).getTime(); const timeRangeDurationSeconds = 1; @@ -645,49 +643,6 @@ describe('Prometheus Language Provider', () => { }); }); - describe('Metadata utility functions', () => { - const testMetadata: PromMetricsMetadata = { - metric1: { type: 'counter', help: 'Test counter help text' }, - metric2: { type: 'gauge', help: 'Test gauge help text' }, - }; - - describe('getMetadataString', () => { - it('should return formatted string with type and help text', () => { - const result = getMetadataString('metric1', testMetadata); - expect(result).toBe('COUNTER: Test counter help text'); - }); - - it('should return undefined for unknown metrics', () => { - const result = getMetadataString('unknown_metric', testMetadata); - expect(result).toBeUndefined(); - }); - }); - - describe('getMetadataHelp', () => { - it('should return help text for known metric', () => { - const result = getMetadataHelp('metric2', testMetadata); - expect(result).toBe('Test gauge help text'); - }); - - it('should return undefined for unknown metrics', () => { - const result = getMetadataHelp('unknown_metric', testMetadata); - expect(result).toBeUndefined(); - }); - }); - - describe('getMetadataType', () => { - it('should return type for known metric', () => { - const result = getMetadataType('metric1', testMetadata); - expect(result).toBe('counter'); - }); - - it('should return undefined for unknown metrics', () => { - const result = getMetadataType('unknown_metric', testMetadata); - expect(result).toBeUndefined(); - }); - }); - }); - describe('fetchSuggestions', () => { it('should send POST request with correct parameters', async () => { const timeRange = getMockTimeRange(); @@ -827,3 +782,71 @@ describe('removeQuotesIfExist', () => { expect(result).toBe('12345'); }); }); + +describe('processSeries', () => { + it('should extract metrics and label keys from series data', () => { + const result = processSeries([ + { + __name__: 'alerts', + alertname: 'AppCrash', + alertstate: 'firing', + instance: 'host.docker.internal:3000', + job: 'grafana', + severity: 'critical', + }, + { + __name__: 'alerts', + alertname: 'AppCrash', + alertstate: 'firing', + instance: 'prometheus-utf8:9112', + job: 'prometheus-utf8', + severity: 'critical', + }, + { + __name__: 'counters_logins', + app: 'backend', + geohash: '9wvfgzurfzb', + instance: 'fake-prometheus-data:9091', + job: 'fake-data-gen', + server: 'backend-01', + }, + ]); + + // Check structure + expect(result).toHaveProperty('metrics'); + expect(result).toHaveProperty('labelKeys'); + + // Verify metrics are extracted correctly + expect(result.metrics).toEqual(['alerts', 'counters_logins']); + + // Verify all metrics are unique + expect(result.metrics.length).toBe(new Set(result.metrics).size); + + // Verify label keys are extracted correctly and don't include __name__ + expect(result.labelKeys).toContain('instance'); + expect(result.labelKeys).toContain('job'); + expect(result.labelKeys).not.toContain('__name__'); + + // Verify all label keys are unique + expect(result.labelKeys.length).toBe(new Set(result.labelKeys).size); + }); + + it('should handle empty series data', () => { + const result = processSeries([]); + + expect(result.metrics).toEqual([]); + expect(result.labelKeys).toEqual([]); + }); + + it('should handle series without __name__ attribute', () => { + const series = [ + { instance: 'localhost:9090', job: 'prometheus' }, + { instance: 'localhost:9100', job: 'node' }, + ]; + + const result = processSeries(series); + + expect(result.metrics).toEqual([]); + expect(result.labelKeys).toEqual(['instance', 'job']); + }); +}); diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index c8f4d68c29a..c6405015232 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -23,6 +23,7 @@ import { extractLabelMatchers, fixSummariesMetadata, getClientCacheDurationInMinutes, + getRangeSnapInterval, processHistogramMetrics, processLabels, toPromLikeQuery, @@ -82,29 +83,6 @@ const getDefaultCacheHeaders = (cacheLevel: PrometheusCacheLevel) => { return; }; -export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined { - if (!metadata[metric]) { - return undefined; - } - const { type, help } = metadata[metric]; - return `${type.toUpperCase()}: ${help}`; -} - -export function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined { - if (!metadata[metric]) { - return undefined; - } - return metadata[metric].help; -} - -export function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined { - if (!metadata[metric]) { - return undefined; - } - return metadata[metric].type; -} - -const secondsInDay = 86400; export default class PromQlLanguageProvider extends LanguageProvider { histogramMetrics: string[]; metrics: string[]; @@ -142,23 +120,147 @@ export default class PromQlLanguageProvider extends LanguageProvider { return []; } + // Prevent ts yelling + console.log( + this._withLabelsApiFetchLabelKeys, + this._withLabelsApiFetchLabelValues, + this._withSeriesApiFetchAllSeries, + this._withSeriesApiFetchLabelKeys, + this._withSeriesApiFetchLabelValues + ); + this.metrics = (await this.fetchLabelValues(timeRange, '__name__')) || []; this.histogramMetrics = processHistogramMetrics(this.metrics).sort(); return Promise.all([this.loadMetricsMetadata(), this.fetchLabels(timeRange)]); }; - async loadMetricsMetadata() { + // ====================================================================== + + /** + * Fetches metadata for metrics from Prometheus. + * Sets cache headers based on the configured metadata cache duration. + * + * @returns {Promise} Promise that resolves when metadata has been fetched + */ + fetchMetadata = async () => { + const secondsInDay = 86400; const headers = buildCacheHeaders(this.datasource.getDaysToCacheMetadata() * secondsInDay); - this.metricsMetadata = fixSummariesMetadata( - await this.request( - API_V1.METADATA, - {}, - { - showErrorAlert: false, - ...headers, - } - ) + const metadata = await this.request( + API_V1.METADATA, + {}, + { + showErrorAlert: false, + ...headers, + } ); + this.metricsMetadata = fixSummariesMetadata(metadata); + return this.metricsMetadata; + }; + + // =================================== + // Labels API + // =================================== + + /** + * Fetches all available label keys from Prometheus using labels endpoint. + * Uses the labels endpoint with optional match parameter for filtering. + * + * @param {TimeRange} timeRange - Time range to use for the query + * @param {string} match - Optional label matcher to filter results + * @param {string} limit - Maximum number of results to return + * @returns {Promise} Array of label keys sorted alphabetically + */ + private _withLabelsApiFetchLabelKeys = async ( + timeRange: TimeRange, + match?: string, + limit: string = DEFAULT_SERIES_LIMIT + ): Promise => { + let url = API_V1.LABELS; + const timeParams = getRangeSnapInterval(this.datasource.cacheLevel, timeRange); + const searchParams = { limit, ...timeParams, ...(match ? { 'match[]': match } : {}) }; + + const res = await this.request(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel)); + if (Array.isArray(res)) { + this.labelKeys = res.slice().sort(); + return this.labelKeys.slice(); + } + + return []; + }; + + /** + * Fetches all values for a specific label key from Prometheus using labels values endpoint. + * + * @param {TimeRange} timeRange - Time range to use for the query + * @param {string} labelKey - The label key to fetch values for + * @param {string} match - Optional label matcher to filter results + * @param {string} limit - Maximum number of results to return + * @returns {Promise} Array of label values + */ + private _withLabelsApiFetchLabelValues = async ( + timeRange: TimeRange, + labelKey: string, + match?: string, + limit: string = DEFAULT_SERIES_LIMIT + ): Promise => { + const timeParams = this.datasource.getAdjustedInterval(timeRange); + const searchParams = { limit, ...timeParams, ...(match ? { 'match[]': match } : {}) }; + const interpolatedName = this.datasource.interpolateString(labelKey); + const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName)); + const url = API_V1.LABELS_VALUES(interpolatedAndEscapedName); + const value = await this.request(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel)); + return value ?? []; + }; + + // =================================== + // Series API + // =================================== + + /** + * Fetches all time series that match a specific label matcher using series endpoint. + * + * @param {TimeRange} timeRange - Time range to use for the query + * @param {string} match - Label matcher to filter time series + * @param {string} limit - Maximum number of series to return + */ + private _withSeriesApiFetchAllSeries = async ( + timeRange: TimeRange, + match: string, + limit: string = DEFAULT_SERIES_LIMIT + ) => { + const timeParams = this.datasource.getTimeRangeParams(timeRange); + const searchParams = { ...timeParams, 'match[]': match, limit }; + return await this.request(API_V1.SERIES, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel)); + }; + + private _withSeriesApiFetchLabelKeys = async ( + timeRange: TimeRange, + match: string, + limit: string = DEFAULT_SERIES_LIMIT + ): Promise => { + const series = await this._withSeriesApiFetchAllSeries(timeRange, match, limit); + const { labelKeys } = processSeries(series); + return labelKeys; + }; + + private _withSeriesApiFetchLabelValues = async ( + timeRange: TimeRange, + labelKey: string, + match: string, + limit: string = DEFAULT_SERIES_LIMIT + ): Promise => { + const series = await this._withSeriesApiFetchAllSeries(timeRange, match, limit); + const { labelValues } = processSeries(series, labelKey); + return labelValues; + }; + + // ====================================================================== + + /** + * @deprecated Use fetchMetadata instead + */ + async loadMetricsMetadata() { + this.fetchMetadata(); } getLabelKeys(): string[] { @@ -472,6 +574,36 @@ export const exportToAbstractQuery = (query: PromQuery): AbstractQuery => { }; }; +export function processSeries(series: Array<{ [key: string]: string }>, findValuesForKey?: string) { + const metrics: Set = new Set(); + const labelKeys: Set = new Set(); + const labelValues: Set = new Set(); + + // Extract metrics and label keys + series.forEach((item) => { + // Add the __name__ value to metrics + if ('__name__' in item) { + metrics.add(item.__name__); + } + + // Add all keys except __name__ to labelKeys + Object.keys(item).forEach((key) => { + if (key !== '__name__') { + labelKeys.add(key); + } + if (findValuesForKey && key === findValuesForKey) { + labelValues.add(item[key]); + } + }); + }); + + return { + metrics: Array.from(metrics).sort(), + labelKeys: Array.from(labelKeys).sort(), + labelValues: Array.from(labelValues).sort(), + }; +} + /** * Checks if an error is a cancelled request error. * Used to avoid logging cancelled request errors. diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx index 19a6bec7815..10ebde457ac 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx @@ -4,8 +4,8 @@ import { useCallback } from 'react'; import { SelectableValue, TimeRange } from '@grafana/data'; import { PrometheusDatasource } from '../../datasource'; -import { getMetadataString } from '../../language_provider'; import { truncateResult } from '../../language_utils'; +import { PromMetricsMetadata } from '../../types'; import { promQueryModeller } from '../PromQueryModeller'; import { regexifyLabelValuesQueryString } from '../parsingUtils'; import { QueryBuilderLabelFilter } from '../shared/types'; @@ -254,3 +254,11 @@ async function getMetrics( description: getMetadataString(m, datasource.languageProvider.metricsMetadata!), })); } + +export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined { + if (!metadata[metric]) { + return undefined; + } + const { type, help } = metadata[metric]; + return `${type.toUpperCase()}: ${help}`; +} diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts index 393b6b3a4ca..ee583335383 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts @@ -4,7 +4,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { reportInteraction } from '@grafana/runtime'; import { PrometheusDatasource } from '../../../../datasource'; -import { getMetadataHelp, getMetadataType } from '../../../../language_provider'; +import { PromMetricsMetadata } from '../../../../types'; import { regexifyLabelValuesQueryString } from '../../../parsingUtils'; import { QueryBuilderLabelFilter } from '../../../shared/types'; import { PromVisualQuery } from '../../../types'; @@ -89,6 +89,20 @@ function buildMetricData(metric: string, datasource: PrometheusDatasource): Metr return metricData; } +export function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined { + if (!metadata[metric]) { + return undefined; + } + return metadata[metric].help; +} + +export function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined { + if (!metadata[metric]) { + return undefined; + } + return metadata[metric].type; +} + /** * The filtered and paginated metrics displayed in the modal * */