logmanager.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict;"
  5. var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
  6. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  7. XPCOMUtils.defineLazyModuleGetter(this, "Services",
  8. "resource://gre/modules/Services.jsm");
  9. XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
  10. "resource://gre/modules/FileUtils.jsm");
  11. XPCOMUtils.defineLazyModuleGetter(this, "Log",
  12. "resource://gre/modules/Log.jsm");
  13. XPCOMUtils.defineLazyModuleGetter(this, "OS",
  14. "resource://gre/modules/osfile.jsm");
  15. XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
  16. "resource://services-common/utils.js");
  17. Cu.import("resource://gre/modules/Preferences.jsm");
  18. Cu.import("resource://gre/modules/Task.jsm");
  19. this.EXPORTED_SYMBOLS = [
  20. "LogManager",
  21. ];
  22. const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days
  23. // "shared" logs (ie, where the same log name is used by multiple LogManager
  24. // instances) are a fact of life here - eg, FirefoxAccounts logs are used by
  25. // both Sync and Reading List.
  26. // However, different instances have different pref branches, so we need to
  27. // handle when one pref branch says "Debug" and the other says "Error"
  28. // So we (a) keep singleton console and dump appenders and (b) keep track
  29. // of the minimum (ie, most verbose) level and use that.
  30. // This avoids (a) the most recent setter winning (as that is indeterminate)
  31. // and (b) multiple dump/console appenders being added to the same log multiple
  32. // times, which would cause messages to appear twice.
  33. // Singletons used by each instance.
  34. var formatter;
  35. var dumpAppender;
  36. var consoleAppender;
  37. // A set of all preference roots used by all instances.
  38. var allBranches = new Set();
  39. // A storage appender that is flushable to a file on disk. Policies for
  40. // when to flush, to what file, log rotation etc are up to the consumer
  41. // (although it does maintain a .sawError property to help the consumer decide
  42. // based on its policies)
  43. function FlushableStorageAppender(formatter) {
  44. Log.StorageStreamAppender.call(this, formatter);
  45. this.sawError = false;
  46. }
  47. FlushableStorageAppender.prototype = {
  48. __proto__: Log.StorageStreamAppender.prototype,
  49. append(message) {
  50. if (message.level >= Log.Level.Error) {
  51. this.sawError = true;
  52. }
  53. Log.StorageStreamAppender.prototype.append.call(this, message);
  54. },
  55. reset() {
  56. Log.StorageStreamAppender.prototype.reset.call(this);
  57. this.sawError = false;
  58. },
  59. // Flush the current stream to a file. Somewhat counter-intuitively, you
  60. // must pass a log which will be written to with details of the operation.
  61. flushToFile: Task.async(function* (subdirArray, filename, log) {
  62. let inStream = this.getInputStream();
  63. this.reset();
  64. if (!inStream) {
  65. log.debug("Failed to flush log to a file - no input stream");
  66. return;
  67. }
  68. log.debug("Flushing file log");
  69. log.trace("Beginning stream copy to " + filename + ": " + Date.now());
  70. try {
  71. yield this._copyStreamToFile(inStream, subdirArray, filename, log);
  72. log.trace("onCopyComplete", Date.now());
  73. } catch (ex) {
  74. log.error("Failed to copy log stream to file", ex);
  75. }
  76. }),
  77. /**
  78. * Copy an input stream to the named file, doing everything off the main
  79. * thread.
  80. * subDirArray is an array of path components, relative to the profile
  81. * directory, where the file will be created.
  82. * outputFileName is the filename to create.
  83. * Returns a promise that is resolved on completion or rejected with an error.
  84. */
  85. _copyStreamToFile: Task.async(function* (inputStream, subdirArray, outputFileName, log) {
  86. // The log data could be large, so we don't want to pass it all in a single
  87. // message, so use BUFFER_SIZE chunks.
  88. const BUFFER_SIZE = 8192;
  89. // get a binary stream
  90. let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
  91. binaryStream.setInputStream(inputStream);
  92. let outputDirectory = OS.Path.join(OS.Constants.Path.profileDir, ...subdirArray);
  93. yield OS.File.makeDir(outputDirectory, { ignoreExisting: true, from: OS.Constants.Path.profileDir });
  94. let fullOutputFileName = OS.Path.join(outputDirectory, outputFileName);
  95. let output = yield OS.File.open(fullOutputFileName, { write: true} );
  96. try {
  97. while (true) {
  98. let available = binaryStream.available();
  99. if (!available) {
  100. break;
  101. }
  102. let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE));
  103. yield output.write(new Uint8Array(chunk));
  104. }
  105. } finally {
  106. try {
  107. binaryStream.close(); // inputStream is closed by the binaryStream
  108. yield output.close();
  109. } catch (ex) {
  110. log.error("Failed to close the input stream", ex);
  111. }
  112. }
  113. log.trace("finished copy to", fullOutputFileName);
  114. }),
  115. }
  116. // The public LogManager object.
  117. function LogManager(prefRoot, logNames, logFilePrefix) {
  118. this._prefObservers = [];
  119. this.init(prefRoot, logNames, logFilePrefix);
  120. }
  121. LogManager.prototype = {
  122. _cleaningUpFileLogs: false,
  123. init(prefRoot, logNames, logFilePrefix) {
  124. if (prefRoot instanceof Preferences) {
  125. this._prefs = prefRoot;
  126. } else {
  127. this._prefs = new Preferences(prefRoot);
  128. }
  129. this.logFilePrefix = logFilePrefix;
  130. if (!formatter) {
  131. // Create a formatter and various appenders to attach to the logs.
  132. formatter = new Log.BasicFormatter();
  133. consoleAppender = new Log.ConsoleAppender(formatter);
  134. dumpAppender = new Log.DumpAppender(formatter);
  135. }
  136. allBranches.add(this._prefs._branchStr);
  137. // We create a preference observer for all our prefs so they are magically
  138. // reflected if the pref changes after creation.
  139. let setupAppender = (appender, prefName, defaultLevel, findSmallest = false) => {
  140. let observer = newVal => {
  141. let level = Log.Level[newVal] || defaultLevel;
  142. if (findSmallest) {
  143. // As some of our appenders have global impact (ie, there is only one
  144. // place 'dump' goes to), we need to find the smallest value from all
  145. // prefs controlling this appender.
  146. // For example, if consumerA has dump=Debug then consumerB sets
  147. // dump=Error, we need to keep dump=Debug so consumerA is respected.
  148. for (let branch of allBranches) {
  149. let lookPrefBranch = new Preferences(branch);
  150. let lookVal = Log.Level[lookPrefBranch.get(prefName)];
  151. if (lookVal && lookVal < level) {
  152. level = lookVal;
  153. }
  154. }
  155. }
  156. appender.level = level;
  157. }
  158. this._prefs.observe(prefName, observer, this);
  159. this._prefObservers.push([prefName, observer]);
  160. // and call the observer now with the current pref value.
  161. observer(this._prefs.get(prefName));
  162. return observer;
  163. }
  164. this._observeConsolePref = setupAppender(consoleAppender, "log.appender.console", Log.Level.Fatal, true);
  165. this._observeDumpPref = setupAppender(dumpAppender, "log.appender.dump", Log.Level.Error, true);
  166. // The file appender doesn't get the special singleton behaviour.
  167. let fapp = this._fileAppender = new FlushableStorageAppender(formatter);
  168. // the stream gets a default of Debug as the user must go out of their way
  169. // to see the stuff spewed to it.
  170. this._observeStreamPref = setupAppender(fapp, "log.appender.file.level", Log.Level.Debug);
  171. // now attach the appenders to all our logs.
  172. for (let logName of logNames) {
  173. let log = Log.repository.getLogger(logName);
  174. for (let appender of [fapp, dumpAppender, consoleAppender]) {
  175. log.addAppender(appender);
  176. }
  177. }
  178. // and use the first specified log as a "root" for our log.
  179. this._log = Log.repository.getLogger(logNames[0] + ".LogManager");
  180. },
  181. /**
  182. * Cleanup this instance
  183. */
  184. finalize() {
  185. for (let [name, pref] of this._prefObservers) {
  186. this._prefs.ignore(name, pref, this);
  187. }
  188. this._prefObservers = [];
  189. try {
  190. allBranches.delete(this._prefs._branchStr);
  191. } catch (e) {}
  192. this._prefs = null;
  193. },
  194. get _logFileSubDirectoryEntries() {
  195. // At this point we don't allow a custom directory for the logs, nor allow
  196. // it to be outside the profile directory.
  197. // This returns an array of the the relative directory entries below the
  198. // profile dir, and is the directory about:sync-log uses.
  199. return ["weave", "logs"];
  200. },
  201. get sawError() {
  202. return this._fileAppender.sawError;
  203. },
  204. // Result values for resetFileLog.
  205. SUCCESS_LOG_WRITTEN: "success-log-written",
  206. ERROR_LOG_WRITTEN: "error-log-written",
  207. /**
  208. * Possibly generate a log file for all accumulated log messages and refresh
  209. * the input & output streams.
  210. * Whether a "success" or "error" log is written is determined based on
  211. * whether an "Error" log entry was written to any of the logs.
  212. * Returns a promise that resolves on completion with either null (for no
  213. * file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was
  214. * written, or ERROR_LOG_WRITTEN if an "error" log was written.
  215. */
  216. resetFileLog: Task.async(function* () {
  217. try {
  218. let flushToFile;
  219. let reasonPrefix;
  220. let reason;
  221. if (this._fileAppender.sawError) {
  222. reason = this.ERROR_LOG_WRITTEN;
  223. flushToFile = this._prefs.get("log.appender.file.logOnError", true);
  224. reasonPrefix = "error";
  225. } else {
  226. reason = this.SUCCESS_LOG_WRITTEN;
  227. flushToFile = this._prefs.get("log.appender.file.logOnSuccess", false);
  228. reasonPrefix = "success";
  229. }
  230. // might as well avoid creating an input stream if we aren't going to use it.
  231. if (!flushToFile) {
  232. this._fileAppender.reset();
  233. return null;
  234. }
  235. // We have reasonPrefix at the start of the filename so all "error"
  236. // logs are grouped in about:sync-log.
  237. let filename = reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt";
  238. yield this._fileAppender.flushToFile(this._logFileSubDirectoryEntries, filename, this._log);
  239. // It's not completely clear to markh why we only do log cleanups
  240. // for errors, but for now the Sync semantics have been copied...
  241. // (one theory is that only cleaning up on error makes it less
  242. // likely old error logs would be removed, but that's not true if
  243. // there are occasional errors - let's address this later!)
  244. if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) {
  245. this._log.trace("Scheduling cleanup.");
  246. // Note we don't return/yield or otherwise wait on this promise - it
  247. // continues in the background
  248. this.cleanupLogs().catch(err => {
  249. this._log.error("Failed to cleanup logs", err);
  250. });
  251. }
  252. return reason;
  253. } catch (ex) {
  254. this._log.error("Failed to resetFileLog", ex);
  255. return null;
  256. }
  257. }),
  258. /**
  259. * Finds all logs older than maxErrorAge and deletes them using async I/O.
  260. */
  261. cleanupLogs: Task.async(function* () {
  262. this._cleaningUpFileLogs = true;
  263. let logDir = FileUtils.getDir("ProfD", this._logFileSubDirectoryEntries);
  264. let iterator = new OS.File.DirectoryIterator(logDir.path);
  265. let maxAge = this._prefs.get("log.appender.file.maxErrorAge", DEFAULT_MAX_ERROR_AGE);
  266. let threshold = Date.now() - 1000 * maxAge;
  267. this._log.debug("Log cleanup threshold time: " + threshold);
  268. yield iterator.forEach(Task.async(function* (entry) {
  269. // Note that we don't check this.logFilePrefix is in the name - we cleanup
  270. // all files in this directory regardless of that prefix so old logfiles
  271. // for prefixes no longer in use are still cleaned up. See bug 1279145.
  272. if (!entry.name.startsWith("error-") &&
  273. !entry.name.startsWith("success-")) {
  274. return;
  275. }
  276. try {
  277. // need to call .stat() as the enumerator doesn't give that to us on *nix.
  278. let info = yield OS.File.stat(entry.path);
  279. if (info.lastModificationDate.getTime() >= threshold) {
  280. return;
  281. }
  282. this._log.trace(" > Cleanup removing " + entry.name +
  283. " (" + info.lastModificationDate.getTime() + ")");
  284. yield OS.File.remove(entry.path);
  285. this._log.trace("Deleted " + entry.name);
  286. } catch (ex) {
  287. this._log.debug("Encountered error trying to clean up old log file "
  288. + entry.name, ex);
  289. }
  290. }.bind(this)));
  291. iterator.close();
  292. this._cleaningUpFileLogs = false;
  293. this._log.debug("Done deleting files.");
  294. // This notification is used only for tests.
  295. Services.obs.notifyObservers(null, "services-tests:common:log-manager:cleanup-logs", null);
  296. }),
  297. }