argo-cd
202 строки · 9.4 Кб
1import {DataLoader} from 'argo-ui';
2import * as classNames from 'classnames';
3import * as React from 'react';
4import {useEffect, useState, useRef} from 'react';
5import {bufferTime, delay, retryWhen} from 'rxjs/operators';
6
7import {LogEntry} from '../../../shared/models';
8import {services, ViewPreferences} from '../../../shared/services';
9
10import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
11
12import './pod-logs-viewer.scss';
13import {CopyLogsButton} from './copy-logs-button';
14import {DownloadLogsButton} from './download-logs-button';
15import {ContainerSelector} from './container-selector';
16import {FollowToggleButton} from './follow-toggle-button';
17import {ShowPreviousLogsToggleButton} from './show-previous-logs-toggle-button';
18import {TimestampsToggleButton} from './timestamps-toggle-button';
19import {DarkModeToggleButton} from './dark-mode-toggle-button';
20import {FullscreenButton} from './fullscreen-button';
21import {Spacer} from '../../../shared/components/spacer';
22import {LogMessageFilter} from './log-message-filter';
23import {SinceSecondsSelector} from './since-seconds-selector';
24import {TailSelector} from './tail-selector';
25import {PodNamesToggleButton} from './pod-names-toggle-button';
26import {AutoScrollButton} from './auto-scroll-button';
27import {WrapLinesButton} from './wrap-lines-button';
28import Ansi from 'ansi-to-react';
29
30export interface PodLogsProps {
31namespace: string;
32applicationNamespace: string;
33applicationName: string;
34podName?: string;
35containerName: string;
36group?: string;
37kind?: string;
38name?: string;
39timestamp?: string;
40containerGroups?: any[];
41onClickContainer?: (group: any, i: number, tab: string) => void;
42}
43
44// ansi colors, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
45const red = '\u001b[31m';
46const green = '\u001b[32m';
47const yellow = '\u001b[33m';
48const blue = '\u001b[34m';
49const magenta = '\u001b[35m';
50const cyan = '\u001b[36m';
51const colors = [red, green, yellow, blue, magenta, cyan];
52const reset = '\u001b[0m';
53const whiteOnYellow = '\u001b[1m\u001b[43;1m\u001b[37m';
54
55// cheap string hash function
56function stringHashCode(str: string) {
57let hash = 0;
58for (let i = 0; i < str.length; i++) {
59// tslint:disable-next-line:no-bitwise
60hash = str.charCodeAt(i) + ((hash << 5) - hash);
61}
62return hash;
63}
64
65// ansi color for pod name
66function podColor(podName: string) {
67return colors[stringHashCode(podName) % colors.length];
68}
69
70// https://2ality.com/2012/09/empty-regexp.html
71const matchNothing = /.^/;
72
73export const PodsLogsViewer = (props: PodLogsProps) => {
74const {containerName, onClickContainer, timestamp, containerGroups, applicationName, applicationNamespace, namespace, podName, group, kind, name} = props;
75const queryParams = new URLSearchParams(location.search);
76const [viewPodNames, setViewPodNames] = useState(queryParams.get('viewPodNames') === 'true');
77const [follow, setFollow] = useState(queryParams.get('follow') !== 'false');
78const [viewTimestamps, setViewTimestamps] = useState(queryParams.get('viewTimestamps') === 'true');
79const [previous, setPreviousLogs] = useState(queryParams.get('showPreviousLogs') === 'true');
80const [tail, setTail] = useState<number>(parseInt(queryParams.get('tail'), 10) || 1000);
81const [sinceSeconds, setSinceSeconds] = useState(0);
82const [filter, setFilter] = useState(queryParams.get('filterText') || '');
83const [highlight, setHighlight] = useState<RegExp>(matchNothing);
84const [scrollToBottom, setScrollToBottom] = useState(true);
85const [logs, setLogs] = useState<LogEntry[]>([]);
86const logsContainerRef = useRef(null);
87
88useEffect(() => {
89if (viewPodNames) {
90setViewTimestamps(false);
91}
92}, [viewPodNames]);
93
94useEffect(() => {
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
97setHighlight(filter === '' ? matchNothing : new RegExp(filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'));
98}, [filter]);
99
100if (!containerName || containerName === '') {
101return <div>Pod does not have container with name {containerName}</div>;
102}
103
104useEffect(() => setScrollToBottom(true), [follow]);
105
106useEffect(() => {
107if (scrollToBottom) {
108const element = logsContainerRef.current;
109if (element) {
110element.scrollTop = element.scrollHeight;
111}
112}
113}, [logs, scrollToBottom]);
114
115useEffect(() => {
116setLogs([]);
117const logsSource = services.applications
118.getContainerLogs({
119applicationName,
120appNamespace: applicationNamespace,
121namespace,
122podName,
123resource: {group, kind, name},
124containerName,
125tail,
126follow,
127sinceSeconds,
128filter,
129previous
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
135return () => logsSource.unsubscribe();
136}, [applicationName, applicationNamespace, namespace, podName, group, kind, name, containerName, tail, follow, sinceSeconds, filter, previous]);
137
138const handleScroll = (event: React.WheelEvent<HTMLDivElement>) => {
139if (event.deltaY < 0) setScrollToBottom(false);
140};
141
142const 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
148log.content?.replace(highlight, (substring: string) => whiteOnYellow + substring + reset);
149const 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
159return (
160<DataLoader load={() => services.viewPreferences.getPreferences()}>
161{(prefs: ViewPreferences) => {
162return (
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};
203