diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/adapters/yarn-log.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/adapters/yarn-log.js new file mode 100644 index 0000000..df29b71 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/adapters/yarn-log.js @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import AbstractAdapter from './abstract'; + +export default AbstractAdapter.extend({ + address: "timelineWebAddress", + // restNameSpace: "timelineV2", // Use ATSv2 when it supports log APIs. + restNameSpace: "timeline", //Using ATSv1.5 now, would be supported by ATSv2 very soon. + serverName: "ATS", + + urlForQuery(query/*, modelName*/) { + var url = this._buildURL(); + var containerId = query['containerId']; + delete query.containerId; + return url + '/containers/' + containerId + '/logs'; + }, + + fetchLogFileContent(containerId, logFile) { + var url = this._buildURL(); + url = url + '/containers/' + containerId + '/logs/' + logFile; + return Ember.$.ajax({url: url, type: 'GET', dataType: 'text'}); + } +}); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/components/collapsible-panel.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/components/collapsible-panel.js new file mode 100644 index 0000000..6a40f8f --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/components/collapsible-panel.js @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Ember from 'ember'; + +export default Ember.Component.extend({ + classNames: ['pull-right'], + + targetId: '', + initialClosedState: false, + + didInsertElement() { + if (!this.get('targetId')) { + this.$('.toggle_switch').hide(); + } + if (this.get('targetId') && this.get('initialClosedState')) { + this.$('.toggle_switch').show(); + this.toggleToggleSwitchArrow(); + Ember.$('#' + this.get('targetId')).removeClass('panel-collapsed').show(); + } + }, + + toggleToggleSwitchArrow() { + let $toggleArrow = this.$('.toggle_switch').find('span'); + if ($toggleArrow.hasClass('glyphicon-chevron-up')) { + $toggleArrow.removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down'); + } else { + $toggleArrow.removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up'); + } + }, + + toggleCollapsiblePanel() { + let $collapsiblePanel = Ember.$('#' + this.get('targetId')); + if ($collapsiblePanel.hasClass('panel-collapsed')) { + $collapsiblePanel.removeClass('panel-collapsed'); + $collapsiblePanel.slideDown(); + } else { + $collapsiblePanel.addClass('panel-collapsed'); + $collapsiblePanel.slideUp(); + } + }, + + actions: { + togglePanelCollapse() { + this.toggleToggleSwitchArrow(); + this.toggleCollapsiblePanel(); + } + } +}); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/components/timeline-view.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/components/timeline-view.js index 865fe52..420d793 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/components/timeline-view.js +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/components/timeline-view.js @@ -472,9 +472,5 @@ export default Ember.Component.extend({ prop = 'http://' + prop; } return prop; - }, - - isDataEmpty: Ember.computed(function() { - return this.modelArr.length === 0; - }) + } }); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/controllers/yarn-app-attempt.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/controllers/yarn-app-attempt.js index 1121a84..504b66e 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/controllers/yarn-app-attempt.js +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/controllers/yarn-app-attempt.js @@ -22,7 +22,7 @@ export default Ember.Controller.extend({ queryParams: ["service"], service: undefined, - breadcrumbs: Ember.computed("model.attempt.appId", function () { + breadcrumbs: Ember.computed("model.attempt.appId", "model.attempt.id", function () { var appId = this.get("model.attempt.appId"); var attemptId = this.get("model.attempt.id"); var serviceName = this.get('service'); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/controllers/yarn-app/logs.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/controllers/yarn-app/logs.js new file mode 100644 index 0000000..c996f05 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/controllers/yarn-app/logs.js @@ -0,0 +1,204 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Ember from 'ember'; + +export default Ember.Controller.extend({ + queryParams: ["service"], + service: undefined, + + selectedAttemptId: '', + attemptContainerList: null, + selectedContainerId: '', + selectedLogFileName: '', + containerLogFiles: null, + selectedLogFileContent: '', + + _isLoadingTopPanel: false, + _isLoadingBottomPanel: false, + + actions: { + showContainersForAttemptId(attemptId) { + this.set('selectedAttemptId', ''); + if (attemptId) { + this.set('_isLoadingTopPanel', true); + this.set('selectedAttemptId', attemptId); + this.fetchContainersForAttemptId(attemptId).then((hash) => { + let containers = null; + if (hash.rmContainers.get('length') > 0 && hash.rmContainers.get('content')) { + containers = (containers || []).concat(hash.rmContainers.get('content')); + } + if (hash.tsContainers.get('length') > 0 && hash.tsContainers.get('content')) { + containers = (containers || []).concat(hash.tsContainers.get('content')); + } + this.set('attemptContainerList', containers); + }).finally(() => { + this.set('_isLoadingTopPanel', false); + }); + } else { + this.set('attemptContainerList', null); + this.set('selectedContainerId', ''); + this.set('containerLogFiles', null); + this.set('selectedLogFileName', ''); + this.set('selectedLogFileContent', ''); + } + }, + + showLogFilesForContainerId(containerId) { + this.set('selectedContainerId', ''); + this.set('containerLogFiles', null); + this.set('selectedLogFileName', ''); + this.set('selectedLogFileContent', ''); + if (containerId) { + this.set('_isLoadingBottomPanel', true); + this.set('selectedContainerId', containerId); + this.fetchLogFilesForContainerId(containerId).then((hash) => { + if (hash.logs.get('length') > 0) { + this.set('containerLogFiles', hash.logs); + } else { + this.set('containerLogFiles', null); + } + }).finally(() => { + this.set('_isLoadingBottomPanel', false); + }); + } + }, + + showContentForLogFile(logFile) { + this.set('selectedLogFileName', ''); + Ember.$("#logContentTextArea1234554321").val(''); + this.set('showFullLog', false); + if (logFile) { + this.set('_isLoadingBottomPanel', true); + this.set('selectedLogFileName', logFile); + this.fetchContentForLogFile(this.get('selectedContainerId'), logFile).then((content) => { + this.set('selectedLogFileContent', content.trim()); + }, () => { + this.set('selectedLogFileContent', ''); + }).always(() => { + this.set('_isLoadingBottomPanel', false); + }); + } else { + this.set('selectedLogFileContent', ''); + } + }, + + findNextTextInLogContent() { + let searchInputElem = document.getElementById('logSeachInput98765'); + this.send('searchTextInLogContent', searchInputElem.value); + }, + + searchTextInLogContent(searchText) { + Ember.$('body').scrollTop(278); + let textAreaElem = document.getElementById('logContentTextArea1234554321'); + let logContent = textAreaElem.innerText; + let startIndex = this.searchTextStartIndex || 0; + if (startIndex === -1) { + startIndex = this.searchTextStartIndex = 0; + } + if (this.prevSearchText !== searchText) { + startIndex = this.searchTextStartIndex = 0; + } + if (searchText && searchText.trim()) { + searchText = searchText.trim(); + this.prevSearchText = searchText; + if (startIndex === 0) { + startIndex = logContent.indexOf(searchText, 0); + } + let endIndex = startIndex + searchText.length; + if (document.createRange && window.getSelection) { + let range = document.createRange(); + range.selectNodeContents(textAreaElem); + range.setStart(textAreaElem.childNodes.item(0), startIndex); + range.setEnd(textAreaElem.childNodes.item(0), endIndex); + let selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } + this.searchTextStartIndex = logContent.indexOf(searchText, endIndex + 1); + } else { + this.searchTextStartIndex = 0; + } + }, + + showFullLogFileContent() { + this.set('showFullLog', true); + this.notifyPropertyChange('selectedLogFileContent'); + } + }, + + attemptList: Ember.computed('model.attempts', function() { + let attempts = this.get('model.attempts'); + let list = null; + if (attempts && attempts.get('length') && attempts.get('content')) { + list = [].concat(attempts.get('content')); + } + return list; + }), + + fetchContainersForAttemptId(attemptId) { + return Ember.RSVP.hash({ + rmContainers: this.store.query('yarn-container', { + app_attempt_id: attemptId + }).catch(function() { + return Ember.A(); + }), + tsContainers: this.store.query('yarn-timeline-container', { + app_attempt_id: attemptId + }).catch(function() { + return Ember.A(); + }) + }); + }, + + fetchLogFilesForContainerId(containerId) { + return Ember.RSVP.hash({ + logs: this.store.query('yarn-log', { + containerId: containerId + }).catch(function() { + return Ember.A(); + }) + }); + }, + + fetchContentForLogFile(containerId, logFile) { + let logAdapter = this.store.adapterFor('yarn-log'); + return logAdapter.fetchLogFileContent(containerId, logFile); + }, + + resetAfterRefresh() { + this.set('selectedAttemptId', ''); + this.set('attemptContainerList', null); + this.set('selectedContainerId', ''); + this.set('selectedLogFileName', ''); + this.set('containerLogFiles', null); + this.set('selectedLogFileContent', ''); + }, + + showFullLog: false, + + showLastFewLinesOfLogContent: Ember.computed('selectedLogFileContent', function() { + let content = this.get('selectedLogFileContent'); + let lines = content.split('\n'); + if (this.get('showFullLog') || lines.length < 10) { + return content; + } + return lines.slice(lines.length - 10).join('\n'); + }) + +}); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/models/yarn-log.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/models/yarn-log.js new file mode 100644 index 0000000..f022bc7 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/models/yarn-log.js @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import DS from 'ember-data'; + +export default DS.Model.extend({ + fileName: DS.attr('string'), + fileSize: DS.attr('string'), + lastModifiedTime: DS.attr('string'), + containerId: DS.attr('string'), + nodeId: DS.attr('string') +}); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/router.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/router.js index 9013142..6ced3b3 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/router.js +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/router.js @@ -53,6 +53,7 @@ Router.map(function() { this.route('info', {path: '/:app_id/info'}); this.route('attempts', {path: '/:app_id/attempts'}); this.route('charts', {path: '/:app_id/charts'}); + this.route('logs', {path: '/:app_id/logs'}); }); this.route('yarn-app-attempt', { path: '/yarn-app-attempt/:app_attempt_id'}); this.route('error'); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app-attempt.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app-attempt.js index b561bf6..829cfe9 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app-attempt.js +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app-attempt.js @@ -26,11 +26,13 @@ export default AbstractRoute.extend(AppAttemptMixin, { attempt: this.fetchAttemptInfoFromRMorATS(param.app_attempt_id, this.store), rmContainers: this.store.query('yarn-container', { app_attempt_id: param.app_attempt_id + }).catch(function() { + return Ember.A(); }), tsContainers: this.store.query('yarn-timeline-container', { app_attempt_id: param.app_attempt_id }).catch(function() { - return []; + return Ember.A(); }) }); }, diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app/attempts.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app/attempts.js index fcea111..23e92f9 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app/attempts.js +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app/attempts.js @@ -26,7 +26,9 @@ export default AbstractRoute.extend(AppAttemptMixin, { return Ember.RSVP.hash({ appId: param.app_id, serviceName: param.service, - attempts: this.fetchAttemptListFromRMorATS(param.app_id, this.store) + attempts: this.fetchAttemptListFromRMorATS(param.app_id, this.store).catch(function() { + return Ember.A(); + }) }); }, diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app/logs.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app/logs.js new file mode 100644 index 0000000..5c34c96 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/routes/yarn-app/logs.js @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Ember from 'ember'; +import AbstractRoute from '../abstract'; +import AppAttemptMixin from 'yarn-ui/mixins/app-attempt'; + +export default AbstractRoute.extend(AppAttemptMixin, { + model(param, transition) { + transition.send('updateBreadcrumbs', param.app_id, param.service, [{text: 'Logs'}]); + return Ember.RSVP.hash({ + appId: param.app_id, + serviceName: param.service, + attempts: this.fetchAttemptListFromRMorATS(param.app_id, this.store).catch(function() { + return []; + }) + }); + }, + + unloadAll() { + this.store.unloadAll('yarn-app-attempt'); + this.store.unloadAll('yarn-timeline-appattempt'); + this.store.unloadAll('yarn-container'); + this.store.unloadAll('yarn-timeline-container'); + this.store.unloadAll('yarn-log'); + if (this.controller) { + this.controller.resetAfterRefresh(); + } + } +}); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/serializers/yarn-log.js b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/serializers/yarn-log.js new file mode 100644 index 0000000..57b2a14 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/serializers/yarn-log.js @@ -0,0 +1,31 @@ +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({ + internalNormalizeSingleResponse(store, primaryModelClass, payload, containerId, nodeId) { + var fixedPayload = { + id: "yarn_log_" + payload.fileName + "_" + Date.now(), + type: primaryModelClass.modelName, + attributes: { + fileName: payload.fileName, + fileSize: payload.fileSize, + lastModifiedTime: payload.lastModifiedTime, + containerId: containerId, + nodeId: nodeId + } + }; + return fixedPayload; + }, + + normalizeArrayResponse(store, primaryModelClass, payload/*, id, requestType*/) { + var normalizedArrayResponse = { + data: [] + }; + if (payload && payload.containerLogsInfo && payload.containerLogsInfo.containerLogInfo) { + normalizedArrayResponse.data = payload.containerLogsInfo.containerLogInfo.map((paylog) => { + return this.internalNormalizeSingleResponse(store, primaryModelClass, paylog, + payload.containerLogsInfo.containerId, payload.containerLogsInfo.nodeId); + }); + } + return normalizedArrayResponse; + } +}); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/styles/app.css b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/styles/app.css index 8b8ea56..0b5dfcd 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/styles/app.css +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/styles/app.css @@ -397,3 +397,18 @@ div.attempt-info-panel table > tbody > tr > td:last-of-type { width: 14px; display: inline-block; } + +.log-content-area { + width: 100%; + border: none; + min-height: 470px; + max-height: 470px; + overflow: auto; + font-family: monospace; + margin-bottom: -15px; + padding: 10px; + border-top: 1px solid #ddd; + background-color: #efefef; + color: #000; + margin-top: 30px; +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/templates/components/collapsible-panel.hbs b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/templates/components/collapsible-panel.hbs new file mode 100644 index 0000000..aab80b1 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/templates/components/collapsible-panel.hbs @@ -0,0 +1,21 @@ +{{! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +}} + + + + diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/templates/components/timeline-view.hbs b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/templates/components/timeline-view.hbs index a39d9b1..ba54a45 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/templates/components/timeline-view.hbs +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/app/templates/components/timeline-view.hbs @@ -25,49 +25,41 @@ Containers {{/if}} - {{#if isDataEmpty}} -
-
+
+