Logs.tsx 15 KB

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