Update Stdout/Stderr/Stdout+Stderr button (#5063)

* log web

* change

* Get Log

* Url Token Refresh

* merge log&log.1

* fix merge part

* fix refresh part

* fix by prettier

* fix state

Co-authored-by: Binyang Li <binyli@microsoft.com>
This commit is contained in:
AmberMsy 2020-11-11 14:35:04 +08:00 коммит произвёл GitHub
Родитель df25b98569
Коммит 5fbefb87c6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 114 добавлений и 123 удалений

Просмотреть файл

@ -48,7 +48,7 @@ import t from '../../../../../components/tachyons.scss';
import Context from './context'; import Context from './context';
import Timer from './timer'; import Timer from './timer';
import { getContainerLog } from '../conn'; import { getContainerLog, getContainerLogList } from '../conn';
import config from '../../../../../config/webportal.config'; import config from '../../../../../config/webportal.config';
import MonacoPanel from '../../../../../components/monaco-panel'; import MonacoPanel from '../../../../../components/monaco-panel';
import StatusBadge from '../../../../../components/status-badge'; import StatusBadge from '../../../../../components/status-badge';
@ -137,7 +137,10 @@ export default class TaskRoleContainerList extends React.Component {
monacoProps: null, monacoProps: null,
monacoTitle: '', monacoTitle: '',
monacoFooterButton: null, monacoFooterButton: null,
logUrl: null, fullLogUrls: null,
tailLogUrls: null,
logListUrl: null,
logType: null,
items: props.tasks, items: props.tasks,
ordering: { field: null, descending: false }, ordering: { field: null, descending: false },
hideDialog: true, hideDialog: true,
@ -145,7 +148,7 @@ export default class TaskRoleContainerList extends React.Component {
this.showSshInfo = this.showSshInfo.bind(this); this.showSshInfo = this.showSshInfo.bind(this);
this.onDismiss = this.onDismiss.bind(this); this.onDismiss = this.onDismiss.bind(this);
this.showContainerLog = this.showContainerLog.bind(this); this.showContainerTailLog = this.showContainerTailLog.bind(this);
this.onRenderRow = this.onRenderRow.bind(this); this.onRenderRow = this.onRenderRow.bind(this);
this.logAutoRefresh = this.logAutoRefresh.bind(this); this.logAutoRefresh = this.logAutoRefresh.bind(this);
this.onColumnClick = this.onColumnClick.bind(this); this.onColumnClick = this.onColumnClick.bind(this);
@ -159,12 +162,12 @@ export default class TaskRoleContainerList extends React.Component {
} }
logAutoRefresh() { logAutoRefresh() {
const { logUrl } = this.state; const { fullLogUrls, tailLogUrls, logListUrl, logType } = this.state;
getContainerLog(logUrl) getContainerLog(tailLogUrls, fullLogUrls, logType)
.then(({ text, fullLogLink }) => .then(({ text, fullLogLink }) =>
this.setState( this.setState(
prevState => prevState =>
prevState.logUrl === logUrl && { prevState.tailLogUrls[logType] === tailLogUrls[logType] && {
monacoProps: { value: text }, monacoProps: { value: text },
monacoFooterButton: ( monacoFooterButton: (
<PrimaryButton <PrimaryButton
@ -179,14 +182,17 @@ export default class TaskRoleContainerList extends React.Component {
}, },
), ),
) )
.catch(err => .catch(err => {
this.setState( this.setState(
prevState => prevState =>
prevState.logUrl === logUrl && { prevState.tailLogUrls[logType] === tailLogUrls[logType] && {
monacoProps: { value: err.message }, monacoProps: { value: err.message },
}, },
), );
); if (err.message === '403') {
this.showContainerTailLog(logListUrl, logType);
}
});
} }
onDismiss() { onDismiss() {
@ -194,7 +200,8 @@ export default class TaskRoleContainerList extends React.Component {
monacoProps: null, monacoProps: null,
monacoTitle: '', monacoTitle: '',
monacoFooterButton: null, monacoFooterButton: null,
logUrl: null, fullLogUrls: null,
tailLogUrls: null,
}); });
} }
@ -213,40 +220,52 @@ export default class TaskRoleContainerList extends React.Component {
} }
} }
showContainerLog(logUrl, logType) { convertObjectFormat(logUrls) {
let title; const logs = {};
let logHint; for (const p in logUrls.locations) {
logs[logUrls.locations[p].name] = logUrls.locations[p].uri;
}
return logs;
}
if (config.logType === 'yarn') { showContainerTailLog(logListUrl, logType) {
logHint = 'Last 4096 bytes'; let title;
} else if (config.logType === 'log-manager') { let logHint = '';
logHint = 'Last 16384 bytes'; this.setState({ logListUrl: logListUrl });
} else { getContainerLogList(logListUrl)
logHint = ''; .then(({ fullLogUrls, tailLogUrls }) => {
} if (config.logType === 'log-manager') {
switch (logType) { logHint = 'Last 16384 bytes';
case 'stdout': }
title = `Standard Output (${logHint})`; switch (logType) {
break; case 'stdout':
case 'stderr': title = `Standard Output (${logHint})`;
title = `Standard Error (${logHint})`; break;
break; case 'stderr':
case 'stdall': title = `Standard Error (${logHint})`;
title = `User logs (${logHint}. Notice: The logs may out of order when merging stdout & stderr streams)`; break;
break; case 'all':
default: title = `User logs (${logHint}. Notice: The logs may out of order when merging stdout & stderr streams)`;
throw new Error(`Unsupported log type`); break;
} default:
this.setState( throw new Error(`Unsupported log type`);
{ }
monacoProps: { value: 'Loading...' }, this.setState(
monacoTitle: title, {
logUrl, monacoProps: { value: 'Loading...' },
}, monacoTitle: title,
() => { fullLogUrls: this.convertObjectFormat(fullLogUrls),
this.logAutoRefresh(); // start immediately tailLogUrls: this.convertObjectFormat(tailLogUrls),
}, logType,
); },
() => {
this.logAutoRefresh(); // start immediately
},
);
})
.catch(err => {
this.setState({ monacoProps: { value: err.message } });
});
} }
showSshInfo(id, containerPorts, containerIp) { showSshInfo(id, containerPorts, containerIp) {
@ -424,7 +443,7 @@ export default class TaskRoleContainerList extends React.Component {
monacoTitle, monacoTitle,
monacoProps, monacoProps,
monacoFooterButton, monacoFooterButton,
logUrl, tailLogUrls,
items, items,
} = this.state; } = this.state;
const { showMoreDiagnostics } = this.props; const { showMoreDiagnostics } = this.props;
@ -443,7 +462,9 @@ export default class TaskRoleContainerList extends React.Component {
</ThemeProvider> </ThemeProvider>
{/* Timer */} {/* Timer */}
<Timer <Timer
interval={isNil(monacoProps) || isEmpty(logUrl) ? null : interval} interval={
isNil(monacoProps) || isEmpty(tailLogUrls) ? null : interval
}
func={this.logAutoRefresh} func={this.logAutoRefresh}
/> />
{/* Monaco Editor Panel */} {/* Monaco Editor Panel */}
@ -624,8 +645,8 @@ export default class TaskRoleContainerList extends React.Component {
iconProps={{ iconName: 'TextDocument' }} iconProps={{ iconName: 'TextDocument' }}
text='Stdout' text='Stdout'
onClick={() => onClick={() =>
this.showContainerLog( this.showContainerTailLog(
`${item.containerLog}user.pai.stdout`, `${config.restServerUri}${item.containerLog}`,
'stdout', 'stdout',
) )
} }
@ -640,8 +661,8 @@ export default class TaskRoleContainerList extends React.Component {
iconProps={{ iconName: 'Error' }} iconProps={{ iconName: 'Error' }}
text='Stderr' text='Stderr'
onClick={() => onClick={() =>
this.showContainerLog( this.showContainerTailLog(
`${item.containerLog}user.pai.stderr`, `${config.restServerUri}${item.containerLog}`,
'stderr', 'stderr',
) )
} }
@ -662,23 +683,11 @@ export default class TaskRoleContainerList extends React.Component {
iconProps: { iconName: 'TextDocument' }, iconProps: { iconName: 'TextDocument' },
disabled: isNil(item.containerId), disabled: isNil(item.containerId),
onClick: () => onClick: () =>
this.showContainerLog( this.showContainerTailLog(
`${item.containerLog}user.pai.all`, `${config.restServerUri}${item.containerLog}`,
'stdall', 'all',
), ),
}, },
{
key: 'trackingPage',
name:
config.launcherType === 'yarn'
? 'Go to Yarn Tracking Page'
: 'Browse log folder',
iconProps: { iconName: 'Link' },
href: isNil(item.containerLog)
? item.containerLog
: item.containerLog.replace('/tail/', '/'),
target: '_blank',
},
], ],
}} }}
disabled={isNil(item.containerId)} disabled={isNil(item.containerId)}

Просмотреть файл

@ -12,7 +12,6 @@ import config from '../../../../config/webportal.config';
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const userName = params.get('username'); const userName = params.get('username');
const jobName = params.get('jobName'); const jobName = params.get('jobName');
const absoluteUrlRegExp = /^[a-z][a-z\d+.-]*:/;
const token = cookies.get('token'); const token = cookies.get('token');
const client = new PAIV2.OpenPAIClient({ const client = new PAIV2.OpenPAIClient({
@ -157,77 +156,60 @@ export async function stopJob() {
); );
} }
export async function getContainerLog(logUrl) { export async function getContainerLogList(logListUrl) {
const ret = { const res = await Promise.all([
fullLogLink: logUrl, fetch(`${logListUrl}`, {
text: null, headers: {
Authorization: `Bearer ${token}`,
},
}),
fetch(`${logListUrl}?tail-mode=true`, {
headers: {
Authorization: `Bearer ${token}`,
},
}),
]);
const resp = res.find(r => !r.ok);
if (resp) {
throw new Error('Log folder can not be retrieved');
}
const logUrls = await Promise.all(res.map(r => r.json()));
return {
fullLogUrls: logUrls[0],
tailLogUrls: logUrls[1],
}; };
const res = await fetch(logUrl); }
var text = await res.text();
export async function getContainerLog(tailLogUrls, fullLogUrls, logType) {
const res = await fetch(tailLogUrls[logType]);
if (!res.ok) { if (!res.ok) {
throw new Error(res.statusText); throw new Error(res.status);
} }
let text = await res.text();
const contentType = res.headers.get('content-type'); // Check log type. The log type is in LOG_TYPE only support log-manager.
if (!contentType) { if (config.logType === 'log-manager') {
throw new Error(`Log not available`);
}
// Check log type. The log type is in LOG_TYPE and should be yarn|log-manager.
if (config.logType === 'yarn') {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const content = doc.getElementsByClassName('content')[0];
const pre = content.getElementsByTagName('pre')[0];
ret.text = pre.innerText;
// fetch full log link
if (pre.previousElementSibling) {
const link = pre.previousElementSibling.getElementsByTagName('a');
if (link.length === 1) {
ret.fullLogLink = link[0].getAttribute('href');
// relative link
if (ret.fullLogLink && !absoluteUrlRegExp.test(ret.fullLogLink)) {
let baseUrl = res.url;
// check base tag
const baseTags = doc.getElementsByTagName('base');
// There can be only one <base> element in a document.
if (baseTags.length > 0 && baseTags[0].hasAttribute('href')) {
baseUrl = baseTags[0].getAttribute('href');
// relative base tag url
if (!absoluteUrlRegExp.test(baseUrl)) {
baseUrl = new URL(baseUrl, res.url);
}
}
const url = new URL(ret.fullLogLink, baseUrl);
ret.fullLogLink = url.href;
}
}
}
return ret;
} catch (e) {
throw new Error(`Log not available`);
}
} else if (config.logType === 'log-manager') {
// Try to get roated log if currently log content is less than 15KB // Try to get roated log if currently log content is less than 15KB
if (text.length <= 15 * 1024) { if (text.length <= 15 * 1024 && tailLogUrls[logType + '.1']) {
const fullLogUrl = logUrl.replace('/tail/', '/full/'); const rotatedLogUrl = tailLogUrls[logType + '.1'];
const rotatedLogUrl = logUrl + '.1';
const rotatedLogRes = await fetch(rotatedLogUrl); const rotatedLogRes = await fetch(rotatedLogUrl);
const fullLogRes = await fetch(fullLogUrl); const fullLogRes = await fetch(fullLogUrls[logType]);
const rotatedText = await rotatedLogRes.text(); const rotatedText = await rotatedLogRes.text();
const fullLog = await fullLogRes.text(); const fullLog = await fullLogRes.text();
if (rotatedLogRes.ok && rotatedText.trim() !== 'No such file!') { if (rotatedLogRes.ok) {
text = rotatedText text = rotatedText
.concat('\n--------log is rotated, may be lost during this--------\n') .concat(
'\n ------- log is rotated, may be lost during this ------- \n',
)
.concat(fullLog); .concat(fullLog);
} }
// get last 16KB // get last 16KB
text = text.slice(-16 * 1024); text = text.slice(-16 * 1024);
} }
ret.text = text; return {
ret.fullLogLink = logUrl.replace('/tail/', '/full/'); fullLogLink: fullLogUrls[logType],
return ret; text: text,
};
} else { } else {
throw new Error(`Log not available`); throw new Error(`Log not available`);
} }