Logs.tsx 15 KB

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