2025-02-05 01:40:17 +08:00
|
|
|
import { css } from '@emotion/css';
|
2025-05-07 06:26:54 +08:00
|
|
|
import { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
2025-02-28 00:34:02 +08:00
|
|
|
import tinycolor from 'tinycolor2';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
2025-05-07 06:26:54 +08:00
|
|
|
import { Button } from '@grafana/ui';
|
|
|
|
import { t } from 'app/core/internationalization';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
2025-03-26 19:48:26 +08:00
|
|
|
import { LogMessageAnsi } from '../LogMessageAnsi';
|
2025-02-27 18:31:55 +08:00
|
|
|
|
2025-02-28 00:34:02 +08:00
|
|
|
import { LogLineMenu } from './LogLineMenu';
|
2025-04-04 20:53:12 +08:00
|
|
|
import { useLogIsPinned, useLogListContext } from './LogListContext';
|
2025-03-26 19:48:26 +08:00
|
|
|
import { LogListModel } from './processing';
|
2025-05-07 06:26:54 +08:00
|
|
|
import {
|
|
|
|
FIELD_GAP_MULTIPLIER,
|
|
|
|
hasUnderOrOverflow,
|
|
|
|
getLineHeight,
|
|
|
|
LogFieldDimension,
|
|
|
|
TRUNCATION_LINE_COUNT,
|
|
|
|
} from './virtualization';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
interface Props {
|
2025-02-27 18:31:55 +08:00
|
|
|
displayedFields: string[];
|
2025-02-05 01:40:17 +08:00
|
|
|
index: number;
|
2025-02-14 19:52:34 +08:00
|
|
|
log: LogListModel;
|
2025-02-05 01:40:17 +08:00
|
|
|
showTime: boolean;
|
|
|
|
style: CSSProperties;
|
2025-02-27 18:31:55 +08:00
|
|
|
styles: LogLineStyles;
|
2025-05-07 06:26:54 +08:00
|
|
|
onOverflow?: (index: number, id: string, height?: number) => void;
|
2025-02-14 19:52:34 +08:00
|
|
|
variant?: 'infinite-scroll';
|
2025-02-05 01:40:17 +08:00
|
|
|
wrapLogMessage: boolean;
|
|
|
|
}
|
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
export const LogLine = ({
|
|
|
|
displayedFields,
|
|
|
|
index,
|
|
|
|
log,
|
|
|
|
style,
|
|
|
|
styles,
|
|
|
|
onOverflow,
|
|
|
|
showTime,
|
|
|
|
variant,
|
|
|
|
wrapLogMessage,
|
|
|
|
}: Props) => {
|
2025-04-04 20:53:12 +08:00
|
|
|
const { onLogLineHover } = useLogListContext();
|
2025-05-07 06:26:54 +08:00
|
|
|
const [collapsed, setCollapsed] = useState<boolean | undefined>(
|
|
|
|
wrapLogMessage && log.collapsed !== undefined ? log.collapsed : undefined
|
|
|
|
);
|
2025-02-05 01:40:17 +08:00
|
|
|
const logLineRef = useRef<HTMLDivElement | null>(null);
|
2025-02-28 00:34:02 +08:00
|
|
|
const pinned = useLogIsPinned(log);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!onOverflow || !logLineRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const calculatedHeight = typeof style.height === 'number' ? style.height : undefined;
|
2025-05-07 06:26:54 +08:00
|
|
|
const actualHeight = hasUnderOrOverflow(logLineRef.current, calculatedHeight, log.collapsed);
|
2025-02-05 01:40:17 +08:00
|
|
|
if (actualHeight) {
|
|
|
|
onOverflow(index, log.uid, actualHeight);
|
|
|
|
}
|
2025-05-07 06:26:54 +08:00
|
|
|
}, [index, log.collapsed, log.uid, onOverflow, style.height]);
|
|
|
|
|
|
|
|
const handleMouseOver = useCallback(() => onLogLineHover?.(log), [log, onLogLineHover]);
|
|
|
|
|
|
|
|
const handleExpandCollapse = useCallback(() => {
|
|
|
|
const newState = !collapsed;
|
|
|
|
setCollapsed(newState);
|
|
|
|
log.setCollapsedState(newState);
|
|
|
|
onOverflow?.(index, log.uid);
|
|
|
|
}, [collapsed, index, log, onOverflow]);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
return (
|
2025-05-07 06:26:54 +08:00
|
|
|
<div style={style}>
|
|
|
|
<div
|
|
|
|
className={`${styles.logLine} ${variant ?? ''} ${pinned ? styles.pinnedLogLine : ''}`}
|
|
|
|
ref={onOverflow ? logLineRef : undefined}
|
|
|
|
onMouseEnter={handleMouseOver}
|
2025-05-07 22:43:48 +08:00
|
|
|
onFocus={handleMouseOver}
|
2025-05-07 06:26:54 +08:00
|
|
|
>
|
|
|
|
<LogLineMenu styles={styles} log={log} />
|
|
|
|
<div
|
|
|
|
className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`} ${collapsed === true ? styles.collapsedLogLine : ''}`}
|
|
|
|
>
|
|
|
|
<Log
|
|
|
|
displayedFields={displayedFields}
|
|
|
|
log={log}
|
|
|
|
showTime={showTime}
|
|
|
|
styles={styles}
|
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
/>
|
|
|
|
</div>
|
2025-02-05 01:40:17 +08:00
|
|
|
</div>
|
2025-05-07 06:26:54 +08:00
|
|
|
{collapsed === true && (
|
|
|
|
<div className={styles.expandCollapseControl}>
|
|
|
|
<Button
|
|
|
|
variant="primary"
|
|
|
|
fill="text"
|
|
|
|
size="sm"
|
|
|
|
className={styles.expandCollapseControlButton}
|
|
|
|
onClick={handleExpandCollapse}
|
|
|
|
>
|
|
|
|
{t('logs.log-line.show-more', 'show more')}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{collapsed === false && (
|
|
|
|
<div className={styles.expandCollapseControl}>
|
|
|
|
<Button
|
|
|
|
variant="primary"
|
|
|
|
fill="text"
|
|
|
|
size="sm"
|
|
|
|
className={styles.expandCollapseControlButton}
|
|
|
|
onClick={handleExpandCollapse}
|
|
|
|
>
|
|
|
|
{t('logs.log-line.show-less', 'show less')}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
)}
|
2025-02-05 01:40:17 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
interface LogProps {
|
|
|
|
displayedFields: string[];
|
|
|
|
log: LogListModel;
|
|
|
|
showTime: boolean;
|
2025-02-28 00:34:02 +08:00
|
|
|
styles: LogLineStyles;
|
2025-03-26 19:48:26 +08:00
|
|
|
wrapLogMessage: boolean;
|
2025-02-27 18:31:55 +08:00
|
|
|
}
|
|
|
|
|
2025-03-26 19:48:26 +08:00
|
|
|
const Log = ({ displayedFields, log, showTime, styles, wrapLogMessage }: LogProps) => {
|
2025-02-27 18:31:55 +08:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{showTime && <span className={`${styles.timestamp} level-${log.logLevel} field`}>{log.timestamp}</span>}
|
2025-03-26 19:48:26 +08:00
|
|
|
{
|
|
|
|
// When logs are unwrapped, we want an empty column space to align with other log lines.
|
|
|
|
}
|
|
|
|
{(log.displayLevel || !wrapLogMessage) && (
|
|
|
|
<span className={`${styles.level} level-${log.logLevel} field`}>{log.displayLevel}</span>
|
|
|
|
)}
|
2025-02-27 18:31:55 +08:00
|
|
|
{displayedFields.length > 0 ? (
|
2025-03-26 19:48:26 +08:00
|
|
|
displayedFields.map((field) =>
|
|
|
|
field === LOG_LINE_BODY_FIELD_NAME ? (
|
|
|
|
<LogLineBody log={log} key={field} />
|
|
|
|
) : (
|
|
|
|
<span className="field" title={field} key={field}>
|
2025-05-07 06:26:54 +08:00
|
|
|
{log.getDisplayedFieldValue(field)}
|
2025-03-26 19:48:26 +08:00
|
|
|
</span>
|
|
|
|
)
|
|
|
|
)
|
2025-02-27 18:31:55 +08:00
|
|
|
) : (
|
2025-03-26 19:48:26 +08:00
|
|
|
<LogLineBody log={log} />
|
2025-02-27 18:31:55 +08:00
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2025-03-26 19:48:26 +08:00
|
|
|
const LogLineBody = ({ log }: { log: LogListModel }) => {
|
2025-04-04 20:53:12 +08:00
|
|
|
const { syntaxHighlighting } = useLogListContext();
|
|
|
|
|
2025-03-26 19:48:26 +08:00
|
|
|
if (log.hasAnsi) {
|
|
|
|
const needsHighlighter =
|
|
|
|
log.searchWords && log.searchWords.length > 0 && log.searchWords[0] && log.searchWords[0].length > 0;
|
|
|
|
const highlight = needsHighlighter ? { searchWords: log.searchWords ?? [], highlightClassName: '' } : undefined;
|
|
|
|
return (
|
2025-04-04 20:53:12 +08:00
|
|
|
<span className="field no-highlighting">
|
2025-03-26 19:48:26 +08:00
|
|
|
<LogMessageAnsi value={log.body} highlight={highlight} />
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-04-04 20:53:12 +08:00
|
|
|
if (!syntaxHighlighting) {
|
|
|
|
return <span className="field no-highlighting">{log.body}</span>;
|
|
|
|
}
|
|
|
|
|
2025-03-26 19:48:26 +08:00
|
|
|
return <span className="field log-syntax-highlight" dangerouslySetInnerHTML={{ __html: log.highlightedBody }} />;
|
|
|
|
};
|
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
export function getGridTemplateColumns(dimensions: LogFieldDimension[]) {
|
|
|
|
const columns = dimensions.map((dimension) => dimension.width).join('px ');
|
|
|
|
return `${columns}px 1fr`;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type LogLineStyles = ReturnType<typeof getStyles>;
|
2025-02-14 19:52:34 +08:00
|
|
|
export const getStyles = (theme: GrafanaTheme2) => {
|
2025-02-05 01:40:17 +08:00
|
|
|
const colors = {
|
|
|
|
critical: '#B877D9',
|
2025-04-04 20:53:12 +08:00
|
|
|
error: theme.colors.error.text,
|
2025-02-05 01:40:17 +08:00
|
|
|
warning: '#FBAD37',
|
2025-04-04 20:53:12 +08:00
|
|
|
debug: '#6E9FFF',
|
2025-02-05 01:40:17 +08:00
|
|
|
trace: '#6ed0e0',
|
2025-04-04 20:53:12 +08:00
|
|
|
info: '#6CCF8E',
|
2025-03-26 19:48:26 +08:00
|
|
|
metadata: theme.colors.text.primary,
|
|
|
|
parsedField: theme.colors.text.primary,
|
2025-02-05 01:40:17 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
logLine: css({
|
2025-03-26 19:48:26 +08:00
|
|
|
color: tinycolor(theme.colors.text.secondary).setAlpha(0.75).toRgbString(),
|
2025-02-28 00:34:02 +08:00
|
|
|
display: 'flex',
|
|
|
|
gap: theme.spacing(0.5),
|
|
|
|
flexDirection: 'row',
|
2025-02-05 01:40:17 +08:00
|
|
|
fontFamily: theme.typography.fontFamilyMonospace,
|
|
|
|
fontSize: theme.typography.fontSize,
|
|
|
|
wordBreak: 'break-all',
|
2025-05-07 06:26:54 +08:00
|
|
|
cursor: 'pointer',
|
2025-02-05 01:40:17 +08:00
|
|
|
'&:hover': {
|
2025-04-04 20:53:12 +08:00
|
|
|
background: `hsla(0, 0%, 0%, 0.2)`,
|
2025-02-05 01:40:17 +08:00
|
|
|
},
|
2025-02-14 19:52:34 +08:00
|
|
|
'&.infinite-scroll': {
|
|
|
|
'&::before': {
|
|
|
|
borderTop: `solid 1px ${theme.colors.border.strong}`,
|
|
|
|
content: '""',
|
|
|
|
height: 0,
|
|
|
|
left: 0,
|
|
|
|
position: 'absolute',
|
|
|
|
top: -3,
|
|
|
|
width: '100%',
|
|
|
|
},
|
|
|
|
},
|
2025-03-26 19:48:26 +08:00
|
|
|
'& .log-syntax-highlight': {
|
|
|
|
'.log-token-string': {
|
|
|
|
color: tinycolor(theme.colors.text.secondary).setAlpha(0.75).toRgbString(),
|
|
|
|
},
|
|
|
|
'.log-token-duration': {
|
|
|
|
color: theme.colors.success.text,
|
|
|
|
},
|
|
|
|
'.log-token-size': {
|
|
|
|
color: theme.colors.success.text,
|
|
|
|
},
|
|
|
|
'.log-token-uuid': {
|
|
|
|
color: theme.colors.success.text,
|
|
|
|
},
|
|
|
|
'.log-token-key': {
|
|
|
|
color: colors.parsedField,
|
|
|
|
opacity: 0.9,
|
|
|
|
fontWeight: theme.typography.fontWeightMedium,
|
|
|
|
},
|
|
|
|
'.log-token-json-key': {
|
|
|
|
color: colors.parsedField,
|
|
|
|
opacity: 0.9,
|
|
|
|
fontWeight: theme.typography.fontWeightMedium,
|
|
|
|
},
|
|
|
|
'.log-token-label': {
|
|
|
|
color: colors.metadata,
|
|
|
|
fontWeight: theme.typography.fontWeightBold,
|
|
|
|
},
|
|
|
|
'.log-token-method': {
|
|
|
|
color: theme.colors.info.shade,
|
|
|
|
},
|
|
|
|
},
|
2025-04-04 20:53:12 +08:00
|
|
|
'& .no-highlighting': {
|
|
|
|
color: theme.colors.text.primary,
|
|
|
|
},
|
2025-02-14 19:52:34 +08:00
|
|
|
}),
|
2025-02-28 00:34:02 +08:00
|
|
|
pinnedLogLine: css({
|
|
|
|
backgroundColor: tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString(),
|
|
|
|
}),
|
|
|
|
menuIcon: css({
|
|
|
|
height: getLineHeight(),
|
|
|
|
margin: 0,
|
|
|
|
padding: theme.spacing(0, 0, 0, 0.5),
|
|
|
|
}),
|
2025-02-14 19:52:34 +08:00
|
|
|
logLineMessage: css({
|
|
|
|
fontFamily: theme.typography.fontFamily,
|
2025-03-26 19:48:26 +08:00
|
|
|
justifyContent: 'center',
|
2025-02-05 01:40:17 +08:00
|
|
|
}),
|
|
|
|
timestamp: css({
|
2025-03-26 19:48:26 +08:00
|
|
|
color: theme.colors.text.disabled,
|
2025-02-05 01:40:17 +08:00
|
|
|
display: 'inline-block',
|
|
|
|
}),
|
|
|
|
level: css({
|
|
|
|
color: theme.colors.text.secondary,
|
|
|
|
fontWeight: theme.typography.fontWeightBold,
|
2025-03-26 19:48:26 +08:00
|
|
|
textTransform: 'uppercase',
|
2025-02-05 01:40:17 +08:00
|
|
|
display: 'inline-block',
|
|
|
|
'&.level-critical': {
|
|
|
|
color: colors.critical,
|
|
|
|
},
|
|
|
|
'&.level-error': {
|
|
|
|
color: colors.error,
|
|
|
|
},
|
|
|
|
'&.level-warning': {
|
|
|
|
color: colors.warning,
|
|
|
|
},
|
|
|
|
'&.level-info': {
|
|
|
|
color: colors.info,
|
|
|
|
},
|
|
|
|
'&.level-debug': {
|
|
|
|
color: colors.debug,
|
|
|
|
},
|
|
|
|
}),
|
2025-02-14 19:52:34 +08:00
|
|
|
loadMoreButton: css({
|
|
|
|
background: 'transparent',
|
|
|
|
border: 'none',
|
|
|
|
display: 'inline',
|
|
|
|
}),
|
2025-02-05 01:40:17 +08:00
|
|
|
overflows: css({
|
|
|
|
outline: 'solid 1px red',
|
|
|
|
}),
|
|
|
|
unwrappedLogLine: css({
|
2025-02-27 18:31:55 +08:00
|
|
|
display: 'grid',
|
|
|
|
gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER),
|
2025-02-05 01:40:17 +08:00
|
|
|
whiteSpace: 'pre',
|
2025-02-14 19:52:34 +08:00
|
|
|
paddingBottom: theme.spacing(0.75),
|
2025-02-05 01:40:17 +08:00
|
|
|
}),
|
|
|
|
wrappedLogLine: css({
|
2025-03-26 19:48:26 +08:00
|
|
|
alignSelf: 'flex-start',
|
2025-02-14 19:52:34 +08:00
|
|
|
paddingBottom: theme.spacing(0.75),
|
2025-03-26 19:48:26 +08:00
|
|
|
whiteSpace: 'pre-wrap',
|
2025-02-27 18:31:55 +08:00
|
|
|
'& .field': {
|
|
|
|
marginRight: theme.spacing(FIELD_GAP_MULTIPLIER),
|
|
|
|
},
|
|
|
|
'& .field:last-child': {
|
|
|
|
marginRight: 0,
|
|
|
|
},
|
2025-02-05 01:40:17 +08:00
|
|
|
}),
|
2025-05-07 06:26:54 +08:00
|
|
|
collapsedLogLine: css({
|
|
|
|
maxHeight: `${TRUNCATION_LINE_COUNT * getLineHeight()}px`,
|
|
|
|
overflow: 'hidden',
|
|
|
|
}),
|
|
|
|
expandCollapseControl: css({
|
|
|
|
display: 'flex',
|
|
|
|
justifyContent: 'center',
|
|
|
|
}),
|
|
|
|
expandCollapseControlButton: css({
|
|
|
|
fontWeight: theme.typography.fontWeightLight,
|
|
|
|
height: getLineHeight(),
|
|
|
|
margin: 0,
|
|
|
|
}),
|
2025-02-05 01:40:17 +08:00
|
|
|
};
|
|
|
|
};
|