Qt日志管理

本文是对Qt调试技术的部分运用,并将其作为日志管理使用。 Qt提供了对日志按信息类型及日志类别进行分类处理、自定义日志处理程序及日志样板的能力,且这些能力都是线程安全的。

信息类型 Message Type


Qt将日志信息分为调试(Debug)、通知(Info)、警告(Warning)、严重(Critical)及致命(Fatal)等五种类型。

日志类别 Logging Category


日志类别用于表示日志记录的某一区域。每种日志类别,都可以按信息类型进行配置,以决定是否启用每种信息类型的记录。但致命错误不在此列。

对于所有的日志类别,都可以在编译时通过宏QT_NO_DEBUG_OUTPUTQT_NO_INFO_OUTPUTQT_NO_WARNING_OUTPUT决定对应的日志类别是否启用。

创建类别对象

通常情况下,通过Q_DECLARE_LOGGING_CATEGORY()Q_LOGGING_CATEGORY()声明和创建对象。

// 头文件,声明
Q_DECLARE_LOGGING_CATEGORY(coreScene)

// 源文件,定义
Q_LOGGING_CATEGORY(coreScene, "core.scene")

所有的类别对象都应当由此进行配置,且不同的对象也可表示同一日志类别。因此不建议跨模块导出及直接操作类别对象,或子类化QLoggingCategory。 对于在不同部分或跨库使用同一日志类别,可通过Q_DECLARE_EXPORTED_LOGGING_CATEGORY()使用。使用该宏声明的类别对象将附加EXPORT_MACRO限定,从动态库中导出该类别。 对类别命名,遵循如下约束:

  • 名称仅使用字母与数字
  • 名称使用半角句号进一步分类
  • 避免使用debuginfowarningcritical等作为类别名称
  • 带有前缀qt的类别名称仅供Qt模块使用

Q_LOGGING_CATEGORY()隐式定义的QLoggingCategory对象将在首次访问时以线程安全方式创建

检查类别配置

通过QLoggingCategory提供的isDebugEnabled()isInfoEnabled()isWarningEnabled()isCriticalEnabled()isEnabled()等方法来检查给定消息类型是否应当记录。 qCDebug()qCWarning()qCCritical()等宏可防止在未启用相应类别的消息类型时对参数求值,因此无需进行显式检查。

// usbEntries()仅在driverUsb类被启用时被访问
qCDebug(driverUsb) << "devices: " << usbEntries();

默认类别配置

QLoggingCategory构造函数和宏Q_LOGGING_CATEGORY()都接受一个可选的QtMsgType参数,该参数会禁用所有严重性较低的消息类型。 如下声明,将忽略QtDebugMsgQtInfoMsg类型的消息。

Q_LOGGING_CATEGORY(driverUsbEvents, "driver.usb.events", QtWarningMsg)

默认情况下,除前缀为qt的Qt内部类别不记录QtDebugMsg类型信息外,所有信息都会记录。 信息类型是否记录,不受C++构建配置的影响。亦即无论调试,还是发布模式,这些信息的输出都不受影响。

类别配置

通过设置日志记录规则或安装自定义过滤器可以覆盖类别的默认配置。

日志规则

日志规则用于控制对应类别日志是否记录。规则的每行以如下文本形式配置:

<category>[.<type>] = true|false

<category>是类别的名称,可以用*作为第一个或最后一个字符的通配符号;也可以在这两个位置上都用*作为通配符号。可选的<type>必须是debuginfowarningcritical。不符合此规定的配置将被忽略。 规则按文本顺序先后加载。适用于同一类别、类型的规则,靠后的生效。 规则支持多种形式的配置方式,它们按如下顺序先后加载:

  1. 配置文件[QLibraryInfo::DataPath]/qtlogging.ini
  2. 配置文件 QtProject/qtlogging.ini
[Rules]
*.debug=false
driver.usb.debug=true
  1. C++方法 QLoggingCategory::setFilterRules()
QLoggingCategory::setFilterRules("*.debug=false\n"
                                 "driver.usb.debug=true");
  1. 指向配置文件的环境变量 QT_LOGGING_CONF
QT_LOGGING_CONF=/path/to/ini
  1. ;分割的规则环境变量 QT_LOGGING_RULES
QT_LOGGING_RULES="*.debug=false;driver.usb.debug=true"

对于配置文件,规则会从Rules部分加载。而QtProject表示QStandardPaths::GenericConfigLocation返回的所有路径。 通过设置QT_LOGGING_DEBUG,可以找出日志记录规则的加载路径。

C++ 宏 C++ Macros


每种信息类型,Qt都提供了全局C++宏用于输出该类型信息。普通宏使用默认的日志类别,而分类宏允许自定义日志类别。

枚举 类型 普通宏 分类宏 说明
QtDebugMsg 调试 qDebug() qCDebug() 自定义调试信息
QtInfoMsg 4 通知 qInfo() qCInfo() 通知信息
QtWarningMsg 1 警告 qWarning() qCWarning() 应用程序、库的警告及可恢复性错误信息
QtCriticalMsg、QtSystemMsg 2 严重 qCritical() qCCritical() 严重错误信息及系统错误信息
QtFatalMsg 3 致命 qFatal() qCFatal() 退出前报告致命错误信息

消息处理程序

消息处理程序是从Qt日志基础设施中打印调试、信息、警告、严重和致命消息的函数。默认情况下,Qt使用一个标准的消息处理程序,该程序会根据操作系统和Qt配置,将消息格式化并打印到不同的接收器中。可以安装自己的消息处理程序实现将消息记录到文件系统等自定义消息处理方式。

注意

由于Qt支持将相关消息按日志类别分组,并根据类别和消息类型启用或禁用日志记录,并且日志过滤在创建消息之前完成。因此禁用的类型和类别的消息将无法到达消息处理程序。

样版化

Qt可以通过调用qSetMessagePattern()或设置环境变量QT_MESSAGE_PATTERN,提供更多的元信息来丰富日志信息。在自定义的消息处理程序中,可以使用qFormatLogMessage()来保持这种格式。

约束

  • 消息处理程序必须是可重入的。因为它可能会被不同的线程并行调用。亦即向公共接收器(如数据库或文件)的写入往往需要同步。
  • 消息处理程序本身应尽量精简,以免阻塞。为避免递归,在消息处理程序中生成的任何日志信息都将被忽略。
  • 消息处理程序应始终返回。对于致命消息,应用程序会在处理完该消息后立即终止。

安装

通过qInstallMessageHandler()为应用程序安装消息处理程序。应用程序同时只有一个消息处理程序。如果以前安装过自定义消息处理程序,函数将返回一个指向它的指针。可以通过再次调用该方法重新安装该处理程序。调用qInstallMessageHandler(nullptr)将恢复默认的消息处理程序。

#include <QApplication>
#include <stdio.h>
#include <stdlib.h>

QtMessageHandler originalHandler = nullptr;

void logToFile(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
    QString message = qFormatLogMessage(type, context, msg);
    static FILE *f = fopen("log.txt", "a");
    fprintf(f, "%s\n", qPrintable(message));
    fflush(f);

    if (originalHandler)
        *originalHandler(type, context, msg);
}

int main(int argc, char **argv)
{
    originalHandler = qInstallMessageHandler(logToFile);
    QApplication app(argc, argv);
    ...
    return app.exec();
}

消息样板

通过函数qSetMessagePattern()或宏QT_MESSAGE_PATTERN可以控制默认消息处理程序的输出内容。

占位符

占位符 描述
%{appname} QCoreApplication::applicationName()
%{category} 日志类别
%{file} 源文件路径
%{function} 函数名
%{line} 所处源文件行号
%{message} 实际消息
%{pid} QCoreApplication::applicationPid()
%{threadid} 线程编号
%{qthreadptr} QThread::currentThread()
%{type} "debug", "warning", "critical" 或 "fatal"
%{time process} 进程启动到此消息经历的秒数
%{time boot} 系统启动到此消息经历的秒数
%{time [format]} 消息生成的系统时间,通过QDataTime::toString格式化。默认使用Qt::ISODate格式
%{backtrace [depth=N] [separator="..."]} 调用栈回溯,默认深度为5,分隔符为“|”。目前仅适用于gibc

条件

只有条件满足时,其内容才会输出。

%{if-<conditional>}……%{endif}

当前支持按信息类型及是否默认类别进行判断。

条件 说明
debug 是否调试信息
info 是否通知信息
warning 是否警告信息
critical 是否严重信息
fatal 是否致命信息
category 是否带类别

默认样板

%{if-category}%{category}: %{endif}%{message}

样板设置

可以通过环境变量QT_MESSAGE_PATTERN和函数qSetMessagePattern()设置样板。同时设置的情况下,环境变量优先。

QT_MESSAGE_PATTERN="[%{time yyyyMMdd h:mm:ss.zzz t} %{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{file}:%{line} - %{message}"
qSetMessagePattern("[%{time yyyyMMdd h:mm:ss.zzz t} %{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{file}:%{line} - %{message}")

类别过滤器

过滤器提供对日志类别更精细的控制。通过QLoggingCategory::installFilter安装过滤器,用于确定启用哪些类别和信息类型。如果为nullptr,则启用默认的过滤器。

安装过滤器时,每个已存在的QLoggingCategory对象,都会传递给过滤器。过滤器通过调用setEnabled()修改每个类别的配置。未修改的类别配置将保留上一次的设定。

static QLoggingCategory::CategoryFilter oldCategoryFilter = nullptr;

void myCategoryFilter(QLoggingCategory *category)
{
    // For a category set up after this filter is installed, we first set it up
    // with the old filter. This ensures that any driver.usb logging configured
    // by the user is kept, aside from the one level we override; and any new
    // categories we're not interested in get configured by the old filter.
    if (oldCategoryFilter)
        oldCategoryFilter(category);

    // Tweak driver.usb's logging, over-riding the default filter:
    if (qstrcmp(category->categoryName(), "driver.usb") == 0)
        category->setEnabled(QtDebugMsg, true);
}
……
oldCategoryFilter = QLoggingCategory::installFilter(myCategoryFilter);

过滤器可能在不同的线程中调用,但不会被同时调用。并且在处理过程中,应当注意不要调用QLoggingCategory的任何静态函数。