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();
}
}