datasource.tsx 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181
  1. import { cloneDeep, defaults } from 'lodash';
  2. import LRU from 'lru-cache';
  3. import React from 'react';
  4. import { forkJoin, lastValueFrom, merge, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs';
  5. import { catchError, filter, map, tap } from 'rxjs/operators';
  6. import {
  7. AnnotationEvent,
  8. CoreApp,
  9. DataQueryError,
  10. DataQueryRequest,
  11. DataQueryResponse,
  12. DataSourceInstanceSettings,
  13. DataSourceWithQueryExportSupport,
  14. DataSourceWithQueryImportSupport,
  15. dateMath,
  16. DateTime,
  17. AbstractQuery,
  18. LoadingState,
  19. rangeUtil,
  20. ScopedVars,
  21. TimeRange,
  22. DataFrame,
  23. dateTime,
  24. } from '@grafana/data';
  25. import {
  26. BackendSrvRequest,
  27. FetchError,
  28. FetchResponse,
  29. getBackendSrv,
  30. DataSourceWithBackend,
  31. BackendDataSourceResponse,
  32. toDataQueryResponse,
  33. } from '@grafana/runtime';
  34. import { Badge, BadgeColor, Tooltip } from '@grafana/ui';
  35. import { safeStringifyValue } from 'app/core/utils/explore';
  36. import { discoverDataSourceFeatures } from 'app/features/alerting/unified/api/buildInfo';
  37. import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  38. import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
  39. import { PromApplication, PromApiFeatures } from 'app/types/unified-alerting-dto';
  40. import { addLabelToQuery } from './add_label_to_query';
  41. import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
  42. import PrometheusLanguageProvider from './language_provider';
  43. import { expandRecordingRules } from './language_utils';
  44. import { renderLegendFormat } from './legend';
  45. import PrometheusMetricFindQuery from './metric_find_query';
  46. import { getInitHints, getQueryHints } from './query_hints';
  47. import { getOriginalMetricName, transform, transformV2 } from './result_transformer';
  48. import {
  49. ExemplarTraceIdDestination,
  50. PromDataErrorResponse,
  51. PromDataSuccessResponse,
  52. PromExemplarData,
  53. PromMatrixData,
  54. PromOptions,
  55. PromQuery,
  56. PromQueryRequest,
  57. PromQueryType,
  58. PromScalarData,
  59. PromVectorData,
  60. } from './types';
  61. import { PrometheusVariableSupport } from './variables';
  62. const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
  63. const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels'];
  64. export class PrometheusDatasource
  65. extends DataSourceWithBackend<PromQuery, PromOptions>
  66. implements DataSourceWithQueryImportSupport<PromQuery>, DataSourceWithQueryExportSupport<PromQuery>
  67. {
  68. type: string;
  69. editorSrc: string;
  70. ruleMappings: { [index: string]: string };
  71. url: string;
  72. id: number;
  73. directUrl: string;
  74. access: 'direct' | 'proxy';
  75. basicAuth: any;
  76. withCredentials: any;
  77. metricsNameCache = new LRU<string, string[]>({ max: 10 });
  78. interval: string;
  79. queryTimeout: string | undefined;
  80. httpMethod: string;
  81. languageProvider: PrometheusLanguageProvider;
  82. exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined;
  83. lookupsDisabled: boolean;
  84. customQueryParameters: any;
  85. exemplarsAvailable: boolean;
  86. subType: PromApplication;
  87. rulerEnabled: boolean;
  88. constructor(
  89. instanceSettings: DataSourceInstanceSettings<PromOptions>,
  90. private readonly templateSrv: TemplateSrv = getTemplateSrv(),
  91. private readonly timeSrv: TimeSrv = getTimeSrv(),
  92. languageProvider?: PrometheusLanguageProvider
  93. ) {
  94. super(instanceSettings);
  95. this.type = 'prometheus';
  96. this.subType = PromApplication.Prometheus;
  97. this.rulerEnabled = false;
  98. this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
  99. this.id = instanceSettings.id;
  100. this.url = instanceSettings.url!;
  101. this.access = instanceSettings.access;
  102. this.basicAuth = instanceSettings.basicAuth;
  103. this.withCredentials = instanceSettings.withCredentials;
  104. this.interval = instanceSettings.jsonData.timeInterval || '15s';
  105. this.queryTimeout = instanceSettings.jsonData.queryTimeout;
  106. this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
  107. // `directUrl` is never undefined, we set it at https://github.com/grafana/grafana/blob/main/pkg/api/frontendsettings.go#L108
  108. // here we "fall back" to this.url to make typescript happy, but it should never happen
  109. this.directUrl = instanceSettings.jsonData.directUrl ?? this.url;
  110. this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations;
  111. this.ruleMappings = {};
  112. this.languageProvider = languageProvider ?? new PrometheusLanguageProvider(this);
  113. this.lookupsDisabled = instanceSettings.jsonData.disableMetricsLookup ?? false;
  114. this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters);
  115. this.variables = new PrometheusVariableSupport(this, this.templateSrv, this.timeSrv);
  116. this.exemplarsAvailable = true;
  117. // This needs to be here and cannot be static because of how annotations typing affects casting of data source
  118. // objects to DataSourceApi types.
  119. // We don't use the default processing for prometheus.
  120. // See standardAnnotationSupport.ts/[shouldUseMappingUI|shouldUseLegacyRunner]
  121. this.annotations = {
  122. QueryEditor: AnnotationQueryEditor,
  123. };
  124. }
  125. init = async () => {
  126. this.loadRules();
  127. this.exemplarsAvailable = await this.areExemplarsAvailable();
  128. };
  129. getQueryDisplayText(query: PromQuery) {
  130. return query.expr;
  131. }
  132. _addTracingHeaders(httpOptions: PromQueryRequest, options: DataQueryRequest<PromQuery>) {
  133. httpOptions.headers = {};
  134. const proxyMode = !this.url.match(/^http/);
  135. if (proxyMode) {
  136. httpOptions.headers['X-Dashboard-Id'] = options.dashboardId;
  137. httpOptions.headers['X-Panel-Id'] = options.panelId;
  138. }
  139. }
  140. /**
  141. * Any request done from this data source should go through here as it contains some common processing for the
  142. * request. Any processing done here needs to be also copied on the backend as this goes through data source proxy
  143. * but not through the same code as alerting.
  144. */
  145. _request<T = any>(
  146. url: string,
  147. data: Record<string, string> | null,
  148. overrides: Partial<BackendSrvRequest> = {}
  149. ): Observable<FetchResponse<T>> {
  150. data = data || {};
  151. for (const [key, value] of this.customQueryParameters) {
  152. if (data[key] == null) {
  153. data[key] = value;
  154. }
  155. }
  156. let queryUrl = this.url + url;
  157. if (url.startsWith(`/api/datasources/${this.id}`)) {
  158. // This url is meant to be a replacement for the whole URL. Replace the entire URL
  159. queryUrl = url;
  160. }
  161. const options: BackendSrvRequest = defaults(overrides, {
  162. url: queryUrl,
  163. method: this.httpMethod,
  164. headers: {},
  165. });
  166. if (options.method === 'GET') {
  167. if (data && Object.keys(data).length) {
  168. options.url =
  169. options.url +
  170. (options.url.search(/\?/) >= 0 ? '&' : '?') +
  171. Object.entries(data)
  172. .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
  173. .join('&');
  174. }
  175. } else {
  176. options.headers!['Content-Type'] = 'application/x-www-form-urlencoded';
  177. options.data = data;
  178. }
  179. if (this.basicAuth || this.withCredentials) {
  180. options.withCredentials = true;
  181. }
  182. if (this.basicAuth) {
  183. options.headers!.Authorization = this.basicAuth;
  184. }
  185. return getBackendSrv().fetch<T>(options);
  186. }
  187. async importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise<PromQuery[]> {
  188. return abstractQueries.map((abstractQuery) => this.languageProvider.importFromAbstractQuery(abstractQuery));
  189. }
  190. async exportToAbstractQueries(queries: PromQuery[]): Promise<AbstractQuery[]> {
  191. return queries.map((query) => this.languageProvider.exportToAbstractQuery(query));
  192. }
  193. // Use this for tab completion features, wont publish response to other components
  194. async metadataRequest<T = any>(url: string, params = {}) {
  195. // If URL includes endpoint that supports POST and GET method, try to use configured method. This might fail as POST is supported only in v2.10+.
  196. if (GET_AND_POST_METADATA_ENDPOINTS.some((endpoint) => url.includes(endpoint))) {
  197. try {
  198. return await lastValueFrom(
  199. this._request<T>(`/api/datasources/${this.id}/resources${url}`, params, {
  200. method: this.httpMethod,
  201. hideFromInspector: true,
  202. showErrorAlert: false,
  203. })
  204. );
  205. } catch (err) {
  206. // If status code of error is Method Not Allowed (405) and HTTP method is POST, retry with GET
  207. if (this.httpMethod === 'POST' && (err.status === 405 || err.status === 400)) {
  208. console.warn(`Couldn't use configured POST HTTP method for this request. Trying to use GET method instead.`);
  209. } else {
  210. throw err;
  211. }
  212. }
  213. }
  214. return await lastValueFrom(
  215. this._request<T>(`/api/datasources/${this.id}/resources${url}`, params, {
  216. method: 'GET',
  217. hideFromInspector: true,
  218. })
  219. ); // toPromise until we change getTagValues, getTagKeys to Observable
  220. }
  221. interpolateQueryExpr(value: string | string[] = [], variable: any) {
  222. // if no multi or include all do not regexEscape
  223. if (!variable.multi && !variable.includeAll) {
  224. return prometheusRegularEscape(value);
  225. }
  226. if (typeof value === 'string') {
  227. return prometheusSpecialRegexEscape(value);
  228. }
  229. const escapedValues = value.map((val) => prometheusSpecialRegexEscape(val));
  230. if (escapedValues.length === 1) {
  231. return escapedValues[0];
  232. }
  233. return '(' + escapedValues.join('|') + ')';
  234. }
  235. targetContainsTemplate(target: PromQuery) {
  236. return this.templateSrv.containsTemplate(target.expr);
  237. }
  238. prepareTargets = (options: DataQueryRequest<PromQuery>, start: number, end: number) => {
  239. const queries: PromQueryRequest[] = [];
  240. const activeTargets: PromQuery[] = [];
  241. const clonedTargets = cloneDeep(options.targets);
  242. for (const target of clonedTargets) {
  243. if (!target.expr || target.hide) {
  244. continue;
  245. }
  246. target.requestId = options.panelId + target.refId;
  247. const metricName = this.languageProvider.histogramMetrics.find((m) => target.expr.includes(m));
  248. // In Explore, we run both (instant and range) queries if both are true (selected) or both are undefined (legacy Explore queries)
  249. if (options.app === CoreApp.Explore && target.range === target.instant) {
  250. // Create instant target
  251. const instantTarget: any = cloneDeep(target);
  252. instantTarget.format = 'table';
  253. instantTarget.instant = true;
  254. instantTarget.range = false;
  255. instantTarget.valueWithRefId = true;
  256. delete instantTarget.maxDataPoints;
  257. instantTarget.requestId += '_instant';
  258. // Create range target
  259. const rangeTarget: any = cloneDeep(target);
  260. rangeTarget.format = 'time_series';
  261. rangeTarget.instant = false;
  262. instantTarget.range = true;
  263. // Create exemplar query
  264. if (target.exemplar) {
  265. // Only create exemplar target for different metric names
  266. if (
  267. !metricName ||
  268. (metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName)))
  269. ) {
  270. const exemplarTarget = cloneDeep(target);
  271. exemplarTarget.instant = false;
  272. exemplarTarget.requestId += '_exemplar';
  273. queries.push(this.createQuery(exemplarTarget, options, start, end));
  274. activeTargets.push(exemplarTarget);
  275. }
  276. instantTarget.exemplar = false;
  277. rangeTarget.exemplar = false;
  278. }
  279. // Add both targets to activeTargets and queries arrays
  280. activeTargets.push(instantTarget, rangeTarget);
  281. queries.push(
  282. this.createQuery(instantTarget, options, start, end),
  283. this.createQuery(rangeTarget, options, start, end)
  284. );
  285. // If running only instant query in Explore, format as table
  286. } else if (target.instant && options.app === CoreApp.Explore) {
  287. const instantTarget: any = cloneDeep(target);
  288. instantTarget.format = 'table';
  289. queries.push(this.createQuery(instantTarget, options, start, end));
  290. activeTargets.push(instantTarget);
  291. } else {
  292. // It doesn't make sense to query for exemplars in dashboard if only instant is selected
  293. if (target.exemplar && !target.instant) {
  294. if (
  295. !metricName ||
  296. (metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName)))
  297. ) {
  298. const exemplarTarget = cloneDeep(target);
  299. exemplarTarget.requestId += '_exemplar';
  300. queries.push(this.createQuery(exemplarTarget, options, start, end));
  301. activeTargets.push(exemplarTarget);
  302. }
  303. target.exemplar = false;
  304. }
  305. queries.push(this.createQuery(target, options, start, end));
  306. activeTargets.push(target);
  307. }
  308. }
  309. return {
  310. queries,
  311. activeTargets,
  312. };
  313. };
  314. shouldRunExemplarQuery(target: PromQuery, request: DataQueryRequest<PromQuery>): boolean {
  315. if (target.exemplar) {
  316. // We check all already processed targets and only create exemplar target for not used metric names
  317. const metricName = this.languageProvider.histogramMetrics.find((m) => target.expr.includes(m));
  318. // Remove targets that weren't processed yet (in targets array they are after current target)
  319. const currentTargetIdx = request.targets.findIndex((t) => t.refId === target.refId);
  320. const targets = request.targets.slice(0, currentTargetIdx).filter((t) => !t.hide);
  321. if (!metricName || (metricName && !targets.some((t) => t.expr.includes(metricName)))) {
  322. return true;
  323. }
  324. return false;
  325. }
  326. return false;
  327. }
  328. processTargetV2(target: PromQuery, request: DataQueryRequest<PromQuery>) {
  329. const processedTarget = {
  330. ...target,
  331. queryType: PromQueryType.timeSeriesQuery,
  332. exemplar: this.shouldRunExemplarQuery(target, request),
  333. requestId: request.panelId + target.refId,
  334. // We need to pass utcOffsetSec to backend to calculate aligned range
  335. utcOffsetSec: this.timeSrv.timeRange().to.utcOffset() * 60,
  336. };
  337. return processedTarget;
  338. }
  339. query(request: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
  340. if (this.access === 'proxy') {
  341. const targets = request.targets.map((target) => this.processTargetV2(target, request));
  342. return super
  343. .query({ ...request, targets })
  344. .pipe(
  345. map((response) =>
  346. transformV2(response, request, { exemplarTraceIdDestinations: this.exemplarTraceIdDestinations })
  347. )
  348. );
  349. // Run queries trough browser/proxy
  350. } else {
  351. const start = this.getPrometheusTime(request.range.from, false);
  352. const end = this.getPrometheusTime(request.range.to, true);
  353. const { queries, activeTargets } = this.prepareTargets(request, start, end);
  354. // No valid targets, return the empty result to save a round trip.
  355. if (!queries || !queries.length) {
  356. return of({
  357. data: [],
  358. state: LoadingState.Done,
  359. });
  360. }
  361. if (request.app === CoreApp.Explore) {
  362. return this.exploreQuery(queries, activeTargets, end);
  363. }
  364. return this.panelsQuery(queries, activeTargets, end, request.requestId, request.scopedVars);
  365. }
  366. }
  367. private exploreQuery(queries: PromQueryRequest[], activeTargets: PromQuery[], end: number) {
  368. let runningQueriesCount = queries.length;
  369. const subQueries = queries.map((query, index) => {
  370. const target = activeTargets[index];
  371. const filterAndMapResponse = pipe(
  372. // Decrease the counter here. We assume that each request returns only single value and then completes
  373. // (should hold until there is some streaming requests involved).
  374. tap(() => runningQueriesCount--),
  375. filter((response: any) => (response.cancelled ? false : true)),
  376. map((response: any) => {
  377. const data = transform(response, {
  378. query,
  379. target,
  380. responseListLength: queries.length,
  381. exemplarTraceIdDestinations: this.exemplarTraceIdDestinations,
  382. });
  383. return {
  384. data,
  385. key: query.requestId,
  386. state: runningQueriesCount === 0 ? LoadingState.Done : LoadingState.Loading,
  387. } as DataQueryResponse;
  388. })
  389. );
  390. return this.runQuery(query, end, filterAndMapResponse);
  391. });
  392. return merge(...subQueries);
  393. }
  394. private panelsQuery(
  395. queries: PromQueryRequest[],
  396. activeTargets: PromQuery[],
  397. end: number,
  398. requestId: string,
  399. scopedVars: ScopedVars
  400. ) {
  401. const observables = queries.map((query, index) => {
  402. const target = activeTargets[index];
  403. const filterAndMapResponse = pipe(
  404. filter((response: any) => (response.cancelled ? false : true)),
  405. map((response: any) => {
  406. const data = transform(response, {
  407. query,
  408. target,
  409. responseListLength: queries.length,
  410. scopedVars,
  411. exemplarTraceIdDestinations: this.exemplarTraceIdDestinations,
  412. });
  413. return data;
  414. })
  415. );
  416. return this.runQuery(query, end, filterAndMapResponse);
  417. });
  418. return forkJoin(observables).pipe(
  419. map((results) => {
  420. const data = results.reduce((result, current) => {
  421. return [...result, ...current];
  422. }, []);
  423. return {
  424. data,
  425. key: requestId,
  426. state: LoadingState.Done,
  427. };
  428. })
  429. );
  430. }
  431. private runQuery<T>(query: PromQueryRequest, end: number, filter: OperatorFunction<any, T>): Observable<T> {
  432. if (query.instant) {
  433. return this.performInstantQuery(query, end).pipe(filter);
  434. }
  435. if (query.exemplar) {
  436. return this.getExemplars(query).pipe(
  437. catchError(() => {
  438. return of({
  439. data: [],
  440. state: LoadingState.Done,
  441. });
  442. }),
  443. filter
  444. );
  445. }
  446. return this.performTimeSeriesQuery(query, query.start, query.end).pipe(filter);
  447. }
  448. createQuery(target: PromQuery, options: DataQueryRequest<PromQuery>, start: number, end: number) {
  449. const query: PromQueryRequest = {
  450. hinting: target.hinting,
  451. instant: target.instant,
  452. exemplar: target.exemplar,
  453. step: 0,
  454. expr: '',
  455. requestId: target.requestId,
  456. refId: target.refId,
  457. start: 0,
  458. end: 0,
  459. };
  460. const range = Math.ceil(end - start);
  461. // options.interval is the dynamically calculated interval
  462. let interval: number = rangeUtil.intervalToSeconds(options.interval);
  463. // Minimum interval ("Min step"), if specified for the query, or same as interval otherwise.
  464. const minInterval = rangeUtil.intervalToSeconds(
  465. this.templateSrv.replace(target.interval || options.interval, options.scopedVars)
  466. );
  467. // Scrape interval as specified for the query ("Min step") or otherwise taken from the datasource.
  468. // Min step field can have template variables in it, make sure to replace it.
  469. const scrapeInterval = target.interval
  470. ? rangeUtil.intervalToSeconds(this.templateSrv.replace(target.interval, options.scopedVars))
  471. : rangeUtil.intervalToSeconds(this.interval);
  472. const intervalFactor = target.intervalFactor || 1;
  473. // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
  474. const adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
  475. let scopedVars = {
  476. ...options.scopedVars,
  477. ...this.getRangeScopedVars(options.range),
  478. ...this.getRateIntervalScopedVariable(adjustedInterval, scrapeInterval),
  479. };
  480. // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
  481. if (interval !== adjustedInterval) {
  482. interval = adjustedInterval;
  483. scopedVars = Object.assign({}, options.scopedVars, {
  484. __interval: { text: interval + 's', value: interval + 's' },
  485. __interval_ms: { text: interval * 1000, value: interval * 1000 },
  486. ...this.getRateIntervalScopedVariable(interval, scrapeInterval),
  487. ...this.getRangeScopedVars(options.range),
  488. });
  489. }
  490. query.step = interval;
  491. let expr = target.expr;
  492. // Apply adhoc filters
  493. expr = this.enhanceExprWithAdHocFilters(expr);
  494. // Only replace vars in expression after having (possibly) updated interval vars
  495. query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr);
  496. // Align query interval with step to allow query caching and to ensure
  497. // that about-same-time query results look the same.
  498. const adjusted = alignRange(start, end, query.step, this.timeSrv.timeRange().to.utcOffset() * 60);
  499. query.start = adjusted.start;
  500. query.end = adjusted.end;
  501. this._addTracingHeaders(query, options);
  502. return query;
  503. }
  504. getRateIntervalScopedVariable(interval: number, scrapeInterval: number) {
  505. // Fall back to the default scrape interval of 15s if scrapeInterval is 0 for some reason.
  506. if (scrapeInterval === 0) {
  507. scrapeInterval = 15;
  508. }
  509. const rateInterval = Math.max(interval + scrapeInterval, 4 * scrapeInterval);
  510. return { __rate_interval: { text: rateInterval + 's', value: rateInterval + 's' } };
  511. }
  512. adjustInterval(interval: number, minInterval: number, range: number, intervalFactor: number) {
  513. // Prometheus will drop queries that might return more than 11000 data points.
  514. // Calculate a safe interval as an additional minimum to take into account.
  515. // Fractional safeIntervals are allowed, however serve little purpose if the interval is greater than 1
  516. // If this is the case take the ceil of the value.
  517. let safeInterval = range / 11000;
  518. if (safeInterval > 1) {
  519. safeInterval = Math.ceil(safeInterval);
  520. }
  521. return Math.max(interval * intervalFactor, minInterval, safeInterval);
  522. }
  523. performTimeSeriesQuery(query: PromQueryRequest, start: number, end: number) {
  524. if (start > end) {
  525. throw { message: 'Invalid time range' };
  526. }
  527. const url = '/api/v1/query_range';
  528. const data: any = {
  529. query: query.expr,
  530. start,
  531. end,
  532. step: query.step,
  533. };
  534. if (this.queryTimeout) {
  535. data['timeout'] = this.queryTimeout;
  536. }
  537. return this._request<PromDataSuccessResponse<PromMatrixData>>(url, data, {
  538. requestId: query.requestId,
  539. headers: query.headers,
  540. }).pipe(
  541. catchError((err: FetchError<PromDataErrorResponse<PromMatrixData>>) => {
  542. if (err.cancelled) {
  543. return of(err);
  544. }
  545. return throwError(this.handleErrors(err, query));
  546. })
  547. );
  548. }
  549. performInstantQuery(
  550. query: PromQueryRequest,
  551. time: number
  552. ): Observable<FetchResponse<PromDataSuccessResponse<PromVectorData | PromScalarData>> | FetchError> {
  553. const url = '/api/v1/query';
  554. const data: any = {
  555. query: query.expr,
  556. time,
  557. };
  558. if (this.queryTimeout) {
  559. data['timeout'] = this.queryTimeout;
  560. }
  561. return this._request<PromDataSuccessResponse<PromVectorData | PromScalarData>>(url, data, {
  562. requestId: query.requestId,
  563. headers: query.headers,
  564. }).pipe(
  565. catchError((err: FetchError<PromDataErrorResponse<PromVectorData | PromScalarData>>) => {
  566. if (err.cancelled) {
  567. return of(err);
  568. }
  569. return throwError(this.handleErrors(err, query));
  570. })
  571. );
  572. }
  573. handleErrors = (err: any, target: PromQuery) => {
  574. const error: DataQueryError = {
  575. message: (err && err.statusText) || 'Unknown error during query transaction. Please check JS console logs.',
  576. refId: target.refId,
  577. };
  578. if (err.data) {
  579. if (typeof err.data === 'string') {
  580. error.message = err.data;
  581. } else if (err.data.error) {
  582. error.message = safeStringifyValue(err.data.error);
  583. }
  584. } else if (err.message) {
  585. error.message = err.message;
  586. } else if (typeof err === 'string') {
  587. error.message = err;
  588. }
  589. error.status = err.status;
  590. error.statusText = err.statusText;
  591. return error;
  592. };
  593. metricFindQuery(query: string) {
  594. if (!query) {
  595. return Promise.resolve([]);
  596. }
  597. const scopedVars = {
  598. __interval: { text: this.interval, value: this.interval },
  599. __interval_ms: { text: rangeUtil.intervalToMs(this.interval), value: rangeUtil.intervalToMs(this.interval) },
  600. ...this.getRangeScopedVars(this.timeSrv.timeRange()),
  601. };
  602. const interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr);
  603. const metricFindQuery = new PrometheusMetricFindQuery(this, interpolated);
  604. return metricFindQuery.process();
  605. }
  606. getRangeScopedVars(range: TimeRange = this.timeSrv.timeRange()) {
  607. const msRange = range.to.diff(range.from);
  608. const sRange = Math.round(msRange / 1000);
  609. return {
  610. __range_ms: { text: msRange, value: msRange },
  611. __range_s: { text: sRange, value: sRange },
  612. __range: { text: sRange + 's', value: sRange + 's' },
  613. };
  614. }
  615. async annotationQuery(options: any): Promise<AnnotationEvent[]> {
  616. const annotation = options.annotation;
  617. const { expr = '' } = annotation;
  618. if (!expr) {
  619. return Promise.resolve([]);
  620. }
  621. const step = options.annotation.step || ANNOTATION_QUERY_STEP_DEFAULT;
  622. const queryModel = {
  623. expr,
  624. range: true,
  625. instant: false,
  626. exemplar: false,
  627. interval: step,
  628. queryType: PromQueryType.timeSeriesQuery,
  629. refId: 'X',
  630. datasource: this.getRef(),
  631. };
  632. return await lastValueFrom(
  633. getBackendSrv()
  634. .fetch<BackendDataSourceResponse>({
  635. url: '/api/ds/query',
  636. method: 'POST',
  637. data: {
  638. from: (this.getPrometheusTime(options.range.from, false) * 1000).toString(),
  639. to: (this.getPrometheusTime(options.range.to, true) * 1000).toString(),
  640. queries: [this.applyTemplateVariables(queryModel, {})],
  641. },
  642. requestId: `prom-query-${annotation.name}`,
  643. })
  644. .pipe(
  645. map((rsp: FetchResponse<BackendDataSourceResponse>) => {
  646. return this.processAnnotationResponse(options, rsp.data);
  647. })
  648. )
  649. );
  650. }
  651. processAnnotationResponse = (options: any, data: BackendDataSourceResponse) => {
  652. const frames: DataFrame[] = toDataQueryResponse({ data: data }).data;
  653. if (!frames || !frames.length) {
  654. return [];
  655. }
  656. const annotation = options.annotation;
  657. const { tagKeys = '', titleFormat = '', textFormat = '' } = annotation;
  658. const step = rangeUtil.intervalToSeconds(annotation.step || ANNOTATION_QUERY_STEP_DEFAULT) * 1000;
  659. const tagKeysArray = tagKeys.split(',');
  660. const eventList: AnnotationEvent[] = [];
  661. for (const frame of frames) {
  662. const timeField = frame.fields[0];
  663. const valueField = frame.fields[1];
  664. const labels = valueField?.labels || {};
  665. const tags = Object.keys(labels)
  666. .filter((label) => tagKeysArray.includes(label))
  667. .map((label) => labels[label]);
  668. const timeValueTuple: Array<[number, number]> = [];
  669. let idx = 0;
  670. valueField.values.toArray().forEach((value: string) => {
  671. let timeStampValue: number;
  672. let valueValue: number;
  673. const time = timeField.values.get(idx);
  674. // If we want to use value as a time, we use value as timeStampValue and valueValue will be 1
  675. if (options.annotation.useValueForTime) {
  676. timeStampValue = Math.floor(parseFloat(value));
  677. valueValue = 1;
  678. } else {
  679. timeStampValue = Math.floor(parseFloat(time));
  680. valueValue = parseFloat(value);
  681. }
  682. idx++;
  683. timeValueTuple.push([timeStampValue, valueValue]);
  684. });
  685. const activeValues = timeValueTuple.filter((value) => value[1] >= 1);
  686. const activeValuesTimestamps = activeValues.map((value) => value[0]);
  687. // Instead of creating singular annotation for each active event we group events into region if they are less
  688. // or equal to `step` apart.
  689. let latestEvent: AnnotationEvent | null = null;
  690. for (const timestamp of activeValuesTimestamps) {
  691. // We already have event `open` and we have new event that is inside the `step` so we just update the end.
  692. if (latestEvent && (latestEvent.timeEnd ?? 0) + step >= timestamp) {
  693. latestEvent.timeEnd = timestamp;
  694. continue;
  695. }
  696. // Event exists but new one is outside of the `step` so we add it to eventList.
  697. if (latestEvent) {
  698. eventList.push(latestEvent);
  699. }
  700. // We start a new region.
  701. latestEvent = {
  702. time: timestamp,
  703. timeEnd: timestamp,
  704. annotation,
  705. title: renderLegendFormat(titleFormat, labels),
  706. tags,
  707. text: renderLegendFormat(textFormat, labels),
  708. };
  709. }
  710. if (latestEvent) {
  711. // Finish up last point if we have one
  712. latestEvent.timeEnd = activeValuesTimestamps[activeValuesTimestamps.length - 1];
  713. eventList.push(latestEvent);
  714. }
  715. }
  716. return eventList;
  717. };
  718. getExemplars(query: PromQueryRequest) {
  719. const url = '/api/v1/query_exemplars';
  720. return this._request<PromDataSuccessResponse<PromExemplarData>>(
  721. url,
  722. { query: query.expr, start: query.start.toString(), end: query.end.toString() },
  723. { requestId: query.requestId, headers: query.headers }
  724. );
  725. }
  726. async getSubtitle(): Promise<JSX.Element | null> {
  727. const buildInfo = await this.getBuildInfo();
  728. return buildInfo ? this.getBuildInfoMessage(buildInfo) : null;
  729. }
  730. async getTagKeys(options?: any) {
  731. if (options?.series) {
  732. // Get tags for the provided series only
  733. const seriesLabels: Array<Record<string, string[]>> = await Promise.all(
  734. options.series.map((series: string) => this.languageProvider.fetchSeriesLabels(series))
  735. );
  736. const uniqueLabels = [...new Set(...seriesLabels.map((value) => Object.keys(value)))];
  737. return uniqueLabels.map((value: any) => ({ text: value }));
  738. } else {
  739. // Get all tags
  740. const result = await this.metadataRequest('/api/v1/labels');
  741. return result?.data?.data?.map((value: any) => ({ text: value })) ?? [];
  742. }
  743. }
  744. async getTagValues(options: { key?: string } = {}) {
  745. const result = await this.metadataRequest(`/api/v1/label/${options.key}/values`);
  746. return result?.data?.data?.map((value: any) => ({ text: value })) ?? [];
  747. }
  748. async getBuildInfo() {
  749. try {
  750. const buildInfo = await discoverDataSourceFeatures({ url: this.url, name: this.name, type: 'prometheus' });
  751. return buildInfo;
  752. } catch (error) {
  753. // We don't want to break the rest of functionality if build info does not work correctly
  754. return undefined;
  755. }
  756. }
  757. getBuildInfoMessage(buildInfo: PromApiFeatures) {
  758. const enabled = <Badge color="green" icon="check" text="Ruler API enabled" />;
  759. const disabled = <Badge color="orange" icon="exclamation-triangle" text="Ruler API not enabled" />;
  760. const unsupported = (
  761. <Tooltip
  762. placement="top"
  763. content="Prometheus does not allow editing rules, connect to either a Mimir or Cortex datasource to manage alerts via Grafana."
  764. >
  765. <div>
  766. <Badge color="red" icon="exclamation-triangle" text="Ruler API not supported" />
  767. </div>
  768. </Tooltip>
  769. );
  770. const LOGOS = {
  771. [PromApplication.Lotex]: '/public/app/plugins/datasource/prometheus/img/cortex_logo.svg',
  772. [PromApplication.Mimir]: '/public/app/plugins/datasource/prometheus/img/mimir_logo.svg',
  773. [PromApplication.Prometheus]: '/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg',
  774. };
  775. const COLORS: Record<PromApplication, BadgeColor> = {
  776. [PromApplication.Lotex]: 'blue',
  777. [PromApplication.Mimir]: 'orange',
  778. [PromApplication.Prometheus]: 'red',
  779. };
  780. const AppDisplayNames: Record<PromApplication, string> = {
  781. [PromApplication.Lotex]: 'Cortex',
  782. [PromApplication.Mimir]: 'Mimir',
  783. [PromApplication.Prometheus]: 'Prometheus',
  784. };
  785. // this will inform the user about what "subtype" the datasource is; Mimir, Cortex or vanilla Prometheus
  786. const applicationSubType = (
  787. <Badge
  788. text={
  789. <span>
  790. <img
  791. style={{ width: 14, height: 14, verticalAlign: 'text-bottom' }}
  792. src={LOGOS[buildInfo.application ?? PromApplication.Prometheus]}
  793. />{' '}
  794. {buildInfo.application ? AppDisplayNames[buildInfo.application] : 'Unknown'}
  795. </span>
  796. }
  797. color={COLORS[buildInfo.application ?? PromApplication.Prometheus]}
  798. />
  799. );
  800. return (
  801. <div
  802. style={{
  803. display: 'grid',
  804. gridTemplateColumns: 'max-content max-content',
  805. rowGap: '0.5rem',
  806. columnGap: '2rem',
  807. marginTop: '1rem',
  808. }}
  809. >
  810. <div>Type</div>
  811. <div>{applicationSubType}</div>
  812. <>
  813. <div>Ruler API</div>
  814. {/* Prometheus does not have a Ruler API – so show that it is not supported */}
  815. {buildInfo.application === PromApplication.Prometheus && <div>{unsupported}</div>}
  816. {buildInfo.application !== PromApplication.Prometheus && (
  817. <div>{buildInfo.features.rulerApiEnabled ? enabled : disabled}</div>
  818. )}
  819. </>
  820. </div>
  821. );
  822. }
  823. async testDatasource() {
  824. const now = new Date().getTime();
  825. const request: DataQueryRequest<PromQuery> = {
  826. targets: [{ refId: 'test', expr: '1+1', instant: true }],
  827. requestId: `${this.id}-health`,
  828. scopedVars: {},
  829. dashboardId: 0,
  830. panelId: 0,
  831. interval: '1m',
  832. intervalMs: 60000,
  833. maxDataPoints: 1,
  834. range: {
  835. from: dateTime(now - 1000),
  836. to: dateTime(now),
  837. },
  838. } as DataQueryRequest<PromQuery>;
  839. const buildInfo = await this.getBuildInfo();
  840. return lastValueFrom(this.query(request))
  841. .then((res: DataQueryResponse) => {
  842. if (!res || !res.data || res.state !== LoadingState.Done) {
  843. return { status: 'error', message: `Error reading Prometheus: ${res?.error?.message}` };
  844. } else {
  845. return {
  846. status: 'success',
  847. message: 'Data source is working',
  848. details: buildInfo && {
  849. verboseMessage: this.getBuildInfoMessage(buildInfo),
  850. },
  851. };
  852. }
  853. })
  854. .catch((err: any) => {
  855. console.error('Prometheus Error', err);
  856. return { status: 'error', message: err.message };
  857. });
  858. }
  859. interpolateVariablesInQueries(queries: PromQuery[], scopedVars: ScopedVars): PromQuery[] {
  860. let expandedQueries = queries;
  861. if (queries && queries.length) {
  862. expandedQueries = queries.map((query) => {
  863. const expandedQuery = {
  864. ...query,
  865. datasource: this.getRef(),
  866. expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr),
  867. interval: this.templateSrv.replace(query.interval, scopedVars),
  868. };
  869. return expandedQuery;
  870. });
  871. }
  872. return expandedQueries;
  873. }
  874. getQueryHints(query: PromQuery, result: any[]) {
  875. return getQueryHints(query.expr ?? '', result, this);
  876. }
  877. getInitHints() {
  878. return getInitHints(this);
  879. }
  880. async loadRules() {
  881. try {
  882. const res = await this.metadataRequest('/api/v1/rules');
  883. const groups = res.data?.data?.groups;
  884. if (groups) {
  885. this.ruleMappings = extractRuleMappingFromGroups(groups);
  886. }
  887. } catch (e) {
  888. console.log('Rules API is experimental. Ignore next error.');
  889. console.error(e);
  890. }
  891. }
  892. async areExemplarsAvailable() {
  893. try {
  894. const res = await this.getResource('/api/v1/query_exemplars', {
  895. query: 'test',
  896. start: dateTime().subtract(30, 'minutes').valueOf(),
  897. end: dateTime().valueOf(),
  898. });
  899. if (res.data.status === 'success') {
  900. return true;
  901. }
  902. return false;
  903. } catch (err) {
  904. return false;
  905. }
  906. }
  907. modifyQuery(query: PromQuery, action: any): PromQuery {
  908. let expression = query.expr ?? '';
  909. switch (action.type) {
  910. case 'ADD_FILTER': {
  911. expression = addLabelToQuery(expression, action.key, action.value);
  912. break;
  913. }
  914. case 'ADD_FILTER_OUT': {
  915. expression = addLabelToQuery(expression, action.key, action.value, '!=');
  916. break;
  917. }
  918. case 'ADD_HISTOGRAM_QUANTILE': {
  919. expression = `histogram_quantile(0.95, sum(rate(${expression}[$__rate_interval])) by (le))`;
  920. break;
  921. }
  922. case 'ADD_RATE': {
  923. expression = `rate(${expression}[$__rate_interval])`;
  924. break;
  925. }
  926. case 'ADD_SUM': {
  927. expression = `sum(${expression.trim()}) by ($1)`;
  928. break;
  929. }
  930. case 'EXPAND_RULES': {
  931. if (action.mapping) {
  932. expression = expandRecordingRules(expression, action.mapping);
  933. }
  934. break;
  935. }
  936. default:
  937. break;
  938. }
  939. return { ...query, expr: expression };
  940. }
  941. getPrometheusTime(date: string | DateTime, roundUp: boolean) {
  942. if (typeof date === 'string') {
  943. date = dateMath.parse(date, roundUp)!;
  944. }
  945. return Math.ceil(date.valueOf() / 1000);
  946. }
  947. getTimeRangeParams(): { start: string; end: string } {
  948. const range = this.timeSrv.timeRange();
  949. return {
  950. start: this.getPrometheusTime(range.from, false).toString(),
  951. end: this.getPrometheusTime(range.to, true).toString(),
  952. };
  953. }
  954. getOriginalMetricName(labelData: { [key: string]: string }) {
  955. return getOriginalMetricName(labelData);
  956. }
  957. enhanceExprWithAdHocFilters(expr: string) {
  958. const adhocFilters = this.templateSrv.getAdhocFilters(this.name);
  959. const finalQuery = adhocFilters.reduce((acc: string, filter: { key?: any; operator?: any; value?: any }) => {
  960. const { key, operator } = filter;
  961. let { value } = filter;
  962. if (operator === '=~' || operator === '!~') {
  963. value = prometheusRegularEscape(value);
  964. }
  965. return addLabelToQuery(acc, key, value, operator);
  966. }, expr);
  967. return finalQuery;
  968. }
  969. // Used when running queries trough backend
  970. filterQuery(query: PromQuery): boolean {
  971. if (query.hide || !query.expr) {
  972. return false;
  973. }
  974. return true;
  975. }
  976. // Used when running queries trough backend
  977. applyTemplateVariables(target: PromQuery, scopedVars: ScopedVars): Record<string, any> {
  978. const variables = cloneDeep(scopedVars);
  979. // We want to interpolate these variables on backend
  980. delete variables.__interval;
  981. delete variables.__interval_ms;
  982. //Add ad hoc filters
  983. const expr = this.enhanceExprWithAdHocFilters(target.expr);
  984. return {
  985. ...target,
  986. legendFormat: this.templateSrv.replace(target.legendFormat, variables),
  987. expr: this.templateSrv.replace(expr, variables, this.interpolateQueryExpr),
  988. interval: this.templateSrv.replace(target.interval, variables),
  989. };
  990. }
  991. getVariables(): string[] {
  992. return this.templateSrv.getVariables().map((v) => `$${v.name}`);
  993. }
  994. interpolateString(string: string) {
  995. return this.templateSrv.replace(string, undefined, this.interpolateQueryExpr);
  996. }
  997. }
  998. /**
  999. * Align query range to step.
  1000. * Rounds start and end down to a multiple of step.
  1001. * @param start Timestamp marking the beginning of the range.
  1002. * @param end Timestamp marking the end of the range.
  1003. * @param step Interval to align start and end with.
  1004. * @param utcOffsetSec Number of seconds current timezone is offset from UTC
  1005. */
  1006. export function alignRange(
  1007. start: number,
  1008. end: number,
  1009. step: number,
  1010. utcOffsetSec: number
  1011. ): { end: number; start: number } {
  1012. const alignedEnd = Math.floor((end + utcOffsetSec) / step) * step - utcOffsetSec;
  1013. const alignedStart = Math.floor((start + utcOffsetSec) / step) * step - utcOffsetSec;
  1014. return {
  1015. end: alignedEnd,
  1016. start: alignedStart,
  1017. };
  1018. }
  1019. export function extractRuleMappingFromGroups(groups: any[]) {
  1020. return groups.reduce(
  1021. (mapping, group) =>
  1022. group.rules
  1023. .filter((rule: any) => rule.type === 'recording')
  1024. .reduce(
  1025. (acc: { [key: string]: string }, rule: any) => ({
  1026. ...acc,
  1027. [rule.name]: rule.query,
  1028. }),
  1029. mapping
  1030. ),
  1031. {}
  1032. );
  1033. }
  1034. // NOTE: these two functions are very similar to the escapeLabelValueIn* functions
  1035. // in language_utils.ts, but they are not exactly the same algorithm, and we found
  1036. // no way to reuse one in the another or vice versa.
  1037. export function prometheusRegularEscape(value: any) {
  1038. return typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, "\\\\'") : value;
  1039. }
  1040. export function prometheusSpecialRegexEscape(value: any) {
  1041. return typeof value === 'string' ? value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&') : value;
  1042. }