Logs.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import _ from 'lodash';
  2. import React, { Fragment, PureComponent } from 'react';
  3. import Highlighter from 'react-highlight-words';
  4. import * as rangeUtil from 'app/core/utils/rangeutil';
  5. import { RawTimeRange } from 'app/types/series';
  6. import {
  7. LogsDedupStrategy,
  8. LogsModel,
  9. dedupLogRows,
  10. filterLogLevels,
  11. LogLevel,
  12. LogsStreamLabels,
  13. LogsMetaKind,
  14. } from 'app/core/logs_model';
  15. import { findHighlightChunksInText } from 'app/core/utils/text';
  16. import { Switch } from 'app/core/components/Switch/Switch';
  17. import Graph from './Graph';
  18. const graphOptions = {
  19. series: {
  20. bars: {
  21. show: true,
  22. lineWidth: 5,
  23. // barWidth: 10,
  24. },
  25. // stack: true,
  26. },
  27. yaxis: {
  28. tickDecimals: 0,
  29. },
  30. };
  31. function renderMetaItem(value: any, kind: LogsMetaKind) {
  32. if (kind === LogsMetaKind.LabelsMap) {
  33. return (
  34. <span className="logs-meta-item__value-labels">
  35. <Labels labels={value} />
  36. </span>
  37. );
  38. }
  39. return value;
  40. }
  41. class Label extends PureComponent<{
  42. label: string;
  43. value: string;
  44. onClickLabel?: (label: string, value: string) => void;
  45. }> {
  46. onClickLabel = () => {
  47. const { onClickLabel, label, value } = this.props;
  48. if (onClickLabel) {
  49. onClickLabel(label, value);
  50. }
  51. };
  52. render() {
  53. const { label, value } = this.props;
  54. const tooltip = `${label}: ${value}`;
  55. return (
  56. <span className="logs-label" title={tooltip} onClick={this.onClickLabel}>
  57. {value}
  58. </span>
  59. );
  60. }
  61. }
  62. class Labels extends PureComponent<{
  63. labels: LogsStreamLabels;
  64. onClickLabel?: (label: string, value: string) => void;
  65. }> {
  66. render() {
  67. const { labels, onClickLabel } = this.props;
  68. return Object.keys(labels).map(key => (
  69. <Label key={key} label={key} value={labels[key]} onClickLabel={onClickLabel} />
  70. ));
  71. }
  72. }
  73. interface LogsProps {
  74. className?: string;
  75. data: LogsModel;
  76. loading: boolean;
  77. position: string;
  78. range?: RawTimeRange;
  79. scanning?: boolean;
  80. scanRange?: RawTimeRange;
  81. onChangeTime?: (range: RawTimeRange) => void;
  82. onClickLabel?: (label: string, value: string) => void;
  83. onStartScanning?: () => void;
  84. onStopScanning?: () => void;
  85. }
  86. interface LogsState {
  87. dedup: LogsDedupStrategy;
  88. hiddenLogLevels: Set<LogLevel>;
  89. showLabels: boolean | null; // Tristate: null means auto
  90. showLocalTime: boolean;
  91. showUtc: boolean;
  92. }
  93. export default class Logs extends PureComponent<LogsProps, LogsState> {
  94. state = {
  95. dedup: LogsDedupStrategy.none,
  96. hiddenLogLevels: new Set(),
  97. showLabels: null,
  98. showLocalTime: true,
  99. showUtc: false,
  100. };
  101. onChangeDedup = (dedup: LogsDedupStrategy) => {
  102. this.setState(prevState => {
  103. if (prevState.dedup === dedup) {
  104. return { dedup: LogsDedupStrategy.none };
  105. }
  106. return { dedup };
  107. });
  108. };
  109. onChangeLabels = (event: React.SyntheticEvent) => {
  110. const target = event.target as HTMLInputElement;
  111. this.setState({
  112. showLabels: target.checked,
  113. });
  114. };
  115. onChangeLocalTime = (event: React.SyntheticEvent) => {
  116. const target = event.target as HTMLInputElement;
  117. this.setState({
  118. showLocalTime: target.checked,
  119. });
  120. };
  121. onChangeUtc = (event: React.SyntheticEvent) => {
  122. const target = event.target as HTMLInputElement;
  123. this.setState({
  124. showUtc: target.checked,
  125. });
  126. };
  127. onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set<string>) => {
  128. const hiddenLogLevels: Set<LogLevel> = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level]));
  129. this.setState({ hiddenLogLevels });
  130. };
  131. onClickScan = (event: React.SyntheticEvent) => {
  132. event.preventDefault();
  133. this.props.onStartScanning();
  134. };
  135. onClickStopScan = (event: React.SyntheticEvent) => {
  136. event.preventDefault();
  137. this.props.onStopScanning();
  138. };
  139. render() {
  140. const { className = '', data, loading = false, onClickLabel, position, range, scanning, scanRange } = this.props;
  141. const { dedup, hiddenLogLevels, showLocalTime, showUtc } = this.state;
  142. let { showLabels } = this.state;
  143. const hasData = data && data.rows && data.rows.length > 0;
  144. // Filtering
  145. const filteredData = filterLogLevels(data, hiddenLogLevels);
  146. const dedupedData = dedupLogRows(filteredData, dedup);
  147. const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
  148. const meta = [...data.meta];
  149. if (dedup !== LogsDedupStrategy.none) {
  150. meta.push({
  151. label: 'Dedup count',
  152. value: dedupCount,
  153. kind: LogsMetaKind.Number,
  154. });
  155. }
  156. // Check for labels
  157. if (showLabels === null && hasData) {
  158. showLabels = data.rows.some(row => _.size(row.uniqueLabels) > 0);
  159. }
  160. // Grid options
  161. const cssColumnSizes = ['3px']; // Log-level indicator line
  162. if (showUtc) {
  163. cssColumnSizes.push('minmax(100px, max-content)');
  164. }
  165. if (showLocalTime) {
  166. cssColumnSizes.push('minmax(100px, max-content)');
  167. }
  168. if (showLabels) {
  169. cssColumnSizes.push('minmax(100px, 25%)');
  170. }
  171. cssColumnSizes.push('1fr');
  172. const logEntriesStyle = {
  173. gridTemplateColumns: cssColumnSizes.join(' '),
  174. };
  175. const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
  176. return (
  177. <div className={`${className} logs`}>
  178. <div className="logs-graph">
  179. <Graph
  180. data={data.series}
  181. height="100px"
  182. range={range}
  183. id={`explore-logs-graph-${position}`}
  184. onChangeTime={this.props.onChangeTime}
  185. onToggleSeries={this.onToggleLogLevel}
  186. userOptions={graphOptions}
  187. />
  188. </div>
  189. <div className="logs-options">
  190. <div className="logs-controls">
  191. <Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} small />
  192. <Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} small />
  193. <Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} small />
  194. <Switch
  195. label="Dedup: off"
  196. checked={dedup === LogsDedupStrategy.none}
  197. onChange={() => this.onChangeDedup(LogsDedupStrategy.none)}
  198. small
  199. />
  200. <Switch
  201. label="Dedup: exact"
  202. checked={dedup === LogsDedupStrategy.exact}
  203. onChange={() => this.onChangeDedup(LogsDedupStrategy.exact)}
  204. small
  205. />
  206. <Switch
  207. label="Dedup: numbers"
  208. checked={dedup === LogsDedupStrategy.numbers}
  209. onChange={() => this.onChangeDedup(LogsDedupStrategy.numbers)}
  210. small
  211. />
  212. <Switch
  213. label="Dedup: signature"
  214. checked={dedup === LogsDedupStrategy.signature}
  215. onChange={() => this.onChangeDedup(LogsDedupStrategy.signature)}
  216. small
  217. />
  218. {hasData &&
  219. meta && (
  220. <div className="logs-meta">
  221. {meta.map(item => (
  222. <div className="logs-meta-item" key={item.label}>
  223. <span className="logs-meta-item__label">{item.label}:</span>
  224. <span className="logs-meta-item__value">{renderMetaItem(item.value, item.kind)}</span>
  225. </div>
  226. ))}
  227. </div>
  228. )}
  229. </div>
  230. </div>
  231. <div className="logs-entries" style={logEntriesStyle}>
  232. {hasData &&
  233. dedupedData.rows.map(row => (
  234. <Fragment key={row.key + row.duplicates}>
  235. <div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
  236. {row.duplicates > 0 && (
  237. <div className="logs-row-level__duplicates" title={`${row.duplicates} duplicates`}>
  238. {Array.apply(null, { length: row.duplicates }).map((bogus, index) => (
  239. <div className="logs-row-level__duplicate" key={`${index}`} />
  240. ))}
  241. </div>
  242. )}
  243. </div>
  244. {showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
  245. {showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
  246. {showLabels && (
  247. <div className="logs-row-labels">
  248. <Labels labels={row.uniqueLabels} onClickLabel={onClickLabel} />
  249. </div>
  250. )}
  251. <div>
  252. <Highlighter
  253. textToHighlight={row.entry}
  254. searchWords={row.searchWords}
  255. findChunks={findHighlightChunksInText}
  256. highlightClassName="logs-row-match-highlight"
  257. />
  258. </div>
  259. </Fragment>
  260. ))}
  261. </div>
  262. {!loading &&
  263. !hasData &&
  264. !scanning && (
  265. <div className="logs-nodata">
  266. No logs found.
  267. <a className="link" onClick={this.onClickScan}>
  268. Scan for older logs
  269. </a>
  270. </div>
  271. )}
  272. {scanning && (
  273. <div className="logs-nodata">
  274. <span>{scanText}</span>
  275. <a className="link" onClick={this.onClickStopScan}>
  276. Stop scan
  277. </a>
  278. </div>
  279. )}
  280. </div>
  281. );
  282. }
  283. }