result_transformer.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. import { descending, deviation } from 'd3';
  2. import { partition, groupBy } from 'lodash';
  3. import {
  4. ArrayDataFrame,
  5. ArrayVector,
  6. DataFrame,
  7. DataLink,
  8. DataTopic,
  9. Field,
  10. FieldType,
  11. formatLabels,
  12. getDisplayProcessor,
  13. Labels,
  14. MutableField,
  15. ScopedVars,
  16. TIME_SERIES_TIME_FIELD_NAME,
  17. TIME_SERIES_VALUE_FIELD_NAME,
  18. DataQueryResponse,
  19. DataQueryRequest,
  20. PreferredVisualisationType,
  21. CoreApp,
  22. DataFrameType,
  23. } from '@grafana/data';
  24. import { FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
  25. import { renderLegendFormat } from './legend';
  26. import {
  27. ExemplarTraceIdDestination,
  28. isExemplarData,
  29. isMatrixData,
  30. MatrixOrVectorResult,
  31. PromDataSuccessResponse,
  32. PromMetric,
  33. PromQuery,
  34. PromQueryRequest,
  35. PromValue,
  36. TransformOptions,
  37. } from './types';
  38. // handles case-insensitive Inf, +Inf, -Inf (with optional "inity" suffix)
  39. const INFINITY_SAMPLE_REGEX = /^[+-]?inf(?:inity)?$/i;
  40. interface TimeAndValue {
  41. [TIME_SERIES_TIME_FIELD_NAME]: number;
  42. [TIME_SERIES_VALUE_FIELD_NAME]: number;
  43. }
  44. const isTableResult = (dataFrame: DataFrame, options: DataQueryRequest<PromQuery>): boolean => {
  45. // We want to process vector and scalar results in Explore as table
  46. if (
  47. options.app === CoreApp.Explore &&
  48. (dataFrame.meta?.custom?.resultType === 'vector' || dataFrame.meta?.custom?.resultType === 'scalar')
  49. ) {
  50. return true;
  51. }
  52. // We want to process all dataFrames with target.format === 'table' as table
  53. const target = options.targets.find((target) => target.refId === dataFrame.refId);
  54. return target?.format === 'table';
  55. };
  56. const isHeatmapResult = (dataFrame: DataFrame, options: DataQueryRequest<PromQuery>): boolean => {
  57. const target = options.targets.find((target) => target.refId === dataFrame.refId);
  58. return target?.format === 'heatmap';
  59. };
  60. // V2 result trasnformer used to transform query results from queries that were run trough prometheus backend
  61. export function transformV2(
  62. response: DataQueryResponse,
  63. request: DataQueryRequest<PromQuery>,
  64. options: { exemplarTraceIdDestinations?: ExemplarTraceIdDestination[] }
  65. ) {
  66. const [tableFrames, framesWithoutTable] = partition<DataFrame>(response.data, (df) => isTableResult(df, request));
  67. const processedTableFrames = transformDFToTable(tableFrames);
  68. const [exemplarFrames, framesWithoutTableAndExemplars] = partition<DataFrame>(
  69. framesWithoutTable,
  70. (df) => df.meta?.custom?.resultType === 'exemplar'
  71. );
  72. // EXEMPLAR FRAMES: We enrich exemplar frames with data links and add dataTopic meta info
  73. const { exemplarTraceIdDestinations: destinations } = options;
  74. const processedExemplarFrames = exemplarFrames.map((dataFrame) => {
  75. if (destinations?.length) {
  76. for (const exemplarTraceIdDestination of destinations) {
  77. const traceIDField = dataFrame.fields.find((field) => field.name === exemplarTraceIdDestination.name);
  78. if (traceIDField) {
  79. const links = getDataLinks(exemplarTraceIdDestination);
  80. traceIDField.config.links = traceIDField.config.links?.length
  81. ? [...traceIDField.config.links, ...links]
  82. : links;
  83. }
  84. }
  85. }
  86. return { ...dataFrame, meta: { ...dataFrame.meta, dataTopic: DataTopic.Annotations } };
  87. });
  88. const [heatmapResults, framesWithoutTableHeatmapsAndExemplars] = partition<DataFrame>(
  89. framesWithoutTableAndExemplars,
  90. (df) => isHeatmapResult(df, request)
  91. );
  92. const processedHeatmapFrames = mergeHeatmapFrames(
  93. transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel))
  94. );
  95. // Everything else is processed as time_series result and graph preferredVisualisationType
  96. const otherFrames = framesWithoutTableHeatmapsAndExemplars.map((dataFrame) => {
  97. const df = {
  98. ...dataFrame,
  99. meta: {
  100. ...dataFrame.meta,
  101. preferredVisualisationType: 'graph',
  102. },
  103. } as DataFrame;
  104. return df;
  105. });
  106. return {
  107. ...response,
  108. data: [...otherFrames, ...processedTableFrames, ...processedHeatmapFrames, ...processedExemplarFrames],
  109. };
  110. }
  111. export function transformDFToTable(dfs: DataFrame[]): DataFrame[] {
  112. // If no dataFrames or if 1 dataFrames with no values, return original dataFrame
  113. if (dfs.length === 0 || (dfs.length === 1 && dfs[0].length === 0)) {
  114. return dfs;
  115. }
  116. // Group results by refId and process dataFrames with the same refId as 1 dataFrame
  117. const dataFramesByRefId = groupBy(dfs, 'refId');
  118. const refIds = Object.keys(dataFramesByRefId);
  119. const frames = refIds.map((refId) => {
  120. // Create timeField, valueField and labelFields
  121. const valueText = getValueText(refIds.length, refId);
  122. const valueField = getValueField({ data: [], valueName: valueText });
  123. const timeField = getTimeField([]);
  124. const labelFields: MutableField[] = [];
  125. // Fill labelsFields with labels from dataFrames
  126. dataFramesByRefId[refId].forEach((df) => {
  127. const frameValueField = df.fields[1];
  128. const promLabels = frameValueField.labels ?? {};
  129. Object.keys(promLabels)
  130. .sort()
  131. .forEach((label) => {
  132. // If we don't have label in labelFields, add it
  133. if (!labelFields.some((l) => l.name === label)) {
  134. const numberField = label === 'le';
  135. labelFields.push({
  136. name: label,
  137. config: { filterable: true },
  138. type: numberField ? FieldType.number : FieldType.string,
  139. values: new ArrayVector(),
  140. });
  141. }
  142. });
  143. });
  144. // Fill valueField, timeField and labelFields with values
  145. dataFramesByRefId[refId].forEach((df) => {
  146. df.fields[0].values.toArray().forEach((value) => timeField.values.add(value));
  147. df.fields[1].values.toArray().forEach((value) => {
  148. valueField.values.add(parseSampleValue(value));
  149. const labelsForField = df.fields[1].labels ?? {};
  150. labelFields.forEach((field) => field.values.add(getLabelValue(labelsForField, field.name)));
  151. });
  152. });
  153. const fields = [timeField, ...labelFields, valueField];
  154. return {
  155. refId,
  156. fields,
  157. meta: { ...dfs[0].meta, preferredVisualisationType: 'table' as PreferredVisualisationType },
  158. length: timeField.values.length,
  159. };
  160. });
  161. return frames;
  162. }
  163. function getValueText(responseLength: number, refId = '') {
  164. return responseLength > 1 ? `Value #${refId}` : 'Value';
  165. }
  166. export function transform(
  167. response: FetchResponse<PromDataSuccessResponse>,
  168. transformOptions: {
  169. query: PromQueryRequest;
  170. exemplarTraceIdDestinations?: ExemplarTraceIdDestination[];
  171. target: PromQuery;
  172. responseListLength: number;
  173. scopedVars?: ScopedVars;
  174. }
  175. ) {
  176. // Create options object from transformOptions
  177. const options: TransformOptions = {
  178. format: transformOptions.target.format,
  179. step: transformOptions.query.step,
  180. legendFormat: transformOptions.target.legendFormat,
  181. start: transformOptions.query.start,
  182. end: transformOptions.query.end,
  183. query: transformOptions.query.expr,
  184. responseListLength: transformOptions.responseListLength,
  185. scopedVars: transformOptions.scopedVars,
  186. refId: transformOptions.target.refId,
  187. valueWithRefId: transformOptions.target.valueWithRefId,
  188. meta: {
  189. // Fix for showing of Prometheus results in Explore table
  190. preferredVisualisationType: transformOptions.query.instant ? 'table' : 'graph',
  191. },
  192. };
  193. const prometheusResult = response.data.data;
  194. if (isExemplarData(prometheusResult)) {
  195. const events: TimeAndValue[] = [];
  196. prometheusResult.forEach((exemplarData) => {
  197. const data = exemplarData.exemplars.map((exemplar) => {
  198. return {
  199. [TIME_SERIES_TIME_FIELD_NAME]: exemplar.timestamp * 1000,
  200. [TIME_SERIES_VALUE_FIELD_NAME]: exemplar.value,
  201. ...exemplar.labels,
  202. ...exemplarData.seriesLabels,
  203. };
  204. });
  205. events.push(...data);
  206. });
  207. // Grouping exemplars by step
  208. const sampledExemplars = sampleExemplars(events, options);
  209. const dataFrame = new ArrayDataFrame(sampledExemplars);
  210. dataFrame.meta = { dataTopic: DataTopic.Annotations };
  211. // Add data links if configured
  212. if (transformOptions.exemplarTraceIdDestinations?.length) {
  213. for (const exemplarTraceIdDestination of transformOptions.exemplarTraceIdDestinations) {
  214. const traceIDField = dataFrame.fields.find((field) => field.name === exemplarTraceIdDestination.name);
  215. if (traceIDField) {
  216. const links = getDataLinks(exemplarTraceIdDestination);
  217. traceIDField.config.links = traceIDField.config.links?.length
  218. ? [...traceIDField.config.links, ...links]
  219. : links;
  220. }
  221. }
  222. }
  223. return [dataFrame];
  224. }
  225. if (!prometheusResult?.result) {
  226. return [];
  227. }
  228. // Return early if result type is scalar
  229. if (prometheusResult.resultType === 'scalar') {
  230. return [
  231. {
  232. meta: options.meta,
  233. refId: options.refId,
  234. length: 1,
  235. fields: [getTimeField([prometheusResult.result]), getValueField({ data: [prometheusResult.result] })],
  236. },
  237. ];
  238. }
  239. // Return early again if the format is table, this needs special transformation.
  240. if (options.format === 'table') {
  241. const tableData = transformMetricDataToTable(prometheusResult.result, options);
  242. return [tableData];
  243. }
  244. // Process matrix and vector results to DataFrame
  245. const dataFrame: DataFrame[] = [];
  246. prometheusResult.result.forEach((data: MatrixOrVectorResult) => dataFrame.push(transformToDataFrame(data, options)));
  247. // When format is heatmap use the already created data frames and transform it more
  248. if (options.format === 'heatmap') {
  249. return mergeHeatmapFrames(transformToHistogramOverTime(dataFrame.sort(sortSeriesByLabel)));
  250. }
  251. // Return matrix or vector result as DataFrame[]
  252. return dataFrame;
  253. }
  254. function getDataLinks(options: ExemplarTraceIdDestination): DataLink[] {
  255. const dataLinks: DataLink[] = [];
  256. if (options.datasourceUid) {
  257. const dataSourceSrv = getDataSourceSrv();
  258. const dsSettings = dataSourceSrv.getInstanceSettings(options.datasourceUid);
  259. dataLinks.push({
  260. title: options.urlDisplayLabel || `Query with ${dsSettings?.name}`,
  261. url: '',
  262. internal: {
  263. query: { query: '${__value.raw}', queryType: 'traceId' },
  264. datasourceUid: options.datasourceUid,
  265. datasourceName: dsSettings?.name ?? 'Data source not found',
  266. },
  267. });
  268. }
  269. if (options.url) {
  270. dataLinks.push({
  271. title: options.urlDisplayLabel || `Go to ${options.url}`,
  272. url: options.url,
  273. targetBlank: true,
  274. });
  275. }
  276. return dataLinks;
  277. }
  278. /**
  279. * Reduce the density of the exemplars by making sure that the highest value exemplar is included
  280. * and then only the ones that are 2 times the standard deviation of the all the values.
  281. * This makes sure not to show too many dots near each other.
  282. */
  283. function sampleExemplars(events: TimeAndValue[], options: TransformOptions) {
  284. const step = options.step || 15;
  285. const bucketedExemplars: { [ts: string]: TimeAndValue[] } = {};
  286. const values: number[] = [];
  287. for (const exemplar of events) {
  288. // Align exemplar timestamp to nearest step second
  289. const alignedTs = String(Math.floor(exemplar[TIME_SERIES_TIME_FIELD_NAME] / 1000 / step) * step * 1000);
  290. if (!bucketedExemplars[alignedTs]) {
  291. // New bucket found
  292. bucketedExemplars[alignedTs] = [];
  293. }
  294. bucketedExemplars[alignedTs].push(exemplar);
  295. values.push(exemplar[TIME_SERIES_VALUE_FIELD_NAME]);
  296. }
  297. // Getting exemplars from each bucket
  298. const standardDeviation = deviation(values);
  299. const sampledBuckets = Object.keys(bucketedExemplars).sort();
  300. const sampledExemplars = [];
  301. for (const ts of sampledBuckets) {
  302. const exemplarsInBucket = bucketedExemplars[ts];
  303. if (exemplarsInBucket.length === 1) {
  304. sampledExemplars.push(exemplarsInBucket[0]);
  305. } else {
  306. // Choose which values to sample
  307. const bucketValues = exemplarsInBucket.map((ex) => ex[TIME_SERIES_VALUE_FIELD_NAME]).sort(descending);
  308. const sampledBucketValues = bucketValues.reduce((acc: number[], curr) => {
  309. if (acc.length === 0) {
  310. // First value is max and is always added
  311. acc.push(curr);
  312. } else {
  313. // Then take values only when at least 2 standard deviation distance to previously taken value
  314. const prev = acc[acc.length - 1];
  315. if (standardDeviation && prev - curr >= 2 * standardDeviation) {
  316. acc.push(curr);
  317. }
  318. }
  319. return acc;
  320. }, []);
  321. // Find the exemplars for the sampled values
  322. sampledExemplars.push(
  323. ...sampledBucketValues.map(
  324. (value) => exemplarsInBucket.find((ex) => ex[TIME_SERIES_VALUE_FIELD_NAME] === value)!
  325. )
  326. );
  327. }
  328. }
  329. return sampledExemplars;
  330. }
  331. /**
  332. * Transforms matrix and vector result from Prometheus result to DataFrame
  333. */
  334. function transformToDataFrame(data: MatrixOrVectorResult, options: TransformOptions): DataFrame {
  335. const { name, labels } = createLabelInfo(data.metric, options);
  336. const fields: Field[] = [];
  337. if (isMatrixData(data)) {
  338. const stepMs = options.step ? options.step * 1000 : NaN;
  339. let baseTimestamp = options.start * 1000;
  340. const dps: PromValue[] = [];
  341. for (const value of data.values) {
  342. let dpValue: number | null = parseSampleValue(value[1]);
  343. if (isNaN(dpValue)) {
  344. dpValue = null;
  345. }
  346. const timestamp = value[0] * 1000;
  347. for (let t = baseTimestamp; t < timestamp; t += stepMs) {
  348. dps.push([t, null]);
  349. }
  350. baseTimestamp = timestamp + stepMs;
  351. dps.push([timestamp, dpValue]);
  352. }
  353. const endTimestamp = options.end * 1000;
  354. for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) {
  355. dps.push([t, null]);
  356. }
  357. fields.push(getTimeField(dps, true));
  358. fields.push(getValueField({ data: dps, parseValue: false, labels, displayNameFromDS: name }));
  359. } else {
  360. fields.push(getTimeField([data.value]));
  361. fields.push(getValueField({ data: [data.value], labels, displayNameFromDS: name }));
  362. }
  363. return {
  364. meta: options.meta,
  365. refId: options.refId,
  366. length: fields[0].values.length,
  367. fields,
  368. name,
  369. };
  370. }
  371. function transformMetricDataToTable(md: MatrixOrVectorResult[], options: TransformOptions): DataFrame {
  372. if (!md || md.length === 0) {
  373. return {
  374. meta: options.meta,
  375. refId: options.refId,
  376. length: 0,
  377. fields: [],
  378. };
  379. }
  380. const valueText = options.responseListLength > 1 || options.valueWithRefId ? `Value #${options.refId}` : 'Value';
  381. const timeField = getTimeField([]);
  382. const metricFields = Object.keys(md.reduce((acc, series) => ({ ...acc, ...series.metric }), {}))
  383. .sort()
  384. .map((label) => {
  385. // Labels have string field type, otherwise table tries to figure out the type which can result in unexpected results
  386. // Only "le" label has a number field type
  387. const numberField = label === 'le';
  388. return {
  389. name: label,
  390. config: { filterable: true },
  391. type: numberField ? FieldType.number : FieldType.string,
  392. values: new ArrayVector(),
  393. };
  394. });
  395. const valueField = getValueField({ data: [], valueName: valueText });
  396. md.forEach((d) => {
  397. if (isMatrixData(d)) {
  398. d.values.forEach((val) => {
  399. timeField.values.add(val[0] * 1000);
  400. metricFields.forEach((metricField) => metricField.values.add(getLabelValue(d.metric, metricField.name)));
  401. valueField.values.add(parseSampleValue(val[1]));
  402. });
  403. } else {
  404. timeField.values.add(d.value[0] * 1000);
  405. metricFields.forEach((metricField) => metricField.values.add(getLabelValue(d.metric, metricField.name)));
  406. valueField.values.add(parseSampleValue(d.value[1]));
  407. }
  408. });
  409. return {
  410. meta: options.meta,
  411. refId: options.refId,
  412. length: timeField.values.length,
  413. fields: [timeField, ...metricFields, valueField],
  414. };
  415. }
  416. function getLabelValue(metric: PromMetric, label: string): string | number {
  417. if (metric.hasOwnProperty(label)) {
  418. if (label === 'le') {
  419. return parseSampleValue(metric[label]);
  420. }
  421. return metric[label];
  422. }
  423. return '';
  424. }
  425. function getTimeField(data: PromValue[], isMs = false): MutableField {
  426. return {
  427. name: TIME_SERIES_TIME_FIELD_NAME,
  428. type: FieldType.time,
  429. config: {},
  430. values: new ArrayVector<number>(data.map((val) => (isMs ? val[0] : val[0] * 1000))),
  431. };
  432. }
  433. type ValueFieldOptions = {
  434. data: PromValue[];
  435. valueName?: string;
  436. parseValue?: boolean;
  437. labels?: Labels;
  438. displayNameFromDS?: string;
  439. };
  440. function getValueField({
  441. data,
  442. valueName = TIME_SERIES_VALUE_FIELD_NAME,
  443. parseValue = true,
  444. labels,
  445. displayNameFromDS,
  446. }: ValueFieldOptions): MutableField {
  447. return {
  448. name: valueName,
  449. type: FieldType.number,
  450. display: getDisplayProcessor(),
  451. config: {
  452. displayNameFromDS,
  453. },
  454. labels,
  455. values: new ArrayVector<number | null>(data.map((val) => (parseValue ? parseSampleValue(val[1]) : val[1]))),
  456. };
  457. }
  458. function createLabelInfo(labels: { [key: string]: string }, options: TransformOptions) {
  459. if (options?.legendFormat) {
  460. const title = renderLegendFormat(getTemplateSrv().replace(options.legendFormat, options?.scopedVars), labels);
  461. return { name: title, labels };
  462. }
  463. const { __name__, ...labelsWithoutName } = labels;
  464. const labelPart = formatLabels(labelsWithoutName);
  465. let title = `${__name__ ?? ''}${labelPart}`;
  466. if (!title) {
  467. title = options.query;
  468. }
  469. return { name: title, labels: labelsWithoutName };
  470. }
  471. export function getOriginalMetricName(labelData: { [key: string]: string }) {
  472. const metricName = labelData.__name__ || '';
  473. delete labelData.__name__;
  474. const labelPart = Object.entries(labelData)
  475. .map((label) => `${label[0]}="${label[1]}"`)
  476. .join(',');
  477. return `${metricName}{${labelPart}}`;
  478. }
  479. function mergeHeatmapFrames(frames: DataFrame[]): DataFrame[] {
  480. if (frames.length === 0) {
  481. return [];
  482. }
  483. const timeField = frames[0].fields.find((field) => field.type === FieldType.time)!;
  484. const countFields = frames.map((frame) => {
  485. let field = frame.fields.find((field) => field.type === FieldType.number)!;
  486. return {
  487. ...field,
  488. name: field.config.displayNameFromDS!,
  489. };
  490. });
  491. return [
  492. {
  493. ...frames[0],
  494. meta: {
  495. ...frames[0].meta,
  496. type: DataFrameType.HeatmapRows,
  497. },
  498. fields: [timeField!, ...countFields],
  499. },
  500. ];
  501. }
  502. function transformToHistogramOverTime(seriesList: DataFrame[]) {
  503. /* t1 = timestamp1, t2 = timestamp2 etc.
  504. t1 t2 t3 t1 t2 t3
  505. le10 10 10 0 => 10 10 0
  506. le20 20 10 30 => 10 0 30
  507. le30 30 10 35 => 10 0 5
  508. */
  509. for (let i = seriesList.length - 1; i > 0; i--) {
  510. const topSeries = seriesList[i].fields.find((s) => s.name === TIME_SERIES_VALUE_FIELD_NAME);
  511. const bottomSeries = seriesList[i - 1].fields.find((s) => s.name === TIME_SERIES_VALUE_FIELD_NAME);
  512. if (!topSeries || !bottomSeries) {
  513. throw new Error('Prometheus heatmap transform error: data should be a time series');
  514. }
  515. for (let j = 0; j < topSeries.values.length; j++) {
  516. const bottomPoint = bottomSeries.values.get(j) || [0];
  517. topSeries.values.toArray()[j] -= bottomPoint;
  518. }
  519. }
  520. return seriesList;
  521. }
  522. function sortSeriesByLabel(s1: DataFrame, s2: DataFrame): number {
  523. let le1, le2;
  524. try {
  525. // fail if not integer. might happen with bad queries
  526. le1 = parseSampleValue(s1.name ?? '');
  527. le2 = parseSampleValue(s2.name ?? '');
  528. } catch (err) {
  529. console.error(err);
  530. return 0;
  531. }
  532. if (le1 > le2) {
  533. return 1;
  534. }
  535. if (le1 < le2) {
  536. return -1;
  537. }
  538. return 0;
  539. }
  540. /** @internal */
  541. export function parseSampleValue(value: string): number {
  542. if (INFINITY_SAMPLE_REGEX.test(value)) {
  543. return value[0] === '-' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
  544. }
  545. return parseFloat(value);
  546. }