diff --git a/.gitignore b/.gitignore index 3798f23..36213c5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,13 @@ _ReSharper*/ [TestResult*/ artifacts/ publish/ +*.zip +*.zip.txt +*.zip.b64 +*.zip.b64.txt +*.base64 +*.base64.txt +*.certutil.txt HIP-VMs.txt hostcheck.ps diff --git a/src/BizTalkMonitorConfigGenerator/BizTalk/BizTalkCatalogDiscoveryService.cs b/src/BizTalkMonitorConfigGenerator/BizTalk/BizTalkCatalogDiscoveryService.cs index 293b190..ff894af 100644 --- a/src/BizTalkMonitorConfigGenerator/BizTalk/BizTalkCatalogDiscoveryService.cs +++ b/src/BizTalkMonitorConfigGenerator/BizTalk/BizTalkCatalogDiscoveryService.cs @@ -9,12 +9,32 @@ using Microsoft.BizTalk.ExplorerOM; namespace BizTalkMonitorConfigGenerator.BizTalk { + /// + /// Discovers monitorable endpoints from the BizTalk management catalog. + /// public sealed class BizTalkCatalogDiscoveryService { + /// + /// BizTalk discovery options. + /// private readonly BizTalkOptions _options; + + /// + /// Output defaults used while normalizing endpoints. + /// private readonly OutputOptions _outputOptions; + + /// + /// Logger used for discovery decisions. + /// private readonly GeneratorFileLogger _logger; + /// + /// Initializes a new BizTalk catalog discovery service. + /// + /// BizTalk discovery options. + /// Output defaults for generated targets. + /// Discovery logger. public BizTalkCatalogDiscoveryService(BizTalkOptions options, OutputOptions outputOptions, GeneratorFileLogger logger) { _options = options ?? new BizTalkOptions(); @@ -22,10 +42,17 @@ namespace BizTalkMonitorConfigGenerator.BizTalk _logger = logger; } + /// + /// Discovers all configured BizTalk artifact endpoints and returns monitor-compatible endpoints. + /// + /// The discovered endpoints sorted by application, target name and endpoint. public IReadOnlyCollection Discover() { var discovered = new List(); + var inspectedApplications = 0; + var skippedApplications = 0; + _logger.Info("BizTalk-Katalog wird geöffnet. IncludeSendPorts=" + _options.IncludeSendPorts + ", IncludeReceiveLocations=" + _options.IncludeReceiveLocations + "."); var catalog = new BtsCatalogExplorer(); catalog.ConnectionString = _options.ConnectionString; @@ -38,10 +65,14 @@ namespace BizTalkMonitorConfigGenerator.BizTalk if (ShouldSkipApplication(application)) { + skippedApplications++; _logger.Info("BizTalk-Anwendung übersprungen: " + application.Name); continue; } + inspectedApplications++; + _logger.Info("BizTalk-Anwendung wird analysiert: " + application.Name); + if (_options.IncludeSendPorts) { DiscoverSendPorts(application, discovered); @@ -53,6 +84,8 @@ namespace BizTalkMonitorConfigGenerator.BizTalk } } + _logger.Info("BizTalk-Discovery beendet. AnalysierteAnwendungen=" + inspectedApplications + ", UebersprungeneAnwendungen=" + skippedApplications + ", UnterstuetzteTargets=" + discovered.Count + "."); + return discovered .OrderBy(x => x.MappedApplicationName, StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.TargetName, StringComparer.OrdinalIgnoreCase) @@ -60,6 +93,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk .ToList(); } + /// + /// Discovers send port transports from one BizTalk application. + /// + /// The BizTalk application to inspect. + /// The target collection receiving supported endpoints. private void DiscoverSendPorts(Application application, List discovered) { foreach (SendPort sendPort in application.SendPorts) @@ -90,6 +128,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk } } + /// + /// Discovers receive locations from one BizTalk application. + /// + /// The BizTalk application to inspect. + /// The target collection receiving supported endpoints. private void DiscoverReceiveLocations(Application application, List discovered) { foreach (ReceivePort receivePort in application.ReceivePorts) @@ -131,6 +174,16 @@ namespace BizTalkMonitorConfigGenerator.BizTalk } } + /// + /// Adds a send port transport when it exists and can be normalized. + /// + /// The target collection receiving supported endpoints. + /// The source BizTalk application name. + /// The artifact type label. + /// The base name for the generated target. + /// The source artifact name. + /// The BizTalk transport information. + /// True when the transport is a secondary send port transport. private void AddTransportIfSupported(List discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, TransportInfo transport, bool isSecondary) { if (transport == null) @@ -150,6 +203,17 @@ namespace BizTalkMonitorConfigGenerator.BizTalk isSecondary); } + /// + /// Adds a discovered endpoint when artifact filters and endpoint normalization allow it. + /// + /// The target collection receiving supported endpoints. + /// The source BizTalk application name. + /// The artifact type label. + /// The base name for the generated target. + /// The source artifact name. + /// The BizTalk adapter name. + /// The raw address read from BizTalk. + /// True when the endpoint is from a secondary transport. private void AddEndpointIfSupported(List discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, string adapterName, string sourceAddress, bool isSecondary) { if (ShouldSkipArtifact(artifactName)) @@ -190,6 +254,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk _logger.Discovery("ADD", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, normalizedEndpoint, "Zuordnung zu ApplicationName='" + mappedApplication + "'."); } + /// + /// Determines whether a BizTalk application should be skipped. + /// + /// The BizTalk application to inspect. + /// True when the application should be skipped. private bool ShouldSkipApplication(Application application) { if (application == null) @@ -205,6 +274,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return _options.IgnoreApplications.Any(pattern => WildcardMatch(application.Name, pattern)); } + /// + /// Determines whether a BizTalk artifact name matches a configured ignore pattern. + /// + /// The artifact name to inspect. + /// True when the artifact should be skipped. private bool ShouldSkipArtifact(string artifactName) { if (string.IsNullOrWhiteSpace(artifactName)) @@ -215,6 +289,13 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return _options.IgnoreArtifactNamePatterns.Any(pattern => WildcardMatch(artifactName, pattern)); } + /// + /// Resolves the generated monitor application name for a BizTalk artifact. + /// + /// The source BizTalk application name. + /// The source artifact type. + /// The source artifact name. + /// The generated monitor application name. private string ResolveMappedApplicationName(string bizTalkApplicationName, string artifactType, string artifactName) { foreach (var rule in _options.ArtifactMappings.Where(r => r != null && !string.IsNullOrWhiteSpace(r.ApplicationName))) @@ -241,6 +322,14 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return string.IsNullOrWhiteSpace(bizTalkApplicationName) ? "Unassigned Application" : bizTalkApplicationName.Trim(); } + /// + /// Builds a generated monitor target name from BizTalk artifact metadata. + /// + /// The source artifact type. + /// The base artifact display name. + /// The BizTalk adapter name. + /// True when the endpoint is from a secondary transport. + /// The generated target name. private string BuildTargetName(string artifactType, string baseName, string adapterName, bool isSecondary) { var prefix = string.Equals(artifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase) @@ -261,6 +350,16 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return name; } + /// + /// Normalizes a BizTalk adapter address into a monitor-compatible endpoint. + /// + /// The raw address read from BizTalk. + /// The BizTalk adapter name. + /// The normalized monitor endpoint when successful. + /// The resolved explicit port when available. + /// The generated HTTP method when applicable. + /// The skip reason when normalization fails. + /// True when the address can be monitored; otherwise false. private bool TryNormalizeEndpoint(string sourceAddress, string adapterName, out string normalizedEndpoint, out int? port, out string httpMethod, out string reason) { normalizedEndpoint = string.Empty; @@ -386,6 +485,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return true; } + /// + /// Safely resolves the adapter name from a BizTalk transport. + /// + /// The BizTalk transport information. + /// The adapter name or an empty string. private static string ResolveAdapterName(TransportInfo transport) { try @@ -398,6 +502,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk } } + /// + /// Safely resolves the adapter name from a BizTalk protocol type. + /// + /// The BizTalk protocol type. + /// The adapter name or an empty string. private static string ResolveAdapterName(ProtocolType transportType) { try @@ -410,6 +519,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk } } + /// + /// Safely reads the address from a BizTalk transport. + /// + /// The BizTalk transport information. + /// The transport address or an empty string. private static string SafeAddress(TransportInfo transport) { try @@ -422,12 +536,23 @@ namespace BizTalkMonitorConfigGenerator.BizTalk } } + /// + /// Checks whether an adapter name contains a fragment. + /// + /// The adapter name to inspect. + /// The fragment to search for. + /// True when the adapter name contains the fragment. private static bool AdapterContains(string adapterName, string fragment) { return !string.IsNullOrWhiteSpace(adapterName) && adapterName.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0; } + /// + /// Detects semicolon-separated email recipient lists that are not monitorable endpoints. + /// + /// The address value to inspect. + /// True when the value looks like an email address list. private static bool LooksLikeEmailAddressList(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -475,6 +600,12 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return true; } + /// + /// Applies simple wildcard matching with asterisks and question marks. + /// + /// The input value. + /// The wildcard pattern. + /// True when the input matches the pattern. private static bool WildcardMatch(string input, string pattern) { if (string.IsNullOrWhiteSpace(pattern)) @@ -487,6 +618,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return Regex.IsMatch(input, regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } + /// + /// Determines whether a value looks like a host:port endpoint. + /// + /// The value to inspect. + /// True when the value contains a host and numeric port. private static bool LooksLikeHostPort(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -497,6 +633,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk return Regex.IsMatch(value.Trim(), @"^[a-zA-Z0-9._-]+:\d{1,5}($|/.*$)"); } + /// + /// Parses a port from a host:port value. + /// + /// The value containing a port. + /// The parsed port, or null when no port is found. private static int? ParsePort(string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/src/BizTalkMonitorConfigGenerator/BizTalk/DiscoveredEndpoint.cs b/src/BizTalkMonitorConfigGenerator/BizTalk/DiscoveredEndpoint.cs index d0f1007..d0e6b77 100644 --- a/src/BizTalkMonitorConfigGenerator/BizTalk/DiscoveredEndpoint.cs +++ b/src/BizTalkMonitorConfigGenerator/BizTalk/DiscoveredEndpoint.cs @@ -1,18 +1,68 @@ namespace BizTalkMonitorConfigGenerator.BizTalk { + /// + /// Represents one BizTalk artifact endpoint that can be written as a monitor target. + /// public sealed class DiscoveredEndpoint { + /// + /// Gets or sets the BizTalk artifact type. + /// public string ArtifactType { get; set; } + + /// + /// Gets or sets the source BizTalk application name. + /// public string BizTalkApplicationName { get; set; } + + /// + /// Gets or sets the generated monitor application name. + /// public string MappedApplicationName { get; set; } + + /// + /// Gets or sets the source BizTalk artifact name. + /// public string ArtifactName { get; set; } + + /// + /// Gets or sets the BizTalk adapter name. + /// public string AdapterName { get; set; } + + /// + /// Gets or sets the raw address read from BizTalk. + /// public string SourceAddress { get; set; } + + /// + /// Gets or sets the monitor-compatible endpoint. + /// public string NormalizedEndpoint { get; set; } + + /// + /// Gets or sets the generated monitor target name. + /// public string TargetName { get; set; } + + /// + /// Gets or sets the HTTP method for generated HTTP targets. + /// public string HttpMethod { get; set; } + + /// + /// Gets or sets the explicit target port, when resolved. + /// public int? Port { get; set; } + + /// + /// Gets or sets an optional UNC path validation override. + /// public bool? ValidateUncPathAccess { get; set; } + + /// + /// Gets or sets discovery detail text for diagnostics. + /// public string Detail { get; set; } } } diff --git a/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs b/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs index 9c55b5c..d528d20 100644 --- a/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs +++ b/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs @@ -7,16 +7,51 @@ using HostAvailabilityMonitor.Configuration; namespace BizTalkMonitorConfigGenerator.Configuration { + /// + /// Root configuration for the BizTalk monitor configuration generator. + /// public sealed class GeneratorConfiguration { + /// + /// Gets or sets the generator name shown in logs and Event Log entries. + /// public string GeneratorName { get; set; } = "BizTalkMonitorConfigGenerator"; + + /// + /// Gets or sets the environment label used in logs and generated output. + /// public string EnvironmentName { get; set; } = "Unspecified Environment"; + + /// + /// Gets or sets rolling file log settings. + /// public GeneratorLoggingOptions Logging { get; set; } = new GeneratorLoggingOptions(); + + /// + /// Gets or sets Windows Event Log settings. + /// public GeneratorEventLogOptions EventLog { get; set; } = new GeneratorEventLogOptions(); + + /// + /// Gets or sets BizTalk discovery settings. + /// public BizTalkOptions BizTalk { get; set; } = new BizTalkOptions(); + + /// + /// Gets or sets output file and target defaults. + /// public OutputOptions Output { get; set; } = new OutputOptions(); + + /// + /// Gets or sets the monitor configuration template used for generated output. + /// public MonitorConfiguration GeneratedMonitorConfig { get; set; } = new MonitorConfiguration(); + /// + /// Loads and normalizes generator configuration from a JSON file. + /// + /// The JSON configuration path. + /// The normalized generator configuration. public static GeneratorConfiguration Load(string path) { if (!File.Exists(path)) @@ -74,6 +109,9 @@ namespace BizTalkMonitorConfigGenerator.Configuration return config; } + /// + /// Validates generator settings and applies safe defaults where possible. + /// public void Validate() { if (string.IsNullOrWhiteSpace(BizTalk.ConnectionString)) @@ -115,8 +153,16 @@ namespace BizTalkMonitorConfigGenerator.Configuration } } + /// + /// Provides recipient list normalization helpers shared by generator configuration validation. + /// internal static class RecipientListHelper { + /// + /// Removes null, empty and duplicate recipient entries. + /// + /// The recipient list from configuration. + /// A normalized recipient list. internal static List SanitizeRecipientList(List values) { if (values == null) @@ -131,6 +177,11 @@ namespace BizTalkMonitorConfigGenerator.Configuration .ToList(); } + /// + /// Counts all configured email recipients across To, Cc and Bcc. + /// + /// The email options to inspect. + /// The number of configured recipients. internal static int GetConfiguredRecipientCount(EmailOptions email) { if (email == null) @@ -144,65 +195,216 @@ namespace BizTalkMonitorConfigGenerator.Configuration } } + /// + /// Defines rolling file log settings for the generator. + /// public sealed class GeneratorLoggingOptions { + /// + /// Gets or sets the directory where generator log files are written. + /// public string LogDirectory { get; set; } = "logs"; + + /// + /// Gets or sets the daily generator log file prefix. + /// public string FilePrefix { get; set; } = "biztalk-monitor-config-generator"; + + /// + /// Gets or sets the number of daily generator log files to retain. + /// public int RetentionDays { get; set; } = 5; } + /// + /// Defines Windows Event Log behavior for the generator. + /// public sealed class GeneratorEventLogOptions { + /// + /// Gets or sets a value indicating whether Event Log writes are enabled. + /// public bool Enabled { get; set; } = true; + + /// + /// Gets or sets the Windows Event Log name. + /// public string LogName { get; set; } = "Application"; + + /// + /// Gets or sets the Event Log source name. + /// public string Source { get; set; } = "BizTalkMonitorConfigGenerator"; + + /// + /// Gets or sets whether the generator should try to create the Event Log source. + /// public bool CreateSourceIfMissing { get; set; } = false; + + /// + /// Gets or sets whether successful generator events should be written. + /// public bool WriteSuccessEntries { get; set; } = false; + + /// + /// Gets or sets whether generator summary events should be written. + /// public bool WriteSummaryEntries { get; set; } = true; } + /// + /// Defines BizTalk artifact discovery behavior. + /// public sealed class BizTalkOptions { + /// + /// Gets or sets the BizTalk management database connection string. + /// public string ConnectionString { get; set; } = "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI"; + + /// + /// Gets or sets whether send ports should be discovered. + /// public bool IncludeSendPorts { get; set; } = true; + + /// + /// Gets or sets whether receive locations should be discovered. + /// public bool IncludeReceiveLocations { get; set; } = true; + + /// + /// Gets or sets whether only started send ports should be included. + /// public bool OnlyStartedSendPorts { get; set; } = true; + + /// + /// Gets or sets whether only enabled receive locations should be included. + /// public bool OnlyEnabledReceiveLocations { get; set; } = true; + + /// + /// Gets or sets whether secondary send port transports should be included. + /// public bool IncludeSecondaryTransportOnSendPorts { get; set; } = true; + + /// + /// Gets or sets whether dynamic send ports should be included. + /// public bool IncludeDynamicSendPorts { get; set; } = false; + + /// + /// Gets or sets whether local file paths should be emitted as monitor targets. + /// public bool IncludeLocalFilePaths { get; set; } = false; + + /// + /// Gets or sets whether BizTalk system applications should be skipped. + /// public bool IgnoreSystemApplications { get; set; } = true; + + /// + /// Gets or sets application name wildcard patterns to skip. + /// public List IgnoreApplications { get; set; } = new List(); + + /// + /// Gets or sets artifact name wildcard patterns to skip. + /// public List IgnoreArtifactNamePatterns { get; set; } = new List(); + + /// + /// Gets or sets application-level mapping rules for generated ApplicationName values. + /// public List ApplicationMappings { get; set; } = new List(); + + /// + /// Gets or sets artifact-level mapping rules for generated ApplicationName values. + /// public List ArtifactMappings { get; set; } = new List(); } + /// + /// Defines output file behavior and defaults for generated monitor targets. + /// public sealed class OutputOptions { + /// + /// Gets or sets the generated monitor configuration output path. + /// public string Path { get; set; } = "generated-monitor-appsettings.json"; + + /// + /// Gets or sets whether an existing output file may be overwritten. + /// public bool OverwriteExisting { get; set; } = true; + + /// + /// Gets or sets whether no discovered targets should produce a failing exit code. + /// public bool FailIfNoTargetsFound { get; set; } = false; + + /// + /// Gets or sets the timeout assigned to generated targets. + /// public int TargetTimeoutSeconds { get; set; } = 10; + + /// + /// Gets or sets whether generated targets are enabled by default. + /// public bool TargetEnabled { get; set; } = true; + + /// + /// Gets or sets the HTTP method assigned to generated HTTP targets. + /// public string DefaultHttpMethod { get; set; } = "HEAD"; } + /// + /// Maps a BizTalk application name pattern to a monitor application name. + /// public sealed class ApplicationMappingRule { + /// + /// Gets or sets the BizTalk application wildcard pattern. + /// public string BizTalkApplicationName { get; set; } = string.Empty; + + /// + /// Gets or sets the generated monitor application name. + /// public string ApplicationName { get; set; } = string.Empty; } + /// + /// Maps a BizTalk artifact pattern to a monitor application name. + /// public sealed class ArtifactMappingRule { + /// + /// Gets or sets the optional BizTalk artifact type filter. + /// public string ArtifactType { get; set; } = string.Empty; + + /// + /// Gets or sets the BizTalk artifact name wildcard pattern. + /// public string ArtifactNamePattern { get; set; } = string.Empty; + + /// + /// Gets or sets the generated monitor application name. + /// public string ApplicationName { get; set; } = string.Empty; } + /// + /// Provides monitor configuration template helpers used by the generator. + /// internal static class MonitorConfigurationTemplateExtensions { + /// + /// Validates the monitor template without requiring generated targets. + /// + /// The monitor template to validate. public static void ValidateTemplateWithoutTargets(this MonitorConfiguration configuration) { if (configuration == null) @@ -274,6 +476,11 @@ namespace BizTalkMonitorConfigGenerator.Configuration } } + /// + /// Clones the monitor template while leaving the target list empty for generated output. + /// + /// The monitor template to clone. + /// A monitor configuration clone without targets. public static MonitorConfiguration CloneWithoutTargets(this MonitorConfiguration configuration) { var clone = new MonitorConfiguration diff --git a/src/BizTalkMonitorConfigGenerator/Logging/GeneratorEventLogger.cs b/src/BizTalkMonitorConfigGenerator/Logging/GeneratorEventLogger.cs index 3982de8..b6c08e0 100644 --- a/src/BizTalkMonitorConfigGenerator/Logging/GeneratorEventLogger.cs +++ b/src/BizTalkMonitorConfigGenerator/Logging/GeneratorEventLogger.cs @@ -5,12 +5,31 @@ using BizTalkMonitorConfigGenerator.Configuration; namespace BizTalkMonitorConfigGenerator.Logging { + /// + /// Best-effort Windows Event Log writer for generator lifecycle and summary events. + /// public sealed class GeneratorEventLogger { + /// + /// Event Log settings from generator configuration. + /// private readonly GeneratorEventLogOptions _options; + + /// + /// File logger used when Event Log writes fail. + /// private readonly GeneratorFileLogger _fallbackLogger; + + /// + /// Indicates that Event Log writing has been disabled for this process. + /// private bool _disabled; + /// + /// Initializes a new generator Event Log writer. + /// + /// The Event Log configuration. + /// The fallback file logger. public GeneratorEventLogger(GeneratorEventLogOptions options, GeneratorFileLogger fallbackLogger) { _options = options ?? new GeneratorEventLogOptions(); @@ -18,6 +37,9 @@ namespace BizTalkMonitorConfigGenerator.Logging _disabled = !_options.Enabled; } + /// + /// Optionally creates the configured Event Log source. + /// public void TryInitialize() { if (_disabled || !_options.CreateSourceIfMissing) @@ -43,11 +65,21 @@ namespace BizTalkMonitorConfigGenerator.Logging } } + /// + /// Writes an informational event. + /// + /// The event message. + /// The event identifier. public void Info(string message, int eventId) { Write(message, EventLogEntryType.Information, eventId, true); } + /// + /// Writes an informational event only when success entries are enabled. + /// + /// The event message. + /// The event identifier. public void InfoIfSuccessEnabled(string message, int eventId) { if (_options.WriteSuccessEntries) @@ -56,16 +88,31 @@ namespace BizTalkMonitorConfigGenerator.Logging } } + /// + /// Writes a warning event. + /// + /// The event message. + /// The event identifier. public void Warning(string message, int eventId) { Write(message, EventLogEntryType.Warning, eventId, true); } + /// + /// Writes an error event. + /// + /// The event message. + /// The event identifier. public void Error(string message, int eventId) { Write(message, EventLogEntryType.Error, eventId, true); } + /// + /// Writes a generator summary event when summary entries are enabled. + /// + /// The summary message. + /// True when the summary should be written as a warning. public void Summary(string message, bool hasWarnings) { if (!_options.WriteSummaryEntries) @@ -76,6 +123,13 @@ namespace BizTalkMonitorConfigGenerator.Logging Write(message, hasWarnings ? EventLogEntryType.Warning : EventLogEntryType.Information, hasWarnings ? 2100 : 1100, true); } + /// + /// Writes one Event Log entry and disables future Event Log writes on failure. + /// + /// The event message. + /// The Windows Event Log entry type. + /// The event identifier. + /// True to write the event when the logger is enabled. private void Write(string message, EventLogEntryType entryType, int eventId, bool forceWrite) { if (_disabled || !forceWrite) @@ -101,6 +155,10 @@ namespace BizTalkMonitorConfigGenerator.Logging } } + /// + /// Disables Event Log writes and records the reason in the fallback log. + /// + /// The reason Event Log writes were disabled. private void DisableAndFallback(string reason) { if (_disabled) diff --git a/src/BizTalkMonitorConfigGenerator/Logging/GeneratorFileLogger.cs b/src/BizTalkMonitorConfigGenerator/Logging/GeneratorFileLogger.cs index d33b9a0..6e27476 100644 --- a/src/BizTalkMonitorConfigGenerator/Logging/GeneratorFileLogger.cs +++ b/src/BizTalkMonitorConfigGenerator/Logging/GeneratorFileLogger.cs @@ -5,15 +5,49 @@ using System.Threading; namespace BizTalkMonitorConfigGenerator.Logging { + /// + /// Writes rolling log files for BizTalk monitor configuration generation. + /// public sealed class GeneratorFileLogger { + /// + /// Synchronizes file writes within the current process. + /// private readonly object _sync = new object(); + + /// + /// Directory where generator logs are stored. + /// private readonly string _directory; + + /// + /// Prefix used for daily generator log files. + /// private readonly string _filePrefix; + + /// + /// Number of daily log files to retain. + /// private readonly int _retentionDays; + + /// + /// Generator name written to log lines. + /// private readonly string _generatorName; + + /// + /// Environment name written to log lines. + /// private readonly string _environmentName; + /// + /// Initializes a new generator file logger. + /// + /// The target log directory. + /// The daily log file prefix. + /// The number of daily log files to retain. + /// The generator name written to log lines. + /// The environment name written to log lines. public GeneratorFileLogger(string directory, string filePrefix, int retentionDays, string generatorName, string environmentName) { _directory = directory; @@ -24,6 +58,9 @@ namespace BizTalkMonitorConfigGenerator.Logging Directory.CreateDirectory(_directory); } + /// + /// Deletes old rolling log files according to the retention setting. + /// public void CleanupOldFiles() { if (_retentionDays <= 0) @@ -55,21 +92,44 @@ namespace BizTalkMonitorConfigGenerator.Logging } } + /// + /// Writes an informational log entry. + /// + /// The message to write. public void Info(string message) { Write("INFO", message); } + /// + /// Writes a warning log entry. + /// + /// The message to write. public void Warn(string message) { Write("WARN", message); } + /// + /// Writes an error log entry. + /// + /// The message to write. public void Error(string message) { Write("ERROR", message); } + /// + /// Writes one structured discovery decision. + /// + /// The discovery action, such as ADD or SKIP. + /// The BizTalk artifact type. + /// The BizTalk application name. + /// The artifact name. + /// The adapter name. + /// The source address read from BizTalk. + /// The generated monitor endpoint, if any. + /// The discovery detail or skip reason. public void Discovery(string action, string artifactType, string applicationName, string artifactName, string adapterName, string sourceAddress, string normalizedEndpoint, string detail) { Write("DISCOVERY", string.Format( @@ -85,6 +145,11 @@ namespace BizTalkMonitorConfigGenerator.Logging Escape(detail))); } + /// + /// Formats and appends a generator log line. + /// + /// The log level text. + /// The formatted message payload. private void Write(string level, string message) { var line = string.Format( @@ -104,6 +169,11 @@ namespace BizTalkMonitorConfigGenerator.Logging } } + /// + /// Appends a line to a file and retries transient file access failures. + /// + /// The log file path. + /// The full line to append. private void AppendWithRetries(string path, string line) { Exception lastException = null; @@ -132,12 +202,22 @@ namespace BizTalkMonitorConfigGenerator.Logging } } + /// + /// Builds the daily generator log file path for the current local date. + /// + /// The current log file path. private string GetCurrentLogFilePath() { var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"; return Path.Combine(_directory, fileName); } + /// + /// Extracts the date encoded in a rolling generator log file name. + /// + /// The log file path to inspect. + /// The parsed file date when parsing succeeds. + /// True when the date could be parsed; otherwise false. private bool TryGetDateFromFileName(string path, out DateTime date) { date = DateTime.MinValue; @@ -156,6 +236,11 @@ namespace BizTalkMonitorConfigGenerator.Logging out date); } + /// + /// Removes line breaks from values before they are written to one-line logs. + /// + /// The value to escape. + /// The log-safe value. private static string Escape(string value) { return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " "); diff --git a/src/BizTalkMonitorConfigGenerator/Program.cs b/src/BizTalkMonitorConfigGenerator/Program.cs index 5133ca1..c2b2596 100644 --- a/src/BizTalkMonitorConfigGenerator/Program.cs +++ b/src/BizTalkMonitorConfigGenerator/Program.cs @@ -10,48 +10,71 @@ using HostAvailabilityMonitor.Configuration; namespace BizTalkMonitorConfigGenerator { + /// + /// Console entry point and orchestration workflow for the BizTalk monitor config generator. + /// internal static class Program { + /// + /// Runs the generator once. + /// + /// Optional command-line arguments. The first argument may be the generator configuration path. + /// The process exit code. private static int Main(string[] args) { GeneratorFileLogger logger = null; GeneratorEventLogger eventLogger = null; + string baseDirectory = null; + string configPath = null; try { - var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; - var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]) + baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]) ? args[0] : Path.Combine(baseDirectory, "generator-settings.json"); + configPath = Path.GetFullPath(configPath); + + WriteConsoleBanner("BizTalk Monitor Config Generator", configPath); + WriteConsoleInfo("Lade Generator-Konfiguration..."); var configuration = GeneratorConfiguration.Load(configPath); configuration.Validate(); + WriteConsoleInfo("Konfiguration geladen. Umgebung=" + configuration.EnvironmentName + "."); var logDirectory = ResolvePath(baseDirectory, configuration.Logging.LogDirectory); + WriteConsoleInfo("Initialisiere Dateilog: " + BuildLogFilePreview(logDirectory, configuration.Logging.FilePrefix)); logger = new GeneratorFileLogger(logDirectory, configuration.Logging.FilePrefix, configuration.Logging.RetentionDays, configuration.GeneratorName, configuration.EnvironmentName); logger.CleanupOldFiles(); logger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + "."); logger.Info("Generator-Konfiguration geladen: " + Path.GetFullPath(configPath)); + logger.Info("Ausgabeziel: " + ResolvePath(baseDirectory, configuration.Output.Path)); + WriteConsoleInfo("Initialisiere Windows Event Log. Quelle=" + configuration.EventLog.Source + ", Log=" + configuration.EventLog.LogName + ", Aktiv=" + configuration.EventLog.Enabled + "."); eventLogger = new GeneratorEventLogger(configuration.EventLog, logger); eventLogger.TryInitialize(); eventLogger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000); + WriteConsoleInfo("Starte BizTalk-Discovery. SendPorts=" + configuration.BizTalk.IncludeSendPorts + ", ReceiveLocations=" + configuration.BizTalk.IncludeReceiveLocations + "."); var discoveryService = new BizTalkCatalogDiscoveryService(configuration.BizTalk, configuration.Output, logger); var discovered = discoveryService.Discover(); logger.Info("Discovery abgeschlossen. Gefundene unterstützte Targets=" + discovered.Count + "."); + WriteConsoleInfo("Discovery abgeschlossen. Gefundene unterstuetzte Targets=" + discovered.Count + "."); var outputConfiguration = BuildMonitorConfiguration(configuration, discovered); var outputPath = ResolvePath(baseDirectory, configuration.Output.Path); + WriteConsoleInfo("Schreibe Monitor-Konfiguration: " + outputPath); JsonFileWriter.WritePrettyJsonAtomic(outputPath, outputConfiguration, configuration.Output.OverwriteExisting); logger.Info("Monitor-Konfiguration geschrieben: " + outputPath); eventLogger.InfoIfSuccessEnabled("Monitor-Konfiguration erfolgreich generiert: " + outputPath, 1001); + WriteConsoleInfo("Monitor-Konfiguration geschrieben."); if (discovered.Count == 0) { var message = "Es wurden keine aktiven und unterstützten BizTalk-Endpunkte gefunden. Ausgabe wurde dennoch geschrieben: " + outputPath; logger.Warn(message); eventLogger.Summary(message, true); + WriteConsoleWarn(message); return configuration.Output.FailIfNoTargetsFound ? 2 : 0; } @@ -62,13 +85,21 @@ namespace BizTalkMonitorConfigGenerator outputPath); logger.Info(summary); eventLogger.Summary(summary, false); + WriteConsoleInfo(summary); return 0; } catch (Exception ex) { + WriteConsoleFatal("Generatorlauf fehlgeschlagen.", configPath, ex); + if (logger != null) { logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message); + logger.Error("Details: " + ex); + } + else + { + TryWriteFallbackFailureLog(baseDirectory, configPath, ex); } if (eventLogger != null) @@ -80,6 +111,12 @@ namespace BizTalkMonitorConfigGenerator } } + /// + /// Builds the final monitor configuration from the template and discovered endpoints. + /// + /// The generator configuration. + /// The discovered BizTalk endpoints. + /// The monitor configuration to write. private static MonitorConfiguration BuildMonitorConfiguration(GeneratorConfiguration configuration, IReadOnlyCollection discovered) { var result = configuration.GeneratedMonitorConfig.CloneWithoutTargets(); @@ -114,6 +151,12 @@ namespace BizTalkMonitorConfigGenerator return result; } + /// + /// Resolves a configured path against the application base directory. + /// + /// The application base directory. + /// The configured relative or absolute path. + /// The resolved path. private static string ResolvePath(string baseDirectory, string configuredPath) { if (Path.IsPathRooted(configuredPath)) @@ -123,5 +166,103 @@ namespace BizTalkMonitorConfigGenerator return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath)); } + + /// + /// Writes the initial console banner before configuration and file logging are available. + /// + /// The program title. + /// The resolved configuration path. + private static void WriteConsoleBanner(string title, string configPath) + { + TryWriteConsoleLine(Console.Out, "============================================================"); + TryWriteConsoleLine(Console.Out, title); + TryWriteConsoleLine(Console.Out, "Server: " + Environment.MachineName); + TryWriteConsoleLine(Console.Out, "Start: " + DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz")); + TryWriteConsoleLine(Console.Out, "Config: " + (configPath ?? "(nicht aufgeloest)")); + TryWriteConsoleLine(Console.Out, "============================================================"); + } + + /// + /// Writes a console information message. + /// + /// The message to write. + private static void WriteConsoleInfo(string message) + { + TryWriteConsoleLine(Console.Out, "[INFO] " + message); + } + + /// + /// Writes a console warning message. + /// + /// The message to write. + private static void WriteConsoleWarn(string message) + { + TryWriteConsoleLine(Console.Out, "[WARN] " + message); + } + + /// + /// Writes a fatal console error with actionable context. + /// + /// The user-facing failure message. + /// The configuration path used for the run. + /// The exception that ended the run. + private static void WriteConsoleFatal(string message, string configPath, Exception exception) + { + TryWriteConsoleLine(Console.Error, "[ERROR] " + message); + TryWriteConsoleLine(Console.Error, "[ERROR] Konfiguration: " + (configPath ?? "(nicht aufgeloest)")); + TryWriteConsoleLine(Console.Error, "[ERROR] Ursache: " + exception.GetType().Name + ": " + exception.Message); + TryWriteConsoleLine(Console.Error, "[ERROR] Details: " + exception); + } + + /// + /// Writes one line to a console writer and ignores secondary console failures. + /// + /// The console writer. + /// The line to write. + private static void TryWriteConsoleLine(TextWriter writer, string message) + { + try + { + writer.WriteLine(message); + } + catch + { + } + } + + /// + /// Writes a fallback fatal log when configured logging could not be initialized. + /// + /// The application base directory. + /// The configuration path used for the run. + /// The exception that ended the run. + private static void TryWriteFallbackFailureLog(string baseDirectory, string configPath, Exception exception) + { + try + { + var directory = ResolvePath(baseDirectory ?? AppDomain.CurrentDomain.BaseDirectory, "logs"); + var fallbackLogger = new GeneratorFileLogger(directory, "biztalk-monitor-config-generator-startup", 5, "BizTalkMonitorConfigGenerator", "Startup"); + fallbackLogger.Error("Generator konnte nicht gestartet werden. Konfiguration=" + (configPath ?? "(nicht aufgeloest)") + ". Fehler=" + exception.GetType().Name + ": " + exception.Message); + fallbackLogger.Error("Details: " + exception); + WriteConsoleInfo("Fallback-Log geschrieben: " + BuildLogFilePreview(directory, "biztalk-monitor-config-generator-startup")); + } + catch (Exception fallbackEx) + { + TryWriteConsoleLine(Console.Error, "[ERROR] Fallback-Log konnte nicht geschrieben werden: " + fallbackEx.GetType().Name + ": " + fallbackEx.Message); + } + } + + /// + /// Builds the expected daily log file path for console diagnostics. + /// + /// The log directory. + /// The rolling file prefix. + /// The expected log file path for the current local date. + private static string BuildLogFilePreview(string directory, string filePrefix) + { + var safeDirectory = string.IsNullOrWhiteSpace(directory) ? AppDomain.CurrentDomain.BaseDirectory : directory; + var safePrefix = string.IsNullOrWhiteSpace(filePrefix) ? "biztalk-monitor-config-generator" : filePrefix; + return Path.Combine(safeDirectory, safePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log"); + } } } diff --git a/src/BizTalkMonitorConfigGenerator/Properties/AssemblyInfo.cs b/src/BizTalkMonitorConfigGenerator/Properties/AssemblyInfo.cs index 9b435e3..f0fbd06 100644 --- a/src/BizTalkMonitorConfigGenerator/Properties/AssemblyInfo.cs +++ b/src/BizTalkMonitorConfigGenerator/Properties/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Reflection; using System.Runtime.InteropServices; +// Assembly metadata for the BizTalkMonitorConfigGenerator console application. [assembly: AssemblyTitle("BizTalkMonitorConfigGenerator")] [assembly: AssemblyDescription("Generates HostAvailabilityMonitor JSON config from active BizTalk send ports and receive locations.")] [assembly: AssemblyConfiguration("")] diff --git a/src/BizTalkMonitorConfigGenerator/Serialization/JsonFileWriter.cs b/src/BizTalkMonitorConfigGenerator/Serialization/JsonFileWriter.cs index 191ed9d..0d3bb82 100644 --- a/src/BizTalkMonitorConfigGenerator/Serialization/JsonFileWriter.cs +++ b/src/BizTalkMonitorConfigGenerator/Serialization/JsonFileWriter.cs @@ -5,8 +5,17 @@ using System.Web.Script.Serialization; namespace BizTalkMonitorConfigGenerator.Serialization { + /// + /// Writes generator output JSON files with stable formatting and atomic replacement. + /// public static class JsonFileWriter { + /// + /// Serializes an object as pretty JSON and replaces the target file atomically. + /// + /// The target JSON file path. + /// The value to serialize. + /// True to replace an existing file; otherwise false. public static void WritePrettyJsonAtomic(string path, object value, bool overwriteExisting) { if (string.IsNullOrWhiteSpace(path)) @@ -44,6 +53,11 @@ namespace BizTalkMonitorConfigGenerator.Serialization File.Move(tempPath, fullPath); } + /// + /// Formats compact JavaScriptSerializer JSON with indentation. + /// + /// The compact JSON string to format. + /// The formatted JSON string. private static string PrettyPrint(string json) { if (string.IsNullOrWhiteSpace(json)) @@ -126,6 +140,11 @@ namespace BizTalkMonitorConfigGenerator.Serialization return sb.ToString(); } + /// + /// Appends indentation spaces for the current JSON nesting level. + /// + /// The output string builder. + /// The indentation level. private static void AppendIndent(StringBuilder sb, int indent) { for (var i = 0; i < indent; i++) diff --git a/src/HostAvailabilityMonitor/Configuration/HipVirtualMachineConfiguration.cs b/src/HostAvailabilityMonitor/Configuration/HipVirtualMachineConfiguration.cs index 458c92c..60ad280 100644 --- a/src/HostAvailabilityMonitor/Configuration/HipVirtualMachineConfiguration.cs +++ b/src/HostAvailabilityMonitor/Configuration/HipVirtualMachineConfiguration.cs @@ -5,10 +5,21 @@ using System.Web.Script.Serialization; namespace HostAvailabilityMonitor.Configuration { + /// + /// Represents the external HIP virtual machine configuration file. + /// public sealed class HipVirtualMachineConfiguration { + /// + /// Gets or sets the configured virtual machines. + /// public List VirtualMachines { get; set; } = new List(); + /// + /// Loads and deserializes a HIP virtual machine configuration file. + /// + /// The JSON file path to load. + /// The normalized HIP virtual machine configuration. public static HipVirtualMachineConfiguration Load(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -34,12 +45,34 @@ namespace HostAvailabilityMonitor.Configuration } } + /// + /// Describes one HIP virtual machine and the checks that should be generated for it. + /// public sealed class HipVirtualMachine { + /// + /// Gets or sets the logical group used for reporting. + /// public string Group { get; set; } = string.Empty; + + /// + /// Gets or sets the virtual machine display name. + /// public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the host name or IP address to probe. + /// public string Address { get; set; } = string.Empty; + + /// + /// Gets or sets an optional description for mail and summary output. + /// public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether monitor targets should be generated for this VM. + /// public bool Enabled { get; set; } = true; } } diff --git a/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs b/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs index 37e87fc..45f9551 100644 --- a/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs +++ b/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs @@ -6,17 +6,56 @@ using System.Web.Script.Serialization; namespace HostAvailabilityMonitor.Configuration { + /// + /// Root configuration for the host availability monitor. + /// public sealed class MonitorConfiguration { + /// + /// Gets or sets the monitor name shown in logs, event log entries and email. + /// public string ApplicationName { get; set; } = "HostAvailabilityMonitor"; + + /// + /// Gets or sets the environment label shown in logs, event log entries and email. + /// public string EnvironmentName { get; set; } = "Unspecified Environment"; + + /// + /// Gets or sets runtime behavior such as concurrency and state persistence. + /// public RuntimeOptions Runtime { get; set; } = new RuntimeOptions(); + + /// + /// Gets or sets rolling file log settings. + /// public LoggingOptions Logging { get; set; } = new LoggingOptions(); + + /// + /// Gets or sets Windows Application Event Log settings. + /// public EventLogOptions EventLog { get; set; } = new EventLogOptions(); + + /// + /// Gets or sets SMTP and notification settings. + /// public EmailOptions Email { get; set; } = new EmailOptions(); + + /// + /// Gets or sets optional static HIP virtual machine monitoring settings. + /// public HipVirtualMachineOptions HipVirtualMachines { get; set; } = new HipVirtualMachineOptions(); + + /// + /// Gets or sets the endpoint targets to probe. + /// public List Targets { get; set; } = new List(); + /// + /// Loads and normalizes monitor configuration from a JSON file. + /// + /// The JSON configuration path. + /// The normalized monitor configuration. public static MonitorConfiguration Load(string path) { if (!File.Exists(path)) @@ -84,6 +123,9 @@ namespace HostAvailabilityMonitor.Configuration return config; } + /// + /// Validates monitor settings and applies safe defaults where possible. + /// public void Validate() { Targets = Targets ?? new List(); @@ -177,6 +219,12 @@ namespace HostAvailabilityMonitor.Configuration } } } + + /// + /// Removes null, empty and duplicate recipient entries. + /// + /// The recipient list from configuration. + /// A normalized recipient list. private static List SanitizeRecipientList(List values) { if (values == null) @@ -191,6 +239,11 @@ namespace HostAvailabilityMonitor.Configuration .ToList(); } + /// + /// Counts all configured email recipients across To, Cc and Bcc. + /// + /// The email options to inspect. + /// The number of configured recipients. private static int GetConfiguredRecipientCount(EmailOptions email) { if (email == null) @@ -204,93 +257,330 @@ namespace HostAvailabilityMonitor.Configuration } } + /// + /// Defines monitor runtime behavior. + /// public sealed class RuntimeOptions { + /// + /// Gets or sets the maximum number of concurrent endpoint probes. + /// public int MaxConcurrency { get; set; } = 4; + + /// + /// Gets or sets whether UNC checks should validate path access after the SMB port check. + /// public bool DefaultValidateUncPathAccess { get; set; } = false; + + /// + /// Gets or sets whether endpoint failures should produce a non-zero process exit code. + /// public bool NonZeroExitCodeOnAnyFailure { get; set; } = false; + + /// + /// Gets or sets whether execution errors should produce a non-zero process exit code. + /// public bool NonZeroExitCodeOnExecutionError { get; set; } = true; + + /// + /// Gets or sets the state directory path. + /// public string StateDirectory { get; set; } = "state"; } + /// + /// Defines rolling file log settings. + /// public sealed class LoggingOptions { + /// + /// Gets or sets the directory where monitor log files are written. + /// public string LogDirectory { get; set; } = "logs"; + + /// + /// Gets or sets the daily log file prefix. + /// public string FilePrefix { get; set; } = "host-availability-monitor"; + + /// + /// Gets or sets the number of daily log files to retain. + /// public int RetentionDays { get; set; } = 5; } + /// + /// Defines Windows Event Log behavior. + /// public sealed class EventLogOptions { + /// + /// Gets or sets a value indicating whether Event Log writes are enabled. + /// public bool Enabled { get; set; } = true; + + /// + /// Gets or sets the Windows Event Log name. + /// public string LogName { get; set; } = "Application"; + + /// + /// Gets or sets the Event Log source name. + /// public string Source { get; set; } = "HostAvailabilityMonitor"; + + /// + /// Gets or sets whether the application should try to create the Event Log source. + /// public bool CreateSourceIfMissing { get; set; } = false; + + /// + /// Gets or sets whether successful endpoint checks should be written to Event Log. + /// public bool WriteSuccessEntries { get; set; } = false; + + /// + /// Gets or sets whether the run summary should be written to Event Log. + /// public bool WriteSummaryEntries { get; set; } = true; } + /// + /// Defines SMTP and notification behavior. + /// public sealed class EmailOptions { + /// + /// Gets or sets a value indicating whether email delivery is enabled. + /// public bool Enabled { get; set; } = false; + + /// + /// Gets or sets the SMTP server host name. + /// public string SmtpHost { get; set; } = string.Empty; + + /// + /// Gets or sets the SMTP server port. + /// public int SmtpPort { get; set; } = 25; + + /// + /// Gets or sets whether SMTP should use SSL/TLS. + /// public bool UseSsl { get; set; } = false; + + /// + /// Gets or sets whether default Windows credentials should be used for SMTP. + /// public bool UseDefaultCredentials { get; set; } = false; + + /// + /// Gets or sets the SMTP user name when default credentials are disabled. + /// public string Username { get; set; } = string.Empty; + + /// + /// Gets or sets the SMTP password when default credentials are disabled. + /// public string Password { get; set; } = string.Empty; + + /// + /// Gets or sets the sender email address. + /// public string From { get; set; } = string.Empty; + + /// + /// Gets or sets primary email recipients. + /// public List To { get; set; } = new List(); + + /// + /// Gets or sets CC email recipients. + /// public List Cc { get; set; } = new List(); + + /// + /// Gets or sets BCC email recipients. + /// public List Bcc { get; set; } = new List(); + + /// + /// Gets or sets the subject prefix used for all notification emails. + /// public string SubjectPrefix { get; set; } = "[HostAvailabilityMonitor]"; + + /// + /// Gets or sets whether failure notification emails should be sent. + /// public bool SendOnFailure { get; set; } = true; + + /// + /// Gets or sets whether recovery notification emails should be sent. + /// public bool SendOnRecovery { get; set; } = true; + + /// + /// Gets or sets whether the once-per-day summary email should be sent. + /// public bool SendDailySummary { get; set; } = false; + + /// + /// Gets or sets the local server hour at or after which the daily summary may be sent. + /// public int DailySummaryHourLocal { get; set; } = 14; + + /// + /// Gets or sets the local server minute at or after which the daily summary may be sent. + /// public int DailySummaryMinuteLocal { get; set; } = 0; + + /// + /// Gets or sets whether failure and recovery emails should only be sent on state changes. + /// public bool OnlyOnStateChange { get; set; } = true; + + /// + /// Gets or sets the cooldown in minutes for repeated failure emails. + /// public int CooldownMinutes { get; set; } = 0; + + /// + /// Gets or sets the SMTP timeout in seconds. + /// public int TimeoutSeconds { get; set; } = 30; } + /// + /// Defines optional static HIP virtual machine probe generation. + /// public sealed class HipVirtualMachineOptions { + /// + /// Gets or sets whether HIP virtual machine checks are enabled. + /// public bool Enabled { get; set; } = false; + + /// + /// Gets or sets the HIP VM JSON configuration path. + /// public string ConfigPath { get; set; } = "hip-vms.json"; + + /// + /// Gets or sets the application name assigned to generated HIP VM targets. + /// public string ApplicationName { get; set; } = "HIP Virtual Machines"; + + /// + /// Gets or sets the timeout in seconds for generated HIP VM targets. + /// public int TimeoutSeconds { get; set; } = 5; + + /// + /// Gets or sets the RDP port used for generated RDP checks. + /// public int RdpPort { get; set; } = 3389; + + /// + /// Gets or sets whether ICMP network checks should be generated. + /// public bool CheckNetwork { get; set; } = true; + + /// + /// Gets or sets whether RDP TCP checks should be generated. + /// public bool CheckRdp { get; set; } = true; } + /// + /// Defines one endpoint target that can be probed by the monitor. + /// public sealed class TargetOptions { + /// + /// Gets or sets the configured target name. + /// public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the logical application owning the target. + /// public string ApplicationName { get; set; } = string.Empty; + + /// + /// Gets or sets the endpoint address to probe. + /// public string Endpoint { get; set; } = string.Empty; + + /// + /// Gets or sets the probe timeout in seconds. + /// public int TimeoutSeconds { get; set; } = 10; + + /// + /// Gets or sets an optional explicit port override. + /// public int? Port { get; set; } + + /// + /// Gets or sets an optional UNC path access validation override. + /// public bool? ValidateUncPathAccess { get; set; } + + /// + /// Gets or sets the HTTP method used for HTTP and HTTPS probes. + /// public string HttpMethod { get; set; } + + /// + /// Gets or sets the source that created the target. + /// public string Source { get; set; } + + /// + /// Gets or sets an optional group label for reporting. + /// public string Group { get; set; } + + /// + /// Gets or sets an optional target description. + /// public string Description { get; set; } + + /// + /// Gets or sets an optional machine name for generated VM targets. + /// public string MachineName { get; set; } + + /// + /// Gets or sets an optional check name for generated targets. + /// public string CheckName { get; set; } + + /// + /// Gets or sets a value indicating whether the target should be probed. + /// public bool Enabled { get; set; } = true; + /// + /// Gets the display name used in logs, status and notification output. + /// public string DisplayName { get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name; } } + /// + /// Gets the normalized application name used for grouping. + /// public string EffectiveApplicationName { get { return string.IsNullOrWhiteSpace(ApplicationName) ? "Unassigned Application" : ApplicationName.Trim(); } } + /// + /// Gets the stable state key used to persist target state across runs. + /// public string StateKey { get @@ -307,6 +597,11 @@ namespace HostAvailabilityMonitor.Configuration } } + /// + /// Trims a string for state-key generation. + /// + /// The configured value to normalize. + /// The trimmed value, or an empty string for null and whitespace. private static string Normalize(string value) { return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); diff --git a/src/HostAvailabilityMonitor/Logging/FileLogger.cs b/src/HostAvailabilityMonitor/Logging/FileLogger.cs index a78df20..8102aa0 100644 --- a/src/HostAvailabilityMonitor/Logging/FileLogger.cs +++ b/src/HostAvailabilityMonitor/Logging/FileLogger.cs @@ -7,15 +7,49 @@ using HostAvailabilityMonitor.Monitoring; namespace HostAvailabilityMonitor.Logging { + /// + /// Writes rolling monitor log files with daily file names and probe-specific fields. + /// public sealed class FileLogger { + /// + /// Synchronizes writes within the current process. + /// private readonly object _sync = new object(); + + /// + /// Directory where log files are stored. + /// private readonly string _directory; + + /// + /// Prefix used for daily log file names. + /// private readonly string _filePrefix; + + /// + /// Number of daily log files to retain. + /// private readonly int _retentionDays; + + /// + /// Monitor name included in structured probe log fields. + /// private readonly string _monitorName; + + /// + /// Environment name included in structured probe log fields. + /// private readonly string _environmentName; + /// + /// Initializes a new rolling file logger. + /// + /// The target log directory. + /// The daily log file prefix. + /// The number of daily log files to retain. + /// The monitor name written to probe log lines. + /// The environment name written to probe log lines. public FileLogger(string directory, string filePrefix, int retentionDays, string monitorName, string environmentName) { _directory = directory; @@ -27,6 +61,9 @@ namespace HostAvailabilityMonitor.Logging Directory.CreateDirectory(_directory); } + /// + /// Deletes old rolling log files according to the retention setting. + /// public void CleanupOldFiles() { if (_retentionDays <= 0) @@ -63,21 +100,37 @@ namespace HostAvailabilityMonitor.Logging } } + /// + /// Writes an informational log entry. + /// + /// The message to write. public void Info(string message) { Write("INFO", message); } + /// + /// Writes a warning log entry. + /// + /// The message to write. public void Warn(string message) { Write("WARN", message); } + /// + /// Writes an error log entry. + /// + /// The message to write. public void Error(string message) { Write("ERROR", message); } + /// + /// Writes a structured probe result entry. + /// + /// The probe result to write. public void Probe(ProbeResult result) { var level = result.IsSkipped ? "SKIP" : (result.IsSuccess ? "SUCCESS" : "FAIL"); @@ -137,6 +190,11 @@ namespace HostAvailabilityMonitor.Logging Write(level, string.Join(" | ", parts)); } + /// + /// Formats and appends a log line. + /// + /// The log level text. + /// The formatted message payload. private void Write(string level, string message) { var line = string.Format( @@ -155,6 +213,11 @@ namespace HostAvailabilityMonitor.Logging } } + /// + /// Appends a line to a file and retries transient file access failures. + /// + /// The log file path. + /// The full line to append. private void AppendWithRetries(string path, string line) { Exception lastException = null; @@ -184,12 +247,22 @@ namespace HostAvailabilityMonitor.Logging } } + /// + /// Builds the daily log file path for the current local date. + /// + /// The current log file path. private string GetCurrentLogFilePath() { var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"; return Path.Combine(_directory, fileName); } + /// + /// Extracts the date encoded in a rolling log file name. + /// + /// The log file path to inspect. + /// The parsed file date when parsing succeeds. + /// True when the date could be parsed; otherwise false. private bool TryGetDateFromFileName(string path, out DateTime date) { date = DateTime.MinValue; @@ -204,6 +277,11 @@ namespace HostAvailabilityMonitor.Logging return DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date); } + /// + /// Removes line breaks from values before they are written to one-line logs. + /// + /// The value to escape. + /// The log-safe value. private static string Escape(string value) { return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " "); diff --git a/src/HostAvailabilityMonitor/Logging/WindowsEventLogger.cs b/src/HostAvailabilityMonitor/Logging/WindowsEventLogger.cs index 0e8400e..4658982 100644 --- a/src/HostAvailabilityMonitor/Logging/WindowsEventLogger.cs +++ b/src/HostAvailabilityMonitor/Logging/WindowsEventLogger.cs @@ -3,24 +3,46 @@ using System.Diagnostics; using System.Security; using HostAvailabilityMonitor.Configuration; -namespace HostAvailabilityMonitor.Logging -{ - public sealed class WindowsEventLogger - { - private readonly EventLogOptions _options; - private readonly FileLogger _fallbackLogger; - private bool _disabled; - - public WindowsEventLogger(EventLogOptions options, FileLogger fallbackLogger) - { - _options = options; +namespace HostAvailabilityMonitor.Logging +{ + /// + /// Best-effort Windows Event Log writer for monitor lifecycle, probe and summary events. + /// + public sealed class WindowsEventLogger + { + /// + /// Event Log settings from monitor configuration. + /// + private readonly EventLogOptions _options; + + /// + /// File logger used when Event Log writes fail. + /// + private readonly FileLogger _fallbackLogger; + + /// + /// Indicates that Event Log writing has been disabled for this process. + /// + private bool _disabled; + + /// + /// Initializes a new Windows Event Log writer. + /// + /// The Event Log configuration. + /// The fallback file logger. + public WindowsEventLogger(EventLogOptions options, FileLogger fallbackLogger) + { + _options = options; _fallbackLogger = fallbackLogger; - _disabled = !_options.Enabled; - } - - public void TryInitialize() - { - if (_disabled) + _disabled = !_options.Enabled; + } + + /// + /// Optionally creates the configured Event Log source. + /// + public void TryInitialize() + { + if (_disabled) { return; } @@ -46,35 +68,60 @@ namespace HostAvailabilityMonitor.Logging catch (Exception ex) { _fallbackLogger.Warn("EventLog-Initialisierung konnte die Quelle nicht anlegen. " + ex.GetType().Name + ": " + ex.Message); - } - } - - public void Info(string message, int eventId) - { - Write(message, EventLogEntryType.Information, eventId, forceWrite: true); - } - - public void InfoIfSuccessEnabled(string message, int eventId) - { - if (_options.WriteSuccessEntries) + } + } + + /// + /// Writes an informational event. + /// + /// The event message. + /// The event identifier. + public void Info(string message, int eventId) + { + Write(message, EventLogEntryType.Information, eventId, forceWrite: true); + } + + /// + /// Writes an informational event only when success entries are enabled. + /// + /// The event message. + /// The event identifier. + public void InfoIfSuccessEnabled(string message, int eventId) + { + if (_options.WriteSuccessEntries) { Write(message, EventLogEntryType.Information, eventId, forceWrite: true); - } - } - - public void Warning(string message, int eventId) - { - Write(message, EventLogEntryType.Warning, eventId, forceWrite: true); - } - - public void Error(string message, int eventId) - { - Write(message, EventLogEntryType.Error, eventId, forceWrite: true); - } - - public void Summary(string message, bool hasFailures) - { - if (!_options.WriteSummaryEntries) + } + } + + /// + /// Writes a warning event. + /// + /// The event message. + /// The event identifier. + public void Warning(string message, int eventId) + { + Write(message, EventLogEntryType.Warning, eventId, forceWrite: true); + } + + /// + /// Writes an error event. + /// + /// The event message. + /// The event identifier. + public void Error(string message, int eventId) + { + Write(message, EventLogEntryType.Error, eventId, forceWrite: true); + } + + /// + /// Writes a run summary event when summary entries are enabled. + /// + /// The summary message. + /// True when the summary should be written as a warning. + public void Summary(string message, bool hasFailures) + { + if (!_options.WriteSummaryEntries) { return; } @@ -83,12 +130,19 @@ namespace HostAvailabilityMonitor.Logging message, hasFailures ? EventLogEntryType.Warning : EventLogEntryType.Information, hasFailures ? 2100 : 1100, - forceWrite: true); - } - - private void Write(string message, EventLogEntryType entryType, int eventId, bool forceWrite) - { - if (_disabled || !forceWrite) + forceWrite: true); + } + + /// + /// Writes one Event Log entry and disables future Event Log writes on failure. + /// + /// The event message. + /// The Windows Event Log entry type. + /// The event identifier. + /// True to write the event when the logger is enabled. + private void Write(string message, EventLogEntryType entryType, int eventId, bool forceWrite) + { + if (_disabled || !forceWrite) { return; } @@ -108,12 +162,16 @@ namespace HostAvailabilityMonitor.Logging catch (Exception ex) { DisableAndFallback("EventLog-Schreiben fehlgeschlagen. " + ex.GetType().Name + ": " + ex.Message); - } - } - - private void DisableAndFallback(string reason) - { - if (_disabled) + } + } + + /// + /// Disables Event Log writes and records the reason in the fallback log. + /// + /// The reason Event Log writes were disabled. + private void DisableAndFallback(string reason) + { + if (_disabled) { return; } diff --git a/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs b/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs index 8d2d9b1..9be7c30 100644 --- a/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs +++ b/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs @@ -10,17 +10,37 @@ using HostAvailabilityMonitor.Configuration; namespace HostAvailabilityMonitor.Monitoring { + /// + /// Executes endpoint probes for all supported target kinds. + /// public sealed class EndpointProbeService { + /// + /// Shared HTTP client with per-request timeout handling through cancellation tokens. + /// private static readonly HttpClient HttpClient = CreateHttpClient(); + + /// + /// Runtime options that influence probe behavior. + /// private readonly RuntimeOptions _runtimeOptions; + /// + /// Initializes a new endpoint probe service. + /// + /// Runtime options used for defaults such as UNC validation. public EndpointProbeService(RuntimeOptions runtimeOptions) { _runtimeOptions = runtimeOptions ?? new RuntimeOptions(); ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; } + /// + /// Probes one target by resolving its endpoint syntax to the appropriate check implementation. + /// + /// The target configuration to probe. + /// A token that can cancel the probe. + /// The probe result for the target. public async Task ProbeAsync(TargetOptions target, CancellationToken cancellationToken) { var startedAt = DateTimeOffset.Now; @@ -88,6 +108,14 @@ namespace HostAvailabilityMonitor.Monitoring } } + /// + /// Probes a plain host value by using TCP when a port is configured, otherwise ICMP. + /// + /// The target configuration. + /// The host name or address. + /// The local start time of the probe. + /// A token that can cancel the probe. + /// The probe result. private async Task CheckPlainHostAsync(TargetOptions target, string host, DateTimeOffset startedAt, CancellationToken cancellationToken) { if (target.Port.HasValue) @@ -98,6 +126,14 @@ namespace HostAvailabilityMonitor.Monitoring return await CheckIcmpAsync(target, host, startedAt, host, cancellationToken).ConfigureAwait(false); } + /// + /// Probes a file URI by routing UNC-like URIs to UNC checks and local paths to file checks. + /// + /// The target configuration. + /// The file URI to probe. + /// The local start time of the probe. + /// A token that can cancel the probe. + /// The probe result. private async Task CheckFileUriAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, CancellationToken cancellationToken) { if (uri.IsUnc || !string.IsNullOrWhiteSpace(uri.Host)) @@ -115,6 +151,14 @@ namespace HostAvailabilityMonitor.Monitoring return await CheckLocalPathAsync(target, localPath, startedAt, cancellationToken).ConfigureAwait(false); } + /// + /// Checks whether a local file or directory exists within the configured timeout. + /// + /// The target configuration. + /// The local file system path. + /// The local start time of the probe. + /// A token that can cancel the probe. + /// The probe result. private async Task CheckLocalPathAsync(TargetOptions target, string path, DateTimeOffset startedAt, CancellationToken cancellationToken) { try @@ -139,6 +183,14 @@ namespace HostAvailabilityMonitor.Monitoring } } + /// + /// Checks a UNC target by validating SMB reachability and optionally path access. + /// + /// The target configuration. + /// The UNC endpoint path. + /// The local start time of the probe. + /// A token that can cancel the probe. + /// The probe result. private async Task CheckUncAsync(TargetOptions target, string endpoint, DateTimeOffset startedAt, CancellationToken cancellationToken) { var host = ExtractUncServer(endpoint); @@ -178,6 +230,15 @@ namespace HostAvailabilityMonitor.Monitoring } } + /// + /// Checks an HTTP or HTTPS endpoint and falls back from HEAD to GET when required. + /// + /// The target configuration. + /// The HTTP or HTTPS URI. + /// The local start time of the probe. + /// The resolved HTTP target kind. + /// A token that can cancel the probe. + /// The probe result. private async Task CheckHttpAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken) { var methodText = string.IsNullOrWhiteSpace(target.HttpMethod) ? "HEAD" : target.HttpMethod.Trim().ToUpperInvariant(); @@ -220,6 +281,15 @@ namespace HostAvailabilityMonitor.Monitoring } } + /// + /// Retries an HTTP endpoint with GET after HEAD was rejected or failed. + /// + /// The target configuration. + /// The HTTP or HTTPS URI. + /// The local start time of the probe. + /// The resolved HTTP target kind. + /// A token that can cancel the probe. + /// The probe result. private async Task RetryHttpWithGetAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken) { using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) @@ -244,6 +314,17 @@ namespace HostAvailabilityMonitor.Monitoring } } + /// + /// Checks TCP connectivity to a host and port. + /// + /// The target configuration. + /// The host name or address. + /// The target TCP port. + /// The local start time of the probe. + /// The resolved target kind. + /// The endpoint string reported in the result. + /// A token that can cancel the probe. + /// The probe result. private async Task CheckTcpAsync(TargetOptions target, string host, int? port, DateTimeOffset startedAt, TargetKind kind, string endpoint, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(host)) @@ -299,6 +380,15 @@ namespace HostAvailabilityMonitor.Monitoring } } + /// + /// Checks ICMP reachability with the configured timeout. + /// + /// The target configuration. + /// The host name or address. + /// The local start time of the probe. + /// The endpoint string reported in the result. + /// A token that can cancel the probe. + /// The probe result. private async Task CheckIcmpAsync(TargetOptions target, string host, DateTimeOffset startedAt, string endpoint, CancellationToken cancellationToken) { using (var ping = new Ping()) @@ -332,6 +422,12 @@ namespace HostAvailabilityMonitor.Monitoring } } + /// + /// Resolves the effective port for a URI. + /// + /// The URI being probed. + /// The default port for the target kind. + /// The explicit URI port, the default port, or null. private static int? ResolvePort(Uri uri, int? defaultPort) { if (uri == null) @@ -347,6 +443,11 @@ namespace HostAvailabilityMonitor.Monitoring return defaultPort; } + /// + /// Extracts the server component from a UNC path. + /// + /// The UNC path. + /// The server component, or an empty string. private static string ExtractUncServer(string endpoint) { if (string.IsNullOrWhiteSpace(endpoint)) @@ -359,6 +460,10 @@ namespace HostAvailabilityMonitor.Monitoring return firstSeparator >= 0 ? trimmed.Substring(0, firstSeparator) : trimmed; } + /// + /// Creates the shared HTTP client used by all HTTP probes. + /// + /// A configured HTTP client. private static HttpClient CreateHttpClient() { var handler = new HttpClientHandler(); @@ -367,21 +472,70 @@ namespace HostAvailabilityMonitor.Monitoring return client; } + /// + /// Creates a successful probe result. + /// + /// The target configuration. + /// The resolved target kind. + /// The endpoint reported in the result. + /// The local start time of the probe. + /// The resolved host. + /// The resolved port. + /// The result detail text. + /// The HTTP status code, when applicable. + /// The successful probe result. private static ProbeResult Success(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string host, int? port, string detail, int? httpStatusCode) { return BuildResult(target, kind, endpoint, startedAt, true, false, "Reachable", detail, host, port, httpStatusCode, null); } + /// + /// Creates a failed probe result. + /// + /// The target configuration. + /// The resolved target kind. + /// The endpoint reported in the result. + /// The local start time of the probe. + /// The result detail text. + /// The exception that caused the failure, if any. + /// The resolved host. + /// The resolved port. + /// The failed probe result. private static ProbeResult Fail(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string detail, Exception ex, string host, int? port) { return BuildResult(target, kind, endpoint, startedAt, false, false, "Unreachable", detail, host, port, null, ex == null ? null : ex.GetType().Name); } + /// + /// Creates a skipped probe result. + /// + /// The target configuration. + /// The resolved target kind. + /// The endpoint reported in the result. + /// The local start time of the probe. + /// The skip reason. + /// The skipped probe result. private static ProbeResult Skip(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string detail) { return BuildResult(target, kind, endpoint, startedAt, false, true, "Skipped", detail, null, null, null, null); } + /// + /// Creates the common probe result object. + /// + /// The target configuration. + /// The resolved target kind. + /// The endpoint reported in the result. + /// The local start time of the probe. + /// True when the probe succeeded. + /// True when the target was intentionally skipped. + /// The high-level status text. + /// The detailed result text. + /// The resolved host. + /// The resolved port. + /// The HTTP status code, when applicable. + /// The exception type, when applicable. + /// The populated probe result. private static ProbeResult BuildResult(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, bool isSuccess, bool isSkipped, string statusText, string detail, string host, int? port, int? httpStatusCode, string errorType) { return new ProbeResult @@ -409,6 +563,14 @@ namespace HostAvailabilityMonitor.Monitoring }; } + /// + /// Creates a skipped result for endpoints that should not be network-probed. + /// + /// The target configuration. + /// The endpoint value to inspect. + /// The local start time of the probe. + /// The skipped result when a skip rule matches. + /// True when a skip result was created; otherwise false. private static bool TryCreateSkipResult(TargetOptions target, string endpoint, DateTimeOffset startedAt, out ProbeResult result) { result = null; @@ -422,6 +584,12 @@ namespace HostAvailabilityMonitor.Monitoring return false; } + /// + /// Detects BizTalk SMTP adapter targets that contain recipient lists instead of network endpoints. + /// + /// The target configuration. + /// The endpoint value to inspect. + /// True when the target looks like an SMTP recipient list. private static bool LooksLikeSmtpAdapterTarget(TargetOptions target, string endpoint) { var name = target == null ? string.Empty : (target.Name ?? string.Empty); @@ -435,6 +603,11 @@ namespace HostAvailabilityMonitor.Monitoring return LooksLikeEmailAddressList(value); } + /// + /// Detects semicolon-separated email recipient lists that are not monitorable endpoints. + /// + /// The endpoint value to inspect. + /// True when the value looks like an email address list. private static bool LooksLikeEmailAddressList(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -482,6 +655,14 @@ namespace HostAvailabilityMonitor.Monitoring return true; } + /// + /// Awaits a task until it completes or the timeout expires. + /// + /// The task result type. + /// The task to await. + /// The maximum wait time. + /// A token that can cancel the wait. + /// A completion result containing the task result when it completed in time. private static async Task> CompleteWithinAsync(Task task, TimeSpan timeout, CancellationToken cancellationToken) { var delayTask = Task.Delay(timeout, cancellationToken); @@ -494,6 +675,13 @@ namespace HostAvailabilityMonitor.Monitoring return new CompletionResult(true, await task.ConfigureAwait(false)); } + /// + /// Awaits a non-generic task until it completes or the timeout expires. + /// + /// The task to await. + /// The maximum wait time. + /// A token that can cancel the wait. + /// A completion result indicating whether the task completed in time. private static async Task> CompleteWithinAsync(Task task, TimeSpan timeout, CancellationToken cancellationToken) { var delayTask = Task.Delay(timeout, cancellationToken); @@ -507,15 +695,31 @@ namespace HostAvailabilityMonitor.Monitoring return new CompletionResult(true, true); } + /// + /// Holds the outcome of a timeout-wrapped task. + /// + /// The wrapped task result type. private struct CompletionResult { + /// + /// Initializes a completion result. + /// + /// True when the task completed before the timeout. + /// The task result or a default value. public CompletionResult(bool completed, T result) { Completed = completed; Result = result; } + /// + /// Gets a value indicating whether the wrapped task completed in time. + /// public bool Completed { get; private set; } + + /// + /// Gets the wrapped task result. + /// public T Result { get; private set; } } } diff --git a/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs b/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs index c5fc745..c0c602d 100644 --- a/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs +++ b/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs @@ -2,43 +2,175 @@ using System; namespace HostAvailabilityMonitor.Monitoring { + /// + /// Identifies the endpoint probe strategy used for a target. + /// public enum TargetKind { + /// + /// The target type could not be resolved. + /// Unknown = 0, + + /// + /// A UNC or SMB path target. + /// Unc = 1, + + /// + /// An HTTP endpoint target. + /// Http = 2, + + /// + /// An HTTPS endpoint target. + /// Https = 3, + + /// + /// An SFTP endpoint target. + /// Sftp = 4, + + /// + /// A generic TCP endpoint target. + /// Tcp = 5, + + /// + /// An ICMP ping target. + /// Icmp = 6, + + /// + /// A plain host target resolved through target options. + /// Host = 7, + + /// + /// An FTP endpoint target. + /// Ftp = 8, + + /// + /// A local or file URI target. + /// File = 9, + + /// + /// An RDP TCP endpoint target. + /// Rdp = 10 } + /// + /// Contains the result of probing one configured target. + /// public sealed class ProbeResult { + /// + /// Gets or sets the stable state key for the target. + /// public string StateKey { get; set; } + + /// + /// Gets or sets the display name of the target. + /// public string Name { get; set; } + + /// + /// Gets or sets the logical application name. + /// public string ApplicationName { get; set; } + + /// + /// Gets or sets the endpoint that was probed. + /// public string Endpoint { get; set; } + + /// + /// Gets or sets the resolved target kind. + /// public TargetKind Kind { get; set; } + + /// + /// Gets or sets a value indicating whether the probe succeeded. + /// public bool IsSuccess { get; set; } + + /// + /// Gets or sets a value indicating whether the target was intentionally skipped. + /// public bool IsSkipped { get; set; } + + /// + /// Gets or sets the high-level status text. + /// public string StatusText { get; set; } + + /// + /// Gets or sets the detailed probe result message. + /// public string Detail { get; set; } + + /// + /// Gets or sets the resolved host name or address. + /// public string Host { get; set; } + + /// + /// Gets or sets the resolved port, when applicable. + /// public int? Port { get; set; } + + /// + /// Gets or sets the source that created the target. + /// public string Source { get; set; } + + /// + /// Gets or sets the group label used in reports. + /// public string Group { get; set; } + + /// + /// Gets or sets the target description. + /// public string Description { get; set; } + + /// + /// Gets or sets the machine name for generated VM checks. + /// public string MachineName { get; set; } + + /// + /// Gets or sets the check name for generated checks. + /// public string CheckName { get; set; } + + /// + /// Gets or sets the HTTP status code returned by HTTP probes. + /// public int? HttpStatusCode { get; set; } + + /// + /// Gets or sets the exception type observed during failed probes. + /// public string ErrorType { get; set; } + + /// + /// Gets or sets the local start time of the probe. + /// public DateTimeOffset StartedAtLocal { get; set; } + + /// + /// Gets or sets the local finish time of the probe. + /// public DateTimeOffset FinishedAtLocal { get; set; } + + /// + /// Gets the elapsed probe duration. + /// public TimeSpan Duration { get { return FinishedAtLocal - StartedAtLocal; } diff --git a/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs b/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs index a249601..a0bab57 100644 --- a/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs +++ b/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs @@ -12,12 +12,32 @@ using HostAvailabilityMonitor.State; namespace HostAvailabilityMonitor.Notifications { + /// + /// Sends failure, recovery and daily summary emails for monitor results. + /// public sealed class EmailNotifier { + /// + /// SMTP and notification settings. + /// private readonly EmailOptions _options; + + /// + /// Monitor name written into subjects and message bodies. + /// private readonly string _monitorName; + + /// + /// Environment name written into subjects and message bodies. + /// private readonly string _environmentName; + /// + /// Initializes a new email notifier. + /// + /// SMTP and notification options. + /// The monitor name for outgoing messages. + /// The environment name for outgoing messages. public EmailNotifier(EmailOptions options, string monitorName, string environmentName) { _options = options ?? new EmailOptions(); @@ -25,11 +45,20 @@ namespace HostAvailabilityMonitor.Notifications _environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim(); } + /// + /// Gets a value indicating whether email delivery is enabled. + /// public bool IsEnabled { get { return _options.Enabled; } } + /// + /// Determines whether a probe result should trigger a failure or recovery notification. + /// + /// The current probe result. + /// The previous persisted state for the target. + /// True when a notification should be sent. public bool ShouldNotify(ProbeResult result, TargetState previousState) { if (!IsEnabled || result == null) @@ -72,6 +101,12 @@ namespace HostAvailabilityMonitor.Notifications return cooldownPassed; } + /// + /// Determines whether the daily summary should be sent on this run. + /// + /// The persisted monitor metadata. + /// The current local server time. + /// True when the daily summary should be sent. public bool ShouldSendDailySummary(MonitorStateMetadata metadata, DateTime nowLocal) { if (!IsEnabled || !_options.SendDailySummary) @@ -89,6 +124,14 @@ namespace HostAvailabilityMonitor.Notifications return metadata == null || !string.Equals(metadata.LastDailySummarySentLocalDate, localDate, StringComparison.Ordinal); } + /// + /// Sends one notification email for the supplied failures and recoveries. + /// + /// The failed probe results to include. + /// The recovered probe results to include. + /// The current target state dictionary. + /// A token that can cancel SMTP delivery. + /// A task that completes when delivery has finished. public async Task SendAsync( IReadOnlyCollection failures, IReadOnlyCollection recoveries, @@ -112,6 +155,12 @@ namespace HostAvailabilityMonitor.Notifications await SendMailAsync(subject, body, cancellationToken).ConfigureAwait(false); } + /// + /// Sends the once-per-day status summary email. + /// + /// The summary data to render. + /// A token that can cancel SMTP delivery. + /// A task that completes when delivery has finished. public async Task SendDailySummaryAsync(DailySummaryEmailData summary, CancellationToken cancellationToken) { if (!IsEnabled || summary == null) @@ -124,6 +173,13 @@ namespace HostAvailabilityMonitor.Notifications await SendMailAsync(subject, body, cancellationToken).ConfigureAwait(false); } + /// + /// Sends one SMTP email. + /// + /// The email subject. + /// The plain-text email body. + /// A token that can cancel SMTP delivery before sending starts. + /// A task that completes when SMTP delivery has finished. private async Task SendMailAsync(string subject, string body, CancellationToken cancellationToken) { using (var message = new MailMessage()) @@ -161,6 +217,11 @@ namespace HostAvailabilityMonitor.Notifications } } + /// + /// Adds normalized recipient addresses to a mail address collection. + /// + /// The destination mail address collection. + /// The configured recipient strings. private static void AddRecipients(MailAddressCollection collection, IEnumerable recipients) { if (collection == null || recipients == null) @@ -174,6 +235,12 @@ namespace HostAvailabilityMonitor.Notifications } } + /// + /// Detects a transition from previously down to currently up. + /// + /// The current probe result. + /// The previous persisted state for the target. + /// True when the result is a recovery transition. private static bool IsRecoveryTransition(ProbeResult result, TargetState previousState) { return result != null @@ -182,6 +249,12 @@ namespace HostAvailabilityMonitor.Notifications && !previousState.IsUp; } + /// + /// Builds the subject for failure and recovery notification emails. + /// + /// The number of failures in the message. + /// The number of recoveries in the message. + /// The notification subject. private string BuildSubject(int failureCount, int recoveryCount) { var prefix = string.IsNullOrWhiteSpace(_options.SubjectPrefix) ? "[HostAvailabilityMonitor]" : _options.SubjectPrefix.Trim(); @@ -200,6 +273,11 @@ namespace HostAvailabilityMonitor.Notifications return string.Format("{0} {1} {2} Endpoint(s) wieder erreichbar", prefix, envPart, recoveryCount); } + /// + /// Builds the subject for a daily summary email. + /// + /// The summary data. + /// The daily summary subject. private string BuildDailySummarySubject(DailySummaryEmailData summary) { var prefix = string.IsNullOrWhiteSpace(_options.SubjectPrefix) ? "[HostAvailabilityMonitor]" : _options.SubjectPrefix.Trim(); @@ -207,6 +285,13 @@ namespace HostAvailabilityMonitor.Notifications return string.Format("{0} {1} Tagesstatus {2:yyyy-MM-dd}", prefix, envPart, summary.GeneratedAtLocal.LocalDateTime.Date); } + /// + /// Builds the plain-text body for failure and recovery notifications. + /// + /// The failed probe results to include. + /// The recovered probe results to include. + /// The current target state dictionary. + /// The email body. private string BuildBody(IReadOnlyCollection failures, IReadOnlyCollection recoveries, IDictionary currentState) { var sb = new StringBuilder(); @@ -239,6 +324,11 @@ namespace HostAvailabilityMonitor.Notifications return sb.ToString(); } + /// + /// Builds the plain-text body for a daily summary email. + /// + /// The summary data to render. + /// The email body. private string BuildDailySummaryBody(DailySummaryEmailData summary) { var sb = new StringBuilder(); @@ -281,6 +371,13 @@ namespace HostAvailabilityMonitor.Notifications return sb.ToString(); } + /// + /// Appends one probe result to an email body. + /// + /// The message body builder. + /// The result to append. + /// The current target state dictionary. + /// The transition text to show. private static void AppendResult(StringBuilder sb, ProbeResult result, IDictionary currentState, string transitionText) { sb.AppendLine("- Anwendung / Application: " + Safe(result.ApplicationName)); @@ -328,6 +425,11 @@ namespace HostAvailabilityMonitor.Notifications } } + /// + /// Appends a compact HIP VM network and RDP snapshot to the daily summary. + /// + /// The message body builder. + /// The current probe results. private static void AppendHipVirtualMachineSnapshot(StringBuilder sb, IReadOnlyCollection results) { if (results == null) @@ -371,6 +473,11 @@ namespace HostAvailabilityMonitor.Notifications } } + /// + /// Builds a grouping key for HIP VM snapshot rows. + /// + /// The probe result to key. + /// The grouping key. private static string BuildVmSnapshotKey(ProbeResult result) { return string.Join("|", new[] @@ -381,6 +488,11 @@ namespace HostAvailabilityMonitor.Notifications }); } + /// + /// Formats one probe result as a compact status string. + /// + /// The result to format. + /// A compact status string. private static string FormatCompactStatus(ProbeResult result) { if (result == null) @@ -396,6 +508,11 @@ namespace HostAvailabilityMonitor.Notifications return (result.IsSuccess ? "UP" : "DOWN") + " - " + Safe(result.Detail); } + /// + /// Returns the first non-empty value from a sequence. + /// + /// The values to inspect. + /// The first non-empty value, or an empty string. private static string FirstNonEmpty(IEnumerable values) { if (values == null) @@ -406,26 +523,85 @@ namespace HostAvailabilityMonitor.Notifications return values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)) ?? string.Empty; } + /// + /// Converts null or whitespace values into a readable placeholder. + /// + /// The value to normalize. + /// The original value or n/a. private static string Safe(string value) { return string.IsNullOrWhiteSpace(value) ? "n/a" : value; } } + /// + /// Contains all data required to render a daily status summary email. + /// public sealed class DailySummaryEmailData { + /// + /// Gets or sets the local timestamp when the summary is generated. + /// public DateTimeOffset GeneratedAtLocal { get; set; } + + /// + /// Gets or sets the local start timestamp of the summary window. + /// public DateTimeOffset WindowStartLocal { get; set; } + + /// + /// Gets or sets today's total probe count from the rolling log. + /// public int ProbesTodayTotal { get; set; } + + /// + /// Gets or sets today's successful probe count from the rolling log. + /// public int ProbesTodaySuccess { get; set; } + + /// + /// Gets or sets today's failed probe count from the rolling log. + /// public int ProbesTodayFailure { get; set; } + + /// + /// Gets or sets today's skipped probe count from the rolling log. + /// public int ProbesTodaySkipped { get; set; } + + /// + /// Gets or sets the number of targets in the latest run. + /// public int CurrentTargetsTotal { get; set; } + + /// + /// Gets or sets the number of currently reachable targets. + /// public int CurrentTargetsUp { get; set; } + + /// + /// Gets or sets the number of currently unreachable targets. + /// public int CurrentTargetsDown { get; set; } + + /// + /// Gets or sets the number of currently skipped targets. + /// public int CurrentTargetsSkipped { get; set; } + + /// + /// Gets or sets the currently failed probe results. + /// public List CurrentFailures { get; set; } = new List(); + + /// + /// Gets or sets all current probe results. + /// public List CurrentResults { get; set; } = new List(); + + /// + /// Gets or sets the current target state dictionary. + /// public IDictionary CurrentState { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); } } diff --git a/src/HostAvailabilityMonitor/Program.cs b/src/HostAvailabilityMonitor/Program.cs index e7b70aa..97dd506 100644 --- a/src/HostAvailabilityMonitor/Program.cs +++ b/src/HostAvailabilityMonitor/Program.cs @@ -12,44 +12,67 @@ using HostAvailabilityMonitor.State; namespace HostAvailabilityMonitor { + /// + /// Console entry point and orchestration workflow for one monitor run. + /// internal static class Program { + /// + /// Starts the monitor synchronously for the console host. + /// + /// Optional command-line arguments. The first argument may be the configuration path. + /// The process exit code. private static int Main(string[] args) { return MainAsync(args).GetAwaiter().GetResult(); } + /// + /// Executes one complete monitor run. + /// + /// Optional command-line arguments. The first argument may be the configuration path. + /// The process exit code. private static async Task MainAsync(string[] args) { FileLogger logger = null; WindowsEventLogger eventLogger = null; var nonZeroExitCodeOnExecutionError = true; + string baseDirectory = null; + string configPath = null; try { - var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; - var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]) + baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]) ? args[0] : Path.Combine(baseDirectory, "appsettings.json"); var fullConfigPath = Path.GetFullPath(configPath); + WriteConsoleBanner("Host Availability Monitor", fullConfigPath); + WriteConsoleInfo("Lade Monitor-Konfiguration..."); + var config = MonitorConfiguration.Load(configPath); config.Validate(); nonZeroExitCodeOnExecutionError = config.Runtime.NonZeroExitCodeOnExecutionError; + WriteConsoleInfo("Konfiguration geladen. Umgebung=" + config.EnvironmentName + "."); var logDirectory = ResolvePath(baseDirectory, config.Logging.LogDirectory); var stateDirectory = ResolvePath(baseDirectory, config.Runtime.StateDirectory); var stateFilePath = Path.Combine(stateDirectory, "monitor-state.json"); + WriteConsoleInfo("Initialisiere Dateilog: " + BuildLogFilePreview(logDirectory, config.Logging.FilePrefix)); logger = new FileLogger(logDirectory, config.Logging.FilePrefix, config.Logging.RetentionDays, config.ApplicationName, config.EnvironmentName); logger.CleanupOldFiles(); logger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + "."); logger.Info("Konfiguration geladen: " + fullConfigPath); + logger.Info("Statusdatei: " + stateFilePath); + WriteConsoleInfo("Initialisiere Windows Event Log. Quelle=" + config.EventLog.Source + ", Log=" + config.EventLog.LogName + ", Aktiv=" + config.EventLog.Enabled + "."); eventLogger = new WindowsEventLogger(config.EventLog, logger); eventLogger.TryInitialize(); eventLogger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000); + WriteConsoleInfo("Lade Statusdatei: " + stateFilePath); Directory.CreateDirectory(stateDirectory); var stateStore = new MonitorStateStore(stateFilePath); MonitorStateDocument stateDocument; @@ -69,15 +92,23 @@ namespace HostAvailabilityMonitor var currentState = new Dictionary(previousState, StringComparer.OrdinalIgnoreCase); var probeService = new EndpointProbeService(config.Runtime); var enabledTargets = BuildEnabledTargets(config, fullConfigPath, baseDirectory, logger); + logger.Info("Aktivierte Targets vorbereitet. Anzahl=" + enabledTargets.Count + ", MaxConcurrency=" + config.Runtime.MaxConcurrency + "."); + eventLogger.Info("Monitor-Targets vorbereitet. Umgebung=" + config.EnvironmentName + ", AktivierteTargets=" + enabledTargets.Count + ", MaxConcurrency=" + config.Runtime.MaxConcurrency + ".", 1002); + WriteConsoleInfo("Aktivierte Targets vorbereitet: " + enabledTargets.Count + "."); if (enabledTargets.Count == 0) { logger.Warn("Es sind keine aktivierten Targets vorhanden. Der Lauf endet ohne Checks."); eventLogger.Summary(config.ApplicationName + " ausgeführt, aber es waren keine aktivierten Targets vorhanden. Umgebung=" + config.EnvironmentName + ".", false); + WriteConsoleWarn("Keine aktivierten Targets vorhanden. Der Lauf endet ohne Checks."); return 0; } + WriteConsoleInfo("Starte Endpoint-Checks..."); + logger.Info("Endpoint-Checks starten. Targets=" + enabledTargets.Count + "."); + eventLogger.Info("Endpoint-Checks gestartet. Umgebung=" + config.EnvironmentName + ", Targets=" + enabledTargets.Count + ".", 1003); var results = await ExecuteChecksAsync(enabledTargets, probeService, config.Runtime.MaxConcurrency).ConfigureAwait(false); + WriteConsoleInfo("Endpoint-Checks abgeschlossen. Ergebnisse=" + results.Count + "."); foreach (var result in results.OrderBy(r => r.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(r => r.Name, StringComparer.OrdinalIgnoreCase)) { @@ -168,6 +199,9 @@ namespace HostAvailabilityMonitor await TrySendDailySummaryAsync(config, notifier, stateDocument, currentState, results, logDirectory, logger, eventLogger).ConfigureAwait(false); stateStore.Save(stateDocument); + logger.Info("Statusdatei gespeichert: " + stateFilePath); + eventLogger.Info("Monitor-Status gespeichert. Umgebung=" + config.EnvironmentName + ", Pfad=" + stateFilePath + ".", 1004); + WriteConsoleInfo("Statusdatei gespeichert: " + stateFilePath); var total = results.Count(r => !r.IsSkipped); var ok = results.Count(r => r.IsSuccess); @@ -176,14 +210,22 @@ namespace HostAvailabilityMonitor var summary = config.ApplicationName + " Lauf beendet. Umgebung=" + config.EnvironmentName + ", Gesamt=" + total + ", Erfolgreich=" + ok + ", Fehlgeschlagen=" + failed + ", Uebersprungen=" + skipped + "."; logger.Info(summary); eventLogger.Summary(summary, failed > 0); + WriteConsoleInfo(summary); return failed > 0 && config.Runtime.NonZeroExitCodeOnAnyFailure ? 2 : 0; } catch (Exception ex) { + WriteConsoleFatal("Monitorlauf fehlgeschlagen.", configPath, ex); + if (logger != null) { logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message); + logger.Error("Details: " + ex); + } + else + { + TryWriteFallbackFailureLog(baseDirectory, configPath, ex); } if (eventLogger != null) @@ -195,6 +237,13 @@ namespace HostAvailabilityMonitor } } + /// + /// Executes endpoint checks with the configured concurrency limit. + /// + /// The enabled targets to probe. + /// The endpoint probe service. + /// The maximum number of concurrent probes. + /// The collected probe results. private static async Task> ExecuteChecksAsync(IReadOnlyCollection targets, EndpointProbeService probeService, int maxConcurrency) { var results = new List(); @@ -220,6 +269,14 @@ namespace HostAvailabilityMonitor return results; } + /// + /// Builds the enabled target list from static configuration and optional HIP VM configuration. + /// + /// The monitor configuration. + /// The full monitor configuration path. + /// The application base directory. + /// The file logger for operational messages. + /// The enabled targets to probe. private static List BuildEnabledTargets(MonitorConfiguration config, string configPath, string baseDirectory, FileLogger logger) { var targets = config.Targets == null @@ -242,6 +299,13 @@ namespace HostAvailabilityMonitor return targets; } + /// + /// Creates monitor targets for enabled HIP virtual machines. + /// + /// The HIP virtual machine configuration. + /// The HIP virtual machine monitor options. + /// The file logger for skipped VM messages. + /// The generated monitor targets. private static List CreateHipVirtualMachineTargets(HipVirtualMachineConfiguration vmConfig, HipVirtualMachineOptions options, FileLogger logger) { var targets = new List(); @@ -297,6 +361,18 @@ namespace HostAvailabilityMonitor return targets; } + /// + /// Creates one generated target for a HIP virtual machine check. + /// + /// The source HIP virtual machine. + /// The display machine name. + /// The application name assigned to generated targets. + /// The technical check name. + /// The display check name. + /// The generated endpoint. + /// The generated port, when applicable. + /// The generated target timeout. + /// The generated target. private static TargetOptions CreateHipVirtualMachineTarget( HipVirtualMachine vm, string machineName, @@ -323,6 +399,13 @@ namespace HostAvailabilityMonitor }; } + /// + /// Resolves a configuration-relative path against the configuration directory first, then the application base directory. + /// + /// The main configuration path. + /// The application base directory. + /// The configured relative or absolute path. + /// The resolved path. private static string ResolveConfigurationPath(string configPath, string baseDirectory, string configuredPath) { if (Path.IsPathRooted(configuredPath)) @@ -340,6 +423,18 @@ namespace HostAvailabilityMonitor return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath)); } + /// + /// Sends the daily summary email when it is due and updates state metadata only after successful delivery. + /// + /// The monitor configuration. + /// The email notifier. + /// The mutable state document. + /// The current target state dictionary. + /// The latest probe results. + /// The log directory used to calculate daily counters. + /// The file logger. + /// The Event Log writer. + /// A task that completes when the summary check has finished. private static async Task TrySendDailySummaryAsync( MonitorConfiguration config, EmailNotifier notifier, @@ -395,6 +490,14 @@ namespace HostAvailabilityMonitor } } + /// + /// Reads today's probe counters from the current rolling log file. + /// + /// The log directory. + /// The monitor log file prefix. + /// The current local server time. + /// The file logger used for fallback warnings. + /// The counters found in today's log file. private static ProbeLogCounters ReadTodayProbeCounters(string logDirectory, string filePrefix, DateTime nowLocal, FileLogger logger) { var counters = new ProbeLogCounters(); @@ -435,6 +538,12 @@ namespace HostAvailabilityMonitor return counters; } + /// + /// Updates persisted state for one non-skipped probe result. + /// + /// The mutable current state dictionary. + /// The latest probe result. + /// The previously persisted target state. private static void UpdateState(Dictionary currentState, ProbeResult result, TargetState previousState) { var nowUtc = DateTime.UtcNow; @@ -477,6 +586,12 @@ namespace HostAvailabilityMonitor currentState[result.StateKey] = state; } + /// + /// Resolves a configured path against the application base directory. + /// + /// The application base directory. + /// The configured relative or absolute path. + /// The resolved path. private static string ResolvePath(string baseDirectory, string configuredPath) { if (Path.IsPathRooted(configuredPath)) @@ -487,6 +602,12 @@ namespace HostAvailabilityMonitor return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath)); } + /// + /// Builds a structured Event Log message for one probe result. + /// + /// The monitor configuration. + /// The probe result. + /// The formatted Event Log message. private static string BuildEventMessage(MonitorConfiguration config, ProbeResult result) { return string.Format( @@ -504,10 +625,122 @@ namespace HostAvailabilityMonitor result.Detail); } + /// + /// Writes the initial console banner before configuration and file logging are available. + /// + /// The program title. + /// The resolved configuration path. + private static void WriteConsoleBanner(string title, string configPath) + { + TryWriteConsoleLine(Console.Out, "============================================================"); + TryWriteConsoleLine(Console.Out, title); + TryWriteConsoleLine(Console.Out, "Server: " + Environment.MachineName); + TryWriteConsoleLine(Console.Out, "Start: " + DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz")); + TryWriteConsoleLine(Console.Out, "Config: " + (configPath ?? "(nicht aufgeloest)")); + TryWriteConsoleLine(Console.Out, "============================================================"); + } + + /// + /// Writes a console information message. + /// + /// The message to write. + private static void WriteConsoleInfo(string message) + { + TryWriteConsoleLine(Console.Out, "[INFO] " + message); + } + + /// + /// Writes a console warning message. + /// + /// The message to write. + private static void WriteConsoleWarn(string message) + { + TryWriteConsoleLine(Console.Out, "[WARN] " + message); + } + + /// + /// Writes a fatal console error with actionable context. + /// + /// The user-facing failure message. + /// The configuration path used for the run. + /// The exception that ended the run. + private static void WriteConsoleFatal(string message, string configPath, Exception exception) + { + TryWriteConsoleLine(Console.Error, "[ERROR] " + message); + TryWriteConsoleLine(Console.Error, "[ERROR] Konfiguration: " + (configPath ?? "(nicht aufgeloest)")); + TryWriteConsoleLine(Console.Error, "[ERROR] Ursache: " + exception.GetType().Name + ": " + exception.Message); + TryWriteConsoleLine(Console.Error, "[ERROR] Details: " + exception); + } + + /// + /// Writes one line to a console writer and ignores secondary console failures. + /// + /// The console writer. + /// The line to write. + private static void TryWriteConsoleLine(TextWriter writer, string message) + { + try + { + writer.WriteLine(message); + } + catch + { + } + } + + /// + /// Writes a fallback fatal log when configured logging could not be initialized. + /// + /// The application base directory. + /// The configuration path used for the run. + /// The exception that ended the run. + private static void TryWriteFallbackFailureLog(string baseDirectory, string configPath, Exception exception) + { + try + { + var directory = ResolvePath(baseDirectory ?? AppDomain.CurrentDomain.BaseDirectory, "logs"); + var fallbackLogger = new FileLogger(directory, "host-availability-monitor-startup", 5, "HostAvailabilityMonitor", "Startup"); + fallbackLogger.Error("Monitor konnte nicht gestartet werden. Konfiguration=" + (configPath ?? "(nicht aufgeloest)") + ". Fehler=" + exception.GetType().Name + ": " + exception.Message); + fallbackLogger.Error("Details: " + exception); + WriteConsoleInfo("Fallback-Log geschrieben: " + BuildLogFilePreview(directory, "host-availability-monitor-startup")); + } + catch (Exception fallbackEx) + { + TryWriteConsoleLine(Console.Error, "[ERROR] Fallback-Log konnte nicht geschrieben werden: " + fallbackEx.GetType().Name + ": " + fallbackEx.Message); + } + } + + /// + /// Builds the expected daily log file path for console diagnostics. + /// + /// The log directory. + /// The rolling file prefix. + /// The expected log file path for the current local date. + private static string BuildLogFilePreview(string directory, string filePrefix) + { + var safeDirectory = string.IsNullOrWhiteSpace(directory) ? AppDomain.CurrentDomain.BaseDirectory : directory; + var safePrefix = string.IsNullOrWhiteSpace(filePrefix) ? "host-availability-monitor" : filePrefix; + return Path.Combine(safeDirectory, safePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log"); + } + + /// + /// Holds daily probe counters parsed from the rolling log. + /// private sealed class ProbeLogCounters { + /// + /// Gets or sets today's successful probe count. + /// public int SuccessCount { get; set; } + + /// + /// Gets or sets today's failed probe count. + /// public int FailureCount { get; set; } + + /// + /// Gets or sets today's skipped probe count. + /// public int SkipCount { get; set; } } } diff --git a/src/HostAvailabilityMonitor/Properties/AssemblyInfo.cs b/src/HostAvailabilityMonitor/Properties/AssemblyInfo.cs index c9583d1..29c82f0 100644 --- a/src/HostAvailabilityMonitor/Properties/AssemblyInfo.cs +++ b/src/HostAvailabilityMonitor/Properties/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Reflection; using System.Runtime.InteropServices; +// Assembly metadata for the HostAvailabilityMonitor console application. [assembly: AssemblyTitle("HostAvailabilityMonitor")] [assembly: AssemblyDescription("Windows Server 2019 kompatibler Host Availability Monitor auf .NET Framework 4.7.2")] [assembly: AssemblyConfiguration("")] diff --git a/src/HostAvailabilityMonitor/State/MonitorStateStore.cs b/src/HostAvailabilityMonitor/State/MonitorStateStore.cs index 5acc5dc..3663f2c 100644 --- a/src/HostAvailabilityMonitor/State/MonitorStateStore.cs +++ b/src/HostAvailabilityMonitor/State/MonitorStateStore.cs @@ -5,15 +5,29 @@ using System.Web.Script.Serialization; namespace HostAvailabilityMonitor.State { + /// + /// Loads and saves the persisted monitor state file. + /// public sealed class MonitorStateStore { + /// + /// Full path to the JSON state file. + /// private readonly string _filePath; + /// + /// Initializes a new state store. + /// + /// The state JSON file path. public MonitorStateStore(string filePath) { _filePath = filePath; } + /// + /// Loads the current state document and supports the legacy dictionary-only format. + /// + /// The loaded and normalized state document. public MonitorStateDocument LoadDocument() { if (!File.Exists(_filePath)) @@ -54,11 +68,19 @@ namespace HostAvailabilityMonitor.State } } + /// + /// Loads only the target state dictionary. + /// + /// The target states keyed by target state key. public Dictionary Load() { return LoadDocument().TargetStates; } + /// + /// Saves a complete monitor state document atomically. + /// + /// The document to persist. public void Save(MonitorStateDocument document) { var directory = Path.GetDirectoryName(_filePath); @@ -90,6 +112,10 @@ namespace HostAvailabilityMonitor.State } } + /// + /// Saves a target state dictionary using an empty metadata object. + /// + /// The target states to persist. public void Save(Dictionary states) { Save(new MonitorStateDocument @@ -101,6 +127,11 @@ namespace HostAvailabilityMonitor.State }); } + /// + /// Ensures that state document collections and metadata are initialized. + /// + /// The document to normalize. + /// The normalized document. private static MonitorStateDocument Normalize(MonitorStateDocument document) { if (document == null) @@ -116,24 +147,66 @@ namespace HostAvailabilityMonitor.State } } + /// + /// Root object persisted in the monitor state file. + /// public sealed class MonitorStateDocument { + /// + /// Gets or sets target states keyed by target state key. + /// public Dictionary TargetStates { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets state metadata that is not tied to one target. + /// public MonitorStateMetadata Metadata { get; set; } = new MonitorStateMetadata(); } + /// + /// Contains monitor state metadata shared across targets. + /// public sealed class MonitorStateMetadata { + /// + /// Gets or sets the local date for the most recent successfully sent daily summary email. + /// public string LastDailySummarySentLocalDate { get; set; } = string.Empty; } + /// + /// Persists the last known status and notification metadata for one target. + /// public sealed class TargetState { + /// + /// Gets or sets whether the target was up during the most recent non-skipped probe. + /// public bool IsUp { get; set; } + + /// + /// Gets or sets the number of consecutive failed probes. + /// public int ConsecutiveFailures { get; set; } + + /// + /// Gets or sets the UTC time of the most recent check. + /// public DateTime LastCheckedUtc { get; set; } + + /// + /// Gets or sets the UTC time when the target last changed between up and down. + /// public DateTime LastChangedUtc { get; set; } + + /// + /// Gets or sets the UTC time when the most recent notification was sent. + /// public DateTime? LastNotificationUtc { get; set; } + + /// + /// Gets or sets the most recent probe detail text. + /// public string LastDetail { get; set; } = string.Empty; } }