diff --git a/Dokumentation.md b/Dokumentation.md index ea576bf..58bdce1 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -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: diff --git a/README.md b/README.md index bcee680..1f167e6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/deployment/checkmk/biztalk_checkmk_pulse.cmd b/deployment/checkmk/biztalk_checkmk_pulse.cmd index 0205c59..8f3af51 100644 --- a/deployment/checkmk/biztalk_checkmk_pulse.cmd +++ b/deployment/checkmk/biztalk_checkmk_pulse.cmd @@ -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 - diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md index 342bb84..e346fa9 100644 --- a/docs/CheckmkServices.md +++ b/docs/CheckmkServices.md @@ -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 ``` Das ist fuer Anwendungsteams hilfreich, kann aber die Service-Anzahl erhoehen. Fuer den Basisausbau wird empfohlen, diese Option deaktiviert zu lassen. - diff --git a/docs/ExampleOutput.md b/docs/ExampleOutput.md index 493288d..3a00e4a 100644 --- a/docs/ExampleOutput.md +++ b/docs/ExampleOutput.md @@ -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=\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=\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: \BIZTALK01$. Massnahme: \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 \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. ``` - diff --git a/docs/Integration.md b/docs/Integration.md index 507075b..4632e83 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -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` diff --git a/src/BizTalkCheckmkPulse/App.config b/src/BizTalkCheckmkPulse/App.config index 03970a9..2f701d6 100644 --- a/src/BizTalkCheckmkPulse/App.config +++ b/src/BizTalkCheckmkPulse/App.config @@ -1,17 +1,20 @@ - + + + + - + @@ -25,4 +28,3 @@ - diff --git a/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj index d514d37..71a4534 100644 --- a/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj +++ b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj @@ -36,6 +36,7 @@ + @@ -44,6 +45,7 @@ + @@ -52,4 +54,3 @@ - diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs index f45e2bd..92cfe16 100644 --- a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs +++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs @@ -6,6 +6,9 @@ using System.Text; namespace BizTalkCheckmkPulse { + /// + /// Formatiert Probe-Ergebnisse als robuste Checkmk-Local-Check-Zeilen. + /// internal sealed class CheckmkLocalFormatter { private const int HostStopped = 1; @@ -18,20 +21,30 @@ namespace BizTalkCheckmkPulse private const int HostUnknown = 8; private readonly MonitoringOptions _options; + /// + /// Initialisiert den Formatter mit Service-Namen und Schwellwerten. + /// + /// Monitoring-Konfiguration. public CheckmkLocalFormatter(MonitoringOptions options) { _options = options; } + /// + /// Formatiert alle stabilen und optionalen Checkmk-Services. + /// + /// Vollstaendiges Ergebnis der Probes. + /// Gueltige Checkmk-Local-Check-Zeilen. public IEnumerable 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 } } + /// + /// Erzeugt minimale Beispielzeilen ohne WMI-, SQL- oder Event-Log-Zugriff. + /// + /// Checkmk-Zeilen zur reinen Format- und Deployment-Pruefung. public IEnumerable 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."); } + /// + /// Erzeugt fuer einen nicht abgefangenen Programmfehler UNKNOWN-Zeilen. + /// + /// Bereits aufbereitete technische Fehlermeldung. + /// UNKNOWN-Zeilen fuer alle stabilen Services. public IEnumerable 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); } + /// + /// Formatiert Erreichbarkeit und Basisdaten des BizTalk-WMI-Providers. + /// + /// Probe-Ergebnis. + /// Checkmk-Zeile fuer BizTalk Platform. 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)); } + /// + /// Formatiert die integrierten Anmeldetests an den BizTalk-Datenbanken. + /// + /// Probe-Ergebnis mit SQL-Zielen und Identitaeten. + /// Checkmk-Zeile fuer BizTalk SQL Access. + 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))); + } + + /// + /// Formatiert Anzahl und Kritikalitaet suspendierter Service Instances. + /// + /// Probe-Ergebnis. + /// Checkmk-Zeile fuer suspendierte Instanzen. 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); } + /// + /// Formatiert den Laufzeitzustand aller BizTalk Host Instances. + /// + /// Probe-Ergebnis. + /// Checkmk-Zeile fuer Host Instances. 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()); } + /// + /// Formatiert aggregierte Receive-Location-, Send-Port- und Orchestration-Zustaende. + /// + /// Probe-Ergebnis. + /// Checkmk-Zeile fuer Runtime Artifacts. 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()); } + /// + /// Formatiert relevante Fehler und Warnungen aus dem Application Event Log. + /// + /// Probe-Ergebnis. + /// Checkmk-Zeile fuer das BizTalk Event Log. 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); } + /// + /// Erzeugt bei aktivierter Option einen Suspensions-Service je Anwendung. + /// + /// Probe-Ergebnis mit suspendierten Instanzen. + /// Checkmk-Zeilen je betroffener BizTalk-Anwendung. private IEnumerable 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); } } + /// + /// Bestimmt den Checkmk-Zustand aus den beiden Suspensionszaehlern. + /// + /// Anzahl fortsetzbarer Instanzen. + /// Anzahl nicht fortsetzbarer Instanzen. + /// Zustand gemaess konfigurierten Schwellwerten. private CheckState DetermineSuspensionState(int resumable, int nonresumable) { if (_options.CritNonResumableThreshold > 0 && nonresumable >= _options.CritNonResumableThreshold) @@ -239,6 +372,14 @@ namespace BizTalkCheckmkPulse return CheckState.Ok; } + /// + /// Erstellt Details mit betroffenen Anwendungen und aktuellen Beispielen. + /// + /// Suspendierte Instanzen. + /// Gesamtanzahl. + /// Anzahl fortsetzbarer Instanzen. + /// Anzahl nicht fortsetzbarer Instanzen. + /// Kompakter Detailtext. private string BuildSuspensionDetail(IEnumerable instances, int total, int resumable, int nonresumable) { var builder = new StringBuilder(); @@ -260,6 +401,14 @@ namespace BizTalkCheckmkPulse return builder.ToString(); } + /// + /// Baut eine syntaktisch gueltige Checkmk-Local-Check-Zeile. + /// + /// Numerischer Checkmk-Zustand. + /// Stabiler Service-Name. + /// Performance-Daten oder Bindestrich. + /// Menschenlesbare Service-Ausgabe. + /// Vollstaendige Local-Check-Zeile. 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 diagnostics) + /// + /// Ergaenzt bis zu drei strukturierte Diagnosen am Detailtext. + /// + /// Vorhandener Service-Detailtext. + /// Relevante Diagnosen. + /// Detailtext mit Ursache, Massnahme und technischen Details. + private static string AppendDiagnostics(string detail, IEnumerable diagnostics) { - var visible = diagnostics == null ? new string[0] : diagnostics.Take(3).ToArray(); - if (visible.Length == 0) - { - return detail; - } - - return detail + "; diagnostics: " + string.Join("; ", visible); + var visible = diagnostics == null + ? new string[0] + : diagnostics.Take(3).Select(x => x.ToDisplayText()).ToArray(); + return visible.Length == 0 + ? detail + : detail + "; diagnostics: " + string.Join("; ", visible); } + /// + /// Filtert Diagnosen nach Bereich und optionalen Komponentenfragmenten. + /// + /// Probe-Ergebnis mit allen Diagnosen. + /// Gesuchter Diagnosebereich. + /// Optionale Fragmente betroffener Komponenten. + /// Passende Diagnosen in Entstehungsreihenfolge. + private static IEnumerable 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))); + } + + /// + /// Haengt eine nicht leere Werteliste an einen Detailtext an. + /// + /// Zu erweiternder Text. + /// Bezeichnung der Liste. + /// Zu formatierende Werte. private static void AppendList(StringBuilder builder, string label, IEnumerable values) { var list = values.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); @@ -289,6 +465,12 @@ namespace BizTalkCheckmkPulse } } + /// + /// Haengt einen optionalen benannten Wert an einen Detailtext an. + /// + /// Zu erweiternder Text. + /// Bezeichnung des Wertes. + /// Optionaler Wert. private static void AppendOptional(StringBuilder builder, string label, string value) { if (!string.IsNullOrWhiteSpace(value)) @@ -297,6 +479,12 @@ namespace BizTalkCheckmkPulse } } + /// + /// Verbindet SQL-Server und Datenbank zu einer kompakten Anzeige. + /// + /// SQL-Server oder Instanz. + /// Datenbankname. + /// Kombination aus Server und Datenbank oder leerer Text. 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); } + /// + /// Uebersetzt den numerischen BizTalk Host ServiceState in einen Namen. + /// + /// Numerischer WMI-Zustand. + /// Menschenlesbarer Zustandsname. private static string HostStateName(int state) { switch (state) @@ -332,16 +525,31 @@ namespace BizTalkCheckmkPulse } } + /// + /// Ersetzt leere Werte durch eine sichtbare Unknown-Markierung. + /// + /// Zu normalisierender Text. + /// Getrimmter Wert oder unknown. private static string EmptyAsUnknown(string value) { return string.IsNullOrWhiteSpace(value) ? "unknown" : value.Trim(); } + /// + /// Entfernt fuer Checkmk problematische Zeichen aus Service-Namen. + /// + /// Urspruenglicher Service-Name. + /// Sicherer Service-Name. private static string SanitizeService(string value) { return SanitizeDetail(value).Replace("\"", "'"); } + /// + /// Normalisiert Detailtext auf eine einzelne Checkmk-kompatible Zeile. + /// + /// Urspruenglicher Detailtext. + /// Einzeiliger, nicht leerer Detailtext. private static string SanitizeDetail(string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/src/BizTalkCheckmkPulse/EventLogProbe.cs b/src/BizTalkCheckmkPulse/EventLogProbe.cs index 717c98e..dc78433 100644 --- a/src/BizTalkCheckmkPulse/EventLogProbe.cs +++ b/src/BizTalkCheckmkPulse/EventLogProbe.cs @@ -4,15 +4,26 @@ using System.Linq; namespace BizTalkCheckmkPulse { + /// + /// Zaehlt BizTalk-bezogene Fehler und Warnungen im lokalen Application Event Log. + /// internal sealed class EventLogProbe { private readonly MonitoringOptions _options; + /// + /// Initialisiert den Event-Log-Probe mit Zeitraum, Quellen und Schwellwerten. + /// + /// Monitoring-Konfiguration. public EventLogProbe(MonitoringOptions options) { _options = options; } + /// + /// Liest das Application Event Log rueckwaerts bis zum Beginn des Zeitfensters. + /// + /// Ergebnisobjekt fuer Zaehler und Fehlerstatus. public void Query(ProbeResult result) { result.EventLog.Since = DateTime.Now.AddMinutes(-_options.EventLogLookbackMinutes); @@ -55,6 +66,11 @@ namespace BizTalkCheckmkPulse } } + /// + /// Prueft, ob eine Event-Log-Quelle zu den konfigurierten BizTalk-Quellen gehoert. + /// + /// Name der Windows Event-Log-Quelle. + /// true, wenn die Quelle ueberwacht werden soll. private bool IsRelevantSource(string source) { if (string.IsNullOrWhiteSpace(source)) @@ -66,4 +82,3 @@ namespace BizTalkCheckmkPulse } } } - diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs index 57dbce4..f684462 100644 --- a/src/BizTalkCheckmkPulse/Models.cs +++ b/src/BizTalkCheckmkPulse/Models.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; namespace BizTalkCheckmkPulse { + /// + /// Definiert die von Checkmk erwarteten numerischen Service-Zustaende. + /// internal enum CheckState { Ok = 0, @@ -11,35 +14,113 @@ namespace BizTalkCheckmkPulse Unknown = 3 } + /// + /// Unterscheidet fortsetzbare und nicht fortsetzbare suspendierte Instanzen. + /// internal enum SuspendedKind { Resumable, NonResumable } + /// + /// Kennzeichnet den technischen Bereich, in dem eine Diagnose entstanden ist. + /// + internal enum DiagnosticArea + { + Wmi, + Sql, + EventLog, + General + } + + /// + /// Klassifiziert Fehler, damit Bediener eine passende Massnahme erhalten. + /// + internal enum DiagnosticCategory + { + Permission, + Connectivity, + Timeout, + Configuration, + Provider, + Unexpected + } + + /// + /// Strukturierte Diagnose mit Ursache und konkreter Handlungsempfehlung. + /// + 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; } + + /// + /// Erstellt eine kompakte, fuer Checkmk geeignete Diagnosezeile. + /// + /// Menschenlesbarer Text mit Kategorie, Ursache und Massnahme. + 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; + } + } + + /// + /// Sammelt alle Ergebnisse eines Programmlaufs. + /// internal sealed class ProbeResult { + /// + /// Initialisiert leere Ergebnislisten und Zustandsobjekte. + /// public ProbeResult() { - Diagnostics = new List(); + Diagnostics = new List(); Platform = new PlatformState(); HostInstances = new List(); SuspendedInstances = new List(); Applications = new List(); + SqlTargets = new List(); EventLog = new EventLogState(); } - public List Diagnostics { get; private set; } + public List Diagnostics { get; private set; } public PlatformState Platform { get; private set; } public List HostInstances { get; private set; } public List SuspendedInstances { get; private set; } public List Applications { get; private set; } + public List SqlTargets { get; private set; } + public string ExecutionIdentity { get; set; } + public string NetworkIdentityHint { get; set; } public EventLogState EventLog { get; private set; } } + /// + /// Beschreibt Erreichbarkeit und Teilverfuegbarkeit der BizTalk-WMI-Daten. + /// 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; } } + /// + /// Ergebnis der integrierten Anmeldung an einer BizTalk-Datenbank. + /// + 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; } + } + + /// + /// Laufzeitzustand einer BizTalk Host Instance. + /// internal sealed class HostInstanceState { public string InstanceName { get; set; } @@ -56,6 +154,9 @@ namespace BizTalkCheckmkPulse public int ServiceState { get; set; } } + /// + /// Relevante Daten einer suspendierten BizTalk Service Instance. + /// internal sealed class SuspendedInstance { public string ApplicationName { get; set; } @@ -68,6 +169,9 @@ namespace BizTalkCheckmkPulse public SuspendedKind Kind { get; set; } } + /// + /// Aggregierte Zustandszaehler einer BizTalk-Anwendung. + /// internal sealed class ApplicationRuntimeState { public string ApplicationName { get; set; } @@ -86,6 +190,9 @@ namespace BizTalkCheckmkPulse public int OrchestrationUnknown { get; set; } } + /// + /// Ergebnis der Auswertung des lokalen Windows Application Event Logs. + /// internal sealed class EventLogState { public bool Available { get; set; } @@ -95,4 +202,3 @@ namespace BizTalkCheckmkPulse public string Failure { get; set; } } } - diff --git a/src/BizTalkCheckmkPulse/MonitoringOptions.cs b/src/BizTalkCheckmkPulse/MonitoringOptions.cs index 7dffb89..e00b9ed 100644 --- a/src/BizTalkCheckmkPulse/MonitoringOptions.cs +++ b/src/BizTalkCheckmkPulse/MonitoringOptions.cs @@ -6,12 +6,17 @@ using System.Linq; namespace BizTalkCheckmkPulse { + /// + /// Enthaelt alle konfigurierbaren Schwellwerte und Probe-Optionen. + /// 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 EventLogSources { get; set; } public bool SelfTest { get; set; } + /// + /// Setzt konservative Standardwerte fuer BizTalk Server 2020. + /// 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" }; } + /// + /// Erzeugt den stabilen Checkmk-Service-Namen mit optionalem Umgebungsnamen. + /// + /// Fachlicher Suffix des Services. + /// Vollstaendiger Service-Name fuer Checkmk. public string ServiceName(string suffix) { var prefix = string.IsNullOrWhiteSpace(EnvironmentName) @@ -50,6 +65,11 @@ namespace BizTalkCheckmkPulse return prefix.Trim() + " " + suffix; } + /// + /// Laedt Optionen aus App.config und ueberschreibt sie anschliessend mit Argumenten. + /// + /// Beim Programmstart uebergebene Kommandozeilenargumente. + /// Validierte Monitoring-Optionen. 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; } + /// + /// Wendet unterstuetzte Kommandozeilenargumente auf die Optionen an. + /// + /// Zu aktualisierende Optionen. + /// Kommandozeilenargumente. 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; + } } } + /// + /// Liest einen getrimmten Konfigurationswert. + /// + /// AppSettings-Sammlung. + /// Name des Konfigurationswertes. + /// Ersatzwert bei fehlendem Eintrag. + /// Konfigurierter oder ersetzter Textwert. private static string ReadString(System.Collections.Specialized.NameValueCollection settings, string key, string fallback) { var value = settings[key]; return value == null ? fallback : value.Trim(); } + /// + /// Liest eine Ganzzahl und begrenzt sie auf den erlaubten Wertebereich. + /// + /// AppSettings-Sammlung. + /// Name des Konfigurationswertes. + /// Ersatzwert bei ungueltiger Eingabe. + /// Kleinster erlaubter Wert. + /// Groesster erlaubter Wert. + /// Validierter Zahlenwert. 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; } + /// + /// Liest einen booleschen Konfigurationswert. + /// + /// AppSettings-Sammlung. + /// Name des Konfigurationswertes. + /// Ersatzwert bei ungueltiger Eingabe. + /// Konfigurierter oder ersetzter Wahrheitswert. private static bool ReadBool(System.Collections.Specialized.NameValueCollection settings, string key, bool fallback) { bool parsed; return bool.TryParse(settings[key], out parsed) ? parsed : fallback; } + /// + /// Zerlegt die konfigurierten Event-Log-Quellen und entfernt Duplikate. + /// + /// Mit Pipe, Semikolon oder Komma getrennte Quellen. + /// Eindeutige Liste relevanter Event-Log-Quellen. private static IReadOnlyList ReadSources(string value) { return (value ?? string.Empty) @@ -140,10 +199,15 @@ namespace BizTalkCheckmkPulse .ToArray(); } + /// + /// Prueft einen Wert ohne Beachtung der Gross-/Kleinschreibung gegen Kandidaten. + /// + /// Zu pruefender Wert. + /// Erlaubte Vergleichswerte. + /// true, wenn ein Kandidat passt. private static bool EqualsAny(string value, params string[] candidates) { return candidates.Any(x => string.Equals(value, x, StringComparison.OrdinalIgnoreCase)); } } } - diff --git a/src/BizTalkCheckmkPulse/Program.cs b/src/BizTalkCheckmkPulse/Program.cs index 8a81dab..157bba7 100644 --- a/src/BizTalkCheckmkPulse/Program.cs +++ b/src/BizTalkCheckmkPulse/Program.cs @@ -4,8 +4,16 @@ using System.Text; namespace BizTalkCheckmkPulse { + /// + /// Einstiegspunkt des Checkmk Local Checks. + /// internal static class Program { + /// + /// Laedt die Konfiguration, fuehrt die Probes aus und schreibt Checkmk-Zeilen nach STDOUT. + /// + /// Kommandozeilenargumente wie --self-test oder --environment. + /// Immer 0, damit Diagnosefehler als UNKNOWN-Service statt als kaputte Agent-Sektion erscheinen. 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 } } } - diff --git a/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs b/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs new file mode 100644 index 0000000..c3cbc21 --- /dev/null +++ b/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs @@ -0,0 +1,293 @@ +using System; +using System.Data.SqlClient; +using System.Linq; +using System.Security.Principal; + +namespace BizTalkCheckmkPulse +{ + /// + /// Prueft mit der aktuellen Windows-Identitaet die Anmeldung an den von WMI + /// ermittelten BizTalk Management- und MessageBox-Datenbanken. + /// + internal sealed class SqlConnectivityProbe + { + private readonly MonitoringOptions _options; + + /// + /// Initialisiert die SQL-Diagnose mit den Monitoring-Optionen. + /// + /// Konfiguration fuer Aktivierung und Timeout. + public SqlConnectivityProbe(MonitoringOptions options) + { + _options = options; + } + + /// + /// Ermittelt die aktuelle Identitaet und prueft alle bekannten SQL-Ziele. + /// + /// Ergebnisobjekt, das um SQL-Zustaende und Diagnosen erweitert wird. + 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); + } + } + + /// + /// Speichert die lokale Prozessidentitaet und einen Hinweis auf die Netzwerkidentitaet. + /// + /// Zu aktualisierendes Ergebnisobjekt. + 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 + ? "\\" + 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 + }); + } + } + + /// + /// Fuegt ein eindeutiges und vollstaendig beschriebenes SQL-Ziel hinzu. + /// + /// Ergebnisobjekt mit der SQL-Zielliste. + /// Fachliche Rolle der Datenbank. + /// SQL-Server oder Instanzname. + /// Name der BizTalk-Datenbank. + 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() + }); + } + + /// + /// Oeffnet eine integrierte SQL-Verbindung und fuehrt eine harmlose Testabfrage aus. + /// + /// Ergebnisobjekt fuer eventuell entstehende Diagnosen. + /// Zu pruefendes SQL-Ziel. + 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 + }); + } + } + + /// + /// Ordnet bekannte SQL-Fehlernummern einer handlungsorientierten Kategorie zu. + /// + /// Von System.Data.SqlClient gelieferter Fehler. + /// Diagnosekategorie fuer Berechtigung, Timeout oder Verbindung. + 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().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().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; + } + + /// + /// Erstellt eine kurze Fehlerursache fuer die Checkmk-Serviceausgabe. + /// + /// Klassifizierte SQL-Fehlerart. + /// Vermutete Identitaet am entfernten SQL Server. + /// Menschenlesbare Fehlerzusammenfassung. + 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."; + } + } + + /// + /// Liefert eine konkrete, zur SQL-Fehlerkategorie passende Handlungsempfehlung. + /// + /// Klassifizierte SQL-Fehlerart. + /// Vermutete Identitaet am entfernten SQL Server. + /// Betroffenes SQL-Ziel. + /// Empfohlener Diagnoseschritt. + 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."; + } + } + + /// + /// Sucht mehrere Teiltexte ohne Beachtung der Gross-/Kleinschreibung. + /// + /// Zu untersuchender Text. + /// Gesuchte Teiltexte. + /// true, wenn mindestens ein Teiltext enthalten ist. + private static bool ContainsAny(string value, params string[] candidates) + { + return candidates.Any(x => value.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0); + } + } +} diff --git a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs index ad41504..dd1480c 100644 --- a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs +++ b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs @@ -2,9 +2,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Management; +using System.Runtime.InteropServices; namespace BizTalkCheckmkPulse { + /// + /// Liest BizTalk Server 2020 ueber den lokalen Microsoft-WMI-Provider aus. + /// 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; + /// + /// Initialisiert den WMI-Probe mit den konfigurierten Timeouts. + /// + /// Monitoring-Konfiguration. public WmiBizTalkProbe(MonitoringOptions options) { _options = options; } + /// + /// Verbindet sich mit dem BizTalk-WMI-Namespace und liest alle Datenbereiche. + /// + /// Ergebnisobjekt fuer Messwerte, Verfuegbarkeiten und Diagnosen. public void Query(ProbeResult result) { var server = string.IsNullOrWhiteSpace(_options.Server) || _options.Server == "." @@ -32,37 +45,87 @@ namespace BizTalkCheckmkPulse : _options.Server.Trim(); result.Platform.ServerName = server; - var scope = new ManagementScope("\\\\" + server + "\\" + NamespacePath); - scope.Connect(); - result.Platform.WmiConnected = true; + if (!TryConnect(scope, result)) + { + return; + } - QueryPlatform(scope, result); - QueryHostInstances(scope, result); + result.Platform.WmiConnected = true; + result.Platform.PlatformDataAvailable = QueryPlatform(scope, result); + result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result); var applicationIndex = BuildApplicationIndex(scope, result); - QueryApplicationRuntime(scope, result); - QuerySuspendedInstances(scope, result, applicationIndex); + result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result); + result.Platform.SuspendedInstancesDataAvailable = QuerySuspendedInstances(scope, result, applicationIndex); } - private void QueryPlatform(ManagementScope scope, ProbeResult result) + /// + /// Baut die WMI-Verbindung auf und wandelt Fehler in handlungsorientierte Diagnosen um. + /// + /// Zu verbindender WMI-Scope. + /// Ergebnisobjekt fuer eine moegliche Diagnose. + /// true, wenn der Namespace verbunden wurde. + 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; + } + } + + /// + /// Liest BizTalk-Gruppe sowie Management- und MessageBox-Datenbankziele. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Zu aktualisierendes Ergebnisobjekt. + /// true, wenn beide Pflichtklassen gelesen wurden. + 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; } + /// + /// Erstellt einen Index, um suspendierte Instanzen BizTalk-Anwendungen zuzuordnen. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Ergebnisobjekt fuer optionale Diagnosen. + /// Index aus moeglichen Artefaktschluesseln und Anwendungsnamen. private Dictionary BuildApplicationIndex(ManagementScope scope, ProbeResult result) { var index = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -73,9 +136,16 @@ namespace BizTalkCheckmkPulse return index; } + /// + /// Fuegt die Schluessel einer WMI-Artefaktklasse zum Anwendungsindex hinzu. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Ergebnisobjekt fuer optionale Diagnosen. + /// Zu erweiternder Anwendungsindex. + /// Name der auszulesenden WMI-Klasse. private void AddArtifactIndex(ManagementScope scope, ProbeResult result, Dictionary 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) + /// + /// Liest alle BizTalk Host Instances und deren Dienstzustand. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Zu aktualisierendes Ergebnisobjekt. + /// true, wenn die Pflichtabfrage erfolgreich war. + 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) + /// + /// Aggregiert Receive Locations, Send Ports und Orchestrations pro Anwendung. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Zu aktualisierendes Ergebnisobjekt. + /// true, wenn alle drei Pflichtabfragen erfolgreich waren. + private bool QueryApplicationRuntime(ManagementScope scope, ProbeResult result) { var apps = new Dictionary(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 applicationIndex) + /// + /// Liest ausschliesslich fortsetzbar und nicht fortsetzbar suspendierte Instanzen. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Zu aktualisierendes Ergebnisobjekt. + /// Index fuer die Zuordnung zur BizTalk-Anwendung. + /// true, wenn die Pflichtabfrage erfolgreich war. + private bool QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary 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 action, bool required) + /// + /// Fuehrt eine WQL-Abfrage aus und erfasst erwartbare Providerfehler strukturiert. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Ergebnisobjekt fuer Diagnosen. + /// Menschenlesbare Bezeichnung der Abfrage. + /// Auszufuehrende WQL-Abfrage. + /// Verarbeitung fuer jedes gefundene WMI-Objekt. + /// Kennzeichen, ob die Abfrage fuer einen stabilen Service erforderlich ist. + /// true, wenn die Abfrage vollstaendig ausgefuehrt wurde. + private bool TryQuery(ManagementScope scope, ProbeResult result, string label, string queryText, Action 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; } } + /// + /// Enumeriert eine WQL-Abfrage mit festem Timeout und gibt jedes Objekt weiter. + /// + /// Verbundener BizTalk-WMI-Scope. + /// Auszufuehrende WQL-Abfrage. + /// Verarbeitung fuer jedes gefundene Objekt. private void ForEachObject(ManagementScope scope, string queryText, Action action) { var queryOptions = new EnumerationOptions @@ -225,6 +325,12 @@ namespace BizTalkCheckmkPulse } } + /// + /// Liefert eine vorhandene oder neu angelegte Aggregation fuer eine Anwendung. + /// + /// Aggregation nach Anwendungsname. + /// Zu suchender Anwendungsname. + /// Aggregation der angeforderten Anwendung. private static ApplicationRuntimeState GetApplication(Dictionary apps, string applicationName) { var name = string.IsNullOrWhiteSpace(applicationName) ? UnknownApplication : applicationName.Trim(); @@ -238,6 +344,12 @@ namespace BizTalkCheckmkPulse return app; } + /// + /// Ermittelt den Anwendungsnamen direkt oder ueber den Artefaktindex. + /// + /// Suspendierte WMI-Instanz. + /// Index bekannter Artefaktschluessel. + /// Ermittelter Anwendungsname oder (unknown). private static string ResolveApplication(ManagementBaseObject item, Dictionary applicationIndex) { var direct = GetApplicationName(item); @@ -258,6 +370,11 @@ namespace BizTalkCheckmkPulse return UnknownApplication; } + /// + /// Liefert moegliche Schluessel fuer die Zuordnung eines WMI-Objekts. + /// + /// Auszuwertendes WMI-Objekt. + /// Vorhandene oder leere Kandidatenwerte. private static IEnumerable CandidateKeys(ManagementBaseObject item) { yield return WmiHelpers.GetString(item, "ApplicationName"); @@ -266,6 +383,11 @@ namespace BizTalkCheckmkPulse yield return WmiHelpers.GetString(item, "HostName"); } + /// + /// Liest den Anwendungsnamen aus den je Klasse unterschiedlichen Properties. + /// + /// Auszuwertendes WMI-Objekt. + /// Anwendungsname oder (unknown). private static string GetApplicationName(ManagementBaseObject item) { return FirstNonEmpty( @@ -274,15 +396,167 @@ namespace BizTalkCheckmkPulse WmiHelpers.GetString(item, "BizTalkApplication")) ?? UnknownApplication; } + /// + /// Liefert den ersten nicht leeren Textwert. + /// + /// Zu pruefende Werte in Prioritaetsreihenfolge. + /// Erster nicht leerer Wert oder null. 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) + /// + /// Prueft, ob eine Exception aus dem erwarteten WMI-/COM-Fehlerbereich stammt. + /// + /// Zu klassifizierende Exception. + /// true fuer behandelbare WMI-, Rechte- und COM-Fehler. + 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; + } + + /// + /// Erzeugt aus einer WMI-Exception eine strukturierte Diagnose. + /// + /// Ergebnisobjekt fuer die Diagnose. + /// Betroffener Namespace oder Klassenname. + /// Ausgeloeste Exception. + /// Kennzeichen fuer eine Pflichtabfrage. + /// Kennzeichen fuer Fehler beim Namespace-Verbindungsaufbau. + 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 + }); + } + + /// + /// Erzeugt eine Konfigurationsdiagnose, wenn eine erfolgreiche Pflichtabfrage keine Instanz liefert. + /// + /// Ergebnisobjekt fuer die Diagnose. + /// Betroffene WMI-Klasse. + /// Beschreibung der fehlenden BizTalk-Konfiguration. + 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." + }); + } + + /// + /// Klassifiziert WMI-Fehler anhand von Exception-Typ, Status und HRESULT. + /// + /// Zu klassifizierende Exception. + /// Handlungsorientierte Diagnosekategorie. + 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; + } + + /// + /// Erstellt eine kurze Fehlerursache fuer einen WMI-Fehler. + /// + /// Klassifizierte Fehlerart. + /// Betroffene Komponente. + /// Kennzeichen fuer den Namespace-Verbindungsaufbau. + /// Menschenlesbare Fehlerzusammenfassung. + 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."; + } + } + + /// + /// Liefert eine konkrete Handlungsempfehlung fuer einen WMI-Fehler. + /// + /// Klassifizierte Fehlerart. + /// Kennzeichen fuer den Namespace-Verbindungsaufbau. + /// Empfohlener Diagnoseschritt. + 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 \\" + 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."; + } + } + + /// + /// Sucht mehrere Teiltexte ohne Beachtung der Gross-/Kleinschreibung. + /// + /// Zu untersuchender Text. + /// Gesuchte Teiltexte. + /// true, wenn mindestens ein Teiltext enthalten ist. + private static bool ContainsAny(string value, params string[] candidates) + { + return candidates.Any(x => value.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0); } } } - diff --git a/src/BizTalkCheckmkPulse/WmiHelpers.cs b/src/BizTalkCheckmkPulse/WmiHelpers.cs index 09c435e..00cd415 100644 --- a/src/BizTalkCheckmkPulse/WmiHelpers.cs +++ b/src/BizTalkCheckmkPulse/WmiHelpers.cs @@ -4,8 +4,17 @@ using System.Management; namespace BizTalkCheckmkPulse { + /// + /// Konvertiert optionale WMI-Properties defensiv in .NET-Typen. + /// internal static class WmiHelpers { + /// + /// Liest eine WMI-Property als getrimmten Text. + /// + /// WMI-Objekt mit der Property. + /// Name der Property. + /// Textwert oder leerer Text bei fehlender Property. public static string GetString(ManagementBaseObject item, string propertyName) { if (item == null || string.IsNullOrWhiteSpace(propertyName)) @@ -24,6 +33,13 @@ namespace BizTalkCheckmkPulse } } + /// + /// Liest eine WMI-Property als Ganzzahl. + /// + /// WMI-Objekt mit der Property. + /// Name der Property. + /// Ersatzwert bei fehlender oder ungueltiger Property. + /// Konvertierter oder ersetzter Zahlenwert. 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; } + /// + /// Liest eine WMI-Property als booleschen Wert. + /// + /// WMI-Objekt mit der Property. + /// Name der Property. + /// Ersatzwert bei fehlender oder ungueltiger Property. + /// Konvertierter oder ersetzter Wahrheitswert. 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; } + /// + /// Konvertiert einen DMTF-Zeitstempel aus einer WMI-Property. + /// + /// WMI-Objekt mit der Property. + /// Name der Zeitstempel-Property. + /// Zeitpunkt oder null bei fehlendem beziehungsweise ungueltigem Wert. public static DateTime? GetDmtfDateTime(ManagementBaseObject item, string propertyName) { var value = GetString(item, propertyName); @@ -57,4 +86,3 @@ namespace BizTalkCheckmkPulse } } } -