Add BizTalk endpoint reachability monitoring

This commit is contained in:
2026-08-04 09:48:00 +02:00
parent 826d87fef7
commit 91e0db5619
20 changed files with 1714 additions and 38 deletions
+10
View File
@@ -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
};
}
}
}
}
+116
View File
@@ -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>
+7 -2
View File
@@ -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")]
+2 -2
View File
@@ -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)