Logs.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import _ from 'lodash';
  2. import React, { PureComponent } from 'react';
  3. import Highlighter from 'react-highlight-words';
  4. import classnames from 'classnames';
  5. import * as rangeUtil from 'app/core/utils/rangeutil';
  6. import { RawTimeRange } from 'app/types/series';
  7. import {
  8. LogsDedupStrategy,
  9. LogsModel,
  10. dedupLogRows,
  11. filterLogLevels,
  12. getParser,
  13. LogLevel,
  14. LogsMetaKind,
  15. LogsLabelStat,
  16. LogsParser,
  17. LogRow,
  18. calculateFieldStats,
  19. } from 'app/core/logs_model';
  20. import { findHighlightChunksInText } from 'app/core/utils/text';
  21. import { Switch } from 'app/core/components/Switch/Switch';
  22. import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
  23. import Graph from './Graph';
  24. import LogLabels, { Stats } from './LogLabels';
  25. const PREVIEW_LIMIT = 100;
  26. const graphOptions = {
  27. series: {
  28. stack: true,
  29. bars: {
  30. show: true,
  31. lineWidth: 5,
  32. // barWidth: 10,
  33. },
  34. // stack: true,
  35. },
  36. yaxis: {
  37. tickDecimals: 0,
  38. },
  39. };
  40. /**
  41. * Renders a highlighted field.
  42. * When hovering, a stats icon is shown.
  43. */
  44. const FieldHighlight = onClick => props => {
  45. return (
  46. <span className={props.className} style={props.style}>
  47. {props.children}
  48. <span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
  49. </span>
  50. );
  51. };
  52. interface RowProps {
  53. highlighterExpressions?: string[];
  54. row: LogRow;
  55. showDuplicates: boolean;
  56. showLabels: boolean | null; // Tristate: null means auto
  57. showLocalTime: boolean;
  58. showUtc: boolean;
  59. getRows: () => LogRow[];
  60. onClickLabel?: (label: string, value: string) => void;
  61. }
  62. interface RowState {
  63. fieldCount: number;
  64. fieldLabel: string;
  65. fieldStats: LogsLabelStat[];
  66. fieldValue: string;
  67. parsed: boolean;
  68. parser: LogsParser;
  69. parsedFieldHighlights: string[];
  70. showFieldStats: boolean;
  71. }
  72. /**
  73. * Renders a log line.
  74. *
  75. * When user hovers over it for a certain time, it lazily parses the log line.
  76. * Once a parser is found, it will determine fields, that will be highlighted.
  77. * When the user requests stats for a field, they will be calculated and rendered below the row.
  78. */
  79. class Row extends PureComponent<RowProps, RowState> {
  80. mouseMessageTimer: NodeJS.Timer;
  81. state = {
  82. fieldCount: 0,
  83. fieldLabel: null,
  84. fieldStats: null,
  85. fieldValue: null,
  86. parsed: false,
  87. parser: null,
  88. parsedFieldHighlights: [],
  89. showFieldStats: false,
  90. };
  91. componentWillUnmount() {
  92. clearTimeout(this.mouseMessageTimer);
  93. }
  94. onClickClose = () => {
  95. this.setState({ showFieldStats: false });
  96. };
  97. onClickHighlight = (fieldText: string) => {
  98. const { getRows } = this.props;
  99. const { parser } = this.state;
  100. const fieldMatch = fieldText.match(parser.fieldRegex);
  101. if (fieldMatch) {
  102. const allRows = getRows();
  103. // Build value-agnostic row matcher based on the field label
  104. const fieldLabel = fieldMatch[1];
  105. const fieldValue = fieldMatch[2];
  106. const matcher = parser.buildMatcher(fieldLabel);
  107. const fieldStats = calculateFieldStats(allRows, matcher);
  108. const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
  109. this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
  110. }
  111. };
  112. onMouseOverMessage = () => {
  113. // Don't parse right away, user might move along
  114. this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
  115. };
  116. onMouseOutMessage = () => {
  117. clearTimeout(this.mouseMessageTimer);
  118. this.setState({ parsed: false });
  119. };
  120. parseMessage = () => {
  121. if (!this.state.parsed) {
  122. const { row } = this.props;
  123. const parser = getParser(row.entry);
  124. if (parser) {
  125. // Use parser to highlight detected fields
  126. const parsedFieldHighlights = [];
  127. this.props.row.entry.replace(new RegExp(parser.fieldRegex, 'g'), substring => {
  128. parsedFieldHighlights.push(substring.trim());
  129. return '';
  130. });
  131. this.setState({ parsedFieldHighlights, parsed: true, parser });
  132. }
  133. }
  134. };
  135. render() {
  136. const {
  137. getRows,
  138. highlighterExpressions,
  139. onClickLabel,
  140. row,
  141. showDuplicates,
  142. showLabels,
  143. showLocalTime,
  144. showUtc,
  145. } = this.props;
  146. const {
  147. fieldCount,
  148. fieldLabel,
  149. fieldStats,
  150. fieldValue,
  151. parsed,
  152. parsedFieldHighlights,
  153. showFieldStats,
  154. } = this.state;
  155. const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
  156. const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
  157. const needsHighlighter = highlights && highlights.length > 0;
  158. const highlightClassName = classnames('logs-row__match-highlight', {
  159. 'logs-row__match-highlight--preview': previewHighlights,
  160. });
  161. return (
  162. <div className="logs-row">
  163. {showDuplicates && (
  164. <div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
  165. )}
  166. <div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
  167. {showUtc && (
  168. <div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
  169. {row.timestamp}
  170. </div>
  171. )}
  172. {showLocalTime && (
  173. <div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
  174. {row.timeLocal}
  175. </div>
  176. )}
  177. {showLabels && (
  178. <div className="logs-row__labels">
  179. <LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
  180. </div>
  181. )}
  182. <div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
  183. {parsed && (
  184. <Highlighter
  185. autoEscape
  186. highlightTag={FieldHighlight(this.onClickHighlight)}
  187. textToHighlight={row.entry}
  188. searchWords={parsedFieldHighlights}
  189. highlightClassName="logs-row__field-highlight"
  190. />
  191. )}
  192. {!parsed &&
  193. needsHighlighter && (
  194. <Highlighter
  195. textToHighlight={row.entry}
  196. searchWords={highlights}
  197. findChunks={findHighlightChunksInText}
  198. highlightClassName={highlightClassName}
  199. />
  200. )}
  201. {!parsed && !needsHighlighter && row.entry}
  202. {showFieldStats && (
  203. <div className="logs-row__stats">
  204. <Stats
  205. stats={fieldStats}
  206. label={fieldLabel}
  207. value={fieldValue}
  208. onClickClose={this.onClickClose}
  209. rowCount={fieldCount}
  210. />
  211. </div>
  212. )}
  213. </div>
  214. </div>
  215. );
  216. }
  217. }
  218. function renderMetaItem(value: any, kind: LogsMetaKind) {
  219. if (kind === LogsMetaKind.LabelsMap) {
  220. return (
  221. <span className="logs-meta-item__labels">
  222. <LogLabels labels={value} plain />
  223. </span>
  224. );
  225. }
  226. return value;
  227. }
  228. interface LogsProps {
  229. data: LogsModel;
  230. highlighterExpressions: string[];
  231. loading: boolean;
  232. position: string;
  233. range?: RawTimeRange;
  234. scanning?: boolean;
  235. scanRange?: RawTimeRange;
  236. onChangeTime?: (range: RawTimeRange) => void;
  237. onClickLabel?: (label: string, value: string) => void;
  238. onStartScanning?: () => void;
  239. onStopScanning?: () => void;
  240. }
  241. interface LogsState {
  242. dedup: LogsDedupStrategy;
  243. deferLogs: boolean;
  244. hiddenLogLevels: Set<LogLevel>;
  245. renderAll: boolean;
  246. showLabels: boolean | null; // Tristate: null means auto
  247. showLocalTime: boolean;
  248. showUtc: boolean;
  249. }
  250. export default class Logs extends PureComponent<LogsProps, LogsState> {
  251. deferLogsTimer: NodeJS.Timer;
  252. renderAllTimer: NodeJS.Timer;
  253. state = {
  254. dedup: LogsDedupStrategy.none,
  255. deferLogs: true,
  256. hiddenLogLevels: new Set(),
  257. renderAll: false,
  258. showLabels: null,
  259. showLocalTime: true,
  260. showUtc: false,
  261. };
  262. componentDidMount() {
  263. // Staged rendering
  264. if (this.state.deferLogs) {
  265. const { data } = this.props;
  266. const rowCount = data && data.rows ? data.rows.length : 0;
  267. // Render all right away if not too far over the limit
  268. const renderAll = rowCount <= PREVIEW_LIMIT * 2;
  269. this.deferLogsTimer = setTimeout(() => this.setState({ deferLogs: false, renderAll }), rowCount);
  270. }
  271. }
  272. componentDidUpdate(prevProps, prevState) {
  273. // Staged rendering
  274. if (prevState.deferLogs && !this.state.deferLogs && !this.state.renderAll) {
  275. this.renderAllTimer = setTimeout(() => this.setState({ renderAll: true }), 2000);
  276. }
  277. }
  278. componentWillUnmount() {
  279. clearTimeout(this.deferLogsTimer);
  280. clearTimeout(this.renderAllTimer);
  281. }
  282. onChangeDedup = (dedup: LogsDedupStrategy) => {
  283. this.setState(prevState => {
  284. if (prevState.dedup === dedup) {
  285. return { dedup: LogsDedupStrategy.none };
  286. }
  287. return { dedup };
  288. });
  289. };
  290. onChangeLabels = (event: React.SyntheticEvent) => {
  291. const target = event.target as HTMLInputElement;
  292. this.setState({
  293. showLabels: target.checked,
  294. });
  295. };
  296. onChangeLocalTime = (event: React.SyntheticEvent) => {
  297. const target = event.target as HTMLInputElement;
  298. this.setState({
  299. showLocalTime: target.checked,
  300. });
  301. };
  302. onChangeUtc = (event: React.SyntheticEvent) => {
  303. const target = event.target as HTMLInputElement;
  304. this.setState({
  305. showUtc: target.checked,
  306. });
  307. };
  308. onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set<string>) => {
  309. const hiddenLogLevels: Set<LogLevel> = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level]));
  310. this.setState({ hiddenLogLevels });
  311. };
  312. onClickScan = (event: React.SyntheticEvent) => {
  313. event.preventDefault();
  314. this.props.onStartScanning();
  315. };
  316. onClickStopScan = (event: React.SyntheticEvent) => {
  317. event.preventDefault();
  318. this.props.onStopScanning();
  319. };
  320. render() {
  321. const {
  322. data,
  323. highlighterExpressions,
  324. loading = false,
  325. onClickLabel,
  326. position,
  327. range,
  328. scanning,
  329. scanRange,
  330. } = this.props;
  331. const { dedup, deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc } = this.state;
  332. let { showLabels } = this.state;
  333. const hasData = data && data.rows && data.rows.length > 0;
  334. const showDuplicates = dedup !== LogsDedupStrategy.none;
  335. // Filtering
  336. const filteredData = filterLogLevels(data, hiddenLogLevels);
  337. const dedupedData = dedupLogRows(filteredData, dedup);
  338. const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
  339. const meta = [...data.meta];
  340. if (dedup !== LogsDedupStrategy.none) {
  341. meta.push({
  342. label: 'Dedup count',
  343. value: dedupCount,
  344. kind: LogsMetaKind.Number,
  345. });
  346. }
  347. // Staged rendering
  348. const processedRows = dedupedData.rows;
  349. const firstRows = processedRows.slice(0, PREVIEW_LIMIT);
  350. const lastRows = processedRows.slice(PREVIEW_LIMIT);
  351. // Check for labels
  352. if (showLabels === null) {
  353. if (hasData) {
  354. showLabels = data.rows.some(row => _.size(row.uniqueLabels) > 0);
  355. } else {
  356. showLabels = true;
  357. }
  358. }
  359. // Grid options
  360. // const cssColumnSizes = [];
  361. // if (showDuplicates) {
  362. // cssColumnSizes.push('max-content');
  363. // }
  364. // // Log-level indicator line
  365. // cssColumnSizes.push('3px');
  366. // if (showUtc) {
  367. // cssColumnSizes.push('minmax(220px, max-content)');
  368. // }
  369. // if (showLocalTime) {
  370. // cssColumnSizes.push('minmax(140px, max-content)');
  371. // }
  372. // if (showLabels) {
  373. // cssColumnSizes.push('fit-content(20%)');
  374. // }
  375. // cssColumnSizes.push('1fr');
  376. // const logEntriesStyle = {
  377. // gridTemplateColumns: cssColumnSizes.join(' '),
  378. // };
  379. const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
  380. // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
  381. const getRows = () => processedRows;
  382. return (
  383. <div className="logs-panel">
  384. <div className="logs-panel-graph">
  385. <Graph
  386. data={data.series}
  387. height="100px"
  388. range={range}
  389. id={`explore-logs-graph-${position}`}
  390. onChangeTime={this.props.onChangeTime}
  391. onToggleSeries={this.onToggleLogLevel}
  392. userOptions={graphOptions}
  393. />
  394. </div>
  395. <div className="logs-panel-options">
  396. <div className="logs-panel-controls">
  397. <Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} small />
  398. <Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} small />
  399. <Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} small />
  400. <ToggleButtonGroup
  401. label="Dedup"
  402. onChange={this.onChangeDedup}
  403. value={dedup}
  404. render={({ selectedValue, onChange }) =>
  405. Object.keys(LogsDedupStrategy).map((dedupType, i) => (
  406. <ToggleButton
  407. className="btn-small"
  408. key={i}
  409. value={dedupType}
  410. onChange={onChange}
  411. selected={selectedValue === dedupType}
  412. >
  413. {dedupType}
  414. </ToggleButton>
  415. ))
  416. }
  417. />
  418. {hasData &&
  419. meta && (
  420. <div className="logs-panel-meta">
  421. {meta.map(item => (
  422. <div className="logs-panel-meta__item" key={item.label}>
  423. <span className="logs-panel-meta__label">{item.label}:</span>
  424. <span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
  425. </div>
  426. ))}
  427. </div>
  428. )}
  429. </div>
  430. </div>
  431. <div className="logs-rows">
  432. {hasData &&
  433. !deferLogs &&
  434. // Only inject highlighterExpression in the first set for performance reasons
  435. firstRows.map(row => (
  436. <Row
  437. key={row.key + row.duplicates}
  438. getRows={getRows}
  439. highlighterExpressions={highlighterExpressions}
  440. row={row}
  441. showDuplicates={showDuplicates}
  442. showLabels={showLabels}
  443. showLocalTime={showLocalTime}
  444. showUtc={showUtc}
  445. onClickLabel={onClickLabel}
  446. />
  447. ))}
  448. {hasData &&
  449. !deferLogs &&
  450. renderAll &&
  451. lastRows.map(row => (
  452. <Row
  453. key={row.key + row.duplicates}
  454. getRows={getRows}
  455. row={row}
  456. showDuplicates={showDuplicates}
  457. showLabels={showLabels}
  458. showLocalTime={showLocalTime}
  459. showUtc={showUtc}
  460. onClickLabel={onClickLabel}
  461. />
  462. ))}
  463. {hasData && deferLogs && <span>Rendering {dedupedData.rows.length} rows...</span>}
  464. </div>
  465. {!loading &&
  466. !hasData &&
  467. !scanning && (
  468. <div className="logs-panel-nodata">
  469. No logs found.
  470. <a className="link" onClick={this.onClickScan}>
  471. Scan for older logs
  472. </a>
  473. </div>
  474. )}
  475. {scanning && (
  476. <div className="logs-panel-nodata">
  477. <span>{scanText}</span>
  478. <a className="link" onClick={this.onClickStopScan}>
  479. Stop scan
  480. </a>
  481. </div>
  482. )}
  483. </div>
  484. );
  485. }
  486. }