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
+
+
+
+
-