【问题标题】:Reading Windows Logs efficiently and fast高效快速地读取 Windows 日志
【发布时间】:2019-04-25 02:37:10
【问题描述】:

我想要完成的是一个 C# 应用程序,它将从 Windows 事件日志中读取日志并将它们存储在其他地方。这必须很快,因为将安装它的某些设备会产生大量的日志/秒。

到目前为止,我已经尝试了三种方法:

Local WMI:效果不好,由于需要加载的集合大小导致的错误和异常太多。 EventLogReader:我认为这是一个完美的解决方案,因为它允许您使用 XPath 表达式查询事件日志。问题在于,当您想要获取每个日志的消息内容(通过调用 FormatDescription())时,对于长时间的收集来说,时间太长了。 例如:如果我浏览它们,我可以在 0.11 秒内读取 12k 条日志。 如果我为每个日志添加一行来存储消息,则需要将近 6 分钟才能完成完全相同的操作,这对于如此少的日志数量来说是完全疯狂的。 我不知道是否可以对 EventLogReader 进行任何优化以更快地获取消息,我在 MS 文档和 Internet 上都找不到任何内容。

我还发现您可以使用名为 EventLog 的类来读取日志条目。但是,该技术不允许您输入任何类型的过滤器,因此您基本上必须将整个日志列表加载到内存中,然后根据需要将其过滤掉。 这是一个例子:

EventLog eventLog = EventLog.GetEventLogs().FirstOrDefault(el => el.Log.Equals("Security", StringComparison.OrdinalIgnoreCase));
var newEntries = (from entry in eventLog.Entries.OfType()
orderby entry.TimeWritten ascending
where entry.TimeWritten > takefrom
select entry);

尽管在获取消息方面速度更快,但内存使用率可能很高,我不想在部署此解决方案的设备上造成任何问题。

谁能帮我解决这个问题?我找不到任何解决方法或方法来实现这样的目标。

谢谢!

【问题讨论】:

  • the use of memory might be high and I don't want to cause any issues on the devices where this solution will get deployed. 意味着用户将在移动日志时使用该应用程序?我只是使用一个任务来处理工作..
  • 不,这只是意味着我不想在服务上获得 SystemOutOfMemoryException 或类似的东西,因为内存使用率很高。我只是想知道是否有比目前我能找到的更有效的解决方法。
  • 这基本上将在 Windows 服务中运行,我不知道第三种方法(EventLog)是否有一种方法可以过滤日志集合,而无需读取内存中的所有内容然后应用一个 LINQ 查询。
  • 我想你想使用EventLog的构造函数:new EventLog("Security")。此外,如果它作为服务运行,我将在第一次运行期间(在后台)复制现有日志,然后在您的服务中将EntryWrittenEventHandler 挂接到EventLog 中以处理新创建的日志。
  • 那么,您基本上是在告诉我为我需要阅读的每个日志文件订阅 EntryWrittenEventHandler?然后每次触发此事件时,我都会拥有新创建的日志,并且可以将其转发到任何我想要的地方。对吗?

标签: c# windows logging


【解决方案1】:

你可以试试 EventLogReader 类。见https://docs.microsoft.com/en-us/previous-versions/bb671200(v=vs.90)

它比 EventLog 类更好,因为访问 EventLog.Entries 集合具有令人讨厌的属性,当您从它读取时,它的计数可能会改变。更糟糕的是,读取发生在 IO 线程池线程上,这将使您的应用程序因未处理的异常而崩溃。至少几年前是这样的。

EventLogReader 还使您能够提供查询字符串来过滤您感兴趣的事件。如果您编写一个新的应用程序,这就是要走的路。

这是一个展示如何并行阅读的应用程序:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Threading.Tasks;

namespace EventLogReading
{
    class Program
    {
        static volatile bool myHasStoppedReading = false;

        static void ParseEventsParallel()
        {
            var sw = Stopwatch.StartNew();
            var query = new EventLogQuery("Application", PathType.LogName, "*");

            const int BatchSize = 100;

            ConcurrentQueue<EventRecord> events = new ConcurrentQueue<EventRecord>();
            var readerTask = Task.Factory.StartNew(() =>
            {
                using (EventLogReader reader = new EventLogReader(query))
                {
                    EventRecord ev;
                    bool bFirst = true;
                    int count = 0;
                    while ((ev = reader.ReadEvent()) != null)
                    {
                        if ( count % BatchSize == 0)
                        {
                            events.Enqueue(ev);
                        }
                        count++;
                    }
                }
                myHasStoppedReading = true;
            });

            ConcurrentQueue<KeyValuePair<string, EventRecord>> eventsWithStrings = new ConcurrentQueue<KeyValuePair<string, EventRecord>>();

            Action conversion = () =>
            {
                EventRecord ev = null;
                using (var reader = new EventLogReader(query))
                {
                    while (!myHasStoppedReading || events.TryDequeue(out ev))
                    {
                        if (ev != null)
                        {
                            reader.Seek(ev.Bookmark);
                            for (int i = 0; i < BatchSize; i++)
                            {
                                ev = reader.ReadEvent();
                                if (ev == null)
                                {
                                    break;
                                }
                                eventsWithStrings.Enqueue(new KeyValuePair<string, EventRecord>(ev.FormatDescription(), ev));
                            }
                        }
                    }
                }
            };

            Parallel.Invoke(Enumerable.Repeat(conversion, 8).ToArray());

            sw.Stop();
            Console.WriteLine($"Got {eventsWithStrings.Count} events with strings in {sw.Elapsed.TotalMilliseconds:N3}ms");
        }

        static void ParseEvents()
        {
            var sw = Stopwatch.StartNew();
            List<KeyValuePair<string, EventRecord>> parsedEvents = new List<KeyValuePair<string, EventRecord>>();
                
            using (EventLogReader reader = new EventLogReader(new EventLogQuery("Application", PathType.LogName, "*")))
            {
                EventRecord ev;
                while ((ev = reader.ReadEvent()) != null)
                {
                    parsedEvents.Add(new KeyValuePair<string, EventRecord>(ev.FormatDescription(), ev));
                }
            }

            sw.Stop();
            Console.WriteLine($"Got {parsedEvents.Count} events with strings in {sw.Elapsed.TotalMilliseconds:N3}ms");
        }

        static void Main(string[] args)
        {
            ParseEvents();
            ParseEventsParallel();
        }
    }
}
Got 20322 events with strings in 19,320.047ms
Got 20323 events with strings in 5,327.064ms

这提供了一个不错的 4 倍加速。我需要使用一些技巧来提高速度,因为出于某种奇怪的原因,类 ProviderMetadataCachedInformation 不是线程安全的,并且在内部使用 Format 方法周围的锁(this),这会破坏并行读取. 关键技巧是再次打开转换线程中的事件日志,然后通过事件书签 Api 读取一堆查询事件。这样你就可以独立地格式化字符串了。

更新1

我在 .NET 5 中进行了更改,将性能提高了 3 倍,最高可达 20。请参阅 https://github.com/dotnet/runtime/issues/34568。 您还可以从 .NET Core 复制 EventLogReader 类并改用这个类,这将为您提供相同的加速。

我的博文描述了完整的传奇:https://aloiskraus.wordpress.com/2020/07/20/ms-performance-hud-analyze-eventlog-reading-performance-in-realtime/

【讨论】:

  • 您将使用for-loop 循环遍历EventLog.Entries 集合,因此如果在迭代时计数会发生变化,这无关紧要。每当 OP 使用事件拦截新的事件条目时,这个问题也得到了解决。我同意查询EventLogReader 更好(不是性能方面),因为它允许开箱即用的查询。我什至认为因为for-loopEventLog.Entries 甚至比EventLogReader 还要快。
  • 感谢您的回复!。就像我之前说的,EventLogReader 的问题是,当您通过“FormatDescription()”方法访问每个日志的消息时,性能会大大降低。我知道它更快,因为您可以仅在内存中查询和检索您可能想要的日​​志幻灯片,但另一方面,处理每个日志需要太多时间,因此对于我必须完成的工作来说,这不是一个可接受的解决方案。
  • 你总是可以用多个线程来格式化东西,如果你可以使用它们应该会给你一个不错的加速。
  • 我不知道您将如何做到这一点,您可以为每个日志文件创建一个线程来尝试分析,但这是我认为的最远距离。可以显着提高性能的方法是尝试存储 FormatDescription 查找的消息模板,但这似乎太复杂而无法实现。
  • @LucasÁlvarezLacasa:我已经用平行阅读更新了我的样本。
【解决方案2】:

我们讨论了一点关于读取 cmets 中现有日志的内容,可以通过以下方式访问带有Security 标记的日志:

 var eventLog = new EventLog("Security");
 for (int i = 0; i < eventLog.Entries.Count; i++)
 {
      Console.WriteLine($"{eventLog.Entries[i].Message}");
 }

这可能不是最干净(性能方面)的方式,但我怀疑任何其他方式都会更快,因为您自己已经通过尝试不同的技术发现了这一点。 对 Alois 帖子的一个小编辑二人组:EventLogReader 开箱即用并不比EventLog 快,尤其是在使用上面代码块中显示的for-loop 机制时,我认为EventLog 更快——它只访问循环内的条目使用它们的索引,Entries 集合只是一个参考,而在使用EventLogReader 时,它将首先执行查询并循环该结果,这应该会更慢。正如对 Alois 帖子的评论:如果您不需要使用查询选项,只需使用 EventLog 变体。如果确实需要查询,请按原样使用EventLogReader 可以在比使用EventLog 时更低的级别进行查询(仅限LINQ 查询,这当然比在执行查找时查询要慢)。

为了防止您将来再次遇到这种麻烦,并且因为您说您正在运行服务,我将使用 EventLog 类的 EntryWritten 事件:

    var eventLog = new EventLog("Security")
    {
        EnableRaisingEvents = true
    };
    eventLog.EntryWritten += EventLog_EntryWritten;

    // .. read existing logs or do other work ..

    private static void EventLog_EntryWritten(object sender, EntryWrittenEventArgs e)
    {
        Console.WriteLine($"received new entry: {e.Entry.Message}");
    }

请注意,您必须EnableRaisingEvents 设置为true,以便在记录新条目时触发事件。启动(例如)Task 也是一个很好的做法(同样,性能方面),这样系统就不会在排队调用您的事件时锁定自己。

如果您想检索 所有 新创建的事件,此方法可以正常工作,如果您想检索新创建的事件但对这些事件使用查询(过滤器),您可以查看 @987654325 @ 类,但在你的情况下,当没有限制时,我只使用 EntryWritten 事件,因为你不需要过滤器并且为了简单而简单。

【讨论】:

  • 如果运行这个任务的服务停止了怎么办?有没有办法从某个点重新启用这个回调?或者我将不得不从那一点重新开始,并且可能会在中间丢失日志?。
  • 我想说的是,假设我正在使用回调读取日志。突然我的服务停止了。如果我重新开始并注册回调,我将丢失中间生成的日志。我说的对吗?
  • 是的,这些日志不会通过事件处理程序记录,尽管您可以轻松编写一些代码来检索您处理的最后一个日志和最近的日志之间的日志(日志被保存顺序,索引)。
  • 是的,这看起来是个不错的方法。我要试一试。谢谢你,兄弟!。我会发布我可能得到的任何消息。
  • Jevgeni Geurtsen,我用 EventLogWatcher 创建了一个小型 POC,但你最终得到了一个 EventRecord,因此,你通过“FormatDescription()”获取消息的速度变慢了。看起来解决这个问题最有效的方法是使用带有 OnEntryWritten 回调的 EventLog。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-25
  • 2010-12-06
  • 1970-01-01
  • 2011-01-08
  • 2019-09-20
  • 2021-11-21
  • 2023-03-10
相关资源
最近更新 更多