Updated to .NET Framework 4.7.2 due to platform limitations, added email cc, bcc and some fixes.

This commit is contained in:
2026-04-21 11:46:55 +02:00
parent c267069d02
commit b8b702bda3
40 changed files with 5666 additions and 2064 deletions
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
@@ -0,0 +1,517 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using BizTalkMonitorConfigGenerator.Configuration;
using BizTalkMonitorConfigGenerator.Logging;
using Microsoft.BizTalk.ExplorerOM;
namespace BizTalkMonitorConfigGenerator.BizTalk
{
public sealed class BizTalkCatalogDiscoveryService
{
private readonly BizTalkOptions _options;
private readonly OutputOptions _outputOptions;
private readonly GeneratorFileLogger _logger;
public BizTalkCatalogDiscoveryService(BizTalkOptions options, OutputOptions outputOptions, GeneratorFileLogger logger)
{
_options = options ?? new BizTalkOptions();
_outputOptions = outputOptions ?? new OutputOptions();
_logger = logger;
}
public IReadOnlyCollection<DiscoveredEndpoint> Discover()
{
var discovered = new List<DiscoveredEndpoint>();
var catalog = new BtsCatalogExplorer();
catalog.ConnectionString = _options.ConnectionString;
foreach (Application application in catalog.Applications)
{
if (application == null)
{
continue;
}
if (ShouldSkipApplication(application))
{
_logger.Info("BizTalk-Anwendung übersprungen: " + application.Name);
continue;
}
if (_options.IncludeSendPorts)
{
DiscoverSendPorts(application, discovered);
}
if (_options.IncludeReceiveLocations)
{
DiscoverReceiveLocations(application, discovered);
}
}
return discovered
.OrderBy(x => x.MappedApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.TargetName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.NormalizedEndpoint, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private void DiscoverSendPorts(Application application, List<DiscoveredEndpoint> discovered)
{
foreach (SendPort sendPort in application.SendPorts)
{
if (sendPort == null)
{
continue;
}
if (_options.OnlyStartedSendPorts && sendPort.Status != PortStatus.Started)
{
_logger.Discovery("SKIP", "SendPort", application.Name, sendPort.Name, ResolveAdapterName(sendPort.PrimaryTransport), SafeAddress(sendPort.PrimaryTransport), string.Empty, "SendPort ist nicht gestartet. Status=" + sendPort.Status);
continue;
}
if (sendPort.IsDynamic && !_options.IncludeDynamicSendPorts)
{
_logger.Discovery("SKIP", "SendPort", application.Name, sendPort.Name, ResolveAdapterName(sendPort.PrimaryTransport), SafeAddress(sendPort.PrimaryTransport), string.Empty, "Dynamischer SendPort wird per Konfiguration übersprungen.");
continue;
}
AddTransportIfSupported(discovered, application.Name, "SendPort", sendPort.Name, sendPort.Name, sendPort.PrimaryTransport, false);
if (_options.IncludeSecondaryTransportOnSendPorts)
{
AddTransportIfSupported(discovered, application.Name, "SendPortSecondary", sendPort.Name + " (Secondary Transport)", sendPort.Name, sendPort.SecondaryTransport, true);
}
}
}
private void DiscoverReceiveLocations(Application application, List<DiscoveredEndpoint> discovered)
{
foreach (ReceivePort receivePort in application.ReceivePorts)
{
if (receivePort == null)
{
continue;
}
foreach (ReceiveLocation receiveLocation in receivePort.ReceiveLocations)
{
if (receiveLocation == null)
{
continue;
}
if (_options.OnlyEnabledReceiveLocations && !receiveLocation.Enable)
{
_logger.Discovery("SKIP", "ReceiveLocation", application.Name, receiveLocation.Name, ResolveAdapterName(receiveLocation.TransportType), receiveLocation.Address, string.Empty, "Receive Location ist deaktiviert.");
continue;
}
if (ShouldSkipArtifact(receiveLocation.Name))
{
_logger.Discovery("SKIP", "ReceiveLocation", application.Name, receiveLocation.Name, ResolveAdapterName(receiveLocation.TransportType), receiveLocation.Address, string.Empty, "Receive Location entspricht einem Ignore-Pattern.");
continue;
}
AddEndpointIfSupported(
discovered,
application.Name,
"ReceiveLocation",
receivePort.Name + " / " + receiveLocation.Name,
receiveLocation.Name,
ResolveAdapterName(receiveLocation.TransportType),
receiveLocation.Address,
false);
}
}
}
private void AddTransportIfSupported(List<DiscoveredEndpoint> discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, TransportInfo transport, bool isSecondary)
{
if (transport == null)
{
_logger.Discovery("SKIP", artifactType, bizTalkApplicationName, artifactName, string.Empty, string.Empty, string.Empty, "Kein Transport konfiguriert.");
return;
}
AddEndpointIfSupported(
discovered,
bizTalkApplicationName,
artifactType,
targetNameBase + (isSecondary ? " (Secondary)" : " (Primary)"),
artifactName,
ResolveAdapterName(transport),
SafeAddress(transport),
isSecondary);
}
private void AddEndpointIfSupported(List<DiscoveredEndpoint> discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, string adapterName, string sourceAddress, bool isSecondary)
{
if (ShouldSkipArtifact(artifactName))
{
_logger.Discovery("SKIP", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, string.Empty, "Artefakt entspricht einem Ignore-Pattern.");
return;
}
string normalizedEndpoint;
int? port;
string httpMethod;
string reason;
if (!TryNormalizeEndpoint(sourceAddress, adapterName, out normalizedEndpoint, out port, out httpMethod, out reason))
{
_logger.Discovery("SKIP", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, string.Empty, reason);
return;
}
var mappedApplication = ResolveMappedApplicationName(bizTalkApplicationName, artifactType, artifactName);
var targetName = BuildTargetName(artifactType, targetNameBase, adapterName, isSecondary);
var endpoint = new DiscoveredEndpoint
{
ArtifactType = artifactType,
BizTalkApplicationName = bizTalkApplicationName,
MappedApplicationName = mappedApplication,
ArtifactName = artifactName,
AdapterName = adapterName,
SourceAddress = sourceAddress,
NormalizedEndpoint = normalizedEndpoint,
TargetName = targetName,
HttpMethod = httpMethod,
Port = port,
ValidateUncPathAccess = null,
Detail = "Endpoint erfolgreich aus BizTalk übernommen."
};
discovered.Add(endpoint);
_logger.Discovery("ADD", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, normalizedEndpoint, "Zuordnung zu ApplicationName='" + mappedApplication + "'.");
}
private bool ShouldSkipApplication(Application application)
{
if (application == null)
{
return true;
}
if (_options.IgnoreSystemApplications && application.IsSystem)
{
return true;
}
return _options.IgnoreApplications.Any(pattern => WildcardMatch(application.Name, pattern));
}
private bool ShouldSkipArtifact(string artifactName)
{
if (string.IsNullOrWhiteSpace(artifactName))
{
return false;
}
return _options.IgnoreArtifactNamePatterns.Any(pattern => WildcardMatch(artifactName, pattern));
}
private string ResolveMappedApplicationName(string bizTalkApplicationName, string artifactType, string artifactName)
{
foreach (var rule in _options.ArtifactMappings.Where(r => r != null && !string.IsNullOrWhiteSpace(r.ApplicationName)))
{
if (!string.IsNullOrWhiteSpace(rule.ArtifactType) && !string.Equals(rule.ArtifactType.Trim(), artifactType, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (WildcardMatch(artifactName, rule.ArtifactNamePattern))
{
return rule.ApplicationName.Trim();
}
}
foreach (var rule in _options.ApplicationMappings.Where(r => r != null && !string.IsNullOrWhiteSpace(r.ApplicationName)))
{
if (WildcardMatch(bizTalkApplicationName, rule.BizTalkApplicationName))
{
return rule.ApplicationName.Trim();
}
}
return string.IsNullOrWhiteSpace(bizTalkApplicationName) ? "Unassigned Application" : bizTalkApplicationName.Trim();
}
private string BuildTargetName(string artifactType, string baseName, string adapterName, bool isSecondary)
{
var prefix = string.Equals(artifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
? "ReceiveLocation"
: "SendPort";
var name = prefix + ": " + baseName;
if (!string.IsNullOrWhiteSpace(adapterName))
{
name += " [" + adapterName + "]";
}
if (isSecondary && name.IndexOf("Secondary", StringComparison.OrdinalIgnoreCase) < 0)
{
name += " [Secondary]";
}
return name;
}
private bool TryNormalizeEndpoint(string sourceAddress, string adapterName, out string normalizedEndpoint, out int? port, out string httpMethod, out string reason)
{
normalizedEndpoint = string.Empty;
port = null;
httpMethod = null;
reason = string.Empty;
var address = (sourceAddress ?? string.Empty).Trim();
var adapter = (adapterName ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(address))
{
reason = "Address/URI ist leer.";
return false;
}
if (AdapterContains(adapter, "SMTP"))
{
reason = "SMTP-Adapter wird uebersprungen, weil dessen Address-Wert keine pruefbare Netzwerkadresse, sondern eine Empfaengerliste ist.";
return false;
}
if (LooksLikeEmailAddressList(address))
{
reason = "Adresse sieht wie eine SMTP-/E-Mail-Empfaengerliste aus und wird uebersprungen.";
return false;
}
if (address.StartsWith("\\\\", StringComparison.Ordinal))
{
normalizedEndpoint = address;
return true;
}
if (Path.IsPathRooted(address))
{
if (!_options.IncludeLocalFilePaths)
{
reason = "Lokaler Dateipfad wird per Konfiguration nicht übernommen.";
return false;
}
normalizedEndpoint = address;
return true;
}
Uri uri;
if (Uri.TryCreate(address, UriKind.Absolute, out uri))
{
switch ((uri.Scheme ?? string.Empty).ToLowerInvariant())
{
case "http":
case "https":
normalizedEndpoint = uri.AbsoluteUri;
httpMethod = _outputOptions.DefaultHttpMethod;
return true;
case "ftp":
case "sftp":
case "tcp":
case "icmp":
case "file":
normalizedEndpoint = uri.IsAbsoluteUri ? uri.AbsoluteUri : address;
return true;
case "net.tcp":
if (string.IsNullOrWhiteSpace(uri.Host) || uri.Port <= 0)
{
reason = "net.tcp-Adresse konnte nicht auf Host/Port aufgelöst werden.";
return false;
}
normalizedEndpoint = "tcp://" + uri.Host + ":" + uri.Port + uri.PathAndQuery;
port = uri.Port;
return true;
case "net.pipe":
case "npipe":
reason = "Named-Pipe-Endpunkte werden vom Monitor nicht unterstützt.";
return false;
default:
reason = "Schema '" + uri.Scheme + "' wird vom Monitor nicht unterstützt.";
return false;
}
}
if (LooksLikeHostPort(address))
{
if (AdapterContains(adapter, "SFTP"))
{
normalizedEndpoint = "sftp://" + address;
port = ParsePort(address);
return true;
}
if (AdapterContains(adapter, "FTP"))
{
normalizedEndpoint = "ftp://" + address;
port = ParsePort(address);
return true;
}
normalizedEndpoint = "tcp://" + address;
port = ParsePort(address);
return true;
}
if (AdapterContains(adapter, "SFTP"))
{
normalizedEndpoint = "sftp://" + address.TrimStart('/');
return true;
}
if (AdapterContains(adapter, "FTP"))
{
normalizedEndpoint = "ftp://" + address.TrimStart('/');
return true;
}
if (AdapterContains(adapter, "HTTP") || AdapterContains(adapter, "SOAP") || AdapterContains(adapter, "WCF"))
{
reason = "HTTP/WCF-Adapteradresse konnte nicht als absolute URI erkannt werden.";
return false;
}
normalizedEndpoint = address;
return true;
}
private static string ResolveAdapterName(TransportInfo transport)
{
try
{
return transport == null || transport.TransportType == null ? string.Empty : transport.TransportType.Name;
}
catch
{
return string.Empty;
}
}
private static string ResolveAdapterName(ProtocolType transportType)
{
try
{
return transportType == null ? string.Empty : transportType.Name;
}
catch
{
return string.Empty;
}
}
private static string SafeAddress(TransportInfo transport)
{
try
{
return transport == null ? string.Empty : transport.Address;
}
catch
{
return string.Empty;
}
}
private static bool AdapterContains(string adapterName, string fragment)
{
return !string.IsNullOrWhiteSpace(adapterName)
&& adapterName.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool LooksLikeEmailAddressList(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var trimmed = value.Trim();
if (trimmed.IndexOf('@') < 0)
{
return false;
}
if (trimmed.IndexOf("http://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("https://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("sftp://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("ftp://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("tcp://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("file://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.StartsWith("\\", StringComparison.Ordinal))
{
return false;
}
var entries = trimmed.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
if (entries.Length == 0)
{
return false;
}
foreach (var entry in entries)
{
var part = entry.Trim();
if (string.IsNullOrWhiteSpace(part))
{
continue;
}
if (part.IndexOf('@') <= 0 || part.IndexOf(' ') >= 0)
{
return false;
}
}
return true;
}
private static bool WildcardMatch(string input, string pattern)
{
if (string.IsNullOrWhiteSpace(pattern))
{
return false;
}
input = input ?? string.Empty;
var regex = "^" + Regex.Escape(pattern.Trim()).Replace("\\*", ".*").Replace("\\?", ".") + "$";
return Regex.IsMatch(input, regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private static bool LooksLikeHostPort(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
return Regex.IsMatch(value.Trim(), @"^[a-zA-Z0-9._-]+:\d{1,5}($|/.*$)");
}
private static int? ParsePort(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var match = Regex.Match(value, @":(?<port>\d{1,5})(?:$|/)");
int parsed;
if (match.Success && int.TryParse(match.Groups["port"].Value, out parsed))
{
return parsed;
}
return null;
}
}
}
@@ -0,0 +1,18 @@
namespace BizTalkMonitorConfigGenerator.BizTalk
{
public sealed class DiscoveredEndpoint
{
public string ArtifactType { get; set; }
public string BizTalkApplicationName { get; set; }
public string MappedApplicationName { get; set; }
public string ArtifactName { get; set; }
public string AdapterName { get; set; }
public string SourceAddress { get; set; }
public string NormalizedEndpoint { get; set; }
public string TargetName { get; set; }
public string HttpMethod { get; set; }
public int? Port { get; set; }
public bool? ValidateUncPathAccess { get; set; }
public string Detail { get; set; }
}
}
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{E8C93E45-DA2B-4E30-8891-637A2C7FAE0B}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkMonitorConfigGenerator</RootNamespace>
<AssemblyName>BizTalkMonitorConfigGenerator</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.BizTalk.ExplorerOM" />
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Web.Extensions" />
</ItemGroup>
<ItemGroup>
<Compile Include="BizTalk\BizTalkCatalogDiscoveryService.cs" />
<Compile Include="BizTalk\DiscoveredEndpoint.cs" />
<Compile Include="Configuration\GeneratorConfiguration.cs" />
<Compile Include="Logging\GeneratorFileLogger.cs" />
<Compile Include="Logging\GeneratorEventLogger.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Serialization\JsonFileWriter.cs" />
<Compile Include="..\HostAvailabilityMonitor\Configuration\MonitorConfiguration.cs">
<Link>Shared\MonitorConfiguration.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<Content Include="generator-settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -0,0 +1,315 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Script.Serialization;
using HostAvailabilityMonitor.Configuration;
namespace BizTalkMonitorConfigGenerator.Configuration
{
public sealed class GeneratorConfiguration
{
public string GeneratorName { get; set; } = "BizTalkMonitorConfigGenerator";
public string EnvironmentName { get; set; } = "Unspecified Environment";
public GeneratorLoggingOptions Logging { get; set; } = new GeneratorLoggingOptions();
public GeneratorEventLogOptions EventLog { get; set; } = new GeneratorEventLogOptions();
public BizTalkOptions BizTalk { get; set; } = new BizTalkOptions();
public OutputOptions Output { get; set; } = new OutputOptions();
public MonitorConfiguration GeneratedMonitorConfig { get; set; } = new MonitorConfiguration();
public static GeneratorConfiguration Load(string path)
{
if (!File.Exists(path))
{
throw new FileNotFoundException("Konfigurationsdatei wurde nicht gefunden: " + path, path);
}
var json = File.ReadAllText(path);
var serializer = new JavaScriptSerializer();
var config = serializer.Deserialize<GeneratorConfiguration>(json);
if (config == null)
{
throw new InvalidOperationException("Generator-Konfiguration konnte nicht deserialisiert werden.");
}
config.Logging = config.Logging ?? new GeneratorLoggingOptions();
config.EventLog = config.EventLog ?? new GeneratorEventLogOptions();
config.BizTalk = config.BizTalk ?? new BizTalkOptions();
config.Output = config.Output ?? new OutputOptions();
config.GeneratedMonitorConfig = config.GeneratedMonitorConfig ?? new MonitorConfiguration();
config.GeneratedMonitorConfig.Runtime = config.GeneratedMonitorConfig.Runtime ?? new RuntimeOptions();
config.GeneratedMonitorConfig.Logging = config.GeneratedMonitorConfig.Logging ?? new LoggingOptions();
config.GeneratedMonitorConfig.EventLog = config.GeneratedMonitorConfig.EventLog ?? new EventLogOptions();
config.GeneratedMonitorConfig.Email = config.GeneratedMonitorConfig.Email ?? new EmailOptions();
config.GeneratedMonitorConfig.Email.To = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.To);
config.GeneratedMonitorConfig.Email.Cc = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.Cc);
config.GeneratedMonitorConfig.Email.Bcc = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.Bcc);
config.GeneratedMonitorConfig.Targets = config.GeneratedMonitorConfig.Targets ?? new List<TargetOptions>();
config.BizTalk.IgnoreApplications = config.BizTalk.IgnoreApplications ?? new List<string>();
config.BizTalk.IgnoreArtifactNamePatterns = config.BizTalk.IgnoreArtifactNamePatterns ?? new List<string>();
config.BizTalk.ApplicationMappings = config.BizTalk.ApplicationMappings ?? new List<ApplicationMappingRule>();
config.BizTalk.ArtifactMappings = config.BizTalk.ArtifactMappings ?? new List<ArtifactMappingRule>();
if (string.IsNullOrWhiteSpace(config.GeneratorName))
{
config.GeneratorName = "BizTalkMonitorConfigGenerator";
}
if (string.IsNullOrWhiteSpace(config.EnvironmentName))
{
config.EnvironmentName = "Unspecified Environment";
}
if (string.IsNullOrWhiteSpace(config.GeneratedMonitorConfig.ApplicationName))
{
config.GeneratedMonitorConfig.ApplicationName = "HostAvailabilityMonitor";
}
if (string.IsNullOrWhiteSpace(config.GeneratedMonitorConfig.EnvironmentName))
{
config.GeneratedMonitorConfig.EnvironmentName = config.EnvironmentName;
}
return config;
}
public void Validate()
{
if (string.IsNullOrWhiteSpace(BizTalk.ConnectionString))
{
throw new InvalidOperationException("BizTalk.ConnectionString ist leer.");
}
if (string.IsNullOrWhiteSpace(Output.Path))
{
throw new InvalidOperationException("Output.Path ist leer.");
}
if (!BizTalk.IncludeSendPorts && !BizTalk.IncludeReceiveLocations)
{
throw new InvalidOperationException("Mindestens eine Quelle muss aktiviert sein: IncludeSendPorts oder IncludeReceiveLocations.");
}
if (Logging.RetentionDays < 0)
{
throw new InvalidOperationException("Logging.RetentionDays darf nicht negativ sein.");
}
if (Output.TargetTimeoutSeconds <= 0)
{
Output.TargetTimeoutSeconds = 10;
}
if (string.IsNullOrWhiteSpace(Output.DefaultHttpMethod))
{
Output.DefaultHttpMethod = "HEAD";
}
if (GeneratedMonitorConfig.Targets == null)
{
GeneratedMonitorConfig.Targets = new List<TargetOptions>();
}
GeneratedMonitorConfig.ValidateTemplateWithoutTargets();
}
}
internal static class RecipientListHelper
{
internal static List<string> SanitizeRecipientList(List<string> values)
{
if (values == null)
{
return new List<string>();
}
return values
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
internal static int GetConfiguredRecipientCount(EmailOptions email)
{
if (email == null)
{
return 0;
}
return SanitizeRecipientList(email.To).Count
+ SanitizeRecipientList(email.Cc).Count
+ SanitizeRecipientList(email.Bcc).Count;
}
}
public sealed class GeneratorLoggingOptions
{
public string LogDirectory { get; set; } = "logs";
public string FilePrefix { get; set; } = "biztalk-monitor-config-generator";
public int RetentionDays { get; set; } = 5;
}
public sealed class GeneratorEventLogOptions
{
public bool Enabled { get; set; } = true;
public string LogName { get; set; } = "Application";
public string Source { get; set; } = "BizTalkMonitorConfigGenerator";
public bool CreateSourceIfMissing { get; set; } = false;
public bool WriteSuccessEntries { get; set; } = false;
public bool WriteSummaryEntries { get; set; } = true;
}
public sealed class BizTalkOptions
{
public string ConnectionString { get; set; } = "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI";
public bool IncludeSendPorts { get; set; } = true;
public bool IncludeReceiveLocations { get; set; } = true;
public bool OnlyStartedSendPorts { get; set; } = true;
public bool OnlyEnabledReceiveLocations { get; set; } = true;
public bool IncludeSecondaryTransportOnSendPorts { get; set; } = true;
public bool IncludeDynamicSendPorts { get; set; } = false;
public bool IncludeLocalFilePaths { get; set; } = false;
public bool IgnoreSystemApplications { get; set; } = true;
public List<string> IgnoreApplications { get; set; } = new List<string>();
public List<string> IgnoreArtifactNamePatterns { get; set; } = new List<string>();
public List<ApplicationMappingRule> ApplicationMappings { get; set; } = new List<ApplicationMappingRule>();
public List<ArtifactMappingRule> ArtifactMappings { get; set; } = new List<ArtifactMappingRule>();
}
public sealed class OutputOptions
{
public string Path { get; set; } = "generated-monitor-appsettings.json";
public bool OverwriteExisting { get; set; } = true;
public bool FailIfNoTargetsFound { get; set; } = false;
public int TargetTimeoutSeconds { get; set; } = 10;
public bool TargetEnabled { get; set; } = true;
public string DefaultHttpMethod { get; set; } = "HEAD";
}
public sealed class ApplicationMappingRule
{
public string BizTalkApplicationName { get; set; } = string.Empty;
public string ApplicationName { get; set; } = string.Empty;
}
public sealed class ArtifactMappingRule
{
public string ArtifactType { get; set; } = string.Empty;
public string ArtifactNamePattern { get; set; } = string.Empty;
public string ApplicationName { get; set; } = string.Empty;
}
internal static class MonitorConfigurationTemplateExtensions
{
public static void ValidateTemplateWithoutTargets(this MonitorConfiguration configuration)
{
if (configuration == null)
{
throw new InvalidOperationException("GeneratedMonitorConfig ist null.");
}
configuration.Runtime = configuration.Runtime ?? new RuntimeOptions();
configuration.Logging = configuration.Logging ?? new LoggingOptions();
configuration.EventLog = configuration.EventLog ?? new EventLogOptions();
configuration.Email = configuration.Email ?? new EmailOptions();
configuration.Targets = configuration.Targets ?? new List<TargetOptions>();
if (configuration.Runtime.MaxConcurrency <= 0)
{
configuration.Runtime.MaxConcurrency = 1;
}
if (configuration.Logging.RetentionDays < 0)
{
throw new InvalidOperationException("GeneratedMonitorConfig.Logging.RetentionDays darf nicht negativ sein.");
}
if (configuration.Email.Enabled)
{
if (string.IsNullOrWhiteSpace(configuration.Email.SmtpHost))
{
throw new InvalidOperationException("GeneratedMonitorConfig.Email.Enabled=true, aber SmtpHost ist leer.");
}
if (string.IsNullOrWhiteSpace(configuration.Email.From))
{
throw new InvalidOperationException("GeneratedMonitorConfig.Email.Enabled=true, aber From ist leer.");
}
if (RecipientListHelper.GetConfiguredRecipientCount(configuration.Email) <= 0)
{
throw new InvalidOperationException("GeneratedMonitorConfig.Email.Enabled=true, aber es ist kein Empfänger in Email.To, Email.Cc oder Email.Bcc definiert.");
}
if (configuration.Email.DailySummaryHourLocal < 0 || configuration.Email.DailySummaryHourLocal > 23)
{
throw new InvalidOperationException("GeneratedMonitorConfig.Email.DailySummaryHourLocal muss zwischen 0 und 23 liegen.");
}
if (configuration.Email.DailySummaryMinuteLocal < 0 || configuration.Email.DailySummaryMinuteLocal > 59)
{
throw new InvalidOperationException("GeneratedMonitorConfig.Email.DailySummaryMinuteLocal muss zwischen 0 und 59 liegen.");
}
}
}
public static MonitorConfiguration CloneWithoutTargets(this MonitorConfiguration configuration)
{
var clone = new MonitorConfiguration
{
ApplicationName = configuration.ApplicationName,
EnvironmentName = configuration.EnvironmentName,
Runtime = new RuntimeOptions
{
MaxConcurrency = configuration.Runtime.MaxConcurrency,
DefaultValidateUncPathAccess = configuration.Runtime.DefaultValidateUncPathAccess,
NonZeroExitCodeOnAnyFailure = configuration.Runtime.NonZeroExitCodeOnAnyFailure,
NonZeroExitCodeOnExecutionError = configuration.Runtime.NonZeroExitCodeOnExecutionError,
StateDirectory = configuration.Runtime.StateDirectory
},
Logging = new LoggingOptions
{
LogDirectory = configuration.Logging.LogDirectory,
FilePrefix = configuration.Logging.FilePrefix,
RetentionDays = configuration.Logging.RetentionDays
},
EventLog = new EventLogOptions
{
Enabled = configuration.EventLog.Enabled,
LogName = configuration.EventLog.LogName,
Source = configuration.EventLog.Source,
CreateSourceIfMissing = configuration.EventLog.CreateSourceIfMissing,
WriteSuccessEntries = configuration.EventLog.WriteSuccessEntries,
WriteSummaryEntries = configuration.EventLog.WriteSummaryEntries
},
Email = new EmailOptions
{
Enabled = configuration.Email.Enabled,
SmtpHost = configuration.Email.SmtpHost,
SmtpPort = configuration.Email.SmtpPort,
UseSsl = configuration.Email.UseSsl,
UseDefaultCredentials = configuration.Email.UseDefaultCredentials,
Username = configuration.Email.Username,
Password = configuration.Email.Password,
From = configuration.Email.From,
To = configuration.Email.To == null ? new List<string>() : configuration.Email.To.ToList(),
Cc = configuration.Email.Cc == null ? new List<string>() : configuration.Email.Cc.ToList(),
Bcc = configuration.Email.Bcc == null ? new List<string>() : configuration.Email.Bcc.ToList(),
SubjectPrefix = configuration.Email.SubjectPrefix,
SendOnFailure = configuration.Email.SendOnFailure,
SendOnRecovery = configuration.Email.SendOnRecovery,
SendDailySummary = configuration.Email.SendDailySummary,
DailySummaryHourLocal = configuration.Email.DailySummaryHourLocal,
DailySummaryMinuteLocal = configuration.Email.DailySummaryMinuteLocal,
OnlyOnStateChange = configuration.Email.OnlyOnStateChange,
CooldownMinutes = configuration.Email.CooldownMinutes,
TimeoutSeconds = configuration.Email.TimeoutSeconds
},
Targets = new List<TargetOptions>()
};
return clone;
}
}
}
@@ -0,0 +1,115 @@
using System;
using System.Diagnostics;
using System.Security;
using BizTalkMonitorConfigGenerator.Configuration;
namespace BizTalkMonitorConfigGenerator.Logging
{
public sealed class GeneratorEventLogger
{
private readonly GeneratorEventLogOptions _options;
private readonly GeneratorFileLogger _fallbackLogger;
private bool _disabled;
public GeneratorEventLogger(GeneratorEventLogOptions options, GeneratorFileLogger fallbackLogger)
{
_options = options ?? new GeneratorEventLogOptions();
_fallbackLogger = fallbackLogger;
_disabled = !_options.Enabled;
}
public void TryInitialize()
{
if (_disabled || !_options.CreateSourceIfMissing)
{
return;
}
try
{
if (!EventLog.SourceExists(_options.Source))
{
EventLog.CreateEventSource(new EventSourceCreationData(_options.Source, _options.LogName));
_fallbackLogger.Info("EventLog-Quelle '" + _options.Source + "' wurde neu angelegt.");
}
}
catch (SecurityException ex)
{
_fallbackLogger.Warn("EventLog-Quelle konnte nicht geprüft/angelegt werden (fehlende Rechte). " + ex.Message);
}
catch (Exception ex)
{
_fallbackLogger.Warn("EventLog-Initialisierung fehlgeschlagen. " + ex.GetType().Name + ": " + ex.Message);
}
}
public void Info(string message, int eventId)
{
Write(message, EventLogEntryType.Information, eventId, true);
}
public void InfoIfSuccessEnabled(string message, int eventId)
{
if (_options.WriteSuccessEntries)
{
Write(message, EventLogEntryType.Information, eventId, true);
}
}
public void Warning(string message, int eventId)
{
Write(message, EventLogEntryType.Warning, eventId, true);
}
public void Error(string message, int eventId)
{
Write(message, EventLogEntryType.Error, eventId, true);
}
public void Summary(string message, bool hasWarnings)
{
if (!_options.WriteSummaryEntries)
{
return;
}
Write(message, hasWarnings ? EventLogEntryType.Warning : EventLogEntryType.Information, hasWarnings ? 2100 : 1100, true);
}
private void Write(string message, EventLogEntryType entryType, int eventId, bool forceWrite)
{
if (_disabled || !forceWrite)
{
return;
}
try
{
EventLog.WriteEntry(_options.Source, message, entryType, eventId);
}
catch (SecurityException ex)
{
DisableAndFallback("EventLog-Schreiben fehlgeschlagen (SecurityException). " + ex.Message);
}
catch (ArgumentException ex)
{
DisableAndFallback("EventLog-Schreiben fehlgeschlagen. Vermutlich fehlt die Event Source '" + _options.Source + "'. " + ex.Message);
}
catch (Exception ex)
{
DisableAndFallback("EventLog-Schreiben fehlgeschlagen. " + ex.GetType().Name + ": " + ex.Message);
}
}
private void DisableAndFallback(string reason)
{
if (_disabled)
{
return;
}
_disabled = true;
_fallbackLogger.Warn(reason);
}
}
}
@@ -0,0 +1,164 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading;
namespace BizTalkMonitorConfigGenerator.Logging
{
public sealed class GeneratorFileLogger
{
private readonly object _sync = new object();
private readonly string _directory;
private readonly string _filePrefix;
private readonly int _retentionDays;
private readonly string _generatorName;
private readonly string _environmentName;
public GeneratorFileLogger(string directory, string filePrefix, int retentionDays, string generatorName, string environmentName)
{
_directory = directory;
_filePrefix = filePrefix;
_retentionDays = retentionDays;
_generatorName = string.IsNullOrWhiteSpace(generatorName) ? "BizTalkMonitorConfigGenerator" : generatorName.Trim();
_environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim();
Directory.CreateDirectory(_directory);
}
public void CleanupOldFiles()
{
if (_retentionDays <= 0)
{
return;
}
var cutoff = DateTime.Today.AddDays(-Math.Max(0, _retentionDays - 1));
foreach (var file in Directory.EnumerateFiles(_directory, _filePrefix + "-*.log", SearchOption.TopDirectoryOnly))
{
try
{
DateTime fileDate;
if (TryGetDateFromFileName(file, out fileDate))
{
if (fileDate < cutoff)
{
File.Delete(file);
}
}
else if (File.GetLastWriteTime(file).Date < cutoff)
{
File.Delete(file);
}
}
catch
{
}
}
}
public void Info(string message)
{
Write("INFO", message);
}
public void Warn(string message)
{
Write("WARN", message);
}
public void Error(string message)
{
Write("ERROR", message);
}
public void Discovery(string action, string artifactType, string applicationName, string artifactName, string adapterName, string sourceAddress, string normalizedEndpoint, string detail)
{
Write("DISCOVERY", string.Format(
CultureInfo.InvariantCulture,
"Action={0} | ArtifactType={1} | BizTalkApplication={2} | ArtifactName={3} | Adapter={4} | SourceAddress={5} | NormalizedEndpoint={6} | Detail={7}",
Escape(action),
Escape(artifactType),
Escape(applicationName),
Escape(artifactName),
Escape(adapterName),
Escape(sourceAddress),
Escape(normalizedEndpoint),
Escape(detail)));
}
private void Write(string level, string message)
{
var line = string.Format(
CultureInfo.InvariantCulture,
"{0:yyyy-MM-dd HH:mm:ss.fff zzz} | {1} | Generator={2} | Environment={3} | {4}{5}",
DateTimeOffset.Now,
level,
Escape(_generatorName),
Escape(_environmentName),
message,
Environment.NewLine);
var path = GetCurrentLogFilePath();
lock (_sync)
{
AppendWithRetries(path, line);
}
}
private void AppendWithRetries(string path, string line)
{
Exception lastException = null;
for (var attempt = 1; attempt <= 3; attempt++)
{
try
{
File.AppendAllText(path, line);
return;
}
catch (IOException ex)
{
lastException = ex;
Thread.Sleep(100 * attempt);
}
catch (UnauthorizedAccessException ex)
{
lastException = ex;
Thread.Sleep(100 * attempt);
}
}
if (lastException != null)
{
throw lastException;
}
}
private string GetCurrentLogFilePath()
{
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
return Path.Combine(_directory, fileName);
}
private bool TryGetDateFromFileName(string path, out DateTime date)
{
date = DateTime.MinValue;
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
var prefix = _filePrefix + "-";
if (!fileNameWithoutExtension.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return DateTime.TryParseExact(
fileNameWithoutExtension.Substring(prefix.Length),
"yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out date);
}
private static string Escape(string value)
{
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
}
}
}
@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BizTalkMonitorConfigGenerator.BizTalk;
using BizTalkMonitorConfigGenerator.Configuration;
using BizTalkMonitorConfigGenerator.Logging;
using BizTalkMonitorConfigGenerator.Serialization;
using HostAvailabilityMonitor.Configuration;
namespace BizTalkMonitorConfigGenerator
{
internal static class Program
{
private static int Main(string[] args)
{
GeneratorFileLogger logger = null;
GeneratorEventLogger eventLogger = null;
try
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
? args[0]
: Path.Combine(baseDirectory, "generator-settings.json");
var configuration = GeneratorConfiguration.Load(configPath);
configuration.Validate();
var logDirectory = ResolvePath(baseDirectory, configuration.Logging.LogDirectory);
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));
eventLogger = new GeneratorEventLogger(configuration.EventLog, logger);
eventLogger.TryInitialize();
eventLogger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000);
var discoveryService = new BizTalkCatalogDiscoveryService(configuration.BizTalk, configuration.Output, logger);
var discovered = discoveryService.Discover();
logger.Info("Discovery abgeschlossen. Gefundene unterstützte Targets=" + discovered.Count + ".");
var outputConfiguration = BuildMonitorConfiguration(configuration, discovered);
var outputPath = ResolvePath(baseDirectory, configuration.Output.Path);
JsonFileWriter.WritePrettyJsonAtomic(outputPath, outputConfiguration, configuration.Output.OverwriteExisting);
logger.Info("Monitor-Konfiguration geschrieben: " + outputPath);
eventLogger.InfoIfSuccessEnabled("Monitor-Konfiguration erfolgreich generiert: " + outputPath, 1001);
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);
return configuration.Output.FailIfNoTargetsFound ? 2 : 0;
}
var summary = string.Format(
"Generatorlauf erfolgreich beendet. Umgebung={0}, GesamtTargets={1}, Ausgabedatei={2}",
configuration.EnvironmentName,
discovered.Count,
outputPath);
logger.Info(summary);
eventLogger.Summary(summary, false);
return 0;
}
catch (Exception ex)
{
if (logger != null)
{
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
}
if (eventLogger != null)
{
eventLogger.Error("Fataler Fehler: " + ex.Message, 9000);
}
return 1;
}
}
private static MonitorConfiguration BuildMonitorConfiguration(GeneratorConfiguration configuration, IReadOnlyCollection<DiscoveredEndpoint> discovered)
{
var result = configuration.GeneratedMonitorConfig.CloneWithoutTargets();
result.EnvironmentName = string.IsNullOrWhiteSpace(configuration.GeneratedMonitorConfig.EnvironmentName)
? configuration.EnvironmentName
: configuration.GeneratedMonitorConfig.EnvironmentName;
var targets = new List<TargetOptions>();
foreach (var item in discovered)
{
var target = new TargetOptions
{
Name = item.TargetName,
ApplicationName = item.MappedApplicationName,
Endpoint = item.NormalizedEndpoint,
TimeoutSeconds = configuration.Output.TargetTimeoutSeconds,
Port = item.Port,
ValidateUncPathAccess = item.ValidateUncPathAccess,
HttpMethod = item.HttpMethod,
Enabled = configuration.Output.TargetEnabled
};
targets.Add(target);
}
result.Targets = targets
.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Endpoint, StringComparer.OrdinalIgnoreCase)
.ToList();
return result;
}
private static string ResolvePath(string baseDirectory, string configuredPath)
{
if (Path.IsPathRooted(configuredPath))
{
return configuredPath;
}
return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath));
}
}
}
@@ -0,0 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("BizTalkMonitorConfigGenerator")]
[assembly: AssemblyDescription("Generates HostAvailabilityMonitor JSON config from active BizTalk send ports and receive locations.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("JR IT Services")]
[assembly: AssemblyProduct("BizTalkMonitorConfigGenerator")]
[assembly: AssemblyCopyright("Copyright © JR IT Services")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("f03c8d6d-1619-4cb6-b8a2-19d96473fddd")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -0,0 +1,137 @@
using System;
using System.IO;
using System.Text;
using System.Web.Script.Serialization;
namespace BizTalkMonitorConfigGenerator.Serialization
{
public static class JsonFileWriter
{
public static void WritePrettyJsonAtomic(string path, object value, bool overwriteExisting)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("Pfad ist leer.", nameof(path));
}
var fullPath = Path.GetFullPath(path);
var directory = Path.GetDirectoryName(fullPath);
if (string.IsNullOrWhiteSpace(directory))
{
throw new InvalidOperationException("Aus dem Zielpfad konnte kein Verzeichnis aufgelöst werden: " + path);
}
Directory.CreateDirectory(directory);
if (File.Exists(fullPath) && !overwriteExisting)
{
throw new IOException("Zieldatei existiert bereits und OverwriteExisting=false: " + fullPath);
}
var serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = int.MaxValue;
var compact = serializer.Serialize(value);
var pretty = PrettyPrint(compact);
var tempPath = fullPath + ".tmp";
File.WriteAllText(tempPath, pretty, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
if (File.Exists(fullPath))
{
File.Copy(fullPath, fullPath + ".bak", true);
File.Delete(fullPath);
}
File.Move(tempPath, fullPath);
}
private static string PrettyPrint(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return string.Empty;
}
var sb = new StringBuilder();
var indent = 0;
var quoted = false;
for (var i = 0; i < json.Length; i++)
{
var ch = json[i];
switch (ch)
{
case '{':
case '[':
sb.Append(ch);
if (!quoted)
{
sb.AppendLine();
indent++;
AppendIndent(sb, indent);
}
break;
case '}':
case ']':
if (!quoted)
{
sb.AppendLine();
indent--;
AppendIndent(sb, indent);
sb.Append(ch);
}
else
{
sb.Append(ch);
}
break;
case '"':
sb.Append(ch);
if (i > 0 && json[i - 1] == '\\')
{
break;
}
quoted = !quoted;
break;
case ',':
sb.Append(ch);
if (!quoted)
{
sb.AppendLine();
AppendIndent(sb, indent);
}
break;
case ':':
if (quoted)
{
sb.Append(ch);
}
else
{
sb.Append(": ");
}
break;
default:
if (!quoted && char.IsWhiteSpace(ch))
{
break;
}
sb.Append(ch);
break;
}
}
return sb.ToString();
}
private static void AppendIndent(StringBuilder sb, int indent)
{
for (var i = 0; i < indent; i++)
{
sb.Append(" ");
}
}
}
}
@@ -0,0 +1,99 @@
{
"GeneratorName": "BizTalkMonitorConfigGenerator",
"EnvironmentName": "HIP Produktion",
"Logging": {
"LogDirectory": "logs",
"FilePrefix": "biztalk-monitor-config-generator",
"RetentionDays": 5
},
"EventLog": {
"Enabled": true,
"LogName": "Application",
"Source": "BizTalkMonitorConfigGenerator",
"CreateSourceIfMissing": false,
"WriteSuccessEntries": false,
"WriteSummaryEntries": true
},
"BizTalk": {
"ConnectionString": "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI",
"IncludeSendPorts": true,
"IncludeReceiveLocations": true,
"OnlyStartedSendPorts": true,
"OnlyEnabledReceiveLocations": true,
"IncludeSecondaryTransportOnSendPorts": true,
"IncludeDynamicSendPorts": false,
"IncludeLocalFilePaths": false,
"IgnoreSystemApplications": true,
"IgnoreApplications": [
"BizTalk.System"
],
"IgnoreArtifactNamePatterns": [],
"ApplicationMappings": [
{
"BizTalkApplicationName": "HIP*",
"ApplicationName": "HIP"
}
],
"ArtifactMappings": []
},
"Output": {
"Path": "generated-monitor-appsettings.json",
"OverwriteExisting": true,
"FailIfNoTargetsFound": false,
"TargetTimeoutSeconds": 10,
"TargetEnabled": true,
"DefaultHttpMethod": "HEAD"
},
"GeneratedMonitorConfig": {
"ApplicationName": "HostAvailabilityMonitor",
"EnvironmentName": "HIP Produktion",
"Runtime": {
"MaxConcurrency": 4,
"DefaultValidateUncPathAccess": false,
"NonZeroExitCodeOnAnyFailure": false,
"NonZeroExitCodeOnExecutionError": true,
"StateDirectory": "state"
},
"Logging": {
"LogDirectory": "logs",
"FilePrefix": "host-availability-monitor",
"RetentionDays": 5
},
"EventLog": {
"Enabled": true,
"LogName": "Application",
"Source": "HostAvailabilityMonitor",
"CreateSourceIfMissing": false,
"WriteSuccessEntries": false,
"WriteSummaryEntries": true
},
"Email": {
"Enabled": false,
"SmtpHost": "smtp.example.local",
"SmtpPort": 25,
"UseSsl": false,
"UseDefaultCredentials": false,
"Username": "",
"Password": "",
"From": "monitor@example.local",
"To": [
"operations@example.local",
"integration-oncall@example.local"
],
"Cc": [
"integration-leads@example.local"
],
"Bcc": [],
"SubjectPrefix": "[HostAvailabilityMonitor]",
"SendOnFailure": true,
"SendOnRecovery": true,
"SendDailySummary": true,
"DailySummaryHourLocal": 14,
"DailySummaryMinuteLocal": 0,
"OnlyOnStateChange": true,
"CooldownMinutes": 0,
"TimeoutSeconds": 30
},
"Targets": []
}
}
+6 -6
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
@@ -1,179 +1,272 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Web.Script.Serialization;
namespace HostAvailabilityMonitor.Configuration
{
public sealed class MonitorConfiguration
{
public string ApplicationName { get; set; } = "HostAvailabilityMonitor";
public RuntimeOptions Runtime { get; set; } = new RuntimeOptions();
public LoggingOptions Logging { get; set; } = new LoggingOptions();
public EventLogOptions EventLog { get; set; } = new EventLogOptions();
public EmailOptions Email { get; set; } = new EmailOptions();
public List<TargetOptions> Targets { get; set; } = new List<TargetOptions>();
public static MonitorConfiguration Load(string path)
{
if (!File.Exists(path))
{
throw new FileNotFoundException("Konfigurationsdatei wurde nicht gefunden: " + path, path);
}
var json = File.ReadAllText(path);
var serializer = new JavaScriptSerializer();
var config = serializer.Deserialize<MonitorConfiguration>(json);
if (config == null)
{
throw new InvalidOperationException("Konfigurationsdatei konnte nicht deserialisiert werden.");
}
config.Runtime = config.Runtime ?? new RuntimeOptions();
config.Logging = config.Logging ?? new LoggingOptions();
config.EventLog = config.EventLog ?? new EventLogOptions();
config.Email = config.Email ?? new EmailOptions();
config.Targets = config.Targets ?? new List<TargetOptions>();
foreach (var target in config.Targets)
{
if (target == null)
{
continue;
}
if (target.TimeoutSeconds <= 0)
{
target.TimeoutSeconds = 10;
}
}
return config;
}
public void Validate()
{
if (Targets == null || Targets.Count == 0)
{
throw new InvalidOperationException("Es wurde kein Ziel in der Konfiguration definiert.");
}
foreach (var target in Targets)
{
if (target == null)
{
throw new InvalidOperationException("Die Konfiguration enthaelt ein leeres Target-Objekt.");
}
if (string.IsNullOrWhiteSpace(target.Endpoint))
{
throw new InvalidOperationException("Mindestens ein Target hat keinen Endpoint-Wert.");
}
if (target.TimeoutSeconds <= 0)
{
throw new InvalidOperationException("Target '" + target.DisplayName + "' hat einen ungueltigen TimeoutSeconds-Wert.");
}
}
if (Runtime.MaxConcurrency <= 0)
{
Runtime.MaxConcurrency = 1;
}
if (Logging.RetentionDays < 0)
{
throw new InvalidOperationException("Logging.RetentionDays darf nicht negativ sein.");
}
if (Email.Enabled)
{
if (string.IsNullOrWhiteSpace(Email.SmtpHost))
{
throw new InvalidOperationException("Email.Enabled=true, aber SmtpHost ist leer.");
}
if (string.IsNullOrWhiteSpace(Email.From))
{
throw new InvalidOperationException("Email.Enabled=true, aber From ist leer.");
}
if (Email.To == null || Email.To.Count == 0)
{
throw new InvalidOperationException("Email.Enabled=true, aber es sind keine Empfaenger in Email.To konfiguriert.");
}
if (Email.TimeoutSeconds <= 0)
{
Email.TimeoutSeconds = 30;
}
}
}
}
public sealed class RuntimeOptions
{
public int MaxConcurrency { get; set; } = 4;
public bool DefaultValidateUncPathAccess { get; set; } = false;
public bool NonZeroExitCodeOnAnyFailure { get; set; } = false;
public bool NonZeroExitCodeOnExecutionError { get; set; } = true;
public string StateDirectory { get; set; } = "state";
}
public sealed class LoggingOptions
{
public string LogDirectory { get; set; } = "logs";
public string FilePrefix { get; set; } = "host-availability-monitor";
public int RetentionDays { get; set; } = 5;
}
public sealed class EventLogOptions
{
public bool Enabled { get; set; } = true;
public string LogName { get; set; } = "Application";
public string Source { get; set; } = "HostAvailabilityMonitor";
public bool CreateSourceIfMissing { get; set; } = false;
public bool WriteSuccessEntries { get; set; } = false;
public bool WriteSummaryEntries { get; set; } = true;
}
public sealed class EmailOptions
{
public bool Enabled { get; set; } = false;
public string SmtpHost { get; set; } = string.Empty;
public int SmtpPort { get; set; } = 25;
public bool UseSsl { get; set; } = false;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string From { get; set; } = string.Empty;
public List<string> To { get; set; } = new List<string>();
public string SubjectPrefix { get; set; } = "[HostAvailabilityMonitor]";
public bool SendOnFailure { get; set; } = true;
public bool SendOnRecovery { get; set; } = true;
public bool OnlyOnStateChange { get; set; } = true;
public int CooldownMinutes { get; set; } = 0;
public int TimeoutSeconds { get; set; } = 30;
}
public sealed class TargetOptions
{
public string Name { get; set; } = string.Empty;
public string Endpoint { get; set; } = string.Empty;
public int TimeoutSeconds { get; set; } = 10;
public int? Port { get; set; }
public bool? ValidateUncPathAccess { get; set; }
public string HttpMethod { get; set; }
public bool Enabled { get; set; } = true;
public string DisplayName
{
get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name; }
}
public string StateKey
{
get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name.Trim(); }
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Script.Serialization;
namespace HostAvailabilityMonitor.Configuration
{
public sealed class MonitorConfiguration
{
public string ApplicationName { get; set; } = "HostAvailabilityMonitor";
public string EnvironmentName { get; set; } = "Unspecified Environment";
public RuntimeOptions Runtime { get; set; } = new RuntimeOptions();
public LoggingOptions Logging { get; set; } = new LoggingOptions();
public EventLogOptions EventLog { get; set; } = new EventLogOptions();
public EmailOptions Email { get; set; } = new EmailOptions();
public List<TargetOptions> Targets { get; set; } = new List<TargetOptions>();
public static MonitorConfiguration Load(string path)
{
if (!File.Exists(path))
{
throw new FileNotFoundException("Konfigurationsdatei wurde nicht gefunden: " + path, path);
}
var json = File.ReadAllText(path);
var serializer = new JavaScriptSerializer();
var config = serializer.Deserialize<MonitorConfiguration>(json);
if (config == null)
{
throw new InvalidOperationException("Konfigurationsdatei konnte nicht deserialisiert werden.");
}
config.Runtime = config.Runtime ?? new RuntimeOptions();
config.Logging = config.Logging ?? new LoggingOptions();
config.EventLog = config.EventLog ?? new EventLogOptions();
config.Email = config.Email ?? new EmailOptions();
config.Email.To = SanitizeRecipientList(config.Email.To);
config.Email.Cc = SanitizeRecipientList(config.Email.Cc);
config.Email.Bcc = SanitizeRecipientList(config.Email.Bcc);
config.Targets = config.Targets ?? new List<TargetOptions>();
if (string.IsNullOrWhiteSpace(config.ApplicationName))
{
config.ApplicationName = "HostAvailabilityMonitor";
}
if (string.IsNullOrWhiteSpace(config.EnvironmentName))
{
config.EnvironmentName = "Unspecified Environment";
}
foreach (var target in config.Targets)
{
if (target == null)
{
continue;
}
if (target.TimeoutSeconds <= 0)
{
target.TimeoutSeconds = 10;
}
}
if (config.Email.TimeoutSeconds <= 0)
{
config.Email.TimeoutSeconds = 30;
}
if (config.Email.DailySummaryHourLocal < 0 || config.Email.DailySummaryHourLocal > 23)
{
config.Email.DailySummaryHourLocal = 14;
}
if (config.Email.DailySummaryMinuteLocal < 0 || config.Email.DailySummaryMinuteLocal > 59)
{
config.Email.DailySummaryMinuteLocal = 0;
}
return config;
}
public void Validate()
{
if (Targets == null || Targets.Count == 0)
{
throw new InvalidOperationException("Es wurde kein Ziel in der Konfiguration definiert.");
}
foreach (var target in Targets)
{
if (target == null)
{
throw new InvalidOperationException("Die Konfiguration enthaelt ein leeres Target-Objekt.");
}
if (string.IsNullOrWhiteSpace(target.Endpoint))
{
throw new InvalidOperationException("Mindestens ein Target hat keinen Endpoint-Wert.");
}
if (target.TimeoutSeconds <= 0)
{
throw new InvalidOperationException("Target '" + target.DisplayName + "' hat einen ungueltigen TimeoutSeconds-Wert.");
}
}
if (Runtime.MaxConcurrency <= 0)
{
Runtime.MaxConcurrency = 1;
}
if (Logging.RetentionDays < 0)
{
throw new InvalidOperationException("Logging.RetentionDays darf nicht negativ sein.");
}
if (Email.Enabled)
{
if (string.IsNullOrWhiteSpace(Email.SmtpHost))
{
throw new InvalidOperationException("Email.Enabled=true, aber SmtpHost ist leer.");
}
if (string.IsNullOrWhiteSpace(Email.From))
{
throw new InvalidOperationException("Email.Enabled=true, aber From ist leer.");
}
if (GetConfiguredRecipientCount(Email) <= 0)
{
throw new InvalidOperationException("Email.Enabled=true, aber es ist kein Empfaenger in Email.To, Email.Cc oder Email.Bcc konfiguriert.");
}
if (Email.TimeoutSeconds <= 0)
{
Email.TimeoutSeconds = 30;
}
if (Email.DailySummaryHourLocal < 0 || Email.DailySummaryHourLocal > 23)
{
throw new InvalidOperationException("Email.DailySummaryHourLocal muss zwischen 0 und 23 liegen.");
}
if (Email.DailySummaryMinuteLocal < 0 || Email.DailySummaryMinuteLocal > 59)
{
throw new InvalidOperationException("Email.DailySummaryMinuteLocal muss zwischen 0 und 59 liegen.");
}
}
}
private static List<string> SanitizeRecipientList(List<string> values)
{
if (values == null)
{
return new List<string>();
}
return values
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static int GetConfiguredRecipientCount(EmailOptions email)
{
if (email == null)
{
return 0;
}
return SanitizeRecipientList(email.To).Count
+ SanitizeRecipientList(email.Cc).Count
+ SanitizeRecipientList(email.Bcc).Count;
}
}
public sealed class RuntimeOptions
{
public int MaxConcurrency { get; set; } = 4;
public bool DefaultValidateUncPathAccess { get; set; } = false;
public bool NonZeroExitCodeOnAnyFailure { get; set; } = false;
public bool NonZeroExitCodeOnExecutionError { get; set; } = true;
public string StateDirectory { get; set; } = "state";
}
public sealed class LoggingOptions
{
public string LogDirectory { get; set; } = "logs";
public string FilePrefix { get; set; } = "host-availability-monitor";
public int RetentionDays { get; set; } = 5;
}
public sealed class EventLogOptions
{
public bool Enabled { get; set; } = true;
public string LogName { get; set; } = "Application";
public string Source { get; set; } = "HostAvailabilityMonitor";
public bool CreateSourceIfMissing { get; set; } = false;
public bool WriteSuccessEntries { get; set; } = false;
public bool WriteSummaryEntries { get; set; } = true;
}
public sealed class EmailOptions
{
public bool Enabled { get; set; } = false;
public string SmtpHost { get; set; } = string.Empty;
public int SmtpPort { get; set; } = 25;
public bool UseSsl { get; set; } = false;
public bool UseDefaultCredentials { get; set; } = false;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string From { get; set; } = string.Empty;
public List<string> To { get; set; } = new List<string>();
public List<string> Cc { get; set; } = new List<string>();
public List<string> Bcc { get; set; } = new List<string>();
public string SubjectPrefix { get; set; } = "[HostAvailabilityMonitor]";
public bool SendOnFailure { get; set; } = true;
public bool SendOnRecovery { get; set; } = true;
public bool SendDailySummary { get; set; } = false;
public int DailySummaryHourLocal { get; set; } = 14;
public int DailySummaryMinuteLocal { get; set; } = 0;
public bool OnlyOnStateChange { get; set; } = true;
public int CooldownMinutes { get; set; } = 0;
public int TimeoutSeconds { get; set; } = 30;
}
public sealed class TargetOptions
{
public string Name { get; set; } = string.Empty;
public string ApplicationName { get; set; } = string.Empty;
public string Endpoint { get; set; } = string.Empty;
public int TimeoutSeconds { get; set; } = 10;
public int? Port { get; set; }
public bool? ValidateUncPathAccess { get; set; }
public string HttpMethod { get; set; }
public bool Enabled { get; set; } = true;
public string DisplayName
{
get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name; }
}
public string EffectiveApplicationName
{
get { return string.IsNullOrWhiteSpace(ApplicationName) ? "Unassigned Application" : ApplicationName.Trim(); }
}
public string StateKey
{
get
{
var parts = new[]
{
Normalize(ApplicationName),
Normalize(Name),
Normalize(Endpoint)
}.Where(x => !string.IsNullOrWhiteSpace(x));
var key = string.Join("|", parts);
return string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("N") : key;
}
}
private static string Normalize(string value)
{
return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
}
}
}
@@ -1,61 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>HostAvailabilityMonitor</RootNamespace>
<AssemblyName>HostAvailabilityMonitor</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Web.Extensions" />
</ItemGroup>
<ItemGroup>
<Compile Include="Configuration\MonitorConfiguration.cs" />
<Compile Include="Logging\FileLogger.cs" />
<Compile Include="Logging\WindowsEventLogger.cs" />
<Compile Include="Monitoring\EndpointProbeService.cs" />
<Compile Include="Monitoring\ProbeResult.cs" />
<Compile Include="Notifications\EmailNotifier.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="State\MonitorStateStore.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>HostAvailabilityMonitor</RootNamespace>
<AssemblyName>HostAvailabilityMonitor</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Web.Extensions" />
</ItemGroup>
<ItemGroup>
<Compile Include="Configuration\MonitorConfiguration.cs" />
<Compile Include="Logging\FileLogger.cs" />
<Compile Include="Logging\WindowsEventLogger.cs" />
<Compile Include="Monitoring\EndpointProbeService.cs" />
<Compile Include="Monitoring\ProbeResult.cs" />
<Compile Include="Notifications\EmailNotifier.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="State\MonitorStateStore.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+192 -185
View File
@@ -1,185 +1,192 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using HostAvailabilityMonitor.Monitoring;
namespace HostAvailabilityMonitor.Logging
{
public sealed class FileLogger
{
private readonly object _sync = new object();
private readonly string _directory;
private readonly string _filePrefix;
private readonly int _retentionDays;
public FileLogger(string directory, string filePrefix, int retentionDays)
{
_directory = directory;
_filePrefix = filePrefix;
_retentionDays = retentionDays;
Directory.CreateDirectory(_directory);
}
public void CleanupOldFiles()
{
if (_retentionDays <= 0)
{
return;
}
var cutoff = DateTime.Today.AddDays(-Math.Max(0, _retentionDays - 1));
foreach (var file in Directory.EnumerateFiles(_directory, _filePrefix + "-*.log", SearchOption.TopDirectoryOnly))
{
try
{
if (TryGetDateFromFileName(file, out var fileDate))
{
if (fileDate < cutoff)
{
File.Delete(file);
}
}
else
{
var info = new FileInfo(file);
if (info.LastWriteTime.Date < cutoff)
{
info.Delete();
}
}
}
catch
{
// Cleanup darf den Lauf nicht blockieren.
}
}
}
public void Info(string message)
{
Write("INFO", message);
}
public void Warn(string message)
{
Write("WARN", message);
}
public void Error(string message)
{
Write("ERROR", message);
}
public void Probe(ProbeResult result)
{
var level = result.IsSuccess ? "SUCCESS" : "FAIL";
var parts = new List<string>
{
"Name=" + Escape(result.Name),
"Kind=" + result.Kind,
"Endpoint=" + Escape(result.Endpoint),
"DurationMs=" + Math.Max(0L, (long)result.Duration.TotalMilliseconds),
"Status=" + Escape(result.StatusText),
"Detail=" + Escape(result.Detail)
};
if (!string.IsNullOrWhiteSpace(result.Host))
{
parts.Add("Host=" + Escape(result.Host));
}
if (result.Port.HasValue)
{
parts.Add("Port=" + result.Port.Value);
}
if (result.HttpStatusCode.HasValue)
{
parts.Add("HttpStatus=" + result.HttpStatusCode.Value);
}
if (!string.IsNullOrWhiteSpace(result.ErrorType))
{
parts.Add("ErrorType=" + Escape(result.ErrorType));
}
Write(level, string.Join(" | ", parts));
}
private void Write(string level, string message)
{
var line = string.Format(
CultureInfo.InvariantCulture,
"{0:yyyy-MM-dd HH:mm:ss.fff zzz} | {1} | {2}{3}",
DateTimeOffset.Now,
level,
message,
Environment.NewLine);
var path = GetCurrentLogFilePath();
lock (_sync)
{
AppendWithRetries(path, line);
}
}
private void AppendWithRetries(string path, string line)
{
Exception lastException = null;
for (var attempt = 1; attempt <= 3; attempt++)
{
try
{
File.AppendAllText(path, line);
return;
}
catch (IOException ex)
{
lastException = ex;
Thread.Sleep(100 * attempt);
}
catch (UnauthorizedAccessException ex)
{
lastException = ex;
Thread.Sleep(100 * attempt);
}
}
if (lastException != null)
{
throw lastException;
}
}
private string GetCurrentLogFilePath()
{
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
return Path.Combine(_directory, fileName);
}
private bool TryGetDateFromFileName(string path, out DateTime date)
{
date = DateTime.MinValue;
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
var prefix = _filePrefix + "-";
if (!fileNameWithoutExtension.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var datePart = fileNameWithoutExtension.Substring(prefix.Length);
return DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
}
private static string Escape(string value)
{
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
}
}
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using HostAvailabilityMonitor.Monitoring;
namespace HostAvailabilityMonitor.Logging
{
public sealed class FileLogger
{
private readonly object _sync = new object();
private readonly string _directory;
private readonly string _filePrefix;
private readonly int _retentionDays;
private readonly string _monitorName;
private readonly string _environmentName;
public FileLogger(string directory, string filePrefix, int retentionDays, string monitorName, string environmentName)
{
_directory = directory;
_filePrefix = filePrefix;
_retentionDays = retentionDays;
_monitorName = string.IsNullOrWhiteSpace(monitorName) ? "HostAvailabilityMonitor" : monitorName.Trim();
_environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim();
Directory.CreateDirectory(_directory);
}
public void CleanupOldFiles()
{
if (_retentionDays <= 0)
{
return;
}
var cutoff = DateTime.Today.AddDays(-Math.Max(0, _retentionDays - 1));
foreach (var file in Directory.EnumerateFiles(_directory, _filePrefix + "-*.log", SearchOption.TopDirectoryOnly))
{
try
{
DateTime fileDate;
if (TryGetDateFromFileName(file, out fileDate))
{
if (fileDate < cutoff)
{
File.Delete(file);
}
}
else
{
var info = new FileInfo(file);
if (info.LastWriteTime.Date < cutoff)
{
info.Delete();
}
}
}
catch
{
// Cleanup darf den Lauf nicht blockieren.
}
}
}
public void Info(string message)
{
Write("INFO", message);
}
public void Warn(string message)
{
Write("WARN", message);
}
public void Error(string message)
{
Write("ERROR", message);
}
public void Probe(ProbeResult result)
{
var level = result.IsSkipped ? "SKIP" : (result.IsSuccess ? "SUCCESS" : "FAIL");
var parts = new List<string>
{
"Monitor=" + Escape(_monitorName),
"Environment=" + Escape(_environmentName),
"Application=" + Escape(result.ApplicationName),
"Name=" + Escape(result.Name),
"Kind=" + result.Kind,
"Endpoint=" + Escape(result.Endpoint),
"DurationMs=" + Math.Max(0L, (long)result.Duration.TotalMilliseconds),
"Status=" + Escape(result.StatusText),
"Detail=" + Escape(result.Detail)
};
if (!string.IsNullOrWhiteSpace(result.Host))
{
parts.Add("Host=" + Escape(result.Host));
}
if (result.Port.HasValue)
{
parts.Add("Port=" + result.Port.Value);
}
if (result.HttpStatusCode.HasValue)
{
parts.Add("HttpStatus=" + result.HttpStatusCode.Value);
}
if (!string.IsNullOrWhiteSpace(result.ErrorType))
{
parts.Add("ErrorType=" + Escape(result.ErrorType));
}
Write(level, string.Join(" | ", parts));
}
private void Write(string level, string message)
{
var line = string.Format(
CultureInfo.InvariantCulture,
"{0:yyyy-MM-dd HH:mm:ss.fff zzz} | {1} | {2}{3}",
DateTimeOffset.Now,
level,
message,
Environment.NewLine);
var path = GetCurrentLogFilePath();
lock (_sync)
{
AppendWithRetries(path, line);
}
}
private void AppendWithRetries(string path, string line)
{
Exception lastException = null;
for (var attempt = 1; attempt <= 3; attempt++)
{
try
{
File.AppendAllText(path, line);
return;
}
catch (IOException ex)
{
lastException = ex;
Thread.Sleep(100 * attempt);
}
catch (UnauthorizedAccessException ex)
{
lastException = ex;
Thread.Sleep(100 * attempt);
}
}
if (lastException != null)
{
throw lastException;
}
}
private string GetCurrentLogFilePath()
{
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
return Path.Combine(_directory, fileName);
}
private bool TryGetDateFromFileName(string path, out DateTime date)
{
date = DateTime.MinValue;
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
var prefix = _filePrefix + "-";
if (!fileNameWithoutExtension.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var datePart = fileNameWithoutExtension.Substring(prefix.Length);
return DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
}
private static string Escape(string value)
{
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
}
}
}
@@ -1,357 +1,515 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using HostAvailabilityMonitor.Configuration;
namespace HostAvailabilityMonitor.Monitoring
{
public sealed class EndpointProbeService
{
private static readonly HttpClient HttpClient = CreateHttpClient();
private readonly RuntimeOptions _runtimeOptions;
public EndpointProbeService(RuntimeOptions runtimeOptions)
{
_runtimeOptions = runtimeOptions;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
}
public async Task<ProbeResult> ProbeAsync(TargetOptions target, CancellationToken cancellationToken)
{
var startedAt = DateTimeOffset.Now;
try
{
var endpoint = (target.Endpoint ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(endpoint))
{
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Leerer Endpoint.", null, null, null);
}
if (endpoint.StartsWith("\\\\", StringComparison.Ordinal))
{
return await CheckUncAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
}
Uri uri;
if (Uri.TryCreate(endpoint, UriKind.Absolute, out uri))
{
var scheme = uri.Scheme.ToLowerInvariant();
switch (scheme)
{
case "http":
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Http, cancellationToken).ConfigureAwait(false);
case "https":
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Https, cancellationToken).ConfigureAwait(false);
case "sftp":
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 22), startedAt, TargetKind.Sftp, endpoint, cancellationToken).ConfigureAwait(false);
case "tcp":
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, null), startedAt, TargetKind.Tcp, endpoint, cancellationToken).ConfigureAwait(false);
case "icmp":
return await CheckIcmpAsync(target, uri.Host, startedAt, endpoint).ConfigureAwait(false);
case "ftp":
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 21), startedAt, TargetKind.Ftp, endpoint, cancellationToken).ConfigureAwait(false);
default:
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Nicht unterstütztes Schema '" + uri.Scheme + "'. Unterstützt: UNC, http, https, sftp, ftp, tcp, icmp.", null, uri.Host, uri.IsDefaultPort ? (int?)null : uri.Port);
}
}
return await CheckPlainHostAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, "Timeout oder Abbruch während des Checks.", ex, null, null);
}
catch (Exception ex)
{
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, ex.Message, ex, null, null);
}
}
private async Task<ProbeResult> CheckPlainHostAsync(TargetOptions target, string host, DateTimeOffset startedAt, CancellationToken cancellationToken)
{
if (target.Port.HasValue)
{
return await CheckTcpAsync(target, host, target.Port.Value, startedAt, TargetKind.Host, host, cancellationToken).ConfigureAwait(false);
}
return await CheckIcmpAsync(target, host, startedAt, host).ConfigureAwait(false);
}
private async Task<ProbeResult> CheckUncAsync(TargetOptions target, string endpoint, DateTimeOffset startedAt, CancellationToken cancellationToken)
{
var host = ExtractUncServer(endpoint);
var port = target.Port ?? 445;
var tcpResult = await CheckTcpAsync(target, host, port, startedAt, TargetKind.Unc, endpoint, cancellationToken).ConfigureAwait(false);
if (!tcpResult.IsSuccess)
{
return tcpResult;
}
var validatePath = target.ValidateUncPathAccess ?? _runtimeOptions.DefaultValidateUncPathAccess;
if (!validatePath)
{
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfadvalidierung übersprungen.", null);
}
try
{
var existsTask = Task.Run(function: () => Directory.Exists(endpoint) || File.Exists(endpoint), cancellationToken);
var timed = await CompleteWithinAsync(existsTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
if (!timed.Completed)
{
return Fail(target, TargetKind.Unc, endpoint, startedAt, "Timeout während der UNC-Pfadvalidierung.", null, host, port);
}
if (timed.Result)
{
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfad wurde erfolgreich validiert.", null);
}
return Fail(target, TargetKind.Unc, endpoint, startedAt, "SMB-Port erreichbar, aber UNC-Pfadvalidierung war nicht erfolgreich.", null, host, port);
}
catch (Exception ex)
{
return Fail(target, TargetKind.Unc, endpoint, startedAt, "UNC-Pfadvalidierung fehlgeschlagen: " + ex.Message, ex, host, port);
}
}
private async Task<ProbeResult> CheckHttpAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken)
{
var timeout = TimeSpan.FromSeconds(target.TimeoutSeconds);
var methodText = string.IsNullOrWhiteSpace(target.HttpMethod) ? "HEAD" : target.HttpMethod.Trim().ToUpperInvariant();
var method = new HttpMethod(methodText);
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
using (var request = new HttpRequestMessage(method, uri))
{
timeoutCts.CancelAfter(timeout);
try
{
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
{
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase)
&& (response.StatusCode == HttpStatusCode.MethodNotAllowed || response.StatusCode == HttpStatusCode.NotImplemented))
{
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, new InvalidOperationException("HEAD nicht unterstützt (" + (int)response.StatusCode + " " + response.ReasonPhrase + ")."), cancellationToken).ConfigureAwait(false);
}
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HTTP-Antwort erhalten (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
}
}
catch (HttpRequestException ex)
{
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase))
{
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, ex, cancellationToken).ConfigureAwait(false);
}
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
catch (TaskCanceledException ex)
{
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check ist in einen Timeout gelaufen.", ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
catch (Exception ex)
{
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
}
}
private async Task<ProbeResult> RetryHttpWithGetAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, Exception headException, CancellationToken cancellationToken)
{
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
{
timeoutCts.CancelAfter(TimeSpan.FromSeconds(target.TimeoutSeconds));
try
{
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
{
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HEAD nicht erfolgreich (" + headException.GetType().Name + "), GET erfolgreich (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
}
}
catch (Exception ex)
{
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen. HEAD: " + headException.Message + "; GET: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
}
}
private async Task<ProbeResult> CheckTcpAsync(TargetOptions target, string host, int? port, DateTimeOffset startedAt, TargetKind kind, string endpoint, CancellationToken cancellationToken)
{
if (!port.HasValue || port.Value <= 0)
{
return Fail(target, kind, endpoint, startedAt, "Für diesen Endpoint konnte kein gültiger TCP-Port ermittelt werden.", null, host, port);
}
var timeout = TimeSpan.FromSeconds(target.TimeoutSeconds);
using (var client = new TcpClient())
{
try
{
var connectTask = client.ConnectAsync(host, port.Value);
var completedTask = await Task.WhenAny(connectTask, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false);
if (completedTask != connectTask)
{
try
{
client.Close();
}
catch
{
}
cancellationToken.ThrowIfCancellationRequested();
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: Timeout.", null, host, port);
}
await connectTask.ConfigureAwait(false);
return Success(target, kind, endpoint, startedAt, host, port, "TCP-Verbindung erfolgreich aufgebaut.", null);
}
catch (Exception ex)
{
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: " + ex.Message, ex, host, port);
}
}
}
private async Task<ProbeResult> CheckIcmpAsync(TargetOptions target, string host, DateTimeOffset startedAt, string endpoint)
{
try
{
using (var ping = new Ping())
{
var reply = await ping.SendPingAsync(host, (int)TimeSpan.FromSeconds(target.TimeoutSeconds).TotalMilliseconds).ConfigureAwait(false);
if (reply.Status == IPStatus.Success)
{
return Success(target, TargetKind.Icmp, endpoint, startedAt, host, null, "ICMP erfolgreich, RTT=" + reply.RoundtripTime + "ms.", null);
}
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP fehlgeschlagen: " + reply.Status + ".", null, host, null);
}
}
catch (Exception ex)
{
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check fehlgeschlagen: " + ex.Message, ex, host, null);
}
}
private static ProbeResult Success(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string host, int? port, string detail, int? httpStatusCode)
{
return new ProbeResult
{
StateKey = target.StateKey,
Name = target.DisplayName,
Endpoint = endpoint,
Kind = kind,
IsSuccess = true,
StatusText = "Reachable",
Detail = detail,
Host = host,
Port = port,
HttpStatusCode = httpStatusCode,
ErrorType = string.Empty,
StartedAtLocal = startedAt,
FinishedAtLocal = DateTimeOffset.Now
};
}
private static ProbeResult Fail(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string detail, Exception exception, string host, int? port)
{
return new ProbeResult
{
StateKey = target.StateKey,
Name = target.DisplayName,
Endpoint = endpoint,
Kind = kind,
IsSuccess = false,
StatusText = "Unreachable",
Detail = detail,
Host = host,
Port = port,
ErrorType = exception == null ? string.Empty : exception.GetType().Name,
StartedAtLocal = startedAt,
FinishedAtLocal = DateTimeOffset.Now
};
}
private static HttpClient CreateHttpClient()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = false,
UseCookies = false,
UseProxy = false
};
var client = new HttpClient(handler);
client.Timeout = Timeout.InfiniteTimeSpan;
return client;
}
private static int? ResolvePort(Uri uri, int? fallback)
{
if (uri == null)
{
return fallback;
}
if (!uri.IsDefaultPort && uri.Port > 0)
{
return uri.Port;
}
return fallback;
}
private static string ExtractUncServer(string uncPath)
{
if (string.IsNullOrWhiteSpace(uncPath))
{
return string.Empty;
}
var trimmed = uncPath.TrimStart('\\');
var firstSeparator = trimmed.IndexOf('\\');
if (firstSeparator < 0)
{
return trimmed;
}
return trimmed.Substring(0, firstSeparator);
}
private static async Task<TimedResult<T>> CompleteWithinAsync<T>(Task<T> task, TimeSpan timeout, CancellationToken cancellationToken)
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false);
if (completedTask == task)
{
return new TimedResult<T>(true, await task.ConfigureAwait(false));
}
cancellationToken.ThrowIfCancellationRequested();
return new TimedResult<T>(false, default(T));
}
private struct TimedResult<T>
{
public TimedResult(bool completed, T result)
{
Completed = completed;
Result = result;
}
public bool Completed { get; private set; }
public T Result { get; private set; }
}
}
}
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using HostAvailabilityMonitor.Configuration;
namespace HostAvailabilityMonitor.Monitoring
{
public sealed class EndpointProbeService
{
private static readonly HttpClient HttpClient = CreateHttpClient();
private readonly RuntimeOptions _runtimeOptions;
public EndpointProbeService(RuntimeOptions runtimeOptions)
{
_runtimeOptions = runtimeOptions ?? new RuntimeOptions();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
}
public async Task<ProbeResult> ProbeAsync(TargetOptions target, CancellationToken cancellationToken)
{
var startedAt = DateTimeOffset.Now;
try
{
var endpoint = (target.Endpoint ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(endpoint))
{
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Leerer Endpoint.", null, null, null);
}
ProbeResult skippedResult;
if (TryCreateSkipResult(target, endpoint, startedAt, out skippedResult))
{
return skippedResult;
}
if (endpoint.StartsWith("\\\\", StringComparison.Ordinal))
{
return await CheckUncAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
}
if (Path.IsPathRooted(endpoint))
{
return await CheckLocalPathAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
}
Uri uri;
if (Uri.TryCreate(endpoint, UriKind.Absolute, out uri))
{
var scheme = uri.Scheme.ToLowerInvariant();
switch (scheme)
{
case "http":
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Http, cancellationToken).ConfigureAwait(false);
case "https":
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Https, cancellationToken).ConfigureAwait(false);
case "sftp":
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 22), startedAt, TargetKind.Sftp, endpoint, cancellationToken).ConfigureAwait(false);
case "tcp":
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, null), startedAt, TargetKind.Tcp, endpoint, cancellationToken).ConfigureAwait(false);
case "icmp":
return await CheckIcmpAsync(target, uri.Host, startedAt, endpoint, cancellationToken).ConfigureAwait(false);
case "ftp":
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 21), startedAt, TargetKind.Ftp, endpoint, cancellationToken).ConfigureAwait(false);
case "file":
return await CheckFileUriAsync(target, uri, startedAt, cancellationToken).ConfigureAwait(false);
default:
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Nicht unterstütztes Schema '" + uri.Scheme + "'. Unterstützt: UNC, file, http, https, sftp, ftp, tcp, icmp.", null, uri.Host, uri.IsDefaultPort ? (int?)null : uri.Port);
}
}
return await CheckPlainHostAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, "Timeout oder Abbruch während des Checks.", ex, null, null);
}
catch (Exception ex)
{
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, ex.Message, ex, null, null);
}
}
private async Task<ProbeResult> CheckPlainHostAsync(TargetOptions target, string host, DateTimeOffset startedAt, CancellationToken cancellationToken)
{
if (target.Port.HasValue)
{
return await CheckTcpAsync(target, host, target.Port.Value, startedAt, TargetKind.Host, host, cancellationToken).ConfigureAwait(false);
}
return await CheckIcmpAsync(target, host, startedAt, host, cancellationToken).ConfigureAwait(false);
}
private async Task<ProbeResult> CheckFileUriAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, CancellationToken cancellationToken)
{
if (uri.IsUnc || !string.IsNullOrWhiteSpace(uri.Host))
{
var uncPath = uri.LocalPath;
if (!uncPath.StartsWith("\\\\", StringComparison.Ordinal))
{
uncPath = "\\\\" + uri.Host + uri.LocalPath.Replace('/', '\\');
}
return await CheckUncAsync(target, uncPath, startedAt, cancellationToken).ConfigureAwait(false);
}
var localPath = Uri.UnescapeDataString(uri.LocalPath ?? string.Empty);
return await CheckLocalPathAsync(target, localPath, startedAt, cancellationToken).ConfigureAwait(false);
}
private async Task<ProbeResult> CheckLocalPathAsync(TargetOptions target, string path, DateTimeOffset startedAt, CancellationToken cancellationToken)
{
try
{
var existsTask = Task.Run(function: () => Directory.Exists(path) || File.Exists(path), cancellationToken);
var timed = await CompleteWithinAsync(existsTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
if (!timed.Completed)
{
return Fail(target, TargetKind.File, path, startedAt, "Timeout während der Datei-/Pfadprüfung.", null, null, null);
}
if (timed.Result)
{
return Success(target, TargetKind.File, path, startedAt, Environment.MachineName, null, "Datei- oder Pfadprüfung erfolgreich.", null);
}
return Fail(target, TargetKind.File, path, startedAt, "Datei oder Verzeichnis ist nicht vorhanden oder nicht zugreifbar.", null, Environment.MachineName, null);
}
catch (Exception ex)
{
return Fail(target, TargetKind.File, path, startedAt, "Datei-/Pfadprüfung fehlgeschlagen: " + ex.Message, ex, Environment.MachineName, null);
}
}
private async Task<ProbeResult> CheckUncAsync(TargetOptions target, string endpoint, DateTimeOffset startedAt, CancellationToken cancellationToken)
{
var host = ExtractUncServer(endpoint);
var port = target.Port ?? 445;
var tcpResult = await CheckTcpAsync(target, host, port, startedAt, TargetKind.Unc, endpoint, cancellationToken).ConfigureAwait(false);
if (!tcpResult.IsSuccess)
{
return tcpResult;
}
var validatePath = target.ValidateUncPathAccess ?? _runtimeOptions.DefaultValidateUncPathAccess;
if (!validatePath)
{
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfadvalidierung übersprungen.", null);
}
try
{
var existsTask = Task.Run(function: () => Directory.Exists(endpoint) || File.Exists(endpoint), cancellationToken);
var timed = await CompleteWithinAsync(existsTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
if (!timed.Completed)
{
return Fail(target, TargetKind.Unc, endpoint, startedAt, "Timeout während der UNC-Pfadvalidierung.", null, host, port);
}
if (timed.Result)
{
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfad wurde erfolgreich validiert.", null);
}
return Fail(target, TargetKind.Unc, endpoint, startedAt, "SMB-Port erreichbar, aber UNC-Pfadvalidierung war nicht erfolgreich.", null, host, port);
}
catch (Exception ex)
{
return Fail(target, TargetKind.Unc, endpoint, startedAt, "UNC-Pfadvalidierung fehlgeschlagen: " + ex.Message, ex, host, port);
}
}
private async Task<ProbeResult> CheckHttpAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken)
{
var methodText = string.IsNullOrWhiteSpace(target.HttpMethod) ? "HEAD" : target.HttpMethod.Trim().ToUpperInvariant();
var method = new HttpMethod(methodText);
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
using (var request = new HttpRequestMessage(method, uri))
{
timeoutCts.CancelAfter(TimeSpan.FromSeconds(target.TimeoutSeconds));
try
{
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
{
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase)
&& (response.StatusCode == HttpStatusCode.MethodNotAllowed || response.StatusCode == HttpStatusCode.NotImplemented))
{
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, cancellationToken).ConfigureAwait(false);
}
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HTTP-Antwort erhalten (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
}
}
catch (HttpRequestException ex)
{
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase))
{
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, cancellationToken).ConfigureAwait(false);
}
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
catch (TaskCanceledException ex)
{
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check ist in einen Timeout gelaufen.", ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
catch (Exception ex)
{
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
}
}
private async Task<ProbeResult> RetryHttpWithGetAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken)
{
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
{
timeoutCts.CancelAfter(TimeSpan.FromSeconds(target.TimeoutSeconds));
try
{
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
{
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HTTP-Antwort nach GET-Fallback erhalten (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
}
}
catch (TaskCanceledException ex)
{
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check ist in einen Timeout gelaufen.", ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
catch (Exception ex)
{
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
}
}
}
private async Task<ProbeResult> CheckTcpAsync(TargetOptions target, string host, int? port, DateTimeOffset startedAt, TargetKind kind, string endpoint, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(host))
{
return Fail(target, kind, endpoint, startedAt, "Host ist leer.", null, host, port);
}
if (!port.HasValue || port.Value <= 0 || port.Value > 65535)
{
return Fail(target, kind, endpoint, startedAt, "Es wurde kein gültiger Port aufgelöst.", null, host, port);
}
using (var client = new TcpClient())
using (cancellationToken.Register(() =>
{
try
{
client.Close();
}
catch
{
}
}))
{
try
{
var connectTask = client.ConnectAsync(host, port.Value);
var timed = await CompleteWithinAsync(connectTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
if (!timed.Completed)
{
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung ist in einen Timeout gelaufen.", null, host, port);
}
if (!client.Connected)
{
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung konnte nicht aufgebaut werden.", null, host, port);
}
return Success(target, kind, endpoint, startedAt, host, port, "TCP-Verbindung erfolgreich aufgebaut.", null);
}
catch (SocketException ex)
{
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: " + ex.Message, ex, host, port);
}
catch (ObjectDisposedException ex)
{
return Fail(target, kind, endpoint, startedAt, "TCP-Check wurde abgebrochen oder ist in einen Timeout gelaufen.", ex, host, port);
}
catch (Exception ex)
{
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: " + ex.Message, ex, host, port);
}
}
}
private async Task<ProbeResult> CheckIcmpAsync(TargetOptions target, string host, DateTimeOffset startedAt, string endpoint, CancellationToken cancellationToken)
{
using (var ping = new Ping())
{
try
{
var replyTask = ping.SendPingAsync(host, Math.Max(1000, target.TimeoutSeconds * 1000));
var timed = await CompleteWithinAsync(replyTask, TimeSpan.FromSeconds(target.TimeoutSeconds + 1), cancellationToken).ConfigureAwait(false);
if (!timed.Completed)
{
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check ist in einen Timeout gelaufen.", null, host, null);
}
var reply = timed.Result;
if (reply != null && reply.Status == IPStatus.Success)
{
return Success(target, TargetKind.Icmp, endpoint, startedAt, host, null, "Ping erfolgreich.", null);
}
var status = reply == null ? "Unbekannt" : reply.Status.ToString();
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "Ping fehlgeschlagen: " + status + ".", null, host, null);
}
catch (PingException ex)
{
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check fehlgeschlagen: " + ex.Message, ex, host, null);
}
catch (Exception ex)
{
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check fehlgeschlagen: " + ex.Message, ex, host, null);
}
}
}
private static int? ResolvePort(Uri uri, int? defaultPort)
{
if (uri == null)
{
return defaultPort;
}
if (!uri.IsDefaultPort)
{
return uri.Port;
}
return defaultPort;
}
private static string ExtractUncServer(string endpoint)
{
if (string.IsNullOrWhiteSpace(endpoint))
{
return string.Empty;
}
var trimmed = endpoint.TrimStart('\\');
var firstSeparator = trimmed.IndexOf('\\');
return firstSeparator >= 0 ? trimmed.Substring(0, firstSeparator) : trimmed;
}
private static HttpClient CreateHttpClient()
{
var handler = new HttpClientHandler();
var client = new HttpClient(handler, disposeHandler: true);
client.Timeout = Timeout.InfiniteTimeSpan;
return client;
}
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);
}
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);
}
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);
}
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
{
StateKey = target.StateKey,
Name = target.DisplayName,
ApplicationName = target.EffectiveApplicationName,
Endpoint = endpoint,
Kind = kind,
IsSuccess = isSuccess,
IsSkipped = isSkipped,
StatusText = statusText,
Detail = detail,
Host = host,
Port = port,
HttpStatusCode = httpStatusCode,
ErrorType = errorType,
StartedAtLocal = startedAt,
FinishedAtLocal = DateTimeOffset.Now
};
}
private static bool TryCreateSkipResult(TargetOptions target, string endpoint, DateTimeOffset startedAt, out ProbeResult result)
{
result = null;
if (LooksLikeSmtpAdapterTarget(target, endpoint))
{
result = Skip(target, TargetKind.Unknown, endpoint, startedAt, "Target wurde als SMTP-/E-Mail-Empfängerliste erkannt und wird nicht netzwerktechnisch geprüft.");
return true;
}
return false;
}
private static bool LooksLikeSmtpAdapterTarget(TargetOptions target, string endpoint)
{
var name = target == null ? string.Empty : (target.Name ?? string.Empty);
var value = endpoint ?? string.Empty;
if (name.IndexOf("[SMTP]", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
return LooksLikeEmailAddressList(value);
}
private static bool LooksLikeEmailAddressList(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var trimmed = value.Trim();
if (trimmed.IndexOf('@') < 0)
{
return false;
}
if (trimmed.IndexOf("http://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("https://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("sftp://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("ftp://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("tcp://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("file://", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.StartsWith("\\", StringComparison.Ordinal))
{
return false;
}
var entries = trimmed.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
if (entries.Length == 0)
{
return false;
}
foreach (var entry in entries)
{
var part = entry.Trim();
if (string.IsNullOrWhiteSpace(part))
{
continue;
}
if (part.IndexOf('@') <= 0 || part.IndexOf(' ') >= 0)
{
return false;
}
}
return true;
}
private static async Task<CompletionResult<T>> CompleteWithinAsync<T>(Task<T> task, TimeSpan timeout, CancellationToken cancellationToken)
{
var delayTask = Task.Delay(timeout, cancellationToken);
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
if (completedTask != task)
{
return new CompletionResult<T>(false, default(T));
}
return new CompletionResult<T>(true, await task.ConfigureAwait(false));
}
private static async Task<CompletionResult<bool>> CompleteWithinAsync(Task task, TimeSpan timeout, CancellationToken cancellationToken)
{
var delayTask = Task.Delay(timeout, cancellationToken);
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
if (completedTask != task)
{
return new CompletionResult<bool>(false, false);
}
await task.ConfigureAwait(false);
return new CompletionResult<bool>(true, true);
}
private struct CompletionResult<T>
{
public CompletionResult(bool completed, T result)
{
Completed = completed;
Result = result;
}
public bool Completed { get; private set; }
public T Result { get; private set; }
}
}
}
@@ -1,38 +1,41 @@
using System;
namespace HostAvailabilityMonitor.Monitoring
{
public enum TargetKind
{
Unknown = 0,
Unc = 1,
Http = 2,
Https = 3,
Sftp = 4,
Tcp = 5,
Icmp = 6,
Host = 7,
Ftp = 8
}
public sealed class ProbeResult
{
public string StateKey { get; set; }
public string Name { get; set; }
public string Endpoint { get; set; }
public TargetKind Kind { get; set; }
public bool IsSuccess { get; set; }
public string StatusText { get; set; }
public string Detail { get; set; }
public string Host { get; set; }
public int? Port { get; set; }
public int? HttpStatusCode { get; set; }
public string ErrorType { get; set; }
public DateTimeOffset StartedAtLocal { get; set; }
public DateTimeOffset FinishedAtLocal { get; set; }
public TimeSpan Duration
{
get { return FinishedAtLocal - StartedAtLocal; }
}
}
}
using System;
namespace HostAvailabilityMonitor.Monitoring
{
public enum TargetKind
{
Unknown = 0,
Unc = 1,
Http = 2,
Https = 3,
Sftp = 4,
Tcp = 5,
Icmp = 6,
Host = 7,
Ftp = 8,
File = 9
}
public sealed class ProbeResult
{
public string StateKey { get; set; }
public string Name { get; set; }
public string ApplicationName { get; set; }
public string Endpoint { get; set; }
public TargetKind Kind { get; set; }
public bool IsSuccess { get; set; }
public bool IsSkipped { get; set; }
public string StatusText { get; set; }
public string Detail { get; set; }
public string Host { get; set; }
public int? Port { get; set; }
public int? HttpStatusCode { get; set; }
public string ErrorType { get; set; }
public DateTimeOffset StartedAtLocal { get; set; }
public DateTimeOffset FinishedAtLocal { get; set; }
public TimeSpan Duration
{
get { return FinishedAtLocal - StartedAtLocal; }
}
}
}
@@ -1,196 +1,334 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HostAvailabilityMonitor.Configuration;
using HostAvailabilityMonitor.Monitoring;
using HostAvailabilityMonitor.State;
namespace HostAvailabilityMonitor.Notifications
{
public sealed class EmailNotifier
{
private readonly EmailOptions _options;
public EmailNotifier(EmailOptions options)
{
_options = options ?? new EmailOptions();
}
public bool IsEnabled
{
get { return _options.Enabled; }
}
public bool ShouldNotify(ProbeResult result, TargetState previousState)
{
if (!IsEnabled || result == null)
{
return false;
}
if (result.IsSuccess && !_options.SendOnRecovery)
{
return false;
}
if (!result.IsSuccess && !_options.SendOnFailure)
{
return false;
}
var nowUtc = DateTime.UtcNow;
var cooldown = TimeSpan.FromMinutes(Math.Max(0, _options.CooldownMinutes));
var cooldownPassed = previousState == null
|| !previousState.LastNotificationUtc.HasValue
|| cooldown == TimeSpan.Zero
|| (nowUtc - previousState.LastNotificationUtc.Value) >= cooldown;
if (_options.OnlyOnStateChange)
{
if (previousState == null)
{
return !result.IsSuccess && cooldownPassed;
}
if (previousState.IsUp == result.IsSuccess)
{
return false;
}
return cooldownPassed;
}
if (result.IsSuccess && previousState == null)
{
return false;
}
return cooldownPassed;
}
public async Task SendAsync(
IReadOnlyCollection<ProbeResult> failures,
IReadOnlyCollection<ProbeResult> recoveries,
IDictionary<string, TargetState> currentState,
CancellationToken cancellationToken)
{
if (!IsEnabled)
{
return;
}
var failureCount = failures == null ? 0 : failures.Count;
var recoveryCount = recoveries == null ? 0 : recoveries.Count;
if (failureCount == 0 && recoveryCount == 0)
{
return;
}
var subject = BuildSubject(failureCount, recoveryCount);
var body = BuildBody(failures, recoveries, currentState);
using (var message = new MailMessage())
{
message.From = new MailAddress(_options.From);
foreach (var recipient in _options.To.Where(x => !string.IsNullOrWhiteSpace(x)))
{
message.To.Add(recipient);
}
message.Subject = subject;
message.Body = body;
message.IsBodyHtml = false;
message.BodyEncoding = Encoding.UTF8;
message.SubjectEncoding = Encoding.UTF8;
using (var client = new SmtpClient(_options.SmtpHost, _options.SmtpPort))
{
client.EnableSsl = _options.UseSsl;
client.Timeout = Math.Max(1, _options.TimeoutSeconds) * 1000;
if (!string.IsNullOrWhiteSpace(_options.Username))
{
client.Credentials = new NetworkCredential(_options.Username, _options.Password);
}
cancellationToken.ThrowIfCancellationRequested();
await client.SendMailAsync(message).ConfigureAwait(false);
}
}
}
private string BuildSubject(int failureCount, int recoveryCount)
{
if (failureCount > 0 && recoveryCount > 0)
{
return string.Format("{0} {1} Ausfall/Ausfälle, {2} Recovery/Recoveries", _options.SubjectPrefix, failureCount, recoveryCount);
}
if (failureCount > 0)
{
return string.Format("{0} {1} Ausfall/Ausfälle erkannt", _options.SubjectPrefix, failureCount);
}
return string.Format("{0} {1} Recovery/Recoveries erkannt", _options.SubjectPrefix, recoveryCount);
}
private string BuildBody(IReadOnlyCollection<ProbeResult> failures, IReadOnlyCollection<ProbeResult> recoveries, IDictionary<string, TargetState> currentState)
{
var sb = new StringBuilder();
sb.AppendLine("Host Availability Monitor");
sb.AppendLine("Zeitpunkt: " + DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"));
sb.AppendLine();
if (failures != null && failures.Count > 0)
{
sb.AppendLine("Fehlgeschlagene Checks:");
foreach (var failure in failures.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
{
AppendResult(sb, failure, currentState);
}
sb.AppendLine();
}
if (recoveries != null && recoveries.Count > 0)
{
sb.AppendLine("Wiederhergestellte Checks:");
foreach (var recovery in recoveries.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
{
AppendResult(sb, recovery, currentState);
}
sb.AppendLine();
}
return sb.ToString();
}
private static void AppendResult(StringBuilder sb, ProbeResult result, IDictionary<string, TargetState> currentState)
{
sb.AppendLine("- Name: " + result.Name);
sb.AppendLine(" Endpoint: " + result.Endpoint);
sb.AppendLine(" Status: " + result.StatusText);
sb.AppendLine(" Detail: " + result.Detail);
if (!string.IsNullOrWhiteSpace(result.Host))
{
sb.AppendLine(" Host: " + result.Host);
}
if (result.Port.HasValue)
{
sb.AppendLine(" Port: " + result.Port.Value);
}
sb.AppendLine(" Dauer(ms): " + Math.Max(0L, (long)result.Duration.TotalMilliseconds));
TargetState state;
if (currentState != null && currentState.TryGetValue(result.StateKey, out state) && state != null)
{
sb.AppendLine(" ConsecutiveFailures: " + state.ConsecutiveFailures);
sb.AppendLine(" LastChangedUtc: " + state.LastChangedUtc.ToString("yyyy-MM-dd HH:mm:ss") + "Z");
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HostAvailabilityMonitor.Configuration;
using HostAvailabilityMonitor.Monitoring;
using HostAvailabilityMonitor.State;
namespace HostAvailabilityMonitor.Notifications
{
public sealed class EmailNotifier
{
private readonly EmailOptions _options;
private readonly string _monitorName;
private readonly string _environmentName;
public EmailNotifier(EmailOptions options, string monitorName, string environmentName)
{
_options = options ?? new EmailOptions();
_monitorName = string.IsNullOrWhiteSpace(monitorName) ? "HostAvailabilityMonitor" : monitorName.Trim();
_environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim();
}
public bool IsEnabled
{
get { return _options.Enabled; }
}
public bool ShouldNotify(ProbeResult result, TargetState previousState)
{
if (!IsEnabled || result == null)
{
return false;
}
if (result.IsSkipped)
{
return false;
}
var nowUtc = DateTime.UtcNow;
var cooldown = TimeSpan.FromMinutes(Math.Max(0, _options.CooldownMinutes));
var cooldownPassed = previousState == null
|| !previousState.LastNotificationUtc.HasValue
|| cooldown == TimeSpan.Zero
|| (nowUtc - previousState.LastNotificationUtc.Value) >= cooldown;
if (IsRecoveryTransition(result, previousState))
{
return _options.SendOnRecovery && cooldownPassed;
}
if (result.IsSuccess)
{
return false;
}
if (!_options.SendOnFailure)
{
return false;
}
if (_options.OnlyOnStateChange)
{
return previousState == null || previousState.IsUp;
}
return cooldownPassed;
}
public bool ShouldSendDailySummary(MonitorStateMetadata metadata, DateTime nowLocal)
{
if (!IsEnabled || !_options.SendDailySummary)
{
return false;
}
var trigger = new TimeSpan(Math.Max(0, _options.DailySummaryHourLocal), Math.Max(0, _options.DailySummaryMinuteLocal), 0);
if (nowLocal.TimeOfDay < trigger)
{
return false;
}
var localDate = nowLocal.ToString("yyyy-MM-dd");
return metadata == null || !string.Equals(metadata.LastDailySummarySentLocalDate, localDate, StringComparison.Ordinal);
}
public async Task SendAsync(
IReadOnlyCollection<ProbeResult> failures,
IReadOnlyCollection<ProbeResult> recoveries,
IDictionary<string, TargetState> currentState,
CancellationToken cancellationToken)
{
if (!IsEnabled)
{
return;
}
var failureCount = failures == null ? 0 : failures.Count;
var recoveryCount = recoveries == null ? 0 : recoveries.Count;
if (failureCount == 0 && recoveryCount == 0)
{
return;
}
var subject = BuildSubject(failureCount, recoveryCount);
var body = BuildBody(failures, recoveries, currentState);
await SendMailAsync(subject, body, cancellationToken).ConfigureAwait(false);
}
public async Task SendDailySummaryAsync(DailySummaryEmailData summary, CancellationToken cancellationToken)
{
if (!IsEnabled || summary == null)
{
return;
}
var subject = BuildDailySummarySubject(summary);
var body = BuildDailySummaryBody(summary);
await SendMailAsync(subject, body, cancellationToken).ConfigureAwait(false);
}
private async Task SendMailAsync(string subject, string body, CancellationToken cancellationToken)
{
using (var message = new MailMessage())
{
message.From = new MailAddress(_options.From);
AddRecipients(message.To, _options.To);
AddRecipients(message.CC, _options.Cc);
AddRecipients(message.Bcc, _options.Bcc);
if (message.To.Count + message.CC.Count + message.Bcc.Count <= 0)
{
throw new InvalidOperationException("Es ist kein gueltiger Empfaenger in To, Cc oder Bcc konfiguriert.");
}
message.Subject = subject;
message.Body = body;
message.IsBodyHtml = false;
message.BodyEncoding = Encoding.UTF8;
message.SubjectEncoding = Encoding.UTF8;
using (var client = new SmtpClient(_options.SmtpHost, _options.SmtpPort))
{
client.EnableSsl = _options.UseSsl;
client.Timeout = Math.Max(1, _options.TimeoutSeconds) * 1000;
client.UseDefaultCredentials = _options.UseDefaultCredentials;
if (!_options.UseDefaultCredentials && !string.IsNullOrWhiteSpace(_options.Username))
{
client.Credentials = new NetworkCredential(_options.Username, _options.Password);
}
cancellationToken.ThrowIfCancellationRequested();
await client.SendMailAsync(message).ConfigureAwait(false);
}
}
}
private static void AddRecipients(MailAddressCollection collection, IEnumerable<string> recipients)
{
if (collection == null || recipients == null)
{
return;
}
foreach (var recipient in recipients.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct(StringComparer.OrdinalIgnoreCase))
{
collection.Add(recipient);
}
}
private static bool IsRecoveryTransition(ProbeResult result, TargetState previousState)
{
return result != null
&& result.IsSuccess
&& previousState != null
&& !previousState.IsUp;
}
private string BuildSubject(int failureCount, int recoveryCount)
{
var prefix = string.IsNullOrWhiteSpace(_options.SubjectPrefix) ? "[HostAvailabilityMonitor]" : _options.SubjectPrefix.Trim();
var envPart = "[" + _environmentName + "]";
if (failureCount > 0 && recoveryCount > 0)
{
return string.Format("{0} {1} {2} Ausfall/Ausfälle, {3} Recovery/Recoveries", prefix, envPart, failureCount, recoveryCount);
}
if (failureCount > 0)
{
return string.Format("{0} {1} {2} Ausfall/Ausfälle erkannt", prefix, envPart, failureCount);
}
return string.Format("{0} {1} {2} Endpoint(s) wieder erreichbar", prefix, envPart, recoveryCount);
}
private string BuildDailySummarySubject(DailySummaryEmailData summary)
{
var prefix = string.IsNullOrWhiteSpace(_options.SubjectPrefix) ? "[HostAvailabilityMonitor]" : _options.SubjectPrefix.Trim();
var envPart = "[" + _environmentName + "]";
return string.Format("{0} {1} Tagesstatus {2:yyyy-MM-dd}", prefix, envPart, summary.GeneratedAtLocal.LocalDateTime.Date);
}
private string BuildBody(IReadOnlyCollection<ProbeResult> failures, IReadOnlyCollection<ProbeResult> recoveries, IDictionary<string, TargetState> currentState)
{
var sb = new StringBuilder();
sb.AppendLine(_monitorName);
sb.AppendLine("Umgebung / Environment: " + _environmentName);
sb.AppendLine("Server / Machine: " + Environment.MachineName);
sb.AppendLine("Zeitpunkt / Timestamp: " + DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"));
sb.AppendLine();
if (failures != null && failures.Count > 0)
{
sb.AppendLine("Fehlgeschlagene Checks / Failed checks:");
foreach (var failure in failures.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
{
AppendResult(sb, failure, currentState, "DOWN");
}
sb.AppendLine();
}
if (recoveries != null && recoveries.Count > 0)
{
sb.AppendLine("Wieder erreichbare Checks / Recovered checks:");
foreach (var recovery in recoveries.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
{
AppendResult(sb, recovery, currentState, "DOWN -> UP");
}
sb.AppendLine();
}
return sb.ToString();
}
private string BuildDailySummaryBody(DailySummaryEmailData summary)
{
var sb = new StringBuilder();
sb.AppendLine(_monitorName + " - Tagesstatus / Daily Summary");
sb.AppendLine("Umgebung / Environment: " + _environmentName);
sb.AppendLine("Server / Machine: " + Environment.MachineName);
sb.AppendLine("Zusammenfassung bis / Summary until: " + summary.GeneratedAtLocal.ToString("yyyy-MM-dd HH:mm:ss zzz"));
sb.AppendLine("Zeitfenster / Window: " + summary.WindowStartLocal.ToString("yyyy-MM-dd HH:mm:ss zzz") + " bis " + summary.GeneratedAtLocal.ToString("yyyy-MM-dd HH:mm:ss zzz"));
sb.AppendLine();
sb.AppendLine("Checks des heutigen Tages / Today's checks:");
sb.AppendLine("- Gesamt / Total: " + summary.ProbesTodayTotal);
sb.AppendLine("- Erfolgreich / Successful: " + summary.ProbesTodaySuccess);
sb.AppendLine("- Fehlgeschlagen / Failed: " + summary.ProbesTodayFailure);
sb.AppendLine("- Übersprungen / Skipped: " + summary.ProbesTodaySkipped);
sb.AppendLine();
sb.AppendLine("Aktueller Snapshot des letzten Laufs / Current snapshot of latest run:");
sb.AppendLine("- Targets gesamt / Total targets: " + summary.CurrentTargetsTotal);
sb.AppendLine("- Aktuell UP: " + summary.CurrentTargetsUp);
sb.AppendLine("- Aktuell DOWN: " + summary.CurrentTargetsDown);
sb.AppendLine("- Aktuell übersprungen / Currently skipped: " + summary.CurrentTargetsSkipped);
sb.AppendLine();
if (summary.CurrentFailures != null && summary.CurrentFailures.Count > 0)
{
sb.AppendLine("Aktuell nicht erreichbare Endpunkte / Currently unavailable endpoints:");
foreach (var failure in summary.CurrentFailures.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
{
AppendResult(sb, failure, summary.CurrentState, "Aktuell DOWN / Currently DOWN");
}
}
else
{
sb.AppendLine("Aktuell sind keine Endpunkte down / There are currently no unavailable endpoints.");
}
return sb.ToString();
}
private static void AppendResult(StringBuilder sb, ProbeResult result, IDictionary<string, TargetState> currentState, string transitionText)
{
sb.AppendLine("- Anwendung / Application: " + Safe(result.ApplicationName));
sb.AppendLine(" Name: " + Safe(result.Name));
sb.AppendLine(" Endpoint: " + Safe(result.Endpoint));
sb.AppendLine(" Typ / Kind: " + result.Kind);
sb.AppendLine(" Transition: " + Safe(transitionText));
sb.AppendLine(" Status: " + Safe(result.StatusText));
sb.AppendLine(" Detail: " + Safe(result.Detail));
if (!string.IsNullOrWhiteSpace(result.Host))
{
sb.AppendLine(" Host: " + result.Host);
}
if (result.Port.HasValue)
{
sb.AppendLine(" Port: " + result.Port.Value);
}
if (result.HttpStatusCode.HasValue)
{
sb.AppendLine(" HTTP Status: " + result.HttpStatusCode.Value);
}
sb.AppendLine(" Dauer(ms) / Duration(ms): " + Math.Max(0L, (long)result.Duration.TotalMilliseconds));
TargetState state;
if (currentState != null && currentState.TryGetValue(result.StateKey, out state) && state != null)
{
sb.AppendLine(" ConsecutiveFailures: " + state.ConsecutiveFailures);
sb.AppendLine(" LastChangedUtc: " + state.LastChangedUtc.ToString("yyyy-MM-dd HH:mm:ss") + "Z");
}
}
private static string Safe(string value)
{
return string.IsNullOrWhiteSpace(value) ? "n/a" : value;
}
}
public sealed class DailySummaryEmailData
{
public DateTimeOffset GeneratedAtLocal { get; set; }
public DateTimeOffset WindowStartLocal { get; set; }
public int ProbesTodayTotal { get; set; }
public int ProbesTodaySuccess { get; set; }
public int ProbesTodayFailure { get; set; }
public int ProbesTodaySkipped { get; set; }
public int CurrentTargetsTotal { get; set; }
public int CurrentTargetsUp { get; set; }
public int CurrentTargetsDown { get; set; }
public int CurrentTargetsSkipped { get; set; }
public List<ProbeResult> CurrentFailures { get; set; } = new List<ProbeResult>();
public IDictionary<string, TargetState> CurrentState { get; set; } = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
}
}
+387 -251
View File
@@ -1,251 +1,387 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HostAvailabilityMonitor.Configuration;
using HostAvailabilityMonitor.Logging;
using HostAvailabilityMonitor.Monitoring;
using HostAvailabilityMonitor.Notifications;
using HostAvailabilityMonitor.State;
namespace HostAvailabilityMonitor
{
internal static class Program
{
private static int Main(string[] args)
{
return MainAsync(args).GetAwaiter().GetResult();
}
private static async Task<int> MainAsync(string[] args)
{
FileLogger logger = null;
WindowsEventLogger eventLogger = null;
var nonZeroExitCodeOnExecutionError = true;
try
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
? args[0]
: Path.Combine(baseDirectory, "appsettings.json");
var config = MonitorConfiguration.Load(configPath);
config.Validate();
nonZeroExitCodeOnExecutionError = config.Runtime.NonZeroExitCodeOnExecutionError;
var logDirectory = ResolvePath(baseDirectory, config.Logging.LogDirectory);
var stateDirectory = ResolvePath(baseDirectory, config.Runtime.StateDirectory);
var stateFilePath = Path.Combine(stateDirectory, "monitor-state.json");
logger = new FileLogger(logDirectory, config.Logging.FilePrefix, config.Logging.RetentionDays);
logger.CleanupOldFiles();
logger.Info(config.ApplicationName + " gestartet.");
logger.Info("Konfiguration geladen: " + Path.GetFullPath(configPath));
eventLogger = new WindowsEventLogger(config.EventLog, logger);
eventLogger.TryInitialize();
eventLogger.Info(config.ApplicationName + " gestartet.", 1000);
Directory.CreateDirectory(stateDirectory);
var stateStore = new MonitorStateStore(stateFilePath);
Dictionary<string, TargetState> previousState;
try
{
previousState = stateStore.Load();
}
catch (Exception ex)
{
logger.Warn("Statusdatei konnte nicht geladen werden. Es wird mit leerem Zustand fortgefahren. " + ex.GetType().Name + ": " + ex.Message);
previousState = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
}
var currentState = new Dictionary<string, TargetState>(previousState, StringComparer.OrdinalIgnoreCase);
var probeService = new EndpointProbeService(config.Runtime);
var enabledTargets = config.Targets.Where(t => t != null && t.Enabled).ToList();
if (enabledTargets.Count == 0)
{
logger.Warn("Es sind keine aktivierten Targets vorhanden. Der Lauf endet ohne Checks.");
eventLogger.Summary("HostAvailabilityMonitor ausgeführt, aber es waren keine aktivierten Targets vorhanden.", false);
return 0;
}
var results = await ExecuteChecksAsync(enabledTargets, probeService, config.Runtime.MaxConcurrency).ConfigureAwait(false);
foreach (var result in results.OrderBy(r => r.Name, StringComparer.OrdinalIgnoreCase))
{
logger.Probe(result);
TargetState previous;
previousState.TryGetValue(result.StateKey, out previous);
UpdateState(currentState, result, previous);
if (!result.IsSuccess)
{
eventLogger.Warning("Check fehlgeschlagen: " + result.Name + " | " + result.Endpoint + " | " + result.Detail, 2000);
}
else
{
eventLogger.InfoIfSuccessEnabled("Check erfolgreich: " + result.Name + " | " + result.Endpoint + " | " + result.Detail, 1001);
}
}
var notifier = new EmailNotifier(config.Email);
var failuresToNotify = new List<ProbeResult>();
var recoveriesToNotify = new List<ProbeResult>();
var notifiedKeys = new List<string>();
foreach (var result in results)
{
TargetState oldState;
previousState.TryGetValue(result.StateKey, out oldState);
if (!notifier.ShouldNotify(result, oldState))
{
continue;
}
if (result.IsSuccess)
{
recoveriesToNotify.Add(result);
}
else
{
failuresToNotify.Add(result);
}
notifiedKeys.Add(result.StateKey);
}
if (notifier.IsEnabled)
{
try
{
await notifier.SendAsync(failuresToNotify, recoveriesToNotify, currentState, CancellationToken.None).ConfigureAwait(false);
if (failuresToNotify.Count > 0 || recoveriesToNotify.Count > 0)
{
foreach (var key in notifiedKeys)
{
TargetState stateEntry;
if (currentState.TryGetValue(key, out stateEntry) && stateEntry != null)
{
stateEntry.LastNotificationUtc = DateTime.UtcNow;
}
}
logger.Info("Benachrichtigungs-E-Mail versendet. Failures=" + failuresToNotify.Count + ", Recoveries=" + recoveriesToNotify.Count);
}
}
catch (Exception ex)
{
logger.Error("E-Mail-Versand fehlgeschlagen: " + ex.GetType().Name + ": " + ex.Message);
eventLogger.Error("E-Mail-Versand fehlgeschlagen: " + ex.Message, 9001);
}
}
stateStore.Save(currentState);
var total = results.Count;
var ok = results.Count(r => r.IsSuccess);
var failed = total - ok;
var summary = "Lauf beendet. Gesamt=" + total + ", Erfolgreich=" + ok + ", Fehlgeschlagen=" + failed + ".";
logger.Info(summary);
eventLogger.Summary(summary, failed > 0);
return failed > 0 && config.Runtime.NonZeroExitCodeOnAnyFailure ? 2 : 0;
}
catch (Exception ex)
{
if (logger != null)
{
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
}
if (eventLogger != null)
{
eventLogger.Error("Fataler Fehler: " + ex.Message, 9000);
}
return nonZeroExitCodeOnExecutionError ? 1 : 0;
}
}
private static async Task<List<ProbeResult>> ExecuteChecksAsync(IReadOnlyCollection<TargetOptions> targets, EndpointProbeService probeService, int maxConcurrency)
{
var results = new List<ProbeResult>();
using (var semaphore = new SemaphoreSlim(Math.Max(1, maxConcurrency)))
{
var tasks = targets.Select(async target =>
{
await semaphore.WaitAsync().ConfigureAwait(false);
try
{
return await probeService.ProbeAsync(target, CancellationToken.None).ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}).ToArray();
var completed = await Task.WhenAll(tasks).ConfigureAwait(false);
results.AddRange(completed);
}
return results;
}
private static void UpdateState(Dictionary<string, TargetState> currentState, ProbeResult result, TargetState previousState)
{
var nowUtc = DateTime.UtcNow;
TargetState state;
if (previousState == null)
{
state = new TargetState
{
IsUp = result.IsSuccess,
LastCheckedUtc = nowUtc,
LastChangedUtc = nowUtc,
ConsecutiveFailures = result.IsSuccess ? 0 : 1,
LastDetail = result.Detail
};
}
else
{
state = new TargetState
{
IsUp = previousState.IsUp,
LastCheckedUtc = nowUtc,
LastChangedUtc = previousState.LastChangedUtc,
ConsecutiveFailures = previousState.ConsecutiveFailures,
LastNotificationUtc = previousState.LastNotificationUtc,
LastDetail = result.Detail
};
}
if (previousState == null || previousState.IsUp != result.IsSuccess)
{
state.LastChangedUtc = nowUtc;
}
state.IsUp = result.IsSuccess;
state.LastCheckedUtc = nowUtc;
state.LastDetail = result.Detail;
state.ConsecutiveFailures = result.IsSuccess ? 0 : Math.Max(1, (previousState == null ? 0 : previousState.ConsecutiveFailures) + 1);
currentState[result.StateKey] = state;
}
private static string ResolvePath(string baseDirectory, string configuredPath)
{
if (Path.IsPathRooted(configuredPath))
{
return configuredPath;
}
return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath));
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HostAvailabilityMonitor.Configuration;
using HostAvailabilityMonitor.Logging;
using HostAvailabilityMonitor.Monitoring;
using HostAvailabilityMonitor.Notifications;
using HostAvailabilityMonitor.State;
namespace HostAvailabilityMonitor
{
internal static class Program
{
private static int Main(string[] args)
{
return MainAsync(args).GetAwaiter().GetResult();
}
private static async Task<int> MainAsync(string[] args)
{
FileLogger logger = null;
WindowsEventLogger eventLogger = null;
var nonZeroExitCodeOnExecutionError = true;
try
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
? args[0]
: Path.Combine(baseDirectory, "appsettings.json");
var config = MonitorConfiguration.Load(configPath);
config.Validate();
nonZeroExitCodeOnExecutionError = config.Runtime.NonZeroExitCodeOnExecutionError;
var logDirectory = ResolvePath(baseDirectory, config.Logging.LogDirectory);
var stateDirectory = ResolvePath(baseDirectory, config.Runtime.StateDirectory);
var stateFilePath = Path.Combine(stateDirectory, "monitor-state.json");
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: " + Path.GetFullPath(configPath));
eventLogger = new WindowsEventLogger(config.EventLog, logger);
eventLogger.TryInitialize();
eventLogger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000);
Directory.CreateDirectory(stateDirectory);
var stateStore = new MonitorStateStore(stateFilePath);
MonitorStateDocument stateDocument;
Dictionary<string, TargetState> previousState;
try
{
stateDocument = stateStore.LoadDocument();
previousState = stateDocument.TargetStates;
}
catch (Exception ex)
{
logger.Warn("Statusdatei konnte nicht geladen werden. Es wird mit leerem Zustand fortgefahren. " + ex.GetType().Name + ": " + ex.Message);
stateDocument = new MonitorStateDocument();
previousState = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
}
var currentState = new Dictionary<string, TargetState>(previousState, StringComparer.OrdinalIgnoreCase);
var probeService = new EndpointProbeService(config.Runtime);
var enabledTargets = config.Targets.Where(t => t != null && t.Enabled).ToList();
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);
return 0;
}
var results = await ExecuteChecksAsync(enabledTargets, probeService, config.Runtime.MaxConcurrency).ConfigureAwait(false);
foreach (var result in results.OrderBy(r => r.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(r => r.Name, StringComparer.OrdinalIgnoreCase))
{
logger.Probe(result);
TargetState previous;
previousState.TryGetValue(result.StateKey, out previous);
var isRecovery = previous != null && !previous.IsUp && result.IsSuccess;
if (!result.IsSkipped)
{
UpdateState(currentState, result, previous);
}
var eventMessage = BuildEventMessage(config, result);
if (result.IsSkipped)
{
logger.Warn("Target uebersprungen. Umgebung=" + config.EnvironmentName + ", Anwendung=" + result.ApplicationName + ", Name=" + result.Name + ", Endpoint=" + result.Endpoint + ", Detail=" + result.Detail + ".");
eventLogger.Info("Target uebersprungen. " + eventMessage, 1100);
}
else if (!result.IsSuccess)
{
eventLogger.Warning(eventMessage, 2000);
}
else if (isRecovery)
{
logger.Info("Recovery erkannt. Umgebung=" + config.EnvironmentName + ", Anwendung=" + result.ApplicationName + ", Name=" + result.Name + ", Endpoint=" + result.Endpoint + ".");
eventLogger.Info("Recovery erkannt. " + eventMessage, 1201);
}
else
{
eventLogger.InfoIfSuccessEnabled(eventMessage, 1001);
}
}
stateDocument.TargetStates = currentState;
stateDocument.Metadata = stateDocument.Metadata ?? new MonitorStateMetadata();
var notifier = new EmailNotifier(config.Email, config.ApplicationName, config.EnvironmentName);
var failuresToNotify = new List<ProbeResult>();
var recoveriesToNotify = new List<ProbeResult>();
var notifiedKeys = new List<string>();
foreach (var result in results)
{
TargetState oldState;
previousState.TryGetValue(result.StateKey, out oldState);
if (!notifier.ShouldNotify(result, oldState))
{
continue;
}
if (result.IsSuccess)
{
recoveriesToNotify.Add(result);
}
else
{
failuresToNotify.Add(result);
}
notifiedKeys.Add(result.StateKey);
}
if (notifier.IsEnabled)
{
try
{
await notifier.SendAsync(failuresToNotify, recoveriesToNotify, currentState, CancellationToken.None).ConfigureAwait(false);
if (failuresToNotify.Count > 0 || recoveriesToNotify.Count > 0)
{
foreach (var key in notifiedKeys)
{
TargetState stateEntry;
if (currentState.TryGetValue(key, out stateEntry) && stateEntry != null)
{
stateEntry.LastNotificationUtc = DateTime.UtcNow;
}
}
logger.Info("Benachrichtigungs-E-Mail versendet. Umgebung=" + config.EnvironmentName + ", Failures=" + failuresToNotify.Count + ", Recoveries=" + recoveriesToNotify.Count + ".");
}
}
catch (Exception ex)
{
logger.Error("E-Mail-Versand fehlgeschlagen: " + ex.GetType().Name + ": " + ex.Message);
eventLogger.Error("E-Mail-Versand fehlgeschlagen. Umgebung=" + config.EnvironmentName + ". Fehler=" + ex.Message, 9001);
}
}
await TrySendDailySummaryAsync(config, notifier, stateDocument, currentState, results, logDirectory, logger, eventLogger).ConfigureAwait(false);
stateStore.Save(stateDocument);
var total = results.Count(r => !r.IsSkipped);
var ok = results.Count(r => r.IsSuccess);
var failed = results.Count(r => !r.IsSkipped && !r.IsSuccess);
var skipped = results.Count(r => r.IsSkipped);
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);
return failed > 0 && config.Runtime.NonZeroExitCodeOnAnyFailure ? 2 : 0;
}
catch (Exception ex)
{
if (logger != null)
{
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
}
if (eventLogger != null)
{
eventLogger.Error("Fataler Fehler: " + ex.Message, 9000);
}
return nonZeroExitCodeOnExecutionError ? 1 : 0;
}
}
private static async Task<List<ProbeResult>> ExecuteChecksAsync(IReadOnlyCollection<TargetOptions> targets, EndpointProbeService probeService, int maxConcurrency)
{
var results = new List<ProbeResult>();
using (var semaphore = new SemaphoreSlim(Math.Max(1, maxConcurrency)))
{
var tasks = targets.Select(async target =>
{
await semaphore.WaitAsync().ConfigureAwait(false);
try
{
return await probeService.ProbeAsync(target, CancellationToken.None).ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}).ToArray();
var completed = await Task.WhenAll(tasks).ConfigureAwait(false);
results.AddRange(completed);
}
return results;
}
private static async Task TrySendDailySummaryAsync(
MonitorConfiguration config,
EmailNotifier notifier,
MonitorStateDocument stateDocument,
IDictionary<string, TargetState> currentState,
IReadOnlyCollection<ProbeResult> results,
string logDirectory,
FileLogger logger,
WindowsEventLogger eventLogger)
{
if (config == null || notifier == null || stateDocument == null)
{
return;
}
var nowLocal = DateTime.Now;
if (!notifier.ShouldSendDailySummary(stateDocument.Metadata, nowLocal))
{
return;
}
try
{
var counters = ReadTodayProbeCounters(logDirectory, config.Logging.FilePrefix, nowLocal, logger);
var latestResults = results == null ? new List<ProbeResult>() : results.ToList();
var currentFailures = latestResults.Where(r => !r.IsSkipped && !r.IsSuccess).ToList();
var summary = new DailySummaryEmailData
{
GeneratedAtLocal = DateTimeOffset.Now,
WindowStartLocal = new DateTimeOffset(nowLocal.Date),
ProbesTodaySuccess = counters.SuccessCount,
ProbesTodayFailure = counters.FailureCount,
ProbesTodaySkipped = counters.SkipCount,
ProbesTodayTotal = counters.SuccessCount + counters.FailureCount + counters.SkipCount,
CurrentTargetsTotal = latestResults.Count,
CurrentTargetsUp = latestResults.Count(r => r.IsSuccess),
CurrentTargetsDown = latestResults.Count(r => !r.IsSkipped && !r.IsSuccess),
CurrentTargetsSkipped = latestResults.Count(r => r.IsSkipped),
CurrentFailures = currentFailures,
CurrentState = currentState
};
await notifier.SendDailySummaryAsync(summary, CancellationToken.None).ConfigureAwait(false);
stateDocument.Metadata.LastDailySummarySentLocalDate = nowLocal.ToString("yyyy-MM-dd");
logger.Info("Tagesstatus-E-Mail versendet. Umgebung=" + config.EnvironmentName + ", Versandzeitpunkt=" + nowLocal.ToString("yyyy-MM-dd HH:mm:ss") + ", AktuellDown=" + summary.CurrentTargetsDown + ", AktuellUebersprungen=" + summary.CurrentTargetsSkipped + ", TagesChecks=" + summary.ProbesTodayTotal + ".");
eventLogger.Info("Tagesstatus-E-Mail versendet. Umgebung=" + config.EnvironmentName + ", AktuellDown=" + summary.CurrentTargetsDown + ", AktuellUebersprungen=" + summary.CurrentTargetsSkipped + ", TagesChecks=" + summary.ProbesTodayTotal + ".", 1300);
}
catch (Exception ex)
{
logger.Error("Tagesstatus-E-Mail konnte nicht versendet werden: " + ex.GetType().Name + ": " + ex.Message);
eventLogger.Error("Tagesstatus-E-Mail fehlgeschlagen. Umgebung=" + config.EnvironmentName + ". Fehler=" + ex.Message, 9002);
}
}
private static ProbeLogCounters ReadTodayProbeCounters(string logDirectory, string filePrefix, DateTime nowLocal, FileLogger logger)
{
var counters = new ProbeLogCounters();
try
{
var logPath = Path.Combine(logDirectory, filePrefix + "-" + nowLocal.ToString("yyyy-MM-dd") + ".log");
if (!File.Exists(logPath))
{
return counters;
}
foreach (var line in File.ReadLines(logPath))
{
if (line.IndexOf("Monitor=", StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
if (line.IndexOf("| SUCCESS |", StringComparison.OrdinalIgnoreCase) >= 0)
{
counters.SuccessCount++;
}
else if (line.IndexOf("| FAIL |", StringComparison.OrdinalIgnoreCase) >= 0)
{
counters.FailureCount++;
}
else if (line.IndexOf("| SKIP |", StringComparison.OrdinalIgnoreCase) >= 0)
{
counters.SkipCount++;
}
}
}
catch (Exception ex)
{
logger.Warn("Tageslog konnte für die Status-Mail nicht ausgewertet werden. Es wird mit 0 Tageszählern fortgefahren. " + ex.GetType().Name + ": " + ex.Message);
}
return counters;
}
private static void UpdateState(Dictionary<string, TargetState> currentState, ProbeResult result, TargetState previousState)
{
var nowUtc = DateTime.UtcNow;
TargetState state;
if (previousState == null)
{
state = new TargetState
{
IsUp = result.IsSuccess,
LastCheckedUtc = nowUtc,
LastChangedUtc = nowUtc,
ConsecutiveFailures = result.IsSuccess ? 0 : 1,
LastDetail = result.Detail
};
}
else
{
state = new TargetState
{
IsUp = previousState.IsUp,
LastCheckedUtc = nowUtc,
LastChangedUtc = previousState.LastChangedUtc,
ConsecutiveFailures = previousState.ConsecutiveFailures,
LastNotificationUtc = previousState.LastNotificationUtc,
LastDetail = result.Detail
};
}
if (previousState == null || previousState.IsUp != result.IsSuccess)
{
state.LastChangedUtc = nowUtc;
}
state.IsUp = result.IsSuccess;
state.LastCheckedUtc = nowUtc;
state.LastDetail = result.Detail;
state.ConsecutiveFailures = result.IsSuccess ? 0 : Math.Max(1, (previousState == null ? 0 : previousState.ConsecutiveFailures) + 1);
currentState[result.StateKey] = state;
}
private static string ResolvePath(string baseDirectory, string configuredPath)
{
if (Path.IsPathRooted(configuredPath))
{
return configuredPath;
}
return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath));
}
private static string BuildEventMessage(MonitorConfiguration config, ProbeResult result)
{
return string.Format(
"Umgebung={0} | Anwendung={1} | Name={2} | Endpoint={3} | Status={4} | Detail={5}",
config.EnvironmentName,
result.ApplicationName,
result.Name,
result.Endpoint,
result.StatusText,
result.Detail);
}
private sealed class ProbeLogCounters
{
public int SuccessCount { get; set; }
public int FailureCount { get; set; }
public int SkipCount { get; set; }
}
}
}
@@ -1,15 +1,15 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("HostAvailabilityMonitor")]
[assembly: AssemblyDescription("Windows Server 2019 kompatibler Host Availability Monitor auf .NET Framework 4.8")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("JR IT Services")]
[assembly: AssemblyProduct("HostAvailabilityMonitor")]
[assembly: AssemblyCopyright("Copyright © JR IT Services")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("8f55f0bf-6041-4367-93a6-777387f436bf")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("HostAvailabilityMonitor")]
[assembly: AssemblyDescription("Windows Server 2019 kompatibler Host Availability Monitor auf .NET Framework 4.7.2")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("JR IT Services")]
[assembly: AssemblyProduct("HostAvailabilityMonitor")]
[assembly: AssemblyCopyright("Copyright © JR IT Services")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("8f55f0bf-6041-4367-93a6-777387f436bf")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -1,76 +1,139 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Web.Script.Serialization;
namespace HostAvailabilityMonitor.State
{
public sealed class MonitorStateStore
{
private readonly string _filePath;
public MonitorStateStore(string filePath)
{
_filePath = filePath;
}
public Dictionary<string, TargetState> Load()
{
if (!File.Exists(_filePath))
{
return new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
}
var json = File.ReadAllText(_filePath);
var serializer = new JavaScriptSerializer();
var states = serializer.Deserialize<Dictionary<string, TargetState>>(json);
if (states == null)
{
return new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
}
return new Dictionary<string, TargetState>(states, StringComparer.OrdinalIgnoreCase);
}
public void Save(Dictionary<string, TargetState> states)
{
var directory = Path.GetDirectoryName(_filePath);
if (string.IsNullOrWhiteSpace(directory))
{
throw new InvalidOperationException("Das Verzeichnis der Statusdatei konnte nicht ermittelt werden.");
}
Directory.CreateDirectory(directory);
var serializer = new JavaScriptSerializer();
var json = serializer.Serialize(states ?? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase));
var tempFile = _filePath + ".tmp";
File.WriteAllText(tempFile, json);
if (File.Exists(_filePath))
{
var backupFile = _filePath + ".bak";
File.Replace(tempFile, _filePath, backupFile, true);
if (File.Exists(backupFile))
{
File.Delete(backupFile);
}
}
else
{
File.Move(tempFile, _filePath);
}
}
}
public sealed class TargetState
{
public bool IsUp { get; set; }
public int ConsecutiveFailures { get; set; }
public DateTime LastCheckedUtc { get; set; }
public DateTime LastChangedUtc { get; set; }
public DateTime? LastNotificationUtc { get; set; }
public string LastDetail { get; set; } = string.Empty;
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Web.Script.Serialization;
namespace HostAvailabilityMonitor.State
{
public sealed class MonitorStateStore
{
private readonly string _filePath;
public MonitorStateStore(string filePath)
{
_filePath = filePath;
}
public MonitorStateDocument LoadDocument()
{
if (!File.Exists(_filePath))
{
return new MonitorStateDocument();
}
var json = File.ReadAllText(_filePath);
var serializer = new JavaScriptSerializer();
try
{
var document = serializer.Deserialize<MonitorStateDocument>(json);
if (document != null && document.TargetStates != null)
{
return Normalize(document);
}
}
catch
{
// Fall back to legacy format.
}
try
{
var states = serializer.Deserialize<Dictionary<string, TargetState>>(json);
return new MonitorStateDocument
{
TargetStates = states == null
? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, TargetState>(states, StringComparer.OrdinalIgnoreCase),
Metadata = new MonitorStateMetadata()
};
}
catch
{
return new MonitorStateDocument();
}
}
public Dictionary<string, TargetState> Load()
{
return LoadDocument().TargetStates;
}
public void Save(MonitorStateDocument document)
{
var directory = Path.GetDirectoryName(_filePath);
if (string.IsNullOrWhiteSpace(directory))
{
throw new InvalidOperationException("Das Verzeichnis der Statusdatei konnte nicht ermittelt werden.");
}
Directory.CreateDirectory(directory);
var normalized = Normalize(document ?? new MonitorStateDocument());
var serializer = new JavaScriptSerializer();
var json = serializer.Serialize(normalized);
var tempFile = _filePath + ".tmp";
File.WriteAllText(tempFile, json);
if (File.Exists(_filePath))
{
var backupFile = _filePath + ".bak";
File.Replace(tempFile, _filePath, backupFile, true);
if (File.Exists(backupFile))
{
File.Delete(backupFile);
}
}
else
{
File.Move(tempFile, _filePath);
}
}
public void Save(Dictionary<string, TargetState> states)
{
Save(new MonitorStateDocument
{
TargetStates = states == null
? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, TargetState>(states, StringComparer.OrdinalIgnoreCase),
Metadata = new MonitorStateMetadata()
});
}
private static MonitorStateDocument Normalize(MonitorStateDocument document)
{
if (document == null)
{
document = new MonitorStateDocument();
}
document.TargetStates = document.TargetStates == null
? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, TargetState>(document.TargetStates, StringComparer.OrdinalIgnoreCase);
document.Metadata = document.Metadata ?? new MonitorStateMetadata();
return document;
}
}
public sealed class MonitorStateDocument
{
public Dictionary<string, TargetState> TargetStates { get; set; } = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
public MonitorStateMetadata Metadata { get; set; } = new MonitorStateMetadata();
}
public sealed class MonitorStateMetadata
{
public string LastDailySummarySentLocalDate { get; set; } = string.Empty;
}
public sealed class TargetState
{
public bool IsUp { get; set; }
public int ConsecutiveFailures { get; set; }
public DateTime LastCheckedUtc { get; set; }
public DateTime LastChangedUtc { get; set; }
public DateTime? LastNotificationUtc { get; set; }
public string LastDetail { get; set; } = string.Empty;
}
}
+92 -70
View File
@@ -1,70 +1,92 @@
{
"ApplicationName": "HostAvailabilityMonitor",
"Runtime": {
"MaxConcurrency": 4,
"DefaultValidateUncPathAccess": false,
"NonZeroExitCodeOnAnyFailure": false,
"NonZeroExitCodeOnExecutionError": true,
"StateDirectory": "state"
},
"Logging": {
"LogDirectory": "logs",
"FilePrefix": "host-availability-monitor",
"RetentionDays": 5
},
"EventLog": {
"Enabled": true,
"LogName": "Application",
"Source": "HostAvailabilityMonitor",
"CreateSourceIfMissing": false,
"WriteSuccessEntries": false,
"WriteSummaryEntries": true
},
"Email": {
"Enabled": false,
"SmtpHost": "smtp.example.local",
"SmtpPort": 25,
"UseSsl": false,
"Username": "",
"Password": "",
"From": "monitor@example.local",
"To": [
"operations@example.local"
],
"SubjectPrefix": "[HostAvailabilityMonitor]",
"SendOnFailure": true,
"SendOnRecovery": true,
"OnlyOnStateChange": true,
"CooldownMinutes": 0,
"TimeoutSeconds": 30
},
"Targets": [
{
"Name": "Internes Fileshare 1",
"Endpoint": "\\\\internerserver1\\share1",
"TimeoutSeconds": 8,
"ValidateUncPathAccess": false,
"Enabled": true
},
{
"Name": "Externe Webseite",
"Endpoint": "https://host1.de/url",
"TimeoutSeconds": 10,
"HttpMethod": "HEAD",
"Enabled": true
},
{
"Name": "SFTP Partner A",
"Endpoint": "sftp://partner-sftp.example.local/inbound",
"TimeoutSeconds": 10,
"Port": 22,
"Enabled": true
},
{
"Name": "Custom TCP Service",
"Endpoint": "tcp://host2.example.local:8443",
"TimeoutSeconds": 10,
"Enabled": true
}
]
}
{
"ApplicationName": "HostAvailabilityMonitor",
"EnvironmentName": "HIP Produktion",
"Runtime": {
"MaxConcurrency": 4,
"DefaultValidateUncPathAccess": false,
"NonZeroExitCodeOnAnyFailure": false,
"NonZeroExitCodeOnExecutionError": true,
"StateDirectory": "state"
},
"Logging": {
"LogDirectory": "logs",
"FilePrefix": "host-availability-monitor",
"RetentionDays": 5
},
"EventLog": {
"Enabled": true,
"LogName": "Application",
"Source": "HostAvailabilityMonitor",
"CreateSourceIfMissing": false,
"WriteSuccessEntries": false,
"WriteSummaryEntries": true
},
"Email": {
"Enabled": false,
"SmtpHost": "smtp.example.local",
"SmtpPort": 25,
"UseSsl": false,
"UseDefaultCredentials": false,
"Username": "",
"Password": "",
"From": "monitor@example.local",
"To": [
"operations@example.local",
"integration-oncall@example.local"
],
"Cc": [
"integration-leads@example.local"
],
"Bcc": [],
"SubjectPrefix": "[HostAvailabilityMonitor]",
"SendOnFailure": true,
"SendOnRecovery": true,
"SendDailySummary": true,
"DailySummaryHourLocal": 14,
"DailySummaryMinuteLocal": 0,
"OnlyOnStateChange": true,
"CooldownMinutes": 0,
"TimeoutSeconds": 30
},
"Targets": [
{
"Name": "Internes Fileshare 1",
"ApplicationName": "HIP FileTransfer",
"Endpoint": "\\\\internerserver1\\share1",
"TimeoutSeconds": 8,
"ValidateUncPathAccess": false,
"Enabled": true
},
{
"Name": "Externe Webseite",
"ApplicationName": "HIP WebPortal",
"Endpoint": "https://host1.de/url",
"TimeoutSeconds": 10,
"HttpMethod": "HEAD",
"Enabled": true
},
{
"Name": "SFTP Partner A",
"ApplicationName": "HIP Partneranbindung",
"Endpoint": "sftp://partner-sftp.example.local/inbound",
"TimeoutSeconds": 10,
"Port": 22,
"Enabled": true
},
{
"Name": "Network File URI",
"ApplicationName": "HIP Dokumentenservice",
"Endpoint": "file://fileserver01/share/export",
"TimeoutSeconds": 10,
"ValidateUncPathAccess": false,
"Enabled": true
},
{
"Name": "Custom TCP Service",
"ApplicationName": "HIP Adapterdienst",
"Endpoint": "tcp://host2.example.local:8443",
"TimeoutSeconds": 10,
"Enabled": true
}
]
}