Regex Logviewer






4.97/5 (8投票s)
利用正则表达式解析日志
新功能: Regex Logviewer 现在有了 Codeplex 页面,欢迎大家参与。
更新: 版本1.4是对所有代码进行的彻底重构
- 添加了自动检测模式,可将不同日志自动集成到单一视图中
- 日志处理已分离为一个可分离的引擎组件
- 移除了代码中对数据集的使用
- 添加了 Ctrl+F 搜索功能

引言
日志包含海量信息,尤其在今天服务器每分钟都会收到成千上万的请求时,但从中筛选信息非常困难且回报不高,因为日志中充斥着无用行和错误级别的声明,而当您考虑多台服务器、日志分区和多系统架构时,这个问题会更加复杂。
这个日志查看器是我花费几年时间一点一点编写出来的,目的是为了能够快速地筛选和排序日志,以便在调查问题时获取所需信息。
所以,以下是 Regex Logviewer 目前的一些功能:
- 使用正则表达式解析您的日志(可为您自己的任何日志创建解析器)
- 性能和内存优化
- 处理多达一百万行日志(甚至更多)
- 实时监听日志变化
- 自动检测给定文件的正确解析器
- 支持拖放
- 可以从日志创建 CSV 摘要,以显示错误分布/重要性
- 支持多重排序和过滤
- 能够将多个日志文件集成到一个统一的界面中
- 可以与 .log 文件类型关联
- 代码页可通过配置调整/无配置时自动检测
- 能够从多台机器/目录收集日志文件
- 已在 winXp, win7, win8 上测试并通过
背景
我的日志查看器项目始于2007年左右,当时我需要一个好的日志查看工具,但找不到任何符合我要求(简单的用户界面、快速解析以及我所需的所有排序/过滤功能)的像样的工具。
经过多次在线搜索,我非常惊讶地发现竟然没有很棒的应用程序能帮我完成我的工作,于是我决定自己写一个——就像很多人之前做的那样(在我当时工作的项目中已经存在另外两个日志查看器,但由于过于复杂、笨重或过时而没有被使用)。
第一个版本包含一个硬编码的解析器,用于我的日志格式,该格式相当复杂,因为它需要处理那些破坏传统日志行格式的令人抓狂的异常。尽管如此,它还是奏效了,并且像实时监听这样的功能也很快跟进。
在几次调整后,我意识到硬编码的解析器浪费了良好的显示/过滤基础设施,为了使其更通用,我将解析代码移至使用正则表达式,在学习了正则表达式的先行断言后,它能很好地处理所有非 XML 日志结构(扁平的 XML 结构还可以,但树状 XML 不太适合这种方法)。
配置程序
日志行为配置存储在可执行文件所在目录的 BehaviorConfig.xml 文件中,其中包含如下 XML 条目:
<LogBehavior>
    <BehaviorName>STLog4Net</BehaviorName>
    <Grade>0</Grade>
    <!--C# date format: http://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx-->
    <DateFormat>yyyy-MM-dd HH:mm:ss,fff</DateFormat>
    <ParserRegexPatternCData><![CDATA[
    ^(?<date>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2},\d{3})\s[\w.\s]*\[\s*(?<thread>
        [\w\d]*)\]\s*(?<level>\w*)\s*[-\s]*(?<info>.*?)\n(?<exinfo>.*?)(?=\d{4}-\d{2}|\z)
    ]]></ParserRegexPatternCData>
    <ParserRegexOptionsString>IgnoreCase Multiline Singleline</ParserRegexOptionsString>
  </LogBehavior>  
- 正则表达式存储在 cdata中,因为正则表达式中的某些字符可能会与 XML 字符(如 '<')发生冲突。
- 在识别日志段时,请使用以下日志查看器已知的组名:- date= 日期,应为- <DateFormat>标签中指定的日期格式
- info= 日志行中通常表示的信息
- exinfo= 后续的异常行或其他扩展信息(一行或多行)
- level= 日志级别
- thread= 线程的名称/编号
- user= 写入日志的用户(对应用服务器很有用)
- machine= 计算机名称(如果日志中存在,有助于读取多日志文件)
 
- ParserRegexOptionsString 包含 正则表达式选项,包括:- IgnoreCase- 指定不区分大小写的匹配
- Multiline- 多行模式。改变了 ^ 和 $ 的含义,使其匹配行的开头和结尾
- Singleline- 改变了点 (.) 的含义,使其匹配每个字符(而不是除了 '\n' 之外的所有字符)
- IgnorePatternWhitespace- 从模式中删除未转义的空格
- RightToLeft- 指定搜索方向是从右到左而不是从左到右
 
- DateFormat - 包含解析日志中日期的格式- 使用 C# 日期格式,该格式在此 处 进行了描述。
 
- 关于上面示例中显示的正则表达式:- 以 ^(开始)符号开头意味着我们期望在行的最开头进行匹配。
- 多个已知的命名组(见上文),看起来像这样:(?<date> ....)
- 最后,我们有一个 (?=) 子句,这是一个先行断言,只有从这一点向前查看的字符匹配给定的模式时才会匹配。声明的模式是下一日志行的日期部分,或者 \z = 已解析字符串的结尾,这意味着我们通过遇到新日志行或文件末尾来完成匹配(就像人类会做的那样)。这是解析异常行和其他破坏日志文件模式的行的诀窍。
 
性能陷阱
我成功地使用这个日志查看器处理了多达 500K 条不同的加载日志行,它在这方面表现相当不错,在内存消耗和速度方面都有很好的性能(它确实会变得相当大,但也在预期范围内)。
为了最大限度地利用此工具,您可以接受以下提示作为通用指南:
- 在解析长批文件时,您应该打开一个过滤器(例如 level=ERROR),以避免由于网格显示事件导致的解析缓慢。- 懒得设置?在 logViewer.exe.config 文件中设置 DefaultLogLevel = ERROR将在启动时更改默认过滤器。
 
- 懒得设置?在 logViewer.exe.config 文件中设置 
- 在处理大量信息时,请使用批量收集来收集多个服务器日志并使用行过滤器,这将在导入前过滤行,而不是像常规过滤器那样仅更改网格视图。- 此外,在输入多个目录/服务器共享进行批量处理时,*.lgs 文件(预设)是文本文件,因此编辑它们非常容易,无需 GUI。
 
- 在编写正则表达式解析器时,您应该先使用正则表达式程序(如 expresso 或 regex buddy)对其进行测试。正则表达式通常会非常快,但代码中的微小改动可能导致其成为性能噩梦。
- 当使用日志查看器索引大量活动日志时,您应该关闭实时监听,以免被更新淹没。您可以在 LogViewer.exe.config 中使用 "LiveListeningOnByDefault = False" 来默认关闭它。
关于代码
虽然代码大部分不复杂,但包含了一些有趣的点:
- 实时监听过程会保存每个日志行的位置,并定期轮询检查更改。- 如果日志小于记住的位置,则从头开始解析,因为这通常表明日志已被清空并重新创建或循环(旧日志移至 log.1,日志文件被重新创建以避免日志文件过大)。
- 下面可以看到执行轮询的计时器滴答代码:
 private void timer1_Tick(object sender, EventArgs e) { List<string> list = new List<string>(); list.AddRange(m_colWatchedFiles.Keys); foreach (string file in list) { long lngPrevLength = m_colWatchedFiles[file]; if (File.Exists(file)) { long lngFileLength = (long)new FileInfo(file).Length; //file was swapped, and a new file was created => smaller filesize if (lngPrevLength > lngFileLength) { //we will adjust our counters to keep track with the file. //(the following code will take care of the rest as usual) m_colWatchedFiles[file] = 0; lngPrevLength = 0; } //file changed (more entries were added) if (lngPrevLength < lngFileLength) { long lngNewLength = ParseLogFileRegExp(file, lngPrevLength); m_colWatchedFiles[file] = lngNewLength; if (!chkPinTrack.Checked && dataGridView1.Rows.Count > 0) dataGridView1.FirstDisplayedCell = dataGridView1.Rows[0].Cells[0]; } } } lblCount.Text = "Total Count: " + m_dvMainView.Count; lblMemory.Text = "Used Ram: " + ((double)Process.GetCurrentProcess().WorkingSet64 / 1000000d).ToString(".00") + " MB"; }
- 核心解析被导入到 DataTable中,这简化了过滤和绑定到datagrid的过程。我曾尝试将其转换为 Linq+WPF,并且将来可能会这样做,但这需要时间,然后我将不得不再次关注其性能。
- 关于性能,进行了一些调整:- 由于数据行在过滤之前创建,并且我发现 DataRow的创建会将其附加到它所属的表中,因此我使用一个虚拟表来创建行,然后仅在它们匹配过滤器时才将它们导入到实际表中(有点像使用一个中间struct)。之后会处理掉虚拟表,从而防止内存过度消耗。
- 内存消耗会受到监控并在主窗体底部报告。
- 进一步的性能提升可能来自于多线程解析,但由于 DataTables绝对不是线程安全的,因此需要调用更简单的(poco 实体)数据结构,这意味着更多的工作。
- 说了这么多,它运行得很顺畅,没有损坏,为什么要修复呢?
 
- 由于数据行在过滤之前创建,并且我发现 
- ProgressBarManger是一个- static类,它在另一个 GUI 线程中维护一个带有- progressBar的- Form实例,这使得它即使在程序负载很高时也能响应。- 每次进度更新都会调用正确的线程,并且通过限制步骤数和检查步骤是否已更改来最小化更新。(因此,如果您有 100 个步骤,最多只能进行 100 次增量更改)。
 
请参阅下面的 progressBarManager 代码。
public static void CreateInThread()
{
    m_frm = new FrmProgressBar();
    m_frm.SetLableText(m_labelText);
    m_frm.SetTotalProgressSteps(m_intProgressSteps);
    //creation is done by Application.run, which runs a new message pump in the thread.
    Thread t = new Thread((ThreadStart)delegate
    {
        Application.Run(m_frm);
    });
    //apartment nust be STA to run GUI
    t.SetApartmentState(ApartmentState.STA); 
    //background threads close when main thread is done.
    t.IsBackground = true;
    t.Start();
    while (m_frm.Visible == false)
        Thread.Sleep(50);
}
public static void ShowProgressBar(long intFullProgressBarValue)
{
    if (m_frm ==null)
        CreateInThread();
    
    m_intFullProgressBarValue = intFullProgressBarValue;
    
    m_frm.Invoke((ThreadStart)delegate
    {
        if (!m_frm.Visible)
            m_frm.Show();
        m_frm.ProgressBarControl.Value = 0;
    });
}
        
static string m_labelText = "Adding Files";
public static void SetLableText(string text)
{
    m_labelText = text;
    if (m_frm != null)
    {
        if (m_frm.InvokeRequired)
        {
            m_frm.Invoke((ThreadStart)delegate
            {
                m_frm.SetLableText(text);   
            });
        }
        else
        {
            m_frm.SetLableText(text);
        }
    }
}
public static void SetProgress(long intermediateValue)
{
    int newValue = (int)(((double)intermediateValue / 
      (double)m_intFullProgressBarValue) * (double)m_intProgressSteps);
    if (newValue > m_intProgressSteps)
        newValue = m_intProgressSteps;
    m_intIntermediateValue = intermediateValue;
    if (newValue > m_intPrevValue && m_frm!=null)
    {
        if (m_frm.InvokeRequired)
        {
            m_frm.Invoke((ThreadStart)delegate
            {
                m_frm.ProgressBarControl.Value = newValue;
                m_intPrevValue = m_frm.ProgressBarControl.Value;
                m_frm.SetLableText(m_labelText);
                m_frm.Invalidate();
                m_frm.Refresh();
            });
        }
        else
        {
            m_frm.ProgressBarControl.Value = newValue;
            m_frm.SetLableText(m_labelText);
        }
        m_intPrevValue = newValue;
        Application.DoEvents();
    }
}


