Skip to content

Commit

Permalink
Used LogViewer of PF for pipeline logs
Browse files Browse the repository at this point in the history
  • Loading branch information
lokanandaprabhu committed Sep 17, 2024
1 parent fd27abc commit 82307d0
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 308 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@patternfly/react-component-groups": "^5.1.0",
"@patternfly/react-core": "^5.2.1",
"@patternfly/react-icons": "5.2.1",
"@patternfly/react-log-viewer": "5.3.0",
"@patternfly/react-table": "5.2.1",
"@patternfly/react-tokens": "5.2.1",
"@patternfly/react-topology": "5.2.1",
Expand Down
13 changes: 0 additions & 13 deletions src/components/logs/Logs.scss

This file was deleted.

296 changes: 189 additions & 107 deletions src/components/logs/Logs.tsx
Original file line number Diff line number Diff line change
@@ -1,151 +1,233 @@
import * as React from 'react';
import { Alert } from '@patternfly/react-core';
import { LogViewer } from '@patternfly/react-log-viewer';
import { Base64 } from 'js-base64';
import { throttle } from 'lodash';
import { useTranslation } from 'react-i18next';
import { consoleFetchText } from '@openshift-console/dynamic-plugin-sdk';
import { LOG_SOURCE_TERMINATED } from '../../consts';
import { WSFactory } from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s/ws-factory';
import { ContainerSpec, PodKind } from '../../types';
import { ContainerSpec, ContainerStatus, PodKind } from '../../types';
import { PodModel } from '../../models';
import { resourceURL } from '../utils/k8s-utils';
import './Logs.scss';
import { containerToLogSourceStatus } from '../utils/pipeline-utils';
import './MultiStreamLogs.scss';

consoleFetchText;
type LogsProps = {
resource: PodKind;
resourceStatus: string;
container: ContainerSpec;
render: boolean;
autoScroll?: boolean;
onComplete: (containerName: string) => void;
containers: ContainerSpec[];
setCurrentLogsGetter?: (getter: () => string) => void;
};

type LogData = {
[containerName: string]: {
logs: string[];
status: string;
};
};

const processLogData = (
logData: LogData,
containers: ContainerSpec[],
): string => {
let result = '';

containers.map(({ name: containerName }) => {
if (logData[containerName]) {
const { logs } = logData[containerName];
const uniqueLogs = Array.from(new Set(logs));
const filteredLogs = uniqueLogs.filter((log) => log.trim() !== '');
const formattedContainerName = `${containerName.toUpperCase()}`;

if (filteredLogs.length === 0) {
result += `${formattedContainerName}\n\n`;
} else {
const joinedLogs = filteredLogs.join('\n');
result += `${formattedContainerName}\n${joinedLogs}\n\n`;
}
}
});
return result;
};

const Logs: React.FC<LogsProps> = ({
resource,
resourceStatus,
container,
onComplete,
render,
autoScroll = true,
containers,
setCurrentLogsGetter,
}) => {
if (!resource) return null;
const { t } = useTranslation('plugin__pipelines-console-plugin');
const { name } = container;
const { kind, metadata = {} } = resource;
const { metadata = {} } = resource;
const { name: resName, namespace: resNamespace } = metadata;
const scrollToRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const [error, setError] = React.useState<boolean>(false);
const resourceStatusRef = React.useRef<string>(resourceStatus);
const onCompleteRef = React.useRef<(name) => void>();
const blockContentRef = React.useRef<string>('');
onCompleteRef.current = onComplete;

const addContentAndScroll = React.useCallback(
throttle(() => {
if (contentRef.current) {
contentRef.current.innerText += blockContentRef.current;
}
if (scrollToRef.current) {
scrollToRef.current.scrollIntoView({
behavior: 'smooth',
block: 'end',
});
}
blockContentRef.current = '';
}, 1000),
[],
const [logData, setLogData] = React.useState<LogData>({});
const [formattedLogString, setFormattedLogString] = React.useState('');
const [scrollToRow, setScrollToRow] = React.useState<number>(0);
const [activeContainers, setActiveContainers] = React.useState<Set<string>>(
new Set(),
);

const appendMessage = React.useRef<(blockContent) => void>();
React.useEffect(() => {
setCurrentLogsGetter(() => {
return formattedLogString;
});
}, [setCurrentLogsGetter, formattedLogString]);

const appendMessage = React.useCallback(
(containerName: string, blockContent: string, resourceStatus: string) => {
if (blockContent) {
setLogData((prevLogData) => {
if (resourceStatus === 'terminated') {
// Replace the entire content with blockContent
return {
...prevLogData,
[containerName]: {
logs: [blockContent],
status: resourceStatus,
},
};
} else {
// Otherwise, append the blockContent to the existing logs
const existingLogs = prevLogData[containerName]?.logs || [];
const updatedLogs = [...existingLogs, blockContent.trimEnd()];

appendMessage.current = React.useCallback(
(blockContent: string) => {
blockContentRef.current += blockContent;
if (scrollToRef.current && blockContent && render && autoScroll) {
addContentAndScroll();
return {
...prevLogData,
[containerName]: {
logs: updatedLogs,
status: resourceStatus,
},
};
}
});
}
},
[autoScroll, render, addContentAndScroll],
[],
);

if (resourceStatusRef.current !== resourceStatus) {
resourceStatusRef.current = resourceStatus;
}
const retryWebSocket = (
watchURL: string,
wsOpts: any,
onMessage: (message: string) => void,
onError: () => void,
retryCount = 0,
) => {
let ws = new WSFactory(watchURL, wsOpts);
const handleError = () => {
if (retryCount < 5) {
setTimeout(() => {
retryWebSocket(watchURL, wsOpts, onMessage, onError, retryCount + 1);
}, 3000); // Retry after 3 seconds
} else {
onError();
}
};

ws.onmessage((msg) => {
const message = Base64.decode(msg);
onMessage(message);
}).onerror(() => {
handleError();
});

return ws;
};

React.useEffect(() => {
let loaded = false;
let ws: WSFactory;
const urlOpts = {
ns: resNamespace,
name: resName,
path: 'log',
queryParams: {
container: name,
follow: 'true',
timestamps: 'true',
},
};
const watchURL = resourceURL(PodModel, urlOpts);
if (resourceStatusRef.current === LOG_SOURCE_TERMINATED) {
consoleFetchText(watchURL)
.then((res) => {
if (loaded) return;
appendMessage.current(res);
onCompleteRef.current(name);
})
.catch(() => {
if (loaded) return;
setError(true);
onCompleteRef.current(name);
});
} else {
const wsOpts = {
host: 'auto',
path: watchURL,
subprotocols: ['base64.binary.k8s.io'],
containers.forEach((container) => {
if (activeContainers.has(container.name)) return;
setActiveContainers((prev) => new Set(prev).add(container.name));
let loaded = false;
let ws: WSFactory;
const { name } = container;
const urlOpts = {
ns: resNamespace,
name: resName,
path: 'log',
queryParams: {
container: name,
follow: 'true',
timestamps: 'true',
},
};
ws = new WSFactory(watchURL, wsOpts);
ws.onmessage((msg) => {
if (loaded) return;
const message = Base64.decode(msg);
appendMessage.current(message);
})
.onclose(() => {
onCompleteRef.current(name);
})
.onerror(() => {
if (loaded) return;
setError(true);
onCompleteRef.current(name);
});
}
return () => {
loaded = true;
ws && ws.destroy();
};
}, [kind, name, resName, resNamespace]);
const watchURL = resourceURL(PodModel, urlOpts);

const containerStatus: ContainerStatus[] =
resource?.status?.containerStatuses ?? [];
const statusIndex = containerStatus.findIndex(
(c) => c.name === container.name,
);
const resourceStatus = containerToLogSourceStatus(
containerStatus[statusIndex],
);

if (resourceStatus === LOG_SOURCE_TERMINATED) {
consoleFetchText(watchURL)
.then((res) => {
if (loaded) return;
appendMessage(name, res, resourceStatus);
})
.catch(() => {
if (loaded) return;
setError(true);
});
} else {
const wsOpts = {
host: 'auto',
path: watchURL,
subprotocols: ['base64.binary.k8s.io'],
};
ws = retryWebSocket(
watchURL,
wsOpts,
(message) => {
if (loaded) return;
setError(false);
appendMessage(name, message, resourceStatus);
},
() => {
if (loaded) return;
setError(true);
},
);
}
return () => {
loaded = true;
if (ws) {
ws.destroy();
}
};
});
}, [
resName,
resNamespace,
resource?.status?.containerStatuses,
activeContainers,
]);

React.useEffect(() => {
if (scrollToRef.current && render && autoScroll) {
addContentAndScroll();
}
}, [autoScroll, render, addContentAndScroll]);
const formattedString = processLogData(logData, containers);
setFormattedLogString(formattedString);
const totalLines = formattedString.split('\n').length;
setScrollToRow(totalLines);
}, [logData]);

return (
<div className="odc-logs" style={{ display: render ? '' : 'none' }}>
<p className="odc-logs__name">{name}</p>
<div className="odc-logs-logviewer">
{error && (
<Alert
variant="danger"
isInline
title={t('An error occurred while retrieving the requested logs.')}
/>
)}
<div>
<div className="odc-logs__content" ref={contentRef} />
<div ref={scrollToRef} />
</div>
<LogViewer
hasLineNumbers={false}
isTextWrapped={false}
data={formattedLogString}
theme="dark"
scrollToRow={scrollToRow}
height="100%"
/>
</div>
);
};
Expand Down
11 changes: 11 additions & 0 deletions src/components/logs/MultiStreamLogs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
width: 100%;
}
}
&__logviewer {
background-color: var(--pf-v5-global--palette--black-1000);
color: var(--pf-v5-global--Color--light-100);
font-family: Menlo, Monaco, 'Courier New', monospace;
height: 100%;
}
&__taskName {
background-color: var(--pf-v5-global--BackgroundColor--dark-300);
padding: var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md);
Expand All @@ -45,3 +51,8 @@
margin-left: var(--pf-v5-global--spacer--sm);
}
}

.odc-logs-logviewer {
background-color: var(--pf-v5-global--palette--black-1000);
height: 100%;
}
Loading

0 comments on commit 82307d0

Please sign in to comment.