gecko/services/common/logmanager.js

280 lines
10 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict;"
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");
this.EXPORTED_SYMBOLS = [
"LogManager",
];
const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days
// "shared" logs (ie, where the same log name is used by multiple LogManager
// instances) are a fact of life here - eg, FirefoxAccounts logs are used by
// both Sync and Reading List.
// However, different instances have different pref branches, so we need to
// handle when one pref branch says "Debug" and the other says "Error"
// So we (a) keep singleton console and dump appenders and (b) keep track
// of the minimum (ie, most verbose) level and use that.
// This avoids (a) the most recent setter winning (as that is indeterminate)
// and (b) multiple dump/console appenders being added to the same log multiple
// times, which would cause messages to appear twice.
// Singletons used by each instance.
let formatter;
let dumpAppender;
let consoleAppender;
// A set of all preference roots used by all instances.
let allBranches = new Set();
// The public LogManager object.
function LogManager(prefRoot, logNames, logFilePrefix) {
this.init(prefRoot, logNames, logFilePrefix);
}
LogManager.prototype = {
REASON_SUCCESS: "success",
REASON_ERROR: "error",
_cleaningUpFileLogs: false,
_prefObservers: [],
init(prefRoot, logNames, logFilePrefix) {
if (prefRoot instanceof Preferences) {
this._prefs = prefRoot;
} else {
this._prefs = new Preferences(prefRoot);
}
this.logFilePrefix = logFilePrefix;
if (!formatter) {
// Create a formatter and various appenders to attach to the logs.
formatter = new Log.BasicFormatter();
consoleAppender = new Log.ConsoleAppender(formatter);
dumpAppender = new Log.DumpAppender(formatter);
}
allBranches.add(this._prefs._branchStr);
// We create a preference observer for all our prefs so they are magically
// reflected if the pref changes after creation.
let setupAppender = (appender, prefName, defaultLevel, findSmallest = false) => {
let observer = newVal => {
let level = Log.Level[newVal] || defaultLevel;
if (findSmallest) {
// We need to find the smallest value from all prefs controlling this appender.
for (let branch of allBranches) {
let lookPrefBranch = new Preferences(branch);
let lookVal = Log.Level[lookPrefBranch.get(prefName)];
if (lookVal && lookVal < level) {
level = lookVal;
}
}
}
appender.level = level;
}
this._prefs.observe(prefName, observer, this);
this._prefObservers.push([prefName, observer]);
// and call the observer now with the current pref value.
observer(this._prefs.get(prefName));
return observer;
}
this._observeConsolePref = setupAppender(consoleAppender, "log.appender.console", Log.Level.Error, true);
this._observeDumpPref = setupAppender(dumpAppender, "log.appender.dump", Log.Level.Error, true);
// The file appender doesn't get the special singleton behaviour.
let fapp = this._fileAppender = new Log.StorageStreamAppender(formatter);
// the stream gets a default of Debug as the user must go out of their way
// to see the stuff spewed to it.
this._observeStreamPref = setupAppender(fapp, "log.appender.file.level", Log.Level.Debug);
// now attach the appenders to all our logs.
for (let logName of logNames) {
let log = Log.repository.getLogger(logName);
for (let appender of [fapp, dumpAppender, consoleAppender]) {
log.addAppender(appender);
}
}
// and use the first specified log as a "root" for our log.
this._log = Log.repository.getLogger(logNames[0] + ".LogManager");
},
/**
* Cleanup this instance
*/
finalize() {
for (let [name, pref] of this._prefObservers) {
this._prefs.ignore(name, pref, this);
}
this._prefObservers = [];
try {
allBranches.delete(this._prefs._branchStr);
} catch (e) {}
this._prefs = null;
},
get _logFileDirectory() {
// At this point we don't allow a custom directory for the logs so
// about:sync-log can be used. We could fix this later if necessary.
return FileUtils.getDir("ProfD", ["weave", "logs"]);
},
/**
* Copy an input stream to the named file, doing everything off the main
* thread.
* outputFile is an nsIFile, but is used only for the name.
* Returns a promise that is resolved with the file modification date on
* completion or rejected if there is an error.
*/
_copyStreamToFile: Task.async(function* (inputStream, outputFile) {
// The log data could be large, so we don't want to pass it all in a single
// message, so use BUFFER_SIZE chunks.
const BUFFER_SIZE = 8192;
// get a binary stream
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(inputStream);
yield OS.File.makeDir(outputFile.parent.path, { ignoreExisting: true });
let output = yield OS.File.open(outputFile.path, { write: true} );
try {
while (true) {
let available = binaryStream.available();
if (!available) {
break;
}
let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE));
yield output.write(new Uint8Array(chunk));
}
} finally {
try {
binaryStream.close(); // inputStream is closed by the binaryStream
yield output.close();
} catch (ex) {
this._log.error("Failed to close the input stream", ex);
}
}
this._log.trace("finished copy to", outputFile.path);
}),
/**
* Possibly generate a log file for all accumulated log messages and refresh
* the input & output streams.
* Returns a promise that resolves on completion or rejects if the file could
* not be written.
*/
resetFileLog: Task.async(function* (reason) {
try {
let flushToFile;
let reasonPrefix;
switch (reason) {
case this.REASON_SUCCESS:
flushToFile = this._prefs.get("log.appender.file.logOnSuccess", false);
reasonPrefix = "success";
break;
case this.REASON_ERROR:
flushToFile = this._prefs.get("log.appender.file.logOnError", true);
reasonPrefix = "error";
break;
default:
throw new Error("Invalid reason");
}
// might as well avoid creating an input stream if we aren't going to use it.
if (!flushToFile) {
this._fileAppender.reset();
return;
}
let inStream = this._fileAppender.getInputStream();
this._fileAppender.reset();
if (inStream) {
this._log.debug("Flushing file log");
// We have reasonPrefix at the start of the filename so all "error"
// logs are grouped in about:sync-log.
let filename = reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt";
let file = this._logFileDirectory;
file.append(filename);
this._log.trace("Beginning stream copy to " + file.leafName + ": " +
Date.now());
try {
yield this._copyStreamToFile(inStream, file);
this._log.trace("onCopyComplete", Date.now());
} catch (ex) {
this._log.error("Failed to copy log stream to file", ex);
return;
}
// It's not completely clear to markh why we only do log cleanups
// for errors, but for now the Sync semantics have been copied...
// (one theory is that only cleaning up on error makes it less
// likely old error logs would be removed, but that's not true if
// there are occasional errors - let's address this later!)
if (reason == this.REASON_ERROR && !this._cleaningUpFileLogs) {
this._log.trace("Scheduling cleanup.");
// Note we don't return/yield or otherwise wait on this promise - it
// continues in the background
this.cleanupLogs().catch(err => {
this._log.error("Failed to cleanup logs", err);
});
}
}
} catch (ex) {
this._log.error("Failed to resetFileLog", ex)
}
}),
/**
* Finds all logs older than maxErrorAge and deletes them using async I/O.
*/
cleanupLogs: Task.async(function* () {
this._cleaningUpFileLogs = true;
let iterator = new OS.File.DirectoryIterator(this._logFileDirectory.path);
let maxAge = this._prefs.get("log.appender.file.maxErrorAge", DEFAULT_MAX_ERROR_AGE);
let threshold = Date.now() - 1000 * maxAge;
this._log.debug("Log cleanup threshold time: " + threshold);
yield iterator.forEach(Task.async(function* (entry) {
if (!entry.name.startsWith("error-" + this.logFilePrefix + "-") &&
!entry.name.startsWith("success-" + this.logFilePrefix + "-")) {
return;
}
try {
// need to call .stat() as the enumerator doesn't give that to us on *nix.
let info = yield OS.File.stat(entry.path);
if (info.lastModificationDate.getTime() >= threshold) {
return;
}
this._log.trace(" > Cleanup removing " + entry.name +
" (" + info.lastModificationDate.getTime() + ")");
yield OS.File.remove(entry.path);
this._log.trace("Deleted " + entry.name);
} catch (ex) {
this._log.debug("Encountered error trying to clean up old log file "
+ entry.name, ex);
}
}.bind(this)));
this._cleaningUpFileLogs = false;
this._log.debug("Done deleting files.");
// This notification is used only for tests.
Services.obs.notifyObservers(null, "services-tests:common:log-manager:cleanup-logs", null);
}),
}