简介
本文介绍的方式来设计一个记录器,做什么文章的描述,建议:可能什么也没有发生时尽可能快地工作。尽可能在必要时尽可能多的信息,登录。
它的设计,而不是执行的问题。我想明确指出这一点。的文章中提供的实现是非常简单的(尽管它包含了大量的C巫术),它的只有约200行代码。此外,不应该采取这种实现是严重的项目,应当制定出适应的需要。例如,您可能会增加缓冲,自动重定向到一个新文件文件变得太大时,自动删除旧文件,等等。
但此日志的设计在我看来是非常宝贵的的。我使用了一个类似的类型,在我所有的应用程序的日志记录,它已被证明是伟大的。记录
日志是一个令人惊讶的重要组成部分的方案。 ,程序变得更加复杂和大 - 更重要的是有适当的记录。在许多情况下(如调试和崩溃的调查),日志几乎所有的信息你只有件。这是很难想象没有记录运行24 / 7的服务器。
是许多不同的记录系统。有很简单的包装类写入到文件(没有任何问题,每个人都可以在几分钟内创建),也有正在与缓冲,格式化的选项更严重的伐木工人,过滤,输出选择,等还有名为quot库阿帕奇logquot;(又名log4cxx),这是一个超巨型夸夸其谈的日志库加载功能万吨。
无论选择何种库,记录有以下不可避免的缺点:性能命中
记录是沉重的。在一些关键的代码块(如内环内)登录时,记录的性能完全可以杀死。许多记录库缓存(之前,实际上将输出发送到文件/控制台/等),从而降低了负载显著。但是:他们仍然是沉重的。即使没有书面资料,输出字符串的格式是一个复杂的操作。另外,在一个多线程的环境中,追加的高速缓存的东西需要同步(即联锁操作),其中也有对性能产生重大影响。缓存可能会导致许多令人吃惊的坏消息。例如,当一个程序崩溃,所有的缓存信息丢失。不良信息/垃圾比例
通常情况下,这是难以预料的问题出在哪里出现,会有什么问题的调查有用。记录过多导致性能下降,再加上日志成为无用的废话吨加载。在另一边,禁用日志记录在一些地方是危险的 - 你可能会删除的东西,那将是非常有价值的,如果不测。
许多记录库支持过滤。也就是说,你可以把很多日志语句在代码;然而,他们可能没有真正在运行过程中记录。一些库需要重新编译这个工作,别人需要重新启动程序,有的甚至可以在运行时重新配置。在这样一种方式,您可能平时工作在非常低的日志水平(过滤掉几乎所有的东西),并在调试过程中,把记录的水平。有的图书馆甚至允许不同的库/类不同的过滤选项。
到目前为止过滤是好的。但仍然不够好。其实,这意味着,如果第一次(当日志级别低)发生崩溃 - 你几乎没有任何信息。然后你开始调试:提高日志级别,并尝试重现该问题。但有些问题很难重现,特别是那些涉及到时序,线程等也提高日志级别后,程序的行为有所不同 - 由于采伐性能的影响,时间可能会改变。其实,你可能无法重现的问题时,日志上。如何杀死两个单杆野兔
现在,让我们看看另一个日志设计。在大多数记录器,在运行过程中,你使用一种类型的功能:一些函数/宏/运营商,是应该做的日志。在我们的设计中,我们有三个操作:记录,把所谓的检查站,倾倒了所有的检查站。以下是这些操作的语法:// Actual logging.
PUTLOG("This is logged immediately. SomeStr=%s, SomeNum=%d"
<< szSomeStr << nSomeNum);
// Declaring a checkpoint
CHECKPOINT("This is a checkpoint. SomeNum=%d" << nSomeNum)
// This sends all the checkpoints into the log
Log::PrintCheckpoints();
日志和检查点声明支持可变数目的参数与printf类似的格式字符串。唯一的区别是参数LT,LT;,而不是逗号分隔。
检查站是一个临时的对象,其中包含一个表达式,它是随时可以登录,但还没有登录。它的生命宣言(大括号)的范围内。如果没有什么不好的事情发生 - 它被悄悄地拆除的范围是退出时,并没有记录。然而,如果东西不好的事情发生 - 你通话记录:PrintCheckpoints和所有检查站被追加到日志一次。
把事情说清楚,让我们来看看下面的例子:{C}
当日志:PrintCheckpoints是第一次(在最内层的范围内)的称为 - 它会看到所有三个检查站。当它被称为未来的时间 - 最内层的检查点,将已被删除,它会只看到quot; Onequot;和"Twoquot;而且,当日志:PrintCheckpoints是所谓的第三次 - 只有第一个检查站将被打印。
在上面的例子中,所有的检查站位于一个功能,但是这不是强制性的。关卡可能会在不同的功能宣称,他们仍然都是可见的。此外,如果一个函数的递归调用 - 其检查站将被视为递归以及。换句话说,检查站是可见的,直到他们的生存期到期。
让我们来看看另一个例子,这是我们的记录更多或不太现实的示范:void SomeWorkerFunc()
{
CHECKPOINT(_T("WorkerThread=%d") << GetCurrentThreadId())
// ...
// eventually process some I/O for client
pClient->ProcessIo();
// ...
}
void Client::ProcessIo()
{
CHECKPOINT(_T("Request from client ID=%u") << m_ID)
// ...
// eventually decide to send something to the client
CHECKPOINT(_T("Sending file path=%s") << szSomeFilePath)
HANDLE hFile = CreateFile(szSomeFilePath, /* ... */);
ReadFile(/* ... */);
// ...
}
void ReadFile(/* ... */)
{
CHECKPOINT(_T("Attempt to read %u bytes", dwBytes)
TestSysCall(ReadFile(hFile, pBuf, dwBytes, /* ... */));
}
void TestSysCall(BOOL bVal)
{
if (!bVal)
{
PUTLOG(_T("Error=%u") << GetLastError());
Log::PrintCheckpoints();
throw SomeException;
}
}
在这个例子中,如果没有什么不好的事情发生,什么都不会被记录。然而,如果ReadFile的失败,以下将被排放到日志:Error=187
Attempt to read 2048 bytes
Sending file path path=C:\Data\SomeFile.bin
Request from client ID=22487
WorkerThread=3047
注意:这里报告错误TestSysCall的唯一功能。它没有任何有关它发生的背景下的信息,它不知道什么起源的错误。它只有(并记录)错误代码。
是有可能实现这样一个传统的记录(检查站)的行为?理论上 - 是的。但是,这一要求的日志代码的数量巨大,看起来别扭,使程序完全不可读。代码看起来像这样:
也就是说,在这种情况下,你必须通过所有的上下文信息的所有功能和打印所有的日志语句。您将无法使用统一(上下文)功能,如I / O错误检查等常见的操作
事实上,没有人确实此。相反,人们要么不登录上下文信息,或记录它们之前有一个真正需要(我们已经讨论了这一缺点)。
事情的变化,当您使用检查点。现在,你应该毫不犹豫地提供尽可能多的信息,只在需要时将实际登录。实施细节PUTLOG
让我们开始与PUTLOG宏。它作为参数表达式,并立即记录本表达。表达式中的第一件事必须是一个格式化字符串。根据不同的multi-byte/Unicode编译器选项,字符串应该是多字节或Unicode。使用_T()宏自动使用正确的字符串,它的价值。因之参数必须由指定的格式化字符串的内容。如果没有参数需要 - 任何人都不应被指定。LT,LT;运营商(如你可能已经注意到)的参数必须分开。表达式求值一次。这是很重要的,如果表达的评价有副作用(例如:它可能包含有一些副作用的函数调用)。
正如我已经在开始时说,实施的日志是非常原始,是不应该使用。它不支持过滤。也就是说,表达的是始终评估和记录。还有没有可能选择哪个日志写入(的情况下,你可能想有几个不同的事情的日志输出)。您可能会注意到,到目前为止,我们没有看到任何记录初始化方法,也没有地方,我们可以指定哪些文件记录到。这是因为这种实现不写任何文件!它只是发送到调试输出格式化的字符串。
你应该治疗,只为骨架实施。如果你喜欢这个主意 - 提前去,把它和它适应您的需求。举例来说,如果要添加多个Logger对象的支持,你或许应该重新PUTLOG宏接受一个参数 - Logger对象。此外,您可能会支持过滤:要么运行时的过滤器选项(存储在指定的Logger对象)或编译时的日志记录的宏的定义。然而,你必须小心:不记录时,你有一个选项,评估或不给定的表达式。当然,更好地没有对其进行评估(性能提升),但小心副作用。CHECKPOINT
现在来的主要课程:检查站宏。有关如何设计和实施检查站的决定时,有某种程度的自由。我的实现的主要目标是:必须轻,并尽可能快。使用方便。
也就是说,如果它不会很方便,你会不会想用它。而且,如果这将是沉重和缓慢 - 你可能想,但不应该使用它。
为了了解究竟是什么检查点,我们首先需要认识到,在传统采伐执行哪些步骤。比方说,我们下面的日志:PUTLOG(_T("My name is %s, Elapsed time=%d")
<< GetMyName() << TimeNow() - nTimeStart);
在这个例子中,下面的步骤进行:记录表达式求值。被称为GetName和TimeNow。时间减去nTimeStart的值是计算出来的。日志记录的字符串格式化。这意味着一些缓冲区分配,呼吁建立最终的字符串和一个printf之类的函数的一个变种。或者,一些装饰品可能会做最后的字符串(开始时的时间戳,截至年底行)。该字符串被发送到输出(文件,控制台等)
CHECKPOINT宏作为一个参数的记录表达。这个表达式是立即进行评估,但最终的日志记录字符串没有被格式化,并没有发出。这意味着,只有上述链的第一步。
在这个例子中,GetName和TimeNow马上打电话,和时间减去nTimeStart价值计算。这是所有。所有剩下的步骤被推迟,直到检查点,将实际登录。这有以下影响:出色的性能。如果日志表达式不包含重的东西(如函数调用) - ,表达的评价是非常快的。由于实际的日志字符串的格式是推迟 - 表达式中的所有参数必须保持有效。
最后一句是什么意思?简单地说 - GetMyName和TimeNow返回值必须保持有效,直到检查点的生存期到期。例如,GetMyName可能会返回一个指针到一些字符串,这个字符串是最终删除/修改,而检查点还活着。这是必须避免的。
另一种变种:GetMyName而不是原始的字符串的指针返回值C字符串对象(无论是CString的,性病::字符串,或其他生物)。返回的对象是暂时的,它的寿命是有限的声明。 PUTLOG以来记录的字符串格式化立即使用这种事情有没有问题,但检查点使用,这将产生灾难性的后果。
是它可以实现检查点没有这些弊端?是的,但你必须为此付出。变种之一是立即格式的日志串。另一种方法 - 定义检查点,因此,它会立即使所有指定的字符串的副本。
付出的代价是否值得?绝对不是,太昂贵了。目前的实施过程中使用初始化短短几年的CPU周期,几个周期的清理,这是可以忽略不计在99%的情况下,我可以想像,即使是在最关键的代码块。而且,另一方面,它涉及到堆操作,重复的字符串,将数百到数千周期。这将使使用检查点不值钱。毕竟,在大多数情况下,这种考虑是无关紧要的。大多是用来检查站(至少我)没有在所有的参数,或者只是一些原始的。而且,如果它发生,你真的想用一些字符串可能过期 - 一种重复他们明确。在深入的技术细节
日志表达式解析(PUTLOG和检查点),并转换成一个记录结构,这是由以下定义:template <size_t nSize>
struct Record {
PCTSTR m_szFmt;
int m_pParams[nSize / sizeof(int)];
};
m_szFmt是格式字符串,m_pParams - 所需要的所有参数格式最后的日志串,装在printf类似的功能确认的方式。
注意:记录是一个模板结构,它的模板参数是用于格式化所需的包装参数的大小。在运行时,实际的格式化参数都放在那里,但大小须持有他们是在编译时决定。这是如何做到的呢?那么,这里涉及激烈的彗星模板巫术。我让你,亲爱的读者,拼图这一点从代码。如果我现在告诉你如何编译和工程 - 我可能会破坏的文章,你可以得到的最显着的喜悦。
PUTLOG宏建立这样的记录,并立即记录。而且,检查点宏执行以下操作:生成的记录。声明一个检查点的类,它封装上面记录的临时对象。这个对象quot; registersquot;本身在c'tor(构造)和quot; unregistersquot;本身在德TOR(析构函数)。
是什么"; registerquot;和quot; unregisterquot;意思呢?让我们来看看代码。检查点的定义是这样:struct Checkpoint {
__declspec(thread) static Checkpoint* s_pTop;
Checkpoint* m_pPrev;
// ...
template <size_t>
Checkpoint(/* ... */); // register
Checkpoint(); // unregister
};
s_pTop点最近quot; registeredquot;检查点,并m_pPrev成员之一,它取代的每一个检查点点。以这样的方式,检查站,形成一个堆栈的排序。
注意:s_pTop变量声明__declspec(线程)的语义。这意味着它是通过TLS访问。 (谁不熟悉的:TLS - quot;线程本地storagequot;这就像一个全局变量,但是,它对于每个线程是唯一的。)访问通过TLS的变量是非常快的。从性能的角度来看,这是访问一个普通变量相媲美。
__declspec(线程)不能使用,如果你的代码的一部分驻留在一个DLL。如果是这样的话 - 你必须明确访问的TLS(TlsGetValue / TlsSetValue)。您还必须确保您使用相同的TLS索引的EXE和所有的DLL,否则,您会收到几个检查站链,而只有一个,将在每个模块中可见。异常处理和检查站
在上面的例子,有一个明确的呼叫日志:PrintCheckpoints当程序检测到一个问题。特别是,其中一宗个案中,我们把它称为正是在抛出异常之前。但是,我们不能保证这个函数将被调用,在每一个被抛出的异常。有时候,我们将不得不调用一些第三方代码(如STL,MFC提高等),这是不知道我们的测井系统,它只是抛出一个异常。此外,异常可能是含蓄地提出,OS /硬件(如访问冲突)。顺便说一下,在这种情况下,它的最重要的转储的所有信息,但不幸的是,我们没有机会转储检查站。
后异常被抛出和捕获 - 倾销检查站是不可能的。因为他们不存在了。回想一下,检查站的存在,直到他们的生存期到期,异常被捕获后,堆栈已经平仓和所有自动(栈)变量被销毁。
幸运的是,有一个解决方案。有一个可能性就抛出异常后,做的东西,但之前的捕获和栈展开的发生。使用SEH(结构化异常处理),这是可能的的。那些不熟悉与SEH - 有大量的在线文档对此,我建议阅读。我也建议你阅读{A}。
这样的伎俩,我们应该可以更换或包裹传统的try / catch块与__try / __except的。这是你应该做的://///////////////////////////////
// traditional exc handling block
try {
DoSomething();
catch (/* ... */) {
Log::PrintCheckpoints(); // Oops! There's nothing there
}
/////////////////////////////////
// Now putting the __try/__except in the middle:
try {
DoSomethingGuarded();
catch (/* ... */) {
// Checkpoints already dumped
// Now - handle the exception
}
void DoSomethingGuarded()
{
__try {
DoSomething();
} __except (Log::PrintCheckpoints(), EXCEPTION_CONTINUE_SEARCH) {
// never get there
}
}
/////////////////////////////////
// Use only __try/__except, omit the traditional try/catch
__try {
DoSomething();
} __except (Log::PrintCheckpoints(), EXCEPTION_EXECUTE_HANDLER) {
// Checkpoints are already dumped.
// Now - handle the exception
}
对于那些不熟悉与SEH,可能看起来复杂。但事实上,这是并不复杂。传统的C异常处理机制的实施(至少由MSVC)通过SEH的。然而,与C异常处理机制的SEH,是隐蔽。在以"revealquot,必须使用纯SEH的。结论
我一直使用现在这种类型很长一段时间的记录(多年),它已被证明是伟大的。我恨膨胀缓慢而沉重的东西的节目,我非常的最低限度。尤其是当涉及到高性能的方案,必须从硬件的最大挤压。而且,我个人觉得,这个日志记录击中的汗水点。
其实,我quot; inventedquot;这样一个记录,当我得知有关SEH和开始使用它。这显然优于C异常处理,再加上发现,C异常SEH的实施,使SEH实际上涵盖了所有的异常和崩溃处理。 SEH的最大的优势之一是异常后,你会得到一个机会做栈展开之前发生的事情。这是非常宝贵的调试的,因为整个协议栈,包括提高代码的异常,在这一点上仍然是有效的,它包含了非常有价值的信息。
在某些时候,我开始把一些quot; decorationsquot;在栈上,因为原料栈是没有那么多可读。这些装饰品包括一个单一字符串。最后,我想这些字符串添加到一些运行时参数,但字符串格式化是沉重的。此外,格式化字符串的长度是不知道在编译的时候,因此,在运行时,它应该是分配堆(否则可能会被截断,日志字符串),这是非常杀性能。这样,我开始使用quot; deferredquot;格式。有一天,我意识到检查站不仅是好的事故调查,他们实际上可以完全取代传统的日志记录。只要转换到检查站的所有日志记录语句。这将大部分的垃圾扔掉,因为没有必要通过所有的上下文信息无处不在。而且,日志的检查站,只在必要时:时例外(在他们认为异常的情况下),并在几个地方按需。
顺便说一下,最近,我有一些经验,这是一个巨大的和非常受欢迎的记录着巨大的选项和功能库与Apache日志(又名log4cxx)。我很惊讶:它出来,在传统的采伐外,他们也有类似的东西我没有什么!这就是所谓的NDC - ";嵌套诊断contextsquot;,这是每一个线程的后进先出队列(栈)的字符串,你可以压入/弹出,它们会自动添加到每个日志消息。
然而,他们的工作,这使得他们的国家数据中心的使用非常有限的,在我看来,也只有一个部分。这是非常重要的,这一机制以快速,便捷,log4cxx的NDC失去了这两个条件。从性能的角度来看,它的可怕 - 它采用了大量堆操作,字符串重复,等,从使用的方便,它的难言之隐 - 它只是需要字符串。此外,它允许不可控的推/弹出字符串:特别,你可以把一个字符串的地方而不是范围退出后回弹出。这显然会导致一个烂摊子。为了防止这种情况,必须使用RAII包装。否则,你的quot;嵌套诊断contextquot;可能包含的信息,没有更多的实际。此外,所有的NDC字符串自动添加到每个日志消息,这是不是你想要的的。
我跟一些人真的钦佩log4cxx。我问的NDC的主要用途应该是什么。他们告诉我关于线程的东西。也就是说,在多线程应用程序中,不同的线程登录到相同的文件,以及一切混合。 NDC是很好的区分来源于哪里。
但是,这是荒谬的。人根本没有意识到这种思想的巨大潜力。我做什么,更多或更少,应NDC的最明显的用途。这只是NDC的一定要快和轻量级,它不是。
正如我已经说 - 提供的实施是一个骨架。它没有做实际的日志记录。您可以使用一些其他的日志系统,支持过滤,缓冲等的包装,如果这是适用于您的情况。
我会明白的意见,正反两方面的。欢迎新的想法和批评。