diff --git a/loleaflet/Makefile.am b/loleaflet/Makefile.am
index 2e43e2df637d..1e51e415639d 100644
--- a/loleaflet/Makefile.am
+++ b/loleaflet/Makefile.am
@@ -94,7 +94,8 @@ LOLEAFLET_ADMIN_JS =\
admin/src/AdminSocketOverview.js \
admin/src/AdminSocketAnalytics.js \
admin/src/AdminSocketSettings.js \
- admin/src/AdminSocketHistory.js
+ admin/src/AdminSocketHistory.js \
+ admin/src/AdminSocketLog.js
NODE_MODULES_SRC =\
autolinker@3.14.1 \
diff --git a/loleaflet/admin/admin.html b/loleaflet/admin/admin.html
index acd114098507..2b60f767bdfe 100644
--- a/loleaflet/admin/admin.html
+++ b/loleaflet/admin/admin.html
@@ -59,6 +59,7 @@
+
diff --git a/loleaflet/admin/admin.strings.js b/loleaflet/admin/admin.strings.js
index 7dea1cdd3014..91d90b4f1378 100644
--- a/loleaflet/admin/admin.strings.js
+++ b/loleaflet/admin/admin.strings.js
@@ -9,6 +9,7 @@ l10nstrings.strOverview = _('Overview');
l10nstrings.strCurrent = _('(current)');
l10nstrings.strAnalytics = _('Analytics');
l10nstrings.strHistory = _('History');
+l10nstrings.strLog = _('Log');
l10nstrings.strDashboard = _('Dashboard');
l10nstrings.strUsersOnline = _('Users online');
l10nstrings.strUserName = _('User Name');
@@ -40,7 +41,7 @@ l10nstrings.strDocuments = _('Documents:');
l10nstrings.strExpired = _('Expired:');
l10nstrings.strRefresh = _('Refresh');
l10nstrings.strShutdown = _('Shutdown Server');
-l10nstrings.strServerUptime = _('Server uptime')
+l10nstrings.strServerUptime = _('Server uptime');
if (module) {
module.exports = l10nstrings;
diff --git a/loleaflet/admin/adminAnalytics.html b/loleaflet/admin/adminAnalytics.html
index 7667c67df07a..af3b6eb31562 100644
--- a/loleaflet/admin/adminAnalytics.html
+++ b/loleaflet/admin/adminAnalytics.html
@@ -59,6 +59,7 @@
+
diff --git a/loleaflet/admin/adminHistory.html b/loleaflet/admin/adminHistory.html
index 60ad593d38fa..ec652011e4b7 100644
--- a/loleaflet/admin/adminHistory.html
+++ b/loleaflet/admin/adminHistory.html
@@ -59,6 +59,7 @@
+
diff --git a/loleaflet/admin/adminLog.html b/loleaflet/admin/adminLog.html
new file mode 100644
index 000000000000..73036828e12c
--- /dev/null
+++ b/loleaflet/admin/adminLog.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+ LibreOffice Online - Admin console
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/loleaflet/admin/adminSettings.html b/loleaflet/admin/adminSettings.html
index 0d669beab4af..00fb85c53b78 100644
--- a/loleaflet/admin/adminSettings.html
+++ b/loleaflet/admin/adminSettings.html
@@ -59,6 +59,7 @@
+
diff --git a/loleaflet/admin/src/AdminSocketLog.js b/loleaflet/admin/src/AdminSocketLog.js
new file mode 100644
index 000000000000..59808755812b
--- /dev/null
+++ b/loleaflet/admin/src/AdminSocketLog.js
@@ -0,0 +1,121 @@
+/* -*- js-indent-level: 8 -*- */
+/*
+ Socket to be intialized on opening the log page in Admin console
+*/
+/* global Admin $ AdminSocketBase */
+var AdminSocketLog = AdminSocketBase.extend({
+ constructor: function(host) {
+ this.base(host);
+ // There is a "$" is never used error. Let's get rid of this. This is vanilla script and has not more lines than the one with JQuery.
+ $('#form-channel-list').id;
+ },
+
+ refreshLog: function() {
+ this.socket.send('log_lines');
+ },
+
+ pullChannelList: function() {
+ this.socket.send('channel_list');
+ },
+
+ sendChannelListLogLevels: function(e) {
+ e.stopPropagation();
+
+ // We change the colour of the button when we send the data and change it back when the task is done (in function applyChannelList). But it is happening too fast.
+ document.getElementById('update-log-levels').classList.add('btn-warning');
+ document.getElementById('update-log-levels').classList.remove('btn-primary');
+
+ // Get the form.
+ var form = document.getElementById('form-channel-list');
+
+ // Get channel select elements.
+ var selectList = form.querySelectorAll('select');
+
+ // Prepare the statement.
+ var textToSend = 'update-log-levels';
+ for (var i = 0; i < selectList.length; i++) {
+ textToSend += ' ' + selectList[i].getAttribute('name').replace('channel-', '') + '=' + selectList[i].value;
+ }
+
+ this.socket.send(textToSend);
+ },
+
+ onSocketOpen: function() {
+ // Base class' onSocketOpen handles authentication
+ this.base.call(this);
+
+ document.getElementById('refresh-log').onclick = this.refreshLog.bind(this);
+ document.getElementById('update-log-levels').onclick = this.sendChannelListLogLevels.bind(this);
+
+ this.pullChannelList();
+ this.refreshLog();
+ },
+
+ applyChannelList: function(channelListStr) {
+ var channelListArr = channelListStr.split(' '); // Every item holds: channel name + = + log level.
+
+ // Here we have the log channel list and their respective log levels.
+ // We will create items for them. User will be able to set the log level for each channel.
+ var channelForm = document.getElementById('form-channel-list');
+ channelForm.innerHTML = ''; // Clear and refill it.
+ var optionList = Array('none', 'fatal', 'critical', 'error', 'warning', 'notice', 'information', 'debug', 'trace');
+ var innerHTML = ''; // Of select elements.
+ for (var i = 0; i < optionList.length; i++) {
+ innerHTML += '';
+ }
+
+ for (i = 0; i < channelListArr.length; i++) {
+ if (channelListArr[i].split('=').length === 2) {
+ var channelName = channelListArr[i].split('=')[0];
+ var channelLogLevel = channelListArr[i].split('=')[1];
+
+ var newDiv = document.createElement('div');
+ newDiv.className = 'form-group';
+
+ var newLabel = document.createElement('label');
+ newLabel.className = 'control-label col-sm-6';
+ newLabel.setAttribute('for', 'channel-' + channelName);
+ newLabel.innerText = channelName;
+
+ var newSubDivision = document.createElement('div');
+ newSubDivision.className = 'col-sm-6';
+
+ var newSelectElement = document.createElement('select');
+ newSelectElement.name = 'channel-' + channelName;
+ newSelectElement.id = 'channel-' + channelName;
+ newSelectElement.innerHTML = innerHTML;
+ newSelectElement.value = channelLogLevel;
+ newSelectElement.style.width = '120px';
+ newSelectElement.className = 'form-control';
+
+ channelForm.appendChild(newDiv);
+ newDiv.appendChild(newLabel);
+ newDiv.appendChild(newSubDivision);
+ newSubDivision.appendChild(newSelectElement);
+ }
+ }
+
+ document.getElementById('update-log-levels').classList.remove('btn-warning');
+ document.getElementById('update-log-levels').classList.add('btn-primary');
+ },
+
+ onSocketMessage: function(e) {
+ if (e.data.startsWith('log_lines')) {
+ var result = e.data;
+ result = result.substring(10, result.length);
+ document.getElementById('log-lines').value = result;
+ }
+ else if (e.data.startsWith('channel_list')) {
+ var channelListStr = e.data.substring(13, e.data.length);
+ this.applyChannelList(channelListStr);
+ }
+ },
+
+ onSocketClose: function() {
+
+ }
+});
+
+Admin.Log = function(host) {
+ return new AdminSocketLog(host);
+};
diff --git a/wsd/Admin.cpp b/wsd/Admin.cpp
index 92fe30e8aea1..5e666832606f 100644
--- a/wsd/Admin.cpp
+++ b/wsd/Admin.cpp
@@ -151,6 +151,9 @@ void AdminSocketHandler::handleMessage(const std::vector &payload)
else if (tokens.equals(0, "uptime"))
sendTextFrame("uptime " + std::to_string(model.getServerUptime()));
+ else if (tokens.equals(0, "log_lines"))
+ sendTextFrame("log_lines " + _admin->getLogLines());
+
else if (tokens.equals(0, "kill") && tokens.size() == 2)
{
try
@@ -197,6 +200,10 @@ void AdminSocketHandler::handleMessage(const std::vector &payload)
sendTextFrame(oss.str());
}
+ else if (tokens.equals(0, "channel_list"))
+ {
+ sendTextFrame("channel_list " + _admin->getChannelLogLevels());
+ }
else if (tokens.equals(0, "shutdown"))
{
LOG_INF("Shutdown requested by admin.");
@@ -272,6 +279,18 @@ void AdminSocketHandler::handleMessage(const std::vector &payload)
}
}
}
+ else if (tokens.equals(0, "update-log-levels") && tokens.size() > 1) {
+ for (size_t i = 1; i < tokens.size(); i++)
+ {
+ StringVector _channel(LOOLProtocol::tokenize(tokens[i], '='));
+ if (_channel.size() == 2)
+ {
+ _admin->setChannelLogLevel((_channel[0] != "?" ? _channel[0]: ""), _channel[1]);
+ }
+ }
+ // Let's send back the current log levels in return. So the user can be sure of the values.
+ sendTextFrame("channel_list " + _admin->getChannelLogLevels());
+ }
}
AdminSocketHandler::AdminSocketHandler(Admin* adminManager,
@@ -550,19 +569,106 @@ size_t Admin::getTotalCpuUsage()
unsigned Admin::getMemStatsInterval()
{
+ assertCorrectThread();
return _memStatsTaskIntervalMs;
}
unsigned Admin::getCpuStatsInterval()
{
+ assertCorrectThread();
return _cpuStatsTaskIntervalMs;
}
unsigned Admin::getNetStatsInterval()
{
+ assertCorrectThread();
return _netStatsTaskIntervalMs;
}
+std::string Admin::getChannelLogLevels()
+{
+ std::string result = "";
+ // Get the list of channels..
+ std::vector nameList;
+ Log::logger().names(nameList);
+
+ std::string levelList[9] = {"none", "fatal", "critical", "error", "warning", "notice", "information", "debug", "trace"};
+
+ for (size_t i = 0; i < nameList.size(); i++)
+ {
+ result += (nameList[i] != "" ? nameList[i]: "?") + "=" + levelList[Log::logger().get(nameList[i]).getLevel()] + (i != nameList.size() - 1 ? " ": "");
+ }
+
+ return result;
+}
+
+void Admin::setChannelLogLevel(std::string _channelName, std::string _level)
+{
+ assertCorrectThread();
+
+ std::string levelList[9] = {"none", "fatal", "critical", "error", "warning", "notice", "information", "debug", "trace"};
+ if (std::find(std::begin(levelList), std::end(levelList), _level) == std::end(levelList))
+ {
+ _level = "trace";
+ }
+
+ // Get the list of channels..
+ std::vector nameList;
+ Log::logger().names(nameList);
+
+ for (size_t i = 0; i < nameList.size(); i++)
+ {
+ if (nameList[i] == _channelName)
+ {
+ Log::logger().get(nameList[i]).setLevel(_level);
+ break;
+ }
+ }
+}
+
+std::string Admin::getLogLines()
+{
+ assertCorrectThread();
+
+ try {
+ int lineCount = 500;
+ std::string fName = LOOLWSD::getPathFromConfig("logging.file.property[0]");
+ std::ifstream infile(fName);
+
+ std::string line;
+ std::deque lines;
+
+ while (std::getline(infile, line))
+ {
+ std::istringstream iss(line);
+ lines.push_back(line);
+ if (lines.size() > (size_t)lineCount)
+ {
+ lines.pop_front();
+ }
+ }
+
+ infile.close();
+
+ if (lines.size() < (size_t)lineCount)
+ {
+ lineCount = (int)lines.size();
+ }
+
+ line = ""; // Use the same variable to include result.
+ // Newest will be on top.
+ for (int i = lineCount - 1; i >= 0; i--)
+ {
+ line += "\n" + lines[i];
+ }
+
+ return line;
+ }
+ catch (const std::exception& e) {
+ return "Could not read the log file.";
+ }
+}
+
AdminModel& Admin::getModel()
{
return _model;
diff --git a/wsd/Admin.hpp b/wsd/Admin.hpp
index 1e7ebe9a6648..ae3967e0687b 100644
--- a/wsd/Admin.hpp
+++ b/wsd/Admin.hpp
@@ -105,6 +105,12 @@ class Admin : public SocketPoll
unsigned getNetStatsInterval();
+ std::string getChannelLogLevels();
+
+ void setChannelLogLevel(std::string _channelName, std::string _level);
+
+ std::string getLogLines();
+
void rescheduleMemTimer(unsigned interval);
void rescheduleCpuTimer(unsigned interval);
@@ -180,9 +186,9 @@ class Admin : public SocketPoll
};
std::vector _pendingConnects;
- std::atomic _cpuStatsTaskIntervalMs;
- std::atomic _memStatsTaskIntervalMs;
- std::atomic _netStatsTaskIntervalMs;
+ int _cpuStatsTaskIntervalMs;
+ int _memStatsTaskIntervalMs;
+ int _netStatsTaskIntervalMs;
DocProcSettings _defDocProcSettings;
// Don't update any more frequently than this since it's excessive.
diff --git a/wsd/FileServer.cpp b/wsd/FileServer.cpp
index 8e34c357e430..b7cd523a3982 100644
--- a/wsd/FileServer.cpp
+++ b/wsd/FileServer.cpp
@@ -363,7 +363,8 @@ void FileServerRequestHandler::handleRequest(const HTTPRequest& request,
if (endPoint == "admin.html" ||
endPoint == "adminSettings.html" ||
endPoint == "adminHistory.html" ||
- endPoint == "adminAnalytics.html")
+ endPoint == "adminAnalytics.html" ||
+ endPoint == "adminLog.html")
{
preprocessAdminFile(request, requestDetails, socket);
return;
diff --git a/wsd/protocol.txt b/wsd/protocol.txt
index 9b1f43e03b56..927348bfffca 100644
--- a/wsd/protocol.txt
+++ b/wsd/protocol.txt
@@ -736,6 +736,15 @@ settings
Queries the server for configurable settings from admin console.
+log_lines
+ Gets last n lines from "loolwsd.log" file.
+
+channel_list
+ Gets the log channels list with their respective log levels.
+
+update-log-levels
+ Updates the log channels' log levels with the given log levels. Format is: "update-log-levels channel1name=loglevel channel2name=loglevel".
+
set ...
Sets a particular setting (must be one returned as response to