Logs.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import { css } from '@emotion/css';
  2. import { capitalize } from 'lodash';
  3. import memoizeOne from 'memoize-one';
  4. import React, { PureComponent, createRef } from 'react';
  5. import {
  6. rangeUtil,
  7. RawTimeRange,
  8. LogLevel,
  9. TimeZone,
  10. AbsoluteTimeRange,
  11. LogsDedupStrategy,
  12. LogRowModel,
  13. LogsDedupDescription,
  14. LogsMetaItem,
  15. LogsSortOrder,
  16. LinkModel,
  17. Field,
  18. DataQuery,
  19. DataFrame,
  20. GrafanaTheme2,
  21. LoadingState,
  22. } from '@grafana/data';
  23. import { reportInteraction } from '@grafana/runtime';
  24. import { TooltipDisplayMode } from '@grafana/schema';
  25. import {
  26. RadioButtonGroup,
  27. LogRows,
  28. Button,
  29. InlineField,
  30. InlineFieldRow,
  31. InlineSwitch,
  32. withTheme2,
  33. Themeable2,
  34. } from '@grafana/ui';
  35. import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
  36. import { dedupLogRows, filterLogLevels } from 'app/core/logs_model';
  37. import store from 'app/core/store';
  38. import { ExploreId } from 'app/types/explore';
  39. import { ExploreGraph } from './ExploreGraph';
  40. import { LogsMetaRow } from './LogsMetaRow';
  41. import LogsNavigation from './LogsNavigation';
  42. const SETTINGS_KEYS = {
  43. showLabels: 'grafana.explore.logs.showLabels',
  44. showTime: 'grafana.explore.logs.showTime',
  45. wrapLogMessage: 'grafana.explore.logs.wrapLogMessage',
  46. prettifyLogMessage: 'grafana.explore.logs.prettifyLogMessage',
  47. logsSortOrder: 'grafana.explore.logs.sortOrder',
  48. };
  49. interface Props extends Themeable2 {
  50. width: number;
  51. logRows: LogRowModel[];
  52. logsMeta?: LogsMetaItem[];
  53. logsSeries?: DataFrame[];
  54. logsQueries?: DataQuery[];
  55. visibleRange?: AbsoluteTimeRange;
  56. theme: GrafanaTheme2;
  57. loading: boolean;
  58. loadingState: LoadingState;
  59. absoluteRange: AbsoluteTimeRange;
  60. timeZone: TimeZone;
  61. scanning?: boolean;
  62. scanRange?: RawTimeRange;
  63. exploreId: ExploreId;
  64. datasourceType?: string;
  65. showContextToggle?: (row?: LogRowModel) => boolean;
  66. onChangeTime: (range: AbsoluteTimeRange) => void;
  67. onClickFilterLabel?: (key: string, value: string) => void;
  68. onClickFilterOutLabel?: (key: string, value: string) => void;
  69. onStartScanning?: () => void;
  70. onStopScanning?: () => void;
  71. getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
  72. getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
  73. addResultsToCache: () => void;
  74. clearCache: () => void;
  75. }
  76. interface State {
  77. showLabels: boolean;
  78. showTime: boolean;
  79. wrapLogMessage: boolean;
  80. prettifyLogMessage: boolean;
  81. dedupStrategy: LogsDedupStrategy;
  82. hiddenLogLevels: LogLevel[];
  83. logsSortOrder: LogsSortOrder | null;
  84. isFlipping: boolean;
  85. showDetectedFields: string[];
  86. forceEscape: boolean;
  87. }
  88. class UnthemedLogs extends PureComponent<Props, State> {
  89. flipOrderTimer?: number;
  90. cancelFlippingTimer?: number;
  91. topLogsRef = createRef<HTMLDivElement>();
  92. state: State = {
  93. showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
  94. showTime: store.getBool(SETTINGS_KEYS.showTime, true),
  95. wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true),
  96. prettifyLogMessage: store.getBool(SETTINGS_KEYS.prettifyLogMessage, false),
  97. dedupStrategy: LogsDedupStrategy.none,
  98. hiddenLogLevels: [],
  99. logsSortOrder: store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending,
  100. isFlipping: false,
  101. showDetectedFields: [],
  102. forceEscape: false,
  103. };
  104. componentWillUnmount() {
  105. if (this.flipOrderTimer) {
  106. window.clearTimeout(this.flipOrderTimer);
  107. }
  108. if (this.cancelFlippingTimer) {
  109. window.clearTimeout(this.cancelFlippingTimer);
  110. }
  111. }
  112. onChangeLogsSortOrder = () => {
  113. this.setState({ isFlipping: true });
  114. // we are using setTimeout here to make sure that disabled button is rendered before the rendering of reordered logs
  115. this.flipOrderTimer = window.setTimeout(() => {
  116. this.setState((prevState) => {
  117. const newSortOrder =
  118. prevState.logsSortOrder === LogsSortOrder.Descending ? LogsSortOrder.Ascending : LogsSortOrder.Descending;
  119. store.set(SETTINGS_KEYS.logsSortOrder, newSortOrder);
  120. return { logsSortOrder: newSortOrder };
  121. });
  122. }, 0);
  123. this.cancelFlippingTimer = window.setTimeout(() => this.setState({ isFlipping: false }), 1000);
  124. };
  125. onEscapeNewlines = () => {
  126. this.setState((prevState) => ({
  127. forceEscape: !prevState.forceEscape,
  128. }));
  129. };
  130. onChangeDedup = (dedupStrategy: LogsDedupStrategy) => {
  131. reportInteraction('grafana_explore_logs_deduplication_clicked', {
  132. deduplicationType: dedupStrategy,
  133. datasourceType: this.props.datasourceType,
  134. });
  135. this.setState({ dedupStrategy });
  136. };
  137. onChangeLabels = (event: React.ChangeEvent<HTMLInputElement>) => {
  138. const { target } = event;
  139. if (target) {
  140. const showLabels = target.checked;
  141. this.setState({
  142. showLabels,
  143. });
  144. store.set(SETTINGS_KEYS.showLabels, showLabels);
  145. }
  146. };
  147. onChangeTime = (event: React.ChangeEvent<HTMLInputElement>) => {
  148. const { target } = event;
  149. if (target) {
  150. const showTime = target.checked;
  151. this.setState({
  152. showTime,
  153. });
  154. store.set(SETTINGS_KEYS.showTime, showTime);
  155. }
  156. };
  157. onChangeWrapLogMessage = (event: React.ChangeEvent<HTMLInputElement>) => {
  158. const { target } = event;
  159. if (target) {
  160. const wrapLogMessage = target.checked;
  161. this.setState({
  162. wrapLogMessage,
  163. });
  164. store.set(SETTINGS_KEYS.wrapLogMessage, wrapLogMessage);
  165. }
  166. };
  167. onChangePrettifyLogMessage = (event: React.ChangeEvent<HTMLInputElement>) => {
  168. const { target } = event;
  169. if (target) {
  170. const prettifyLogMessage = target.checked;
  171. this.setState({
  172. prettifyLogMessage,
  173. });
  174. store.set(SETTINGS_KEYS.prettifyLogMessage, prettifyLogMessage);
  175. }
  176. };
  177. onToggleLogLevel = (hiddenRawLevels: string[]) => {
  178. const hiddenLogLevels = hiddenRawLevels.map((level) => LogLevel[level as LogLevel]);
  179. this.setState({ hiddenLogLevels });
  180. };
  181. onClickScan = (event: React.SyntheticEvent) => {
  182. event.preventDefault();
  183. if (this.props.onStartScanning) {
  184. this.props.onStartScanning();
  185. }
  186. };
  187. onClickStopScan = (event: React.SyntheticEvent) => {
  188. event.preventDefault();
  189. if (this.props.onStopScanning) {
  190. this.props.onStopScanning();
  191. }
  192. };
  193. showDetectedField = (key: string) => {
  194. const index = this.state.showDetectedFields.indexOf(key);
  195. if (index === -1) {
  196. this.setState((state) => {
  197. return {
  198. showDetectedFields: state.showDetectedFields.concat(key),
  199. };
  200. });
  201. }
  202. };
  203. hideDetectedField = (key: string) => {
  204. const index = this.state.showDetectedFields.indexOf(key);
  205. if (index > -1) {
  206. this.setState((state) => {
  207. return {
  208. showDetectedFields: state.showDetectedFields.filter((k) => key !== k),
  209. };
  210. });
  211. }
  212. };
  213. clearDetectedFields = () => {
  214. this.setState((state) => {
  215. return {
  216. showDetectedFields: [],
  217. };
  218. });
  219. };
  220. checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => {
  221. return !!logRows.some((r) => r.hasUnescapedContent);
  222. });
  223. dedupRows = memoizeOne((logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => {
  224. const dedupedRows = dedupLogRows(logRows, dedupStrategy);
  225. const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0);
  226. return { dedupedRows, dedupCount };
  227. });
  228. filterRows = memoizeOne((logRows: LogRowModel[], hiddenLogLevels: LogLevel[]) => {
  229. return filterLogLevels(logRows, new Set(hiddenLogLevels));
  230. });
  231. createNavigationRange = memoizeOne((logRows: LogRowModel[]): { from: number; to: number } | undefined => {
  232. if (!logRows || logRows.length === 0) {
  233. return undefined;
  234. }
  235. const firstTimeStamp = logRows[0].timeEpochMs;
  236. const lastTimeStamp = logRows[logRows.length - 1].timeEpochMs;
  237. if (lastTimeStamp < firstTimeStamp) {
  238. return { from: lastTimeStamp, to: firstTimeStamp };
  239. }
  240. return { from: firstTimeStamp, to: lastTimeStamp };
  241. });
  242. scrollToTopLogs = () => this.topLogsRef.current?.scrollIntoView();
  243. render() {
  244. const {
  245. width,
  246. logRows,
  247. logsMeta,
  248. logsSeries,
  249. visibleRange,
  250. loading = false,
  251. loadingState,
  252. onClickFilterLabel,
  253. onClickFilterOutLabel,
  254. timeZone,
  255. scanning,
  256. scanRange,
  257. showContextToggle,
  258. absoluteRange,
  259. onChangeTime,
  260. getFieldLinks,
  261. theme,
  262. logsQueries,
  263. clearCache,
  264. addResultsToCache,
  265. exploreId,
  266. } = this.props;
  267. const {
  268. showLabels,
  269. showTime,
  270. wrapLogMessage,
  271. prettifyLogMessage,
  272. dedupStrategy,
  273. hiddenLogLevels,
  274. logsSortOrder,
  275. isFlipping,
  276. showDetectedFields,
  277. forceEscape,
  278. } = this.state;
  279. const styles = getStyles(theme, wrapLogMessage);
  280. const hasData = logRows && logRows.length > 0;
  281. const hasUnescapedContent = this.checkUnescapedContent(logRows);
  282. const filteredLogs = this.filterRows(logRows, hiddenLogLevels);
  283. const { dedupedRows, dedupCount } = this.dedupRows(filteredLogs, dedupStrategy);
  284. const navigationRange = this.createNavigationRange(logRows);
  285. const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
  286. return (
  287. <>
  288. {logsSeries && logsSeries.length ? (
  289. <>
  290. <div className={styles.infoText}>
  291. This datasource does not support full-range histograms. The graph is based on the logs seen in the
  292. response.
  293. </div>
  294. <ExploreGraph
  295. graphStyle="lines"
  296. data={logsSeries}
  297. height={150}
  298. width={width}
  299. tooltipDisplayMode={TooltipDisplayMode.Multi}
  300. absoluteRange={visibleRange || absoluteRange}
  301. timeZone={timeZone}
  302. loadingState={loadingState}
  303. onChangeTime={onChangeTime}
  304. onHiddenSeriesChanged={this.onToggleLogLevel}
  305. />
  306. </>
  307. ) : undefined}
  308. <div className={styles.logOptions} ref={this.topLogsRef}>
  309. <InlineFieldRow>
  310. <InlineField label="Time" className={styles.horizontalInlineLabel} transparent>
  311. <InlineSwitch
  312. value={showTime}
  313. onChange={this.onChangeTime}
  314. className={styles.horizontalInlineSwitch}
  315. transparent
  316. id={`show-time_${exploreId}`}
  317. />
  318. </InlineField>
  319. <InlineField label="Unique labels" className={styles.horizontalInlineLabel} transparent>
  320. <InlineSwitch
  321. value={showLabels}
  322. onChange={this.onChangeLabels}
  323. className={styles.horizontalInlineSwitch}
  324. transparent
  325. id={`unique-labels_${exploreId}`}
  326. />
  327. </InlineField>
  328. <InlineField label="Wrap lines" className={styles.horizontalInlineLabel} transparent>
  329. <InlineSwitch
  330. value={wrapLogMessage}
  331. onChange={this.onChangeWrapLogMessage}
  332. className={styles.horizontalInlineSwitch}
  333. transparent
  334. id={`wrap-lines_${exploreId}`}
  335. />
  336. </InlineField>
  337. <InlineField label="Prettify JSON" className={styles.horizontalInlineLabel} transparent>
  338. <InlineSwitch
  339. value={prettifyLogMessage}
  340. onChange={this.onChangePrettifyLogMessage}
  341. className={styles.horizontalInlineSwitch}
  342. transparent
  343. id={`prettify_${exploreId}`}
  344. />
  345. </InlineField>
  346. <InlineField label="Dedup" className={styles.horizontalInlineLabel} transparent>
  347. <RadioButtonGroup
  348. options={Object.values(LogsDedupStrategy).map((dedupType) => ({
  349. label: capitalize(dedupType),
  350. value: dedupType,
  351. description: LogsDedupDescription[dedupType],
  352. }))}
  353. value={dedupStrategy}
  354. onChange={this.onChangeDedup}
  355. className={styles.radioButtons}
  356. />
  357. </InlineField>
  358. </InlineFieldRow>
  359. <div>
  360. <InlineField label="Display results" className={styles.horizontalInlineLabel} transparent>
  361. <RadioButtonGroup
  362. disabled={isFlipping}
  363. options={[
  364. {
  365. label: 'Newest first',
  366. value: LogsSortOrder.Descending,
  367. description: 'Show results newest to oldest',
  368. },
  369. {
  370. label: 'Oldest first',
  371. value: LogsSortOrder.Ascending,
  372. description: 'Show results oldest to newest',
  373. },
  374. ]}
  375. value={logsSortOrder}
  376. onChange={this.onChangeLogsSortOrder}
  377. className={styles.radioButtons}
  378. />
  379. </InlineField>
  380. </div>
  381. </div>
  382. <LogsMetaRow
  383. logRows={logRows}
  384. meta={logsMeta || []}
  385. dedupStrategy={dedupStrategy}
  386. dedupCount={dedupCount}
  387. hasUnescapedContent={hasUnescapedContent}
  388. forceEscape={forceEscape}
  389. showDetectedFields={showDetectedFields}
  390. onEscapeNewlines={this.onEscapeNewlines}
  391. clearDetectedFields={this.clearDetectedFields}
  392. />
  393. <div className={styles.logsSection}>
  394. <div className={styles.logRows} data-testid="logRows">
  395. <LogRows
  396. logRows={logRows}
  397. deduplicatedRows={dedupedRows}
  398. dedupStrategy={dedupStrategy}
  399. getRowContext={this.props.getRowContext}
  400. onClickFilterLabel={onClickFilterLabel}
  401. onClickFilterOutLabel={onClickFilterOutLabel}
  402. showContextToggle={showContextToggle}
  403. showLabels={showLabels}
  404. showTime={showTime}
  405. enableLogDetails={true}
  406. forceEscape={forceEscape}
  407. wrapLogMessage={wrapLogMessage}
  408. prettifyLogMessage={prettifyLogMessage}
  409. timeZone={timeZone}
  410. getFieldLinks={getFieldLinks}
  411. logsSortOrder={logsSortOrder}
  412. showDetectedFields={showDetectedFields}
  413. onClickShowDetectedField={this.showDetectedField}
  414. onClickHideDetectedField={this.hideDetectedField}
  415. />
  416. </div>
  417. <LogsNavigation
  418. logsSortOrder={logsSortOrder}
  419. visibleRange={navigationRange ?? absoluteRange}
  420. absoluteRange={absoluteRange}
  421. timeZone={timeZone}
  422. onChangeTime={onChangeTime}
  423. loading={loading}
  424. queries={logsQueries ?? []}
  425. scrollToTopLogs={this.scrollToTopLogs}
  426. addResultsToCache={addResultsToCache}
  427. clearCache={clearCache}
  428. />
  429. </div>
  430. {!loading && !hasData && !scanning && (
  431. <div className={styles.noData}>
  432. No logs found.
  433. <Button size="xs" fill="text" onClick={this.onClickScan}>
  434. Scan for older logs
  435. </Button>
  436. </div>
  437. )}
  438. {scanning && (
  439. <div className={styles.noData}>
  440. <span>{scanText}</span>
  441. <Button size="xs" fill="text" onClick={this.onClickStopScan}>
  442. Stop scan
  443. </Button>
  444. </div>
  445. )}
  446. </>
  447. );
  448. }
  449. }
  450. export const Logs = withTheme2(UnthemedLogs);
  451. const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean) => {
  452. return {
  453. noData: css`
  454. > * {
  455. margin-left: 0.5em;
  456. }
  457. `,
  458. logOptions: css`
  459. display: flex;
  460. justify-content: space-between;
  461. align-items: baseline;
  462. flex-wrap: wrap;
  463. background-color: ${theme.colors.background.primary};
  464. padding: ${theme.spacing(1, 2)};
  465. border-radius: ${theme.shape.borderRadius()};
  466. margin: ${theme.spacing(2, 0, 1)};
  467. border: 1px solid ${theme.colors.border.medium};
  468. `,
  469. headerButton: css`
  470. margin: ${theme.spacing(0.5, 0, 0, 1)};
  471. `,
  472. horizontalInlineLabel: css`
  473. > label {
  474. margin-right: 0;
  475. }
  476. `,
  477. horizontalInlineSwitch: css`
  478. padding: 0 ${theme.spacing(1)} 0 0;
  479. `,
  480. radioButtons: css`
  481. margin: 0;
  482. `,
  483. logsSection: css`
  484. display: flex;
  485. flex-direction: row;
  486. justify-content: space-between;
  487. `,
  488. logRows: css`
  489. overflow-x: ${wrapLogMessage ? 'unset' : 'scroll'};
  490. overflow-y: visible;
  491. width: 100%;
  492. `,
  493. infoText: css`
  494. font-size: ${theme.typography.size.sm};
  495. color: ${theme.colors.text.secondary};
  496. `,
  497. };
  498. };