ui-modules/utils/logbook/logbook.js (248 lines of code) (raw):

/* * 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 './logbook.template.html'; import brooklynApi from '../brooklyn.api/brooklyn.api'; const MODULE_NAME = 'brooklyn.component.logbook'; angular.module(MODULE_NAME, [brooklynApi]) .directive('brLogbook', logbook); export default MODULE_NAME; export function logbook() { return { template: template, restrict: 'E', scope: { taskId: '@', entityId: '@', }, controller: ['$scope', '$element', '$interval', 'brBrandInfo', 'logbookApi', controller], controllerAs: 'vm' }; function controller($scope, $element, $interval, brBrandInfo, logbookApi) { let vm = this; let refreshFunction = null; let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable')); let queryParametersChanged = 1; // Fresh start, new parameters! let datetimeToScrollTo = ''; // Set up cancellation of auto-scrolling down. if (autoScrollableElement.addEventListener) { let wheelHandler = () => { $scope.$apply(() => { cacheDatetimeToScrollTo(); $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight; }); } // Chrome, Safari, Opera autoScrollableElement.addEventListener("mousewheel", wheelHandler, false); // Firefox autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false); } $scope.isAutoScrollDown = true; // Auto-scroll down, by default. $scope.autoRefresh = false; $scope.waitingResponse = false; $scope.logtext = ''; $scope.wordwrap = true; $scope.logEntries = []; $scope.minNumberOfItems = 1; $scope.maxNumberOfItems = 10000; // Initialize search parameters. $scope.search = { logLevels: [ {name: 'Info', value: 'INFO', selected: true}, {name: 'Warn', value: 'WARN', selected: true}, {name: 'Error', value: 'ERROR', selected: true}, {name: 'Fatal', value: 'FATAL', selected: true}, {name: 'Debug', value: 'DEBUG', selected: true}, ], latest: true, recursive: false, dateTimeFrom: null, dateTimeTo: null, numberOfItems: 1000, phrase: '' }; // Define search result filters. $scope.fieldsToShow = ['timestamp', 'class', 'message'] $scope.logFields = [ {name: 'Timestamp', value: 'timestamp', selected: true}, {name: 'Task ID', value: 'taskId', selected: false}, {name: 'Entity IDs', value: 'entityIds', selected: false}, {name: 'Log level', value: 'level', selected: true}, {name: 'Bundle ID', value: 'bundleId', selected: false}, {name: 'Class', value: 'class', selected: true}, {name: 'Thread name', value: 'threadName', selected: false}, {name: 'Message', value: 'message', selected: true}, ]; // Watch for search parameters changes. $scope.$watch('search', () => { queryParametersChanged++; if ($scope.autoRefresh) { displayInProgress(); } }, true); $scope.$watch('search.latest', () => { datetimeToScrollTo = ''; $scope.isAutoScrollDown = $scope.search.latest; if ($scope.search.latest) { scrollToMostRecentLogEntry(); } else { scrollToFirstLogEntry(); } }, true); $scope.$on('$destroy', stopAutoRefresh); /** * @returns {boolean} True if number of items is a number and within a supported range, false otherwise. */ vm.isValidNumber = () => { return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems; } /** * Handles the click event on the log entry. * * @param {Object} logEntry The clicked log entry data. */ vm.logEntryOnClick = (logEntry) => { pinLogEntry(logEntry); }; /** * Starts an auto-query. Performs new query each time search parameters change. */ vm.autoQuery = () => { let autoRefresh = !$scope.autoRefresh; // Calculate new value first. if (autoRefresh) { vm.singleQuery(); startAutoRefresh(); } else { stopAutoRefresh(); } $scope.autoRefresh = autoRefresh; // Now, set the new value. }; /** * Performs a single query with search parameters selected. */ vm.singleQuery = () => { queryParametersChanged = 1; displayInProgress(); doQuery(); }; /** * Converts log entry to string. * * @param {Object} entry The log entry to convert. * @returns {String} log entry converted to string. */ vm.covertLogEntryToString = (entry) => { return getCheckedBoxes($scope.logFields).reduce((output, fieldKey) => { if (entry[fieldKey]) { output.push(entry[fieldKey]) } return output; }, []).join(' '); } /** * Caches the datetime of the first item in the visible area of the query result. */ function cacheDatetimeToScrollTo() { let element = Array.from($element.find('pre')).find(item => item.offsetTop > (autoScrollableElement.scrollTop + autoScrollableElement.offsetTop - 1)); let firstLogEntryInTheVisibleArea = $scope.logEntries.find(logEntry => logEntry.lineId === element.id); if (firstLogEntryInTheVisibleArea) { datetimeToScrollTo = getLogEntryTimestamp(firstLogEntryInTheVisibleArea); } } /** * Displays 'Loading...' text in query result area. */ function displayInProgress() { $scope.logtext = 'Loading...'; $scope.logEntries = []; } /** * @returns {boolean} true if current query is a tail request, false otherwise. */ function isTail() { return $scope.search.latest && !$scope.search.dateTimeTo; } /** * Performs a logbook query. */ function doQuery() { if ($scope.autoRefresh && queryParametersChanged > 1) { queryParametersChanged = 1; displayInProgress(); return; // User is still editing query parameters. } if (!vm.isValidNumber()) { console.error('number of items is invalid', $scope.search.numberOfItems) return; } let isNewQueryParameters = queryParametersChanged > 0; // new parameters! queryParametersChanged = 0; // reset the count. // Take into account timezone offset of the browser. let dateTimeFrom = getUtcTimestamp($scope.search.dateTimeFrom); let dateTimeTo = getUtcTimestamp($scope.search.dateTimeTo) if (isTail() && !isNewQueryParameters && !isEmpty($scope.logEntries)) { dateTimeFrom = getLogEntryTimestamp($scope.logEntries.slice(-1)[0]) } const levels = getCheckedBoxes($scope.search.logLevels); const params = { levels: levels, tail: $scope.search.latest, recursive: $scope.search.recursive, searchPhrase: $scope.search.phrase, taskId: $scope.taskId, entityId: $scope.entityId, numberOfItems: $scope.search.numberOfItems, dateTimeFrom: dateTimeFrom, dateTimeTo: dateTimeTo, } logbookApi.logbookQuery(params, true).then((newLogEntries) => { if (isNewQueryParameters) { // New query. // Re-draw all entries. $scope.logEntries = newLogEntries; } else if (!isEmpty(newLogEntries) && !isEmpty($scope.logEntries) && isTail() && $scope.autoRefresh) { // Tail query. // Use line IDs to resolve the overlap, if any. let lastLogEntryDisplayed = $scope.logEntries[$scope.logEntries.length - 1]; let indexOfLogEntryInTheNewBatch = newLogEntries.findIndex(({lineId}) => lineId === lastLogEntryDisplayed.lineId); if (indexOfLogEntryInTheNewBatch >= 0) { // Append new log entries without overlap. $scope.logEntries = $scope.logEntries.concat(newLogEntries.slice(indexOfLogEntryInTheNewBatch + 1)); } else { // Append all new log entries, there is no overlap. $scope.logEntries = $scope.logEntries.concat(newLogEntries) } // Display not more of lines than was requested. $scope.logEntries.slice(-$scope.search.numberOfItems); } // Auto-scroll. if (!isEmpty($scope.logEntries.length)) { if ($scope.isAutoScrollDown) { scrollToMostRecentLogEntry(); } else if (datetimeToScrollTo && datetimeToScrollTo >= getLogEntryTimestamp($scope.logEntries[0])) { scrollToLogEntryWithDateTime(datetimeToScrollTo); } } // Display 'No results' if user stopped editing search parameters, it can be the case that previous query // result was empty when the user resumed editing of search parameters. if (isEmpty($scope.logEntries) && queryParametersChanged === 0) { $scope.logtext = 'No results.'; } }, (error) => { $scope.logtext = 'Error getting the logs: \n' + error.error.message; console.log(JSON.stringify(error)); }).finally(() => { $scope.waitingResponse = false; }); } /** * Extracts timestamp from the log entry. * * @param logEntry The log entry. * @returns {number} The extracted date-time. */ function getLogEntryTimestamp(logEntry) { return Date.parse(logEntry.timestamp.replace(',', '.')) } /** * Extracts UTC timestamp from the date. * * @param {Date|number} date The date to get UTC timestamp of. * @returns {number|undefined} The UTC timestamp. */ function getUtcTimestamp(date) { const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000; if (date instanceof Date) { return date.valueOf() - timezoneOffsetMs; } else if (typeof date === 'number') { return date - timezoneOffsetMs; } else { return undefined; } } /** * @returns {boolean} true if array is empty, false otherwise. */ function isEmpty(array) { return array.length === 0; } /** * Gets all checked boxes from the group of elements. * * @param {Object} checkBoxGroup The checkbox group. * @returns {Array.<Object>} The checked boxes. */ function getCheckedBoxes(checkBoxGroup) { let checkedBoxes = []; checkBoxGroup.forEach((item) => { if (item.selected) { checkedBoxes.push(item.value); } }); return checkedBoxes; } /** * Starts the auto-refresh of the logbook content. */ function startAutoRefresh() { refreshFunction = $interval(() => { // Do a new query only if parameters have changed or it is a tail. if (queryParametersChanged > 0 || isTail()) { doQuery(); } }, 1000); } /** * Stops the auto-refresh of the logbook content. */ function stopAutoRefresh() { if (refreshFunction) { $interval.cancel(refreshFunction); } } /** * Scrolls down to the most recent log entry. */ function scrollToMostRecentLogEntry() { $scope.$applyAsync(() => { autoScrollableElement.scrollTop = autoScrollableElement.scrollHeight; }); } /** * Scrolls up to the first log entry. */ function scrollToFirstLogEntry() { $scope.$applyAsync(() => { autoScrollableElement.scrollTop = 0; }); } /** * Scrolls down or up to the log entry with the closets datetime of interest. * * @param {Object} datetime The datetime of the log entry to scroll to. */ function scrollToLogEntryWithDateTime(datetime) { $scope.$applyAsync(() => { let logEntryWithDateTimeToScrollTo = $scope.logEntries.find(logEntry => getLogEntryTimestamp(logEntry) >= datetime); if (logEntryWithDateTimeToScrollTo) { let elementWithDateTimeToScrollTo = Array.from($element.find('pre')).find(element => element.id === logEntryWithDateTimeToScrollTo.lineId); if (logEntryWithDateTimeToScrollTo) { autoScrollableElement.scrollTop = elementWithDateTimeToScrollTo.offsetTop - autoScrollableElement.offsetTop; } } }); } /** * Pins the log entry. Pinned log entries are displayed even if they are not present in the new query, positioned * in the right order, until unpinned. * * @param {Object} logEntry The log entry to pin. */ function pinLogEntry(logEntry) { // TODO: reserved for future. // logEntry.isPinned = !logEntry.isPinned; } } }