├── .gitignore ├── JQSentry.pro ├── README.md ├── demos ├── PostLogDemo │ ├── PostLogDemo.pro │ └── cpp │ │ └── main.cpp ├── PostMinidumpDemo │ ├── PostMinidumpDemo.pro │ ├── cpp │ │ └── main.cpp │ └── data │ │ ├── data.qrc │ │ └── test.dmp ├── PostPerformanceDemo │ ├── PostPerformanceDemo.pro │ └── cpp │ │ └── main.cpp └── demos.pro ├── doc ├── 1.1.png ├── 1.2.png ├── 1.3.png ├── 1.4.png ├── 1.5.png ├── 1.6.png ├── 2.1.png ├── 2.2.png ├── 2.3.png ├── 2.4.png ├── 2.5.png ├── 2.6.png ├── 2.7.png └── 2.8.png └── library └── JQLibrary ├── JQSentry.pri ├── include ├── JQDeclare ├── JQSentry │ ├── JQSentry │ ├── jqsentry.h │ └── jqsentry.inc └── jqdeclare.hpp └── src └── JQSentry └── jqsentry.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | ### C++ ### 2 | # Prerequisites 3 | *.d 4 | 5 | # Compiled Object files 6 | *.slo 7 | *.lo 8 | *.o 9 | *.obj 10 | 11 | # Precompiled Headers 12 | *.gch 13 | *.pch 14 | 15 | # Fortran module files 16 | *.mod 17 | *.smod 18 | 19 | 20 | ### Qt ### 21 | 22 | # Qt-es 23 | 24 | /.qmake.cache 25 | /.qmake.stash 26 | *.pro.user 27 | *.pro.user.* 28 | *.qbs.user 29 | *.qbs.user.* 30 | *.moc 31 | moc_*.cpp 32 | qrc_*.cpp 33 | ui_*.h 34 | Makefile* 35 | *build-* 36 | 37 | # QtCreator 38 | 39 | *.autosave 40 | 41 | # QtCtreator Qml 42 | *.qmlproject.user 43 | *.qmlproject.user.* 44 | 45 | # QtCtreator CMake 46 | CMakeLists.txt.user* 47 | 48 | ### macOS ### 49 | *.DS_Store 50 | -------------------------------------------------------------------------------- /JQSentry.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | 3 | SUBDIRS += demos 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 引言 2 | 3 | 工欲善其事,必先利其器。 4 | 5 | 软件工程越来越复杂,一定要依赖现代化的方式,帮助我们快速定位,分析问题。 6 | 7 | 这里我们从日志相关3大功能,log、minidump和performance展开,讲述如何在Qt中使用Sentry在线收集日志 8 | 9 | 10 | ## 关于Sentry 11 | 12 | Sentry平台,从简单的说是一个在线日志收集平台。从复杂说的可以帮我们处理从bug发生、定位、源码追溯、bug管理、修复、追踪,甚至是关联git和ci等一系列复杂流程。 13 | 14 | 使用Sentry是为了帮助我们从繁琐、复杂的日志收集工作中解放出来,提升开发效率。同时Sentry几乎支持全平台全语言,也提供http接口方便各类框架接入,保证了扩展性和适用性。 15 | 16 | 如果希望更深入了解Sentry,可以直接访问Sentry官网: 17 | https://sentry.io/welcome/ 18 | 19 | Sentry是一个开源平台,可以通过docker部署Sentry到本地离线环境,或者到自己的公网服务器。 20 | 21 | 如果不想自己部署,也可以直接使用在线版的Sentry,但是这个针对不同使用量收费不同,也有一些限制。自己部署则完全免费也无功能限制。初次使用推荐到Sentry官网注册,全程操作不到5分钟即可完成。 22 | 23 | Sentry已经提供了C++ SDK,如果想直接使用Sentry提供的C++ SDK,可以参考:https://docs.sentry.io/platforms/native/ 24 | 25 | 而为什么我没有用Sentry的C++ SDK,反而自己开发了一个JQSentry。因为如果你看到这里了,想必你已经有了Qt环境了,而Qt又自己的网络系统(QNetwork),自己的日志系统(QDebug),使用这些已经足够和Sentry通过HTTP直接对接。考虑到C++各种abi问题和平台兼容问题,没有必要再拖一个额外的C++库,增加不确定性。 26 | 27 | 另外JQSentry和Qt深度结合,可以在全局捕获日志,使用也更为方便。 28 | 29 | 30 | ## 关于JQSentry 31 | 32 | JQSentry基于Sentry的HTTP接口封装而来,目前一共有3个功能 33 | 34 | * 日志数据收集,对应Sentry中Issues模块 35 | 36 | * minidump数据收集,对应Sentry中Issues模块 37 | 38 | * performance数据收集,对应Sentry中Performance模块 39 | 40 | 为了保证使用足够轻量级,方便嵌入到各种系统中。JQSentry已经封装在一个cpp和几个h文件中,并且只依赖Qt库。 41 | 42 | 理论上可以部署到 Qt5 & C++11 的所有环境中。 43 | 44 | 本库源码均已开源在了GitHub上。 45 | 46 | GitHub地址:https://github.com/188080501/JQSentry 47 | 48 | 方便的话,帮我点个星星,或者反馈一下使用意见,这是对我莫大的帮助。 49 | 50 | 若你遇到问题、有了更好的建议或者想要一些新功能,都可以直接在GitHub上提交Issues:https://github.com/188080501/JQSentry/issues 51 | 52 | 如果需要扩展JQSentry,增加新数据或者模块,可以参考以下官网文档: 53 | 54 | * 日志数据:https://docs.sentry.io/api/events/retrieve-the-latest-event-for-an-issue/ 55 | 56 | * minidump数据:https://docs.sentry.io/platforms/native/guides/minidumps/ 57 | 58 | * performance数据:https://develop.sentry.dev/sdk/envelopes/ & https://docs.sentry.io/product/performance/getting-started/ 59 | 60 | 61 | ## Sentry的注册和基本使用 62 | 63 | 如果你已经注册了Sentry,或者已经有了自己的Sentry环境,可以跳过这一步 64 | 65 | * 注册帐号 66 | 67 | 这里都是标准步骤了,下一步下一步就行 68 | 69 | https://sentry.io/signup/ 70 | 71 | > 注:这里貌似不能使用QQ邮箱 72 | 73 | > 注:有一个欢迎界面,如果不需要看教程可以直接点右下角的 ```Skip this onboarding```,如下图 74 | 75 | ![](./doc/1.1.png) 76 | 77 | * 主界面 78 | 79 | 主界面如下图,左侧tab都是对应相应的模块和功能。Sentry可以创建多个项目,方便管理。我这里已经创建好了一个项目,分类是Native,名字是jason-vt。这里名字是全局唯一的,因此如果有其他人用过这个项目名字了,会自动加上一个尾缀以防重复,例如我这边的 ```-vx``` 80 | 81 | ![](./doc/1.2.png) 82 | 83 | * Issues界面 84 | 85 | 这里是显示log和minidump信息的主要界面,如果没有上传过任何数据,显示如下图: 86 | 87 | ![](./doc/1.3.png) 88 | 89 | * Performance界面 90 | 91 | 这里是显示性能数据和链路追踪的主要界面,如果没有上传过任何信息,显示如下图: 92 | 93 | ![](./doc/1.4.png) 94 | 95 | * 获取DSN 96 | 97 | DSN就相当于一个key,和项目绑定。有了DSN才可以上传数据到对应的项目中。 98 | 99 | 请勿对外泄漏DSN,本Demo中DSN为测试使用,请替换成你自己项目实际DSN。 100 | 101 | 具体获取DSN的路径如下: 102 | 103 | * 打开Settings 104 | 105 | * 打开Projects 106 | 107 | * 找到自己的项目,并打开 108 | 109 | ![](./doc/1.5.png) 110 | 111 | * 打开 Client Keys(DSN) 112 | 113 | * 复制DSN中的字符串,黏贴至自己软件工程(非Sentry平台)相应配置中 114 | 115 | ![](./doc/1.6.png) 116 | 117 | 118 | ## 使用JQSentry 119 | 120 | ### 上传log 121 | 122 | 只需要2步即可完成一个log上传 123 | 124 | * 初始化模块,并设置DSN 125 | 126 | ``` 127 | JQSentry::initialize( "https://key@o495303.ingest.sentry.io/123456" ); 128 | ``` 129 | 130 | * 上传 131 | 132 | ``` 133 | JQSentry::postLog( "This is a debug" ); 134 | ``` 135 | 136 | * 查看数据 137 | 138 | 运行Demo中的PostLogDemo工程,在Sentry的Issues界面中可以看到如下数据 139 | 140 | ![](./doc/2.1.png) 141 | 142 | 打开日志后,可以看到详细信息。 143 | 144 | ![](./doc/2.2.png) 145 | 146 | JQSentry已经收集了一些基本信息,包括: 147 | 148 | * 发生时间 149 | 150 | * 打印log的文件、函数、行信息(需要使用QDebug系统才有) 151 | 152 | * Qt版本(显示为浏览器,请忽略这个问题) 153 | 154 | * 系统信息 155 | 156 | * IP信息 157 | 158 | 如果需要额外信息,例如发布版本号,或者自定义的tag,可自行添加 159 | 160 | ### 上传minidump 161 | 162 | 和上传log一样,需要先初始化,再上传minidump 163 | 164 | 这里需要注意的是,JQSentry不负责minidump原始文件的收集。 165 | 166 | 因为minidump文件收集是一个大概念,比较复杂,而且不同平台处理不一样,因此暂时没计划集成到JQSentry中 167 | 168 | minidump文件收集需要使用OS API,例如Windows下可以使用```SetUnhandledExceptionFilter``` 169 | 170 | Demo中的dmp文件是我提前从其他程序中收集好的,仅供测试。 171 | 172 | * 上传 173 | 174 | ``` 175 | QFile minidumpFile( ":/test.dmp" ); 176 | minidumpFile.open( QIODevice::ReadOnly ); 177 | 178 | JQSentry::postMinidump( 179 | "This is a minidump", 180 | "test", 181 | minidumpFile.readAll() ); 182 | ``` 183 | 184 | * 查看数据 185 | 186 | 运行Demo中的PostMinidumpDemo工程,在Sentry的Issues界面中可以看到如下数据 187 | 188 | ![](./doc/2.3.png) 189 | 190 | 打开后,可以看到详细信息 191 | 192 | ![](./doc/2.4.png) 193 | 194 | 在只上传minidump的情况下,Sentry已经可以分析错误的类型,崩溃的模块等大致信息 195 | 196 | 如果附带PDB,则可以定位到源码级别,甚至是关联到对应到git记录。但是PDB因为文件体积,安全性等原因,一般不上传。 197 | 198 | 如果需要进一步调试,可以下载minidump文件到本地,通过VS等工具进一步调试。在页面最下方可以下载。 199 | 200 | ![](./doc/2.5.png) 201 | 202 | ### 上传performance 203 | 204 | 和上传log一样,需要先初始化,再上传performance 205 | 206 | * 上传 207 | 208 | ``` 209 | void something() 210 | { 211 | // 通过手动方式生成span,生命周期结束后就会自动上传数据 212 | // 指定parent的span,生命周期结束后,会写入结果数据到root span中,等待root span生命周期结束后一起上传 213 | // 结果数据包括创建span是指定的operationName、description、span创建时间和span销毁时间 214 | 215 | // 可以附带数据,方便调试 216 | QJsonObject data; 217 | 218 | data[ "key" ] = "mykey1"; 219 | data[ "value" ] = "myvalue1"; 220 | 221 | auto rootSpan = JQSentrySpan::create( "WorkResult", "saveToFile", data ); 222 | 223 | QThread::msleep( 20 ); // 模拟耗时操作 224 | 225 | for ( auto index = 0; index < 3; ++index ) 226 | { 227 | auto readyDataSpan = JQSentrySpan::create( rootSpan, "DataProvider", "readyData" ); 228 | 229 | QThread::msleep( 20 ); 230 | 231 | if ( index == 1 ) 232 | { 233 | // 不需要上传时可以cancel 234 | readyDataSpan->cancel(); 235 | } 236 | } 237 | 238 | { 239 | auto saveStep1Span = JQSentrySpan::create( rootSpan, "DataSaver", "saveStep1" ); 240 | 241 | QThread::msleep( 5 ); 242 | 243 | { 244 | // 可以不指定description 245 | auto saveStep2Span = JQSentrySpan::create( saveStep1Span, "DataSaver" ); 246 | 247 | QThread::msleep( 10 ); 248 | 249 | { 250 | // 也可以提前释放span 251 | saveStep1Span.clear(); 252 | 253 | auto saveStep3Span = JQSentrySpan::create( saveStep2Span, "DataSaver", "saveStep3" ); 254 | 255 | // 指定status,具体可以填写哪些值,请参考HTTP的status,不可以自定义 256 | saveStep3Span->setStatus( "internal_error" ); 257 | 258 | QThread::msleep( 50 ); 259 | } 260 | } 261 | } 262 | 263 | { 264 | auto cleanSpan = JQSentrySpan::create( rootSpan, "WorkResult", "cleanBuffer", data ); 265 | 266 | QThread::msleep( 10 ); 267 | } 268 | 269 | QThread::msleep( 5 ); 270 | } 271 | ``` 272 | 273 | * 查看数据 274 | 275 | 运行Demo中的PostMinidumpDemo工程,在Sentry的Performance界面中可以看到如下数据 276 | 277 | ![](./doc/2.6.png) 278 | 279 | 注意这里Filter是可选的,默认是按照耗时来排序,可以根据自己需求选择 280 | 281 | ![](./doc/2.7.png) 282 | 283 | 根据ID,可以定位到具体到一组Span,这里总耗时就是之前代码中root span的生命周期 284 | 285 | ![](./doc/2.8.png) 286 | 287 | 288 | ## Sentry局限性 289 | 290 | * 事件延迟 291 | 292 | 虽然Sentry是在线日志收集系统,但是不代表post完后数据可以立即刷新出来。一般的log可能有几秒到几十秒延迟,minidump和performance可能有几十秒甚至几分钟级别延迟。追求低延迟可以考虑自己部署Sentry。我自己部署到本地的Sentry,延迟就明显比官方在线版本的低。 293 | 294 | * 网络可用性 295 | 296 | 官方在线版本Sentry的服务器,毕竟是部署在国外,没有代理加速的话打开会很慢,极端情况下还会丢数据(上传失败)。如果要追求高可用性,建议还是自己部署Sentry。 297 | 298 | * 性能 299 | 300 | 和那些每秒万级别,几十万级别的日志系统不同。Sentry这边处理速度明显要慢很多。我自己部署的Sentry,用的8代i7 CPU,16G内存,SSD硬盘。对事件处理速度峰值也在几百每秒,再高的话就处理不过来了。当然这个问题可以通过加CPU、加内存、加硬盘解决。但是说到底Sentry性能还是受限,对于大并发量的log,建议还是通过文件方式存储在本地。 301 | -------------------------------------------------------------------------------- /demos/PostLogDemo/PostLogDemo.pro: -------------------------------------------------------------------------------- 1 | QT += core network concurrent 2 | QT -= gui 3 | 4 | CONFIG += c++11 5 | 6 | TEMPLATE = app 7 | 8 | include( $$PWD/../../library/JQLibrary/JQSentry.pri ) 9 | 10 | SOURCES += \ 11 | $$PWD/cpp/main.cpp 12 | 13 | mac { 14 | CONFIG += sdk_no_version_check 15 | } 16 | -------------------------------------------------------------------------------- /demos/PostLogDemo/cpp/main.cpp: -------------------------------------------------------------------------------- 1 | // Qt lib import 2 | #include 3 | 4 | // JQLibrary import 5 | #include 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | qSetMessagePattern( "%{time hh:mm:ss.zzz}: %{message}" ); 10 | 11 | QCoreApplication app( argc, argv ); 12 | 13 | // 这里初始化填写DSN,请换成实际DSN,这里写的是我测试用的 14 | JQSentry::initialize( "https://e9b577341ff2463e95d4944ffd3b9a39@o495303.ingest.sentry.io/5567822" ); 15 | 16 | // 手动上传一条log,指定类型为 QtDebugMsg 17 | JQSentry::postLog( "This is a debug", QtDebugMsg ); 18 | 19 | // 注册全局消息捕获,注册后会自动捕获 qWarning 和 qCritical 中包含的信息,也包括Qt内部报错 20 | JQSentry::installMessageHandler(); 21 | 22 | // 注册全局消息捕获后,使用QDebug系列接口就可以完成对应的上传 23 | qWarning() << "This is a warning"; 24 | qCritical() << "This is a critical"; 25 | 26 | // 注册全局消息捕获后,也会捕获Qt内部的报错,这里模拟一次Qt发出的报错 27 | QFile file( "qrc:/this_file_not_exists" ); 28 | file.write( "balabala" ); 29 | 30 | // Sentry的数据会在后台上传,主线程必须开启事件循环 31 | QTimer::singleShot( 5000, &app, &QCoreApplication::quit ); 32 | return app.exec(); 33 | } 34 | -------------------------------------------------------------------------------- /demos/PostMinidumpDemo/PostMinidumpDemo.pro: -------------------------------------------------------------------------------- 1 | QT += core network concurrent 2 | QT -= gui 3 | 4 | CONFIG += c++11 5 | 6 | TEMPLATE = app 7 | 8 | include( $$PWD/../../library/JQLibrary/JQSentry.pri ) 9 | 10 | SOURCES += \ 11 | $$PWD/cpp/main.cpp 12 | 13 | RESOURCES += \ 14 | $$PWD/data/data.qrc 15 | 16 | mac { 17 | CONFIG += sdk_no_version_check 18 | } 19 | -------------------------------------------------------------------------------- /demos/PostMinidumpDemo/cpp/main.cpp: -------------------------------------------------------------------------------- 1 | // Qt lib import 2 | #include 3 | 4 | // JQLibrary import 5 | #include 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | qSetMessagePattern( "%{time hh:mm:ss.zzz}: %{message}" ); 10 | 11 | QCoreApplication app( argc, argv ); 12 | 13 | // 这里初始化填写DSN,请换成实际DSN,这里写的是我测试用的 14 | JQSentry::initialize( "https://e9b577341ff2463e95d4944ffd3b9a39@o495303.ingest.sentry.io/5567822" ); 15 | 16 | QFile minidumpFile( ":/test.dmp" ); 17 | minidumpFile.open( QIODevice::ReadOnly ); 18 | 19 | // 手动上传minidump 20 | JQSentry::postMinidump( 21 | "This is a minidump", 22 | "test", 23 | minidumpFile.readAll() ); 24 | 25 | // Sentry的数据会在后台上传,主线程必须开启事件循环 26 | QTimer::singleShot( 5000, &app, &QCoreApplication::quit ); 27 | return app.exec(); 28 | } 29 | -------------------------------------------------------------------------------- /demos/PostMinidumpDemo/data/data.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | test.dmp 4 | 5 | 6 | -------------------------------------------------------------------------------- /demos/PostMinidumpDemo/data/test.dmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/demos/PostMinidumpDemo/data/test.dmp -------------------------------------------------------------------------------- /demos/PostPerformanceDemo/PostPerformanceDemo.pro: -------------------------------------------------------------------------------- 1 | QT += core network concurrent 2 | QT -= gui 3 | 4 | CONFIG += c++11 5 | 6 | TEMPLATE = app 7 | 8 | include( $$PWD/../../library/JQLibrary/JQSentry.pri ) 9 | 10 | SOURCES += \ 11 | $$PWD/cpp/main.cpp 12 | 13 | mac { 14 | CONFIG += sdk_no_version_check 15 | } 16 | -------------------------------------------------------------------------------- /demos/PostPerformanceDemo/cpp/main.cpp: -------------------------------------------------------------------------------- 1 | // Qt lib import 2 | #include 3 | 4 | // JQLibrary import 5 | #include 6 | 7 | void something() 8 | { 9 | // 通过手动方式生成span,生命周期结束后就会自动上传数据 10 | // 指定parent的span,生命周期结束后,会写入结果数据到root span中,等待root span生命周期结束后一起上传 11 | // 结果数据包括创建span是指定的operationName、description、span创建时间和span销毁时间 12 | 13 | // 可以附带数据,方便调试 14 | QJsonObject data; 15 | 16 | data[ "key" ] = "mykey1"; 17 | data[ "value" ] = "myvalue1"; 18 | 19 | auto rootSpan = JQSentrySpan::create( "WorkResult", "saveToFile", data ); 20 | 21 | QThread::msleep( 20 ); // 模拟耗时操作 22 | 23 | for ( auto index = 0; index < 3; ++index ) 24 | { 25 | auto readyDataSpan = JQSentrySpan::create( rootSpan, "DataProvider", "readyData" ); 26 | 27 | QThread::msleep( 20 ); 28 | 29 | if ( index == 1 ) 30 | { 31 | // 不需要上传时可以cancel 32 | readyDataSpan->cancel(); 33 | } 34 | } 35 | 36 | { 37 | auto saveStep1Span = JQSentrySpan::create( rootSpan, "DataSaver", "saveStep1" ); 38 | 39 | QThread::msleep( 5 ); 40 | 41 | { 42 | // 可以不指定description 43 | auto saveStep2Span = JQSentrySpan::create( saveStep1Span, "DataSaver" ); 44 | 45 | QThread::msleep( 10 ); 46 | 47 | { 48 | // 也可以提前释放span 49 | saveStep1Span.clear(); 50 | 51 | auto saveStep3Span = JQSentrySpan::create( saveStep2Span, "DataSaver", "saveStep3" ); 52 | 53 | // 指定status,具体可以填写哪些值,请参考HTTP的status,不可以自定义 54 | saveStep3Span->setStatus( "internal_error" ); 55 | 56 | QThread::msleep( 50 ); 57 | } 58 | } 59 | } 60 | 61 | { 62 | auto cleanSpan = JQSentrySpan::create( rootSpan, "WorkResult", "cleanBuffer", data ); 63 | 64 | QThread::msleep( 10 ); 65 | } 66 | 67 | QThread::msleep( 5 ); 68 | } 69 | 70 | int main(int argc, char *argv[]) 71 | { 72 | qSetMessagePattern( "%{time hh:mm:ss.zzz}: %{message}" ); 73 | 74 | QCoreApplication app( argc, argv ); 75 | 76 | // 这里初始化填写DSN,请换成实际DSN,这里写的是我测试用的 77 | JQSentry::initialize( "https://e9b577341ff2463e95d4944ffd3b9a39@o495303.ingest.sentry.io/5567822" ); 78 | 79 | something(); 80 | 81 | // Sentry的数据会在后台上传,主线程必须开启事件循环 82 | QTimer::singleShot( 5000, &app, &QCoreApplication::quit ); 83 | return app.exec(); 84 | } 85 | -------------------------------------------------------------------------------- /demos/demos.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | 3 | SUBDIRS += PostLogDemo 4 | SUBDIRS += PostMinidumpDemo 5 | SUBDIRS += PostPerformanceDemo 6 | -------------------------------------------------------------------------------- /doc/1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/1.1.png -------------------------------------------------------------------------------- /doc/1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/1.2.png -------------------------------------------------------------------------------- /doc/1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/1.3.png -------------------------------------------------------------------------------- /doc/1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/1.4.png -------------------------------------------------------------------------------- /doc/1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/1.5.png -------------------------------------------------------------------------------- /doc/1.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/1.6.png -------------------------------------------------------------------------------- /doc/2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.1.png -------------------------------------------------------------------------------- /doc/2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.2.png -------------------------------------------------------------------------------- /doc/2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.3.png -------------------------------------------------------------------------------- /doc/2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.4.png -------------------------------------------------------------------------------- /doc/2.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.5.png -------------------------------------------------------------------------------- /doc/2.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.6.png -------------------------------------------------------------------------------- /doc/2.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.7.png -------------------------------------------------------------------------------- /doc/2.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/188080501/JQSentry/91819d003170769c03df65c93412d1d9f95badb1/doc/2.8.png -------------------------------------------------------------------------------- /library/JQLibrary/JQSentry.pri: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of JQLibrary 3 | # 4 | # Copyright: Jason 5 | # 6 | # Contact email: 188080501@qq.com 7 | # 8 | # GNU Lesser General Public License Usage 9 | # Alternatively, this file may be used under the terms of the GNU Lesser 10 | # General Public License version 2.1 or version 3 as published by the Free 11 | # Software Foundation and appearing in the file LICENSE.LGPLv21 and 12 | # LICENSE.LGPLv3 included in the packaging of this file. Please review the 13 | # following information to ensure the GNU Lesser General Public License 14 | # requirements will be met: https://www.gnu.org/licenses/lgpl.html and 15 | # http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. 16 | # 17 | 18 | QT *= network 19 | 20 | DEFINES *= QT_MESSAGELOGCONTEXT 21 | 22 | INCLUDEPATH *= \ 23 | $$PWD/include/JQSentry/ 24 | 25 | !contains( DEFINES, JQLIBRARY_EXPORT_ENABLE ) | contains( DEFINES, JQLIBRARY_EXPORT_MODE ) { 26 | 27 | HEADERS *= \ 28 | $$PWD/include/JQSentry/jqsentry.h \ 29 | $$PWD/include/JQSentry/jqsentry.inc 30 | 31 | SOURCES *= \ 32 | $$PWD/src/JQSentry/jqsentry.cpp 33 | } 34 | -------------------------------------------------------------------------------- /library/JQLibrary/include/JQDeclare: -------------------------------------------------------------------------------- 1 | #include "jqdeclare.hpp" 2 | -------------------------------------------------------------------------------- /library/JQLibrary/include/JQSentry/JQSentry: -------------------------------------------------------------------------------- 1 | // .h include 2 | #include "jqsentry.h" 3 | -------------------------------------------------------------------------------- /library/JQLibrary/include/JQSentry/jqsentry.h: -------------------------------------------------------------------------------- 1 | #ifndef JQLIBRARY_INCLUDE_JQSENTRY_JQSENTRY_H_ 2 | #define JQLIBRARY_INCLUDE_JQSENTRY_JQSENTRY_H_ 3 | 4 | // C++ lib import 5 | #include 6 | #include 7 | 8 | // Qt lib import 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // JQLibrary lib import 20 | #include <../JQDeclare> 21 | 22 | class JQSentrySpan; 23 | class JQSentrySpan; 24 | 25 | struct JQSentrySpanData 26 | { 27 | QString operationName; 28 | QString description; 29 | QJsonValue data; 30 | QString status; 31 | 32 | QString parentSpanId; 33 | QString spanId; 34 | 35 | std::chrono::system_clock::duration startTime; 36 | std::chrono::system_clock::duration endTime; 37 | }; 38 | 39 | class JQSentryTransit: public QObject 40 | { 41 | Q_OBJECT 42 | Q_DISABLE_COPY( JQSentryTransit ) 43 | 44 | public: 45 | JQSentryTransit() = default; 46 | 47 | ~JQSentryTransit() = default; 48 | 49 | public slots: 50 | void transit(const std::function &callback); 51 | }; 52 | 53 | class JQLIBRARY_EXPORT JQSentry: public QObject 54 | { 55 | Q_OBJECT 56 | Q_DISABLE_COPY( JQSentry ) 57 | 58 | JQSentry() = default; 59 | 60 | public: 61 | ~JQSentry() = default; 62 | 63 | static bool initialize(const QString &dsn); 64 | 65 | static bool isAvailable(); 66 | 67 | static void installMessageHandler(const int &acceptedType = QtWarningMsg | QtCriticalMsg); 68 | 69 | static bool serverReachable(); 70 | 71 | 72 | static bool postLog(const QString &log, const QtMsgType &type = QtDebugMsg, const QMessageLogContext &context = { }); 73 | 74 | static bool postMinidump(const QString &log, const QString &dumpFileName, const QByteArray &dumpFileData); 75 | 76 | static bool postPerformance(const QVector< JQSentrySpanData > &spanDataList); 77 | 78 | 79 | inline static void setServerName(const QString &serverName); 80 | 81 | inline static void setUserId(const QString &userId); 82 | 83 | inline static void setUserName(const QString &userName); 84 | 85 | inline static void setRelease(const QString &release); 86 | 87 | inline static void setLoggerName(const QString &loggerName); 88 | 89 | private: 90 | static void handleReply(QNetworkReply *reply); 91 | 92 | static QString getLogLevel(const QtMsgType &type); 93 | 94 | static QJsonObject sentryData(); 95 | 96 | static QByteArray xSentryAuth(); 97 | 98 | static QJsonValue toSentryTime(const QDateTime &time); 99 | 100 | static QJsonValue toSentryTime(const std::chrono::system_clock::duration &time); 101 | 102 | private: 103 | static QSharedPointer< QNetworkAccessManager > networkAccessManager_; 104 | static QPointer< JQSentryTransit > transit_; 105 | static bool continueFlag_; 106 | 107 | static QString clientName_; 108 | static QString clientVersion_; 109 | 110 | static QString dsn_; 111 | static QString protocol_; 112 | static QString publicKey_; 113 | static QString host_; 114 | static quint16 port_; 115 | static QString path_; 116 | static QString projectId_; 117 | 118 | static QString serverName_; 119 | static QString userId_; 120 | static QString userName_; 121 | static QString userIpAddress_; 122 | static QString release_; 123 | }; 124 | 125 | class JQLIBRARY_EXPORT JQSentrySpan 126 | { 127 | private: 128 | JQSentrySpan( 129 | const QString & operationName, 130 | const QString & description, 131 | const QJsonValue &data ); 132 | 133 | public: 134 | ~JQSentrySpan(); 135 | 136 | static QSharedPointer< JQSentrySpan > create( 137 | const QSharedPointer< JQSentrySpan > &parent, 138 | const QString & operationName, 139 | const QString & description = QString(), 140 | const QJsonValue & data = QJsonValue() ); 141 | 142 | inline static QSharedPointer< JQSentrySpan > create( 143 | const QString & operationName, 144 | const QString & description = QString(), 145 | const QJsonValue & data = QJsonValue() ); 146 | 147 | static inline void setEnabled(const bool &enabled); 148 | 149 | inline void cancel(); 150 | 151 | inline void setStatus(const QString &status); 152 | 153 | inline JQSentrySpanData spanData() const; 154 | 155 | private: 156 | static QMutex mutex_; 157 | static bool enabled_; 158 | 159 | JQSentrySpanData spanData_; 160 | bool isCancel_ = false; 161 | 162 | QWeakPointer< JQSentrySpan > rootSpan_; 163 | QVector< JQSentrySpanData > spanDataList_; 164 | }; 165 | 166 | // .inc include 167 | #include "jqsentry.inc" 168 | 169 | #endif//JQLIBRARY_INCLUDE_JQSENTRY_JQSENTRY_H_ 170 | -------------------------------------------------------------------------------- /library/JQLibrary/include/JQSentry/jqsentry.inc: -------------------------------------------------------------------------------- 1 | #ifndef JQLIBRARY_INCLUDE_JQSENTRY_JQSENTRY_INC_ 2 | #define JQLIBRARY_INCLUDE_JQSENTRY_JQSENTRY_INC_ 3 | 4 | // .h include 5 | #include "jqsentry.h" 6 | 7 | // JQSentry 8 | inline void JQSentry::setServerName(const QString &serverName) 9 | { 10 | serverName_ = serverName; 11 | } 12 | 13 | inline void JQSentry::setUserId(const QString &userId) 14 | { 15 | userId_ = userId; 16 | } 17 | 18 | inline void JQSentry::setUserName(const QString &userName) 19 | { 20 | userName_ = userName; 21 | } 22 | 23 | inline void JQSentry::setRelease(const QString &release) 24 | { 25 | release_ = release; 26 | } 27 | 28 | inline void JQSentry::setLoggerName(const QString &loggerName) 29 | { 30 | clientName_ = loggerName; 31 | } 32 | 33 | // JQSentrySpan 34 | inline QSharedPointer< JQSentrySpan > JQSentrySpan::create( 35 | const QString & operationName, 36 | const QString & description, 37 | const QJsonValue & data ) 38 | { 39 | return create( 40 | nullptr, 41 | operationName, 42 | description, 43 | data ); 44 | } 45 | 46 | inline void JQSentrySpan::setEnabled(const bool &enabled) 47 | { 48 | enabled_ = enabled; 49 | } 50 | 51 | inline void JQSentrySpan::cancel() 52 | { 53 | isCancel_ = true; 54 | } 55 | 56 | inline void JQSentrySpan::setStatus(const QString &status) 57 | { 58 | spanData_.status = status; 59 | } 60 | 61 | inline JQSentrySpanData JQSentrySpan::spanData() const 62 | { 63 | return spanData_; 64 | } 65 | 66 | #endif//JQLIBRARY_INCLUDE_JQSENTRY_JQSENTRY_INC_ 67 | -------------------------------------------------------------------------------- /library/JQLibrary/include/jqdeclare.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of JQLibrary 3 | 4 | Copyright: Jason and others 5 | 6 | Contact email: 188080501@qq.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | "Software"), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | 28 | #ifndef JQLIBRARY_INCLUDE_JQDECLARE_HPP_ 29 | #define JQLIBRARY_INCLUDE_JQDECLARE_HPP_ 30 | 31 | // Macro define 32 | #define JQPROPERTYDECLARE( Type, name, setName, ... ) \ 33 | private: \ 34 | Type name##_ __VA_ARGS__; \ 35 | \ 36 | public: \ 37 | inline const Type &name() const { return name##_; } \ 38 | inline void setName( const Type &name ) { name##_ = name; } \ 39 | \ 40 | private: 41 | 42 | #define JQPROPERTYDECLAREWITHSLOT( Type, name, setName, ... ) \ 43 | private: \ 44 | Type name##_ __VA_ARGS__; \ 45 | public Q_SLOTS: \ 46 | Type name() const { return name##_; } \ 47 | void setName( const Type &name ) { name##_ = name; } \ 48 | \ 49 | private: 50 | 51 | #define JQPTRPROPERTYDECLARE( Type, name, setName, ... ) \ 52 | private: \ 53 | Type *name##_ __VA_ARGS__; \ 54 | \ 55 | public: \ 56 | inline const Type *name() const { return name##_; } \ 57 | inline void setName( const Type &name ) \ 58 | { \ 59 | if ( name##_ ) \ 60 | { \ 61 | delete name##_; \ 62 | } \ 63 | name##_ = new Type( name ); \ 64 | } \ 65 | \ 66 | private: 67 | 68 | #define JQ_READ_AND_SET_PROPERTY( Type, name, setName ) \ 69 | public: \ 70 | inline const Type &name() const { return name##_; } \ 71 | inline void setName( const Type &name ) { name##_ = name; } \ 72 | \ 73 | private: 74 | 75 | #define JQ_STATIC_READ_AND_SET_PROPERTY( Type, name, setName ) \ 76 | public: \ 77 | static inline const Type &name() { return name##_; } \ 78 | static inline void setName( const Type &name ) { name##_ = name; } \ 79 | \ 80 | private: 81 | 82 | #define JQ_STATIC_SET_PROPERTY( Type, name, setName ) \ 83 | public: \ 84 | static inline void setName( const Type &name ) { name##_ = name; } \ 85 | \ 86 | private: 87 | 88 | #define RUNONOUTRANGEHELPER2( x, y ) x##y 89 | #define RUNONOUTRANGEHELPER( x, y ) RUNONOUTRANGEHELPER2( x, y ) 90 | #define RUNONOUTRANGE( ... ) \ 91 | auto RUNONOUTRANGEHELPER( runOnOutRangeCallback, __LINE__ ) = __VA_ARGS__; \ 92 | QSharedPointer< int > RUNONOUTRANGEHELPER( runOnOutRange, __LINE__ )( \ 93 | new int, \ 94 | [ RUNONOUTRANGEHELPER( runOnOutRangeCallback, __LINE__ ) ]( int *data ) { \ 95 | RUNONOUTRANGEHELPER( runOnOutRangeCallback, __LINE__ ) \ 96 | (); \ 97 | delete data; \ 98 | } ); \ 99 | if ( RUNONOUTRANGEHELPER( runOnOutRange, __LINE__ ).data() == nullptr ) \ 100 | { \ 101 | exit( -1 ); \ 102 | } 103 | 104 | #define RUNONOUTRANGETIMER( message ) \ 105 | const auto &&runOnOutRangeTimerTime = QDateTime::currentMSecsSinceEpoch(); \ 106 | RUNONOUTRANGE( [ = ]() { \ 107 | qDebug() << message << ( QDateTime::currentMSecsSinceEpoch() - runOnOutRangeTimerTime ); \ 108 | } ) 109 | 110 | #define JQCONST( property ) static_cast< const decltype( property ) >( property ) 111 | 112 | #define JQTICKCOUNTERMESSAGE( message ) \ 113 | { \ 114 | static JQTickCounter tickCounter; \ 115 | tickCounter.tick(); \ 116 | qDebug() << message << tickCounter.tickPerSecond(); \ 117 | } 118 | 119 | #define JQBUILDDATETIMESTRING \ 120 | ( QDateTime( \ 121 | QLocale( QLocale::English ).toDate( QString( __DATE__ ).replace( " ", " 0" ), "MMM dd yyyy" ), \ 122 | QTime::fromString( __TIME__, "hh:mm:ss" ) ) \ 123 | .toString( "yyyy-MM-dd hh:mm:ss" ) \ 124 | .toLatin1() \ 125 | .data() ) 126 | 127 | #define JQONLYONCE \ 128 | if ( []() { \ 129 | static auto flag = true; \ 130 | if ( flag ) \ 131 | { \ 132 | flag = false; \ 133 | return true; \ 134 | } \ 135 | return false; \ 136 | }() ) 137 | 138 | #define JQSKIPFIRST \ 139 | if ( []() { \ 140 | static auto flag = true; \ 141 | if ( flag ) \ 142 | { \ 143 | flag = false; \ 144 | return false; \ 145 | } \ 146 | return true; \ 147 | }() ) 148 | 149 | #define JQINTERVAL( timeInterval ) \ 150 | if ( []() { \ 151 | static qint64 lastTime = 0; \ 152 | const auto && currentTime = QDateTime::currentMSecsSinceEpoch(); \ 153 | if ( qAbs( currentTime - lastTime ) > timeInterval ) \ 154 | { \ 155 | lastTime = currentTime; \ 156 | return true; \ 157 | } \ 158 | return false; \ 159 | }() ) 160 | 161 | // Export 162 | #ifdef JQLIBRARY_EXPORT_ENABLE 163 | # ifdef JQLIBRARY_EXPORT_MODE 164 | # define JQLIBRARY_EXPORT Q_DECL_EXPORT 165 | # else 166 | # define JQLIBRARY_EXPORT Q_DECL_IMPORT 167 | # endif 168 | #else 169 | # define JQLIBRARY_EXPORT 170 | #endif 171 | 172 | #endif // JQLIBRARY_INCLUDE_JQDECLARE_HPP_ 173 | -------------------------------------------------------------------------------- /library/JQLibrary/src/JQSentry/jqsentry.cpp: -------------------------------------------------------------------------------- 1 | // .h include 2 | #include "jqsentry.h" 3 | 4 | // Qt lib import 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // JQSentryTransit 20 | void JQSentryTransit::transit(const std::function &callback) 21 | { 22 | if ( QThread::currentThread() == this->thread() ) 23 | { 24 | callback(); 25 | } 26 | else 27 | { 28 | QMetaObject::invokeMethod( 29 | this, 30 | "transit", 31 | Qt::QueuedConnection, 32 | Q_ARG( std::function, callback ) ); 33 | } 34 | } 35 | 36 | // JQSentry 37 | QSharedPointer< QNetworkAccessManager > JQSentry::networkAccessManager_; 38 | QPointer< JQSentryTransit > JQSentry::transit_; 39 | bool JQSentry::continueFlag_ = true; 40 | 41 | QString JQSentry::clientName_ = "JQSentry"; 42 | QString JQSentry::clientVersion_ = "1.5"; 43 | 44 | QString JQSentry::dsn_; 45 | QString JQSentry::protocol_; 46 | QString JQSentry::publicKey_; 47 | QString JQSentry::host_; 48 | quint16 JQSentry::port_; 49 | QString JQSentry::path_; 50 | QString JQSentry::projectId_; 51 | 52 | QString JQSentry::serverName_; 53 | QString JQSentry::userId_; 54 | QString JQSentry::userName_; 55 | QString JQSentry::userIpAddress_; 56 | QString JQSentry::release_; 57 | 58 | bool JQSentry::initialize(const QString &dsn) 59 | { 60 | if ( networkAccessManager_ ) 61 | { 62 | qDebug() << "JQSentry::initialized: Already initialized"; 63 | return false; 64 | } 65 | 66 | if ( !qApp ) 67 | { 68 | qDebug() << "JQSentry::initialized: Cannot be used without QApplication"; 69 | return false; 70 | } 71 | 72 | QUrl url( dsn ); 73 | if ( dsn.isEmpty() || !url.isValid() ) 74 | { 75 | qDebug() << "JQSentry::initialize: DSN is not valid"; 76 | return false; 77 | } 78 | 79 | if ( ( url.scheme().toLower() == "https" ) && ( !QSslSocket::supportsSsl() ) ) 80 | { 81 | qDebug() << "JQSentry::initialize: Protocol is https but ssl is not support"; 82 | return false; 83 | } 84 | 85 | qRegisterMetaType< std::function >( "std::function" ); 86 | 87 | networkAccessManager_.reset( new QNetworkAccessManager ); 88 | transit_ = new JQSentryTransit; 89 | 90 | dsn_ = dsn; 91 | protocol_ = url.scheme(); 92 | publicKey_ = url.userName(); 93 | host_ = url.host(); 94 | port_ = static_cast< quint16 >( url.port( ( dsn.startsWith( "https" ) ) ? ( 443 ) : ( 80 ) ) ); 95 | path_ = url.path(); 96 | projectId_ = url.fileName(); 97 | 98 | int i = path_.lastIndexOf( '/' ); 99 | if ( i >= 0 ) 100 | { 101 | path_ = path_.left( i ); 102 | } 103 | 104 | userName_ = qgetenv( "USER" ); 105 | if ( userName_.isEmpty() ) 106 | { 107 | userName_ = qgetenv( "USERNAME" ); 108 | } 109 | 110 | userIpAddress_ = []()->QString 111 | { 112 | for ( const auto &address: QNetworkInterface::allAddresses() ) 113 | { 114 | if ( !address.isLoopback() && ( address.protocol() == QAbstractSocket::IPv4Protocol ) ) 115 | { 116 | return address.toString(); 117 | } 118 | } 119 | 120 | return { }; 121 | }(); 122 | 123 | #ifndef QT_NO_DEBUG 124 | qDebug().noquote() << "JQSentry::initialize: succeed, project id:" << projectId_; 125 | #endif 126 | 127 | QObject::connect( qApp, &QCoreApplication::aboutToQuit, []() { 128 | continueFlag_ = false; 129 | networkAccessManager_.clear(); 130 | transit_->deleteLater(); 131 | } ); 132 | 133 | return true; 134 | } 135 | 136 | bool JQSentry::isAvailable() 137 | { 138 | return networkAccessManager_ && continueFlag_; 139 | } 140 | 141 | void JQSentry::installMessageHandler(const int &acceptedType_) 142 | { 143 | static const auto defaultHandler = qInstallMessageHandler( nullptr ); 144 | static const auto acceptedType = acceptedType_; 145 | 146 | class HelperClass 147 | { 148 | public: 149 | static void messageHandler( 150 | QtMsgType type, 151 | const QMessageLogContext &context, 152 | const QString & log ) 153 | { 154 | defaultHandler( type, context, log ); 155 | 156 | if ( acceptedType & type ) 157 | { 158 | JQSentry::postLog( log, type, context ); 159 | } 160 | } 161 | }; 162 | 163 | qInstallMessageHandler( HelperClass::messageHandler ); 164 | } 165 | 166 | bool JQSentry::serverReachable() 167 | { 168 | QTcpSocket socket; 169 | 170 | socket.connectToHost( host_, port_ ); 171 | socket.waitForConnected( 5000 ); 172 | 173 | return socket.state() == QAbstractSocket::ConnectedState; 174 | } 175 | 176 | bool JQSentry::postLog(const QString &log, const QtMsgType &type, const QMessageLogContext &context) 177 | { 178 | if ( !isAvailable() ) { return false; } 179 | 180 | if ( log.contains( "QDisabledNetworkReply" ) ) { return false; } 181 | 182 | if ( log.contains( "QOpenGLContext" ) ) { return false; } 183 | 184 | if ( log.contains( "Execution of PAC script" ) ) { return false; } 185 | 186 | if ( QThread::currentThread() != transit_->thread() ) 187 | { 188 | const auto fileName = context.file; 189 | const auto lineNumber = context.line; 190 | const auto functionName = context.function; 191 | const auto category = context.category; 192 | 193 | transit_->transit( [ = ]() 194 | { 195 | JQSentry::postLog( log, type, { fileName, lineNumber, functionName, category } ); 196 | } ); 197 | return true; 198 | } 199 | 200 | auto data = sentryData(); 201 | 202 | data[ "event_id" ] = QUuid::createUuid().toString().mid( 1, 36 ); 203 | data[ "level" ] = getLogLevel( type ); 204 | data[ "message" ] = log; 205 | 206 | { 207 | QJsonObject tagData; 208 | 209 | if ( !QString( context.file ).isEmpty() ) 210 | { 211 | tagData[ "file" ] = QFileInfo( context.file ).fileName(); 212 | } 213 | 214 | data[ "tags" ] = tagData; 215 | } 216 | 217 | if ( !QString( context.file ).isEmpty() ) 218 | { 219 | QString function = context.function; 220 | function.replace( "static ", "" ); 221 | function.replace( "__cdecl ", "" ); 222 | function.replace( "(anonymous class)::", "" ); 223 | 224 | data[ "culprit" ] = QString( "file: %1 / function: %2 / line: %3" ) 225 | .arg( 226 | QFileInfo( context.file ).fileName(), 227 | function, 228 | QString::number( context.line ) ); 229 | } 230 | 231 | const auto url = QString( "%1://%2:%3%4/api/%5/store/" ) 232 | .arg( protocol_ ) 233 | .arg( host_ ) 234 | .arg( port_ ) 235 | .arg( path_ ) 236 | .arg( projectId_ ); 237 | const auto auth = xSentryAuth(); 238 | 239 | QNetworkRequest request( url ); 240 | request.setHeader( QNetworkRequest::ContentTypeHeader, "application/json" ); 241 | request.setRawHeader( "X-Sentry-Auth", xSentryAuth() ); 242 | 243 | handleReply( networkAccessManager_->post( request, QJsonDocument( data ).toJson( QJsonDocument::Compact ) ) ); 244 | 245 | return true; 246 | } 247 | 248 | bool JQSentry::postMinidump(const QString &log, const QString &dumpFileName, const QByteArray &dumpFileData) 249 | { 250 | if ( !isAvailable() ) { return false; } 251 | 252 | auto data = sentryData(); 253 | 254 | data[ "message" ] = log; 255 | 256 | auto multiPart = new QHttpMultiPart; 257 | multiPart->setBoundary( QString( "-----%1" ).arg( QUuid::createUuid().toString().mid( 1, 36 ) ).toUtf8() ); 258 | 259 | { 260 | QHttpPart part; 261 | 262 | part.setHeader( QNetworkRequest::ContentDispositionHeader, QVariant( "form-data; name=\"sentry\"" ) ); 263 | part.setBody( QJsonDocument( data ).toJson( QJsonDocument::Compact ) ); 264 | 265 | multiPart->append( part ); 266 | } 267 | 268 | { 269 | QHttpPart part; 270 | 271 | part.setHeader( QNetworkRequest::ContentTypeHeader, QVariant( "file" ) ); 272 | part.setHeader( QNetworkRequest::ContentDispositionHeader, QVariant( "form-data; name=\"upload_file_minidump\"; filename=\"" + dumpFileName + ".dmp\"" ) ); 273 | part.setBody( dumpFileData ); 274 | 275 | multiPart->append( part ); 276 | } 277 | 278 | { 279 | QHttpPart part; 280 | 281 | part.setHeader( QNetworkRequest::ContentDispositionHeader, QVariant( "form-data; name=\"some_file\"; filename=\"" + dumpFileName + ".dmp\"" ) ); 282 | part.setBody( dumpFileData ); 283 | 284 | multiPart->append( part ); 285 | } 286 | 287 | const auto url = QString( "%1://%2:%3%4/api/%5/minidump/" ) 288 | .arg( protocol_ ) 289 | .arg( host_ ) 290 | .arg( port_ ) 291 | .arg( path_ ) 292 | .arg( projectId_ ); 293 | 294 | QNetworkRequest request( url ); 295 | request.setRawHeader( "Content-Type", "multipart/form-data; boundary=" + multiPart->boundary() ); 296 | request.setRawHeader( "X-Sentry-Auth", xSentryAuth() ); 297 | 298 | auto reply = networkAccessManager_->post( request, multiPart ); 299 | multiPart->setParent( reply ); 300 | 301 | handleReply( reply ); 302 | 303 | return true; 304 | } 305 | 306 | bool JQSentry::postPerformance(const QVector< JQSentrySpanData > &spanDataList) 307 | { 308 | if ( !isAvailable() ) { return false; } 309 | 310 | if ( spanDataList.isEmpty() ) { return false; } 311 | 312 | if ( QThread::currentThread() != transit_->thread() ) 313 | { 314 | transit_->transit( [ = ]() 315 | { 316 | JQSentry::postPerformance( spanDataList ); 317 | } ); 318 | return true; 319 | } 320 | 321 | const auto eventId = QUuid::createUuid().toString().mid( 1, 36 ).remove( "-" ); 322 | const auto traceId = QUuid::createUuid().toString().mid( 1, 36 ).remove( "-" ); 323 | 324 | auto data = sentryData(); 325 | 326 | QJsonObject eventObject; 327 | 328 | eventObject[ "event_id" ] = eventId; 329 | 330 | QJsonObject transactionObject; 331 | 332 | transactionObject[ "type" ] = "transaction"; 333 | 334 | { 335 | QJsonArray sampleRates; 336 | QJsonObject sampleRate; 337 | 338 | sampleRate[ "id" ] = "client_rate"; 339 | sampleRate[ "rate" ] = "1"; 340 | 341 | sampleRates.push_back( sampleRate ); 342 | transactionObject[ "sample_rates" ] = sampleRates; 343 | } 344 | 345 | QDateTime testTime = QDateTime::currentDateTime(); 346 | 347 | auto performanceObject = sentryData(); 348 | 349 | performanceObject[ "event_id" ] = eventId; 350 | performanceObject[ "type" ] = "transaction"; 351 | performanceObject[ "timestamp" ] = toSentryTime( spanDataList.first().endTime ); 352 | performanceObject[ "start_timestamp" ] = toSentryTime( spanDataList.first().startTime ); 353 | performanceObject[ "transaction" ] = spanDataList.first().description; 354 | 355 | { 356 | auto contexts = performanceObject[ "contexts" ].toObject(); 357 | QJsonObject trace; 358 | 359 | trace[ "op" ] = spanDataList.first().operationName; 360 | if ( !spanDataList.first().description.isEmpty() ) 361 | { 362 | trace[ "description" ] = spanDataList.first().description; 363 | } 364 | trace[ "span_id" ] = spanDataList.first().spanId; 365 | trace[ "trace_id" ] = traceId; 366 | trace[ "status" ] = spanDataList.first().status; 367 | if ( !spanDataList.first().data.isNull() ) 368 | { 369 | trace[ "data" ] = spanDataList.first().data; 370 | } 371 | 372 | contexts[ "trace" ] = trace; 373 | performanceObject[ "contexts" ] = contexts; 374 | } 375 | 376 | { 377 | QJsonArray spans; 378 | 379 | for ( auto spanIndex = 1; spanIndex < spanDataList.size(); ++spanIndex ) 380 | { 381 | QJsonObject span; 382 | 383 | span[ "op" ] = spanDataList[ spanIndex ].operationName; 384 | span[ "description" ] = spanDataList[ spanIndex ].description; 385 | span[ "status" ] = spanDataList[ spanIndex ].status; 386 | if ( !spanDataList[ spanIndex ].data.isNull() ) 387 | { 388 | span[ "data" ] = spanDataList[ spanIndex ].data; 389 | } 390 | span[ "parent_span_id" ] = spanDataList[ spanIndex ].parentSpanId; 391 | span[ "span_id" ] = spanDataList[ spanIndex ].spanId; 392 | span[ "trace_id" ] = traceId; 393 | span[ "start_timestamp" ] = toSentryTime( spanDataList[ spanIndex ].startTime ); 394 | span[ "timestamp" ] = toSentryTime( spanDataList[ spanIndex ].endTime ); 395 | 396 | spans.push_back( span ); 397 | } 398 | 399 | performanceObject[ "spans" ] = spans; 400 | } 401 | 402 | const auto url = QString( "%1://%2:%3%4/api/%5/envelope/" ) 403 | .arg( protocol_ ) 404 | .arg( host_ ) 405 | .arg( port_ ) 406 | .arg( path_ ) 407 | .arg( projectId_ ); 408 | const auto auth = xSentryAuth(); 409 | 410 | QNetworkRequest request( url ); 411 | request.setHeader( QNetworkRequest::ContentTypeHeader, "application/json" ); 412 | request.setRawHeader( "X-Sentry-Auth", xSentryAuth() ); 413 | 414 | QByteArray postData; 415 | 416 | postData += QJsonDocument( eventObject ).toJson( QJsonDocument::Compact ); 417 | postData += "\n"; 418 | postData += QJsonDocument( transactionObject ).toJson( QJsonDocument::Compact ); 419 | postData += "\n"; 420 | postData += QJsonDocument( performanceObject ).toJson( QJsonDocument::Compact ); 421 | postData += "\n"; 422 | 423 | handleReply( networkAccessManager_->post( request, postData ) ); 424 | 425 | return true; 426 | } 427 | 428 | void JQSentry::handleReply(QNetworkReply *reply) 429 | { 430 | QSharedPointer< bool > isCalled( new bool( false ) ); 431 | 432 | QObject::connect( reply, &QNetworkReply::finished, [ reply, isCalled ]() 433 | { 434 | if ( *isCalled ) { return; } 435 | *isCalled = true; 436 | 437 | const auto &&rawData = reply->readAll(); 438 | if ( rawData.size() != 36 ) 439 | { 440 | const auto &&data = QJsonDocument::fromJson( rawData ).object(); 441 | if ( data[ "id" ].toString().isEmpty() ) 442 | { 443 | qDebug() << "JQSentry::handleReply: data error:" << rawData.constData(); 444 | } 445 | } 446 | 447 | reply->deleteLater(); 448 | } ); 449 | 450 | #if ( QT_VERSION >= 0x050F00 ) 451 | QObject::connect( reply, static_cast< void( QNetworkReply::* )( QNetworkReply::NetworkError ) >( &QNetworkReply::errorOccurred ), [ reply, isCalled ](const QNetworkReply::NetworkError &code) 452 | #else 453 | QObject::connect( reply, static_cast< void( QNetworkReply::* )( QNetworkReply::NetworkError ) >( &QNetworkReply::error ), [ reply, isCalled ](const QNetworkReply::NetworkError &code) 454 | #endif 455 | { 456 | if ( *isCalled ) { return; } 457 | *isCalled = true; 458 | 459 | qDebug() << "JQSentry::handleReply: error:" << code << ", message:" << reply->readAll().constData(); 460 | 461 | reply->deleteLater(); 462 | } ); 463 | } 464 | 465 | QString JQSentry::getLogLevel(const QtMsgType &type) 466 | { 467 | switch ( type ) 468 | { 469 | case QtDebugMsg: return "debug"; 470 | case QtWarningMsg: return "warning"; 471 | case QtCriticalMsg: return "error"; 472 | case QtFatalMsg: return "fatal"; 473 | case QtInfoMsg: return "info"; 474 | } 475 | 476 | return "debug"; 477 | } 478 | 479 | QJsonObject JQSentry::sentryData() 480 | { 481 | QJsonObject data; 482 | 483 | data[ "timestamp" ] = toSentryTime( std::chrono::system_clock::now().time_since_epoch() ); 484 | data[ "platform" ] = "C++/Qt"; 485 | data[ "logger" ] = clientName_; 486 | 487 | { 488 | QJsonObject sdkData; 489 | 490 | sdkData[ "name" ] = clientName_; 491 | sdkData[ "version" ] = clientVersion_; 492 | 493 | data[ "sdk" ] = sdkData; 494 | } 495 | 496 | { 497 | QJsonObject contextsData; 498 | 499 | { 500 | QJsonObject osData; 501 | 502 | osData[ "name" ] = QSysInfo::productType(); 503 | osData[ "version" ] = QSysInfo::productVersion(); 504 | osData[ "type" ] = "os"; 505 | 506 | contextsData[ "os" ] = osData; 507 | } 508 | 509 | { 510 | QJsonObject browserData; 511 | 512 | browserData[ "name" ] = "Qt"; 513 | browserData[ "version" ] = QT_VERSION_STR; 514 | 515 | contextsData[ "browser" ] = browserData; 516 | } 517 | 518 | data[ "contexts" ] = contextsData; 519 | } 520 | 521 | { 522 | QJsonObject userData; 523 | 524 | if ( !userId_.isEmpty() ) 525 | { 526 | userData[ "id" ] = userId_; 527 | } 528 | if ( !userName_.isEmpty() ) 529 | { 530 | userData[ "username" ] = userName_; 531 | } 532 | if ( !userIpAddress_.isEmpty() ) 533 | { 534 | userData[ "ip_address" ] = userIpAddress_; 535 | } 536 | 537 | data[ "user" ] = userData; 538 | } 539 | 540 | if ( !release_.isEmpty() ) 541 | { 542 | data[ "release" ] = release_; 543 | } 544 | 545 | if ( !serverName_.isEmpty() ) 546 | { 547 | data[ "server_name" ] = serverName_; 548 | } 549 | 550 | return data; 551 | } 552 | 553 | QByteArray JQSentry::xSentryAuth() 554 | { 555 | return QString( "Sentry " 556 | "sentry_version=5,sentry_timestamp=%1,sentry_key=%2,sentry_secret=%3" ) 557 | .arg( 558 | QString::number( QDateTime::currentDateTime().toTime_t() ), 559 | publicKey_, 560 | "" ).toUtf8(); 561 | } 562 | 563 | QJsonValue JQSentry::toSentryTime(const QDateTime &time) 564 | { 565 | return static_cast< qreal >( time.toMSecsSinceEpoch() ) / 1000.0; 566 | } 567 | 568 | QJsonValue JQSentry::toSentryTime(const std::chrono::system_clock::duration &time) 569 | { 570 | return static_cast< double >( std::chrono::duration_cast< std::chrono::microseconds >( time ).count() ) / 1000000.0; 571 | } 572 | 573 | // JQSentrySpan 574 | QMutex JQSentrySpan::mutex_; 575 | bool JQSentrySpan::enabled_ = true; 576 | 577 | JQSentrySpan::JQSentrySpan( 578 | const QString & operationName, 579 | const QString & description, 580 | const QJsonValue &data ) 581 | { 582 | spanData_.operationName = operationName; 583 | spanData_.description = description; 584 | spanData_.status = "ok"; 585 | spanData_.data = data; 586 | 587 | spanData_.spanId = QUuid::createUuid().toString().mid( 1, 36 ).remove( "-" ).mid( 0, 16 ); 588 | 589 | spanData_.startTime = std::chrono::system_clock::now().time_since_epoch(); 590 | } 591 | 592 | JQSentrySpan::~JQSentrySpan() 593 | { 594 | spanData_.endTime = std::chrono::system_clock::now().time_since_epoch(); 595 | 596 | if ( !enabled_ ) { return; } 597 | 598 | QMutexLocker locker( &mutex_ ); 599 | 600 | if ( isCancel_ ) { return; } 601 | 602 | if ( spanDataList_.isEmpty() ) 603 | { 604 | if ( rootSpan_ ) 605 | { 606 | rootSpan_.toStrongRef()->spanDataList_.push_back( spanData_ ); 607 | } 608 | 609 | } 610 | else 611 | { 612 | spanDataList_[ 0 ] = spanData_; 613 | 614 | JQSentry::postPerformance( spanDataList_ ); 615 | } 616 | } 617 | 618 | QSharedPointer< JQSentrySpan > JQSentrySpan::create( 619 | const QSharedPointer< JQSentrySpan > &parent, 620 | const QString & operationName, 621 | const QString & description, 622 | const QJsonValue & data ) 623 | { 624 | QMutexLocker locker( &mutex_ ); 625 | 626 | QSharedPointer< JQSentrySpan > result( new JQSentrySpan( operationName, description, data ) ); 627 | 628 | if ( parent ) 629 | { 630 | result->rootSpan_ = parent->rootSpan_; 631 | result->spanData_.parentSpanId = parent->spanData_.spanId; 632 | } 633 | else 634 | { 635 | result->rootSpan_ = result.toWeakRef(); 636 | result->spanDataList_.push_back( { } ); 637 | } 638 | 639 | return result; 640 | } 641 | --------------------------------------------------------------------------------