Logs.tsx 15 KB

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