65.9K
CodeProject 正在变化。 阅读更多。
Home

非常易于使用的ATL/MFC/非MFC应用程序日志记录器

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.78/5 (8投票s)

2003年12月28日

13分钟阅读

viewsIcon

98260

downloadIcon

1202

非常易于使用的ATL/MFC/非MFC应用程序日志记录器。

Sample Image - EASYLogger.jpg

引言

许多程序员希望了解他们的正在运行的应用程序背后发生了什么。例如:你成功地构建了你的debug版本,但在release版本中却出现了问题,你的应用程序崩溃了。这时,你会问自己:“我的代码哪里出错了?”。你就可以通过一些日志记录来找到答案。你只需要将感兴趣的对象转储到文件中即可。但这并不是一个好主意,因为你必须一遍又一遍地重写相同的代码。另一个问题是线程安全:同一个应用程序的两个实例想在同一个文件或某种“单一用户”介质上进行日志记录。这时,我的日志记录器就派上用场了。

日志记录器本身

这个想法借鉴于Java。Java有一个名为java.util.logging.Logger的类。它是一个功能强大的日志记录系统。因此,从Java复制过来的概念包括:

  • 可以将同一条消息记录到多个位置:文件、数据库、套接字、窗口等。我的实现包括文件(控制台或磁盘文件)、数据库(任何支持标准SQL语法的数据库)和窗口(应用程序启动时会出现一个对话框)。
  • 多种日志级别。它们是:
    • SEVERE 是一个表示严重故障的消息级别。通常,SEVERE消息应描述非常重要且会阻止正常程序执行的事件。它们应该能够被最终用户和系统管理员 reasonably intelligibly 理解。
    • WARNING 是一个表示潜在问题的消息级别。通常,WARNING消息应描述对最终用户或系统管理员有用的事件,或表示潜在问题的事件。
    • INFO 是一个信息性消息级别。通常,INFO消息会写入控制台或其等效设备。因此,INFO级别应该仅用于对最终用户和系统管理员有意义的、 reasonably significant 的消息。
    • CONFIG 是一个静态配置消息级别。CONFIG消息旨在提供各种静态配置信息,以帮助调试与特定配置相关的问题。

      例如,CONFIG消息可能包括CPU类型、图形深度、GUI外观等。

    • FINE 是一个提供跟踪信息的消息级别。FINEFINERFINEST都旨在提供相对详细的跟踪。这三个级别的确切含义在不同子系统之间会有所不同,但总的来说,FINEST应该用于最详细的输出,FINER用于次详细的输出,而FINE用于最低量(也是最重要的)消息。

      总的来说,FINE级别应该用于对非特定子系统有专门兴趣的开发者来说广泛有用的信息。FINE消息可能包括一些轻微的(可恢复的)故障。指示潜在性能问题的也值得记录为FINE

    • FINER 表示一个相当详细的跟踪消息。默认情况下,调用函数进入、返回或抛出异常的日志记录在此级别进行跟踪。
    • FINEST 表示一个高度详细的跟踪消息。
  • 可以选择记录或不记录消息。这个选择是基于消息的日志级别。例如,如果我们只想记录INFOWARNINGSEVERE消息,我们将日志记录器的日志级别设置为INFO。任何级别等于或大于此级别的消息都将被记录。任何其他消息都将被丢弃。
  • 可以选择完全不进行日志记录。只需将日志记录器设置为LEVEL_OFF
  • 该日志记录器是系统范围的(每个系统只有一个)。一些优点是:
    • 您的所有应用程序都使用同一个日志记录器。这可以节省内存。日志记录器是一个进程外服务器。这意味着日志记录器和客户端应用程序不共享相同的内存空间。这在你不小心在应用程序中造成缓冲区溢出时非常有帮助。最坏的情况下,应用程序会终止,但日志记录器会保持运行。
    • 同一日志介质上的安全线程:文件、窗口、数据库等。
  • 可以选择哪个应用程序在哪个日志介质上进行日志记录。例如:App1.exe(第一个实例)->log1.logApp2.exe->log1.loglog2.logApp3.exe->所有可用的日志介质;App1.exe(第二个实例)->log3.log和/或log1.log
  • 每个日志记录位置都有一个单独的日志级别。
  • 无需重新编译应用程序即可删除或添加新的日志记录位置。您甚至可以在客户端应用程序运行时删除或添加新的日志记录位置。
  • 在客户端应用程序运行时,可以为每个日志记录位置设置日志级别。
  • 双击日志窗口可跳转到指定消息所在的文件和行。
  • 通过一个简单的宏(但不推荐)可以完全从源代码中移除所有日志记录代码。我建议你保留USE_LOGGER宏,如果你不想记录任何东西,就不要添加任何日志记录位置。之后,如果你想进行一些日志记录,你可以使用日志记录器的可视化界面来添加日志记录位置,而无需重新编译你的应用程序。在这种情况下,你必须先启动日志记录器(如果日志记录器已激活,请不要执行此操作),添加你的日志记录位置,然后启动你的应用程序。如果你在启动日志记录器之前启动你的应用程序,日志记录器将自动启动,但由于你没有定义任何日志记录位置,你的应用程序的第一条消息将会丢失(这个问题很快就会通过在日志记录器中实现保存方法来解决)。

准备项目

  1. 在zip存档中,有一个名为ready_for_use的目录。将其复制到您喜欢的位置。
  2. 创建一个项目(任何类型都可以:ATL/MFC/Win32 Console等)。
  3. LoggerWrapper.hLoggerWrapper.cpp 文件添加到您的项目,并确保您的项目可以访问该目录下的文件(请参阅下图)。如果您的项目没有 stdafx.h 文件,请注释掉 LoggerWrapper.cpp 文件顶部的 #include "stdafx.h"。如果您的项目有,请保留它。
  4. 定义 _WIN32_DCOM(请参阅下图)。
  5. #include "LoggerWrapper.h" 添加到您项目中的 stdafx.h 文件中。如果您没有,请确保将 #include 行添加到您项目最顶层的头文件中。
  6. 在应用程序初始化时,按照下面的描述添加日志记录位置。对于MFC应用程序,这一点位于 InitInstance() 函数的顶部。
  7. 只需按照下面的描述进行日志记录即可。

Sample screenshot

如何使用日志记录器

如前所述,日志记录器是系统范围的。因此,每次尝试实例化日志记录器时,服务器都会为您提供同一个实例。这意味着一个应用程序对日志记录器所做的任何更改都会影响所有使用该日志记录器的其他应用程序。这既是优点也是缺点。优点是可以从一个应用程序修改另一个应用程序的日志记录风格。缺点是可能会意外地这样做。

日志记录器可以同时以可视化和编程方式使用。接下来,我将详细解释日志记录器暴露的以编程方式使用的接口(只有3个函数)。

添加日志记录位置的函数

  1. int AddNewWindowLogLocation(BOOL customizeVisualy, DWORD ownerProcessID, LEVEL loggingLevel, DWORD* locationID, char* windowTitle,unsigned long int maxRecords, BOOL alwaysOnTop, BOOL autoScroll);

    添加一个窗口日志记录位置。记录的消息将显示在一个窗口中;双击一条记录可在MSDEV中跳转到文件/行。

    返回值:成功则返回非零,失败则返回0;

    • customizeVisualy - 如果为非零,则表示此处理程序将以可视化方式进行自定义。所有剩余的参数(locationID除外)都将被忽略。如果为零,则其余参数将按如下方式考虑:
    • ownerProcessID - 将使用此日志记录过程的进程的PID。可以是系统中的任何PID(进程ID)或-1。当指定-1时,所有正在运行和将来运行的应用程序都将使用此位置进行日志记录。当指定单个PID(!=-1)时,只有该PID可以访问此位置。所有其他传入的日志记录请求都将被丢弃。提示:使用GetCurrentProcessID()函数;
    • loggingLevel - 此位置的日志记录级别。有关详细信息,请阅读引言部分;
    • locationID - 这是函数将新创建的日志记录位置的ID放入的地方。您可以使用此ID更改日志记录位置的属性(例如:日志级别、删除日志记录位置等);
    • windowTitle - 新日志窗口的标题;
    • maxRecords - 窗口中的最大记录数。如果达到此数量,为使下一条传入消息有空间,列表中的现有记录将自动删除。
    • alwaysOnTop - 如果为非零,则窗口日志记录位置将始终保持在最顶层。(在窗口创建后无法更改此设置。这个问题将尽快解决。)
    • autoScroll - 如果为非零,则窗口将自动滚动。按鼠标左键将暂时禁用自动滚动。按鼠标右键将重新启用自动滚动。如果没有此标志,鼠标左键和右键单击将不起作用。
  2. int AddNewFileLogLocation(BOOL customizeVisualy, DWORD ownerProcessID, LEVEL loggingLevel, DWORD* locationID, char *fileName, long maxSize, int allowRead, int allowWrite, int rotate);

    添加一个新的文件位置。传入的日志消息将被保存在文件中;

    • customizeVisualyownerProcessIDloggingLevellocationID - 请参阅AdddNewWindowLogLocation
    • fileName - 将保存日志消息的文件名。不要提供任何扩展名。*_%d.log*会自动附加。%d(一个数字)- 请参阅maxSize
    • maxSize - 文件的最大大小。如果文件增长到超过此值,将创建一个新文件或覆盖现有文件。(有关详细信息,请参阅rotate);
    • allowRead - 目前保留(待实现!!!);
    • allowWrite - 目前保留(待实现!!!);
    • rotate - 如果为非零,当文件增长到超过maxSize时,文件将被截断为零。如果rotate==0,将创建一个新文件,并在文件末尾附加一个计数编号。请参阅fileName参数;
  3. int AddNewDatabaseLogLocation(BOOL customizeVisualy, DWORD ownerProcessID, LEVEL loggingLevel, DWORD* locationID, char* connectingString, char* tableName, char* levelCol, char* timeCol, char* sequenceNumberCol, char* sourceFileCol, char* sourceLineCol, char* threadIdCol, char* processIdCol, char* messageCol);

    添加一个新的数据库位置。传入的日志消息将被存储到数据库中。我强烈建议您至少使用一次可视化配置,以便正确确定连接字符串。如果您不想使用可视化界面,只需复制粘贴连接字符串即可。

    • customizeVisualyownerProcessIDloggingLevellocationID - 请参阅AdddNewWindowLogLocation
    • connectingString - 用于连接数据库的连接字符串。此字符串因数据库引擎而异。如果此字符串不正确,日志记录位置将不会被添加到日志记录器中。如果需要,请使用可视化界面构建连接字符串并复制粘贴;
    • tableName - 连接字符串中选定的数据库中用于存储日志消息的表名;
    • levelCol - 级别列的名称。此列将保存要存储的消息的级别。必须是文本列。必须至少有32个字符(字节)长。
    • timeCol - 时间列的名称。此列将保存消息的时间戳。必须是日期和时间列。必须是完整的日期和时间:年、月、日、时、分和秒,顺序任意。
    • sequenceNumberCol - 序列号列的名称。此列将保存消息的顺序。此顺序相对于日志记录位置,而不是相对于表。一个或多个数据库位置可以记录到同一个表中,因此您可能会有具有相同sequenceNumber但来自不同日志记录位置的记录。必须是数字列。必须至少有4个字节长;
    • sourceFileCol - 源文件列的名称。此列将保存消息的源代码文件路径。请参阅MSDN中的__FILE__。必须是文本列。必须至少有512个字符(字节)长;
    • sourceLineCol - 源行号列的名称。此列将保存消息的源代码行号。请参阅MSDN中的__LINE__。必须是数字列。必须至少有4个字节长;
    • threadIdCol - 线程ID列的名称。此列将保存消息的源线程。请参阅MSDN中的GetCurrentThreadID()。必须是数字列。必须至少有4个字节长;
    • processIdCol - 进程ID列的名称。此列将保存消息的源进程。请参阅MSDN中的GetCurrentProcessID()。必须是数字列。必须至少有4个字节长;
    • messageCol - 消息列的名称。此列将保存消息本身。必须是文本列。必须至少有65536个字节长。如果您的数据库引擎不允许如此大的文本列,您可以自由修改它,但您必须同时修改LoggerWrapper.h中的#define MAX_MESSAGELENGTH(请阅读文件开头以获取更多信息)。

现在让我们看一些添加日志记录位置的示例。

/*Will add a database logging location using the visual 
method (All parameters ignored)*/
DWORD locationID1;
AddNewDatabaseLogLocation(TRUE, 0, LEVEL_ALL, 
    &locationID1, NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL, NULL, NULL);
/*Will add a database logging location programatically. 
YOU MUST CHANGE THE CONNECTING STRING!!!*/
DWORD locationID2;
AddNewDatabaseLogLocation(FALSE, -1, LEVEL_ALL, &locationID2,
    "Provider=MSDASQL.1;Persist Security Info=False;Data Source=MySQLTest",
    "logrecords", "levelCol", "timeCol", "sequenceNumberCol",
    "sourceFileCol", "sourceLineCol", "threadIdCol", "processIdCol",
    "messageCol");

/*Will add a new window logging location 
using the visual method (All parameters ignored)*/
DWORD locationID3;
AddNewWindowLogLocation(TRUE, 0, LEVEL_ALL, 
    &locationID3, NULL, 0, TRUE, TRUE);

/*Will add a new window logging location programatically.*/
DWORD locationID4;
AddNewWindowLogLocation(FALSE, -1, LEVEL_CONFIG, &locationID4,
    "Logging handler added programmatically", 1024, FALSE, FALSE);

/*Will add a new file logging location using 
the visual method (All parameters ignored)*/
DWORD locationID5;
AddNewFileLogLocation(TRUE,0,LEVEL_ALL, &locationID5, NULL,0,0,0,0);

/*Will add a new file logging location programatically.*/
DWORD locationID6;
AddNewFileLogLocation(FALSE,-1,LEVEL_ALL, 
    &locationID6, "./testlog",1024*1024,1,1,0);

现在我们使用一些define来方便地进行实际的日志记录。请注意双括号()。这些define是:

  • FINEST_LOG(());
  • FINER_LOG(());
  • FINE_LOG(());
  • CONFIG_LOG(());
  • INFO_LOG(());
  • WARNING_LOG(());
  • SEVERE_LOG(());

这些宏是类似printf的宏。所以你可以这样做:

INFO_LOG(( "finest log test message: number: %d, text: '%s', double: %f",
    1234,
    "a simple text",
    12.9987 ));/*or*/INFO_LOG(("CMDIFrameWnd::OnCreate"));
if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1)
{
    SEVERE_LOG(("OnCreate function failed!!!"));
    return -1;
}

可视化使用日志记录器

使用日志记录器进行可视化操作就像用斧头杀死一只兔子一样容易(Carmagedon万岁!!!)。我认为你可以自己理解发生了什么。我必须提醒你,ready_to_use目录下的LoggerAddIn.dll文件是一个MSDEV插件。只需激活它:工具->自定义,加载项和宏选项卡->然后浏览LoggerAddIn.dll。激活后,如果你双击一个窗口日志记录位置,并且MSDEV已打开,日志记录器会将你带到日志消息对应的文件/行。

待办事项列表

  • 必须为日志记录器的各种操作创建Getter和Setter。例如:以编程方式设置/获取日志级别,或者获取当前处理程序列表,或者以编程方式删除处理程序,等等。所有这些都是通过从内部向LoggerMonitor发送一些Windows消息来完成的。总之:所有可视化接口都必须以编程方式公开。
  • 将MSDEV中的调试窗口添加为一个新的日志记录位置。
  • 您脑海中任何好的想法,请与我分享。

更新 #1

  • 现在,用于添加日志记录位置的所有3个函数都接受一个名为locationID的额外参数。您可以在设置/获取日志级别等操作中使用此ID。
  • 已添加以编程方式设置/获取日志级别。
  • 修复了一些关于从外部线程(非销毁对话框的线程)创建无模式对话框的问题。

文件结束

请原谅我的蹩脚英语。如果您对此问题有任何意见,请随时与我联系以纠正问题。我现在正在等待您的反馈。

© . All rights reserved.