feat(log): extract reporting and log API failures

Summary: We now log API failures

Test Plan: manual

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2216
This commit is contained in:
Evan Morikawa 2015-11-06 12:20:01 -08:00
parent bfb16bbe1a
commit 455b4563a9
13 changed files with 93 additions and 200 deletions

1
.gitignore vendored
View file

@ -21,7 +21,6 @@ yoursway-create-dmg
/_site
/.sass-cache
!spec/fixtures/packages/package-with-incompatible-native-module/node_modules
#emacs

View file

@ -69,8 +69,7 @@
{ label: 'Run Plugin Specs...', command: 'application:run-package-specs' }
{ label: 'Run N1 Specs', command: 'application:run-all-specs' }
{ type: 'separator' }
{ label: 'Open Detailed Logs', command: 'window:open-errorreporter-logs' }
{ label: 'Ship Detailed Logs to Nylas', command: 'application:ship-logs' }
{ label: 'Open Detailed Logs', command: 'window:open-errorlogger-logs' }
]
}
{

View file

@ -49,8 +49,7 @@
{ label: 'Run Plugin &Specs...', command: 'application:run-package-specs' }
{ label: 'Run &N1 Specs', command: 'application:run-all-specs' }
{ type: 'separator' }
{ label: 'Open Detailed Logs', command: 'window:open-errorreporter-logs' }
{ label: 'Ship Detailed Logs to Nylas', command: 'application:ship-logs' }
{ label: 'Open Detailed Logs', command: 'window:open-errorlogger-logs' }
]
}
{

View file

@ -38,8 +38,7 @@
{ label: 'Run Plugin &Specs...', command: 'application:run-package-specs' }
{ label: 'Run &N1 Specs', command: 'application:run-all-specs' }
{ type: 'separator' }
{ label: 'Open Detailed Logs', command: 'window:open-errorreporter-logs' }
{ label: 'Ship Detailed Logs to Nylas', command: 'application:ship-logs' }
{ label: 'Open Detailed Logs', command: 'window:open-errorlogger-logs' }
]
}
{

View file

@ -144,7 +144,7 @@ class Atom extends Model
unless @inDevMode() or @inSpecMode()
require('grim').deprecate = ->
@setupErrorHandling()
@setupErrorLogger()
@unsubscribe()
@ -222,11 +222,12 @@ class Atom extends Model
# Start our error reporting to the backend and attach error handlers
# to the window and the Bluebird Promise library, converting things
# back through the sourcemap as necessary.
setupErrorHandling: ->
ErrorReporter = require './error-reporter'
@errorReporter = new ErrorReporter
setupErrorLogger: ->
ErrorLogger = require './error-logger'
@errorLogger = new ErrorLogger
inSpecMode: @inSpecMode()
inDevMode: @inDevMode()
resourcePath: @getLoadSettings().resourcePath
sourceMapCache = {}

View file

@ -220,14 +220,6 @@ class Application
resourcePath: @resourcePath
safeMode: @windowManager.focusedWindow()?.safeMode
@on 'application:ship-logs', ->
global.errorReporter.shipLogs("User triggered.")
dialog.showMessageBox
type: 'warning'
buttons: ['OK']
message: 'Your local N1 logs have been sent to LogStash.'
title: 'Logs Shipped'
@on 'application:run-package-specs', ->
dialog.showOpenDialog {
title: 'Choose a Package Directory'

View file

@ -1,14 +1,14 @@
global.shellStartTime = Date.now()
app = require 'app'
fs = require 'fs'
fs = require 'fs-plus'
path = require 'path'
optimist = require 'optimist'
start = ->
args = parseCommandLine()
global.errorReporter = setupErrorReporter(args)
global.errorLogger = setupErrorLogger(args)
setupCoffeeScript()
@ -59,9 +59,12 @@ global.devResourcePath = process.env.N1_PATH ? process.cwd()
# Normalize to make sure drive letter case is consistent on Windows
global.devResourcePath = path.normalize(global.devResourcePath) if global.devResourcePath
setupErrorReporter = (args={}) ->
ErrorReporter = require '../error-reporter'
return new ErrorReporter({inSpecMode: args.test, inDevMode: args.devMode})
setupErrorLogger = (args={}) ->
ErrorLogger = require '../error-logger'
return new ErrorLogger
inSpecMode: args.test
inDevMode: args.devMode
resourcePath: args.resourcePath
setupCrashReporter = ->
# In the future, we may want to collect actual native crash reports,

View file

@ -1,12 +1,12 @@
// Generated by CoffeeScript 1.9.2
(function() {
var app, fs, optimist, parseCommandLine, path, ref, setupCoffeeScript, setupCrashReporter, setupErrorReporter, start;
var app, fs, optimist, parseCommandLine, path, ref, setupCoffeeScript, setupCrashReporter, setupErrorLogger, start;
global.shellStartTime = Date.now();
app = require('app');
fs = require('fs');
fs = require('fs-plus');
path = require('path');
@ -15,7 +15,7 @@
start = function() {
var SquirrelUpdate, addPathToOpen, addUrlToOpen, args, squirrelCommand;
args = parseCommandLine();
global.errorReporter = setupErrorReporter(args);
global.errorLogger = setupErrorLogger(args);
setupCoffeeScript();
if (process.platform === 'win32') {
SquirrelUpdate = require('./squirrel-update');
@ -69,15 +69,16 @@
global.devResourcePath = path.normalize(global.devResourcePath);
}
setupErrorReporter = function(args) {
var ErrorReporter;
setupErrorLogger = function(args) {
var ErrorLogger;
if (args == null) {
args = {};
}
ErrorReporter = require('../error-reporter');
return new ErrorReporter({
ErrorLogger = require('../error-logger');
return new ErrorLogger({
inSpecMode: args.test,
inDevMode: args.devMode
inDevMode: args.devMode,
resourcePath: args.resourcePath
});
};
@ -103,7 +104,7 @@
var args, devMode, executedFrom, logFile, newWindow, options, packageDirectoryPath, packageManifest, packageManifestPath, pathsToOpen, pidToKillWhenClosed, resourcePath, safeMode, specDirectory, specFilePattern, specsOnCommandLine, test, version;
version = app.getVersion();
options = optimist(process.argv.slice(1));
options.usage("Atom Editor v" + version + "\n\nUsage: atom [options] [path ...]\n\nOne or more paths to files or folders to open may be specified.\n\nFile paths will open in the current window.\n\nFolder paths will open in an existing window if that folder has already been\nopened or a new window if it hasn't.\n\nEnvironment Variables:\nN1_PATH The path from which Atom loads source code in dev mode.\n Defaults to `cwd`.");
options.usage("Atom Editor v" + version + "\n\nUsage: atom [options] [path ...]\n\nOne or more paths to files or folders to open may be specified.\n\nFile paths will open in the current window.\n\nFolder paths will open in an existing window if that folder has already been\nopened or a new window if it hasn't.\n\nEnvironment Variables:\nN1_PATH The path from which Atom loads source code in dev mode.\n Defaults to `cwd`.");
options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.');
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.');
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.');

View file

@ -1,8 +1,7 @@
// This file cannot be Coffeescript because it loads before the Coffeescript
// interpreter. Note that it runs in both browser and renderer processes.
var ErrorReporter, raven, _, fs, path, app, os, remote;
raven = require('raven');
var ErrorLogger, _, fs, path, app, os, remote;
os = require('os');
_ = require('underscore');
fs = require('fs-plus');
@ -38,16 +37,18 @@ Object.defineProperty(Error.prototype, 'toJSON', {
configurable: true
});
module.exports = ErrorReporter = (function() {
module.exports = ErrorLogger = (function() {
function ErrorReporter(modes) {
function ErrorLogger(args) {
var self = this;
this.inSpecMode = modes.inSpecMode
this.inDevMode = modes.inDevMode
this.inSpecMode = args.inSpecMode
this.inDevMode = args.inDevMode
this.resourcePath = args.resourcePath
this.extensions = this.setupErrorLoggerExtensions(args)
if (!this.inSpecMode) {
this._setupSentry();
this._cleanOldLogFiles();
this._setupNewLogFile();
this._hookProcessOutputs();
@ -57,20 +58,37 @@ module.exports = ErrorReporter = (function() {
console.debug = _.bind(this.consoleDebug, this);
}
ErrorReporter.prototype._setupSentry = function() {
// Initialize the Sentry connector
this.client = new raven.Client('https://7a32cb0189ff4595a55c98ffb7939c46:f791c3c402b343068bed056b8b504dd5@sentry.nylas.com/4');
this.client.on('error', function(e) {
console.log(e.reason);
console.log(e.statusCode);
return console.log(e.response);
});
}
ErrorLogger.prototype.setupErrorLoggerExtensions = function(args) {
var extension, extensionConstructor, extensionPath, extensions, extensionsPath, i, len, ref;
if (args == null) {
args = {};
}
extensions = [];
extensionsPath = path.join(args.resourcePath, 'src', 'error-logger-extensions');
ref = fs.listSync(extensionsPath);
for (i = 0, len = ref.length; i < len; i++) {
extensionPath = ref[i];
if (path.basename(extensionPath)[0] === '.') {
continue;
}
extensionConstructor = require(extensionPath);
if (!(typeof extensionConstructor === "function")) {
throw new Error("Logger Extensions must return an extension constructor");
}
extension = new extensionConstructor({
inSpecMode: args.inSpecMode,
inDevMode: args.inDevMode,
resourcePath: args.resourcePath
});
extensions.push(extension);
}
return extensions;
};
// If we're the browser process, remove log files that are more than
// two days old. These log files get pretty big because we're logging
// so verbosely.
ErrorReporter.prototype._cleanOldLogFiles = function() {
ErrorLogger.prototype._cleanOldLogFiles = function() {
if (process.type === 'browser') {
fs.readdir(tmpPath, function(err, files) {
if (err) {
@ -95,10 +113,7 @@ module.exports = ErrorReporter = (function() {
}
}
ErrorReporter.prototype._setupNewLogFile = function() {
this.shipLogsQueued = false;
this.shipLogsTime = 0;
ErrorLogger.prototype._setupNewLogFile = function() {
// Open a file write stream to log output from this process
console.log("Streaming log data to "+logpath);
@ -111,7 +126,7 @@ module.exports = ErrorReporter = (function() {
});
}
ErrorReporter.prototype._hookProcessOutputs = function() {
ErrorLogger.prototype._hookProcessOutputs = function() {
var self = this;
// Override stdout and stderr to pipe their output to the file
// in addition to calling through to the existing implementation
@ -138,15 +153,30 @@ module.exports = ErrorReporter = (function() {
});
}
ErrorReporter.prototype._catchUncaughtErrors = function() {
ErrorLogger.prototype.notifyExtensions = function() {
var command, args;
command = arguments[0]
args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];
for (var i=0; i < this.extensions.length; i++) {
extension = this.extensions[i]
extension[command].apply(this, args);
}
}
ErrorLogger.prototype.apiDebug = function(error) {
this.appendLog(error, error.statusCode, error.message);
this.notifyExtensions("onDidLogAPIError", error);
}
ErrorLogger.prototype._catchUncaughtErrors = function() {
var self = this;
// Link to the appropriate error handlers for the browser
// or renderer process
if (process.type === 'renderer') {
atom.onDidThrowError(function(_arg) {
return self.reportError(_arg.originalError, {
'message': _arg.message
});
if (self.inSpecMode || self.inDevMode) { return };
self.appendLog(_arg.originalError, _arg.message);
self.notifyExtensions("onDidThrowError", _arg.originalError, _arg.message)
});
} else if (process.type === 'browser') {
@ -157,7 +187,7 @@ module.exports = ErrorReporter = (function() {
if (error == null) {
error = {};
}
self.reportError(error);
self.notifyExtensions("onDidThrowError", error)
if (error.message != null) {
nslog(error.message);
}
@ -172,7 +202,7 @@ module.exports = ErrorReporter = (function() {
// or `false`, don't print in console as the first parameter.
// This makes it easy for developers to turn on and off
// "verbose console" mode.
ErrorReporter.prototype.consoleDebug = function() {
ErrorLogger.prototype.consoleDebug = function() {
var args = [];
var showIt = arguments[0];
for (var ii = 1; ii < arguments.length; ii++) {
@ -184,7 +214,7 @@ module.exports = ErrorReporter = (function() {
this.appendLog.apply(this, [args]);
}
ErrorReporter.prototype.appendLog = function(obj) {
ErrorLogger.prototype.appendLog = function(obj) {
if (this.inSpecMode) { return };
try {
@ -196,94 +226,19 @@ module.exports = ErrorReporter = (function() {
this.logstream.write(message, 'utf8', function (err) {
if (err) {
console.error("ErrorReporter: Unable to write to the log stream!" + err.toString());
console.error("ErrorLogger: Unable to write to the log stream!" + err.toString());
}
});
} catch (err) {
console.error("ErrorReporter: Unable to write to the log stream." + err.toString());
console.error("ErrorLogger: Unable to write to the log stream." + err.toString());
}
};
ErrorReporter.prototype.openLogs = function() {
ErrorLogger.prototype.openLogs = function() {
var shell = require('shell');
shell.openItem(logpath);
};
ErrorReporter.prototype.shipLogs = function(reason) {
if (this.inSpecMode) { return };
if (!this.shipLogsQueued) {
var timeSinceLogShip = Date.now() - this.shipLogsTime;
if (timeSinceLogShip > 20000) {
this.runShipLogsTask(reason);
} else {
this.shipLogsQueued = true;
var self = this;
setTimeout(function() {
self.runShipLogsTask(reason);
self.shipLogsQueued = false;
}, 20000 - timeSinceLogShip);
}
}
};
ErrorReporter.prototype.runShipLogsTask = function(reason) {
if (this.inSpecMode) { return };
var self = this;
this.shipLogsTime = Date.now();
if (!reason) {
reason = "";
}
var logPattern = null;
if (process.type === 'renderer') {
logPattern = "Nylas-N1-"+remote.process.pid+"[.0-9]*.log$";
} else {
logPattern = "Nylas-N1-"+process.pid+"[.0-9]*.log$";
}
console.log("ErrorReporter: Shipping Logs. " + reason);
Task = require('./task');
ship = Task.once(fs.absolute('./tasks/ship-logs-task'), tmpPath, logPattern, function() {
self.appendLog("ErrorReporter: Shipped Logs.");
});
};
ErrorReporter.prototype.getVersion = function() {
var _ref;
return (typeof atom !== "undefined" && atom !== null ? atom.getVersion() : void 0) ||
((_ref = require('app')) != null ? _ref.getVersion() : void 0);
};
ErrorReporter.prototype.reportError = function(err, metadata) {
if (this.inSpecMode || this.inDevMode) { return };
// Never send user auth tokens
if (err.requestOptions && err.requestOptions.auth) {
delete err.requestOptions['auth'];
}
// Never send message bodies
if (err.requestOptions && err.requestOptions.body && err.requestOptions.body.body) {
delete err.requestOptions.body['body'];
}
this.client.captureError(err, {
extra: metadata,
tags: {
'platform': process.platform,
'version': this.getVersion()
}
});
this.appendLog(err, metadata);
this.shipLogs('Exception occurred');
};
return ErrorReporter;
return ErrorLogger;
})();

View file

@ -97,6 +97,7 @@ class NylasAPIRequest
response ?= {}
response.statusCode = TimeoutErrorCode
apiError = new APIError({error, response, body, requestOptions: @options})
atom.errorLogger.apiDebug(apiError)
@options.error?(apiError)
reject(apiError)
else

View file

@ -189,7 +189,7 @@ class DatabaseStore extends NylasStore
_handleSetupError: (err) =>
console.error(err)
console.log(atom.getWindowType())
atom.errorReporter.reportError(err)
atom.emitError(err)
app = require('remote').getGlobal('application')
app.rebuildDatabase()

View file

@ -1,56 +0,0 @@
fs = require 'fs'
path = require 'path'
request = require 'request'
detailedLogging = true
detailedLog = (msg) ->
console.log(msg) if detailedLogging
module.exports = (dir, regexPattern) ->
callback = @async()
console.log("Running log ship: #{dir}, #{regexPattern}")
fs.readdir dir, (err, files) ->
log("readdir error: #{err}") if err
logs = []
logFilter = new RegExp(regexPattern)
for file in files
if logFilter.test(file)
filepath = path.join(dir, file)
stats = fs.statSync(filepath)
logs.push(filepath) if stats["size"] > 0
remaining = 0
finished = ->
remaining -= 1
if remaining is 0
callback()
if logs.length is 0
detailedLog("No logs found to upload.")
callback()
return
logs.forEach (log) ->
remaining += 1
url = 'https://edgehill.nylas.com/ingest-log'
formData =
file:
value: fs.createReadStream(log, {flags: 'r'})
options:
filename: 'log.txt',
contentType: 'text/plain'
request.post {url, formData}, (err, response, body) ->
if err
detailedLog("Error uploading #{log}: #{err.toString()}")
else if response.statusCode isnt 200
detailedLog("Error uploading #{log}: status code #{response.statusCode}")
else
detailedLog("Successfully uploaded #{log}")
fs.truncate log, (err) =>
console.log(err) if err
fs.unlink log, (err) =>
console.log(err) if err
finished()

View file

@ -71,8 +71,8 @@ class WindowEventHandler
@subscribeToCommand $(window), 'window:toggle-dev-tools', ->
atom.toggleDevTools()
@subscribeToCommand $(window), 'window:open-errorreporter-logs', ->
atom.errorReporter.openLogs()
@subscribeToCommand $(window), 'window:open-errorlogger-logs', ->
atom.errorLogger.openLogs()
@subscribeToCommand $(window), 'window:toggle-component-regions', ->
ComponentRegistry = require './component-registry'