mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Terminal Output] bug fixes to text sizer and missed lines rendered issue. (#142524)
* removed complex lines per frame logic. caused too many edge cases. tests added to prevent future regressions * fix fit to screen option (when changing from fullscreen to not. also button state). increased playback speed to make up for removal of multi line per frame rendering * fixed tests * removing tty loading technique due to problems with unique char_device in multi container sessions on the same pod Co-authored-by: Karl Godard <karlgodard@elastic.co>
This commit is contained in:
parent
b3a749e55a
commit
6de0091178
10 changed files with 64 additions and 127 deletions
|
@ -48,8 +48,7 @@ export const ALERT_STATUS = {
|
|||
export const LOCAL_STORAGE_DISPLAY_OPTIONS_KEY = 'sessionView:displayOptions';
|
||||
export const MOUSE_EVENT_PLACEHOLDER = { stopPropagation: () => undefined } as React.MouseEvent;
|
||||
export const DEBOUNCE_TIMEOUT = 500;
|
||||
export const DEFAULT_TTY_PLAYSPEED_MS = 50; // milliseconds per render loop
|
||||
export const TTY_LINES_PER_FRAME = 5; // number of lines to print to xterm on each render loop
|
||||
export const DEFAULT_TTY_PLAYSPEED_MS = 30; // milliseconds per render loop
|
||||
export const TTY_LINES_PRE_SEEK = 200; // number of lines to redraw before the point we are seeking to.
|
||||
export const DEFAULT_TTY_FONT_SIZE = 11;
|
||||
export const DEFAULT_TTY_ROWS = 66;
|
||||
|
|
|
@ -39,6 +39,8 @@ describe('SessionView component', () => {
|
|||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
global.ResizeObserver = require('resize-observer-polyfill');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -8,11 +8,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
|
|||
import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock';
|
||||
import { useIOLines, useXtermPlayer, XtermPlayerDeps } from './hooks';
|
||||
import { ProcessEventsPage } from '../../../common/types/process_tree';
|
||||
import {
|
||||
DEFAULT_TTY_FONT_SIZE,
|
||||
DEFAULT_TTY_PLAYSPEED_MS,
|
||||
TTY_LINES_PER_FRAME,
|
||||
} from '../../../common/constants';
|
||||
import { DEFAULT_TTY_FONT_SIZE, DEFAULT_TTY_PLAYSPEED_MS } from '../../../common/constants';
|
||||
|
||||
const VIM_LINE_START = 22;
|
||||
|
||||
|
@ -132,9 +128,7 @@ describe('TTYPlayer/hooks', () => {
|
|||
jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10);
|
||||
});
|
||||
|
||||
const expectedLineNumber = Math.min(initialProps.lines.length - 1, TTY_LINES_PER_FRAME * 10);
|
||||
|
||||
expect(result.current.currentLine).toBe(expectedLineNumber);
|
||||
expect(result.current.currentLine).toBe(10);
|
||||
});
|
||||
|
||||
it('allows the user to stop', async () => {
|
||||
|
@ -150,9 +144,7 @@ describe('TTYPlayer/hooks', () => {
|
|||
act(() => {
|
||||
jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10);
|
||||
});
|
||||
const expectedLineNumber = Math.min(initialProps.lines.length - 1, TTY_LINES_PER_FRAME * 10);
|
||||
|
||||
expect(result.current.currentLine).toBe(expectedLineNumber); // should not have advanced
|
||||
expect(result.current.currentLine).toBe(10); // should not have advanced
|
||||
});
|
||||
|
||||
it('should stop when it reaches the end of the array of lines', async () => {
|
||||
|
@ -182,6 +174,39 @@ describe('TTYPlayer/hooks', () => {
|
|||
expect(result.current.terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('256');
|
||||
});
|
||||
|
||||
it('ensure the first few render loops have printed the right lines', async () => {
|
||||
const { result, rerender } = renderHook((props) => useXtermPlayer(props), {
|
||||
initialProps,
|
||||
});
|
||||
|
||||
const LOOPS = 6;
|
||||
|
||||
rerender({ ...initialProps, isPlaying: true });
|
||||
|
||||
act(() => {
|
||||
// advance render loop
|
||||
jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * LOOPS);
|
||||
});
|
||||
|
||||
rerender({ ...initialProps, isPlaying: false });
|
||||
|
||||
expect(result.current.terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('256');
|
||||
expect(result.current.terminal.buffer.active.getLine(1)?.translateToString(true)).toBe(',');
|
||||
expect(result.current.terminal.buffer.active.getLine(2)?.translateToString(true)).toBe(
|
||||
' Some Companies Puppet instance'
|
||||
);
|
||||
expect(result.current.terminal.buffer.active.getLine(3)?.translateToString(true)).toBe(
|
||||
' | | | CentOS Stream release 8 on x86_64'
|
||||
);
|
||||
expect(result.current.terminal.buffer.active.getLine(4)?.translateToString(true)).toBe(
|
||||
' *********************** Load average: 1.23, 1.01, 0.63'
|
||||
);
|
||||
expect(result.current.terminal.buffer.active.getLine(5)?.translateToString(true)).toBe(
|
||||
' ************************ '
|
||||
);
|
||||
expect(result.current.currentLine).toBe(LOOPS);
|
||||
});
|
||||
|
||||
it('will allow a plain text search highlight on the last line printed', async () => {
|
||||
const { result: xTermResult } = renderHook((props) => useXtermPlayer(props), {
|
||||
initialProps,
|
||||
|
|
|
@ -29,7 +29,6 @@ import {
|
|||
DEFAULT_TTY_ROWS,
|
||||
DEFAULT_TTY_COLS,
|
||||
TTY_LINE_SPLITTER_REGEX,
|
||||
TTY_LINES_PER_FRAME,
|
||||
TTY_LINES_PRE_SEEK,
|
||||
} from '../../../common/constants';
|
||||
|
||||
|
@ -226,6 +225,7 @@ export const useXtermPlayer = ({
|
|||
|
||||
if (clear) {
|
||||
linesToPrint = lines.slice(Math.max(0, lineNumber - TTY_LINES_PRE_SEEK), lineNumber + 1);
|
||||
|
||||
try {
|
||||
terminal.reset();
|
||||
terminal.clear();
|
||||
|
@ -234,7 +234,7 @@ export const useXtermPlayer = ({
|
|||
// there is some random race condition with the jump to feature that causes these calls to error out.
|
||||
}
|
||||
} else {
|
||||
linesToPrint = lines.slice(lineNumber, lineNumber + TTY_LINES_PER_FRAME);
|
||||
linesToPrint = lines.slice(lineNumber, lineNumber + 1);
|
||||
}
|
||||
|
||||
linesToPrint.forEach((line, index) => {
|
||||
|
@ -243,7 +243,7 @@ export const useXtermPlayer = ({
|
|||
}
|
||||
});
|
||||
},
|
||||
[terminal, lines]
|
||||
[lines, terminal]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -284,9 +284,9 @@ export const useXtermPlayer = ({
|
|||
if (!hasNextPage && currentLine === lines.length - 1) {
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
const nextLine = Math.min(lines.length - 1, currentLine + TTY_LINES_PER_FRAME);
|
||||
setCurrentLine(nextLine);
|
||||
const nextLine = Math.min(lines.length - 1, currentLine + 1);
|
||||
render(nextLine, false);
|
||||
setCurrentLine(nextLine);
|
||||
}
|
||||
}, playSpeed);
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ describe('TTYPlayer component', () => {
|
|||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
global.ResizeObserver = require('resize-observer-polyfill');
|
||||
});
|
||||
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
EuiButton,
|
||||
EuiBetaBadge,
|
||||
} from '@elastic/eui';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { throttle } from 'lodash';
|
||||
import { ProcessEvent } from '../../../common/types/process_tree';
|
||||
import { TTYSearchBar } from '../tty_search_bar';
|
||||
|
@ -45,7 +46,7 @@ export const TTYPlayer = ({
|
|||
autoSeekToEntityId,
|
||||
}: TTYPlayerDeps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const { ref: scrollRef, height: containerHeight = 1 } = useResizeObserver<HTMLDivElement>({});
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetching, refetch } =
|
||||
useFetchIOEvents(sessionEntityId);
|
||||
|
@ -188,7 +189,7 @@ export const TTYPlayer = ({
|
|||
textSizer={
|
||||
<TTYTextSizer
|
||||
tty={tty}
|
||||
containerHeight={scrollRef?.current?.offsetHeight || 0}
|
||||
containerHeight={containerHeight}
|
||||
fontSize={fontSize}
|
||||
onFontSizeChanged={setFontSize}
|
||||
isFullscreen={isFullscreen}
|
||||
|
|
|
@ -79,9 +79,15 @@ describe('TTYTextSizer component', () => {
|
|||
|
||||
it('emits a font size to fit to full screen, when isFullscreen = true', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<TTYTextSizer {...props} isFullscreen={true} containerHeight={400} />
|
||||
<TTYTextSizer {...props} isFullscreen containerHeight={400} />
|
||||
);
|
||||
|
||||
const zoomFitBtn = renderResult.queryByTestId('sessionView:TTYZoomFit');
|
||||
|
||||
if (zoomFitBtn) {
|
||||
userEvent.click(zoomFitBtn);
|
||||
}
|
||||
|
||||
expect(props.onFontSizeChanged).toHaveBeenCalledTimes(1);
|
||||
expect(props.onFontSizeChanged).toHaveBeenCalledWith(FULL_SCREEN_FONT_SIZE);
|
||||
});
|
||||
|
|
|
@ -65,13 +65,7 @@ export const TTYTextSizer = ({
|
|||
onFontSizeChanged(newSize);
|
||||
}
|
||||
}
|
||||
}, [containerHeight, fit, fontSize, onFontSizeChanged, tty?.rows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFullscreen) {
|
||||
setFit(true);
|
||||
}
|
||||
}, [isFullscreen]);
|
||||
}, [isFullscreen, containerHeight, fit, fontSize, onFontSizeChanged, tty?.rows]);
|
||||
|
||||
const onToggleFit = useCallback(() => {
|
||||
const newValue = !fit;
|
||||
|
@ -100,7 +94,8 @@ export const TTYTextSizer = ({
|
|||
display={fit ? 'fill' : 'empty'}
|
||||
iconType={fit ? 'expand' : 'minimize'}
|
||||
onClick={onToggleFit}
|
||||
{...commonButtonProps}
|
||||
size="s"
|
||||
color="ghost"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -4,16 +4,13 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
import { EVENT_ACTION } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
GET_TOTAL_IO_BYTES_ROUTE,
|
||||
PROCESS_EVENTS_INDEX,
|
||||
TOTAL_BYTES_CAPTURED_PROPERTY,
|
||||
TTY_CHAR_DEVICE_MAJOR_PROPERTY,
|
||||
TTY_CHAR_DEVICE_MINOR_PROPERTY,
|
||||
HOST_ID_PROPERTY,
|
||||
ENTRY_SESSION_ENTITY_ID_PROPERTY,
|
||||
} from '../../common/constants';
|
||||
import { getTTYQueryPredicates } from './io_events_route';
|
||||
|
||||
export const registerGetTotalIOBytesRoute = (router: IRouter) => {
|
||||
router.get(
|
||||
|
@ -30,30 +27,14 @@ export const registerGetTotalIOBytesRoute = (router: IRouter) => {
|
|||
const { sessionEntityId } = request.query;
|
||||
|
||||
try {
|
||||
const ttyPredicates = await getTTYQueryPredicates(client, sessionEntityId);
|
||||
|
||||
if (!ttyPredicates) {
|
||||
return response.ok({ body: { total: 0 } });
|
||||
}
|
||||
|
||||
const search = await client.search({
|
||||
index: [PROCESS_EVENTS_INDEX],
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { [TTY_CHAR_DEVICE_MAJOR_PROPERTY]: ttyPredicates.ttyMajor } },
|
||||
{ term: { [TTY_CHAR_DEVICE_MINOR_PROPERTY]: ttyPredicates.ttyMinor } },
|
||||
{ term: { [HOST_ID_PROPERTY]: ttyPredicates.hostId } },
|
||||
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
|
||||
{ term: { [EVENT_ACTION]: 'text_output' } },
|
||||
{
|
||||
range: {
|
||||
[TIMESTAMP]: {
|
||||
gte: ttyPredicates.range[0],
|
||||
lte: ttyPredicates.range[1],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -8,75 +8,17 @@ import { schema } from '@kbn/config-schema';
|
|||
import { IRouter } from '@kbn/core/server';
|
||||
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { parse } from '@kbn/datemath';
|
||||
import { Aggregate } from '../../common/types/aggregate';
|
||||
import { EventAction, EventKind, ProcessEvent } from '../../common/types/process_tree';
|
||||
import { EventAction, EventKind } from '../../common/types/process_tree';
|
||||
import {
|
||||
IO_EVENTS_ROUTE,
|
||||
IO_EVENTS_PER_PAGE,
|
||||
PROCESS_EVENTS_INDEX,
|
||||
ENTRY_SESSION_ENTITY_ID_PROPERTY,
|
||||
TTY_CHAR_DEVICE_MAJOR_PROPERTY,
|
||||
TTY_CHAR_DEVICE_MINOR_PROPERTY,
|
||||
HOST_ID_PROPERTY,
|
||||
PROCESS_ENTITY_ID_PROPERTY,
|
||||
PROCESS_EVENTS_PER_PAGE,
|
||||
} from '../../common/constants';
|
||||
|
||||
/**
|
||||
* Grabs the most recent event for the session and extracts the TTY char_device
|
||||
* major/minor numbers, boot id, and session date range to use in querying for tty IO events.
|
||||
* This is done so that any process from any session that writes to this TTY at the time of
|
||||
* this session will be shown in the TTY Player. e.g. wall
|
||||
*/
|
||||
export const getTTYQueryPredicates = async (
|
||||
client: ElasticsearchClient,
|
||||
sessionEntityId: string
|
||||
) => {
|
||||
const lastEventQuery = await client.search({
|
||||
index: [PROCESS_EVENTS_INDEX],
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{ term: { [EVENT_ACTION]: 'fork' } },
|
||||
{ term: { [EVENT_ACTION]: 'exec' } },
|
||||
{ term: { [EVENT_ACTION]: 'end' } },
|
||||
{ term: { [EVENT_ACTION]: 'text_output' } },
|
||||
],
|
||||
must: [{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } }],
|
||||
},
|
||||
},
|
||||
size: 1,
|
||||
sort: [{ [TIMESTAMP]: 'desc' }],
|
||||
},
|
||||
});
|
||||
|
||||
const lastEventHits = lastEventQuery.hits.hits;
|
||||
|
||||
if (lastEventHits.length > 0) {
|
||||
const lastEvent: ProcessEvent = lastEventHits[0]._source as ProcessEvent;
|
||||
const lastEventTime = lastEvent['@timestamp'];
|
||||
const rangeEnd =
|
||||
(lastEventTime && parse(lastEventTime)?.toISOString()) || new Date().toISOString();
|
||||
const range = [lastEvent?.process?.entry_leader?.start, rangeEnd];
|
||||
const tty = lastEvent?.process?.entry_leader?.tty;
|
||||
const hostId = lastEvent?.host?.id;
|
||||
|
||||
if (tty?.char_device?.major !== undefined && tty?.char_device?.minor !== undefined && hostId) {
|
||||
return {
|
||||
ttyMajor: tty.char_device.major,
|
||||
ttyMinor: tty.char_device.minor,
|
||||
hostId,
|
||||
range,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const registerIOEventsRoute = (router: IRouter) => {
|
||||
router.get(
|
||||
{
|
||||
|
@ -94,30 +36,14 @@ export const registerIOEventsRoute = (router: IRouter) => {
|
|||
const { sessionEntityId, cursor, pageSize = IO_EVENTS_PER_PAGE } = request.query;
|
||||
|
||||
try {
|
||||
const ttyPredicates = await getTTYQueryPredicates(client, sessionEntityId);
|
||||
|
||||
if (!ttyPredicates) {
|
||||
return response.ok({ body: { total: 0, events: [] } });
|
||||
}
|
||||
|
||||
const search = await client.search({
|
||||
index: [PROCESS_EVENTS_INDEX],
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { [TTY_CHAR_DEVICE_MAJOR_PROPERTY]: ttyPredicates.ttyMajor } },
|
||||
{ term: { [TTY_CHAR_DEVICE_MINOR_PROPERTY]: ttyPredicates.ttyMinor } },
|
||||
{ term: { [HOST_ID_PROPERTY]: ttyPredicates.hostId } },
|
||||
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
|
||||
{ term: { [EVENT_ACTION]: 'text_output' } },
|
||||
{
|
||||
range: {
|
||||
[TIMESTAMP]: {
|
||||
gte: ttyPredicates.range[0]?.toString(),
|
||||
lte: ttyPredicates.range[1]?.toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue