Logs.tsx 15 KB

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