考虑一种基于浏览器的Anti-Blog-Spam的方法

2007年9月22日 没有评论

上面发的这个关于blog comment spam的文章被同样的人再次spam,而且正好被Ace的两个回复夹了三明治。实在让我郁闷了一把。

既然我不能对live space做什么让它不接收spam comment,那么我只能让我自己眼不见为净了。既然必须要我自己访问自己的blog的时候才会发现自己的blog是不是被spam了并且会手动删除上面的spam comment,那么我就把这个过程自动化好了。

基于GreaseMonkey好了。

先搭个架子,然后向里面填算法来判断这个comment是不是spam,目前的话,先基于关键字好了,看到那个叫做boubo的链接就删掉。

标签: ,

TabIMSwitch 1.1.0.32

2007年9月4日 1 条评论

一边骂微软的输入法标准,一边还要继续完成我一直想做的几个feature,其中之一就是能够更加细致的控制输入法的状态,类似半角/全角的东西。

因为上文所说的原因,所以最后用了IMM,虽然功能不够多,而且不保证能够完全的控制微软拼音和日文Natural IME,但是基本上够用了。

接下来打算研究一下TSF,似乎有点意思。

标签:

标准太多是复杂的根源

2007年8月31日 没有评论

有感于微软的文字输入法的标准:

Windows 95/98/NT4: Advanced Input Method Manager
Windows 95-XP: IMM
Windows XP以上: Text Services Framework

Windows 2000兼容Windows 98的IMM架构,也有有限的TSF支持。
Windows XP下面默认使用的是TSF,IMM仍然可用,但是功能有限。

Vista下面?不知道,也许和XP差不多。

而且,TSF基于COM,如果我要用了这个模型的话,这可真是个大杂烩了。

也许搞定了之后,我可以有机会弄清楚当XPCOM和COM并存,程序会变成什么样子。

困惑

标签:

被微软的输入法打败了

2007年8月22日 1 条评论

在做TabIMSwitch的新版本的时候,彻底被微软的输入法体系搞郁闷了。

我现在有两个可用的Windows XP系统,一个是我的笔记本,XP Pro 中文版,一个是公司的机器,XP Pro英文版,两台机器都单独装了微软拼音2003(公司因为Office是英文,默认不带微软拼音;笔记本因为不愿用盗版,所以只装了Viewer,也要单独装微软拼音)。本来这个应该非常常见的情况,可是在测试TabIMSwitch的时候被彻底的搞郁闷了。

这个新版本的目的主要是为了解决smth上面有网友提出说不希望自动添加英文Locale。之前我做了一个TabIMSwitch 1.1.0.1的内部版本,这个在我的中文系统上面工作的很好。但是网友回复说仍然不能工作,后来调查之后发现可能是因为英文系统的缘故。然后果然在公司的英文XP上面重现了出来。从目前的情况来看,可能的结果就是在英文XP系统上面带有的微软拼音2003都无法支持,如果不想自动添加英文Locale的话。

造成这个问题的原因很有意思,说起来应该从TabIMSwitch的工作方式开始。

TabIMSwitch的工作机理很简单:TabIMSwitch由两个主要的部分,一个是前端的content\firefoxOverlay.xul及其带有的一批js文件,另一个就是用C++写成的XPCOM组件,位于src\tabimswitch目录下面。两者之间通过Firefox的XPConnect机制联系。XPCOM最初做的很简单,只是封装了一下Windows的KeyboardLayout的系列函数。当Firefox启动之后,会自动加载XPCOM组件和前端的xul文件,并且在xul当中进行必要的消息的hook。当TabIMSwitch发现当前的标签页发生了变化的时候,就会获取到Firefox当前的输入法状态,并将其存入到一个列表当中,然后从这个列表当中获取即将切换到的标签页的输入法(以前保存下来的),并且将其设置成Firefox的当前输入法。

问题在于,微软关于输入法的说明非常的零散,不过基本上围绕在Keyboard Layout和IME两节当中。根据文档的描述,微软拼音输入法和“中文-美式键盘”之间的区别就在于它们是不同的Keyboard Layout。因此,只要存下来这个Keyboard Layout就OK了。TabIMSwitch 1.0.*系列也是这么实现的。

但是就是这种简单的实现在英文版XP+微软拼音输入法上面不工作,经过跟踪之后发现,原因竟然在于,在这种环境下面微软拼音和“中文-美式键盘”之间是完全相同的:无论通过GetKeyboardLayout函数还是GetKeyboardLayoutName函数,得到的都是相同的Keyboard Layout。当时在1.0.*系列当中为了解决这个问题,就自动Load了一个04090409的Keyboard Layout,也就是“英文-美式键盘”,来避免和微软拼音的冲突。所以就有了前面网友提出的那个问题。

后来考虑是不是可以通过IME接口来对输入法进行更加细致的控制,结果非常无奈:似乎对于IME接口来说,微软拼音和“中文-美式键盘”也是相同的。我找到了微软MSDN当中提供的一个sample,揭示了IME接口的使用方式。但是就使这个示例程序,在我的英文XP下面也歇菜了。

目前只能说:完全被微软自己的两个东西搞疯掉了。

标签:

TabIMSwitch的一个里程碑

2007年8月11日 3 条评论

TabIMSwitch被收录在addons.mozine.cn上面了。

http://addons.mozine.cn/firefox/610/

昨天被管理员添加进去的,今天已经有了73下载量,和之前在googlecode上面的累积量差不多。

hoho

标签:

TabIMSwitch接下来的工作

2007年7月28日 4 条评论

接下来对于TabIMSwitch还有一些东西需要实现,一些是现在不具有的功能,一些是现在已知的一些问题。

  • 计划当中占有很重要的比重的一个特性还没有实现:当焦点转换到地址栏的时候,无论当前的输入法是什么,都需要转换成英文输入法。
  • 目前缺少必要的垃圾回收机制,可能会有轻微的内存泄露(多少呢?大概打开一个tab页十几个字节),需要加上。
  • 需要申请一个合适的地方做更新。(XPI可以有一个“更新URL”的选项,目前指到了一个不存在的地方)。
  • 我很懒,所以希望可以根据URL的pattern(正则表达式或者通配符)来自动启用或者禁用输入法。(这个可能要些功夫实现)。

大体上能够想得到的就这么多。还有就是找一个托管的地方把代码放上去,正在考虑sourceforge还是google code……

update:
最后选定使用Google Code,原因之一是有人说Google Code起步比较容易,不像sourceforge需要填一大堆的表格;原因之二是对于Google的好感微笑。

TabIMSwitch将使用BSD Lisence发布,不使用GPL的原因可以写一篇很长的blog,这里地方太小,写不下。
项目主页:http://tabimswitch.googlecode.com

Firefox自动切换输入法的插件 TabIMSwitch 1.0开始Alpha

2007年7月28日 2 条评论

我期望当中的小东西终于可以工作了,因为目前只在我的XP SP2+FF2.0上面测试通过,而且我的两个FF版本一个干净的什么都没有,一个插件成堆,不敢保证兼容性,先暂时带着一个Alpha好了。用Firefox并且在Windows下面的朋友们帮我测试一下。

如果你已经在使用Firefox了,可以点击安装插件。安装并重启Firefox之后,在地址栏输入
chrome://tabimswitch/content/testing.xul
然后回车,会看到一个很简单的页面,这个页面是用来检测后台的模块是否正确的注册给了Firefox。点击上面的“Show current”,如果弹出来这样一个窗口,那么表明这个东西基本可以workHTML了,如果你碰巧和我一样在用微软拼音,点点另外两个试试看好了。

插件能够正确工作的图片

接下来你需要做的,就是尽情的在各个标签页之间切换,然后感受一下不同的标签页能够具有不同的输入法的好处吧。眨眼

如果遇到什么bug,或者有什么新的建议,在这里留言,或者发信到下面两个邮箱:
zhangc AT ustc.edu
ftofficer.zhangc AT gmail.com
垃圾邮件很多,所以标题要容易理解一点……

OK,这也算是我曾经写过的帖子的后续吧。

标签:

Effective Debug Logging 的一个索引

2007年7月25日 没有评论

在这个系列文章当中我把经验总结成了30个Tips,由于散布在文章各处,不便查找,因此在这里建立一个索引,以便可以方便的引用。

  • 设计目标
    • Tip 1:调试日志的设计应该以“便于定位错误”为中心目标
  • 输出调试日志的方式
    • Tip 2:选用合适的日志输出方式,以便于收集和处理为目标设计。用久经考验的方案,输出到stdout/stderr或具有统一文件名格式的日志文件。
  • 调试日志的分级和分类
    • Tip 3:使用日志分级显示的机制,在正常运行当中控制日志的输出量,调试情况下输出尽可能细致。
    • Tip 4:在日志当中记录当前日志的级别,便于拿到日志之后的过滤和分析。
    • Tip 5:添加调试日志的工作越简单,开发人员越喜欢在代码当中添加调试日志。
    • Tip 6:即使需要对于每个模块分别控制日志级别,最好也能够提供一个全局的日志级别控制选项。
    • Tip 7:在子系统级别上,让不同的子系统把日志输出到不同的日志文件。
    • Tip 8:在子系统内部,对于不同的模块,使用不同的模块标识,便于开发人员进行分析和过滤。
    • Tip 9:使用一些tricks来简化调试日志的添加,减少开发人员在写调试日志时需要考虑的问题。
  • 如何写调试日志
    • Tip 10:在日志条目当中包含必要的元信息,例如时间、PID/TID、日志级别和分类、代码文件、代码行、函数等。
    • Tip 11:在调试日志的时间当中,包含必要的时区信息。
    • Tip 12:在调试信息当中包含可以区分不同操作对象的键值,以便保证对同一对象操作的日志的相关性。
    • Tip 13:在每一个存在错误处理的地方,在错误处理分支上添加错误级别的日志,在正常流程上添加信息或调试级别的日志。
    • Tip 14:对于业务逻辑造成的条件分支,在每个分支的入口处添加日志,保证任何分支路径都在调试日志覆盖之下。
    • Tip 15:尽量保证每条日志信息的完整性,根据通用的句法模式来组织每一条调试信息。
    • Tip 16:在日志当中输出错误代码时,同时输出错误代码的类别。
    • Tip 17:在线程函数的开始和结束处添加日志,并提供能够简单的区分线程类型的字符串。
    • Tip 18:在进行可能比较耗时的操作之前和之后,添加日志进行跟踪,以便发现异常情况或进行简单的性能分析。
    • Tip 19:在对象的构造、析构和拷贝处添加调试日志,其中包含对象的地址或其他可标示该对象的键值,用于跟踪对象的生命周期。
    • Tip 20:如果必要,可以在较低的调试级别上提供对象或协议数据的转储(dump),以便分析。
    • Tip 21:如果必要,在日志当中输出软件运行的环境信息,例如软件的配置和操作系统信息。
    • Tip 22:如果使用异常,则尽可能在异常当中包含完整的可以构建一个错误日志的元信息,并在catch异常处将其打印到调试日志。
  • 如何分析调试日志
    • Tip 23:使用切分和过滤工具将日志根据相关性划分成多个小的日志模块进行分析。
    • Tip 24:当需要跨越子系统进行跟踪时,可以使用日志组合工具将日志组合之后再进行切分。
    • Tip 25:尝试将已知问题的日志模式加以记录,以便可以在问题到来时容易的确定是否是已知问题。
    • Tip 26:使用工具定位PID变动的位置并查找特征字符串,可以检测到程序的崩溃。
    • Tip 27:通过分析日志之间的时间差,可以检测性能问题或死锁。
    • Tip 28:通过分析日志当中资源分配和释放的配对情况,可以用来检测内存或资源泄漏。
    • Tip 29:使用自动化工具来辅助路径跟踪。
    • Tip 30:工具只是辅助,人的耐心、洞察力和经验才是分析日志的最重要资本。
  • 标签:

    Effective Debug Logging (part 4)

    2007年7月25日 没有评论

    这是这个系列的第四部分,也是最后一部分,我们将讨论如何有效的分析调试日志,并给出一些典型的日志分析方法和经验。

    如何分析调试日志

    当支持人员或者开发人员拿到调试日志时,通常的分析方法是先找到用户遇到的问题所对应的日志,从这里出发,进行跟踪,并分析出现这种问题的原因。在这个过程当中,存在三个必要的步骤:发现问题所对应的日志,跟踪,分析原因,这也是分析调试日志的一般方法。

    在整个过程当中,发现问题所对应的日志所在往往会消耗大量的时间。主要原因之一是分析人员拿到的日志往往是最低调试级别的,往往日志的条目非常巨大,而且可能不同
    的模块、不同的线程的调试日志混杂在一起,导致难于分析。因此对于如此大量日志,通过必要的辅助工具支持能够大大的提高效率。

    第一种辅助工具是日志过滤和切分工具。根据日志的所属的不同模块、级别,或者根据线程或日志的相关性键值,对日志进行过滤和切分,将其划分成多个小的相关的日志模块,将极大的方便进行分析。这种工具可以用脚本实现,或者用grep过滤,甚至直接使用编辑器的查找功能(如ultraedit的“列出所有行”功能),通常将在很多时候就足够了,因此不做细致的讨论。

    Tip 23:使用切分和过滤工具将日志根据相关性划分成多个小的日志模块进行分析。

    有的时候问题可能出现在不同子系统之间的交互之上,或者对于某一个对象的操作会跨越多个子系统,而不同的子系统的日志文件可能具有不同的文件前
    缀。因此,必要时可以用日志组合工具将多个子系统的日志组合之后,再经过日志切分工具进行切分。这种操作通常就需要两个子系统之间保持一定的日志格式的规
    约,以便工具可以容易的找到相关性的键值并根据这个键值进行组合。

    Tip 24:当需要跨越子系统进行跟踪时,可以使用日志组合工具将日志组合之后再进行切分。

    对于划分好的子模块的分析就是发挥人类洞察力的时候了,人类的智慧就应该用在这种地方:)。而一旦通过某种方式发现了问题所对应的日志,接下来的工作就相对简单了。而一旦发现了问题所在,我们建议将这种问题所对应的日志抽取出来适当的模式,并将过程或结果加以记录,用可运行的脚本或需要手动操作的条文都可以。这样未来就可以根据这些积累,快速的确定已知问题。

    Tip 25:尝试将已知问题的日志模式加以记录,以便可以在问题到来时容易的确定是否是已知问题。

    对于某些特定的问题,我们已经有了相应的日志模式。

    第一类问题是程序崩溃,这种情况很容易通过分析PID变动之前的日志查出原因,因此,用某种工具快速的定位PID变动的地方,并查找附近的特征字符串,即可知道是否发生了崩溃。

    Tip 26:使用工具定位PID变动的位置并查找特征字符串,可以检测到程序的崩溃。

    第一类问题是性能问题或死锁。这也是Tip
    18的来源。我们可以通过求出操作开始之前和结束之后的两条日志之间的时间差值来得到这个操作持续的时间,或者识别这些操作是否完成,是否陷
    入了无限等待或超时等等。这种模式很容易通过自动化工具来找到。而对于性能问题,可以通过工具分析日志,统计其中最耗时的一些操作,并对其进行统计学分析,对于实际用户环境当中的性能瓶颈也可以有很好的预测,对于将来的设计和改进也是非常重要的资料。

    Tip 27:通过分析日志之间的时间差,可以检测性能问题或死锁。

    另一种问题是内存或资源的泄漏。虽然这种问题可能不是可以仅仅通过调试日志解决的。但是在有的时候,唯一能够用来调试内存泄漏的只有调试日志,因此先从
    调试日志当中分析,有时也能够发现一些问题,至少可以排除掉一些可能。如果根据Tip
    19的建议,在对象的构造、拷贝、析构的时候都加上了必要的日志,那么通过简单的脚本扫描所有的相关的日志项即可知道是否所有的对象都得到了正确的析构
    了。如果需要对于内存泄漏问题进行更多的分析,也可以建立一种机制,在日志当中记录所有的内存分配和释放的操作,并加以分析。

    Tip 28:通过分析日志当中资源分配和释放的配对情况,可以用来检测内存或资源泄漏。

    在发现了问题之后,接下来的就是根据日志进行程序执行路径的跟踪。由于可能需要经过复杂的程序执行路径才能够出现问题,完全的手动跟踪是一件非常消耗人力物力的事情。由于在日志当中的记录了代码文件和行数,以及函数名,这种动作也可以一定程度上通过工具自动化寻找下面的执行路径,加快跟踪的速度。

    Tip 29:使用自动化工具来辅助路径跟踪。

    虽然我们一直在强调辅助工具的重要性,但更重要的还是日志的清晰程度和分析人员的耐心以及洞察力,而这种洞察力往往需要通过经验的积累才能获得。工具只能够用来辅助查找已知问题和给出建议,人的努力才是发现问题、解决问题的最终关键。

    Tip 30:工具只是辅助,人的耐心、洞察力和经验才是分析日志的最重要资本。

    总结

    至此,这个四个部分的文章告一段落。我写这篇个系列文章的目的在于希望能够总结和分享有关调试日志经验,并为接下来的工作当中对于调试日志的书写、处理的标准化作一些思考。这些虽然都经验,但毕竟是一家之言,难免狭隘甚至错误,希望有更多的人可以参与讨论,给出更多的建议和分享更多的经验。

    标签:

    Effective Debug Logging (part 3)

    2007年7月23日 1 条评论

    在前面的两个部分当中,我们讨论了将调试日志写到什么地方,以及如何对调试日志进行分级和分类。但是至今我们仍然没有涉及到最核心的问题:应该在什么地方写调试日志?应该怎么写调试日志?这就是这一部分的主题。

    如何写调试日志

    如何写调试日志的核心问题就是,应该在什么地方写什么内容。在考虑写日志的时候,我们的最基本的考量仍然是在第一部分当中提到的核心标准:如何便于添加、便于分析处理,进而容易定位错误。

    现在考虑我们应该在调试日志当中包含哪些内容,让我们能够从各个方面来定位错误。首先毋庸置疑,必要的元信息是应该被打印出来的,例如调试日志被打印出来的时间,而根据第二部分的建议,调试日志的级别和分类信息也是必要的。为了便于跟踪多线程程序,把进程ID和线程ID包含在其中也是天经地义的事情。如果为了便于开发人员定位日志在代码当中的位置,把打印日志的语句所在的代码文件、代码行以及函数名称包含在其中也是必要的。幸运的是这些问题在一个好的日志记录模块之下,大多数都可以通过一些辅助函数或系统内置的宏(例如__FILE__,__LINE__,__FUNCTION__)自动获取和添加,不需要开发人员去过多关心。因此需要开发人员添加日志时真正需要关心的问题其实只剩下了这条日志的级别和日志的消息字符串了。

    Tip 10:在日志条目当中包含必要的元信息,例如时间、PID/TID、日志级别和分类、代码文件、代码行、函数等。

    在这里单独把时间的输出提出来讨论一下。在时间的打印上,有多种方式可供选择。一般的说,我们比较倾向于使用标准的格式来打印时间,如RFC822当中规定的标准Internet时间格式,或者asctime输出的格式,同时在其中包含必要的时区信息。使用标准的格式打印时间,可以使得日志便于被脚本程序处理,因为主流的脚本程序都有相应的库来进行时间的解析;添加必要的时区信息是为了得到更多的关于客户环境的信息,例如夏令时切换和非标准的整小时时区。

    Tip 11:在调试日志的时间当中,包含必要的时区信息。

    日志的消息字符串是日志当中灵活性最大的部分。正因为灵活性大,开发人员也往往可能遗漏某些信息,导致在拿到用户处搜集的调试日志之后,无法进行有效的分析。根据我的一些经验,在这里就如何包含有效的信息给出一些讨论。

    第一个需要考虑的问题是日志的相关性。例如一个采用异步IO模型的服务器程序,通过有限的几个线程来服务大量的会话,因为每个线程都可能服务多个会话,而每个会话可能先后由不同的线程来服务,因此需要在日志当中添加某种可以将这些日志关联起来的键值,通过这个键值,分析日志的开发人员就可以将对于同一个对象的操作聚合在一起进行分析。这个键值应该可以唯一地标示所操作的对象(例如会话对象),比如可以使用对象的地址、会话当中socket的值,业务相关的对象ID如邮件的Message ID等等。

    Tip 12:在调试信息当中包含可以区分不同操作对象的键值,以便保证对同一对象操作的日志的相关性。

    另一个问题是保证根据拿到的日志可以对程序的执行路径进行跟踪。一个简单的方法就是在各个条件分支处添加合适的日志,保证每个分支都被日志覆盖。通常条件分支分为两类,一类是错误处理的分支,另一类是业务逻辑相关的分支。对于前者,比较常见的就是在函数调用结束之后,对于返回值得检查,例如

    FILE* pf = fopen(fileName, "r");
    if ( NULL == pf ) {
       DebugLog(LOG_ERROR, "open file %s to read failed. CRT errno=%d", fileName, errno);
       // Other error handling code
    }

    DebugLog(LOG_INFO, "Open file %s to read succeeded", fileName);

    在这个例子当中,无论进入哪个分支,均有调试日志打印出来。而且,在正常的运行状态下,客户可以将日志级别调到错误或更高,这样除非出错,用户不需要操心大量的日志输出所占用的磁盘空间和造成的效率损失。通过在错误处理分支上添加错误级别的日志,在正常流程上添加信息或调试级别的日志,能够有效的跟踪错误发生之前的程序执行路径。

    而对于业务逻辑造成的条件分支,也可以通过类似的方式,在每个分支的入口处添加日志。在理想的情况下,任何条件分支语句都应该可以被调试日志覆盖到。

    Tip 13:在每一个存在错误处理的地方,在错误处理分支上添加错误级别的日志,在正常流程上添加信息或调试级别的日志。

    Tip 14:对于业务逻辑造成的条件分支,在每个分支的入口处添加日志,保证任何分支路径都在调试日志覆盖之下。

    上面的例子还展示了打印调试日志时需要注意的另外几个问题。为了使得日志容易理解,最好让日志根据通用的句法来组织,例如例子当中,就根据句法“对什么做了什么操作,结果如何,为什么”来组织的。相反,如果在打开文件出错的时候仅仅打印出"Cannot open file",其实对于排错并没有什么用处:拿到这个日志的开发人员既无法知道打开哪个文件出错,也不知道为什么出错。

    这里列出一些通用的句法模式:

    • [操作] [某对象] [成功/失败],(如果失败,失败原因是[某原因])
      例子:Open file [d:\test.txt] for read failed. Win32 Last Error=2. The system cannot file the file specified.
    • 现在[条件]成立,需要做[某种操作]
      例子:Command [TEST] is not recoginized, will ignore it.
    • 现在系统处在[某种状态],(如果不是正常状态,什么原因)
      例子:Virus scanning is disabled, invalid Activation Code is provided.
      例子2:SMTP server status is SMTP_EHLO_ISSUED.
    • [某对象]进行了[某种操作]
      例子:Server thread #1 started.
    • [某对象]即将进行[某种操作]
      例子:Will send response [220 ready] to socket 0x1234.

    Tip 15:尽量保证每条日志信息的完整性,根据通用的句法模式来组织每一条调试信息。

    在上述句法当中,常常需要输出出错的原因,我们可以采用几种方式输出:可以通过打印错误代码实现,或者将其转换为易于理解的字符串打印。前者比较容易加入程序当中,但是后者更便于察看。但是建议即使使用后者的方式,同时也打印出原始的错误代码,因为对于开发人员来讲,错误代码更具有确定性,也更容易从代码的角度定位错误的原因。与此同时,在打印错误代码时建议将“这是什么类型的错误代码”打印出来,因为不同的库和代码使用的错误码体系也不同,在一个稍稍复杂的Windows程序当中,就至少存在CRT errno,Win32 Last Error,COM HRESULT,NTSTATUS,Winsock Last Error,可能还包括应用程序自定义的错误代码。如果ä¸
    打印出“什么类型的错误码”,则很容易将这些错误码混淆,使得难于跟踪。例如在例子当中,输出"CRT errno=%d",这样分析日志的时候开发人员可以去CRT的errno可能的取值当中去查找对应的原因,如果单单输出"error no=%d",则可能造成混淆,例如13在CRT errno当中表示Access Denied,而在Win32 error code当中表示invalid data。

    Tip 16:在日志当中输出错误代码时,同时输出错误代码的类别。

    在多线程程序当中,因为不同的线程可能打印的日志类型也不同,例如代理服务器软件的服务端线程和客户端线程所打印的日志显然不同。在需要使用脚本等工具自动分析的情况下,需要能够简单的区分线程的类型。同时,由于线程的生命周期可能比整个进程要短,因此在线程入口函数的开始处和结束处输出必要的信息,并且在开始处提供能够区分不同线程的日志信息,例如"Server listen thread #1 started",或者"Client listen thread #1 exit with code 0x00"等等。

    Tip 17:在线程函数的开始和结束处添加日志,并提供能够简单的区分线程类型的字符串。

    有的时候,线程可能需要等待某个同步对象,因为等待的结果多种多样,并且可能因为其他线程调用TerminateThread或类似的函数导致其退出,因此建议在等待操作之前打印一条日志,并在等待之后根据等待的结果打印出另一条日志,这样的好处在于,开发人员可以从日志当中发现线程非正常中止或非正常的等待结果。同样的建议适用于需要进行一些耗时的操作的时候,例如调用socket的IO相关函数的时候。这种日志有时也可以在没有性能分析器(Performance Profiler)的时候进行简单的性能分析。

    Tip 18:在进行可能比较耗时的操作之前和之后,添加日志进行跟踪,以便发现异常情况或进行简单的性能分析。

    与线程的生命周期跟踪类似,对于对象的构造和析构最好也能够进行适当的跟踪。这种日志信息建议添加在构造函数、拷贝构造函数、析构函数和赋值运算符当中。使用这种跟踪信息,可以方便对于内存泄漏的检查,也是和工具的分析。

    Tip 19:在对象的构造、析构和拷贝处添加调试日志,其中包含对象的地址或其他可标示该对象的键值,用于跟踪对象的生命周期。

    对于某些具有复杂状态的对象,有可能需要在跟踪时了解各个操作之后的对象状态,尤其当对象牵涉到复杂的线程同步的时候。因此可能需要为某些对象提供类似dump的操作,并在需要的时候将dump的结果写入调试日志。或者对于协议相关的问题,可以在每次收到数据之后或发送数据之前dump数据并写入日志,以便于更加细致的分析。这种日志可能非常消耗执行时间,因为他们可能被频繁的打印出来,而且可能每次的数据量都非常大,因此这种日志通常情况下建议使用非常低的级别,至少不应高于Debug或Tracing级别,如果必要,甚至可以单独成为一个级别。

    Tip 20:如果必要,可以在较低的调试级别上提供对象或协议数据的转储(dump),以便分析。

    有时候软件产品的配置可能非常复杂,而不同的配置往往意味着不同的行为和对于环境的不同的依赖。因此,处于排错的需要,可以在日志当中输出一些与用户环境相关的信息,例如当前操作系统的版本,以及必要的配置项的值。但是如果使用这种方法,注意不要泄漏用户隐私,例如密码散列,或者其他隐私数据。

    Tip 21:如果必要,在日志当中输出软件运行的环境信息,例如软件的配置和操作系统信息。

    最后讨论一下异常的使用。通常情况下,虽然异常对于写出简单的程序非常有效,但是往往在出错时难于跟踪。因为异常往往会沿着调用栈依次向上传递,而在最外层被catch。这样,即使在最外层的catch处,其实也很难对于错误的细节原因给出答案。而C++标准库所给出的预定义异常数量又太少,因此使用try-catch往往是分析调试日志的大敌。为了尽可能的减少使用异常对分析调试日志带来的难度,建议在throw exception的地方将尽可能详细的信息保存在exception对象当中,究竟保存哪些信息可以借鉴一条完整的错误日志应该具有的信息,例如抛出异常的语句所在的代码行和代码文件,抛出异常的原因,必要的相关信息,例如无法打开的文件名。而无论在什么地方这个异常被catch,都应该将这些信息从中dump出来,并打印成调试日志。

    显然,这样会极大的增加异常对象的复杂程度,但是我们认为,为了让调试日志更加容易理解和跟踪,这种复杂程度是必要的,而且与调试日志的打印类似,我们可以通过一些宏或者辅助函数来达到简化编程的目的。

    Tip 22:如果使用异常,则尽可能在异常当中包含完整的可以构建一个错误日志的元信息,并在catch异常处将其打印到调试日志。

    所有的这些写日志的建议,最终的目的还是能够容易的被人看懂,已经能够容易的被自动化程序分析。现在我们有了组织良好的日志文件,如何对其进行分析,这就是下一部分要讨论的问题。

    标签: