diff --git a/src/Seq.Client.EventLog.sln.DotSettings b/src/Seq.Client.EventLog.sln.DotSettings new file mode 100644 index 0000000..f310526 --- /dev/null +++ b/src/Seq.Client.EventLog.sln.DotSettings @@ -0,0 +1,6 @@ + + True + True + True + 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 8cde164..b3c9fe8 100644 --- a/src/Seq.Client.EventLog/App.config +++ b/src/Seq.Client.EventLog/App.config @@ -1,21 +1,22 @@ - + + - - -
- - - - - - - - - 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..dc3d7cd --- /dev/null +++ b/src/Seq.Client.EventLog/Config.cs @@ -0,0 +1,109 @@ +using System; +using System.Configuration; +using System.IO; +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"]; + LogToFile = GetBool(ConfigurationManager.AppSettings["LogToFile"], true); + LogFolder = ConfigurationManager.AppSettings["LogFolder"]; + HeartbeatInterval = GetInt(ConfigurationManager.AppSettings["HeartbeatInterval"]); + HeartbeatsBeforeReset = GetInt(ConfigurationManager.AppSettings["HeartbeatsBeforeReset"]); + + //Minimum is 0 (disabled) + if (HeartbeatInterval < 0) + HeartbeatInterval = 600; + //Maximum is 3600 + if (HeartbeatInterval > 3600) + HeartbeatInterval = 3600; + + if (HeartbeatsBeforeReset < 0) + HeartbeatsBeforeReset = 0; + + IsDebug = GetBool(ConfigurationManager.AppSettings["IsDebug"]); + + 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 bool LogToFile { get; } + 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 + /// + /// This will filter out nulls that could otherwise cause exceptions + /// + /// An object that can be converted to an int + /// + public 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; + } + } +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLog.ico b/src/Seq.Client.EventLog/EventLog.ico new file mode 100644 index 0000000..c405f33 Binary files /dev/null and b/src/Seq.Client.EventLog/EventLog.ico differ diff --git a/src/Seq.Client.EventLog/EventLog.png b/src/Seq.Client.EventLog/EventLog.png new file mode 100644 index 0000000..ba49ef6 Binary files /dev/null and b/src/Seq.Client.EventLog/EventLog.png differ diff --git a/src/Seq.Client.EventLog/EventLogClient.cs b/src/Seq.Client.EventLog/EventLogClient.cs index 3c70669..9ff0a37 100644 --- a/src/Seq.Client.EventLog/EventLogClient.cs +++ b/src/Seq.Client.EventLog/EventLogClient.cs @@ -1,68 +1,19 @@ -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using Newtonsoft.Json; -using Serilog; - -namespace Seq.Client.EventLog +namespace Seq.Client.EventLog { - class EventLogClient + internal class EventLogClient { - private List _eventLogListeners; - - public void Start(string configuration = null) - { - LoadListeners(configuration); - ValidateListeners(); - StartListeners(); - } - - public void Stop() - { - StopListeners(); - } - - 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("Loading listener configuration from {ConfigurationFilePath}", filePath); - var file = File.ReadAllText(filePath); - - _eventLogListeners = JsonConvert.DeserializeObject>(file); - } - - private void ValidateListeners() - { - foreach (var listener in _eventLogListeners) - { - listener.Validate(); - } - } - - private void StartListeners() + public static void Start(bool isInteractive = false, string configuration = null) { - foreach (var listener in _eventLogListeners) - { - listener.Start(); - } + 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 438b016..b1f95a0 100644 --- a/src/Seq.Client.EventLog/EventLogListener.cs +++ b/src/Seq.Client.EventLog/EventLogListener.cs @@ -1,151 +1,376 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; using System.Threading; using System.Threading.Tasks; +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; + + //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 bool ProcessRetroactiveEntries { get; set; } + + 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. - public List LogLevels { get; set; } + 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 System.Diagnostics.EventLog _eventLog; - private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); - private Task _retroactiveLoadingTask; - private volatile bool _started; + public string InitialTimeEstimate { get; set; } + 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. + public bool StoreLastEntry { 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)) + 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) { - throw new InvalidOperationException($"A {nameof(LogName)} must be specified for the listener."); + 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."); } - public void Start() + public void Start(bool isInteractive = false) { try { - Serilog.Log.Information("Starting listener for {LogName} on {MachineName}", LogName, MachineName ?? "."); + 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("ProcessRetroactiveEntries", ProcessRetroactiveEntries) + .AddProperty("StoreLastEntry", StoreLastEntry) + .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(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 = 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; + _eventLog = new EventLogQuery(LogName, PathType.LogName, "*"); + _isInteractive = isInteractive; + var session = new EventLogSession(); + if (string.IsNullOrEmpty(MachineName)) + session = new EventLogSession(MachineName); + _eventLog.Session = session; + _watcher = GetWatcherConfig(); + _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 ?? "."); + Log.Exception(ex).AddProperty("Message", ex.Message) + .AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) + .AddProperty("RemoteServer", MachineName, false, false).Add( + "[{LogAppName:l}] Failed to start {ListenerType:l} listener for {LogName:l} on {MachineName:l}: {Message:l}"); } } - System.Diagnostics.EventLog OpenEventLog() + private EventLogWatcher GetWatcherConfig() { - var eventLog = new System.Diagnostics.EventLog(LogName); - if (!string.IsNullOrWhiteSpace(MachineName)) - { - eventLog.MachineName = MachineName; - } + var eventLog = new EventLogReader(_eventLog); - return eventLog; - } + if (ProcessRetroactiveEntries || StoreLastEntry) + ServiceManager.SaveBookmarks = true; - public void Stop() - { - try + if (CurrentBookmark != null && (ProcessRetroactiveEntries || StoreLastEntry)) { - if (!_started) - return; - - _cancel.Cancel(); - _eventLog.EnableRaisingEvents = false; + //Go back a position to allow the bookmark to be read + eventLog.Seek(CurrentBookmark, -1); + var checkBookmark = eventLog.ReadEvent(); - // This would be a little racy if start and stop were ever called on different threads, but - // this isn't done, currently. - _retroactiveLoadingTask?.Wait(); + if (checkBookmark != null) + { + checkBookmark.Dispose(); + 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); + } - _eventLog.Close(); - _eventLog.Dispose(); + 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) + { + var firstEvent = eventLog.ReadEvent(); + if (firstEvent != null) + { + 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); + } - Serilog.Log.Information("Listener stopped"); + Log.Debug().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .Add( + "[{LogAppName:l}] Cannot determine first event for {LogName:l} on {MachineName:l} - processing new events"); } - catch (Exception ex) + else { - Serilog.Log.Error(ex, "Failed to stop listener"); + Log.Debug().AddProperty("LogAppName", LogAppName) + .AddProperty("LogName", LogName) + .Add("[{LogAppName:l}] Processing new events for {LogName:l} on {MachineName:l}"); } + + return new EventLogWatcher(_eventLog); } - private void SendRetroactiveEntries() + public void Stop() { try { - using (var eventLog = OpenEventLog()) - { - Serilog.Log.Information("Processing {EntryCount} retroactive entries in {LogName}", eventLog.Entries.Count, LogName); + if (!_started) + return; - foreach (EventLogEntry entry in eventLog.Entries) - { - if (_cancel.IsCancellationRequested) - { - Serilog.Log.Warning("Canceling retroactive event loading"); - return; - } + _cancel.Cancel(); + _watcher.Enabled = false; + _watcher.Dispose(); - HandleEventLogEntry(entry, eventLog.Log).GetAwaiter().GetResult(); - } - } + 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) { - Serilog.Log.Error(ex, "Failed to send retroactive entries in {LogName} on {MachineName}", LogName, MachineName ?? "."); + Log.Exception(ex).AddProperty("Message", ex.Message) + .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}"); } } - private void OnEntryWritten(object sender, EntryWrittenEventArgs args) + private async void OnEntryWritten(object sender, EventRecordWrittenEventArgs args) { try { - HandleEventLogEntry(args.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 && (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) + ServiceManager.OldEvents++; + else if (args.EventRecord == null) + ServiceManager.EmptyEvents++; } catch (Exception ex) { - Serilog.Log.Error(ex, "Failed to handle an event log entry"); + Log.Exception(ex).AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("Message", ex.Message) + .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}"); } } - private async Task HandleEventLogEntry(EventLogEntry entry, string logName) + 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 && !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 + { + 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"); + } + + IEnumerable keywordsDisplayNames; + // some entries throw a "EventLogNotFoundException" or "EventLogProviderDisabledException" when accessing the .KeywordsDisplayNames property + try + { + keywordsDisplayNames = entry.KeywordsDisplayNames; + } catch (EventLogNotFoundException ex) + { + keywordsDisplayNames = new string[] { ex.ToString() }; + } + catch (EventLogProviderDisabledException ex) + { + 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(); + } - await SeqApi.PostRawEvents(entry.ToDto(logName)); + 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", keywordsDisplayNames) + .AddProperty("RemoteServer", MachineName, false, false) + .AddProperty("ListenerType", Extensions.GetListenerType(MachineName)) + .AddProperty("EventLevel", levelDisplayName) + .AddProperty("EventLevelId", entry.Level) + .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) + .AddProperty("Tags", Extensions.GetArray(Tags), false, false) + .AddProperty("InitialTimeEstimate", InitialTimeEstimate, false, false) + .AddProperty("RemainingTimeEstimate", RemainingTimeEstimate, false, false) + .AddProperty("DueDate", DueDate, false, false) + .AddProperty(eventProperties) + .Add(MessageTemplate); + + ServiceManager.EventsProcessed++; + } + catch (Exception ex) + { + Log.Exception(ex).AddProperty("Message", ex.Message) + .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}"); + } } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/EventLogListeners.json b/src/Seq.Client.EventLog/EventLogListeners.json index 88565fe..d8f4b6e 100644 --- a/src/Seq.Client.EventLog/EventLogListeners.json +++ b/src/Seq.Client.EventLog/EventLogListeners.json @@ -1,22 +1,48 @@ [ { "LogName": "Application", - "LogLevels": [ 1, 2 ], - "ProcessRetroactiveEntries": true + "LogLevels": [1, 2] }, { + "LogAppName": "Security Monitor", "LogName": "Security", - "LogLevels": [ 16 ], - "ProcessRetroactiveEntries": true + //"LogLevels": [16], + "MessageTemplate": "[{LogAppName:l}] - {ListenerType:l} - ({EventLevel:l}) - Event Id {EventId} - {EventSummary:l}", + "ProjectKey": "TEST", + "Priority": "High", + "Responders": "MattM", + "Tags": "Extra,Secure,Logging", + "InitialTimeEstimate": "1h", + "RemainingTimeEstimate": "1h", + "DueDate": "7d", + "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, + "GuidIsEmpty": false + }, + //{ + // "LogName": "Security", + // "LogLevels": [ 16 ], + // "MachineName": "TESTPC" + //}, { "LogName": "Setup", - "LogLevels": [ 1, 2 ], "ProcessRetroactiveEntries": true }, { "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 3bdc6cb..fed083b 100644 --- a/src/Seq.Client.EventLog/Extensions.cs +++ b/src/Seq.Client.EventLog/Extensions.cs @@ -1,54 +1,124 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Xml.Linq; +using Lurgle.Logging; namespace Seq.Client.EventLog { public static class Extensions { - private static string MapLogLevel(EventLogEntryType type) + public static LurgLevel MapLogLevel(EventRecord entry) { - switch (type) + if (entry.Level == null && entry.Keywords == null) + return LurgLevel.Debug; + + if (entry.Keywords != null) { - 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"; + if (((StandardEventKeywords) entry.Keywords).HasFlag(StandardEventKeywords.AuditSuccess)) + return LurgLevel.Information; + + 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: + 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) + public static IEnumerable GetArray(string value) + { + return (value ?? "") + .Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()) + .ToArray(); + } + + public static string GetListenerType(string remoteServer) + { + if (string.IsNullOrEmpty(remoteServer) || remoteServer == ".") + return "Local"; + return string.Format($"\\\\{remoteServer}"); + } + + public static Dictionary ParseXml(string xml) { - return new RawEvents + var result = new Dictionary(); + if (string.IsNullOrEmpty(xml)) + return result; + + try + { + var xmlDoc = XElement.Parse(xml); + return ProcessNode(xmlDoc); + } + catch (Exception ex) { - Events = new[] - { - new RawEvent + 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))) { - Timestamp = entry.TimeGenerated, - Level = MapLogLevel(entry.EntryType), - MessageTemplate = entry.Message, - Properties = new Dictionary + if (!result.ContainsKey(node.Key)) + { + result.Add(node.Key, node.Value); + } + else { - { "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 } + // 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; + } + + 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 != 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)) + : message; } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/Program.cs b/src/Seq.Client.EventLog/Program.cs index b8abe50..556f84a 100644 --- a/src/Seq.Client.EventLog/Program.cs +++ b/src/Seq.Client.EventLog/Program.cs @@ -1,20 +1,21 @@ 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 { - 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) { @@ -22,18 +23,15 @@ 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) { 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); @@ -46,72 +44,108 @@ public static void Main(string[] args) } } - static void RunInteractive(string configFilePath) + private 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 in interactive mode on {MachineName:l} ...", + Config.AppName, Config.AppVersion); - var client = new EventLogClient(); - client.Start(configFilePath); + var unused = new EventLogClient(); + EventLogClient.Start(true, configFilePath); + ServiceManager.Start(true); var done = new ManualResetEvent(false); Console.CancelKeyPress += (s, e) => { - Log.Information("Ctrl+C pressed, stopping"); - client.Stop(); + Log.Debug().Add("Ctrl+C pressed, stopping"); + EventLogClient.Stop(); done.Set(); }; done.WaitOne(); - Log.Information("Stopped"); + ServiceManager.Stop(); + Log.Debug() + .Add("{AppName:l} v{AppVersion:l} Stopped in interactive mode on {MachineName:l}", Config.AppName, + Config.AppVersion); } catch (Exception ex) { - Log.Fatal(ex, "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 { - Log.CloseAndFlush(); + Logging.Close(); } } - static void RunService() + private static void RunService() { - var logFile = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - typeof(Program).Assembly.GetName().Name, - "ServiceLog.txt"); + 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 (!Directory.Exists(logFolder)) + Directory.CreateDirectory(logFolder); - Log.Logger = new LoggerConfiguration() - .WriteTo.File( - logFile, - rollingInterval: RollingInterval.Day, - rollOnFileSizeLimit: true, - retainedFileCountLimit: 7, - fileSizeLimitBytes: 10_000_000, - shared: true) - .CreateLogger(); + 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.Information("Running as service"); + Log.Debug() + .Add("{AppName:l} v{AppVersion:l} Starting as service on {MachineName:l} ...", Config.AppName, + Config.AppVersion); + Log.Debug() + .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()); - Log.Information("Stopped"); + ServiceManager.Stop(); + Log.Debug() + .Add("{AppName:l} v{AppVersion:l} Stopped as service on {MachineName:l}", Config.AppName, + Config.AppVersion); } catch (Exception ex) { - Log.Fatal(ex, "Exception thrown from service host"); + Log.Exception(ex).AddProperty("Message", ex.Message) + .Add("Exception thrown from service host on {MachineName:l}: {Message:l}"); } finally { - Log.CloseAndFlush(); + Logging.Close(); } } } -} +} \ No newline at end of file diff --git a/src/Seq.Client.EventLog/ProjectInstaller.cs b/src/Seq.Client.EventLog/ProjectInstaller.cs index aad38b1..a4554a7 100644 --- a/src/Seq.Client.EventLog/ProjectInstaller.cs +++ b/src/Seq.Client.EventLog/ProjectInstaller.cs @@ -1,19 +1,16 @@ -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; + +// ReSharper disable ClassNeverInstantiated.Global 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/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..25180d0 100644 --- a/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj +++ b/src/Seq.Client.EventLog/Seq.Client.EventLog.csproj @@ -1,6 +1,6 @@  - - + + Debug AnyCPU @@ -9,15 +9,15 @@ Properties Seq.Client.EventLog Seq.Client.EventLog - v4.6.2 + net472 512 true AnyCPU true - full - false + portable + False bin\Debug\ DEBUG;TRACE prompt @@ -25,7 +25,7 @@ AnyCPU - full + portable true bin\Release\ TRACE @@ -35,21 +35,12 @@ + 3.2.2 + Connor O'Shea and contributors + Connor O'Shea + EventLog.ico - - ..\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 - @@ -64,56 +55,19 @@ - - - - - Component - - - ProjectInstaller.cs - - - True - True - Settings.settings - - - - - - Component - - - Service.cs - - - - - - - Designer - PreserveNewest - - Designer - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - ProjectInstaller.cs - - - Service.cs - + + + 13.0.3 + + + + -