2023-02-01 22:28:10 +08:00
|
|
|
import { cx } from '@emotion/css';
|
2023-04-14 23:05:43 +08:00
|
|
|
import { debounce } from 'lodash';
|
2023-11-06 22:59:48 +08:00
|
|
|
import memoizeOne from 'memoize-one';
|
2023-11-16 17:48:10 +08:00
|
|
|
import React, { PureComponent, MouseEvent } from 'react';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2023-12-21 05:10:29 +08:00
|
|
|
import {
|
|
|
|
Field,
|
|
|
|
LinkModel,
|
|
|
|
LogRowModel,
|
|
|
|
LogsSortOrder,
|
|
|
|
dateTimeFormat,
|
|
|
|
CoreApp,
|
|
|
|
DataFrame,
|
|
|
|
LogRowContextOptions,
|
|
|
|
} from '@grafana/data';
|
2022-10-11 17:04:43 +08:00
|
|
|
import { reportInteraction } from '@grafana/runtime';
|
2023-12-21 05:10:29 +08:00
|
|
|
import { DataQuery, TimeZone } from '@grafana/schema';
|
2023-02-01 22:28:10 +08:00
|
|
|
import { withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui';
|
2023-12-21 05:10:29 +08:00
|
|
|
import { LokiQuery } from 'app/plugins/datasource/loki/types';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2022-09-30 18:16:47 +08:00
|
|
|
import { checkLogsError, escapeUnescapedString } from '../utils';
|
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
import { LogDetails } from './LogDetails';
|
|
|
|
import { LogLabels } from './LogLabels';
|
2023-04-11 18:19:28 +08:00
|
|
|
import { LogRowMessage } from './LogRowMessage';
|
|
|
|
import { LogRowMessageDisplayedFields } from './LogRowMessageDisplayedFields';
|
|
|
|
import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
|
|
|
interface Props extends Themeable2 {
|
|
|
|
row: LogRowModel;
|
|
|
|
showDuplicates: boolean;
|
|
|
|
showLabels: boolean;
|
|
|
|
showTime: boolean;
|
|
|
|
wrapLogMessage: boolean;
|
|
|
|
prettifyLogMessage: boolean;
|
|
|
|
timeZone: TimeZone;
|
|
|
|
enableLogDetails: boolean;
|
|
|
|
logsSortOrder?: LogsSortOrder | null;
|
|
|
|
forceEscape?: boolean;
|
2022-09-29 16:00:01 +08:00
|
|
|
app?: CoreApp;
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields?: string[];
|
2022-09-19 16:51:46 +08:00
|
|
|
getRows: () => LogRowModel[];
|
2023-11-27 21:29:00 +08:00
|
|
|
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
|
|
|
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
2022-09-19 16:51:46 +08:00
|
|
|
onContextClick?: () => void;
|
2022-11-10 22:33:17 +08:00
|
|
|
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
2023-10-31 19:37:44 +08:00
|
|
|
showContextToggle?: (row: LogRowModel) => boolean;
|
2023-01-12 02:20:11 +08:00
|
|
|
onClickShowField?: (key: string) => void;
|
|
|
|
onClickHideField?: (key: string) => void;
|
2022-09-19 16:51:46 +08:00
|
|
|
onLogRowHover?: (row?: LogRowModel) => void;
|
2023-04-14 23:05:43 +08:00
|
|
|
onOpenContext: (row: LogRowModel, onClose: () => void) => void;
|
2023-12-21 05:10:29 +08:00
|
|
|
getRowContextQuery?: (
|
|
|
|
row: LogRowModel,
|
|
|
|
options?: LogRowContextOptions,
|
|
|
|
origQuery?: LokiQuery
|
|
|
|
) => Promise<DataQuery | null>;
|
2023-06-16 20:07:51 +08:00
|
|
|
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
2023-02-01 22:28:10 +08:00
|
|
|
styles: LogRowStyles;
|
2023-06-16 20:07:51 +08:00
|
|
|
permalinkedRowId?: string;
|
|
|
|
scrollIntoView?: (element: HTMLElement) => void;
|
2023-09-04 22:30:17 +08:00
|
|
|
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
2023-06-28 21:22:54 +08:00
|
|
|
onPinLine?: (row: LogRowModel) => void;
|
|
|
|
onUnpinLine?: (row: LogRowModel) => void;
|
|
|
|
pinned?: boolean;
|
2023-08-18 18:54:08 +08:00
|
|
|
containerRendered?: boolean;
|
2023-11-16 17:48:10 +08:00
|
|
|
handleTextSelection?: (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel) => boolean;
|
2022-09-19 16:51:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
2023-08-16 23:51:18 +08:00
|
|
|
permalinked: boolean;
|
|
|
|
showingContext: boolean;
|
2022-09-19 16:51:46 +08:00
|
|
|
showDetails: boolean;
|
2023-07-14 19:49:08 +08:00
|
|
|
mouseIsOver: boolean;
|
2022-09-19 16:51:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders a log line.
|
|
|
|
*
|
|
|
|
* When user hovers over it for a certain time, it lazily parses the log line.
|
|
|
|
* Once a parser is found, it will determine fields, that will be highlighted.
|
|
|
|
* When the user requests stats for a field, they will be calculated and rendered below the row.
|
|
|
|
*/
|
|
|
|
class UnThemedLogRow extends PureComponent<Props, State> {
|
|
|
|
state: State = {
|
2023-08-16 23:51:18 +08:00
|
|
|
permalinked: false,
|
|
|
|
showingContext: false,
|
2022-09-19 16:51:46 +08:00
|
|
|
showDetails: false,
|
2023-07-14 19:49:08 +08:00
|
|
|
mouseIsOver: false,
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
2023-06-16 20:07:51 +08:00
|
|
|
logLineRef: React.RefObject<HTMLTableRowElement>;
|
|
|
|
|
|
|
|
constructor(props: Props) {
|
|
|
|
super(props);
|
|
|
|
this.logLineRef = React.createRef();
|
|
|
|
}
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2023-04-14 23:05:43 +08:00
|
|
|
// we are debouncing the state change by 3 seconds to highlight the logline after the context closed.
|
|
|
|
debouncedContextClose = debounce(() => {
|
2023-08-16 23:51:18 +08:00
|
|
|
this.setState({ showingContext: false });
|
2023-04-14 23:05:43 +08:00
|
|
|
}, 3000);
|
2022-10-19 17:01:45 +08:00
|
|
|
|
2023-04-14 23:05:43 +08:00
|
|
|
onOpenContext = (row: LogRowModel) => {
|
2023-08-16 23:51:18 +08:00
|
|
|
this.setState({ showingContext: true });
|
2023-04-14 23:05:43 +08:00
|
|
|
this.props.onOpenContext(row, this.debouncedContextClose);
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
|
|
|
|
2023-11-16 17:48:10 +08:00
|
|
|
onRowClick = (e: MouseEvent<HTMLTableRowElement>) => {
|
|
|
|
if (this.props.handleTextSelection?.(e, this.props.row)) {
|
|
|
|
// Event handled by the parent.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
if (!this.props.enableLogDetails) {
|
|
|
|
return;
|
|
|
|
}
|
2022-10-11 17:04:43 +08:00
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_log_details_clicked', {
|
|
|
|
datasourceType: this.props.row.datasourceType,
|
|
|
|
type: this.state.showDetails ? 'close' : 'open',
|
|
|
|
logRowUid: this.props.row.uid,
|
|
|
|
app: this.props.app,
|
|
|
|
});
|
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
this.setState((state) => {
|
|
|
|
return {
|
|
|
|
showDetails: !state.showDetails,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
renderTimeStamp(epochMs: number) {
|
|
|
|
return dateTimeFormat(epochMs, {
|
|
|
|
timeZone: this.props.timeZone,
|
2023-03-08 18:39:56 +08:00
|
|
|
defaultWithMS: true,
|
2022-09-19 16:51:46 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-03 17:19:48 +08:00
|
|
|
onMouseEnter = () => {
|
2023-07-14 19:49:08 +08:00
|
|
|
this.setState({ mouseIsOver: true });
|
2023-03-03 17:19:48 +08:00
|
|
|
if (this.props.onLogRowHover) {
|
|
|
|
this.props.onLogRowHover(this.props.row);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-11-16 17:48:10 +08:00
|
|
|
onMouseMove = (e: MouseEvent) => {
|
|
|
|
// No need to worry about text selection.
|
|
|
|
if (!this.props.handleTextSelection) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// The user is selecting text, so hide the log row menu so it doesn't interfere.
|
|
|
|
if (document.getSelection()?.toString() && e.buttons > 0) {
|
|
|
|
this.setState({ mouseIsOver: false });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-03-03 17:19:48 +08:00
|
|
|
onMouseLeave = () => {
|
2023-07-14 19:49:08 +08:00
|
|
|
this.setState({ mouseIsOver: false });
|
2023-03-03 17:19:48 +08:00
|
|
|
if (this.props.onLogRowHover) {
|
|
|
|
this.props.onLogRowHover(undefined);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-06-16 20:07:51 +08:00
|
|
|
componentDidMount() {
|
|
|
|
this.scrollToLogRow(this.state, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(_: Props, prevState: State) {
|
|
|
|
this.scrollToLogRow(prevState);
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollToLogRow = (prevState: State, mounted = false) => {
|
2023-08-18 18:54:08 +08:00
|
|
|
const { row, permalinkedRowId, scrollIntoView, containerRendered } = this.props;
|
2023-06-20 20:55:51 +08:00
|
|
|
|
|
|
|
if (permalinkedRowId !== row.uid) {
|
2023-06-16 20:07:51 +08:00
|
|
|
// only set the new state if the row is not permalinked anymore or if the component was mounted.
|
2023-08-16 23:51:18 +08:00
|
|
|
if (prevState.permalinked || mounted) {
|
|
|
|
this.setState({ permalinked: false });
|
2023-06-16 20:07:51 +08:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-08-18 18:54:08 +08:00
|
|
|
if (!this.state.permalinked && containerRendered && this.logLineRef.current && scrollIntoView) {
|
2023-07-26 00:00:10 +08:00
|
|
|
// at this point this row is the permalinked row, so we need to scroll to it and highlight it if possible.
|
2023-08-18 18:54:08 +08:00
|
|
|
scrollIntoView(this.logLineRef.current);
|
2023-06-20 20:55:51 +08:00
|
|
|
reportInteraction('grafana_explore_logs_permalink_opened', {
|
|
|
|
datasourceType: row.datasourceType ?? 'unknown',
|
|
|
|
logRowUid: row.uid,
|
|
|
|
});
|
2023-08-16 23:51:18 +08:00
|
|
|
this.setState({ permalinked: true });
|
2023-06-16 20:07:51 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-11-06 22:59:48 +08:00
|
|
|
escapeRow = memoizeOne((row: LogRowModel, forceEscape: boolean | undefined) => {
|
|
|
|
return row.hasUnescapedContent && forceEscape
|
|
|
|
? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) }
|
|
|
|
: row;
|
|
|
|
});
|
|
|
|
|
2023-04-14 23:05:43 +08:00
|
|
|
render() {
|
2022-09-19 16:51:46 +08:00
|
|
|
const {
|
|
|
|
getRows,
|
|
|
|
onClickFilterLabel,
|
|
|
|
onClickFilterOutLabel,
|
2023-01-12 02:20:11 +08:00
|
|
|
onClickShowField,
|
|
|
|
onClickHideField,
|
2022-09-19 16:51:46 +08:00
|
|
|
enableLogDetails,
|
|
|
|
row,
|
|
|
|
showDuplicates,
|
|
|
|
showContextToggle,
|
|
|
|
showLabels,
|
|
|
|
showTime,
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields,
|
2022-09-19 16:51:46 +08:00
|
|
|
wrapLogMessage,
|
|
|
|
prettifyLogMessage,
|
|
|
|
theme,
|
|
|
|
getFieldLinks,
|
|
|
|
forceEscape,
|
2022-09-29 16:00:01 +08:00
|
|
|
app,
|
2023-02-01 22:28:10 +08:00
|
|
|
styles,
|
2023-12-21 05:10:29 +08:00
|
|
|
getRowContextQuery,
|
2022-09-19 16:51:46 +08:00
|
|
|
} = this.props;
|
2023-08-16 23:51:18 +08:00
|
|
|
const { showDetails, showingContext, permalinked } = this.state;
|
2023-02-01 22:28:10 +08:00
|
|
|
const levelStyles = getLogLevelStyles(theme, row.logLevel);
|
2022-09-19 16:51:46 +08:00
|
|
|
const { errorMessage, hasError } = checkLogsError(row);
|
2023-02-01 22:28:10 +08:00
|
|
|
const logRowBackground = cx(styles.logsRow, {
|
2022-09-19 16:51:46 +08:00
|
|
|
[styles.errorLogRow]: hasError,
|
2023-08-16 23:51:18 +08:00
|
|
|
[styles.highlightBackground]: showingContext || permalinked,
|
2023-06-16 20:07:51 +08:00
|
|
|
});
|
|
|
|
const logRowDetailsBackground = cx(styles.logsRow, {
|
|
|
|
[styles.errorLogRow]: hasError,
|
2023-08-16 23:51:18 +08:00
|
|
|
[styles.highlightBackground]: permalinked && !this.state.showDetails,
|
2022-09-19 16:51:46 +08:00
|
|
|
});
|
|
|
|
|
2023-11-06 22:59:48 +08:00
|
|
|
const processedRow = this.escapeRow(row, forceEscape);
|
2022-09-19 16:51:46 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<tr
|
2023-06-16 20:07:51 +08:00
|
|
|
ref={this.logLineRef}
|
2022-09-19 16:51:46 +08:00
|
|
|
className={logRowBackground}
|
2023-11-16 17:48:10 +08:00
|
|
|
onClick={this.onRowClick}
|
2023-03-03 17:19:48 +08:00
|
|
|
onMouseEnter={this.onMouseEnter}
|
|
|
|
onMouseLeave={this.onMouseLeave}
|
2023-11-16 17:48:10 +08:00
|
|
|
onMouseMove={this.onMouseMove}
|
2023-08-03 00:36:53 +08:00
|
|
|
/**
|
|
|
|
* For better accessibility support, we listen to the onFocus event here (to display the LogRowMenuCell), and
|
|
|
|
* to onBlur event in the LogRowMenuCell (to hide it). This way, the LogRowMenuCell is displayed when the user navigates
|
|
|
|
* using the keyboard.
|
|
|
|
*/
|
|
|
|
onFocus={this.onMouseEnter}
|
2022-09-19 16:51:46 +08:00
|
|
|
>
|
|
|
|
{showDuplicates && (
|
2023-02-01 22:28:10 +08:00
|
|
|
<td className={styles.logsRowDuplicates}>
|
2022-09-19 16:51:46 +08:00
|
|
|
{processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}
|
|
|
|
</td>
|
|
|
|
)}
|
2023-02-01 22:28:10 +08:00
|
|
|
<td className={hasError ? '' : `${levelStyles.logsRowLevelColor} ${styles.logsRowLevel}`}>
|
2022-09-19 16:51:46 +08:00
|
|
|
{hasError && (
|
|
|
|
<Tooltip content={`Error: ${errorMessage}`} placement="right" theme="error">
|
2023-02-01 22:28:10 +08:00
|
|
|
<Icon className={styles.logIconError} name="exclamation-triangle" size="xs" />
|
2022-09-19 16:51:46 +08:00
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
</td>
|
|
|
|
{enableLogDetails && (
|
2023-02-01 22:28:10 +08:00
|
|
|
<td title={showDetails ? 'Hide log details' : 'See log details'} className={styles.logsRowToggleDetails}>
|
2022-09-19 16:51:46 +08:00
|
|
|
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
|
|
|
</td>
|
|
|
|
)}
|
2023-02-01 22:28:10 +08:00
|
|
|
{showTime && <td className={styles.logsRowLocalTime}>{this.renderTimeStamp(row.timeEpochMs)}</td>}
|
2022-09-19 16:51:46 +08:00
|
|
|
{showLabels && processedRow.uniqueLabels && (
|
2023-02-01 22:28:10 +08:00
|
|
|
<td className={styles.logsRowLabels}>
|
2022-09-19 16:51:46 +08:00
|
|
|
<LogLabels labels={processedRow.uniqueLabels} />
|
|
|
|
</td>
|
|
|
|
)}
|
2023-01-12 02:20:11 +08:00
|
|
|
{displayedFields && displayedFields.length > 0 ? (
|
|
|
|
<LogRowMessageDisplayedFields
|
2022-09-19 16:51:46 +08:00
|
|
|
row={processedRow}
|
2023-07-11 20:50:53 +08:00
|
|
|
showContextToggle={showContextToggle}
|
|
|
|
detectedFields={displayedFields}
|
2022-09-19 16:51:46 +08:00
|
|
|
getFieldLinks={getFieldLinks}
|
|
|
|
wrapLogMessage={wrapLogMessage}
|
2023-07-11 20:50:53 +08:00
|
|
|
onOpenContext={this.onOpenContext}
|
|
|
|
onPermalinkClick={this.props.onPermalinkClick}
|
|
|
|
styles={styles}
|
|
|
|
onPinLine={this.props.onPinLine}
|
|
|
|
onUnpinLine={this.props.onUnpinLine}
|
|
|
|
pinned={this.props.pinned}
|
2023-07-17 23:20:25 +08:00
|
|
|
mouseIsOver={this.state.mouseIsOver}
|
2023-08-03 00:36:53 +08:00
|
|
|
onBlur={this.onMouseLeave}
|
2022-09-19 16:51:46 +08:00
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<LogRowMessage
|
|
|
|
row={processedRow}
|
|
|
|
showContextToggle={showContextToggle}
|
2023-12-21 05:10:29 +08:00
|
|
|
getRowContextQuery={getRowContextQuery}
|
2022-09-19 16:51:46 +08:00
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
prettifyLogMessage={prettifyLogMessage}
|
2023-04-14 23:05:43 +08:00
|
|
|
onOpenContext={this.onOpenContext}
|
2023-06-16 20:07:51 +08:00
|
|
|
onPermalinkClick={this.props.onPermalinkClick}
|
2022-09-29 16:00:01 +08:00
|
|
|
app={app}
|
2023-02-01 22:28:10 +08:00
|
|
|
styles={styles}
|
2023-06-28 21:22:54 +08:00
|
|
|
onPinLine={this.props.onPinLine}
|
|
|
|
onUnpinLine={this.props.onUnpinLine}
|
|
|
|
pinned={this.props.pinned}
|
2023-07-14 19:49:08 +08:00
|
|
|
mouseIsOver={this.state.mouseIsOver}
|
2023-08-03 00:36:53 +08:00
|
|
|
onBlur={this.onMouseLeave}
|
2022-09-19 16:51:46 +08:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</tr>
|
|
|
|
{this.state.showDetails && (
|
|
|
|
<LogDetails
|
2023-06-16 20:07:51 +08:00
|
|
|
className={logRowDetailsBackground}
|
2022-09-19 16:51:46 +08:00
|
|
|
showDuplicates={showDuplicates}
|
|
|
|
getFieldLinks={getFieldLinks}
|
|
|
|
onClickFilterLabel={onClickFilterLabel}
|
|
|
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
2023-01-12 02:20:11 +08:00
|
|
|
onClickShowField={onClickShowField}
|
|
|
|
onClickHideField={onClickHideField}
|
2022-09-19 16:51:46 +08:00
|
|
|
getRows={getRows}
|
|
|
|
row={processedRow}
|
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
hasError={hasError}
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields={displayedFields}
|
2022-10-13 21:26:59 +08:00
|
|
|
app={app}
|
2023-03-03 17:19:48 +08:00
|
|
|
styles={styles}
|
2023-07-24 16:22:47 +08:00
|
|
|
isFilterLabelActive={this.props.isFilterLabelActive}
|
2022-09-19 16:51:46 +08:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const LogRow = withTheme2(UnThemedLogRow);
|
|
|
|
LogRow.displayName = 'LogRow';
|