blob: 2196885a3a51beb1381c527220919984aa82f8bb [file] [log] [blame]
/*
* 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 angular from "angular";
import template from './stream.template.html';
const MODULE_NAME = 'inspector.stream';
angular.module(MODULE_NAME, [])
.directive('stream', streamDirective);
export default MODULE_NAME;
export function streamDirective() {
return {
template: template,
restrict: 'E',
scope: {
autoUpdate: '=?',
tail: '=?',
activityId: '@',
streamType: '@',
},
controller: ['$scope', '$interval', '$element', 'activityApi', controller]
};
function controller($scope, $interval, $element, activityApi) {
$scope.autoUpdate = $scope.autoUpdate !== false;
$scope.tail = $scope.tail !== false;
// Content filtering features
$scope.filteredStream = [];
$scope.streamProcessedUpTo = 0;
$scope.otherLogLines = 0;
$scope.errorLogLines = 0;
$scope.debugLogLines = 0;
$scope.traceLogLines = 0;
$scope.warningLogLines = 0;
$scope.isDisplayOther = $scope.isDisplayOther !== false;
$scope.isDisplayError = $scope.isDisplayError !== false;
$scope.isDisplayDebug = $scope.isDisplayDebug !== false;
$scope.isDisplayTrace = $scope.isDisplayTrace !== false;
$scope.isDisplayWarning = $scope.isDisplayWarning !== false;
$scope.isFilterContent = isFilterContent;
$scope.isDisplayFormattedItem = isDisplayFormattedItem;
$scope.getFormattedItemLogLevel = getFormattedItemLogLevel;
// CLI XML features
$scope.cliXml = false;
$scope.cliXmlIdentified = false;
$scope.toggleCliXml = toggleCliXml;
$scope.isCliXmlSupported = isCliXmlSupported;
$scope.cliXmlVerificationRequired = isWinRmStream(); // CLI XML verification is required only when stream is WinRM
let autoScrollableElement = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
let refreshFunction;
// Set up cancellation of auto-scrolling on scrolling up.
autoScrollableElement.forEach(item => {
if (item.addEventListener)
{
let wheelHandler = () => {
$scope.$apply(() => {
$scope.tail = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
});
}
// IE9, Chrome, Safari, Opera
item.addEventListener("mousewheel", wheelHandler, false);
// Firefox
item.addEventListener("DOMMouseScroll", wheelHandler, false);
}
});
// Watch the 'tail' and auto-scroll down if auto-scroll is enabled.
$scope.$watch('tail', () => {
if ($scope.tail) {
$scope.$applyAsync(() => {
autoScrollableElement.forEach(item => item.scrollTop = item.scrollHeight);
});
}
});
$scope.$watch('autoUpdate', ()=> {
if ($scope.autoUpdate) {
refreshFunction = $interval(updateStream, 1000);
} else {
cancelUpdate();
}
});
$scope.$on('$destroy', cancelUpdate);
/**
* Updates the stream data.
*/
function updateStream() {
activityApi.activityStream($scope.activityId, $scope.streamType).then((response)=> {
// 1. Try to identify CLI XML output.
const CLI_XML_HEADER_SIZE = 100; // estimated headers size in WinRM that can contain indication of CLI XML output
if ($scope.cliXmlVerificationRequired && typeof response.data === 'string' && response.data.length >= CLI_XML_HEADER_SIZE) {
let header = response.data.slice(0, CLI_XML_HEADER_SIZE);
if (header.includes('#< CLIXML') || header.includes('xmlns="http://schemas.microsoft.com/powershell')) {
$scope.cliXmlIdentified = true;
}
$scope.cliXmlVerificationRequired = false; // perform verification once, if conditions match
}
// 2. Update the stream data holder in this directive.
$scope.stream = response.data;
// 3. Filter the content where relevant and display it.
updateFilteredContent();
}).catch((error)=> {
if (error.data) {
$scope.error = error.data.message;
}
}).finally(() => {
if ($scope.tail) {
$scope.$applyAsync(() => {
autoScrollableElement.forEach(item => item.scrollTop = item.scrollHeight);
});
}
})
}
/**
* Cancels the auto-update of the streamed content.
*/
function cancelUpdate() {
if (refreshFunction) {
$interval.cancel(refreshFunction);
}
}
/**
* @returns {boolean} True if CLI XML is supported, and false otherwise. CLI XML is expected in WinRM stream only.
*/
function isCliXmlSupported() {
return isWinRmStream() && $scope.cliXmlIdentified === true;
}
/**
* @returns {boolean} True if stream type is WinRM, and false otherwise.
*/
function isWinRmStream() {
return $scope.streamType === 'winrm';
}
/**
* Switches content format to CLI XML and back.
*/
function toggleCliXml() {
$scope.cliXml = !$scope.cliXml;
updateFilteredContent();
}
/**
* @returns {boolean} True if logging filter should be displayed, and false otherwise.
*/
function isFilterContent() {
return isCliXmlSupported() && $scope.cliXml !== true;
}
/**
* @returns {string} Returns class name of the formatted item log level.
*/
function getFormattedItemLogLevel(formattedItem) {
if (formattedItem.isWarning) {
return 'log-warning';
} else if (formattedItem.isError) {
return 'log-error';
} if (formattedItem.isDebug) {
return 'log-debug';
}
return 'log-trace';
}
/**
* @returns {boolean} True if formatted item should be displayed, and false otherwise.
*/
function isDisplayFormattedItem(formattedItem) {
return formattedItem.isWarning && $scope.isDisplayWarning
|| formattedItem.isDebug && $scope.isDisplayDebug
|| formattedItem.isError && $scope.isDisplayError
|| formattedItem.isTrace && $scope.isDisplayTrace
|| formattedItem.isOther && $scope.isDisplayOther;
}
/**
* Formats CLI XML output and displays it in 'filtered-stream-content' field.
*/
function formatCliXmlContent() {
// Slice at index of last closing tag ending wth the new line
let streamTags = $scope.stream.match(/<\/(.*?)>\n/g);
let lastClosingTagIndex = $scope.stream.lastIndexOf(streamTags[streamTags.length-1]);
let newCliXmlData = $scope.stream.slice($scope.streamProcessedUpTo, lastClosingTagIndex);
if (!newCliXmlData) {
return;
}
$scope.streamProcessedUpTo += newCliXmlData.length;
newCliXmlData.split(/\n/g).forEach(item => {
let formattedItem = {
id: ($scope.filteredStream.length), // ng-repeat requires unique items, array length fits the bill
text: item,
isOther: false,
isError: false,
isDebug: false,
isTrace: false,
isWarning: false
};
if (/<s s="warning">/i.test(item)) {
$scope.warningLogLines++;
formattedItem.isWarning = true;
} else if (/<s s="debug">/i.test(item)) {
$scope.debugLogLines++;
formattedItem.isDebug = true;
} else if (/<s s="verbose">/i.test(item)) {
$scope.traceLogLines++;
formattedItem.isTrace = true;
} else if (/<s s="error">/i.test(item)) {
$scope.errorLogLines++;
formattedItem.isError = true;
} else {
$scope.otherLogLines++;
formattedItem.isOther = true;
}
// Remove CLI XML string tags for know log levels
if (!formattedItem.isOther) {
formattedItem.text = item.replace(/<s s="(.*?)">|\t/gi, '');
}
// Remove CLI XML markers, newlines and replace tabs with spaces
formattedItem.text = formattedItem.text.replace(/<\/s>|_x000[a-z0-9]_|\n/gi, '').replace(/\t/g,' ');
// Push update item and let ng-repeat update the content
$scope.filteredStream.push(formattedItem);
});
}
/**
* Filters stream content as per selected filters if filtering is enabled, e.g. display/hide warnings or errors.
*/
function updateFilteredContent() {
if (!isFilterContent()) {
return;
}
// Format new CLI XML content
if (isCliXmlSupported()) {
formatCliXmlContent();
}
}
updateStream();
}
}