AlphaLog





5.00/5 (1投票)
一个用于过滤和合并大型文本文件的工具。
引言
AlphaLog 是一个开发人员工具,能够解析、合并和过滤各种日志文件。该工具支持插件模型,允许开发人员创建与其特定日志文件类型兼容的插件。
该工具仍处于积极开发中,还有一些功能尚缺失。但是,我觉得现在是开始记录该工具的好时机。我将随着工具中功能的添加而更新本文。

背景
在我的工作中,我遇到过一些场景,需要研究多个系统生成的日志文件,以了解导致错误的事件链。这些系统通常会生成各种格式的日志文件,这使得合并文件变得具有挑战性。此外,我经常发现当将系统日志设置为调试模式时,生成的信息量使得分析日志文件变得困难。
通常我只对特定时间范围内的特定日志条目感兴趣。因此,我需要一个工具,能够合并和过滤以各种不同格式生成的日志条目。
使用代码
下图提供了 AlphaLog 结构的基本概述

我已尽力使 AlphaLog 的 API 尽可能简单。
- 当 AlphaLog 获得一个新的日志文件时,它会查询每个 Columniser Descriptor以找到一个有效的列分析器。
- 一旦找到匹配项,就会为该特定文件创建一个新的 Columniser 实例。
- 一个或多个订阅者可以订阅日志文件的更新。
- 当 AlphaLog 上的 Update方法被调用时,日志文件会由列分析器处理,并且任何日志条目都会推送给订阅者。
因此,AlphaLogParser 负责管理列分析器和订阅者的生命周期。
列分析器
在日志文件可以合并或过滤之前,必须将它们解析为通用格式。这就是引入列分析器概念的地方。列分析器本质上是一个插件,开发人员创建它来处理他们的日志文件类型。AlphaLog 已经开发了一些通用列分析器。它们如下:
- RegexColumniser:一种旨在解析文本文件的列分析器。该列分析器利用正则表达式定义的命名组将日志条目解析为字典。
- CSVColumniser: 一种旨在解析 CSV 文件的列分析器。CSV 文件必须定义标题,因为列标题用于将日志条目解析为字典。
- XMLColumniser: 一种旨在通过一组预定义的 XPath 规则解析 XML 文件的列分析器。
用户还可以定义专有的列分析器来解析定制的日志文件。AlphaLog 代码库中一个这样的例子是 log4net XML 列。该列分析器不需要任何配置,它只是解析 log4net XML 格式的日志文件。CsvColumniser 的示例类图如下:

| 名称 | 描述 | 
|---|---|
| IColumniserDescriptor | 列分析器描述符的接口。 | 
| ColumnizerDescriptor | ColumniserDescriptors 的抽象基类。 | 
| CsvColumniserDescriptor | CSV 文件描述符的实现。 | 
| IColumniser | 列分析器的接口 | 
| 列分析器 | Columnisers 的抽象基类。 | 
| CsvColumniser | CSV 文件的列分析器。 | 
UI 插件
在定义新的列分析器时,用户可以选择定义一个视图,允许配置该列分析器。
这允许开发人员为出现在设置窗口中的 UI 创建一个插件。这些列分析器可以在下图中列分析器节点下看到。

AlphaLogParser
AplhaLogParser 负责为提供的文件选择一个列分析器,并对观察到的文件执行类型检查。
当添加文件时,解析器尝试使用 Columniser Descriptors 选择一个匹配的 Columniser。然后解析器确保所选列分析器的数据类型不与现有列分析器冲突。
public Details Add(string file)
{
    if (this.disposed)
    {
        throw new ObjectDisposedException(typeof(AlphaLogParser).Name);
    }
    IColumnizer tempColumniser = this.GetColumniser(file);
    tempColumniser.Subscribe(this.alphaLogSubscribers.AsObserver());
    var columniserDetails = new Details(tempColumniser.File, tempColumniser.Name);
    if (this.UpdateMappingTable(tempColumniser))
    {
        columniserDetails = new ErrorDetails(file, "The data types conflict with another file.");
        if (this.progress != null)
        {
            this.progress.Report(ProcessState.Error, 0);
        }
    }
    return columniserDetails;
}
Remove 方法会删除映射到指定文件的列分析器,并重建用于类型检查的映射表。
public void Remove(string file)
{
    if (this.disposed)
    {
        throw new ObjectDisposedException(typeof(AlphaLogParser).Name);
    }
    if (this.activeColumnisers.ContainsKey(file))
    {
        this.activeColumnisers[file].Dispose();
        this.activeColumnisers.Remove(file);
    }
    if (this.RebuildMappingTable())
    {
        if (this.progress != null)
        {
            this.progress.Report(ProcessState.Error, 0);
        }
    }
    else
    {
        if (this.progress != null)
        {
            this.progress.Report(ProcessState.None, 0);
        }
    }
}
    
    
关注点
在开发 AlphaLog 的过程中,必须克服许多技术挑战。
匹配器
将来,我计划为 AlphaLog 的过滤阶段引入一个插件模型。然而,在第一个实现中,我为过滤阶段构建了一个简单的解释器。
Matchers 库是 AlphaLog 中一个有趣的部分,因为它本质上是一个简单的规则引擎,可以在许多不同的项目中找到应用。Matchers 允许以抽象语法树的形式定义规则,该抽象语法树可以序列化为 XML。提供了一个 WPF 用户界面,允许用户定义规则。规则引擎的类图如下:

| 名称 | 描述 | 
|---|---|
| IMatch | Matchers 的接口。 | 
| MatchBase | Matchers 的抽象基类。 | 
| MatchFactory | Matchers 的工厂。 | 
| MatcherType | 可用 Matchers 的枚举。 | 
| NumericBase | 基于数字的 Matchers 的抽象基类。 | 
| TextBase | 基于字符串/文本的 Matchers 的抽象基类。 | 
| GroupBase | 聚合/分组其他 Matchers 的 Matchers 的抽象基类。 | 
在 C# 代码中定义的简单规则如下所示。
new And(
    new ContainsMatcher("Hello", "Message", false),
    new ExactMatcher("Hello", "Message", false));
显然,在 C# 代码中定义规则并不是特别有用,因为你可以直接编写代码。真正的优势在于能够从 XML 序列化和反序列化规则。
<And>
   <Contains searchField="Message" searchValue="Hello" caseSensitive="false" />
   <Exact searchField="Message" searchValue="World" caseSensitive="false"/>
</And
动态列
我在开发 AlphaLog 时遇到的更具挑战性的问题之一是动态定义数据网格列。由于 AlphaLog 可以处理多种日志文件类型,因此不可能为数据网格定义一组固定的列。因此,我需要在运行时生成列项。
我发现以下文章为我指明了正确的方向。
- http://www.reimers.dk/jacob-reimers-blog/auto-generating-datagrid-columns-from-dynamicobjects
- https://codeproject.org.cn/Articles/575856/Windows-Forms-Binding-through-ITypedList-interface
本质上,WPF Datagrid 在使用“自动生成列”功能时,会使用反射来确定需要生成哪些列。由于我们无法创建强类型类,因此我们必须覆盖在反射日志条目对象时返回的信息。
实现 ITypedList 接口允许您提供集合的不同视图。由于单个日志条目可以有不同数量的列,DynamicItemCollection 类会合并列信息。
public class DynamicItemCollection<T> : ObservableCollection<T>, ITypedList
    where T : PropertyDescriptorCollection, ICustomTypeDescriptor
{
    /// <summary>
    /// Initializes a new instance of the <see cref="DynamicItemCollection{T}"/> class.
    /// </summary>
    /// <param name="toList">
    /// The to list.
    /// </param>
    public DynamicItemCollection(List<T> toList)
        : base(toList)
    {
    }
    /// <summary>
    /// The get item properties.
    /// </summary>
    /// <param name="listAccessors">
    /// The list accessors.
    /// </param>
    /// <returns>
    /// The <see cref="PropertyDescriptorCollection"/>.
    /// </returns>
    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        var set = new HashSet<PropertyDescriptor>();
        foreach (T item in this.Items)
        {
            foreach (object i in item)
            {
                set.Add((PropertyDescriptor)i);
            }
        }
        return new PropertyDescriptorCollection(set.ToArray());
    }
    /// <summary>
    /// The get list name.
    /// </summary>
    /// <param name="listAccessors">
    /// The list accessors.
    /// </param>
    /// <returns>
    /// The <see cref="string"/>.
    /// </returns>
    public string GetListName(PropertyDescriptor[] listAccessors)
    {
        return null;
    }
}
PropertyDescriptorCollection
LogDescriptorCollection 为单个日志条目提供了一个包装器。
public class LogDescriptorCollection : PropertyDescriptorCollection, ICustomTypeDescriptor
{
    /// <summary>
    ///     The log items.
    /// </summary>
    private readonly Dictionary<string, object> logItems = new Dictionary<string, object>();
    /// <summary>
    /// Initializes a new instance of the <see cref="LogDescriptorCollection"/> class.
    /// </summary>
    /// <param name="dictionary">
    /// The dictionary.
    /// </param>
    public LogDescriptorCollection(Dictionary<string, object> dictionary)
        : base(
            dictionary.Where(k => k.Value != null)
                .Select(k => new LogItemDescriptor(k.Key, k.Value.GetType()))
                .ToArray())
    {
        this.logItems = dictionary;
    }
    ...
    /// <summary>
    ///     Returns the properties for this instance of a component.
    /// </summary>
    /// <returns>
    ///     A <see cref="T:System.ComponentModel.PropertyDescriptorCollection" /> that represents the properties for this
    ///     component instance.
    /// </returns>
    public PropertyDescriptorCollection GetProperties()
    {
        var col = new PropertyDescriptorCollection(null);
        foreach (var item in this.logItems.Where(k => k.Value != null))
        {
            col.Add(new LogItemDescriptor(item.Key, item.Value.GetType()));
        }
        return col;
    }
    ... 
}
PropertyDescriptor
PropertyDescriptor 类公开一个字典条目(日志项),用于反射目的。
public class LogItemDescriptor : PropertyDescriptor
{
    /// <summary>
    ///     The m_prop type.
    /// </summary>
    private readonly Type type;
    /// <summary>
    /// Initializes a new instance of the <see cref="LogItemDescriptor"/> class.
    /// </summary>
    /// <param name="name">
    /// The name.
    /// </param>
    /// <param name="type">
    /// The type.
    /// </param>
    public LogItemDescriptor(string name, Type type)
        : base(name, null)
    {
        this.type = type;
    }
    ...
    /// <summary>
    /// When overridden in a derived class, gets the current value of the property on a component.
    /// </summary>
    /// <returns>
    /// The value of a property for a given component.
    /// </returns>
    /// <param name="component">
    /// The component with the property for which to retrieve the value.
    /// </param>
    public override object GetValue(object component)
    {
        return ((LogDescriptorCollection)component).Items[this.Name];
    }
    /// <summary>
    /// When overridden in a derived class, sets the value of the component to a different value.
    /// </summary>
    /// <param name="component">
    /// The component with the property value that is to be set.
    /// </param>
    /// <param name="value">
    /// The new value.
    /// </param>
    public override void SetValue(object component, object value)
    {
        ((LogDescriptorCollection)component).Items[this.Name] = value;
    }
    ...
}
应用程序设置
由于 AlphaLog 的性质,需要存储大量的应用程序设置。我设计了一个简单的存储库,它以 XML 格式存储应用程序设置。这可以在下面的类图中看到

| 名称 | 描述 | 
|---|---|
| ISettingsManager | 主设置存储库的接口。 | 
| SettingsManager | 设置存储库的具体实现。 | 
| IRepository | 可以从 XML 重构的对象的接口。 | 
| 存储库 | 存储库的抽象基类。 | 
| IPersist | 可以序列化为 XML 的对象的接口 | 
| MatcherDatabase | 存储 MatcherItem 对象的存储库。 | 
| MatcherItems | 可以存储在 MatcherDatabase 存储库中的项。 | 
SettingsManager 类基于服务定位器模式。SettingsManager 是存储库的存储库。GetSettings 方法允许获取由其类型指定的存储库。GetSettings 方法的代码如下:
public TSettings GetsSetting<TSettings>() where TSettings : IRepository, new()
{
    IRepository settings = null;
    if (!this.settingsCollection.TryGetValue(typeof(TSettings), out settings))
    {
        var newSettings = new TSettings();
        XElement settingsElement = this.GetElement(newSettings.SettingsName);
        if (settingsElement != null)
        {
            newSettings.Initialize(settingsElement);
        }
        this.settingsCollection[typeof(TSettings)] = newSettings;
        return newSettings;
    }
    return (TSettings)settings;
}
给定一个 settings manager 实例,MatcherDatabase 存储库可以按如下方式获取:
[ImportingConstructor]
public ManageMatcherVM(ISettingsManager settingsManager)
    : base("Matchers")
{
    this.settings = settingsManager.GetsSetting<FieldSettings>();
    this.matchers = settingsManager.GetsSetting<MatcherDatabase>();
    
    ...
}
            
源代码
如果您想查看库的源代码和演示应用程序,可以在我的 Bit Bucket 站点上找到代码。
https://bitbucket.org/chrism233/alphalog
git 存储库地址如下:
https://chrism233@bitbucket.org/chrism233/alphalog.git
历史
| 日期 | 变更 | 
|---|---|
| 07/01/2015 | 初始发布。 | 
| 06/10/2015 | 上传的包含可执行文件的演示 zip 文件。 | 


