Skip to content

Commit

Permalink
New Logs Panel: Displayed fields support (grafana#100643)
Browse files Browse the repository at this point in the history
* LogList: add displayedFields and getFieldLinks props

* Render displayed fields

* LogLine: rename function

* Refactor log dimensions

* Generate styles in parent component

* Log List: implement tabular unwrapped logs

* Rename class

* Log line: center fields

* Parametrize field gap

* Virtualization: update measurement to support displayed fields

* Shorten visible level

* Do not calculate dimensions when logs are wrapped

* Logs Navigation: fix width when flag is enabled

* Pass styles to LogLineMessage

* Formatting

* Fix unwrapped logs when showTime is off

* LogLine: update css selectors for fields
  • Loading branch information
matyax authored Feb 27, 2025
1 parent 58457d4 commit 03dcd25
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 47 deletions.
2 changes: 2 additions & 0 deletions public/app/features/explore/Logs/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1070,8 +1070,10 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
<LogList
app={CoreApp.Explore}
containerElement={logsContainerRef.current}
displayedFields={displayedFields}
eventBus={eventBus}
forceEscape={forceEscape}
getFieldLinks={getFieldLinks}
loadMore={loadMoreLogs}
logs={dedupedRows}
showTime={showTime}
Expand Down
2 changes: 1 addition & 1 deletion public/app/features/explore/Logs/LogsNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean) => {
return {
navContainer: css({
maxHeight: navContainerHeight,
width: oldestLogsFirst ? '58px' : 'auto',
width: oldestLogsFirst && !config.featureToggles.newLogsPanel ? '58px' : 'auto',
display: 'flex',
flexDirection: 'column',
justifyContent: config.featureToggles.logsInfiniteScrolling
Expand Down
28 changes: 24 additions & 4 deletions public/app/features/logs/components/panel/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window'

import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { Spinner } from '@grafana/ui';
import { Spinner, useTheme2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';

import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll';

import { LogLine } from './LogLine';
import { getStyles, LogLine } from './LogLine';
import { LogLineMessage } from './LogLineMessage';
import { LogListModel } from './processing';

Expand All @@ -22,6 +22,7 @@ interface ChildrenProps {

interface Props {
children: (props: ChildrenProps) => ReactNode;
displayedFields: string[];
handleOverflow: (index: number, id: string, height: number) => void;
loadMore?: (range: AbsoluteTimeRange) => void;
logs: LogListModel[];
Expand All @@ -38,6 +39,7 @@ type InfiniteLoaderState = 'idle' | 'out-of-bounds' | 'pre-scroll' | 'loading';

export const InfiniteScroll = ({
children,
displayedFields,
handleOverflow,
loadMore,
logs,
Expand All @@ -57,6 +59,8 @@ export const InfiniteScroll = ({
const lastEvent = useRef<Event | WheelEvent | null>(null);
const countRef = useRef(0);
const lastLogOfPage = useRef<string[]>([]);
const theme = useTheme2();
const styles = getStyles(theme);

useEffect(() => {
// Logs have not changed, ignore effect
Expand Down Expand Up @@ -132,24 +136,40 @@ export const InfiniteScroll = ({
({ index, style }: ListChildComponentProps) => {
if (!logs[index] && infiniteLoaderState !== 'idle') {
return (
<LogLineMessage style={style} onClick={infiniteLoaderState === 'pre-scroll' ? onLoadMore : undefined}>
<LogLineMessage
style={style}
styles={styles}
onClick={infiniteLoaderState === 'pre-scroll' ? onLoadMore : undefined}
>
{getMessageFromInfiniteLoaderState(infiniteLoaderState, sortOrder)}
</LogLineMessage>
);
}
return (
<LogLine
displayedFields={displayedFields}
index={index}
log={logs[index]}
showTime={showTime}
style={style}
styles={styles}
variant={getLogLineVariant(logs, index, lastLogOfPage.current)}
wrapLogMessage={wrapLogMessage}
onOverflow={handleOverflow}
/>
);
},
[handleOverflow, infiniteLoaderState, logs, onLoadMore, showTime, sortOrder, wrapLogMessage]
[
displayedFields,
handleOverflow,
infiniteLoaderState,
logs,
onLoadMore,
showTime,
sortOrder,
styles,
wrapLogMessage,
]
);

const onItemsRendered = useCallback(
Expand Down
86 changes: 73 additions & 13 deletions public/app/features/logs/components/panel/LogLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,35 @@ import { css } from '@emotion/css';
import { CSSProperties, useEffect, useRef } from 'react';

import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2 } from '@grafana/ui';

import { LogListModel } from './processing';
import { hasUnderOrOverflow } from './virtualization';
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';

import { LogFieldDimension, LogListModel } from './processing';
import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow } from './virtualization';

interface Props {
displayedFields: string[];
index: number;
log: LogListModel;
showTime: boolean;
style: CSSProperties;
styles: LogLineStyles;
onOverflow?: (index: number, id: string, height: number) => void;
variant?: 'infinite-scroll';
wrapLogMessage: boolean;
}

export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrapLogMessage }: Props) => {
const theme = useTheme2();
const styles = getStyles(theme);
export const LogLine = ({
displayedFields,
index,
log,
style,
styles,
onOverflow,
showTime,
variant,
wrapLogMessage,
}: Props) => {
const logLineRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
Expand All @@ -34,16 +45,59 @@ export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrap
}, [index, log.uid, onOverflow, style.height]);

return (
<div style={style} className={`${styles.logLine} ${variant}`} ref={onOverflow ? logLineRef : undefined}>
<div className={wrapLogMessage ? styles.wrappedLogLine : styles.unwrappedLogLine}>
{showTime && <span className={`${styles.timestamp} level-${log.logLevel}`}>{log.timestamp}</span>}
{log.logLevel && <span className={`${styles.level} level-${log.logLevel}`}>{log.logLevel}</span>}
{log.body}
<div style={style} className={`${styles.logLine} ${variant ?? ''}`} ref={onOverflow ? logLineRef : undefined}>
<div className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`}`}>
<Log displayedFields={displayedFields} log={log} showTime={showTime} styles={styles} />
</div>
</div>
);
};

interface LogProps {
displayedFields: string[];
log: LogListModel;
showTime: boolean;
styles: ReturnType<typeof getStyles>;
}

const Log = ({ displayedFields, log, showTime, styles }: LogProps) => {
return (
<>
{showTime && <span className={`${styles.timestamp} level-${log.logLevel} field`}>{log.timestamp}</span>}
<span className={`${styles.level} level-${log.logLevel} field`}>{log.displayLevel}</span>
{displayedFields.length > 0 ? (
displayedFields.map((field) => (
<span className="field" title={field}>
{getDisplayedFieldValue(field, log)}
</span>
))
) : (
<span className="field">{log.body}</span>
)}
</>
);
};

export function getDisplayedFieldValue(fieldName: string, log: LogListModel): string {
if (fieldName === LOG_LINE_BODY_FIELD_NAME) {
return log.body;
}
if (log.labels[fieldName] != null) {
return log.labels[fieldName];
}
const field = log.fields.find((field) => {
return field.keys[0] === fieldName;
});

return field ? field.values.toString() : '';
}

export function getGridTemplateColumns(dimensions: LogFieldDimension[]) {
const columns = dimensions.map((dimension) => dimension.width).join('px ');
return `${columns}px 1fr`;
}

export type LogLineStyles = ReturnType<typeof getStyles>;
export const getStyles = (theme: GrafanaTheme2) => {
const colors = {
critical: '#B877D9',
Expand Down Expand Up @@ -82,7 +136,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
timestamp: css({
color: theme.colors.text.secondary,
display: 'inline-block',
marginRight: theme.spacing(1),
'&.level-critical': {
color: colors.critical,
},
Expand All @@ -103,7 +156,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
color: theme.colors.text.secondary,
fontWeight: theme.typography.fontWeightBold,
display: 'inline-block',
marginRight: theme.spacing(1),
'&.level-critical': {
color: colors.critical,
},
Expand All @@ -129,12 +181,20 @@ export const getStyles = (theme: GrafanaTheme2) => {
outline: 'solid 1px red',
}),
unwrappedLogLine: css({
display: 'grid',
gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER),
whiteSpace: 'pre',
paddingBottom: theme.spacing(0.75),
}),
wrappedLogLine: css({
whiteSpace: 'pre-wrap',
paddingBottom: theme.spacing(0.75),
'& .field': {
marginRight: theme.spacing(FIELD_GAP_MULTIPLIER),
},
'& .field:last-child': {
marginRight: 0,
},
}),
};
};
9 changes: 3 additions & 6 deletions public/app/features/logs/components/panel/LogLineMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { CSSProperties, ReactNode } from 'react';

import { useTheme2 } from '@grafana/ui';

import { getStyles } from './LogLine';
import { LogLineStyles } from './LogLine';

interface Props {
children: ReactNode;
onClick?: () => void;
style: CSSProperties;
styles: LogLineStyles;
}

export const LogLineMessage = ({ children, onClick, style }: Props) => {
const theme = useTheme2();
const styles = getStyles(theme);
export const LogLineMessage = ({ children, onClick, style, styles }: Props) => {
return (
<div style={style} className={`${styles.logLine} ${styles.logLineMessage}`}>
{onClick ? (
Expand Down
56 changes: 49 additions & 7 deletions public/app/features/logs/components/panel/LogList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { VariableSizeList } from 'react-window';

import { AbsoluteTimeRange, CoreApp, EventBus, LogRowModel, LogsSortOrder, TimeRange } from '@grafana/data';
import {
AbsoluteTimeRange,
CoreApp,
DataFrame,
EventBus,
Field,
LinkModel,
LogRowModel,
LogsSortOrder,
TimeRange,
} from '@grafana/data';
import { useTheme2 } from '@grafana/ui';

import { InfiniteScroll } from './InfiniteScroll';
import { preProcessLogs, LogListModel } from './processing';
import { getGridTemplateColumns } from './LogLine';
import { preProcessLogs, LogListModel, calculateFieldDimensions, LogFieldDimension } from './processing';
import {
getLogLineSize,
init as initVirtualization,
Expand All @@ -15,14 +27,18 @@ import {
storeLogLineSize,
} from './virtualization';

export type GetFieldLinksFn = (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;

interface Props {
app: CoreApp;
logs: LogRowModel[];
containerElement: HTMLDivElement;
displayedFields: string[];
eventBus: EventBus;
forceEscape?: boolean;
getFieldLinks?: GetFieldLinksFn;
initialScrollPosition?: 'top' | 'bottom';
loadMore?: (range: AbsoluteTimeRange) => void;
logs: LogRowModel[];
showTime: boolean;
sortOrder: LogsSortOrder;
timeRange: TimeRange;
Expand All @@ -33,8 +49,10 @@ interface Props {
export const LogList = ({
app,
containerElement,
displayedFields = [],
eventBus,
forceEscape = false,
getFieldLinks,
initialScrollPosition = 'top',
loadMore,
logs,
Expand All @@ -52,6 +70,11 @@ export const LogList = ({
const listRef = useRef<VariableSizeList | null>(null);
const widthRef = useRef(containerElement.clientWidth);
const scrollRef = useRef<HTMLDivElement | null>(null);
const dimensions = useMemo(
() => (wrapLogMessage ? [] : calculateFieldDimensions(processedLogs, displayedFields)),
[displayedFields, processedLogs, wrapLogMessage]
);
const styles = getStyles(dimensions, { showTime });

useEffect(() => {
initVirtualization(theme);
Expand All @@ -65,9 +88,11 @@ export const LogList = ({
}, [eventBus, logs.length]);

useEffect(() => {
setProcessedLogs(preProcessLogs(logs, { wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone }));
setProcessedLogs(
preProcessLogs(logs, { getFieldLinks, wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone })
);
listRef.current?.resetAfterIndex(0);
}, [forceEscape, logs, sortOrder, timeZone, wrapLogMessage]);
}, [forceEscape, getFieldLinks, logs, sortOrder, timeZone, wrapLogMessage]);

useEffect(() => {
const handleResize = debounce(() => {
Expand Down Expand Up @@ -110,6 +135,7 @@ export const LogList = ({

return (
<InfiniteScroll
displayedFields={displayedFields}
handleOverflow={handleOverflow}
logs={processedLogs}
loadMore={loadMore}
Expand All @@ -123,9 +149,13 @@ export const LogList = ({
>
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
<VariableSizeList
className={styles.logList}
height={listHeight}
itemCount={itemCount}
itemSize={getLogLineSize.bind(null, processedLogs, containerElement, { wrap: wrapLogMessage, showTime })}
itemSize={getLogLineSize.bind(null, processedLogs, containerElement, displayedFields, {
wrap: wrapLogMessage,
showTime,
})}
itemKey={getItemKey}
layout="vertical"
onItemsRendered={onItemsRendered}
Expand All @@ -141,6 +171,18 @@ export const LogList = ({
);
};

function getStyles(dimensions: LogFieldDimension[], { showTime }: { showTime: boolean }) {
const columns = showTime ? dimensions : dimensions.filter((_, index) => index > 0);
return {
logList: css({
'& .unwrapped-log-line': {
display: 'grid',
gridTemplateColumns: getGridTemplateColumns(columns),
},
}),
};
}

function handleScrollToEvent(event: ScrollToLogsEvent, logsCount: number, list: VariableSizeList | null) {
if (event.payload.scrollTo === 'top') {
list?.scrollTo(0);
Expand Down
Loading

0 comments on commit 03dcd25

Please sign in to comment.