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

This commit is contained in:
Xavier Michelon
2023-03-06 13:02:57 +01:00
parent 227bbf1c03
commit 2aa4e7c9da
13 changed files with 211 additions and 45 deletions

View File

@ -32,6 +32,7 @@ namespace {
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag. QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The AppController instance. /// \return The AppController instance.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -71,36 +72,34 @@ ProcessMonitor *AppController::bridgeMonitor() const {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] function The function that caught the exception. /// \param[in] exception The exception that triggered the fatal error.
/// \param[in] message The error message.
/// \param[in] details The details for the error.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void AppController::onFatalError(QString const &function, QString const &message, QString const& details) { void AppController::onFatalError(Exception const &exception) {
QString fullMessage = QString("%1(): %2").arg(function, message); sentry_uuid_t uuid = reportSentryException("AppController got notified of a fatal error", exception);
if (!details.isEmpty())
fullMessage += "\n\nDetails:\n" + details; QMessageBox::critical(nullptr, tr("Error"), exception.what());
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);
restart(true); 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); qApp->exit(EXIT_FAILURE);
} }
void AppController::restart(bool isCrashing) { void AppController::restart(bool isCrashing) {
if (!launcher_.isEmpty()) { if (!launcher_.isEmpty()) {
QProcess p; QProcess p;
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_,launcherArgs_.join(" "))); log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
QStringList args = launcherArgs_; QStringList args = launcherArgs_;
if (isCrashing) if (isCrashing) {
args.append(noWindowFlag); args.append(noWindowFlag);
}
p.startDetached(launcher_, args); p.startDetached(launcher_, args);
p.waitForStarted(); p.waitForStarted();
} }
} }
void AppController::setLauncherArgs(const QString& launcher, const QStringList& args){
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
launcher_ = launcher; launcher_ = launcher;
launcherArgs_ = args; launcherArgs_ = args;
} }

View File

@ -20,21 +20,16 @@
#define BRIDGE_GUI_APP_CONTROLLER_H #define BRIDGE_GUI_APP_CONTROLLER_H
// @formatter:off
class QMLBackend; class QMLBackend;
namespace bridgepp { namespace bridgepp {
class Log; class Log;
class Overseer; class Overseer;
class GRPCClient; class GRPCClient;
class ProcessMonitor; class ProcessMonitor;
class Exception;
} }
// @formatter:off
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -58,7 +53,7 @@ public: // member functions.
void setLauncherArgs(const QString& launcher, const QStringList& args); void setLauncherArgs(const QString& launcher, const QStringList& args);
public slots: 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 private: // member functions
AppController(); ///< Default constructor. AppController(); ///< Default constructor.

View File

@ -112,6 +112,7 @@ add_executable(bridge-gui
BridgeApp.cpp BridgeApp.h BridgeApp.cpp BridgeApp.h
CommandLine.cpp CommandLine.h CommandLine.cpp CommandLine.h
EventStreamWorker.cpp EventStreamWorker.h EventStreamWorker.cpp EventStreamWorker.h
LogUtils.cpp LogUtils.h
main.cpp main.cpp
Pch.h Pch.h
BuildConfig.h BuildConfig.h

View File

@ -52,7 +52,7 @@ void EventStreamReader::run() {
emit finished(); emit finished();
} }
catch (Exception const &e) { 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()); emit error(e.qwhat());
} }
} }

View File

@ -0,0 +1,62 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "LogUtils.h"
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
namespace {
qsizetype const logFileTailMaxLength = 25 * 1024; ///< The maximum length of the portion of log returned by tailOfLatestBridgeLog()
}
//****************************************************************************************************************************************************
/// \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

@ -0,0 +1,26 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_LOG_UTILS_H
#define BRIDGE_GUI_LOG_UTILS_H
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
#endif //BRIDGE_GUI_LOG_UTILS_H

View File

@ -19,14 +19,15 @@
#include "QMLBackend.h" #include "QMLBackend.h"
#include "EventStreamWorker.h" #include "EventStreamWorker.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "LogUtils.h"
#include <bridgepp/GRPC/GRPCClient.h> #include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Exception/Exception.h> #include <bridgepp/Exception/Exception.h>
#include <bridgepp/Worker/Overseer.h> #include <bridgepp/Worker/Overseer.h>
#define HANDLE_EXCEPTION(x) try { x } \ #define HANDLE_EXCEPTION(x) try { x } \
catch (Exception const &e) { emit fatalError(__func__, e.qwhat(), e.details()); } \ catch (Exception const &e) { emit fatalError(e); } \
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred"), QString()); } 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_BOOL(x) HANDLE_EXCEPTION(x) return false;
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString(); #define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0; #define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
@ -594,7 +595,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
HANDLE_EXCEPTION( HANDLE_EXCEPTION(
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) { if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot", 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); app().grpc().login(username, password);
) )

View File

@ -235,7 +235,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void selectUser(QString const); ///< Signal that request the given user account to be displayed. void selectUser(QString const); ///< Signal that request the given user account to be displayed.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise. // 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 private: // member functions
void retrieveUserList(); ///< Retrieve the list of users via gRPC. void retrieveUserList(); ///< Retrieve the list of users via gRPC.

View File

@ -18,14 +18,34 @@
#include "SentryUtils.h" #include "SentryUtils.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include <bridgepp/BridgeUtils.h> #include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
#include <QByteArray> #include <QByteArray>
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QString> #include <QString>
#include <QSysInfo> #include <QSysInfo>
static constexpr const char *LoggerName = "bridge-gui"; 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;
}
}
}
QByteArray getProtectedHostname() { QByteArray getProtectedHostname() {
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256); QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
return hostname.toHex(); return hostname.toHex();
@ -63,12 +83,15 @@ sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir)
sentry_options_set_release(sentryOptions, appVersion(PROJECT_VER).toUtf8()); sentry_options_set_release(sentryOptions, appVersion(PROJECT_VER).toUtf8());
sentry_options_set_max_breadcrumbs(sentryOptions, 50); sentry_options_set_max_breadcrumbs(sentryOptions, 50);
sentry_options_set_environment(sentryOptions, PROJECT_BUILD_ENV); 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. // Enable this for debugging sentry.
// sentry_options_set_debug(sentryOptions, 1); // sentry_options_set_debug(sentryOptions, 1);
return sentryOptions; return sentryOptions;
} }
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message) { sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message) {
auto event = sentry_value_new_message_event(level, LoggerName, message); auto event = sentry_value_new_message_event(level, LoggerName, message);
return sentry_capture_event(event); return sentry_capture_event(event);
@ -92,3 +115,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

@ -21,9 +21,11 @@
#include <sentry.h> #include <sentry.h>
void setSentryReportScope(); void setSentryReportScope();
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir); sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message); 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 #endif //BRIDGE_GUI_SENTRYUTILS_H

View File

@ -22,6 +22,7 @@
#include "QMLBackend.h" #include "QMLBackend.h"
#include "SentryUtils.h" #include "SentryUtils.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "LogUtils.h"
#include <bridgepp/BridgeUtils.h> #include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h> #include <bridgepp/Exception/Exception.h>
#include <bridgepp/FocusGRPC/FocusGRPCClient.h> #include <bridgepp/FocusGRPC/FocusGRPCClient.h>
@ -238,7 +239,7 @@ void focusOtherInstance() {
} }
catch (Exception const &e) { catch (Exception const &e) {
app().log().error(e.qwhat()); app().log().error(e.qwhat());
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during focusOtherInstance()", "Exception", e.what()); 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())); app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(e.qwhat()));
} }
} }
@ -332,7 +333,8 @@ int main(int argc, char *argv[]) {
if (!cliOptions.attach) { if (!cliOptions.attach) {
if (isBridgeRunning()) { 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. // before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
@ -359,6 +361,7 @@ int main(int argc, char *argv[]) {
QQuickWindow::setSceneGraphBackend(cliOptions.useSoftwareRenderer ? "software" : "rhi"); QQuickWindow::setSceneGraphBackend(cliOptions.useSoftwareRenderer ? "software" : "rhi");
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend())); log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine)); std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext())); std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
@ -420,16 +423,9 @@ int main(int argc, char *argv[]) {
return result; return result;
} }
catch (Exception const &e) { catch (Exception const &e) {
QString fullMessage = e.qwhat(); sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
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());
QMessageBox::critical(nullptr, "Error", e.qwhat()); QMessageBox::critical(nullptr, "Error", e.qwhat());
QTextStream errStream(stderr); QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
errStream << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.qwhat() << "\n";
if (hasDetails)
errStream << "\nDetails:\n" << e.details() << "\n";
return EXIT_FAILURE; return EXIT_FAILURE;
} }
} }

View File

@ -25,12 +25,15 @@ namespace bridgepp {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] what A description of the exception. /// \param[in] what A description of the exception.
/// \param[in] details The optional details for 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() : std::exception()
, qwhat_(std::move(qwhat)) , qwhat_(std::move(qwhat))
, what_(qwhat_.toLocal8Bit()) , 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) : std::exception(ref)
, qwhat_(ref.qwhat_) , qwhat_(ref.qwhat_)
, what_(ref.what_) , 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) : std::exception(ref)
, qwhat_(ref.qwhat_) , qwhat_(ref.qwhat_)
, what_(ref.what_) , 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 } // namespace bridgepp

View File

@ -31,7 +31,8 @@ namespace bridgepp {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
class Exception : public std::exception { class Exception : public std::exception {
public: // member functions 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 const &ref) noexcept; ///< copy constructor
Exception(Exception &&ref) noexcept; ///< copy constructor Exception(Exception &&ref) noexcept; ///< copy constructor
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator 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 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 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 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 private: // data members
QString const qwhat_; ///< The description of the exception. 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. 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 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.
}; };