479 lines
12 KiB
JavaScript
479 lines
12 KiB
JavaScript
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// 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.
|
|
|
|
/**
|
|
* @fileoverview This file implements a store for goog.debug.Logger data.
|
|
*/
|
|
|
|
goog.provide('goog.gears.LogStore');
|
|
goog.provide('goog.gears.LogStore.Query');
|
|
|
|
goog.require('goog.async.Delay');
|
|
goog.require('goog.debug.LogManager');
|
|
goog.require('goog.gears.BaseStore');
|
|
goog.require('goog.gears.BaseStore.SchemaType');
|
|
goog.require('goog.json');
|
|
goog.require('goog.log');
|
|
goog.require('goog.log.Level');
|
|
goog.require('goog.log.LogRecord');
|
|
|
|
|
|
|
|
/**
|
|
* Implements a store for goog.debug.Logger data.
|
|
* @param {goog.gears.Database} database Database.
|
|
* @param {?string=} opt_tableName Name of logging table to use.
|
|
* @extends {goog.gears.BaseStore}
|
|
* @constructor
|
|
*/
|
|
goog.gears.LogStore = function(database, opt_tableName) {
|
|
goog.gears.BaseStore.call(this, database);
|
|
|
|
/**
|
|
* Name of log table.
|
|
* @type {string}
|
|
*/
|
|
var tableName = opt_tableName || goog.gears.LogStore.DEFAULT_TABLE_NAME_;
|
|
this.tableName_ = tableName;
|
|
|
|
// Override BaseStore schema attribute.
|
|
this.schema = [
|
|
{
|
|
type: goog.gears.BaseStore.SchemaType.TABLE,
|
|
name: tableName,
|
|
columns: [
|
|
// Unique ID.
|
|
'id INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
// Timestamp.
|
|
'millis BIGINT',
|
|
// #goog.log.Level value.
|
|
'level INTEGER',
|
|
// Message.
|
|
'msg TEXT',
|
|
// Name of logger object.
|
|
'logger TEXT',
|
|
// Serialized error object.
|
|
'exception TEXT',
|
|
// Full exception text.
|
|
'exceptionText TEXT'
|
|
]
|
|
},
|
|
{
|
|
type: goog.gears.BaseStore.SchemaType.INDEX,
|
|
name: tableName + 'MillisIndex',
|
|
isUnique: false,
|
|
tableName: tableName,
|
|
columns: ['millis']
|
|
},
|
|
{
|
|
type: goog.gears.BaseStore.SchemaType.INDEX,
|
|
name: tableName + 'LevelIndex',
|
|
isUnique: false,
|
|
tableName: tableName,
|
|
columns: ['level']
|
|
}
|
|
];
|
|
|
|
/**
|
|
* Buffered log records not yet flushed to DB.
|
|
* @type {Array.<goog.log.LogRecord>}
|
|
* @private
|
|
*/
|
|
this.records_ = [];
|
|
|
|
/**
|
|
* Save the publish handler so it can be removed.
|
|
* @type {Function}
|
|
* @private
|
|
*/
|
|
this.publishHandler_ = goog.bind(this.addLogRecord, this);
|
|
};
|
|
goog.inherits(goog.gears.LogStore, goog.gears.BaseStore);
|
|
|
|
|
|
/** @override */
|
|
goog.gears.LogStore.prototype.version = 1;
|
|
|
|
|
|
/**
|
|
* Whether we are currently capturing logger output.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.prototype.isCapturing_ = false;
|
|
|
|
|
|
/**
|
|
* Size of buffered log data messages.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.prototype.bufferSize_ = 0;
|
|
|
|
|
|
/**
|
|
* Scheduler for pruning action.
|
|
* @type {goog.async.Delay?}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.prototype.delay_ = null;
|
|
|
|
|
|
/**
|
|
* Use this to protect against recursive flushing.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.prototype.isFlushing_ = false;
|
|
|
|
|
|
/**
|
|
* Logger.
|
|
* @type {goog.log.Logger}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.prototype.logger_ =
|
|
goog.log.getLogger('goog.gears.LogStore');
|
|
|
|
|
|
/**
|
|
* Default value for how many records we keep when pruning.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.DEFAULT_PRUNE_KEEPER_COUNT_ = 1000;
|
|
|
|
|
|
/**
|
|
* Default value for how often to auto-prune (10 minutes).
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.DEFAULT_AUTOPRUNE_INTERVAL_MILLIS_ = 10 * 60 * 1000;
|
|
|
|
|
|
/**
|
|
* The name for the log table.
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.DEFAULT_TABLE_NAME_ = 'GoogGearsDebugLogStore';
|
|
|
|
|
|
/**
|
|
* Max message bytes to buffer before flushing to database.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.MAX_BUFFER_BYTES_ = 200000;
|
|
|
|
|
|
/**
|
|
* Flush buffered log records.
|
|
*/
|
|
goog.gears.LogStore.prototype.flush = function() {
|
|
if (this.isFlushing_ || !this.getDatabaseInternal()) {
|
|
return;
|
|
}
|
|
this.isFlushing_ = true;
|
|
|
|
// Grab local copy of records so database can log during this process.
|
|
goog.log.info(this.logger_, 'flushing ' + this.records_.length + ' records');
|
|
var records = this.records_;
|
|
this.records_ = [];
|
|
|
|
for (var i = 0; i < records.length; i++) {
|
|
var record = records[i];
|
|
var exception = record.getException();
|
|
var serializedException = exception ? goog.json.serialize(exception) : '';
|
|
var statement = 'INSERT INTO ' + this.tableName_ +
|
|
' (millis, level, msg, logger, exception, exceptionText)' +
|
|
' VALUES (?, ?, ?, ?, ?, ?)';
|
|
this.getDatabaseInternal().execute(statement,
|
|
record.getMillis(), record.getLevel().value, record.getMessage(),
|
|
record.getLoggerName(), serializedException,
|
|
record.getExceptionText() || '');
|
|
}
|
|
|
|
this.isFlushing_ = false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Create new delay object for auto-pruning. Does not stop or
|
|
* start auto-pruning, call #startAutoPrune and #startAutoPrune for that.
|
|
* @param {?number=} opt_count Number of records of recent hitory to keep.
|
|
* @param {?number=} opt_interval Milliseconds to wait before next pruning.
|
|
*/
|
|
goog.gears.LogStore.prototype.createAutoPruneDelay = function(
|
|
opt_count, opt_interval) {
|
|
if (this.delay_) {
|
|
this.delay_.dispose();
|
|
this.delay_ = null;
|
|
}
|
|
var interval = typeof opt_interval == 'number' ?
|
|
opt_interval : goog.gears.LogStore.DEFAULT_AUTOPRUNE_INTERVAL_MILLIS_;
|
|
var listener = goog.bind(this.autoPrune_, this, opt_count);
|
|
this.delay_ = new goog.async.Delay(listener, interval);
|
|
};
|
|
|
|
|
|
/**
|
|
* Enable periodic pruning. As a side effect, this also flushes the memory
|
|
* buffer.
|
|
*/
|
|
goog.gears.LogStore.prototype.startAutoPrune = function() {
|
|
if (!this.delay_) {
|
|
this.createAutoPruneDelay(
|
|
goog.gears.LogStore.DEFAULT_PRUNE_KEEPER_COUNT_,
|
|
goog.gears.LogStore.DEFAULT_AUTOPRUNE_INTERVAL_MILLIS_);
|
|
}
|
|
this.delay_.fire();
|
|
};
|
|
|
|
|
|
/**
|
|
* Disable scheduled pruning.
|
|
*/
|
|
goog.gears.LogStore.prototype.stopAutoPrune = function() {
|
|
if (this.delay_) {
|
|
this.delay_.stop();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean} True iff auto prune timer is active.
|
|
*/
|
|
goog.gears.LogStore.prototype.isAutoPruneActive = function() {
|
|
return !!this.delay_ && this.delay_.isActive();
|
|
};
|
|
|
|
|
|
/**
|
|
* Prune, and schedule next pruning.
|
|
* @param {?number=} opt_count Number of records of recent hitory to keep.
|
|
* @private
|
|
*/
|
|
goog.gears.LogStore.prototype.autoPrune_ = function(opt_count) {
|
|
this.pruneBeforeCount(opt_count);
|
|
this.delay_.start();
|
|
};
|
|
|
|
|
|
/**
|
|
* Keep some number of most recent log records and delete all older ones.
|
|
* @param {?number=} opt_count Number of records of recent history to keep. If
|
|
* unspecified, we use #goog.gears.LogStore.DEFAULT_PRUNE_KEEPER_COUNT_.
|
|
* Pass in 0 to delete all log records.
|
|
*/
|
|
goog.gears.LogStore.prototype.pruneBeforeCount = function(opt_count) {
|
|
if (!this.getDatabaseInternal()) {
|
|
return;
|
|
}
|
|
var count = typeof opt_count == 'number' ?
|
|
opt_count : goog.gears.LogStore.DEFAULT_PRUNE_KEEPER_COUNT_;
|
|
goog.log.info(this.logger_, 'pruning before ' + count + ' records ago');
|
|
this.flush();
|
|
this.getDatabaseInternal().execute('DELETE FROM ' + this.tableName_ +
|
|
' WHERE id <= ((SELECT MAX(id) FROM ' + this.tableName_ + ') - ?)',
|
|
count);
|
|
};
|
|
|
|
|
|
/**
|
|
* Delete log record #id and all older records.
|
|
* @param {number} sequenceNumber ID before which we delete all records.
|
|
*/
|
|
goog.gears.LogStore.prototype.pruneBeforeSequenceNumber =
|
|
function(sequenceNumber) {
|
|
if (!this.getDatabaseInternal()) {
|
|
return;
|
|
}
|
|
goog.log.info(this.logger_,
|
|
'pruning before sequence number ' + sequenceNumber);
|
|
this.flush();
|
|
this.getDatabaseInternal().execute(
|
|
'DELETE FROM ' + this.tableName_ + ' WHERE id <= ?',
|
|
sequenceNumber);
|
|
};
|
|
|
|
|
|
/**
|
|
* Whether we are currently capturing logger output.
|
|
* @return {boolean} Whether we are currently capturing logger output.
|
|
*/
|
|
goog.gears.LogStore.prototype.isCapturing = function() {
|
|
return this.isCapturing_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets whether we are currently capturing logger output.
|
|
* @param {boolean} capturing Whether to capture logger output.
|
|
*/
|
|
goog.gears.LogStore.prototype.setCapturing = function(capturing) {
|
|
if (capturing != this.isCapturing_) {
|
|
this.isCapturing_ = capturing;
|
|
|
|
// Attach or detach handler from the root logger.
|
|
var rootLogger = goog.debug.LogManager.getRoot();
|
|
if (capturing) {
|
|
goog.log.addHandler(rootLogger, this.publishHandler_);
|
|
goog.log.info(this.logger_, 'enabled');
|
|
} else {
|
|
goog.log.info(this.logger_, 'disabling');
|
|
goog.log.removeHandler(rootLogger, this.publishHandler_);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a log record.
|
|
* @param {goog.log.LogRecord} logRecord the LogRecord.
|
|
*/
|
|
goog.gears.LogStore.prototype.addLogRecord = function(logRecord) {
|
|
this.records_.push(logRecord);
|
|
this.bufferSize_ += logRecord.getMessage().length;
|
|
var exceptionText = logRecord.getExceptionText();
|
|
if (exceptionText) {
|
|
this.bufferSize_ += exceptionText.length;
|
|
}
|
|
if (this.bufferSize_ >= goog.gears.LogStore.MAX_BUFFER_BYTES_) {
|
|
this.flush();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Select log records.
|
|
* @param {goog.gears.LogStore.Query} query Query object.
|
|
* @return {Array.<goog.log.LogRecord>} Selected logs in descending
|
|
* order of creation time.
|
|
*/
|
|
goog.gears.LogStore.prototype.select = function(query) {
|
|
if (!this.getDatabaseInternal()) {
|
|
// This should only occur if we've been disposed.
|
|
return [];
|
|
}
|
|
this.flush();
|
|
|
|
// TODO(user) Perhaps have Query object build this SQL string so we can
|
|
// omit unneeded WHERE clauses.
|
|
var statement =
|
|
'SELECT id, millis, level, msg, logger, exception, exceptionText' +
|
|
' FROM ' + this.tableName_ +
|
|
' WHERE level >= ? AND millis >= ? AND millis <= ?' +
|
|
' AND msg like ? and logger like ?' +
|
|
' ORDER BY id DESC LIMIT ?';
|
|
var rows = this.getDatabaseInternal().queryObjectArray(statement,
|
|
query.level.value, query.minMillis, query.maxMillis,
|
|
query.msgLike, query.loggerLike, query.limit);
|
|
|
|
var result = Array(rows.length);
|
|
for (var i = rows.length - 1; i >= 0; i--) {
|
|
var row = rows[i];
|
|
|
|
// Parse fields, allowing for invalid values.
|
|
var sequenceNumber = Number(row['id']) || 0;
|
|
var level = goog.log.Level.getPredefinedLevelByValue(
|
|
Number(row['level']) || 0);
|
|
var msg = row['msg'] || '';
|
|
var loggerName = row['logger'] || '';
|
|
var millis = Number(row['millis']) || 0;
|
|
var serializedException = row['exception'];
|
|
var exception = serializedException ?
|
|
goog.json.parse(serializedException) : null;
|
|
var exceptionText = row['exceptionText'] || '';
|
|
|
|
// Create record.
|
|
var record = new goog.log.LogRecord(level, msg, loggerName,
|
|
millis, sequenceNumber);
|
|
if (exception) {
|
|
record.setException(exception);
|
|
record.setExceptionText(exceptionText);
|
|
}
|
|
|
|
result[i] = record;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.gears.LogStore.prototype.disposeInternal = function() {
|
|
this.flush();
|
|
|
|
goog.gears.LogStore.superClass_.disposeInternal.call(this);
|
|
|
|
if (this.delay_) {
|
|
this.delay_.dispose();
|
|
this.delay_ = null;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Query to select log records.
|
|
* @constructor
|
|
*/
|
|
goog.gears.LogStore.Query = function() {
|
|
};
|
|
|
|
|
|
/**
|
|
* Minimum logging level.
|
|
* @type {goog.log.Level}
|
|
*/
|
|
goog.gears.LogStore.Query.prototype.level = goog.log.Level.ALL;
|
|
|
|
|
|
/**
|
|
* Minimum timestamp, inclusive.
|
|
* @type {number}
|
|
*/
|
|
goog.gears.LogStore.Query.prototype.minMillis = -1;
|
|
|
|
|
|
/**
|
|
* Maximum timestamp, inclusive.
|
|
* @type {number}
|
|
*/
|
|
goog.gears.LogStore.Query.prototype.maxMillis = Infinity;
|
|
|
|
|
|
/**
|
|
* Message 'like' pattern.
|
|
* See http://www.sqlite.org/lang_expr.html#likeFunc for 'like' syntax.
|
|
* @type {string}
|
|
*/
|
|
goog.gears.LogStore.Query.prototype.msgLike = '%';
|
|
|
|
|
|
/**
|
|
* Logger name 'like' pattern.
|
|
* See http://www.sqlite.org/lang_expr.html#likeFunc for 'like' syntax.
|
|
* @type {string}
|
|
*/
|
|
goog.gears.LogStore.Query.prototype.loggerLike = '%';
|
|
|
|
|
|
/**
|
|
* Max # recent records to return. -1 means no limit.
|
|
* @type {number}
|
|
*/
|
|
goog.gears.LogStore.Query.prototype.limit = -1;
|