Initial commit for the biztalk suspended isntances local check
build / build (push) Has been cancelled

This commit is contained in:
2026-07-08 15:15:36 +02:00
commit e13167151b
18 changed files with 1931 additions and 0 deletions
+13
View File
@@ -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
+22
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
bin/
obj/
artifacts/
*.user
*.suo
.vs/
TestResults/
+22
View File
@@ -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
+226
View File
@@ -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.
+196
View File
@@ -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 <gitea-url>
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
@@ -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
+88
View File
@@ -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`
+34
View File
@@ -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%
+25
View File
@@ -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
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="ServiceName" value="BizTalk Suspended Instances" />
<add key="HostInstancesServiceName" value="BizTalk Host Instances" />
<add key="RuntimeArtifactsServiceName" value="BizTalk Runtime Artifacts" />
<add key="Server" value="." />
<add key="WarnResumableThreshold" value="1" />
<add key="CritNonResumableThreshold" value="1" />
<add key="QueryTimeoutSeconds" value="25" />
<add key="MaxApplicationDetails" value="12" />
<add key="MaxRuntimeApplicationDetails" value="10" />
<add key="AlertOnStoppedHostInstances" value="true" />
<add key="AlertOnArtifactRuntimeIssues" value="false" />
<add key="EmitPerApplicationServices" value="false" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
</configuration>
@@ -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<string, string> _byArtifactName = new Dictionary<string, string>(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;
}
}
}
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{ED7AF215-F12B-4E2E-811B-693A537C9493}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkSuspendedInstancesCheck</RootNamespace>
<AssemblyName>BizTalkSuspendedInstancesCheck</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Management" />
</ItemGroup>
<ItemGroup>
<Compile Include="ApplicationIndex.cs" />
<Compile Include="CheckmkFormatter.cs" />
<Compile Include="Models.cs" />
<Compile Include="MonitoringOptions.cs" />
<Compile Include="Program.cs" />
<Compile Include="WmiBizTalkProbe.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -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<string> 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<string> 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<HostInstanceState> 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<ApplicationRuntimeState> 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<SuspendedInstance> 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<SuspendedInstance> 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<string> 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) ? "<unknown>" : 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();
}
}
}
@@ -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<SuspendedInstance>();
HostInstances = new List<HostInstanceState>();
Applications = new List<ApplicationRuntimeState>();
Diagnostics = new List<string>();
}
public List<SuspendedInstance> SuspendedInstances { get; private set; }
public List<HostInstanceState> HostInstances { get; private set; }
public List<ApplicationRuntimeState> Applications { get; private set; }
public List<string> Diagnostics { get; private set; }
}
}
@@ -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<string, Action<string>>(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<string> 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();
}
}
}
@@ -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;
}
}
}
}
@@ -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<string, ApplicationRuntimeState>(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<ManagementObject> 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<string, ApplicationRuntimeState> 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;
}
}
}