Updated logging plus more resilient coding to allow better run experience
This commit is contained in:
@@ -24,6 +24,13 @@ _ReSharper*/
|
||||
[TestResult*/
|
||||
artifacts/
|
||||
publish/
|
||||
*.zip
|
||||
*.zip.txt
|
||||
*.zip.b64
|
||||
*.zip.b64.txt
|
||||
*.base64
|
||||
*.base64.txt
|
||||
*.certutil.txt
|
||||
|
||||
HIP-VMs.txt
|
||||
hostcheck.ps
|
||||
|
||||
@@ -9,12 +9,32 @@ using Microsoft.BizTalk.ExplorerOM;
|
||||
|
||||
namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
{
|
||||
/// <summary>
|
||||
/// Discovers monitorable endpoints from the BizTalk management catalog.
|
||||
/// </summary>
|
||||
public sealed class BizTalkCatalogDiscoveryService
|
||||
{
|
||||
/// <summary>
|
||||
/// BizTalk discovery options.
|
||||
/// </summary>
|
||||
private readonly BizTalkOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Output defaults used while normalizing endpoints.
|
||||
/// </summary>
|
||||
private readonly OutputOptions _outputOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Logger used for discovery decisions.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
_options = options ?? new BizTalkOptions();
|
||||
@@ -22,10 +42,17 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
_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()
|
||||
{
|
||||
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();
|
||||
catalog.ConnectionString = _options.ConnectionString;
|
||||
|
||||
@@ -38,10 +65,14 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
|
||||
if (ShouldSkipApplication(application))
|
||||
{
|
||||
skippedApplications++;
|
||||
_logger.Info("BizTalk-Anwendung übersprungen: " + application.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
inspectedApplications++;
|
||||
_logger.Info("BizTalk-Anwendung wird analysiert: " + application.Name);
|
||||
|
||||
if (_options.IncludeSendPorts)
|
||||
{
|
||||
DiscoverSendPorts(application, discovered);
|
||||
@@ -53,6 +84,8 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("BizTalk-Discovery beendet. AnalysierteAnwendungen=" + inspectedApplications + ", UebersprungeneAnwendungen=" + skippedApplications + ", UnterstuetzteTargets=" + discovered.Count + ".");
|
||||
|
||||
return discovered
|
||||
.OrderBy(x => x.MappedApplicationName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.TargetName, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -60,6 +93,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (transport == null)
|
||||
@@ -150,6 +203,17 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
if (ShouldSkipArtifact(artifactName))
|
||||
@@ -190,6 +254,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
_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)
|
||||
{
|
||||
if (application == null)
|
||||
@@ -205,6 +274,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifactName))
|
||||
@@ -215,6 +289,13 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var prefix = string.Equals(artifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -261,6 +350,16 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
normalizedEndpoint = string.Empty;
|
||||
@@ -386,6 +485,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(adapterName)
|
||||
&& 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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
@@ -475,6 +600,12 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
@@ -487,6 +618,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
@@ -497,6 +633,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
@@ -1,18 +1,68 @@
|
||||
namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents one BizTalk artifact endpoint that can be written as a monitor target.
|
||||
/// </summary>
|
||||
public sealed class DiscoveredEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk artifact type.
|
||||
/// </summary>
|
||||
public string ArtifactType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source BizTalk application name.
|
||||
/// </summary>
|
||||
public string BizTalkApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated monitor application name.
|
||||
/// </summary>
|
||||
public string MappedApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source BizTalk artifact name.
|
||||
/// </summary>
|
||||
public string ArtifactName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk adapter name.
|
||||
/// </summary>
|
||||
public string AdapterName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw address read from BizTalk.
|
||||
/// </summary>
|
||||
public string SourceAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the monitor-compatible endpoint.
|
||||
/// </summary>
|
||||
public string NormalizedEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated monitor target name.
|
||||
/// </summary>
|
||||
public string TargetName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP method for generated HTTP targets.
|
||||
/// </summary>
|
||||
public string HttpMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the explicit target port, when resolved.
|
||||
/// </summary>
|
||||
public int? Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional UNC path validation override.
|
||||
/// </summary>
|
||||
public bool? ValidateUncPathAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets discovery detail text for diagnostics.
|
||||
/// </summary>
|
||||
public string Detail { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,51 @@ using HostAvailabilityMonitor.Configuration;
|
||||
|
||||
namespace BizTalkMonitorConfigGenerator.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Root configuration for the BizTalk monitor configuration generator.
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the environment label used in logs and generated output.
|
||||
/// </summary>
|
||||
public string EnvironmentName { get; set; } = "Unspecified Environment";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets rolling file log settings.
|
||||
/// </summary>
|
||||
public GeneratorLoggingOptions Logging { get; set; } = new GeneratorLoggingOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Windows Event Log settings.
|
||||
/// </summary>
|
||||
public GeneratorEventLogOptions EventLog { get; set; } = new GeneratorEventLogOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets BizTalk discovery settings.
|
||||
/// </summary>
|
||||
public BizTalkOptions BizTalk { get; set; } = new BizTalkOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets output file and target defaults.
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
@@ -74,6 +109,9 @@ namespace BizTalkMonitorConfigGenerator.Configuration
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates generator settings and applies safe defaults where possible.
|
||||
/// </summary>
|
||||
public void Validate()
|
||||
{
|
||||
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
|
||||
{
|
||||
/// <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)
|
||||
{
|
||||
if (values == null)
|
||||
@@ -131,6 +177,11 @@ namespace BizTalkMonitorConfigGenerator.Configuration
|
||||
.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)
|
||||
{
|
||||
if (email == null)
|
||||
@@ -144,65 +195,216 @@ namespace BizTalkMonitorConfigGenerator.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines rolling file log settings for the generator.
|
||||
/// </summary>
|
||||
public sealed class GeneratorLoggingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the directory where generator log files are written.
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of daily generator log files to retain.
|
||||
/// </summary>
|
||||
public int RetentionDays { get; set; } = 5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines Windows Event Log behavior for the generator.
|
||||
/// </summary>
|
||||
public sealed class GeneratorEventLogOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Event Log writes are enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Windows Event Log name.
|
||||
/// </summary>
|
||||
public string LogName { get; set; } = "Application";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Event Log source name.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether successful generator events should be written.
|
||||
/// </summary>
|
||||
public bool WriteSuccessEntries { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether generator summary events should be written.
|
||||
/// </summary>
|
||||
public bool WriteSummaryEntries { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines BizTalk artifact discovery behavior.
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether send ports should be discovered.
|
||||
/// </summary>
|
||||
public bool IncludeSendPorts { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether receive locations should be discovered.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether only enabled receive locations should be included.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether dynamic send ports should be included.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether BizTalk system applications should be skipped.
|
||||
/// </summary>
|
||||
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>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets artifact name wildcard patterns to skip.
|
||||
/// </summary>
|
||||
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>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets artifact-level mapping rules for generated ApplicationName values.
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the generated monitor configuration output path.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether no discovered targets should produce a failing exit code.
|
||||
/// </summary>
|
||||
public bool FailIfNoTargetsFound { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout assigned to generated targets.
|
||||
/// </summary>
|
||||
public int TargetTimeoutSeconds { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether generated targets are enabled by default.
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a BizTalk application name pattern to a monitor application name.
|
||||
/// </summary>
|
||||
public sealed class ApplicationMappingRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk application wildcard pattern.
|
||||
/// </summary>
|
||||
public string BizTalkApplicationName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated monitor application name.
|
||||
/// </summary>
|
||||
public string ApplicationName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a BizTalk artifact pattern to a monitor application name.
|
||||
/// </summary>
|
||||
public sealed class ArtifactMappingRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the optional BizTalk artifact type filter.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated monitor application name.
|
||||
/// </summary>
|
||||
public string ApplicationName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides monitor configuration template helpers used by the generator.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var clone = new MonitorConfiguration
|
||||
|
||||
@@ -5,12 +5,31 @@ using BizTalkMonitorConfigGenerator.Configuration;
|
||||
|
||||
namespace BizTalkMonitorConfigGenerator.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Best-effort Windows Event Log writer for generator lifecycle and summary events.
|
||||
/// </summary>
|
||||
public sealed class GeneratorEventLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Event Log settings from generator configuration.
|
||||
/// </summary>
|
||||
private readonly GeneratorEventLogOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// File logger used when Event Log writes fail.
|
||||
/// </summary>
|
||||
private readonly GeneratorFileLogger _fallbackLogger;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that Event Log writing has been disabled for this process.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
_options = options ?? new GeneratorEventLogOptions();
|
||||
@@ -18,6 +37,9 @@ namespace BizTalkMonitorConfigGenerator.Logging
|
||||
_disabled = !_options.Enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optionally creates the configured Event Log source.
|
||||
/// </summary>
|
||||
public void TryInitialize()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!_options.WriteSummaryEntries)
|
||||
@@ -76,6 +123,13 @@ namespace BizTalkMonitorConfigGenerator.Logging
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (_disabled)
|
||||
|
||||
@@ -5,15 +5,49 @@ using System.Threading;
|
||||
|
||||
namespace BizTalkMonitorConfigGenerator.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes rolling log files for BizTalk monitor configuration generation.
|
||||
/// </summary>
|
||||
public sealed class GeneratorFileLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronizes file writes within the current process.
|
||||
/// </summary>
|
||||
private readonly object _sync = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Directory where generator logs are stored.
|
||||
/// </summary>
|
||||
private readonly string _directory;
|
||||
|
||||
/// <summary>
|
||||
/// Prefix used for daily generator log files.
|
||||
/// </summary>
|
||||
private readonly string _filePrefix;
|
||||
|
||||
/// <summary>
|
||||
/// Number of daily log files to retain.
|
||||
/// </summary>
|
||||
private readonly int _retentionDays;
|
||||
|
||||
/// <summary>
|
||||
/// Generator name written to log lines.
|
||||
/// </summary>
|
||||
private readonly string _generatorName;
|
||||
|
||||
/// <summary>
|
||||
/// Environment name written to log lines.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
_directory = directory;
|
||||
@@ -24,6 +58,9 @@ namespace BizTalkMonitorConfigGenerator.Logging
|
||||
Directory.CreateDirectory(_directory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes old rolling log files according to the retention setting.
|
||||
/// </summary>
|
||||
public void CleanupOldFiles()
|
||||
{
|
||||
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)
|
||||
{
|
||||
Write("INFO", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a warning log entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public void Warn(string message)
|
||||
{
|
||||
Write("WARN", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error log entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public void Error(string 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)
|
||||
{
|
||||
Write("DISCOVERY", string.Format(
|
||||
@@ -85,6 +145,11 @@ namespace BizTalkMonitorConfigGenerator.Logging
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
|
||||
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)
|
||||
{
|
||||
date = DateTime.MinValue;
|
||||
@@ -156,6 +236,11 @@ namespace BizTalkMonitorConfigGenerator.Logging
|
||||
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)
|
||||
{
|
||||
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
|
||||
|
||||
@@ -10,48 +10,71 @@ using HostAvailabilityMonitor.Configuration;
|
||||
|
||||
namespace BizTalkMonitorConfigGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Console entry point and orchestration workflow for the BizTalk monitor config generator.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
GeneratorFileLogger logger = null;
|
||||
GeneratorEventLogger eventLogger = null;
|
||||
string baseDirectory = null;
|
||||
string configPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
|
||||
baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
|
||||
? args[0]
|
||||
: Path.Combine(baseDirectory, "generator-settings.json");
|
||||
configPath = Path.GetFullPath(configPath);
|
||||
|
||||
WriteConsoleBanner("BizTalk Monitor Config Generator", configPath);
|
||||
WriteConsoleInfo("Lade Generator-Konfiguration...");
|
||||
|
||||
var configuration = GeneratorConfiguration.Load(configPath);
|
||||
configuration.Validate();
|
||||
WriteConsoleInfo("Konfiguration geladen. Umgebung=" + configuration.EnvironmentName + ".");
|
||||
|
||||
var logDirectory = ResolvePath(baseDirectory, configuration.Logging.LogDirectory);
|
||||
WriteConsoleInfo("Initialisiere Dateilog: " + BuildLogFilePreview(logDirectory, configuration.Logging.FilePrefix));
|
||||
logger = new GeneratorFileLogger(logDirectory, configuration.Logging.FilePrefix, configuration.Logging.RetentionDays, configuration.GeneratorName, configuration.EnvironmentName);
|
||||
logger.CleanupOldFiles();
|
||||
logger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + ".");
|
||||
logger.Info("Generator-Konfiguration geladen: " + Path.GetFullPath(configPath));
|
||||
logger.Info("Ausgabeziel: " + ResolvePath(baseDirectory, configuration.Output.Path));
|
||||
|
||||
WriteConsoleInfo("Initialisiere Windows Event Log. Quelle=" + configuration.EventLog.Source + ", Log=" + configuration.EventLog.LogName + ", Aktiv=" + configuration.EventLog.Enabled + ".");
|
||||
eventLogger = new GeneratorEventLogger(configuration.EventLog, logger);
|
||||
eventLogger.TryInitialize();
|
||||
eventLogger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000);
|
||||
|
||||
WriteConsoleInfo("Starte BizTalk-Discovery. SendPorts=" + configuration.BizTalk.IncludeSendPorts + ", ReceiveLocations=" + configuration.BizTalk.IncludeReceiveLocations + ".");
|
||||
var discoveryService = new BizTalkCatalogDiscoveryService(configuration.BizTalk, configuration.Output, logger);
|
||||
var discovered = discoveryService.Discover();
|
||||
logger.Info("Discovery abgeschlossen. Gefundene unterstützte Targets=" + discovered.Count + ".");
|
||||
WriteConsoleInfo("Discovery abgeschlossen. Gefundene unterstuetzte Targets=" + discovered.Count + ".");
|
||||
|
||||
var outputConfiguration = BuildMonitorConfiguration(configuration, discovered);
|
||||
var outputPath = ResolvePath(baseDirectory, configuration.Output.Path);
|
||||
WriteConsoleInfo("Schreibe Monitor-Konfiguration: " + outputPath);
|
||||
JsonFileWriter.WritePrettyJsonAtomic(outputPath, outputConfiguration, configuration.Output.OverwriteExisting);
|
||||
logger.Info("Monitor-Konfiguration geschrieben: " + outputPath);
|
||||
eventLogger.InfoIfSuccessEnabled("Monitor-Konfiguration erfolgreich generiert: " + outputPath, 1001);
|
||||
WriteConsoleInfo("Monitor-Konfiguration geschrieben.");
|
||||
|
||||
if (discovered.Count == 0)
|
||||
{
|
||||
var message = "Es wurden keine aktiven und unterstützten BizTalk-Endpunkte gefunden. Ausgabe wurde dennoch geschrieben: " + outputPath;
|
||||
logger.Warn(message);
|
||||
eventLogger.Summary(message, true);
|
||||
WriteConsoleWarn(message);
|
||||
return configuration.Output.FailIfNoTargetsFound ? 2 : 0;
|
||||
}
|
||||
|
||||
@@ -62,13 +85,21 @@ namespace BizTalkMonitorConfigGenerator
|
||||
outputPath);
|
||||
logger.Info(summary);
|
||||
eventLogger.Summary(summary, false);
|
||||
WriteConsoleInfo(summary);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteConsoleFatal("Generatorlauf fehlgeschlagen.", configPath, ex);
|
||||
|
||||
if (logger != null)
|
||||
{
|
||||
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
|
||||
logger.Error("Details: " + ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
TryWriteFallbackFailureLog(baseDirectory, configPath, ex);
|
||||
}
|
||||
|
||||
if (eventLogger != null)
|
||||
@@ -80,6 +111,12 @@ namespace BizTalkMonitorConfigGenerator
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var result = configuration.GeneratedMonitorConfig.CloneWithoutTargets();
|
||||
@@ -114,6 +151,12 @@ namespace BizTalkMonitorConfigGenerator
|
||||
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)
|
||||
{
|
||||
if (Path.IsPathRooted(configuredPath))
|
||||
@@ -123,5 +166,103 @@ namespace BizTalkMonitorConfigGenerator
|
||||
|
||||
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.Runtime.InteropServices;
|
||||
|
||||
// Assembly metadata for the BizTalkMonitorConfigGenerator console application.
|
||||
[assembly: AssemblyTitle("BizTalkMonitorConfigGenerator")]
|
||||
[assembly: AssemblyDescription("Generates HostAvailabilityMonitor JSON config from active BizTalk send ports and receive locations.")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
|
||||
@@ -5,8 +5,17 @@ using System.Web.Script.Serialization;
|
||||
|
||||
namespace BizTalkMonitorConfigGenerator.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes generator output JSON files with stable formatting and atomic replacement.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
@@ -44,6 +53,11 @@ namespace BizTalkMonitorConfigGenerator.Serialization
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
@@ -126,6 +140,11 @@ namespace BizTalkMonitorConfigGenerator.Serialization
|
||||
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)
|
||||
{
|
||||
for (var i = 0; i < indent; i++)
|
||||
|
||||
@@ -5,10 +5,21 @@ using System.Web.Script.Serialization;
|
||||
|
||||
namespace HostAvailabilityMonitor.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the external HIP virtual machine configuration file.
|
||||
/// </summary>
|
||||
public sealed class HipVirtualMachineConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the configured virtual machines.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the logical group used for reporting.
|
||||
/// </summary>
|
||||
public string Group { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the virtual machine display name.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional description for mail and summary output.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,56 @@ using System.Web.Script.Serialization;
|
||||
|
||||
namespace HostAvailabilityMonitor.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Root configuration for the host availability monitor.
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the environment label shown in logs, event log entries and email.
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets rolling file log settings.
|
||||
/// </summary>
|
||||
public LoggingOptions Logging { get; set; } = new LoggingOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Windows Application Event Log settings.
|
||||
/// </summary>
|
||||
public EventLogOptions EventLog { get; set; } = new EventLogOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets SMTP and notification settings.
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint targets to probe.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
@@ -84,6 +123,9 @@ namespace HostAvailabilityMonitor.Configuration
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates monitor settings and applies safe defaults where possible.
|
||||
/// </summary>
|
||||
public void Validate()
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (values == null)
|
||||
@@ -191,6 +239,11 @@ namespace HostAvailabilityMonitor.Configuration
|
||||
.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)
|
||||
{
|
||||
if (email == null)
|
||||
@@ -204,93 +257,330 @@ namespace HostAvailabilityMonitor.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines monitor runtime behavior.
|
||||
/// </summary>
|
||||
public sealed class RuntimeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of concurrent endpoint probes.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether endpoint failures should produce a non-zero process exit code.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state directory path.
|
||||
/// </summary>
|
||||
public string StateDirectory { get; set; } = "state";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines rolling file log settings.
|
||||
/// </summary>
|
||||
public sealed class LoggingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the directory where monitor log files are written.
|
||||
/// </summary>
|
||||
public string LogDirectory { get; set; } = "logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the daily log file prefix.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines Windows Event Log behavior.
|
||||
/// </summary>
|
||||
public sealed class EventLogOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Event Log writes are enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Windows Event Log name.
|
||||
/// </summary>
|
||||
public string LogName { get; set; } = "Application";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Event Log source name.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether successful endpoint checks should be written to Event Log.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines SMTP and notification behavior.
|
||||
/// </summary>
|
||||
public sealed class EmailOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether email delivery is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SMTP server host name.
|
||||
/// </summary>
|
||||
public string SmtpHost { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SMTP server port.
|
||||
/// </summary>
|
||||
public int SmtpPort { get; set; } = 25;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether SMTP should use SSL/TLS.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SMTP user name when default credentials are disabled.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender email address.
|
||||
/// </summary>
|
||||
public string From { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets primary email recipients.
|
||||
/// </summary>
|
||||
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>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets BCC email recipients.
|
||||
/// </summary>
|
||||
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]";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether failure notification emails should be sent.
|
||||
/// </summary>
|
||||
public bool SendOnFailure { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether recovery notification emails should be sent.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether failure and recovery emails should only be sent on state changes.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SMTP timeout in seconds.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines optional static HIP virtual machine probe generation.
|
||||
/// </summary>
|
||||
public sealed class HipVirtualMachineOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether HIP virtual machine checks are enabled.
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the application name assigned to generated HIP VM targets.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RDP port used for generated RDP checks.
|
||||
/// </summary>
|
||||
public int RdpPort { get; set; } = 3389;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether ICMP network checks should be generated.
|
||||
/// </summary>
|
||||
public bool CheckNetwork { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether RDP TCP checks should be generated.
|
||||
/// </summary>
|
||||
public bool CheckRdp { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines one endpoint target that can be probed by the monitor.
|
||||
/// </summary>
|
||||
public sealed class TargetOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the configured target name.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint address to probe.
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the probe timeout in seconds.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional explicit port override.
|
||||
/// </summary>
|
||||
public int? Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional UNC path access validation override.
|
||||
/// </summary>
|
||||
public bool? ValidateUncPathAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP method used for HTTP and HTTPS probes.
|
||||
/// </summary>
|
||||
public string HttpMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source that created the target.
|
||||
/// </summary>
|
||||
public string Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional group label for reporting.
|
||||
/// </summary>
|
||||
public string Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional target description.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional machine name for generated VM targets.
|
||||
/// </summary>
|
||||
public string MachineName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional check name for generated targets.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name used in logs, status and notification output.
|
||||
/// </summary>
|
||||
public string DisplayName
|
||||
{
|
||||
get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the normalized application name used for grouping.
|
||||
/// </summary>
|
||||
public string EffectiveApplicationName
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
|
||||
|
||||
@@ -7,15 +7,49 @@ using HostAvailabilityMonitor.Monitoring;
|
||||
|
||||
namespace HostAvailabilityMonitor.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes rolling monitor log files with daily file names and probe-specific fields.
|
||||
/// </summary>
|
||||
public sealed class FileLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronizes writes within the current process.
|
||||
/// </summary>
|
||||
private readonly object _sync = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Directory where log files are stored.
|
||||
/// </summary>
|
||||
private readonly string _directory;
|
||||
|
||||
/// <summary>
|
||||
/// Prefix used for daily log file names.
|
||||
/// </summary>
|
||||
private readonly string _filePrefix;
|
||||
|
||||
/// <summary>
|
||||
/// Number of daily log files to retain.
|
||||
/// </summary>
|
||||
private readonly int _retentionDays;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor name included in structured probe log fields.
|
||||
/// </summary>
|
||||
private readonly string _monitorName;
|
||||
|
||||
/// <summary>
|
||||
/// Environment name included in structured probe log fields.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
_directory = directory;
|
||||
@@ -27,6 +61,9 @@ namespace HostAvailabilityMonitor.Logging
|
||||
Directory.CreateDirectory(_directory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes old rolling log files according to the retention setting.
|
||||
/// </summary>
|
||||
public void CleanupOldFiles()
|
||||
{
|
||||
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)
|
||||
{
|
||||
Write("INFO", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a warning log entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public void Warn(string message)
|
||||
{
|
||||
Write("WARN", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error log entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public void Error(string 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)
|
||||
{
|
||||
var level = result.IsSkipped ? "SKIP" : (result.IsSuccess ? "SUCCESS" : "FAIL");
|
||||
@@ -137,6 +190,11 @@ namespace HostAvailabilityMonitor.Logging
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
|
||||
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)
|
||||
{
|
||||
date = DateTime.MinValue;
|
||||
@@ -204,6 +277,11 @@ namespace HostAvailabilityMonitor.Logging
|
||||
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)
|
||||
{
|
||||
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
|
||||
|
||||
@@ -5,12 +5,31 @@ using HostAvailabilityMonitor.Configuration;
|
||||
|
||||
namespace HostAvailabilityMonitor.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Best-effort Windows Event Log writer for monitor lifecycle, probe and summary events.
|
||||
/// </summary>
|
||||
public sealed class WindowsEventLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Event Log settings from monitor configuration.
|
||||
/// </summary>
|
||||
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;
|
||||
@@ -18,6 +37,9 @@ namespace HostAvailabilityMonitor.Logging
|
||||
_disabled = !_options.Enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optionally creates the configured Event Log source.
|
||||
/// </summary>
|
||||
public void TryInitialize()
|
||||
{
|
||||
if (_disabled)
|
||||
@@ -49,11 +71,21 @@ namespace HostAvailabilityMonitor.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)
|
||||
{
|
||||
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)
|
||||
@@ -62,16 +94,31 @@ namespace HostAvailabilityMonitor.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)
|
||||
{
|
||||
Write(message, EventLogEntryType.Warning, eventId, forceWrite: 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)
|
||||
{
|
||||
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)
|
||||
@@ -86,6 +133,13 @@ namespace HostAvailabilityMonitor.Logging
|
||||
forceWrite: 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)
|
||||
{
|
||||
if (_disabled || !forceWrite)
|
||||
@@ -111,6 +165,10 @@ namespace HostAvailabilityMonitor.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)
|
||||
{
|
||||
if (_disabled)
|
||||
|
||||
@@ -10,17 +10,37 @@ using HostAvailabilityMonitor.Configuration;
|
||||
|
||||
namespace HostAvailabilityMonitor.Monitoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes endpoint probes for all supported target kinds.
|
||||
/// </summary>
|
||||
public sealed class EndpointProbeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared HTTP client with per-request timeout handling through cancellation tokens.
|
||||
/// </summary>
|
||||
private static readonly HttpClient HttpClient = CreateHttpClient();
|
||||
|
||||
/// <summary>
|
||||
/// Runtime options that influence probe behavior.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
_runtimeOptions = runtimeOptions ?? new RuntimeOptions();
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (target.Port.HasValue)
|
||||
@@ -98,6 +126,14 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
if (uri.IsUnc || !string.IsNullOrWhiteSpace(uri.Host))
|
||||
@@ -115,6 +151,14 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (uri == null)
|
||||
@@ -347,6 +443,11 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
@@ -359,6 +460,10 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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()
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
@@ -367,21 +472,70 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
result = null;
|
||||
@@ -422,6 +584,12 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
var name = target == null ? string.Empty : (target.Name ?? string.Empty);
|
||||
@@ -435,6 +603,11 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
@@ -482,6 +655,14 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
var delayTask = Task.Delay(timeout, cancellationToken);
|
||||
@@ -494,6 +675,13 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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)
|
||||
{
|
||||
var delayTask = Task.Delay(timeout, cancellationToken);
|
||||
@@ -507,15 +695,31 @@ namespace HostAvailabilityMonitor.Monitoring
|
||||
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>
|
||||
{
|
||||
/// <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)
|
||||
{
|
||||
Completed = completed;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the wrapped task completed in time.
|
||||
/// </summary>
|
||||
public bool Completed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped task result.
|
||||
/// </summary>
|
||||
public T Result { get; private set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,43 +2,175 @@ using System;
|
||||
|
||||
namespace HostAvailabilityMonitor.Monitoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the endpoint probe strategy used for a target.
|
||||
/// </summary>
|
||||
public enum TargetKind
|
||||
{
|
||||
/// <summary>
|
||||
/// The target type could not be resolved.
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A UNC or SMB path target.
|
||||
/// </summary>
|
||||
Unc = 1,
|
||||
|
||||
/// <summary>
|
||||
/// An HTTP endpoint target.
|
||||
/// </summary>
|
||||
Http = 2,
|
||||
|
||||
/// <summary>
|
||||
/// An HTTPS endpoint target.
|
||||
/// </summary>
|
||||
Https = 3,
|
||||
|
||||
/// <summary>
|
||||
/// An SFTP endpoint target.
|
||||
/// </summary>
|
||||
Sftp = 4,
|
||||
|
||||
/// <summary>
|
||||
/// A generic TCP endpoint target.
|
||||
/// </summary>
|
||||
Tcp = 5,
|
||||
|
||||
/// <summary>
|
||||
/// An ICMP ping target.
|
||||
/// </summary>
|
||||
Icmp = 6,
|
||||
|
||||
/// <summary>
|
||||
/// A plain host target resolved through target options.
|
||||
/// </summary>
|
||||
Host = 7,
|
||||
|
||||
/// <summary>
|
||||
/// An FTP endpoint target.
|
||||
/// </summary>
|
||||
Ftp = 8,
|
||||
|
||||
/// <summary>
|
||||
/// A local or file URI target.
|
||||
/// </summary>
|
||||
File = 9,
|
||||
|
||||
/// <summary>
|
||||
/// An RDP TCP endpoint target.
|
||||
/// </summary>
|
||||
Rdp = 10
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the result of probing one configured target.
|
||||
/// </summary>
|
||||
public sealed class ProbeResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the stable state key for the target.
|
||||
/// </summary>
|
||||
public string StateKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the target.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logical application name.
|
||||
/// </summary>
|
||||
public string ApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint that was probed.
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolved target kind.
|
||||
/// </summary>
|
||||
public TargetKind Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the probe succeeded.
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the target was intentionally skipped.
|
||||
/// </summary>
|
||||
public bool IsSkipped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the high-level status text.
|
||||
/// </summary>
|
||||
public string StatusText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the detailed probe result message.
|
||||
/// </summary>
|
||||
public string Detail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolved host name or address.
|
||||
/// </summary>
|
||||
public string Host { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolved port, when applicable.
|
||||
/// </summary>
|
||||
public int? Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source that created the target.
|
||||
/// </summary>
|
||||
public string Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the group label used in reports.
|
||||
/// </summary>
|
||||
public string Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target description.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the machine name for generated VM checks.
|
||||
/// </summary>
|
||||
public string MachineName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the check name for generated checks.
|
||||
/// </summary>
|
||||
public string CheckName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP status code returned by HTTP probes.
|
||||
/// </summary>
|
||||
public int? HttpStatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception type observed during failed probes.
|
||||
/// </summary>
|
||||
public string ErrorType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local start time of the probe.
|
||||
/// </summary>
|
||||
public DateTimeOffset StartedAtLocal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local finish time of the probe.
|
||||
/// </summary>
|
||||
public DateTimeOffset FinishedAtLocal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the elapsed probe duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration
|
||||
{
|
||||
get { return FinishedAtLocal - StartedAtLocal; }
|
||||
|
||||
@@ -12,12 +12,32 @@ using HostAvailabilityMonitor.State;
|
||||
|
||||
namespace HostAvailabilityMonitor.Notifications
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends failure, recovery and daily summary emails for monitor results.
|
||||
/// </summary>
|
||||
public sealed class EmailNotifier
|
||||
{
|
||||
/// <summary>
|
||||
/// SMTP and notification settings.
|
||||
/// </summary>
|
||||
private readonly EmailOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor name written into subjects and message bodies.
|
||||
/// </summary>
|
||||
private readonly string _monitorName;
|
||||
|
||||
/// <summary>
|
||||
/// Environment name written into subjects and message bodies.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
_options = options ?? new EmailOptions();
|
||||
@@ -25,11 +45,20 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
_environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether email delivery is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!IsEnabled || result == null)
|
||||
@@ -72,6 +101,12 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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)
|
||||
{
|
||||
if (!IsEnabled || !_options.SendDailySummary)
|
||||
@@ -89,6 +124,14 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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(
|
||||
IReadOnlyCollection<ProbeResult> failures,
|
||||
IReadOnlyCollection<ProbeResult> recoveries,
|
||||
@@ -112,6 +155,12 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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)
|
||||
{
|
||||
if (!IsEnabled || summary == null)
|
||||
@@ -124,6 +173,13 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return result != null
|
||||
@@ -182,6 +249,12 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
&& !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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
@@ -239,6 +324,11 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
@@ -281,6 +371,13 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (result == null)
|
||||
@@ -396,6 +508,11 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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)
|
||||
{
|
||||
if (values == null)
|
||||
@@ -406,26 +523,85 @@ namespace HostAvailabilityMonitor.Notifications
|
||||
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)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "n/a" : value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains all data required to render a daily status summary email.
|
||||
/// </summary>
|
||||
public sealed class DailySummaryEmailData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the local timestamp when the summary is generated.
|
||||
/// </summary>
|
||||
public DateTimeOffset GeneratedAtLocal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local start timestamp of the summary window.
|
||||
/// </summary>
|
||||
public DateTimeOffset WindowStartLocal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets today's total probe count from the rolling log.
|
||||
/// </summary>
|
||||
public int ProbesTodayTotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets today's successful probe count from the rolling log.
|
||||
/// </summary>
|
||||
public int ProbesTodaySuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets today's failed probe count from the rolling log.
|
||||
/// </summary>
|
||||
public int ProbesTodayFailure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets today's skipped probe count from the rolling log.
|
||||
/// </summary>
|
||||
public int ProbesTodaySkipped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of targets in the latest run.
|
||||
/// </summary>
|
||||
public int CurrentTargetsTotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of currently reachable targets.
|
||||
/// </summary>
|
||||
public int CurrentTargetsUp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of currently unreachable targets.
|
||||
/// </summary>
|
||||
public int CurrentTargetsDown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of currently skipped targets.
|
||||
/// </summary>
|
||||
public int CurrentTargetsSkipped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently failed probe results.
|
||||
/// </summary>
|
||||
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>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current target state dictionary.
|
||||
/// </summary>
|
||||
public IDictionary<string, TargetState> CurrentState { get; set; } = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,44 +12,67 @@ using HostAvailabilityMonitor.State;
|
||||
|
||||
namespace HostAvailabilityMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Console entry point and orchestration workflow for one monitor run.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
FileLogger logger = null;
|
||||
WindowsEventLogger eventLogger = null;
|
||||
var nonZeroExitCodeOnExecutionError = true;
|
||||
string baseDirectory = null;
|
||||
string configPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
|
||||
baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
|
||||
? args[0]
|
||||
: Path.Combine(baseDirectory, "appsettings.json");
|
||||
var fullConfigPath = Path.GetFullPath(configPath);
|
||||
|
||||
WriteConsoleBanner("Host Availability Monitor", fullConfigPath);
|
||||
WriteConsoleInfo("Lade Monitor-Konfiguration...");
|
||||
|
||||
var config = MonitorConfiguration.Load(configPath);
|
||||
config.Validate();
|
||||
nonZeroExitCodeOnExecutionError = config.Runtime.NonZeroExitCodeOnExecutionError;
|
||||
WriteConsoleInfo("Konfiguration geladen. Umgebung=" + config.EnvironmentName + ".");
|
||||
|
||||
var logDirectory = ResolvePath(baseDirectory, config.Logging.LogDirectory);
|
||||
var stateDirectory = ResolvePath(baseDirectory, config.Runtime.StateDirectory);
|
||||
var stateFilePath = Path.Combine(stateDirectory, "monitor-state.json");
|
||||
|
||||
WriteConsoleInfo("Initialisiere Dateilog: " + BuildLogFilePreview(logDirectory, config.Logging.FilePrefix));
|
||||
logger = new FileLogger(logDirectory, config.Logging.FilePrefix, config.Logging.RetentionDays, config.ApplicationName, config.EnvironmentName);
|
||||
logger.CleanupOldFiles();
|
||||
logger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + ".");
|
||||
logger.Info("Konfiguration geladen: " + fullConfigPath);
|
||||
logger.Info("Statusdatei: " + stateFilePath);
|
||||
|
||||
WriteConsoleInfo("Initialisiere Windows Event Log. Quelle=" + config.EventLog.Source + ", Log=" + config.EventLog.LogName + ", Aktiv=" + config.EventLog.Enabled + ".");
|
||||
eventLogger = new WindowsEventLogger(config.EventLog, logger);
|
||||
eventLogger.TryInitialize();
|
||||
eventLogger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000);
|
||||
|
||||
WriteConsoleInfo("Lade Statusdatei: " + stateFilePath);
|
||||
Directory.CreateDirectory(stateDirectory);
|
||||
var stateStore = new MonitorStateStore(stateFilePath);
|
||||
MonitorStateDocument stateDocument;
|
||||
@@ -69,15 +92,23 @@ namespace HostAvailabilityMonitor
|
||||
var currentState = new Dictionary<string, TargetState>(previousState, StringComparer.OrdinalIgnoreCase);
|
||||
var probeService = new EndpointProbeService(config.Runtime);
|
||||
var enabledTargets = BuildEnabledTargets(config, fullConfigPath, baseDirectory, logger);
|
||||
logger.Info("Aktivierte Targets vorbereitet. Anzahl=" + enabledTargets.Count + ", MaxConcurrency=" + config.Runtime.MaxConcurrency + ".");
|
||||
eventLogger.Info("Monitor-Targets vorbereitet. Umgebung=" + config.EnvironmentName + ", AktivierteTargets=" + enabledTargets.Count + ", MaxConcurrency=" + config.Runtime.MaxConcurrency + ".", 1002);
|
||||
WriteConsoleInfo("Aktivierte Targets vorbereitet: " + enabledTargets.Count + ".");
|
||||
|
||||
if (enabledTargets.Count == 0)
|
||||
{
|
||||
logger.Warn("Es sind keine aktivierten Targets vorhanden. Der Lauf endet ohne Checks.");
|
||||
eventLogger.Summary(config.ApplicationName + " ausgeführt, aber es waren keine aktivierten Targets vorhanden. Umgebung=" + config.EnvironmentName + ".", false);
|
||||
WriteConsoleWarn("Keine aktivierten Targets vorhanden. Der Lauf endet ohne Checks.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
WriteConsoleInfo("Starte Endpoint-Checks...");
|
||||
logger.Info("Endpoint-Checks starten. Targets=" + enabledTargets.Count + ".");
|
||||
eventLogger.Info("Endpoint-Checks gestartet. Umgebung=" + config.EnvironmentName + ", Targets=" + enabledTargets.Count + ".", 1003);
|
||||
var results = await ExecuteChecksAsync(enabledTargets, probeService, config.Runtime.MaxConcurrency).ConfigureAwait(false);
|
||||
WriteConsoleInfo("Endpoint-Checks abgeschlossen. Ergebnisse=" + results.Count + ".");
|
||||
|
||||
foreach (var result in results.OrderBy(r => r.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(r => r.Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -168,6 +199,9 @@ namespace HostAvailabilityMonitor
|
||||
|
||||
await TrySendDailySummaryAsync(config, notifier, stateDocument, currentState, results, logDirectory, logger, eventLogger).ConfigureAwait(false);
|
||||
stateStore.Save(stateDocument);
|
||||
logger.Info("Statusdatei gespeichert: " + stateFilePath);
|
||||
eventLogger.Info("Monitor-Status gespeichert. Umgebung=" + config.EnvironmentName + ", Pfad=" + stateFilePath + ".", 1004);
|
||||
WriteConsoleInfo("Statusdatei gespeichert: " + stateFilePath);
|
||||
|
||||
var total = results.Count(r => !r.IsSkipped);
|
||||
var ok = results.Count(r => r.IsSuccess);
|
||||
@@ -176,14 +210,22 @@ namespace HostAvailabilityMonitor
|
||||
var summary = config.ApplicationName + " Lauf beendet. Umgebung=" + config.EnvironmentName + ", Gesamt=" + total + ", Erfolgreich=" + ok + ", Fehlgeschlagen=" + failed + ", Uebersprungen=" + skipped + ".";
|
||||
logger.Info(summary);
|
||||
eventLogger.Summary(summary, failed > 0);
|
||||
WriteConsoleInfo(summary);
|
||||
|
||||
return failed > 0 && config.Runtime.NonZeroExitCodeOnAnyFailure ? 2 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteConsoleFatal("Monitorlauf fehlgeschlagen.", configPath, ex);
|
||||
|
||||
if (logger != null)
|
||||
{
|
||||
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
|
||||
logger.Error("Details: " + ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
TryWriteFallbackFailureLog(baseDirectory, configPath, ex);
|
||||
}
|
||||
|
||||
if (eventLogger != null)
|
||||
@@ -195,6 +237,13 @@ namespace HostAvailabilityMonitor
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var results = new List<ProbeResult>();
|
||||
@@ -220,6 +269,14 @@ namespace HostAvailabilityMonitor
|
||||
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)
|
||||
{
|
||||
var targets = config.Targets == null
|
||||
@@ -242,6 +299,13 @@ namespace HostAvailabilityMonitor
|
||||
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)
|
||||
{
|
||||
var targets = new List<TargetOptions>();
|
||||
@@ -297,6 +361,18 @@ namespace HostAvailabilityMonitor
|
||||
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(
|
||||
HipVirtualMachine vm,
|
||||
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)
|
||||
{
|
||||
if (Path.IsPathRooted(configuredPath))
|
||||
@@ -340,6 +423,18 @@ namespace HostAvailabilityMonitor
|
||||
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(
|
||||
MonitorConfiguration config,
|
||||
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)
|
||||
{
|
||||
var counters = new ProbeLogCounters();
|
||||
@@ -435,6 +538,12 @@ namespace HostAvailabilityMonitor
|
||||
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)
|
||||
{
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
@@ -477,6 +586,12 @@ namespace HostAvailabilityMonitor
|
||||
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)
|
||||
{
|
||||
if (Path.IsPathRooted(configuredPath))
|
||||
@@ -487,6 +602,12 @@ namespace HostAvailabilityMonitor
|
||||
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)
|
||||
{
|
||||
return string.Format(
|
||||
@@ -504,10 +625,122 @@ namespace HostAvailabilityMonitor
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets today's successful probe count.
|
||||
/// </summary>
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets today's failed probe count.
|
||||
/// </summary>
|
||||
public int FailureCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets today's skipped probe count.
|
||||
/// </summary>
|
||||
public int SkipCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// Assembly metadata for the HostAvailabilityMonitor console application.
|
||||
[assembly: AssemblyTitle("HostAvailabilityMonitor")]
|
||||
[assembly: AssemblyDescription("Windows Server 2019 kompatibler Host Availability Monitor auf .NET Framework 4.7.2")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
|
||||
@@ -5,15 +5,29 @@ using System.Web.Script.Serialization;
|
||||
|
||||
namespace HostAvailabilityMonitor.State
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads and saves the persisted monitor state file.
|
||||
/// </summary>
|
||||
public sealed class MonitorStateStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Full path to the JSON state file.
|
||||
/// </summary>
|
||||
private readonly string _filePath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new state store.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The state JSON file path.</param>
|
||||
public MonitorStateStore(string 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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (document == null)
|
||||
@@ -116,24 +147,66 @@ namespace HostAvailabilityMonitor.State
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Root object persisted in the monitor state file.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets state metadata that is not tied to one target.
|
||||
/// </summary>
|
||||
public MonitorStateMetadata Metadata { get; set; } = new MonitorStateMetadata();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains monitor state metadata shared across targets.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists the last known status and notification metadata for one target.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of consecutive failed probes.
|
||||
/// </summary>
|
||||
public int ConsecutiveFailures { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UTC time of the most recent check.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UTC time when the most recent notification was sent.
|
||||
/// </summary>
|
||||
public DateTime? LastNotificationUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the most recent probe detail text.
|
||||
/// </summary>
|
||||
public string LastDetail { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user