From 7660dc4555b3d50a7db5438ba72ae0ec7eeba924 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Wed, 22 Jul 2026 11:34:28 +0200 Subject: [PATCH] Add BizTalk Checkmk Pulse local check --- .editorconfig | 13 + .gitignore | 8 + BizTalkCheckmkPulse.sln | 22 ++ Dokumentation.md | 265 +++++++++++++ README.md | 157 ++++++++ deployment/checkmk/biztalk_checkmk_pulse.cmd | 14 + docs/CheckmkServices.md | 144 +++++++ docs/ExampleOutput.md | 31 ++ docs/Integration.md | 71 ++++ scripts/build-release.cmd | 12 + scripts/package-release.cmd | 20 + src/BizTalkCheckmkPulse/App.config | 28 ++ .../BizTalkCheckmkPulse.csproj | 55 +++ .../CheckmkLocalFormatter.cs | 355 ++++++++++++++++++ src/BizTalkCheckmkPulse/EventLogProbe.cs | 69 ++++ src/BizTalkCheckmkPulse/Models.cs | 98 +++++ src/BizTalkCheckmkPulse/MonitoringOptions.cs | 149 ++++++++ src/BizTalkCheckmkPulse/Program.cs | 60 +++ src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs | 288 ++++++++++++++ src/BizTalkCheckmkPulse/WmiHelpers.cs | 60 +++ 20 files changed, 1919 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 BizTalkCheckmkPulse.sln create mode 100644 Dokumentation.md create mode 100644 README.md create mode 100644 deployment/checkmk/biztalk_checkmk_pulse.cmd create mode 100644 docs/CheckmkServices.md create mode 100644 docs/ExampleOutput.md create mode 100644 docs/Integration.md create mode 100644 scripts/build-release.cmd create mode 100644 scripts/package-release.cmd create mode 100644 src/BizTalkCheckmkPulse/App.config create mode 100644 src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj create mode 100644 src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs create mode 100644 src/BizTalkCheckmkPulse/EventLogProbe.cs create mode 100644 src/BizTalkCheckmkPulse/Models.cs create mode 100644 src/BizTalkCheckmkPulse/MonitoringOptions.cs create mode 100644 src/BizTalkCheckmkPulse/Program.cs create mode 100644 src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs create mode 100644 src/BizTalkCheckmkPulse/WmiHelpers.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f2608c --- /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 + +[*.md] +end_of_line = lf +trim_trailing_whitespace = false + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2844608 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/ +obj/ +artifacts/ +.vs/ +*.user +*.suo +*.nupkg + diff --git a/BizTalkCheckmkPulse.sln b/BizTalkCheckmkPulse.sln new file mode 100644 index 0000000..0558533 --- /dev/null +++ b/BizTalkCheckmkPulse.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}") = "BizTalkCheckmkPulse", "src\BizTalkCheckmkPulse\BizTalkCheckmkPulse.csproj", "{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.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..c45c85c --- /dev/null +++ b/Dokumentation.md @@ -0,0 +1,265 @@ +# Dokumentation: BizTalk Checkmk Pulse + +## Zielbild + +Ziel ist ein wartbares Monitoring fuer BizTalk Server 2020 in den Umgebungen `ACC`, `DEV`, `TST` und `PRD`. Jede Umgebung besitzt einen BizTalk Server 2020 und einen SQL Server. SQL Server wird mit dem Checkmk-eigenen MSSQL-Plugin ueberwacht; fuer BizTalk liefert dieses Projekt die fehlende fachliche und technische Laufzeitsicht. + +Das Monitoring soll: + +- auf jeder BizTalk-Maschine lokal laufen +- ohne PowerShell-Abhaengigkeit funktionieren +- keine BizTalk-DLLs im Build erzwingen +- Checkmk-2.4-kompatible Services und Metriken erzeugen +- service-discovery-freundlich und dashboard-tauglich sein +- bei Fehlern gueltige `UNKNOWN`-Services statt kaputter Agent-Ausgaben liefern + +## Technische Bewertung + +### Option A: Checkmk Local Check mit C#/.NET Framework + +Bewertung: empfohlen und umgesetzt. + +Vorteile: + +- Checkmk 2.4 unterstuetzt Local Checks direkt. +- Windows Server mit BizTalk 2020 bringt .NET Framework in der Regel passend mit. +- `System.Management` kann BizTalk-WMI lesen. +- Keine PowerShell Execution Policy, keine Script-Signing-Frage. +- Kein serverseitiges Checkmk-Python-Plugin notwendig. +- Rollout ist eine einfache Dateiablage unter `%ProgramData%\checkmk\agent\local`. + +Nachteile: + +- Schwellwerte sind in der `.exe.config`, nicht als Checkmk-Regelsatz in WATO. +- Eigene Graphing-Definitionen sind nicht enthalten; Checkmk zeigt Local-Check-Metriken trotzdem als Performance-Daten und Graphen. + +### Option B: Agent Plugin plus serverseitiges Checkmk-Plugin + +Bewertung: technisch elegant, aber fuer den ersten produktiven Schritt schwerer. + +Vorteile: + +- Checkmk-Regeln, Discovery und Metrikdefinitionen koennen sauber zentral modelliert werden. +- Bessere langfristige Erweiterbarkeit als MKP. + +Nachteile: + +- Checkmk-Check-API-Versionen muessen enger gepflegt werden. +- Server-seitige Installation in jeder Site erforderlich. +- Mehr Aufwand fuer Managed-Services-Betrieb und Updates. + +Empfehlung: als Version 2 dieses Projekts denkbar, wenn die Local-Check-Variante stabil in PRD laeuft und zentrale Regelsaetze wirklich benoetigt werden. + +### Option C: PowerShell Local Check + +Bewertung: nicht empfohlen fuer diese Umgebung. + +Vorteile: + +- Schnell zu schreiben. +- WMI/CIM-Zugriff ist komfortabel. + +Nachteile: + +- PowerShell ist in vielen Serverumgebungen eingeschraenkt oder signaturpflichtig. +- Ausfuehrungsverhalten im Checkmk-Agent-Kontext ist haeufiger fehleranfaellig. + +### Option D: BizTalk ExplorerOM/OperationsOM + +Bewertung: fachlich stark, deployseitig unnoetig schwer. + +Vorteile: + +- Hoehere BizTalk-Abstraktion als rohe WMI-Klassen. + +Nachteile: + +- BizTalk-DLL-Versionen muessen beim Build und teilweise zur Laufzeit passen. +- Build-Agenten brauchen BizTalk-Komponenten oder SDK-Dateien. +- Fuer die benoetigten Zustandsdaten reicht WMI aus. + +## Architektur + +```text +Checkmk Windows Agent + | + | startet lokale Checks aus %ProgramData%\checkmk\agent\local + v +biztalk_checkmk_pulse.cmd + | + | startet + v +BizTalkCheckmkPulse.exe + | + | liest lokal + +-- WMI root\MicrosoftBizTalkServer + +-- Windows Application Event Log + | + 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. + +## Datenquellen + +### BizTalk WMI Namespace + +Namespace: + +```text +root\MicrosoftBizTalkServer +``` + +Genutzte Klassen: + +| Klasse | Zweck | +| --- | --- | +| `MSBTS_GroupSetting` | BizTalk-Gruppe und Management-DB-Hinweise. | +| `MSBTS_MessageBoxSetting` | MessageBox-DB-Hinweise. | +| `MSBTS_HostInstance` | Host-Instance-Zustand. | +| `MSBTS_ServiceInstance` | Suspended service instances. | +| `MSBTS_ReceiveLocation` | Receive-Location-Zustand. | +| `MSBTS_SendPort` | Send-Port-Zustand. | +| `MSBTS_Orchestration` | Orchestration-Zustand. | +| `MSBTS_ReceivePort` | Application-Mapping fuer Details. | + +### Windows Application Event Log + +Der Check liest standardmaessig das lokale Application Log fuer die letzten 60 Minuten und filtert auf Quellen wie: + +- `BizTalk Server` +- `XLANG/s` +- `ENTSSO` +- `BizTalk Server Application` +- `BizTalk Server EDI` + +Die Liste ist ueber `EventLogSources` konfigurierbar. + +## Resilienz + +Das Plugin ist bewusst defensiv gebaut: + +- WMI-Queries haben ein konfigurierbares Timeout. +- 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. +- Fatal Errors erzeugen `UNKNOWN` fuer alle stabilen Services. +- Event-Log-Auswertung ist isoliert; ein Fehler dort bricht WMI-Monitoring nicht ab. +- Service-Namen sind stabil, damit Service Discovery nicht bei jedem Lauf neue Services erzeugt. + +## Statusmodell + +### BizTalk Platform + +- `OK`: BizTalk-WMI erreichbar. +- `UNKNOWN`: WMI nicht erreichbar oder kompletter Programmfehler. + +Dieser Service ist der Integrationsindikator. Wenn er `UNKNOWN` ist, sind Berechtigungen, BizTalk-Installation oder WMI-Repository zu pruefen. + +### BizTalk Suspended Instances + +- `OK`: keine suspendierten Instanzen. +- `WARN`: mindestens `WarnResumableThreshold` resumable suspended instances. +- `CRIT`: mindestens `CritNonResumableThreshold` non-resumable suspended instances. +- `UNKNOWN`: Datenquelle nicht lesbar. + +### BizTalk Host Instances + +- `OK`: alle Host-Instanzen sind started. +- `WARN`: mindestens eine Host-Instanz ist pending. +- `CRIT`: mindestens eine Host-Instanz ist stopped oder unknown. +- `UNKNOWN`: keine Host-Instanzen gefunden oder Datenquelle nicht lesbar. + +### BizTalk Runtime Artifacts + +- `OK`: Artefakte lesbar, keine unbekannten Statuswerte. +- `WARN`: nur wenn `AlertOnArtifactRuntimeIssues=true` und deaktivierte/inaktive Artefakte vorhanden sind. +- `CRIT`: unbekannte Send-Port- oder Orchestration-Statuswerte. + +Deaktivierte Receive Locations und gestoppte Ports koennen in BizTalk fachlich korrekt sein. Deshalb ist die Alarmierung hier standardmaessig informativ. + +### BizTalk Event Log + +- `OK`: keine relevanten Fehler/Warnungen ueber Schwellwert. +- `WARN`: Fehler oder Warnungen ab `EventLogWarnThreshold`. +- `CRIT`: Fehler ab `EventLogCritThreshold`. +- `UNKNOWN`: Event Log nicht lesbar. + +## Dashboard-Empfehlung + +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` +- 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 + +## Rollout-Vorgehen + +1. Build-Paket erzeugen. +2. In `DEV` auf dem BizTalk-Server installieren. +3. `--self-test` und normalen Lauf ausfuehren. +4. Agent-Dump pruefen. +5. Checkmk Discovery durchfuehren. +6. Eine Woche Messwerte und false positives beobachten. +7. Nach `TST` und `ACC` uebernehmen. +8. In `PRD` mit `AlertOnArtifactRuntimeIssues=false` starten. +9. Nach Betriebsfreigabe Schwellwerte feinjustieren. + +## Troubleshooting + +### Service bleibt UNKNOWN + +Pruefen: + +```cmd +"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" +``` + +Wenn WMI nicht erreichbar ist: + +```cmd +wmic /namespace:\\root\MicrosoftBizTalkServer path MSBTS_HostInstance get HostName,RunningServer,ServiceState +``` + +Der Checkmk Windows Agent laeuft normalerweise als LocalSystem. Daher immer auch den Agent-Dump verwenden: + +```cmd +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump +``` + +### Keine Services in Discovery + +Pruefen: + +- Liegt `biztalk_checkmk_pulse.cmd` direkt unter `%ProgramData%\checkmk\agent\local`? +- Gibt der Wrapper direkt eine Zeile im Format `0 "Service" metric=value Details` aus? +- Wurde der Checkmk-Agent nach Policy-/Bakery-Aenderungen neu ausgerollt? + +### Runtime Artifacts zeigt deaktivierte Artefakte + +Das ist standardmaessig `OK`, damit gewollt deaktivierte BizTalk-Artefakte nicht alarmieren. Fuer strengere PRD-Standards: + +```xml + +``` + +## Weiterentwicklung + +Sinnvolle naechste Ausbaustufen: + +- MKP mit Agent-Bakery-Regel fuer zentrale Konfiguration. +- Optionales serverseitiges Check-Plugin nach Checkmk Check API V2. +- Custom Dashboard/View als Checkmk GUI Extension. +- Ergaenzung um MessageBox-Spool/Tracking-Daten, falls operativ benoetigt. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cd747d --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# BizTalk Checkmk Pulse + +`BizTalk Checkmk Pulse` ist ein lokaler Checkmk-Check fuer Microsoft BizTalk Server 2020 auf Windows. Er liest BizTalk-Betriebsdaten ueber WMI aus `root\MicrosoftBizTalkServer`, erzeugt Checkmk-Local-Check-Ausgaben und kann dadurch ohne serverseitigen Python-Check in Checkmk 2.4 integriert werden. + +Der Ansatz ist fuer Umgebungen wie `ACC`, `DEV`, `TST` und `PRD` gedacht, in denen pro Umgebung ein BizTalk Server 2020 und ein SQL Server betrieben werden. SQL Server bleibt beim mitgelieferten Checkmk-MSSQL-Plugin; dieses Projekt ergaenzt die BizTalk-spezifische Sicht. + +## Warum Local Check statt serverseitigem Check-Plugin? + +Checkmk 2.4 kann Windows-Agent-Plugins und Local Checks direkt ausfuehren. Fuer diese Umgebung ist ein C#/.NET-Framework-Programm mit `.cmd`-Wrapper die robusteste Variante: + +- keine PowerShell-Ausfuehrung erforderlich +- keine BizTalk-ExplorerOM/OperationsOM-DLL als Build-Abhaengigkeit +- keine Python-Check-API-Abhaengigkeit auf der Checkmk-Site +- einfache Verteilung auf die BizTalk-Maschinen per Dateiablage, Softwareverteilung oder Agent Bakery +- automatische Service Discovery in Checkmk + +## Erzeugte Checkmk-Services + +Standardmaessig entstehen diese stabilen Services: + +- `BizTalk Platform` +- `BizTalk Suspended Instances` +- `BizTalk Host Instances` +- `BizTalk Runtime Artifacts` +- `BizTalk Event Log` + +Wenn `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` gesetzt wird, wird der Name vor den Suffix gesetzt, z.B. `BizTalk PRD Suspended Instances`. Das ist praktisch, wenn die Umgebung bereits im Hostnamen oder Ordner abgebildet ist, aber nicht zwingend noetig. + +Details zu Statuslogik und Metriken stehen in [docs/CheckmkServices.md](docs/CheckmkServices.md). +Beispielausgaben stehen in [docs/ExampleOutput.md](docs/ExampleOutput.md). + +## Build + +Voraussetzungen auf einem Windows-Build-Host: + +- Visual Studio 2022 Build Tools oder Visual Studio +- MSBuild im `PATH` +- .NET Framework 4.7.2 Developer Pack + +Build: + +```cmd +scripts\build-release.cmd +``` + +Deployment-Paket erstellen: + +```cmd +scripts\package-release.cmd +``` + +Ergebnis: + +```text +artifacts\BizTalkCheckmkPulse-deploy\ + biztalk_checkmk_pulse.cmd + BizTalkCheckmkPulse\ + BizTalkCheckmkPulse.exe + BizTalkCheckmkPulse.exe.config +``` + +Validierung nach dem Build: + +```cmd +artifacts\BizTalkCheckmkPulse-deploy\biztalk_checkmk_pulse.cmd --self-test +``` + +## Installation auf dem BizTalk-Server + +Kopiere den Inhalt von `artifacts\BizTalkCheckmkPulse-deploy` nach: + +```text +%ProgramData%\checkmk\agent\local +``` + +Zielstruktur: + +```text +%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd +%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe +%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config +``` + +Manueller Test auf dem BizTalk-Server: + +```cmd +"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test +"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" +``` + +Agent-Ausgabe wie Checkmk sie sieht: + +```cmd +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump +``` + +Danach in Checkmk fuer den BizTalk-Host eine Service Discovery ausfuehren, die neuen Services aufnehmen und Changes aktivieren. + +## Konfiguration + +Die Konfiguration liegt neben der EXE: + +```text +%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config +``` + +Wichtige Werte: + +| Key | Default | Bedeutung | +| --- | --- | --- | +| `Server` | `.` | Lokaler BizTalk-Server. Remote-WMI ist moeglich, aber nicht empfohlen. | +| `ServicePrefix` | `BizTalk` | Prefix fuer alle Checkmk-Services. | +| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST` oder `PRD`. | +| `QueryTimeoutSeconds` | `25` | WMI-Timeout pro Query. | +| `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. | +| `EmitPerApplicationSuspensionServices` | `false` | Erzeugt zusaetzliche Services pro Anwendung mit Suspensions. | +| `ProbeEventLog` | `true` | Liest das Windows Application Log nach BizTalk-bezogenen Sources. | +| `EventLogLookbackMinutes` | `60` | Zeitraum fuer Event-Log-Auswertung. | +| `EventLogWarnThreshold` | `1` | WARN ab n Errors oder Warnings. | +| `EventLogCritThreshold` | `10` | CRIT ab n Errors. | + +Empfehlung: + +- In `DEV`, `TST`, `ACC`: `AlertOnArtifactRuntimeIssues=false`, damit bewusst gestoppte Artefakte nicht rauschen. +- In `PRD`: erst zwei Wochen beobachten; danach nur aktivieren, wenn die Runtime-Artefakte wirklich als Betriebsstandard immer aktiv sein muessen. +- `EmitPerApplicationSuspensionServices=true` nur verwenden, wenn Anwendungsteams eigene Services benoetigen. Sonst bleibt die Discovery schlanker. + +## Integration in Checkmk Managed Services Edition 2.4 + +Manuelle Integration: + +1. Dateien auf dem BizTalk-Server nach `%ProgramData%\checkmk\agent\local` kopieren. +2. Optional `BizTalkCheckmkPulse.exe.config` je Umgebung anpassen. +3. Agent-Dump pruefen. +4. Service Discovery auf dem BizTalk-Host ausfuehren. +5. Services in ein BizTalk-Dashboard aufnehmen. + +Integration ueber Agent Bakery: + +1. Deployment-Dateien in der Checkmk-Site als Custom-Agent-Dateien bereitstellen. +2. Windows-Agent-Regel fuer die BizTalk-Hosts erstellen. +3. Agent backen und auf ACC/DEV/TST/PRD-BizTalk-Hosts ausrollen. +4. Discovery und Dashboard-Aufnahme durchfuehren. + +Die Checkmk-Dokumentation nennt fuer Windows Local Checks `%ProgramData%\checkmk\agent\local` und fuer Windows Agent Plugins `%ProgramData%\checkmk\agent\plugins`. Dieses Projekt nutzt bewusst `local`, weil der Zustand direkt vom Host berechnet und sofort als Checkmk-Service geliefert wird. + +## Quellen + +- Checkmk Local Checks: https://docs.checkmk.com/latest/en/localchecks.html +- Checkmk Windows Agent und Plugin-Pfade: https://docs.checkmk.com/latest/en/agent_windows.html +- Checkmk Agent-Based Plugin-Entwicklung: https://docs.checkmk.com/latest/en/devel_check_plugins.html +- Checkmk Bakery API: https://docs.checkmk.com/latest/en/bakery_api.html +- Checkmk MKP-Pakete: https://docs.checkmk.com/latest/en/mkps.html +- Microsoft BizTalk WMI `MSBTS_ServiceInstance`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-wmi +- Microsoft BizTalk WMI `ServiceStatus`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-servicestatus-property-wmi diff --git a/deployment/checkmk/biztalk_checkmk_pulse.cmd b/deployment/checkmk/biztalk_checkmk_pulse.cmd new file mode 100644 index 0000000..0205c59 --- /dev/null +++ b/deployment/checkmk/biztalk_checkmk_pulse.cmd @@ -0,0 +1,14 @@ +@echo off +setlocal + +set "BASE=%~dp0BizTalkCheckmkPulse" +set "EXE=%BASE%\BizTalkCheckmkPulse.exe" + +if not exist "%EXE%" ( + echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe not found below %BASE% + exit /b 0 +) + +"%EXE%" %* +exit /b 0 + diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md new file mode 100644 index 0000000..342bb84 --- /dev/null +++ b/docs/CheckmkServices.md @@ -0,0 +1,144 @@ +# Checkmk Services und Metriken + +Alle Services werden als Checkmk Local Checks ausgegeben. Das Format ist: + +```text + "" +``` + +Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt: zwischen den vier Feldern steht genau ein normales Leerzeichen. + +## BizTalk Platform + +Zweck: + +- Prueft, ob der BizTalk-WMI-Namespace erreichbar ist. +- Zeigt Server, Gruppe, Management-DB und MessageBox-DB, soweit WMI diese Werte liefert. + +Status: + +| Status | Bedingung | +| --- | --- | +| `OK` | WMI-Verbindung erfolgreich. | +| `UNKNOWN` | WMI-Verbindung oder Programmstart fehlgeschlagen. | + +Metriken: + +- keine + +## BizTalk Suspended Instances + +Zweck: + +- Ermittelt suspendierte BizTalk-Service-Instanzen. +- Unterscheidet resumable und non-resumable. +- Zeigt betroffene Anwendungen und Beispiele in der Summary. + +Status: + +| Status | Bedingung | +| --- | --- | +| `OK` | keine suspendierten Instanzen. | +| `WARN` | `biztalk_suspended_resumable >= WarnResumableThreshold`. | +| `CRIT` | `biztalk_suspended_nonresumable >= CritNonResumableThreshold`. | +| `UNKNOWN` | WMI oder Programmfehler. | + +Metriken: + +| Metrik | Bedeutung | +| --- | --- | +| `biztalk_suspended_total` | Summe aller suspendierten Instanzen. | +| `biztalk_suspended_resumable` | Resumable suspended instances. | +| `biztalk_suspended_nonresumable` | Non-resumable suspended instances. | + +## BizTalk Host Instances + +Zweck: + +- Ueberwacht BizTalk Host Instances auf dem BizTalk-Server. +- Nicht gestartete Host Instances sind normalerweise betriebsrelevant. + +Status: + +| Status | Bedingung | +| --- | --- | +| `OK` | alle Host Instances sind started. | +| `WARN` | mindestens eine Host Instance ist pending. | +| `CRIT` | mindestens eine Host Instance ist stopped oder unknown. | +| `UNKNOWN` | keine Host Instances gefunden oder WMI/Programmfehler. | + +Metriken: + +| Metrik | Bedeutung | +| --- | --- | +| `biztalk_host_instances_total` | Gesamtzahl Host Instances. | +| `biztalk_host_instances_started` | Gestartete Host Instances. | +| `biztalk_host_instances_stopped` | Gestoppte Host Instances. | +| `biztalk_host_instances_pending` | StartPending/StopPending. | +| `biztalk_host_instances_unknown` | Nicht interpretierbarer Status. | + +## BizTalk Runtime Artifacts + +Zweck: + +- Summiert Laufzeitzustaende von Receive Locations, Send Ports und Orchestrations. +- Liefert dashboard-taugliche Zahlen zur Laufzeitlandschaft. + +Status: + +| Status | Bedingung | +| --- | --- | +| `OK` | Artefakte lesbar; keine unbekannten Statuswerte. | +| `WARN` | Nur bei `AlertOnArtifactRuntimeIssues=true` und inaktiven/deaktivierten Artefakten. | +| `CRIT` | Unbekannte Send-Port- oder Orchestration-Statuswerte. | +| `UNKNOWN` | Programmfehler. | + +Metriken: + +| Metrik | Bedeutung | +| --- | --- | +| `biztalk_applications` | Anzahl erkannter BizTalk-Anwendungen. | +| `biztalk_receive_locations` | Gesamtzahl Receive Locations. | +| `biztalk_receive_locations_disabled` | Deaktivierte Receive Locations. | +| `biztalk_send_ports` | Gesamtzahl Send Ports. | +| `biztalk_send_ports_started` | Gestartete Send Ports. | +| `biztalk_send_ports_inactive` | Bound oder stopped Send Ports. | +| `biztalk_send_ports_unknown` | Nicht interpretierbarer Send-Port-Status. | +| `biztalk_orchestrations` | Gesamtzahl Orchestrations. | +| `biztalk_orchestrations_started` | Gestartete Orchestrations. | +| `biztalk_orchestrations_inactive` | Bound, unbound oder stopped Orchestrations. | +| `biztalk_orchestrations_unknown` | Nicht interpretierbarer Orchestration-Status. | + +## BizTalk Event Log + +Zweck: + +- Erfasst BizTalk-bezogene Errors und Warnings aus dem Windows Application Log. +- Ergaenzt WMI-Zustandsdaten um aktuelle Laufzeitereignisse. + +Status: + +| Status | Bedingung | +| --- | --- | +| `OK` | Schwellwerte nicht erreicht. | +| `WARN` | Errors oder Warnings ab `EventLogWarnThreshold`. | +| `CRIT` | Errors ab `EventLogCritThreshold`. | +| `UNKNOWN` | Application Event Log nicht lesbar. | + +Metriken: + +| Metrik | Bedeutung | +| --- | --- | +| `biztalk_eventlog_errors` | BizTalk-bezogene Errors im Lookback-Fenster. | +| `biztalk_eventlog_warnings` | BizTalk-bezogene Warnings im Lookback-Fenster. | + +## Optionale per-Application-Services + +Wenn `EmitPerApplicationSuspensionServices=true` gesetzt ist, werden zusaetzliche Services fuer Anwendungen erzeugt, die suspendierte Instanzen haben: + +```text +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 new file mode 100644 index 0000000..493288d --- /dev/null +++ b/docs/ExampleOutput.md @@ -0,0 +1,31 @@ +# Beispielausgaben + +## Self-Test + +```text +0 "BizTalk Platform" - Self test OK. Checkmk local check output is valid. +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 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 +0 "BizTalk Event Log" biztalk_eventlog_errors=0;1;10;0|biztalk_eventlog_warnings=0;1;;0 BizTalk-related Application log entries in last 60 minutes: errors=0, warnings=0 +``` + +## Suspended Instances + +```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=OrderProcessing(R=2, NR=1); examples=Send.Invoice on ProcessingHost +``` + +## Fehlendes Deployment + +```text +3 "BizTalk Platform" - BizTalkCheckmkPulse.exe not found below C:\ProgramData\checkmk\agent\local\BizTalkCheckmkPulse +``` + diff --git a/docs/Integration.md b/docs/Integration.md new file mode 100644 index 0000000..5cfda8e --- /dev/null +++ b/docs/Integration.md @@ -0,0 +1,71 @@ +# Integration in Checkmk + +## Zielstruktur auf dem BizTalk-Server + +```text +%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd +%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe +%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config +``` + +## Manuelle Installation + +1. Release-Paket auf den BizTalk-Server kopieren. +2. Inhalt nach `%ProgramData%\checkmk\agent\local` kopieren. +3. Optional `EnvironmentName` in der `.exe.config` setzen. +4. Test ausfuehren: + +```cmd +"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test +"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" +``` + +5. Agent-Ausgabe pruefen: + +```cmd +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump +``` + +6. In Checkmk: + +- Host oeffnen +- Service Discovery ausfuehren +- gefundene `BizTalk ...` Services aufnehmen +- Changes aktivieren + +## Agent Bakery + +Checkmk Managed Services Edition 2.4 enthaelt die kommerziellen Mechanismen fuer Agent Bakery. Fuer einen sauberen Rollout: + +1. Deployment-Dateien als Custom Files oder ueber ein spaeteres MKP bereitstellen. +2. Regel nur auf BizTalk-Hosts anwenden, z.B. Host-Tag `application:biztalk`. +3. Gebackenen Windows-Agenten fuer die BizTalk-Hosts installieren. +4. Service Discovery ausfuehren. + +Hinweis: Dieses Repository enthaelt bewusst noch kein Bakery-Python-Plugin. Die manuelle bzw. dateibasierte Verteilung ist die risikoarme erste Version. Eine Bakery-Erweiterung ist eine sinnvolle Version-2-Ausbaustufe. + +## Empfohlene Host-Struktur + +Host-Tags oder Ordner: + +- `env:ACC` +- `env:DEV` +- `env:TST` +- `env:PRD` +- `app:biztalk` + +Service-Filter fuer Views: + +```text +Service starts with: BizTalk +``` + +Dashboard-Kacheln: + +- Host/Service state fuer BizTalk-Host +- Service state fuer `BizTalk Suspended Instances` +- Graph `biztalk_suspended_total` +- Graph `biztalk_host_instances_stopped` +- Graph `biztalk_eventlog_errors` +- MSSQL-Services des SQL-Servers daneben + diff --git a/scripts/build-release.cmd b/scripts/build-release.cmd new file mode 100644 index 0000000..f166915 --- /dev/null +++ b/scripts/build-release.cmd @@ -0,0 +1,12 @@ +@echo off +setlocal + +where msbuild.exe >nul 2>nul +if errorlevel 1 ( + echo MSBuild.exe was not found in PATH. Run this from a Visual Studio Developer Command Prompt. + exit /b 1 +) + +msbuild BizTalkCheckmkPulse.sln /p:Configuration=Release /p:Platform="Any CPU" /m /v:minimal +exit /b %ERRORLEVEL% + diff --git a/scripts/package-release.cmd b/scripts/package-release.cmd new file mode 100644 index 0000000..1940db2 --- /dev/null +++ b/scripts/package-release.cmd @@ -0,0 +1,20 @@ +@echo off +setlocal + +call "%~dp0build-release.cmd" +if errorlevel 1 exit /b 1 + +set "ROOT=%~dp0.." +set "OUT=%ROOT%\artifacts\BizTalkCheckmkPulse-deploy" +set "APP=%OUT%\BizTalkCheckmkPulse" + +if exist "%OUT%" rmdir /s /q "%OUT%" +mkdir "%APP%" + +copy "%ROOT%\deployment\checkmk\biztalk_checkmk_pulse.cmd" "%OUT%\" >nul +copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe" "%APP%\" >nul +copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe.config" "%APP%\" >nul + +echo Deployment package written to %OUT% +exit /b 0 + diff --git a/src/BizTalkCheckmkPulse/App.config b/src/BizTalkCheckmkPulse/App.config new file mode 100644 index 0000000..03970a9 --- /dev/null +++ b/src/BizTalkCheckmkPulse/App.config @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj new file mode 100644 index 0000000..d514d37 --- /dev/null +++ b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {A4D4D050-9EA7-4A71-B510-7D9D699B9F38} + Exe + BizTalkCheckmkPulse + BizTalkCheckmkPulse + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs new file mode 100644 index 0000000..f45e2bd --- /dev/null +++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace BizTalkCheckmkPulse +{ + internal sealed class CheckmkLocalFormatter + { + private const int HostStopped = 1; + private const int HostStartPending = 2; + private const int HostStopPending = 3; + private const int HostStarted = 4; + private const int HostContinuePending = 5; + private const int HostPausePending = 6; + private const int HostPaused = 7; + private const int HostUnknown = 8; + private readonly MonitoringOptions _options; + + public CheckmkLocalFormatter(MonitoringOptions options) + { + _options = options; + } + + public IEnumerable Format(ProbeResult result) + { + yield return FormatPlatform(result); + yield return FormatSuspendedInstances(result); + yield return FormatHostInstances(result); + yield return FormatRuntimeArtifacts(result); + yield return FormatEventLog(result); + + if (_options.EmitPerApplicationSuspensionServices) + { + foreach (var line in FormatApplicationSuspensions(result)) + { + yield return line; + } + } + } + + 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("Suspended Instances"), "biztalk_suspended_total=0;;;0", "Self test OK."); + } + + 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); + } + + private string FormatPlatform(ProbeResult result) + { + var state = result.Platform.WmiConnected ? CheckState.Ok : CheckState.Unknown; + var detail = new StringBuilder(); + detail.Append(result.Platform.WmiConnected ? "BizTalk WMI reachable" : "BizTalk WMI not reachable"); + 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)); + } + + private string FormatSuspendedInstances(ProbeResult result) + { + 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); + var state = DetermineSuspensionState(resumable, nonresumable); + var metrics = 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); + + 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); + } + + private string FormatHostInstances(ProbeResult result) + { + 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); + var pending = result.HostInstances.Count(x => x.ServiceState == HostStartPending || x.ServiceState == HostStopPending || x.ServiceState == HostContinuePending || x.ServiceState == HostPausePending); + var unknown = total - started - stopped - pending; + var state = total == 0 ? CheckState.Unknown : stopped > 0 || unknown > 0 ? CheckState.Critical : pending > 0 ? CheckState.Warning : CheckState.Ok; + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_host_instances_total={0};;;0|biztalk_host_instances_started={1};;;0|biztalk_host_instances_stopped={2};;1;0|biztalk_host_instances_pending={3};1;;0|biztalk_host_instances_unknown={4};;1;0", + total, + started, + stopped, + pending, + unknown); + + var detail = new StringBuilder(); + detail.Append("Host instances total=").Append(total) + .Append(", started=").Append(started) + .Append(", stopped=").Append(stopped) + .Append(", pending=").Append(pending) + .Append(", unknown=").Append(unknown); + + var affected = result.HostInstances + .Where(x => x.ServiceState != HostStarted) + .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()); + } + + private string FormatRuntimeArtifacts(ProbeResult result) + { + var receiveLocations = result.Applications.Sum(x => x.ReceiveLocationTotal); + var receiveLocationsDisabled = result.Applications.Sum(x => x.ReceiveLocationDisabled); + var sendPorts = result.Applications.Sum(x => x.SendPortTotal); + var sendPortsStarted = result.Applications.Sum(x => x.SendPortStarted); + var sendPortsInactive = result.Applications.Sum(x => x.SendPortStopped + x.SendPortBound); + var sendPortsUnknown = result.Applications.Sum(x => x.SendPortUnknown); + var orchestrations = result.Applications.Sum(x => x.OrchestrationTotal); + var orchestrationsStarted = result.Applications.Sum(x => x.OrchestrationStarted); + var orchestrationsInactive = result.Applications.Sum(x => x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound); + var orchestrationsUnknown = result.Applications.Sum(x => x.OrchestrationUnknown); + var state = sendPortsUnknown > 0 || orchestrationsUnknown > 0 + ? CheckState.Critical + : _options.AlertOnArtifactRuntimeIssues && (receiveLocationsDisabled > 0 || sendPortsInactive > 0 || orchestrationsInactive > 0) + ? CheckState.Warning + : CheckState.Ok; + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_applications={0};;;0|biztalk_receive_locations={1};;;0|biztalk_receive_locations_disabled={2};1;;0|biztalk_send_ports={3};;;0|biztalk_send_ports_started={4};;;0|biztalk_send_ports_inactive={5};1;;0|biztalk_send_ports_unknown={6};;1;0|biztalk_orchestrations={7};;;0|biztalk_orchestrations_started={8};;;0|biztalk_orchestrations_inactive={9};1;;0|biztalk_orchestrations_unknown={10};;1;0", + result.Applications.Count, + receiveLocations, + receiveLocationsDisabled, + sendPorts, + sendPortsStarted, + sendPortsInactive, + sendPortsUnknown, + orchestrations, + orchestrationsStarted, + orchestrationsInactive, + orchestrationsUnknown); + + var detail = new StringBuilder(); + detail.Append("Applications=").Append(result.Applications.Count) + .Append("; receive_locations total=").Append(receiveLocations).Append(", disabled=").Append(receiveLocationsDisabled) + .Append("; send_ports total=").Append(sendPorts).Append(", started=").Append(sendPortsStarted).Append(", inactive=").Append(sendPortsInactive).Append(", unknown=").Append(sendPortsUnknown) + .Append("; orchestrations total=").Append(orchestrations).Append(", started=").Append(orchestrationsStarted).Append(", inactive=").Append(orchestrationsInactive).Append(", unknown=").Append(orchestrationsUnknown); + + var affected = result.Applications + .Where(x => x.ReceiveLocationDisabled > 0 || x.SendPortStopped + x.SendPortBound + x.SendPortUnknown > 0 || x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound + x.OrchestrationUnknown > 0) + .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()); + } + + private string FormatEventLog(ProbeResult result) + { + if (!_options.ProbeEventLog) + { + return BuildLine(CheckState.Ok, _options.ServiceName("Event Log"), "-", "Event log probe disabled."); + } + + if (!result.EventLog.Available) + { + return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", "Application event log could not be read: " + result.EventLog.Failure); + } + + var state = _options.EventLogCritThreshold > 0 && result.EventLog.Errors >= _options.EventLogCritThreshold + ? CheckState.Critical + : _options.EventLogWarnThreshold > 0 && (result.EventLog.Errors >= _options.EventLogWarnThreshold || result.EventLog.Warnings >= _options.EventLogWarnThreshold) + ? CheckState.Warning + : CheckState.Ok; + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_eventlog_errors={0};{1};{2};0|biztalk_eventlog_warnings={3};{1};;0", + result.EventLog.Errors, + _options.EventLogWarnThreshold, + _options.EventLogCritThreshold, + result.EventLog.Warnings); + var detail = string.Format( + CultureInfo.InvariantCulture, + "BizTalk-related Application log entries in last {0} minutes: errors={1}, warnings={2}", + _options.EventLogLookbackMinutes, + result.EventLog.Errors, + result.EventLog.Warnings); + + return BuildLine(state, _options.ServiceName("Event Log"), metrics, detail); + } + + private IEnumerable FormatApplicationSuspensions(ProbeResult result) + { + foreach (var group in result.SuspendedInstances.GroupBy(x => x.ApplicationName).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) + { + var resumable = group.Count(x => x.Kind == SuspendedKind.Resumable); + var nonresumable = group.Count(x => x.Kind == SuspendedKind.NonResumable); + var state = DetermineSuspensionState(resumable, nonresumable); + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_suspended_total={0};;;0|biztalk_suspended_resumable={1};{2};;0|biztalk_suspended_nonresumable={3};;{4};0", + group.Count(), + resumable, + _options.WarnResumableThreshold, + nonresumable, + _options.CritNonResumableThreshold); + + yield return BuildLine(state, _options.ServiceName("Suspended " + group.Key), metrics, "Suspended instances: resumable=" + resumable + ", nonresumable=" + nonresumable); + } + } + + private CheckState DetermineSuspensionState(int resumable, int nonresumable) + { + if (_options.CritNonResumableThreshold > 0 && nonresumable >= _options.CritNonResumableThreshold) + { + return CheckState.Critical; + } + + if (_options.WarnResumableThreshold > 0 && resumable >= _options.WarnResumableThreshold) + { + return CheckState.Warning; + } + + return CheckState.Ok; + } + + private string BuildSuspensionDetail(IEnumerable instances, int total, int resumable, int nonresumable) + { + var builder = new StringBuilder(); + builder.Append(total).Append(" suspended BizTalk service instance(s): resumable=").Append(resumable) + .Append(", nonresumable=").Append(nonresumable); + + var apps = instances + .GroupBy(x => x.ApplicationName) + .OrderByDescending(x => x.Count()) + .Take(_options.MaxSummaryItems) + .Select(x => x.Key + "(R=" + x.Count(y => y.Kind == SuspendedKind.Resumable) + ", NR=" + x.Count(y => y.Kind == SuspendedKind.NonResumable) + ")"); + AppendList(builder, "applications", apps); + + var examples = instances + .OrderByDescending(x => x.SuspendTime ?? DateTime.MinValue) + .Take(Math.Min(_options.MaxSummaryItems, 5)) + .Select(x => EmptyAsUnknown(x.ServiceName) + " on " + EmptyAsUnknown(x.HostName)); + AppendList(builder, "examples", examples); + return builder.ToString(); + } + + private static string BuildLine(CheckState state, string serviceName, string metrics, string detail) + { + return ((int)state).ToString(CultureInfo.InvariantCulture) + + " \"" + SanitizeService(serviceName) + "\" " + + (string.IsNullOrWhiteSpace(metrics) ? "-" : metrics) + + " " + + SanitizeDetail(detail); + } + + 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); + } + + private static void AppendList(StringBuilder builder, string label, IEnumerable values) + { + var list = values.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (list.Length > 0) + { + builder.Append("; ").Append(label).Append("=").Append(string.Join(", ", list)); + } + } + + private static void AppendOptional(StringBuilder builder, string label, string value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + builder.Append(", ").Append(label).Append("=").Append(value); + } + } + + private static string JoinDb(string server, string database) + { + if (string.IsNullOrWhiteSpace(server) && string.IsNullOrWhiteSpace(database)) + { + return string.Empty; + } + + return EmptyAsUnknown(server) + "\\" + EmptyAsUnknown(database); + } + + private static string HostStateName(int state) + { + switch (state) + { + case HostStopped: + return "Stopped"; + case HostStartPending: + return "StartPending"; + case HostStopPending: + return "StopPending"; + case HostStarted: + return "Started"; + case HostContinuePending: + return "ContinuePending"; + case HostPausePending: + return "PausePending"; + case HostPaused: + return "Paused"; + case HostUnknown: + return "Unknown"; + default: + return "Unknown(" + state.ToString(CultureInfo.InvariantCulture) + ")"; + } + } + + private static string EmptyAsUnknown(string value) + { + return string.IsNullOrWhiteSpace(value) ? "unknown" : value.Trim(); + } + + private static string SanitizeService(string value) + { + return SanitizeDetail(value).Replace("\"", "'"); + } + + private static string SanitizeDetail(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return "No details."; + } + + return value.Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' ').Trim(); + } + } +} diff --git a/src/BizTalkCheckmkPulse/EventLogProbe.cs b/src/BizTalkCheckmkPulse/EventLogProbe.cs new file mode 100644 index 0000000..717c98e --- /dev/null +++ b/src/BizTalkCheckmkPulse/EventLogProbe.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics; +using System.Linq; + +namespace BizTalkCheckmkPulse +{ + internal sealed class EventLogProbe + { + private readonly MonitoringOptions _options; + + public EventLogProbe(MonitoringOptions options) + { + _options = options; + } + + public void Query(ProbeResult result) + { + result.EventLog.Since = DateTime.Now.AddMinutes(-_options.EventLogLookbackMinutes); + + try + { + using (var log = new EventLog("Application", ".")) + { + var entries = log.Entries; + for (var i = entries.Count - 1; i >= 0; i--) + { + var entry = entries[i]; + if (entry.TimeGenerated < result.EventLog.Since) + { + break; + } + + if (!IsRelevantSource(entry.Source)) + { + continue; + } + + if (entry.EntryType == EventLogEntryType.Error) + { + result.EventLog.Errors++; + } + else if (entry.EntryType == EventLogEntryType.Warning) + { + result.EventLog.Warnings++; + } + } + } + + result.EventLog.Available = true; + } + catch (Exception ex) + { + result.EventLog.Available = false; + result.EventLog.Failure = ex.GetType().Name + ": " + ex.Message; + } + } + + private bool IsRelevantSource(string source) + { + if (string.IsNullOrWhiteSpace(source)) + { + return false; + } + + return _options.EventLogSources.Any(x => source.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0); + } + } +} + diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs new file mode 100644 index 0000000..57dbce4 --- /dev/null +++ b/src/BizTalkCheckmkPulse/Models.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace BizTalkCheckmkPulse +{ + internal enum CheckState + { + Ok = 0, + Warning = 1, + Critical = 2, + Unknown = 3 + } + + internal enum SuspendedKind + { + Resumable, + NonResumable + } + + internal sealed class ProbeResult + { + public ProbeResult() + { + Diagnostics = new List(); + Platform = new PlatformState(); + HostInstances = new List(); + SuspendedInstances = new List(); + Applications = new List(); + EventLog = new EventLogState(); + } + + 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 EventLogState EventLog { get; private set; } + } + + internal sealed class PlatformState + { + public bool WmiConnected { get; set; } + public string ServerName { get; set; } + public string GroupName { get; set; } + public string ManagementDbServer { get; set; } + public string ManagementDbName { get; set; } + public string MessageBoxDbServer { get; set; } + public string MessageBoxDbName { 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 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 ErrorDescription { get; set; } + public DateTime? SuspendTime { get; set; } + public SuspendedKind Kind { get; set; } + } + + internal sealed class ApplicationRuntimeState + { + 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 EventLogState + { + public bool Available { get; set; } + public int Errors { get; set; } + public int Warnings { get; set; } + public DateTime Since { get; set; } + public string Failure { get; set; } + } +} + diff --git a/src/BizTalkCheckmkPulse/MonitoringOptions.cs b/src/BizTalkCheckmkPulse/MonitoringOptions.cs new file mode 100644 index 0000000..7dffb89 --- /dev/null +++ b/src/BizTalkCheckmkPulse/MonitoringOptions.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Globalization; +using System.Linq; + +namespace BizTalkCheckmkPulse +{ + 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 int WarnResumableThreshold { get; set; } + public int CritNonResumableThreshold { get; set; } + public int MaxSummaryItems { get; set; } + public bool AlertOnArtifactRuntimeIssues { get; set; } + public bool EmitPerApplicationSuspensionServices { get; set; } + public bool ProbeEventLog { get; set; } + public int EventLogLookbackMinutes { get; set; } + public int EventLogWarnThreshold { get; set; } + public int EventLogCritThreshold { get; set; } + public IReadOnlyList EventLogSources { get; set; } + public bool SelfTest { get; set; } + + public MonitoringOptions() + { + Server = "."; + ServicePrefix = "BizTalk"; + EnvironmentName = string.Empty; + QueryTimeoutSeconds = 25; + WarnResumableThreshold = 1; + CritNonResumableThreshold = 1; + MaxSummaryItems = 12; + AlertOnArtifactRuntimeIssues = false; + EmitPerApplicationSuspensionServices = false; + ProbeEventLog = true; + EventLogLookbackMinutes = 60; + EventLogWarnThreshold = 1; + EventLogCritThreshold = 10; + EventLogSources = new[] { "BizTalk Server", "XLANG/s", "ENTSSO", "BizTalk Server Application", "BizTalk Server EDI" }; + } + + public string ServiceName(string suffix) + { + var prefix = string.IsNullOrWhiteSpace(EnvironmentName) + ? ServicePrefix + : ServicePrefix + " " + EnvironmentName.Trim(); + return prefix.Trim() + " " + suffix; + } + + public static MonitoringOptions Load(string[] args) + { + var options = new MonitoringOptions(); + var settings = ConfigurationManager.AppSettings; + + options.Server = ReadString(settings, "Server", options.Server); + options.ServicePrefix = ReadString(settings, "ServicePrefix", options.ServicePrefix); + options.EnvironmentName = ReadString(settings, "EnvironmentName", options.EnvironmentName); + options.QueryTimeoutSeconds = ReadInt(settings, "QueryTimeoutSeconds", options.QueryTimeoutSeconds, 5, 120); + 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); + options.AlertOnArtifactRuntimeIssues = ReadBool(settings, "AlertOnArtifactRuntimeIssues", options.AlertOnArtifactRuntimeIssues); + options.EmitPerApplicationSuspensionServices = ReadBool(settings, "EmitPerApplicationSuspensionServices", options.EmitPerApplicationSuspensionServices); + options.ProbeEventLog = ReadBool(settings, "ProbeEventLog", options.ProbeEventLog); + options.EventLogLookbackMinutes = ReadInt(settings, "EventLogLookbackMinutes", options.EventLogLookbackMinutes, 1, 10080); + options.EventLogWarnThreshold = ReadInt(settings, "EventLogWarnThreshold", options.EventLogWarnThreshold, 0, 1000000); + options.EventLogCritThreshold = ReadInt(settings, "EventLogCritThreshold", options.EventLogCritThreshold, 0, 1000000); + options.EventLogSources = ReadSources(ReadString(settings, "EventLogSources", string.Join("|", options.EventLogSources))); + + ApplyArguments(options, args ?? new string[0]); + return options; + } + + private static void ApplyArguments(MonitoringOptions options, string[] args) + { + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (EqualsAny(arg, "--self-test", "/self-test")) + { + options.SelfTest = true; + } + else if (EqualsAny(arg, "--server", "/server") && i + 1 < args.Length) + { + options.Server = args[++i]; + } + else if (EqualsAny(arg, "--environment", "/environment") && i + 1 < args.Length) + { + options.EnvironmentName = args[++i]; + } + else if (EqualsAny(arg, "--emit-app-services", "/emit-app-services")) + { + options.EmitPerApplicationSuspensionServices = true; + } + else if (EqualsAny(arg, "--alert-artifacts", "/alert-artifacts")) + { + options.AlertOnArtifactRuntimeIssues = true; + } + } + } + + private static string ReadString(System.Collections.Specialized.NameValueCollection settings, string key, string fallback) + { + var value = settings[key]; + return value == null ? fallback : value.Trim(); + } + + private static int ReadInt(System.Collections.Specialized.NameValueCollection settings, string key, int fallback, int min, int max) + { + int parsed; + if (!int.TryParse(settings[key], NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed)) + { + return fallback; + } + + if (parsed < min) + { + return min; + } + + return parsed > max ? max : parsed; + } + + private static bool ReadBool(System.Collections.Specialized.NameValueCollection settings, string key, bool fallback) + { + bool parsed; + return bool.TryParse(settings[key], out parsed) ? parsed : fallback; + } + + private static IReadOnlyList ReadSources(string value) + { + return (value ?? string.Empty) + .Split(new[] { '|', ';', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => x.Length > 0) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + 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 new file mode 100644 index 0000000..8a81dab --- /dev/null +++ b/src/BizTalkCheckmkPulse/Program.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BizTalkCheckmkPulse +{ + 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 CheckmkLocalFormatter(options); + + if (options.SelfTest) + { + foreach (var line in formatter.FormatSelfTest()) + { + Console.WriteLine(line); + } + + return 0; + } + + var result = new ProbeResult(); + var wmiProbe = new WmiBizTalkProbe(options); + wmiProbe.Query(result); + + if (options.ProbeEventLog) + { + var eventLogProbe = new EventLogProbe(options); + eventLogProbe.Query(result); + } + + foreach (var line in formatter.Format(result)) + { + Console.WriteLine(line); + } + + return 0; + } + catch (Exception ex) + { + var fallbackOptions = options ?? new MonitoringOptions(); + var formatter = new CheckmkLocalFormatter(fallbackOptions); + foreach (var line in formatter.FormatFatal("BizTalk Checkmk Pulse failed: " + ex.GetType().Name + ": " + ex.Message)) + { + Console.WriteLine(line); + } + + return 0; + } + } + } +} + diff --git a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs new file mode 100644 index 0000000..ad41504 --- /dev/null +++ b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management; + +namespace BizTalkCheckmkPulse +{ + internal sealed class WmiBizTalkProbe + { + private const string NamespacePath = "root\\MicrosoftBizTalkServer"; + private const string UnknownApplication = "(unknown)"; + private const int SuspendedResumable = 4; + private const int SuspendedNonResumable = 32; + 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 readonly MonitoringOptions _options; + + public WmiBizTalkProbe(MonitoringOptions options) + { + _options = options; + } + + public void Query(ProbeResult result) + { + var server = string.IsNullOrWhiteSpace(_options.Server) || _options.Server == "." + ? Environment.MachineName + : _options.Server.Trim(); + + result.Platform.ServerName = server; + + var scope = new ManagementScope("\\\\" + server + "\\" + NamespacePath); + scope.Connect(); + result.Platform.WmiConnected = true; + + QueryPlatform(scope, result); + QueryHostInstances(scope, result); + var applicationIndex = BuildApplicationIndex(scope, result); + QueryApplicationRuntime(scope, result); + QuerySuspendedInstances(scope, result, applicationIndex); + } + + private void QueryPlatform(ManagementScope scope, ProbeResult result) + { + TryQuery(scope, result, "MSBTS_GroupSetting", "SELECT * FROM MSBTS_GroupSetting", item => + { + 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 => + { + 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); + } + + private Dictionary BuildApplicationIndex(ManagementScope scope, ProbeResult result) + { + var index = new Dictionary(StringComparer.OrdinalIgnoreCase); + AddArtifactIndex(scope, result, index, "MSBTS_SendPort"); + AddArtifactIndex(scope, result, index, "MSBTS_ReceivePort"); + AddArtifactIndex(scope, result, index, "MSBTS_ReceiveLocation"); + AddArtifactIndex(scope, result, index, "MSBTS_Orchestration"); + return index; + } + + private void AddArtifactIndex(ManagementScope scope, ProbeResult result, Dictionary index, string className) + { + TryQuery(scope, result, className, "SELECT * FROM " + className, item => + { + var app = GetApplicationName(item); + foreach (var key in CandidateKeys(item)) + { + if (!string.IsNullOrWhiteSpace(key) && !index.ContainsKey(key)) + { + index.Add(key, app); + } + } + }, false); + } + + private void QueryHostInstances(ManagementScope scope, ProbeResult result) + { + TryQuery(scope, result, "MSBTS_HostInstance", "SELECT * FROM MSBTS_HostInstance", item => + { + result.HostInstances.Add(new HostInstanceState + { + InstanceName = FirstNonEmpty(WmiHelpers.GetString(item, "InstanceName"), WmiHelpers.GetString(item, "Name")), + HostName = WmiHelpers.GetString(item, "HostName"), + RunningServer = WmiHelpers.GetString(item, "RunningServer"), + ServiceState = WmiHelpers.GetInt32(item, "ServiceState", 0) + }); + }, true); + } + + private void QueryApplicationRuntime(ManagementScope scope, ProbeResult result) + { + var apps = new Dictionary(StringComparer.OrdinalIgnoreCase); + + TryQuery(scope, result, "MSBTS_ReceiveLocation", "SELECT * FROM MSBTS_ReceiveLocation", item => + { + var app = GetApplication(apps, GetApplicationName(item)); + app.ReceiveLocationTotal++; + if (WmiHelpers.GetBoolean(item, "IsDisabled", false)) + { + app.ReceiveLocationDisabled++; + } + }, true); + + TryQuery(scope, result, "MSBTS_SendPort", "SELECT * FROM MSBTS_SendPort", item => + { + var app = GetApplication(apps, GetApplicationName(item)); + app.SendPortTotal++; + switch (WmiHelpers.GetInt32(item, "Status", 0)) + { + case SendPortStarted: + app.SendPortStarted++; + break; + case SendPortStopped: + app.SendPortStopped++; + break; + case SendPortBound: + app.SendPortBound++; + break; + default: + app.SendPortUnknown++; + break; + } + }, true); + + TryQuery(scope, result, "MSBTS_Orchestration", "SELECT * FROM MSBTS_Orchestration", item => + { + var app = GetApplication(apps, GetApplicationName(item)); + app.OrchestrationTotal++; + switch (WmiHelpers.GetInt32(item, "OrchestrationStatus", 0)) + { + case OrchestrationStarted: + app.OrchestrationStarted++; + break; + case OrchestrationStopped: + app.OrchestrationStopped++; + break; + case OrchestrationBound: + app.OrchestrationBound++; + break; + case OrchestrationUnbound: + app.OrchestrationUnbound++; + break; + default: + app.OrchestrationUnknown++; + break; + } + }, true); + + result.Applications.AddRange(apps.Values.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)); + } + + private void QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary applicationIndex) + { + 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 + { + ApplicationName = ResolveApplication(item, applicationIndex), + ServiceName = FirstNonEmpty(WmiHelpers.GetString(item, "ServiceName"), WmiHelpers.GetString(item, "Name")), + HostName = WmiHelpers.GetString(item, "HostName"), + ServiceClass = WmiHelpers.GetString(item, "ServiceClass"), + InstanceId = WmiHelpers.GetString(item, "InstanceID"), + ErrorDescription = WmiHelpers.GetString(item, "ErrorDescription"), + SuspendTime = WmiHelpers.GetDmtfDateTime(item, "SuspendTime"), + Kind = serviceStatus == SuspendedNonResumable ? SuspendedKind.NonResumable : SuspendedKind.Resumable + }); + }, true); + } + + private void TryQuery(ManagementScope scope, ProbeResult result, string label, string queryText, Action action, bool required) + { + try + { + ForEachObject(scope, queryText, action); + } + catch (ManagementException 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); + } + } + + private void ForEachObject(ManagementScope scope, string queryText, Action action) + { + var queryOptions = new EnumerationOptions + { + ReturnImmediately = true, + Rewindable = false, + Timeout = TimeSpan.FromSeconds(_options.QueryTimeoutSeconds) + }; + + using (var searcher = new ManagementObjectSearcher(scope, new ObjectQuery(queryText), queryOptions)) + using (var collection = searcher.Get()) + { + foreach (ManagementObject item in collection) + { + using (item) + { + action(item); + } + } + } + } + + private static ApplicationRuntimeState GetApplication(Dictionary apps, string applicationName) + { + var name = string.IsNullOrWhiteSpace(applicationName) ? UnknownApplication : applicationName.Trim(); + ApplicationRuntimeState app; + if (!apps.TryGetValue(name, out app)) + { + app = new ApplicationRuntimeState { ApplicationName = name }; + apps.Add(name, app); + } + + return app; + } + + private static string ResolveApplication(ManagementBaseObject item, Dictionary applicationIndex) + { + var direct = GetApplicationName(item); + if (!string.Equals(direct, UnknownApplication, StringComparison.OrdinalIgnoreCase)) + { + return direct; + } + + foreach (var key in CandidateKeys(item)) + { + string app; + if (!string.IsNullOrWhiteSpace(key) && applicationIndex.TryGetValue(key, out app)) + { + return app; + } + } + + return UnknownApplication; + } + + private static IEnumerable CandidateKeys(ManagementBaseObject item) + { + yield return WmiHelpers.GetString(item, "ApplicationName"); + yield return WmiHelpers.GetString(item, "ServiceName"); + yield return WmiHelpers.GetString(item, "Name"); + yield return WmiHelpers.GetString(item, "HostName"); + } + + private static string GetApplicationName(ManagementBaseObject item) + { + return FirstNonEmpty( + WmiHelpers.GetString(item, "ApplicationName"), + WmiHelpers.GetString(item, "Application"), + WmiHelpers.GetString(item, "BizTalkApplication")) ?? UnknownApplication; + } + + 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) + { + result.Diagnostics.Add((required ? "Required" : "Optional") + " WMI query " + label + " failed: " + message); + } + } +} + diff --git a/src/BizTalkCheckmkPulse/WmiHelpers.cs b/src/BizTalkCheckmkPulse/WmiHelpers.cs new file mode 100644 index 0000000..09c435e --- /dev/null +++ b/src/BizTalkCheckmkPulse/WmiHelpers.cs @@ -0,0 +1,60 @@ +using System; +using System.Globalization; +using System.Management; + +namespace BizTalkCheckmkPulse +{ + internal static class WmiHelpers + { + public static string GetString(ManagementBaseObject item, string propertyName) + { + if (item == null || string.IsNullOrWhiteSpace(propertyName)) + { + return string.Empty; + } + + try + { + var value = item.Properties[propertyName] == null ? null : item.Properties[propertyName].Value; + return value == null ? string.Empty : Convert.ToString(value, CultureInfo.InvariantCulture).Trim(); + } + catch (ManagementException) + { + return string.Empty; + } + } + + public static int GetInt32(ManagementBaseObject item, string propertyName, int fallback) + { + var value = GetString(item, propertyName); + int parsed; + return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : fallback; + } + + public static bool GetBoolean(ManagementBaseObject item, string propertyName, bool fallback) + { + var value = GetString(item, propertyName); + bool parsed; + return bool.TryParse(value, out parsed) ? parsed : fallback; + } + + public static DateTime? GetDmtfDateTime(ManagementBaseObject item, string propertyName) + { + var value = GetString(item, propertyName); + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + try + { + return ManagementDateTimeConverter.ToDateTime(value); + } + catch (ArgumentOutOfRangeException) + { + return null; + } + } + } +} +