/*
* Copyright 2017 Anyware Services
*
* Licensed 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.
*/
/**
* A log tool displayed logs from the server. It can be configured to show only a specific category of logs.
* @private
*/
Ext.define('Ametys.plugins.coreui.log.ServerLogTool', {
extend: "Ametys.plugins.coreui.log.AbstractLogTool",
/**
* @cfg {Number} [refreshTimer=2000] The logs refresh wait time between , in milliseconds.
* The tool also waits for the previous request to resolve before launching a new timer, which can result in a few additional seconds between refreshes if there are a lot of data to process or if the server lags.
*/
/**
* @property {Number} _lastLogTimestamp The most recent timestamp received.
* @private
*/
/**
* @property {Number} _timerId The id of the timer for the next refresh.
* @private
*/
/**
* @property {Boolean} _isRunning True if the timer is running.
*/
/**
* @private
* @property {String[]} logsCategory The list of categories displayed. See Ametys.plugins.coreui.log.ServerLogTool#setParams.
*/
/**
* @private
* @property {String[]} serverRole The role of server component to get log events. See Ametys.plugins.coreui.log.ServerLogTool#setParams.
*/
/**
* @private
* @property {String[]} serverId The id of extension to get log events. See Ametys.plugins.coreui.log.ServerLogTool#setParams.
*/
constructor: function(config)
{
this.callParent(arguments);
this._detailsTpl = new Ext.XTemplate(
"<tpl if='timestamp'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_TIMESTAMP_TEXT}}</b> {timestamp}<br /></tpl>",
"<tpl if='user'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_USER_TEXT}}</b> {user}<br /></tpl>",
"<tpl if='thread'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_THREAD_TEXT}}</b> {thread}<br /></tpl>",
"<tpl if='level'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LEVEL_TEXT}}</b> {level}<br /></tpl>",
"<tpl if='category'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORY_TEXT}}</b> {category}<br /></tpl>",
"<tpl if='requestURI'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_REQUESTURI_TEXT}}</b> {requestURI}<br /></tpl>",
"<tpl if='message'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_MESSAGE_TEXT}}</b> <div class='log-message'>{message}</div><br /></tpl>",
"<tpl if='location'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LOCATION_TEXT}}</b> {location}<br /></tpl>",
"<tpl if='callstack'><br /><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CALLSTACK_TEXT}}</b><br />{callstack}</tpl>"
);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onModified, this);
},
getMBSelectionInteraction: function()
{
return Ametys.tool.Tool.MB_TYPE_ACTIVE;
},
/**
* Listener when a category has been modified
* Will update the state of the buttons effectively upon the current selection.
* @param {Ametys.message.Message} message The modified message.
* @protected
*/
_onModified: function (message)
{
var me = this;
var modifiedCategories = [];
var targets = message.getTargets(Ametys.message.MessageTarget.LOG_CATEGORY);
if (targets.length > 0)
{
Ametys.message.MessageTargetFactory.createTargets({
id: Ametys.message.MessageTarget.LOG_CATEGORY,
parameters: {
ids: this.logsCategory
}
}, function(targets) {
var levels = {};
Ext.Array.each(targets, function (target) {
var params = target.getParameters();
levels[params.id] = params.effectiveLevel;
});
me._updateHint(levels);
});
}
},
sendCurrentSelection: function()
{
if (this.logsCategory && this.logsCategory.length > 0)
{
Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: {
id: Ametys.message.MessageTarget.LOG_CATEGORY,
parameters: {
ids: this.logsCategory
}
},
callback: Ext.bind(this._newSelectionSent, this)
});
}
else
{
Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: {
id: Ametys.message.MessageTarget.LOG_CATEGORY,
parameters: {
ids: ['root']
}
}
});
}
},
/**
* @private
* When sendCurrentSelection sent a new selection => let's update the hint with level info
* @param {Ametys.message.Message} message The selection message
*/
_newSelectionSent: function(message)
{
var levels = {};
Ext.Array.each(message.getTargets(), function (target) {
var params = target.getParameters();
levels[params.id] = params.effectiveLevel;
});
this._updateHint(levels);
},
/**
* @private
* Update the hint part of the tool
* @param {Object} logLevels The association category - loglevel
*/
_updateHint: function(logLevels)
{
if (!this.isNotDestroyed())
{
return;
}
var hint = this.grid.getDockedItems("#categories-hint")[0];
if (hint)
{
if (this.logsCategory.length > 0)
{
var logsLink = [];
for (var i = 0; i < this.logsCategory.length; i++)
{
let canOpen = (Ametys.tool.ToolsManager.getFactory("uitool-admin-logslevel") != null);
let hrefString = (canOpen ? " href=\"javascript:void(0)\"" : "");
let onclickString = (canOpen ? " onclick=\"Ametys.tool.ToolsManager.openTool('uitool-admin-logslevel', {'expandCategories' : ['" + this.logsCategory[i] + "']})\"" : "");
let titleString = (canOpen ? " title=\"" + Ext.String.format("{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORIE_OPEN_HINT}}", this.logsCategory[i]) + "\"" : "");
logsLink.push("<a class=\"link\""
+ hrefString
+ titleString
+ onclickString
+ ">"
+ this.logsCategory[i]
+ (logLevels && logLevels[this.logsCategory[i]] ? " (" + "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORIES_HINT_LEVEL}} " + logLevels[this.logsCategory[i]] + ")" : "")
+ "</a>")
}
hint.items.get(0).update(Ext.String.format("{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORIES_HINT}}", logsLink.join(", ")));
hint.show();
}
else
{
hint.hide();
}
}
},
/**
* @inheritdoc
* @param {String/String[]} params.category Limit the logs to one or more specified categories. If empty or omitted, all categories are displayed.
* @param {String} [serverRole=org.ametys.core.ui.RibbonControlsManager] The server role used to get logs.
* @param {String} serverId The server id to get logs. If no serverRole is configured, expected id is the id of a button controller. Can not be null.
*/
setParams: function(params)
{
if (this._timer)
{
clearTimeout(this._timer);
}
this.callParent(arguments);
if (params.title)
{
this.setTitle(Ext.String.format("{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LABEL_WITH_CATEGORIES}}", params.title));
}
else
{
this.setTitle(this.getInitialConfig().title);
}
this.serverRole = params.serverRole || 'org.ametys.core.ui.RibbonControlsManager';
this.serverId = params.serverId;
this.logsCategory = Ext.Array.from(params.category || null);
this._refreshTimer = params.refreshTimer || 2000;
this._updateHint();
this._lastLogTimestamp = null;
this._isRunning = true;
this._unsetPauseDecorator();
this._logsQueue = [];
this._store.removeAll();
this.showOutOfDate();
},
refresh: function(manual)
{
this.updateLogs(true);
this.showUpToDate();
if (manual)
{
this.showRefreshing();
}
},
getStore: function()
{
this._store = Ext.create("Ext.data.ArrayStore",{
autoDestroy: true,
trackRemoved: false,
proxy: { type: 'memory' },
sorters: [{property: 'timestamp', direction: 'DESC'}],
model: "Ametys.plugins.coreui.log.ServerLogTool.ServerLogEntry"
});
return this._store;
},
getColumns: function()
{
return [{
stateId: 'grid-timestamp',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_TIMESTAMP_HEADER}}",
width: 130,
sortable: true,
dataIndex: 'timestamp',
renderer: Ext.bind(this.datetimeRenderer, this)
},
{
stateId: 'grid-user',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_USER_HEADER}}",
width: 100,
sortable: true,
dataIndex: 'user',
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'string'
}
},
{
stateId: 'grid-thread',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_THREAD_HEADER}}",
width: 150,
sortable: true,
dataIndex: 'thread',
hidden: true,
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'string'
}
},
{
stateId: 'grid-level',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LEVEL_HEADER}}",
width: 75,
sortable: true,
dataIndex: 'level',
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'list',
store: Ext.create('Ext.data.Store', {
fields: ['id','text'],
data: [
{id: "DEBUG", text: 'Debug'},
{id: "INFO", text: 'Info'},
{id: "WARN", text: 'Warn'},
{id: "ERROR", text: 'Error'},
{id: "FATAL", text: 'Fatal'}
]
})
}
},
{
stateId: 'grid-category',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORY_HEADER}}",
width: 250,
flex: 0.25,
sortable: true,
dataIndex: 'category',
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'string'
}
},
{
stateId: 'grid-request-uri',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_REQUESTURI_HEADER}}",
width: 300,
sortable: true,
dataIndex: 'requestURI',
flex: 0.25,
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'string'
}
},
{
stateId: 'grid-message',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_MESSAGE_HEADER}}",
width: 400,
sortable: true,
dataIndex: 'message',
flex: 0.5,
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'string'
}
},
{
stateId: 'grid-location',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LOCATION_HEADER}}",
width: 100,
sortable: true,
dataIndex: 'location',
hidden: true,
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'string'
}
},
{
stateId: 'grid-callstack',
header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CALLSTACK_HEADER}}",
width: 100,
sortable: true,
dataIndex: 'callstack',
hidden: true,
renderer: Ext.bind(this.defaultRenderer, this),
filter: {
type: 'string'
}
}];
},
getDetailsText: function(records)
{
var record = records[0];
var data = {
timestamp: Ext.Date.format(new Date(record.get("timestamp")), "d/m H:i:s.u"),
user: record.get('user'),
level: record.get('level'),
category: record.get('category'),
requestURI: Ext.String.htmlEncode(record.get('requestURI')),
message: this.formatLogMessage(Ext.String.htmlEncode(record.get('message'))),
thread: record.get('thread'),
location: Ext.String.htmlEncode(record.get('location')),
callstack: record.get('callstack') ? Ext.String.stacktraceJavaToHTML(record.get('callstack')) : null
};
return this._detailsTpl.applyTemplate(data);
},
getDockedItems: function()
{
return [{
dock: 'top',
xtype: 'container',
hidden: true,
itemId: 'categories-hint',
layout: 'fit',
cls: 'x-component-tool-hintmessage',
items: [{
xtype: 'component',
cls: 'hintmessage-limited-content'
}]
}];
},
/**
* Update the grid by calling the server to retrieve the lastest logs.
* @param [force=false] force the refreshing, (ignoring the running state)
*/
updateLogs: function(force)
{
if (!this._isRunning && !force)
{
return;
}
Ametys.data.ServerComm.callMethod({
role: this.serverRole,
id: this.serverId,
methodName: "getEvents",
parameters: [this._lastLogTimestamp, this.logsCategory],
callback: {
handler: this._updateLogsCb,
scope: this
}
});
},
/**
* True if the tool is fetching live update, false if the log rotation has been paused.
*/
isRunning: function()
{
return this._isRunning;
},
/**
* Callback after receiving the logs from the server.
* @param {Object[]} response The logs.
* @private
*/
_updateLogsCb: function(response)
{
if (!this.isNotDestroyed())
{
return;
}
var logs = [];
Ext.Array.each(response, function (data) {
logs.push(Ext.create("Ametys.plugins.coreui.log.ServerLogTool.ServerLogEntry", {
timestamp: data.timestamp,
user: data.user,
level: data.level,
category: data.category,
requestURI: data.requestURI,
message: data.message,
location: data.location,
thread: data.thread,
callstack: data.callstack
}));
this._lastLogTimestamp = Math.max(data.timestamp, this._lastLogTimestamp);
}, this);
if (logs.length > 0)
{
this._logsQueue = this._logsQueue.concat(logs);
var count = this._logsQueue.length;
if (count > 1000)
{
this._logsQueue.splice(0, count - 1000);
}
this._store.loadData(this._logsQueue);
}
if (this._isRunning)
{
if (this._timer)
{
clearTimeout(this._timer);
}
this._timer = setTimeout(Ext.bind(this.showOutOfDate, this), this._refreshTimer);
}
this.showRefreshed();
},
onClose: function()
{
if (this._isRunning)
{
this._isRunning = false;
clearTimeout(this._timer);
this._timer = null;
}
this.callParent(arguments);
},
getRowClass: function(record)
{
return "msg-level-" + record.get('levelCode');
},
/**
* Default renderer for the columns. Apply the coloration.
* @param {Number} value The level
* @param {Object} metadata A collection of metadata about the current cell; can be used or modified by the renderer. Recognized properties are: tdCls, tdAttr, and style
* @param {Ext.data.Model} record The record for the current row
* @private
*/
defaultRenderer: function(value, metadata, record)
{
return Ext.String.htmlEncode(value);
},
/**
* Datetime renderer for the columns. Format the timestamp into a human readable time.
* @param {Number} value The level
* @param {Object} metadata A collection of metadata about the current cell; can be used or modified by the renderer. Recognized properties are: tdCls, tdAttr, and style
* @param {Ext.data.Model} record The record for the current row
* @private
*/
datetimeRenderer: function(value, metadata, record)
{
return Ext.Date.format(new Date(value), "d/m H:i:s.u");
},
/**
* Action to pause or unpause sending update requests to the server.
* @param {Boolean} pause True to pause, false to unpause.
* @private
*/
pauseUpdates: function(pause)
{
if (!pause && !this._isRunning)
{
// Reset the original decorator
this._unsetPauseDecorator();
this._isRunning = true;
// we force the manual to display the refreshing spinner
this.refresh(true);
}
if (pause && this._isRunning)
{
// Set a decorator to show the log are paused
this._setPauseDecorator();
this._isRunning = false;
clearTimeout(this._timer);
this._timer = null;
}
},
_setPauseDecorator: function()
{
this.setIconDecorator('decorator-ametysicon-multimedia-command-pause');
this.setIconDecoratorType('action-ui');
},
_unsetPauseDecorator: function()
{
this.setIconDecorator(this.getInitialConfig('icon-decorator'));
this.setIconDecoratorType(this.getInitialConfig('icon-decorator-type'));
},
/**
* Clear the logs from the grid.
* @private
*/
clearLogs: function()
{
this._store.removeAll();
this._logsQueue = [];
},
/**
* Format a log message to keep new lines and indentation spaces
* @param {String} message The log message
* @return {String} The html formatted log message
*/
formatLogMessage: function(message)
{
return message.split(/\r?\n/g)
.map(function (line) {
return line.replace(/^\s+/, function (startOfLine) {
// replace the empty chars at the start of the line by non breakable spaces
return startOfLine.replace(/ /g,'\xA0').replace(/\t/g,'\xA0\xA0\xA0\xA0');
});
})
.join('<br />');
}
});