From 643dee6739904070ce958c2b05cd7b2b9a53d96c Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 27 Jun 2025 11:00:25 +0100 Subject: [PATCH] Prometheus: Refactoring in datasource.ts (#107208) * start cleanup * update vars * prettier --- packages/grafana-prometheus/src/constants.ts | 29 ++- packages/grafana-prometheus/src/datasource.ts | 189 ++++++++++-------- packages/grafana-prometheus/src/index.ts | 4 +- 3 files changed, 126 insertions(+), 96 deletions(-) diff --git a/packages/grafana-prometheus/src/constants.ts b/packages/grafana-prometheus/src/constants.ts index 12e6d9ce083..f54ed124c20 100644 --- a/packages/grafana-prometheus/src/constants.ts +++ b/packages/grafana-prometheus/src/constants.ts @@ -6,28 +6,43 @@ export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000; export const PROM_CONFIG_LABEL_WIDTH = 30; export const LIST_ITEM_SIZE = 25; + export const LAST_USED_LABELS_KEY = 'grafana.datasources.prometheus.browser.labels'; -// single duration input export const DURATION_REGEX = /^$|^\d+(ms|[Mwdhmsy])$/; -// multiple duration input export const MULTIPLE_DURATION_REGEX = /(\d+)(.+)/; export const NON_NEGATIVE_INTEGER_REGEX = /^(0|[1-9]\d*)(\.\d+)?(e\+?\d+)?$/; // non-negative integers, including scientific notation export const EMPTY_SELECTOR = '{}'; + export const DEFAULT_SERIES_LIMIT = 40000; + export const MATCH_ALL_LABELS_STR = '__name__!=""'; + export const MATCH_ALL_LABELS = '{__name__!=""}'; + export const METRIC_LABEL = '__name__'; +export const durationError = 'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s'; + +export const countError = 'Value is not valid, you can use non-negative integers, including scientific notation'; + +export const seriesLimitError = + 'Value is not valid, you can use only numbers or leave it empty to use default limit or set 0 to have no limit.'; + +export const InstantQueryRefIdIndex = '-Instant'; + +export const GET_AND_POST_METADATA_ENDPOINTS = [ + 'api/v1/query', + 'api/v1/query_range', + 'api/v1/series', + 'api/v1/labels', + 'suggestions', +]; + /** * @deprecated */ export const REMOVE_SERIES_LIMIT = 'none'; - -export const durationError = 'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s'; -export const countError = 'Value is not valid, you can use non-negative integers, including scientific notation'; -export const seriesLimitError = - 'Value is not valid, you can use only numbers or leave it empty to use default limit or set 0 to have no limit.'; diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index f2e70b358cc..204b452efd6 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -1,4 +1,3 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/datasource.ts import { defaults } from 'lodash'; import { tz } from 'moment-timezone'; import { lastValueFrom, Observable, throwError } from 'rxjs'; @@ -42,7 +41,12 @@ import { import { addLabelToQuery } from './add_label_to_query'; import { PrometheusAnnotationSupport } from './annotations'; -import { DEFAULT_SERIES_LIMIT, SUGGESTIONS_LIMIT } from './constants'; +import { + DEFAULT_SERIES_LIMIT, + GET_AND_POST_METADATA_ENDPOINTS, + InstantQueryRefIdIndex, + SUGGESTIONS_LIMIT, +} from './constants'; import { prometheusRegularEscape, prometheusSpecialRegexEscape } from './escaping'; import { exportToAbstractQuery, @@ -72,44 +76,34 @@ import { import { utf8Support, wrapUtf8Filters } from './utf8_support'; import { PrometheusVariableSupport } from './variables'; -const GET_AND_POST_METADATA_ENDPOINTS = [ - 'api/v1/query', - 'api/v1/query_range', - 'api/v1/series', - 'api/v1/labels', - 'suggestions', -]; - -export const InstantQueryRefIdIndex = '-Instant'; - export class PrometheusDatasource extends DataSourceWithBackend implements DataSourceWithQueryImportSupport, DataSourceWithQueryExportSupport { - type: string; - ruleMappings: RuleQueryMapping; - hasIncrementalQuery: boolean; - url: string; - id: number; access: 'direct' | 'proxy'; basicAuth: any; - withCredentials: boolean; - interval: string; - httpMethod: string; - languageProvider: PrometheusLanguageProviderInterface; - exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined; - lookupsDisabled: boolean; + cache: QueryCache; + cacheLevel: PrometheusCacheLevel; customQueryParameters: URLSearchParams; datasourceConfigurationPrometheusFlavor?: PromApplication; datasourceConfigurationPrometheusVersion?: string; disableRecordingRules: boolean; - defaultEditor?: QueryEditorMode; + exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined; exemplarsAvailable: boolean; - cacheLevel: PrometheusCacheLevel; - cache: QueryCache; + hasIncrementalQuery: boolean; + httpMethod: string; + id: number; + interval: string; + languageProvider: PrometheusLanguageProviderInterface; + lookupsDisabled: boolean; metricNamesAutocompleteSuggestionLimit: number; + ruleMappings: RuleQueryMapping; seriesEndpoint: boolean; seriesLimit: number; + type: string; + url: string; + withCredentials: boolean; + defaultEditor?: QueryEditorMode; constructor( instanceSettings: DataSourceInstanceSettings, @@ -118,48 +112,106 @@ export class PrometheusDatasource ) { super(instanceSettings); - this.type = 'prometheus'; - this.id = instanceSettings.id; - this.url = instanceSettings.url!; + // DATASOURCE CONFIGURATION PROPERTIES this.access = instanceSettings.access; this.basicAuth = instanceSettings.basicAuth; - this.withCredentials = Boolean(instanceSettings.withCredentials); - this.interval = instanceSettings.jsonData.timeInterval || '15s'; - this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; - this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations; - this.hasIncrementalQuery = instanceSettings.jsonData.incrementalQuerying ?? false; - this.ruleMappings = {}; - this.lookupsDisabled = instanceSettings.jsonData.disableMetricsLookup ?? false; - this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters); - this.datasourceConfigurationPrometheusFlavor = instanceSettings.jsonData.prometheusType; - this.datasourceConfigurationPrometheusVersion = instanceSettings.jsonData.prometheusVersion; - this.seriesLimit = instanceSettings.jsonData.seriesLimit ?? DEFAULT_SERIES_LIMIT; - this.seriesEndpoint = instanceSettings.jsonData.seriesEndpoint ?? false; - this.defaultEditor = instanceSettings.jsonData.defaultEditor; - this.disableRecordingRules = instanceSettings.jsonData.disableRecordingRules ?? false; - this.variables = new PrometheusVariableSupport(this, this.templateSrv); - this.exemplarsAvailable = true; - this.cacheLevel = instanceSettings.jsonData.cacheLevel ?? PrometheusCacheLevel.Low; - this.metricNamesAutocompleteSuggestionLimit = - instanceSettings.jsonData.codeModeMetricNamesSuggestionLimit ?? SUGGESTIONS_LIMIT; - this.cache = new QueryCache({ getTargetSignature: this.getPrometheusTargetSignature.bind(this), overlapString: instanceSettings.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow, applyInterpolation: this.interpolateString.bind(this), }); + this.cacheLevel = instanceSettings.jsonData.cacheLevel ?? PrometheusCacheLevel.Low; + this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters); + this.datasourceConfigurationPrometheusFlavor = instanceSettings.jsonData.prometheusType; + this.datasourceConfigurationPrometheusVersion = instanceSettings.jsonData.prometheusVersion; + this.disableRecordingRules = instanceSettings.jsonData.disableRecordingRules ?? false; + this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations; + this.exemplarsAvailable = true; + this.hasIncrementalQuery = instanceSettings.jsonData.incrementalQuerying ?? false; + this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; + this.id = instanceSettings.id; + this.interval = instanceSettings.jsonData.timeInterval || '15s'; + this.lookupsDisabled = instanceSettings.jsonData.disableMetricsLookup ?? false; + this.metricNamesAutocompleteSuggestionLimit = + instanceSettings.jsonData.codeModeMetricNamesSuggestionLimit ?? SUGGESTIONS_LIMIT; + this.ruleMappings = {}; + this.seriesEndpoint = instanceSettings.jsonData.seriesEndpoint ?? false; + this.seriesLimit = instanceSettings.jsonData.seriesLimit ?? DEFAULT_SERIES_LIMIT; + this.type = 'prometheus'; + this.url = instanceSettings.url!; + this.withCredentials = Boolean(instanceSettings.withCredentials); + this.defaultEditor = instanceSettings.jsonData.defaultEditor; + // INHERITED PROPERTIES this.annotations = PrometheusAnnotationSupport(this); + this.variables = new PrometheusVariableSupport(this, this.templateSrv); + + // LANGUAGE PROVIDER + // This needs to be the last thing we initialize. this.languageProvider = languageProvider ?? new PrometheusLanguageProvider(this); } - init = async () => { + /** + * Initializes the Prometheus datasource by loading recording rules and checking exemplar availability. + * + * This method performs two key initialization tasks: Loads recording rules from the + * Prometheus API and checks if exemplars are available by testing the exemplars API endpoint. + */ + init = async (): Promise => { if (!this.disableRecordingRules) { this.loadRules(); } this.exemplarsAvailable = await this.areExemplarsAvailable(); }; + /** + * Loads recording rules from the Prometheus API and extracts rule mappings. + * + * This method fetches rules from the `/api/v1/rules` endpoint and processes + * them to create a mapping of rule names to their corresponding queries and labels. + * The rules API is experimental, so errors are logged but not thrown. + */ + private async loadRules(): Promise { + try { + const params = {}; + const options = { showErrorAlert: false }; + const res = await this.metadataRequest('/api/v1/rules', params, options); + const ruleGroups = res.data?.data?.groups; + + if (ruleGroups) { + this.ruleMappings = extractRuleMappingFromGroups(ruleGroups); + } + } catch (err) { + console.log('Rules API is experimental. Ignore next error.'); + console.error(err); + } + } + + /** + * Checks if exemplars are available by testing the exemplars API endpoint. + * + * This method makes a test request to the `/api/v1/query_exemplars` endpoint to determine + * if the Prometheus instance supports exemplars. The test uses a simple query with a + * 30-minute time range. If the request succeeds with a 'success' status, exemplars + * are considered available. Errors are caught and return false to avoid breaking + * the datasource initialization. + */ + private async areExemplarsAvailable(): Promise { + try { + const params = { + query: 'test', + start: dateTime().subtract(30, 'minutes').valueOf().toString(), + end: dateTime().valueOf().toString(), + }; + const options = { showErrorAlert: false }; + const res = await this.metadataRequest('/api/v1/query_exemplars', params, options); + + return res.data.status === 'success'; + } catch (err) { + return false; + } + } + getQueryDisplayText(query: PromQuery) { return query.expr; } @@ -604,43 +656,6 @@ export class PrometheusDatasource return getQueryHints(query.expr ?? '', result, this); } - async loadRules() { - try { - const res = await this.metadataRequest('/api/v1/rules', {}, { showErrorAlert: false }); - const groups = res.data?.data?.groups; - - if (groups) { - this.ruleMappings = extractRuleMappingFromGroups(groups); - } - } catch (e) { - console.log('Rules API is experimental. Ignore next error.'); - console.error(e); - } - } - - async areExemplarsAvailable() { - try { - const res = await this.metadataRequest( - '/api/v1/query_exemplars', - { - query: 'test', - start: dateTime().subtract(30, 'minutes').valueOf().toString(), - end: dateTime().valueOf().toString(), - }, - { - // Avoid alerting the user if this test fails - showErrorAlert: false, - } - ); - if (res.data.status === 'success') { - return true; - } - return false; - } catch (err) { - return false; - } - } - modifyQuery(query: PromQuery, action: QueryFixAction): PromQuery { let expression = query.expr ?? ''; switch (action.type) { diff --git a/packages/grafana-prometheus/src/index.ts b/packages/grafana-prometheus/src/index.ts index 0b2629eb62a..1afbe8a3fa4 100644 --- a/packages/grafana-prometheus/src/index.ts +++ b/packages/grafana-prometheus/src/index.ts @@ -20,7 +20,7 @@ export { PromVariableQueryEditor } from './components/VariableQueryEditor'; // Main export export { ConfigEditor } from './configuration/ConfigEditor'; export { overhaulStyles, validateInput, docsTip } from './configuration/shared/utils'; -export { PROM_CONFIG_LABEL_WIDTH } from './constants'; +export { PROM_CONFIG_LABEL_WIDTH, InstantQueryRefIdIndex } from './constants'; // The parts export { AlertingSettingsOverhaul } from './configuration/AlertingSettingsOverhaul'; export { DataSourceHttpSettingsOverhaul } from './configuration/DataSourceHttpSettingsOverhaul'; @@ -54,7 +54,7 @@ export { MetricsModal } from './querybuilder/components/metrics-modal/MetricsMod // SRC/ // Main export -export { PrometheusDatasource, InstantQueryRefIdIndex } from './datasource'; +export { PrometheusDatasource } from './datasource'; // The parts export { addLabelToQuery } from './add_label_to_query'; export { type QueryEditorMode, type PromQueryFormat, type Prometheus } from './dataquery';