From e13167151bd5ef88b795862c45681066427c61cc Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Wed, 8 Jul 2026 15:15:36 +0200 Subject: [PATCH] Initial commit for the biztalk suspended isntances local check --- .editorconfig | 13 + .gitea/workflows/build.yml | 22 + .gitignore | 8 + BizTalkSuspendedInstancesCheck.sln | 22 + Dokumentation.md | 226 +++++++++ README.md | 196 ++++++++ .../checkmk/biztalk_suspended_instances.cmd | 19 + docs/CheckmkServices.md | 88 ++++ scripts/build-release.cmd | 34 ++ scripts/package-release.cmd | 25 + src/BizTalkSuspendedInstancesCheck/App.config | 20 + .../ApplicationIndex.cs | 136 ++++++ .../BizTalkSuspendedInstancesCheck.csproj | 56 +++ .../CheckmkFormatter.cs | 449 ++++++++++++++++++ src/BizTalkSuspendedInstancesCheck/Models.cs | 112 +++++ .../MonitoringOptions.cs | 200 ++++++++ src/BizTalkSuspendedInstancesCheck/Program.cs | 43 ++ .../WmiBizTalkProbe.cs | 262 ++++++++++ 18 files changed, 1931 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 BizTalkSuspendedInstancesCheck.sln create mode 100644 Dokumentation.md create mode 100644 README.md create mode 100644 deployment/checkmk/biztalk_suspended_instances.cmd create mode 100644 docs/CheckmkServices.md create mode 100644 scripts/build-release.cmd create mode 100644 scripts/package-release.cmd create mode 100644 src/BizTalkSuspendedInstancesCheck/App.config create mode 100644 src/BizTalkSuspendedInstancesCheck/ApplicationIndex.cs create mode 100644 src/BizTalkSuspendedInstancesCheck/BizTalkSuspendedInstancesCheck.csproj create mode 100644 src/BizTalkSuspendedInstancesCheck/CheckmkFormatter.cs create mode 100644 src/BizTalkSuspendedInstancesCheck/Models.cs create mode 100644 src/BizTalkSuspendedInstancesCheck/MonitoringOptions.cs create mode 100644 src/BizTalkSuspendedInstancesCheck/Program.cs create mode 100644 src/BizTalkSuspendedInstancesCheck/WmiBizTalkProbe.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..14c0fec --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{md,yml,yaml}] +indent_size = 2 + diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..64c4bbc --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,22 @@ +name: build + +on: + push: + pull_request: + +jobs: + build: + runs-on: windows + steps: + - uses: actions/checkout@v4 + - name: Build release + shell: cmd + run: scripts\build-release.cmd + - name: Package deployment folder + shell: cmd + run: scripts\package-release.cmd + - name: Upload deployment artifact + uses: actions/upload-artifact@v4 + with: + name: BizTalkSuspendedInstancesCheck-deploy + path: artifacts\BizTalkSuspendedInstancesCheck-deploy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd3b0d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/ +obj/ +artifacts/ +*.user +*.suo +.vs/ +TestResults/ + diff --git a/BizTalkSuspendedInstancesCheck.sln b/BizTalkSuspendedInstancesCheck.sln new file mode 100644 index 0000000..9202b89 --- /dev/null +++ b/BizTalkSuspendedInstancesCheck.sln @@ -0,0 +1,22 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkSuspendedInstancesCheck", "src\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.csproj", "{ED7AF215-F12B-4E2E-811B-693A537C9493}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ED7AF215-F12B-4E2E-811B-693A537C9493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED7AF215-F12B-4E2E-811B-693A537C9493}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED7AF215-F12B-4E2E-811B-693A537C9493}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED7AF215-F12B-4E2E-811B-693A537C9493}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal + diff --git a/Dokumentation.md b/Dokumentation.md new file mode 100644 index 0000000..8f4f952 --- /dev/null +++ b/Dokumentation.md @@ -0,0 +1,226 @@ +# Dokumentation: BizTalk Runtime Checkmk Local Check + +## Ziel + +Der Check beantwortet fuer den lokalen BizTalk-Server: + +- Gibt es suspendierte BizTalk-Service-Instanzen? +- Sind sie resumable oder non-resumable? +- Welche BizTalk-Anwendungen sind betroffen? +- Sind BizTalk Host-Instanzen gestartet? +- Wie ist der aggregierte Laufzeitstatus wichtiger BizTalk-Artefakte? + +Er ist fuer Checkmk Windows Agent Local Checks gebaut und benoetigt keine PowerShell. + +## Architektur + +Komponenten: + +- `BizTalkSuspendedInstancesCheck.exe`: C#/.NET-Framework-Konsolenprogramm +- `BizTalkSuspendedInstancesCheck.exe.config`: Konfiguration +- `biztalk_suspended_instances.cmd`: Checkmk-Wrapper fuer `%ProgramData%\checkmk\agent\local` + +Laufzeitfluss: + +1. Checkmk Windows Agent ruft `biztalk_suspended_instances.cmd` auf. +2. Der Wrapper startet die EXE. +3. Die EXE fragt `root\MicrosoftBizTalkServer` per WMI ab. +4. Die EXE schreibt mehrere Checkmk-konforme Local-Check-Zeilen nach STDOUT. + +Die Anwendung gibt selbst bei Fehlern eine syntaktisch gueltige Checkmk-Zeile aus. Dadurch wird der Monitoring-Zustand `UNKNOWN` sichtbar, statt den Agenten still zu stoeren. + +## Ausfuehrung durch Checkmk + +Der Check wird als Checkmk Local Check betrieben. Das bedeutet: Checkmk fuehrt auf dem BizTalk-Server keine zentrale Abfrage gegen BizTalk aus, sondern der Checkmk Windows Agent startet lokal abgelegte Checks und sammelt deren STDOUT ein. + +Der relevante Agent-Pfad auf Windows ist: + +```cmd +%ProgramData%\checkmk\agent\local +``` + +In diesem Verzeichnis liegt unser mitgelieferter Wrapper: + +```text +%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd +``` + +Der Wrapper ruft die eigentliche C#-EXE auf: + +```text +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe +``` + +Die vollstaendige Zielstruktur ist: + +```text +%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe.config +``` + +Das Aufrufskript muss nicht separat neu entwickelt werden. Es ist Bestandteil dieses Repositories: + +```text +deployment\checkmk\biztalk_suspended_instances.cmd +``` + +Beim Paketieren mit `scripts\package-release.cmd` wird dieses Skript in den Deployment-Ordner kopiert. Der Inhalt von `artifacts\BizTalkSuspendedInstancesCheck-deploy` kann komplett nach `%ProgramData%\checkmk\agent\local` kopiert werden. + +Der technische Ablauf ist: + +1. Checkmk Windows Agent startet `biztalk_suspended_instances.cmd`. +2. Die `.cmd` sucht `BizTalkSuspendedInstancesCheck.exe` zuerst im Unterordner `BizTalkSuspendedInstancesCheck`. +3. Falls die EXE dort nicht liegt, sucht die `.cmd` die EXE direkt neben dem Skript. +4. Falls die EXE nicht gefunden wird, erzeugt die `.cmd` eine gueltige Checkmk-Zeile mit Status `UNKNOWN`. +5. Falls die EXE gefunden wird, fragt sie BizTalk per WMI ab. +6. Die EXE schreibt die Checkmk-Services nach STDOUT. +7. Der Checkmk Agent nimmt diese Zeilen in seine Agentenausgabe auf. +8. Nach Checkmk Service Discovery koennen die Services uebernommen werden. + +Manueller Test auf dem BizTalk-Server: + +```cmd +"%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd" +``` + +Agentenausgabe pruefen: + +```cmd +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump +``` + +## WMI-Abfragen + +Primaere Abfrage: + +```sql +SELECT * FROM MSBTS_ServiceInstance WHERE ServiceStatus = 4 OR ServiceStatus = 32 +``` + +Statuswerte: + +- `4`: Suspended (resumable) +- `32`: Suspended (not resumable) + +Weitere Betriebsabfragen: + +```sql +SELECT * FROM MSBTS_HostInstance +SELECT * FROM MSBTS_ReceiveLocation +SELECT * FROM MSBTS_SendPort +SELECT * FROM MSBTS_Orchestration +``` + +Aus Betriebssicht werden damit folgende Risiken sichtbar: + +- Gestoppte oder haengende Host-Instanzen. +- Deaktivierte Receive Locations. +- Nicht gestartete Send Ports. +- Nicht gestartete oder nicht gebundene Orchestrations. +- Unbekannte WMI-Statuswerte, die auf Versions-/Schemaabweichungen oder Lesefehler hindeuten koennen. + +Zur Zuordnung der Anwendung wird zuerst versucht, eine Anwendungs-Property direkt aus der Service-Instanz zu lesen. Falls BizTalk diese nicht liefert, baut der Check einen lokalen Index ueber bekannte BizTalk-Artefakte: + +- `MSBTS_SendPort` +- `MSBTS_ReceivePort` +- `MSBTS_ReceiveLocation` +- `MSBTS_Orchestration` + +Wenn keine Zuordnung moeglich ist, erscheint die Instanz unter `(Unknown Application)`. Das ist absichtlich kein Fehler, weil BizTalk nicht fuer jede Instanz eine direkte Anwendungseigenschaft liefern muss. + +## Checkmk-Ausgabe + +Der Check gibt standardmaessig drei stabile Services aus. Es werden keine dynamischen Services pro Anwendung erzeugt, damit Checkmk Service Discovery stabil bleibt. + +Beispiel ohne Befund: + +```text +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;;;0|biztalk_host_instances_pending=0;;;0|biztalk_host_instances_unknown=0;;;0 total=4, started=4, stopped=0, pending=0, unknown=0 +0 "BizTalk Runtime Artifacts" biztalk_applications=6;;;0|biztalk_receive_locations=20;;;0|biztalk_receive_locations_disabled=0;;;0|biztalk_send_ports=30;;;0|biztalk_send_ports_started=30;;;0|biztalk_send_ports_stopped=0;;;0|biztalk_send_ports_bound=0;;;0|biztalk_send_ports_unknown=0;;;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=12;;;0|biztalk_orchestrations_stopped=0;;;0|biztalk_orchestrations_bound=0;;;0|biztalk_orchestrations_unbound=0;;;0|biztalk_orchestrations_unknown=0;;;0 applications=6 | ReceiveLocations total=20, disabled=0 | SendPorts total=30, started=30, stopped=0, bound=0, unknown=0 | Orchestrations total=12, started=12, stopped=0, bound=0, unbound=0, unknown=0 +``` + +Beispiel mit non-resumable Instanz: + +```text +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=2. ApplicationA R=2 NR=0; ApplicationB R=0 NR=1 +``` + +Beispiel fuer Host-Instanzen: + +```text +2 "BizTalk Host Instances" biztalk_host_instances_total=4;;;0|biztalk_host_instances_started=3;;;0|biztalk_host_instances_stopped=1;;;0|biztalk_host_instances_pending=0;;;0|biztalk_host_instances_unknown=0;;;0 total=4, started=3, stopped=1, pending=0, unknown=0 | Not started: ProcessingHost@BIZTALK01=Stopped +``` + +Beispiel fuer Artefaktstatus: + +```text +0 "BizTalk Runtime Artifacts" biztalk_applications=6;;;0|biztalk_receive_locations=20;;;0|biztalk_receive_locations_disabled=1;;;0|biztalk_send_ports=30;;;0|biztalk_send_ports_started=28;;;0|biztalk_send_ports_stopped=2;;;0|biztalk_send_ports_bound=0;;;0|biztalk_send_ports_unknown=0;;;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=11;;;0|biztalk_orchestrations_stopped=1;;;0|biztalk_orchestrations_bound=0;;;0|biztalk_orchestrations_unbound=0;;;0|biztalk_orchestrations_unknown=0;;;0 applications=6 | ReceiveLocations total=20, disabled=1 | SendPorts total=30, started=28, stopped=2, bound=0, unknown=0 | Orchestrations total=12, started=11, stopped=1, bound=0, unbound=0, unknown=0 | Notable apps: ApplicationA RL disabled=1, SP stopped/bound/unknown=2/0/0, Orch stopped/bound/unbound/unknown=1/0/0/0 +``` + +## Schwellwerte + +Default: + +- Resumable ab `1`: `WARN` +- Non-resumable ab `1`: `CRIT` +- Host-Instanz `Stopped` oder unbekannt: `CRIT` +- Host-Instanz `StartPending` oder `StopPending`: `WARN` +- Artefakte mit unbekanntem Status: `CRIT` +- Deaktivierte/inaktive Artefakte: nur Anzeige, optional `WARN` mit `AlertOnArtifactRuntimeIssues=true` + +Begruendung: + +- Resumable Suspensions sind stoerungsrelevant, aber potentiell wiederaufnehmbar. +- Non-resumable Suspensions verlangen normalerweise manuelle Bereinigung oder fachliche Analyse und werden daher kritisch bewertet. +- Gestoppte Host-Instanzen wirken direkt auf die Verarbeitung und werden daher kritisch bewertet. +- Deaktivierte Receive Locations, gestoppte Send Ports und gestoppte Orchestrations koennen gewollt sein; deshalb werden sie zuerst als Betriebsinformation ausgegeben. + +## Betrieb + +Empfohlener Installationspfad: + +```text +%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe.config +``` + +Der Wrapper sucht die EXE zuerst im Unterordner `BizTalkSuspendedInstancesCheck` und danach direkt neben der `.cmd`. Damit funktioniert sowohl eine saubere Unterordner-Installation als auch ein flaches Deployment. + +Nach Aenderungen an den Service-Namen muss in Checkmk eine erneute Service Discovery erfolgen. Reine Schwellwert- oder Anzeigeaenderungen brauchen keine neue Discovery. + +## Rechte + +Der Check muss unter dem Konto des Checkmk Windows Agent auf dem BizTalk-Server WMI-Leserechte fuer `root\MicrosoftBizTalkServer` haben. In der Praxis ist das meist `LocalSystem` oder ein dediziertes Agent-Servicekonto mit lokalen Rechten. + +## Resilienz + +- Keine PowerShell-Abhaengigkeit. +- Keine BizTalk-Assembly-Abhaengigkeit im Build. +- UTF-8-Ausgabe wird in der EXE gesetzt. +- WMI-Timeout ist konfigurierbar. +- Fehler erzeugen `UNKNOWN` statt leerer Ausgabe. +- Artefakt-Index ist best effort; fehlende Anwendungszuordnung verhindert den Status nicht. +- Ein stabiler Checkmk-Service vermeidet Service-Discovery-Flapping bei wechselnden Anwendungsnamen. +- Runtime-Artefakte werden aggregiert und begrenzt ausgegeben, damit Checkmk-Summaries lesbar bleiben. + +## Konfiguration + +Wichtige `appSettings`: + +- `ServiceName`: Service fuer suspendierte Instanzen. +- `HostInstancesServiceName`: Service fuer Host-Instanzen. +- `RuntimeArtifactsServiceName`: Service fuer Artefaktstatus. +- `MaxApplicationDetails`: maximale Anzahl Anwendungen in der Suspensions-Summary. +- `MaxRuntimeApplicationDetails`: maximale Anzahl auffaelliger Anwendungen in der Runtime-Summary. +- `AlertOnStoppedHostInstances`: Host-Instanz-Abweichungen alarmieren. +- `AlertOnArtifactRuntimeIssues`: deaktivierte/inaktive Artefakte als `WARN` werten. +- `EmitPerApplicationServices`: zusaetzliche Suspensions-Services pro Anwendung erzeugen. Nur verwenden, wenn dynamische Services in Checkmk gewuenscht sind. + +## Gitea + +Das Repository enthaelt `.gitea/workflows/build.yml`. Der Workflow erwartet einen Windows Runner mit Visual Studio Build Tools und .NET Framework 4.6.1 Developer Pack. + +Wenn euer Gitea Runner anders gelabelt ist, `runs-on` in `.gitea/workflows/build.yml` anpassen. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8153b6d --- /dev/null +++ b/README.md @@ -0,0 +1,196 @@ +# BizTalk Runtime Checkmk Local Check + +Lokaler Checkmk-Check fuer Microsoft BizTalk Server 2020. Der Check prueft ueber WMI im Namespace `root\MicrosoftBizTalkServer`, ob suspendierte BizTalk-Service-Instanzen vorhanden sind, und liefert zusaetzliche Betriebsinformationen zu Host-Instanzen und BizTalk-Artefakten. + +## Entscheidung + +PowerShell ist in der Zielumgebung abgeriegelt. Die sinnvollste Option ist deshalb ein kleines C#/.NET-Framework-Konsolenprogramm, das per `.cmd` aus dem Checkmk-Agent-Local-Verzeichnis gestartet wird. + +Bewertung: + +- C#/.NET Framework: geeignet. Auf Windows Server mit BizTalk vorhanden, direkte WMI-Unterstuetzung via `System.Management`, keine PowerShell-Ausfuehrung, keine BizTalk-DLL als Compile-Time-Abhaengigkeit. +- PowerShell: fachlich moeglich, aber in der Umgebung ausgeschlossen. +- VBScript/JScript: technisch moeglich, aber schlechter wartbar und fehleranfaelliger bei Encoding und Fehlerbehandlung. +- BizTalk OperationsOM/ExplorerOM DLLs: fachlich moeglich, aber deploy- und buildseitig unnoetig schwer, da die DLLs auf Build- und Laufzeitumgebung passen muessen. + +Der Check nutzt fuer Suspensions die WMI-Klasse `MSBTS_ServiceInstance`. Microsoft dokumentiert fuer `ServiceStatus` den Wert `4` als `Suspended (resumable)` und `32` als `Suspended (not resumable)`. Fuer die weiteren Betriebsinformationen werden `MSBTS_HostInstance`, `MSBTS_ReceiveLocation`, `MSBTS_SendPort` und `MSBTS_Orchestration` gelesen. + +## Checkmk-Verhalten + +Der Check erzeugt standardmaessig drei stabile Checkmk-Services: + +- `BizTalk Suspended Instances` +- `BizTalk Host Instances` +- `BizTalk Runtime Artifacts` + +### BizTalk Suspended Instances + +Statuslogik: + +- `OK`: keine suspendierten Instanzen +- `WARN`: mindestens eine resumable suspendierte Instanz +- `CRIT`: mindestens eine non-resumable suspendierte Instanz +- `UNKNOWN`: WMI-Zugriff, Konfiguration oder Laufzeitfehler + +Metriken: + +- `biztalk_suspended_total` +- `biztalk_suspended_resumable` +- `biztalk_suspended_nonresumable` + +Die betroffenen BizTalk-Anwendungen stehen in der Checkmk-Summary, z.B. `ApplicationA R=2 NR=0; ApplicationB R=0 NR=1`. + +### BizTalk Host Instances + +Statuslogik: + +- `OK`: alle Host-Instanzen sind `Started` +- `WARN`: Host-Instanzen sind in `StartPending` oder `StopPending` +- `CRIT`: mindestens eine Host-Instanz ist `Stopped` oder hat einen unbekannten Zustand +- `UNKNOWN`: WMI liefert keine Host-Instanzen + +Metriken: + +- `biztalk_host_instances_total` +- `biztalk_host_instances_started` +- `biztalk_host_instances_stopped` +- `biztalk_host_instances_pending` +- `biztalk_host_instances_unknown` + +Die Summary zeigt Gesamtzahlen und bis zu 12 nicht gestartete Host-Instanzen im Format `Host@Server=Status`. + +### BizTalk Runtime Artifacts + +Dieser Service ist standardmaessig informativ (`OK`), solange keine unbekannten WMI-Zustaende auftreten. Grund: deaktivierte Receive Locations, gestoppte Send Ports oder gestoppte Orchestrations koennen in BizTalk fachlich beabsichtigt sein. + +Statuslogik: + +- `OK`: Artefaktstatus lesbar, keine unbekannten Statuswerte +- `CRIT`: Send Ports oder Orchestrations haben unbekannte WMI-Statuswerte +- Optional `WARN`: wenn `AlertOnArtifactRuntimeIssues=true` und deaktivierte/inaktive Artefakte gefunden werden + +Metriken: + +- `biztalk_applications` +- `biztalk_receive_locations`, `biztalk_receive_locations_disabled` +- `biztalk_send_ports`, `biztalk_send_ports_started`, `biztalk_send_ports_stopped`, `biztalk_send_ports_bound`, `biztalk_send_ports_unknown` +- `biztalk_orchestrations`, `biztalk_orchestrations_started`, `biztalk_orchestrations_stopped`, `biztalk_orchestrations_bound`, `biztalk_orchestrations_unbound`, `biztalk_orchestrations_unknown` + +Die Summary ist bewusst bereits lesbar formatiert und enthaelt Abschnittstrenner, z.B. `ReceiveLocations total=20, disabled=1 | SendPorts ... | Notable apps: ...`. + +## Build + +Voraussetzungen auf dem Build-Host: + +- Windows +- Visual Studio Build Tools oder Visual Studio mit MSBuild +- .NET Framework 4.6.1 Developer Pack + +Build: + +```cmd +scripts\build-release.cmd +``` + +Deployment-Ordner erzeugen: + +```cmd +scripts\package-release.cmd +``` + +Das Ergebnis liegt unter `artifacts\BizTalkSuspendedInstancesCheck-deploy`. + +Der Gitea-Workflow `.gitea/workflows/build.yml` fuehrt denselben Release-Build und die Paketierung aus. + +## Wie Checkmk den Check ausfuehrt + +Checkmk ruft unser C#-Programm nicht direkt per zentraler Checkmk-Regel auf. Der Checkmk Windows Agent startet lokale Checks auf dem BizTalk-Server automatisch aus dem lokalen Agent-Verzeichnis: + +```cmd +%ProgramData%\checkmk\agent\local +``` + +Dort muss eine ausfuehrbare Datei liegen. Fuer diesen Check ist das der bereits mitgelieferte Wrapper: + +```text +%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd +``` + +Der Wrapper startet anschliessend die eigentliche EXE: + +```text +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe +``` + +Zielstruktur auf dem BizTalk-Server: + +```text +%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe.config +``` + +Der Ablauf ist: + +1. Checkmk Windows Agent startet `biztalk_suspended_instances.cmd`. +2. Die `.cmd` sucht die EXE im Unterordner `BizTalkSuspendedInstancesCheck`. +3. Die EXE liest BizTalk per WMI. +4. Die EXE schreibt Checkmk-Local-Check-Zeilen nach STDOUT. +5. Der Checkmk Agent liefert diese Zeilen an den Checkmk Server. +6. Nach Service Discovery erscheinen die Services in Checkmk. + +Das Skript muss also nicht mehr neu erstellt werden. Es liegt im Repository unter `deployment\checkmk\biztalk_suspended_instances.cmd` und wird beim Paketieren in den Deployment-Ordner kopiert. Falls die EXE fehlt, gibt die `.cmd` eine gueltige `UNKNOWN`-Zeile aus, damit ein unvollstaendiges Deployment in Checkmk sichtbar wird. + +## Deployment auf BizTalk-Server + +1. Release bauen oder aus Gitea-Artefakt laden. +2. Inhalt von `artifacts\BizTalkSuspendedInstancesCheck-deploy` nach `%ProgramData%\checkmk\agent\local` kopieren. +3. Auf dem BizTalk-Server als lokaler Administrator testen: + +```cmd +"%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd" +``` + +4. Agentenausgabe pruefen: + +```cmd +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump +``` + +5. In Checkmk Service Discovery fuer den Host ausfuehren und den Service aufnehmen. + +## Konfiguration + +Die Datei `BizTalkSuspendedInstancesCheck.exe.config` kann neben der EXE angepasst werden. + +Wichtige Werte: + +- `ServiceName`: Checkmk-Service-Name +- `HostInstancesServiceName`: Service-Name fuer Host-Instanzen +- `RuntimeArtifactsServiceName`: Service-Name fuer Artefaktstatus +- `Server`: leer oder `.` fuer lokalen Server +- `WarnResumableThreshold`: Default `1` +- `CritNonResumableThreshold`: Default `1` +- `QueryTimeoutSeconds`: Default `25` +- `MaxApplicationDetails`: Anzahl Anwendungen in der Summary, Default `12` +- `MaxRuntimeApplicationDetails`: Anzahl auffaelliger Anwendungen in der Runtime-Summary, Default `10` +- `AlertOnStoppedHostInstances`: Host-Instanzen mit Status `Stopped`/unbekannt alarmieren, Default `true` +- `AlertOnArtifactRuntimeIssues`: deaktivierte/inaktive Artefakte als `WARN` werten, Default `false` +- `EmitPerApplicationServices`: optional zusaetzliche Services pro Anwendung fuer Suspensions, Default `false` + +## Gitea + +Das Verzeichnis ist als eigenes Git-Repository mit Branch `main` angelegt. Fuer ein neues Gitea-Repository: + +```cmd +git remote add origin +git add . +git commit -m "Add BizTalk Checkmk local check" +git push -u origin main +``` + +## Quellen + +- Checkmk Local Checks: https://docs.checkmk.com/latest/de/localchecks.html +- Microsoft BizTalk `MSBTS_ServiceInstance`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-wmi +- Microsoft BizTalk `ServiceStatus`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-servicestatus-property-wmi diff --git a/deployment/checkmk/biztalk_suspended_instances.cmd b/deployment/checkmk/biztalk_suspended_instances.cmd new file mode 100644 index 0000000..cc5ec21 --- /dev/null +++ b/deployment/checkmk/biztalk_suspended_instances.cmd @@ -0,0 +1,19 @@ +@echo off +setlocal + +set "SERVICE_NAME=BizTalk Suspended Instances" +set "CHECK_DIR=%~dp0BizTalkSuspendedInstancesCheck" +set "CHECK_EXE=%CHECK_DIR%\BizTalkSuspendedInstancesCheck.exe" + +if not exist "%CHECK_EXE%" ( + set "CHECK_EXE=%~dp0BizTalkSuspendedInstancesCheck.exe" +) + +if not exist "%CHECK_EXE%" ( + echo 3 "%SERVICE_NAME%" - Check executable not found. Expected "%CHECK_DIR%\BizTalkSuspendedInstancesCheck.exe". + exit /b 0 +) + +"%CHECK_EXE%" %* +exit /b 0 + diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md new file mode 100644 index 0000000..0f81cae --- /dev/null +++ b/docs/CheckmkServices.md @@ -0,0 +1,88 @@ +# Checkmk Services und Metriken + +Dieser Check erzeugt standardmaessig drei stabile Local-Check-Services. + +## Aufruf + +Checkmk startet unter Windows lokale Checks aus: + +```cmd +%ProgramData%\checkmk\agent\local +``` + +Der mitgelieferte Wrapper ist: + +```text +%ProgramData%\checkmk\agent\local\biztalk_suspended_instances.cmd +``` + +Er ruft die EXE im Unterordner auf: + +```text +%ProgramData%\checkmk\agent\local\BizTalkSuspendedInstancesCheck\BizTalkSuspendedInstancesCheck.exe +``` + +Das Skript liegt im Repository unter `deployment\checkmk\biztalk_suspended_instances.cmd` und wird durch `scripts\package-release.cmd` in den Deployment-Ordner kopiert. + +## BizTalk Suspended Instances + +Zweck: suspendierte Service-Instanzen erkennen und betroffene Anwendungen direkt in der Summary anzeigen. + +Status: + +- `OK`: keine Suspensions +- `WARN`: resumable Suspensions ab Schwellwert +- `CRIT`: non-resumable Suspensions ab Schwellwert +- `UNKNOWN`: Check- oder WMI-Fehler + +Metriken: + +- `biztalk_suspended_total` +- `biztalk_suspended_resumable` +- `biztalk_suspended_nonresumable` + +## BizTalk Host Instances + +Zweck: gestartete, gestoppte, pending und unbekannte Host-Instanzen sichtbar machen. + +Status: + +- `OK`: alle Host-Instanzen gestartet +- `WARN`: mindestens eine Host-Instanz pending +- `CRIT`: mindestens eine Host-Instanz stopped oder unknown +- `UNKNOWN`: WMI liefert keine Host-Instanzen + +Metriken: + +- `biztalk_host_instances_total` +- `biztalk_host_instances_started` +- `biztalk_host_instances_stopped` +- `biztalk_host_instances_pending` +- `biztalk_host_instances_unknown` + +## BizTalk Runtime Artifacts + +Zweck: aggregierte Betriebsuebersicht fuer Receive Locations, Send Ports und Orchestrations. + +Status: + +- `OK`: Statuswerte lesbar +- `CRIT`: unbekannte Send-Port- oder Orchestration-Statuswerte +- Optional `WARN`: wenn `AlertOnArtifactRuntimeIssues=true` und deaktivierte/inaktive Artefakte vorhanden sind + +Metriken: + +- `biztalk_applications` +- `biztalk_receive_locations` +- `biztalk_receive_locations_disabled` +- `biztalk_send_ports` +- `biztalk_send_ports_started` +- `biztalk_send_ports_stopped` +- `biztalk_send_ports_bound` +- `biztalk_send_ports_unknown` +- `biztalk_orchestrations` +- `biztalk_orchestrations_started` +- `biztalk_orchestrations_stopped` +- `biztalk_orchestrations_bound` +- `biztalk_orchestrations_unbound` +- `biztalk_orchestrations_unknown` diff --git a/scripts/build-release.cmd b/scripts/build-release.cmd new file mode 100644 index 0000000..6ee947a --- /dev/null +++ b/scripts/build-release.cmd @@ -0,0 +1,34 @@ +@echo off +setlocal + +set "ROOT=%~dp0.." +set "SOLUTION=%ROOT%\BizTalkSuspendedInstancesCheck.sln" + +if not defined MSBUILD ( + set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" + if exist "%VSWHERE%" ( + for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe`) do ( + set "MSBUILD=%%i" + goto :found_msbuild + ) + ) +) + +:found_msbuild +if not defined MSBUILD ( + for /f "tokens=*" %%i in ('where msbuild 2^>nul') do ( + set "MSBUILD=%%i" + goto :found_msbuild_path + ) +) + +:found_msbuild_path +if not defined MSBUILD ( + echo MSBuild was not found. Install Visual Studio Build Tools and the .NET Framework 4.6.1 Developer Pack. + exit /b 1 +) + +echo Using MSBuild: %MSBUILD% +"%MSBUILD%" "%SOLUTION%" /m /restore /p:Configuration=Release /p:Platform="Any CPU" +exit /b %ERRORLEVEL% + diff --git a/scripts/package-release.cmd b/scripts/package-release.cmd new file mode 100644 index 0000000..ad8fdbd --- /dev/null +++ b/scripts/package-release.cmd @@ -0,0 +1,25 @@ +@echo off +setlocal + +set "ROOT=%~dp0.." +set "PROJECT_BIN=%ROOT%\src\BizTalkSuspendedInstancesCheck\bin\Release" +set "ARTIFACTS=%ROOT%\artifacts" +set "DEPLOY=%ARTIFACTS%\BizTalkSuspendedInstancesCheck-deploy" +set "APPDIR=%DEPLOY%\BizTalkSuspendedInstancesCheck" + +call "%ROOT%\scripts\build-release.cmd" +if errorlevel 1 exit /b %ERRORLEVEL% + +if exist "%DEPLOY%" rmdir /s /q "%DEPLOY%" +mkdir "%APPDIR%" + +copy "%ROOT%\deployment\checkmk\biztalk_suspended_instances.cmd" "%DEPLOY%\" >nul +copy "%PROJECT_BIN%\BizTalkSuspendedInstancesCheck.exe" "%APPDIR%\" >nul +copy "%PROJECT_BIN%\BizTalkSuspendedInstancesCheck.exe.config" "%APPDIR%\" >nul +if exist "%PROJECT_BIN%\BizTalkSuspendedInstancesCheck.pdb" copy "%PROJECT_BIN%\BizTalkSuspendedInstancesCheck.pdb" "%APPDIR%\" >nul +copy "%ROOT%\README.md" "%DEPLOY%\" >nul +copy "%ROOT%\Dokumentation.md" "%DEPLOY%\" >nul +if exist "%ROOT%\docs" xcopy "%ROOT%\docs" "%DEPLOY%\docs\" /e /i /y >nul + +echo Deployment folder created: %DEPLOY% +exit /b 0 diff --git a/src/BizTalkSuspendedInstancesCheck/App.config b/src/BizTalkSuspendedInstancesCheck/App.config new file mode 100644 index 0000000..a155260 --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/App.config @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/BizTalkSuspendedInstancesCheck/ApplicationIndex.cs b/src/BizTalkSuspendedInstancesCheck/ApplicationIndex.cs new file mode 100644 index 0000000..d6a38ae --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/ApplicationIndex.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Management; + +namespace BizTalkSuspendedInstancesCheck +{ + internal sealed class ApplicationIndex + { + private const string UnknownApplication = "(Unknown Application)"; + private readonly Dictionary _byArtifactName = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public void Add(ManagementBaseObject item) + { + var application = FirstNonEmpty( + SafeGetString(item, "ApplicationName"), + SafeGetString(item, "Application"), + SafeGetString(item, "BizTalkApplication")); + + if (string.IsNullOrWhiteSpace(application)) + { + return; + } + + AddName(SafeGetString(item, "Name"), application); + AddName(SafeGetString(item, "ServiceName"), application); + AddName(SafeGetString(item, "ReceivePortName"), application); + AddName(SafeGetString(item, "SendPortName"), application); + } + + public string Resolve(ManagementBaseObject serviceInstance) + { + var direct = FirstNonEmpty( + SafeGetString(serviceInstance, "ApplicationName"), + SafeGetString(serviceInstance, "Application"), + SafeGetString(serviceInstance, "BizTalkApplication")); + + if (!string.IsNullOrWhiteSpace(direct)) + { + return direct; + } + + var serviceName = SafeGetString(serviceInstance, "ServiceName"); + var name = SafeGetString(serviceInstance, "Name"); + var serviceClass = SafeGetString(serviceInstance, "ServiceClass"); + + return ResolveByName(serviceName) + ?? ResolveByName(name) + ?? ResolveByName(LastSegment(serviceName)) + ?? ResolveByName(LastSegment(name)) + ?? ResolveByName(serviceClass) + ?? UnknownApplication; + } + + private void AddName(string name, string application) + { + if (string.IsNullOrWhiteSpace(name)) + { + return; + } + + var normalized = name.Trim(); + if (!_byArtifactName.ContainsKey(normalized)) + { + _byArtifactName.Add(normalized, application.Trim()); + } + + var lastSegment = LastSegment(normalized); + if (!string.IsNullOrWhiteSpace(lastSegment) && !_byArtifactName.ContainsKey(lastSegment)) + { + _byArtifactName.Add(lastSegment, application.Trim()); + } + } + + private string ResolveByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return null; + } + + string application; + return _byArtifactName.TryGetValue(name.Trim(), out application) ? application : null; + } + + private static string LastSegment(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var trimmed = value.Trim(); + var index = trimmed.LastIndexOf('.'); + if (index < 0 || index + 1 >= trimmed.Length) + { + return trimmed; + } + + return trimmed.Substring(index + 1); + } + + private static string FirstNonEmpty(params string[] values) + { + foreach (var value in values) + { + if (!string.IsNullOrWhiteSpace(value)) + { + return value.Trim(); + } + } + + return null; + } + + internal static string SafeGetString(ManagementBaseObject item, string propertyName) + { + try + { + foreach (PropertyData property in item.Properties) + { + if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + var value = item[property.Name]; + return value == null ? string.Empty : Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture); + } + } + } + catch (ManagementException) + { + } + + return string.Empty; + } + } +} + diff --git a/src/BizTalkSuspendedInstancesCheck/BizTalkSuspendedInstancesCheck.csproj b/src/BizTalkSuspendedInstancesCheck/BizTalkSuspendedInstancesCheck.csproj new file mode 100644 index 0000000..7284bcd --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/BizTalkSuspendedInstancesCheck.csproj @@ -0,0 +1,56 @@ + + + + + Debug + AnyCPU + {ED7AF215-F12B-4E2E-811B-693A537C9493} + Exe + BizTalkSuspendedInstancesCheck + BizTalkSuspendedInstancesCheck + v4.6.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BizTalkSuspendedInstancesCheck/CheckmkFormatter.cs b/src/BizTalkSuspendedInstancesCheck/CheckmkFormatter.cs new file mode 100644 index 0000000..fbf498c --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/CheckmkFormatter.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace BizTalkSuspendedInstancesCheck +{ + internal sealed class CheckmkFormatter + { + private const int HostStopped = 1; + private const int HostStartPending = 2; + private const int HostStopPending = 3; + private const int HostStarted = 4; + private readonly MonitoringOptions _options; + + public CheckmkFormatter(MonitoringOptions options) + { + _options = options; + } + + public IEnumerable Format(ProbeResult result) + { + yield return FormatAggregate(result); + yield return FormatHostInstances(result); + yield return FormatRuntimeArtifacts(result); + + if (_options.EmitPerApplicationServices) + { + foreach (var line in FormatApplications(result)) + { + yield return line; + } + } + } + + public string FormatUnknown(string message) + { + return BuildLine(CheckState.Unknown, _options.ServiceName, "-", SanitizeDetail(message)); + } + + public string FormatSelfTest() + { + return BuildLine(CheckState.Ok, _options.ServiceName, "-", "Self test OK. Checkmk local check output format is valid."); + } + + private string FormatHostInstances(ProbeResult result) + { + var hosts = result.HostInstances; + var total = hosts.Count; + var started = hosts.Count(x => x.ServiceState == HostStarted); + var stopped = hosts.Count(x => x.ServiceState == HostStopped); + var pending = hosts.Count(x => x.ServiceState == HostStartPending || x.ServiceState == HostStopPending); + var unknown = total - started - stopped - pending; + var state = DetermineHostState(total, stopped, pending, unknown); + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_host_instances_total={0};;;0|biztalk_host_instances_started={1};;;0|biztalk_host_instances_stopped={2};;;0|biztalk_host_instances_pending={3};;;0|biztalk_host_instances_unknown={4};;;0", + total, + started, + stopped, + pending, + unknown); + var detail = BuildHostDetail(hosts, total, started, stopped, pending, unknown); + + return BuildLine(state, _options.HostInstancesServiceName, metrics, AppendDiagnostics(detail, result.Diagnostics)); + } + + private string FormatRuntimeArtifacts(ProbeResult result) + { + var applications = result.Applications; + var receiveLocations = applications.Sum(x => x.ReceiveLocationTotal); + var receiveLocationsDisabled = applications.Sum(x => x.ReceiveLocationDisabled); + var sendPorts = applications.Sum(x => x.SendPortTotal); + var sendPortsStarted = applications.Sum(x => x.SendPortStarted); + var sendPortsStopped = applications.Sum(x => x.SendPortStopped); + var sendPortsBound = applications.Sum(x => x.SendPortBound); + var sendPortsUnknown = applications.Sum(x => x.SendPortUnknown); + var orchestrations = applications.Sum(x => x.OrchestrationTotal); + var orchestrationsStarted = applications.Sum(x => x.OrchestrationStarted); + var orchestrationsStopped = applications.Sum(x => x.OrchestrationStopped); + var orchestrationsBound = applications.Sum(x => x.OrchestrationBound); + var orchestrationsUnbound = applications.Sum(x => x.OrchestrationUnbound); + var orchestrationsUnknown = applications.Sum(x => x.OrchestrationUnknown); + var state = DetermineRuntimeArtifactState( + receiveLocationsDisabled, + sendPortsStopped + sendPortsBound, + sendPortsUnknown, + orchestrationsStopped + orchestrationsBound + orchestrationsUnbound, + orchestrationsUnknown); + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_applications={0};;;0|biztalk_receive_locations={1};;;0|biztalk_receive_locations_disabled={2};;;0|biztalk_send_ports={3};;;0|biztalk_send_ports_started={4};;;0|biztalk_send_ports_stopped={5};;;0|biztalk_send_ports_bound={6};;;0|biztalk_send_ports_unknown={7};;;0|biztalk_orchestrations={8};;;0|biztalk_orchestrations_started={9};;;0|biztalk_orchestrations_stopped={10};;;0|biztalk_orchestrations_bound={11};;;0|biztalk_orchestrations_unbound={12};;;0|biztalk_orchestrations_unknown={13};;;0", + applications.Count, + receiveLocations, + receiveLocationsDisabled, + sendPorts, + sendPortsStarted, + sendPortsStopped, + sendPortsBound, + sendPortsUnknown, + orchestrations, + orchestrationsStarted, + orchestrationsStopped, + orchestrationsBound, + orchestrationsUnbound, + orchestrationsUnknown); + var detail = BuildRuntimeArtifactDetail( + applications, + receiveLocations, + receiveLocationsDisabled, + sendPorts, + sendPortsStarted, + sendPortsStopped, + sendPortsBound, + sendPortsUnknown, + orchestrations, + orchestrationsStarted, + orchestrationsStopped, + orchestrationsBound, + orchestrationsUnbound, + orchestrationsUnknown); + + return BuildLine(state, _options.RuntimeArtifactsServiceName, metrics, AppendDiagnostics(detail, result.Diagnostics)); + } + + private string FormatAggregate(ProbeResult result) + { + var instances = result.SuspendedInstances; + var resumable = instances.Count(x => x.Kind == SuspendedKind.Resumable); + var nonresumable = instances.Count(x => x.Kind == SuspendedKind.NonResumable); + var state = DetermineState(resumable, nonresumable); + var metrics = BuildMetrics(instances.Count, resumable, nonresumable); + var detail = BuildAggregateDetail(result, instances.Count, resumable, nonresumable); + + return BuildLine(state, _options.ServiceName, metrics, detail); + } + + private IEnumerable FormatApplications(ProbeResult result) + { + foreach (var group in result.SuspendedInstances.GroupBy(x => x.ApplicationName).OrderBy(x => x.Key)) + { + var resumable = group.Count(x => x.Kind == SuspendedKind.Resumable); + var nonresumable = group.Count(x => x.Kind == SuspendedKind.NonResumable); + var state = DetermineState(resumable, nonresumable); + var serviceName = _options.ServiceName + " " + group.Key; + var metrics = BuildMetrics(group.Count(), resumable, nonresumable); + var detail = group.Count() + " suspended instance(s): resumable=" + resumable + ", nonresumable=" + nonresumable; + yield return BuildLine(state, serviceName, metrics, detail); + } + } + + private CheckState DetermineState(int resumable, int nonresumable) + { + if (_options.CritNonResumableThreshold > 0 && nonresumable >= _options.CritNonResumableThreshold) + { + return CheckState.Critical; + } + + if (_options.WarnResumableThreshold > 0 && resumable >= _options.WarnResumableThreshold) + { + return CheckState.Warn; + } + + return CheckState.Ok; + } + + private CheckState DetermineHostState(int total, int stopped, int pending, int unknown) + { + if (total == 0) + { + return CheckState.Unknown; + } + + if (!_options.AlertOnStoppedHostInstances) + { + return unknown > 0 ? CheckState.Warn : CheckState.Ok; + } + + if (stopped > 0 || unknown > 0) + { + return CheckState.Critical; + } + + if (pending > 0) + { + return CheckState.Warn; + } + + return CheckState.Ok; + } + + private CheckState DetermineRuntimeArtifactState(int disabledReceiveLocations, int inactiveSendPorts, int unknownSendPorts, int inactiveOrchestrations, int unknownOrchestrations) + { + if (unknownSendPorts > 0 || unknownOrchestrations > 0) + { + return CheckState.Critical; + } + + if (_options.AlertOnArtifactRuntimeIssues && (disabledReceiveLocations > 0 || inactiveSendPorts > 0 || inactiveOrchestrations > 0)) + { + return CheckState.Warn; + } + + return CheckState.Ok; + } + + private string BuildMetrics(int total, int resumable, int nonresumable) + { + return string.Format( + CultureInfo.InvariantCulture, + "biztalk_suspended_total={0};;;0|biztalk_suspended_resumable={1};{2};;0|biztalk_suspended_nonresumable={3};;{4};0", + total, + resumable, + _options.WarnResumableThreshold, + nonresumable, + _options.CritNonResumableThreshold); + } + + private string BuildAggregateDetail(ProbeResult result, int total, int resumable, int nonresumable) + { + if (total == 0) + { + return AppendDiagnostics("No suspended BizTalk service instances found.", result.Diagnostics); + } + + var builder = new StringBuilder(); + builder.Append(total) + .Append(" suspended BizTalk service instance(s): resumable=") + .Append(resumable) + .Append(", nonresumable=") + .Append(nonresumable) + .Append(", applications=") + .Append(result.SuspendedInstances.Select(x => x.ApplicationName).Distinct(StringComparer.OrdinalIgnoreCase).Count()) + .Append(". "); + + builder.Append(BuildApplicationSummary(result.SuspendedInstances)); + AppendInstanceHints(builder, result.SuspendedInstances); + + return AppendDiagnostics(builder.ToString(), result.Diagnostics); + } + + private string BuildHostDetail(IEnumerable hosts, int total, int started, int stopped, int pending, int unknown) + { + if (total == 0) + { + return "No BizTalk host instances returned by WMI."; + } + + var detail = "total=" + total + + ", started=" + started + + ", stopped=" + stopped + + ", pending=" + pending + + ", unknown=" + unknown; + + var notStarted = hosts + .Where(x => x.ServiceState != HostStarted) + .OrderBy(x => x.RunningServer) + .ThenBy(x => x.HostName) + .ThenBy(x => x.InstanceName) + .Take(12) + .Select(x => EmptyAsUnknown(x.HostName) + "@" + EmptyAsUnknown(x.RunningServer) + "=" + FormatHostState(x.ServiceState)); + + var notStartedText = string.Join("; ", notStarted); + if (!string.IsNullOrWhiteSpace(notStartedText)) + { + detail += " | Not started: " + notStartedText; + } + + return detail; + } + + private string BuildRuntimeArtifactDetail( + IEnumerable applications, + int receiveLocations, + int receiveLocationsDisabled, + int sendPorts, + int sendPortsStarted, + int sendPortsStopped, + int sendPortsBound, + int sendPortsUnknown, + int orchestrations, + int orchestrationsStarted, + int orchestrationsStopped, + int orchestrationsBound, + int orchestrationsUnbound, + int orchestrationsUnknown) + { + var appList = applications.ToList(); + var detail = "applications=" + appList.Count + + " | ReceiveLocations total=" + receiveLocations + ", disabled=" + receiveLocationsDisabled + + " | SendPorts total=" + sendPorts + ", started=" + sendPortsStarted + ", stopped=" + sendPortsStopped + ", bound=" + sendPortsBound + ", unknown=" + sendPortsUnknown + + " | Orchestrations total=" + orchestrations + ", started=" + orchestrationsStarted + ", stopped=" + orchestrationsStopped + ", bound=" + orchestrationsBound + ", unbound=" + orchestrationsUnbound + ", unknown=" + orchestrationsUnknown; + + var notable = appList + .Where(HasRuntimeDifference) + .OrderByDescending(x => x.SendPortUnknown + x.OrchestrationUnknown) + .ThenByDescending(x => x.ReceiveLocationDisabled + x.SendPortStopped + x.SendPortBound + x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound) + .ThenBy(x => x.ApplicationName) + .Take(_options.MaxRuntimeApplicationDetails) + .Select(FormatApplicationRuntime); + + var notableText = string.Join("; ", notable); + if (!string.IsNullOrWhiteSpace(notableText)) + { + detail += " | Notable apps: " + notableText; + } + + return detail; + } + + private string BuildApplicationSummary(IEnumerable instances) + { + var groups = instances + .GroupBy(x => x.ApplicationName) + .OrderByDescending(x => x.Count(y => y.Kind == SuspendedKind.NonResumable)) + .ThenByDescending(x => x.Count()) + .ThenBy(x => x.Key) + .ToList(); + + var shown = groups.Take(_options.MaxApplicationDetails) + .Select(group => group.Key + " R=" + group.Count(x => x.Kind == SuspendedKind.Resumable) + " NR=" + group.Count(x => x.Kind == SuspendedKind.NonResumable)); + + var summary = string.Join("; ", shown); + if (groups.Count > _options.MaxApplicationDetails) + { + summary += "; +" + (groups.Count - _options.MaxApplicationDetails).ToString(CultureInfo.InvariantCulture) + " more application(s)"; + } + + return summary; + } + + private static void AppendInstanceHints(StringBuilder builder, IEnumerable instances) + { + var hints = instances + .OrderByDescending(x => x.Kind == SuspendedKind.NonResumable) + .ThenBy(x => x.ApplicationName) + .Take(5) + .Select(x => "host=" + EmptyAsUnknown(x.HostName) + ", service=" + EmptyAsUnknown(x.ServiceName) + ", id=" + EmptyAsUnknown(x.InstanceId)); + + builder.Append("\\nExamples: ").Append(string.Join(" | ", hints)); + } + + private static bool HasRuntimeDifference(ApplicationRuntimeState app) + { + return app.ReceiveLocationDisabled > 0 + || app.SendPortStopped > 0 + || app.SendPortBound > 0 + || app.SendPortUnknown > 0 + || app.OrchestrationStopped > 0 + || app.OrchestrationBound > 0 + || app.OrchestrationUnbound > 0 + || app.OrchestrationUnknown > 0; + } + + private static string FormatApplicationRuntime(ApplicationRuntimeState app) + { + return app.ApplicationName + + " RL disabled=" + app.ReceiveLocationDisabled + + ", SP stopped/bound/unknown=" + app.SendPortStopped + "/" + app.SendPortBound + "/" + app.SendPortUnknown + + ", Orch stopped/bound/unbound/unknown=" + app.OrchestrationStopped + "/" + app.OrchestrationBound + "/" + app.OrchestrationUnbound + "/" + app.OrchestrationUnknown; + } + + private static string FormatHostState(int state) + { + switch (state) + { + case HostStarted: return "Started"; + case HostStopped: return "Stopped"; + case HostStartPending: return "StartPending"; + case HostStopPending: return "StopPending"; + default: return "Unknown " + state; + } + } + + private static string AppendDiagnostics(string detail, IEnumerable diagnostics) + { + var diagnosticText = string.Join("; ", diagnostics.Where(x => !string.IsNullOrWhiteSpace(x))); + if (string.IsNullOrWhiteSpace(diagnosticText)) + { + return detail; + } + + return detail + "\\nDiagnostics: " + diagnosticText; + } + + private static string BuildLine(CheckState state, string serviceName, string metrics, string detail) + { + return ((int)state).ToString(CultureInfo.InvariantCulture) + + " \"" + + SanitizeServiceName(serviceName) + + "\" " + + (string.IsNullOrWhiteSpace(metrics) ? "-" : metrics) + + " " + + SanitizeDetail(detail); + } + + private static string SanitizeServiceName(string value) + { + var sanitized = string.IsNullOrWhiteSpace(value) ? "BizTalk Suspended Instances" : value.Trim(); + var invalid = new[] { ';', '~', '!', '$', '%', '^', '&', '*', '|', '\\', '\'', '"', '<', '>', '?', ',', '(', ')', '=', '`' }; + foreach (var item in invalid) + { + sanitized = sanitized.Replace(item, ' '); + } + + return CollapseSpaces(sanitized); + } + + private static string SanitizeDetail(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return "No details."; + } + + return value.Replace("\r\n", "\\n").Replace("\r", "\\n").Replace("\n", "\\n"); + } + + private static string EmptyAsUnknown(string value) + { + return string.IsNullOrWhiteSpace(value) ? "" : value.Trim(); + } + + private static string CollapseSpaces(string value) + { + var builder = new StringBuilder(); + var previousSpace = false; + foreach (var character in value) + { + if (char.IsWhiteSpace(character)) + { + if (!previousSpace) + { + builder.Append(' '); + previousSpace = true; + } + + continue; + } + + builder.Append(character); + previousSpace = false; + } + + return builder.ToString().Trim(); + } + } +} diff --git a/src/BizTalkSuspendedInstancesCheck/Models.cs b/src/BizTalkSuspendedInstancesCheck/Models.cs new file mode 100644 index 0000000..d43fd99 --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/Models.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; + +namespace BizTalkSuspendedInstancesCheck +{ + internal enum CheckState + { + Ok = 0, + Warn = 1, + Critical = 2, + Unknown = 3 + } + + internal enum SuspendedKind + { + Resumable, + NonResumable + } + + internal sealed class SuspendedInstance + { + public string ApplicationName { get; set; } + + public string ServiceName { get; set; } + + public string HostName { get; set; } + + public string ServiceClass { get; set; } + + public string InstanceId { get; set; } + + public string ErrorId { get; set; } + + public string ErrorCategory { get; set; } + + public string ErrorDescription { get; set; } + + public string SuspendTime { get; set; } + + public int ServiceStatus { get; set; } + + public SuspendedKind Kind { get; set; } + } + + internal sealed class HostInstanceState + { + public string InstanceName { get; set; } + + public string HostName { get; set; } + + public string RunningServer { get; set; } + + public int ServiceState { get; set; } + } + + internal sealed class ApplicationRuntimeState + { + public ApplicationRuntimeState() + { + ReceiveLocationTotal = 0; + SendPortTotal = 0; + OrchestrationTotal = 0; + } + + public string ApplicationName { get; set; } + + public int ReceiveLocationTotal { get; set; } + + public int ReceiveLocationDisabled { get; set; } + + public int SendPortTotal { get; set; } + + public int SendPortStarted { get; set; } + + public int SendPortStopped { get; set; } + + public int SendPortBound { get; set; } + + public int SendPortUnknown { get; set; } + + public int OrchestrationTotal { get; set; } + + public int OrchestrationStarted { get; set; } + + public int OrchestrationStopped { get; set; } + + public int OrchestrationBound { get; set; } + + public int OrchestrationUnbound { get; set; } + + public int OrchestrationUnknown { get; set; } + } + + internal sealed class ProbeResult + { + public ProbeResult() + { + SuspendedInstances = new List(); + HostInstances = new List(); + Applications = new List(); + Diagnostics = new List(); + } + + public List SuspendedInstances { get; private set; } + + public List HostInstances { get; private set; } + + public List Applications { get; private set; } + + public List Diagnostics { get; private set; } + } +} diff --git a/src/BizTalkSuspendedInstancesCheck/MonitoringOptions.cs b/src/BizTalkSuspendedInstancesCheck/MonitoringOptions.cs new file mode 100644 index 0000000..e867ceb --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/MonitoringOptions.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Globalization; + +namespace BizTalkSuspendedInstancesCheck +{ + internal sealed class MonitoringOptions + { + public MonitoringOptions() + { + ServiceName = "BizTalk Suspended Instances"; + HostInstancesServiceName = "BizTalk Host Instances"; + RuntimeArtifactsServiceName = "BizTalk Runtime Artifacts"; + Server = "."; + WarnResumableThreshold = 1; + CritNonResumableThreshold = 1; + QueryTimeoutSeconds = 25; + MaxApplicationDetails = 12; + MaxRuntimeApplicationDetails = 10; + AlertOnStoppedHostInstances = true; + AlertOnArtifactRuntimeIssues = false; + EmitPerApplicationServices = false; + } + + public string ServiceName { get; set; } + + public string HostInstancesServiceName { get; set; } + + public string RuntimeArtifactsServiceName { get; set; } + + public string Server { get; set; } + + public int WarnResumableThreshold { get; set; } + + public int CritNonResumableThreshold { get; set; } + + public int QueryTimeoutSeconds { get; set; } + + public int MaxApplicationDetails { get; set; } + + public int MaxRuntimeApplicationDetails { get; set; } + + public bool AlertOnStoppedHostInstances { get; set; } + + public bool AlertOnArtifactRuntimeIssues { get; set; } + + public bool EmitPerApplicationServices { get; set; } + + public bool SelfTest { get; set; } + + public static MonitoringOptions Load(string[] args) + { + var options = new MonitoringOptions + { + ServiceName = ReadString("ServiceName", "BizTalk Suspended Instances"), + HostInstancesServiceName = ReadString("HostInstancesServiceName", "BizTalk Host Instances"), + RuntimeArtifactsServiceName = ReadString("RuntimeArtifactsServiceName", "BizTalk Runtime Artifacts"), + Server = ReadString("Server", "."), + WarnResumableThreshold = ReadInt("WarnResumableThreshold", 1, 0, 1000000), + CritNonResumableThreshold = ReadInt("CritNonResumableThreshold", 1, 0, 1000000), + QueryTimeoutSeconds = ReadInt("QueryTimeoutSeconds", 25, 3, 300), + MaxApplicationDetails = ReadInt("MaxApplicationDetails", 12, 1, 100), + MaxRuntimeApplicationDetails = ReadInt("MaxRuntimeApplicationDetails", 10, 1, 100), + AlertOnStoppedHostInstances = ReadBool("AlertOnStoppedHostInstances", true), + AlertOnArtifactRuntimeIssues = ReadBool("AlertOnArtifactRuntimeIssues", false), + EmitPerApplicationServices = ReadBool("EmitPerApplicationServices", false) + }; + + ApplyArguments(options, args ?? new string[0]); + return options; + } + + private static void ApplyArguments(MonitoringOptions options, string[] args) + { + var map = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "--service-name", value => options.ServiceName = RequireValue("--service-name", value) }, + { "--host-service-name", value => options.HostInstancesServiceName = RequireValue("--host-service-name", value) }, + { "--runtime-service-name", value => options.RuntimeArtifactsServiceName = RequireValue("--runtime-service-name", value) }, + { "--server", value => options.Server = RequireValue("--server", value) }, + { "--warn-resumable", value => options.WarnResumableThreshold = ParseInt("--warn-resumable", value, 0, 1000000) }, + { "--crit-nonresumable", value => options.CritNonResumableThreshold = ParseInt("--crit-nonresumable", value, 0, 1000000) }, + { "--timeout-seconds", value => options.QueryTimeoutSeconds = ParseInt("--timeout-seconds", value, 3, 300) }, + { "--max-application-details", value => options.MaxApplicationDetails = ParseInt("--max-application-details", value, 1, 100) }, + { "--max-runtime-application-details", value => options.MaxRuntimeApplicationDetails = ParseInt("--max-runtime-application-details", value, 1, 100) } + }; + + for (var index = 0; index < args.Length; index++) + { + var arg = args[index]; + if (string.Equals(arg, "--self-test", StringComparison.OrdinalIgnoreCase)) + { + options.SelfTest = true; + continue; + } + + if (string.Equals(arg, "--emit-per-application-services", StringComparison.OrdinalIgnoreCase)) + { + options.EmitPerApplicationServices = true; + continue; + } + + if (string.Equals(arg, "--no-host-alerts", StringComparison.OrdinalIgnoreCase)) + { + options.AlertOnStoppedHostInstances = false; + continue; + } + + if (string.Equals(arg, "--alert-on-artifact-runtime-issues", StringComparison.OrdinalIgnoreCase)) + { + options.AlertOnArtifactRuntimeIssues = true; + continue; + } + + Action setter; + if (!map.TryGetValue(arg, out setter)) + { + throw new ArgumentException("Unknown argument: " + arg); + } + + if (index + 1 >= args.Length) + { + throw new ArgumentException("Missing value for argument: " + arg); + } + + setter(args[++index]); + } + } + + private static string ReadString(string key, string fallback) + { + var value = ConfigurationManager.AppSettings[key]; + return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim(); + } + + private static int ReadInt(string key, int fallback, int min, int max) + { + return ParseInt(key, ConfigurationManager.AppSettings[key], min, max, fallback); + } + + private static bool ReadBool(string key, bool fallback) + { + var value = ConfigurationManager.AppSettings[key]; + if (string.IsNullOrWhiteSpace(value)) + { + return fallback; + } + + bool parsed; + return bool.TryParse(value, out parsed) ? parsed : fallback; + } + + private static int ParseInt(string name, string value, int min, int max) + { + return ParseInt(name, value, min, max, null); + } + + private static int ParseInt(string name, string value, int min, int max, int? fallback) + { + if (string.IsNullOrWhiteSpace(value)) + { + if (fallback.HasValue) + { + return fallback.Value; + } + + throw new ArgumentException("Missing integer value for " + name); + } + + int parsed; + if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed)) + { + if (fallback.HasValue) + { + return fallback.Value; + } + + throw new ArgumentException("Invalid integer value for " + name + ": " + value); + } + + if (parsed < min || parsed > max) + { + throw new ArgumentOutOfRangeException(name, "Value must be between " + min + " and " + max + "."); + } + + return parsed; + } + + private static string RequireValue(string name, string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Missing value for " + name); + } + + return value.Trim(); + } + } +} diff --git a/src/BizTalkSuspendedInstancesCheck/Program.cs b/src/BizTalkSuspendedInstancesCheck/Program.cs new file mode 100644 index 0000000..6cb38c6 --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/Program.cs @@ -0,0 +1,43 @@ +using System; +using System.Text; + +namespace BizTalkSuspendedInstancesCheck +{ + internal static class Program + { + private static int Main(string[] args) + { + MonitoringOptions options = null; + + try + { + Console.OutputEncoding = new UTF8Encoding(false); + options = MonitoringOptions.Load(args); + var formatter = new CheckmkFormatter(options); + + if (options.SelfTest) + { + Console.WriteLine(formatter.FormatSelfTest()); + return 0; + } + + var probe = new WmiBizTalkProbe(options); + var result = probe.Query(); + foreach (var line in formatter.Format(result)) + { + Console.WriteLine(line); + } + + return 0; + } + catch (Exception ex) + { + var fallbackOptions = options ?? new MonitoringOptions(); + var formatter = new CheckmkFormatter(fallbackOptions); + Console.WriteLine(formatter.FormatUnknown("BizTalk suspended instance check failed: " + ex.GetType().Name + ": " + ex.Message)); + return 0; + } + } + } +} + diff --git a/src/BizTalkSuspendedInstancesCheck/WmiBizTalkProbe.cs b/src/BizTalkSuspendedInstancesCheck/WmiBizTalkProbe.cs new file mode 100644 index 0000000..b0d8c4b --- /dev/null +++ b/src/BizTalkSuspendedInstancesCheck/WmiBizTalkProbe.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Management; + +namespace BizTalkSuspendedInstancesCheck +{ + internal sealed class WmiBizTalkProbe + { + private const int SendPortBound = 1; + private const int SendPortStopped = 2; + private const int SendPortStarted = 3; + private const int OrchestrationUnbound = 1; + private const int OrchestrationBound = 2; + private const int OrchestrationStopped = 3; + private const int OrchestrationStarted = 4; + private const int SuspendedNonResumable = 32; + private const string NamespacePath = "root\\MicrosoftBizTalkServer"; + private const string UnknownApplication = "(Unknown Application)"; + private readonly MonitoringOptions _options; + + public WmiBizTalkProbe(MonitoringOptions options) + { + _options = options; + } + + public ProbeResult Query() + { + var result = new ProbeResult(); + var server = string.IsNullOrWhiteSpace(_options.Server) || _options.Server == "." + ? Environment.MachineName + : _options.Server.Trim(); + + var scope = new ManagementScope("\\\\" + server + "\\" + NamespacePath); + scope.Connect(); + + var index = BuildApplicationIndex(scope, result); + QueryHostInstances(scope, result); + QueryApplicationRuntime(scope, result); + ForEachObject(scope, "SELECT * FROM MSBTS_ServiceInstance WHERE ServiceStatus = 4 OR ServiceStatus = 32", item => + { + result.SuspendedInstances.Add(ToSuspendedInstance(item, index)); + }); + + return result; + } + + private ApplicationIndex BuildApplicationIndex(ManagementScope scope, ProbeResult result) + { + var index = new ApplicationIndex(); + AddArtifacts(scope, result, index, "MSBTS_SendPort"); + AddArtifacts(scope, result, index, "MSBTS_ReceivePort"); + AddArtifacts(scope, result, index, "MSBTS_ReceiveLocation"); + AddArtifacts(scope, result, index, "MSBTS_Orchestration"); + return index; + } + + private void AddArtifacts(ManagementScope scope, ProbeResult result, ApplicationIndex index, string className) + { + try + { + ForEachObject(scope, "SELECT * FROM " + className, item => + { + index.Add(item); + }); + } + catch (ManagementException ex) + { + result.Diagnostics.Add("Application mapping skipped for " + className + ": " + ex.Message); + } + catch (UnauthorizedAccessException ex) + { + result.Diagnostics.Add("Application mapping skipped for " + className + ": " + ex.Message); + } + } + + private void QueryHostInstances(ManagementScope scope, ProbeResult result) + { + ForEachObject(scope, "SELECT * FROM MSBTS_HostInstance", item => + { + result.HostInstances.Add(new HostInstanceState + { + InstanceName = FirstNonEmpty( + ApplicationIndex.SafeGetString(item, "InstanceName"), + ApplicationIndex.SafeGetString(item, "Name")), + HostName = ApplicationIndex.SafeGetString(item, "HostName"), + RunningServer = ApplicationIndex.SafeGetString(item, "RunningServer"), + ServiceState = SafeGetInt32(item, "ServiceState", 0) + }); + }); + } + + private void QueryApplicationRuntime(ManagementScope scope, ProbeResult result) + { + var applications = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ForEachObject(scope, "SELECT * FROM MSBTS_ReceiveLocation", item => + { + var app = GetApplication(applications, GetApplicationName(item)); + app.ReceiveLocationTotal++; + if (SafeGetBoolean(item, "IsDisabled", false)) + { + app.ReceiveLocationDisabled++; + } + }); + + ForEachObject(scope, "SELECT * FROM MSBTS_SendPort", item => + { + var app = GetApplication(applications, GetApplicationName(item)); + var status = SafeGetInt32(item, "Status", 0); + app.SendPortTotal++; + switch (status) + { + case SendPortStarted: + app.SendPortStarted++; + break; + case SendPortStopped: + app.SendPortStopped++; + break; + case SendPortBound: + app.SendPortBound++; + break; + default: + app.SendPortUnknown++; + break; + } + }); + + ForEachObject(scope, "SELECT * FROM MSBTS_Orchestration", item => + { + var app = GetApplication(applications, GetApplicationName(item)); + var status = SafeGetInt32(item, "OrchestrationStatus", 0); + app.OrchestrationTotal++; + switch (status) + { + case OrchestrationStarted: + app.OrchestrationStarted++; + break; + case OrchestrationStopped: + app.OrchestrationStopped++; + break; + case OrchestrationBound: + app.OrchestrationBound++; + break; + case OrchestrationUnbound: + app.OrchestrationUnbound++; + break; + default: + app.OrchestrationUnknown++; + break; + } + }); + + result.Applications.AddRange(applications.Values.OrderBy(x => x.ApplicationName)); + } + + private void ForEachObject(ManagementScope scope, string queryText, Action action) + { + var queryOptions = new EnumerationOptions + { + ReturnImmediately = true, + Rewindable = false, + Timeout = TimeSpan.FromSeconds(_options.QueryTimeoutSeconds), + Context = new ManagementNamedValueCollection() + }; + + queryOptions.Context.Add("IterationDelayMS", 10); + + var query = new ObjectQuery(queryText); + using (var searcher = new ManagementObjectSearcher(scope, query, queryOptions)) + using (var collection = searcher.Get()) + { + foreach (ManagementObject item in collection) + { + using (item) + { + action(item); + } + } + } + } + + private static SuspendedInstance ToSuspendedInstance(ManagementBaseObject item, ApplicationIndex index) + { + var status = SafeGetInt32(item, "ServiceStatus", 0); + return new SuspendedInstance + { + ApplicationName = index.Resolve(item), + ServiceName = ApplicationIndex.SafeGetString(item, "ServiceName"), + HostName = ApplicationIndex.SafeGetString(item, "HostName"), + ServiceClass = ApplicationIndex.SafeGetString(item, "ServiceClass"), + InstanceId = ApplicationIndex.SafeGetString(item, "InstanceID"), + ErrorId = ApplicationIndex.SafeGetString(item, "ErrorID"), + ErrorCategory = ApplicationIndex.SafeGetString(item, "ErrorCategory"), + ErrorDescription = ApplicationIndex.SafeGetString(item, "ErrorDescription"), + SuspendTime = ApplicationIndex.SafeGetString(item, "SuspendTime"), + ServiceStatus = status, + Kind = status == SuspendedNonResumable ? SuspendedKind.NonResumable : SuspendedKind.Resumable + }; + } + + private static int SafeGetInt32(ManagementBaseObject item, string propertyName, int fallback) + { + var value = ApplicationIndex.SafeGetString(item, propertyName); + if (string.IsNullOrWhiteSpace(value)) + { + return fallback; + } + + int parsed; + return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : fallback; + } + + private static bool SafeGetBoolean(ManagementBaseObject item, string propertyName, bool fallback) + { + var value = ApplicationIndex.SafeGetString(item, propertyName); + if (string.IsNullOrWhiteSpace(value)) + { + return fallback; + } + + bool parsed; + return bool.TryParse(value, out parsed) ? parsed : fallback; + } + + private static string GetApplicationName(ManagementBaseObject item) + { + return FirstNonEmpty( + ApplicationIndex.SafeGetString(item, "ApplicationName"), + ApplicationIndex.SafeGetString(item, "Application"), + ApplicationIndex.SafeGetString(item, "BizTalkApplication")) + ?? UnknownApplication; + } + + private static ApplicationRuntimeState GetApplication(Dictionary applications, string applicationName) + { + var normalized = string.IsNullOrWhiteSpace(applicationName) ? UnknownApplication : applicationName.Trim(); + ApplicationRuntimeState app; + if (!applications.TryGetValue(normalized, out app)) + { + app = new ApplicationRuntimeState { ApplicationName = normalized }; + applications.Add(normalized, app); + } + + return app; + } + + private static string FirstNonEmpty(params string[] values) + { + foreach (var value in values) + { + if (!string.IsNullOrWhiteSpace(value)) + { + return value.Trim(); + } + } + + return null; + } + } +}