C++ sentry 如何压缩日志文件

项目中在使用 sentry 上传事件的 attachment 函数过程中发现,附带的 log 文件是未压缩的,于是有了需求,即需要在 sentry 内部将未压缩的文件流压缩后再上传给服务器

这个需求看似挺简单的,其实过程挺坎坷的,因为要看 sentry 的源码,并对 zlib 的库有一定的了解才行。

这个文章为了以后同样有此需求的人作参考。

sentry 分为 sentry 源码和 crashpad 源码两部分,日常使用中,我们可以利用 sentry 自带的崩溃自动上传机制查看崩溃时的堆栈,不过堆栈只能看见崩溃的地方(局部的几个函数),并不能知晓

用户上下文的行为,复现一个崩溃要了解用户是以什么样的“动作”触发的,所以在崩溃时能够看到调试日志,对于开发人员来说是很重要的,不过在阅读 sentry 源码发现,sentry 附加的文件是从本地读取的,

也就是说 sentry 内部拿到本地文件的路径,传给 fopen 和 fread,并且内部没有集成压缩的 api,需要我们修改源码来实现压缩。

在不大面积修改 sentry 源码的前提下,我打算在本地项目中把日志先压缩到 buffer 内,再把 buffer 传给 sentry,这样 sentry 只需要一个接受 buffer 参数的接口即可,正好 sentry 内的 sentry_attachment_s 的结构体有 buf 参数,

struct sentry_attachment_s {
    sentry_path_t *path;
    sentry_attachment_t *next;
    char *buf;
    size_t buf_len;
};

这样我们可以手动创建一个接受 buffer 的接口,比如

static void
add_attachment_buf(sentry_options_t *opts, const char *buf, size_t buf_len)
{
    if (!buf) {
        return;
    }
    sentry_attachment_t *attachment = SENTRY_MAKE(sentry_attachment_t);
    if (!attachment) {
        sentry__path_free(path);
        return;
    }
    attachment->buf = buf;
    attachment->buf_len = buf_len;
    attachment->next = opts->attachments;
    opts->attachments = attachment;
}

#ifdef SENTRY_PLATFORM_WINDOWS
void
sentry_options_add_attachment_buf(
    sentry_options_t *opts, const char *buf, size_t buf_len)
{
    add_attachment_buf(opts, buf, buf_len);
}

说明一下,sentry 内部是链表读取文件路径的,可以使用这个接口替换已有接受文件路径的接口调用

之后,我们还需要提前压缩文件,即输入未压缩的文件数据,输出压缩后的文件数据,这里我们可以使用 zlib 库,如下

int CompressToGzip(const char* input, int inputSize, char* output) {
  z_stream zs;
  zs.zalloc = Z_NULL;
  zs.zfree = Z_NULL;
  zs.opaque = Z_NULL;
  zs.avail_in = (uInt)inputSize;
  zs.next_in = (Bytef*)input;
  //  zs.avail_out = (uInt)outputSize;
  zs.next_out = (Bytef*)output;

  // hard to believe they don't have a macro for gzip encoding, "Add 16" is the
  // best thing zlib can do: "Add 16 to windowBits to write a simple gzip header
  // and trailer around the compressed data instead of a zlib wrapper"
  deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8,
               Z_DEFAULT_STRATEGY);
  deflate(&zs, Z_FINISH);
  deflateEnd(&zs);
  return zs.total_out;
}

这个函数接受未压缩文件的输入和大小,输出压缩后的数据,返回压缩后的大小,压缩格式是 .gz

调用的话就很简单了,

void Send(std::string log, int severity, sentry_extra_data extra_data) {
  int input_size;
  const char* input = file_size(L"D:\\project\\bin\\Debug\\debug.log", &input_size);
  int output_size = input_size;
  char* output = new char[input_size];
  int size = CompressToGzip(input, input_size, output);
  sentry_options_add_attachment_buf(options, output, size);
delete output;
if (!extra_data.extra_data_key.empty()) { sentry_set_extra(extra_data.extra_data_key.c_str(), extra_data.extra_data_value); } auto event = sentry_value_new_message_event(sentry_level_e(severity), nullptr, log.c_str()); sentry_capture_event(event); }

未压缩文件的输入和大小的函数如下,

char* file_size(const wchar_t* filename, int* filesize) {
  FILE* fp = _wfopen(filename, L"rb");
  fseek(fp, 0L, SEEK_END);

  *filesize = ftell(fp);

  file_buffer = new char[*filesize];

  rewind(fp);
  fread(file_buffer, 1, *filesize, fp);

  fclose(fp);
  return file_buffer;
}

另外我们还需要在 sentry 内部修改上传日志的文件名,可以参考下面这个函数,不使用 path 参数,而是手动赋值一个文件名给 *s

const sentry_pathchar_t *
sentry__path_filename(const sentry_path_t *path)
{
    const wchar_t *s = L"debug.log.gz";
    const wchar_t *ptr = s;
    size_t idx = wcslen(s);

    while (true) {
        if (s[idx] == L'/' || s[idx] == L'\\') {
            ptr = s + idx + 1;
            break;
        }
        if (idx > 0) {
            idx -= 1;
        } else {
            break;
        }
    }

    return ptr;
}

这些步骤都完成后,上传日志给 sentry 后应该就可以看到压缩的文件了


在这个需求进行到这边时,我以为差不多要完成了,发现压缩日志只在正常上传日志的事件中出现,而在崩溃时,sentry 自动上传的崩溃事件中并没有附加压缩文件,嗯,果然没那么简单

重新梳理了 sentry 源码,了解了 crash 的机制,并结合使用 sentry 过程时,需要一个 crashpad_handler.exe 程序才能完成崩溃自动上传事件,差不多摸透崩溃自动上传的工作原理。

 

CreateProcess 可以在父进程中打开子进程,is_embedded_in_dll 是判断是否有 crashpad_handler.exe,没有的话就 rundll32.exe 代替,并和平结束进程,有的话,就使用 command_line 参数,command_line 是个长字符串,

类型为 wstring,里面包括了 sentry 上报日志所需要的各种变量,比如 dsn,user_key,attachments 文件路径等等

crashpad_handler.exe 子进程就是程序崩溃后执行的,故要压缩其中的日志文件也要在 crashpad 代码模块中进行,如下,

CopyFileContent 即是读取本地文件以及写入 sentry 日志的操作,我们不需要改动它,而是新写一个 CopyCompressFileContent 函数,这个函数只需要筛选出对应的日志名字,再压缩它

注意添加头文件 

#include "third_party/zlib/zlib/zlib.h"
void CopyCompressFileContent(FileReaderInterface* file_reader,
                             FileWriterInterface* file_writer) {
  char buf[4096];
  char output[4096];
  FileOperationResult read_result;
z_stream zs;
zs.zalloc = Z_NULL;
zs.zfree = Z_NULL;
zs.opaque = Z_NULL; do { read_result = file_reader->Read(buf, sizeof(buf)); if (read_result < 0) { break; } zs.avail_in = (uInt)read_result; zs.next_in = (Bytef*)buf; zs.avail_out = (uInt)sizeof(output); zs.next_out = (Bytef*)output; deflateInit2( &zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY); deflate(&zs, Z_FINISH); deflateEnd(&zs); if (read_result > 0 && !file_writer->Write(output, zs.total_out)) { break; } } while (read_result > 0); }

有了压缩之后,同时把生成的最终文件名字改成 .gz,比如,

在程序崩溃后,我们在 sentry 不仅能够看到崩溃堆栈,也能查看调试日志以复现问题并解决掉


 一些注意的地方,zlib 库只能用于 .gz 的解压缩,不能用于 zip 的解压缩

参考:https://www.zlib.net/zlib_faq.html#faq11

在对 z_stream 赋值时,一定要确保 avail_in 和 avail_out 都有足够的大小,不然 deflate 容易出现 Z_BUF_ERROR 错误,这个错误只是个提示,并不是致命的,它告诉我们需要足够大的缓冲区才行

参考:zlib Z_BUF_ERROR with specific file and specific buffer sizes

PS: 在一开始我将 zs.avail_out 注释了,导致我花了很久才找到错误的地方(注释的原因是网上某篇教程提供的例子中这样写的,没多想)

并且我习惯新创建一个工程来测试例子的可靠性,如果没问题我再集成到项目中,在注释 zs.avail_out 的工程中,可以正常压缩文件,这使我在很长一段时间内忽略 avail_out 的赋值,而在 crashpad 工程中集成压缩代码时,

zs.total_out 会一直返回 0,导致压缩文件失败。并且 deflate 报 Z_BUF_ERROR,在对 zs.avail_out 赋值后,压缩成功,至于为何在不同项目中会成功或失败,我们要在后面花时间去研究它。

如果想要压缩为 zip 文件,则需要使用其他库,比如 minizip 库,参考:

嗯,就写到这,后续想到更多的细节会继续补充,希望能帮助到有类似需求的人

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值