From 0c5152fb5f14688642ee6594bc1159933d687b6c Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 28 Feb 2018 07:19:28 +0100 Subject: [PATCH] feat(log-viewer): introduce the log viewer component (#1666) --- app/__module.js | 2 + app/docker/__module.js | 12 ++ .../containersDatatable.html | 2 +- .../tasks-datatable/tasksDatatable.html | 6 + .../tasks-datatable/tasksDatatable.js | 3 +- .../components/log-viewer/log-viewer.js | 8 ++ .../components/log-viewer/logViewer.html | 62 ++++++++++ .../log-viewer/logViewerController.js | 35 ++++++ app/docker/models/container.js | 32 +++++ app/docker/models/containerDetails.js | 18 --- app/docker/models/containerStats.js | 12 -- app/docker/rest/container.js | 5 + app/docker/rest/containerLogs.js | 21 ---- app/docker/rest/response/handlers.js | 12 ++ app/docker/rest/service.js | 7 +- app/docker/rest/serviceLogs.js | 20 --- app/docker/rest/task.js | 9 +- app/docker/services/containerService.js | 12 ++ app/docker/services/serviceService.js | 12 ++ app/docker/services/taskService.js | 12 ++ .../views/containers/edit/container.html | 2 +- .../logs/containerLogsController.js | 115 +++++++++--------- .../views/containers/logs/containerlogs.html | 50 +------- .../views/services/edit/includes/tasks.html | 1 + app/docker/views/services/edit/service.html | 2 +- .../services/logs/serviceLogsController.js | 115 ++++++++---------- .../views/services/logs/servicelogs.html | 52 +------- app/docker/views/stacks/edit/stack.html | 1 + app/docker/views/tasks/edit/task.html | 3 + app/docker/views/tasks/edit/taskController.js | 8 +- .../views/tasks/logs/taskLogsController.js | 73 +++++++++++ app/docker/views/tasks/logs/tasklogs.html | 10 ++ .../code-editor/codeEditorController.js | 14 +-- package.json | 2 + vendor.yml | 4 + yarn.lock | 8 ++ 36 files changed, 458 insertions(+), 304 deletions(-) create mode 100644 app/docker/components/log-viewer/log-viewer.js create mode 100644 app/docker/components/log-viewer/logViewer.html create mode 100644 app/docker/components/log-viewer/logViewerController.js delete mode 100644 app/docker/models/containerDetails.js delete mode 100644 app/docker/models/containerStats.js delete mode 100644 app/docker/rest/containerLogs.js delete mode 100644 app/docker/rest/serviceLogs.js create mode 100644 app/docker/views/tasks/logs/taskLogsController.js create mode 100644 app/docker/views/tasks/logs/tasklogs.html diff --git a/app/__module.js b/app/__module.js index 15371bdf6..98591b7e6 100644 --- a/app/__module.js +++ b/app/__module.js @@ -13,6 +13,8 @@ angular.module('portainer', [ 'angular-google-analytics', 'angular-json-tree', 'angular-loading-bar', + 'angular-clipboard', + 'luegg.directives', 'portainer.templates', 'portainer.app', 'portainer.docker', diff --git a/app/docker/__module.js b/app/docker/__module.js index e4777fcb9..0ede8298d 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -389,6 +389,17 @@ angular.module('portainer.docker', ['portainer.app']) } }; + var taskLogs = { + name: 'docker.tasks.task.logs', + url: '/logs', + views: { + 'content@': { + templateUrl: 'app/docker/views/tasks/logs/tasklogs.html', + controller: 'TaskLogsController' + } + } + }; + var templates = { name: 'docker.templates', url: '/templates', @@ -488,6 +499,7 @@ angular.module('portainer.docker', ['portainer.app']) $stateRegistryProvider.register(swarmVisualizer); $stateRegistryProvider.register(tasks); $stateRegistryProvider.register(task); + $stateRegistryProvider.register(taskLogs); $stateRegistryProvider.register(templates); $stateRegistryProvider.register(templatesLinuxServer); $stateRegistryProvider.register(volumes); diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 8c6e30c68..f9e82b812 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -196,7 +196,7 @@
- +
diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html index 654ee3a22..ff8adfbf8 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html @@ -54,6 +54,7 @@ + Actions @@ -63,6 +64,11 @@ {{ item.Slot ? item.Slot : '-' }} {{ item.NodeId | tasknodename: $ctrl.nodes }} {{ item.Updated | getisodate }} + + + View logs + + Loading... diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatable.js b/app/docker/components/datatables/tasks-datatable/tasksDatatable.js index c8bae7d68..b560bdade 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatable.js +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatable.js @@ -10,6 +10,7 @@ angular.module('portainer.docker').component('tasksDatatable', { reverseOrder: '<', nodes: '<', showTextFilter: '<', - showSlotColumn: '<' + showSlotColumn: '<', + showLogsButton: '<' } }); diff --git a/app/docker/components/log-viewer/log-viewer.js b/app/docker/components/log-viewer/log-viewer.js new file mode 100644 index 000000000..5c7dc6d6c --- /dev/null +++ b/app/docker/components/log-viewer/log-viewer.js @@ -0,0 +1,8 @@ +angular.module('portainer.docker').component('logViewer', { + templateUrl: 'app/docker/components/log-viewer/logViewer.html', + controller: 'LogViewerController', + bindings: { + data: '=', + logCollectionChange: '<' + } +}); diff --git a/app/docker/components/log-viewer/logViewer.html b/app/docker/components/log-viewer/logViewer.html new file mode 100644 index 000000000..e61923b38 --- /dev/null +++ b/app/docker/components/log-viewer/logViewer.html @@ -0,0 +1,62 @@ +
+
+ + + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ + + + + +
+
+
+
+
+
+
+ +
+
+
+      

{{ line }}

+

No log line matching the '{{ $ctrl.state.search }}' filter

+

No logs available

+
+
+
diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js new file mode 100644 index 000000000..10b544c58 --- /dev/null +++ b/app/docker/components/log-viewer/logViewerController.js @@ -0,0 +1,35 @@ +angular.module('portainer.docker') +.controller('LogViewerController', ['clipboard', +function (clipboard) { + var ctrl = this; + + this.state = { + copySupported: clipboard.supported, + logCollection: true, + autoScroll: true, + search: '', + filteredLogs: [], + selectedLines: [] + }; + + this.copy = function() { + clipboard.copyText(this.state.filteredLogs); + $('#refreshRateChange').show(); + $('#refreshRateChange').fadeOut(1500); + }; + + this.copySelection = function() { + clipboard.copyText(this.state.selectedLines); + $('#refreshRateChange').show(); + $('#refreshRateChange').fadeOut(1500); + }; + + this.selectLine = function(line) { + var idx = this.state.selectedLines.indexOf(line); + if (idx === -1) { + this.state.selectedLines.push(line); + } else { + this.state.selectedLines.splice(idx, 1); + } + }; +}]); diff --git a/app/docker/models/container.js b/app/docker/models/container.js index c37f2fe93..64d4d7047 100644 --- a/app/docker/models/container.js +++ b/app/docker/models/container.js @@ -36,3 +36,35 @@ function ContainerViewModel(data) { } } } + +function ContainerStatsViewModel(data) { + this.Date = data.read; + this.MemoryUsage = data.memory_stats.usage; + this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage; + this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage; + this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage; + this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage; + if (data.cpu_stats.cpu_usage.percpu_usage) { + this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length; + } + this.Networks = _.values(data.networks); +} + +function ContainerDetailsViewModel(data) { + this.Model = data; + this.Id = data.Id; + this.State = data.State; + this.Created = data.Created; + this.Name = data.Name; + this.NetworkSettings = data.NetworkSettings; + this.Args = data.Args; + this.Image = data.Image; + this.Config = data.Config; + this.HostConfig = data.HostConfig; + this.Mounts = data.Mounts; + if (data.Portainer) { + if (data.Portainer.ResourceControl) { + this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); + } + } +} diff --git a/app/docker/models/containerDetails.js b/app/docker/models/containerDetails.js deleted file mode 100644 index eae58c105..000000000 --- a/app/docker/models/containerDetails.js +++ /dev/null @@ -1,18 +0,0 @@ -function ContainerDetailsViewModel(data) { - this.Model = data; - this.Id = data.Id; - this.State = data.State; - this.Created = data.Created; - this.Name = data.Name; - this.NetworkSettings = data.NetworkSettings; - this.Args = data.Args; - this.Image = data.Image; - this.Config = data.Config; - this.HostConfig = data.HostConfig; - this.Mounts = data.Mounts; - if (data.Portainer) { - if (data.Portainer.ResourceControl) { - this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); - } - } -} diff --git a/app/docker/models/containerStats.js b/app/docker/models/containerStats.js deleted file mode 100644 index aad3b48b3..000000000 --- a/app/docker/models/containerStats.js +++ /dev/null @@ -1,12 +0,0 @@ -function ContainerStatsViewModel(data) { - this.Date = data.read; - this.MemoryUsage = data.memory_stats.usage; - this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage; - this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage; - this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage; - this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage; - if (data.cpu_stats.cpu_usage.percpu_usage) { - this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length; - } - this.Networks = _.values(data.networks); -} diff --git a/app/docker/rest/container.js b/app/docker/rest/container.js index cfa0a0587..b2acedd59 100644 --- a/app/docker/rest/container.js +++ b/app/docker/rest/container.js @@ -13,6 +13,11 @@ angular.module('portainer.docker') kill: {method: 'POST', params: {id: '@id', action: 'kill'}}, pause: {method: 'POST', params: {id: '@id', action: 'pause'}}, unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}}, + logs: { + method: 'GET', params: { id: '@id', action: 'logs' }, + timeout: 4500, ignoreLoadingBar: true, + transformResponse: logsHandler, isArray: true + }, stats: { method: 'GET', params: { id: '@id', stream: false, action: 'stats' }, timeout: 4500, ignoreLoadingBar: true diff --git a/app/docker/rest/containerLogs.js b/app/docker/rest/containerLogs.js deleted file mode 100644 index f4520f3b0..000000000 --- a/app/docker/rest/containerLogs.js +++ /dev/null @@ -1,21 +0,0 @@ -angular.module('portainer.docker') -.factory('ContainerLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return { - get: function (id, params, callback) { - $http({ - method: 'GET', - url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/logs', - params: { - 'stdout': params.stdout || 0, - 'stderr': params.stderr || 0, - 'timestamps': params.timestamps || 0, - 'tail': params.tail || 'all' - }, - ignoreLoadingBar: true - }).success(callback).error(function (data, status, headers, config) { - console.log(data); - }); - } - }; -}]); diff --git a/app/docker/rest/response/handlers.js b/app/docker/rest/response/handlers.js index 03aa6c1d1..53e660c39 100644 --- a/app/docker/rest/response/handlers.js +++ b/app/docker/rest/response/handlers.js @@ -44,6 +44,18 @@ function genericHandler(data) { return response; } +// The Docker API returns the logs as a single string. +// This handler will return an array with each line being an entry. +// It will also strip the 8 first characters of each line and remove any ANSI code related character sequences. +function logsHandler(data) { + var logs = data; + logs = logs.substring(8); + logs = logs.replace(/\n(.{8})/g, '\n\r'); + logs = logs.replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); + return logs.split('\n'); +} + // Image delete API returns an array on success (Docker 1.9 -> Docker 1.12). // On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message // container the error (Docker = 1.12). diff --git a/app/docker/rest/service.js b/app/docker/rest/service.js index 66e19468c..3af88acd2 100644 --- a/app/docker/rest/service.js +++ b/app/docker/rest/service.js @@ -13,6 +13,11 @@ angular.module('portainer.docker') ignoreLoadingBar: true }, update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} }, - remove: { method: 'DELETE', params: {id: '@id'} } + remove: { method: 'DELETE', params: {id: '@id'} }, + logs: { + method: 'GET', params: { id: '@id', action: 'logs' }, + timeout: 4500, ignoreLoadingBar: true, + transformResponse: logsHandler, isArray: true + } }); }]); diff --git a/app/docker/rest/serviceLogs.js b/app/docker/rest/serviceLogs.js deleted file mode 100644 index fbd25ec66..000000000 --- a/app/docker/rest/serviceLogs.js +++ /dev/null @@ -1,20 +0,0 @@ -angular.module('portainer.docker') -.factory('ServiceLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ServiceLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return { - get: function (id, params, callback) { - $http({ - method: 'GET', - url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/services/' + id + '/logs', - params: { - 'stdout': params.stdout || 0, - 'stderr': params.stderr || 0, - 'timestamps': params.timestamps || 0, - 'tail': params.tail || 'all' - } - }).success(callback).error(function (data, status, headers, config) { - console.log(data); - }); - } - }; -}]); diff --git a/app/docker/rest/task.js b/app/docker/rest/task.js index 73032ae12..9683b05ba 100644 --- a/app/docker/rest/task.js +++ b/app/docker/rest/task.js @@ -1,11 +1,16 @@ angular.module('portainer.docker') .factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action', { endpointId: EndpointProvider.endpointID }, { get: { method: 'GET', params: {id: '@id'} }, - query: { method: 'GET', isArray: true, params: {filters: '@filters'} } + query: { method: 'GET', isArray: true, params: {filters: '@filters'} }, + logs: { + method: 'GET', params: { id: '@id', action: 'logs' }, + timeout: 4500, ignoreLoadingBar: true, + transformResponse: logsHandler, isArray: true + } }); }]); diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index 204dbe545..51efd1b8e 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -131,6 +131,18 @@ angular.module('portainer.docker') return deferred.promise; }; + service.logs = function(id, stdout, stderr, timestamps, tail) { + var parameters = { + id: id, + stdout: stdout || 0, + stderr: stderr || 0, + timestamps: timestamps || 0, + tail: tail || 'all' + }; + + return Container.logs(parameters).$promise; + }; + service.containerStats = function(id) { var deferred = $q.defer(); diff --git a/app/docker/services/serviceService.js b/app/docker/services/serviceService.js index 7f136b6ab..1e28b75f9 100644 --- a/app/docker/services/serviceService.js +++ b/app/docker/services/serviceService.js @@ -58,5 +58,17 @@ angular.module('portainer.docker') return Service.update({ id: service.Id, version: service.Version }, config).$promise; }; + service.logs = function(id, stdout, stderr, timestamps, tail) { + var parameters = { + id: id, + stdout: stdout || 0, + stderr: stderr || 0, + timestamps: timestamps || 0, + tail: tail || 'all' + }; + + return Service.logs(parameters).$promise; + }; + return service; }]); diff --git a/app/docker/services/taskService.js b/app/docker/services/taskService.js index 3280c5c15..4fdbe4331 100644 --- a/app/docker/services/taskService.js +++ b/app/docker/services/taskService.js @@ -35,5 +35,17 @@ angular.module('portainer.docker') return deferred.promise; }; + service.logs = function(id, stdout, stderr, timestamps, tail) { + var parameters = { + id: id, + stdout: stdout || 0, + stderr: stderr || 0, + timestamps: timestamps || 0, + tail: tail || 'all' + }; + + return Task.logs(parameters).$promise; + }; + return service; }]); diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index 58fa5f819..e384da895 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -85,7 +85,7 @@
Stats - Logs + Logs Console Inspect
diff --git a/app/docker/views/containers/logs/containerLogsController.js b/app/docker/views/containers/logs/containerLogsController.js index dc7169164..c874d2db8 100644 --- a/app/docker/views/containers/logs/containerLogsController.js +++ b/app/docker/views/containers/logs/containerLogsController.js @@ -1,70 +1,71 @@ angular.module('portainer.docker') -.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', 'Notifications', -function ($scope, $transition$, $anchorScroll, ContainerLogs, Container, Notifications) { - $scope.state = {}; - $scope.state.displayTimestampsOut = false; - $scope.state.displayTimestampsErr = false; - $scope.stdout = ''; - $scope.stderr = ''; - $scope.tailLines = 2000; +.controller('ContainerLogsController', ['$scope', '$transition$', '$interval', 'ContainerService', 'Notifications', +function ($scope, $transition$, $interval, ContainerService, Notifications) { + $scope.state = { + refreshRate: 3, + lineCount: 2000 + }; - Container.get({id: $transition$.params().id}, function (d) { - $scope.container = d; - }, function (e) { - Notifications.error('Failure', e, 'Unable to retrieve container info'); + $scope.changeLogCollection = function(logCollectionStatus) { + if (!logCollectionStatus) { + stopRepeater(); + } else { + setUpdateRepeater(); + } + }; + + $scope.$on('$destroy', function() { + stopRepeater(); }); - function getLogs() { - getLogsStdout(); - getLogsStderr(); + function stopRepeater() { + var repeater = $scope.repeater; + if (angular.isDefined(repeater)) { + $interval.cancel(repeater); + repeater = null; + } } - function getLogsStderr() { - ContainerLogs.get($transition$.params().id, { - stdout: 0, - stderr: 1, - timestamps: $scope.state.displayTimestampsErr, - tail: $scope.tailLines - }, function (data, status, headers, config) { - // Replace carriage returns with newlines to clean up output - data = data.replace(/[\r]/g, '\n'); - // Strip 8 byte header from each line of output - data = data.substring(8); - data = data.replace(/\n(.{8})/g, '\n'); - $scope.stderr = data; + function update(logs) { + $scope.logs = logs; + } + + function setUpdateRepeater() { + var refreshRate = $scope.state.refreshRate; + $scope.repeater = $interval(function() { + ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount) + .then(function success(data) { + $scope.logs = data; + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve container logs'); + }); + }, refreshRate * 1000); + } + + function startLogPolling() { + ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount) + .then(function success(data) { + $scope.logs = data; + setUpdateRepeater(); + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve container logs'); }); } - function getLogsStdout() { - ContainerLogs.get($transition$.params().id, { - stdout: 1, - stderr: 0, - timestamps: $scope.state.displayTimestampsOut, - tail: $scope.tailLines - }, function (data, status, headers, config) { - // Replace carriage returns with newlines to clean up output - data = data.replace(/[\r]/g, '\n'); - // Strip 8 byte header from each line of output - data = data.substring(8); - data = data.replace(/\n(.{8})/g, '\n'); - $scope.stdout = data; + function initView() { + ContainerService.container($transition$.params().id) + .then(function success(data) { + $scope.container = data; + startLogPolling(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve container information'); }); } - // initial call - getLogs(); - var logIntervalId = window.setInterval(getLogs, 5000); - - $scope.$on('$destroy', function () { - // clearing interval when view changes - clearInterval(logIntervalId); - }); - - $scope.toggleTimestampsOut = function () { - getLogsStdout(); - }; - - $scope.toggleTimestampsErr = function () { - getLogsStderr(); - }; + initView(); }]); diff --git a/app/docker/views/containers/logs/containerlogs.html b/app/docker/views/containers/logs/containerlogs.html index 86ce31b13..48d0ef68e 100644 --- a/app/docker/views/containers/logs/containerlogs.html +++ b/app/docker/views/containers/logs/containerlogs.html @@ -5,50 +5,6 @@ -
-
- - -
- -
-
{{ container.Name|trimcontainername }}
-
Name
-
-
-
-
- -
-
- - - - - - - -
-
{{stdout}}
-
-
-
-
-
- -
-
- - - - - - - -
-
{{stderr}}
-
-
-
-
-
+ diff --git a/app/docker/views/services/edit/includes/tasks.html b/app/docker/views/services/edit/includes/tasks.html index 2cf7fdbaf..369276b9a 100644 --- a/app/docker/views/services/edit/includes/tasks.html +++ b/app/docker/views/services/edit/includes/tasks.html @@ -6,5 +6,6 @@ nodes="nodes" show-text-filter="true" show-slot-column="service.Mode !== 'global'" + show-logs-button="applicationState.endpoint.apiVersion >= 1.30" > diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html index 059da3dca..c2d86925c 100644 --- a/app/docker/views/services/edit/service.html +++ b/app/docker/views/services/edit/service.html @@ -73,7 +73,7 @@ - Service logs + Service logs