Add BizTalk endpoint reachability monitoring
This commit is contained in:
@@ -14,6 +14,16 @@
|
||||
<add key="LogDirectory" value="%ProgramData%\BizTalkCheckmkPulse\logs" />
|
||||
<add key="LogRetentionDays" value="30" />
|
||||
|
||||
<!-- Automatisch gepflegter, geheimnisfreier Katalog aktiver Send-/Receive-Endpunkte. -->
|
||||
<add key="ProbeEndpointConnectivity" value="true" />
|
||||
<add key="EndpointCatalogPath" value="%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml" />
|
||||
<add key="EndpointCatalogMaxBytes" value="1048576" />
|
||||
<!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. -->
|
||||
<add key="EndpointDiscoveryIntervalHours" value="168" />
|
||||
<add key="EndpointProbeTimeoutMilliseconds" value="3000" />
|
||||
<add key="EndpointProbeMaxConcurrency" value="12" />
|
||||
<add key="EndpointMaxCount" value="500" />
|
||||
|
||||
<add key="QueryTimeoutSeconds" value="25" />
|
||||
<!-- Testet die Anmeldung des privilegierten Provider-Kontos an den ermittelten BizTalk-Datenbanken. -->
|
||||
<add key="ProbeSqlConnectivity" value="true" />
|
||||
|
||||
@@ -38,11 +38,16 @@
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="CheckmkLocalFormatter.cs" />
|
||||
<Compile Include="EventLogProbe.cs" />
|
||||
<Compile Include="EndpointAddressParser.cs" />
|
||||
<Compile Include="EndpointCatalogStore.cs" />
|
||||
<Compile Include="EndpointConnectivityProbe.cs" />
|
||||
<Compile Include="FileLogger.cs" />
|
||||
<Compile Include="MonitoringOptions.cs" />
|
||||
<Compile Include="Models.cs" />
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace BizTalkCheckmkPulse
|
||||
yield return FormatHostInstances(result);
|
||||
yield return FormatReceiveLocations(result);
|
||||
yield return FormatSendPorts(result);
|
||||
yield return FormatEndpointConnectivity(result);
|
||||
yield return FormatOrchestrations(result);
|
||||
yield return FormatEventLog(result);
|
||||
|
||||
@@ -70,6 +71,7 @@ namespace BizTalkCheckmkPulse
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("Host Instances"), "biztalk_host_instances_total=0;;;0", "Self test OK. No WMI query was executed.");
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("Receive Locations"), "biztalk_receive_locations_total=0;;;0", "Self test OK. No WMI query was executed.");
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("Send Ports"), "biztalk_send_ports_total=0;;;0", "Self test OK. No WMI query was executed.");
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("Endpoint Reachability"), "biztalk_endpoints_active=0;;;0", "Self test OK. No network connection was opened.");
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("Orchestrations"), "biztalk_orchestrations_total=0;;;0", "Self test OK. No WMI query was executed.");
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("Event Log"), "biztalk_eventlog_errors=0;;;0", "Self test OK. No event log was read.");
|
||||
}
|
||||
@@ -88,6 +90,7 @@ namespace BizTalkCheckmkPulse
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", action);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Receive Locations"), "-", action);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Send Ports"), "-", action);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Endpoint Reachability"), "-", action);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Orchestrations"), "-", action);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", action);
|
||||
}
|
||||
@@ -106,6 +109,7 @@ namespace BizTalkCheckmkPulse
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", detail);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Receive Locations"), "-", detail);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Send Ports"), "-", detail);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Endpoint Reachability"), "-", detail);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Orchestrations"), "-", detail);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", detail);
|
||||
}
|
||||
@@ -350,6 +354,80 @@ namespace BizTalkCheckmkPulse
|
||||
return BuildLine(state, _options.ServiceName("Send Ports"), metrics, detail.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert die Netzwerk-Erreichbarkeit aller aktuell aktiven, pruefbaren BizTalk-Endpunkte.
|
||||
/// </summary>
|
||||
private string FormatEndpointConnectivity(ProbeResult result)
|
||||
{
|
||||
var endpointState = result.EndpointConnectivity;
|
||||
if (endpointState.Disabled)
|
||||
{
|
||||
return BuildLine(
|
||||
CheckState.Ok,
|
||||
_options.ServiceName("Endpoint Reachability"),
|
||||
"-",
|
||||
"Endpoint connectivity probe disabled by configuration.");
|
||||
}
|
||||
|
||||
if (!endpointState.RuntimeStateAvailable || !endpointState.CatalogAvailable)
|
||||
{
|
||||
return BuildLine(
|
||||
CheckState.Unknown,
|
||||
_options.ServiceName("Endpoint Reachability"),
|
||||
"-",
|
||||
"Endpoint-Pruefung nicht verlaesslich moeglich. " + EmptyAsUnknown(endpointState.Failure));
|
||||
}
|
||||
|
||||
var failed = endpointState.Results.Where(x => !x.Available).ToArray();
|
||||
var available = endpointState.Results.Count(x => x.Available);
|
||||
var incomplete = endpointState.Results.Count != endpointState.Active
|
||||
|| endpointState.UnsupportedActive > 0
|
||||
|| endpointState.RefreshRequired && !endpointState.RefreshSucceeded
|
||||
|| !string.IsNullOrWhiteSpace(endpointState.Failure);
|
||||
var state = failed.Length > 0
|
||||
? CheckState.Critical
|
||||
: incomplete
|
||||
? CheckState.Unknown
|
||||
: CheckState.Ok;
|
||||
var metrics = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_tested={2};;;0|biztalk_endpoints_available={3};;;0|biztalk_endpoints_failed={4};;1;0|biztalk_endpoints_unsupported={5};;1;0|biztalk_endpoints_inactive_skipped={6};;;0",
|
||||
endpointState.Configured,
|
||||
endpointState.Active,
|
||||
endpointState.Results.Count,
|
||||
available,
|
||||
failed.Length,
|
||||
endpointState.UnsupportedActive,
|
||||
endpointState.SkippedInactive);
|
||||
var detail = new StringBuilder();
|
||||
if (failed.Length == 0 && !incomplete)
|
||||
{
|
||||
detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar");
|
||||
}
|
||||
else
|
||||
{
|
||||
detail.Append("Endpoint reachability active=").Append(endpointState.Active)
|
||||
.Append(", tested=").Append(endpointState.Results.Count)
|
||||
.Append(", available=").Append(available)
|
||||
.Append(", failed=").Append(failed.Length)
|
||||
.Append(", unsupported_external=").Append(endpointState.UnsupportedActive);
|
||||
AppendLimitedList(detail, "unavailable", failed.Select(x =>
|
||||
EndpointConnectivityProbe.Display(x.Endpoint) + "(" + EmptyAsUnknown(x.Failure) + ")"));
|
||||
if (!string.IsNullOrWhiteSpace(endpointState.Failure))
|
||||
{
|
||||
detail.Append("; catalog_or_probe_error=").Append(endpointState.Failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (endpointState.CatalogSynchronizedUtc.HasValue)
|
||||
{
|
||||
detail.Append("; catalog_utc=")
|
||||
.Append(endpointState.CatalogSynchronizedUtc.Value.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return BuildLine(state, _options.ServiceName("Endpoint Reachability"), metrics, detail.ToString());
|
||||
}
|
||||
|
||||
private string FormatOrchestrations(ProbeResult result)
|
||||
{
|
||||
if (!result.Platform.OrchestrationsDataAvailable)
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Reduziert adapter-spezifische BizTalk-Adressen auf einen geheimnisfreien Host/Port-Test.
|
||||
/// </summary>
|
||||
internal static class EndpointAddressParser
|
||||
{
|
||||
private static readonly Regex HostPortPattern = new Regex(
|
||||
@"^(?<host>\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):(?<port>\d{1,5})(?:[/\\].*)?$",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag.
|
||||
/// </summary>
|
||||
public static bool TryCreate(
|
||||
EndpointCandidate candidate,
|
||||
out EndpointCatalogEntry entry,
|
||||
out string reason)
|
||||
{
|
||||
entry = null;
|
||||
reason = string.Empty;
|
||||
if (candidate == null)
|
||||
{
|
||||
reason = "Endpoint-Kandidat fehlt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate.Dynamic)
|
||||
{
|
||||
reason = "Dynamischer Send Port besitzt kein statisch pruefbares Ziel.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var address = (candidate.Address ?? string.Empty).Trim();
|
||||
if (address.Length == 0)
|
||||
{
|
||||
reason = "Transportadresse ist leer.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string protocol;
|
||||
string host;
|
||||
int port;
|
||||
if (!TryResolve(address, candidate.AdapterName, out protocol, out host, out port, out reason))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = new EndpointCatalogEntry
|
||||
{
|
||||
Key = candidate.Key,
|
||||
ArtifactType = candidate.ArtifactType,
|
||||
ApplicationName = candidate.ApplicationName,
|
||||
ArtifactName = candidate.ArtifactName,
|
||||
TransportRole = candidate.TransportRole,
|
||||
AdapterName = candidate.AdapterName,
|
||||
Protocol = protocol,
|
||||
Host = host,
|
||||
Port = port,
|
||||
Enabled = true,
|
||||
AutoDiscovered = true
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen.
|
||||
/// </summary>
|
||||
public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
|
||||
{
|
||||
if (candidate == null || candidate.Dynamic)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var address = (candidate.Address ?? string.Empty).Trim();
|
||||
if (address.Length == 0
|
||||
|| LooksLikeEmailRecipients(address)
|
||||
|| Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|
||||
|| address.StartsWith("/", StringComparison.Ordinal)
|
||||
|| address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|
||||
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Contains(candidate.AdapterName, "SMTP")
|
||||
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address.StartsWith("\\\\", StringComparison.Ordinal)
|
||||
|| HostPortPattern.IsMatch(address)
|
||||
|| Regex.IsMatch(address, @"(?:FORMATNAME:)?DIRECT=(?:OS|TCP):", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(address, UriKind.Absolute, out uri))
|
||||
{
|
||||
return !string.Equals(uri.Scheme, "file", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(uri.Scheme, "mailto", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return Contains(candidate.AdapterName, "SFTP")
|
||||
|| Contains(candidate.AdapterName, "FTP")
|
||||
|| Contains(candidate.AdapterName, "HTTP")
|
||||
|| Contains(candidate.AdapterName, "SOAP")
|
||||
|| Contains(candidate.AdapterName, "WCF");
|
||||
}
|
||||
|
||||
private static bool TryResolve(
|
||||
string address,
|
||||
string adapterName,
|
||||
out string protocol,
|
||||
out string host,
|
||||
out int port,
|
||||
out string reason)
|
||||
{
|
||||
protocol = "TCP";
|
||||
host = string.Empty;
|
||||
port = 0;
|
||||
reason = string.Empty;
|
||||
|
||||
if (LooksLikeEmailRecipients(address))
|
||||
{
|
||||
reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
{
|
||||
var end = address.IndexOf('\\', 2);
|
||||
host = end < 0 ? address.Substring(2) : address.Substring(2, end - 2);
|
||||
port = 445;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|
||||
|| Path.IsPathRooted(address) && !address.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
reason = "Lokaler Dateipfad benoetigt keine externe Netzwerkprobe.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryResolveMsmq(address, out host))
|
||||
{
|
||||
port = 1801;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(address, UriKind.Absolute, out uri)
|
||||
&& !string.IsNullOrWhiteSpace(uri.Scheme))
|
||||
{
|
||||
var scheme = uri.Scheme.ToLowerInvariant();
|
||||
if (scheme == "file")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri.Host))
|
||||
{
|
||||
reason = "Lokaler file-Endpunkt benoetigt keine externe Netzwerkprobe.";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = uri.Host;
|
||||
port = 445;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (scheme == "net.pipe" || scheme == "npipe")
|
||||
{
|
||||
reason = "Named Pipes sind keine TCP-/UDP-Endpunkte.";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = uri.Host;
|
||||
protocol = scheme == "udp" ? "UDP" : "TCP";
|
||||
port = ResolvePort(uri, scheme);
|
||||
if (port <= 0 && HasExplicitPort(address))
|
||||
{
|
||||
port = uri.Port;
|
||||
}
|
||||
|
||||
if (port <= 0)
|
||||
{
|
||||
reason = "Fuer das Schema '" + scheme + "' konnte kein TCP-/UDP-Port bestimmt werden.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
var match = HostPortPattern.Match(address);
|
||||
if (match.Success)
|
||||
{
|
||||
host = match.Groups["host"].Value.Trim('[', ']');
|
||||
if (!int.TryParse(match.Groups["port"].Value, out port))
|
||||
{
|
||||
port = 0;
|
||||
}
|
||||
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (Contains(adapterName, "SFTP"))
|
||||
{
|
||||
host = address.Trim('/');
|
||||
port = 22;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (Contains(adapterName, "FTP"))
|
||||
{
|
||||
host = address.Trim('/');
|
||||
port = 21;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
reason = "Transportadresse enthaelt keinen sicher bestimmbaren Host und Port.";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int ResolvePort(Uri uri, string scheme)
|
||||
{
|
||||
if (uri.Port > 0 && !uri.IsDefaultPort)
|
||||
{
|
||||
return uri.Port;
|
||||
}
|
||||
|
||||
switch (scheme)
|
||||
{
|
||||
case "http": return 80;
|
||||
case "https": return 443;
|
||||
case "ftp": return 21;
|
||||
case "sftp": return 22;
|
||||
case "smb": return 445;
|
||||
case "smtp": return 25;
|
||||
case "ldap": return 389;
|
||||
case "ldaps": return 636;
|
||||
case "pop3": return 110;
|
||||
case "pop3s": return 995;
|
||||
case "imap": return 143;
|
||||
case "imaps": return 993;
|
||||
case "msmq": return 1801;
|
||||
default: return uri.Port > 0 ? uri.Port : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasExplicitPort(string address)
|
||||
{
|
||||
var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3);
|
||||
var authority = authorityEnd < 0 ? address : address.Substring(0, authorityEnd);
|
||||
return Regex.IsMatch(authority, @":\d{1,5}$", RegexOptions.CultureInvariant);
|
||||
}
|
||||
|
||||
private static bool TryResolveMsmq(string address, out string host)
|
||||
{
|
||||
host = string.Empty;
|
||||
var match = Regex.Match(
|
||||
address,
|
||||
@"(?:FORMATNAME:)?DIRECT=(?:OS|TCP):(?<host>[^\\/;]+)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
host = match.Groups["host"].Value.Trim();
|
||||
return host.Length > 0;
|
||||
}
|
||||
|
||||
private static bool LooksLikeEmailRecipients(string value)
|
||||
{
|
||||
return value.IndexOf('@') > 0
|
||||
&& value.IndexOf("://", StringComparison.Ordinal) < 0
|
||||
&& !value.StartsWith("\\\\", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool Contains(string value, string fragment)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(value)
|
||||
&& value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static bool Validate(string host, int port, out string reason)
|
||||
{
|
||||
host = (host ?? string.Empty).Trim();
|
||||
if (host.Length == 0 || host.IndexOfAny(new[] { ' ', '\t', '\r', '\n', '*', '%' }) >= 0)
|
||||
{
|
||||
reason = "Host ist leer oder enthaelt nicht aufgeloeste Platzhalter.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (port < 1 || port > 65535)
|
||||
{
|
||||
reason = "Port liegt ausserhalb des gueltigen Bereichs.";
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Liest und schreibt die lokale Endpoint-Konfiguration atomar und mit strikter Validierung.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCatalogStore
|
||||
{
|
||||
private const string RootName = "BizTalkEndpointCatalog";
|
||||
private const string Version = "1";
|
||||
private readonly string _path;
|
||||
private readonly int _maxBytes;
|
||||
private readonly int _maxEntries;
|
||||
|
||||
public EndpointCatalogStore(string path, int maxBytes, int maxEntries)
|
||||
{
|
||||
_path = path;
|
||||
_maxBytes = maxBytes;
|
||||
_maxEntries = maxEntries;
|
||||
}
|
||||
|
||||
public EndpointCatalog Read(string environmentName)
|
||||
{
|
||||
var info = new FileInfo(_path);
|
||||
if (!info.Exists)
|
||||
{
|
||||
throw new FileNotFoundException("Endpoint catalog does not exist.", _path);
|
||||
}
|
||||
|
||||
if (info.Length <= 0 || info.Length > _maxBytes)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog size is outside the configured range.");
|
||||
}
|
||||
|
||||
XDocument document;
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit,
|
||||
XmlResolver = null,
|
||||
MaxCharactersInDocument = _maxBytes
|
||||
};
|
||||
using (var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
|
||||
using (var reader = XmlReader.Create(stream, settings))
|
||||
{
|
||||
document = XDocument.Load(reader, LoadOptions.None);
|
||||
}
|
||||
|
||||
var root = document.Root;
|
||||
if (root == null
|
||||
|| root.Name.LocalName != RootName
|
||||
|| ReadAttribute(root, "version") != Version)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog format or version is invalid.");
|
||||
}
|
||||
|
||||
var machine = ReadAttribute(root, "machine");
|
||||
if (!string.Equals(machine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog was created for a different machine.");
|
||||
}
|
||||
|
||||
var catalogEnvironment = ReadAttribute(root, "environment");
|
||||
if (!string.Equals(catalogEnvironment, environmentName ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog was created for a different environment.");
|
||||
}
|
||||
|
||||
DateTime synchronizedUtc;
|
||||
if (!DateTime.TryParseExact(
|
||||
ReadAttribute(root, "synchronizedUtc"),
|
||||
"o",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out synchronizedUtc))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog synchronization timestamp is invalid.");
|
||||
}
|
||||
|
||||
var catalog = new EndpointCatalog
|
||||
{
|
||||
MachineName = machine,
|
||||
EnvironmentName = catalogEnvironment,
|
||||
SynchronizedUtc = synchronizedUtc,
|
||||
ActiveCandidates = ReadInt(root, "activeCandidates", 0, _maxEntries),
|
||||
UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, _maxEntries)
|
||||
};
|
||||
|
||||
foreach (var node in root.Elements("Endpoint"))
|
||||
{
|
||||
if (catalog.Entries.Count >= _maxEntries)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount.");
|
||||
}
|
||||
|
||||
var entry = new EndpointCatalogEntry
|
||||
{
|
||||
Key = ReadAttribute(node, "key"),
|
||||
ArtifactType = ReadAttribute(node, "artifactType"),
|
||||
ApplicationName = ReadAttribute(node, "application"),
|
||||
ArtifactName = ReadAttribute(node, "artifact"),
|
||||
TransportRole = ReadAttribute(node, "transportRole"),
|
||||
AdapterName = ReadAttribute(node, "adapter"),
|
||||
Protocol = ReadAttribute(node, "protocol").ToUpperInvariant(),
|
||||
Host = ReadAttribute(node, "host"),
|
||||
Port = ReadInt(node, "port", 1, 65535),
|
||||
Enabled = ReadBool(node, "enabled"),
|
||||
AutoDiscovered = ReadBool(node, "autoDiscovered")
|
||||
};
|
||||
ValidateEntry(entry);
|
||||
catalog.Entries.Add(entry);
|
||||
}
|
||||
|
||||
if (catalog.Entries.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != catalog.Entries.Count)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog contains duplicate keys.");
|
||||
}
|
||||
|
||||
return catalog;
|
||||
}
|
||||
|
||||
public void Write(EndpointCatalog catalog)
|
||||
{
|
||||
if (catalog == null)
|
||||
{
|
||||
throw new ArgumentNullException("catalog");
|
||||
}
|
||||
|
||||
if (catalog.Entries.Count > _maxEntries)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount.");
|
||||
}
|
||||
|
||||
foreach (var entry in catalog.Entries)
|
||||
{
|
||||
ValidateEntry(entry);
|
||||
}
|
||||
|
||||
var root = new XElement(
|
||||
RootName,
|
||||
new XAttribute("version", Version),
|
||||
new XAttribute("machine", Environment.MachineName),
|
||||
new XAttribute("environment", catalog.EnvironmentName ?? string.Empty),
|
||||
new XAttribute("synchronizedUtc", catalog.SynchronizedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)),
|
||||
new XAttribute("activeCandidates", catalog.ActiveCandidates),
|
||||
new XAttribute("unsupportedCandidates", catalog.UnsupportedCandidates));
|
||||
foreach (var entry in catalog.Entries
|
||||
.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.ArtifactType, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.ArtifactName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.TransportRole, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
root.Add(new XElement(
|
||||
"Endpoint",
|
||||
new XAttribute("key", entry.Key),
|
||||
new XAttribute("artifactType", entry.ArtifactType ?? string.Empty),
|
||||
new XAttribute("application", entry.ApplicationName ?? string.Empty),
|
||||
new XAttribute("artifact", entry.ArtifactName ?? string.Empty),
|
||||
new XAttribute("transportRole", entry.TransportRole ?? string.Empty),
|
||||
new XAttribute("adapter", entry.AdapterName ?? string.Empty),
|
||||
new XAttribute("protocol", entry.Protocol),
|
||||
new XAttribute("host", entry.Host),
|
||||
new XAttribute("port", entry.Port),
|
||||
new XAttribute("enabled", entry.Enabled),
|
||||
new XAttribute("autoDiscovered", entry.AutoDiscovered)));
|
||||
}
|
||||
|
||||
var document = new XDocument(new XDeclaration("1.0", "utf-8", null), root);
|
||||
byte[] bytes;
|
||||
using (var memory = new MemoryStream())
|
||||
using (var writer = XmlWriter.Create(memory, new XmlWriterSettings
|
||||
{
|
||||
Encoding = new UTF8Encoding(false),
|
||||
Indent = true,
|
||||
NewLineChars = "\r\n",
|
||||
NewLineHandling = NewLineHandling.Replace
|
||||
}))
|
||||
{
|
||||
document.Save(writer);
|
||||
writer.Flush();
|
||||
bytes = memory.ToArray();
|
||||
}
|
||||
|
||||
if (bytes.Length > _maxBytes)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog exceeds EndpointCatalogMaxBytes.");
|
||||
}
|
||||
|
||||
AtomicWrite(bytes);
|
||||
}
|
||||
|
||||
private void AtomicWrite(byte[] bytes)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_path);
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
throw new InvalidOperationException("Endpoint catalog path has no parent directory.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
var temporaryPath = Path.Combine(directory, Path.GetFileName(_path) + "." + Guid.NewGuid().ToString("N") + ".tmp");
|
||||
try
|
||||
{
|
||||
using (var stream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
stream.Flush(true);
|
||||
}
|
||||
|
||||
if (File.Exists(_path))
|
||||
{
|
||||
File.Replace(temporaryPath, _path, null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(temporaryPath, _path);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporaryPath))
|
||||
{
|
||||
try { File.Delete(temporaryPath); } catch (IOException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateEntry(EndpointCatalogEntry entry)
|
||||
{
|
||||
if (entry == null
|
||||
|| string.IsNullOrWhiteSpace(entry.Key)
|
||||
|| string.IsNullOrWhiteSpace(entry.ArtifactType)
|
||||
|| string.IsNullOrWhiteSpace(entry.ArtifactName)
|
||||
|| string.IsNullOrWhiteSpace(entry.Host)
|
||||
|| entry.Host.IndexOfAny(new[] { '\r', '\n', '\t', ' ' }) >= 0
|
||||
|| entry.Port < 1
|
||||
|| entry.Port > 65535
|
||||
|| !(string.Equals(entry.Protocol, "TCP", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(entry.Protocol, "UDP", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog contains an invalid entry.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadAttribute(XElement element, string name)
|
||||
{
|
||||
var attribute = element.Attribute(name);
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog attribute is missing: " + name);
|
||||
}
|
||||
|
||||
return attribute.Value.Trim();
|
||||
}
|
||||
|
||||
private static int ReadInt(XElement element, string name, int min, int max)
|
||||
{
|
||||
int value;
|
||||
if (!int.TryParse(ReadAttribute(element, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out value)
|
||||
|| value < min
|
||||
|| value > max)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog integer is invalid: " + name);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool ReadBool(XElement element, string name)
|
||||
{
|
||||
bool value;
|
||||
if (!bool.TryParse(ReadAttribute(element, name), out value))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog boolean is invalid: " + name);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Pflegt den woechentlichen Endpoint-Katalog und prueft aktive Ziele parallel auf Netzwerkebene.
|
||||
/// </summary>
|
||||
internal sealed class EndpointConnectivityProbe
|
||||
{
|
||||
private readonly MonitoringOptions _options;
|
||||
private readonly FileLogger _logger;
|
||||
|
||||
public EndpointConnectivityProbe(MonitoringOptions options, FileLogger logger)
|
||||
{
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Query(ProbeResult result)
|
||||
{
|
||||
var state = result.EndpointConnectivity;
|
||||
if (!_options.ProbeEndpointConnectivity)
|
||||
{
|
||||
state.Disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state.RuntimeStateAvailable = result.Platform.ReceiveLocationsDataAvailable
|
||||
&& result.Platform.SendPortsDataAvailable;
|
||||
if (!state.RuntimeStateAvailable)
|
||||
{
|
||||
state.Failure = "Aktive Send Ports und Receive Locations konnten nicht vollstaendig bestimmt werden.";
|
||||
return;
|
||||
}
|
||||
|
||||
var store = new EndpointCatalogStore(
|
||||
_options.EndpointCatalogPath,
|
||||
_options.EndpointCatalogMaxBytes,
|
||||
_options.EndpointMaxCount);
|
||||
EndpointCatalog catalog = null;
|
||||
string readFailure = null;
|
||||
try
|
||||
{
|
||||
catalog = store.Read(_options.EnvironmentName);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
readFailure = ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
readFailure = ex.GetType().Name + ": " + ex.Message;
|
||||
_logger.Warning("Endpoint catalog rejected. path=" + _options.EndpointCatalogPath + " reason=" + readFailure);
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
state.RefreshRequired = catalog == null
|
||||
|| catalog.SynchronizedUtc > now.AddMinutes(5)
|
||||
|| now - catalog.SynchronizedUtc >= TimeSpan.FromHours(_options.EndpointDiscoveryIntervalHours);
|
||||
if (state.RefreshRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
catalog = Synchronize(catalog, result.EndpointCandidates, now);
|
||||
store.Write(catalog);
|
||||
state.RefreshSucceeded = true;
|
||||
_logger.Info(
|
||||
"Endpoint catalog synchronized. path=" + _options.EndpointCatalogPath
|
||||
+ " active_candidates=" + catalog.ActiveCandidates
|
||||
+ " configured=" + catalog.Entries.Count
|
||||
+ " unsupported_external=" + catalog.UnsupportedCandidates);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state.RefreshSucceeded = false;
|
||||
state.Failure = "Endpoint-Katalog konnte nicht synchronisiert werden: " + ex.GetType().Name + ": " + ex.Message;
|
||||
_logger.Error("Endpoint catalog synchronization failed.", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.RefreshSucceeded = true;
|
||||
}
|
||||
|
||||
if (catalog == null)
|
||||
{
|
||||
state.CatalogAvailable = false;
|
||||
if (string.IsNullOrWhiteSpace(state.Failure))
|
||||
{
|
||||
state.Failure = "Keine gueltige Endpoint-Konfiguration verfuegbar: " + readFailure;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.CatalogAvailable = true;
|
||||
state.CatalogSynchronizedUtc = catalog.SynchronizedUtc;
|
||||
state.Configured = catalog.Entries.Count;
|
||||
state.UnsupportedActive = CountUnsupportedExternal(result.EndpointCandidates);
|
||||
|
||||
var activeKeys = new HashSet<string>(
|
||||
result.EndpointCandidates.Where(x => x.Active).Select(x => x.Key),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var activeEntries = catalog.Entries
|
||||
.Where(x => x.Enabled)
|
||||
.Where(x => string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
|
||||
|| activeKeys.Contains(x.Key))
|
||||
.Take(_options.EndpointMaxCount)
|
||||
.ToArray();
|
||||
state.Active = activeEntries.Length;
|
||||
state.SkippedInactive = catalog.Entries.Count(x => x.Enabled) - activeEntries.Length;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var probeResult in ProbeAllAsync(activeEntries).GetAwaiter().GetResult())
|
||||
{
|
||||
state.Results.Add(probeResult);
|
||||
if (!probeResult.Available)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Endpoint unavailable. endpoint=" + Display(probeResult.Endpoint)
|
||||
+ " duration_ms=" + probeResult.DurationMilliseconds
|
||||
+ " reason=" + probeResult.Failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state.Failure = "Endpoint-Probes konnten nicht abgeschlossen werden: " + ex.GetType().Name + ": " + ex.Message;
|
||||
_logger.Error("Endpoint probes failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal static EndpointCatalog SynchronizeCatalog(
|
||||
EndpointCatalog existing,
|
||||
IEnumerable<EndpointCandidate> candidates,
|
||||
DateTime synchronizedUtc)
|
||||
{
|
||||
var active = (candidates ?? Enumerable.Empty<EndpointCandidate>())
|
||||
.Where(x => x != null && x.Active)
|
||||
.ToArray();
|
||||
var discovered = new List<EndpointCatalogEntry>();
|
||||
var unsupported = 0;
|
||||
foreach (var candidate in active)
|
||||
{
|
||||
EndpointCatalogEntry entry;
|
||||
string reason;
|
||||
if (EndpointAddressParser.TryCreate(candidate, out entry, out reason))
|
||||
{
|
||||
discovered.Add(entry);
|
||||
}
|
||||
else if (EndpointAddressParser.IsPotentialExternalEndpoint(candidate))
|
||||
{
|
||||
unsupported++;
|
||||
}
|
||||
}
|
||||
|
||||
var manual = existing == null
|
||||
? new EndpointCatalogEntry[0]
|
||||
: existing.Entries.Where(x => !x.AutoDiscovered).ToArray();
|
||||
var manualKeys = new HashSet<string>(manual.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
|
||||
var merged = manual
|
||||
.Concat(discovered.Where(x => !manualKeys.Contains(x.Key)))
|
||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(x => x.First())
|
||||
.ToArray();
|
||||
|
||||
var catalog = new EndpointCatalog
|
||||
{
|
||||
MachineName = Environment.MachineName,
|
||||
EnvironmentName = existing == null ? string.Empty : existing.EnvironmentName,
|
||||
SynchronizedUtc = synchronizedUtc.ToUniversalTime(),
|
||||
ActiveCandidates = active.Length,
|
||||
UnsupportedCandidates = unsupported
|
||||
};
|
||||
catalog.Entries.AddRange(merged);
|
||||
return catalog;
|
||||
}
|
||||
|
||||
private EndpointCatalog Synchronize(
|
||||
EndpointCatalog existing,
|
||||
IEnumerable<EndpointCandidate> candidates,
|
||||
DateTime synchronizedUtc)
|
||||
{
|
||||
var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc);
|
||||
catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty;
|
||||
if (catalog.Entries.Count > _options.EndpointMaxCount
|
||||
|| catalog.ActiveCandidates > _options.EndpointMaxCount)
|
||||
{
|
||||
throw new InvalidDataException("Discovered endpoint candidates exceed EndpointMaxCount.");
|
||||
}
|
||||
|
||||
return catalog;
|
||||
}
|
||||
|
||||
private static int CountUnsupportedExternal(IEnumerable<EndpointCandidate> candidates)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var candidate in candidates.Where(x => x.Active))
|
||||
{
|
||||
EndpointCatalogEntry ignored;
|
||||
string reason;
|
||||
if (!EndpointAddressParser.TryCreate(candidate, out ignored, out reason)
|
||||
&& EndpointAddressParser.IsPotentialExternalEndpoint(candidate))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints)
|
||||
{
|
||||
var unique = endpoints
|
||||
.GroupBy(x => x.Protocol.ToUpperInvariant() + "|" + x.Host.ToUpperInvariant() + "|" + x.Port)
|
||||
.ToArray();
|
||||
var outcomes = new Dictionary<string, NetworkOutcome>(StringComparer.OrdinalIgnoreCase);
|
||||
using (var gate = new SemaphoreSlim(_options.EndpointProbeMaxConcurrency))
|
||||
{
|
||||
var tasks = unique.Select(async group =>
|
||||
{
|
||||
await gate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var endpoint = group.First();
|
||||
var outcome = await ProbeOneAsync(endpoint).ConfigureAwait(false);
|
||||
lock (outcomes)
|
||||
{
|
||||
outcomes[group.Key] = outcome;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}).ToArray();
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return endpoints.Select(endpoint =>
|
||||
{
|
||||
var key = endpoint.Protocol.ToUpperInvariant() + "|" + endpoint.Host.ToUpperInvariant() + "|" + endpoint.Port;
|
||||
var outcome = outcomes[key];
|
||||
return new EndpointProbeResult
|
||||
{
|
||||
Endpoint = endpoint,
|
||||
Available = outcome.Available,
|
||||
DurationMilliseconds = outcome.DurationMilliseconds,
|
||||
Failure = outcome.Failure
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private async Task<NetworkOutcome> ProbeOneAsync(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
return string.Equals(endpoint.Protocol, "UDP", StringComparison.OrdinalIgnoreCase)
|
||||
? await ProbeUdpAsync(endpoint).ConfigureAwait(false)
|
||||
: await ProbeTcpAsync(endpoint).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<NetworkOutcome> ProbeTcpAsync(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
try
|
||||
{
|
||||
var connect = client.ConnectAsync(endpoint.Host, endpoint.Port);
|
||||
var completed = await Task.WhenAny(connect, Task.Delay(_options.EndpointProbeTimeoutMilliseconds)).ConfigureAwait(false);
|
||||
if (completed != connect)
|
||||
{
|
||||
client.Close();
|
||||
ObserveFault(connect);
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "TCP timeout");
|
||||
}
|
||||
|
||||
await connect.ConfigureAwait(false);
|
||||
return client.Connected
|
||||
? NetworkOutcome.Success(stopwatch.ElapsedMilliseconds)
|
||||
: NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "TCP connection not established");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, CompactFailure(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NetworkOutcome> ProbeUdpAsync(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var resolution = Dns.GetHostAddressesAsync(endpoint.Host);
|
||||
var completed = await Task.WhenAny(resolution, Task.Delay(_options.EndpointProbeTimeoutMilliseconds)).ConfigureAwait(false);
|
||||
if (completed != resolution)
|
||||
{
|
||||
ObserveFault(resolution);
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "UDP DNS timeout");
|
||||
}
|
||||
|
||||
var addresses = await resolution.ConfigureAwait(false);
|
||||
var address = addresses.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork)
|
||||
?? addresses.FirstOrDefault();
|
||||
if (address == null)
|
||||
{
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "UDP host resolved without an address");
|
||||
}
|
||||
|
||||
using (var socket = new Socket(address.AddressFamily, SocketType.Dgram, ProtocolType.Udp))
|
||||
{
|
||||
socket.Connect(new IPEndPoint(address, endpoint.Port));
|
||||
socket.Send(new byte[0]);
|
||||
}
|
||||
|
||||
// UDP ist verbindungslos: Erfolg bestaetigt DNS, Route und lokalen Datagrammversand,
|
||||
// aber ohne applikationsspezifische Antwort nicht den entfernten Dienstzustand.
|
||||
return NetworkOutcome.Success(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, CompactFailure(ex));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ObserveFault(Task task)
|
||||
{
|
||||
task.ContinueWith(
|
||||
completed => { var ignored = completed.Exception; },
|
||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
|
||||
private static string CompactFailure(Exception exception)
|
||||
{
|
||||
var socket = exception as SocketException;
|
||||
return socket == null
|
||||
? exception.GetType().Name + ": " + exception.Message
|
||||
: "SocketError=" + socket.SocketErrorCode + ": " + socket.Message;
|
||||
}
|
||||
|
||||
internal static string Display(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
var artifact = string.IsNullOrWhiteSpace(endpoint.ApplicationName)
|
||||
|| string.Equals(endpoint.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)
|
||||
? endpoint.ArtifactName
|
||||
: endpoint.ApplicationName + "\\" + endpoint.ArtifactName;
|
||||
return endpoint.ArtifactType + ":" + artifact
|
||||
+ "[" + endpoint.TransportRole + "]->"
|
||||
+ endpoint.Host + ":" + endpoint.Port + "/" + endpoint.Protocol.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private sealed class NetworkOutcome
|
||||
{
|
||||
public bool Available { get; private set; }
|
||||
public long DurationMilliseconds { get; private set; }
|
||||
public string Failure { get; private set; }
|
||||
|
||||
public static NetworkOutcome Success(long durationMilliseconds)
|
||||
{
|
||||
return new NetworkOutcome { Available = true, DurationMilliseconds = durationMilliseconds };
|
||||
}
|
||||
|
||||
public static NetworkOutcome Failed(long durationMilliseconds, string failure)
|
||||
{
|
||||
return new NetworkOutcome
|
||||
{
|
||||
Available = false,
|
||||
DurationMilliseconds = durationMilliseconds,
|
||||
Failure = failure
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,8 @@ namespace BizTalkCheckmkPulse
|
||||
Applications = new List<ApplicationRuntimeState>();
|
||||
SqlTargets = new List<SqlAccessState>();
|
||||
EventLog = new EventLogState();
|
||||
EndpointCandidates = new List<EndpointCandidate>();
|
||||
EndpointConnectivity = new EndpointConnectivityState();
|
||||
}
|
||||
|
||||
public List<ProbeDiagnostic> Diagnostics { get; private set; }
|
||||
@@ -114,6 +116,8 @@ namespace BizTalkCheckmkPulse
|
||||
public string ExecutionIdentity { get; set; }
|
||||
public string NetworkIdentityHint { get; set; }
|
||||
public EventLogState EventLog { get; private set; }
|
||||
public List<EndpointCandidate> EndpointCandidates { get; private set; }
|
||||
public EndpointConnectivityState EndpointConnectivity { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,4 +240,116 @@ namespace BizTalkCheckmkPulse
|
||||
public DateTime Since { get; set; }
|
||||
public string Failure { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rohdaten eines BizTalk-Transports aus einer bereits ausgefuehrten Runtime-Abfrage.
|
||||
/// Vollstaendige URIs werden nur im Speicher gehalten und weder persistiert noch ausgegeben.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCandidate
|
||||
{
|
||||
public string ArtifactType { get; set; }
|
||||
public string ApplicationName { get; set; }
|
||||
public string ArtifactName { get; set; }
|
||||
public string TransportRole { get; set; }
|
||||
public string AdapterName { get; set; }
|
||||
public string Address { get; set; }
|
||||
public bool Active { get; set; }
|
||||
public bool Dynamic { get; set; }
|
||||
|
||||
public string Key
|
||||
{
|
||||
get
|
||||
{
|
||||
return EndpointCatalogEntry.BuildKey(
|
||||
ArtifactType,
|
||||
ApplicationName,
|
||||
ArtifactName,
|
||||
TransportRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ein geheimnisfreier, lokal persistierbarer Netzwerk-Endpunkt.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCatalogEntry
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string ArtifactType { get; set; }
|
||||
public string ApplicationName { get; set; }
|
||||
public string ArtifactName { get; set; }
|
||||
public string TransportRole { get; set; }
|
||||
public string AdapterName { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public bool AutoDiscovered { get; set; }
|
||||
|
||||
public static string BuildKey(string artifactType, string applicationName, string artifactName, string transportRole)
|
||||
{
|
||||
return NormalizeKeyPart(artifactType)
|
||||
+ "|" + NormalizeKeyPart(applicationName)
|
||||
+ "|" + NormalizeKeyPart(artifactName)
|
||||
+ "|" + NormalizeKeyPart(transportRole);
|
||||
}
|
||||
|
||||
private static string NormalizeKeyPart(string value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim().ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Versionierter Inhalt der automatisch gepflegten lokalen Endpoint-Konfiguration.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCatalog
|
||||
{
|
||||
public EndpointCatalog()
|
||||
{
|
||||
Entries = new List<EndpointCatalogEntry>();
|
||||
}
|
||||
|
||||
public DateTime SynchronizedUtc { get; set; }
|
||||
public string MachineName { get; set; }
|
||||
public string EnvironmentName { get; set; }
|
||||
public int ActiveCandidates { get; set; }
|
||||
public int UnsupportedCandidates { get; set; }
|
||||
public List<EndpointCatalogEntry> Entries { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ergebnis einer einzelnen TCP- beziehungsweise UDP-Netzwerkprobe.
|
||||
/// </summary>
|
||||
internal sealed class EndpointProbeResult
|
||||
{
|
||||
public EndpointCatalogEntry Endpoint { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public long DurationMilliseconds { get; set; }
|
||||
public string Failure { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregierter Zustand des Endpoint-Katalogs und aller aktiven Netzwerkprobes.
|
||||
/// </summary>
|
||||
internal sealed class EndpointConnectivityState
|
||||
{
|
||||
public EndpointConnectivityState()
|
||||
{
|
||||
Results = new List<EndpointProbeResult>();
|
||||
}
|
||||
|
||||
public bool Disabled { get; set; }
|
||||
public bool CatalogAvailable { get; set; }
|
||||
public bool RuntimeStateAvailable { get; set; }
|
||||
public bool RefreshRequired { get; set; }
|
||||
public bool RefreshSucceeded { get; set; }
|
||||
public DateTime? CatalogSynchronizedUtc { get; set; }
|
||||
public int Configured { get; set; }
|
||||
public int Active { get; set; }
|
||||
public int SkippedInactive { get; set; }
|
||||
public int UnsupportedActive { get; set; }
|
||||
public string Failure { get; set; }
|
||||
public List<EndpointProbeResult> Results { get; private set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,13 @@ namespace BizTalkCheckmkPulse
|
||||
public int SnapshotMaxBytes { get; set; }
|
||||
public string LogDirectory { get; set; }
|
||||
public int LogRetentionDays { get; set; }
|
||||
public bool ProbeEndpointConnectivity { get; set; }
|
||||
public string EndpointCatalogPath { get; set; }
|
||||
public int EndpointCatalogMaxBytes { get; set; }
|
||||
public int EndpointDiscoveryIntervalHours { get; set; }
|
||||
public int EndpointProbeTimeoutMilliseconds { get; set; }
|
||||
public int EndpointProbeMaxConcurrency { get; set; }
|
||||
public int EndpointMaxCount { get; set; }
|
||||
public bool Collect { get; set; }
|
||||
public bool SelfTest { get; set; }
|
||||
|
||||
@@ -73,6 +80,13 @@ namespace BizTalkCheckmkPulse
|
||||
SnapshotMaxBytes = 1048576;
|
||||
LogDirectory = Path.Combine(commonData, "BizTalkCheckmkPulse", "logs");
|
||||
LogRetentionDays = 30;
|
||||
ProbeEndpointConnectivity = true;
|
||||
EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml");
|
||||
EndpointCatalogMaxBytes = 1048576;
|
||||
EndpointDiscoveryIntervalHours = 168;
|
||||
EndpointProbeTimeoutMilliseconds = 3000;
|
||||
EndpointProbeMaxConcurrency = 12;
|
||||
EndpointMaxCount = 500;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -124,6 +138,13 @@ namespace BizTalkCheckmkPulse
|
||||
options.SnapshotMaxBytes = ReadInt(settings, "SnapshotMaxBytes", options.SnapshotMaxBytes, 4096, 16777216);
|
||||
options.LogDirectory = Environment.ExpandEnvironmentVariables(ReadString(settings, "LogDirectory", options.LogDirectory));
|
||||
options.LogRetentionDays = ReadInt(settings, "LogRetentionDays", options.LogRetentionDays, 1, 365);
|
||||
options.ProbeEndpointConnectivity = ReadBool(settings, "ProbeEndpointConnectivity", options.ProbeEndpointConnectivity);
|
||||
options.EndpointCatalogPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "EndpointCatalogPath", options.EndpointCatalogPath));
|
||||
options.EndpointCatalogMaxBytes = ReadInt(settings, "EndpointCatalogMaxBytes", options.EndpointCatalogMaxBytes, 4096, 16777216);
|
||||
options.EndpointDiscoveryIntervalHours = ReadInt(settings, "EndpointDiscoveryIntervalHours", options.EndpointDiscoveryIntervalHours, 1, 8760);
|
||||
options.EndpointProbeTimeoutMilliseconds = ReadInt(settings, "EndpointProbeTimeoutMilliseconds", options.EndpointProbeTimeoutMilliseconds, 250, 30000);
|
||||
options.EndpointProbeMaxConcurrency = ReadInt(settings, "EndpointProbeMaxConcurrency", options.EndpointProbeMaxConcurrency, 1, 64);
|
||||
options.EndpointMaxCount = ReadInt(settings, "EndpointMaxCount", options.EndpointMaxCount, 1, 5000);
|
||||
|
||||
ApplyArguments(options, args ?? new string[0]);
|
||||
if (!options.SelfTest)
|
||||
@@ -192,6 +213,19 @@ namespace BizTalkCheckmkPulse
|
||||
{
|
||||
throw new ConfigurationErrorsException("LogDirectory must be an absolute path.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.EndpointCatalogPath) || !Path.IsPathRooted(options.EndpointCatalogPath))
|
||||
{
|
||||
throw new ConfigurationErrorsException("EndpointCatalogPath must be an absolute path.");
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
Path.GetFullPath(options.EndpointCatalogPath),
|
||||
Path.GetFullPath(options.SnapshotPath),
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ConfigurationErrorsException("EndpointCatalogPath and SnapshotPath must be different files.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace BizTalkCheckmkPulse
|
||||
using (new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
|
||||
{
|
||||
logger.Info("Collection started. snapshot=" + options.SnapshotPath);
|
||||
var result = Collect(options);
|
||||
var result = Collect(options, logger);
|
||||
foreach (var diagnostic in result.Diagnostics)
|
||||
{
|
||||
logger.Warning("Probe diagnostic: " + diagnostic.ToDisplayText());
|
||||
@@ -98,6 +98,10 @@ namespace BizTalkCheckmkPulse
|
||||
+ result.SendPorts.Count
|
||||
+ " send_ports_not_started="
|
||||
+ result.SendPorts.Count(x => x.Status != 3)
|
||||
+ " endpoints_active="
|
||||
+ result.EndpointConnectivity.Active
|
||||
+ " endpoints_failed="
|
||||
+ result.EndpointConnectivity.Results.Count(x => !x.Available)
|
||||
+ " elapsed_ms="
|
||||
+ stopwatch.ElapsedMilliseconds);
|
||||
return 0;
|
||||
@@ -155,11 +159,12 @@ namespace BizTalkCheckmkPulse
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static ProbeResult Collect(MonitoringOptions options)
|
||||
private static ProbeResult Collect(MonitoringOptions options, FileLogger logger)
|
||||
{
|
||||
var result = new ProbeResult();
|
||||
new WmiBizTalkProbe(options).Query(result);
|
||||
new SqlConnectivityProbe(options).Query(result);
|
||||
new EndpointConnectivityProbe(options, logger).Query(result);
|
||||
|
||||
if (options.ProbeEventLog)
|
||||
{
|
||||
|
||||
@@ -6,5 +6,5 @@ using System.Reflection;
|
||||
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
||||
[assembly: AssemblyVersion("2.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||
[assembly: AssemblyVersion("2.2.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.2.0.0")]
|
||||
|
||||
@@ -258,9 +258,9 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
|
||||
if (count < 8)
|
||||
if (count < 9)
|
||||
{
|
||||
throw new InvalidDataException("Snapshot must contain all eight stable services.");
|
||||
throw new InvalidDataException("Snapshot must contain all nine stable services.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +221,17 @@ namespace BizTalkCheckmkPulse
|
||||
ApplicationName = applicationName,
|
||||
IsDisabled = isDisabled
|
||||
});
|
||||
result.EndpointCandidates.Add(new EndpointCandidate
|
||||
{
|
||||
ArtifactType = "ReceiveLocation",
|
||||
ApplicationName = applicationName,
|
||||
ArtifactName = name,
|
||||
TransportRole = "Inbound",
|
||||
AdapterName = WmiHelpers.GetString(item, "AdapterName"),
|
||||
Address = WmiHelpers.GetString(item, "InboundTransportURL"),
|
||||
Active = isDisabled == false,
|
||||
Dynamic = false
|
||||
});
|
||||
var app = GetApplication(apps, applicationName);
|
||||
app.ReceiveLocationTotal++;
|
||||
if (isDisabled == true)
|
||||
@@ -241,6 +252,33 @@ namespace BizTalkCheckmkPulse
|
||||
ApplicationName = applicationName,
|
||||
Status = status
|
||||
});
|
||||
var isDynamic = WmiHelpers.GetBoolean(item, "IsDynamic", false);
|
||||
result.EndpointCandidates.Add(new EndpointCandidate
|
||||
{
|
||||
ArtifactType = "SendPort",
|
||||
ApplicationName = applicationName,
|
||||
ArtifactName = string.IsNullOrWhiteSpace(name) ? "(unknown)" : name,
|
||||
TransportRole = "Primary",
|
||||
AdapterName = WmiHelpers.GetString(item, "PTTransportType"),
|
||||
Address = WmiHelpers.GetString(item, "PTAddress"),
|
||||
Active = status == SendPortStarted,
|
||||
Dynamic = isDynamic
|
||||
});
|
||||
var secondaryAddress = WmiHelpers.GetString(item, "STAddress");
|
||||
if (!string.IsNullOrWhiteSpace(secondaryAddress))
|
||||
{
|
||||
result.EndpointCandidates.Add(new EndpointCandidate
|
||||
{
|
||||
ArtifactType = "SendPort",
|
||||
ApplicationName = applicationName,
|
||||
ArtifactName = string.IsNullOrWhiteSpace(name) ? "(unknown)" : name,
|
||||
TransportRole = "Secondary",
|
||||
AdapterName = WmiHelpers.GetString(item, "STTransportType"),
|
||||
Address = secondaryAddress,
|
||||
Active = status == SendPortStarted,
|
||||
Dynamic = isDynamic
|
||||
});
|
||||
}
|
||||
var app = GetApplication(apps, applicationName);
|
||||
app.SendPortTotal++;
|
||||
switch (status)
|
||||
|
||||
Reference in New Issue
Block a user