|
|
|
@ -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<PromQuery, PromOptions> |
|
|
|
|
implements DataSourceWithQueryImportSupport<PromQuery>, DataSourceWithQueryExportSupport<PromQuery> |
|
|
|
|
{ |
|
|
|
|
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<PromQuery>; |
|
|
|
|
cacheLevel: PrometheusCacheLevel; |
|
|
|
|
customQueryParameters: URLSearchParams; |
|
|
|
|
datasourceConfigurationPrometheusFlavor?: PromApplication; |
|
|
|
|
datasourceConfigurationPrometheusVersion?: string; |
|
|
|
|
disableRecordingRules: boolean; |
|
|
|
|
defaultEditor?: QueryEditorMode; |
|
|
|
|
exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined; |
|
|
|
|
exemplarsAvailable: boolean; |
|
|
|
|
cacheLevel: PrometheusCacheLevel; |
|
|
|
|
cache: QueryCache<PromQuery>; |
|
|
|
|
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<PromOptions>, |
|
|
|
@ -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.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.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.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations; |
|
|
|
|
this.exemplarsAvailable = true; |
|
|
|
|
this.cacheLevel = instanceSettings.jsonData.cacheLevel ?? PrometheusCacheLevel.Low; |
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
this.cache = new QueryCache({ |
|
|
|
|
getTargetSignature: this.getPrometheusTargetSignature.bind(this), |
|
|
|
|
overlapString: instanceSettings.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow, |
|
|
|
|
applyInterpolation: this.interpolateString.bind(this), |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// 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<void> => { |
|
|
|
|
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<void> { |
|
|
|
|
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<boolean> { |
|
|
|
|
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) { |
|
|
|
|