Skip to content

Instantly share code, notes, and snippets.

@aroben
Last active December 14, 2015 07:09
Show Gist options
  • Save aroben/5048415 to your computer and use it in GitHub Desktop.
Save aroben/5048415 to your computer and use it in GitHub Desktop.
How GitHub for Windows sends exceptions to Haystack
module Central
class DotNetException
def self.report(app, params={})
if $stats
if params['type'] == 'crash'
$stats.increment "#{app}.crashes"
else
$stats.increment "#{app}.exceptions"
end
end
exception_message = params.delete 'exception_message'
message = "#{params.delete 'log_message'}\n#{exception_message}".strip
params[:message] = message if message.length > 0
if params[:backtrace]
frames = params[:backtrace].scan(/^\s+\S+\s+(\S+\(.*\))/).flatten
first_stack_frame = frames.find { |frame| frame =~ /^GitHub\./ } || frames.first
end
unless first_stack_frame
if params[:class] == 'System.Net.WebException'
# WebException's don't have a meaningful backtrace, so we use the
# exception message instead.
first_stack_frame = exception_message
else
first_stack_frame = 'unknown'
end
end
rollup_context = "#{params[:class]}\n#{first_stack_frame}"
rollup = Digest::MD5.hexdigest rollup_context
Failbot.report(Exception.new, params.merge(:rollup => rollup, :rollup_context => rollup_context))
end
end
end
using System;
using System.Diagnostics;
using System.Globalization;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
using GitHub.Api;
using GitHub.Extensions;
using GitHub.IO;
using NLog;
using NLog.Targets;
using ReactiveUI;
using RestSharp;
using LogManager = NLog.LogManager;
namespace GitHub.Helpers
{
[Target("Haystack")]
public class HaystackTarget : Target
{
public static readonly string IsFatalExceptionProperty = "IsFatalException";
static readonly Logger log = LogManager.GetCurrentClassLogger();
readonly IHaystackContext context;
readonly IRestClient restClient;
Exception lastException;
bool hasSeenFatalException;
// We get our exceptions into Haystack by way of Central.
public HaystackTarget(IHaystackContext context, IProgram program, IEnvironment environment)
: this(context, CreateRestClient(program, environment))
{
}
public HaystackTarget(IHaystackContext context, IRestClient restClient)
{
Ensure.ArgumentNotNull(context, "context");
Ensure.ArgumentNotNull(restClient, "restClient");
this.context = context;
this.restClient = restClient;
}
static IRestClient CreateRestClient(IProgram program, IEnvironment environment)
{
var client = new RestClient("https://central.github.com/")
{
HttpFactory = new GitHubHttpFactory(program, environment)
};
// We get our exceptions into Haystack by way of Central.
client.AddDefaultParameter("version", (Assembly.GetEntryAssembly() ?? typeof(HaystackTarget).Assembly).GetName().Version, ParameterType.GetOrPost);
client.AddDefaultParameter("culture", CultureInfo.CurrentCulture.Name, ParameterType.GetOrPost);
client.AddDefaultParameter("os_version", Environment.OSVersion.GetOperatingSystemVersionString(), ParameterType.GetOrPost);
return client;
}
protected override void Write(LogEventInfo logEvent)
{
if (!IsFakeRestClient(restClient))
{
// If the rest client is real, we only want to log the exception if
// We're running the app for real in a production environment.
#if DEBUG
// Always return for DEBUG builds. We don't want to log our
// local builds.
return;
#else
if (RxApp.InUnitTestRunner()) return;
#endif
}
Debug.Assert(logEvent != null, "Somehow the logEvent is null");
if (logEvent.Exception == null || logEvent.Exception == lastException || hasSeenFatalException || logEvent.LoggerName == log.Name)
return;
Debug.Assert(logEvent.Level != null, "Somehow the logEvent.Level is null");
lastException = logEvent.Exception;
context.CurrentGitHubUserName
.Catch<string, Exception>(ex =>
{
log.ErrorException("Failed to get current GitHub username", ex);
return Observable.Return<string>(null);
})
.Select(userName =>
{
var request = new RestRequest("/api/windows/exception", Method.POST);
request.AddParameter("desktop_path",
context.Environment.DesktopPath,
ParameterType.GetOrPost);
request.AddParameter("desktopdirectory_path",
context.Environment.DesktopDirectoryPath,
ParameterType.GetOrPost);
request.AddParameter("applicationdata_path",
context.Environment.ApplicationDataPath,
ParameterType.GetOrPost);
request.AddParameter("localgithubapplicationdata_path",
context.Environment.LocalGitHubApplicationDataPath,
ParameterType.GetOrPost);
request.AddParameter("userprofile_path",
context.Environment.UserProfilePath,
ParameterType.GetOrPost);
request.AddParameter("class", logEvent.Exception.InnermostException().GetType().FullName, ParameterType.GetOrPost);
request.AddParameter("logger_name", logEvent.LoggerName, ParameterType.GetOrPost);
request.AddParameter("log_level", logEvent.Level.Name, ParameterType.GetOrPost);
request.AddParameter("log_message", logEvent.FormattedMessage, ParameterType.GetOrPost);
request.AddParameter("exception_message", logEvent.Exception.InnermostException().VerboseMessage(), ParameterType.GetOrPost);
request.AddParameter("backtrace", logEvent.Exception.FullStackTrace(), ParameterType.GetOrPost);
try
{
var repoNameWithOwner = context.SelectedRepositoryNameWithOwner;
if (repoNameWithOwner != null)
request.AddParameter("repo", repoNameWithOwner, ParameterType.GetOrPost);
}
catch (Exception ex)
{
log.ErrorException("Failed to get current GitHub repository", ex);
// Oh well. We'll just send the report without a repo name.
}
string enterpriseHost = null;
try
{
enterpriseHost = context.EnterpriseHost;
request.AddParameter("enterprise_host", enterpriseHost);
}
catch (Exception ex)
{
log.ErrorException("Failed to determine enterprise host", ex);
// Oh well. We'll just send the report without that info.
}
if (!userName.IsNullOrEmpty())
{
if (!enterpriseHost.IsNullOrEmpty())
userName = string.Format(CultureInfo.InvariantCulture, "{0}@{1}", userName, enterpriseHost);
request.AddParameter("user", userName, ParameterType.GetOrPost);
}
if (logEvent.Properties.ContainsKey(IsFatalExceptionProperty))
{
hasSeenFatalException = true;
request.AddParameter("type", "crash", ParameterType.GetOrPost);
request.Timeout = 2000;
restClient.Execute(request);
}
else
{
restClient.ExecuteAsync(request, response => {});
}
return Observable.Return(Unit.Default);
})
.Subscribe(_ => {}, ex =>
{
try
{
var message = string.Format(CultureInfo.InvariantCulture, "Failed to send exception to Haystack. Log event and exception we were trying to send:{0}{1}{0}{2}{0}{0}Exception that prevented sending it:", Environment.NewLine, logEvent, logEvent.Exception);
log.ErrorException(message, ex);
}
catch (Exception)
{
// Now there's *really* nothing we can do.
}
});
}
static bool IsFakeRestClient(IRestClient restClient)
{
return restClient.GetType().Name.Contains("Proxy", StringComparison.OrdinalIgnoreCase);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment