result_transformer.ts 5.4 KB

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