import { cx } from '@emotion/css'; import { debounce } from 'lodash'; import React, { PureComponent } from 'react'; import { Field, LinkModel, LogRowModel, LogsSortOrder, dateTimeFormat, CoreApp, DataFrame } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; import { withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui'; import { checkLogsError, escapeUnescapedString } from '../utils'; import { LogDetails } from './LogDetails'; import { LogLabels } from './LogLabels'; import { LogRowMessage } from './LogRowMessage'; import { LogRowMessageDisplayedFields } from './LogRowMessageDisplayedFields'; import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles'; 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; app?: CoreApp; displayedFields?: string[]; getRows: () => LogRowModel[]; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; onContextClick?: () => void; getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; showContextToggle?: (row?: LogRowModel) => boolean; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; onLogRowHover?: (row?: LogRowModel) => void; onOpenContext: (row: LogRowModel, onClose: () => void) => void; styles: LogRowStyles; } interface State { showContext: boolean; showDetails: boolean; } /** * 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 { state: State = { showContext: false, showDetails: false, }; // we are debouncing the state change by 3 seconds to highlight the logline after the context closed. debouncedContextClose = debounce(() => { this.setState({ showContext: false }); }, 3000); onOpenContext = (row: LogRowModel) => { this.setState({ showContext: true }); this.props.onOpenContext(row, this.debouncedContextClose); }; toggleDetails = () => { if (!this.props.enableLogDetails) { return; } 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, }); this.setState((state) => { return { showDetails: !state.showDetails, }; }); }; renderTimeStamp(epochMs: number) { return dateTimeFormat(epochMs, { timeZone: this.props.timeZone, defaultWithMS: true, }); } onMouseEnter = () => { if (this.props.onLogRowHover) { this.props.onLogRowHover(this.props.row); } }; onMouseLeave = () => { if (this.props.onLogRowHover) { this.props.onLogRowHover(undefined); } }; render() { const { getRows, onClickFilterLabel, onClickFilterOutLabel, onClickShowField, onClickHideField, enableLogDetails, row, showDuplicates, showContextToggle, showLabels, showTime, displayedFields, wrapLogMessage, prettifyLogMessage, theme, getFieldLinks, forceEscape, app, styles, } = this.props; const { showDetails, showContext } = this.state; const levelStyles = getLogLevelStyles(theme, row.logLevel); const { errorMessage, hasError } = checkLogsError(row); const logRowBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, [styles.contextBackground]: showContext, }); const processedRow = row.hasUnescapedContent && forceEscape ? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) } : row; return ( <> {showDuplicates && ( {processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null} )} {hasError && ( )} {enableLogDetails && ( )} {showTime && {this.renderTimeStamp(row.timeEpochMs)}} {showLabels && processedRow.uniqueLabels && ( )} {displayedFields && displayedFields.length > 0 ? ( ) : ( )} {this.state.showDetails && ( )} ); } } export const LogRow = withTheme2(UnThemedLogRow); LogRow.displayName = 'LogRow';