Mars-XLog

596 阅读10分钟

1. 介绍

xlog是微信官方的终端基础组件mars中可以独立使用的日志模块,是一个使用c++编写的业务行无关,平台性无关的基础组件。目前已接入微信Android、IOS、Mac、Window、WP等客户端。

1.1 优点

  1. 使用C++高性能记录日志,避免了使用Java在加密,压缩过程中产生的大量GC。

  2. 使用mmap,既有直接写内存的性能,又有直接写文件的可靠性。

    1. 使用mmap,避免了写文件时内核空间和用户空间频繁切换,以及dirty page回写时带来的不可控的CPU峰值。

      1. 写文件时,Dirty page的回写时机:

        1. 定时回写。
        2. 调用 write 的时候,发现 dirty page 占用内存超过系统内存一定比例。
        3. 内存不足。
      2. mmap的回写时机:

        1. 不设置 MAP_NOSYNC 情况下 30s-60s(仅限FreeBSD,一款类UNIX操作系统)
        2. 调用 msync 或者 munmap
        3. 进程 crash
        4. 内存不足
    2. 使用mmap,避免了使用内存作为写文件的中间buffer时,当程序被系统杀死或发生crash,在内存中的日志信息丢失。

  3. 默认提供压缩功能。

    1. 先压缩再加密。效率比先加密再压缩高。因为压缩后数据量变少,并且加密前重复字符也比较多(时间等)。
    2. 流式压缩日志,避免多条日志集中压缩导致的cpu峰值导致程序卡顿。
    3. 一定大小作为一个压缩单位,即使压缩单位中有部分数据单位损坏,不影响这个单位中损坏部分之前的数据,只影响这个单位中损坏部分之后的数据。
  4. 默认提供 ECDH+TEA的混合加密算法。

    1. ECDH密钥协商层(安全通道建立)
    2. TEA数据加密层(高效传输)
  5. 启动时清理日志,避免占用户空间。

2. 如何引入

2.1 Gradle

dependencies {
    implementation 'com.tencent.mars:mars-xlog:1.2.6'
}

与官方文档说明不同,该方式一样能使用加密

2.2 本地编译

2.2.1 安装环境

  1. 安装cmake,在安装过程中勾选添加到环境变量,或者手动把cmake的bin目录配置到环境变量中。

  2. 安装python,要求3.10版本以上。在安装过程中勾选添加到环境变量,或者手动配置环境变量指向python路径。

    1. python从3.6版本开始,在命令行时输入的不是python、python3,而是py

    2. 如果是在Windows 11上,可能会出现运行时启动应用商店的情况。打开设置,在应用>高级应用设置>应用执行别名,关闭应用安装程序即可。

  3. 下载ndk-r20b,并配置环境变量 NDK_ROOT 指向 ndk 路径。

  4. 如果是Windows系统还要安装cygwin,要安装其中的make,gcc,gbd,然后把cygwin的bin目录配置到环境变量中。

2.2.2 编译

  1. 所有的编译脚本都在mars/mars 目录, 运行编译脚本之前也必须cd到此目录,在当前目录下运行,默认是编译 armeabi-v7a,arm64-v8a的,如果需要其他 CPU 架构,把编译脚本中的aarchs = {'armeabi-v7a', 'arm64-v8a'}稍作修改即可。

  2. 执行命令

       py build_android.py
    
  3. 执行后,会让选择

       Enter menu:
       1. Clean && build mars.
       2. Build incrementally mars.
       3. Clean && build xlog.
       4. Exit
       ```
    
  4. 选择3

  5. 输出结果全部在 mars/mars/libraries/mars_xlog_sdk 目录中,把mars_xlog_sdk/src目录下的java文件,以及mars_xlog_sdk/libs目录下的so文件复制到你的项目中。

3. 如何使用

3.1 初始化XLog

在Application或着别的地方初始化XLog

private void initXLog() {
    final String logPath = this.getFilesDir() + "/xlog";
    // this is necessary, or may crash for SIGBUS
    final String cachePath = this.getFilesDir() + "/xlog_cache";

    // privateKey = "145aa7717bf9745b91e9569b80bbf1eedaa6cc6cd0e26317d810e35710f44cf8"
    String publicKey = "572d1e2710ae5fbca54c76a382fdd44050b3a675cb2bf39feebe85ef63d947aff0fa4943f1112e8b6af34bebebbaefa1a0aae055d9259b89a1858f7cc9af9df1";
    String logFileNamePrefix = "xlog";
    // debug包最低输出Verbose级别的日志,release包最低输出Info级别的日志
    int logLevel = BuildConfig.DEBUG ? Xlog.LEVEL_VERBOSE : Xlog.LEVEL_INFO;
    Xlog.open(true, logLevel, Xlog.AppednerModeAsync, cachePath, logPath, logFileNamePrefix, publicKey);

    Xlog xlog = new Xlog();
    // debug包输出日志到控制台,release包则不输出
    xlog.setConsoleLogOpen(0, BuildConfig.DEBUG);
    Log.setLogImp(xlog);
}

注意如果App使用了多进程,不要把多个进程的日志输出到同一个文件中,保证每个进程独享一个日志文件。而且保存 log 的目录请使用单独的目录,不要存放任何其他文件防止被 xlog 自动清理功能误删。

3.2 输出日志

像Android的Log一样使用XLog的Log,例如:

Log.d(TAG, Message)

3.3 关闭日志

在退出程序时关闭日志:

Log.appenderClose();

不要在Application的onTermincate()中调用,因为这个方法在模拟器上才会被回调

 /**
* This method is for use in emulated process environments.  It will
* never be called on a production Android device, where processes are
* removed by simply killing them; no user code (including this callback)
* is executed when doing so.
*/
@CallSuper
public void onTerminate() {
}

可以在例如App的MainActivity#onBackpress()这些退出App的方法中调用

4. 查看日志

4.1 从设备下载日志文件

如果按 3.1 的代码设置日志路径,getFilesDir()对应的路径是 /data/data/你的应用包名/files,这个目录在手机文件管理器是看不到的,可以通过Android Studio的Device Explorer,找到xlog目录下的日志文件

4.2 解密日志文件

4.2.1 仅压缩

如果不想使用加密模块,public key 参数设置为空字符即可,解密脚本使用 decode_mars_nocrypt_log_file.py, 但这样日志会只压缩不加密。

4.2 使用第三方解密软件

  1. 下载 compose-multiplatform-xlog-decode 或者 YXlogDecode

  2. 用软件打开日志文件,并设置 privateKey

  3. 解密日志

4.3 使用官方解密方式

参考 XLog加密使用指引 配置环境并解密日志

5. 源码分析

5.1 初始化

调用链:

Xlog.open()
    ->Java_com_tencent_mars_xlog_Xlog_appenderOpen()
         ->appender_open()
             ->XloggerAppender.NewInstance()
                 ->XloggerAppender.open()
             ->xlogger_SetAppender()
             ->BOOT_RUN_EXIT()      

各步骤代码:

// 在 Xlog.java 文件中
public static void open(boolean isLoadLib, int level, int mode, String cacheDir, String logDir, String nameprefix, String pubkey) {
    if (isLoadLib) {
        System.loadLibrary("c++_shared");
        System.loadLibrary("marsxlog");
    }

    XLogConfig logConfig = new XLogConfig();
    // ... 初始化XLogConfig的代码 ... 
    appenderOpen(logConfig);
}

private static native void appenderOpen(XLogConfig logConfig);
// 在 Java2C_Xlog.cc 文件中
JNIEXPORT void JNICALL Java_com_tencent_mars_xlog_Xlog_appenderOpen(JNIEnv* env, jclass clazz, jobject _log_config) {
 // ... 将Java层数据转换为C++层数据 ...
 appender_open(config);
 xlogger_SetLevel((TLogLevel)level);
}
// 在 appender.cc 文件中
void appender_open(const XLogConfig& _config) {
    // ... 非法情况判断 ...
    sg_default_appender = XloggerAppender::NewInstance(_config, sg_max_byte_size);
    // ... 初始化sg_default_appender ...
    xlogger_SetAppender(&xlogger_appender);
    BOOT_RUN_EXIT(appender_release_default_appender);
}
  1. XloggerAppender::NewInstance()
// 在 appender.cc 文件中

XloggerAppender* XloggerAppender::NewInstance(const XLogConfig& _config, uint64_t _max_byte_size) {
    return new XloggerAppender(_config, _max_byte_size);
}

/**
 * thread_async_ 的初始化使用了 boost::bind 来绑定成员函数 __AsyncLogThread ,
 * 当线程启动时会执行 this->__AsyncLogThread(),
 * __AsyncLogThread 是XloggerAppender的私有成员函数,用于异步日志写入的核心逻辑
 */
XloggerAppender::XloggerAppender(const XLogConfig& _config, uint64_t _max_byte_size)
: thread_async_(boost::bind(&XloggerAppender::__AsyncLogThread, this)), max_file_size_(_max_byte_size) {
    Open(_config);
}

/**
 * 该函数主要完成以下工作:
 * 1. 创建缓存目录
 * 2. 创建日志目录
 * 3. 初始化mmap内存映射文件
 * 4. 将上一次退出app时保留在缓存文件中的数据写入日志文件中
 * 5. 写入初始化日志头信息
 */
void XloggerAppender::Open(const XLogConfig& _config){
    // ... 其它代码 ...
    
    // region 1. 创建缓存目录
   
    // 当缓存路径是一个非空字符串,创建缓存目录并启动后台线程,删除过期的日志缓存文件、移动旧日志缓存文件
    if (!config_.cachedir_.empty()) {
        boost::filesystem::create_directories(config_.cachedir_);

        thread_timeout_cache_ =
            std::make_unique<comm::Thread>(boost::bind(&XloggerAppender::__DelTimeoutFile, this, config_.cachedir_));
        thread_timeout_cache_->start_after(2 * 60 * 1000); // 两分钟后启动
        thread_moveold_ = std::make_unique<comm::Thread>(boost::bind(&XloggerAppender::__MoveOldFiles,
                                                                     this,
                                                                     config_.cachedir_,
                                                                     config_.logdir_,
                                                                     config_.nameprefix_));
        thread_moveold_->start_after(3 * 60 * 1000);  // 三分钟后启动
        // ... 其它代码 ...
    }
    
    // endregion
    
    //  region 2. 创建日志目录 
    
    // 启动后台线程,删除日志目录下的过期日志文件
    thread_timeout_log_ =
        std::make_unique<comm::Thread>(boost::bind(&XloggerAppender::__DelTimeoutFile, this, config_.logdir_));
    thread_timeout_log_->start_after(2 * 60 * 1000); // 两分钟后启动
    // 创建日志目录
    boost::filesystem::create_directories(config_.logdir_);
    
    // endregion
    
    // ... 其它代码 ...
    
    // region 3. 初始化mmap内存映射文件
    
    char mmap_file_path[512] = {0};
    // 初始化mmap内存映射文件的路径
    snprintf(mmap_file_path,
             sizeof(mmap_file_path),
             "%s/%s.mmap3",
             config_.cachedir_.empty() ? config_.logdir_.c_str() : config_.cachedir_.c_str(),
             config_.nameprefix_.c_str());
    bool use_mmap = false;
    // 将磁盘文件映射到进程的虚拟内存地址空间,kBufferBlockLength是映射的大小(150KB),mmap_file_用于接收成功映射后的文件对象
    if (OpenMmapFile(mmap_file_path, kBufferBlockLength, mmap_file_)) {
        // 如果使用ZSTD压缩模式(由Facebook开发,采用有限状态熵(FSE)编码技术,在压缩率、速度和资源消耗之间实现更优平衡。)
        if (_config.compress_mode_ == kZstd) {
            log_buff_ = new LogZstdBuffer(mmap_file_.data(),
                                          kBufferBlockLength,
                                          true,
                                          _config.pub_key_.c_str(),
                                          _config.compress_level_);
        } else {
            // 如果使用ZLIB压缩模式(基于LZ77与哈夫曼编码结合的DEFLATE算法,压缩率中等,但压缩和解压速度较慢。)
            log_buff_ = new LogZlibBuffer(mmap_file_.data(), kBufferBlockLength, true, _config.pub_key_.c_str());
        }
        use_mmap = true;
    } else {
        // 如果mmap文件映射失败,使用普通的内存分配方式创建日志缓冲区
        char* buffer = new char[kBufferBlockLength];
        if (_config.compress_mode_ == kZstd) {
            log_buff_ =
                new LogZstdBuffer(buffer, kBufferBlockLength, true, _config.pub_key_.c_str(), _config.compress_level_);
        } else {
            log_buff_ = new LogZlibBuffer(buffer, kBufferBlockLength, true, _config.pub_key_.c_str());
        }
        use_mmap = false;
    }
    
     if (nullptr == log_buff_->GetData().Ptr()) {
        if (use_mmap && mmap_file_.is_open())
            CloseMmapFile(mmap_file_);
        return;
    }
    
    //  endregion
    
    // region 4. 将上一次退出app时保留在缓存文件中的数据写入日志文件中
    
    AutoBuffer buffer;
    // 将日志缓冲区中的数据刷新到buffer中
    log_buff_->Flush(buffer);
    
    // ... 其它代码 ...
    
    // 检查buffer中是否有数据
    if (buffer.Ptr()) {
        // 写入提示信息,表示mmap数据的开始
        WriteTips2File("~~~~~ begin of mmap ~~~~~\n");
        // 将buffer中的数据写入日志文件
        __Log2File(buffer.Ptr(), buffer.Length(), false);
        // 写入提示信息,表示mmap数据的结束,并附带标记信息
        WriteTips2File("~~~~~ end of mmap ~~~~~%s\n", mark_info);
    }
    
    // endregion
    
    // region 5. 写入初始化日志头信息
    
    // appender_info存储日志追加器信息
    char appender_info[728] = {0};
    // 格式化日志追加器的信息,包括日期、时间和标记信息。这是每次open的时候会打印的信息,方便定位问题。
    snprintf(appender_info, sizeof(appender_info), "^^^^^^^^^^" __DATE__ "^^^" __TIME__ "^^^^^^^^^^%s", mark_info);
    // 将日志追加器的信息写入mmap3日志缓存文件
    Write(nullptr, appender_info);

    char logmsg[256] = {0};
    // 格式化获取mmap数据所花费的时间,并存储到logmsg数组中
    snprintf(logmsg, sizeof(logmsg), "get mmap time: %" PRIu64, (int64_t)get_mmap_time);
    // 将获取mmap数据所花费的时间写入mmap3日志缓存文件
    Write(nullptr, logmsg);

    // 写入MARS项目的URL信息
    Write(nullptr, "MARS_URL: " MARS_URL);
    // 写入MARS项目的路径信息
    Write(nullptr, "MARS_PATH: " MARS_PATH);
    // 写入MARS项目的版本信息
    Write(nullptr, "MARS_REVISION: " MARS_REVISION);
    // 写入MARS项目的构建时间信息
    Write(nullptr, "MARS_BUILD_TIME: " MARS_BUILD_TIME);
    // 写入MARS项目的构建标签信息
    Write(nullptr, "MARS_BUILD_JOB: " MARS_TAG);

    // 格式化日志追加器的模式和是否使用mmap的信息,并存储到logmsg数组中
    snprintf(logmsg, sizeof(logmsg), "log appender mode:%d, use mmap:%d", (int)config_.mode_, use_mmap);
    // 将日志追加器的模式和是否使用mmap的信息写入日志文件
    Write(nullptr, logmsg);

    // 检查缓存目录是否为空
    if (!config_.cachedir_.empty()) {
        // 获取缓存目录的空间信息
        boost::filesystem::space_info info = boost::filesystem::space(config_.cachedir_);
        // 格式化缓存目录的空间信息,包括总容量、可用空间和空闲空间,并存储到logmsg数组中
        snprintf(logmsg,
                 sizeof(logmsg),
                 "cache dir space info, capacity:%" PRIuMAX " free:%" PRIuMAX " available:%" PRIuMAX,
                 info.capacity,
                 info.free,
                 info.available);
        // 将缓存目录的空间信息写入日志文件
        Write(nullptr, logmsg);
    
    // endregion 
    
}

6. 业务场景

6.1 如何捕获Java层的crash信息?

 // 系统默认的Exception为 KillApplicationHandler
Thread.UncaughtExceptionHandler systemHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((thread, ex) ->
        {
            Log.e(TAG, android.util.Log.getStackTraceString(ex));
            if (systemHandler != null) {
                systemHandler.uncaughtException(thread, ex);
            }
        }
);