github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.tsx (about) 1 import {DataLoader} from 'argo-ui'; 2 import * as classNames from 'classnames'; 3 import * as React from 'react'; 4 import {useEffect, useState, useRef} from 'react'; 5 import {bufferTime, delay, retryWhen} from 'rxjs/operators'; 6 7 import {LogEntry} from '../../../shared/models'; 8 import {services, ViewPreferences} from '../../../shared/services'; 9 10 import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'; 11 12 import './pod-logs-viewer.scss'; 13 import {CopyLogsButton} from './copy-logs-button'; 14 import {DownloadLogsButton} from './download-logs-button'; 15 import {ContainerSelector} from './container-selector'; 16 import {FollowToggleButton} from './follow-toggle-button'; 17 import {ShowPreviousLogsToggleButton} from './show-previous-logs-toggle-button'; 18 import {TimestampsToggleButton} from './timestamps-toggle-button'; 19 import {DarkModeToggleButton} from './dark-mode-toggle-button'; 20 import {FullscreenButton} from './fullscreen-button'; 21 import {Spacer} from '../../../shared/components/spacer'; 22 import {LogMessageFilter} from './log-message-filter'; 23 import {SinceSecondsSelector} from './since-seconds-selector'; 24 import {TailSelector} from './tail-selector'; 25 import {PodNamesToggleButton} from './pod-names-toggle-button'; 26 import {AutoScrollButton} from './auto-scroll-button'; 27 import {WrapLinesButton} from './wrap-lines-button'; 28 import Ansi from 'ansi-to-react'; 29 30 export interface PodLogsProps { 31 namespace: string; 32 applicationNamespace: string; 33 applicationName: string; 34 podName?: string; 35 containerName: string; 36 group?: string; 37 kind?: string; 38 name?: string; 39 timestamp?: string; 40 containerGroups?: any[]; 41 onClickContainer?: (group: any, i: number, tab: string) => void; 42 } 43 44 // ansi colors, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors 45 const red = '\u001b[31m'; 46 const green = '\u001b[32m'; 47 const yellow = '\u001b[33m'; 48 const blue = '\u001b[34m'; 49 const magenta = '\u001b[35m'; 50 const cyan = '\u001b[36m'; 51 const colors = [red, green, yellow, blue, magenta, cyan]; 52 const reset = '\u001b[0m'; 53 const whiteOnYellow = '\u001b[1m\u001b[43;1m\u001b[37m'; 54 55 // cheap string hash function 56 function stringHashCode(str: string) { 57 let hash = 0; 58 for (let i = 0; i < str.length; i++) { 59 // tslint:disable-next-line:no-bitwise 60 hash = str.charCodeAt(i) + ((hash << 5) - hash); 61 } 62 return hash; 63 } 64 65 // ansi color for pod name 66 function podColor(podName: string) { 67 return colors[Math.abs(stringHashCode(podName) % colors.length)]; 68 } 69 70 // https://2ality.com/2012/09/empty-regexp.html 71 const matchNothing = /.^/; 72 73 export const PodsLogsViewer = (props: PodLogsProps) => { 74 const {containerName, onClickContainer, timestamp, containerGroups, applicationName, applicationNamespace, namespace, podName, group, kind, name} = props; 75 const queryParams = new URLSearchParams(location.search); 76 const [viewPodNames, setViewPodNames] = useState(queryParams.get('viewPodNames') === 'true'); 77 const [follow, setFollow] = useState(queryParams.get('follow') !== 'false'); 78 const [viewTimestamps, setViewTimestamps] = useState(queryParams.get('viewTimestamps') === 'true'); 79 const [previous, setPreviousLogs] = useState(queryParams.get('showPreviousLogs') === 'true'); 80 const [tail, setTail] = useState<number>(parseInt(queryParams.get('tail'), 10) || 1000); 81 const [sinceSeconds, setSinceSeconds] = useState(0); 82 const [filter, setFilter] = useState(queryParams.get('filterText') || ''); 83 const [highlight, setHighlight] = useState<RegExp>(matchNothing); 84 const [scrollToBottom, setScrollToBottom] = useState(true); 85 const [logs, setLogs] = useState<LogEntry[]>([]); 86 const logsContainerRef = useRef(null); 87 88 useEffect(() => { 89 if (viewPodNames) { 90 setViewTimestamps(false); 91 } 92 }, [viewPodNames]); 93 94 useEffect(() => { 95 // https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript 96 // matchNothing this is chosen instead of empty regexp, because that would match everything and break colored logs 97 setHighlight(filter === '' ? matchNothing : new RegExp(filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g')); 98 }, [filter]); 99 100 if (!containerName || containerName === '') { 101 return <div>Pod does not have container with name {containerName}</div>; 102 } 103 104 useEffect(() => setScrollToBottom(true), [follow]); 105 106 useEffect(() => { 107 if (scrollToBottom) { 108 const element = logsContainerRef.current; 109 if (element) { 110 element.scrollTop = element.scrollHeight; 111 } 112 } 113 }, [logs, scrollToBottom]); 114 115 useEffect(() => { 116 setLogs([]); 117 const logsSource = services.applications 118 .getContainerLogs({ 119 applicationName, 120 appNamespace: applicationNamespace, 121 namespace, 122 podName, 123 resource: {group, kind, name}, 124 containerName, 125 tail, 126 follow, 127 sinceSeconds, 128 filter, 129 previous 130 }) // accumulate log changes and render only once every 100ms to reduce CPU usage 131 .pipe(bufferTime(100)) 132 .pipe(retryWhen(errors => errors.pipe(delay(500)))) 133 .subscribe(log => setLogs(previousLogs => previousLogs.concat(log))); 134 135 return () => logsSource.unsubscribe(); 136 }, [applicationName, applicationNamespace, namespace, podName, group, kind, name, containerName, tail, follow, sinceSeconds, filter, previous]); 137 138 const handleScroll = (event: React.WheelEvent<HTMLDivElement>) => { 139 if (event.deltaY < 0) setScrollToBottom(false); 140 }; 141 142 const renderLog = (log: LogEntry, lineNum: number) => 143 // show the pod name if there are multiple pods, pad with spaces to align 144 (viewPodNames ? (lineNum === 0 || logs[lineNum - 1].podName !== log.podName ? podColor(podName) + log.podName + reset : ' '.repeat(log.podName.length)) + ' ' : '') + 145 // show the timestamp if requested, pad with spaces to align 146 (viewTimestamps ? (lineNum === 0 || (logs[lineNum - 1].timeStamp !== log.timeStamp ? log.timeStampStr : '').padEnd(30)) + ' ' : '') + 147 // show the log content, highlight the filter text 148 log.content?.replace(highlight, (substring: string) => whiteOnYellow + substring + reset); 149 const logsContent = (width: number, height: number, isWrapped: boolean) => ( 150 <div ref={logsContainerRef} onScroll={handleScroll} style={{width, height, overflow: 'scroll'}}> 151 {logs.map((log, lineNum) => ( 152 <div key={lineNum} style={{whiteSpace: isWrapped ? 'normal' : 'pre', lineHeight: '16px'}} className='noscroll'> 153 <Ansi>{renderLog(log, lineNum)}</Ansi> 154 </div> 155 ))} 156 </div> 157 ); 158 159 return ( 160 <DataLoader load={() => services.viewPreferences.getPreferences()}> 161 {(prefs: ViewPreferences) => { 162 return ( 163 <React.Fragment> 164 <div className='pod-logs-viewer__settings'> 165 <span> 166 <FollowToggleButton follow={follow} setFollow={setFollow} /> 167 {follow && <AutoScrollButton scrollToBottom={scrollToBottom} setScrollToBottom={setScrollToBottom} />} 168 <ShowPreviousLogsToggleButton setPreviousLogs={setPreviousLogs} showPreviousLogs={previous} /> 169 <Spacer /> 170 <ContainerSelector containerGroups={containerGroups} containerName={containerName} onClickContainer={onClickContainer} /> 171 <Spacer /> 172 {!follow && ( 173 <> 174 <SinceSecondsSelector sinceSeconds={sinceSeconds} setSinceSeconds={n => setSinceSeconds(n)} /> 175 <TailSelector tail={tail} setTail={setTail} /> 176 </> 177 )} 178 <LogMessageFilter filterText={filter} setFilterText={setFilter} /> 179 </span> 180 <Spacer /> 181 <span> 182 <WrapLinesButton prefs={prefs} /> 183 <PodNamesToggleButton viewPodNames={viewPodNames} setViewPodNames={setViewPodNames} /> 184 <TimestampsToggleButton setViewTimestamps={setViewTimestamps} viewTimestamps={viewTimestamps} timestamp={timestamp} /> 185 <DarkModeToggleButton prefs={prefs} /> 186 </span> 187 <Spacer /> 188 <span> 189 <CopyLogsButton logs={logs} /> 190 <DownloadLogsButton {...props} /> 191 <FullscreenButton {...props} /> 192 </span> 193 </div> 194 <div className={classNames('pod-logs-viewer', {'pod-logs-viewer--inverted': prefs.appDetails.darkMode})} onWheel={handleScroll}> 195 <AutoSizer>{({width, height}: {width: number; height: number}) => logsContent(width, height, prefs.appDetails.wrapLines)}</AutoSizer> 196 </div> 197 </React.Fragment> 198 ); 199 }} 200 </DataLoader> 201 ); 202 };