feat(GODT-2446): Attach logs to sentry reports for relevant bridge-gui exceptions.

Cherry picked from release/perth_narrows (2aa4e7c)

# Conflicts:
#	internal/frontend/bridge-gui/bridge-gui/AppController.cpp
#	internal/frontend/bridge-gui/bridge-gui/AppController.h
#	internal/frontend/bridge-gui/bridge-gui/LogUtils.cpp
#	internal/frontend/bridge-gui/bridge-gui/LogUtils.h
#	internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
#	internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp
#	internal/frontend/bridge-gui/bridge-gui/SentryUtils.h
#	internal/frontend/bridge-gui/bridge-gui/main.cpp
This commit is contained in:
Xavier Michelon
2023-03-07 10:02:09 +01:00
parent 21ffde316d
commit febc994cec
12 changed files with 149 additions and 38 deletions

View File

@ -90,20 +90,14 @@ Settings &AppController::settings() {
//****************************************************************************************************************************************************
/// \param[in] function The function that caught the exception.
/// \param[in] message The error message.
/// \param[in] details The details for the error.
/// \param[in] exception The exception that triggered the fatal error.
//****************************************************************************************************************************************************
void AppController::onFatalError(QString const &function, QString const &message, QString const &details) {
QString fullMessage = QString("%1(): %2").arg(function, message);
if (!details.isEmpty()) {
fullMessage += "\n\nDetails:\n" + details;
}
sentry_uuid_s const uuid = reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception",
fullMessage.toLocal8Bit());
QMessageBox::critical(nullptr, tr("Error"), message);
void AppController::onFatalError(Exception const &exception) {
sentry_uuid_t uuid = reportSentryException("AppController got notified of a fatal error", exception);
QMessageBox::critical(nullptr, tr("Error"), exception.what());
restart(true);
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), fullMessage));
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), exception.detailedWhat()));
qApp->exit(EXIT_FAILURE);
}

View File

@ -28,6 +28,7 @@ class Log;
class Overseer;
class GRPCClient;
class ProcessMonitor;
class Exception;
}
//@formatter:on
@ -54,7 +55,7 @@ public: // member functions.
void setLauncherArgs(const QString &launcher, const QStringList &args);
public slots:
void onFatalError(QString const &function, QString const &message, QString const &details); ///< Handle fatal errors.
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
private: // member functions
AppController(); ///< Default constructor.

View File

@ -52,7 +52,7 @@ void EventStreamReader::run() {
emit finished();
}
catch (Exception const &e) {
reportSentryException(SENTRY_LEVEL_ERROR, "Error during event stream read", "Exception", e.what());
reportSentryException("Error during event stream read", e);
emit error(e.qwhat());
}
}

View File

@ -24,6 +24,11 @@
using namespace bridgepp;
namespace {
qsizetype const logFileTailMaxLength = 25 * 1024; ///< The maximum length of the portion of log returned by tailOfLatestBridgeLog()
}
//****************************************************************************************************************************************************
/// \return user logs directory used by bridge.
//****************************************************************************************************************************************************
@ -64,3 +69,36 @@ Log &initLog() {
return log;
}
//****************************************************************************************************************************************************
/// \brief Return the path of the latest bridge log.
/// \return The path of the latest bridge log file.
/// \return An empty string if no bridge log file was found.
//****************************************************************************************************************************************************
QString latestBridgeLogPath() {
QDir const logsDir(userLogsDir());
if (logsDir.isEmpty()) {
return QString();
}
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
return lhs.birthTime() < rhs.birthTime();
});
return files.back().absoluteFilePath();
}
//****************************************************************************************************************************************************
/// Return the maxSize last bytes of the latest bridge log.
//****************************************************************************************************************************************************
QByteArray tailOfLatestBridgeLog() {
QString path = latestBridgeLogPath();
if (path.isEmpty()) {
return QByteArray();
}
QFile file(path);
return file.open(QIODevice::Text | QIODevice::ReadOnly) ? file.readAll().right(logFileTailMaxLength) : QByteArray();
}

View File

@ -24,6 +24,7 @@
bridgepp::Log &initLog(); ///< Initialize the application log.
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
#endif //BRIDGE_GUI_LOG_UTILS_H

View File

@ -19,15 +19,16 @@
#include "QMLBackend.h"
#include "BuildConfig.h"
#include "EventStreamWorker.h"
#include "LogUtils.h"
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Worker/Overseer.h>
#define HANDLE_EXCEPTION(x) try { x } \
catch (Exception const &e) { emit fatalError(__func__, e.qwhat(), e.details()); } \
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred"), QString()); }
catch (Exception const &e) { emit fatalError(e); } \
catch (...) { emit fatalError(Exception("An unknown exception occurred", QString(), __func__)); }
#define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false;
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
@ -596,7 +597,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
HANDLE_EXCEPTION(
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
"This error exists for test purposes and should be ignored.");
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
}
app().grpc().login(username, password);
)

View File

@ -237,7 +237,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
void fatalError(QString const &function, QString const &message, QString const &details) const; ///< Signal emitted when an fatal error occurs.
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
private: // member functions
void retrieveUserList(); ///< Retrieve the list of users via gRPC.

View File

@ -18,6 +18,7 @@
#include "SentryUtils.h"
#include "BuildConfig.h"
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
using namespace bridgepp;
@ -26,6 +27,23 @@ using namespace bridgepp;
static constexpr const char *LoggerName = "bridge-gui";
//****************************************************************************************************************************************************
/// \return The temporary file used for sentry attachment.
//****************************************************************************************************************************************************
QString sentryAttachmentFilePath() {
static QString path;
if (!path.isEmpty()) {
return path;
}
while (true) {
path = QDir::temp().absoluteFilePath(QUuid::createUuid().toString(QUuid::WithoutBraces) + ".txt"); // Sentry does not offer preview for .log files.
if (!QFileInfo::exists(path)) {
return path;
}
}
}
//****************************************************************************************************************************************************
/// \brief Get a hash of the computer's host name
//****************************************************************************************************************************************************
@ -96,6 +114,8 @@ sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir)
sentry_options_set_release(sentryOptions, appVersion(PROJECT_VER).toUtf8());
sentry_options_set_max_breadcrumbs(sentryOptions, 50);
sentry_options_set_environment(sentryOptions, PROJECT_BUILD_ENV);
QByteArray const array = sentryAttachmentFilePath().toLocal8Bit();
sentry_options_add_attachment(sentryOptions, array.constData());
// Enable this for debugging sentry.
// sentry_options_set_debug(sentryOptions, 1);
@ -132,3 +152,30 @@ sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, c
}
//****************************************************************************************************************************************************
/// \param[in] message The message for the exception.
/// \param[in] function The name of the function that triggered the exception.
/// \param[in] exception The exception.
/// \return The Sentry exception UUID.
//****************************************************************************************************************************************************
sentry_uuid_t reportSentryException(QString const &message, bridgepp::Exception const exception) {
QByteArray const attachment = exception.attachment();
QFile file(sentryAttachmentFilePath());
bool const hasAttachment = !attachment.isEmpty();
if (hasAttachment) {
if (file.open(QIODevice::Text | QIODevice::WriteOnly)) {
file.write(attachment);
file.close();
}
}
sentry_uuid_t const uuid = reportSentryException(SENTRY_LEVEL_ERROR, message.toLocal8Bit(), "Exception",
exception.detailedWhat().toLocal8Bit());
if (hasAttachment) {
file.remove();
}
return uuid;
}

View File

@ -25,6 +25,7 @@ void initSentry();
void setSentryReportScope();
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);
sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception);
sentry_uuid_t reportSentryException(QString const& message, bridgepp::Exception const exception);
#endif //BRIDGE_GUI_SENTRYUTILS_H

View File

@ -219,8 +219,8 @@ void focusOtherInstance() {
}
catch (Exception const &e) {
app().log().error(e.qwhat());
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during focusOtherInstance()", "Exception", e.what());
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat()));
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(e.qwhat()));
}
}
@ -308,7 +308,8 @@ int main(int argc, char *argv[]) {
if (!cliOptions.attach) {
if (isBridgeRunning()) {
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.");
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
QString(), QString(), tailOfLatestBridgeLog());
}
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
@ -337,6 +338,7 @@ int main(int argc, char *argv[]) {
QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi");
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
QQmlApplicationEngine engine;
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
@ -398,17 +400,9 @@ int main(int argc, char *argv[]) {
return result;
}
catch (Exception const &e) {
QString fullMessage = e.qwhat();
bool const hasDetails = !e.details().isEmpty();
if (hasDetails)
fullMessage += "\n\nDetails:\n" + e.details();
sentry_uuid_s const uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during main", "Exception", fullMessage.toLocal8Bit());
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QMessageBox::critical(nullptr, "Error", e.qwhat());
QTextStream errStream(stderr);
errStream << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.qwhat() << "\n";
if (hasDetails)
errStream << "\nDetails:\n" << e.details() << "\n";
closeBridgeApp();
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE;
}
}

View File

@ -25,12 +25,15 @@ namespace bridgepp {
//****************************************************************************************************************************************************
/// \param[in] what A description of the exception.
/// \param[in] details The optional details for the exception.
/// \param[in] function The name of the calling function.
//****************************************************************************************************************************************************
Exception::Exception(QString qwhat, QString details) noexcept
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment) noexcept
: std::exception()
, qwhat_(std::move(qwhat))
, what_(qwhat_.toLocal8Bit())
, details_(std::move(details)) {
, details_(std::move(details))
, function_(std::move(function))
, attachment_(std::move(attachment)) {
}
@ -41,7 +44,9 @@ Exception::Exception(Exception const &ref) noexcept
: std::exception(ref)
, qwhat_(ref.qwhat_)
, what_(ref.what_)
, details_(ref.details_) {
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_) {
}
@ -52,7 +57,9 @@ Exception::Exception(Exception &&ref) noexcept
: std::exception(ref)
, qwhat_(ref.qwhat_)
, what_(ref.what_)
, details_(ref.details_) {
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_) {
}
@ -80,4 +87,26 @@ QString Exception::details() const noexcept {
}
//****************************************************************************************************************************************************
/// \return The attachment for the exception.
//****************************************************************************************************************************************************
QByteArray Exception::attachment() const noexcept {
return attachment_;
}
//****************************************************************************************************************************************************
/// \return The details exception.
//****************************************************************************************************************************************************
QString Exception::detailedWhat() const {
QString result = qwhat_;
if (!function_.isEmpty()) {
result = QString("%1(): %2").arg(function_, result);
}
if (!details_.isEmpty()) {
result += "\n\nDetails:\n" + details_;
}
return result;
}
} // namespace bridgepp

View File

@ -31,7 +31,8 @@ namespace bridgepp {
//****************************************************************************************************************************************************
class Exception : public std::exception {
public: // member functions
explicit Exception(QString qwhat = QString(), QString details = QString()) noexcept; ///< Constructor
explicit Exception(QString qwhat = QString(), QString details = QString(), QString function = QString(),
QByteArray attachment = QByteArray()) noexcept; ///< Constructor
Exception(Exception const &ref) noexcept; ///< copy constructor
Exception(Exception &&ref) noexcept; ///< copy constructor
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
@ -40,11 +41,15 @@ public: // member functions
QString qwhat() const noexcept; ///< Return the description of the exception as a QString
const char *what() const noexcept override; ///< Return the description of the exception as C style string
QString details() const noexcept; ///< Return the details for the exception
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
private: // data members
QString const qwhat_; ///< The description of the exception.
QByteArray const what_; ///< The c-string version of the qwhat message. Stored as a QByteArray for automatic lifetime management.
QString const details_; ///< The optional details for the exception.
QString const function_; ///< The name of the function that created the exception.
QByteArray const attachment_; ///< The attachment to add to the exception.
};