Updated logging plus more resilient coding to allow better run experience

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