2022-09-19 16:51:46 +08:00
|
|
|
import { cx, css } from '@emotion/css';
|
|
|
|
import React, { PureComponent } from 'react';
|
|
|
|
|
|
|
|
import {
|
|
|
|
Field,
|
|
|
|
LinkModel,
|
|
|
|
LogRowModel,
|
|
|
|
LogsSortOrder,
|
|
|
|
TimeZone,
|
|
|
|
DataQueryResponse,
|
|
|
|
dateTimeFormat,
|
|
|
|
GrafanaTheme2,
|
2022-09-29 16:00:01 +08:00
|
|
|
CoreApp,
|
2022-11-10 22:33:17 +08:00
|
|
|
DataFrame,
|
2023-01-27 22:12:01 +08:00
|
|
|
DataSourceWithLogsContextSupport,
|
2022-09-19 16:51:46 +08:00
|
|
|
} from '@grafana/data';
|
2022-10-11 17:04:43 +08:00
|
|
|
import { reportInteraction } from '@grafana/runtime';
|
2022-09-19 16:51:46 +08:00
|
|
|
import { styleMixins, withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui';
|
|
|
|
|
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';
|
|
|
|
import {
|
|
|
|
LogRowContextRows,
|
|
|
|
LogRowContextQueryErrors,
|
|
|
|
HasMoreContextRows,
|
|
|
|
LogRowContextProvider,
|
|
|
|
RowContextOptions,
|
|
|
|
} from './LogRowContextProvider';
|
|
|
|
import { LogRowMessage } from './LogRowMessage';
|
2023-01-12 02:20:11 +08:00
|
|
|
import { LogRowMessageDisplayedFields } from './LogRowMessageDisplayedFields';
|
2022-09-19 16:51:46 +08:00
|
|
|
import { getLogRowStyles } from './getLogRowStyles';
|
|
|
|
|
|
|
|
//Components
|
|
|
|
|
|
|
|
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 20:51:20 +08:00
|
|
|
scrollElement?: HTMLDivElement;
|
2022-09-29 16:00:01 +08:00
|
|
|
showRowMenu?: boolean;
|
|
|
|
app?: CoreApp;
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields?: string[];
|
2022-09-19 16:51:46 +08:00
|
|
|
getRows: () => LogRowModel[];
|
|
|
|
onClickFilterLabel?: (key: string, value: string) => void;
|
|
|
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
|
|
|
onContextClick?: () => void;
|
|
|
|
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
|
2023-01-27 22:12:01 +08:00
|
|
|
getLogRowContextUi?: (row: LogRowModel) => React.ReactNode;
|
2022-11-10 22:33:17 +08:00
|
|
|
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
2022-09-19 16:51:46 +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;
|
2022-09-29 16:00:01 +08:00
|
|
|
toggleContextIsOpen?: () => void;
|
2022-09-19 16:51:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
showContext: boolean;
|
|
|
|
showDetails: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => {
|
|
|
|
return {
|
|
|
|
topVerticalAlign: css`
|
|
|
|
label: topVerticalAlign;
|
|
|
|
margin-top: -${theme.spacing(0.9)};
|
|
|
|
margin-left: -${theme.spacing(0.25)};
|
|
|
|
`,
|
|
|
|
detailsOpen: css`
|
|
|
|
&:hover {
|
|
|
|
background-color: ${styleMixins.hoverColor(theme.colors.background.primary, theme)};
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
errorLogRow: css`
|
|
|
|
label: erroredLogRow;
|
|
|
|
color: ${theme.colors.text.secondary};
|
|
|
|
`,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
/**
|
|
|
|
* 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 = {
|
|
|
|
showContext: false,
|
|
|
|
showDetails: false,
|
|
|
|
};
|
|
|
|
|
2022-10-19 17:01:45 +08:00
|
|
|
toggleContext = (method: string) => {
|
|
|
|
const { datasourceType, uid: logRowUid } = this.props.row;
|
|
|
|
reportInteraction('grafana_explore_logs_log_context_clicked', {
|
|
|
|
datasourceType,
|
|
|
|
logRowUid,
|
|
|
|
type: method,
|
|
|
|
});
|
|
|
|
|
2022-09-29 16:00:01 +08:00
|
|
|
this.props.toggleContextIsOpen?.();
|
2022-09-19 16:51:46 +08:00
|
|
|
this.setState((state) => {
|
|
|
|
return {
|
|
|
|
showContext: !state.showContext,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
toggleDetails = () => {
|
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
renderLogRow(
|
|
|
|
context?: LogRowContextRows,
|
|
|
|
errors?: LogRowContextQueryErrors,
|
|
|
|
hasMoreContextRows?: HasMoreContextRows,
|
2022-09-28 19:37:49 +08:00
|
|
|
updateLimit?: () => void,
|
2023-01-27 22:12:01 +08:00
|
|
|
logsSortOrder?: LogsSortOrder | null,
|
|
|
|
getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi'],
|
|
|
|
runContextQuery?: () => void
|
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,
|
2022-09-29 16:00:01 +08:00
|
|
|
showRowMenu,
|
2022-09-19 16:51:46 +08:00
|
|
|
showLabels,
|
|
|
|
showTime,
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields,
|
2022-09-19 16:51:46 +08:00
|
|
|
wrapLogMessage,
|
|
|
|
prettifyLogMessage,
|
|
|
|
theme,
|
|
|
|
getFieldLinks,
|
|
|
|
forceEscape,
|
|
|
|
onLogRowHover,
|
2022-09-29 16:00:01 +08:00
|
|
|
app,
|
2022-09-29 20:51:20 +08:00
|
|
|
scrollElement,
|
2022-09-19 16:51:46 +08:00
|
|
|
} = this.props;
|
|
|
|
const { showDetails, showContext } = this.state;
|
|
|
|
const style = getLogRowStyles(theme, row.logLevel);
|
|
|
|
const styles = getStyles(theme);
|
|
|
|
const { errorMessage, hasError } = checkLogsError(row);
|
|
|
|
const logRowBackground = cx(style.logsRow, {
|
|
|
|
[styles.errorLogRow]: hasError,
|
2022-10-13 17:42:40 +08:00
|
|
|
[style.contextBackground]: showContext,
|
2022-09-19 16:51:46 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const processedRow =
|
|
|
|
row.hasUnescapedContent && forceEscape
|
|
|
|
? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) }
|
|
|
|
: row;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<tr
|
|
|
|
className={logRowBackground}
|
|
|
|
onClick={this.toggleDetails}
|
|
|
|
onMouseEnter={() => {
|
|
|
|
onLogRowHover && onLogRowHover(row);
|
|
|
|
}}
|
|
|
|
onMouseLeave={() => {
|
|
|
|
onLogRowHover && onLogRowHover(undefined);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{showDuplicates && (
|
|
|
|
<td className={style.logsRowDuplicates}>
|
|
|
|
{processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}
|
|
|
|
</td>
|
|
|
|
)}
|
|
|
|
<td className={cx({ [style.logsRowLevel]: !hasError })}>
|
|
|
|
{hasError && (
|
|
|
|
<Tooltip content={`Error: ${errorMessage}`} placement="right" theme="error">
|
|
|
|
<Icon className={style.logIconError} name="exclamation-triangle" size="xs" />
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
</td>
|
|
|
|
{enableLogDetails && (
|
|
|
|
<td title={showDetails ? 'Hide log details' : 'See log details'} className={style.logsRowToggleDetails}>
|
|
|
|
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
|
|
|
</td>
|
|
|
|
)}
|
|
|
|
{showTime && <td className={style.logsRowLocalTime}>{this.renderTimeStamp(row.timeEpochMs)}</td>}
|
|
|
|
{showLabels && processedRow.uniqueLabels && (
|
|
|
|
<td className={style.logsRowLabels}>
|
|
|
|
<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-01-12 02:20:11 +08:00
|
|
|
showDetectedFields={displayedFields!}
|
2022-09-19 16:51:46 +08:00
|
|
|
getFieldLinks={getFieldLinks}
|
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<LogRowMessage
|
|
|
|
row={processedRow}
|
|
|
|
getRows={getRows}
|
|
|
|
errors={errors}
|
|
|
|
hasMoreContextRows={hasMoreContextRows}
|
2023-01-27 22:12:01 +08:00
|
|
|
getLogRowContextUi={getLogRowContextUi}
|
|
|
|
runContextQuery={runContextQuery}
|
2022-09-19 16:51:46 +08:00
|
|
|
updateLimit={updateLimit}
|
|
|
|
context={context}
|
|
|
|
contextIsOpen={showContext}
|
|
|
|
showContextToggle={showContextToggle}
|
2022-09-29 16:00:01 +08:00
|
|
|
showRowMenu={showRowMenu}
|
2022-09-19 16:51:46 +08:00
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
prettifyLogMessage={prettifyLogMessage}
|
|
|
|
onToggleContext={this.toggleContext}
|
2022-09-29 16:00:01 +08:00
|
|
|
app={app}
|
2022-09-29 20:51:20 +08:00
|
|
|
scrollElement={scrollElement}
|
2022-09-28 19:37:49 +08:00
|
|
|
logsSortOrder={logsSortOrder}
|
2022-09-19 16:51:46 +08:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</tr>
|
|
|
|
{this.state.showDetails && (
|
|
|
|
<LogDetails
|
|
|
|
className={logRowBackground}
|
|
|
|
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}
|
2022-09-19 16:51:46 +08:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const { showContext } = this.state;
|
2023-01-27 22:12:01 +08:00
|
|
|
const { logsSortOrder, row, getRowContext, getLogRowContextUi } = this.props;
|
2022-09-19 16:51:46 +08:00
|
|
|
|
|
|
|
if (showContext) {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<LogRowContextProvider row={row} getRowContext={getRowContext} logsSortOrder={logsSortOrder}>
|
2023-01-27 22:12:01 +08:00
|
|
|
{({ result, errors, hasMoreContextRows, updateLimit, runContextQuery, logsSortOrder }) => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{this.renderLogRow(
|
|
|
|
result,
|
|
|
|
errors,
|
|
|
|
hasMoreContextRows,
|
|
|
|
updateLimit,
|
|
|
|
logsSortOrder,
|
|
|
|
getLogRowContextUi,
|
|
|
|
runContextQuery
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
2022-09-19 16:51:46 +08:00
|
|
|
}}
|
|
|
|
</LogRowContextProvider>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.renderLogRow();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const LogRow = withTheme2(UnThemedLogRow);
|
|
|
|
LogRow.displayName = 'LogRow';
|