Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 94 additions & 9 deletions frontend/src/components/common/Resource/LogsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { KubeObject } from '../../../lib/k8s/KubeObject';
import Pod from '../../../lib/k8s/pod';
import ReplicaSet from '../../../lib/k8s/replicaSet';
import { Activity } from '../../activity/Activity';
import { colorizePrettifiedLog } from '../../pod/jsonHandling';
import ActionButton from '../ActionButton';
import { LogViewer } from '../LogViewer';
import { LightTooltip } from '../Tooltip';
Expand Down Expand Up @@ -67,6 +68,9 @@ function LogsButtonContent({ item }: LogsButtonProps) {
const [lines, setLines] = useState<number>(100);
const [showPrevious, setShowPrevious] = React.useState<boolean>(false);
const [showReconnectButton, setShowReconnectButton] = useState(false);
const [prettifyLogs, setPrettifyLogs] = useState<boolean>(false);
const [formatJsonValues, setFormatJsonValues] = useState<boolean>(false);
const [hasJsonLogs, setHasJsonLogs] = useState<boolean>(false);

const xtermRef = React.useRef<XTerminal | null>(null);
const { t } = useTranslation(['glossary', 'translation']);
Expand Down Expand Up @@ -141,6 +145,14 @@ function LogsButtonContent({ item }: LogsButtonProps) {
setShowPrevious(previous => !previous);
}

function handlePrettifyChange() {
setPrettifyLogs(prev => !prev);
}

function handleFormatJsonValuesChange() {
setFormatJsonValues(prev => !prev);
}

// Handler for initial logs button click
async function onMount() {
if (item instanceof Deployment || item instanceof ReplicaSet || item instanceof DaemonSet) {
Expand Down Expand Up @@ -222,16 +234,19 @@ function LogsButtonContent({ item }: LogsButtonProps) {
return timestampA.localeCompare(timestampB);
});

const displayLogs =
prettifyLogs && hasJsonLogs ? allLogs.map(log => colorizePrettifiedLog(log)) : allLogs;

if (xtermRef.current) {
xtermRef.current.clear();
xtermRef.current.write(allLogs.join('').replaceAll('\n', '\r\n'));
xtermRef.current.write(displayLogs.join('').replaceAll('\n', '\r\n'));
}

setLogs({
logs: allLogs,
lastLineShown: allLogs.length - 1,
});
}, [allPodLogs]);
}, [allPodLogs, prettifyLogs, hasJsonLogs]);

// Function to fetch and aggregate logs from all pods
function fetchAllPodsLogs(pods: Pod[], container: string): () => void {
Expand All @@ -243,7 +258,16 @@ function LogsButtonContent({ item }: LogsButtonProps) {
pods.forEach(pod => {
const cleanup = pod.getLogs(
container,
({ logs: newLogs }: { logs: string[]; hasJsonLogs?: boolean }) => {
({
logs: newLogs,
hasJsonLogs: newHasJsonLogs,
}: {
logs: string[];
hasJsonLogs?: boolean;
}) => {
if (newHasJsonLogs) {
setHasJsonLogs(true);
}
const podName = pod.getName();
setAllPodLogs(current => {
const updated = {
Expand All @@ -258,6 +282,8 @@ function LogsButtonContent({ item }: LogsButtonProps) {
showPrevious,
showTimestamps,
follow,
prettifyLogs,
formatJsonValues,
onReconnectStop: () => {
setShowReconnectButton(true);
},
Expand All @@ -277,6 +303,7 @@ function LogsButtonContent({ item }: LogsButtonProps) {
if (selectedContainer) {
clearLogs();
setAllPodLogs({}); // Clear aggregated logs when switching pods
setHasJsonLogs(false);

// Handle paused logs state
if (!follow && logs.logs.length > 0) {
Expand All @@ -296,8 +323,17 @@ function LogsButtonContent({ item }: LogsButtonProps) {
let lastLogLength = 0;
cleanup = pod.getLogs(
selectedContainer,
({ logs: newLogs }: { logs: string[]; hasJsonLogs?: boolean }) => {
({
logs: newLogs,
hasJsonLogs: newHasJsonLogs,
}: {
logs: string[];
hasJsonLogs?: boolean;
}) => {
if (!isSubscribed) return;
if (newHasJsonLogs) {
setHasJsonLogs(true);
}

setLogs(current => {
const terminalRef = xtermRef.current;
Expand All @@ -310,10 +346,12 @@ function LogsButtonContent({ item }: LogsButtonProps) {
const endIdx = Math.min(startIdx + CHUNK_SIZE, newLogs.length);

// Process only the new chunk of logs
const newLogContent = newLogs
.slice(startIdx, endIdx)
.join('')
.replaceAll('\n', '\r\n');
const logSlice = newLogs.slice(startIdx, endIdx);
const displaySlice =
prettifyLogs && newHasJsonLogs
? logSlice.map(log => colorizePrettifiedLog(log))
: logSlice;
const newLogContent = displaySlice.join('').replaceAll('\n', '\r\n');

terminalRef.write(newLogContent);
lastLogLength = endIdx;
Expand Down Expand Up @@ -342,6 +380,8 @@ function LogsButtonContent({ item }: LogsButtonProps) {
showPrevious,
showTimestamps,
follow,
prettifyLogs,
formatJsonValues,
onReconnectStop: () => {
if (isSubscribed) {
setShowReconnectButton(true);
Expand All @@ -359,7 +399,18 @@ function LogsButtonContent({ item }: LogsButtonProps) {
cleanup();
}
};
}, [selectedPodIndex, selectedContainer, lines, showTimestamps, follow, clearLogs, t, pods]);
}, [
selectedPodIndex,
selectedContainer,
lines,
showTimestamps,
follow,
clearLogs,
t,
pods,
prettifyLogs,
formatJsonValues,
]);

// Effect to process logs when allPodLogs changes - only for "All Pods" mode
React.useEffect(() => {
Expand Down Expand Up @@ -496,6 +547,40 @@ function LogsButtonContent({ item }: LogsButtonProps) {
disabled={false}
/>
</LightTooltip>

{/* Prettify JSON logs switch */}
{hasJsonLogs && (
<PaddedFormControlLabel
label={t('translation|Prettify')}
control={
<Switch
checked={prettifyLogs}
onChange={handlePrettifyChange}
size="small"
sx={{ transform: 'scale(0.8)' }}
/>
}
/>
)}

{/* Format JSON values switch */}
{hasJsonLogs && (
<LightTooltip
title={t('translation|Show JSON values in plain text by removing escape characters.')}
>
<PaddedFormControlLabel
label={t('translation|Format')}
control={
<Switch
checked={formatJsonValues}
onChange={handleFormatJsonValuesChange}
size="small"
sx={{ transform: 'scale(0.8)' }}
/>
}
/>
</LightTooltip>
)}
</Box>,
];

Expand Down
25 changes: 16 additions & 9 deletions frontend/src/lib/k8s/pod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,16 @@ class Pod extends KubeObject<KubePod> {
.replace(/\\\\/g, '\\'); // Backslash
}

function prettifyLogLine(logLine: string): string {
try {
const jsonMatch = logLine.match(/(\{.*\})/);
if (!jsonMatch) return logLine;
function findJsonBounds(line: string): { start: number; end: number } | null {
const start = line.indexOf('{');
const end = line.lastIndexOf('}');
if (start === -1 || end <= start) return null;
return { start, end };
}

const jsonStr = jsonMatch[1];
function prettifyLogLine(logLine: string, jsonBounds: { start: number; end: number }): string {
try {
const jsonStr = logLine.substring(jsonBounds.start, jsonBounds.end + 1);
const jsonObj = JSON.parse(jsonStr);

const valueReplacer = formatJsonValues
Expand All @@ -205,7 +209,7 @@ class Pod extends KubeObject<KubePod> {
: prettyJson;

if (showTimestamps) {
const timestamp = logLine.slice(0, jsonMatch.index).trim();
const timestamp = logLine.slice(0, jsonBounds.start).trim();
return timestamp ? `${timestamp}\n${terminalReadyJson}\n` : `${terminalReadyJson}\n`;
} else {
return `${terminalReadyJson}\n`;
Expand All @@ -221,9 +225,12 @@ class Pod extends KubeObject<KubePod> {
const decodedLog = Base64.decode(item);
if (!decodedLog || decodedLog.trim() === '') return;
const trimmedLog = decodedLog.trim();
const jsonMatch = trimmedLog.match(/(\{.*\})/);
if (jsonMatch) hasJsonLogs = true;
const processedLog = hasJsonLogs && prettifyLogs ? prettifyLogLine(decodedLog) : decodedLog;
const jsonBounds = findJsonBounds(trimmedLog);

const processedLog =
!!jsonBounds && prettifyLogs ? prettifyLogLine(decodedLog, jsonBounds) : decodedLog;

hasJsonLogs = !!jsonBounds;
logs.push(processedLog);
onLogs({ logs, hasJsonLogs });
}
Expand Down
Loading