Harden BizTalk WMI monitoring

This commit is contained in:
2026-07-29 09:01:39 +02:00
parent 78e8881d38
commit 8d2934e343
17 changed files with 427 additions and 88 deletions
@@ -40,6 +40,7 @@
<Reference Include="System.Management" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="CheckmkLocalFormatter.cs" />
<Compile Include="EventLogProbe.cs" />
<Compile Include="MonitoringOptions.cs" />
@@ -62,6 +62,9 @@ namespace BizTalkCheckmkPulse
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.");
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("Runtime Artifacts"), "biztalk_applications=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.");
}
/// <summary>
@@ -96,7 +99,7 @@ namespace BizTalkCheckmkPulse
AppendOptional(detail, "mgmt_db", JoinDb(result.Platform.ManagementDbServer, result.Platform.ManagementDbName));
AppendOptional(detail, "msgbox_db", JoinDb(result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName));
var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting", "MSBTS_MessageBoxSetting");
var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting");
return BuildLine(state, _options.ServiceName("Platform"), "-", AppendDiagnostics(detail.ToString(), diagnostics));
}
@@ -334,7 +337,10 @@ namespace BizTalkCheckmkPulse
/// <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))
foreach (var group in result.SuspendedInstances
.Where(x => !string.Equals(x.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase))
.GroupBy(x => x.ApplicationName)
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
{
var resumable = group.Count(x => x.Kind == SuspendedKind.Resumable);
var nonresumable = group.Count(x => x.Kind == SuspendedKind.NonResumable);
+1
View File
@@ -43,6 +43,7 @@ namespace BizTalkCheckmkPulse
Connectivity,
Timeout,
Configuration,
Schema,
Provider,
Unexpected
}
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")]
@@ -35,7 +35,7 @@ namespace BizTalkCheckmkPulse
}
AddTarget(result, "Management", result.Platform.ManagementDbServer, result.Platform.ManagementDbName);
AddTarget(result, "MessageBox", result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName);
AddTarget(result, "MasterMessageBox", result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName);
if (!result.Platform.PlatformDataAvailable)
{
@@ -46,7 +46,7 @@ namespace BizTalkCheckmkPulse
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."
Action = "Zuerst den Service 'BizTalk Platform' und MSBTS_GroupSetting pruefen. Danach den Agent-Dump erneut ausfuehren."
});
}
+159 -56
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
@@ -23,6 +24,8 @@ namespace BizTalkCheckmkPulse
private const int OrchestrationStopped = 3;
private const int OrchestrationStarted = 4;
private const int EAccessDenied = unchecked((int)0x80070005);
internal const string GroupSettingQuery =
"SELECT Name, MgmtDbServerName, MgmtDbName, SubscriptionDBServerName, SubscriptionDBName FROM MSBTS_GroupSetting";
private readonly MonitoringOptions _options;
/// <summary>
@@ -53,9 +56,14 @@ namespace BizTalkCheckmkPulse
result.Platform.WmiConnected = true;
result.Platform.PlatformDataAvailable = QueryPlatform(scope, result);
result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result);
var applicationIndex = BuildApplicationIndex(scope, result);
result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result);
result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result, server);
var applicationIndex = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result, applicationIndex);
if (_options.EmitPerApplicationSuspensionServices)
{
AddArtifactIndex(scope, result, applicationIndex, "MSBTS_ReceivePort");
}
result.Platform.SuspendedInstancesDataAvailable = QuerySuspendedInstances(scope, result, applicationIndex);
}
@@ -74,7 +82,7 @@ namespace BizTalkCheckmkPulse
}
catch (Exception ex) when (IsExpectedWmiException(ex))
{
AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true);
AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true, null, null);
return false;
}
}
@@ -88,23 +96,19 @@ namespace BizTalkCheckmkPulse
private bool QueryPlatform(ManagementScope scope, ProbeResult result)
{
var groupFound = false;
var groupAvailable = TryQuery(scope, result, "MSBTS_GroupSetting", "SELECT * FROM MSBTS_GroupSetting", item =>
var groupAvailable = TryQuery(scope, result, "MSBTS_GroupSetting", GroupSettingQuery, item =>
{
if (groupFound)
{
return;
}
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);
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"));
}
result.Platform.ManagementDbServer = WmiHelpers.GetString(item, "MgmtDbServerName");
result.Platform.ManagementDbName = WmiHelpers.GetString(item, "MgmtDbName");
result.Platform.MessageBoxDbServer = WmiHelpers.GetString(item, "SubscriptionDBServerName");
result.Platform.MessageBoxDbName = WmiHelpers.GetString(item, "SubscriptionDBName");
}, true);
if (groupAvailable && !groupFound)
@@ -112,32 +116,33 @@ namespace BizTalkCheckmkPulse
AddMissingDataDiagnostic(result, "MSBTS_GroupSetting", "Keine BizTalk-Gruppe gefunden.");
}
if (messageBoxAvailable && !messageBoxFound)
var managementTargetComplete = HasDatabaseTarget(
result.Platform.ManagementDbServer,
result.Platform.ManagementDbName);
if (groupAvailable && groupFound && !managementTargetComplete)
{
AddMissingDataDiagnostic(result, "MSBTS_MessageBoxSetting", "Keine BizTalk-MessageBox-Datenbank gefunden.");
AddMissingDataDiagnostic(
result,
"MSBTS_GroupSetting.ManagementDatabase",
"BizTalk Management-Datenbankziel ist in MSBTS_GroupSetting unvollstaendig.");
}
return groupAvailable && groupFound && messageBoxAvailable && messageBoxFound;
var messageBoxTargetComplete = HasDatabaseTarget(
result.Platform.MessageBoxDbServer,
result.Platform.MessageBoxDbName);
if (groupAvailable && groupFound && !messageBoxTargetComplete)
{
AddMissingDataDiagnostic(
result,
"MSBTS_GroupSetting.SubscriptionDatabase",
"Master-MessageBox-Datenbankziel ist in MSBTS_GroupSetting unvollstaendig.");
}
return groupAvailable && groupFound && managementTargetComplete && messageBoxTargetComplete;
}
/// <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);
AddArtifactIndex(scope, result, index, "MSBTS_SendPort");
AddArtifactIndex(scope, result, index, "MSBTS_ReceivePort");
AddArtifactIndex(scope, result, index, "MSBTS_ReceiveLocation");
AddArtifactIndex(scope, result, index, "MSBTS_Orchestration");
return index;
}
/// <summary>
/// Fuegt die Schluessel einer WMI-Artefaktklasse zum Anwendungsindex hinzu.
/// Fuegt die Schluessel einer optionalen WMI-Artefaktklasse zum Anwendungsindex hinzu.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Ergebnisobjekt fuer optionale Diagnosen.</param>
@@ -147,14 +152,7 @@ namespace BizTalkCheckmkPulse
{
TryQuery(scope, result, className + " application index", "SELECT * FROM " + className, item =>
{
var app = GetApplicationName(item);
foreach (var key in CandidateKeys(item))
{
if (!string.IsNullOrWhiteSpace(key) && !index.ContainsKey(key))
{
index.Add(key, app);
}
}
AddArtifactIndexEntry(index, item);
}, false);
}
@@ -163,16 +161,28 @@ namespace BizTalkCheckmkPulse
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <param name="server">BizTalk-Server, dessen Host Instances bewertet werden.</param>
/// <returns><c>true</c>, wenn die Pflichtabfrage erfolgreich war.</returns>
private bool QueryHostInstances(ManagementScope scope, ProbeResult result)
private bool QueryHostInstances(ManagementScope scope, ProbeResult result, string server)
{
return TryQuery(scope, result, "MSBTS_HostInstance", "SELECT * FROM MSBTS_HostInstance", item =>
return TryQuery(
scope,
result,
"MSBTS_HostInstance",
"SELECT Name, HostName, RunningServer, ServiceState FROM MSBTS_HostInstance",
item =>
{
var runningServer = WmiHelpers.GetString(item, "RunningServer");
if (!string.IsNullOrWhiteSpace(runningServer) && !IsSameServer(runningServer, server))
{
return;
}
result.HostInstances.Add(new HostInstanceState
{
InstanceName = FirstNonEmpty(WmiHelpers.GetString(item, "InstanceName"), WmiHelpers.GetString(item, "Name")),
HostName = WmiHelpers.GetString(item, "HostName"),
RunningServer = WmiHelpers.GetString(item, "RunningServer"),
RunningServer = runningServer,
ServiceState = WmiHelpers.GetInt32(item, "ServiceState", 0)
});
}, true);
@@ -183,13 +193,18 @@ namespace BizTalkCheckmkPulse
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <param name="applicationIndex">Best-Effort-Index fuer Suspensionsdetails.</param>
/// <returns><c>true</c>, wenn alle drei Pflichtabfragen erfolgreich waren.</returns>
private bool QueryApplicationRuntime(ManagementScope scope, ProbeResult result)
private bool QueryApplicationRuntime(
ManagementScope scope,
ProbeResult result,
Dictionary<string, string> applicationIndex)
{
var apps = new Dictionary<string, ApplicationRuntimeState>(StringComparer.OrdinalIgnoreCase);
var receiveLocationsAvailable = TryQuery(scope, result, "MSBTS_ReceiveLocation", "SELECT * FROM MSBTS_ReceiveLocation", item =>
{
AddArtifactIndexEntry(applicationIndex, item);
var app = GetApplication(apps, GetApplicationName(item));
app.ReceiveLocationTotal++;
if (WmiHelpers.GetBoolean(item, "IsDisabled", false))
@@ -200,6 +215,7 @@ namespace BizTalkCheckmkPulse
var sendPortsAvailable = TryQuery(scope, result, "MSBTS_SendPort", "SELECT * FROM MSBTS_SendPort", item =>
{
AddArtifactIndexEntry(applicationIndex, item);
var app = GetApplication(apps, GetApplicationName(item));
app.SendPortTotal++;
switch (WmiHelpers.GetInt32(item, "Status", 0))
@@ -221,6 +237,7 @@ namespace BizTalkCheckmkPulse
var orchestrationsAvailable = TryQuery(scope, result, "MSBTS_Orchestration", "SELECT * FROM MSBTS_Orchestration", item =>
{
AddArtifactIndexEntry(applicationIndex, item);
var app = GetApplication(apps, GetApplicationName(item));
app.OrchestrationTotal++;
switch (WmiHelpers.GetInt32(item, "OrchestrationStatus", 0))
@@ -285,6 +302,7 @@ namespace BizTalkCheckmkPulse
/// <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)
{
var stopwatch = Stopwatch.StartNew();
try
{
ForEachObject(scope, queryText, action);
@@ -292,7 +310,7 @@ namespace BizTalkCheckmkPulse
}
catch (Exception ex) when (IsExpectedWmiException(ex))
{
AddWmiDiagnostic(result, label, ex, required, false);
AddWmiDiagnostic(result, label, ex, required, false, queryText, stopwatch.Elapsed);
return false;
}
}
@@ -344,6 +362,28 @@ namespace BizTalkCheckmkPulse
return app;
}
/// <summary>
/// Nimmt nur tatsaechlich vorhandene Anwendungszuordnungen in den Best-Effort-Index auf.
/// </summary>
/// <param name="index">Zu erweiternder Index.</param>
/// <param name="item">BizTalk-Artefakt aus WMI.</param>
private static void AddArtifactIndexEntry(Dictionary<string, string> index, ManagementBaseObject item)
{
var app = GetApplicationName(item);
if (string.Equals(app, UnknownApplication, StringComparison.OrdinalIgnoreCase))
{
return;
}
foreach (var key in CandidateKeys(item))
{
if (!string.IsNullOrWhiteSpace(key) && !index.ContainsKey(key))
{
index.Add(key, app);
}
}
}
/// <summary>
/// Ermittelt den Anwendungsnamen direkt oder ueber den Artefaktindex.
/// </summary>
@@ -406,6 +446,32 @@ namespace BizTalkCheckmkPulse
return values == null ? null : values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
}
/// <summary>
/// Prueft, ob Server und Datenbank eines SQL-Ziels vorhanden sind.
/// </summary>
private static bool HasDatabaseTarget(string server, string database)
{
return !string.IsNullOrWhiteSpace(server) && !string.IsNullOrWhiteSpace(database);
}
/// <summary>
/// Vergleicht kurzen Namen und FQDN eines BizTalk-Servers ohne WQL-Namensfilter.
/// </summary>
internal static bool IsSameServer(string left, string right)
{
return string.Equals(
NormalizeServerName(left),
NormalizeServerName(right),
StringComparison.OrdinalIgnoreCase);
}
private static string NormalizeServerName(string value)
{
var normalized = (value ?? string.Empty).Trim().TrimStart('\\');
var dotIndex = normalized.IndexOf('.');
return dotIndex > 0 ? normalized.Substring(0, dotIndex) : normalized;
}
/// <summary>
/// Prueft, ob eine Exception aus dem erwarteten WMI-/COM-Fehlerbereich stammt.
/// </summary>
@@ -426,9 +492,29 @@ namespace BizTalkCheckmkPulse
/// <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)
private static void AddWmiDiagnostic(
ProbeResult result,
string component,
Exception exception,
bool required,
bool connectionFailure,
string queryText,
TimeSpan? duration)
{
var category = ClassifyWmiException(exception);
var technicalDetails = exception.GetType().Name
+ " HRESULT=0x" + exception.HResult.ToString("X8")
+ ": " + exception.Message;
if (!string.IsNullOrWhiteSpace(queryText))
{
technicalDetails += " WQL={" + queryText + "}";
}
if (duration.HasValue)
{
technicalDetails += " DurationMs=" + Math.Round(duration.Value.TotalMilliseconds);
}
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Wmi,
@@ -437,7 +523,7 @@ namespace BizTalkCheckmkPulse
Required = required,
Summary = BuildWmiSummary(category, component, connectionFailure),
Action = BuildWmiAction(category, connectionFailure),
TechnicalDetails = exception.GetType().Name + " HRESULT=0x" + exception.HResult.ToString("X8") + ": " + exception.Message
TechnicalDetails = technicalDetails
});
}
@@ -477,12 +563,25 @@ namespace BizTalkCheckmkPulse
}
if (managementException != null
&& (managementException.ErrorCode == ManagementStatus.InvalidNamespace
|| managementException.ErrorCode == ManagementStatus.InvalidClass))
&& managementException.ErrorCode == ManagementStatus.InvalidNamespace)
{
return DiagnosticCategory.Configuration;
}
if (managementException != null
&& (managementException.ErrorCode == ManagementStatus.InvalidClass
|| managementException.ErrorCode == ManagementStatus.InvalidProperty
|| managementException.ErrorCode == ManagementStatus.InvalidQuery
|| managementException.ErrorCode == ManagementStatus.UnparsableQuery))
{
return DiagnosticCategory.Schema;
}
if (managementException != null && managementException.ErrorCode == ManagementStatus.Timedout)
{
return DiagnosticCategory.Timeout;
}
var message = exception.Message ?? string.Empty;
if (ContainsAny(message, "timed out", "timeout", "Zeitlimit"))
{
@@ -513,7 +612,9 @@ namespace BizTalkCheckmkPulse
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.";
: "BizTalk-WMI-Konfiguration fuer " + component + " ist unvollstaendig.";
case DiagnosticCategory.Schema:
return "WMI-Klasse oder WQL fuer " + component + " wird vom installierten BizTalk-Provider nicht unterstuetzt.";
case DiagnosticCategory.Timeout:
return "WMI-Abfrage fuer " + component + " hat das konfigurierte Zeitlimit ueberschritten.";
case DiagnosticCategory.Connectivity:
@@ -539,6 +640,8 @@ namespace BizTalkCheckmkPulse
: "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.Schema:
return "Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema und die Microsoft-Dokumentation pruefen. Rechteerhoehung behebt InvalidClass/InvalidQuery nicht.";
case DiagnosticCategory.Timeout:
return "WMI-Dienst, BizTalk-/SQL-Auslastung und Event Logs pruefen; Timeout erst danach erhoehen.";
case DiagnosticCategory.Connectivity: