From d7d45d35a2cdc3b41941709fc4b8b3bb66af08c6 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Sun, 10 Oct 2021 13:05:07 +1100 Subject: [PATCH 01/34] Broad strokes of a conversion - still needs query building, property parsing, etc --- src/Seq.Client.EventLog/App.config | 44 +++-- src/Seq.Client.EventLog/Config.cs | 124 +++++++++++++ src/Seq.Client.EventLog/EventLogClient.cs | 8 +- src/Seq.Client.EventLog/EventLogListener.cs | 166 ++++++++++-------- src/Seq.Client.EventLog/Extensions.cs | 77 ++++---- src/Seq.Client.EventLog/Program.cs | 71 +++++--- .../Properties/AssemblyInfo.cs | 35 ---- .../Properties/Settings.Designer.cs | 44 ----- .../Properties/Settings.settings | 12 -- src/Seq.Client.EventLog/RawEvent.cs | 19 -- src/Seq.Client.EventLog/RawEvents.cs | 7 - .../Seq.Client.EventLog.csproj | 70 ++------ src/Seq.Client.EventLog/SeqApi.cs | 34 ---- src/Seq.Client.EventLog/ServiceManager.cs | 69 ++++++++ src/Seq.Client.EventLog/TimedEventBag.cs | 38 ++++ src/Seq.Client.EventLog/packages.config | 7 - 16 files changed, 452 insertions(+), 373 deletions(-) create mode 100644 src/Seq.Client.EventLog/Config.cs delete mode 100644 src/Seq.Client.EventLog/Properties/AssemblyInfo.cs delete mode 100644 src/Seq.Client.EventLog/Properties/Settings.Designer.cs delete mode 100644 src/Seq.Client.EventLog/Properties/Settings.settings delete mode 100644 src/Seq.Client.EventLog/RawEvent.cs delete mode 100644 src/Seq.Client.EventLog/RawEvents.cs delete mode 100644 src/Seq.Client.EventLog/SeqApi.cs create mode 100644 src/Seq.Client.EventLog/ServiceManager.cs create mode 100644 src/Seq.Client.EventLog/TimedEventBag.cs delete mode 100644 src/Seq.Client.EventLog/packages.config diff --git a/src/Seq.Client.EventLog/App.config b/src/Seq.Client.EventLog/App.config index 8cde164..e3e369a 100644 --- a/src/Seq.Client.EventLog/App.config +++ b/src/Seq.Client.EventLog/App.config @@ -1,21 +1,27 @@ - + + - - -
- - - - - - - - - http://localhost:5341 - - - - - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Config.cs b/src/Seq.Client.EventLog/Config.cs new file mode 100644 index 0000000..607adf2 --- /dev/null +++ b/src/Seq.Client.EventLog/Config.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Seq.Client.EventLog +{ + public static class Config + { + static Config() + { + AppName = ConfigurationManager.AppSettings["AppName"]; + SeqServer = ConfigurationManager.AppSettings["LogSeqServer"]; + SeqApiKey = ConfigurationManager.AppSettings["LogSeqApiKey"]; + LogFolder = ConfigurationManager.AppSettings["LogFolder"]; + HeartbeatInterval = GetInt(ConfigurationManager.AppSettings["HeartbeatInterval"]); + IsDebug = GetBool(ConfigurationManager.AppSettings["IsDebug"]); + ProjectKey = ConfigurationManager.AppSettings["ProjectKey"]; + Responders = ConfigurationManager.AppSettings["Responders"]; + Priority = ConfigurationManager.AppSettings["Priority"]; + Tags = GetArray(ConfigurationManager.AppSettings["Tags"]); + InitialTimeEstimate = ConfigurationManager.AppSettings["InitialTimeEstimate"]; + RemainingTimeEstimate = ConfigurationManager.AppSettings["RemainingTimeEstimate"]; + DueDate = ConfigurationManager.AppSettings["DueDate"]; + + //Must be between 0 and 1 hour in seconds + if (HeartbeatInterval < 0 || HeartbeatInterval > 3600) + HeartbeatInterval = 600000; + else + HeartbeatInterval *= 1000; + + var isSuccess = true; + try + { + if (string.IsNullOrEmpty(AppName)) + AppName = Assembly.GetEntryAssembly()?.GetName().Name; + + AppVersion = Assembly.GetEntryAssembly()?.GetName().Version.ToString(); + if (string.IsNullOrEmpty(LogFolder)) + LogFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + } + catch + { + isSuccess = false; + } + + if (isSuccess) return; + try + { + if (string.IsNullOrEmpty(AppName)) + AppName = Assembly.GetExecutingAssembly().GetName().Name; + + AppVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + if (string.IsNullOrEmpty(LogFolder)) + LogFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase); + } + catch + { + //We surrender ... + AppVersion = string.Empty; + } + } + + public static string AppName { get; } + public static string AppVersion { get; } + public static string SeqServer { get; } + public static string SeqApiKey { get; } + public static string LogFolder { get; } + public static int HeartbeatInterval { get; } + public static bool IsDebug { get; } + public static string ProjectKey { get; } + public static string Priority { get; } + public static string Responders { get; } + public static IEnumerable Tags { get; } + public static string InitialTimeEstimate { get; } + public static string RemainingTimeEstimate { get; } + public static string DueDate { get; } + + /// + /// Convert the supplied to an + /// + /// This will filter out nulls that could otherwise cause exceptions + /// + /// An object that can be converted to an int + /// + private static int GetInt(object sourceObject) + { + var sourceString = string.Empty; + + if (!Convert.IsDBNull(sourceObject)) sourceString = (string) sourceObject; + + if (int.TryParse(sourceString, out var destInt)) return destInt; + + return -1; + } + + /// + /// Convert the supplied to a + /// + /// This will filter out nulls that could otherwise cause exceptions + /// + /// An object that can be converted to a bool + /// Return true if the object is empty + /// + private static bool GetBool(object sourceObject, bool trueIfEmpty = false) + { + var sourceString = string.Empty; + + if (!Convert.IsDBNull(sourceObject)) sourceString = (string) sourceObject; + + return bool.TryParse(sourceString, out var destBool) ? destBool : trueIfEmpty; + } + + private static IEnumerable GetArray(string value) + { + return (value ?? "") + .Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index 3c70669..be65cf6 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -10,11 +10,11 @@ class EventLogClient { private List _eventLogListeners; - public void Start(string configuration = null) + public void Start(bool isInteractive = false, string configuration = null) { LoadListeners(configuration); ValidateListeners(); - StartListeners(); + StartListeners(isInteractive); } public void Stop() @@ -49,11 +49,11 @@ private void ValidateListeners() } } - private void StartListeners() + private void StartListeners(bool isInteractive = false) { foreach (var listener in _eventLogListeners) { - listener.Start(); + listener.Start(isInteractive); } } diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index 438b016..2eb6f02 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -1,27 +1,33 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; using System.Threading; using System.Threading.Tasks; +using Lurgle.Logging; +using Timer = System.Timers.Timer; namespace Seq.Client.EventLog { public class EventLogListener { + private bool _isInteractive; + public string LogName { get; set; } public string MachineName { get; set; } public bool ProcessRetroactiveEntries { get; set; } // These properties allow for the filtering of events that will be sent to Seq. // If they are not specified in the JSON, all events in the log will be sent. - public List LogLevels { get; set; } + public List LogLevels { get; set; } public List EventIds { get; set; } public List Sources { get; set; } - private System.Diagnostics.EventLog _eventLog; + private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); - private Task _retroactiveLoadingTask; + private EventLogQuery _eventLog; private volatile bool _started; + private EventLogWatcher _watcher; public void Validate() { @@ -31,40 +37,35 @@ public void Validate() } } - public void Start() + public void Start(bool isInteractive = false) { try { - Serilog.Log.Information("Starting listener for {LogName} on {MachineName}", LogName, MachineName ?? "."); - - _eventLog = OpenEventLog(); - - if (ProcessRetroactiveEntries) - { - // Start as a new task so it doesn't block the startup of the service. This has - // to go on its own thread to avoid deadlocking via `Wait()`/`Result`. - _retroactiveLoadingTask = Task.Factory.StartNew(SendRetroactiveEntries, TaskCreationOptions.LongRunning); - } - - _eventLog.EntryWritten += OnEntryWritten; - _eventLog.EnableRaisingEvents = true; + Log.Information().Add("Starting listener for {LogName:l} on {MachineName:l}", LogName, + MachineName ?? "."); + + //Update this to calculate a query + _eventLog = new EventLogQuery("Security", PathType.LogName, + "*[System[band(Keywords,9007199254740992) and (EventID=4624)]]"); + + //if (ProcessRetroactiveEntries) + //{ + // // Start as a new task so it doesn't block the startup of the service. This has + // // to go on its own thread to avoid deadlocking via `Wait()`/`Result`. + // _retroactiveLoadingTask = Task.Factory.StartNew(SendRetroactiveEntries, TaskCreationOptions.LongRunning); + //} + + _isInteractive = isInteractive; + _watcher = new EventLogWatcher(_eventLog); + _watcher.EventRecordWritten += OnEntryWritten; + _watcher.Enabled = true; _started = true; } catch (Exception ex) { - Serilog.Log.Error(ex, "Failed to start listener for {LogName} on {MachineName}", LogName, MachineName ?? "."); - } - } - - System.Diagnostics.EventLog OpenEventLog() - { - var eventLog = new System.Diagnostics.EventLog(LogName); - if (!string.IsNullOrWhiteSpace(MachineName)) - { - eventLog.MachineName = MachineName; + Log.Exception(ex).Add("Failed to start listener for {LogName:l} on {MachineName:l}: {Message:l}", + LogName, MachineName ?? ".", ex.Message); } - - return eventLog; } public void Stop() @@ -75,77 +76,102 @@ public void Stop() return; _cancel.Cancel(); - _eventLog.EnableRaisingEvents = false; + _watcher.Enabled = false; + _watcher.Dispose(); // This would be a little racy if start and stop were ever called on different threads, but // this isn't done, currently. - _retroactiveLoadingTask?.Wait(); + //_retroactiveLoadingTask?.Wait(); - _eventLog.Close(); - _eventLog.Dispose(); + Log.Debug().Add("Listener stopped for {LogName:l} on {MachineName:l}", LogName, MachineName ?? "."); - Serilog.Log.Information("Listener stopped"); } catch (Exception ex) { - Serilog.Log.Error(ex, "Failed to stop listener"); + Log.Exception(ex).Add("Failed to stop listener: {Message:l}", ex.Message); } } - private void SendRetroactiveEntries() + //private void SendRetroactiveEntries() + //{ + // try + // { + // using (var eventLog = OpenEventLog()) + // { + // Serilog.Log.Information("Processing {EntryCount} retroactive entries in {LogName}", eventLog.Entries.Count, LogName); + + // foreach (EventLogEntry entry in eventLog.Entries) + // { + // if (_cancel.IsCancellationRequested) + // { + // Serilog.Log.Warning("Canceling retroactive event loading"); + // return; + // } + + // HandleEventLogEntry(entry, eventLog.Log).GetAwaiter().GetResult(); + // } + // } + // } + // catch (Exception ex) + // { + // Serilog.Log.Error(ex, "Failed to send retroactive entries in {LogName} on {MachineName}", LogName, MachineName ?? "."); + // } + //} + + private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs args) { try { - using (var eventLog = OpenEventLog()) - { - Serilog.Log.Information("Processing {EntryCount} retroactive entries in {LogName}", eventLog.Entries.Count, LogName); - - foreach (EventLogEntry entry in eventLog.Entries) - { - if (_cancel.IsCancellationRequested) - { - Serilog.Log.Warning("Canceling retroactive event loading"); - return; - } - - HandleEventLogEntry(entry, eventLog.Log).GetAwaiter().GetResult(); - } - } + //Ensure that events are new and have not been seen already. This addresses a scenario where event logs can repeatedly pass events to the handler. + if (args.EventRecord != null && args.EventRecord.TimeCreated >= ServiceManager.ServiceStart && + !ServiceManager.EventList.Contains(args.EventRecord.RecordId)) + await Task.Run(() => HandleEventLogEntry(args.EventRecord)); + else if (args.EventRecord != null && args.EventRecord.TimeCreated < ServiceManager.ServiceStart) + ServiceManager.OldEvents++; + else if (args.EventRecord == null) + ServiceManager.EmptyEvents++; } catch (Exception ex) { - Serilog.Log.Error(ex, "Failed to send retroactive entries in {LogName} on {MachineName}", LogName, MachineName ?? "."); + Log.Exception(ex).Add("Failed to handle {EventLog} log entry: {Message:l}", LogName, ex.Message); } } - private void OnEntryWritten(object sender, EntryWrittenEventArgs args) + private void HandleEventLogEntry(EventRecord entry) { - try - { - HandleEventLogEntry(args.Entry, _eventLog.Log).GetAwaiter().GetResult(); - } - catch (Exception ex) - { - Serilog.Log.Error(ex, "Failed to handle an event log entry"); - } - } + ServiceManager.EventList.Add(entry.RecordId); - private async Task HandleEventLogEntry(EventLogEntry entry, string logName) - { // Don't send the entry to Seq if it doesn't match the filtered log levels, event IDs, or sources - if (LogLevels != null && LogLevels.Count > 0 && !LogLevels.Contains(entry.EntryType)) + if (LogLevels != null && LogLevels.Count > 0 && entry.Level != null && + !LogLevels.Contains((byte)entry.Level)) + { + ServiceManager.UnhandledEvents++; return; + } // EventID is obsolete -#pragma warning disable 618 - if (EventIds != null && EventIds.Count > 0 && !EventIds.Contains(entry.EventID)) -#pragma warning restore 618 + if (EventIds != null && EventIds.Count > 0 && !EventIds.Contains(entry.Id)) + { + ServiceManager.UnhandledEvents++; return; + } - if (Sources != null && Sources.Count > 0 && !Sources.Contains(entry.Source)) + if (Sources != null && Sources.Count > 0 && !Sources.Contains(entry.ProviderName)) + { + ServiceManager.UnhandledEvents++; return; + } + + try + { - await SeqApi.PostRawEvents(entry.ToDto(logName)); + Log.Level(Extensions.MapLogLevel(entry.Level)).AddProperty("Xml", entry.ToXml()) + .Add(entry.FormatDescription()); + } + catch (Exception ex) + { + Log.Exception(ex).Add("Error parsing event: {Message:l}", ex.Message); + } } } } diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index 3bdc6cb..4da8a3d 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -1,54 +1,55 @@ using System.Collections.Generic; using System.Diagnostics; +using Lurgle.Logging; namespace Seq.Client.EventLog { public static class Extensions { - private static string MapLogLevel(EventLogEntryType type) + public static LurgLevel MapLogLevel(byte? type) { switch (type) { - case EventLogEntryType.Information: - return "Information"; - case EventLogEntryType.Warning: - return "Warning"; - case EventLogEntryType.Error: - return "Error"; - case EventLogEntryType.SuccessAudit: - return "Information"; - case EventLogEntryType.FailureAudit: - return "Warning"; + case (byte?)EventLogEntryType.Information: + return LurgLevel.Information; + case (byte?)EventLogEntryType.Warning: + return LurgLevel.Warning; + case (byte?)EventLogEntryType.Error: + return LurgLevel.Error; + case (byte?)EventLogEntryType.SuccessAudit: + return LurgLevel.Information; + case (byte?)EventLogEntryType.FailureAudit: + return LurgLevel.Warning; default: - return "Debug"; + return LurgLevel.Debug; } } - public static RawEvents ToDto(this EventLogEntry entry, string logName) - { - return new RawEvents - { - Events = new[] - { - new RawEvent - { - Timestamp = entry.TimeGenerated, - Level = MapLogLevel(entry.EntryType), - MessageTemplate = entry.Message, - Properties = new Dictionary - { - { "MachineName", entry.MachineName }, -#pragma warning disable 618 - { "EventId", entry.EventID }, -#pragma warning restore 618 - { "InstanceId", entry.InstanceId }, - { "Source", entry.Source }, - { "Category", entry.CategoryNumber }, - { "EventLogName", logName } - } - }, - } - }; - } +// public static RawEvents ToDto(this EventLogEntry entry, string logName) +// { +// return new RawEvents +// { +// Events = new[] +// { +// new RawEvent +// { +// Timestamp = entry.TimeGenerated, +// Level = MapLogLevel(entry.EntryType), +// MessageTemplate = entry.Message, +// Properties = new Dictionary +// { +// { "MachineName", entry.MachineName }, +//#pragma warning disable 618 +// { "EventId", entry.EventID }, +//#pragma warning restore 618 +// { "InstanceId", entry.InstanceId }, +// { "Source", entry.Source }, +// { "Category", entry.CategoryNumber }, +// { "EventLogName", logName } +// } +// }, +// } +// }; +// } } } diff --git a/src/Seq.Client.EventLog/Program.cs b/src/Seq.Client.EventLog/Program.cs index b8abe50..f3353fa 100644 --- a/src/Seq.Client.EventLog/Program.cs +++ b/src/Seq.Client.EventLog/Program.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; using System.Configuration.Install; using System.IO; using System.Reflection; using System.ServiceProcess; using System.Threading; -using Serilog; +using Lurgle.Logging; namespace Seq.Client.EventLog { @@ -48,69 +49,85 @@ public static void Main(string[] args) static void RunInteractive(string configFilePath) { - Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .CreateLogger(); + Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, + logType: new List {LogType.Console, LogType.Seq}, logSeqServer: Config.SeqServer, + logSeqApiKey: Config.SeqApiKey, logLevel: LurgLevel.Verbose, logLevelConsole: LurgLevel.Verbose, + logLevelSeq: LurgLevel.Verbose)); try { - Log.Information("Running interactively"); + Log.Debug() + .Add("{AppName:l} v{AppVersion:l} Starting ...", Config.AppName, Config.AppVersion); + Log.Debug().Add("Running interactively"); var client = new EventLogClient(); - client.Start(configFilePath); + client.Start(true, configFilePath); + ServiceManager.Start(true); var done = new ManualResetEvent(false); Console.CancelKeyPress += (s, e) => { - Log.Information("Ctrl+C pressed, stopping"); + Log.Debug().Add("Ctrl+C pressed, stopping"); client.Stop(); done.Set(); }; done.WaitOne(); - Log.Information("Stopped"); + ServiceManager.Stop(); + Log.Debug() + .Add("{AppName:l} v{AppVersion:l} Stopped", Config.AppName, Config.AppVersion); } catch (Exception ex) { - Log.Fatal(ex, "An unhandled exception occurred"); + Log.Exception(ex).Add("An unhandled exception occurred"); Environment.ExitCode = 1; } finally { - Log.CloseAndFlush(); + Logging.Close(); } } static void RunService() { - var logFile = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - typeof(Program).Assembly.GetName().Name, - "ServiceLog.txt"); + var logFolder = Config.LogFolder; - Log.Logger = new LoggerConfiguration() - .WriteTo.File( - logFile, - rollingInterval: RollingInterval.Day, - rollOnFileSizeLimit: true, - retainedFileCountLimit: 7, - fileSizeLimitBytes: 10_000_000, - shared: true) - .CreateLogger(); + if (string.IsNullOrEmpty(logFolder)) + logFolder = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty, "Logs"); + + if (!Directory.Exists(logFolder)) + Directory.CreateDirectory(logFolder); + + var logFile = Path.Combine(logFolder ?? string.Empty, "ServiceLog.txt"); + Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, + logType: new List {LogType.File, LogType.Seq}, logDays: 7, logName: Config.AppName, + logFolder: Config.LogFolder, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, + logLevel: LurgLevel.Verbose, logLevelFile: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); try { - Log.Information("Running as service"); + Log.Debug() + .Add("{AppName:l} v{AppVersion:l} Starting ...", Config.AppName, Config.AppVersion); + Log.Debug() + .Add( + "LogFolder: {LogFolder:l}, LogPath: {LogPath:l}, Seq Server: {SeqServer:l}, Api Key: {ApiKeySet}", + Config.LogFolder, logFile, Config.SeqServer, !string.IsNullOrEmpty(Config.SeqApiKey)); + Log.Debug().Add("Running as service"); + ServiceManager.Start(false); ServiceBase.Run(new Service()); - Log.Information("Stopped"); + ServiceManager.Stop(); + Log.Debug() + .Add("{AppName:l} v{AppVersion:l} Stopped", Config.AppName, Config.AppVersion); + } catch (Exception ex) { - Log.Fatal(ex, "Exception thrown from service host"); + Log.Exception(ex).Add("Exception thrown from service host: {Message:l}", ex.Message); } finally { - Log.CloseAndFlush(); + Logging.Close(); } } } diff --git a/src/Seq.Client.EventLog/Properties/AssemblyInfo.cs b/src/Seq.Client.EventLog/Properties/AssemblyInfo.cs deleted file mode 100644 index f91a0e7..0000000 --- a/src/Seq.Client.EventLog/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Seq.Client.EventLog")] -[assembly: AssemblyDescription("Writes Windows Event Log entries to Seq")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Seq.Client.EventLog")] -[assembly: AssemblyCopyright("Copyright © 2018 Connor O'Shea")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("b14232bd-b051-4255-9dec-81ea174660e8")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/src/Seq.Client.EventLog/Properties/Settings.Designer.cs b/src/Seq.Client.EventLog/Properties/Settings.Designer.cs deleted file mode 100644 index 15f84b9..0000000 --- a/src/Seq.Client.EventLog/Properties/Settings.Designer.cs +++ /dev/null @@ -1,44 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Seq.Client.EventLog.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("http://SERVER:5341")] - public string SeqUri { - get { - return ((string)(this["SeqUri"])); - } - } - - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string ApiKey { - get { - return ((string)(this["ApiKey"])); - } - } - } -} diff --git a/src/Seq.Client.EventLog/Properties/Settings.settings b/src/Seq.Client.EventLog/Properties/Settings.settings deleted file mode 100644 index ca888ac..0000000 --- a/src/Seq.Client.EventLog/Properties/Settings.settings +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - http://SERVER:5341 - - - - - - \ No newline at end of file diff --git a/src/Seq.Client.EventLog/RawEvent.cs b/src/Seq.Client.EventLog/RawEvent.cs deleted file mode 100644 index 2e6ba2b..0000000 --- a/src/Seq.Client.EventLog/RawEvent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Seq.Client.EventLog -{ - public class RawEvent - { - public DateTimeOffset Timestamp { get; set; } - - // Uses the Serilog level names - public string Level { get; set; } - - public string MessageTemplate { get; set; } - - public Dictionary Properties { get; set; } - - public string Exception { get; set; } - } -} diff --git a/src/Seq.Client.EventLog/RawEvents.cs b/src/Seq.Client.EventLog/RawEvents.cs deleted file mode 100644 index 909d014..0000000 --- a/src/Seq.Client.EventLog/RawEvents.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Seq.Client.EventLog -{ - public class RawEvents - { - public RawEvent[] Events { get; set; } - } -} diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index a8517a1..9625a58 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -1,6 +1,5 @@  - - + Debug AnyCPU @@ -9,7 +8,7 @@ Properties Seq.Client.EventLog Seq.Client.EventLog - v4.6.2 + net462 512 true @@ -35,21 +34,11 @@ + 3.0.0 + Connor O'Shea and contributors + Connor O'Shea - - ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - ..\packages\Serilog.2.7.1\lib\net46\Serilog.dll - - - ..\packages\Serilog.Sinks.Console.3.1.1\lib\net45\Serilog.Sinks.Console.dll - - - ..\packages\Serilog.Sinks.File.4.0.0\lib\net45\Serilog.Sinks.File.dll - @@ -63,33 +52,6 @@ - - - - - - Component - - - ProjectInstaller.cs - - - True - True - Settings.settings - - - - - - Component - - - Service.cs - - - - Designer @@ -97,23 +59,17 @@ PreserveNewest - - Designer - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - ProjectInstaller.cs - - - Service.cs - + + + + 13.0.1 + + + + - - + - - - - - - - - - \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Config.cs b/src/Seq.Client.EventLog/Config.cs index 607adf2..dd02ecd 100644 --- a/src/Seq.Client.EventLog/Config.cs +++ b/src/Seq.Client.EventLog/Config.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Configuration; using System.IO; -using System.Linq; using System.Reflection; namespace Seq.Client.EventLog @@ -17,13 +15,6 @@ static Config() LogFolder = ConfigurationManager.AppSettings["LogFolder"]; HeartbeatInterval = GetInt(ConfigurationManager.AppSettings["HeartbeatInterval"]); IsDebug = GetBool(ConfigurationManager.AppSettings["IsDebug"]); - ProjectKey = ConfigurationManager.AppSettings["ProjectKey"]; - Responders = ConfigurationManager.AppSettings["Responders"]; - Priority = ConfigurationManager.AppSettings["Priority"]; - Tags = GetArray(ConfigurationManager.AppSettings["Tags"]); - InitialTimeEstimate = ConfigurationManager.AppSettings["InitialTimeEstimate"]; - RemainingTimeEstimate = ConfigurationManager.AppSettings["RemainingTimeEstimate"]; - DueDate = ConfigurationManager.AppSettings["DueDate"]; //Must be between 0 and 1 hour in seconds if (HeartbeatInterval < 0 || HeartbeatInterval > 3600) @@ -70,13 +61,6 @@ static Config() public static string LogFolder { get; } public static int HeartbeatInterval { get; } public static bool IsDebug { get; } - public static string ProjectKey { get; } - public static string Priority { get; } - public static string Responders { get; } - public static IEnumerable Tags { get; } - public static string InitialTimeEstimate { get; } - public static string RemainingTimeEstimate { get; } - public static string DueDate { get; } /// /// Convert the supplied to an @@ -89,7 +73,7 @@ private static int GetInt(object sourceObject) { var sourceString = string.Empty; - if (!Convert.IsDBNull(sourceObject)) sourceString = (string) sourceObject; + if (!Convert.IsDBNull(sourceObject)) sourceString = (string)sourceObject; if (int.TryParse(sourceString, out var destInt)) return destInt; @@ -108,17 +92,9 @@ private static bool GetBool(object sourceObject, bool trueIfEmpty = false) { var sourceString = string.Empty; - if (!Convert.IsDBNull(sourceObject)) sourceString = (string) sourceObject; + if (!Convert.IsDBNull(sourceObject)) sourceString = (string)sourceObject; return bool.TryParse(sourceString, out var destBool) ? destBool : trueIfEmpty; } - - private static IEnumerable GetArray(string value) - { - return (value ?? "") - .Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) - .Select(t => t.Trim()) - .ToArray(); - } } } \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index be65cf6..510b86a 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using Lurgle.Logging; using Newtonsoft.Json; -using Serilog; namespace Seq.Client.EventLog { - class EventLogClient + internal class EventLogClient { private List _eventLogListeners; @@ -35,7 +35,7 @@ private void LoadListeners(string configuration) filePath = configuration; } - Log.Information("Loading listener configuration from {ConfigurationFilePath}", filePath); + Log.Information().Add("Loading listener configuration from {ConfigurationFilePath:l}", filePath); var file = File.ReadAllText(filePath); _eventLogListeners = JsonConvert.DeserializeObject>(file); @@ -43,26 +43,17 @@ private void LoadListeners(string configuration) private void ValidateListeners() { - foreach (var listener in _eventLogListeners) - { - listener.Validate(); - } + foreach (var listener in _eventLogListeners) listener.Validate(); } private void StartListeners(bool isInteractive = false) { - foreach (var listener in _eventLogListeners) - { - listener.Start(isInteractive); - } + foreach (var listener in _eventLogListeners) listener.Start(isInteractive); } private void StopListeners() { - foreach (var listener in _eventLogListeners) - { - listener.Stop(); - } + foreach (var listener in _eventLogListeners) listener.Stop(); } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index 2eb6f02..9b61ce0 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -1,59 +1,58 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.Eventing.Reader; using System.Threading; using System.Threading.Tasks; +using System.Xml.Linq; using Lurgle.Logging; -using Timer = System.Timers.Timer; namespace Seq.Client.EventLog { public class EventLogListener { + private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); + private EventLogQuery _eventLog; private bool _isInteractive; + private volatile bool _started; + private EventLogWatcher _watcher; public string LogName { get; set; } public string MachineName { get; set; } - public bool ProcessRetroactiveEntries { get; set; } // These properties allow for the filtering of events that will be sent to Seq. // If they are not specified in the JSON, all events in the log will be sent. public List LogLevels { get; set; } public List EventIds { get; set; } public List Sources { get; set; } + public string ProjectKey { get; set; } + public string Priority { get; set; } + public string Responders { get; set; } + public string Tags { get; set; } - - private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); - private EventLogQuery _eventLog; - private volatile bool _started; - private EventLogWatcher _watcher; + public string InitialTimeEstimate { get; set; } + public string RemainingTimeEstimate { get; set; } + public string DueDate { get; set; } public void Validate() { if (string.IsNullOrWhiteSpace(LogName)) - { throw new InvalidOperationException($"A {nameof(LogName)} must be specified for the listener."); - } } public void Start(bool isInteractive = false) { try { - Log.Information().Add("Starting listener for {LogName:l} on {MachineName:l}", LogName, - MachineName ?? "."); + Log.Information().AddProperty("LogName", LogName).AddProperty("LogLevels", LogLevels) + .AddProperty("EventIds", EventIds).AddProperty("Sources", Sources) + .AddProperty("ProjectKey", ProjectKey, false, false).AddProperty("Priority", Priority, false, false) + .AddProperty("Responders", Responders, false, false).AddProperty("Tags", Extensions.GetArray(Tags), false, false) + .AddProperty("InitialTimeEstimate", InitialTimeEstimate, false, false) + .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false).AddProperty("DueDate", DueDate, false, false) + .Add("Starting listener for {LogName:l} on {MachineName:l}", LogName); //Update this to calculate a query - _eventLog = new EventLogQuery("Security", PathType.LogName, - "*[System[band(Keywords,9007199254740992) and (EventID=4624)]]"); - - //if (ProcessRetroactiveEntries) - //{ - // // Start as a new task so it doesn't block the startup of the service. This has - // // to go on its own thread to avoid deadlocking via `Wait()`/`Result`. - // _retroactiveLoadingTask = Task.Factory.StartNew(SendRetroactiveEntries, TaskCreationOptions.LongRunning); - //} + _eventLog = new EventLogQuery(LogName, PathType.LogName, "*"); _isInteractive = isInteractive; _watcher = new EventLogWatcher(_eventLog); @@ -63,8 +62,9 @@ public void Start(bool isInteractive = false) } catch (Exception ex) { - Log.Exception(ex).Add("Failed to start listener for {LogName:l} on {MachineName:l}: {Message:l}", - LogName, MachineName ?? ".", ex.Message); + Log.Exception(ex).AddProperty("Message", ex.Message).Add( + "Failed to start listener for {LogName:l} on {MachineName:l}: {Message:l}", + LogName); } } @@ -79,12 +79,7 @@ public void Stop() _watcher.Enabled = false; _watcher.Dispose(); - // This would be a little racy if start and stop were ever called on different threads, but - // this isn't done, currently. - //_retroactiveLoadingTask?.Wait(); - Log.Debug().Add("Listener stopped for {LogName:l} on {MachineName:l}", LogName, MachineName ?? "."); - } catch (Exception ex) { @@ -92,39 +87,12 @@ public void Stop() } } - //private void SendRetroactiveEntries() - //{ - // try - // { - // using (var eventLog = OpenEventLog()) - // { - // Serilog.Log.Information("Processing {EntryCount} retroactive entries in {LogName}", eventLog.Entries.Count, LogName); - - // foreach (EventLogEntry entry in eventLog.Entries) - // { - // if (_cancel.IsCancellationRequested) - // { - // Serilog.Log.Warning("Canceling retroactive event loading"); - // return; - // } - - // HandleEventLogEntry(entry, eventLog.Log).GetAwaiter().GetResult(); - // } - // } - // } - // catch (Exception ex) - // { - // Serilog.Log.Error(ex, "Failed to send retroactive entries in {LogName} on {MachineName}", LogName, MachineName ?? "."); - // } - //} - private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs args) { try { //Ensure that events are new and have not been seen already. This addresses a scenario where event logs can repeatedly pass events to the handler. - if (args.EventRecord != null && args.EventRecord.TimeCreated >= ServiceManager.ServiceStart && - !ServiceManager.EventList.Contains(args.EventRecord.RecordId)) + if (args.EventRecord != null && args.EventRecord.TimeCreated >= ServiceManager.ServiceStart) await Task.Run(() => HandleEventLogEntry(args.EventRecord)); else if (args.EventRecord != null && args.EventRecord.TimeCreated < ServiceManager.ServiceStart) ServiceManager.OldEvents++; @@ -139,8 +107,6 @@ private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs arg private void HandleEventLogEntry(EventRecord entry) { - ServiceManager.EventList.Add(entry.RecordId); - // Don't send the entry to Seq if it doesn't match the filtered log levels, event IDs, or sources if (LogLevels != null && LogLevels.Count > 0 && entry.Level != null && !LogLevels.Contains((byte)entry.Level)) @@ -164,14 +130,79 @@ private void HandleEventLogEntry(EventRecord entry) try { - - Log.Level(Extensions.MapLogLevel(entry.Level)).AddProperty("Xml", entry.ToXml()) - .Add(entry.FormatDescription()); + Log.Level(Extensions.MapLogLevel(entry)).AddProperty("LogName", LogName) + .SetTimestamp(entry.TimeCreated ?? DateTime.Now) + .AddProperty("Provider", entry.ProviderName).AddProperty("EventId", entry.Id) + .AddProperty("KeywordNames", entry.KeywordsDisplayNames) + .AddProperty("EventLevel", entry.LevelDisplayName) + .AddProperty("EventLevelId", entry.Level).AddProperty(ParseXml(entry.ToXml())) + .AddProperty("Description", entry.FormatDescription()) + .AddProperty("Summary", GetMessage(entry.FormatDescription())) + .AddProperty("ProjectKey", ProjectKey, false, false).AddProperty("Priority", Priority, false, false) + .AddProperty("Responders", Responders, false, false).AddProperty("Tags", Extensions.GetArray(Tags), false, false) + .AddProperty("InitialTimeEstimate", InitialTimeEstimate, false, false) + .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false).AddProperty("DueDate", DueDate, false, false) + .Add("[{LogName:l}] - ({EventLevel:l}) - Event Id {EventId} - {Summary:l}"); + + ServiceManager.EventsProcessed++; } catch (Exception ex) { Log.Exception(ex).Add("Error parsing event: {Message:l}", ex.Message); } } + + private Dictionary ParseXml(string xml) + { + var result = new Dictionary(); + if (string.IsNullOrEmpty(xml)) + return result; + + try + { + var xmlDoc = XElement.Parse(xml); + return ProcessNode(xmlDoc); + } + catch (Exception ex) + { + Log.Exception(ex).Add(ex.Message); + return new Dictionary(); + } + } + + private Dictionary ProcessNode(XElement element, int depth = 0, string name = null) + { + var result = new Dictionary(); + var nodeName = !string.IsNullOrEmpty(name) ? name : element.Name.LocalName; + + if (!element.HasElements && !element.IsEmpty) + result.Add(nodeName, element.Value); + else + foreach (var descendant in element.Elements()) + foreach (var node in ProcessNode(descendant, depth + 1, + depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) + ? string.Format($"{nodeName}-{GetName(descendant)}") + : GetName(descendant))) + result.Add(node.Key, node.Value); + + return result; + } + + private string GetName(XElement element) + { + if (element.HasAttributes && + element.FirstAttribute.Name.LocalName.Equals("Name", StringComparison.OrdinalIgnoreCase)) + return element.FirstAttribute.Value; + return element.Name.LocalName; + } + + private string GetMessage(string message) + { + return message.Contains(Environment.NewLine) && + !string.IsNullOrEmpty(message.Substring(0, + message.IndexOf(Environment.NewLine, StringComparison.Ordinal))) + ? message.Substring(0, message.IndexOf(Environment.NewLine, StringComparison.Ordinal)) + : message; + } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index 88565fe..9d69634 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -1,22 +1,29 @@ [ { "LogName": "Application", - "LogLevels": [ 1, 2 ], - "ProcessRetroactiveEntries": true + "LogLevels": [1, 2] }, { "LogName": "Security", - "LogLevels": [ 16 ], - "ProcessRetroactiveEntries": true + "LogLevels": [16], + "ProjectKey": "TEST", + "Priority": "High", + "Responders": "MattM", + "Tags": "Extra,Secure,Logging", + "InitialTimeEstimate": "1h", + "RemainingTimeEstimate": "1h", + "DueDate": "7d" + }, + { + "LogName": "Security", + "LogLevels": [16] }, { "LogName": "Setup", - "LogLevels": [ 1, 2 ], - "ProcessRetroactiveEntries": true + "LogLevels": [1, 2] }, { "LogName": "System", - "LogLevels": [ 1, 2 ], - "ProcessRetroactiveEntries": true + "LogLevels": [1, 2] } -] +] \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index 4da8a3d..9c99641 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -1,55 +1,52 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.Linq; using Lurgle.Logging; namespace Seq.Client.EventLog { public static class Extensions { - public static LurgLevel MapLogLevel(byte? type) + public static LurgLevel MapLogLevel(EventRecord entry) { - switch (type) + if (entry.Level == null && entry.Keywords == null) + return LurgLevel.Debug; + + if (entry.Keywords != null) { - case (byte?)EventLogEntryType.Information: + if (((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords.AuditSuccess)) + return LurgLevel.Error; + + if (((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords.AuditFailure)) + return LurgLevel.Warning; + } + + // ReSharper disable once PossibleInvalidOperationException + switch ((byte)entry.Level) + { + case (byte)EventLogEntryType.Information: return LurgLevel.Information; - case (byte?)EventLogEntryType.Warning: + case (byte)EventLogEntryType.Warning: return LurgLevel.Warning; - case (byte?)EventLogEntryType.Error: + case (byte)EventLogEntryType.Error: return LurgLevel.Error; - case (byte?)EventLogEntryType.SuccessAudit: + case (byte)EventLogEntryType.SuccessAudit: return LurgLevel.Information; - case (byte?)EventLogEntryType.FailureAudit: + case (byte)EventLogEntryType.FailureAudit: return LurgLevel.Warning; default: return LurgLevel.Debug; } } -// public static RawEvents ToDto(this EventLogEntry entry, string logName) -// { -// return new RawEvents -// { -// Events = new[] -// { -// new RawEvent -// { -// Timestamp = entry.TimeGenerated, -// Level = MapLogLevel(entry.EntryType), -// MessageTemplate = entry.Message, -// Properties = new Dictionary -// { -// { "MachineName", entry.MachineName }, -//#pragma warning disable 618 -// { "EventId", entry.EventID }, -//#pragma warning restore 618 -// { "InstanceId", entry.InstanceId }, -// { "Source", entry.Source }, -// { "Category", entry.CategoryNumber }, -// { "EventLogName", logName } -// } -// }, -// } -// }; -// } + public static IEnumerable GetArray(string value) + { + return (value ?? "") + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()) + .ToArray(); + } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Program.cs b/src/Seq.Client.EventLog/Program.cs index f3353fa..c71daa5 100644 --- a/src/Seq.Client.EventLog/Program.cs +++ b/src/Seq.Client.EventLog/Program.cs @@ -9,13 +9,13 @@ namespace Seq.Client.EventLog { - static class Program + internal static class Program { /// - /// The main entry point for the application. - /// The service can be installed or uninstalled from the command line - /// by passing the /install or /uninstall argument, and can be run - /// interactively by specifying the path to the JSON configuration file. + /// The main entry point for the application. + /// The service can be installed or uninstalled from the command line + /// by passing the /install or /uninstall argument, and can be run + /// interactively by specifying the path to the JSON configuration file. /// public static void Main(string[] args) { @@ -23,10 +23,7 @@ public static void Main(string[] args) if (Environment.UserInteractive) { var parameter = string.Concat(args); - if (string.IsNullOrWhiteSpace(parameter)) - { - parameter = null; - } + if (string.IsNullOrWhiteSpace(parameter)) parameter = null; switch (parameter) { @@ -47,10 +44,10 @@ public static void Main(string[] args) } } - static void RunInteractive(string configFilePath) + private static void RunInteractive(string configFilePath) { Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, - logType: new List {LogType.Console, LogType.Seq}, logSeqServer: Config.SeqServer, + logType: new List { LogType.Console, LogType.Seq }, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, logLevel: LurgLevel.Verbose, logLevelConsole: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); @@ -88,7 +85,7 @@ static void RunInteractive(string configFilePath) } } - static void RunService() + private static void RunService() { var logFolder = Config.LogFolder; @@ -101,7 +98,7 @@ static void RunService() var logFile = Path.Combine(logFolder ?? string.Empty, "ServiceLog.txt"); Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, - logType: new List {LogType.File, LogType.Seq}, logDays: 7, logName: Config.AppName, + logType: new List { LogType.File, LogType.Seq }, logDays: 7, logName: Config.AppName, logFolder: Config.LogFolder, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, logLevel: LurgLevel.Verbose, logLevelFile: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); @@ -119,7 +116,6 @@ static void RunService() ServiceManager.Stop(); Log.Debug() .Add("{AppName:l} v{AppVersion:l} Stopped", Config.AppName, Config.AppVersion); - } catch (Exception ex) { @@ -131,4 +127,4 @@ static void RunService() } } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/ProjectInstaller.cs b/src/Seq.Client.EventLog/ProjectInstaller.cs index aad38b1..784e007 100644 --- a/src/Seq.Client.EventLog/ProjectInstaller.cs +++ b/src/Seq.Client.EventLog/ProjectInstaller.cs @@ -1,19 +1,14 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; using System.Configuration.Install; -using System.Linq; -using System.Threading.Tasks; namespace Seq.Client.EventLog { [RunInstaller(true)] - public partial class ProjectInstaller : System.Configuration.Install.Installer + public partial class ProjectInstaller : Installer { public ProjectInstaller() { InitializeComponent(); } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 9625a58..feb13f6 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -1,4 +1,5 @@  + Debug @@ -55,13 +56,14 @@ Designer + PreserveNewest PreserveNewest - + 13.0.1 diff --git a/src/Seq.Client.EventLog/Service.cs b/src/Seq.Client.EventLog/Service.cs index 6362b9c..a14f8b2 100644 --- a/src/Seq.Client.EventLog/Service.cs +++ b/src/Seq.Client.EventLog/Service.cs @@ -7,6 +7,7 @@ public partial class Service : ServiceBase private readonly EventLogClient _client = new EventLogClient(); #region Windows Service Base + public Service() { InitializeComponent(); @@ -16,11 +17,12 @@ protected override void OnStart(string[] args) { _client.Start(); } - + protected override void OnStop() { _client.Stop(); } + #endregion } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index b0ee98a..4a8f774 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Timers; using Lurgle.Logging; @@ -13,17 +9,12 @@ public static class ServiceManager private static Timer _heartbeatTimer; public static readonly DateTime ServiceStart = DateTime.Now; public static long EventsProcessed; + public static long LastProcessed; public static long UnhandledEvents; public static long OldEvents; public static long EmptyEvents; - public static TimedEventBag EventList { get; private set; } private static bool _isInteractive; - static ServiceManager() - { - EventList = new TimedEventBag(600); - } - public static void Start(bool isInteractive) { _isInteractive = isInteractive; @@ -31,8 +22,8 @@ public static void Start(bool isInteractive) if (Config.HeartbeatInterval <= 0) return; //First heartbeat will be at a random interval between 2 and 10 seconds _heartbeatTimer = isInteractive - ? new Timer {Interval = 10000} - : new Timer {Interval = new Random().Next(2000, 10000)}; + ? new Timer { Interval = 10000 } + : new Timer { Interval = new Random().Next(2000, 10000) }; _heartbeatTimer.Elapsed += ServiceHeartbeat; _heartbeatTimer.AutoReset = false; _heartbeatTimer.Start(); @@ -46,18 +37,19 @@ public static void Stop() private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) { Log.Debug() - .AddProperty("ItemCount", EventList.Count) - .AddProperty("EventsProcessed", EventsProcessed) + .AddProperty("EventsProcessed", EventsProcessed - LastProcessed) + .AddProperty("TotalProcessed", EventsProcessed) .AddProperty("OldEvents", OldEvents) .AddProperty("EmptyEvents", EmptyEvents) .AddProperty("UnhandledEvents", UnhandledEvents) .AddProperty("NextTime", DateTime.Now.AddMilliseconds(Config.HeartbeatInterval)) .Add( Config.IsDebug - ? "{AppName:l} Heartbeat [{MachineName:l}] - Event cache: {ItemCount}, Logons detected: {LogonsDetected}, " + - "Non-interactive logons: {NonInteractiveLogons}, Unhandled events: {UnhandledEvents}, Old events seen: {OldEvents}, " + - "Empty events: {EmptyEvents}, Next Heartbeat: {NextTime:H:mm:ss tt}" - : "{AppName:l} Heartbeat [{MachineName:l}] - Event cache: {ItemCount}, Next Heartbeat: {NextTime:H:mm:ss tt}"); + ? "{AppName:l} Heartbeat [{MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + + "Unhandled events: {UnhandledEvents}, Old events seen: {OldEvents}, Empty events: {EmptyEvents}, Next Heartbeat: {NextTime:H:mm:ss tt}" + : "{AppName:l} Heartbeat [{MachineName:l}] - Events Processed {EventsProcessed}, Total Processed: {TotalProcessed}, Next Heartbeat: {NextTime:H:mm:ss tt}"); + + LastProcessed = EventsProcessed; if (_heartbeatTimer.AutoReset) return; //Set the timer to 10 minutes after initial heartbeat @@ -66,4 +58,4 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) _heartbeatTimer.Start(); } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/TimedEventBag.cs b/src/Seq.Client.EventLog/TimedEventBag.cs deleted file mode 100644 index 9d8a5d5..0000000 --- a/src/Seq.Client.EventLog/TimedEventBag.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Microsoft.Extensions.Caching.Memory; - -// ReSharper disable UnusedMember.Global - -namespace Seq.Client.EventLog -{ - public class TimedEventBag - { - //Todo - handle multiple event logs - private readonly MemoryCache _cache; - private readonly MemoryCacheEntryOptions _cachePolicy; - - /// - /// Cache objects that have already been seen, and expire them after X seconds - /// - /// - public TimedEventBag(int expiration) - { - expiration = expiration >= 0 ? expiration : 600; - _cache = new MemoryCache(new MemoryCacheOptions {ExpirationScanFrequency = TimeSpan.FromSeconds(1)}); - _cachePolicy = new MemoryCacheEntryOptions {SlidingExpiration = TimeSpan.FromSeconds(expiration)}; - } - - public int Count => _cache.Count; - - public void Add(long? item) - { - if (item == null) return; - if (!Contains(item)) - _cache.Set(((long) item).ToString(), (long) item, _cachePolicy); - } - - public bool Contains(long? item) - { - return item != null && _cache.TryGetValue(((long) item).ToString(), out _); - } - } -} \ No newline at end of file From c2b713dbf7d66e681891ad1fea439c7284d56cac Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Wed, 13 Oct 2021 13:00:28 +1100 Subject: [PATCH 04/34] Reinstate & enhance remote log capability Add per-log message template Improve logging Add day rollover for stats Allow disabling file logging Reinstate and enhance retroactive processing --- src/Seq.Client.EventLog/App.config | 6 +- src/Seq.Client.EventLog/Config.cs | 16 +- src/Seq.Client.EventLog/EventLogClient.cs | 38 +++- src/Seq.Client.EventLog/EventLogListener.cs | 190 +++++++++++------- .../EventLogListeners.json | 14 +- src/Seq.Client.EventLog/Extensions.cs | 66 +++++- src/Seq.Client.EventLog/Program.cs | 65 ++++-- src/Seq.Client.EventLog/ServiceManager.cs | 40 +++- 8 files changed, 310 insertions(+), 125 deletions(-) diff --git a/src/Seq.Client.EventLog/App.config b/src/Seq.Client.EventLog/App.config index 5db4ebd..80d4c9e 100644 --- a/src/Seq.Client.EventLog/App.config +++ b/src/Seq.Client.EventLog/App.config @@ -8,9 +8,11 @@ - + + + - + diff --git a/src/Seq.Client.EventLog/Config.cs b/src/Seq.Client.EventLog/Config.cs index dd02ecd..fcf4a5a 100644 --- a/src/Seq.Client.EventLog/Config.cs +++ b/src/Seq.Client.EventLog/Config.cs @@ -12,15 +12,18 @@ static Config() AppName = ConfigurationManager.AppSettings["AppName"]; SeqServer = ConfigurationManager.AppSettings["LogSeqServer"]; SeqApiKey = ConfigurationManager.AppSettings["LogSeqApiKey"]; + LogToFile = GetBool(ConfigurationManager.AppSettings["LogSeqApiKey"], true); LogFolder = ConfigurationManager.AppSettings["LogFolder"]; HeartbeatInterval = GetInt(ConfigurationManager.AppSettings["HeartbeatInterval"]); - IsDebug = GetBool(ConfigurationManager.AppSettings["IsDebug"]); - //Must be between 0 and 1 hour in seconds - if (HeartbeatInterval < 0 || HeartbeatInterval > 3600) - HeartbeatInterval = 600000; - else - HeartbeatInterval *= 1000; + //Minimum is 0 (disabled) + if (HeartbeatInterval < 0) + HeartbeatInterval = 600; + //Maximum is 3600 + if (HeartbeatInterval > 3600) + HeartbeatInterval = 3600; + + IsDebug = GetBool(ConfigurationManager.AppSettings["IsDebug"]); var isSuccess = true; try @@ -58,6 +61,7 @@ static Config() public static string AppVersion { get; } public static string SeqServer { get; } public static string SeqApiKey { get; } + public static bool LogToFile { get; } public static string LogFolder { get; } public static int HeartbeatInterval { get; } public static bool IsDebug { get; } diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index 510b86a..d706304 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using Lurgle.Logging; @@ -17,9 +18,11 @@ public void Start(bool isInteractive = false, string configuration = null) StartListeners(isInteractive); } - public void Stop() + public void Stop(string configuration = null) { StopListeners(); + if (ServiceManager.SaveOnExit) + SaveListeners(configuration); } private void LoadListeners(string configuration) @@ -35,7 +38,7 @@ private void LoadListeners(string configuration) filePath = configuration; } - Log.Information().Add("Loading listener configuration from {ConfigurationFilePath:l}", filePath); + Log.Information().Add("Loading listener configuration from {ConfigurationFilePath:l} on {MachineName:l} ...", filePath); var file = File.ReadAllText(filePath); _eventLogListeners = JsonConvert.DeserializeObject>(file); @@ -46,6 +49,35 @@ private void ValidateListeners() foreach (var listener in _eventLogListeners) listener.Validate(); } + private void SaveListeners(string configuration) + { + string filePath; + if (configuration == null) + { + var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + filePath = Path.Combine(directory ?? ".", "EventLogListeners.json"); + } + else + { + filePath = configuration; + } + + try + { + var json = JsonConvert.SerializeObject(_eventLogListeners, Formatting.Indented, + new JsonSerializerSettings() { }); + + Log.Information() + .Add("Saving listener configuration to {ConfigurationFilePath:l} on {MachineName:l} ...", filePath); + File.WriteAllText(filePath, json); + } + catch (Exception ex) + { + Log.Exception(ex).AddProperty("Message", ex.Message) + .Add("Error saving {ConfigurationFilePath:l} on (MachineName:l}: {Message:l}", filePath); + } + } + private void StartListeners(bool isInteractive = false) { foreach (var listener in _eventLogListeners) listener.Start(isInteractive); diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index 9b61ce0..cbac9d9 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -5,6 +5,9 @@ using System.Threading.Tasks; using System.Xml.Linq; using Lurgle.Logging; +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable MemberCanBePrivate.Global namespace Seq.Client.EventLog { @@ -17,8 +20,13 @@ public class EventLogListener private EventLogWatcher _watcher; public string LogName { get; set; } + + //Logged as RemoteServer to avoid conflict with the inbuilt MachineName property public string MachineName { get; set; } + public string MessageTemplate { get; set; } = + "[{LogName:l}] - ({EventLevel:l}) - Event Id {EventId} - {Summary:l}"; + // These properties allow for the filtering of events that will be sent to Seq. // If they are not specified in the JSON, all events in the log will be sent. public List LogLevels { get; set; } @@ -33,6 +41,10 @@ public class EventLogListener public string RemainingTimeEstimate { get; set; } public string DueDate { get; set; } + public bool ProcessRetroactiveEntries { get; set; } + //This is used to save the current place in the logs on service exit + public EventBookmark CurrentBookmark { get; set; } + public void Validate() { if (string.IsNullOrWhiteSpace(LogName)) @@ -45,26 +57,80 @@ public void Start(bool isInteractive = false) { Log.Information().AddProperty("LogName", LogName).AddProperty("LogLevels", LogLevels) .AddProperty("EventIds", EventIds).AddProperty("Sources", Sources) - .AddProperty("ProjectKey", ProjectKey, false, false).AddProperty("Priority", Priority, false, false) - .AddProperty("Responders", Responders, false, false).AddProperty("Tags", Extensions.GetArray(Tags), false, false) + .AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("ProjectKey", ProjectKey, false, false) + .AddProperty("Priority", Priority, false, false) + .AddProperty("Responders", Responders, false, false) + .AddProperty("Tags", Extensions.GetArray(Tags), false, false) .AddProperty("InitialTimeEstimate", InitialTimeEstimate, false, false) - .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false).AddProperty("DueDate", DueDate, false, false) - .Add("Starting listener for {LogName:l} on {MachineName:l}", LogName); + .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false) + .AddProperty("DueDate", DueDate, false, false) + .Add("Starting {ListenerType:l} listener for {LogName:l} on {MachineName:l}", + Extensions.GetListenerType(MachineName), LogName); - //Update this to calculate a query _eventLog = new EventLogQuery(LogName, PathType.LogName, "*"); - _isInteractive = isInteractive; - _watcher = new EventLogWatcher(_eventLog); + var session = new EventLogSession(); + if (string.IsNullOrEmpty(MachineName)) + session = new EventLogSession(MachineName); + _eventLog.Session = session; + + switch (ProcessRetroactiveEntries) + { + case true when CurrentBookmark != null: + var bookmarkChecker = new EventLogReader(_eventLog); + //Go back a position to allow the bookmark to be read + bookmarkChecker.Seek(CurrentBookmark, -1); + var checkBookmark = bookmarkChecker.ReadEvent(); + if (checkBookmark != null) + { + checkBookmark.Dispose(); + Log.Debug().Add("Logging from last bookmark for {LogName:l} on {MachineName:l}", LogName); + _watcher = new EventLogWatcher(_eventLog, CurrentBookmark, true); + + ServiceManager.SaveOnExit = true; + } + else + { + Log.Debug().Add("Cannot find last bookmark for {LogName:l} on {MachineName:l} - processing new events", LogName); + _watcher = new EventLogWatcher(_eventLog); + } + + ServiceManager.SaveOnExit = true; + bookmarkChecker.Dispose(); + break; + case true: + var getFirstEvent = new EventLogReader(_eventLog); + var firstEvent = getFirstEvent.ReadEvent(); + if (firstEvent != null) + { + Log.Debug().Add("Logging from first logged event for {LogName:l} on {MachineName:l}", LogName); + _watcher = new EventLogWatcher(_eventLog, firstEvent.Bookmark, true); + } + else + { + Log.Debug().Add("Cannot determine first event for {LogName:l} on {MachineName:l} - processing new events", LogName); + _watcher = new EventLogWatcher(_eventLog); + } + + ServiceManager.SaveOnExit = true; + break; + default: + Log.Debug().Add("Processing new events for {LogName:l} on {MachineName:l}", LogName); + _watcher = new EventLogWatcher(_eventLog); + break; + } + _watcher.EventRecordWritten += OnEntryWritten; _watcher.Enabled = true; _started = true; } catch (Exception ex) { - Log.Exception(ex).AddProperty("Message", ex.Message).Add( - "Failed to start listener for {LogName:l} on {MachineName:l}: {Message:l}", - LogName); + Log.Exception(ex).AddProperty("Message", ex.Message) + .AddProperty("RemoteServer", MachineName, false, false).Add( + "Failed to start {ListenerType:l} listener for {LogName:l} on {MachineName:l}: {Message:l}", + Extensions.GetListenerType(MachineName), LogName); } } @@ -79,11 +145,14 @@ public void Stop() _watcher.Enabled = false; _watcher.Dispose(); - Log.Debug().Add("Listener stopped for {LogName:l} on {MachineName:l}", LogName, MachineName ?? "."); + Log.Debug().AddProperty("RemoteServer", MachineName, false, false).Add("{ListenerType:l} listener stopped for {LogName:l} on {MachineName:l}", Extensions.GetListenerType(MachineName), LogName); } catch (Exception ex) { - Log.Exception(ex).Add("Failed to stop listener: {Message:l}", ex.Message); + Log.Exception(ex).AddProperty("Message", ex.Message) + .AddProperty("RemoteServer", MachineName, false, false).Add( + "Failed to stop {ListenerType:l} listener for {LogName:l} on {MachineName:l}: {Message:l}", + Extensions.GetListenerType(MachineName), LogName); } } @@ -92,16 +161,19 @@ private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs arg try { //Ensure that events are new and have not been seen already. This addresses a scenario where event logs can repeatedly pass events to the handler. - if (args.EventRecord != null && args.EventRecord.TimeCreated >= ServiceManager.ServiceStart) + if (args.EventRecord != null && (ProcessRetroactiveEntries || !ProcessRetroactiveEntries && args.EventRecord.TimeCreated >= ServiceManager.ServiceStart)) await Task.Run(() => HandleEventLogEntry(args.EventRecord)); - else if (args.EventRecord != null && args.EventRecord.TimeCreated < ServiceManager.ServiceStart) + else if (args.EventRecord != null && !ProcessRetroactiveEntries && args.EventRecord.TimeCreated < ServiceManager.ServiceStart) ServiceManager.OldEvents++; else if (args.EventRecord == null) ServiceManager.EmptyEvents++; } catch (Exception ex) { - Log.Exception(ex).Add("Failed to handle {EventLog} log entry: {Message:l}", LogName, ex.Message); + Log.Exception(ex).AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("Message", ex.Message) + .Add("Failed to handle {ListenerType:l} {LogName:l} log entry on {MachineName:l}: {Message:l}", + Extensions.GetListenerType(MachineName), LogName); } } @@ -130,79 +202,43 @@ private void HandleEventLogEntry(EventRecord entry) try { - Log.Level(Extensions.MapLogLevel(entry)).AddProperty("LogName", LogName) + if (ProcessRetroactiveEntries) + CurrentBookmark = entry.Bookmark; + + Log.Level(Extensions.MapLogLevel(entry)) .SetTimestamp(entry.TimeCreated ?? DateTime.Now) - .AddProperty("Provider", entry.ProviderName).AddProperty("EventId", entry.Id) + .AddProperty("LogName", LogName) + .AddProperty("LogLevels", LogLevels) + .AddProperty("EventIds", EventIds) + .AddProperty("Sources", Sources) + .AddProperty("Provider", entry.ProviderName) + .AddProperty("EventId", entry.Id) .AddProperty("KeywordNames", entry.KeywordsDisplayNames) + .AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) .AddProperty("EventLevel", entry.LevelDisplayName) - .AddProperty("EventLevelId", entry.Level).AddProperty(ParseXml(entry.ToXml())) + .AddProperty("EventLevelId", entry.Level) .AddProperty("Description", entry.FormatDescription()) - .AddProperty("Summary", GetMessage(entry.FormatDescription())) - .AddProperty("ProjectKey", ProjectKey, false, false).AddProperty("Priority", Priority, false, false) - .AddProperty("Responders", Responders, false, false).AddProperty("Tags", Extensions.GetArray(Tags), false, false) + .AddProperty("Summary", Extensions.GetMessage(entry.FormatDescription())) + .AddProperty("ProjectKey", ProjectKey, false, false) + .AddProperty("Priority", Priority, false, false) + .AddProperty("Responders", Responders, false, false) + .AddProperty("Tags", Extensions.GetArray(Tags), false, false) .AddProperty("InitialTimeEstimate", InitialTimeEstimate, false, false) - .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false).AddProperty("DueDate", DueDate, false, false) - .Add("[{LogName:l}] - ({EventLevel:l}) - Event Id {EventId} - {Summary:l}"); + .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false) + .AddProperty("DueDate", DueDate, false, false) + .AddProperty(Extensions.ParseXml(entry.ToXml())) + .Add(MessageTemplate); ServiceManager.EventsProcessed++; } catch (Exception ex) { - Log.Exception(ex).Add("Error parsing event: {Message:l}", ex.Message); - } - } - - private Dictionary ParseXml(string xml) - { - var result = new Dictionary(); - if (string.IsNullOrEmpty(xml)) - return result; - - try - { - var xmlDoc = XElement.Parse(xml); - return ProcessNode(xmlDoc); - } - catch (Exception ex) - { - Log.Exception(ex).Add(ex.Message); - return new Dictionary(); + Log.Exception(ex).AddProperty("Message", ex.Message) + .AddProperty("RemoteServer", MachineName, false, false).Add( + "Error parsing {ListenerType:l} {LogName:l} event on {MachineName:l}: {Message:l}", + Extensions.GetListenerType(MachineName), LogName); } } - - private Dictionary ProcessNode(XElement element, int depth = 0, string name = null) - { - var result = new Dictionary(); - var nodeName = !string.IsNullOrEmpty(name) ? name : element.Name.LocalName; - - if (!element.HasElements && !element.IsEmpty) - result.Add(nodeName, element.Value); - else - foreach (var descendant in element.Elements()) - foreach (var node in ProcessNode(descendant, depth + 1, - depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) - ? string.Format($"{nodeName}-{GetName(descendant)}") - : GetName(descendant))) - result.Add(node.Key, node.Value); - - return result; - } - - private string GetName(XElement element) - { - if (element.HasAttributes && - element.FirstAttribute.Name.LocalName.Equals("Name", StringComparison.OrdinalIgnoreCase)) - return element.FirstAttribute.Value; - return element.Name.LocalName; - } - - private string GetMessage(string message) - { - return message.Contains(Environment.NewLine) && - !string.IsNullOrEmpty(message.Substring(0, - message.IndexOf(Environment.NewLine, StringComparison.Ordinal))) - ? message.Substring(0, message.IndexOf(Environment.NewLine, StringComparison.Ordinal)) - : message; - } } } \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index 9d69634..fe33757 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -5,7 +5,8 @@ }, { "LogName": "Security", - "LogLevels": [16], + //"LogLevels": [16], + "MessageTemplate": "[{LogName:l}] - {ListenerType:l} - ({EventLevel:l}) - Event Id {EventId} - {Summary:l}", "ProjectKey": "TEST", "Priority": "High", "Responders": "MattM", @@ -14,13 +15,14 @@ "RemainingTimeEstimate": "1h", "DueDate": "7d" }, - { - "LogName": "Security", - "LogLevels": [16] - }, + //{ + // "LogName": "Security", + // "LogLevels": [ 16 ], + // "MachineName": "TESTPC" + //}, { "LogName": "Setup", - "LogLevels": [1, 2] + "ProcessRetroactiveEntries": true }, { "LogName": "System", diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index 9c99641..e7684b5 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.Eventing.Reader; using System.Linq; +using System.Xml.Linq; using Lurgle.Logging; namespace Seq.Client.EventLog @@ -17,8 +18,8 @@ public static LurgLevel MapLogLevel(EventRecord entry) if (entry.Keywords != null) { if (((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords.AuditSuccess)) - return LurgLevel.Error; - + return LurgLevel.Information; + if (((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords.AuditFailure)) return LurgLevel.Warning; } @@ -48,5 +49,66 @@ public static IEnumerable GetArray(string value) .Select(t => t.Trim()) .ToArray(); } + + public static string GetListenerType(string remoteServer) + { + if (string.IsNullOrEmpty(remoteServer) || remoteServer == ".") + return "Local"; + else + return string.Format($"\\\\{remoteServer}"); + } + + public static Dictionary ParseXml(string xml) + { + var result = new Dictionary(); + if (string.IsNullOrEmpty(xml)) + return result; + + try + { + var xmlDoc = XElement.Parse(xml); + return ProcessNode(xmlDoc); + } + catch (Exception ex) + { + Log.Exception(ex).Add(ex.Message); + return new Dictionary(); + } + } + + private static Dictionary ProcessNode(XElement element, int depth = 0, string name = null) + { + var result = new Dictionary(); + var nodeName = !string.IsNullOrEmpty(name) ? name : element.Name.LocalName; + + if (!element.HasElements && !element.IsEmpty) + result.Add(nodeName, element.Value); + else + foreach (var descendant in element.Elements()) + foreach (var node in ProcessNode(descendant, depth + 1, + depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) + ? string.Format($"{nodeName}-{GetName(descendant)}") + : GetName(descendant))) + result.Add(node.Key, node.Value); + + return result; + } + + private static string GetName(XElement element) + { + if (element.HasAttributes && + element.FirstAttribute.Name.LocalName.Equals("Name", StringComparison.OrdinalIgnoreCase)) + return element.FirstAttribute.Value; + return element.Name.LocalName; + } + + public static string GetMessage(string message) + { + return message.Contains(Environment.NewLine) && + !string.IsNullOrEmpty(message.Substring(0, + message.IndexOf(Environment.NewLine, StringComparison.Ordinal))) + ? message.Substring(0, message.IndexOf(Environment.NewLine, StringComparison.Ordinal)) + : message; + } } } \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Program.cs b/src/Seq.Client.EventLog/Program.cs index c71daa5..47ad877 100644 --- a/src/Seq.Client.EventLog/Program.cs +++ b/src/Seq.Client.EventLog/Program.cs @@ -54,8 +54,8 @@ private static void RunInteractive(string configFilePath) try { Log.Debug() - .Add("{AppName:l} v{AppVersion:l} Starting ...", Config.AppName, Config.AppVersion); - Log.Debug().Add("Running interactively"); + .Add("{AppName:l} v{AppVersion:l} Starting in interactive mode on {MachineName:l} ...", + Config.AppName, Config.AppVersion); var client = new EventLogClient(); client.Start(true, configFilePath); @@ -65,18 +65,20 @@ private static void RunInteractive(string configFilePath) Console.CancelKeyPress += (s, e) => { Log.Debug().Add("Ctrl+C pressed, stopping"); - client.Stop(); + client.Stop(configFilePath); done.Set(); }; done.WaitOne(); ServiceManager.Stop(); Log.Debug() - .Add("{AppName:l} v{AppVersion:l} Stopped", Config.AppName, Config.AppVersion); + .Add("{AppName:l} v{AppVersion:l} Stopped in interactive mode on {MachineName:l}", Config.AppName, + Config.AppVersion); } catch (Exception ex) { - Log.Exception(ex).Add("An unhandled exception occurred"); + Log.Exception(ex).AddProperty("Message", ex.Message) + .Add("An unhandled exception occurred on {MachineName:l}: {Message:l}"); Environment.ExitCode = 1; } finally @@ -87,39 +89,58 @@ private static void RunInteractive(string configFilePath) private static void RunService() { - var logFolder = Config.LogFolder; + var logFile = string.Empty; + if (Config.LogToFile) + { + var logFolder = Config.LogFolder; - if (string.IsNullOrEmpty(logFolder)) - logFolder = Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty, "Logs"); + if (string.IsNullOrEmpty(logFolder)) + logFolder = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty, "Logs"); - if (!Directory.Exists(logFolder)) - Directory.CreateDirectory(logFolder); + if (!Directory.Exists(logFolder)) + Directory.CreateDirectory(logFolder); - var logFile = Path.Combine(logFolder ?? string.Empty, "ServiceLog.txt"); - Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, - logType: new List { LogType.File, LogType.Seq }, logDays: 7, logName: Config.AppName, - logFolder: Config.LogFolder, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, - logLevel: LurgLevel.Verbose, logLevelFile: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); + logFile = Path.Combine(logFolder ?? string.Empty, "ServiceLog.txt"); + + Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, + logType: new List { LogType.File, LogType.Seq }, logDays: 7, logName: Config.AppName, + logFolder: Config.LogFolder, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, + logLevel: LurgLevel.Verbose, logLevelFile: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); + } + else + { + Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, + logType: new List { LogType.Seq }, logSeqServer: Config.SeqServer, + logSeqApiKey: Config.SeqApiKey, + logLevel: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); + } try { Log.Debug() - .Add("{AppName:l} v{AppVersion:l} Starting ...", Config.AppName, Config.AppVersion); + .Add("{AppName:l} v{AppVersion:l} Starting as service on {MachineName:l} ...", Config.AppName, + Config.AppVersion); Log.Debug() - .Add( - "LogFolder: {LogFolder:l}, LogPath: {LogPath:l}, Seq Server: {SeqServer:l}, Api Key: {ApiKeySet}", - Config.LogFolder, logFile, Config.SeqServer, !string.IsNullOrEmpty(Config.SeqApiKey)); + .AddProperty("LogFolder", Config.LogFolder, false, false) + .AddProperty("LogPath", logFile, false, false) + .AddProperty("SeqServer", Config.SeqServer) + .AddProperty("SeqApiKey", !string.IsNullOrEmpty(Config.SeqApiKey)) + .Add(Config.LogToFile + ? "{AppName:l} ((MachineName:l}) Log Config - LogFolder: {LogFolder:l}, LogPath: {LogPath:l}, Seq Server: {SeqServer:l}, Api Key: {SeqApiKey}" + : "{AppName:l} ((MachineName:l}) Log Config - Seq Server: {SeqServer:l}, Api Key: {SeqApiKey}"); Log.Debug().Add("Running as service"); ServiceManager.Start(false); ServiceBase.Run(new Service()); ServiceManager.Stop(); Log.Debug() - .Add("{AppName:l} v{AppVersion:l} Stopped", Config.AppName, Config.AppVersion); + .Add("{AppName:l} v{AppVersion:l} Stopped as service on {MachineName:l}", Config.AppName, + Config.AppVersion); } catch (Exception ex) { - Log.Exception(ex).Add("Exception thrown from service host: {Message:l}", ex.Message); + Log.Exception(ex).AddProperty("Message", ex.Message) + .Add("Exception thrown from service host on {MachineName:l}: {Message:l}"); } finally { diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index 4a8f774..18eb1f9 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -1,6 +1,7 @@ using System; using System.Timers; using Lurgle.Logging; +// ReSharper disable MemberCanBePrivate.Global namespace Seq.Client.EventLog { @@ -13,13 +14,23 @@ public static class ServiceManager public static long UnhandledEvents; public static long OldEvents; public static long EmptyEvents; + //This will be set if any listener has ProcessRetroactiveEntries enabled + public static bool SaveOnExit; private static bool _isInteractive; + private static DateTime _lastTime = DateTime.Now; + public static void Start(bool isInteractive) { _isInteractive = isInteractive; //Heartbeat timer that can be used to detect if the service is not running - if (Config.HeartbeatInterval <= 0) return; + if (Config.HeartbeatInterval <= 0) + { + if (Config.IsDebug) + Log.Debug().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat").Add("[{HeartbeatName:l} - {MachineName:l}] Heartbeat is disabled ..."); + return; + } + //First heartbeat will be at a random interval between 2 and 10 seconds _heartbeatTimer = isInteractive ? new Timer { Interval = 10000 } @@ -36,25 +47,40 @@ public static void Stop() private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) { + var timeNow = DateTime.Now; + var diff = EventsProcessed - LastProcessed; + + if (timeNow.Day != _lastTime.Day) + { + Log.Debug().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat").Add("[{HeartbeatName:l} - {MachineName:l}] Day rollover, resetting counters ..."); + EventsProcessed = 0; + LastProcessed = 0; + UnhandledEvents = 0; + OldEvents = 0; + EmptyEvents = 0; + } + Log.Debug() - .AddProperty("EventsProcessed", EventsProcessed - LastProcessed) + .AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") + .AddProperty("EventsProcessed", diff) .AddProperty("TotalProcessed", EventsProcessed) .AddProperty("OldEvents", OldEvents) .AddProperty("EmptyEvents", EmptyEvents) .AddProperty("UnhandledEvents", UnhandledEvents) - .AddProperty("NextTime", DateTime.Now.AddMilliseconds(Config.HeartbeatInterval)) + .AddProperty("NextTime", timeNow.AddMilliseconds(_isInteractive ? 60000 : Config.HeartbeatInterval * 1000)) .Add( Config.IsDebug - ? "{AppName:l} Heartbeat [{MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + + ? "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + "Unhandled events: {UnhandledEvents}, Old events seen: {OldEvents}, Empty events: {EmptyEvents}, Next Heartbeat: {NextTime:H:mm:ss tt}" - : "{AppName:l} Heartbeat [{MachineName:l}] - Events Processed {EventsProcessed}, Total Processed: {TotalProcessed}, Next Heartbeat: {NextTime:H:mm:ss tt}"); + : "[{HeartbeatName:l} - {MachineName:l}] - Events Processed {EventsProcessed}, Total Processed: {TotalProcessed}, Next Heartbeat: {NextTime:H:mm:ss tt}"); LastProcessed = EventsProcessed; + _lastTime = timeNow; if (_heartbeatTimer.AutoReset) return; - //Set the timer to 10 minutes after initial heartbeat + //Set the timer to 60 seconds (interactive) or configured heartbeat (service) after initial heartbeat _heartbeatTimer.AutoReset = true; - _heartbeatTimer.Interval = _isInteractive ? 10000 : Config.HeartbeatInterval; + _heartbeatTimer.Interval = _isInteractive ? 60000 : Config.HeartbeatInterval * 1000; _heartbeatTimer.Start(); } } From 43d09c7240f431ff4b3df9142e9d12fd93bcb99e Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Wed, 13 Oct 2021 14:11:36 +1100 Subject: [PATCH 05/34] Code cleanup, add StoreLastEntry option --- src/Seq.Client.EventLog.sln.DotSettings | 3 + src/Seq.Client.EventLog/App.config | 2 +- src/Seq.Client.EventLog/EventLogClient.cs | 6 +- src/Seq.Client.EventLog/EventLogListener.cs | 115 ++++++++++-------- .../EventLogListeners.json | 5 +- src/Seq.Client.EventLog/Extensions.cs | 3 +- src/Seq.Client.EventLog/ProjectInstaller.cs | 2 + src/Seq.Client.EventLog/ServiceManager.cs | 14 ++- 8 files changed, 85 insertions(+), 65 deletions(-) create mode 100644 src/Seq.Client.EventLog.sln.DotSettings diff --git a/src/Seq.Client.EventLog.sln.DotSettings b/src/Seq.Client.EventLog.sln.DotSettings new file mode 100644 index 0000000..921db25 --- /dev/null +++ b/src/Seq.Client.EventLog.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/src/Seq.Client.EventLog/App.config b/src/Seq.Client.EventLog/App.config index 80d4c9e..8be79e9 100644 --- a/src/Seq.Client.EventLog/App.config +++ b/src/Seq.Client.EventLog/App.config @@ -9,7 +9,7 @@ - + diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index d706304..641eb3c 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -38,7 +38,8 @@ private void LoadListeners(string configuration) filePath = configuration; } - Log.Information().Add("Loading listener configuration from {ConfigurationFilePath:l} on {MachineName:l} ...", filePath); + Log.Information() + .Add("Loading listener configuration from {ConfigurationFilePath:l} on {MachineName:l} ...", filePath); var file = File.ReadAllText(filePath); _eventLogListeners = JsonConvert.DeserializeObject>(file); @@ -64,8 +65,7 @@ private void SaveListeners(string configuration) try { - var json = JsonConvert.SerializeObject(_eventLogListeners, Formatting.Indented, - new JsonSerializerSettings() { }); + var json = JsonConvert.SerializeObject(_eventLogListeners, Formatting.Indented); Log.Information() .Add("Saving listener configuration to {ConfigurationFilePath:l} on {MachineName:l} ...", filePath); diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index cbac9d9..e9416f9 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -3,24 +3,27 @@ using System.Diagnostics.Eventing.Reader; using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; using Lurgle.Logging; + // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable MemberCanBePrivate.Global namespace Seq.Client.EventLog { + // ReSharper disable once ClassNeverInstantiated.Global public class EventLogListener { private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); private EventLogQuery _eventLog; + + // ReSharper disable once NotAccessedField.Local private bool _isInteractive; private volatile bool _started; private EventLogWatcher _watcher; public string LogName { get; set; } - + //Logged as RemoteServer to avoid conflict with the inbuilt MachineName property public string MachineName { get; set; } @@ -42,6 +45,10 @@ public class EventLogListener public string DueDate { get; set; } public bool ProcessRetroactiveEntries { get; set; } + + //When ProcessRetroactiveEntries isn't desirable, but processing events that occurred while the service was stopped is needed, use StoreLastEntry. ProcessRetroactiveEntries = true always supersedes this. + public bool StoreLastEntry { get; set; } + //This is used to save the current place in the logs on service exit public EventBookmark CurrentBookmark { get; set; } @@ -74,53 +81,7 @@ public void Start(bool isInteractive = false) if (string.IsNullOrEmpty(MachineName)) session = new EventLogSession(MachineName); _eventLog.Session = session; - - switch (ProcessRetroactiveEntries) - { - case true when CurrentBookmark != null: - var bookmarkChecker = new EventLogReader(_eventLog); - //Go back a position to allow the bookmark to be read - bookmarkChecker.Seek(CurrentBookmark, -1); - var checkBookmark = bookmarkChecker.ReadEvent(); - if (checkBookmark != null) - { - checkBookmark.Dispose(); - Log.Debug().Add("Logging from last bookmark for {LogName:l} on {MachineName:l}", LogName); - _watcher = new EventLogWatcher(_eventLog, CurrentBookmark, true); - - ServiceManager.SaveOnExit = true; - } - else - { - Log.Debug().Add("Cannot find last bookmark for {LogName:l} on {MachineName:l} - processing new events", LogName); - _watcher = new EventLogWatcher(_eventLog); - } - - ServiceManager.SaveOnExit = true; - bookmarkChecker.Dispose(); - break; - case true: - var getFirstEvent = new EventLogReader(_eventLog); - var firstEvent = getFirstEvent.ReadEvent(); - if (firstEvent != null) - { - Log.Debug().Add("Logging from first logged event for {LogName:l} on {MachineName:l}", LogName); - _watcher = new EventLogWatcher(_eventLog, firstEvent.Bookmark, true); - } - else - { - Log.Debug().Add("Cannot determine first event for {LogName:l} on {MachineName:l} - processing new events", LogName); - _watcher = new EventLogWatcher(_eventLog); - } - - ServiceManager.SaveOnExit = true; - break; - default: - Log.Debug().Add("Processing new events for {LogName:l} on {MachineName:l}", LogName); - _watcher = new EventLogWatcher(_eventLog); - break; - } - + _watcher = GetWatcherConfig(); _watcher.EventRecordWritten += OnEntryWritten; _watcher.Enabled = true; _started = true; @@ -134,6 +95,50 @@ public void Start(bool isInteractive = false) } } + private EventLogWatcher GetWatcherConfig() + { + var eventLog = new EventLogReader(_eventLog); + + if (CurrentBookmark != null && (ProcessRetroactiveEntries || StoreLastEntry)) + { + //Go back a position to allow the bookmark to be read + ServiceManager.SaveOnExit = true; + eventLog.Seek(CurrentBookmark, -1); + var checkBookmark = eventLog.ReadEvent(); + + if (checkBookmark != null) + { + checkBookmark.Dispose(); + Log.Debug().Add("Logging from last bookmark for {LogName:l} on {MachineName:l}", LogName); + return new EventLogWatcher(_eventLog, CurrentBookmark, true); + } + + Log.Debug().Add( + "Cannot find last bookmark for {LogName:l} on {MachineName:l} - processing new events", + LogName); + } + else if (ProcessRetroactiveEntries) + { + ServiceManager.SaveOnExit = true; + var firstEvent = eventLog.ReadEvent(); + if (firstEvent != null) + { + Log.Debug().Add("Logging from first logged event for {LogName:l} on {MachineName:l}", LogName); + return new EventLogWatcher(_eventLog, firstEvent.Bookmark, true); + } + + Log.Debug().Add( + "Cannot determine first event for {LogName:l} on {MachineName:l} - processing new events", + LogName); + } + else + { + Log.Debug().Add("Processing new events for {LogName:l} on {MachineName:l}", LogName); + } + + return new EventLogWatcher(_eventLog); + } + public void Stop() { try @@ -145,7 +150,9 @@ public void Stop() _watcher.Enabled = false; _watcher.Dispose(); - Log.Debug().AddProperty("RemoteServer", MachineName, false, false).Add("{ListenerType:l} listener stopped for {LogName:l} on {MachineName:l}", Extensions.GetListenerType(MachineName), LogName); + Log.Debug().AddProperty("RemoteServer", MachineName, false, false).Add( + "{ListenerType:l} listener stopped for {LogName:l} on {MachineName:l}", + Extensions.GetListenerType(MachineName), LogName); } catch (Exception ex) { @@ -161,9 +168,11 @@ private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs arg try { //Ensure that events are new and have not been seen already. This addresses a scenario where event logs can repeatedly pass events to the handler. - if (args.EventRecord != null && (ProcessRetroactiveEntries || !ProcessRetroactiveEntries && args.EventRecord.TimeCreated >= ServiceManager.ServiceStart)) + if (args.EventRecord != null && (ProcessRetroactiveEntries || StoreLastEntry || !ProcessRetroactiveEntries && + args.EventRecord.TimeCreated >= ServiceManager.ServiceStart)) await Task.Run(() => HandleEventLogEntry(args.EventRecord)); - else if (args.EventRecord != null && !ProcessRetroactiveEntries && args.EventRecord.TimeCreated < ServiceManager.ServiceStart) + else if (args.EventRecord != null && !ProcessRetroactiveEntries && + args.EventRecord.TimeCreated < ServiceManager.ServiceStart) ServiceManager.OldEvents++; else if (args.EventRecord == null) ServiceManager.EmptyEvents++; @@ -202,7 +211,7 @@ private void HandleEventLogEntry(EventRecord entry) try { - if (ProcessRetroactiveEntries) + if (ProcessRetroactiveEntries || StoreLastEntry) CurrentBookmark = entry.Bookmark; Log.Level(Extensions.MapLogLevel(entry)) diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index fe33757..3cbc8a2 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -13,7 +13,8 @@ "Tags": "Extra,Secure,Logging", "InitialTimeEstimate": "1h", "RemainingTimeEstimate": "1h", - "DueDate": "7d" + "DueDate": "7d", + "StoreLastEntry": true }, //{ // "LogName": "Security", @@ -22,7 +23,7 @@ //}, { "LogName": "Setup", - "ProcessRetroactiveEntries": true + "ProcessRetroactiveEntries": true }, { "LogName": "System", diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index e7684b5..d61f0eb 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -54,8 +54,7 @@ public static string GetListenerType(string remoteServer) { if (string.IsNullOrEmpty(remoteServer) || remoteServer == ".") return "Local"; - else - return string.Format($"\\\\{remoteServer}"); + return string.Format($"\\\\{remoteServer}"); } public static Dictionary ParseXml(string xml) diff --git a/src/Seq.Client.EventLog/ProjectInstaller.cs b/src/Seq.Client.EventLog/ProjectInstaller.cs index 784e007..a4554a7 100644 --- a/src/Seq.Client.EventLog/ProjectInstaller.cs +++ b/src/Seq.Client.EventLog/ProjectInstaller.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using System.Configuration.Install; +// ReSharper disable ClassNeverInstantiated.Global + namespace Seq.Client.EventLog { [RunInstaller(true)] diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index 18eb1f9..49e89d9 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -1,6 +1,7 @@ using System; using System.Timers; using Lurgle.Logging; + // ReSharper disable MemberCanBePrivate.Global namespace Seq.Client.EventLog @@ -13,12 +14,14 @@ public static class ServiceManager public static long LastProcessed; public static long UnhandledEvents; public static long OldEvents; + public static long EmptyEvents; + //This will be set if any listener has ProcessRetroactiveEntries enabled public static bool SaveOnExit; private static bool _isInteractive; private static DateTime _lastTime = DateTime.Now; - + public static void Start(bool isInteractive) { @@ -27,7 +30,8 @@ public static void Start(bool isInteractive) if (Config.HeartbeatInterval <= 0) { if (Config.IsDebug) - Log.Debug().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat").Add("[{HeartbeatName:l} - {MachineName:l}] Heartbeat is disabled ..."); + Log.Debug().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") + .Add("[{HeartbeatName:l} - {MachineName:l}] Heartbeat is disabled ..."); return; } @@ -52,7 +56,8 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) if (timeNow.Day != _lastTime.Day) { - Log.Debug().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat").Add("[{HeartbeatName:l} - {MachineName:l}] Day rollover, resetting counters ..."); + Log.Debug().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") + .Add("[{HeartbeatName:l} - {MachineName:l}] Day rollover, resetting counters ..."); EventsProcessed = 0; LastProcessed = 0; UnhandledEvents = 0; @@ -67,7 +72,8 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) .AddProperty("OldEvents", OldEvents) .AddProperty("EmptyEvents", EmptyEvents) .AddProperty("UnhandledEvents", UnhandledEvents) - .AddProperty("NextTime", timeNow.AddMilliseconds(_isInteractive ? 60000 : Config.HeartbeatInterval * 1000)) + .AddProperty("NextTime", + timeNow.AddMilliseconds(_isInteractive ? 60000 : Config.HeartbeatInterval * 1000)) .Add( Config.IsDebug ? "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + From e66db3ccacd8a0e0b6a01ff01da4ab8d35f88385 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Wed, 13 Oct 2021 16:01:53 +1100 Subject: [PATCH 06/34] Rename Summary and Description to avoid possible conflict with downstream apps --- src/Seq.Client.EventLog/EventLogListener.cs | 6 +++--- src/Seq.Client.EventLog/EventLogListeners.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index e9416f9..886e32d 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -28,7 +28,7 @@ public class EventLogListener public string MachineName { get; set; } public string MessageTemplate { get; set; } = - "[{LogName:l}] - ({EventLevel:l}) - Event Id {EventId} - {Summary:l}"; + "[{LogName:l}] - ({EventLevel:l}) - Event Id {EventId} - {EventSummary:l}"; // These properties allow for the filtering of events that will be sent to Seq. // If they are not specified in the JSON, all events in the log will be sent. @@ -227,8 +227,8 @@ private void HandleEventLogEntry(EventRecord entry) .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) .AddProperty("EventLevel", entry.LevelDisplayName) .AddProperty("EventLevelId", entry.Level) - .AddProperty("Description", entry.FormatDescription()) - .AddProperty("Summary", Extensions.GetMessage(entry.FormatDescription())) + .AddProperty("EventDescription", entry.FormatDescription()) + .AddProperty("EventSummary", Extensions.GetMessage(entry.FormatDescription())) .AddProperty("ProjectKey", ProjectKey, false, false) .AddProperty("Priority", Priority, false, false) .AddProperty("Responders", Responders, false, false) diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index 3cbc8a2..7ef3bba 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -6,7 +6,7 @@ { "LogName": "Security", //"LogLevels": [16], - "MessageTemplate": "[{LogName:l}] - {ListenerType:l} - ({EventLevel:l}) - Event Id {EventId} - {Summary:l}", + "MessageTemplate": "[{LogName:l}] - {ListenerType:l} - ({EventLevel:l}) - Event Id {EventId} - {EventSummary:l}", "ProjectKey": "TEST", "Priority": "High", "Responders": "MattM", From 526159bdf2e49470a6334ce645d408f88342d6b3 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Thu, 14 Oct 2021 11:15:36 +1100 Subject: [PATCH 07/34] Don't copy app.config - no longer required. --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index feb13f6..ad959db 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -54,10 +54,6 @@ - - Designer - PreserveNewest - PreserveNewest From b7064b07e7f1730c93ffe8596a7762ec7c37c376 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Mon, 18 Oct 2021 11:42:36 +1100 Subject: [PATCH 08/34] Merge Seq.Client.WindowsLogins functionality Ensure parsed event log xml is a valid message template token --- src/Seq.Client.EventLog.sln.DotSettings | 2 + src/Seq.Client.EventLog/Config.cs | 2 +- src/Seq.Client.EventLog/EventLogListener.cs | 149 ++++++++++++++---- .../EventLogListeners.json | 19 ++- src/Seq.Client.EventLog/Extensions.cs | 2 +- src/Seq.Client.EventLog/ServiceManager.cs | 39 ++++- 6 files changed, 171 insertions(+), 42 deletions(-) diff --git a/src/Seq.Client.EventLog.sln.DotSettings b/src/Seq.Client.EventLog.sln.DotSettings index 921db25..fdf24a9 100644 --- a/src/Seq.Client.EventLog.sln.DotSettings +++ b/src/Seq.Client.EventLog.sln.DotSettings @@ -1,3 +1,5 @@  + True + True True True \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Config.cs b/src/Seq.Client.EventLog/Config.cs index fcf4a5a..c81125f 100644 --- a/src/Seq.Client.EventLog/Config.cs +++ b/src/Seq.Client.EventLog/Config.cs @@ -73,7 +73,7 @@ static Config() /// /// An object that can be converted to an int /// - private static int GetInt(object sourceObject) + public static int GetInt(object sourceObject) { var sourceString = string.Empty; diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index 886e32d..dc9344b 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -22,13 +22,15 @@ public class EventLogListener private volatile bool _started; private EventLogWatcher _watcher; + //Allow a per-log appname to be specified + public string LogAppName { get; set; } public string LogName { get; set; } //Logged as RemoteServer to avoid conflict with the inbuilt MachineName property public string MachineName { get; set; } - public string MessageTemplate { get; set; } = - "[{LogName:l}] - ({EventLevel:l}) - Event Id {EventId} - {EventSummary:l}"; + public string MessageTemplate { get; set; } + // These properties allow for the filtering of events that will be sent to Seq. // If they are not specified in the JSON, all events in the log will be sent. @@ -44,6 +46,11 @@ public class EventLogListener public string RemainingTimeEstimate { get; set; } public string DueDate { get; set; } + //Optional mode that monitors successful interactive logins + public bool WindowsLogins { get; set; } + + //Default to filtering empty guids for Windows logins + public bool GuidIsEmpty { get; set; } public bool ProcessRetroactiveEntries { get; set; } //When ProcessRetroactiveEntries isn't desirable, but processing events that occurred while the service was stopped is needed, use StoreLastEntry. ProcessRetroactiveEntries = true always supersedes this. @@ -54,6 +61,23 @@ public class EventLogListener public void Validate() { + if (string.IsNullOrEmpty(LogAppName)) + LogAppName = Config.AppName; + + if (string.IsNullOrEmpty(MessageTemplate)) + MessageTemplate = WindowsLogins + ? "[{LogAppName:l}] - New login detected on {MachineName:l} - {EventData_TargetDomainName:l}\\{EventData_TargetUserName:l} at {EventTime:F}" + : "[{LogAppName:l}] - ({EventLevel:l}) - Event Id {EventId} - {EventSummary:l}"; + + if (WindowsLogins) + { + LogName = "Security"; + LogLevels = new List(); + EventIds = new List { 4624 }; + Sources = new List(); + ServiceManager.WindowsLogins = true; + } + if (string.IsNullOrWhiteSpace(LogName)) throw new InvalidOperationException($"A {nameof(LogName)} must be specified for the listener."); } @@ -62,9 +86,13 @@ public void Start(bool isInteractive = false) { try { - Log.Information().AddProperty("LogName", LogName).AddProperty("LogLevels", LogLevels) + Log.Information().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .AddProperty("LogLevels", LogLevels) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) .AddProperty("EventIds", EventIds).AddProperty("Sources", Sources) .AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("WindowsLogins", WindowsLogins).AddProperty("GuidIsEmpty", GuidIsEmpty) .AddProperty("ProjectKey", ProjectKey, false, false) .AddProperty("Priority", Priority, false, false) .AddProperty("Responders", Responders, false, false) @@ -72,8 +100,9 @@ public void Start(bool isInteractive = false) .AddProperty("InitialTimeEstimate", InitialTimeEstimate, false, false) .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false) .AddProperty("DueDate", DueDate, false, false) - .Add("Starting {ListenerType:l} listener for {LogName:l} on {MachineName:l}", - Extensions.GetListenerType(MachineName), LogName); + .Add(WindowsLogins + ? "[{LogAppName:l}] Starting Windows Logins listener for {LogName:l} on {MachineName:l}" + : "[{LogAppName:l}] Starting {ListenerType:l} listener ({LogAppName:l}) for {LogName:l} on {MachineName:l}"); _eventLog = new EventLogQuery(LogName, PathType.LogName, "*"); _isInteractive = isInteractive; @@ -89,9 +118,11 @@ public void Start(bool isInteractive = false) catch (Exception ex) { Log.Exception(ex).AddProperty("Message", ex.Message) + .AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) .AddProperty("RemoteServer", MachineName, false, false).Add( - "Failed to start {ListenerType:l} listener for {LogName:l} on {MachineName:l}: {Message:l}", - Extensions.GetListenerType(MachineName), LogName); + "[{LogAppName:l}] Failed to start {ListenerType:l} listener for {LogName:l} on {MachineName:l}: {Message:l}"); } } @@ -109,13 +140,16 @@ private EventLogWatcher GetWatcherConfig() if (checkBookmark != null) { checkBookmark.Dispose(); - Log.Debug().Add("Logging from last bookmark for {LogName:l} on {MachineName:l}", LogName); + Log.Debug().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .Add("[{LogAppName:l}] Logging from last bookmark for {LogName:l} on {MachineName:l}"); return new EventLogWatcher(_eventLog, CurrentBookmark, true); } - Log.Debug().Add( - "Cannot find last bookmark for {LogName:l} on {MachineName:l} - processing new events", - LogName); + Log.Debug().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .Add( + "[{LogAppName:l}] Cannot find last bookmark for {LogName:l} on {MachineName:l} - processing new events"); } else if (ProcessRetroactiveEntries) { @@ -123,17 +157,22 @@ private EventLogWatcher GetWatcherConfig() var firstEvent = eventLog.ReadEvent(); if (firstEvent != null) { - Log.Debug().Add("Logging from first logged event for {LogName:l} on {MachineName:l}", LogName); + Log.Debug().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .Add("[{LogAppName:l}] Logging from first logged event for {LogName:l} on {MachineName:l}"); return new EventLogWatcher(_eventLog, firstEvent.Bookmark, true); } - Log.Debug().Add( - "Cannot determine first event for {LogName:l} on {MachineName:l} - processing new events", - LogName); + Log.Debug().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .Add( + "[{LogAppName:l}] Cannot determine first event for {LogName:l} on {MachineName:l} - processing new events"); } else { - Log.Debug().Add("Processing new events for {LogName:l} on {MachineName:l}", LogName); + Log.Debug().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .Add("[{LogAppName:l}] Processing new events for {LogName:l} on {MachineName:l}"); } return new EventLogWatcher(_eventLog); @@ -150,16 +189,21 @@ public void Stop() _watcher.Enabled = false; _watcher.Dispose(); - Log.Debug().AddProperty("RemoteServer", MachineName, false, false).Add( - "{ListenerType:l} listener stopped for {LogName:l} on {MachineName:l}", - Extensions.GetListenerType(MachineName), LogName); + Log.Debug().AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) + .Add("[{LogAppName:l}] {ListenerType:l} listener stopped for {LogName:l} on {MachineName:l}"); } catch (Exception ex) { Log.Exception(ex).AddProperty("Message", ex.Message) - .AddProperty("RemoteServer", MachineName, false, false).Add( - "Failed to stop {ListenerType:l} listener for {LogName:l} on {MachineName:l}: {Message:l}", - Extensions.GetListenerType(MachineName), LogName); + .AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) + .Add( + "[{LogAppName:l}] Failed to stop {ListenerType:l} listener for {LogName:l} on {MachineName:l}: {Message:l}"); } } @@ -168,8 +212,9 @@ private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs arg try { //Ensure that events are new and have not been seen already. This addresses a scenario where event logs can repeatedly pass events to the handler. - if (args.EventRecord != null && (ProcessRetroactiveEntries || StoreLastEntry || !ProcessRetroactiveEntries && - args.EventRecord.TimeCreated >= ServiceManager.ServiceStart)) + if (args.EventRecord != null && (ProcessRetroactiveEntries || StoreLastEntry || + !ProcessRetroactiveEntries && + args.EventRecord.TimeCreated >= ServiceManager.ServiceStart)) await Task.Run(() => HandleEventLogEntry(args.EventRecord)); else if (args.EventRecord != null && !ProcessRetroactiveEntries && args.EventRecord.TimeCreated < ServiceManager.ServiceStart) @@ -181,8 +226,11 @@ private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs arg { Log.Exception(ex).AddProperty("RemoteServer", MachineName, false, false) .AddProperty("Message", ex.Message) - .Add("Failed to handle {ListenerType:l} {LogName:l} log entry on {MachineName:l}: {Message:l}", - Extensions.GetListenerType(MachineName), LogName); + .AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) + .Add( + "[{LogAppName:l}] Failed to handle {ListenerType:l} {LogName:l} log entry on {MachineName:l}: {Message:l}"); } } @@ -214,14 +262,52 @@ private void HandleEventLogEntry(EventRecord entry) if (ProcessRetroactiveEntries || StoreLastEntry) CurrentBookmark = entry.Bookmark; + var eventProperties = Extensions.ParseXml(entry.ToXml()); + + //Windows Logins handler + if (WindowsLogins && eventProperties.TryGetValue("EventData_LogonType", out var logonType) && + eventProperties.TryGetValue("EventData_IpAddress", out var ipAddress) && + eventProperties.TryGetValue("EventData_LogonGuid", out var logonGuid)) + switch (Config.GetInt(logonType)) + { + case 2 when entry.Keywords != null && + ((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords + .AuditSuccess) && !Equals(ipAddress, "-") && + (GuidIsEmpty && logonGuid.Equals("00000000-0000-0000-0000-000000000000") || + !GuidIsEmpty && !logonGuid.Equals("00000000-0000-0000-0000-000000000000")): + case 10 when entry.Keywords != null && + ((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords + .AuditSuccess) && !Equals(ipAddress, "-") && + (GuidIsEmpty && logonGuid.Equals("00000000-0000-0000-0000-000000000000") || + !GuidIsEmpty && !logonGuid.Equals("00000000-0000-0000-0000-000000000000")): + ServiceManager.LogonsDetected++; + break; + default: + ServiceManager.NonInteractiveLogons++; + return; + } + + //Friendly event times + var eventTimeLong = string.Empty; + var eventTimeShort = string.Empty; + if (entry.TimeCreated != null) + { + eventTimeLong = ((DateTime)entry.TimeCreated).ToString("F"); + eventTimeShort = ((DateTime)entry.TimeCreated).ToString("G"); + } + Log.Level(Extensions.MapLogLevel(entry)) .SetTimestamp(entry.TimeCreated ?? DateTime.Now) + .AddProperty("LogAppName", LogAppName) .AddProperty("LogName", LogName) .AddProperty("LogLevels", LogLevels) .AddProperty("EventIds", EventIds) .AddProperty("Sources", Sources) .AddProperty("Provider", entry.ProviderName) .AddProperty("EventId", entry.Id) + .AddProperty("EventTime", entry.TimeCreated) + .AddProperty("EventTimeLong", eventTimeLong) + .AddProperty("EventTimeShort", eventTimeShort) .AddProperty("KeywordNames", entry.KeywordsDisplayNames) .AddProperty("RemoteServer", MachineName, false, false) .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) @@ -236,7 +322,7 @@ private void HandleEventLogEntry(EventRecord entry) .AddProperty("InitialTimeEstimate", InitialTimeEstimate, false, false) .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false) .AddProperty("DueDate", DueDate, false, false) - .AddProperty(Extensions.ParseXml(entry.ToXml())) + .AddProperty(eventProperties) .Add(MessageTemplate); ServiceManager.EventsProcessed++; @@ -244,9 +330,12 @@ private void HandleEventLogEntry(EventRecord entry) catch (Exception ex) { Log.Exception(ex).AddProperty("Message", ex.Message) - .AddProperty("RemoteServer", MachineName, false, false).Add( - "Error parsing {ListenerType:l} {LogName:l} event on {MachineName:l}: {Message:l}", - Extensions.GetListenerType(MachineName), LogName); + .AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) + .Add( + "[{LogAppName:l}] Error parsing {ListenerType:l} {LogName:l} event on {MachineName:l}: {Message:l}"); } } } diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index 7ef3bba..9908243 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -4,9 +4,10 @@ "LogLevels": [1, 2] }, { + "LogAppName": "Security Monitor", "LogName": "Security", //"LogLevels": [16], - "MessageTemplate": "[{LogName:l}] - {ListenerType:l} - ({EventLevel:l}) - Event Id {EventId} - {EventSummary:l}", + "MessageTemplate": "[{LogAppName:l}] - {ListenerType:l} - ({EventLevel:l}) - Event Id {EventId} - {EventSummary:l}", "ProjectKey": "TEST", "Priority": "High", "Responders": "MattM", @@ -14,7 +15,21 @@ "InitialTimeEstimate": "1h", "RemainingTimeEstimate": "1h", "DueDate": "7d", - "StoreLastEntry": true + "StoreLastEntry": true + }, + { + "LogAppName": "Security Logins", + "MessageTemplate": + "[{LogAppName:l}] New login detected on {MachineName:l} - {EventData_TargetDomainName:l}\\{EventData_TargetUserName:l} at {EventTime:F}", + "ProjectKey": "TEST", + "Priority": "High", + "Responders": "MattM", + "Tags": "Extra,Secure,Logging", + "InitialTimeEstimate": "1h", + "RemainingTimeEstimate": "1h", + "DueDate": "7d", + "StoreLastEntry": true, + "WindowsLogins": true }, //{ // "LogName": "Security", diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index d61f0eb..281e270 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -86,7 +86,7 @@ private static Dictionary ProcessNode(XElement element, int dept foreach (var descendant in element.Elements()) foreach (var node in ProcessNode(descendant, depth + 1, depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) - ? string.Format($"{nodeName}-{GetName(descendant)}") + ? string.Format($"{nodeName}_{GetName(descendant)}") : GetName(descendant))) result.Add(node.Key, node.Value); diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index 49e89d9..78e8e2a 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Timers; using Lurgle.Logging; @@ -14,9 +15,13 @@ public static class ServiceManager public static long LastProcessed; public static long UnhandledEvents; public static long OldEvents; + public static long LogonsDetected; + public static long NonInteractiveLogons; public static long EmptyEvents; + //This will be set if any listener is watching for Windows logins + public static bool WindowsLogins; //This will be set if any listener has ProcessRetroactiveEntries enabled public static bool SaveOnExit; private static bool _isInteractive; @@ -63,22 +68,40 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) UnhandledEvents = 0; OldEvents = 0; EmptyEvents = 0; + LogonsDetected = 0; + NonInteractiveLogons = 0; + } + + var serviceCounters = new Dictionary + { + { "EventsProcessed", diff }, { "TotalProcessed", EventsProcessed }, { "OldEvents", OldEvents }, + { "EmptyEvents", EmptyEvents }, { "UnhandledEvents", UnhandledEvents } + }; + + if (WindowsLogins) + { + serviceCounters.Add("LogonsDetected", LogonsDetected); + serviceCounters.Add("NonInteractiveLogons", NonInteractiveLogons); } Log.Debug() .AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") - .AddProperty("EventsProcessed", diff) - .AddProperty("TotalProcessed", EventsProcessed) - .AddProperty("OldEvents", OldEvents) - .AddProperty("EmptyEvents", EmptyEvents) - .AddProperty("UnhandledEvents", UnhandledEvents) + .AddProperty(serviceCounters) .AddProperty("NextTime", timeNow.AddMilliseconds(_isInteractive ? 60000 : Config.HeartbeatInterval * 1000)) .Add( Config.IsDebug - ? "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + - "Unhandled events: {UnhandledEvents}, Old events seen: {OldEvents}, Empty events: {EmptyEvents}, Next Heartbeat: {NextTime:H:mm:ss tt}" - : "[{HeartbeatName:l} - {MachineName:l}] - Events Processed {EventsProcessed}, Total Processed: {TotalProcessed}, Next Heartbeat: {NextTime:H:mm:ss tt}"); + ? WindowsLogins + ? "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + + "LogonsDetected: {LogonsDetected}, NonInteractiveLogons: {NonInteractiveLogons}, " + + "Unhandled events: {UnhandledEvents}, Old events seen: {OldEvents}, Empty events: {EmptyEvents}, Next Heartbeat: {NextTime:H:mm:ss tt}" + : "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + + "Unhandled events: {UnhandledEvents}, Old events seen: {OldEvents}, Empty events: {EmptyEvents}, Next Heartbeat: {NextTime:H:mm:ss tt}" + : WindowsLogins + ? "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + + "LogonsDetected: {LogonsDetected}, NonInteractiveLogons: {NonInteractiveLogons}, Next Heartbeat: {NextTime:H:mm:ss tt}" + : "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + + "Next Heartbeat: {NextTime:H:mm:ss tt}"); LastProcessed = EventsProcessed; _lastTime = timeNow; From 1e05974d7b93f66801533796b1502c80ab767cd9 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Mon, 18 Oct 2021 11:49:18 +1100 Subject: [PATCH 09/34] Version bump --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index ad959db..f243a74 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.0.0 + 3.0.1 Connor O'Shea and contributors Connor O'Shea From 3971de046e5297edbe7757f86e8bc95d9378ce1f Mon Sep 17 00:00:00 2001 From: MattMofDoom Date: Mon, 6 Dec 2021 09:44:41 +1100 Subject: [PATCH 10/34] Correct MachineName reference --- src/Seq.Client.EventLog/EventLogClient.cs | 2 +- src/Seq.Client.EventLog/Program.cs | 4 ++-- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index 641eb3c..f4a7e8a 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -74,7 +74,7 @@ private void SaveListeners(string configuration) catch (Exception ex) { Log.Exception(ex).AddProperty("Message", ex.Message) - .Add("Error saving {ConfigurationFilePath:l} on (MachineName:l}: {Message:l}", filePath); + .Add("Error saving {ConfigurationFilePath:l} on {MachineName:l}: {Message:l}", filePath); } } diff --git a/src/Seq.Client.EventLog/Program.cs b/src/Seq.Client.EventLog/Program.cs index 47ad877..3992d40 100644 --- a/src/Seq.Client.EventLog/Program.cs +++ b/src/Seq.Client.EventLog/Program.cs @@ -127,8 +127,8 @@ private static void RunService() .AddProperty("SeqServer", Config.SeqServer) .AddProperty("SeqApiKey", !string.IsNullOrEmpty(Config.SeqApiKey)) .Add(Config.LogToFile - ? "{AppName:l} ((MachineName:l}) Log Config - LogFolder: {LogFolder:l}, LogPath: {LogPath:l}, Seq Server: {SeqServer:l}, Api Key: {SeqApiKey}" - : "{AppName:l} ((MachineName:l}) Log Config - Seq Server: {SeqServer:l}, Api Key: {SeqApiKey}"); + ? "{AppName:l} ({MachineName:l}) Log Config - LogFolder: {LogFolder:l}, LogPath: {LogPath:l}, Seq Server: {SeqServer:l}, Api Key: {SeqApiKey}" + : "{AppName:l} ({MachineName:l}) Log Config - Seq Server: {SeqServer:l}, Api Key: {SeqApiKey}"); Log.Debug().Add("Running as service"); ServiceManager.Start(false); ServiceBase.Run(new Service()); diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index f243a74..102016e 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.0.1 + 3.0.2 Connor O'Shea and contributors Connor O'Shea From f4746de9a2fb3f85b393edbec7ce5130495bc1e5 Mon Sep 17 00:00:00 2001 From: MattMofDoom Date: Mon, 6 Dec 2021 09:53:34 +1100 Subject: [PATCH 11/34] Add icon --- src/Seq.Client.EventLog/EventLog.ico | Bin 0 -> 188183 bytes src/Seq.Client.EventLog/EventLog.png | Bin 0 -> 27090 bytes .../Seq.Client.EventLog.csproj | 1 + 3 files changed, 1 insertion(+) create mode 100644 src/Seq.Client.EventLog/EventLog.ico create mode 100644 src/Seq.Client.EventLog/EventLog.png diff --git a/src/Seq.Client.EventLog/EventLog.ico b/src/Seq.Client.EventLog/EventLog.ico new file mode 100644 index 0000000000000000000000000000000000000000..c405f33d8b6109545e7e9f1cfcbbab60a9268b46 GIT binary patch literal 188183 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ℑk+-W= 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 exK~s_Yv{x#AwAsZnb0t!8V) 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 H*o6KMBxQsH literal 0 HcmV?d00001 diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 102016e..816051c 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -38,6 +38,7 @@ 3.0.2 Connor O'Shea and contributors Connor O'Shea + EventLog.ico From 85ab1c6594039f376a189b88711386ecaf4094b2 Mon Sep 17 00:00:00 2001 From: MattMofDoom Date: Mon, 6 Dec 2021 10:38:18 +1100 Subject: [PATCH 12/34] Correct Guid detection for Windows login functionality, reflect the GuidIsEmpty setting --- src/Seq.Client.EventLog/EventLogListener.cs | 8 ++++---- src/Seq.Client.EventLog/EventLogListeners.json | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index dc9344b..bcbb57b 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -273,13 +273,13 @@ private void HandleEventLogEntry(EventRecord entry) case 2 when entry.Keywords != null && ((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords .AuditSuccess) && !Equals(ipAddress, "-") && - (GuidIsEmpty && logonGuid.Equals("00000000-0000-0000-0000-000000000000") || - !GuidIsEmpty && !logonGuid.Equals("00000000-0000-0000-0000-000000000000")): + (GuidIsEmpty && logonGuid.Equals("{00000000-0000-0000-0000-000000000000}") || + !GuidIsEmpty && !logonGuid.Equals("{00000000-0000-0000-0000-000000000000}")): case 10 when entry.Keywords != null && ((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords .AuditSuccess) && !Equals(ipAddress, "-") && - (GuidIsEmpty && logonGuid.Equals("00000000-0000-0000-0000-000000000000") || - !GuidIsEmpty && !logonGuid.Equals("00000000-0000-0000-0000-000000000000")): + (GuidIsEmpty && logonGuid.Equals("{00000000-0000-0000-0000-000000000000}") || + !GuidIsEmpty && !logonGuid.Equals("{00000000-0000-0000-0000-000000000000}")): ServiceManager.LogonsDetected++; break; default: diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index 9908243..9d59963 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -29,7 +29,8 @@ "RemainingTimeEstimate": "1h", "DueDate": "7d", "StoreLastEntry": true, - "WindowsLogins": true + "WindowsLogins": true, + "GuidIsEmpty": false }, //{ // "LogName": "Security", From c7b12ffb66996feaa9539bc8e391d071153bb29f Mon Sep 17 00:00:00 2001 From: MattMofDoom Date: Mon, 6 Dec 2021 11:18:54 +1100 Subject: [PATCH 13/34] Periodically save bookmarks on heartbeat --- src/Seq.Client.EventLog.sln.DotSettings | 1 + src/Seq.Client.EventLog/EventLogClient.cs | 81 ++------------------- src/Seq.Client.EventLog/EventLogListener.cs | 13 +++- src/Seq.Client.EventLog/Program.cs | 4 +- src/Seq.Client.EventLog/Service.cs | 4 +- src/Seq.Client.EventLog/ServiceManager.cs | 77 ++++++++++++++++++-- 6 files changed, 93 insertions(+), 87 deletions(-) diff --git a/src/Seq.Client.EventLog.sln.DotSettings b/src/Seq.Client.EventLog.sln.DotSettings index fdf24a9..f310526 100644 --- a/src/Seq.Client.EventLog.sln.DotSettings +++ b/src/Seq.Client.EventLog.sln.DotSettings @@ -1,5 +1,6 @@  True True + True True True \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index f4a7e8a..b8a9677 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -9,83 +9,18 @@ namespace Seq.Client.EventLog { internal class EventLogClient { - private List _eventLogListeners; - - public void Start(bool isInteractive = false, string configuration = null) - { - LoadListeners(configuration); - ValidateListeners(); - StartListeners(isInteractive); - } - - public void Stop(string configuration = null) - { - StopListeners(); - if (ServiceManager.SaveOnExit) - SaveListeners(configuration); - } - - private void LoadListeners(string configuration) - { - string filePath; - if (configuration == null) - { - var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - filePath = Path.Combine(directory ?? ".", "EventLogListeners.json"); - } - else - { - filePath = configuration; - } - - Log.Information() - .Add("Loading listener configuration from {ConfigurationFilePath:l} on {MachineName:l} ...", filePath); - var file = File.ReadAllText(filePath); - - _eventLogListeners = JsonConvert.DeserializeObject>(file); - } - - private void ValidateListeners() - { - foreach (var listener in _eventLogListeners) listener.Validate(); - } - - private void SaveListeners(string configuration) - { - string filePath; - if (configuration == null) - { - var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - filePath = Path.Combine(directory ?? ".", "EventLogListeners.json"); - } - else - { - filePath = configuration; - } - - try - { - var json = JsonConvert.SerializeObject(_eventLogListeners, Formatting.Indented); - - Log.Information() - .Add("Saving listener configuration to {ConfigurationFilePath:l} on {MachineName:l} ...", filePath); - File.WriteAllText(filePath, json); - } - catch (Exception ex) - { - Log.Exception(ex).AddProperty("Message", ex.Message) - .Add("Error saving {ConfigurationFilePath:l} on {MachineName:l}: {Message:l}", filePath); - } - } - - private void StartListeners(bool isInteractive = false) + public static void Start(bool isInteractive = false, string configuration = null) { - foreach (var listener in _eventLogListeners) listener.Start(isInteractive); + ServiceManager.LoadListeners(configuration); + ServiceManager.ValidateListeners(); + ServiceManager.StartListeners(isInteractive); } - private void StopListeners() + public static void Stop() { - foreach (var listener in _eventLogListeners) listener.Stop(); + ServiceManager.StopListeners(); + if (ServiceManager.SaveBookmarks) + ServiceManager.SaveListeners(); } } } \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index bcbb57b..f3638bc 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -90,9 +90,13 @@ public void Start(bool isInteractive = false) .AddProperty("LogName", LogName) .AddProperty("LogLevels", LogLevels) .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) - .AddProperty("EventIds", EventIds).AddProperty("Sources", Sources) + .AddProperty("EventIds", EventIds) + .AddProperty("Sources", Sources) .AddProperty("RemoteServer", MachineName, false, false) - .AddProperty("WindowsLogins", WindowsLogins).AddProperty("GuidIsEmpty", GuidIsEmpty) + .AddProperty("WindowsLogins", WindowsLogins) + .AddProperty("GuidIsEmpty", GuidIsEmpty) + .AddProperty("ProcessRetroactiveEntries", ProcessRetroactiveEntries) + .AddProperty("StoreLastEntry", StoreLastEntry) .AddProperty("ProjectKey", ProjectKey, false, false) .AddProperty("Priority", Priority, false, false) .AddProperty("Responders", Responders, false, false) @@ -130,10 +134,12 @@ private EventLogWatcher GetWatcherConfig() { var eventLog = new EventLogReader(_eventLog); + if (ProcessRetroactiveEntries || StoreLastEntry) + ServiceManager.SaveBookmarks = true; + if (CurrentBookmark != null && (ProcessRetroactiveEntries || StoreLastEntry)) { //Go back a position to allow the bookmark to be read - ServiceManager.SaveOnExit = true; eventLog.Seek(CurrentBookmark, -1); var checkBookmark = eventLog.ReadEvent(); @@ -153,7 +159,6 @@ private EventLogWatcher GetWatcherConfig() } else if (ProcessRetroactiveEntries) { - ServiceManager.SaveOnExit = true; var firstEvent = eventLog.ReadEvent(); if (firstEvent != null) { diff --git a/src/Seq.Client.EventLog/Program.cs b/src/Seq.Client.EventLog/Program.cs index 3992d40..99c1c64 100644 --- a/src/Seq.Client.EventLog/Program.cs +++ b/src/Seq.Client.EventLog/Program.cs @@ -58,14 +58,14 @@ private static void RunInteractive(string configFilePath) Config.AppName, Config.AppVersion); var client = new EventLogClient(); - client.Start(true, configFilePath); + EventLogClient.Start(true, configFilePath); ServiceManager.Start(true); var done = new ManualResetEvent(false); Console.CancelKeyPress += (s, e) => { Log.Debug().Add("Ctrl+C pressed, stopping"); - client.Stop(configFilePath); + EventLogClient.Stop(); done.Set(); }; diff --git a/src/Seq.Client.EventLog/Service.cs b/src/Seq.Client.EventLog/Service.cs index a14f8b2..85840f4 100644 --- a/src/Seq.Client.EventLog/Service.cs +++ b/src/Seq.Client.EventLog/Service.cs @@ -15,12 +15,12 @@ public Service() protected override void OnStart(string[] args) { - _client.Start(); + EventLogClient.Start(); } protected override void OnStop() { - _client.Stop(); + EventLogClient.Stop(); } #endregion diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index 78e8e2a..107b402 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Reflection; using System.Timers; using Lurgle.Logging; +using Newtonsoft.Json; // ReSharper disable MemberCanBePrivate.Global @@ -9,6 +12,8 @@ namespace Seq.Client.EventLog { public static class ServiceManager { + public static string JsonConfigPath { get; set; } + public static List EventLogListeners { get; set; } private static Timer _heartbeatTimer; public static readonly DateTime ServiceStart = DateTime.Now; public static long EventsProcessed; @@ -22,8 +27,9 @@ public static class ServiceManager //This will be set if any listener is watching for Windows logins public static bool WindowsLogins; + //This will be set if any listener has ProcessRetroactiveEntries enabled - public static bool SaveOnExit; + public static bool SaveBookmarks; private static bool _isInteractive; private static DateTime _lastTime = DateTime.Now; @@ -42,8 +48,8 @@ public static void Start(bool isInteractive) //First heartbeat will be at a random interval between 2 and 10 seconds _heartbeatTimer = isInteractive - ? new Timer { Interval = 10000 } - : new Timer { Interval = new Random().Next(2000, 10000) }; + ? new Timer {Interval = 10000} + : new Timer {Interval = new Random().Next(2000, 10000)}; _heartbeatTimer.Elapsed += ServiceHeartbeat; _heartbeatTimer.AutoReset = false; _heartbeatTimer.Start(); @@ -54,6 +60,59 @@ public static void Stop() _heartbeatTimer.Stop(); } + public static void LoadListeners(string configuration) + { + if (configuration == null) + { + var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + JsonConfigPath = Path.Combine(directory ?? ".", "EventLogListeners.json"); + } + else + { + JsonConfigPath = configuration; + } + + Log.Information() + .Add("Loading listener configuration from {ConfigurationFilePath:l} on {MachineName:l} ...", + JsonConfigPath); + var file = File.ReadAllText(JsonConfigPath); + + ServiceManager.EventLogListeners = JsonConvert.DeserializeObject>(file); + } + + public static void ValidateListeners() + { + foreach (var listener in ServiceManager.EventLogListeners) listener.Validate(); + } + + public static void SaveListeners() + { + try + { + var json = JsonConvert.SerializeObject(EventLogListeners, Formatting.Indented); + + Log.Information() + .Add("Saving listener configuration to {ConfigurationFilePath:l} on {MachineName:l} ...", + JsonConfigPath); + File.WriteAllText(JsonConfigPath, json); + } + catch (Exception ex) + { + Log.Exception(ex).AddProperty("Message", ex.Message) + .Add("Error saving {ConfigurationFilePath:l} on {MachineName:l}: {Message:l}", JsonConfigPath); + } + } + + public static void StartListeners(bool isInteractive = false) + { + foreach (var listener in EventLogListeners) listener.Start(isInteractive); + } + + public static void StopListeners() + { + foreach (var listener in EventLogListeners) listener.Stop(); + } + private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) { var timeNow = DateTime.Now; @@ -74,8 +133,8 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) var serviceCounters = new Dictionary { - { "EventsProcessed", diff }, { "TotalProcessed", EventsProcessed }, { "OldEvents", OldEvents }, - { "EmptyEvents", EmptyEvents }, { "UnhandledEvents", UnhandledEvents } + {"EventsProcessed", diff}, {"TotalProcessed", EventsProcessed}, {"OldEvents", OldEvents}, + {"EmptyEvents", EmptyEvents}, {"UnhandledEvents", UnhandledEvents} }; if (WindowsLogins) @@ -106,7 +165,13 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) LastProcessed = EventsProcessed; _lastTime = timeNow; - if (_heartbeatTimer.AutoReset) return; + //Periodically save the listener config if we are storing this + if (SaveBookmarks) + SaveListeners(); + + if (_heartbeatTimer.AutoReset) + return; + //Set the timer to 60 seconds (interactive) or configured heartbeat (service) after initial heartbeat _heartbeatTimer.AutoReset = true; _heartbeatTimer.Interval = _isInteractive ? 60000 : Config.HeartbeatInterval * 1000; From 0f5493eb75f4748f3f60c38941256d52f9d2ca62 Mon Sep 17 00:00:00 2001 From: MattMofDoom Date: Mon, 6 Dec 2021 11:52:58 +1100 Subject: [PATCH 14/34] Load/Save listener config is now a debug log --- src/Seq.Client.EventLog/ServiceManager.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index 107b402..5377aa1 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -72,9 +72,10 @@ public static void LoadListeners(string configuration) JsonConfigPath = configuration; } - Log.Information() - .Add("Loading listener configuration from {ConfigurationFilePath:l} on {MachineName:l} ...", - JsonConfigPath); + if (Config.IsDebug) + Log.Debug() + .Add("Loading listener configuration from {ConfigurationFilePath:l} on {MachineName:l} ...", + JsonConfigPath); var file = File.ReadAllText(JsonConfigPath); ServiceManager.EventLogListeners = JsonConvert.DeserializeObject>(file); @@ -91,9 +92,10 @@ public static void SaveListeners() { var json = JsonConvert.SerializeObject(EventLogListeners, Formatting.Indented); - Log.Information() - .Add("Saving listener configuration to {ConfigurationFilePath:l} on {MachineName:l} ...", - JsonConfigPath); + if (Config.IsDebug) + Log.Debug() + .Add("Saving listener configuration to {ConfigurationFilePath:l} on {MachineName:l} ...", + JsonConfigPath); File.WriteAllText(JsonConfigPath, json); } catch (Exception ex) From d3168bf683f4dc42bb472f9ee14e16a14e724f43 Mon Sep 17 00:00:00 2001 From: MattMofDoom Date: Mon, 24 Jan 2022 15:41:44 +1100 Subject: [PATCH 15/34] Dependency update, code cleanup --- src/Seq.Client.EventLog/Config.cs | 4 ++-- src/Seq.Client.EventLog/EventLogClient.cs | 9 +------ src/Seq.Client.EventLog/EventLogListener.cs | 12 +++++----- .../EventLogListeners.json | 2 +- src/Seq.Client.EventLog/Extensions.cs | 24 +++++++++---------- src/Seq.Client.EventLog/Program.cs | 12 +++++----- .../Seq.Client.EventLog.csproj | 6 ++--- src/Seq.Client.EventLog/Service.cs | 2 -- src/Seq.Client.EventLog/ServiceManager.cs | 8 +++---- 9 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/Seq.Client.EventLog/Config.cs b/src/Seq.Client.EventLog/Config.cs index c81125f..6278333 100644 --- a/src/Seq.Client.EventLog/Config.cs +++ b/src/Seq.Client.EventLog/Config.cs @@ -77,7 +77,7 @@ public static int GetInt(object sourceObject) { var sourceString = string.Empty; - if (!Convert.IsDBNull(sourceObject)) sourceString = (string)sourceObject; + if (!Convert.IsDBNull(sourceObject)) sourceString = (string) sourceObject; if (int.TryParse(sourceString, out var destInt)) return destInt; @@ -96,7 +96,7 @@ private static bool GetBool(object sourceObject, bool trueIfEmpty = false) { var sourceString = string.Empty; - if (!Convert.IsDBNull(sourceObject)) sourceString = (string)sourceObject; + if (!Convert.IsDBNull(sourceObject)) sourceString = (string) sourceObject; return bool.TryParse(sourceString, out var destBool) ? destBool : trueIfEmpty; } diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index b8a9677..9ff0a37 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using Lurgle.Logging; -using Newtonsoft.Json; - -namespace Seq.Client.EventLog +namespace Seq.Client.EventLog { internal class EventLogClient { diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index f3638bc..d31c504 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -73,7 +73,7 @@ public void Validate() { LogName = "Security"; LogLevels = new List(); - EventIds = new List { 4624 }; + EventIds = new List {4624}; Sources = new List(); ServiceManager.WindowsLogins = true; } @@ -243,7 +243,7 @@ private void HandleEventLogEntry(EventRecord entry) { // Don't send the entry to Seq if it doesn't match the filtered log levels, event IDs, or sources if (LogLevels != null && LogLevels.Count > 0 && entry.Level != null && - !LogLevels.Contains((byte)entry.Level)) + !LogLevels.Contains((byte) entry.Level)) { ServiceManager.UnhandledEvents++; return; @@ -276,12 +276,12 @@ private void HandleEventLogEntry(EventRecord entry) switch (Config.GetInt(logonType)) { case 2 when entry.Keywords != null && - ((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords + ((StandardEventKeywords) entry.Keywords).HasFlag(StandardEventKeywords .AuditSuccess) && !Equals(ipAddress, "-") && (GuidIsEmpty && logonGuid.Equals("{00000000-0000-0000-0000-000000000000}") || !GuidIsEmpty && !logonGuid.Equals("{00000000-0000-0000-0000-000000000000}")): case 10 when entry.Keywords != null && - ((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords + ((StandardEventKeywords) entry.Keywords).HasFlag(StandardEventKeywords .AuditSuccess) && !Equals(ipAddress, "-") && (GuidIsEmpty && logonGuid.Equals("{00000000-0000-0000-0000-000000000000}") || !GuidIsEmpty && !logonGuid.Equals("{00000000-0000-0000-0000-000000000000}")): @@ -297,8 +297,8 @@ private void HandleEventLogEntry(EventRecord entry) var eventTimeShort = string.Empty; if (entry.TimeCreated != null) { - eventTimeLong = ((DateTime)entry.TimeCreated).ToString("F"); - eventTimeShort = ((DateTime)entry.TimeCreated).ToString("G"); + eventTimeLong = ((DateTime) entry.TimeCreated).ToString("F"); + eventTimeShort = ((DateTime) entry.TimeCreated).ToString("G"); } Log.Level(Extensions.MapLogLevel(entry)) diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index 9d59963..d8f4b6e 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -30,7 +30,7 @@ "DueDate": "7d", "StoreLastEntry": true, "WindowsLogins": true, - "GuidIsEmpty": false + "GuidIsEmpty": false }, //{ // "LogName": "Security", diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index 281e270..ca46655 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -17,25 +17,25 @@ public static LurgLevel MapLogLevel(EventRecord entry) if (entry.Keywords != null) { - if (((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords.AuditSuccess)) + if (((StandardEventKeywords) entry.Keywords).HasFlag(StandardEventKeywords.AuditSuccess)) return LurgLevel.Information; - if (((StandardEventKeywords)entry.Keywords).HasFlag(StandardEventKeywords.AuditFailure)) + if (((StandardEventKeywords) entry.Keywords).HasFlag(StandardEventKeywords.AuditFailure)) return LurgLevel.Warning; } // ReSharper disable once PossibleInvalidOperationException - switch ((byte)entry.Level) + switch ((byte) entry.Level) { - case (byte)EventLogEntryType.Information: + case (byte) EventLogEntryType.Information: return LurgLevel.Information; - case (byte)EventLogEntryType.Warning: + case (byte) EventLogEntryType.Warning: return LurgLevel.Warning; - case (byte)EventLogEntryType.Error: + case (byte) EventLogEntryType.Error: return LurgLevel.Error; - case (byte)EventLogEntryType.SuccessAudit: + case (byte) EventLogEntryType.SuccessAudit: return LurgLevel.Information; - case (byte)EventLogEntryType.FailureAudit: + case (byte) EventLogEntryType.FailureAudit: return LurgLevel.Warning; default: return LurgLevel.Debug; @@ -45,7 +45,7 @@ public static LurgLevel MapLogLevel(EventRecord entry) public static IEnumerable GetArray(string value) { return (value ?? "") - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) .Select(t => t.Trim()) .ToArray(); } @@ -85,9 +85,9 @@ private static Dictionary ProcessNode(XElement element, int dept else foreach (var descendant in element.Elements()) foreach (var node in ProcessNode(descendant, depth + 1, - depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) - ? string.Format($"{nodeName}_{GetName(descendant)}") - : GetName(descendant))) + depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) + ? string.Format($"{nodeName}_{GetName(descendant)}") + : GetName(descendant))) result.Add(node.Key, node.Value); return result; diff --git a/src/Seq.Client.EventLog/Program.cs b/src/Seq.Client.EventLog/Program.cs index 99c1c64..556f84a 100644 --- a/src/Seq.Client.EventLog/Program.cs +++ b/src/Seq.Client.EventLog/Program.cs @@ -28,10 +28,10 @@ public static void Main(string[] args) switch (parameter) { case "/install": - ManagedInstallerClass.InstallHelper(new[] { Assembly.GetExecutingAssembly().Location }); + ManagedInstallerClass.InstallHelper(new[] {Assembly.GetExecutingAssembly().Location}); break; case "/uninstall": - ManagedInstallerClass.InstallHelper(new[] { "/u", Assembly.GetExecutingAssembly().Location }); + ManagedInstallerClass.InstallHelper(new[] {"/u", Assembly.GetExecutingAssembly().Location}); break; default: RunInteractive(parameter); @@ -47,7 +47,7 @@ public static void Main(string[] args) private static void RunInteractive(string configFilePath) { Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, - logType: new List { LogType.Console, LogType.Seq }, logSeqServer: Config.SeqServer, + logType: new List {LogType.Console, LogType.Seq}, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, logLevel: LurgLevel.Verbose, logLevelConsole: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); @@ -57,7 +57,7 @@ private static void RunInteractive(string configFilePath) .Add("{AppName:l} v{AppVersion:l} Starting in interactive mode on {MachineName:l} ...", Config.AppName, Config.AppVersion); - var client = new EventLogClient(); + var unused = new EventLogClient(); EventLogClient.Start(true, configFilePath); ServiceManager.Start(true); @@ -104,14 +104,14 @@ private static void RunService() logFile = Path.Combine(logFolder ?? string.Empty, "ServiceLog.txt"); Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, - logType: new List { LogType.File, LogType.Seq }, logDays: 7, logName: Config.AppName, + logType: new List {LogType.File, LogType.Seq}, logDays: 7, logName: Config.AppName, logFolder: Config.LogFolder, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, logLevel: LurgLevel.Verbose, logLevelFile: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); } else { Logging.SetConfig(new LoggingConfig(appName: Config.AppName, appVersion: Config.AppVersion, - logType: new List { LogType.Seq }, logSeqServer: Config.SeqServer, + logType: new List {LogType.Seq}, logSeqServer: Config.SeqServer, logSeqApiKey: Config.SeqApiKey, logLevel: LurgLevel.Verbose, logLevelSeq: LurgLevel.Verbose)); } diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 816051c..953f74f 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.0.2 + 3.0.3 Connor O'Shea and contributors Connor O'Shea EventLog.ico @@ -60,8 +60,8 @@ - - + + 13.0.1 diff --git a/src/Seq.Client.EventLog/Service.cs b/src/Seq.Client.EventLog/Service.cs index 85840f4..249d65b 100644 --- a/src/Seq.Client.EventLog/Service.cs +++ b/src/Seq.Client.EventLog/Service.cs @@ -4,8 +4,6 @@ namespace Seq.Client.EventLog { public partial class Service : ServiceBase { - private readonly EventLogClient _client = new EventLogClient(); - #region Windows Service Base public Service() diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index 5377aa1..0a408be 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -12,8 +12,6 @@ namespace Seq.Client.EventLog { public static class ServiceManager { - public static string JsonConfigPath { get; set; } - public static List EventLogListeners { get; set; } private static Timer _heartbeatTimer; public static readonly DateTime ServiceStart = DateTime.Now; public static long EventsProcessed; @@ -32,6 +30,8 @@ public static class ServiceManager public static bool SaveBookmarks; private static bool _isInteractive; private static DateTime _lastTime = DateTime.Now; + public static string JsonConfigPath { get; set; } + public static List EventLogListeners { get; set; } public static void Start(bool isInteractive) @@ -78,12 +78,12 @@ public static void LoadListeners(string configuration) JsonConfigPath); var file = File.ReadAllText(JsonConfigPath); - ServiceManager.EventLogListeners = JsonConvert.DeserializeObject>(file); + EventLogListeners = JsonConvert.DeserializeObject>(file); } public static void ValidateListeners() { - foreach (var listener in ServiceManager.EventLogListeners) listener.Validate(); + foreach (var listener in EventLogListeners) listener.Validate(); } public static void SaveListeners() From 2529ddda03493ccfc937734611d3ff71597db57d Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Mon, 7 Feb 2022 11:40:11 +1100 Subject: [PATCH 16/34] Update Lurgle.Logging --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 953f74f..16c9293 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -60,7 +60,7 @@ - + 13.0.1 From 7260bc248f7e5527123f5d59895b4adf848c6e52 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Thu, 24 Feb 2022 09:53:12 +1100 Subject: [PATCH 17/34] Update Lurgle.Logging to add Splunk support Remove Microsoft.Extensions.Caching.Memory dependency --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 16c9293..28e8bf7 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -60,8 +60,7 @@ - - + 13.0.1 From 783259357f921129230801caf6a0c5b379c534d9 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:54:15 +1100 Subject: [PATCH 18/34] Add detection and recovery for no logs being seen for a configurable number of heartbeats --- src/Seq.Client.EventLog/App.config | 2 ++ src/Seq.Client.EventLog/Config.cs | 5 ++++ .../Seq.Client.EventLog.csproj | 2 +- src/Seq.Client.EventLog/ServiceManager.cs | 24 +++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/App.config b/src/Seq.Client.EventLog/App.config index 8be79e9..b3c9fe8 100644 --- a/src/Seq.Client.EventLog/App.config +++ b/src/Seq.Client.EventLog/App.config @@ -14,6 +14,8 @@ + + diff --git a/src/Seq.Client.EventLog/Config.cs b/src/Seq.Client.EventLog/Config.cs index 6278333..f8f29a3 100644 --- a/src/Seq.Client.EventLog/Config.cs +++ b/src/Seq.Client.EventLog/Config.cs @@ -15,6 +15,7 @@ static Config() LogToFile = GetBool(ConfigurationManager.AppSettings["LogSeqApiKey"], true); LogFolder = ConfigurationManager.AppSettings["LogFolder"]; HeartbeatInterval = GetInt(ConfigurationManager.AppSettings["HeartbeatInterval"]); + HeartbeatsBeforeReset = GetInt(ConfigurationManager.AppSettings["HeartbeatsBeforeReset"]); //Minimum is 0 (disabled) if (HeartbeatInterval < 0) @@ -23,6 +24,9 @@ static Config() if (HeartbeatInterval > 3600) HeartbeatInterval = 3600; + if (HeartbeatsBeforeReset < 0) + HeartbeatsBeforeReset = 0; + IsDebug = GetBool(ConfigurationManager.AppSettings["IsDebug"]); var isSuccess = true; @@ -65,6 +69,7 @@ static Config() public static string LogFolder { get; } public static int HeartbeatInterval { get; } public static bool IsDebug { get; } + public static int HeartbeatsBeforeReset { get; } ///

/// Convert the supplied to an diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 28e8bf7..dea35f3 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.0.3 + 3.0.4 Connor O'Shea and contributors Connor O'Shea EventLog.ico diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index 0a408be..cce5101 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -2,9 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using System.Timers; using Lurgle.Logging; using Newtonsoft.Json; +using Timer = System.Timers.Timer; // ReSharper disable MemberCanBePrivate.Global @@ -33,6 +36,7 @@ public static class ServiceManager public static string JsonConfigPath { get; set; } public static List EventLogListeners { get; set; } + private static int ZeroLogHeartbeats { get; set; } = 0; public static void Start(bool isInteractive) { @@ -145,6 +149,26 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) serviceCounters.Add("NonInteractiveLogons", NonInteractiveLogons); } + if (Config.HeartbeatsBeforeReset > 0) + { + if (EventsProcessed > LastProcessed) + ZeroLogHeartbeats = 0; + else + ZeroLogHeartbeats++; + + if (ZeroLogHeartbeats > Config.HeartbeatsBeforeReset) + { + Log.Warning().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") + .AddProperty("HeartbeatCount", ZeroLogHeartbeats) + .AddProperty("NextTime", timeNow.AddMilliseconds(_isInteractive ? 60000 : Config.HeartbeatInterval * 1000)) + .Add("[{HeartbeatName:l} - {MachineName:l}] - Detected that new no log entries have been seen in the past {Heartbeats} heartbeats, resetting listeners ..."); + + StopListeners(); + Thread.Sleep(1000); + StartListeners(_isInteractive); + } + } + Log.Debug() .AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") .AddProperty(serviceCounters) From 6c5457b4c6e3c9e9681c57e52e4001dc7f66455b Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Fri, 25 Mar 2022 14:07:58 +1100 Subject: [PATCH 19/34] HeartbeatsBeforeReset wasn't properly evaluating all events that were seen --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- src/Seq.Client.EventLog/ServiceManager.cs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index dea35f3..b97a524 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.0.4 + 3.0.5 Connor O'Shea and contributors Connor O'Shea EventLog.ico diff --git a/src/Seq.Client.EventLog/ServiceManager.cs b/src/Seq.Client.EventLog/ServiceManager.cs index cce5101..0a50b3f 100644 --- a/src/Seq.Client.EventLog/ServiceManager.cs +++ b/src/Seq.Client.EventLog/ServiceManager.cs @@ -18,6 +18,7 @@ public static class ServiceManager private static Timer _heartbeatTimer; public static readonly DateTime ServiceStart = DateTime.Now; public static long EventsProcessed; + public static long LastEventsProcessed; public static long LastProcessed; public static long UnhandledEvents; public static long OldEvents; @@ -122,13 +123,14 @@ public static void StopListeners() private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) { var timeNow = DateTime.Now; - var diff = EventsProcessed - LastProcessed; + var diff = EventsProcessed - LastEventsProcessed; if (timeNow.Day != _lastTime.Day) { Log.Debug().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") .Add("[{HeartbeatName:l} - {MachineName:l}] Day rollover, resetting counters ..."); EventsProcessed = 0; + LastEventsProcessed = 0; LastProcessed = 0; UnhandledEvents = 0; OldEvents = 0; @@ -151,7 +153,7 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) if (Config.HeartbeatsBeforeReset > 0) { - if (EventsProcessed > LastProcessed) + if (EventsProcessed + UnhandledEvents + OldEvents + EmptyEvents + LogonsDetected + NonInteractiveLogons > LastProcessed) ZeroLogHeartbeats = 0; else ZeroLogHeartbeats++; @@ -161,7 +163,7 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) Log.Warning().AddProperty("HeartbeatName", $"{Config.AppName} Heartbeat") .AddProperty("HeartbeatCount", ZeroLogHeartbeats) .AddProperty("NextTime", timeNow.AddMilliseconds(_isInteractive ? 60000 : Config.HeartbeatInterval * 1000)) - .Add("[{HeartbeatName:l} - {MachineName:l}] - Detected that new no log entries have been seen in the past {Heartbeats} heartbeats, resetting listeners ..."); + .Add("[{HeartbeatName:l} - {MachineName:l}] - Detected that no new log entries have been seen in the past {HeartbeatCount} heartbeats, resetting listeners ..."); StopListeners(); Thread.Sleep(1000); @@ -188,7 +190,8 @@ private static void ServiceHeartbeat(object sender, ElapsedEventArgs e) : "[{HeartbeatName:l} - {MachineName:l}] - Events Processed: {EventsProcessed}, Total Processed: {TotalProcessed}, " + "Next Heartbeat: {NextTime:H:mm:ss tt}"); - LastProcessed = EventsProcessed; + LastProcessed = EventsProcessed + UnhandledEvents + OldEvents + EmptyEvents + LogonsDetected + NonInteractiveLogons; + LastEventsProcessed = EventsProcessed; _lastTime = timeNow; //Periodically save the listener config if we are storing this From 95a7f3ff40381157160d08225ae490192caa27e9 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Sun, 14 Aug 2022 15:52:31 -0700 Subject: [PATCH 20/34] Updated dependencies --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index b97a524..ab34751 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -60,7 +60,7 @@ - + 13.0.1 From e5b1d38929f7a3880b38d7137094b64a837a3c2e Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Sun, 14 Aug 2022 15:53:02 -0700 Subject: [PATCH 21/34] Version bump --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index ab34751..12d9bcc 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.0.5 + 3.0.6 Connor O'Shea and contributors Connor O'Shea EventLog.ico From 2648209f80bbf38359c083d381dd239eee37a9bf Mon Sep 17 00:00:00 2001 From: OlesChri Date: Fri, 24 Feb 2023 16:09:21 +0100 Subject: [PATCH 22/34] correct copy/paste error in config: LogToFile --- src/Seq.Client.EventLog/Config.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Config.cs b/src/Seq.Client.EventLog/Config.cs index f8f29a3..dc3d7cd 100644 --- a/src/Seq.Client.EventLog/Config.cs +++ b/src/Seq.Client.EventLog/Config.cs @@ -12,7 +12,7 @@ static Config() AppName = ConfigurationManager.AppSettings["AppName"]; SeqServer = ConfigurationManager.AppSettings["LogSeqServer"]; SeqApiKey = ConfigurationManager.AppSettings["LogSeqApiKey"]; - LogToFile = GetBool(ConfigurationManager.AppSettings["LogSeqApiKey"], true); + LogToFile = GetBool(ConfigurationManager.AppSettings["LogToFile"], true); LogFolder = ConfigurationManager.AppSettings["LogFolder"]; HeartbeatInterval = GetInt(ConfigurationManager.AppSettings["HeartbeatInterval"]); HeartbeatsBeforeReset = GetInt(ConfigurationManager.AppSettings["HeartbeatsBeforeReset"]); From a86618da1289032d808bc17ca70ab6e303e198c1 Mon Sep 17 00:00:00 2001 From: OlesChri Date: Fri, 24 Feb 2023 16:22:11 +0100 Subject: [PATCH 23/34] change Target .NET Framework to 4.7.2 --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 12d9bcc..cfb3fe2 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -9,7 +9,7 @@ Properties Seq.Client.EventLog Seq.Client.EventLog - net462 + net472 512 true From 197bf1d4722a1d8e954a19ba55d2fa78d6def68c Mon Sep 17 00:00:00 2001 From: OlesChri Date: Fri, 24 Feb 2023 16:37:36 +0100 Subject: [PATCH 24/34] handle multiple xml node keys with same key value --- src/Seq.Client.EventLog/Extensions.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index ca46655..827e7fd 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -88,7 +88,17 @@ private static Dictionary ProcessNode(XElement element, int dept depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) ? string.Format($"{nodeName}_{GetName(descendant)}") : GetName(descendant))) - result.Add(node.Key, node.Value); + { + if (!result.ContainsKey(node.Key)) result.Add(node.Key, node.Value); + + // handle duplicate keys, hacky but fast + else if (!result.ContainsKey(node.Key + "2")) result.Add(node.Key + "2", node.Value); + else if (!result.ContainsKey(node.Key + "3")) result.Add(node.Key + "3", node.Value); + else if (!result.ContainsKey(node.Key + "4")) result.Add(node.Key + "4", node.Value); + else if (!result.ContainsKey(node.Key + "5")) result.Add(node.Key + "5", node.Value); + else Log.Warning() + .Add("Event contained more than 5 nodes with same key: {NodeKey:l} = {NodeValue:l}", node.Key, node.Value); + } return result; } From 60b5838d2e4034bea26a7b09d16b7ce17b46075d Mon Sep 17 00:00:00 2001 From: OlesChri Date: Fri, 24 Feb 2023 16:40:27 +0100 Subject: [PATCH 25/34] handle null values of message in GetMessage --- src/Seq.Client.EventLog/Extensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index 827e7fd..d518685 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -113,7 +113,8 @@ private static string GetName(XElement element) public static string GetMessage(string message) { - return message.Contains(Environment.NewLine) && + return message != null && + message.Contains(Environment.NewLine) && !string.IsNullOrEmpty(message.Substring(0, message.IndexOf(Environment.NewLine, StringComparison.Ordinal))) ? message.Substring(0, message.IndexOf(Environment.NewLine, StringComparison.Ordinal)) From 658bf0413225cfb402098b66284f45fa5103897c Mon Sep 17 00:00:00 2001 From: OlesChri Date: Tue, 28 Feb 2023 13:47:21 +0100 Subject: [PATCH 26/34] bump version --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index cfb3fe2..84e2543 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.0.6 + 3.1.0 Connor O'Shea and contributors Connor O'Shea EventLog.ico From c68f9420982b8a0a224d9f0929216bb7f2c110c8 Mon Sep 17 00:00:00 2001 From: OlesChri Date: Tue, 28 Feb 2023 14:24:29 +0100 Subject: [PATCH 27/34] avoid keywordsdisplaynames exception --- src/Seq.Client.EventLog/EventLogListener.cs | 12 +++++++++++- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index d31c504..a3ac6f7 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -301,6 +301,16 @@ private void HandleEventLogEntry(EventRecord entry) eventTimeShort = ((DateTime) entry.TimeCreated).ToString("G"); } + IEnumerable keywordsDisplayNames; + // some entries throw a "System.Diagnostics.Eventing.Reader.EventLogNotFoundException" when accessing the .KeywordsDisplayNames property + try + { + keywordsDisplayNames = entry.KeywordsDisplayNames; + } catch (EventLogNotFoundException ex) + { + keywordsDisplayNames = null; + } + Log.Level(Extensions.MapLogLevel(entry)) .SetTimestamp(entry.TimeCreated ?? DateTime.Now) .AddProperty("LogAppName", LogAppName) @@ -313,7 +323,7 @@ private void HandleEventLogEntry(EventRecord entry) .AddProperty("EventTime", entry.TimeCreated) .AddProperty("EventTimeLong", eventTimeLong) .AddProperty("EventTimeShort", eventTimeShort) - .AddProperty("KeywordNames", entry.KeywordsDisplayNames) + .AddProperty("KeywordNames", keywordsDisplayNames) .AddProperty("RemoteServer", MachineName, false, false) .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) .AddProperty("EventLevel", entry.LevelDisplayName) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 84e2543..3471aaa 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.1.0 + 3.1.1 Connor O'Shea and contributors Connor O'Shea EventLog.ico From b7aeb64e58df52e78ecdef8bd5ff46387d67fa2c Mon Sep 17 00:00:00 2001 From: OlesChri Date: Tue, 28 Feb 2023 14:25:36 +0100 Subject: [PATCH 28/34] make pdb portable --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 3471aaa..c426344 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -16,8 +16,8 @@ AnyCPU true - full - false + portable + False bin\Debug\ DEBUG;TRACE prompt @@ -25,7 +25,7 @@ AnyCPU - full + portable true bin\Release\ TRACE From 9ef8517689fff44aeb593c6c13dee6174d71e2e3 Mon Sep 17 00:00:00 2001 From: OlesChri Date: Tue, 28 Feb 2023 14:37:01 +0100 Subject: [PATCH 29/34] handle EventLogProviderDisabledException --- src/Seq.Client.EventLog/EventLogListener.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index a3ac6f7..e9f97db 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -302,13 +302,17 @@ private void HandleEventLogEntry(EventRecord entry) } IEnumerable keywordsDisplayNames; - // some entries throw a "System.Diagnostics.Eventing.Reader.EventLogNotFoundException" when accessing the .KeywordsDisplayNames property + // some entries throw a "EventLogNotFoundException" or "EventLogProviderDisabledException" when accessing the .KeywordsDisplayNames property try { keywordsDisplayNames = entry.KeywordsDisplayNames; } catch (EventLogNotFoundException ex) { - keywordsDisplayNames = null; + keywordsDisplayNames = new string[] { ex.ToString() }; + } + catch (EventLogProviderDisabledException ex) + { + keywordsDisplayNames = new string[] { ex.ToString() }; } Log.Level(Extensions.MapLogLevel(entry)) From 16a9fbc29cc97dc461a27f252fe2c6dfa5fba3cd Mon Sep 17 00:00:00 2001 From: OlesChri Date: Wed, 1 Mar 2023 14:48:47 +0100 Subject: [PATCH 30/34] handle exception for LevelDisplayName property --- src/Seq.Client.EventLog/EventLogListener.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/EventLogListener.cs b/src/Seq.Client.EventLog/EventLogListener.cs index e9f97db..b1f95a0 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -315,6 +315,21 @@ private void HandleEventLogEntry(EventRecord entry) keywordsDisplayNames = new string[] { ex.ToString() }; } + string levelDisplayName; + // some entries throw a "EventLogNotFoundException" or "EventLogProviderDisabledException" when accessing the .LevelDisplayName property + try + { + levelDisplayName = entry.LevelDisplayName; + } + catch (EventLogNotFoundException ex) + { + levelDisplayName = ex.ToString(); + } + catch (EventLogProviderDisabledException ex) + { + levelDisplayName = ex.ToString(); + } + Log.Level(Extensions.MapLogLevel(entry)) .SetTimestamp(entry.TimeCreated ?? DateTime.Now) .AddProperty("LogAppName", LogAppName) @@ -330,7 +345,7 @@ private void HandleEventLogEntry(EventRecord entry) .AddProperty("KeywordNames", keywordsDisplayNames) .AddProperty("RemoteServer", MachineName, false, false) .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) - .AddProperty("EventLevel", entry.LevelDisplayName) + .AddProperty("EventLevel", levelDisplayName) .AddProperty("EventLevelId", entry.Level) .AddProperty("EventDescription", entry.FormatDescription()) .AddProperty("EventSummary", Extensions.GetMessage(entry.FormatDescription())) From b5114505e0f480d5c4b18698affdcf59ff9faac1 Mon Sep 17 00:00:00 2001 From: OlesChri Date: Wed, 1 Mar 2023 15:49:00 +0100 Subject: [PATCH 31/34] improved handling of multi valued properties --- src/Seq.Client.EventLog/Extensions.cs | 26 +++++++++---------- .../Seq.Client.EventLog.csproj | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Seq.Client.EventLog/Extensions.cs b/src/Seq.Client.EventLog/Extensions.cs index d518685..fed083b 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -84,20 +84,20 @@ private static Dictionary ProcessNode(XElement element, int dept result.Add(nodeName, element.Value); else foreach (var descendant in element.Elements()) - foreach (var node in ProcessNode(descendant, depth + 1, - depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) - ? string.Format($"{nodeName}_{GetName(descendant)}") - : GetName(descendant))) + foreach (var node in ProcessNode(descendant, depth + 1, + depth > 0 && !nodeName.Equals("System", StringComparison.OrdinalIgnoreCase) + ? string.Format($"{nodeName}_{GetName(descendant)}") + : GetName(descendant))) { - if (!result.ContainsKey(node.Key)) result.Add(node.Key, node.Value); - - // handle duplicate keys, hacky but fast - else if (!result.ContainsKey(node.Key + "2")) result.Add(node.Key + "2", node.Value); - else if (!result.ContainsKey(node.Key + "3")) result.Add(node.Key + "3", node.Value); - else if (!result.ContainsKey(node.Key + "4")) result.Add(node.Key + "4", node.Value); - else if (!result.ContainsKey(node.Key + "5")) result.Add(node.Key + "5", node.Value); - else Log.Warning() - .Add("Event contained more than 5 nodes with same key: {NodeKey:l} = {NodeValue:l}", node.Key, node.Value); + if (!result.ContainsKey(node.Key)) + { + result.Add(node.Key, node.Value); + } + else + { + // looks like a multi valued property, convert to string and append to existing entry + result[node.Key] = String.Format("{0}, {1}", result[node.Key], node.Value); + } } return result; diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index c426344..0893057 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.1.1 + 3.2.0 Connor O'Shea and contributors Connor O'Shea EventLog.ico From 86906b45b46f8f3d759ac98a9742b8e1a430f181 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Sat, 1 Jul 2023 09:25:02 -0700 Subject: [PATCH 32/34] Update dependencies --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 0893057..28e152d 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.2.0 + 3.2.1 Connor O'Shea and contributors Connor O'Shea EventLog.ico @@ -60,9 +60,9 @@ - + - 13.0.1 + 13.0.3 From 587fae41b6d3efd4da21f8c7bf165c1f762bb8e6 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Tue, 4 Jul 2023 10:54:30 -0700 Subject: [PATCH 33/34] Update Lurgle.Logging --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 28e152d..488eb50 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -60,7 +60,7 @@ - + 13.0.3 From 5f53e9b683b5b827a752c75bf3ee269ee961ab59 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Tue, 4 Jul 2023 11:01:24 -0700 Subject: [PATCH 34/34] Version bump --- src/Seq.Client.EventLog/Seq.Client.EventLog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj index 488eb50..25180d0 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -35,7 +35,7 @@ - 3.2.1 + 3.2.2 Connor O'Shea and contributors Connor O'Shea EventLog.ico