Add WMI and SQL permission diagnostics

This commit is contained in:
2026-07-22 15:26:51 +02:00
parent 31222ad28f
commit 78e8881d38
16 changed files with 1243 additions and 89 deletions
+5 -3
View File
@@ -1,17 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<!-- Empty or "." means local BizTalk machine. -->
<!-- Leer oder "." bedeutet die lokale BizTalk-Maschine. -->
<add key="Server" value="." />
<add key="ServicePrefix" value="BizTalk" />
<add key="EnvironmentName" value="" />
<add key="QueryTimeoutSeconds" value="25" />
<!-- Testet integrierte Windows-Anmeldung an den per WMI ermittelten BizTalk-Datenbanken. -->
<add key="ProbeSqlConnectivity" value="true" />
<add key="SqlConnectionTimeoutSeconds" value="5" />
<add key="WarnResumableThreshold" value="1" />
<add key="CritNonResumableThreshold" value="1" />
<add key="MaxSummaryItems" value="12" />
<!-- In many BizTalk landscapes stopped artifacts can be intentional. -->
<!-- Gestoppte Artefakte koennen in BizTalk-Landschaften fachlich beabsichtigt sein. -->
<add key="AlertOnArtifactRuntimeIssues" value="false" />
<add key="EmitPerApplicationSuspensionServices" value="false" />
@@ -25,4 +28,3 @@
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
@@ -36,6 +36,7 @@
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Management" />
</ItemGroup>
<ItemGroup>
@@ -44,6 +45,7 @@
<Compile Include="MonitoringOptions.cs" />
<Compile Include="Models.cs" />
<Compile Include="Program.cs" />
<Compile Include="SqlConnectivityProbe.cs" />
<Compile Include="WmiBizTalkProbe.cs" />
<Compile Include="WmiHelpers.cs" />
</ItemGroup>
@@ -52,4 +54,3 @@
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+231 -23
View File
@@ -6,6 +6,9 @@ using System.Text;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Formatiert Probe-Ergebnisse als robuste Checkmk-Local-Check-Zeilen.
/// </summary>
internal sealed class CheckmkLocalFormatter
{
private const int HostStopped = 1;
@@ -18,20 +21,30 @@ namespace BizTalkCheckmkPulse
private const int HostUnknown = 8;
private readonly MonitoringOptions _options;
/// <summary>
/// Initialisiert den Formatter mit Service-Namen und Schwellwerten.
/// </summary>
/// <param name="options">Monitoring-Konfiguration.</param>
public CheckmkLocalFormatter(MonitoringOptions options)
{
_options = options;
}
/// <summary>
/// Formatiert alle stabilen und optionalen Checkmk-Services.
/// </summary>
/// <param name="result">Vollstaendiges Ergebnis der Probes.</param>
/// <returns>Gueltige Checkmk-Local-Check-Zeilen.</returns>
public IEnumerable<string> Format(ProbeResult result)
{
yield return FormatPlatform(result);
yield return FormatSqlAccess(result);
yield return FormatSuspendedInstances(result);
yield return FormatHostInstances(result);
yield return FormatRuntimeArtifacts(result);
yield return FormatEventLog(result);
if (_options.EmitPerApplicationSuspensionServices)
if (_options.EmitPerApplicationSuspensionServices && result.Platform.SuspendedInstancesDataAvailable)
{
foreach (var line in FormatApplicationSuspensions(result))
{
@@ -40,36 +53,111 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Erzeugt minimale Beispielzeilen ohne WMI-, SQL- oder Event-Log-Zugriff.
/// </summary>
/// <returns>Checkmk-Zeilen zur reinen Format- und Deployment-Pruefung.</returns>
public IEnumerable<string> FormatSelfTest()
{
yield return BuildLine(CheckState.Ok, _options.ServiceName("Platform"), "-", "Self test OK. Checkmk local check output is valid.");
yield return BuildLine(CheckState.Ok, _options.ServiceName("SQL Access"), "biztalk_sql_targets_total=0;;;0", "Self test OK. No SQL connection was opened.");
yield return BuildLine(CheckState.Ok, _options.ServiceName("Suspended Instances"), "biztalk_suspended_total=0;;;0", "Self test OK.");
}
/// <summary>
/// Erzeugt fuer einen nicht abgefangenen Programmfehler UNKNOWN-Zeilen.
/// </summary>
/// <param name="message">Bereits aufbereitete technische Fehlermeldung.</param>
/// <returns>UNKNOWN-Zeilen fuer alle stabilen Services.</returns>
public IEnumerable<string> FormatFatal(string message)
{
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Platform"), "-", message);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Suspended Instances"), "-", message);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", message);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Runtime Artifacts"), "-", message);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", message);
var action = message + " Massnahme: Programmdatei, Konfiguration und Windows Application Log pruefen; anschliessend cmk-agent-ctl.exe dump erneut ausfuehren.";
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Platform"), "-", action);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("SQL Access"), "-", action);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Suspended Instances"), "-", action);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", action);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Runtime Artifacts"), "-", action);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", action);
}
/// <summary>
/// Formatiert Erreichbarkeit und Basisdaten des BizTalk-WMI-Providers.
/// </summary>
/// <param name="result">Probe-Ergebnis.</param>
/// <returns>Checkmk-Zeile fuer BizTalk Platform.</returns>
private string FormatPlatform(ProbeResult result)
{
var state = result.Platform.WmiConnected ? CheckState.Ok : CheckState.Unknown;
var available = result.Platform.WmiConnected && result.Platform.PlatformDataAvailable;
var state = available ? CheckState.Ok : CheckState.Unknown;
var detail = new StringBuilder();
detail.Append(result.Platform.WmiConnected ? "BizTalk WMI reachable" : "BizTalk WMI not reachable");
detail.Append(available ? "BizTalk WMI and platform data reachable" : "BizTalk WMI or required platform data not readable");
detail.Append(", server=").Append(EmptyAsUnknown(result.Platform.ServerName));
AppendOptional(detail, "group", result.Platform.GroupName);
AppendOptional(detail, "mgmt_db", JoinDb(result.Platform.ManagementDbServer, result.Platform.ManagementDbName));
AppendOptional(detail, "msgbox_db", JoinDb(result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName));
return BuildLine(state, _options.ServiceName("Platform"), "-", AppendDiagnostics(detail.ToString(), result.Diagnostics));
var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting", "MSBTS_MessageBoxSetting");
return BuildLine(state, _options.ServiceName("Platform"), "-", AppendDiagnostics(detail.ToString(), diagnostics));
}
/// <summary>
/// Formatiert die integrierten Anmeldetests an den BizTalk-Datenbanken.
/// </summary>
/// <param name="result">Probe-Ergebnis mit SQL-Zielen und Identitaeten.</param>
/// <returns>Checkmk-Zeile fuer BizTalk SQL Access.</returns>
private string FormatSqlAccess(ProbeResult result)
{
if (!_options.ProbeSqlConnectivity)
{
return BuildLine(CheckState.Ok, _options.ServiceName("SQL Access"), "-", "SQL connectivity probe disabled by configuration.");
}
var total = result.SqlTargets.Count;
var available = result.SqlTargets.Count(x => x.Attempted && x.Available);
var discoveryComplete = result.Platform.PlatformDataAvailable;
var state = discoveryComplete && total > 0 && available == total ? CheckState.Ok : CheckState.Unknown;
var metrics = total == 0
? "-"
: string.Format(
CultureInfo.InvariantCulture,
"biztalk_sql_targets_total={0};;;0|biztalk_sql_targets_available={1};;;0|biztalk_sql_targets_failed={2};;1;0",
total,
available,
total - available);
var detail = new StringBuilder();
detail.Append("SQL access targets=").Append(total)
.Append(", available=").Append(available)
.Append(", discovery_complete=").Append(discoveryComplete)
.Append(", execution_identity=").Append(EmptyAsUnknown(result.ExecutionIdentity))
.Append(", network_identity=").Append(EmptyAsUnknown(result.NetworkIdentityHint));
AppendList(detail, "databases", result.SqlTargets.Select(x =>
x.Role + "=" + EmptyAsUnknown(x.Server) + "\\" + EmptyAsUnknown(x.Database) + ":" + (x.Available ? "OK" : "FAILED")));
return BuildLine(
state,
_options.ServiceName("SQL Access"),
metrics,
AppendDiagnostics(detail.ToString(), SelectDiagnostics(result, DiagnosticArea.Sql)));
}
/// <summary>
/// Formatiert Anzahl und Kritikalitaet suspendierter Service Instances.
/// </summary>
/// <param name="result">Probe-Ergebnis.</param>
/// <returns>Checkmk-Zeile fuer suspendierte Instanzen.</returns>
private string FormatSuspendedInstances(ProbeResult result)
{
if (!result.Platform.SuspendedInstancesDataAvailable)
{
var errorDetail = "Suspendierte Instanzen konnten nicht verlaesslich gelesen werden. Ein leerer Datenbestand wird nicht als OK gewertet.";
return BuildLine(
CheckState.Unknown,
_options.ServiceName("Suspended Instances"),
"-",
AppendDiagnostics(errorDetail, SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_ServiceInstance")));
}
var total = result.SuspendedInstances.Count;
var resumable = result.SuspendedInstances.Count(x => x.Kind == SuspendedKind.Resumable);
var nonresumable = result.SuspendedInstances.Count(x => x.Kind == SuspendedKind.NonResumable);
@@ -86,12 +174,26 @@ namespace BizTalkCheckmkPulse
var detail = total == 0
? "No suspended BizTalk service instances found."
: BuildSuspensionDetail(result.SuspendedInstances, total, resumable, nonresumable);
return BuildLine(state, _options.ServiceName("Suspended Instances"), metrics, detail);
}
/// <summary>
/// Formatiert den Laufzeitzustand aller BizTalk Host Instances.
/// </summary>
/// <param name="result">Probe-Ergebnis.</param>
/// <returns>Checkmk-Zeile fuer Host Instances.</returns>
private string FormatHostInstances(ProbeResult result)
{
if (!result.Platform.HostInstancesDataAvailable)
{
var errorDetail = "BizTalk Host Instances konnten nicht verlaesslich gelesen werden.";
return BuildLine(
CheckState.Unknown,
_options.ServiceName("Host Instances"),
"-",
AppendDiagnostics(errorDetail, SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_HostInstance")));
}
var total = result.HostInstances.Count;
var started = result.HostInstances.Count(x => x.ServiceState == HostStarted);
var stopped = result.HostInstances.Count(x => x.ServiceState == HostStopped || x.ServiceState == HostPaused);
@@ -119,12 +221,26 @@ namespace BizTalkCheckmkPulse
.Take(_options.MaxSummaryItems)
.Select(x => EmptyAsUnknown(x.HostName) + "@" + EmptyAsUnknown(x.RunningServer) + "=" + HostStateName(x.ServiceState));
AppendList(detail, "affected", affected);
return BuildLine(state, _options.ServiceName("Host Instances"), metrics, detail.ToString());
}
/// <summary>
/// Formatiert aggregierte Receive-Location-, Send-Port- und Orchestration-Zustaende.
/// </summary>
/// <param name="result">Probe-Ergebnis.</param>
/// <returns>Checkmk-Zeile fuer Runtime Artifacts.</returns>
private string FormatRuntimeArtifacts(ProbeResult result)
{
if (!result.Platform.RuntimeArtifactsDataAvailable)
{
var errorDetail = "BizTalk Runtime Artifacts konnten nicht vollstaendig gelesen werden. Teilwerte werden nicht als OK gewertet.";
return BuildLine(
CheckState.Unknown,
_options.ServiceName("Runtime Artifacts"),
"-",
AppendDiagnostics(errorDetail, SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_ReceiveLocation", "MSBTS_SendPort", "MSBTS_Orchestration")));
}
var receiveLocations = result.Applications.Sum(x => x.ReceiveLocationTotal);
var receiveLocationsDisabled = result.Applications.Sum(x => x.ReceiveLocationDisabled);
var sendPorts = result.Applications.Sum(x => x.SendPortTotal);
@@ -166,10 +282,14 @@ namespace BizTalkCheckmkPulse
.Take(_options.MaxSummaryItems)
.Select(x => x.ApplicationName + "(rl_disabled=" + x.ReceiveLocationDisabled + ", sp_inactive=" + (x.SendPortStopped + x.SendPortBound) + ", orch_inactive=" + (x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound) + ")");
AppendList(detail, "notable_apps", affected);
return BuildLine(state, _options.ServiceName("Runtime Artifacts"), metrics, detail.ToString());
}
/// <summary>
/// Formatiert relevante Fehler und Warnungen aus dem Application Event Log.
/// </summary>
/// <param name="result">Probe-Ergebnis.</param>
/// <returns>Checkmk-Zeile fuer das BizTalk Event Log.</returns>
private string FormatEventLog(ProbeResult result)
{
if (!_options.ProbeEventLog)
@@ -179,7 +299,11 @@ namespace BizTalkCheckmkPulse
if (!result.EventLog.Available)
{
return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", "Application event log could not be read: " + result.EventLog.Failure);
return BuildLine(
CheckState.Unknown,
_options.ServiceName("Event Log"),
"-",
"Application Event Log konnte nicht gelesen werden: " + result.EventLog.Failure + " Massnahme: lokalen Event-Log-Zugriff des Checkmk-Agentkontos und den Windows Event Log Dienst pruefen.");
}
var state = _options.EventLogCritThreshold > 0 && result.EventLog.Errors >= _options.EventLogCritThreshold
@@ -200,10 +324,14 @@ namespace BizTalkCheckmkPulse
_options.EventLogLookbackMinutes,
result.EventLog.Errors,
result.EventLog.Warnings);
return BuildLine(state, _options.ServiceName("Event Log"), metrics, detail);
}
/// <summary>
/// Erzeugt bei aktivierter Option einen Suspensions-Service je Anwendung.
/// </summary>
/// <param name="result">Probe-Ergebnis mit suspendierten Instanzen.</param>
/// <returns>Checkmk-Zeilen je betroffener BizTalk-Anwendung.</returns>
private IEnumerable<string> FormatApplicationSuspensions(ProbeResult result)
{
foreach (var group in result.SuspendedInstances.GroupBy(x => x.ApplicationName).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
@@ -219,11 +347,16 @@ namespace BizTalkCheckmkPulse
_options.WarnResumableThreshold,
nonresumable,
_options.CritNonResumableThreshold);
yield return BuildLine(state, _options.ServiceName("Suspended " + group.Key), metrics, "Suspended instances: resumable=" + resumable + ", nonresumable=" + nonresumable);
}
}
/// <summary>
/// Bestimmt den Checkmk-Zustand aus den beiden Suspensionszaehlern.
/// </summary>
/// <param name="resumable">Anzahl fortsetzbarer Instanzen.</param>
/// <param name="nonresumable">Anzahl nicht fortsetzbarer Instanzen.</param>
/// <returns>Zustand gemaess konfigurierten Schwellwerten.</returns>
private CheckState DetermineSuspensionState(int resumable, int nonresumable)
{
if (_options.CritNonResumableThreshold > 0 && nonresumable >= _options.CritNonResumableThreshold)
@@ -239,6 +372,14 @@ namespace BizTalkCheckmkPulse
return CheckState.Ok;
}
/// <summary>
/// Erstellt Details mit betroffenen Anwendungen und aktuellen Beispielen.
/// </summary>
/// <param name="instances">Suspendierte Instanzen.</param>
/// <param name="total">Gesamtanzahl.</param>
/// <param name="resumable">Anzahl fortsetzbarer Instanzen.</param>
/// <param name="nonresumable">Anzahl nicht fortsetzbarer Instanzen.</param>
/// <returns>Kompakter Detailtext.</returns>
private string BuildSuspensionDetail(IEnumerable<SuspendedInstance> instances, int total, int resumable, int nonresumable)
{
var builder = new StringBuilder();
@@ -260,6 +401,14 @@ namespace BizTalkCheckmkPulse
return builder.ToString();
}
/// <summary>
/// Baut eine syntaktisch gueltige Checkmk-Local-Check-Zeile.
/// </summary>
/// <param name="state">Numerischer Checkmk-Zustand.</param>
/// <param name="serviceName">Stabiler Service-Name.</param>
/// <param name="metrics">Performance-Daten oder Bindestrich.</param>
/// <param name="detail">Menschenlesbare Service-Ausgabe.</param>
/// <returns>Vollstaendige Local-Check-Zeile.</returns>
private static string BuildLine(CheckState state, string serviceName, string metrics, string detail)
{
return ((int)state).ToString(CultureInfo.InvariantCulture)
@@ -269,17 +418,44 @@ namespace BizTalkCheckmkPulse
+ SanitizeDetail(detail);
}
private static string AppendDiagnostics(string detail, IEnumerable<string> diagnostics)
/// <summary>
/// Ergaenzt bis zu drei strukturierte Diagnosen am Detailtext.
/// </summary>
/// <param name="detail">Vorhandener Service-Detailtext.</param>
/// <param name="diagnostics">Relevante Diagnosen.</param>
/// <returns>Detailtext mit Ursache, Massnahme und technischen Details.</returns>
private static string AppendDiagnostics(string detail, IEnumerable<ProbeDiagnostic> diagnostics)
{
var visible = diagnostics == null ? new string[0] : diagnostics.Take(3).ToArray();
if (visible.Length == 0)
{
return detail;
}
return detail + "; diagnostics: " + string.Join("; ", visible);
var visible = diagnostics == null
? new string[0]
: diagnostics.Take(3).Select(x => x.ToDisplayText()).ToArray();
return visible.Length == 0
? detail
: detail + "; diagnostics: " + string.Join("; ", visible);
}
/// <summary>
/// Filtert Diagnosen nach Bereich und optionalen Komponentenfragmenten.
/// </summary>
/// <param name="result">Probe-Ergebnis mit allen Diagnosen.</param>
/// <param name="area">Gesuchter Diagnosebereich.</param>
/// <param name="components">Optionale Fragmente betroffener Komponenten.</param>
/// <returns>Passende Diagnosen in Entstehungsreihenfolge.</returns>
private static IEnumerable<ProbeDiagnostic> SelectDiagnostics(ProbeResult result, DiagnosticArea area, params string[] components)
{
return result.Diagnostics.Where(x =>
x.Area == area
&& (components == null
|| components.Length == 0
|| components.Any(component => (x.Component ?? string.Empty).IndexOf(component, StringComparison.OrdinalIgnoreCase) >= 0)));
}
/// <summary>
/// Haengt eine nicht leere Werteliste an einen Detailtext an.
/// </summary>
/// <param name="builder">Zu erweiternder Text.</param>
/// <param name="label">Bezeichnung der Liste.</param>
/// <param name="values">Zu formatierende Werte.</param>
private static void AppendList(StringBuilder builder, string label, IEnumerable<string> values)
{
var list = values.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
@@ -289,6 +465,12 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Haengt einen optionalen benannten Wert an einen Detailtext an.
/// </summary>
/// <param name="builder">Zu erweiternder Text.</param>
/// <param name="label">Bezeichnung des Wertes.</param>
/// <param name="value">Optionaler Wert.</param>
private static void AppendOptional(StringBuilder builder, string label, string value)
{
if (!string.IsNullOrWhiteSpace(value))
@@ -297,6 +479,12 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Verbindet SQL-Server und Datenbank zu einer kompakten Anzeige.
/// </summary>
/// <param name="server">SQL-Server oder Instanz.</param>
/// <param name="database">Datenbankname.</param>
/// <returns>Kombination aus Server und Datenbank oder leerer Text.</returns>
private static string JoinDb(string server, string database)
{
if (string.IsNullOrWhiteSpace(server) && string.IsNullOrWhiteSpace(database))
@@ -307,6 +495,11 @@ namespace BizTalkCheckmkPulse
return EmptyAsUnknown(server) + "\\" + EmptyAsUnknown(database);
}
/// <summary>
/// Uebersetzt den numerischen BizTalk Host ServiceState in einen Namen.
/// </summary>
/// <param name="state">Numerischer WMI-Zustand.</param>
/// <returns>Menschenlesbarer Zustandsname.</returns>
private static string HostStateName(int state)
{
switch (state)
@@ -332,16 +525,31 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Ersetzt leere Werte durch eine sichtbare Unknown-Markierung.
/// </summary>
/// <param name="value">Zu normalisierender Text.</param>
/// <returns>Getrimmter Wert oder <c>unknown</c>.</returns>
private static string EmptyAsUnknown(string value)
{
return string.IsNullOrWhiteSpace(value) ? "unknown" : value.Trim();
}
/// <summary>
/// Entfernt fuer Checkmk problematische Zeichen aus Service-Namen.
/// </summary>
/// <param name="value">Urspruenglicher Service-Name.</param>
/// <returns>Sicherer Service-Name.</returns>
private static string SanitizeService(string value)
{
return SanitizeDetail(value).Replace("\"", "'");
}
/// <summary>
/// Normalisiert Detailtext auf eine einzelne Checkmk-kompatible Zeile.
/// </summary>
/// <param name="value">Urspruenglicher Detailtext.</param>
/// <returns>Einzeiliger, nicht leerer Detailtext.</returns>
private static string SanitizeDetail(string value)
{
if (string.IsNullOrWhiteSpace(value))
+16 -1
View File
@@ -4,15 +4,26 @@ using System.Linq;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Zaehlt BizTalk-bezogene Fehler und Warnungen im lokalen Application Event Log.
/// </summary>
internal sealed class EventLogProbe
{
private readonly MonitoringOptions _options;
/// <summary>
/// Initialisiert den Event-Log-Probe mit Zeitraum, Quellen und Schwellwerten.
/// </summary>
/// <param name="options">Monitoring-Konfiguration.</param>
public EventLogProbe(MonitoringOptions options)
{
_options = options;
}
/// <summary>
/// Liest das Application Event Log rueckwaerts bis zum Beginn des Zeitfensters.
/// </summary>
/// <param name="result">Ergebnisobjekt fuer Zaehler und Fehlerstatus.</param>
public void Query(ProbeResult result)
{
result.EventLog.Since = DateTime.Now.AddMinutes(-_options.EventLogLookbackMinutes);
@@ -55,6 +66,11 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Prueft, ob eine Event-Log-Quelle zu den konfigurierten BizTalk-Quellen gehoert.
/// </summary>
/// <param name="source">Name der Windows Event-Log-Quelle.</param>
/// <returns><c>true</c>, wenn die Quelle ueberwacht werden soll.</returns>
private bool IsRelevantSource(string source)
{
if (string.IsNullOrWhiteSpace(source))
@@ -66,4 +82,3 @@ namespace BizTalkCheckmkPulse
}
}
}
+109 -3
View File
@@ -3,6 +3,9 @@ using System.Collections.Generic;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Definiert die von Checkmk erwarteten numerischen Service-Zustaende.
/// </summary>
internal enum CheckState
{
Ok = 0,
@@ -11,35 +14,113 @@ namespace BizTalkCheckmkPulse
Unknown = 3
}
/// <summary>
/// Unterscheidet fortsetzbare und nicht fortsetzbare suspendierte Instanzen.
/// </summary>
internal enum SuspendedKind
{
Resumable,
NonResumable
}
/// <summary>
/// Kennzeichnet den technischen Bereich, in dem eine Diagnose entstanden ist.
/// </summary>
internal enum DiagnosticArea
{
Wmi,
Sql,
EventLog,
General
}
/// <summary>
/// Klassifiziert Fehler, damit Bediener eine passende Massnahme erhalten.
/// </summary>
internal enum DiagnosticCategory
{
Permission,
Connectivity,
Timeout,
Configuration,
Provider,
Unexpected
}
/// <summary>
/// Strukturierte Diagnose mit Ursache und konkreter Handlungsempfehlung.
/// </summary>
internal sealed class ProbeDiagnostic
{
public DiagnosticArea Area { get; set; }
public DiagnosticCategory Category { get; set; }
public string Component { get; set; }
public bool Required { get; set; }
public string Summary { get; set; }
public string Action { get; set; }
public string TechnicalDetails { get; set; }
/// <summary>
/// Erstellt eine kompakte, fuer Checkmk geeignete Diagnosezeile.
/// </summary>
/// <returns>Menschenlesbarer Text mit Kategorie, Ursache und Massnahme.</returns>
public string ToDisplayText()
{
var text = Area + "/" + Category + " [" + (Component ?? "unknown") + "]: " + (Summary ?? "Unbekannter Fehler.");
if (!string.IsNullOrWhiteSpace(Action))
{
text += " Massnahme: " + Action;
}
if (!string.IsNullOrWhiteSpace(TechnicalDetails))
{
text += " Technik: " + TechnicalDetails;
}
return text;
}
}
/// <summary>
/// Sammelt alle Ergebnisse eines Programmlaufs.
/// </summary>
internal sealed class ProbeResult
{
/// <summary>
/// Initialisiert leere Ergebnislisten und Zustandsobjekte.
/// </summary>
public ProbeResult()
{
Diagnostics = new List<string>();
Diagnostics = new List<ProbeDiagnostic>();
Platform = new PlatformState();
HostInstances = new List<HostInstanceState>();
SuspendedInstances = new List<SuspendedInstance>();
Applications = new List<ApplicationRuntimeState>();
SqlTargets = new List<SqlAccessState>();
EventLog = new EventLogState();
}
public List<string> Diagnostics { get; private set; }
public List<ProbeDiagnostic> Diagnostics { get; private set; }
public PlatformState Platform { get; private set; }
public List<HostInstanceState> HostInstances { get; private set; }
public List<SuspendedInstance> SuspendedInstances { get; private set; }
public List<ApplicationRuntimeState> Applications { get; private set; }
public List<SqlAccessState> SqlTargets { get; private set; }
public string ExecutionIdentity { get; set; }
public string NetworkIdentityHint { get; set; }
public EventLogState EventLog { get; private set; }
}
/// <summary>
/// Beschreibt Erreichbarkeit und Teilverfuegbarkeit der BizTalk-WMI-Daten.
/// </summary>
internal sealed class PlatformState
{
public bool WmiConnected { get; set; }
public bool PlatformDataAvailable { get; set; }
public bool HostInstancesDataAvailable { get; set; }
public bool SuspendedInstancesDataAvailable { get; set; }
public bool RuntimeArtifactsDataAvailable { get; set; }
public string ServerName { get; set; }
public string GroupName { get; set; }
public string ManagementDbServer { get; set; }
@@ -48,6 +129,23 @@ namespace BizTalkCheckmkPulse
public string MessageBoxDbName { get; set; }
}
/// <summary>
/// Ergebnis der integrierten Anmeldung an einer BizTalk-Datenbank.
/// </summary>
internal sealed class SqlAccessState
{
public string Role { get; set; }
public string Server { get; set; }
public string Database { get; set; }
public bool Attempted { get; set; }
public bool Available { get; set; }
public DiagnosticCategory? FailureCategory { get; set; }
public string Failure { get; set; }
}
/// <summary>
/// Laufzeitzustand einer BizTalk Host Instance.
/// </summary>
internal sealed class HostInstanceState
{
public string InstanceName { get; set; }
@@ -56,6 +154,9 @@ namespace BizTalkCheckmkPulse
public int ServiceState { get; set; }
}
/// <summary>
/// Relevante Daten einer suspendierten BizTalk Service Instance.
/// </summary>
internal sealed class SuspendedInstance
{
public string ApplicationName { get; set; }
@@ -68,6 +169,9 @@ namespace BizTalkCheckmkPulse
public SuspendedKind Kind { get; set; }
}
/// <summary>
/// Aggregierte Zustandszaehler einer BizTalk-Anwendung.
/// </summary>
internal sealed class ApplicationRuntimeState
{
public string ApplicationName { get; set; }
@@ -86,6 +190,9 @@ namespace BizTalkCheckmkPulse
public int OrchestrationUnknown { get; set; }
}
/// <summary>
/// Ergebnis der Auswertung des lokalen Windows Application Event Logs.
/// </summary>
internal sealed class EventLogState
{
public bool Available { get; set; }
@@ -95,4 +202,3 @@ namespace BizTalkCheckmkPulse
public string Failure { get; set; }
}
}
+65 -1
View File
@@ -6,12 +6,17 @@ using System.Linq;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Enthaelt alle konfigurierbaren Schwellwerte und Probe-Optionen.
/// </summary>
internal sealed class MonitoringOptions
{
public string Server { get; set; }
public string ServicePrefix { get; set; }
public string EnvironmentName { get; set; }
public int QueryTimeoutSeconds { get; set; }
public bool ProbeSqlConnectivity { get; set; }
public int SqlConnectionTimeoutSeconds { get; set; }
public int WarnResumableThreshold { get; set; }
public int CritNonResumableThreshold { get; set; }
public int MaxSummaryItems { get; set; }
@@ -24,12 +29,17 @@ namespace BizTalkCheckmkPulse
public IReadOnlyList<string> EventLogSources { get; set; }
public bool SelfTest { get; set; }
/// <summary>
/// Setzt konservative Standardwerte fuer BizTalk Server 2020.
/// </summary>
public MonitoringOptions()
{
Server = ".";
ServicePrefix = "BizTalk";
EnvironmentName = string.Empty;
QueryTimeoutSeconds = 25;
ProbeSqlConnectivity = true;
SqlConnectionTimeoutSeconds = 5;
WarnResumableThreshold = 1;
CritNonResumableThreshold = 1;
MaxSummaryItems = 12;
@@ -42,6 +52,11 @@ namespace BizTalkCheckmkPulse
EventLogSources = new[] { "BizTalk Server", "XLANG/s", "ENTSSO", "BizTalk Server Application", "BizTalk Server EDI" };
}
/// <summary>
/// Erzeugt den stabilen Checkmk-Service-Namen mit optionalem Umgebungsnamen.
/// </summary>
/// <param name="suffix">Fachlicher Suffix des Services.</param>
/// <returns>Vollstaendiger Service-Name fuer Checkmk.</returns>
public string ServiceName(string suffix)
{
var prefix = string.IsNullOrWhiteSpace(EnvironmentName)
@@ -50,6 +65,11 @@ namespace BizTalkCheckmkPulse
return prefix.Trim() + " " + suffix;
}
/// <summary>
/// Laedt Optionen aus App.config und ueberschreibt sie anschliessend mit Argumenten.
/// </summary>
/// <param name="args">Beim Programmstart uebergebene Kommandozeilenargumente.</param>
/// <returns>Validierte Monitoring-Optionen.</returns>
public static MonitoringOptions Load(string[] args)
{
var options = new MonitoringOptions();
@@ -59,6 +79,8 @@ namespace BizTalkCheckmkPulse
options.ServicePrefix = ReadString(settings, "ServicePrefix", options.ServicePrefix);
options.EnvironmentName = ReadString(settings, "EnvironmentName", options.EnvironmentName);
options.QueryTimeoutSeconds = ReadInt(settings, "QueryTimeoutSeconds", options.QueryTimeoutSeconds, 5, 120);
options.ProbeSqlConnectivity = ReadBool(settings, "ProbeSqlConnectivity", options.ProbeSqlConnectivity);
options.SqlConnectionTimeoutSeconds = ReadInt(settings, "SqlConnectionTimeoutSeconds", options.SqlConnectionTimeoutSeconds, 3, 60);
options.WarnResumableThreshold = ReadInt(settings, "WarnResumableThreshold", options.WarnResumableThreshold, 0, 1000000);
options.CritNonResumableThreshold = ReadInt(settings, "CritNonResumableThreshold", options.CritNonResumableThreshold, 0, 1000000);
options.MaxSummaryItems = ReadInt(settings, "MaxSummaryItems", options.MaxSummaryItems, 1, 100);
@@ -74,6 +96,11 @@ namespace BizTalkCheckmkPulse
return options;
}
/// <summary>
/// Wendet unterstuetzte Kommandozeilenargumente auf die Optionen an.
/// </summary>
/// <param name="options">Zu aktualisierende Optionen.</param>
/// <param name="args">Kommandozeilenargumente.</param>
private static void ApplyArguments(MonitoringOptions options, string[] args)
{
for (var i = 0; i < args.Length; i++)
@@ -99,15 +126,35 @@ namespace BizTalkCheckmkPulse
{
options.AlertOnArtifactRuntimeIssues = true;
}
else if (EqualsAny(arg, "--no-sql-probe", "/no-sql-probe"))
{
options.ProbeSqlConnectivity = false;
}
}
}
/// <summary>
/// Liest einen getrimmten Konfigurationswert.
/// </summary>
/// <param name="settings">AppSettings-Sammlung.</param>
/// <param name="key">Name des Konfigurationswertes.</param>
/// <param name="fallback">Ersatzwert bei fehlendem Eintrag.</param>
/// <returns>Konfigurierter oder ersetzter Textwert.</returns>
private static string ReadString(System.Collections.Specialized.NameValueCollection settings, string key, string fallback)
{
var value = settings[key];
return value == null ? fallback : value.Trim();
}
/// <summary>
/// Liest eine Ganzzahl und begrenzt sie auf den erlaubten Wertebereich.
/// </summary>
/// <param name="settings">AppSettings-Sammlung.</param>
/// <param name="key">Name des Konfigurationswertes.</param>
/// <param name="fallback">Ersatzwert bei ungueltiger Eingabe.</param>
/// <param name="min">Kleinster erlaubter Wert.</param>
/// <param name="max">Groesster erlaubter Wert.</param>
/// <returns>Validierter Zahlenwert.</returns>
private static int ReadInt(System.Collections.Specialized.NameValueCollection settings, string key, int fallback, int min, int max)
{
int parsed;
@@ -124,12 +171,24 @@ namespace BizTalkCheckmkPulse
return parsed > max ? max : parsed;
}
/// <summary>
/// Liest einen booleschen Konfigurationswert.
/// </summary>
/// <param name="settings">AppSettings-Sammlung.</param>
/// <param name="key">Name des Konfigurationswertes.</param>
/// <param name="fallback">Ersatzwert bei ungueltiger Eingabe.</param>
/// <returns>Konfigurierter oder ersetzter Wahrheitswert.</returns>
private static bool ReadBool(System.Collections.Specialized.NameValueCollection settings, string key, bool fallback)
{
bool parsed;
return bool.TryParse(settings[key], out parsed) ? parsed : fallback;
}
/// <summary>
/// Zerlegt die konfigurierten Event-Log-Quellen und entfernt Duplikate.
/// </summary>
/// <param name="value">Mit Pipe, Semikolon oder Komma getrennte Quellen.</param>
/// <returns>Eindeutige Liste relevanter Event-Log-Quellen.</returns>
private static IReadOnlyList<string> ReadSources(string value)
{
return (value ?? string.Empty)
@@ -140,10 +199,15 @@ namespace BizTalkCheckmkPulse
.ToArray();
}
/// <summary>
/// Prueft einen Wert ohne Beachtung der Gross-/Kleinschreibung gegen Kandidaten.
/// </summary>
/// <param name="value">Zu pruefender Wert.</param>
/// <param name="candidates">Erlaubte Vergleichswerte.</param>
/// <returns><c>true</c>, wenn ein Kandidat passt.</returns>
private static bool EqualsAny(string value, params string[] candidates)
{
return candidates.Any(x => string.Equals(value, x, StringComparison.OrdinalIgnoreCase));
}
}
}
+12 -1
View File
@@ -4,8 +4,16 @@ using System.Text;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Einstiegspunkt des Checkmk Local Checks.
/// </summary>
internal static class Program
{
/// <summary>
/// Laedt die Konfiguration, fuehrt die Probes aus und schreibt Checkmk-Zeilen nach STDOUT.
/// </summary>
/// <param name="args">Kommandozeilenargumente wie <c>--self-test</c> oder <c>--environment</c>.</param>
/// <returns>Immer 0, damit Diagnosefehler als UNKNOWN-Service statt als kaputte Agent-Sektion erscheinen.</returns>
private static int Main(string[] args)
{
MonitoringOptions options = null;
@@ -30,6 +38,10 @@ namespace BizTalkCheckmkPulse
var wmiProbe = new WmiBizTalkProbe(options);
wmiProbe.Query(result);
// Der SQL-Test verwendet bewusst dieselbe Windows-Identitaet wie der Checkmk-Agent.
var sqlProbe = new SqlConnectivityProbe(options);
sqlProbe.Query(result);
if (options.ProbeEventLog)
{
var eventLogProbe = new EventLogProbe(options);
@@ -57,4 +69,3 @@ namespace BizTalkCheckmkPulse
}
}
}
@@ -0,0 +1,293 @@
using System;
using System.Data.SqlClient;
using System.Linq;
using System.Security.Principal;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Prueft mit der aktuellen Windows-Identitaet die Anmeldung an den von WMI
/// ermittelten BizTalk Management- und MessageBox-Datenbanken.
/// </summary>
internal sealed class SqlConnectivityProbe
{
private readonly MonitoringOptions _options;
/// <summary>
/// Initialisiert die SQL-Diagnose mit den Monitoring-Optionen.
/// </summary>
/// <param name="options">Konfiguration fuer Aktivierung und Timeout.</param>
public SqlConnectivityProbe(MonitoringOptions options)
{
_options = options;
}
/// <summary>
/// Ermittelt die aktuelle Identitaet und prueft alle bekannten SQL-Ziele.
/// </summary>
/// <param name="result">Ergebnisobjekt, das um SQL-Zustaende und Diagnosen erweitert wird.</param>
public void Query(ProbeResult result)
{
CaptureIdentity(result);
if (!_options.ProbeSqlConnectivity)
{
return;
}
AddTarget(result, "Management", result.Platform.ManagementDbServer, result.Platform.ManagementDbName);
AddTarget(result, "MessageBox", result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName);
if (!result.Platform.PlatformDataAvailable)
{
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Sql,
Category = DiagnosticCategory.Configuration,
Component = "BizTalk database discovery",
Required = true,
Summary = "SQL-Zielermittlung ist unvollstaendig, weil nicht alle erforderlichen BizTalk-Plattformklassen gelesen wurden.",
Action = "Zuerst den Service 'BizTalk Platform' sowie MSBTS_GroupSetting und MSBTS_MessageBoxSetting pruefen. Danach den Agent-Dump erneut ausfuehren."
});
}
if (result.SqlTargets.Count == 0)
{
return;
}
foreach (var target in result.SqlTargets)
{
ProbeTarget(result, target);
}
}
/// <summary>
/// Speichert die lokale Prozessidentitaet und einen Hinweis auf die Netzwerkidentitaet.
/// </summary>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
private static void CaptureIdentity(ProbeResult result)
{
try
{
using (var identity = WindowsIdentity.GetCurrent())
{
result.ExecutionIdentity = identity == null ? "unknown" : identity.Name;
var isLocalSystem = identity != null
&& identity.User != null
&& identity.User.IsWellKnown(WellKnownSidType.LocalSystemSid);
result.NetworkIdentityHint = isLocalSystem
? "<DOMAIN>\\" + Environment.MachineName + "$"
: result.ExecutionIdentity;
}
}
catch (Exception ex)
{
result.ExecutionIdentity = "unknown";
result.NetworkIdentityHint = "unknown";
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.General,
Category = DiagnosticCategory.Unexpected,
Component = "Windows identity",
Required = false,
Summary = "Die Windows-Ausfuehrungsidentitaet konnte nicht bestimmt werden.",
Action = "Agent-Dienstkonto mit 'sc.exe qc CheckMKService' beziehungsweise in services.msc pruefen.",
TechnicalDetails = ex.GetType().Name + ": " + ex.Message
});
}
}
/// <summary>
/// Fuegt ein eindeutiges und vollstaendig beschriebenes SQL-Ziel hinzu.
/// </summary>
/// <param name="result">Ergebnisobjekt mit der SQL-Zielliste.</param>
/// <param name="role">Fachliche Rolle der Datenbank.</param>
/// <param name="server">SQL-Server oder Instanzname.</param>
/// <param name="database">Name der BizTalk-Datenbank.</param>
private static void AddTarget(ProbeResult result, string role, string server, string database)
{
if (string.IsNullOrWhiteSpace(server) || string.IsNullOrWhiteSpace(database))
{
return;
}
var existing = result.SqlTargets.FirstOrDefault(x =>
string.Equals(x.Server, server, StringComparison.OrdinalIgnoreCase)
&& string.Equals(x.Database, database, StringComparison.OrdinalIgnoreCase));
if (existing != null)
{
existing.Role += "+" + role;
return;
}
result.SqlTargets.Add(new SqlAccessState
{
Role = role,
Server = server.Trim(),
Database = database.Trim()
});
}
/// <summary>
/// Oeffnet eine integrierte SQL-Verbindung und fuehrt eine harmlose Testabfrage aus.
/// </summary>
/// <param name="result">Ergebnisobjekt fuer eventuell entstehende Diagnosen.</param>
/// <param name="target">Zu pruefendes SQL-Ziel.</param>
private void ProbeTarget(ProbeResult result, SqlAccessState target)
{
target.Attempted = true;
try
{
var builder = new SqlConnectionStringBuilder
{
DataSource = target.Server,
InitialCatalog = target.Database,
IntegratedSecurity = true,
ConnectTimeout = _options.SqlConnectionTimeoutSeconds,
ApplicationName = "BizTalkCheckmkPulse",
Pooling = false,
PersistSecurityInfo = false
};
using (var connection = new SqlConnection(builder.ConnectionString))
using (var command = new SqlCommand("SELECT 1", connection))
{
command.CommandTimeout = _options.SqlConnectionTimeoutSeconds;
connection.Open();
command.ExecuteScalar();
}
target.Available = true;
}
catch (SqlException ex)
{
var category = ClassifySqlException(ex);
target.Available = false;
target.FailureCategory = category;
target.Failure = "SQL error " + ex.Number + ": " + ex.Message;
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Sql,
Category = category,
Component = target.Role + " " + target.Server + "\\" + target.Database,
Required = true,
Summary = BuildSqlSummary(category, result.NetworkIdentityHint),
Action = BuildSqlAction(category, result.NetworkIdentityHint, target),
TechnicalDetails = "SqlException Number=" + ex.Number + ", State=" + ex.State + ", Class=" + ex.Class + ": " + ex.Message
});
}
catch (Exception ex)
{
target.Available = false;
target.FailureCategory = DiagnosticCategory.Unexpected;
target.Failure = ex.GetType().Name + ": " + ex.Message;
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Sql,
Category = DiagnosticCategory.Unexpected,
Component = target.Role + " " + target.Server + "\\" + target.Database,
Required = true,
Summary = "Unerwarteter Fehler beim SQL-Zugriff.",
Action = "Technische Details und Windows-/SQL-Event-Logs pruefen. Danach den Test im Agent-Dump wiederholen.",
TechnicalDetails = ex.GetType().Name + ": " + ex.Message
});
}
}
/// <summary>
/// Ordnet bekannte SQL-Fehlernummern einer handlungsorientierten Kategorie zu.
/// </summary>
/// <param name="exception">Von System.Data.SqlClient gelieferter Fehler.</param>
/// <returns>Diagnosekategorie fuer Berechtigung, Timeout oder Verbindung.</returns>
private static DiagnosticCategory ClassifySqlException(SqlException exception)
{
var permissionNumbers = new[] { 229, 297, 4060, 916, 18452, 18456 };
var connectivityNumbers = new[] { 2, 53, 64, 121, 233, 258, 10060, 10061, 11001 };
if (exception.Errors.Cast<SqlError>().Any(x => permissionNumbers.Contains(x.Number)))
{
return DiagnosticCategory.Permission;
}
var message = exception.Message ?? string.Empty;
if (ContainsAny(message, "certificate", "SSL Provider", "TLS", "SSPI context", "target principal name"))
{
return DiagnosticCategory.Configuration;
}
if (exception.Number == -2)
{
return DiagnosticCategory.Timeout;
}
if (exception.Errors.Cast<SqlError>().Any(x => connectivityNumbers.Contains(x.Number)))
{
return DiagnosticCategory.Connectivity;
}
if (ContainsAny(message, "login failed", "cannot open database", "permission", "denied", "not authorized"))
{
return DiagnosticCategory.Permission;
}
return DiagnosticCategory.Provider;
}
/// <summary>
/// Erstellt eine kurze Fehlerursache fuer die Checkmk-Serviceausgabe.
/// </summary>
/// <param name="category">Klassifizierte SQL-Fehlerart.</param>
/// <param name="networkIdentity">Vermutete Identitaet am entfernten SQL Server.</param>
/// <returns>Menschenlesbare Fehlerzusammenfassung.</returns>
private static string BuildSqlSummary(DiagnosticCategory category, string networkIdentity)
{
switch (category)
{
case DiagnosticCategory.Permission:
return "SQL Server hat die integrierte Anmeldung abgelehnt. Erwartete Netzwerkidentitaet: " + networkIdentity + ".";
case DiagnosticCategory.Timeout:
return "SQL-Verbindungsaufbau oder Testabfrage hat das konfigurierte Zeitlimit ueberschritten.";
case DiagnosticCategory.Connectivity:
return "SQL Server oder die konfigurierte SQL-Instanz ist aus dem Checkmk-Agent-Kontext nicht erreichbar.";
case DiagnosticCategory.Configuration:
return "SQL-Verbindung scheitert an TLS-, Zertifikats-, SPN- oder SSPI-Konfiguration.";
default:
return "SQL Server hat beim Verbindungstest einen Providerfehler geliefert.";
}
}
/// <summary>
/// Liefert eine konkrete, zur SQL-Fehlerkategorie passende Handlungsempfehlung.
/// </summary>
/// <param name="category">Klassifizierte SQL-Fehlerart.</param>
/// <param name="networkIdentity">Vermutete Identitaet am entfernten SQL Server.</param>
/// <param name="target">Betroffenes SQL-Ziel.</param>
/// <returns>Empfohlener Diagnoseschritt.</returns>
private static string BuildSqlAction(DiagnosticCategory category, string networkIdentity, SqlAccessState target)
{
switch (category)
{
case DiagnosticCategory.Permission:
return networkIdentity + " in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen.";
case DiagnosticCategory.Timeout:
return "Netzwerkpfad, DNS, SQL-Port, Firewall und Auslastung fuer " + target.Server + " pruefen; Timeout nur nach Ursachenanalyse erhoehen.";
case DiagnosticCategory.Connectivity:
return "Server-/Instanzname " + target.Server + ", DNS, SQL-Dienst, TCP-Protokoll und Firewall aus Sicht des BizTalk-Servers pruefen.";
case DiagnosticCategory.Configuration:
return "TLS-Zertifikatskette, SQL-Verschluesselung, SPN und Kerberos/SSPI gemeinsam mit SQL- und Windows-Administration pruefen; Verschluesselung nicht pauschal deaktivieren.";
default:
return "SQL-Fehlernummer mit der SQL-Administration pruefen und den Test gegen " + target.Server + "\\" + target.Database + " im gleichen Dienstkonto-Kontext wiederholen.";
}
}
/// <summary>
/// Sucht mehrere Teiltexte ohne Beachtung der Gross-/Kleinschreibung.
/// </summary>
/// <param name="value">Zu untersuchender Text.</param>
/// <param name="candidates">Gesuchte Teiltexte.</param>
/// <returns><c>true</c>, wenn mindestens ein Teiltext enthalten ist.</returns>
private static bool ContainsAny(string value, params string[] candidates)
{
return candidates.Any(x => value.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0);
}
}
}
+307 -33
View File
@@ -2,9 +2,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Liest BizTalk Server 2020 ueber den lokalen Microsoft-WMI-Provider aus.
/// </summary>
internal sealed class WmiBizTalkProbe
{
private const string NamespacePath = "root\\MicrosoftBizTalkServer";
@@ -18,13 +22,22 @@ namespace BizTalkCheckmkPulse
private const int OrchestrationBound = 2;
private const int OrchestrationStopped = 3;
private const int OrchestrationStarted = 4;
private const int EAccessDenied = unchecked((int)0x80070005);
private readonly MonitoringOptions _options;
/// <summary>
/// Initialisiert den WMI-Probe mit den konfigurierten Timeouts.
/// </summary>
/// <param name="options">Monitoring-Konfiguration.</param>
public WmiBizTalkProbe(MonitoringOptions options)
{
_options = options;
}
/// <summary>
/// Verbindet sich mit dem BizTalk-WMI-Namespace und liest alle Datenbereiche.
/// </summary>
/// <param name="result">Ergebnisobjekt fuer Messwerte, Verfuegbarkeiten und Diagnosen.</param>
public void Query(ProbeResult result)
{
var server = string.IsNullOrWhiteSpace(_options.Server) || _options.Server == "."
@@ -32,37 +45,87 @@ namespace BizTalkCheckmkPulse
: _options.Server.Trim();
result.Platform.ServerName = server;
var scope = new ManagementScope("\\\\" + server + "\\" + NamespacePath);
scope.Connect();
result.Platform.WmiConnected = true;
if (!TryConnect(scope, result))
{
return;
}
QueryPlatform(scope, result);
QueryHostInstances(scope, result);
result.Platform.WmiConnected = true;
result.Platform.PlatformDataAvailable = QueryPlatform(scope, result);
result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result);
var applicationIndex = BuildApplicationIndex(scope, result);
QueryApplicationRuntime(scope, result);
QuerySuspendedInstances(scope, result, applicationIndex);
result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result);
result.Platform.SuspendedInstancesDataAvailable = QuerySuspendedInstances(scope, result, applicationIndex);
}
private void QueryPlatform(ManagementScope scope, ProbeResult result)
/// <summary>
/// Baut die WMI-Verbindung auf und wandelt Fehler in handlungsorientierte Diagnosen um.
/// </summary>
/// <param name="scope">Zu verbindender WMI-Scope.</param>
/// <param name="result">Ergebnisobjekt fuer eine moegliche Diagnose.</param>
/// <returns><c>true</c>, wenn der Namespace verbunden wurde.</returns>
private static bool TryConnect(ManagementScope scope, ProbeResult result)
{
TryQuery(scope, result, "MSBTS_GroupSetting", "SELECT * FROM MSBTS_GroupSetting", item =>
try
{
scope.Connect();
return true;
}
catch (Exception ex) when (IsExpectedWmiException(ex))
{
AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true);
return false;
}
}
/// <summary>
/// Liest BizTalk-Gruppe sowie Management- und MessageBox-Datenbankziele.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <returns><c>true</c>, wenn beide Pflichtklassen gelesen wurden.</returns>
private bool QueryPlatform(ManagementScope scope, ProbeResult result)
{
var groupFound = false;
var groupAvailable = TryQuery(scope, result, "MSBTS_GroupSetting", "SELECT * FROM MSBTS_GroupSetting", item =>
{
groupFound = true;
result.Platform.GroupName = FirstNonEmpty(WmiHelpers.GetString(item, "Name"), WmiHelpers.GetString(item, "MgmtDbName"));
result.Platform.ManagementDbServer = FirstNonEmpty(WmiHelpers.GetString(item, "MgmtDbServerName"), WmiHelpers.GetString(item, "DBServerName"));
result.Platform.ManagementDbName = FirstNonEmpty(WmiHelpers.GetString(item, "MgmtDbName"), WmiHelpers.GetString(item, "DatabaseName"));
}, true);
TryQuery(scope, result, "MSBTS_MessageBoxSetting", "SELECT * FROM MSBTS_MessageBoxSetting", item =>
var messageBoxFound = false;
var messageBoxAvailable = TryQuery(scope, result, "MSBTS_MessageBoxSetting", "SELECT * FROM MSBTS_MessageBoxSetting", item =>
{
messageBoxFound = true;
if (string.IsNullOrWhiteSpace(result.Platform.MessageBoxDbServer))
{
result.Platform.MessageBoxDbServer = FirstNonEmpty(WmiHelpers.GetString(item, "DBServerName"), WmiHelpers.GetString(item, "ServerName"));
result.Platform.MessageBoxDbName = FirstNonEmpty(WmiHelpers.GetString(item, "DBName"), WmiHelpers.GetString(item, "DatabaseName"));
}
}, true);
if (groupAvailable && !groupFound)
{
AddMissingDataDiagnostic(result, "MSBTS_GroupSetting", "Keine BizTalk-Gruppe gefunden.");
}
if (messageBoxAvailable && !messageBoxFound)
{
AddMissingDataDiagnostic(result, "MSBTS_MessageBoxSetting", "Keine BizTalk-MessageBox-Datenbank gefunden.");
}
return groupAvailable && groupFound && messageBoxAvailable && messageBoxFound;
}
/// <summary>
/// Erstellt einen Index, um suspendierte Instanzen BizTalk-Anwendungen zuzuordnen.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Ergebnisobjekt fuer optionale Diagnosen.</param>
/// <returns>Index aus moeglichen Artefaktschluesseln und Anwendungsnamen.</returns>
private Dictionary<string, string> BuildApplicationIndex(ManagementScope scope, ProbeResult result)
{
var index = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -73,9 +136,16 @@ namespace BizTalkCheckmkPulse
return index;
}
/// <summary>
/// Fuegt die Schluessel einer WMI-Artefaktklasse zum Anwendungsindex hinzu.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Ergebnisobjekt fuer optionale Diagnosen.</param>
/// <param name="index">Zu erweiternder Anwendungsindex.</param>
/// <param name="className">Name der auszulesenden WMI-Klasse.</param>
private void AddArtifactIndex(ManagementScope scope, ProbeResult result, Dictionary<string, string> index, string className)
{
TryQuery(scope, result, className, "SELECT * FROM " + className, item =>
TryQuery(scope, result, className + " application index", "SELECT * FROM " + className, item =>
{
var app = GetApplicationName(item);
foreach (var key in CandidateKeys(item))
@@ -88,9 +158,15 @@ namespace BizTalkCheckmkPulse
}, false);
}
private void QueryHostInstances(ManagementScope scope, ProbeResult result)
/// <summary>
/// Liest alle BizTalk Host Instances und deren Dienstzustand.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <returns><c>true</c>, wenn die Pflichtabfrage erfolgreich war.</returns>
private bool QueryHostInstances(ManagementScope scope, ProbeResult result)
{
TryQuery(scope, result, "MSBTS_HostInstance", "SELECT * FROM MSBTS_HostInstance", item =>
return TryQuery(scope, result, "MSBTS_HostInstance", "SELECT * FROM MSBTS_HostInstance", item =>
{
result.HostInstances.Add(new HostInstanceState
{
@@ -102,11 +178,17 @@ namespace BizTalkCheckmkPulse
}, true);
}
private void QueryApplicationRuntime(ManagementScope scope, ProbeResult result)
/// <summary>
/// Aggregiert Receive Locations, Send Ports und Orchestrations pro Anwendung.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <returns><c>true</c>, wenn alle drei Pflichtabfragen erfolgreich waren.</returns>
private bool QueryApplicationRuntime(ManagementScope scope, ProbeResult result)
{
var apps = new Dictionary<string, ApplicationRuntimeState>(StringComparer.OrdinalIgnoreCase);
TryQuery(scope, result, "MSBTS_ReceiveLocation", "SELECT * FROM MSBTS_ReceiveLocation", item =>
var receiveLocationsAvailable = TryQuery(scope, result, "MSBTS_ReceiveLocation", "SELECT * FROM MSBTS_ReceiveLocation", item =>
{
var app = GetApplication(apps, GetApplicationName(item));
app.ReceiveLocationTotal++;
@@ -116,7 +198,7 @@ namespace BizTalkCheckmkPulse
}
}, true);
TryQuery(scope, result, "MSBTS_SendPort", "SELECT * FROM MSBTS_SendPort", item =>
var sendPortsAvailable = TryQuery(scope, result, "MSBTS_SendPort", "SELECT * FROM MSBTS_SendPort", item =>
{
var app = GetApplication(apps, GetApplicationName(item));
app.SendPortTotal++;
@@ -137,7 +219,7 @@ namespace BizTalkCheckmkPulse
}
}, true);
TryQuery(scope, result, "MSBTS_Orchestration", "SELECT * FROM MSBTS_Orchestration", item =>
var orchestrationsAvailable = TryQuery(scope, result, "MSBTS_Orchestration", "SELECT * FROM MSBTS_Orchestration", item =>
{
var app = GetApplication(apps, GetApplicationName(item));
app.OrchestrationTotal++;
@@ -162,11 +244,19 @@ namespace BizTalkCheckmkPulse
}, true);
result.Applications.AddRange(apps.Values.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase));
return receiveLocationsAvailable && sendPortsAvailable && orchestrationsAvailable;
}
private void QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary<string, string> applicationIndex)
/// <summary>
/// Liest ausschliesslich fortsetzbar und nicht fortsetzbar suspendierte Instanzen.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <param name="applicationIndex">Index fuer die Zuordnung zur BizTalk-Anwendung.</param>
/// <returns><c>true</c>, wenn die Pflichtabfrage erfolgreich war.</returns>
private bool QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary<string, string> applicationIndex)
{
TryQuery(scope, result, "MSBTS_ServiceInstance", "SELECT * FROM MSBTS_ServiceInstance WHERE ServiceStatus = 4 OR ServiceStatus = 32", item =>
return TryQuery(scope, result, "MSBTS_ServiceInstance", "SELECT * FROM MSBTS_ServiceInstance WHERE ServiceStatus = 4 OR ServiceStatus = 32", item =>
{
var serviceStatus = WmiHelpers.GetInt32(item, "ServiceStatus", 0);
result.SuspendedInstances.Add(new SuspendedInstance
@@ -183,26 +273,36 @@ namespace BizTalkCheckmkPulse
}, true);
}
private void TryQuery(ManagementScope scope, ProbeResult result, string label, string queryText, Action<ManagementObject> action, bool required)
/// <summary>
/// Fuehrt eine WQL-Abfrage aus und erfasst erwartbare Providerfehler strukturiert.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Ergebnisobjekt fuer Diagnosen.</param>
/// <param name="label">Menschenlesbare Bezeichnung der Abfrage.</param>
/// <param name="queryText">Auszufuehrende WQL-Abfrage.</param>
/// <param name="action">Verarbeitung fuer jedes gefundene WMI-Objekt.</param>
/// <param name="required">Kennzeichen, ob die Abfrage fuer einen stabilen Service erforderlich ist.</param>
/// <returns><c>true</c>, wenn die Abfrage vollstaendig ausgefuehrt wurde.</returns>
private bool TryQuery(ManagementScope scope, ProbeResult result, string label, string queryText, Action<ManagementObject> action, bool required)
{
try
{
ForEachObject(scope, queryText, action);
return true;
}
catch (ManagementException ex)
catch (Exception ex) when (IsExpectedWmiException(ex))
{
AddDiagnostic(result, label, ex.Message, required);
}
catch (UnauthorizedAccessException ex)
{
AddDiagnostic(result, label, ex.Message, required);
}
catch (System.Runtime.InteropServices.COMException ex)
{
AddDiagnostic(result, label, ex.Message, required);
AddWmiDiagnostic(result, label, ex, required, false);
return false;
}
}
/// <summary>
/// Enumeriert eine WQL-Abfrage mit festem Timeout und gibt jedes Objekt weiter.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="queryText">Auszufuehrende WQL-Abfrage.</param>
/// <param name="action">Verarbeitung fuer jedes gefundene Objekt.</param>
private void ForEachObject(ManagementScope scope, string queryText, Action<ManagementObject> action)
{
var queryOptions = new EnumerationOptions
@@ -225,6 +325,12 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Liefert eine vorhandene oder neu angelegte Aggregation fuer eine Anwendung.
/// </summary>
/// <param name="apps">Aggregation nach Anwendungsname.</param>
/// <param name="applicationName">Zu suchender Anwendungsname.</param>
/// <returns>Aggregation der angeforderten Anwendung.</returns>
private static ApplicationRuntimeState GetApplication(Dictionary<string, ApplicationRuntimeState> apps, string applicationName)
{
var name = string.IsNullOrWhiteSpace(applicationName) ? UnknownApplication : applicationName.Trim();
@@ -238,6 +344,12 @@ namespace BizTalkCheckmkPulse
return app;
}
/// <summary>
/// Ermittelt den Anwendungsnamen direkt oder ueber den Artefaktindex.
/// </summary>
/// <param name="item">Suspendierte WMI-Instanz.</param>
/// <param name="applicationIndex">Index bekannter Artefaktschluessel.</param>
/// <returns>Ermittelter Anwendungsname oder <c>(unknown)</c>.</returns>
private static string ResolveApplication(ManagementBaseObject item, Dictionary<string, string> applicationIndex)
{
var direct = GetApplicationName(item);
@@ -258,6 +370,11 @@ namespace BizTalkCheckmkPulse
return UnknownApplication;
}
/// <summary>
/// Liefert moegliche Schluessel fuer die Zuordnung eines WMI-Objekts.
/// </summary>
/// <param name="item">Auszuwertendes WMI-Objekt.</param>
/// <returns>Vorhandene oder leere Kandidatenwerte.</returns>
private static IEnumerable<string> CandidateKeys(ManagementBaseObject item)
{
yield return WmiHelpers.GetString(item, "ApplicationName");
@@ -266,6 +383,11 @@ namespace BizTalkCheckmkPulse
yield return WmiHelpers.GetString(item, "HostName");
}
/// <summary>
/// Liest den Anwendungsnamen aus den je Klasse unterschiedlichen Properties.
/// </summary>
/// <param name="item">Auszuwertendes WMI-Objekt.</param>
/// <returns>Anwendungsname oder <c>(unknown)</c>.</returns>
private static string GetApplicationName(ManagementBaseObject item)
{
return FirstNonEmpty(
@@ -274,15 +396,167 @@ namespace BizTalkCheckmkPulse
WmiHelpers.GetString(item, "BizTalkApplication")) ?? UnknownApplication;
}
/// <summary>
/// Liefert den ersten nicht leeren Textwert.
/// </summary>
/// <param name="values">Zu pruefende Werte in Prioritaetsreihenfolge.</param>
/// <returns>Erster nicht leerer Wert oder <c>null</c>.</returns>
private static string FirstNonEmpty(params string[] values)
{
return values == null ? null : values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
}
private static void AddDiagnostic(ProbeResult result, string label, string message, bool required)
/// <summary>
/// Prueft, ob eine Exception aus dem erwarteten WMI-/COM-Fehlerbereich stammt.
/// </summary>
/// <param name="exception">Zu klassifizierende Exception.</param>
/// <returns><c>true</c> fuer behandelbare WMI-, Rechte- und COM-Fehler.</returns>
private static bool IsExpectedWmiException(Exception exception)
{
result.Diagnostics.Add((required ? "Required" : "Optional") + " WMI query " + label + " failed: " + message);
return exception is ManagementException
|| exception is UnauthorizedAccessException
|| exception is COMException;
}
/// <summary>
/// Erzeugt aus einer WMI-Exception eine strukturierte Diagnose.
/// </summary>
/// <param name="result">Ergebnisobjekt fuer die Diagnose.</param>
/// <param name="component">Betroffener Namespace oder Klassenname.</param>
/// <param name="exception">Ausgeloeste Exception.</param>
/// <param name="required">Kennzeichen fuer eine Pflichtabfrage.</param>
/// <param name="connectionFailure">Kennzeichen fuer Fehler beim Namespace-Verbindungsaufbau.</param>
private static void AddWmiDiagnostic(ProbeResult result, string component, Exception exception, bool required, bool connectionFailure)
{
var category = ClassifyWmiException(exception);
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Wmi,
Category = category,
Component = component,
Required = required,
Summary = BuildWmiSummary(category, component, connectionFailure),
Action = BuildWmiAction(category, connectionFailure),
TechnicalDetails = exception.GetType().Name + " HRESULT=0x" + exception.HResult.ToString("X8") + ": " + exception.Message
});
}
/// <summary>
/// Erzeugt eine Konfigurationsdiagnose, wenn eine erfolgreiche Pflichtabfrage keine Instanz liefert.
/// </summary>
/// <param name="result">Ergebnisobjekt fuer die Diagnose.</param>
/// <param name="component">Betroffene WMI-Klasse.</param>
/// <param name="summary">Beschreibung der fehlenden BizTalk-Konfiguration.</param>
private static void AddMissingDataDiagnostic(ProbeResult result, string component, string summary)
{
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Wmi,
Category = DiagnosticCategory.Configuration,
Component = component,
Required = true,
Summary = summary,
Action = "BizTalk-Gruppenkonfiguration und WMI-Provider pruefen. Danach den Agent-Dump erneut ausfuehren."
});
}
/// <summary>
/// Klassifiziert WMI-Fehler anhand von Exception-Typ, Status und HRESULT.
/// </summary>
/// <param name="exception">Zu klassifizierende Exception.</param>
/// <returns>Handlungsorientierte Diagnosekategorie.</returns>
private static DiagnosticCategory ClassifyWmiException(Exception exception)
{
var managementException = exception as ManagementException;
var comException = exception as COMException;
if (exception is UnauthorizedAccessException
|| (managementException != null && managementException.ErrorCode == ManagementStatus.AccessDenied)
|| (comException != null && comException.ErrorCode == EAccessDenied))
{
return DiagnosticCategory.Permission;
}
if (managementException != null
&& (managementException.ErrorCode == ManagementStatus.InvalidNamespace
|| managementException.ErrorCode == ManagementStatus.InvalidClass))
{
return DiagnosticCategory.Configuration;
}
var message = exception.Message ?? string.Empty;
if (ContainsAny(message, "timed out", "timeout", "Zeitlimit"))
{
return DiagnosticCategory.Timeout;
}
if (ContainsAny(message, "RPC server", "network path", "unavailable", "not available"))
{
return DiagnosticCategory.Connectivity;
}
return DiagnosticCategory.Provider;
}
/// <summary>
/// Erstellt eine kurze Fehlerursache fuer einen WMI-Fehler.
/// </summary>
/// <param name="category">Klassifizierte Fehlerart.</param>
/// <param name="component">Betroffene Komponente.</param>
/// <param name="connectionFailure">Kennzeichen fuer den Namespace-Verbindungsaufbau.</param>
/// <returns>Menschenlesbare Fehlerzusammenfassung.</returns>
private static string BuildWmiSummary(DiagnosticCategory category, string component, bool connectionFailure)
{
switch (category)
{
case DiagnosticCategory.Permission:
return "Zugriff auf " + component + " wurde im LocalSystem-Kontext verweigert.";
case DiagnosticCategory.Configuration:
return connectionFailure
? "BizTalk-WMI-Namespace ist nicht vorhanden oder nicht korrekt registriert."
: "BizTalk-WMI-Klasse " + component + " ist nicht vorhanden oder nicht korrekt registriert.";
case DiagnosticCategory.Timeout:
return "WMI-Abfrage fuer " + component + " hat das konfigurierte Zeitlimit ueberschritten.";
case DiagnosticCategory.Connectivity:
return "WMI-/RPC-Verbindung zu " + component + " konnte nicht hergestellt werden.";
default:
return "BizTalk-WMI-Provider konnte " + component + " nicht auslesen.";
}
}
/// <summary>
/// Liefert eine konkrete Handlungsempfehlung fuer einen WMI-Fehler.
/// </summary>
/// <param name="category">Klassifizierte Fehlerart.</param>
/// <param name="connectionFailure">Kennzeichen fuer den Namespace-Verbindungsaufbau.</param>
/// <returns>Empfohlener Diagnoseschritt.</returns>
private static string BuildWmiAction(DiagnosticCategory category, bool connectionFailure)
{
switch (category)
{
case DiagnosticCategory.Permission:
return connectionFailure
? "Namespace-ACL fuer root\\MicrosoftBizTalkServer gezielt pruefen. LocalSystem benoetigt lokalen Lesezugriff; keine pauschalen WMI-Rechte vergeben."
: "Agent-Dump pruefen und Computerkonto <DOMAIN>\\" + Environment.MachineName + "$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen.";
case DiagnosticCategory.Configuration:
return "BizTalk-WMI-Provider/Namespace auf dem BizTalk-Server pruefen und gegebenenfalls mit dem BizTalk-Setup reparieren.";
case DiagnosticCategory.Timeout:
return "WMI-Dienst, BizTalk-/SQL-Auslastung und Event Logs pruefen; Timeout erst danach erhoehen.";
case DiagnosticCategory.Connectivity:
return "Bei lokalem Zugriff WMI-Dienst und BizTalk-WMI-Provider pruefen; bei konfiguriertem Remote-WMI zusaetzlich RPC, DNS und Firewall pruefen.";
default:
return "Technische Details sowie Windows Application Log und BizTalk-Ereignisse pruefen. Betroffene WMI-Klasse separat testen.";
}
}
/// <summary>
/// Sucht mehrere Teiltexte ohne Beachtung der Gross-/Kleinschreibung.
/// </summary>
/// <param name="value">Zu untersuchender Text.</param>
/// <param name="candidates">Gesuchte Teiltexte.</param>
/// <returns><c>true</c>, wenn mindestens ein Teiltext enthalten ist.</returns>
private static bool ContainsAny(string value, params string[] candidates)
{
return candidates.Any(x => value.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0);
}
}
}
+29 -1
View File
@@ -4,8 +4,17 @@ using System.Management;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Konvertiert optionale WMI-Properties defensiv in .NET-Typen.
/// </summary>
internal static class WmiHelpers
{
/// <summary>
/// Liest eine WMI-Property als getrimmten Text.
/// </summary>
/// <param name="item">WMI-Objekt mit der Property.</param>
/// <param name="propertyName">Name der Property.</param>
/// <returns>Textwert oder leerer Text bei fehlender Property.</returns>
public static string GetString(ManagementBaseObject item, string propertyName)
{
if (item == null || string.IsNullOrWhiteSpace(propertyName))
@@ -24,6 +33,13 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Liest eine WMI-Property als Ganzzahl.
/// </summary>
/// <param name="item">WMI-Objekt mit der Property.</param>
/// <param name="propertyName">Name der Property.</param>
/// <param name="fallback">Ersatzwert bei fehlender oder ungueltiger Property.</param>
/// <returns>Konvertierter oder ersetzter Zahlenwert.</returns>
public static int GetInt32(ManagementBaseObject item, string propertyName, int fallback)
{
var value = GetString(item, propertyName);
@@ -31,6 +47,13 @@ namespace BizTalkCheckmkPulse
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : fallback;
}
/// <summary>
/// Liest eine WMI-Property als booleschen Wert.
/// </summary>
/// <param name="item">WMI-Objekt mit der Property.</param>
/// <param name="propertyName">Name der Property.</param>
/// <param name="fallback">Ersatzwert bei fehlender oder ungueltiger Property.</param>
/// <returns>Konvertierter oder ersetzter Wahrheitswert.</returns>
public static bool GetBoolean(ManagementBaseObject item, string propertyName, bool fallback)
{
var value = GetString(item, propertyName);
@@ -38,6 +61,12 @@ namespace BizTalkCheckmkPulse
return bool.TryParse(value, out parsed) ? parsed : fallback;
}
/// <summary>
/// Konvertiert einen DMTF-Zeitstempel aus einer WMI-Property.
/// </summary>
/// <param name="item">WMI-Objekt mit der Property.</param>
/// <param name="propertyName">Name der Zeitstempel-Property.</param>
/// <returns>Zeitpunkt oder <c>null</c> bei fehlendem beziehungsweise ungueltigem Wert.</returns>
public static DateTime? GetDmtfDateTime(ManagementBaseObject item, string propertyName)
{
var value = GetString(item, propertyName);
@@ -57,4 +86,3 @@ namespace BizTalkCheckmkPulse
}
}
}