高效快速地阅读Windows日志

8
我想要实现一个C#应用程序,它将从Windows事件日志中读取日志并将其存储在其他地方。这必须很快,因为安装了该应用程序的某些设备会生成大量日志。
到目前为止,我尝试了三种方法:
1. 本地WMI:它效果不好,由于需要加载的集合太大而导致太多错误和异常。 2. EventLogReader:我认为这是完美的解决方案,因为它允许您使用XPath表达式按照自己的喜好查询事件日志。问题是,当您想要获取每个日志的消息内容(通过调用FormatDescription())时,对于长集合来说需要太长时间。 例如:如果我只是遍历它们,我可以在0.11秒内读取12k个日志。 如果我添加一行代码来存储每个日志的消息,完全相同的操作需要近6分钟才能完成,这对于如此少量的日志来说完全疯狂。 我不知道是否有任何优化可以对EventLogReader进行,以便更快地获取消息,我在MS文档和互联网上都找不到任何信息。
3. 我还发现可以使用名为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);

虽然消息传递速度更快,但内存使用可能很高,我不想在部署此解决方案的设备上出现任何问题。

有人能帮我吗?我找不到任何解决方法或可行方案。

谢谢!


内存的使用可能很高,我不想在部署此解决方案的设备上引起任何问题。这意味着用户将在移动记录时使用该应用程序?我只需使用任务来处理工作即可。 - Jevgeni Geurtsen
这基本上将在Windows服务中运行,我不知道第三种方法(EventLog)是否有一种过滤日志收集的方式,而无需读取所有内容并应用LINQ查询。 - Lucas Álvarez Lacasa
1
我认为你想要使用 EventLog 的构造函数:new EventLog("Security")。此外,如果它作为服务运行,我建议在第一次运行时(在后台)复制现有的日志,然后在你的服务中钩取 EventLog 中的 EntryWrittenEventHandler 来处理新创建的日志。 - Jevgeni Geurtsen
你能给我提供一小段代码来演示如何做吗?为什么你说我需要先迭代现有元素?我不能在注册时直接开始获取新的元素吗? - Lucas Álvarez Lacasa
我将创建一个代码片段,向您展示我所完成的工作。 - Lucas Álvarez Lacasa
显示剩余4条评论
2个回答

6
你可以尝试使用EventLogReader类。请参见https://learn.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方法周围使用了lock(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.EntriesEventLogReader 更快。 - Jevgeni Geurtsen
谢谢回复!就像我之前所说,EventLogReader的问题在于访问每个日志的消息时,通过“FormatDescription()”方法会遇到巨大的性能下降。我知道它更快,因为您可以查询并仅检索可能想要的日志幻灯片,但另一方面,处理每个日志需要太多时间,因此对于我需要完成的任务来说,这不是可接受的解决方案。 - Lucas Álvarez Lacasa
1
如果您可以使用多个线程,您可以随时使用多个线程格式化内容,这应该会给您带来相当不错的加速效果。 - Alois Kraus
我不知道你会如何做到这一点,你可以为每个要分析的日志文件创建一个线程,但我认为这已经是极限了。 能够显著提高性能的方法是尝试存储FormatDescription寻找的消息模板,但这似乎过于复杂难以实现。 - Lucas Álvarez Lacasa
@LucasÁlvarezLacasa:我已经更新了我的示例,加入了并行读取。 - Alois Kraus
Alois,感谢您的回复。那种方法似乎更好。但是,您仍然会面临通过FormatDescription()获取日志消息的停机时间。我现在正在使用EntryWritten,基本上是获取每个新事件,验证该事件的日期时间是否比我获得的上一个事件更新或相同,并将其推送到ConcurrentQueue中以便稍后传递。这似乎是目前最快的方法。我一完成它就会发布一个POC。 - Lucas Álvarez Lacasa

3

我们在评论中简单讨论了一下读取现有日志的问题,可以通过访问以下链接来访问带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,这样系统就不会在排队调用您的事件时锁定自己。
如果您想检索所有新创建的事件,则此方法可以正常工作。如果您想检索新创建的事件但使用查询(过滤器)来筛选这些事件,则可以查看EventLogWatcher类。但在您的情况下,当没有约束条件时,我只会使用 EntryWritten 事件,因为您不需要过滤器,并且它更加简单易懂。

如果运行这些任务的服务停止了怎么办?有没有一种方法可以从某个点重新启用此回调?还是我必须从那个点重新开始,可能会丢失中间的日志? - Lucas Álvarez Lacasa
我想说的是,假设我正在使用回调函数读取日志。突然我的服务停止了。如果我重新开始并注册回调函数,那么我将失去在中途生成的日志。我说得对吗? - Lucas Álvarez Lacasa
是的,这些日志不会通过事件处理程序记录,尽管您可以轻松编写一些代码,在您最后处理的日志和最新日志之间检索日志(日志按顺序保存,索引)。 - Jevgeni Geurtsen
是的,这看起来是个不错的方法。我会试一试的。谢谢啊!我会发布任何我得到的消息的。 - Lucas Álvarez Lacasa
Jevgeni Geurtsen,我使用EventLogWatcher创建了一个小的POC,但最终你会得到一个EventRecord,因此,你需要通过“FormatDescription()”来获取消息,这会导致减速。看起来解决这个问题最有效的方法是使用带有OnEntryWritten回调的EventLog。 - Lucas Álvarez Lacasa
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接