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
+58 -9
View File
@@ -91,15 +91,16 @@ biztalk_checkmk_pulse.cmd
v
BizTalkCheckmkPulse.exe
|
| liest lokal
| liest lokal und prueft im gleichen Sicherheitskontext
+-- WMI root\MicrosoftBizTalkServer
+-- Windows Application Event Log
+-- SQL-Verbindung zu BizTalkMgmtDb/BizTalkMsgBoxDb
|
v
Checkmk Local Check Zeilen nach STDOUT
```
Der `.cmd`-Wrapper liefert auch dann eine gueltige `UNKNOWN`-Zeile, wenn die EXE fehlt. Die EXE selbst faengt Laufzeitfehler ab und schreibt fuer alle stabilen Services `UNKNOWN`, damit unvollstaendige Deployments oder WMI-Probleme in Checkmk sichtbar bleiben.
Der `.cmd`-Wrapper liefert auch dann eine gueltige `UNKNOWN`-Zeile mit Massnahme, wenn die EXE fehlt oder bereits der Prozessstart mit einem Exitcode fehlschlaegt. Die EXE selbst faengt Laufzeitfehler ab und schreibt fuer alle stabilen Services `UNKNOWN`, damit unvollstaendige Deployments oder WMI-Probleme in Checkmk sichtbar bleiben.
## Datenquellen
@@ -136,6 +137,14 @@ Der Check liest standardmaessig das lokale Application Log fuer die letzten 60 M
Die Liste ist ueber `EventLogSources` konfigurierbar.
### SQL-Zugriffsprobe
Nach erfolgreicher oder teilweise erfolgreicher Plattformabfrage uebernimmt der SQL-Probe die per WMI ermittelten Management- und MessageBox-Ziele. Fuer jedes eindeutige Ziel wird mit `System.Data.SqlClient` eine Verbindung mit integrierter Windows-Authentifizierung geoeffnet und `SELECT 1` ausgefuehrt. Die Verbindung wird unmittelbar danach geschlossen; es werden keine BizTalk-Tabellen gelesen oder veraendert.
Der Test laeuft unter derselben Identitaet wie der Checkmk Local Check. Damit wird sichtbar, ob `LocalSystem` beziehungsweise das Maschinenkonto des BizTalk-Servers das SQL-Ziel tatsaechlich erreichen und die Datenbank oeffnen kann. Die Probe ist mit `ProbeSqlConnectivity=false` deaktivierbar und verwendet `SqlConnectionTimeoutSeconds` mit dem Default 5 Sekunden je Ziel als Timeout.
Im produktiven Betrieb sollte der komplette Local Check asynchron mit 300 Sekunden Cache ausgefuehrt werden. So fuehren fehlende SQL-Rechte nicht bei jedem Checkmk-Abruf zu weiteren fehlgeschlagenen Login-Ereignissen. Der Trade-off ist eine Zustandsverzoegerung von maximal fuenf Minuten.
## Berechtigungsmodell
### LocalSystem und lokaler WMI-Zugriff
@@ -182,6 +191,23 @@ Die Operator-Rolle ist fuer grundlegendes Monitoring und Zustandsabfragen vorges
| Nur Event-Log-Service ist `UNKNOWN` | Lokalen Application-Log-Zugriff pruefen. |
| Fehler bleibt mit Operator-Rolle bestehen | Betroffene Klasse und konkrete BizTalk-/SQL-Rollenanforderung untersuchen. |
### Automatische Diagnose im Programm
WMI-Verbindungsaufbau und jede erforderliche WMI-Klasse werden separat bewertet. Erwartbare Exceptions werden in folgende Kategorien eingeordnet:
| Diagnose | Bedeutung | Ausgegebene Massnahme |
| --- | --- | --- |
| `Wmi/Permission` | Namespace oder BizTalk-WMI-Klasse verweigert den Zugriff. | Namespace-ACL bei Verbindungsfehlern beziehungsweise BizTalk-Operator-Gruppe bei Klassenfehlern pruefen. |
| `Wmi/Connectivity` | WMI-/RPC-Ziel nicht erreichbar. | WMI-Dienst, Provider und bei Remote-WMI zusaetzlich DNS/RPC/Firewall pruefen. |
| `Wmi/Timeout` | WMI-Abfrage ueberschreitet `QueryTimeoutSeconds`. | WMI-, BizTalk- und SQL-Auslastung untersuchen, bevor der Timeout erhoeht wird. |
| `Wmi/Configuration` | Namespace oder Klasse fehlt. | BizTalk-WMI-Provider und BizTalk-Installation pruefen beziehungsweise reparieren. |
| `Sql/Permission` | Login oder Datenbankzugriff wird abgelehnt. | Maschinenkonto der BizTalk-Operator-Gruppe zuordnen, Kerberos erneuern, keine direkten DB-Rollen vergeben. |
| `Sql/Connectivity` | SQL-Server oder Instanz nicht erreichbar. | Servername, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall pruefen. |
| `Sql/Timeout` | SQL-Verbindung oder Testabfrage laeuft in den Timeout. | Netzwerk und SQL-Auslastung pruefen; Timeout nur begruendet anheben. |
| `Sql/Configuration` | WMI-Ziele unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. | Plattformabfragen beziehungsweise Zertifikatskette, Verschluesselung, SPN und Kerberos gezielt pruefen. |
Jede Diagnose enthaelt Bereich/Kategorie, betroffene Komponente, eine kurze Ursache, `Massnahme:` und `Technik:` mit Exception-Typ, HRESULT oder SQL-Fehlernummer. Erforderliche Datenquellen besitzen eigene Verfuegbarkeitsflags. Schlaegt beispielsweise `MSBTS_ServiceInstance` fehl, wird `BizTalk Suspended Instances` zwingend `UNKNOWN`; eine leere Ergebnisliste darf nicht als 'keine Suspensions' fehlinterpretiert werden.
Die Operator-Mitgliedschaft des Computerkontos steht allen auf diesem Server als `LocalSystem` laufenden Diensten fuer Netzwerkzugriffe zur Verfuegung. Falls diese Sicherheitsauswirkung nicht akzeptabel ist, kann ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten Checkmk-Spooldaten erzeugen. Diese Variante ist noch nicht Bestandteil der aktuellen Implementierung. Der komplette Checkmk-Agent sollte nicht allein fuer dieses Plugin auf eine andere Identitaet umgestellt werden, weil dies alle Agent-Sektionen und Local Checks betrifft.
Quellen:
@@ -198,6 +224,10 @@ Quellen:
Das Plugin ist bewusst defensiv gebaut:
- WMI-Queries haben ein konfigurierbares Timeout.
- WMI-Namespace und Pflichtklassen werden getrennt auf Berechtigung, Konfiguration, Erreichbarkeit und Timeout geprueft.
- Management- und MessageBox-Datenbank werden mit der echten Agent-Identitaet getestet.
- SQL-Fehlernummern werden in Berechtigung, Erreichbarkeit, Timeout oder Providerfehler klassifiziert.
- Fehlgeschlagene Pflichtabfragen erzeugen `UNKNOWN` statt irrefuehrender Nullwerte.
- Optionale WMI-Klassen erzeugen Diagnosehinweise statt Totalabbruch.
- Fehlende Properties werden als leer/unknown behandelt.
- Local-Check-Ausgaben verwenden gueltige Checkmk-Zeilen mit genau vier Feldern.
@@ -214,6 +244,14 @@ Das Plugin ist bewusst defensiv gebaut:
Dieser Service ist der Integrationsindikator. Wenn er `UNKNOWN` ist, sind Berechtigungen, BizTalk-Installation oder WMI-Repository zu pruefen.
### BizTalk SQL Access
- `OK`: Datenbankziele vollstaendig ermittelt und integrierte Anmeldung an allen Zielen erfolgreich.
- `UNKNOWN`: Zielermittlung unvollstaendig, Anmeldung verweigert, SQL nicht erreichbar, Timeout oder Providerfehler.
- Bei `ProbeSqlConnectivity=false`: `OK` mit sichtbarem Hinweis, dass die Probe deaktiviert ist.
Metriken: `biztalk_sql_targets_total`, `biztalk_sql_targets_available` und `biztalk_sql_targets_failed`. Die Ausgabe nennt ausserdem `execution_identity`, `network_identity` und den Zustand jedes getesteten Datenbankziels.
### BizTalk Suspended Instances
- `OK`: keine suspendierten Instanzen.
@@ -233,6 +271,7 @@ Dieser Service ist der Integrationsindikator. Wenn er `UNKNOWN` ist, sind Berech
- `OK`: Artefakte lesbar, keine unbekannten Statuswerte.
- `WARN`: nur wenn `AlertOnArtifactRuntimeIssues=true` und deaktivierte/inaktive Artefakte vorhanden sind.
- `CRIT`: unbekannte Send-Port- oder Orchestration-Statuswerte.
- `UNKNOWN`: mindestens eine erforderliche WMI-Artefaktklasse nicht lesbar.
Deaktivierte Receive Locations und gestoppte Ports koennen in BizTalk fachlich korrekt sein. Deshalb ist die Alarmierung hier standardmaessig informativ.
@@ -247,19 +286,20 @@ Deaktivierte Receive Locations und gestoppte Ports koennen in BizTalk fachlich k
Pro Umgebung sollte der BizTalk-Host in einem eigenen Host-Ordner oder Host-Tag fuer `ACC`, `DEV`, `TST`, `PRD` liegen. Im Dashboard eignen sich:
- Service State Widgets fuer die fuenf BizTalk-Services
- Graphen fuer `biztalk_suspended_total`, `biztalk_host_instances_stopped`, `biztalk_eventlog_errors`
- Service State Widgets fuer die sechs stabilen BizTalk-Services
- Graphen fuer `biztalk_sql_targets_failed`, `biztalk_suspended_total`, `biztalk_host_instances_stopped`, `biztalk_eventlog_errors`
- Hostgruppe/Ordner pro Umgebung
- Optional eine View gefiltert auf `Service starts with BizTalk`
Empfohlene Reihenfolge im Dashboard:
1. `BizTalk Platform`
2. `BizTalk Suspended Instances`
3. `BizTalk Host Instances`
4. `BizTalk Event Log`
5. `BizTalk Runtime Artifacts`
6. SQL-Server-Services aus dem Checkmk-MSSQL-Plugin
2. `BizTalk SQL Access`
3. `BizTalk Suspended Instances`
4. `BizTalk Host Instances`
5. `BizTalk Event Log`
6. `BizTalk Runtime Artifacts`
7. SQL-Server-Services aus dem Checkmk-MSSQL-Plugin
## Rollout-Vorgehen
@@ -295,6 +335,15 @@ Der Checkmk Windows Agent laeuft normalerweise als LocalSystem. Daher immer auch
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
```
Die Diagnose im Service-Text nach `Massnahme:` abarbeiten. Wichtige Kategorien:
- `Wmi/Permission`: lokale Namespace-ACL oder BizTalk-Operator-Zuordnung pruefen.
- `Sql/Permission`: Maschinenkonto `DOMAIN\BIZTALKSERVER$` und BizTalk-Operator-Gruppe pruefen.
- `Sql/Connectivity`: SQL-Server-/Instanzname, DNS, SQL-Dienst, TCP und Firewall pruefen.
- `Sql/Timeout`: SQL- und Netzwerkauslastung pruefen; Timeout nicht als erste Massnahme erhoehen.
Der direkte Aufruf des Wrappers kann wegen des angemeldeten Administratorkontos ein anderes Ergebnis liefern als der Agent-Dump. Fuer die Freigabe ist immer der Agent-Dump massgeblich.
### Keine Services in Discovery
Pruefen:
+31 -1
View File
@@ -19,6 +19,7 @@ Checkmk 2.4 kann Windows-Agent-Plugins und Local Checks direkt ausfuehren. Fuer
Standardmaessig entstehen diese stabilen Services:
- `BizTalk Platform`
- `BizTalk SQL Access`
- `BizTalk Suspended Instances`
- `BizTalk Host Instances`
- `BizTalk Runtime Artifacts`
@@ -112,6 +113,8 @@ Wichtige Werte:
| `ServicePrefix` | `BizTalk` | Prefix fuer alle Checkmk-Services. |
| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST` oder `PRD`. |
| `QueryTimeoutSeconds` | `25` | WMI-Timeout pro Query. |
| `ProbeSqlConnectivity` | `true` | Prueft die integrierte Windows-Anmeldung an den per WMI ermittelten Management- und MessageBox-Datenbanken. |
| `SqlConnectionTimeoutSeconds` | `5` | Timeout je SQL-Ziel fuer Verbindungsaufbau und harmlose Testabfrage. |
| `WarnResumableThreshold` | `1` | WARN ab n resumable suspended instances. |
| `CritNonResumableThreshold` | `1` | CRIT ab n non-resumable suspended instances. |
| `AlertOnArtifactRuntimeIssues` | `false` | Wenn `true`, werden deaktivierte Receive Locations und inaktive Ports/Orchestrations als WARN gewertet. |
@@ -194,7 +197,31 @@ Der verbindliche Test erfolgt ueber den Agent Controller:
Select-String -Pattern "BizTalk|Access denied|Unauthorized|UNKNOWN" -Context 0,1
```
Erwartet werden die fuenf `BizTalk ...` Services mit plausiblen Daten. Insbesondere `BizTalk Platform` darf nicht wegen eines WMI- oder Berechtigungsfehlers `UNKNOWN` sein.
Erwartet werden die sechs stabilen `BizTalk ...` Services mit plausiblen Daten. Insbesondere `BizTalk Platform` und `BizTalk SQL Access` duerfen nicht wegen eines WMI-, SQL- oder Berechtigungsfehlers `UNKNOWN` sein.
Das Programm prueft Berechtigungen aktiv:
- Der WMI-Namespace-Verbindungsaufbau und jede erforderliche WMI-Klasse werden getrennt bewertet.
- Fehler werden als `Permission`, `Connectivity`, `Timeout`, `Configuration` oder `Provider` klassifiziert.
- `BizTalk SQL Access` oeffnet mit integrierter Windows-Authentifizierung eine Verbindung zur ermittelten Management- und MessageBox-Datenbank und fuehrt `SELECT 1` aus.
- Die Service-Ausgabe nennt Ausfuehrungsidentitaet, erwartete Netzwerkidentitaet, betroffene Komponente, technische Ursache und konkrete Massnahme.
- Fehlgeschlagene Pflichtabfragen werden nie als leerer, erfolgreicher Datenbestand gewertet. Der betroffene Service wird `UNKNOWN`.
`Sql/Configuration` weist je nach Detailtext entweder auf eine unvollstaendige WMI-Zielermittlung oder auf TLS-, Zertifikats-, SPN-/SSPI-Probleme hin. Die Diagnose empfiehlt bewusst nicht, SQL-Verschluesselung pauschal abzuschalten.
Der SQL-Zugriffstest ersetzt nicht das Checkmk-MSSQL-Plugin auf dem SQL-Server. Er prueft ausschliesslich, ob genau die Identitaet des BizTalk Local Checks die fuer seine Diagnose benoetigten BizTalk-Datenbankziele erreichen kann.
Da bei fehlenden Rechten jeder Lauf einen abgewiesenen SQL-Login erzeugen kann, wird fuer den produktiven Betrieb eine asynchrone Ausfuehrung mit 300 Sekunden Cache empfohlen. Das begrenzt SQL-Logeintraege und Last, verzoegert einen Zustandswechsel aber um maximal fuenf Minuten. Beispiel in `%ProgramData%\checkmk\agent\check_mk.user.yml`:
```yaml
local:
enabled: yes
execution:
- pattern: $CUSTOM_LOCAL_PATH$\biztalk_checkmk_pulse.cmd
async: yes
run: yes
cache_age: 300
```
### Vorgehen bei Berechtigungsfehlern
@@ -213,6 +240,8 @@ Entscheidungsmatrix:
| --- | --- |
| Alle BizTalk-Services liefern plausible Daten | Keine Berechtigungsaenderung erforderlich. |
| Verbindung zu `root\MicrosoftBizTalkServer` scheitert | Namespace, BizTalk-WMI-Provider, WMI-Dienst und Namespace-ACL gezielt pruefen. Keine pauschalen WMI-Rechte vergeben. |
| `BizTalk SQL Access` meldet `Sql/Permission` | Computerkonto `DOMAIN\BIZTALKSERVER$` in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und erneut testen. Keine direkten BizTalk-DB-Rollen vergeben. |
| `BizTalk SQL Access` meldet `Sql/Connectivity` oder `Sql/Timeout` | Server-/Instanzname, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall aus Sicht des BizTalk-Servers pruefen. |
| Nur SQL-gestuetzte BizTalk-Klassen liefern `Access denied` oder `UNKNOWN` | Computerkonto `DOMAIN\BIZTALKSERVER$` zunaechst in `BizTalk Server Operators` aufnehmen. |
| `BizTalk Event Log` ist `UNKNOWN` | Zugriff auf das lokale Application Log pruefen; `LocalSystem` kann es normalerweise lesen. |
| Fehler bleibt trotz Operator-Rolle bestehen | Betroffene WMI-Klasse und BizTalk-/SQL-Rollenzuordnung mit den Fachadministratoren untersuchen. |
@@ -226,6 +255,7 @@ Falls diese Freigabe nicht zulaessig ist, ist die sauberere Alternative ein sepa
Empfohlene Alarmierung:
- `BizTalk Platform`: `UNKNOWN` immer untersuchen, da dann WMI, Rechte oder Deployment betroffen sind.
- `BizTalk SQL Access`: `UNKNOWN` als Integrations-, Berechtigungs- oder Verbindungsproblem behandeln und die eingebettete Massnahme abarbeiten.
- `BizTalk Suspended Instances`: in `PRD` direkt alarmieren; in Nicht-PRD nach Betriebsbedarf.
- `BizTalk Host Instances`: `CRIT` alarmieren, weil gestoppte Host Instances Laufzeitverarbeitung verhindern koennen.
- `BizTalk Runtime Artifacts`: zunaechst beobachten; strengere Alarmierung erst aktivieren, wenn deaktivierte Artefakte nicht fachlich gewollt sind.
+5 -2
View File
@@ -5,10 +5,13 @@ set "BASE=%~dp0BizTalkCheckmkPulse"
set "EXE=%BASE%\BizTalkCheckmkPulse.exe"
if not exist "%EXE%" (
echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe not found below %BASE%
echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen und danach cmk-agent-ctl.exe dump erneut ausfuehren.
exit /b 0
)
"%EXE%" %*
set "EXITCODE=%ERRORLEVEL%"
if not "%EXITCODE%"=="0" (
echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe konnte nicht erfolgreich gestartet werden, Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, EXE/Config-Dateien und Windows Application Log pruefen.
)
exit /b 0
+41 -5
View File
@@ -19,13 +19,50 @@ Status:
| Status | Bedingung |
| --- | --- |
| `OK` | WMI-Verbindung erfolgreich. |
| `UNKNOWN` | WMI-Verbindung oder Programmstart fehlgeschlagen. |
| `OK` | WMI-Verbindung und beide Plattform-Pflichtklassen erfolgreich. |
| `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, `MSBTS_MessageBoxSetting` oder Programmstart fehlgeschlagen. |
Metriken:
- keine
## BizTalk SQL Access
Zweck:
- Ermittelt Management- und MessageBox-Datenbank ueber BizTalk-WMI.
- Oeffnet mit der Windows-Identitaet des Checkmk-Agentprozesses eine integrierte SQL-Verbindung.
- Fuehrt die harmlose Abfrage `SELECT 1` aus und schliesst die Verbindung sofort wieder.
- Zeigt lokale Ausfuehrungsidentitaet und erwartete Netzwerkidentitaet an.
Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prueft den fuer das BizTalk-Monitoring relevanten Zugriff des Local Checks.
Status:
| Status | Bedingung |
| --- | --- |
| `OK` | WMI-Zielermittlung vollstaendig und alle ermittelten Datenbanken erreichbar. |
| `UNKNOWN` | Zielermittlung unvollstaendig, Anmeldung verweigert, SQL nicht erreichbar, Timeout oder Providerfehler. |
| `OK` mit Hinweis | Nur wenn `ProbeSqlConnectivity=false`; der Test ist dann bewusst deaktiviert. |
Fehlerkategorien:
| Kategorie | Typische Bedeutung |
| --- | --- |
| `Sql/Permission` | Login abgelehnt, Datenbank nicht oeffenbar oder fehlende Berechtigung. |
| `Sql/Connectivity` | DNS, SQL-Dienst, Instanz, TCP-Port oder Firewall nicht erreichbar. |
| `Sql/Timeout` | Verbindungsaufbau oder Testabfrage ueberschreitet das konfigurierte Zeitlimit. |
| `Sql/Provider` | Sonstiger von SQL Server oder SqlClient gemeldeter Providerfehler. |
| `Sql/Configuration` | WMI-Ziele unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. |
Metriken:
| Metrik | Bedeutung |
| --- | --- |
| `biztalk_sql_targets_total` | Anzahl eindeutiger Management-/MessageBox-Ziele. |
| `biztalk_sql_targets_available` | Erfolgreich getestete SQL-Ziele. |
| `biztalk_sql_targets_failed` | Fehlgeschlagene SQL-Ziele. |
## BizTalk Suspended Instances
Zweck:
@@ -41,7 +78,7 @@ Status:
| `OK` | keine suspendierten Instanzen. |
| `WARN` | `biztalk_suspended_resumable >= WarnResumableThreshold`. |
| `CRIT` | `biztalk_suspended_nonresumable >= CritNonResumableThreshold`. |
| `UNKNOWN` | WMI oder Programmfehler. |
| `UNKNOWN` | `MSBTS_ServiceInstance` nicht verlaesslich lesbar oder Programmfehler. |
Metriken:
@@ -91,7 +128,7 @@ Status:
| `OK` | Artefakte lesbar; keine unbekannten Statuswerte. |
| `WARN` | Nur bei `AlertOnArtifactRuntimeIssues=true` und inaktiven/deaktivierten Artefakten. |
| `CRIT` | Unbekannte Send-Port- oder Orchestration-Statuswerte. |
| `UNKNOWN` | Programmfehler. |
| `UNKNOWN` | Mindestens eine erforderliche Artefaktklasse nicht lesbar oder Programmfehler. |
Metriken:
@@ -141,4 +178,3 @@ BizTalk Suspended <ApplicationName>
```
Das ist fuer Anwendungsteams hilfreich, kann aber die Service-Anzahl erhoehen. Fuer den Basisausbau wird empfohlen, diese Option deaktiviert zu lassen.
+16 -3
View File
@@ -4,13 +4,15 @@
```text
0 "BizTalk Platform" - Self test OK. Checkmk local check output is valid.
0 "BizTalk SQL Access" biztalk_sql_targets_total=0;;;0 Self test OK. No SQL connection was opened.
0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0 Self test OK.
```
## Gesunder BizTalk-Server
```text
0 "BizTalk Platform" - BizTalk WMI reachable, server=BIZTALK01, group=BizTalkMgmtDb, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb
0 "BizTalk Platform" - BizTalk WMI and platform data reachable, server=BIZTALK01, group=BizTalkMgmtDb, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb
0 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=2;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=2, available=2, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=<DOMAIN>\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:OK, MessageBox=SQL01\BizTalkMsgBoxDb:OK
0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|biztalk_suspended_resumable=0;1;;0|biztalk_suspended_nonresumable=0;;1;0 No suspended BizTalk service instances found.
0 "BizTalk Host Instances" biztalk_host_instances_total=4;;;0|biztalk_host_instances_started=4;;;0|biztalk_host_instances_stopped=0;;1;0|biztalk_host_instances_pending=0;1;;0|biztalk_host_instances_unknown=0;;1;0 Host instances total=4, started=4, stopped=0, pending=0, unknown=0
0 "BizTalk Runtime Artifacts" biztalk_applications=8;;;0|biztalk_receive_locations=24;;;0|biztalk_receive_locations_disabled=0;1;;0|biztalk_send_ports=31;;;0|biztalk_send_ports_started=31;;;0|biztalk_send_ports_inactive=0;1;;0|biztalk_send_ports_unknown=0;;1;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=12;;;0|biztalk_orchestrations_inactive=0;1;;0|biztalk_orchestrations_unknown=0;;1;0 Applications=8; receive_locations total=24, disabled=0; send_ports total=31, started=31, inactive=0, unknown=0; orchestrations total=12, started=12, inactive=0, unknown=0
@@ -23,9 +25,20 @@
2 "BizTalk Suspended Instances" biztalk_suspended_total=3;;;0|biztalk_suspended_resumable=2;1;;0|biztalk_suspended_nonresumable=1;;1;0 3 suspended BizTalk service instance(s): resumable=2, nonresumable=1; applications=OrderProcessing(R=2, NR=1); examples=Send.Invoice on ProcessingHost
```
## SQL-Berechtigungsfehler
```text
3 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=0;;;0|biztalk_sql_targets_failed=2;;1;0 SQL access targets=2, available=0, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=<DOMAIN>\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:FAILED, MessageBox=SQL01\BizTalkMsgBoxDb:FAILED; diagnostics: Sql/Permission [Management SQL01\BizTalkMgmtDb]: SQL Server hat die integrierte Anmeldung abgelehnt. Erwartete Netzwerkidentitaet: <DOMAIN>\BIZTALK01$. Massnahme: <DOMAIN>\BIZTALK01$ in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen. Technik: SqlException Number=18456: Login failed for user
```
## WMI-Berechtigungsfehler
```text
3 "BizTalk Suspended Instances" - Suspendierte Instanzen konnten nicht verlaesslich gelesen werden. Ein leerer Datenbestand wird nicht als OK gewertet.; diagnostics: Wmi/Permission [MSBTS_ServiceInstance]: Zugriff auf MSBTS_ServiceInstance wurde im LocalSystem-Kontext verweigert. Massnahme: Agent-Dump pruefen und Computerkonto <DOMAIN>\BIZTALK01$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen. Technik: UnauthorizedAccessException HRESULT=0x80070005: Access is denied
```
## Fehlendes Deployment
```text
3 "BizTalk Platform" - BizTalkCheckmkPulse.exe not found below C:\ProgramData\checkmk\agent\local\BizTalkCheckmkPulse
3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter C:\ProgramData\checkmk\agent\local\BizTalkCheckmkPulse. Massnahme: Deployment-Paket und Dateirechte pruefen und danach cmk-agent-ctl.exe dump erneut ausfuehren.
```
+23 -2
View File
@@ -51,6 +51,7 @@ Empfohlene Service-Behandlung:
| Service | Empfehlung |
| --- | --- |
| `BizTalk Platform` | `UNKNOWN` immer als Integrationsproblem behandeln. |
| `BizTalk SQL Access` | `UNKNOWN` anhand der Kategorie `Permission`, `Connectivity`, `Timeout`, `Configuration` oder `Provider` bearbeiten. |
| `BizTalk Suspended Instances` | In `PRD` alarmieren; in `ACC`/`TST`/`DEV` nach Teamvereinbarung. |
| `BizTalk Host Instances` | `CRIT` alarmieren. |
| `BizTalk Runtime Artifacts` | Erst beobachten; strenge Alarmierung nur bei klar definiertem Runtime-Sollzustand. |
@@ -94,7 +95,23 @@ Der direkte Start des Wrappers in einer administrativen Eingabeaufforderung ist
Select-String -Pattern "BizTalk|Access denied|Unauthorized|UNKNOWN" -Context 0,1
```
Die Ausgabe muss die fuenf `BizTalk ...` Services mit plausiblen Daten enthalten. `Access denied`, `UnauthorizedAccessException` und ein berechtigungsbedingtes `UNKNOWN` weisen auf eine fehlende Rollenzuordnung hin.
Die Ausgabe muss die sechs stabilen `BizTalk ...` Services mit plausiblen Daten enthalten. `Access denied`, `UnauthorizedAccessException`, `Sql/Permission` und ein berechtigungsbedingtes `UNKNOWN` weisen auf eine fehlende Rollenzuordnung hin.
Die EXE klassifiziert WMI- und SQL-Probleme und schreibt zu jeder Diagnose `Massnahme:` und `Technik:`. Eine fehlgeschlagene erforderliche WMI-Abfrage erzeugt beim betroffenen Service immer `UNKNOWN`; fehlende Daten werden nicht als Nullbestand und damit nicht als `OK` ausgegeben.
Fuer den produktiven Betrieb wird empfohlen, den Local Check alle 300 Sekunden asynchron auszufuehren. Dadurch erzeugen fehlende SQL-Rechte nicht bei jedem Agent-Abruf neue fehlgeschlagene Logins. In `%ProgramData%\checkmk\agent\check_mk.user.yml`:
```yaml
local:
enabled: yes
execution:
- pattern: $CUSTOM_LOCAL_PATH$\biztalk_checkmk_pulse.cmd
async: yes
run: yes
cache_age: 300
```
Alternativ koennen die Checkmk-Kollegen in der Agent Bakery die Regeln `Set execution mode for plug-ins and local checks` und `Set cache age for plug-ins and local checks` verwenden. Der Cache reduziert Last und SQL-Fehlerlogs; ein Zustandswechsel wird dadurch um maximal die konfigurierte Cache-Zeit verzoegert.
### Least-Privilege-Vorgehen
@@ -111,8 +128,11 @@ Entscheidungsmatrix:
| Beobachtung | Bewertung und Massnahme |
| --- | --- |
| Alle fuenf Services liefern plausible Werte | Keine Aenderung erforderlich. |
| Alle sechs stabilen Services liefern plausible Werte | Keine Aenderung erforderlich. |
| `root\MicrosoftBizTalkServer` ist nicht erreichbar | BizTalk-WMI-Provider, WMI-Dienst, Namespace und dessen ACL gezielt pruefen. |
| `BizTalk SQL Access` meldet `Sql/Permission` | Maschinenkonto in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und Agent-Dump wiederholen. |
| `BizTalk SQL Access` meldet `Sql/Connectivity` | SQL-Server-/Instanzname, DNS, Dienst, TCP-Port und Firewall pruefen. |
| `BizTalk SQL Access` meldet `Sql/Timeout` | SQL-/Netzwerkauslastung untersuchen; Timeout nur nach Ursachenanalyse anpassen. |
| Verbindung funktioniert, einzelne SQL-gestuetzte Klassen melden Zugriffsfehler | `DOMAIN\BIZTALKSERVER$` zunaechst in die konfigurierte BizTalk-Operator-Gruppe aufnehmen. |
| Nur `BizTalk Event Log` ist `UNKNOWN` | Lokalen Zugriff auf das Application Event Log pruefen. |
| Fehler bleibt nach Operator-Zuweisung bestehen | Exakte WMI-Klasse anhand der Service-Ausgabe bestimmen und deren BizTalk-/SQL-Rollenanforderung pruefen. |
@@ -152,6 +172,7 @@ Dashboard-Kacheln:
- Host/Service state fuer BizTalk-Host
- Service state fuer `BizTalk Suspended Instances`
- Service state fuer `BizTalk SQL Access`
- Graph `biztalk_suspended_total`
- Graph `biztalk_host_instances_stopped`
- Graph `biztalk_eventlog_errors`
+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>
+229 -21
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;
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);
}
return 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);
}
}
}
+309 -35
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;
QueryPlatform(scope, result);
QueryHostInstances(scope, result);
var applicationIndex = BuildApplicationIndex(scope, result);
QueryApplicationRuntime(scope, result);
QuerySuspendedInstances(scope, result, applicationIndex);
if (!TryConnect(scope, result))
{
return;
}
private void QueryPlatform(ManagementScope scope, ProbeResult result)
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.SuspendedInstancesDataAvailable = QuerySuspendedInstances(scope, result, applicationIndex);
}
/// <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
}
}
}