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

Regex Logviewer

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (8投票s)

2012年12月23日

GPL3

8分钟阅读

viewsIcon

33704

downloadIcon

2269

利用正则表达式解析日志

新功能: 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 将在启动时更改默认过滤器。
  • 在处理大量信息时,请使用批量收集来收集多个服务器日志并使用行过滤器,这将在导入前过滤行,而不是像常规过滤器那样仅更改网格视图。
    • 此外,在输入多个目录/服务器共享进行批量处理时,*.lgs 文件(预设)是文本文件,因此编辑它们非常容易,无需 GUI。
  • 在编写正则表达式解析器时,您应该先使用正则表达式程序(如 expresso 或 regex buddy)对其进行测试。正则表达式通常会非常快,但代码中的微小改动可能导致其成为性能噩梦。
    • 尽量避免使用 .* 和 .+,使用字符类
    • 如果您知道字符数,请使用 {1,5} 来量化字段长度。
    • 使用边界将匹配的起始位置固定在 字符串 的开头和结尾。
  • 当使用日志查看器索引大量活动日志时,您应该关闭实时监听,以免被更新淹没。您可以在 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 线程中维护一个带有 progressBarForm 实例,这使得它即使在程序负载很高时也能响应。
    • 每次进度更新都会调用正确的线程,并且通过限制步骤数和检查步骤是否已更改来最小化更新。(因此,如果您有 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();
    }
}
© . All rights reserved.