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/. */
6 import { Log } from "resource://gre/modules/Log.sys.mjs";
10 ChromeUtils.defineESModuleGetters(lazy, {
11 FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
12 NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
15 const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days
17 // "shared" logs (ie, where the same log name is used by multiple LogManager
18 // instances) are a fact of life here - eg, FirefoxAccounts logs are used by
19 // both Sync and Reading List.
20 // However, different instances have different pref branches, so we need to
21 // handle when one pref branch says "Debug" and the other says "Error"
22 // So we (a) keep singleton console and dump appenders and (b) keep track
23 // of the minimum (ie, most verbose) level and use that.
24 // This avoids (a) the most recent setter winning (as that is indeterminate)
25 // and (b) multiple dump/console appenders being added to the same log multiple
26 // times, which would cause messages to appear twice.
28 // Singletons used by each instance.
33 // A set of all preference roots used by all instances.
34 var allBranches = new Set();
36 const STREAM_SEGMENT_SIZE = 4096;
37 const PR_UINT32_MAX = 0xffffffff;
40 * Append to an nsIStorageStream
42 * This writes logging output to an in-memory stream which can later be read
43 * back as an nsIInputStream. It can be used to avoid expensive I/O operations
44 * during logging. Instead, one can periodically consume the input stream and
45 * e.g. write it to disk asynchronously.
47 class StorageStreamAppender extends Log.Appender {
48 constructor(formatter) {
50 this._name = "StorageStreamAppender";
52 this._converterStream = null; // holds the nsIConverterOutputStream
53 this._outputStream = null; // holds the underlying nsIOutputStream
59 if (!this._outputStream) {
60 // First create a raw stream. We can bail out early if that fails.
61 this._outputStream = this.newOutputStream();
62 if (!this._outputStream) {
66 // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
67 // the instance if we already have one.
68 if (!this._converterStream) {
69 this._converterStream = Cc[
70 "@mozilla.org/intl/converter-output-stream;1"
71 ].createInstance(Ci.nsIConverterOutputStream);
73 this._converterStream.init(this._outputStream, "UTF-8");
75 return this._converterStream;
79 let ss = (this._ss = Cc["@mozilla.org/storagestream;1"].createInstance(
82 ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
83 return ss.getOutputStream(0);
90 return this._ss.newInputStream(0);
94 if (!this._outputStream) {
97 this.outputStream.close();
98 this._outputStream = null;
102 doAppend(formatted) {
107 this.outputStream.writeString(formatted + "\n");
109 if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
110 // The underlying output stream is closed, so let's open a new one
112 this._outputStream = null;
115 this.outputStream.writeString(formatted + "\n");
117 // Ah well, we tried, but something seems to be hosed permanently.
123 // A storage appender that is flushable to a file on disk. Policies for
124 // when to flush, to what file, log rotation etc are up to the consumer
125 // (although it does maintain a .sawError property to help the consumer decide
126 // based on its policies)
127 class FlushableStorageAppender extends StorageStreamAppender {
128 constructor(formatter) {
130 this.sawError = false;
134 if (message.level >= Log.Level.Error) {
135 this.sawError = true;
137 StorageStreamAppender.prototype.append.call(this, message);
142 this.sawError = false;
145 // Flush the current stream to a file. Somewhat counter-intuitively, you
146 // must pass a log which will be written to with details of the operation.
147 async flushToFile(subdirArray, filename, log) {
148 let inStream = this.getInputStream();
151 log.debug("Failed to flush log to a file - no input stream");
154 log.debug("Flushing file log");
155 log.trace("Beginning stream copy to " + filename + ": " + Date.now());
157 await this._copyStreamToFile(inStream, subdirArray, filename, log);
158 log.trace("onCopyComplete", Date.now());
160 log.error("Failed to copy log stream to file", ex);
165 * Copy an input stream to the named file, doing everything off the main
167 * subDirArray is an array of path components, relative to the profile
168 * directory, where the file will be created.
169 * outputFileName is the filename to create.
170 * Returns a promise that is resolved on completion or rejected with an error.
172 async _copyStreamToFile(inputStream, subdirArray, outputFileName, log) {
173 let outputDirectory = PathUtils.join(PathUtils.profileDir, ...subdirArray);
174 await IOUtils.makeDirectory(outputDirectory);
175 let fullOutputFileName = PathUtils.join(outputDirectory, outputFileName);
177 let outputStream = Cc[
178 "@mozilla.org/network/file-output-stream;1"
179 ].createInstance(Ci.nsIFileOutputStream);
182 new lazy.FileUtils.File(fullOutputFileName),
185 Ci.nsIFileOutputStream.DEFER_OPEN
188 await new Promise(resolve =>
189 lazy.NetUtil.asyncCopy(inputStream, outputStream, () => resolve())
192 outputStream.close();
193 log.trace("finished copy to", fullOutputFileName);
197 // The public LogManager object.
198 export function LogManager(prefRoot, logNames, logFilePrefix) {
199 this._prefObservers = [];
200 this.init(prefRoot, logNames, logFilePrefix);
203 LogManager.StorageStreamAppender = StorageStreamAppender;
205 LogManager.prototype = {
206 _cleaningUpFileLogs: false,
208 init(prefRoot, logNames, logFilePrefix) {
209 this._prefs = Services.prefs.getBranch(prefRoot);
210 this._prefsBranch = prefRoot;
212 this.logFilePrefix = logFilePrefix;
214 // Create a formatter and various appenders to attach to the logs.
215 formatter = new Log.BasicFormatter();
216 consoleAppender = new Log.ConsoleAppender(formatter);
217 dumpAppender = new Log.DumpAppender(formatter);
220 allBranches.add(this._prefsBranch);
221 // We create a preference observer for all our prefs so they are magically
222 // reflected if the pref changes after creation.
223 let setupAppender = (
229 let observer = newVal => {
230 let level = Log.Level[newVal] || defaultLevel;
232 // As some of our appenders have global impact (ie, there is only one
233 // place 'dump' goes to), we need to find the smallest value from all
234 // prefs controlling this appender.
235 // For example, if consumerA has dump=Debug then consumerB sets
236 // dump=Error, we need to keep dump=Debug so consumerA is respected.
237 for (let branch of allBranches) {
238 let lookPrefBranch = Services.prefs.getBranch(branch);
239 let lookVal = Log.Level[lookPrefBranch.getCharPref(prefName, null)];
240 if (lookVal && lookVal < level) {
245 appender.level = level;
247 this._prefs.addObserver(prefName, observer);
248 this._prefObservers.push([prefName, observer]);
249 // and call the observer now with the current pref value.
250 observer(this._prefs.getCharPref(prefName, null));
254 this._observeConsolePref = setupAppender(
256 "log.appender.console",
260 this._observeDumpPref = setupAppender(
267 // The file appender doesn't get the special singleton behaviour.
268 let fapp = (this._fileAppender = new FlushableStorageAppender(formatter));
269 // the stream gets a default of Debug as the user must go out of their way
270 // to see the stuff spewed to it.
271 this._observeStreamPref = setupAppender(
273 "log.appender.file.level",
277 // now attach the appenders to all our logs.
278 for (let logName of logNames) {
279 let log = Log.repository.getLogger(logName);
280 for (let appender of [fapp, dumpAppender, consoleAppender]) {
281 log.addAppender(appender);
284 // and use the first specified log as a "root" for our log.
285 this._log = Log.repository.getLogger(logNames[0] + ".LogManager");
289 * Cleanup this instance
292 for (let [prefName, observer] of this._prefObservers) {
293 this._prefs.removeObserver(prefName, observer);
295 this._prefObservers = [];
297 allBranches.delete(this._prefsBranch);
302 get _logFileSubDirectoryEntries() {
303 // At this point we don't allow a custom directory for the logs, nor allow
304 // it to be outside the profile directory.
305 // This returns an array of the the relative directory entries below the
306 // profile dir, and is the directory about:sync-log uses.
307 return ["weave", "logs"];
311 return this._fileAppender.sawError;
314 // Result values for resetFileLog.
315 SUCCESS_LOG_WRITTEN: "success-log-written",
316 ERROR_LOG_WRITTEN: "error-log-written",
319 * Possibly generate a log file for all accumulated log messages and refresh
320 * the input & output streams.
321 * Whether a "success" or "error" log is written is determined based on
322 * whether an "Error" log entry was written to any of the logs.
323 * Returns a promise that resolves on completion with either null (for no
324 * file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was
325 * written, or ERROR_LOG_WRITTEN if an "error" log was written.
327 async resetFileLog() {
332 if (this._fileAppender.sawError) {
333 reason = this.ERROR_LOG_WRITTEN;
334 flushToFile = this._prefs.getBoolPref(
335 "log.appender.file.logOnError",
338 reasonPrefix = "error";
340 reason = this.SUCCESS_LOG_WRITTEN;
341 flushToFile = this._prefs.getBoolPref(
342 "log.appender.file.logOnSuccess",
345 reasonPrefix = "success";
348 // might as well avoid creating an input stream if we aren't going to use it.
350 this._fileAppender.reset();
354 // We have reasonPrefix at the start of the filename so all "error"
355 // logs are grouped in about:sync-log.
357 reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt";
358 await this._fileAppender.flushToFile(
359 this._logFileSubDirectoryEntries,
363 // It's not completely clear to markh why we only do log cleanups
364 // for errors, but for now the Sync semantics have been copied...
365 // (one theory is that only cleaning up on error makes it less
366 // likely old error logs would be removed, but that's not true if
367 // there are occasional errors - let's address this later!)
368 if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) {
369 this._log.trace("Running cleanup.");
371 await this.cleanupLogs();
373 this._log.error("Failed to cleanup logs", err);
378 this._log.error("Failed to resetFileLog", ex);
384 * Finds all logs older than maxErrorAge and deletes them using async I/O.
387 let maxAge = this._prefs.getIntPref(
388 "log.appender.file.maxErrorAge",
389 DEFAULT_MAX_ERROR_AGE
391 let threshold = Date.now() - 1000 * maxAge;
392 this._log.debug("Log cleanup threshold time: " + threshold);
394 let shouldDelete = fileInfo => {
395 return fileInfo.lastModified < threshold;
397 return this._deleteLogFiles(shouldDelete);
401 * Finds all logs and removes them.
404 return this._deleteLogFiles(() => true);
407 // Delete some log files. A callback is invoked for each found log file to
408 // determine if that file should be removed.
409 async _deleteLogFiles(cbShouldDelete) {
410 this._cleaningUpFileLogs = true;
411 let logDir = lazy.FileUtils.getDir(
413 this._logFileSubDirectoryEntries
415 for (const path of await IOUtils.getChildren(logDir.path)) {
416 const name = PathUtils.filename(path);
418 if (!name.startsWith("error-") && !name.startsWith("success-")) {
423 const info = await IOUtils.stat(path);
424 if (!cbShouldDelete(info)) {
428 this._log.trace(` > Cleanup removing ${name} (${info.lastModified})`);
429 await IOUtils.remove(path);
430 this._log.trace(`Deleted ${name}`);
433 `Encountered error trying to clean up old log file ${name}`,
438 this._cleaningUpFileLogs = false;
439 this._log.debug("Done deleting files.");
440 // This notification is used only for tests.
441 Services.obs.notifyObservers(
443 "services-tests:common:log-manager:cleanup-logs"