/*
* Copyright © 2012, 2013 Pedro Agullo Soliveres.
*
* This file is part of Log4js-ext.
*
* Log4js-ext is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* Commercial use is permitted to the extent that the code/component(s)
* do NOT become part of another Open Source or Commercially developed
* licensed development library or toolkit without explicit permission.
*
* Log4js-ext is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Log4js-ext. If not, see <http://www.gnu.org/licenses/>.
*
* This software uses the ExtJs library (http://extjs.com), which is
* distributed under the GPL v3 license (see http://extjs.com/license).
*/
/*jslint strict:false */
(function() {
// "use strict"; //$NON-NLS-1$
// ******************************************
// Special sorting for priority/level column
// Need to define it here before using the type in the LoggingEvent model
Ext.data.Types.LOGLEVEL = {
convert: function(v, n) {
return v;
},
sortType: function(v) {
return Sm.log.Level.getLevelLevel(v);
},
type: 'LOGLEVEL'
};
Ext.define('Sm.log.viewer.LoggingEvent', {
extend: 'Ext.data.Model',
fields: [
// {name: 'time'}, // We don't use this
// {name: 'message'}, // We don't use this
{name: 'hasLoggedObject'},
{name: 'formattedTime'},
{name: 'level', type: Ext.data.Types.LOGLEVEL},
{name: 'category'},
{name: 'formattedMessage'},
{name: 'formattedLoggedObject'},
{name: 'ndc'},
{name: 'formattedMultilineMessage'},
{name: 'formattedMultilineLoggedObject'}
]
});
Ext.define('Sm.log.viewer.Level', {
extend: 'Ext.data.Model',
fields: [
{name: 'level', type: 'int'},
{name: 'name', type: 'string'},
{name: 'iconClass', type: 'string'}
]
});
/**
* A window that can receive log data.
*
* This window provides support viewing log details, sorting, filtering,
* a detail view for large logs, and nice JSON formatting for logged objects.
*
* {@img log-viewer-window.png alt text}
*
*/
Ext.define('Sm.log.LogViewerWindow', { //$NON-NLS-1$
extend : 'Ext.window.Window',
uses : ['Ext.ux.grid.plugin.RowExpander',
'Ext.ux.LiveSearchGridPanelEx',
'Ext.String',
'Ext.Array',
'Ext.form.Panel',
'Ext.grid.Panel',
'Ext.data.ArrayStore',
'Ext.ux.statusbar.StatusBar',
'Sm.log.Level',
'Sm.log.LogViewerAppender'],
layout: 'border',
title: 'Log viewer', // Can use span here, or it bombs in some contexts
// (though not in examples)
resizable: true,
itemId :'windowCId',
iconCls : 'sm-log-viewer-icon',
maximizable: true,
width : 950,
height: 400,
formPadding : 5,
config : {
/**
* @cfg
* @accessor
*
* If true, it will be possible to perform searches across the
* whole grid.
*
* It will be possible to search using regular expressions, and
* results will be highlighted to make it easier to find them, and
* it will be possible to add navigate to the next/prior match.
*/
liveSearchEnabled : true
},
gridCfg : {
xtype: 'grid',
itemId : 'gridCId',
region: 'center',
border: false,
autoScroll: true,
multiSelect : false,
disableSelection: false, // We need this, or bad things will happen
loadMask: true,
viewConfig: {
emptyText : "No logs",
stripeRows: false
},
columns : [
{ dataIndex: 'formattedTime', text: 'Time', width: 140 },
{ dataIndex: 'level',
text: '<span data-qtip="Priority">P.</span>', width: 30,
renderer: function (value) {
var result, level;
value = value.toLowerCase();
result = '<div data-qtip="' + Ext.String.capitalize(value) +
'" class="sm-log-level-' +
value + '-icon" >' +
' </div>';
return result;
}
},
{ dataIndex: 'category', text: 'Category', width: 150},
{ dataIndex: 'ndc', text: 'NDC', width: 50},
{ dataIndex: 'formattedMessage', text: 'Message', width: 300},
{ dataIndex: 'hasLoggedObject',
text: '<span data-qtip="Is there an attached logged object?">' +
'LO?</span>',
width: 30,
renderer: function (value) {
var result = ' ';
if( value ) {
result = '<div data-qtip="' +
'There is a logged object attached to this log entry' +
'" class="sm-log-has-logged-object-true" >' +
' </div>';
}
return result;
}
},
{ dataIndex: 'formattedLoggedObject',
text: 'Logged Object', width: 500}
],
plugins: [
{
pluginId: 'rowExpanderPId',
ptype: 'dvp_rowexpander', // Uses Ext.ux.grid.plugin.RowExpander
// ptype: 'rowexpander', // Uses Ext.ux.RowExpander
rowBodyTpl : [
'<p>' +
// Time, priority, category and NDC
'<b>Time</b>: {formattedTime}' +
'<b> Priority</b>: {level} ' +
'<b> Category</b>: {category} ' +
'<b> NDC</b>: {ndc}' +
'<br>' +
// Message: if formatted message has multiple lines, put it
// in a different line
'<b>Message</b>: ' +
'<tpl if="formattedMultilineMessage != formattedMessage">' +
'<br>' +
'</tpl>' +
'{formattedMultilineMessage}' +
// Logged object: nothing, if there is no logged object
'<tpl if="hasLoggedObject">' +
'<br>' +
'<b>Logged Object</b>:' +
'<br>' +
'<p>{formattedMultilineLoggedObject}</p>' +
'</tpl>' +
'</p>'
]
}
]
},
formCfg : {
region: 'north',
split: true, // In case of window resize, and form items 'moving down'
autoScroll:true,
itemId : 'formCId',
xtype: 'form',
layout: 'column',
border : false,
fieldDefaults : {
selectOnFocus : true,
msgTarget : 'side',
autoFitErrors: true,
labelAlign: 'right',
validateOnChange : true,
fieldLabel: ' '
},
items : [
{ xtype: 'combo',
name: 'filteringLevel',
itemId : 'filteringLevelCId',
valueField : 'level',
displayField: 'name',
width: 125,
labelWidth : 45,
fieldLabel : 'Priority',
allowBlank:false,
autoSelect : true,
forceSelection: true,
editable: false,
typeAhead: false,
listConfig: {
getInnerTpl: function() {
var tpl = '<div class="{iconClass}">{name}</div>';
return tpl;
}
},
/*
djnpInputTooltip:
'<span class="search-field-info-tooltip"></span>' +
"We will show only logs with this or greater priority",
*/
listeners : {
change : {
fn: function() {
var win = this.up( '.window' );
if( this.isValid() ) {
win.applyFilter();
}
}
// ,buffer: 100
}
}
},
{ xtype: 'textfield',
name: 'filteringCategory',
labelWidth: 60,
width: 170,
fieldLabel: 'Category',
vtype: 'emptyOrLengthGreaterThan1',
/*
djnpInputTooltip : {
html :
'<span class="search-field-info-tooltip"></span>' +
"We will look for the entered value <b>anywhere</b> " +
"in the category"
},
*/
listeners : {
change : {
fn: function() {
var win = this.up( '.window' );
if( this.isValid() ) {
win.applyFilter();
}
}
//,buffer: 100
}
}
},
// @todo: almost cut and paste from above, refactor
{ xtype: 'textfield',
name: 'filteringFormattedMessage',
labelWidth: 60,
width: 170,
fieldLabel: 'Message',
vtype: 'emptyOrLengthGreaterThan1',
/*
djnpInputTooltip : {
html :
'<span class="search-field-info-tooltip"></span>' +
"We will look for the entered value <b>anywhere</b> " +
"in the message"
},
*/
listeners : {
change : {
fn: function() {
var win = this.up( '.window' );
if( this.isValid() ) {
win.applyFilter();
}
}
//,buffer: 100
}
}
},
{ xtype: 'textfield',
name: 'filteringNdc',
labelWidth: 30,
width: 130,
fieldLabel: 'NDC',
vtype: 'emptyOrLengthGreaterThan1',
/*
djnpInputTooltip : {
html :
'<span class="search-field-info-tooltip"></span>' +
"We will look for the entered value <b>anywhere</b> " +
"in the NDC"
},
*/
listeners : {
change : {
fn: function() {
var win = this.up( '.window' );
if( this.isValid() ) {
win.applyFilter();
}
}
//,buffer: 100
}
}
},
{ xtype: 'textfield',
name: 'filteringFormattedLoggedObject',
labelWidth: 90,
width: 190,
fieldLabel: 'Logged object',
vtype: 'emptyOrLengthGreaterThan1',
/*
djnpInputTooltip : {
html :
'<span class="search-field-info-tooltip"></span>' +
"We will look for the entered value <b>anywhere</b> " +
"in the logged object"
},
*/
listeners : {
change : {
fn: function() {
var win = this.up( '.window' );
if( this.isValid() ) {
win.applyFilter();
}
}
//,buffer: 100
}
}
}
]
},
listeners : {
destroy : function() {
this.detachLogAppender(true);
}
},
items :[
],
doDelayedFilter : function() {
var me = this;
// Need to restore store to 'non-filtered', and then filter it again
// using our filters
me.store.clearFilter(true);
me.store.filterBy( function(item) {
var ok;
ok = me.filterByLevel(item) &&
me.filterByCategory(item) &&
me.filterByMessage(item) &&
me.filterByNdc(item) &&
me.filterByLoggedObject(item);
return ok;
});
me.lastFilterTime = new Date();
},
applyFilter : function() {
// Filtering can be very slow if there is a flood of log calls.
// In this scenario, the program can slow down and get very
// unresponsive.
// To avoid this scenario, we take the following measures:
// If the last time we attempted to perform a filter was more than
// MIN_REFILTER_TIME ms ago, then we filter immediately. This avoids
// showing a lonely log entry that made into the grid because we
// are using a delayed taks that will clear it after some
// milliseconds.
// Else, we create a task that will execute in DELAY_TIME ms:
// if we keep getting a stream of logs, this will cause the filter
// to execute only when the flood stops.
var me = this, now = new Date(), elapsedTime, MIN_REFILTER_TIME = 1000,
FILTER_DELAY_TIME = 150;
me.lastFilterTime = me.lastFilterTime || new Date();
elapsedTime = now.getTime() - me.lastFilterTime.getTime();
me.doDelayedFilter();
/*
if( elapsedTime > MIN_REFILTER_TIME ) {
me.doDelayedFilter();
}
else {
if( !me.applyFilterTask ) {
me.applyFilterTask = new Ext.util.DelayedTask(function(){
me.doDelayedFilter();
});
}
// Wait FILTER_DEALY_TIME ms before really applying filter. If
// applyFilter
// is called again, the filtering will be cancelled, and we'll
// wait another FILTER_DELAY_TIME ms.
me.applyFilterTask.delay(FILTER_DELAY_TIME);
}
*/
},
filterByLevel : function( item ) {
var me = this, minLevel, thisLevel, thisLevelText;
if( !me.filteringLevel || !me.filteringLevel.rendered) {
return true;
}
minLevel = me.filteringLevel.getValue();
// @todo, why not have numeric level.level in model?
thisLevelText = item.get("level");
Sm.log.util.Assert.assert(thisLevelText);
thisLevel = Sm.log.Level.getLevelLevel(thisLevelText);
return thisLevel >= minLevel;
},
filterByStringFieldWithText : function( item, modelField, formField,
blankModelFieldIsOk, caseSensitive) {
var field, value, modelValue, found;
field = this.form.findField( formField);
Sm.log.util.Assert.assert(field);
value = field.getValue();
// If no filter, the record is in
if( value === '' ) {
return true;
}
modelValue = item.get(modelField);
Sm.log.util.Assert.assert( Ext.isString(modelValue));
// Sometimes we decide that an empty model value is ignored at
// filtering time and then the record is in
if( modelValue === '' && blankModelFieldIsOk) {
return true;
}
Sm.log.util.Assert.assert(modelValue || modelValue === '');
// If the text is anywhere in the model value, then there is a
// match. Take case sensitivity into account
if( !caseSensitive ) {
modelValue = modelValue.toUpperCase();
value = value.toUpperCase();
}
found = modelValue.indexOf(value) >= 0;
return found;
},
filterByCategory : function(item) {
return this.filterByStringFieldWithText(
item, "category", 'filteringCategory' );
},
filterByMessage : function(item) {
return this.filterByStringFieldWithText(
item, "formattedMessage", 'filteringFormattedMessage' );
},
filterByNdc : function(item) {
return this.filterByStringFieldWithText(
item, "ndc", 'filteringNdc' );
},
filterByLoggedObject : function(item) {
return this.filterByStringFieldWithText( item,
"formattedLoggedObject", 'filteringFormattedLoggedObject' );
},
initComponent : function(cfg) {
var me = this, levelToStoreData, levelsData,
filteringLevelCfg, bottomButtonsContainer, pad;
// ******************************************
// Special validation
Ext.form.field.VTypes.emptyOrLengthGreaterThan1 = function(v) {
if( !v) {
return false;
}
Sm.log.util.Assert.assert(Ext.isString(v));
return v.length > 1;
};
Ext.form.field.VTypes.emptyOrLengthGreaterThan1Text =
'Must be empty or have more than one character';
// *****************************************
// Configure store
me.store = Ext.create('Ext.data.ArrayStore',
{model: 'Sm.log.viewer.LoggingEvent',
sorters: [{property: 'formattedTime', direction: 'DESC'}]});
this.applyFilter();
// *****************************************
// Configure grid
me.gridCfg.store = me.store;
me.gridCfg.searchOnCriteriaChange = true;
Ext.Array.forEach( me.gridCfg.columns, function(column) {
column.style = { fontWeight : 'bold'};
});
if( this.liveSearchEnabled ) {
me.gridCfg.bbar = me.gridCfg.bbar || {};
me.gridCfg.bbar.items = me.gridCfg.items || [];
bottomButtonsContainer = me.gridCfg.bbar;
}
else {
bottomButtonsContainer = { xtype : 'statusbar',
dock: 'bottom', items: []};
me.gridCfg.dockedItems =[bottomButtonsContainer];
}
Ext.Array.push( bottomButtonsContainer.items, [
/*
{ xtype : 'button',
text :'Fake: generate logs',
handler : function() {
var win = this.up( '.window' );
win.generateFakeEvents( 1000);
}
},
*/
{ xtype: 'button',
text: 'Clear logs',
tooltip: 'Clears current logs as well as buffered logs',
handler : function() {
var win = this.up( '.window' );
win.clearLog();
}
},
{ xtype: 'button',
itemId : 'stateCId',
tooltip: 'Sets state to Logging/Buffering logs/Stopped',
menu: {
items: [
{ text: 'Log',
iconCls : 'sm-log-state-logging',
tooltip: 'Starts/restarts logging' +
'<p/> <p/>' +
'Shows incoming logs as they arrive: ' +
'when set, will show all buffered logs ' +
'that were pending',
handler: function() {
var win = this.up( '.window' );
win.startLogging();
}
},
{ text: 'Buffer new logs',
tooltip:
'Buffers incoming logs' +
'<p/> <p/>' +
'Buffered logs will be added to the window ' +
'when logging state is set to ' +
'logging again: ' +
'they will not be lost.' +
'<p/> <p/>' +
'This might be useful to avoid ' +
'interferences during debug due to the ' +
'logging window being updated during logging.',
iconCls : 'sm-log-state-buffering',
handler: function() {
var win = this.up( '.window' );
win.bufferLogging();
}
},
{ text: 'Stop logging',
tooltip: 'Stops logging: incoming will be lost',
iconCls : 'sm-log-state-stopped',
handler: function() {
var win = this.up( '.window' );
win.stopLogging();
}
}
]
}
}
]);
if( this.getLiveSearchEnabled() ) {
me.grid = Ext.create('Ext.ux.LiveSearchGridPanelEx', me.gridCfg );
me.grid.hasRowExpanderPlugin =
me.grid.getPlugin( 'rowExpanderPId' );
}
else {
me.grid = Ext.create('Ext.grid.Panel', me.gridCfg );
}
// ******************************************
// Configure form
pad = me.formPadding;
me.formCfg.bodyPadding = pad +", " + pad+ ", 0, 0";
me.formCfg.defaults = me.formCfg.defaults || {};
me.formCfg.defaults.style = {
marginBottom: ' ' + pad + 'px', // The 'px' is *needed*
marginLeft: ' ' + pad + 'px' // The 'px' is *needed*
};
filteringLevelCfg = me.formCfg.items[0];
Sm.log.util.Assert.assert( filteringLevelCfg.name ===
'filteringLevel');
filteringLevelCfg.value = Sm.log.Level.TRACE.getLevel();
me.items = [
me.formCfg,
me.grid
];
me.callParent(arguments);
me.grid = me.down( "#gridCId");
Sm.log.util.Assert.assert( me.grid);
me.formPanel = me.down( "#formCId");
Sm.log.util.Assert.assert( me.formPanel);
me.form = me.formPanel.getForm();
Sm.log.util.Assert.assert( me.form);
me.stateButton = me.down( "#stateCId" );
Sm.log.util.Assert.assert( me.stateButton);
me.filteringLevel = me.down( "#filteringLevelCId");
Sm.log.util.Assert.assert( me.filteringLevel);
levelToStoreData = function ( level ) {
// Unfortunately an ArrayStore can't cope with model objects,
// but rather we must turn them into an array :(
var text= level.name, iconClass =
'sm-log-level-' + text.toLowerCase() + '-icon';
/*
return new Sm.log.viewer.Level(
{ level: level.level, name: text,
iconClass: iconClass});
*/
// Items in array *MUST* have the same order than fields in model!!
return [level.getLevel(), level.getName(), iconClass];
};
levelsData =
[levelToStoreData( Sm.log.Level.FATAL),
levelToStoreData( Sm.log.Level.ERROR),
levelToStoreData( Sm.log.Level.WARN),
levelToStoreData( Sm.log.Level.INFO),
levelToStoreData( Sm.log.Level.DEBUG),
levelToStoreData( Sm.log.Level.TRACE)];
me.filteringLevel.bindStore( Ext.create('Ext.data.ArrayStore',
{ model: 'Sm.log.viewer.Level', autoLoad:true,
data: levelsData
}));
me.filteringLevel.setValue(Sm.log.Level.TRACE.getLevel());
me.on( 'activate', function() {this.focustLastLogIfSortedByTime();},
me, {single: true});
// *****************************************************************
// Delayed setup
me.on( 'boxready', this.boxreadyInitialization, me);
},
boxreadyInitialization : function() {
var me = this;
// ****************************************************************
// Attach appender, if it is there: else, create a new one
if( !this.getAppender() ) {
this.appender = new Sm.log.LogViewerAppender();
}
this.attachLogAppender( this.getAppender());
// Believe it or not, assigning this to title directly bombs
// in some cases (though not in our examples)
me.setTitle(
'<a data-qtip="Click to visit log4js-ext website" ' +
'style="text-decoration: none" target="_new"' +
'href="http://code.google.com/p/log4js-ext/">' +
'<span class="sm-log-viewer-title-1">log4js</span>' +
'<span class="sm-log-viewer-title-2">-ext</span></a>' );
},
startLogging : function() {
var me = this;
Sm.log.util.Assert.assert(me.appender);
if( me.appender.canLog() ) {
this.updateState( 'Set logging state', 'Logging',
'sm-log-state-logging');
me.appender.startLogging();
return true;
}
return false;
},
stopLogging : function() {
var me = this;
Sm.log.util.Assert.assert(me.appender);
this.updateState( 'Set logging state', 'Stopped',
'sm-log-state-stopped');
me.appender.stopLogging();
},
bufferLogging : function() {
var me = this;
Sm.log.util.Assert.assert(me.appender);
this.updateState( 'Set logging state', 'Buffering',
'sm-log-state-buffering');
me.appender.startBuffering();
},
setNoAppenderAttachedState: function() {
var me = this;
Sm.log.util.Assert.assert( !this.getAppender() );
this.updateState( 'Logging state: no appender attached',
'No appender attached',
'sm-log-state-no-appender-attached', true);
},
updateState : function(text, tooltip, iconCls, disabled) {
var me = this, stateIcon;
disabled = disabled || false;
this.stateButton.setText( text );
this.stateButton.setDisabled(disabled);
this.stateButton.setIconCls( iconCls );
//this.logger.trace( 'Logging state changed. Text=' + text +
// ", IconCls=" + iconCls);
},
clearLog : function() {
var me = this;
me.store.removeAll();
if( this.getAppender() ) {
this.getAppender().clearBuffer();
}
},
getAppender : function() {
return this.appender;
},
attachLogAppender : function (appender) {
var me = this;
if( !appender ) {
this.detachLogAppender(false);
}
else {
me.appender = appender;
me.appender.attachViewer( me );
if( me.appender.canLog() ) {
me.startLogging();
}
else {
me.bufferLogging();
}
}
},
detachLogAppender : function(destroying) {
if( this.appender ) {
this.appender.detachViewer();
this.appender = null;
if( !destroying ) {
this.setNoAppenderAttachedState();
}
}
},
doDelayedAppend : function() {
var me = this, loggingEvent, i ;
for( i = 0; i < this.delayedLogs.length; i = i + 1 ) {
loggingEvent = this.delayedLogs[i];
me.store.add(new Sm.log.viewer.LoggingEvent(loggingEvent));
}
this.delayedLogs = [];
// If we don't reapply filters, then the new items are visible
// even if they do not pass the filter criteria
me.applyFilter();
// Focus the last log
this.focustLastLogIfSortedByTime();
},
appendLoggingEvents : function( loggingEvents ) {
var me = this, DELAY_TIME = 50;
this.delayedLogs = (this.delayedLogs || []).concat( loggingEvents );
if( !me.applyLogTask ) {
me.applyLogTask = new Ext.util.DelayedTask(function(){
me.doDelayedAppend();
});
}
// Wait FILTER_DEALY_TIME ms before really applying filter. If
// applyLog
// is called again, the filtering will be cancelled, and we'll
// wait another FILTER_DELAY_TIME ms.
me.applyLogTask.delay(DELAY_TIME);
},
// @private
//
// If sorted by time, we probably want to see things 'as they happen'
// if that's the case, it is amazingly useful to position ourselves
// in the last added log record, *without* losing the last selected
// item, which can act like a 'I was here' mark.
//
// This is a very special case, but probably rather common and really
// worth it when you are debugging :)
focustLastLogIfSortedByTime : function() {
var me = this, row, gridView,
priorSelectedRows, oldSelection = null,
sorters, sorter;
if( me.store.getCount() === 0 ) {
return;
}
sorters = me.store.sorters;
if( sorters.getCount() === 0 ) {
return;
}
sorter = sorters.getAt(0);
if( sorter.property !== 'formattedTime') {
return;
}
if( sorter.direction.toUpperCase() === 'DESC') {
row = 0;
}
else {
row = me.store.getCount() - 1;
}
gridView = me.grid.getView();
priorSelectedRows = gridView.getSelectionModel().getSelection();
Sm.log.util.Assert.assert( priorSelectedRows.length <= 1);
if( priorSelectedRows.length > 0) {
oldSelection = priorSelectedRows[0];
}
gridView.focusRow(row);
// If no old selection, we select last log: that way, when
// we do more logs, we get visual feedback that there have
// been new log since the last time
if( !oldSelection ) {
gridView.getSelectionModel().select(row);
}
}
/*
// @todo pag: remove this and its references
generateFakeEvents : function (count ) {
var result = [],
levels = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"],
li, i,
ndc,
ev;
for( i = 1; i <= count; i = i + 1 ) {
li = i % levels.length;
if( i % 3 === 0) {
ndc = "ndc ate " + ((i % 10) + 2);
}
else {
ndc = "";
}
ev = {
level : levels[li],
ndc : ndc,
formattedMessage : "This is message number " + i,
category : "Category.cate " + i % 10,
formattedTime : new Date( 1990 + i % 100,
(i % 10) + 1, (i % 25) + 1 ).toString()
};
result.push(ev);
}
this.appendLoggingEvents(result);
}
*/
});
}());