result_transformer.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import _ from 'lodash';
  2. import moment from 'moment';
  3. import {
  4. LogLevel,
  5. LogsMetaItem,
  6. LogsModel,
  7. LogRow,
  8. LogsStream,
  9. LogsStreamEntry,
  10. LogsStreamLabels,
  11. } from 'app/core/logs_model';
  12. import { DEFAULT_LIMIT } from './datasource';
  13. /**
  14. * Returns the log level of a log line.
  15. * Parse the line for level words. If no level is found, it returns `LogLevel.none`.
  16. *
  17. * Example: `getLogLevel('WARN 1999-12-31 this is great') // LogLevel.warn`
  18. */
  19. export function getLogLevel(line: string): LogLevel {
  20. if (!line) {
  21. return LogLevel.none;
  22. }
  23. let level: LogLevel;
  24. Object.keys(LogLevel).forEach(key => {
  25. if (!level) {
  26. const regexp = new RegExp(`\\b${key}\\b`, 'i');
  27. if (regexp.test(line)) {
  28. level = LogLevel[key];
  29. }
  30. }
  31. });
  32. if (!level) {
  33. level = LogLevel.none;
  34. }
  35. return level;
  36. }
  37. /**
  38. * Regexp to extract Prometheus-style labels
  39. */
  40. const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
  41. /**
  42. * Returns a map of label keys to value from an input selector string.
  43. *
  44. * Example: `parseLabels('{job="foo", instance="bar"}) // {job: "foo", instance: "bar"}`
  45. */
  46. export function parseLabels(labels: string): LogsStreamLabels {
  47. const labelsByKey: LogsStreamLabels = {};
  48. labels.replace(labelRegexp, (_, key, operator, value) => {
  49. labelsByKey[key] = value;
  50. return '';
  51. });
  52. return labelsByKey;
  53. }
  54. /**
  55. * Returns a map labels that are common to the given label sets.
  56. */
  57. export function findCommonLabels(labelsSets: LogsStreamLabels[]): LogsStreamLabels {
  58. return labelsSets.reduce((acc, labels) => {
  59. if (!labels) {
  60. throw new Error('Need parsed labels to find common labels.');
  61. }
  62. if (!acc) {
  63. // Initial set
  64. acc = { ...labels };
  65. } else {
  66. // Remove incoming labels that are missing or not matching in value
  67. Object.keys(labels).forEach(key => {
  68. if (acc[key] === undefined || acc[key] !== labels[key]) {
  69. delete acc[key];
  70. }
  71. });
  72. // Remove common labels that are missing from incoming label set
  73. Object.keys(acc).forEach(key => {
  74. if (labels[key] === undefined) {
  75. delete acc[key];
  76. }
  77. });
  78. }
  79. return acc;
  80. }, undefined);
  81. }
  82. /**
  83. * Returns a map of labels that are in `labels`, but not in `commonLabels`.
  84. */
  85. export function findUniqueLabels(labels: LogsStreamLabels, commonLabels: LogsStreamLabels): LogsStreamLabels {
  86. const uncommonLabels: LogsStreamLabels = { ...labels };
  87. Object.keys(commonLabels).forEach(key => {
  88. delete uncommonLabels[key];
  89. });
  90. return uncommonLabels;
  91. }
  92. /**
  93. * Serializes the given labels to a string.
  94. */
  95. export function formatLabels(labels: LogsStreamLabels, defaultValue = ''): string {
  96. if (!labels || Object.keys(labels).length === 0) {
  97. return defaultValue;
  98. }
  99. const labelKeys = Object.keys(labels).sort();
  100. const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(', ');
  101. return ['{', cleanSelector, '}'].join('');
  102. }
  103. export function processEntry(entry: LogsStreamEntry, labels: string, uniqueLabels: string, search: string): LogRow {
  104. const { line, timestamp } = entry;
  105. // Assumes unique-ness, needs nanosec precision for timestamp
  106. const key = `EK${timestamp}${labels}`;
  107. const time = moment(timestamp);
  108. const timeEpochMs = time.valueOf();
  109. const timeFromNow = time.fromNow();
  110. const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
  111. const logLevel = getLogLevel(line);
  112. return {
  113. key,
  114. labels,
  115. logLevel,
  116. timeFromNow,
  117. timeEpochMs,
  118. timeLocal,
  119. uniqueLabels,
  120. entry: line,
  121. searchWords: search ? [search] : [],
  122. timestamp: timestamp,
  123. };
  124. }
  125. export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT): LogsModel {
  126. // Find unique labels for each stream
  127. streams = streams.map(stream => ({
  128. ...stream,
  129. parsedLabels: parseLabels(stream.labels),
  130. }));
  131. const commonLabels = findCommonLabels(streams.map(model => model.parsedLabels));
  132. streams = streams.map(stream => ({
  133. ...stream,
  134. uniqueLabels: formatLabels(findUniqueLabels(stream.parsedLabels, commonLabels)),
  135. }));
  136. // Merge stream entries into single list of log rows
  137. const sortedRows: LogRow[] = _.chain(streams)
  138. .reduce(
  139. (acc: LogRow[], stream: LogsStream) => [
  140. ...acc,
  141. ...stream.entries.map(entry => processEntry(entry, stream.labels, stream.uniqueLabels, stream.search)),
  142. ],
  143. []
  144. )
  145. .sortBy('timestamp')
  146. .reverse()
  147. .value();
  148. // Meta data to display in status
  149. const meta: LogsMetaItem[] = [];
  150. if (_.size(commonLabels) > 0) {
  151. meta.push({
  152. label: 'Common labels',
  153. value: formatLabels(commonLabels),
  154. });
  155. }
  156. if (limit) {
  157. meta.push({
  158. label: 'Limit',
  159. value: `${limit} (${sortedRows.length} returned)`,
  160. });
  161. }
  162. return {
  163. meta,
  164. rows: sortedRows,
  165. };
  166. }