diff --git a/frontend/src/components/common/Resource/LogsButton.tsx b/frontend/src/components/common/Resource/LogsButton.tsx index 98cafa2442c..5e2a7c6c812 100644 --- a/frontend/src/components/common/Resource/LogsButton.tsx +++ b/frontend/src/components/common/Resource/LogsButton.tsx @@ -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'; @@ -67,6 +68,9 @@ function LogsButtonContent({ item }: LogsButtonProps) { const [lines, setLines] = useState(100); const [showPrevious, setShowPrevious] = React.useState(false); const [showReconnectButton, setShowReconnectButton] = useState(false); + const [prettifyLogs, setPrettifyLogs] = useState(false); + const [formatJsonValues, setFormatJsonValues] = useState(false); + const [hasJsonLogs, setHasJsonLogs] = useState(false); const xtermRef = React.useRef(null); const { t } = useTranslation(['glossary', 'translation']); @@ -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) { @@ -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 { @@ -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 = { @@ -258,6 +282,8 @@ function LogsButtonContent({ item }: LogsButtonProps) { showPrevious, showTimestamps, follow, + prettifyLogs, + formatJsonValues, onReconnectStop: () => { setShowReconnectButton(true); }, @@ -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) { @@ -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; @@ -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; @@ -342,6 +380,8 @@ function LogsButtonContent({ item }: LogsButtonProps) { showPrevious, showTimestamps, follow, + prettifyLogs, + formatJsonValues, onReconnectStop: () => { if (isSubscribed) { setShowReconnectButton(true); @@ -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(() => { @@ -496,6 +547,40 @@ function LogsButtonContent({ item }: LogsButtonProps) { disabled={false} /> + + {/* Prettify JSON logs switch */} + {hasJsonLogs && ( + + } + /> + )} + + {/* Format JSON values switch */} + {hasJsonLogs && ( + + + } + /> + + )} , ]; diff --git a/frontend/src/lib/k8s/pod.ts b/frontend/src/lib/k8s/pod.ts index 63f88951408..46977ea7840 100644 --- a/frontend/src/lib/k8s/pod.ts +++ b/frontend/src/lib/k8s/pod.ts @@ -186,12 +186,16 @@ class Pod extends KubeObject { .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 @@ -205,7 +209,7 @@ class Pod extends KubeObject { : 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`; @@ -221,9 +225,12 @@ class Pod extends KubeObject { 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 }); }