From 826d87fef7fc454208431fe2e3ccab4cc2085ad2 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Thu, 30 Jul 2026 16:15:20 +0200 Subject: [PATCH] Expand BizTalk operational metrics --- Dokumentation.md | 135 +++++--- README.md | 78 +++-- deployment/Install-BizTalkCheckmkPulse.ps1 | 8 +- deployment/checkmk/biztalk_checkmk_pulse.cmd | 8 +- docs/ACC-WMI-SQL-Berechtigung.txt | 13 +- docs/CheckmkServices.md | 78 ++++- docs/ExampleOutput.md | 54 +-- docs/Integration.md | 20 +- src/BizTalkCheckmkPulse/App.config | 13 +- .../CheckmkLocalFormatter.cs | 318 ++++++++++++++---- src/BizTalkCheckmkPulse/Models.cs | 34 +- src/BizTalkCheckmkPulse/MonitoringOptions.cs | 62 +++- src/BizTalkCheckmkPulse/Program.cs | 19 ++ .../Properties/AssemblyInfo.cs | 4 +- src/BizTalkCheckmkPulse/SnapshotStore.cs | 6 +- src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs | 52 ++- tests/BizTalkCheckmkPulse.Tests/Program.cs | 196 ++++++++++- 17 files changed, 859 insertions(+), 239 deletions(-) diff --git a/Dokumentation.md b/Dokumentation.md index 9f39857..667f4f5 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -12,7 +12,7 @@ Verbindliche Randbedingungen: - Der Checkmk Windows Agent bleibt `LocalSystem`. - `LocalSystem` erhaelt keine BizTalk-/SQL-Gruppenmitgliedschaft. -- Ein separates Dienstkonto oder bevorzugt gMSA sammelt minuetlich. +- Ein normales dediziertes AD-Servicekonto sammelt minuetlich. - Der Agentpfad fuehrt niemals WMI- oder SQL-Abfragen aus. - Fehler muessen als gueltige Checkmk-`UNKNOWN`-Services sichtbar werden. - Schreiben und Lesen duerfen nie einen halben Snapshot exponieren. @@ -46,7 +46,7 @@ Konto. Privilegierte Zone ┌──────────────────────────────────────────────────────────────┐ │ Task Scheduler: "BizTalk Checkmk Pulse Provider" │ -│ Konto: DOMAIN\svc_biztalk_cmk$ (gMSA empfohlen) │ +│ Konto: DOMAIN\svc_biztalk_cmk (RunLevel Limited) │ │ Intervall: 1 Minute, IgnoreNew, Laufzeitlimit 5 Minuten │ │ │ │ BizTalkCheckmkPulse.exe --collect │ @@ -89,7 +89,7 @@ Ablauf: 2. Tageslogs gemaess `LogRetentionDays` bereinigen. 3. exklusives Handle auf `.provider.lock` halten. 4. WMI-, SQL- und Event-Log-Probes ausfuehren. -5. sechs stabile und optionale dynamische Checkmk-Zeilen formatieren. +5. acht stabile und optionale dynamische Checkmk-Zeilen formatieren. 6. Snapshot in einer eindeutigen Temporaerdatei desselben Verzeichnisses schreiben. 7. `Flush(true)` ausfuehren und Temporaerdatei atomar publizieren. @@ -132,19 +132,19 @@ Der Consumer: - gibt bei Erfolg ausschliesslich den validierten Payload aus. Er gibt immer Exitcode `0` zurueck, damit ein Fehler die komplette -Checkmk-Agentsektion nicht zerstoert. Jede Ablehnung erzeugt sechs +Checkmk-Agentsektion nicht zerstoert. Jede Ablehnung erzeugt acht `UNKNOWN`-Zeilen und einen Eintrag im Consumer-Log. ## 4. Snapshot-Vertrag -Version 1: +Version 2: ```text -BIZTALK_CHECKMK_PULSE_SNAPSHOT_V1 +BIZTALK_CHECKMK_PULSE_SNAPSHOT_V2 generatedUtc=2026-07-30T12:34:56.1234567Z machineBase64=QVYyM0FHUFdCSU8x identityBase64=QkVXXHN2Y19iaXp0YWxrX2NtayQ= -payloadLines=6 +payloadLines=8 payloadSha256=<64 hex characters> 0 "BizTalk Platform" - ... @@ -202,7 +202,37 @@ Pflichtklassen: Hostnamen werden clientseitig verglichen. Sonderzeichen und FQDN-/Kurzname gelangen nicht in dynamisch erzeugte WQL-Filter. -### 5.2 SQL-Zugriffsprobe +`MSBTS_ServiceInstance` wird fuer `ServiceStatus=4` (resumable), +`ServiceStatus=32` (non-resumable) und `ServiceClass=64` abgefragt. +`ServiceClass=64` kennzeichnet Routing Failure Reports. Sie werden als eigene +Metrik ausgewiesen und zugleich in `suspended_nonresumable` mitgezaehlt. + +### 5.2 Kompaktes Checkmk-Servicebild + +Die alte Sammelzeile `BizTalk Runtime Artifacts` wurde durch drei kurze +Services ersetzt. Insgesamt entstehen acht stabile Services: + +1. `BizTalk Platform` +2. `BizTalk SQL Access` +3. `BizTalk Suspended Instances` +4. `BizTalk Host Instances` +5. `BizTalk Receive Locations` +6. `BizTalk Send Ports` +7. `BizTalk Orchestrations` +8. `BizTalk Event Log` + +Receive Locations und Send Ports alarmieren nur fuer unerwartete +Aus-Zustaende. Fachlich bewusst deaktivierte Namen werden exakt, ohne +Wildcards, in `ExpectedDisabledReceiveLocations` beziehungsweise +`ExpectedInactiveSendPorts` hinterlegt. Ein Eintrag ist entweder nur der Name +oder `Anwendung\Name`. + +Pro Summary werden maximal `MaxSummaryItems` Treffer angezeigt. Weitere +Artefakte werden als `(+n more)` zusammengefasst. `MaxDetailCharacters` +begrenzt jede Detailausgabe, damit Checkmk-Ansichten kompakt bleiben, waehrend +alle numerischen Metriken vollstaendig erhalten bleiben. + +### 5.3 SQL-Zugriffsprobe Der Provider oeffnet fuer Management- und Master-MessageBox-Datenbank eine kurze `System.Data.SqlClient`-Verbindung mit integrierter @@ -213,7 +243,7 @@ Die Ausgabe `execution_identity=` zeigt deshalb das Provider-Konto, nicht mehr `NT AUTHORITY\SYSTEM`. Der Check beweist, dass genau das Scheduled-Task-Konto die Ziele erreichen kann. -### 5.3 Event Log +### 5.4 Event Log Der Provider liest das lokale Application Log im konfigurierten Zeitfenster und filtert Quellen wie `BizTalk Server`, `XLANG/s`, `ENTSSO`, @@ -286,14 +316,17 @@ Sollkonfiguration: | Programm | `%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe` | | Argument | `--collect` | | Arbeitsverzeichnis | `%ProgramFiles%\BizTalkCheckmkPulse` | -| Benutzer | dediziertes Dienstkonto oder gMSA | +| Benutzer | normales dediziertes AD-Servicekonto | +| Run level | `Limited`; keine lokale Administratorrolle erforderlich | | Mehrfachinstanzen | `IgnoreNew` | | Laufzeitlimit | 5 Minuten | | StartWhenAvailable | aktiv | | Restart | zweimal im Minutenabstand | -Der Task wird mit gespeichertem Dienstkontokennwort beziehungsweise gMSA -ausgefuehrt, also unabhaengig von einer interaktiven Anmeldung. +Der Task wird mit gespeichertem Dienstkontokennwort ausgefuehrt, also +unabhaengig von einer interaktiven Anmeldung. Bei Kennwortwechsel oder +-ablauf muss das Task-Kennwort aktualisiert werden. Ein gMSA ist weiterhin +optional unterstuetzt, aber nicht die produktive Standardannahme. ## 8. Installation @@ -304,25 +337,7 @@ scripts\test-release.cmd scripts\package-release.cmd ``` -### 8.2 gMSA - -Voraussetzungen: - -1. gMSA in AD erstellen und Abrufrecht auf den BizTalk-Server begrenzen. -2. Konto lokal installieren und mit `Test-ADServiceAccount` pruefen. -3. gMSA in die exakt konfigurierte BizTalk-Read-Only-Gruppe aufnehmen. -4. AD-Replikation abwarten. - -Installation: - -```powershell -.\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk$' ` - -Gmsa ` - -EnvironmentName ACC -``` - -### 8.3 regulaeres Dienstkonto +### 8.2 Normales Servicekonto ```powershell .\Install-BizTalkCheckmkPulse.ps1 ` @@ -333,6 +348,19 @@ Installation: Der Installer muss als lokaler Administrator laufen. Er vergibt keine AD-/BizTalk-/SQL-Rechte; diese bleiben getrennte administrative Freigaben. +Der installierte Task selbst laeuft mit `RunLevel Limited`. Der Installer fragt +das Kennwort mit `Get-Credential` ab; es wird nicht in Config oder Log +geschrieben. Das Konto benoetigt `Log on as a batch job`. + +### 8.3 Optionales gMSA + +```powershell +.\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk$' ` + -Gmsa ` + -EnvironmentName ACC +``` + ### 8.4 Checkmk Der installierte Wrapper liegt unter: @@ -350,7 +378,8 @@ Snapshot nicht zusaetzlich verzoegert. Nach dem Agent-Dump: 1. Service Discovery fuer den BizTalk-Host ausfuehren. -2. sechs stabile Services aufnehmen. +2. acht stabile Services aufnehmen; den alten Service + `BizTalk Runtime Artifacts` nach erfolgreicher Discovery entfernen. 3. Changes aktivieren. 4. Views/Benachrichtigungen nach Umgebung konfigurieren. @@ -368,6 +397,12 @@ einzeilige Nachricht. Provider-Erfolge werden pro Lauf geloggt; der Consumer loggt nur abgelehnte Snapshots. Exceptions werden mit Typ, Nachricht, HRESULT-/Providerdetails und Stacktrace einzeilig gespeichert. +Der erfolgreiche Abschluss nennt Suspensions, Routing Failure Reports, +Receive-Location-/Send-Port-Zahlen und Laufzeit. Jede strukturierte +WMI-/SQL-/Event-Log-Diagnose wird zusaetzlich als eigene `WARN`-Zeile +protokolliert und bleibt damit auch ausserhalb der gekuerzten Checkmk-Summary +vollstaendig nachvollziehbar. + Logging ist best effort: Ein blockiertes Log darf Checkmk-Ausgabe oder Snapshot-Publikation nicht zerstoeren. Der Provider entfernt beim Start Dateien aelter als `LogRetentionDays`. @@ -391,7 +426,7 @@ Pruefen: 1. Task existiert und ist aktiviert. 2. `LastTaskResult` und Provider-Log. 3. Dienstkonto kann sich als Batch anmelden. -4. gMSA ist lokal installiert und abrufbar. +4. gespeichertes Task-Kennwort ist nach Rotation/Ablauf noch gueltig. 5. Provider besitzt Modify auf `data` und `logs`. 6. EXE/Config sind ausfuehrbar. @@ -468,7 +503,13 @@ Alarmierung: | --- | --- | | `WarnResumableThreshold` | `1` | | `CritNonResumableThreshold` | `1` | -| `AlertOnArtifactRuntimeIssues` | `false` | +| `CritRoutingFailureThreshold` | `1` | +| `AlertOnArtifactRuntimeIssues` | `true` | +| `ExpectedDisabledReceiveLocations` | leer; exakte Pipe-Liste | +| `ExpectedInactiveSendPorts` | leer; exakte Pipe-Liste | +| `AlertOnInactiveOrchestrations` | `false` | +| `MaxSummaryItems` | `5` | +| `MaxDetailCharacters` | `1600` | | `EmitPerApplicationSuspensionServices` | `false` | | `EventLogWarnThreshold` | `1` | | `EventLogCritThreshold` | `10` | @@ -478,16 +519,20 @@ Alarmierung: Automatisiert: - Release-Build .NET Framework 4.7.2, -- exakt sechs Self-Test-Services, +- exakt acht Self-Test-Services, - unbekannte Quellen werden `UNKNOWN`, - dynamische Anwendungsservices nur bei bekannter Anwendung, - WMI-Queryvertrag nutzt dokumentierte Properties, - eingebetteter SQL-Loginfehler wird `Permission`, - abgewiesener Principal wird extrahiert, +- Routing Failure Reports werden separat und als non-resumable gezaehlt, +- Receive-/Send-Allowlisten trennen bewusste von unerwarteten Aus-Zustaenden, +- ein fehlender `IsDisabled`-Wert wird `UNKNOWN` statt still als enabled, +- betroffene Namen und Gesamtlaenge der Summary bleiben begrenzt, - Snapshot-Roundtrip und Ersatz, - SHA-256-Manipulation wird verworfen, - Stale-Snapshot wird verworfen, -- Consumer-Fallback enthaelt sechs `UNKNOWN`-Services. +- Consumer-Fallback enthaelt acht `UNKNOWN`-Services. Windows-/ACC-Abnahme: @@ -497,10 +542,16 @@ Windows-/ACC-Abnahme: 4. `BizTalk Platform` zeigt `read_only_group=`. 5. `BizTalk SQL Access`: `targets=2`, `available=2`. 6. keine Permission-`UNKNOWN`s. -7. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben. -8. Task deaktivieren: nach 180 Sekunden sechs stale-`UNKNOWN`s. -9. Task wieder aktivieren: naechster Snapshot stellt Echtzustand her. -10. Agent-Dump und Checkmk Service Discovery erfolgreich. +7. Suspensionsmetriken mit der BizTalk Group Hub Page plausibilisieren, + einschliesslich Routing Failure Reports. +8. Eine bewusst deaktivierte Receive Location und einen inaktiven Send Port + ueber die exakten Allowlisten als expected bestaetigen. +9. Einen Testnamen aus der Allowlist entfernen und den erwarteten CRIT mit + kurzem `affected=`-Detail pruefen. +10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben. +11. Task deaktivieren: nach 180 Sekunden acht stale-`UNKNOWN`s. +12. Task wieder aktivieren: naechster Snapshot stellt Echtzustand her. +13. Agent-Dump und Checkmk Service Discovery erfolgreich. Erst nach ACC-Abnahme erfolgt der gestufte Rollout nach DEV/TST/PRD. @@ -537,6 +588,10 @@ ZIP und Base64 werden erst nach dem Git-Commit aus genau diesem Commit erzeugt. - https://learn.microsoft.com/en-us/biztalk/core/windows-groups-and-user-accounts-in-biztalk-server - https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-biztalkreadonlyusergroup-property-wmi - https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi +- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-serviceclass-property-wmi +- https://learn.microsoft.com/en-us/biztalk/core/types-of-message-failures +- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-isdisabled-property-wmi +- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi - https://docs.checkmk.com/latest/en/agent_windows.html - https://docs.checkmk.com/latest/en/localchecks.html - https://docs.checkmk.com/latest/en/spool_directory.html diff --git a/README.md b/README.md index 12dd76b..99037d1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ BizTalk Server 2020 auf Windows Server 2019. Die Anwendung trennt den berechtigten BizTalk-Datenzugriff vollstaendig vom Checkmk-Agenten: ```text -Scheduled Task (dediziertes Dienstkonto oder gMSA) +Scheduled Task (normales dediziertes Servicekonto) | | jede Minute: BizTalkCheckmkPulse.exe --collect v @@ -20,7 +20,7 @@ lokales BizTalk-WMI + BizTalk-SQL + Application Event Log Checkmk Windows Agent (LocalSystem) | v -sechs stabile Checkmk Local Checks +acht kompakte Checkmk Local Checks ``` Damit bekommt `LocalSystem` keine BizTalk- oder SQL-Berechtigung. Nur das @@ -62,7 +62,7 @@ Der Datenaustausch ist bewusst defensiv: Checkmk-Zeilen, korrekte SHA-256-Pruefsumme und ein maximales Alter von standardmaessig 180 Sekunden. - Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien - ergeben sechs gueltige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe. + ergeben acht gueltige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe. - Ein exklusives Lock und die Task-Einstellung `IgnoreNew` verhindern ueberlappende Providerlaeufe. - Ein unerwarteter Providerfehler erzeugt nach Moeglichkeit einen aktuellen @@ -80,9 +80,23 @@ Standardmaessig entstehen: - `BizTalk SQL Access` - `BizTalk Suspended Instances` - `BizTalk Host Instances` -- `BizTalk Runtime Artifacts` +- `BizTalk Receive Locations` +- `BizTalk Send Ports` +- `BizTalk Orchestrations` - `BizTalk Event Log` +Die drei Artefaktbereiche sind absichtlich getrennte Services. Dadurch sind +Zustand und Graphen direkt erkennbar, ohne eine lange Sammelzeile zu lesen: + +- Suspensions: total, resumable, non-resumable und Routing Failure Reports +- Receive Locations: total, enabled, unerwartet/bewusst disabled und unbekannt +- Send Ports: total, started, stopped, bound, unbekannt sowie bewusst inactive +- Orchestrations: total, started, stopped, bound, unbound und unbekannt + +Pro Service werden standardmaessig maximal fuenf betroffene Namen gezeigt. +Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusaetzlich auf +1600 Zeichen begrenzt. Metriken bleiben trotzdem vollstaendig. + Mit `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` wird die Umgebung in den Servicenamen aufgenommen, zum Beispiel `BizTalk ACC Platform`. @@ -106,7 +120,7 @@ BizTalk-Server: - .NET Framework 4.7.2 - Checkmk Windows Agent - administrativer Zugriff fuer die einmalige Installation -- dediziertes AD-Dienstkonto oder bevorzugt gMSA fuer den Provider +- normales dediziertes AD-Servicekonto fuer den Provider Das Provider-Konto benoetigt: @@ -116,9 +130,10 @@ Das Provider-Konto benoetigt: - Mitgliedschaft in der exakt konfigurierten BizTalk Server Read Only Users-Gruppe -Es soll weder lokaler Administrator noch SQL-`sysadmin` sein. Fuer ein gMSA -muss der BizTalk-Server das verwaltete Kennwort abrufen duerfen und das Konto -lokal installiert sein. +Es soll weder lokaler Administrator noch SQL-`sysadmin` sein. Der Scheduled +Task laeuft mit `RunLevel Limited`. Das Servicekonto braucht ein gespeichertes +Task-Kennwort und das Recht `Log on as a batch job`. Ein gMSA bleibt optional, +ist aber fuer diese Installation nicht vorausgesetzt. ## Build und Tests @@ -146,7 +161,7 @@ Format-Self-Test ohne WMI, SQL oder Event Log: artifacts\BizTalkCheckmkPulse-deploy\application\BizTalkCheckmkPulse.exe --self-test ``` -Erwartet werden exakt sechs `OK`-Zeilen. Die Regressionstests pruefen +Erwartet werden exakt acht `OK`-Zeilen. Die Regressionstests pruefen zusaetzlich Snapshot-Roundtrip, atomaren Ersatz, SHA-256-Manipulation, Stale-Erkennung, stabile Fallbacks und die bestehenden BizTalk-WMI-Diagnosen. Ein Mono-Build ist eine hilfreiche Quellcodepruefung, ersetzt aber nicht die @@ -167,9 +182,8 @@ Get-CimInstance ` ``` Ein AD-Administrator nimmt das neue Provider-Konto in -`BizTalkReadOnlyUserGroup` auf. Bei einem gMSA endet der Kontoname mit `$`. -Nach AD-Replikation muss ein regulaeres Dienstkonto einen neuen Anmeldetoken -erhalten; bei gMSA wird der Task nach der Gruppenfreigabe neu gestartet. +`BizTalkReadOnlyUserGroup` auf. Nach AD-Replikation muss das Servicekonto durch +einen neuen Tasklauf einen neuen Anmeldetoken erhalten. Die BizTalk-Konfiguration muss die Domain-Gruppe bereits als Windows-Login und in `BizTalkMgmtDb`, `BizTalkMsgBoxDb`, `BizTalkDTADb`, @@ -178,25 +192,7 @@ in `BizTalkMgmtDb`, `BizTalkMsgBoxDb`, `BizTalkDTADb`, SQL-Administration fuer die Gruppe repariert, nicht als Einzelberechtigung fuer das Provider-Konto. -## Installation mit gMSA - -Deployment-Paket auf den BizTalk-Server kopieren. In administrativer Windows -PowerShell: - -```powershell -Set-Location C:\Temp\BizTalkCheckmkPulse-deploy - -# Optional, falls das gMSA noch nicht lokal installiert wurde: -Install-ADServiceAccount -Identity svc_biztalk_cmk -Test-ADServiceAccount -Identity svc_biztalk_cmk - -.\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk$' ` - -Gmsa ` - -EnvironmentName ACC -``` - -## Installation mit regulaerem Dienstkonto +## Installation mit normalem Servicekonto ```powershell Set-Location C:\Temp\BizTalkCheckmkPulse-deploy @@ -209,6 +205,13 @@ Der Installer fragt das Kennwort ueber `Get-Credential` ab und speichert es durch die Windows-Aufgabenplanung. Das Kennwort steht weder in der Konfigurationsdatei noch in den Logs. +Wenn das Kennwort rotiert oder ablaeuft, muss es im Scheduled Task aktualisiert +werden. Bis dahin wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt +alle acht Services als `UNKNOWN`. + +Ein gMSA kann weiterhin optional mit `-Gmsa` installiert werden; die +produktive Standardbeschreibung geht vom normalen Servicekonto aus. + Der Installer: 1. kopiert EXE und Config nach @@ -265,7 +268,7 @@ Verbindlicher Test im echten `LocalSystem`-Kontext: Select-String -Pattern "BizTalk|UNKNOWN|Snapshot|Permission" -Context 0,1 ``` -Danach in Checkmk eine Service Discovery ausfuehren, die sechs Services +Danach in Checkmk eine Service Discovery ausfuehren, die acht Services aufnehmen und Changes aktivieren. Ein zusaetzlicher Checkmk-Async-Cache ist nicht erforderlich: Der Consumer liest nur eine kleine lokale Datei und der Provider besitzt bereits seinen eigenen Minutentakt. @@ -322,7 +325,13 @@ Wichtige Werte: | `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. | | `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. | | `CritNonResumableThreshold` | `1` | CRIT ab n non-resumable Suspensions. | -| `AlertOnArtifactRuntimeIssues` | `false` | WARN fuer bewusst inaktive Artefakte aktivieren. | +| `CritRoutingFailureThreshold` | `1` | CRIT ab n Routing Failure Reports. | +| `AlertOnArtifactRuntimeIssues` | `true` | Unerwartet deaktivierte Receive Locations bzw. inaktive Send Ports werden CRIT. | +| `ExpectedDisabledReceiveLocations` | leer | Pipe-getrennte exakte Allowlist: `Name` oder `Anwendung\Name`. | +| `ExpectedInactiveSendPorts` | leer | Pipe-getrennte exakte Allowlist: `Name` oder `Anwendung\Name`. | +| `AlertOnInactiveOrchestrations` | `false` | Optional WARN fuer stopped/bound/unbound Orchestrations. | +| `MaxSummaryItems` | `5` | Maximal angezeigte betroffene Artefakte je Service. | +| `MaxDetailCharacters` | `1600` | Harte Obergrenze fuer Checkmk-Summary. | | `EmitPerApplicationSuspensionServices` | `false` | Zusaetzliche Anwendungsservices. | | `EventLogLookbackMinutes` | `60` | Event-Log-Zeitfenster des Providers. | @@ -333,10 +342,11 @@ liest den naechsten atomar publizierten Snapshot. | Beobachtung | Ursache / Massnahme | | --- | --- | -| Alle sechs Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto und ACL pruefen. | +| Alle acht Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto/Kennwort und ACL pruefen. | | Snapshot ist `stale` | `LastTaskResult`, Laufzeit, WMI-/SQL-Timeout und Log pruefen. | | SHA-256 oder Format ungueltig | Datei nicht manuell bearbeiten; Datentraeger/AV und Schreibpfad pruefen, Task neu starten. | | Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` pruefen. | +| Receive Locations / Send Ports sind CRIT | `affected=` pruefen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. | | `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema pruefen; keine Rechte ausweiten. | | Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos pruefen. | | Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL pruefen. | diff --git a/deployment/Install-BizTalkCheckmkPulse.ps1 b/deployment/Install-BizTalkCheckmkPulse.ps1 index 3adad98..73ea7ad 100644 --- a/deployment/Install-BizTalkCheckmkPulse.ps1 +++ b/deployment/Install-BizTalkCheckmkPulse.ps1 @@ -128,7 +128,7 @@ if ($Gmsa) { $principal = New-ScheduledTaskPrincipal ` -UserId $CollectorAccount ` -LogonType Password ` - -RunLevel Highest + -RunLevel Limited $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal Register-ScheduledTask -TaskName $taskName -InputObject $task -Force | Out-Null } @@ -143,7 +143,7 @@ else { $principal = New-ScheduledTaskPrincipal ` -UserId $CollectorAccount ` -LogonType Password ` - -RunLevel Highest + -RunLevel Limited $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal Register-ScheduledTask ` -TaskName $taskName ` @@ -155,8 +155,8 @@ else { Write-Host "Fuehre formatseitigen Self-Test aus ..." $selfTest = & $targetExe --self-test -if ($LASTEXITCODE -ne 0 -or @($selfTest).Count -ne 6) { - throw "Self-Test fehlgeschlagen. Erwartet wurden sechs Checkmk-Zeilen." +if ($LASTEXITCODE -ne 0 -or @($selfTest).Count -ne 8) { + throw "Self-Test fehlgeschlagen. Erwartet wurden acht Checkmk-Zeilen." } Write-Host "Starte den Provider einmalig ..." diff --git a/deployment/checkmk/biztalk_checkmk_pulse.cmd b/deployment/checkmk/biztalk_checkmk_pulse.cmd index 100d4e9..fa6e0e4 100644 --- a/deployment/checkmk/biztalk_checkmk_pulse.cmd +++ b/deployment/checkmk/biztalk_checkmk_pulse.cmd @@ -9,7 +9,9 @@ if not exist "%EXE%" ( echo 3 "BizTalk SQL Access" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. echo 3 "BizTalk Suspended Instances" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. echo 3 "BizTalk Host Instances" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. - echo 3 "BizTalk Runtime Artifacts" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. + echo 3 "BizTalk Receive Locations" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. + echo 3 "BizTalk Send Ports" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. + echo 3 "BizTalk Orchestrations" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. echo 3 "BizTalk Event Log" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. exit /b 0 ) @@ -21,7 +23,9 @@ if not "%EXITCODE%"=="0" ( echo 3 "BizTalk SQL Access" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, EXE/Config-Dateien und Windows Application Log pruefen. echo 3 "BizTalk Suspended Instances" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. echo 3 "BizTalk Host Instances" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. - echo 3 "BizTalk Runtime Artifacts" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. + echo 3 "BizTalk Receive Locations" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Provider-Log pruefen. + echo 3 "BizTalk Send Ports" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Provider-Log pruefen. + echo 3 "BizTalk Orchestrations" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Provider-Log pruefen. echo 3 "BizTalk Event Log" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. ) exit /b 0 diff --git a/docs/ACC-WMI-SQL-Berechtigung.txt b/docs/ACC-WMI-SQL-Berechtigung.txt index 13500c8..7703239 100644 --- a/docs/ACC-WMI-SQL-Berechtigung.txt +++ b/docs/ACC-WMI-SQL-Berechtigung.txt @@ -18,7 +18,7 @@ NEUE VERBINDLICHE ARCHITEKTUR Das Maschinenkonto erhaelt keine BizTalk-Berechtigung. -1. Ein dediziertes Dienstkonto oder bevorzugt gMSA laeuft minuetlich als +1. Ein normales dediziertes Servicekonto laeuft minuetlich als Scheduled Task "BizTalk Checkmk Pulse Provider". 2. Dieses Konto fuehrt BizTalkCheckmkPulse.exe --collect aus. 3. Es schreibt atomar einen SHA-256-geschuetzten Snapshot nach: @@ -64,14 +64,7 @@ NICHT DURCHFUEHREN INSTALLATION ============ -gMSA: - - .\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk$' ` - -Gmsa ` - -EnvironmentName ACC - -Regulaeres Dienstkonto: +Normales Servicekonto: .\Install-BizTalkCheckmkPulse.ps1 ` -CollectorAccount 'BEW\svc_biztalk_cmk' ` @@ -102,6 +95,8 @@ ERWARTET - Provider-Log nennt das dedizierte Konto - BizTalk Platform zeigt read_only_group= - BizTalk SQL Access zeigt targets=2 und available=2 +- Suspensionsmetriken enthalten resumable, non-resumable und Routing Failure Reports +- Receive Locations und Send Ports trennen expected von unexpected inactive - keine berechtigungsbedingten UNKNOWN-Services - LocalSystem besitzt auf data nur Lesezugriff diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md index bcd68f5..e3eaa05 100644 --- a/docs/CheckmkServices.md +++ b/docs/CheckmkServices.md @@ -11,7 +11,7 @@ Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt: Die fachlichen Daten werden minuetlich vom privilegierten Scheduled-Task- Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den integritaets- und altersgeprueften Snapshot. Ist dieser Transport nicht -verfuegbar, liefern alle sechs stabilen Services `UNKNOWN`. +verfuegbar, liefern alle acht stabilen Services `UNKNOWN`. ## BizTalk Platform @@ -77,6 +77,7 @@ Zweck: - Ermittelt suspendierte BizTalk-Service-Instanzen. - Unterscheidet resumable und non-resumable. +- Zaehlt Routing Failure Reports (`ServiceClass=64`) separat. - Zeigt betroffene Anwendungen und Beispiele in der Summary. Status: @@ -86,6 +87,7 @@ Status: | `OK` | keine suspendierten Instanzen. | | `WARN` | `biztalk_suspended_resumable >= WarnResumableThreshold`. | | `CRIT` | `biztalk_suspended_nonresumable >= CritNonResumableThreshold`. | +| `CRIT` | `biztalk_routing_failure_reports >= CritRoutingFailureThreshold`. | | `UNKNOWN` | `MSBTS_ServiceInstance` nicht verlaesslich lesbar oder Programmfehler. | Metriken: @@ -95,6 +97,7 @@ Metriken: | `biztalk_suspended_total` | Summe aller suspendierten Instanzen. | | `biztalk_suspended_resumable` | Resumable suspended instances. | | `biztalk_suspended_nonresumable` | Non-resumable suspended instances. | +| `biztalk_routing_failure_reports` | Routing Failure Reports; Teilmenge der non-resumable Instanzen. | ## BizTalk Host Instances @@ -123,38 +126,85 @@ Metriken: | `biztalk_host_instances_pending` | StartPending/StopPending. | | `biztalk_host_instances_unknown` | Nicht interpretierbarer Status. | -## BizTalk Runtime Artifacts +## BizTalk Receive Locations Zweck: -- Summiert Laufzeitzustaende von Receive Locations, Send Ports und Orchestrations. -- Liefert dashboard-taugliche Zahlen zur Laufzeitlandschaft. +- Zaehlt alle konfigurierten Receive Locations und ihren `IsDisabled`-Zustand. +- Trennt bewusst deaktivierte von unerwartet deaktivierten Namen. 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` | Mindestens eine erforderliche Artefaktklasse nicht lesbar oder Programmfehler. | +| `OK` | Daten lesbar und keine unerwartet deaktivierte Receive Location. | +| `CRIT` | Mindestens eine unerwartet deaktivierte Receive Location bei `AlertOnArtifactRuntimeIssues=true`. | +| `UNKNOWN` | `MSBTS_ReceiveLocation` nicht verlaesslich lesbar oder `IsDisabled` eines Objekts nicht interpretierbar. | Metriken: | Metrik | Bedeutung | | --- | --- | -| `biztalk_applications` | Anzahl der per WMI erkannten Anwendungs-Buckets; `(unknown)` ist moeglich, da die dokumentierten Artefaktklassen keine verlaessliche Application-Property garantieren. | -| `biztalk_receive_locations` | Gesamtzahl Receive Locations. | +| `biztalk_receive_locations_total` | Gesamtzahl Receive Locations. | +| `biztalk_receive_locations_enabled` | Aktivierte Receive Locations. | | `biztalk_receive_locations_disabled` | Deaktivierte Receive Locations. | -| `biztalk_send_ports` | Gesamtzahl Send Ports. | +| `biztalk_receive_locations_unexpected_disabled` | Deaktiviert und nicht in der Allowlist; CRIT ab 1. | +| `biztalk_receive_locations_expected_disabled` | Bewusst deaktiviert und exakt allowlisted. | +| `biztalk_receive_locations_unknown` | Fehlender oder nicht interpretierbarer `IsDisabled`-Wert. | + +## BizTalk Send Ports + +Status: + +| Status | Bedingung | +| --- | --- | +| `OK` | Alle nicht allowlisted Send Ports sind started. | +| `CRIT` | Mindestens ein unerwartet stopped/bound Port oder unbekannter Status. | +| `UNKNOWN` | `MSBTS_SendPort` nicht verlaesslich lesbar. | + +Metriken: + +| Metrik | Bedeutung | +| --- | --- | +| `biztalk_send_ports_total` | Gesamtzahl Send Ports. | | `biztalk_send_ports_started` | Gestartete Send Ports. | -| `biztalk_send_ports_inactive` | Bound oder stopped Send Ports. | +| `biztalk_send_ports_stopped` | Enlisted, Subscription deaktiviert (`Status=2`). | +| `biztalk_send_ports_bound` | Bound/unenlisted (`Status=1`). | +| `biztalk_send_ports_unexpected_inactive` | Stopped/bound und nicht allowlisted; CRIT ab 1. | +| `biztalk_send_ports_expected_inactive` | Bewusst inactive und exakt allowlisted. | | `biztalk_send_ports_unknown` | Nicht interpretierbarer Send-Port-Status. | -| `biztalk_orchestrations` | Gesamtzahl Orchestrations. | + +## BizTalk Orchestrations + +Status: + +| Status | Bedingung | +| --- | --- | +| `OK` | Daten lesbar und kein unbekannter Status; inaktive Orchestrations sind standardmaessig nur Information. | +| `WARN` | stopped/bound/unbound bei `AlertOnInactiveOrchestrations=true`. | +| `CRIT` | Mindestens ein unbekannter Statuswert. | +| `UNKNOWN` | `MSBTS_Orchestration` nicht verlaesslich lesbar. | + +Metriken: + +| Metrik | Bedeutung | +| --- | --- | +| `biztalk_orchestrations_total` | Gesamtzahl Orchestrations. | | `biztalk_orchestrations_started` | Gestartete Orchestrations. | -| `biztalk_orchestrations_inactive` | Bound, unbound oder stopped Orchestrations. | +| `biztalk_orchestrations_stopped` | Gestoppte Orchestrations. | +| `biztalk_orchestrations_bound` | Bound Orchestrations. | +| `biztalk_orchestrations_unbound` | Unbound Orchestrations. | | `biztalk_orchestrations_unknown` | Nicht interpretierbarer Orchestration-Status. | +Allowlist-Syntax fuer Receive Locations und Send Ports: + +```text +Name|Anwendung\Name|Weiterer Name +``` + +Die Auswertung ist case-insensitive und exakt. Wildcards werden bewusst nicht +unterstuetzt, damit eine breite Regel keine neuen Artefaktfehler verdeckt. + ## BizTalk Event Log Zweck: diff --git a/docs/ExampleOutput.md b/docs/ExampleOutput.md index fc7325d..f3eed0f 100644 --- a/docs/ExampleOutput.md +++ b/docs/ExampleOutput.md @@ -7,54 +7,66 @@ 0 "BizTalk SQL Access" biztalk_sql_targets_total=0;;;0 Self test OK. No SQL connection was opened. 0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0 Self test OK. 0 "BizTalk Host Instances" biztalk_host_instances_total=0;;;0 Self test OK. No WMI query was executed. -0 "BizTalk Runtime Artifacts" biztalk_applications=0;;;0 Self test OK. No WMI query was executed. +0 "BizTalk Receive Locations" biztalk_receive_locations_total=0;;;0 Self test OK. No WMI query was executed. +0 "BizTalk Send Ports" biztalk_send_ports_total=0;;;0 Self test OK. No WMI query was executed. +0 "BizTalk Orchestrations" biztalk_orchestrations_total=0;;;0 Self test OK. No WMI query was executed. 0 "BizTalk Event Log" biztalk_eventlog_errors=0;;;0 Self test OK. No event log was read. ``` -## Gesunder BizTalk-Server +## Gesundes kompaktes Servicebild ```text -0 "BizTalk Platform" - BizTalk WMI and platform data reachable, server=BIZTALK01, group=BizTalkMgmtDb, read_only_group=CONTOSO\BizTalk Server Read Only Users, operator_group=CONTOSO\BizTalk Server Operators, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb -0 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=2;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=2, available=2, discovery_complete=True, execution_identity=CONTOSO\svc_biztalk_cmk$, network_identity=CONTOSO\svc_biztalk_cmk$; databases=Management=SQL01\BizTalkMgmtDb:OK, MasterMessageBox=SQL01\BizTalkMsgBoxDb:OK -0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|biztalk_suspended_resumable=0;1;;0|biztalk_suspended_nonresumable=0;;1;0 No suspended BizTalk service instances found. -0 "BizTalk Host Instances" biztalk_host_instances_total=4;;;0|biztalk_host_instances_started=4;;;0|biztalk_host_instances_stopped=0;;1;0|biztalk_host_instances_pending=0;1;;0|biztalk_host_instances_unknown=0;;1;0 Host instances total=4, started=4, stopped=0, pending=0, unknown=0 -0 "BizTalk Runtime Artifacts" biztalk_applications=8;;;0|biztalk_receive_locations=24;;;0|biztalk_receive_locations_disabled=0;1;;0|biztalk_send_ports=31;;;0|biztalk_send_ports_started=31;;;0|biztalk_send_ports_inactive=0;1;;0|biztalk_send_ports_unknown=0;;1;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=12;;;0|biztalk_orchestrations_inactive=0;1;;0|biztalk_orchestrations_unknown=0;;1;0 Applications=8; receive_locations total=24, disabled=0; send_ports total=31, started=31, inactive=0, unknown=0; orchestrations total=12, started=12, inactive=0, unknown=0 -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 +0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|biztalk_suspended_resumable=0;1;;0|biztalk_suspended_nonresumable=0;;1;0|biztalk_routing_failure_reports=0;;1;0 Suspended total=0, resumable=0, nonresumable=0, routing_failure_reports=0. +0 "BizTalk Receive Locations" biztalk_receive_locations_total=24;;;0|biztalk_receive_locations_enabled=23;;;0|biztalk_receive_locations_disabled=1;;;0|biztalk_receive_locations_unexpected_disabled=0;;1;0|biztalk_receive_locations_expected_disabled=1;;;0|biztalk_receive_locations_unknown=0;;1;0 Receive locations total=24, enabled=23, unexpected_disabled=0, expected_disabled=1, unknown=0 +0 "BizTalk Send Ports" biztalk_send_ports_total=31;;;0|biztalk_send_ports_started=30;;;0|biztalk_send_ports_stopped=1;;;0|biztalk_send_ports_bound=0;;;0|biztalk_send_ports_unexpected_inactive=0;;1;0|biztalk_send_ports_expected_inactive=1;;;0|biztalk_send_ports_unknown=0;;1;0 Send ports total=31, started=30, stopped=1, bound=0, unexpected_inactive=0, expected_inactive=1, unknown=0 +0 "BizTalk Orchestrations" biztalk_orchestrations_total=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;;1;0 Orchestrations total=12, started=11, stopped=1, bound=0, unbound=0, unknown=0; affected_apps=Maintenance(inactive=1,unknown=0) ``` -## Suspended Instances +Die bewusst inaktiven Receive-/Send-Artefakte stehen in den exakten +Allowlisten. Sie bleiben als Metrik sichtbar, erzeugen aber keinen Alarm. + +## Suspensions und Routing Failure Report ```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 +2 "BizTalk Suspended Instances" biztalk_suspended_total=3;;;0|biztalk_suspended_resumable=2;1;;0|biztalk_suspended_nonresumable=1;;1;0|biztalk_routing_failure_reports=1;;1;0 Suspended total=3, resumable=2, nonresumable=1, routing_failure_reports=1; applications=OrderProcessing(R=2,NR=0); recent=Receive.Order@ProcessingHost, Send.Invoice@ProcessingHost ``` -## SQL-Berechtigungsfehler +Routing Failure Reports sind eine Teilmenge der non-resumable Instanzen und +werden nicht doppelt zum Total addiert. + +## Unerwartet deaktivierte Receive Locations ```text -3 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=0;;;0|biztalk_sql_targets_failed=2;;1;0 SQL access targets=2, available=0, discovery_complete=True, execution_identity=CONTOSO\svc_biztalk_cmk$, network_identity=CONTOSO\svc_biztalk_cmk$; databases=Management=SQL01\BizTalkMgmtDb:FAILED, MasterMessageBox=SQL01\BizTalkMsgBoxDb:FAILED; diagnostics: Sql/Permission [Management SQL01\BizTalkMgmtDb]: SQL Server hat die integrierte Anmeldung abgelehnt. Massnahme: Provider-Konto und konfigurierte BizTalk-Read-Only-Gruppe mit BTS_READONLY_USERS pruefen; keine direkten BizTalk-DB-Rollen vergeben. Technik: SqlException Number=18456: Login failed for user +2 "BizTalk Receive Locations" biztalk_receive_locations_total=24;;;0|biztalk_receive_locations_enabled=22;;;0|biztalk_receive_locations_disabled=2;;;0|biztalk_receive_locations_unexpected_disabled=1;;1;0|biztalk_receive_locations_expected_disabled=1;;;0|biztalk_receive_locations_unknown=0;;1;0 Receive locations total=24, enabled=22, unexpected_disabled=1, expected_disabled=1, unknown=0; affected=Orders\RL_Order_In ``` -## WMI-Berechtigungsfehler +## Unerwartet inaktive Send Ports ```text -3 "BizTalk Suspended Instances" - Suspendierte Instanzen konnten nicht verlaesslich gelesen werden. Ein leerer Datenbestand wird nicht als OK gewertet.; diagnostics: Wmi/Permission [MSBTS_ServiceInstance]: Zugriff des Provider-Kontos auf MSBTS_ServiceInstance wurde verweigert. Massnahme: Provider-Identitaet und konfigurierte BizTalk-Read-Only-Gruppe pruefen. Technik: UnauthorizedAccessException HRESULT=0x80070005: Access is denied +2 "BizTalk Send Ports" biztalk_send_ports_total=31;;;0|biztalk_send_ports_started=29;;;0|biztalk_send_ports_stopped=1;;;0|biztalk_send_ports_bound=1;;;0|biztalk_send_ports_unexpected_inactive=1;;1;0|biztalk_send_ports_expected_inactive=1;;;0|biztalk_send_ports_unknown=0;;1;0 Send ports total=31, started=29, stopped=1, bound=1, unexpected_inactive=1, expected_inactive=1, unknown=0; affected=Orders\SP_Order_Out(bound) ``` +## Begrenzte Detailausgabe + +```text +2 "BizTalk Receive Locations" ... affected=App\RL1, App\RL2, App\RL3, App\RL4, App\RL5 (+17 more) +``` + +Die Zaehler und Performance-Daten enthalten weiterhin alle 22 betroffenen +Artefakte. Nur die Textliste ist begrenzt. + ## Fehlender oder veralteter Provider-Snapshot +Alle acht Services werden `UNKNOWN`, zum Beispiel: + ```text 3 "BizTalk Platform" - Privilegierter BizTalk-Datensnapshot nicht verfuegbar: Snapshot is stale: age=241s, maximum=180s. Massnahme: Scheduled Task 'BizTalk Checkmk Pulse Provider', Provider-Log, Snapshot-Alter und ACL pruefen. -3 "BizTalk SQL Access" - Privilegierter BizTalk-Datensnapshot nicht verfuegbar: Snapshot is stale: age=241s, maximum=180s. Massnahme: Scheduled Task 'BizTalk Checkmk Pulse Provider', Provider-Log, Snapshot-Alter und ACL pruefen. ``` ## WMI-Schemafehler -```text -3 "BizTalk Runtime Artifacts" - BizTalk Runtime Artifacts konnten nicht vollstaendig gelesen werden. Teilwerte werden nicht als OK gewertet.; diagnostics: Wmi/Schema [MSBTS_Orchestration]: WMI-Klasse oder WQL fuer MSBTS_Orchestration wird vom installierten BizTalk-Provider nicht unterstuetzt. Massnahme: Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema und die Microsoft-Dokumentation pruefen. Rechteerhoehung behebt InvalidClass/InvalidQuery nicht. Technik: ManagementException HRESULT=0x80041010: Invalid class WQL={SELECT * FROM MSBTS_Orchestration} DurationMs=18 -``` - -## Fehlendes Deployment +Nur der fachlich betroffene Service wird `UNKNOWN`: ```text -3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter C:\Program Files\BizTalkCheckmkPulse. Massnahme: Install-BizTalkCheckmkPulse.ps1 erneut als Administrator ausfuehren und danach cmk-agent-ctl.exe dump erneut ausfuehren. +3 "BizTalk Orchestrations" - Orchestrations konnten nicht verlaesslich gelesen werden.; diagnostics: Wmi/Schema [MSBTS_Orchestration]: WMI-Klasse oder WQL wird nicht unterstuetzt. Massnahme: Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema pruefen. ``` diff --git a/docs/Integration.md b/docs/Integration.md index 1299fad..8b3ef11 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -4,7 +4,7 @@ | Team | Aufgabe | | --- | --- | -| AD/Security | dediziertes Dienstkonto oder gMSA; Aufnahme in exakt konfigurierte BizTalk-Read-Only-Gruppe | +| AD/Security | normales dediziertes Servicekonto; Aufnahme in exakt konfigurierte BizTalk-Read-Only-Gruppe | | BizTalk/SQL | Gruppenabbildung und `BTS_READONLY_USERS` bestaetigen; keine Einzelrechte | | Windows | Paket installieren, ACL und Scheduled Task pruefen | | Checkmk | Wrapper verteilen beziehungsweise Installation koordinieren, Discovery und Alarmierung | @@ -15,16 +15,7 @@ Provider-Konto und publiziert einen validierbaren Snapshot. ## Installation -gMSA: - -```powershell -.\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk$' ` - -Gmsa ` - -EnvironmentName ACC -``` - -Regulaeres Dienstkonto: +Normales Servicekonto: ```powershell .\Install-BizTalkCheckmkPulse.ps1 ` @@ -70,7 +61,8 @@ Verbindlicher Agent-Dump: 2. `LastTaskResult=0`, frischen Snapshot und Log pruefen. 3. Agent-Dump im `LocalSystem`-Kontext pruefen. 4. Service Discovery fuer den BizTalk-Host ausfuehren. -5. sechs stabile Services aufnehmen. +5. acht stabile Services aufnehmen und den alten + `BizTalk Runtime Artifacts`-Service entfernen. 6. Changes aktivieren. 7. Views, Servicegruppen und Benachrichtigungen einrichten. @@ -117,11 +109,13 @@ Soll: - Log nennt das dedizierte Providerkonto. - Platform zeigt `read_only_group=...`. - SQL Access zeigt `targets=2`, `available=2`. +- Suspended Instances zeigt total/resumable/non-resumable/routing failures. +- Receive Locations und Send Ports zeigen getrennte expected/unexpected Werte. - keine berechtigungsbedingten `UNKNOWN`-Services. ## Alarmierung der Transportkette -Ein Ausfall des Providers wird ueber alle sechs Services als `UNKNOWN` +Ein Ausfall des Providers wird ueber alle acht Services als `UNKNOWN` sichtbar. Die Summary nennt fehlenden, unlesbaren, ungueltigen oder stale Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso eskaliert werden wie ein technischer Monitoringausfall. diff --git a/src/BizTalkCheckmkPulse/App.config b/src/BizTalkCheckmkPulse/App.config index c2534f6..f02c176 100644 --- a/src/BizTalkCheckmkPulse/App.config +++ b/src/BizTalkCheckmkPulse/App.config @@ -20,10 +20,17 @@ - + + + - - + + + + + + + diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs index f551d5f..c35a277 100644 --- a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs +++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs @@ -19,6 +19,9 @@ namespace BizTalkCheckmkPulse private const int HostPausePending = 6; private const int HostPaused = 7; private const int HostUnknown = 8; + private const int SendPortBound = 1; + private const int SendPortStopped = 2; + private const int SendPortStarted = 3; private readonly MonitoringOptions _options; /// @@ -41,7 +44,9 @@ namespace BizTalkCheckmkPulse yield return FormatSqlAccess(result); yield return FormatSuspendedInstances(result); yield return FormatHostInstances(result); - yield return FormatRuntimeArtifacts(result); + yield return FormatReceiveLocations(result); + yield return FormatSendPorts(result); + yield return FormatOrchestrations(result); yield return FormatEventLog(result); if (_options.EmitPerApplicationSuspensionServices && result.Platform.SuspendedInstancesDataAvailable) @@ -63,7 +68,9 @@ namespace BizTalkCheckmkPulse yield return BuildLine(CheckState.Ok, _options.ServiceName("SQL Access"), "biztalk_sql_targets_total=0;;;0", "Self test OK. No SQL connection was opened."); yield return BuildLine(CheckState.Ok, _options.ServiceName("Suspended Instances"), "biztalk_suspended_total=0;;;0", "Self test OK."); yield return BuildLine(CheckState.Ok, _options.ServiceName("Host Instances"), "biztalk_host_instances_total=0;;;0", "Self test OK. No WMI query was executed."); - yield return BuildLine(CheckState.Ok, _options.ServiceName("Runtime Artifacts"), "biztalk_applications=0;;;0", "Self test OK. No WMI query was executed."); + yield return BuildLine(CheckState.Ok, _options.ServiceName("Receive Locations"), "biztalk_receive_locations_total=0;;;0", "Self test OK. No WMI query was executed."); + yield return BuildLine(CheckState.Ok, _options.ServiceName("Send Ports"), "biztalk_send_ports_total=0;;;0", "Self test OK. No WMI query was executed."); + yield return BuildLine(CheckState.Ok, _options.ServiceName("Orchestrations"), "biztalk_orchestrations_total=0;;;0", "Self test OK. No WMI query was executed."); yield return BuildLine(CheckState.Ok, _options.ServiceName("Event Log"), "biztalk_eventlog_errors=0;;;0", "Self test OK. No event log was read."); } @@ -79,7 +86,9 @@ namespace BizTalkCheckmkPulse yield return BuildLine(CheckState.Unknown, _options.ServiceName("SQL Access"), "-", action); yield return BuildLine(CheckState.Unknown, _options.ServiceName("Suspended Instances"), "-", action); yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", action); - yield return BuildLine(CheckState.Unknown, _options.ServiceName("Runtime Artifacts"), "-", action); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Receive Locations"), "-", action); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Send Ports"), "-", action); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Orchestrations"), "-", action); yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", action); } @@ -95,7 +104,9 @@ namespace BizTalkCheckmkPulse yield return BuildLine(CheckState.Unknown, _options.ServiceName("SQL Access"), "-", detail); yield return BuildLine(CheckState.Unknown, _options.ServiceName("Suspended Instances"), "-", detail); yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", detail); - yield return BuildLine(CheckState.Unknown, _options.ServiceName("Runtime Artifacts"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Receive Locations"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Send Ports"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Orchestrations"), "-", detail); yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", detail); } @@ -182,19 +193,22 @@ namespace BizTalkCheckmkPulse 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 routingFailureReports = result.SuspendedInstances.Count(x => x.IsRoutingFailureReport); + var state = DetermineSuspensionState(resumable, nonresumable, routingFailureReports); var metrics = string.Format( CultureInfo.InvariantCulture, - "biztalk_suspended_total={0};;;0|biztalk_suspended_resumable={1};{2};;0|biztalk_suspended_nonresumable={3};;{4};0", + "biztalk_suspended_total={0};;;0|biztalk_suspended_resumable={1};{2};;0|biztalk_suspended_nonresumable={3};;{4};0|biztalk_routing_failure_reports={5};;{6};0", total, resumable, _options.WarnResumableThreshold, nonresumable, - _options.CritNonResumableThreshold); + _options.CritNonResumableThreshold, + routingFailureReports, + _options.CritRoutingFailureThreshold); var detail = total == 0 - ? "No suspended BizTalk service instances found." - : BuildSuspensionDetail(result.SuspendedInstances, total, resumable, nonresumable); + ? "Suspended total=0, resumable=0, nonresumable=0, routing_failure_reports=0." + : BuildSuspensionDetail(result.SuspendedInstances, total, resumable, nonresumable, routingFailureReports); return BuildLine(state, _options.ServiceName("Suspended Instances"), metrics, detail); } @@ -245,65 +259,147 @@ namespace BizTalkCheckmkPulse return BuildLine(state, _options.ServiceName("Host Instances"), metrics, detail.ToString()); } - /// - /// Formatiert aggregierte Receive-Location-, Send-Port- und Orchestration-Zustaende. - /// - /// Probe-Ergebnis. - /// Checkmk-Zeile fuer Runtime Artifacts. - private string FormatRuntimeArtifacts(ProbeResult result) + private string FormatReceiveLocations(ProbeResult result) { - if (!result.Platform.RuntimeArtifactsDataAvailable) + if (!result.Platform.ReceiveLocationsDataAvailable) { - var errorDetail = "BizTalk Runtime Artifacts konnten nicht vollstaendig gelesen werden. Teilwerte werden nicht als OK gewertet."; return BuildLine( CheckState.Unknown, - _options.ServiceName("Runtime Artifacts"), + _options.ServiceName("Receive Locations"), "-", - AppendDiagnostics(errorDetail, SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_ReceiveLocation", "MSBTS_SendPort", "MSBTS_Orchestration"))); + AppendDiagnostics( + "Receive Locations konnten nicht verlaesslich gelesen werden.", + SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_ReceiveLocation"))); } - 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 + var disabled = result.ReceiveLocations.Where(x => x.IsDisabled == true).ToArray(); + var unknown = result.ReceiveLocations.Where(x => !x.IsDisabled.HasValue).ToArray(); + var expectedDisabled = disabled + .Where(x => _options.IsExpectedDisabledReceiveLocation(x.ApplicationName, x.Name)) + .ToArray(); + var unexpectedDisabled = disabled.Except(expectedDisabled).ToArray(); + var enabled = result.ReceiveLocations.Count - disabled.Length - unknown.Length; + var state = unknown.Length > 0 + ? CheckState.Unknown + : _options.AlertOnArtifactRuntimeIssues && unexpectedDisabled.Length > 0 + ? CheckState.Critical + : CheckState.Ok; + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_receive_locations_total={0};;;0|biztalk_receive_locations_enabled={1};;;0|biztalk_receive_locations_disabled={2};;;0|biztalk_receive_locations_unexpected_disabled={3};;1;0|biztalk_receive_locations_expected_disabled={4};;;0|biztalk_receive_locations_unknown={5};;1;0", + result.ReceiveLocations.Count, + enabled, + disabled.Length, + unexpectedDisabled.Length, + expectedDisabled.Length, + unknown.Length); + var detail = new StringBuilder() + .Append("Receive locations total=").Append(result.ReceiveLocations.Count) + .Append(", enabled=").Append(enabled) + .Append(", unexpected_disabled=").Append(unexpectedDisabled.Length) + .Append(", expected_disabled=").Append(expectedDisabled.Length) + .Append(", unknown=").Append(unknown.Length); + AppendLimitedList(detail, "affected", unexpectedDisabled.Concat(unknown).Select(ReceiveLocationDisplay)); + return BuildLine(state, _options.ServiceName("Receive Locations"), metrics, detail.ToString()); + } + + private string FormatSendPorts(ProbeResult result) + { + if (!result.Platform.SendPortsDataAvailable) + { + return BuildLine( + CheckState.Unknown, + _options.ServiceName("Send Ports"), + "-", + AppendDiagnostics( + "Send Ports konnten nicht verlaesslich gelesen werden.", + SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_SendPort"))); + } + + var started = result.SendPorts.Count(x => x.Status == SendPortStarted); + var stopped = result.SendPorts.Count(x => x.Status == SendPortStopped); + var bound = result.SendPorts.Count(x => x.Status == SendPortBound); + var unknown = result.SendPorts.Where(x => x.Status != SendPortStarted && x.Status != SendPortStopped && x.Status != SendPortBound).ToArray(); + var inactive = result.SendPorts.Where(x => x.Status == SendPortStopped || x.Status == SendPortBound).ToArray(); + var expectedInactive = inactive + .Where(x => _options.IsExpectedInactiveSendPort(x.ApplicationName, x.Name)) + .ToArray(); + var unexpectedInactive = inactive.Except(expectedInactive).ToArray(); + var state = unknown.Length > 0 || (_options.AlertOnArtifactRuntimeIssues && unexpectedInactive.Length > 0) ? CheckState.Critical - : _options.AlertOnArtifactRuntimeIssues && (receiveLocationsDisabled > 0 || sendPortsInactive > 0 || orchestrationsInactive > 0) + : CheckState.Ok; + var metrics = string.Format( + CultureInfo.InvariantCulture, + "biztalk_send_ports_total={0};;;0|biztalk_send_ports_started={1};;;0|biztalk_send_ports_stopped={2};;;0|biztalk_send_ports_bound={3};;;0|biztalk_send_ports_unexpected_inactive={4};;1;0|biztalk_send_ports_expected_inactive={5};;;0|biztalk_send_ports_unknown={6};;1;0", + result.SendPorts.Count, + started, + stopped, + bound, + unexpectedInactive.Length, + expectedInactive.Length, + unknown.Length); + var detail = new StringBuilder() + .Append("Send ports total=").Append(result.SendPorts.Count) + .Append(", started=").Append(started) + .Append(", stopped=").Append(stopped) + .Append(", bound=").Append(bound) + .Append(", unexpected_inactive=").Append(unexpectedInactive.Length) + .Append(", expected_inactive=").Append(expectedInactive.Length) + .Append(", unknown=").Append(unknown.Length); + AppendLimitedList(detail, "affected", unexpectedInactive.Concat(unknown).Select(SendPortDisplay)); + return BuildLine(state, _options.ServiceName("Send Ports"), metrics, detail.ToString()); + } + + private string FormatOrchestrations(ProbeResult result) + { + if (!result.Platform.OrchestrationsDataAvailable) + { + return BuildLine( + CheckState.Unknown, + _options.ServiceName("Orchestrations"), + "-", + AppendDiagnostics( + "Orchestrations konnten nicht verlaesslich gelesen werden.", + SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_Orchestration"))); + } + + var total = result.Applications.Sum(x => x.OrchestrationTotal); + var started = result.Applications.Sum(x => x.OrchestrationStarted); + var stopped = result.Applications.Sum(x => x.OrchestrationStopped); + var bound = result.Applications.Sum(x => x.OrchestrationBound); + var unbound = result.Applications.Sum(x => x.OrchestrationUnbound); + var unknown = result.Applications.Sum(x => x.OrchestrationUnknown); + var inactive = stopped + bound + unbound; + var state = unknown > 0 + ? CheckState.Critical + : _options.AlertOnInactiveOrchestrations && inactive > 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); - + "biztalk_orchestrations_total={0};;;0|biztalk_orchestrations_started={1};;;0|biztalk_orchestrations_stopped={2};;;0|biztalk_orchestrations_bound={3};;;0|biztalk_orchestrations_unbound={4};;;0|biztalk_orchestrations_unknown={5};;1;0", + total, + started, + stopped, + bound, + unbound, + unknown); + var detail = new StringBuilder() + .Append("Orchestrations total=").Append(total) + .Append(", started=").Append(started) + .Append(", stopped=").Append(stopped) + .Append(", bound=").Append(bound) + .Append(", unbound=").Append(unbound) + .Append(", unknown=").Append(unknown); 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()); + .Where(x => x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound + x.OrchestrationUnknown > 0) + .OrderByDescending(x => x.OrchestrationUnknown) + .ThenByDescending(x => x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound) + .Select(x => x.ApplicationName + "(inactive=" + + (x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound) + + ",unknown=" + x.OrchestrationUnknown + ")"); + AppendLimitedList(detail, "affected_apps", affected); + return BuildLine(state, _options.ServiceName("Orchestrations"), metrics, detail.ToString()); } /// @@ -362,16 +458,24 @@ namespace BizTalkCheckmkPulse { var resumable = group.Count(x => x.Kind == SuspendedKind.Resumable); var nonresumable = group.Count(x => x.Kind == SuspendedKind.NonResumable); - var state = DetermineSuspensionState(resumable, nonresumable); + var routingFailureReports = group.Count(x => x.IsRoutingFailureReport); + var state = DetermineSuspensionState(resumable, nonresumable, routingFailureReports); var metrics = string.Format( CultureInfo.InvariantCulture, - "biztalk_suspended_total={0};;;0|biztalk_suspended_resumable={1};{2};;0|biztalk_suspended_nonresumable={3};;{4};0", + "biztalk_suspended_total={0};;;0|biztalk_suspended_resumable={1};{2};;0|biztalk_suspended_nonresumable={3};;{4};0|biztalk_routing_failure_reports={5};;{6};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); + _options.CritNonResumableThreshold, + routingFailureReports, + _options.CritRoutingFailureThreshold); + yield return BuildLine( + state, + _options.ServiceName("Suspended " + group.Key), + metrics, + "Suspended total=" + group.Count() + ", resumable=" + resumable + + ", nonresumable=" + nonresumable + ", routing_failure_reports=" + routingFailureReports); } } @@ -381,8 +485,14 @@ namespace BizTalkCheckmkPulse /// Anzahl fortsetzbarer Instanzen. /// Anzahl nicht fortsetzbarer Instanzen. /// Zustand gemaess konfigurierten Schwellwerten. - private CheckState DetermineSuspensionState(int resumable, int nonresumable) + private CheckState DetermineSuspensionState(int resumable, int nonresumable, int routingFailureReports) { + if (_options.CritRoutingFailureThreshold > 0 + && routingFailureReports >= _options.CritRoutingFailureThreshold) + { + return CheckState.Critical; + } + if (_options.CritNonResumableThreshold > 0 && nonresumable >= _options.CritNonResumableThreshold) { return CheckState.Critical; @@ -404,24 +514,32 @@ namespace BizTalkCheckmkPulse /// Anzahl fortsetzbarer Instanzen. /// Anzahl nicht fortsetzbarer Instanzen. /// Kompakter Detailtext. - private string BuildSuspensionDetail(IEnumerable instances, int total, int resumable, int nonresumable) + private string BuildSuspensionDetail( + IEnumerable instances, + int total, + int resumable, + int nonresumable, + int routingFailureReports) { var builder = new StringBuilder(); - builder.Append(total).Append(" suspended BizTalk service instance(s): resumable=").Append(resumable) - .Append(", nonresumable=").Append(nonresumable); + builder.Append("Suspended total=").Append(total) + .Append(", resumable=").Append(resumable) + .Append(", nonresumable=").Append(nonresumable) + .Append(", routing_failure_reports=").Append(routingFailureReports); var apps = instances + .Where(x => !x.IsRoutingFailureReport + && !string.Equals(x.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)) .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); + .Select(x => x.Key + "(R=" + x.Count(y => y.Kind == SuspendedKind.Resumable) + ",NR=" + x.Count(y => y.Kind == SuspendedKind.NonResumable) + ")"); + AppendLimitedList(builder, "applications", apps); var examples = instances + .Where(x => !x.IsRoutingFailureReport) .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); + .Select(x => EmptyAsUnknown(x.ServiceName) + "@" + EmptyAsUnknown(x.HostName)); + AppendLimitedList(builder, "recent", examples); return builder.ToString(); } @@ -433,13 +551,20 @@ namespace BizTalkCheckmkPulse /// Performance-Daten oder Bindestrich. /// Menschenlesbare Service-Ausgabe. /// Vollstaendige Local-Check-Zeile. - private static string BuildLine(CheckState state, string serviceName, string metrics, string detail) + private string BuildLine(CheckState state, string serviceName, string metrics, string detail) { + var sanitizedDetail = SanitizeDetail(detail); + if (sanitizedDetail.Length > _options.MaxDetailCharacters) + { + sanitizedDetail = sanitizedDetail.Substring(0, _options.MaxDetailCharacters - 16).TrimEnd() + + "... [truncated]"; + } + return ((int)state).ToString(CultureInfo.InvariantCulture) + " \"" + SanitizeService(serviceName) + "\" " + (string.IsNullOrWhiteSpace(metrics) ? "-" : metrics) + " " - + SanitizeDetail(detail); + + sanitizedDetail; } /// @@ -489,6 +614,57 @@ namespace BizTalkCheckmkPulse } } + private void AppendLimitedList(StringBuilder builder, string label, IEnumerable values) + { + var all = values.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (all.Length == 0) + { + return; + } + + var visible = all.Take(_options.MaxSummaryItems).ToArray(); + builder.Append("; ").Append(label).Append("=").Append(string.Join(", ", visible)); + if (all.Length > visible.Length) + { + builder.Append(" (+").Append(all.Length - visible.Length).Append(" more)"); + } + } + + private static string ReceiveLocationDisplay(ReceiveLocationState item) + { + return ArtifactDisplay(item.ApplicationName, item.Name); + } + + private static string SendPortDisplay(SendPortState item) + { + return ArtifactDisplay(item.ApplicationName, item.Name) + + "(" + SendPortStateName(item.Status) + ")"; + } + + private static string ArtifactDisplay(string applicationName, string artifactName) + { + var application = EmptyAsUnknown(applicationName); + var name = EmptyAsUnknown(artifactName); + return string.Equals(application, "(unknown)", StringComparison.OrdinalIgnoreCase) + ? name + : application + "\\" + name; + } + + private static string SendPortStateName(int status) + { + switch (status) + { + case SendPortStarted: + return "started"; + case SendPortStopped: + return "stopped"; + case SendPortBound: + return "bound"; + default: + return "unknown:" + status.ToString(CultureInfo.InvariantCulture); + } + } + /// /// Haengt einen optionalen benannten Wert an einen Detailtext an. /// diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs index 8229c8f..fbb87fa 100644 --- a/src/BizTalkCheckmkPulse/Models.cs +++ b/src/BizTalkCheckmkPulse/Models.cs @@ -96,6 +96,8 @@ namespace BizTalkCheckmkPulse Platform = new PlatformState(); HostInstances = new List(); SuspendedInstances = new List(); + ReceiveLocations = new List(); + SendPorts = new List(); Applications = new List(); SqlTargets = new List(); EventLog = new EventLogState(); @@ -105,6 +107,8 @@ namespace BizTalkCheckmkPulse public PlatformState Platform { get; private set; } public List HostInstances { get; private set; } public List SuspendedInstances { get; private set; } + public List ReceiveLocations { get; private set; } + public List SendPorts { get; private set; } public List Applications { get; private set; } public List SqlTargets { get; private set; } public string ExecutionIdentity { get; set; } @@ -121,7 +125,9 @@ namespace BizTalkCheckmkPulse public bool PlatformDataAvailable { get; set; } public bool HostInstancesDataAvailable { get; set; } public bool SuspendedInstancesDataAvailable { get; set; } - public bool RuntimeArtifactsDataAvailable { get; set; } + public bool ReceiveLocationsDataAvailable { get; set; } + public bool SendPortsDataAvailable { get; set; } + public bool OrchestrationsDataAvailable { get; set; } public string ServerName { get; set; } public string GroupName { get; set; } public string OperatorGroup { get; set; } @@ -169,7 +175,33 @@ namespace BizTalkCheckmkPulse public string InstanceId { get; set; } public string ErrorDescription { get; set; } public DateTime? SuspendTime { get; set; } + public int ServiceClassId { get; set; } public SuspendedKind Kind { get; set; } + + public bool IsRoutingFailureReport + { + get { return ServiceClassId == 64; } + } + } + + /// + /// Konfigurations- und Laufzeitzustand einer Receive Location. + /// + internal sealed class ReceiveLocationState + { + public string Name { get; set; } + public string ApplicationName { get; set; } + public bool? IsDisabled { get; set; } + } + + /// + /// Laufzeitzustand eines Send Ports. + /// + internal sealed class SendPortState + { + public string Name { get; set; } + public string ApplicationName { get; set; } + public int Status { get; set; } } /// diff --git a/src/BizTalkCheckmkPulse/MonitoringOptions.cs b/src/BizTalkCheckmkPulse/MonitoringOptions.cs index a0407ab..1b44645 100644 --- a/src/BizTalkCheckmkPulse/MonitoringOptions.cs +++ b/src/BizTalkCheckmkPulse/MonitoringOptions.cs @@ -20,8 +20,13 @@ namespace BizTalkCheckmkPulse public int SqlConnectionTimeoutSeconds { get; set; } public int WarnResumableThreshold { get; set; } public int CritNonResumableThreshold { get; set; } + public int CritRoutingFailureThreshold { get; set; } public int MaxSummaryItems { get; set; } + public int MaxDetailCharacters { get; set; } public bool AlertOnArtifactRuntimeIssues { get; set; } + public bool AlertOnInactiveOrchestrations { get; set; } + public IReadOnlyList ExpectedDisabledReceiveLocations { get; set; } + public IReadOnlyList ExpectedInactiveSendPorts { get; set; } public bool EmitPerApplicationSuspensionServices { get; set; } public bool ProbeEventLog { get; set; } public int EventLogLookbackMinutes { get; set; } @@ -49,8 +54,13 @@ namespace BizTalkCheckmkPulse SqlConnectionTimeoutSeconds = 5; WarnResumableThreshold = 1; CritNonResumableThreshold = 1; - MaxSummaryItems = 12; - AlertOnArtifactRuntimeIssues = false; + CritRoutingFailureThreshold = 1; + MaxSummaryItems = 5; + MaxDetailCharacters = 1600; + AlertOnArtifactRuntimeIssues = true; + AlertOnInactiveOrchestrations = false; + ExpectedDisabledReceiveLocations = new string[0]; + ExpectedInactiveSendPorts = new string[0]; EmitPerApplicationSuspensionServices = false; ProbeEventLog = true; EventLogLookbackMinutes = 60; @@ -96,8 +106,13 @@ namespace BizTalkCheckmkPulse options.SqlConnectionTimeoutSeconds = ReadInt(settings, "SqlConnectionTimeoutSeconds", options.SqlConnectionTimeoutSeconds, 3, 60); options.WarnResumableThreshold = ReadInt(settings, "WarnResumableThreshold", options.WarnResumableThreshold, 0, 1000000); options.CritNonResumableThreshold = ReadInt(settings, "CritNonResumableThreshold", options.CritNonResumableThreshold, 0, 1000000); + options.CritRoutingFailureThreshold = ReadInt(settings, "CritRoutingFailureThreshold", options.CritRoutingFailureThreshold, 0, 1000000); options.MaxSummaryItems = ReadInt(settings, "MaxSummaryItems", options.MaxSummaryItems, 1, 100); + options.MaxDetailCharacters = ReadInt(settings, "MaxDetailCharacters", options.MaxDetailCharacters, 256, 8192); options.AlertOnArtifactRuntimeIssues = ReadBool(settings, "AlertOnArtifactRuntimeIssues", options.AlertOnArtifactRuntimeIssues); + options.AlertOnInactiveOrchestrations = ReadBool(settings, "AlertOnInactiveOrchestrations", options.AlertOnInactiveOrchestrations); + options.ExpectedDisabledReceiveLocations = ReadExactList(ReadString(settings, "ExpectedDisabledReceiveLocations", string.Empty)); + options.ExpectedInactiveSendPorts = ReadExactList(ReadString(settings, "ExpectedInactiveSendPorts", string.Empty)); options.EmitPerApplicationSuspensionServices = ReadBool(settings, "EmitPerApplicationSuspensionServices", options.EmitPerApplicationSuspensionServices); options.ProbeEventLog = ReadBool(settings, "ProbeEventLog", options.ProbeEventLog); options.EventLogLookbackMinutes = ReadInt(settings, "EventLogLookbackMinutes", options.EventLogLookbackMinutes, 1, 10080); @@ -245,6 +260,49 @@ namespace BizTalkCheckmkPulse .ToArray(); } + /// + /// Liest eine Pipe-getrennte, exakte Allowlist. Wildcards werden bewusst nicht interpretiert. + /// + private static IReadOnlyList ReadExactList(string value) + { + return (value ?? string.Empty) + .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => x.Length > 0) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + internal bool IsExpectedDisabledReceiveLocation(string applicationName, string artifactName) + { + return MatchesArtifact(ExpectedDisabledReceiveLocations, applicationName, artifactName); + } + + internal bool IsExpectedInactiveSendPort(string applicationName, string artifactName) + { + return MatchesArtifact(ExpectedInactiveSendPorts, applicationName, artifactName); + } + + private static bool MatchesArtifact( + IEnumerable configuredEntries, + string applicationName, + string artifactName) + { + var name = (artifactName ?? string.Empty).Trim(); + var application = (applicationName ?? string.Empty).Trim(); + if (name.Length == 0) + { + return false; + } + + var qualified = application.Length == 0 || string.Equals(application, "(unknown)", StringComparison.OrdinalIgnoreCase) + ? string.Empty + : application + "\\" + name; + return configuredEntries.Any(x => + string.Equals(x, name, StringComparison.OrdinalIgnoreCase) + || (qualified.Length > 0 && string.Equals(x, qualified, StringComparison.OrdinalIgnoreCase))); + } + /// /// Prueft einen Wert ohne Beachtung der Gross-/Kleinschreibung gegen Kandidaten. /// diff --git a/src/BizTalkCheckmkPulse/Program.cs b/src/BizTalkCheckmkPulse/Program.cs index 02b1690..d5048ec 100644 --- a/src/BizTalkCheckmkPulse/Program.cs +++ b/src/BizTalkCheckmkPulse/Program.cs @@ -70,6 +70,11 @@ namespace BizTalkCheckmkPulse { logger.Info("Collection started. snapshot=" + options.SnapshotPath); var result = Collect(options); + foreach (var diagnostic in result.Diagnostics) + { + logger.Warning("Probe diagnostic: " + diagnostic.ToDisplayText()); + } + var lines = formatter.Format(result).ToArray(); new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes) .Write(lines, DateTime.UtcNow, CurrentIdentity()); @@ -79,6 +84,20 @@ namespace BizTalkCheckmkPulse + lines.Length + " diagnostics=" + result.Diagnostics.Count + + " suspended_total=" + + result.SuspendedInstances.Count + + " routing_failure_reports=" + + result.SuspendedInstances.Count(x => x.IsRoutingFailureReport) + + " receive_locations_total=" + + result.ReceiveLocations.Count + + " receive_locations_disabled=" + + result.ReceiveLocations.Count(x => x.IsDisabled == true) + + " receive_locations_unknown=" + + result.ReceiveLocations.Count(x => !x.IsDisabled.HasValue) + + " send_ports_total=" + + result.SendPorts.Count + + " send_ports_not_started=" + + result.SendPorts.Count(x => x.Status != 3) + " elapsed_ms=" + stopwatch.ElapsedMilliseconds); return 0; diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs index 21ca04c..76c7c40 100644 --- a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ using System.Reflection; [assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")] [assembly: AssemblyCompany("BEW")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] diff --git a/src/BizTalkCheckmkPulse/SnapshotStore.cs b/src/BizTalkCheckmkPulse/SnapshotStore.cs index b4120b7..2daed38 100644 --- a/src/BizTalkCheckmkPulse/SnapshotStore.cs +++ b/src/BizTalkCheckmkPulse/SnapshotStore.cs @@ -15,7 +15,7 @@ namespace BizTalkCheckmkPulse /// internal sealed class SnapshotStore { - internal const string Magic = "BIZTALK_CHECKMK_PULSE_SNAPSHOT_V1"; + internal const string Magic = "BIZTALK_CHECKMK_PULSE_SNAPSHOT_V2"; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true); private readonly string _path; private readonly int _maxBytes; @@ -258,9 +258,9 @@ namespace BizTalkCheckmkPulse } } - if (count < 6) + if (count < 8) { - throw new InvalidDataException("Snapshot must contain all six stable services."); + throw new InvalidDataException("Snapshot must contain all eight stable services."); } } diff --git a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs index 8a3a003..619e468 100644 --- a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs +++ b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs @@ -26,6 +26,8 @@ namespace BizTalkCheckmkPulse private const int EAccessDenied = unchecked((int)0x80070005); internal const string GroupSettingQuery = "SELECT Name, BizTalkOperatorGroup, BizTalkReadOnlyUserGroup, MgmtDbServerName, MgmtDbName, SubscriptionDBServerName, SubscriptionDBName FROM MSBTS_GroupSetting"; + internal const string SuspendedInstancesQuery = + "SELECT * FROM MSBTS_ServiceInstance WHERE ServiceStatus = 4 OR ServiceStatus = 32 OR ServiceClass = 64"; private readonly MonitoringOptions _options; /// @@ -58,7 +60,7 @@ namespace BizTalkCheckmkPulse result.Platform.PlatformDataAvailable = QueryPlatform(scope, result); result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result, server); var applicationIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); - result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result, applicationIndex); + QueryApplicationRuntime(scope, result, applicationIndex); if (_options.EmitPerApplicationSuspensionServices) { AddArtifactIndex(scope, result, applicationIndex, "MSBTS_ReceivePort"); @@ -196,31 +198,52 @@ namespace BizTalkCheckmkPulse /// Verbundener BizTalk-WMI-Scope. /// Zu aktualisierendes Ergebnisobjekt. /// Best-Effort-Index fuer Suspensionsdetails. - /// true, wenn alle drei Pflichtabfragen erfolgreich waren. - private bool QueryApplicationRuntime( + private void QueryApplicationRuntime( ManagementScope scope, ProbeResult result, Dictionary applicationIndex) { var apps = new Dictionary(StringComparer.OrdinalIgnoreCase); - var receiveLocationsAvailable = TryQuery(scope, result, "MSBTS_ReceiveLocation", "SELECT * FROM MSBTS_ReceiveLocation", item => + result.Platform.ReceiveLocationsDataAvailable = TryQuery(scope, result, "MSBTS_ReceiveLocation", "SELECT * FROM MSBTS_ReceiveLocation", item => { AddArtifactIndexEntry(applicationIndex, item); - var app = GetApplication(apps, GetApplicationName(item)); + var applicationName = GetApplicationName(item); + var name = FirstNonEmpty(WmiHelpers.GetString(item, "Name"), WmiHelpers.GetString(item, "ReceivePortName")) ?? "(unknown)"; + bool parsedIsDisabled; + var isDisabledText = WmiHelpers.GetString(item, "IsDisabled"); + var isDisabled = bool.TryParse(isDisabledText, out parsedIsDisabled) + ? (bool?)parsedIsDisabled + : null; + result.ReceiveLocations.Add(new ReceiveLocationState + { + Name = name, + ApplicationName = applicationName, + IsDisabled = isDisabled + }); + var app = GetApplication(apps, applicationName); app.ReceiveLocationTotal++; - if (WmiHelpers.GetBoolean(item, "IsDisabled", false)) + if (isDisabled == true) { app.ReceiveLocationDisabled++; } }, true); - var sendPortsAvailable = TryQuery(scope, result, "MSBTS_SendPort", "SELECT * FROM MSBTS_SendPort", item => + result.Platform.SendPortsDataAvailable = TryQuery(scope, result, "MSBTS_SendPort", "SELECT * FROM MSBTS_SendPort", item => { AddArtifactIndexEntry(applicationIndex, item); - var app = GetApplication(apps, GetApplicationName(item)); + var applicationName = GetApplicationName(item); + var name = WmiHelpers.GetString(item, "Name"); + var status = WmiHelpers.GetInt32(item, "Status", 0); + result.SendPorts.Add(new SendPortState + { + Name = string.IsNullOrWhiteSpace(name) ? "(unknown)" : name, + ApplicationName = applicationName, + Status = status + }); + var app = GetApplication(apps, applicationName); app.SendPortTotal++; - switch (WmiHelpers.GetInt32(item, "Status", 0)) + switch (status) { case SendPortStarted: app.SendPortStarted++; @@ -237,7 +260,7 @@ namespace BizTalkCheckmkPulse } }, true); - var orchestrationsAvailable = TryQuery(scope, result, "MSBTS_Orchestration", "SELECT * FROM MSBTS_Orchestration", item => + result.Platform.OrchestrationsDataAvailable = TryQuery(scope, result, "MSBTS_Orchestration", "SELECT * FROM MSBTS_Orchestration", item => { AddArtifactIndexEntry(applicationIndex, item); var app = GetApplication(apps, GetApplicationName(item)); @@ -263,7 +286,6 @@ namespace BizTalkCheckmkPulse }, true); result.Applications.AddRange(apps.Values.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)); - return receiveLocationsAvailable && sendPortsAvailable && orchestrationsAvailable; } /// @@ -275,9 +297,10 @@ namespace BizTalkCheckmkPulse /// true, wenn die Pflichtabfrage erfolgreich war. private bool QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary applicationIndex) { - return TryQuery(scope, result, "MSBTS_ServiceInstance", "SELECT * FROM MSBTS_ServiceInstance WHERE ServiceStatus = 4 OR ServiceStatus = 32", item => + return TryQuery(scope, result, "MSBTS_ServiceInstance", SuspendedInstancesQuery, item => { var serviceStatus = WmiHelpers.GetInt32(item, "ServiceStatus", 0); + var serviceClass = WmiHelpers.GetInt32(item, "ServiceClass", 0); result.SuspendedInstances.Add(new SuspendedInstance { ApplicationName = ResolveApplication(item, applicationIndex), @@ -287,7 +310,10 @@ namespace BizTalkCheckmkPulse InstanceId = WmiHelpers.GetString(item, "InstanceID"), ErrorDescription = WmiHelpers.GetString(item, "ErrorDescription"), SuspendTime = WmiHelpers.GetDmtfDateTime(item, "SuspendTime"), - Kind = serviceStatus == SuspendedNonResumable ? SuspendedKind.NonResumable : SuspendedKind.Resumable + ServiceClassId = serviceClass, + Kind = serviceStatus == SuspendedNonResumable || serviceClass == 64 + ? SuspendedKind.NonResumable + : SuspendedKind.Resumable }); }, true); } diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs index 31b6745..5883562 100644 --- a/tests/BizTalkCheckmkPulse.Tests/Program.cs +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -23,6 +23,11 @@ namespace BizTalkCheckmkPulse.Tests Run("ProviderSqlLoginFailureIsPermission", ProviderSqlLoginFailureIsPermission); Run("RejectedSqlPrincipalIsExtracted", RejectedSqlPrincipalIsExtracted); Run("PlatformPermissionPropagatesToSqlDiscovery", PlatformPermissionPropagatesToSqlDiscovery); + Run("RoutingFailureReportsAreCounted", RoutingFailureReportsAreCounted); + Run("ReceiveLocationAllowlistSeparatesExpectedState", ReceiveLocationAllowlistSeparatesExpectedState); + Run("UnknownReceiveLocationStateIsNotGreen", UnknownReceiveLocationStateIsNotGreen); + Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState); + Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded); Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines); Run("SnapshotRejectsTampering", SnapshotRejectsTampering); Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData); @@ -47,16 +52,19 @@ namespace BizTalkCheckmkPulse.Tests private static void SelfTestEmitsAllStableServices() { var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray(); - AssertEqual(6, lines.Length, "self-test line count"); - AssertEqual(6, lines.Distinct(StringComparer.Ordinal).Count(), "unique self-test lines"); + AssertEqual(8, lines.Length, "self-test line count"); + AssertEqual(8, lines.Distinct(StringComparer.Ordinal).Count(), "unique self-test lines"); Assert(lines.All(x => x.StartsWith("0 \"BizTalk ", StringComparison.Ordinal)), "every self-test line must be OK"); Assert(lines.Any(x => x.Contains("\"BizTalk Event Log\"")), "Event Log service missing"); + Assert(lines.Any(x => x.Contains("\"BizTalk Receive Locations\"")), "Receive Locations service missing"); + Assert(lines.Any(x => x.Contains("\"BizTalk Send Ports\"")), "Send Ports service missing"); + Assert(lines.Any(x => x.Contains("\"BizTalk Orchestrations\"")), "Orchestrations service missing"); } private static void UnavailableSourcesAreUnknown() { - var lines = new CheckmkLocalFormatter(new MonitoringOptions()).Format(new ProbeResult()).Take(6).ToArray(); - AssertEqual(6, lines.Length, "stable service count"); + var lines = new CheckmkLocalFormatter(new MonitoringOptions()).Format(new ProbeResult()).Take(8).ToArray(); + AssertEqual(8, lines.Length, "stable service count"); Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "unavailable sources must be UNKNOWN"); } @@ -65,7 +73,7 @@ namespace BizTalkCheckmkPulse.Tests var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true }; var result = CreateSuspensionResult("(unknown)"); var lines = new CheckmkLocalFormatter(options).Format(result).ToArray(); - AssertEqual(6, lines.Length, "unknown application must not create a dynamic service"); + AssertEqual(8, lines.Length, "unknown application must not create a dynamic service"); } private static void KnownApplicationCreatesService() @@ -73,7 +81,7 @@ namespace BizTalkCheckmkPulse.Tests var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true }; var result = CreateSuspensionResult("Orders"); var lines = new CheckmkLocalFormatter(options).Format(result).ToArray(); - AssertEqual(7, lines.Length, "known application should create one dynamic service"); + AssertEqual(9, lines.Length, "known application should create one dynamic service"); Assert(lines.Any(x => x.Contains("\"BizTalk Suspended Orders\"")), "known application service missing"); } @@ -93,6 +101,7 @@ namespace BizTalkCheckmkPulse.Tests Assert(query.Contains("SubscriptionDBServerName"), "master MessageBox server property missing"); Assert(query.Contains("SubscriptionDBName"), "master MessageBox database property missing"); Assert(query.IndexOf("MessageBoxSetting", StringComparison.OrdinalIgnoreCase) < 0, "unsupported MessageBoxSetting class present"); + Assert(WmiBizTalkProbe.SuspendedInstancesQuery.Contains("ServiceClass = 64"), "routing failure report filter missing"); } private static void ProviderSqlLoginFailureIsPermission() @@ -137,6 +146,167 @@ namespace BizTalkCheckmkPulse.Tests "SQL discovery classification"); } + private static void RoutingFailureReportsAreCounted() + { + var result = new ProbeResult(); + result.Platform.SuspendedInstancesDataAvailable = true; + result.SuspendedInstances.Add(new SuspendedInstance + { + ApplicationName = "(unknown)", + ServiceName = string.Empty, + ServiceClassId = 64, + Kind = SuspendedKind.NonResumable + }); + result.SuspendedInstances.Add(new SuspendedInstance + { + ApplicationName = "Orders", + ServiceName = "Receive.Order", + Kind = SuspendedKind.Resumable + }); + + var line = FindServiceLine(result, "Suspended Instances"); + Assert(line.StartsWith("2 \"BizTalk Suspended Instances\"", StringComparison.Ordinal), "routing failure must be CRIT"); + Assert(line.Contains("biztalk_suspended_total=2"), "suspended total metric missing"); + Assert(line.Contains("biztalk_suspended_nonresumable=1"), "non-resumable metric missing"); + Assert(line.Contains("biztalk_routing_failure_reports=1"), "routing failure metric missing"); + Assert(line.Contains("routing_failure_reports=1"), "routing failure summary missing"); + } + + private static void ReceiveLocationAllowlistSeparatesExpectedState() + { + var options = new MonitoringOptions + { + ExpectedDisabledReceiveLocations = new[] { "Maintenance\\RL Expected" } + }; + var result = new ProbeResult(); + result.Platform.ReceiveLocationsDataAvailable = true; + result.ReceiveLocations.Add(new ReceiveLocationState + { + ApplicationName = "Maintenance", + Name = "RL Expected", + IsDisabled = true + }); + result.ReceiveLocations.Add(new ReceiveLocationState + { + ApplicationName = "Orders", + Name = "RL Orders", + IsDisabled = true + }); + result.ReceiveLocations.Add(new ReceiveLocationState + { + ApplicationName = "Orders", + Name = "RL Active", + IsDisabled = false + }); + + var line = FindServiceLine(result, "Receive Locations", options); + Assert(line.StartsWith("2 \"BizTalk Receive Locations\"", StringComparison.Ordinal), "unexpected disabled receive location must be CRIT"); + Assert(line.Contains("biztalk_receive_locations_enabled=1"), "enabled receive metric missing"); + Assert(line.Contains("biztalk_receive_locations_unexpected_disabled=1"), "unexpected disabled metric missing"); + Assert(line.Contains("biztalk_receive_locations_expected_disabled=1"), "expected disabled metric missing"); + Assert(line.Contains(@"Orders\RL Orders"), "affected receive location missing"); + Assert(line.IndexOf("RL Expected", StringComparison.Ordinal) < 0, "expected disabled receive location should not clutter affected list"); + + options.ExpectedDisabledReceiveLocations = new[] { "Maintenance\\RL Expected", "RL Orders" }; + var allExpected = FindServiceLine(result, "Receive Locations", options); + Assert(allExpected.StartsWith("0 \"BizTalk Receive Locations\"", StringComparison.Ordinal), "all expected disabled receive locations should be OK"); + Assert(allExpected.Contains("biztalk_receive_locations_expected_disabled=2"), "all expected receive count missing"); + } + + private static void SendPortAllowlistSeparatesExpectedState() + { + var options = new MonitoringOptions + { + ExpectedInactiveSendPorts = new[] { "Maintenance\\SP Expected" } + }; + var result = new ProbeResult(); + result.Platform.SendPortsDataAvailable = true; + result.SendPorts.Add(new SendPortState + { + ApplicationName = "Maintenance", + Name = "SP Expected", + Status = 2 + }); + result.SendPorts.Add(new SendPortState + { + ApplicationName = "Orders", + Name = "SP Orders", + Status = 1 + }); + result.SendPorts.Add(new SendPortState + { + ApplicationName = "Orders", + Name = "SP Active", + Status = 3 + }); + + var line = FindServiceLine(result, "Send Ports", options); + Assert(line.StartsWith("2 \"BizTalk Send Ports\"", StringComparison.Ordinal), "unexpected inactive send port must be CRIT"); + Assert(line.Contains("biztalk_send_ports_started=1"), "started send port metric missing"); + Assert(line.Contains("biztalk_send_ports_unexpected_inactive=1"), "unexpected inactive send metric missing"); + Assert(line.Contains("biztalk_send_ports_expected_inactive=1"), "expected inactive send metric missing"); + Assert(line.Contains(@"Orders\SP Orders(bound)"), "affected send port missing"); + Assert(line.IndexOf("SP Expected", StringComparison.Ordinal) < 0, "expected inactive send port should not clutter affected list"); + + options.ExpectedInactiveSendPorts = new[] { "Maintenance\\SP Expected", "SP Orders" }; + var allExpected = FindServiceLine(result, "Send Ports", options); + Assert(allExpected.StartsWith("0 \"BizTalk Send Ports\"", StringComparison.Ordinal), "all expected inactive send ports should be OK"); + Assert(allExpected.Contains("biztalk_send_ports_expected_inactive=2"), "all expected send count missing"); + } + + private static void UnknownReceiveLocationStateIsNotGreen() + { + var result = new ProbeResult(); + result.Platform.ReceiveLocationsDataAvailable = true; + result.ReceiveLocations.Add(new ReceiveLocationState + { + ApplicationName = "Orders", + Name = "RL Missing State", + IsDisabled = null + }); + + var line = FindServiceLine(result, "Receive Locations"); + Assert(line.StartsWith("3 \"BizTalk Receive Locations\"", StringComparison.Ordinal), "unknown receive state must be UNKNOWN"); + Assert(line.Contains("biztalk_receive_locations_unknown=1"), "unknown receive metric missing"); + Assert(line.Contains(@"Orders\RL Missing State"), "unknown receive location must be visible"); + } + + private static void ArtifactSummaryIsBounded() + { + var options = new MonitoringOptions + { + MaxSummaryItems = 2, + MaxDetailCharacters = 256 + }; + var result = new ProbeResult(); + result.Platform.ReceiveLocationsDataAvailable = true; + for (var i = 1; i <= 6; i++) + { + result.ReceiveLocations.Add(new ReceiveLocationState + { + ApplicationName = "Application", + Name = "Disabled-" + i, + IsDisabled = true + }); + } + + var line = FindServiceLine(result, "Receive Locations", options); + Assert(line.Contains("(+4 more)"), "omitted item count missing"); + Assert(line.Length < 700, "local check line should remain compact"); + + var longResult = new ProbeResult(); + longResult.Platform.ReceiveLocationsDataAvailable = true; + longResult.ReceiveLocations.Add(new ReceiveLocationState + { + ApplicationName = new string('A', 400), + Name = new string('R', 400), + IsDisabled = true + }); + var truncated = FindServiceLine(longResult, "Receive Locations", options); + Assert(truncated.Contains("[truncated]"), "detail character limit should be visible"); + Assert(truncated.Length < 700, "truncated local check line should remain bounded"); + } + private static void SnapshotRoundTripPreservesLines() { WithTemporarySnapshot((path, store) => @@ -222,7 +392,7 @@ namespace BizTalkCheckmkPulse.Tests var lines = new CheckmkLocalFormatter(new MonitoringOptions()) .FormatSnapshotFailure("Snapshot file does not exist.") .ToArray(); - AssertEqual(6, lines.Length, "snapshot failure stable service count"); + AssertEqual(8, lines.Length, "snapshot failure stable service count"); Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "snapshot failure must be UNKNOWN"); Assert(lines.All(x => x.Contains("Scheduled Task")), "snapshot failure must contain provider action"); } @@ -258,6 +428,18 @@ namespace BizTalkCheckmkPulse.Tests return result; } + private static string FindServiceLine( + ProbeResult result, + string serviceSuffix, + MonitoringOptions options = null) + { + var effectiveOptions = options ?? new MonitoringOptions(); + var token = "\"" + effectiveOptions.ServiceName(serviceSuffix) + "\""; + return new CheckmkLocalFormatter(effectiveOptions) + .Format(result) + .Single(x => x.Contains(token)); + } + private static void Run(string name, Action test) { ExecutedTests++;