Compare commits

..
5 Commits
Author SHA1 Message Date
admin 826d87fef7 Expand BizTalk operational metrics 2026-07-30 16:15:20 +02:00
admin d15bb6539b Separate BizTalk collection from Checkmk agent 2026-07-30 15:50:42 +02:00
admin e3d7f6a780 Clarify BizTalk SQL authorization setup 2026-07-29 15:40:44 +02:00
admin 0001d7b861 Diagnose BizTalk WMI SQL permissions 2026-07-29 13:21:22 +02:00
admin 8d2934e343 Harden BizTalk WMI monitoring 2026-07-29 09:01:39 +02:00
26 changed files with 3398 additions and 911 deletions
+8 -3
View File
@@ -1,9 +1,11 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
# Visual Studio Version 16
VisualStudioVersion = 16.0.31624.102
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse", "src\BizTalkCheckmkPulse\BizTalkCheckmkPulse.csproj", "{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse.Tests", "tests\BizTalkCheckmkPulse.Tests\BizTalkCheckmkPulse.Tests.csproj", "{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -14,9 +16,12 @@ Global
{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Release|Any CPU.Build.0 = Release|Any CPU
{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+550 -323
View File
@@ -1,110 +1,180 @@
# Dokumentation: BizTalk Checkmk Pulse
# Technische Dokumentation: BizTalk Checkmk Pulse
## Zielbild
## 1. Ziel und Randbedingungen
Ziel ist ein wartbares Monitoring fuer BizTalk Server 2020 in den Umgebungen `ACC`, `DEV`, `TST` und `PRD`. Jede Umgebung besitzt einen BizTalk Server 2020 und einen SQL Server. SQL Server wird mit dem Checkmk-eigenen MSSQL-Plugin ueberwacht; fuer BizTalk liefert dieses Projekt die fehlende fachliche und technische Laufzeitsicht.
Die Anwendung ueberwacht BizTalk Server 2020 auf Windows Server 2019 mit
Checkmk 2.4. Pro Umgebung (`ACC`, `DEV`, `TST`, `PRD`) wird lokal auf dem
BizTalk-Server gesammelt. Das Checkmk-MSSQL-Plugin bleibt fuer die allgemeine
SQL-Ueberwachung zustaendig; BizTalk Checkmk Pulse liefert die
BizTalk-spezifische Laufzeit- und Berechtigungssicht.
Das Monitoring soll:
Verbindliche Randbedingungen:
- auf jeder BizTalk-Maschine lokal laufen
- ohne PowerShell-Abhaengigkeit funktionieren
- keine BizTalk-DLLs im Build erzwingen
- Checkmk-2.4-kompatible Services und Metriken erzeugen
- service-discovery-freundlich und dashboard-tauglich sein
- bei Fehlern gueltige `UNKNOWN`-Services statt kaputter Agent-Ausgaben liefern
- Der Checkmk Windows Agent bleibt `LocalSystem`.
- `LocalSystem` erhaelt keine BizTalk-/SQL-Gruppenmitgliedschaft.
- 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.
- Laufzeitcode benoetigt keine PowerShell und keine BizTalk-DLL-Referenz.
- Installation, ACL und Scheduled Task muessen reproduzierbar sein.
## Technische Bewertung
## 2. Ausloeser der Architekturanderung
### Option A: Checkmk Local Check mit C#/.NET Framework
Bewertung: empfohlen und umgesetzt.
Vorteile:
- Checkmk 2.4 unterstuetzt Local Checks direkt.
- Windows Server mit BizTalk 2020 bringt .NET Framework in der Regel passend mit.
- `System.Management` kann BizTalk-WMI lesen.
- Keine PowerShell Execution Policy, keine Script-Signing-Frage.
- Kein serverseitiges Checkmk-Python-Plugin notwendig.
- Rollout ist eine einfache Dateiablage unter `%ProgramData%\checkmk\agent\local`.
Nachteile:
- Schwellwerte sind in der `.exe.config`, nicht als Checkmk-Regelsatz in WATO.
- Eigene Graphing-Definitionen sind nicht enthalten; Checkmk zeigt Local-Check-Metriken trotzdem als Performance-Daten und Graphen.
### Option B: Agent Plugin plus serverseitiges Checkmk-Plugin
Bewertung: technisch elegant, aber fuer den ersten produktiven Schritt schwerer.
Vorteile:
- Checkmk-Regeln, Discovery und Metrikdefinitionen koennen sauber zentral modelliert werden.
- Bessere langfristige Erweiterbarkeit als MKP.
Nachteile:
- Checkmk-Check-API-Versionen muessen enger gepflegt werden.
- Server-seitige Installation in jeder Site erforderlich.
- Mehr Aufwand fuer Managed-Services-Betrieb und Updates.
Empfehlung: als Version 2 dieses Projekts denkbar, wenn die Local-Check-Variante stabil in PRD laeuft und zentrale Regelsaetze wirklich benoetigt werden.
### Option C: PowerShell Local Check
Bewertung: nicht empfohlen fuer diese Umgebung.
Vorteile:
- Schnell zu schreiben.
- WMI/CIM-Zugriff ist komfortabel.
Nachteile:
- PowerShell ist in vielen Serverumgebungen eingeschraenkt oder signaturpflichtig.
- Ausfuehrungsverhalten im Checkmk-Agent-Kontext ist haeufiger fehleranfaellig.
### Option D: BizTalk ExplorerOM/OperationsOM
Bewertung: fachlich stark, deployseitig unnoetig schwer.
Vorteile:
- Hoehere BizTalk-Abstraktion als rohe WMI-Klassen.
Nachteile:
- BizTalk-DLL-Versionen muessen beim Build und teilweise zur Laufzeit passen.
- Build-Agenten brauchen BizTalk-Komponenten oder SDK-Dateien.
- Fuer die benoetigten Zustandsdaten reicht WMI aus.
## Architektur
Der ACC-Agent-Dump vom 29.07.2026 bewies:
```text
Checkmk Windows Agent
|
| startet lokale Checks aus %ProgramData%\checkmk\agent\local
v
biztalk_checkmk_pulse.cmd
|
| startet
v
BizTalkCheckmkPulse.exe
|
| liest lokal und prueft im gleichen Sicherheitskontext
+-- WMI root\MicrosoftBizTalkServer
+-- Windows Application Event Log
+-- SQL-Verbindung zu BizTalkMgmtDb/BizTalkMsgBoxDb
|
v
Checkmk Local Check Zeilen nach STDOUT
Lokaler Prozess: NT AUTHORITY\SYSTEM
Netzwerkprincipal: BEW\AV23AGPWBIO1$
Providerfehler: COMException 0x80131904
Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$'
```
Der `.cmd`-Wrapper liefert auch dann eine gueltige `UNKNOWN`-Zeile mit Massnahme, wenn die EXE fehlt oder bereits der Prozessstart mit einem Exitcode fehlschlaegt. Die EXE selbst faengt Laufzeitfehler ab und schreibt fuer alle stabilen Services `UNKNOWN`, damit unvollstaendige Deployments oder WMI-Probleme in Checkmk sichtbar bleiben.
Lokales `root\MicrosoftBizTalkServer` war erreichbar. Die
datenbankgestuetzten BizTalk-WMI-Klassen scheiterten erst beim Zugriff des
Providers auf den getrennten SQL Server. DCOM-, Firewall- oder pauschale
WMI-ACL-Aenderungen beheben diesen Befund nicht.
## Datenquellen
Eine Berechtigung des Maschinenkontos haette die Netzwerkrolle allen
`LocalSystem`-Diensten des BizTalk-Servers vermittelt. Die jetzt implementierte
Producer/Consumer-Trennung beschraenkt sie stattdessen auf ein dediziertes
Konto.
### BizTalk WMI Namespace
## 3. Architektur
```text
Privilegierte Zone
┌──────────────────────────────────────────────────────────────┐
│ Task Scheduler: "BizTalk Checkmk Pulse Provider" │
│ Konto: DOMAIN\svc_biztalk_cmk (RunLevel Limited) │
│ Intervall: 1 Minute, IgnoreNew, Laufzeitlimit 5 Minuten │
│ │
│ BizTalkCheckmkPulse.exe --collect │
│ ├─ WMI root\MicrosoftBizTalkServer │
│ ├─ integrierter SQL-Test an MgmtDb/MsgBoxDb │
│ ├─ lokales Windows Application Event Log │
│ ├─ Checkmk-Formatierung │
│ └─ atomare Snapshot-Publikation + Provider-Log │
└──────────────────────────────┬───────────────────────────────┘
│ Datei/ACL
v
%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot
v
┌──────────────────────────────────────────────────────────────┐
│ Unprivilegierter Transportpfad │
│ Checkmk-Agent (LocalSystem) │
│ └─ local\biztalk_checkmk_pulse.cmd │
│ └─ BizTalkCheckmkPulse.exe --consume │
│ ├─ Format/Alter/Maschine/SHA-256 validieren │
│ └─ Payload unveraendert nach STDOUT │
└──────────────────────────────────────────────────────────────┘
```
Eine gemeinsame EXE implementiert beide Modi. Das reduziert
Versionsabweichungen. Die zentrale Config unter `%ProgramFiles%` gilt fuer
Provider und Consumer.
### 3.1 Provider-Modus
Aufruf:
```cmd
"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --collect
```
Ablauf:
1. Config laden und absolute Runtimepfade validieren.
2. Tageslogs gemaess `LogRetentionDays` bereinigen.
3. exklusives Handle auf `<SnapshotPath>.provider.lock` halten.
4. WMI-, SQL- und Event-Log-Probes ausfuehren.
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.
8. Laufzeit, Zeilen- und Diagnosezahl loggen.
Exitcodes:
| Code | Bedeutung |
| --- | --- |
| `0` | Sammlung und Snapshot-Publikation erfolgreich. |
| `1` | unerwarteter Providerfehler; aktueller `UNKNOWN`-Snapshot wird versucht. |
| `2` | Parallelstart oder Snapshot-I/O-Fehler. |
Fachliche WMI-/SQL-Fehler werden strukturiert in den Snapshot geschrieben und
muessen nicht zwingend einen Prozessfehler verursachen. So bleibt die genaue
Fehlerkategorie in Checkmk sichtbar.
### 3.2 Consumer-Modus
Aufruf:
```cmd
"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
```
Ohne Modusargument ist `--consume` der sichere Default. Der Checkmk-Wrapper
setzt es trotzdem explizit.
Der Consumer:
- oeffnet nur den Snapshot,
- begrenzt die Dateigroesse vor dem Einlesen,
- validiert striktes UTF-8,
- validiert Formatversion und Header,
- fordert die lokale Maschinenkennung,
- berechnet SHA-256 ueber den exakten Payload,
- prueft Zeilenanzahl und Checkmk-Zeilenform,
- verwirft Zeitstempel mehr als fuenf Minuten in der Zukunft,
- verwirft Snapshots nach `SnapshotMaxAgeSeconds`,
- 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 acht
`UNKNOWN`-Zeilen und einen Eintrag im Consumer-Log.
## 4. Snapshot-Vertrag
Version 2:
```text
BIZTALK_CHECKMK_PULSE_SNAPSHOT_V2
generatedUtc=2026-07-30T12:34:56.1234567Z
machineBase64=QVYyM0FHUFdCSU8x
identityBase64=QkVXXHN2Y19iaXp0YWxrX2NtayQ=
payloadLines=8
payloadSha256=<64 hex characters>
0 "BizTalk Platform" - ...
...
```
Maschine und Identitaet sind Base64-kodiert, um Zeilenumbrueche und
Trennzeichen im Header auszuschliessen. Base64 ist hier keine Verschluesselung.
Die SHA-256-Pruefung schuetzt vor unbemerkter Teil-/Fehlschreibung, nicht vor
einem lokalen Administrator.
### 4.1 Atomare Publikation
Die Temporaerdatei liegt zwingend im Snapshotverzeichnis. Dadurch findet der
Ersatz auf demselben Volume statt. Existiert bereits ein Snapshot, wird
`File.Replace` verwendet; beim ersten Lauf `File.Move`. Bis zum erfolgreichen
Ersatz bleibt der letzte vollstaendige Snapshot erhalten. Seine Alterspruefung
verhindert unbegrenztes Weiterreichen alter Daten.
### 4.2 Keine direkte Checkmk-Spooldatei
Checkmk kann zwar Dateien aus seinem Spoolverzeichnis direkt anhaengen, prueft
deren Inhalt aber laut eigener Dokumentation nicht. Ein fehlerhafter Spool kann
nachfolgende Agentsektionen beeinflussen. Deshalb liegt der Provider-Snapshot
in einem eigenen Verzeichnis und wird erst durch den validierenden Consumer in
die Local-Check-Ausgabe uebernommen.
## 5. Datenquellen
### 5.1 BizTalk WMI
Namespace:
@@ -112,259 +182,416 @@ Namespace:
root\MicrosoftBizTalkServer
```
Genutzte Klassen:
Pflichtklassen:
| Klasse | Zweck |
| --- | --- |
| `MSBTS_GroupSetting` | BizTalk-Gruppe und Management-DB-Hinweise. |
| `MSBTS_MessageBoxSetting` | MessageBox-DB-Hinweise. |
| `MSBTS_HostInstance` | Host-Instance-Zustand. |
| `MSBTS_ServiceInstance` | Suspended service instances. |
| `MSBTS_ReceiveLocation` | Receive-Location-Zustand. |
| `MSBTS_SendPort` | Send-Port-Zustand. |
| `MSBTS_Orchestration` | Orchestration-Zustand. |
| `MSBTS_ReceivePort` | Application-Mapping fuer Details. |
| `MSBTS_GroupSetting` | Gruppe, Read-Only-/Operator-Gruppe, Management- und Master-MessageBox-Ziel. |
| `MSBTS_HostInstance` | lokale Host-Instance-Zustaende. |
| `MSBTS_ServiceInstance` | suspendierte Instanzen. |
| `MSBTS_ReceiveLocation` | Receive-Location-Zustaende. |
| `MSBTS_SendPort` | Send-Port-Zustaende. |
| `MSBTS_Orchestration` | Orchestration-Zustaende. |
### Windows Application Event Log
`MSBTS_GroupSetting` wird mit den dokumentierten Properties
`MgmtDbServerName`, `MgmtDbName`, `SubscriptionDBServerName`,
`SubscriptionDBName`, `BizTalkReadOnlyUserGroup` und
`BizTalkOperatorGroup` gelesen. Eine nicht dokumentierte
`MSBTS_MessageBoxSetting`-Klasse wird nicht abgefragt.
Der Check liest standardmaessig das lokale Application Log fuer die letzten 60 Minuten und filtert auf Quellen wie:
Hostnamen werden clientseitig verglichen. Sonderzeichen und FQDN-/Kurzname
gelangen nicht in dynamisch erzeugte WQL-Filter.
- `BizTalk Server`
- `XLANG/s`
- `ENTSSO`
- `BizTalk Server Application`
- `BizTalk Server EDI`
`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.
Die Liste ist ueber `EventLogSources` konfigurierbar.
### 5.2 Kompaktes Checkmk-Servicebild
### SQL-Zugriffsprobe
Nach erfolgreicher oder teilweise erfolgreicher Plattformabfrage uebernimmt der SQL-Probe die per WMI ermittelten Management- und MessageBox-Ziele. Fuer jedes eindeutige Ziel wird mit `System.Data.SqlClient` eine Verbindung mit integrierter Windows-Authentifizierung geoeffnet und `SELECT 1` ausgefuehrt. Die Verbindung wird unmittelbar danach geschlossen; es werden keine BizTalk-Tabellen gelesen oder veraendert.
Der Test laeuft unter derselben Identitaet wie der Checkmk Local Check. Damit wird sichtbar, ob `LocalSystem` beziehungsweise das Maschinenkonto des BizTalk-Servers das SQL-Ziel tatsaechlich erreichen und die Datenbank oeffnen kann. Die Probe ist mit `ProbeSqlConnectivity=false` deaktivierbar und verwendet `SqlConnectionTimeoutSeconds` mit dem Default 5 Sekunden je Ziel als Timeout.
Im produktiven Betrieb sollte der komplette Local Check asynchron mit 300 Sekunden Cache ausgefuehrt werden. So fuehren fehlende SQL-Rechte nicht bei jedem Checkmk-Abruf zu weiteren fehlgeschlagenen Login-Ereignissen. Der Trade-off ist eine Zustandsverzoegerung von maximal fuenf Minuten.
## Berechtigungsmodell
### LocalSystem und lokaler WMI-Zugriff
Der Checkmk Windows Agent und der Agent Controller laufen standardmaessig als `LocalSystem` (`NT AUTHORITY\SYSTEM`). Der Wrapper und die EXE erben diesen Kontext. Das Plugin verbindet sich lokal mit `\\<eigener-server>\root\MicrosoftBizTalkServer`, setzt keine eigenen Anmeldedaten, nutzt kein Remote-WMI und fuehrt keine veraendernden WMI-Methoden aus. Das lokale Windows Application Event Log wird ebenfalls nur gelesen.
Die lokalen Rechte von `LocalSystem` reichen fuer diese Zugriffe normalerweise aus. Im regulaeren lokalen Betrieb werden deshalb keine zusaetzlichen DCOM-, Firewall- oder pauschalen WMI-Namespace-Freigaben benoetigt.
### Netzwerkidentitaet zum SQL Server
BizTalk-WMI-Klassen koennen ihre Daten aus der BizTalk Management- oder MessageBox-Datenbank beziehen. Liegt SQL Server auf einer anderen Maschine, authentifiziert sich `LocalSystem` dort mit dem Active-Directory-Computerkonto des BizTalk-Servers:
```text
DOMAIN\BIZTALKSERVER$
```
Dieses Konto besitzt nicht automatisch BizTalk- oder SQL-Berechtigungen. Daraus kann die Situation entstehen, dass die Verbindung zum lokalen WMI-Namespace erfolgreich ist, einzelne SQL-gestuetzte WMI-Klassen aber `Access denied`, `UnauthorizedAccessException` oder `UNKNOWN` liefern.
### Pruefung und Freigabe
Der direkte Programmstart in einer administrativen Shell laeuft unter dem angemeldeten Benutzer und ist deshalb kein ausreichender Berechtigungstest. Verbindlich ist die Ausfuehrung durch den Checkmk Agent Controller als `LocalSystem`:
```powershell
& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump |
Select-String -Pattern "BizTalk|Access denied|Unauthorized|UNKNOWN" -Context 0,1
```
Bei Zugriffsfehlern gilt folgendes Least-Privilege-Vorgehen:
1. Computerkonto des BizTalk-Servers ermitteln, normalerweise `DOMAIN\BIZTALKSERVER$`.
2. Die bei der BizTalk-Konfiguration verwendete Operator-Gruppe feststellen.
3. Das Computerkonto in `BizTalk Server Operators` beziehungsweise die kundenspezifisch benannte Operator-Gruppe aufnehmen.
4. Kerberos-Tickets des Systemkontos erneuern oder den Server im Wartungsfenster neu starten.
5. Den Agent-Dump wiederholen.
6. Nur fuer weiterhin abgelehnte, konkret identifizierte WMI-Klassen mit BizTalk- und SQL-Administration pruefen, ob Administratorrechte erforderlich sind.
Die Operator-Rolle ist fuer grundlegendes Monitoring und Zustandsabfragen vorgesehen. Direkte manuelle Aenderungen an den Rollen der BizTalk-SQL-Datenbanken sind zu vermeiden; die durch BizTalk konfigurierte Windows-Gruppe ist die vorgesehene Berechtigungsgrenze.
| Agent-Dump-Ergebnis | Massnahme |
| --- | --- |
| Plausible Werte fuer alle BizTalk-Services | Keine Berechtigungsaenderung. |
| WMI-Namespace nicht erreichbar | BizTalk-WMI-Provider, WMI-Dienst, Namespace und ACL gezielt pruefen. |
| Nur SQL-gestuetzte Klassen scheitern | Computerkonto in die BizTalk-Operator-Gruppe aufnehmen. |
| Nur Event-Log-Service ist `UNKNOWN` | Lokalen Application-Log-Zugriff pruefen. |
| Fehler bleibt mit Operator-Rolle bestehen | Betroffene Klasse und konkrete BizTalk-/SQL-Rollenanforderung untersuchen. |
### Automatische Diagnose im Programm
WMI-Verbindungsaufbau und jede erforderliche WMI-Klasse werden separat bewertet. Erwartbare Exceptions werden in folgende Kategorien eingeordnet:
| Diagnose | Bedeutung | Ausgegebene Massnahme |
| --- | --- | --- |
| `Wmi/Permission` | Namespace oder BizTalk-WMI-Klasse verweigert den Zugriff. | Namespace-ACL bei Verbindungsfehlern beziehungsweise BizTalk-Operator-Gruppe bei Klassenfehlern pruefen. |
| `Wmi/Connectivity` | WMI-/RPC-Ziel nicht erreichbar. | WMI-Dienst, Provider und bei Remote-WMI zusaetzlich DNS/RPC/Firewall pruefen. |
| `Wmi/Timeout` | WMI-Abfrage ueberschreitet `QueryTimeoutSeconds`. | WMI-, BizTalk- und SQL-Auslastung untersuchen, bevor der Timeout erhoeht wird. |
| `Wmi/Configuration` | Namespace oder Klasse fehlt. | BizTalk-WMI-Provider und BizTalk-Installation pruefen beziehungsweise reparieren. |
| `Sql/Permission` | Login oder Datenbankzugriff wird abgelehnt. | Maschinenkonto der BizTalk-Operator-Gruppe zuordnen, Kerberos erneuern, keine direkten DB-Rollen vergeben. |
| `Sql/Connectivity` | SQL-Server oder Instanz nicht erreichbar. | Servername, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall pruefen. |
| `Sql/Timeout` | SQL-Verbindung oder Testabfrage laeuft in den Timeout. | Netzwerk und SQL-Auslastung pruefen; Timeout nur begruendet anheben. |
| `Sql/Configuration` | WMI-Ziele unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. | Plattformabfragen beziehungsweise Zertifikatskette, Verschluesselung, SPN und Kerberos gezielt pruefen. |
Jede Diagnose enthaelt Bereich/Kategorie, betroffene Komponente, eine kurze Ursache, `Massnahme:` und `Technik:` mit Exception-Typ, HRESULT oder SQL-Fehlernummer. Erforderliche Datenquellen besitzen eigene Verfuegbarkeitsflags. Schlaegt beispielsweise `MSBTS_ServiceInstance` fehl, wird `BizTalk Suspended Instances` zwingend `UNKNOWN`; eine leere Ergebnisliste darf nicht als 'keine Suspensions' fehlinterpretiert werden.
Die Operator-Mitgliedschaft des Computerkontos steht allen auf diesem Server als `LocalSystem` laufenden Diensten fuer Netzwerkzugriffe zur Verfuegung. Falls diese Sicherheitsauswirkung nicht akzeptabel ist, kann ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten Checkmk-Spooldaten erzeugen. Diese Variante ist noch nicht Bestandteil der aktuellen Implementierung. Der komplette Checkmk-Agent sollte nicht allein fuer dieses Plugin auf eine andere Identitaet umgestellt werden, weil dies alle Agent-Sektionen und Local Checks betrifft.
Quellen:
- https://docs.checkmk.com/latest/en/agent_windows.html
- https://docs.checkmk.com/latest/en/localchecks.html
- https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights
- https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security
- https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer
- https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces
## Resilienz
Das Plugin ist bewusst defensiv gebaut:
- WMI-Queries haben ein konfigurierbares Timeout.
- WMI-Namespace und Pflichtklassen werden getrennt auf Berechtigung, Konfiguration, Erreichbarkeit und Timeout geprueft.
- Management- und MessageBox-Datenbank werden mit der echten Agent-Identitaet getestet.
- SQL-Fehlernummern werden in Berechtigung, Erreichbarkeit, Timeout oder Providerfehler klassifiziert.
- Fehlgeschlagene Pflichtabfragen erzeugen `UNKNOWN` statt irrefuehrender Nullwerte.
- Optionale WMI-Klassen erzeugen Diagnosehinweise statt Totalabbruch.
- Fehlende Properties werden als leer/unknown behandelt.
- Local-Check-Ausgaben verwenden gueltige Checkmk-Zeilen mit genau vier Feldern.
- Fatal Errors erzeugen `UNKNOWN` fuer alle stabilen Services.
- Event-Log-Auswertung ist isoliert; ein Fehler dort bricht WMI-Monitoring nicht ab.
- Service-Namen sind stabil, damit Service Discovery nicht bei jedem Lauf neue Services erzeugt.
## Statusmodell
### BizTalk Platform
- `OK`: BizTalk-WMI erreichbar.
- `UNKNOWN`: WMI nicht erreichbar oder kompletter Programmfehler.
Dieser Service ist der Integrationsindikator. Wenn er `UNKNOWN` ist, sind Berechtigungen, BizTalk-Installation oder WMI-Repository zu pruefen.
### BizTalk SQL Access
- `OK`: Datenbankziele vollstaendig ermittelt und integrierte Anmeldung an allen Zielen erfolgreich.
- `UNKNOWN`: Zielermittlung unvollstaendig, Anmeldung verweigert, SQL nicht erreichbar, Timeout oder Providerfehler.
- Bei `ProbeSqlConnectivity=false`: `OK` mit sichtbarem Hinweis, dass die Probe deaktiviert ist.
Metriken: `biztalk_sql_targets_total`, `biztalk_sql_targets_available` und `biztalk_sql_targets_failed`. Die Ausgabe nennt ausserdem `execution_identity`, `network_identity` und den Zustand jedes getesteten Datenbankziels.
### BizTalk Suspended Instances
- `OK`: keine suspendierten Instanzen.
- `WARN`: mindestens `WarnResumableThreshold` resumable suspended instances.
- `CRIT`: mindestens `CritNonResumableThreshold` non-resumable suspended instances.
- `UNKNOWN`: Datenquelle nicht lesbar.
### BizTalk Host Instances
- `OK`: alle Host-Instanzen sind started.
- `WARN`: mindestens eine Host-Instanz ist pending.
- `CRIT`: mindestens eine Host-Instanz ist stopped oder unknown.
- `UNKNOWN`: keine Host-Instanzen gefunden oder Datenquelle nicht lesbar.
### BizTalk Runtime Artifacts
- `OK`: Artefakte lesbar, keine unbekannten Statuswerte.
- `WARN`: nur wenn `AlertOnArtifactRuntimeIssues=true` und deaktivierte/inaktive Artefakte vorhanden sind.
- `CRIT`: unbekannte Send-Port- oder Orchestration-Statuswerte.
- `UNKNOWN`: mindestens eine erforderliche WMI-Artefaktklasse nicht lesbar.
Deaktivierte Receive Locations und gestoppte Ports koennen in BizTalk fachlich korrekt sein. Deshalb ist die Alarmierung hier standardmaessig informativ.
### BizTalk Event Log
- `OK`: keine relevanten Fehler/Warnungen ueber Schwellwert.
- `WARN`: Fehler oder Warnungen ab `EventLogWarnThreshold`.
- `CRIT`: Fehler ab `EventLogCritThreshold`.
- `UNKNOWN`: Event Log nicht lesbar.
## Dashboard-Empfehlung
Pro Umgebung sollte der BizTalk-Host in einem eigenen Host-Ordner oder Host-Tag fuer `ACC`, `DEV`, `TST`, `PRD` liegen. Im Dashboard eignen sich:
- Service State Widgets fuer die sechs stabilen BizTalk-Services
- Graphen fuer `biztalk_sql_targets_failed`, `biztalk_suspended_total`, `biztalk_host_instances_stopped`, `biztalk_eventlog_errors`
- Hostgruppe/Ordner pro Umgebung
- Optional eine View gefiltert auf `Service starts with BizTalk`
Empfohlene Reihenfolge im Dashboard:
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 Event Log`
6. `BizTalk Runtime Artifacts`
7. SQL-Server-Services aus dem Checkmk-MSSQL-Plugin
5. `BizTalk Receive Locations`
6. `BizTalk Send Ports`
7. `BizTalk Orchestrations`
8. `BizTalk Event Log`
## Rollout-Vorgehen
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`.
1. Build-Paket erzeugen.
2. In `DEV` auf dem BizTalk-Server installieren.
3. `--self-test` und normalen Lauf ausfuehren.
4. Agent-Dump pruefen.
5. Checkmk Discovery durchfuehren.
6. Eine Woche Messwerte und false positives beobachten.
7. Nach `TST` und `ACC` uebernehmen.
8. In `PRD` mit `AlertOnArtifactRuntimeIssues=false` starten.
9. Nach Betriebsfreigabe Schwellwerte feinjustieren.
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.
## Troubleshooting
### 5.3 SQL-Zugriffsprobe
### Service bleibt UNKNOWN
Der Provider oeffnet fuer Management- und Master-MessageBox-Datenbank eine
kurze `System.Data.SqlClient`-Verbindung mit integrierter
Windows-Authentifizierung, fuehrt `SELECT 1` aus und schliesst sie. Keine
BizTalk-Tabelle wird direkt gelesen oder veraendert.
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.4 Event Log
Der Provider liest das lokale Application Log im konfigurierten Zeitfenster
und filtert Quellen wie `BizTalk Server`, `XLANG/s`, `ENTSSO`,
`BizTalk Server Application` und `BizTalk Server EDI`.
## 6. Berechtigungsmodell
### 6.1 Bevorzugte BizTalk-Rolle
BizTalk Server 2020 fuehrte die `BizTalk Server Read Only Users`-Gruppe ein.
Sie darf Artefakte, Servicezustand, Message Flow und Trackinginformationen
lesen, aber keine administrativen Operationen ausfuehren. SQL-seitig wird sie
ueber `BTS_READONLY_USERS` vermittelt.
Soll:
```text
DOMAIN\svc_biztalk_cmk$
-> konfigurierte BizTalk Server Read Only Users-Domaingruppe
-> vorhandener Windows-Gruppenlogin am SQL Server
-> BTS_READONLY_USERS in den von BizTalk konfigurierten Datenbanken
```
Die genaue Gruppe ist aus
`MSBTS_GroupSetting.BizTalkReadOnlyUserGroup` beziehungsweise der
BizTalk-Administration zu lesen. Nicht vom Standardnamen ausgehen.
Die Operator-Gruppe (`BTS_OPERATORS`) wird erst geprueft, wenn eine konkret
benannte erforderliche WMI-Klasse trotz nachgewiesener Read-Only-Konfiguration
und neuem Anmeldetoken abgewiesen wird. Die Erweiterung muss mit Security,
BizTalk- und SQL-Administration begruendet werden.
Unzulaessig:
- Checkmk-Dienstkonto aendern,
- Maschinenkonto pauschal berechtigen,
- individueller SQL-Login fuer das Provider-Konto,
- direkte ad-hoc DB-Rollen,
- `db_owner` oder `sysadmin`,
- WMI-/DCOM-Vollzugriff als Reaktion auf einen SQL-Loginfehler.
### 6.2 Dateisystem-ACL
Der Installer entfernt die Vererbung an den Runtimeverzeichnissen und setzt
explizite Eintraege mit sprachneutralen Well-known-SIDs:
| Ziel | Collector | SYSTEM | lokale Administratoren |
| --- | --- | --- | --- |
| Runtime-Root | RX | RX | F |
| `data` | Modify | RX | F |
| `logs` | Modify | Modify | F |
`LocalSystem` kann deshalb den Snapshot lesen, aber im Datenverzeichnis nicht
schreiben. Schreibrecht im Logverzeichnis wird benoetigt, um abgelehnte
Snapshots aus dem Consumer-Kontext zu protokollieren.
## 7. Scheduled Task
Name:
```text
BizTalk Checkmk Pulse Provider
```
Sollkonfiguration:
| Einstellung | Wert |
| --- | --- |
| Trigger | einmalig, Wiederholung jede Minute ohne Enddatum |
| Programm | `%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe` |
| Argument | `--collect` |
| Arbeitsverzeichnis | `%ProgramFiles%\BizTalkCheckmkPulse` |
| 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 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
### 8.1 Build
```cmd
scripts\test-release.cmd
scripts\package-release.cmd
```
### 8.2 Normales Servicekonto
```powershell
.\Install-BizTalkCheckmkPulse.ps1 `
-CollectorAccount 'BEW\svc_biztalk_cmk' `
-EnvironmentName ACC
```
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:
```text
%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd
```
Der Agent darf den Consumer synchron ausfuehren. Er liest nur maximal 1 MiB
lokale Daten; ein Checkmk-Async-Cache ist daher nicht notwendig. Falls alte
`check_mk.user.yml`-Regeln fuer den bisherigen 300-Sekunden-Cache existieren,
sollten sie entfernt werden, damit Checkmk den minuetlich aktualisierten
Snapshot nicht zusaetzlich verzoegert.
Nach dem Agent-Dump:
1. Service Discovery fuer den BizTalk-Host ausfuehren.
2. acht stabile Services aufnehmen; den alten Service
`BizTalk Runtime Artifacts` nach erfolgreicher Discovery entfernen.
3. Changes aktivieren.
4. Views/Benachrichtigungen nach Umgebung konfigurieren.
## 9. Logging
Pfad:
```text
%ProgramData%\BizTalkCheckmkPulse\logs\
biztalk-checkmk-pulse-YYYYMMDD.log
```
Jede Zeile enthaelt UTC-Zeit, Level, Komponente, PID, Windows-Identitaet und
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`.
## 10. Diagnose und Runbook
### 10.1 Grundpruefung
```powershell
Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider'
Get-Item "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot"
Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 100
& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
```
### 10.2 Kein Snapshot
Pruefen:
```cmd
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd"
1. Task existiert und ist aktiviert.
2. `LastTaskResult` und Provider-Log.
3. Dienstkonto kann sich als Batch anmelden.
4. gespeichertes Task-Kennwort ist nach Rotation/Ablauf noch gueltig.
5. Provider besitzt Modify auf `data` und `logs`.
6. EXE/Config sind ausfuehrbar.
### 10.3 Staler Snapshot
Der Consumer nennt Alter und Grenzwert. Ursachen:
- Task deaktiviert oder Konto/Kennwort ungueltig,
- vorheriger Lauf haengt,
- WMI-/SQL-Timeouts,
- Task Scheduler lief waehrend Wartung nicht,
- Snapshot-Publikation wird durch AV/EDR oder ACL blockiert.
Nicht einfach `SnapshotMaxAgeSeconds` stark erhoehen. Zuerst den minuetlichen
Lauf wiederherstellen. Bei geplanten Wartungen darf Checkmk bewusst `UNKNOWN`
anzeigen.
### 10.4 SHA-256-/Formatfehler
Snapshot nicht reparieren oder direkt an Checkmk kopieren. Task neu starten,
Datentraeger und AV/EDR pruefen. Wiederholt sich der Fehler, Temporaerdateien,
NTFS-Ereignisse und Schreibrechte untersuchen.
### 10.5 BizTalk-/SQL-Permission
1. Log zeigt `execution_identity` des Providers.
2. `BizTalkReadOnlyUserGroup` gegen BizTalk-Konfiguration abgleichen.
3. AD-Gruppenmitgliedschaft des Provider-Kontos pruefen.
4. neuen Logontoken durch neuen Tasklauf sicherstellen.
5. SQL-seitig Gruppenlogin und `BTS_READONLY_USERS` read-only pruefen.
6. keine Einzelrechte erteilen.
7. Nur wenn eine spezifische Klasse weiter abgewiesen wird, Read-Only-Umfang
mit Microsoft-Dokumentation und BizTalk-Betrieb abgleichen; Operator-Rolle
ist eine bewusste Eskalation.
### 10.6 WMI-Schema
`InvalidClass` oder `InvalidQuery` wird als `Wmi/Schema` klassifiziert.
Berechtigungserhoehung kann eine nicht vorhandene Klasse nicht erzeugen.
Installierte BizTalk-Version, WQL und Properties pruefen.
## 11. Konfiguration
Alle AppSettings befinden sich in:
```text
%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config
```
Wenn WMI nicht erreichbar ist:
Runtime:
| Key | Default | Bereich |
| --- | --- | --- |
| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | absolut |
| `SnapshotMaxAgeSeconds` | `180` | 60 bis 86400 |
| `SnapshotMaxBytes` | `1048576` | 4096 bis 16777216 |
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | absolut |
| `LogRetentionDays` | `30` | 1 bis 365 |
BizTalk-Probes:
| Key | Default |
| --- | --- |
| `Server` | `.` |
| `QueryTimeoutSeconds` | `25` |
| `ProbeSqlConnectivity` | `true` |
| `SqlConnectionTimeoutSeconds` | `5` |
| `ProbeEventLog` | `true` |
| `EventLogLookbackMinutes` | `60` |
Alarmierung:
| Key | Default |
| --- | --- |
| `WarnResumableThreshold` | `1` |
| `CritNonResumableThreshold` | `1` |
| `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` |
## 12. Tests und Abnahmekriterien
Automatisiert:
- Release-Build .NET Framework 4.7.2,
- 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 acht `UNKNOWN`-Services.
Windows-/ACC-Abnahme:
1. `LastTaskResult=0`.
2. Snapshot wird mindestens dreimal minuetlich aktualisiert.
3. Provider-Log nennt das dedizierte Konto.
4. `BizTalk Platform` zeigt `read_only_group=`.
5. `BizTalk SQL Access`: `targets=2`, `available=2`.
6. keine Permission-`UNKNOWN`s.
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.
## 13. Deinstallation und Rollback
```powershell
.\Uninstall-BizTalkCheckmkPulse.ps1
```
`-KeepRuntimeData` erhaelt Logs und Snapshot. Das Skript entfernt Task,
Wrapper und Programmdateien, aber keine AD-Gruppenmitgliedschaft. Die
Berechtigung wird separat nach Change-Verfahren entzogen.
Rollback auf die alte direkte `LocalSystem`-Architektur ist nicht empfohlen.
Falls kurzfristig notwendig, muss Security die Maschinenkonto-Berechtigung
explizit freigeben; der Checkmk-Agentdienst selbst darf nicht auf das
Collector-Konto umgestellt werden.
## 14. Transport per certutil
Die Source-Uebergabe besteht aus ZIP plus Base64-TXT:
```cmd
wmic /namespace:\\root\MicrosoftBizTalkServer path MSBTS_HostInstance get HostName,RunningServer,ServiceState
certutil -decode <datei>.zip.b64.txt <datei>.zip
certutil -hashfile <datei>.zip SHA256
tar -xf <datei>.zip
```
Der Checkmk Windows Agent laeuft normalerweise als LocalSystem. Daher immer auch den Agent-Dump verwenden:
ZIP und Base64 werden erst nach dem Git-Commit aus genau diesem Commit erzeugt.
```cmd
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
```
## 15. Referenzen
Die Diagnose im Service-Text nach `Massnahme:` abarbeiten. Wichtige Kategorien:
- `Wmi/Permission`: lokale Namespace-ACL oder BizTalk-Operator-Zuordnung pruefen.
- `Sql/Permission`: Maschinenkonto `DOMAIN\BIZTALKSERVER$` und BizTalk-Operator-Gruppe pruefen.
- `Sql/Connectivity`: SQL-Server-/Instanzname, DNS, SQL-Dienst, TCP und Firewall pruefen.
- `Sql/Timeout`: SQL- und Netzwerkauslastung pruefen; Timeout nicht als erste Massnahme erhoehen.
Der direkte Aufruf des Wrappers kann wegen des angemeldeten Administratorkontos ein anderes Ergebnis liefern als der Agent-Dump. Fuer die Freigabe ist immer der Agent-Dump massgeblich.
### Keine Services in Discovery
Pruefen:
- Liegt `biztalk_checkmk_pulse.cmd` direkt unter `%ProgramData%\checkmk\agent\local`?
- Gibt der Wrapper direkt eine Zeile im Format `0 "Service" metric=value Details` aus?
- Wurde der Checkmk-Agent nach Policy-/Bakery-Aenderungen neu ausgerollt?
### Runtime Artifacts zeigt deaktivierte Artefakte
Das ist standardmaessig `OK`, damit gewollt deaktivierte BizTalk-Artefakte nicht alarmieren. Fuer strengere PRD-Standards:
```xml
<add key="AlertOnArtifactRuntimeIssues" value="true" />
```
## Weiterentwicklung
Sinnvolle naechste Ausbaustufen:
- MKP mit Agent-Bakery-Regel fuer zentrale Konfiguration.
- Optionales serverseitiges Check-Plugin nach Checkmk Check API V2.
- Custom Dashboard/View als Checkmk GUI Extension.
- Ergaenzung um MessageBox-Spool/Tracking-Daten, falls operativ benoetigt.
- https://learn.microsoft.com/en-us/biztalk/core/managing-biztalk-server-security
- 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
+317 -200
View File
@@ -1,276 +1,393 @@
# BizTalk Checkmk Pulse
`BizTalk Checkmk Pulse` ist ein lokaler Checkmk-Check fuer Microsoft BizTalk Server 2020 auf Windows. Er liest BizTalk-Betriebsdaten ueber WMI aus `root\MicrosoftBizTalkServer`, erzeugt Checkmk-Local-Check-Ausgaben und kann dadurch ohne serverseitigen Python-Check in Checkmk 2.4 integriert werden.
`BizTalk Checkmk Pulse` ist ein robuster Checkmk-2.4-Local-Check fuer Microsoft
BizTalk Server 2020 auf Windows Server 2019. Die Anwendung trennt den
berechtigten BizTalk-Datenzugriff vollstaendig vom Checkmk-Agenten:
Der Ansatz ist fuer Umgebungen wie `ACC`, `DEV`, `TST` und `PRD` gedacht, in denen pro Umgebung ein BizTalk Server 2020 und ein SQL Server betrieben werden. SQL Server bleibt beim mitgelieferten Checkmk-MSSQL-Plugin; dieses Projekt ergaenzt die BizTalk-spezifische Sicht.
```text
Scheduled Task (normales dediziertes Servicekonto)
|
| jede Minute: BizTalkCheckmkPulse.exe --collect
v
lokales BizTalk-WMI + BizTalk-SQL + Application Event Log
|
| atomarer, versionierter, SHA-256-geschuetzter Snapshot
v
%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot
^
| nur lesen: BizTalkCheckmkPulse.exe --consume
|
Checkmk Windows Agent (LocalSystem)
|
v
acht kompakte Checkmk Local Checks
```
## Warum Local Check statt serverseitigem Check-Plugin?
Damit bekommt `LocalSystem` keine BizTalk- oder SQL-Berechtigung. Nur das
dedizierte Provider-Konto wird in die konfigurierte BizTalk Server Read Only
Users-Gruppe aufgenommen. Der Checkmk-Consumer liest keine BizTalk-Datenbank,
verwendet kein WMI und nimmt keine Identitaetswechsel vor.
Checkmk 2.4 kann Windows-Agent-Plugins und Local Checks direkt ausfuehren. Fuer diese Umgebung ist ein C#/.NET-Framework-Programm mit `.cmd`-Wrapper die robusteste Variante:
## Warum die Architektur geaendert wurde
- keine PowerShell-Ausfuehrung erforderlich
- keine BizTalk-ExplorerOM/OperationsOM-DLL als Build-Abhaengigkeit
- keine Python-Check-API-Abhaengigkeit auf der Checkmk-Site
- einfache Verteilung auf die BizTalk-Maschinen per Dateiablage, Softwareverteilung oder Agent Bakery
- automatische Service Discovery in Checkmk
Der ACC-Test vom 29.07.2026 zeigte:
## Erzeugte Checkmk-Services
```text
Login failed for user 'BEW\AV23AGPWBIO1$'
```
Standardmaessig entstehen diese stabilen Services:
Der Checkmk-Agent lief korrekt als `NT AUTHORITY\SYSTEM` und erreichte lokales
BizTalk-WMI. Datenbankgestuetzte WMI-Abfragen wurden jedoch am SQL Server als
Maschinenkonto `BEW\AV23AGPWBIO1$` abgewiesen. Eine Mitgliedschaft des
Computerkontos wuerde die BizTalk-Netzwerkberechtigung allen als `LocalSystem`
laufenden Diensten des Servers geben.
Die neue Trennung reduziert diesen Berechtigungsumfang. Fuer BizTalk Server
2020 ist die konfigurierte `BizTalk Server Read Only Users`-Gruppe mit der
SQL-Datenbankrolle `BTS_READONLY_USERS` die bevorzugte Grenze. Die
`BizTalk Server Operators`-Gruppe ist nur ein dokumentierter Fallback, wenn
eine konkret benoetigte WMI-Klasse trotz bestaetigter Read-Only-Zuordnung
abgewiesen wird. Direkte SQL-Logins, manuelle Datenbankrollen und `sysadmin`
sind keine Loesung.
## Robustheit
Der Datenaustausch ist bewusst defensiv:
- Der Provider schreibt zuerst eine eindeutige Temporaerdatei im Zielordner,
leert Betriebssystempuffer und ersetzt danach den Snapshot atomar.
- Der Snapshot enthaelt Formatversion, UTC-Zeit, Quellmaschine,
Provider-Identitaet, Zeilenanzahl und SHA-256 des Payloads.
- Der Consumer akzeptiert nur denselben Rechner, gueltiges UTF-8, intakte
Checkmk-Zeilen, korrekte SHA-256-Pruefsumme und ein maximales Alter von
standardmaessig 180 Sekunden.
- Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien
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
`UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode.
- Provider und Consumer protokollieren in taegliche Dateien; die Aufbewahrung
ist standardmaessig 30 Tage.
- Snapshotgroesse, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind
begrenzt und konfigurierbar.
## Erzeugte Services
Standardmaessig entstehen:
- `BizTalk Platform`
- `BizTalk SQL Access`
- `BizTalk Suspended Instances`
- `BizTalk Host Instances`
- `BizTalk Runtime Artifacts`
- `BizTalk Receive Locations`
- `BizTalk Send Ports`
- `BizTalk Orchestrations`
- `BizTalk Event Log`
Wenn `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` gesetzt wird, wird der Name vor den Suffix gesetzt, z.B. `BizTalk PRD Suspended Instances`. Das ist praktisch, wenn die Umgebung bereits im Hostnamen oder Ordner abgebildet ist, aber nicht zwingend noetig.
Die drei Artefaktbereiche sind absichtlich getrennte Services. Dadurch sind
Zustand und Graphen direkt erkennbar, ohne eine lange Sammelzeile zu lesen:
Details zu Statuslogik und Metriken stehen in [docs/CheckmkServices.md](docs/CheckmkServices.md).
Beispielausgaben stehen in [docs/ExampleOutput.md](docs/ExampleOutput.md).
- 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
## Build
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.
Voraussetzungen auf einem Windows-Build-Host:
Mit `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` wird die Umgebung in den
Servicenamen aufgenommen, zum Beispiel `BizTalk ACC Platform`.
- Visual Studio 2022 Build Tools oder Visual Studio
Statuslogik und Metriken: [docs/CheckmkServices.md](docs/CheckmkServices.md)
Beispielausgaben: [docs/ExampleOutput.md](docs/ExampleOutput.md)
## Voraussetzungen
Build-Host:
- Visual Studio 2019/2022 Build Tools oder Visual Studio
- MSBuild im `PATH`
- .NET Framework 4.7.2 Developer Pack
Build:
BizTalk-Server:
- Windows Server 2019
- BizTalk Server 2020 und lokaler Namespace
`root\MicrosoftBizTalkServer`
- .NET Framework 4.7.2
- Checkmk Windows Agent
- administrativer Zugriff fuer die einmalige Installation
- normales dediziertes AD-Servicekonto fuer den Provider
Das Provider-Konto benoetigt:
- lokales Recht zur Ausfuehrung als Scheduled Task
- lokalen Lese-/Ausfuehrungszugriff auf die installierte EXE
- Schreibzugriff nur auf Snapshot- und Logverzeichnis
- Mitgliedschaft in der exakt konfigurierten BizTalk Server Read Only
Users-Gruppe
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
```cmd
scripts\build-release.cmd
```
Deployment-Paket erstellen:
```cmd
scripts\test-release.cmd
scripts\package-release.cmd
```
Ergebnis:
Das Paket wird unter `artifacts\BizTalkCheckmkPulse-deploy` erzeugt:
```text
artifacts\BizTalkCheckmkPulse-deploy\
BizTalkCheckmkPulse-deploy\
Install-BizTalkCheckmkPulse.ps1
Uninstall-BizTalkCheckmkPulse.ps1
biztalk_checkmk_pulse.cmd
BizTalkCheckmkPulse\
application\
BizTalkCheckmkPulse.exe
BizTalkCheckmkPulse.exe.config
```
Validierung nach dem Build:
Format-Self-Test ohne WMI, SQL oder Event Log:
```cmd
artifacts\BizTalkCheckmkPulse-deploy\biztalk_checkmk_pulse.cmd --self-test
artifacts\BizTalkCheckmkPulse-deploy\application\BizTalkCheckmkPulse.exe --self-test
```
## Installation auf dem BizTalk-Server
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
Windows-/BizTalk-Laufzeitvalidierung.
Kopiere den Inhalt von `artifacts\BizTalkCheckmkPulse-deploy` nach:
## Berechtigung vorbereiten
Die exakte Read-Only-Gruppe wird in der BizTalk Administration Console unter
den Eigenschaften der BizTalk-Gruppe abgelesen. Ein bereits berechtigtes Konto
kann sie alternativ ermitteln:
```powershell
Get-CimInstance `
-Namespace root/MicrosoftBizTalkServer `
-ClassName MSBTS_GroupSetting |
Select-Object Name, BizTalkReadOnlyUserGroup, BizTalkOperatorGroup,
MgmtDbServerName, MgmtDbName
```
Ein AD-Administrator nimmt das neue Provider-Konto in
`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`,
`BizTalkRuleEngineDb` sowie gegebenenfalls `BAMPrimaryImport` mit
`BTS_READONLY_USERS` abbilden. Eine fehlende Abbildung wird durch BizTalk- und
SQL-Administration fuer die Gruppe repariert, nicht als Einzelberechtigung
fuer das Provider-Konto.
## Installation mit normalem Servicekonto
```powershell
Set-Location C:\Temp\BizTalkCheckmkPulse-deploy
.\Install-BizTalkCheckmkPulse.ps1 `
-CollectorAccount 'BEW\svc_biztalk_cmk' `
-EnvironmentName ACC
```
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
`%ProgramFiles%\BizTalkCheckmkPulse`,
2. erstellt `%ProgramData%\BizTalkCheckmkPulse\data` und `logs`,
3. setzt explizite ACLs fuer Administratoren, Provider und `LocalSystem`,
4. installiert nur den kleinen `.cmd`-Consumer unter
`%ProgramData%\checkmk\agent\local`,
5. registriert `BizTalk Checkmk Pulse Provider` minuetlich mit
`IgnoreNew`, fuenf Minuten Laufzeitlimit und zwei Wiederholungen,
6. fuehrt den Self-Test aus und startet den Provider einmalig.
PowerShell wird nur fuer Installation und Betriebsdiagnose verwendet. Der
minuetliche Provider und der Checkmk-Consumer sind .NET-/CMD-Laufzeitcode und
haengen nicht von der PowerShell Execution Policy ab.
## Verifikation auf dem Server
Task und letzter Lauf:
```powershell
Get-ScheduledTask -TaskName 'BizTalk Checkmk Pulse Provider' |
Select-Object TaskName, State
Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' |
Select-Object LastRunTime, LastTaskResult, NextRunTime
```
Provider-Log:
```powershell
Get-ChildItem "$env:ProgramData\BizTalkCheckmkPulse\logs" |
Sort-Object LastWriteTime -Descending |
Select-Object -First 3 Name, Length, LastWriteTime
Get-Content `
"$env:ProgramData\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" `
-Tail 100
```
Snapshot und Consumer:
```powershell
Get-Item `
"$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" |
Select-Object FullName, Length, LastWriteTimeUtc
& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
```
Verbindlicher Test im echten `LocalSystem`-Kontext:
```powershell
& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump |
Select-String -Pattern "BizTalk|UNKNOWN|Snapshot|Permission" -Context 0,1
```
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.
## Dateisystem und Sicherheitsgrenzen
```text
%ProgramData%\checkmk\agent\local
%ProgramFiles%\BizTalkCheckmkPulse\
BizTalkCheckmkPulse.exe
BizTalkCheckmkPulse.exe.config
%ProgramData%\BizTalkCheckmkPulse\
data\
biztalk-checkmk-pulse.snapshot
biztalk-checkmk-pulse.snapshot.provider.lock
logs\
biztalk-checkmk-pulse-YYYYMMDD.log
%ProgramData%\checkmk\agent\local\
biztalk_checkmk_pulse.cmd
```
Zielstruktur:
ACL-Soll:
```text
%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd
%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe
%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config
```
| Pfad | Provider | LocalSystem | Administratoren |
| --- | --- | --- | --- |
| Programm | Lesen/Ausfuehren | Lesen/Ausfuehren | Vollzugriff |
| `data` | Aendern | Lesen/Ausfuehren | Vollzugriff |
| `logs` | Aendern | Aendern | Vollzugriff |
Manueller Test auf dem BizTalk-Server:
```cmd
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd"
```
Agent-Ausgabe wie Checkmk sie sieht:
```cmd
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
```
Danach in Checkmk fuer den BizTalk-Host eine Service Discovery ausfuehren, die neuen Services aufnehmen und Changes aktivieren.
Der Snapshot enthaelt Monitoringzustand und kompakte Fehlerdetails, aber keine
Passwoerter oder Nachrichteninhalte. Der Consumer validiert die Datei trotzdem
vollstaendig, bevor er sie an Checkmk weitergibt.
## Konfiguration
Die Konfiguration liegt neben der EXE:
Datei:
```text
%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config
%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config
```
Wichtige Werte:
| Key | Default | Bedeutung |
| --- | --- | --- |
| `Server` | `.` | Lokaler BizTalk-Server. Remote-WMI ist moeglich, aber nicht empfohlen. |
| `ServicePrefix` | `BizTalk` | Prefix fuer alle Checkmk-Services. |
| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST` oder `PRD`. |
| `QueryTimeoutSeconds` | `25` | WMI-Timeout pro Query. |
| `ProbeSqlConnectivity` | `true` | Prueft die integrierte Windows-Anmeldung an den per WMI ermittelten Management- und MessageBox-Datenbanken. |
| `SqlConnectionTimeoutSeconds` | `5` | Timeout je SQL-Ziel fuer Verbindungsaufbau und harmlose Testabfrage. |
| `WarnResumableThreshold` | `1` | WARN ab n resumable suspended instances. |
| `CritNonResumableThreshold` | `1` | CRIT ab n non-resumable suspended instances. |
| `AlertOnArtifactRuntimeIssues` | `false` | Wenn `true`, werden deaktivierte Receive Locations und inaktive Ports/Orchestrations als WARN gewertet. |
| `EmitPerApplicationSuspensionServices` | `false` | Erzeugt zusaetzliche Services pro Anwendung mit Suspensions. |
| `ProbeEventLog` | `true` | Liest das Windows Application Log nach BizTalk-bezogenen Sources. |
| `EventLogLookbackMinutes` | `60` | Zeitraum fuer Event-Log-Auswertung. |
| `EventLogWarnThreshold` | `1` | WARN ab n Errors oder Warnings. |
| `EventLogCritThreshold` | `10` | CRIT ab n Errors. |
| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST`, `PRD`. |
| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | Gemeinsame Provider-/Consumer-Datei. |
| `SnapshotMaxAgeSeconds` | `180` | Ab diesem Alter liefert der Consumer `UNKNOWN`. |
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegroesse fuer den Consumer. |
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. |
| `LogRetentionDays` | `30` | Provider bereinigt aeltere Logs. |
| `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. |
| `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. |
| `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. |
| `CritNonResumableThreshold` | `1` | CRIT ab n non-resumable Suspensions. |
| `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. |
Empfehlung:
Nach einer Config-Aenderung den Scheduled Task manuell starten. Der Consumer
liest den naechsten atomar publizierten Snapshot.
- In `DEV`, `TST`, `ACC`: `AlertOnArtifactRuntimeIssues=false`, damit bewusst gestoppte Artefakte nicht rauschen.
- In `PRD`: erst zwei Wochen beobachten; danach nur aktivieren, wenn die Runtime-Artefakte wirklich als Betriebsstandard immer aktiv sein muessen.
- `EmitPerApplicationSuspensionServices=true` nur verwenden, wenn Anwendungsteams eigene Services benoetigen. Sonst bleibt die Discovery schlanker.
## Fehlerbilder
## Integration in Checkmk Managed Services Edition 2.4
| Beobachtung | Ursache / Massnahme |
| --- | --- |
| 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. |
Aufgaben der Checkmk-Kollegen:
Ausfuehrliche Betriebs- und Architekturdetails stehen in
[Dokumentation.md](Dokumentation.md).
1. BizTalk-Hosts in Checkmk anlegen oder bestehende Hosts pruefen.
2. Sicherstellen, dass der Checkmk Windows Agent installiert, registriert und erreichbar ist.
3. Plugin-Dateien auf die BizTalk-Server verteilen, manuell oder per Agent Bakery.
4. Optional `EnvironmentName` je Umgebung setzen, z.B. `ACC`, `DEV`, `TST` oder `PRD`.
5. Agent-Ausgabe mit `cmk-agent-ctl.exe dump` pruefen.
6. Service Discovery fuer jeden BizTalk-Host ausfuehren.
7. Gefundene `BizTalk ...` Services aufnehmen und Changes aktivieren.
8. Views, Dashboards, Servicegruppen und Benachrichtigungen fuer die BizTalk-Services konfigurieren.
Manuelle Integration:
1. Dateien auf dem BizTalk-Server nach `%ProgramData%\checkmk\agent\local` kopieren.
2. Optional `BizTalkCheckmkPulse.exe.config` je Umgebung anpassen.
3. Agent-Dump pruefen.
4. Service Discovery auf dem BizTalk-Host ausfuehren.
5. Services in ein BizTalk-Dashboard aufnehmen.
Integration ueber Agent Bakery:
1. Deployment-Dateien in der Checkmk-Site als Custom-Agent-Dateien bereitstellen.
2. Windows-Agent-Regel fuer die BizTalk-Hosts erstellen.
3. Agent backen und auf ACC/DEV/TST/PRD-BizTalk-Hosts ausrollen.
4. Discovery und Dashboard-Aufnahme durchfuehren.
Die Checkmk-Dokumentation nennt fuer Windows Local Checks `%ProgramData%\checkmk\agent\local` und fuer Windows Agent Plugins `%ProgramData%\checkmk\agent\plugins`. Dieses Projekt nutzt bewusst `local`, weil der Zustand direkt vom Host berechnet und sofort als Checkmk-Service geliefert wird.
## Berechtigungen fuer WMI und BizTalk-Datenbanken
### Ausfuehrungskontext
Der Checkmk Windows Agent und der Agent Controller laufen standardmaessig als `LocalSystem` (`NT AUTHORITY\SYSTEM`). Der `.cmd`-Wrapper und `BizTalkCheckmkPulse.exe` erben diesen Sicherheitskontext.
Das Programm:
- verbindet sich lokal mit `\\<eigener-server>\root\MicrosoftBizTalkServer`
- setzt keine separaten WMI-Anmeldedaten
- verwendet kein Remote-WMI
- ruft keine veraendernden BizTalk-WMI-Methoden auf
- liest zusaetzlich nur das lokale Windows Application Event Log
Fuer den lokalen WMI-Verbindungsaufbau und das lokale Application Event Log reichen die Rechte von `LocalSystem` normalerweise aus. Es muessen deshalb im Regelfall keine DCOM-, Firewall- oder WMI-Namespace-Freigaben eingerichtet werden.
### Zugriff auf einen separaten SQL Server
Einige BizTalk-WMI-Klassen beziehen ihre Daten aus der BizTalk Management- oder MessageBox-Datenbank. Wenn der SQL Server auf einer anderen Maschine laeuft, greift ein unter `LocalSystem` ausgefuehrter Prozess im Netzwerk mit dem Computerkonto des BizTalk-Servers zu:
```text
DOMAIN\BIZTALKSERVER$
```
Die weitreichenden lokalen Rechte von `LocalSystem` ergeben nicht automatisch Berechtigungen auf dem entfernten SQL Server. Deshalb kann die Verbindung zum lokalen WMI-Namespace funktionieren, waehrend einzelne SQL-gestuetzte BizTalk-WMI-Abfragen mit `Access denied`, `UnauthorizedAccessException` oder `UNKNOWN` fehlschlagen.
### Test im echten Checkmk-Kontext
Ein manueller Aufruf von `BizTalkCheckmkPulse.exe` oder des Wrappers verwendet das Konto der angemeldeten Person. Ein erfolgreicher manueller Test beweist daher nicht, dass die Ausfuehrung durch Checkmk als `LocalSystem` ebenfalls funktioniert.
Der verbindliche Test erfolgt ueber den Agent Controller:
## Deinstallation
```powershell
& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump |
Select-String -Pattern "BizTalk|Access denied|Unauthorized|UNKNOWN" -Context 0,1
.\Uninstall-BizTalkCheckmkPulse.ps1
```
Erwartet werden die sechs stabilen `BizTalk ...` Services mit plausiblen Daten. Insbesondere `BizTalk Platform` und `BizTalk SQL Access` duerfen nicht wegen eines WMI-, SQL- oder Berechtigungsfehlers `UNKNOWN` sein.
Das Skript fragt vor dem Entfernen. Mit `-KeepRuntimeData` bleiben Snapshot und
Logs erhalten:
Das Programm prueft Berechtigungen aktiv:
- Der WMI-Namespace-Verbindungsaufbau und jede erforderliche WMI-Klasse werden getrennt bewertet.
- Fehler werden als `Permission`, `Connectivity`, `Timeout`, `Configuration` oder `Provider` klassifiziert.
- `BizTalk SQL Access` oeffnet mit integrierter Windows-Authentifizierung eine Verbindung zur ermittelten Management- und MessageBox-Datenbank und fuehrt `SELECT 1` aus.
- Die Service-Ausgabe nennt Ausfuehrungsidentitaet, erwartete Netzwerkidentitaet, betroffene Komponente, technische Ursache und konkrete Massnahme.
- Fehlgeschlagene Pflichtabfragen werden nie als leerer, erfolgreicher Datenbestand gewertet. Der betroffene Service wird `UNKNOWN`.
`Sql/Configuration` weist je nach Detailtext entweder auf eine unvollstaendige WMI-Zielermittlung oder auf TLS-, Zertifikats-, SPN-/SSPI-Probleme hin. Die Diagnose empfiehlt bewusst nicht, SQL-Verschluesselung pauschal abzuschalten.
Der SQL-Zugriffstest ersetzt nicht das Checkmk-MSSQL-Plugin auf dem SQL-Server. Er prueft ausschliesslich, ob genau die Identitaet des BizTalk Local Checks die fuer seine Diagnose benoetigten BizTalk-Datenbankziele erreichen kann.
Da bei fehlenden Rechten jeder Lauf einen abgewiesenen SQL-Login erzeugen kann, wird fuer den produktiven Betrieb eine asynchrone Ausfuehrung mit 300 Sekunden Cache empfohlen. Das begrenzt SQL-Logeintraege und Last, verzoegert einen Zustandswechsel aber um maximal fuenf Minuten. Beispiel in `%ProgramData%\checkmk\agent\check_mk.user.yml`:
```yaml
local:
enabled: yes
execution:
- pattern: $CUSTOM_LOCAL_PATH$\biztalk_checkmk_pulse.cmd
async: yes
run: yes
cache_age: 300
```powershell
.\Uninstall-BizTalkCheckmkPulse.ps1 -KeepRuntimeData
```
### Vorgehen bei Berechtigungsfehlern
Die AD-Gruppenmitgliedschaft des Provider-Kontos wird bewusst nicht automatisch
geaendert und muss separat durch AD-/BizTalk-Administration entfernt werden.
1. Pruefen, unter welchem Computerkonto der BizTalk-Server im Netzwerk auftritt, normalerweise `DOMAIN\BIZTALKSERVER$`.
2. Die fuer die BizTalk-Gruppe konfigurierte Windows-Gruppe `BizTalk Server Operators` ermitteln. Der tatsaechliche Gruppenname kann bei der BizTalk-Konfiguration angepasst worden sein.
3. Das Computerkonto des BizTalk-Servers in diese Operator-Gruppe aufnehmen.
4. Kerberos-Tickets des Systemkontos erneuern oder den BizTalk-Server in einem Wartungsfenster neu starten.
5. Den Test mit `cmk-agent-ctl.exe dump` wiederholen.
6. Nur wenn konkrete WMI-Klassen weiterhin abgelehnt werden, gemeinsam mit BizTalk- und SQL-Administration pruefen, ob diese Abfrage `BizTalk Server Administrators` benoetigt.
## Certutil-Transport
Die Operator-Rolle ist fuer grundlegende Administration und Monitoring vorgesehen und kann Zustands- und Message-Flow-Informationen lesen, ohne BizTalk-Konfiguration oder Nachrichteninhalte einzusehen. Das Plugin fuehrt ausschliesslich Leseabfragen aus. Direkte manuelle Aenderungen an BizTalk-SQL-Datenbankrollen sollten nicht vorgenommen werden; die von BizTalk konfigurierte Windows-Gruppe ist die vorgesehene Berechtigungsgrenze.
Zu jeder Uebergabe wird ein Source-ZIP und eine certutil-kompatible
Base64-Textdatei erzeugt. Auf Windows:
Entscheidungsmatrix:
```cmd
certutil -decode biztalk-checkmk-pulse-source-<datum>-<commit>.zip.b64.txt biztalk-checkmk-pulse-source.zip
certutil -hashfile biztalk-checkmk-pulse-source.zip SHA256
tar -xf biztalk-checkmk-pulse-source.zip
```
| Ergebnis im Agent-Dump | Massnahme |
| --- | --- |
| Alle BizTalk-Services liefern plausible Daten | Keine Berechtigungsaenderung erforderlich. |
| Verbindung zu `root\MicrosoftBizTalkServer` scheitert | Namespace, BizTalk-WMI-Provider, WMI-Dienst und Namespace-ACL gezielt pruefen. Keine pauschalen WMI-Rechte vergeben. |
| `BizTalk SQL Access` meldet `Sql/Permission` | Computerkonto `DOMAIN\BIZTALKSERVER$` in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und erneut testen. Keine direkten BizTalk-DB-Rollen vergeben. |
| `BizTalk SQL Access` meldet `Sql/Connectivity` oder `Sql/Timeout` | Server-/Instanzname, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall aus Sicht des BizTalk-Servers pruefen. |
| Nur SQL-gestuetzte BizTalk-Klassen liefern `Access denied` oder `UNKNOWN` | Computerkonto `DOMAIN\BIZTALKSERVER$` zunaechst in `BizTalk Server Operators` aufnehmen. |
| `BizTalk Event Log` ist `UNKNOWN` | Zugriff auf das lokale Application Log pruefen; `LocalSystem` kann es normalerweise lesen. |
| Fehler bleibt trotz Operator-Rolle bestehen | Betroffene WMI-Klasse und BizTalk-/SQL-Rollenzuordnung mit den Fachadministratoren untersuchen. |
### Sicherheitsabwaegung und Alternative
Wird das Computerkonto in `BizTalk Server Operators` aufgenommen, erhalten alle Dienste, die auf diesem BizTalk-Server als `LocalSystem` laufen, diese Netzwerkberechtigung. Das ist vor dem Rollout mit der Security- und BizTalk-Administration abzustimmen.
Falls diese Freigabe nicht zulaessig ist, ist die sauberere Alternative ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten. Dieser Collector kann seine Checkmk-Ausgabe in eine Spool-Datei schreiben. Der Checkmk-Agent liest dann nur die bereits erzeugten Daten ein. Den gesamten Checkmk-Agent-Dienst sollte man nicht allein fuer dieses Plugin von `LocalSystem` auf ein anderes Konto umstellen, weil dadurch alle Agent-Sektionen und Local Checks betroffen sind.
Empfohlene Alarmierung:
- `BizTalk Platform`: `UNKNOWN` immer untersuchen, da dann WMI, Rechte oder Deployment betroffen sind.
- `BizTalk SQL Access`: `UNKNOWN` als Integrations-, Berechtigungs- oder Verbindungsproblem behandeln und die eingebettete Massnahme abarbeiten.
- `BizTalk Suspended Instances`: in `PRD` direkt alarmieren; in Nicht-PRD nach Betriebsbedarf.
- `BizTalk Host Instances`: `CRIT` alarmieren, weil gestoppte Host Instances Laufzeitverarbeitung verhindern koennen.
- `BizTalk Runtime Artifacts`: zunaechst beobachten; strengere Alarmierung erst aktivieren, wenn deaktivierte Artefakte nicht fachlich gewollt sind.
- `BizTalk Event Log`: Schwellwerte nach Beobachtungsphase anpassen.
Die konkrete Datei und SHA-256-Summe werden bei der Uebergabe genannt.
## Quellen
- Checkmk Local Checks: https://docs.checkmk.com/latest/en/localchecks.html
- Checkmk Windows Agent und Plugin-Pfade: https://docs.checkmk.com/latest/en/agent_windows.html
- Checkmk Agent-Based Plugin-Entwicklung: https://docs.checkmk.com/latest/en/devel_check_plugins.html
- Checkmk Bakery API: https://docs.checkmk.com/latest/en/bakery_api.html
- Checkmk MKP-Pakete: https://docs.checkmk.com/latest/en/mkps.html
- Microsoft BizTalk WMI `MSBTS_ServiceInstance`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-wmi
- Microsoft BizTalk WMI `ServiceStatus`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-servicestatus-property-wmi
- Microsoft BizTalk Mindestberechtigungen: https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights
- Microsoft BizTalk Access Control: https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security
- Microsoft LocalSystem und Computerkonten: https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer
- Microsoft WMI Namespace Security: https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces
- Microsoft: BizTalk `MSBTS_GroupSetting.BizTalkReadOnlyUserGroup`
- Microsoft: Windows Groups and User Accounts in BizTalk Server
- Microsoft: Managing BizTalk Server Security
- Checkmk: Windows Agent und Local Checks
Die genauen Links stehen in [Dokumentation.md](Dokumentation.md).
+171
View File
@@ -0,0 +1,171 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$CollectorAccount,
[switch]$Gmsa,
[ValidateSet("", "ACC", "DEV", "TST", "PRD")]
[string]$EnvironmentName = "",
[string]$InstallDirectory = "$env:ProgramFiles\BizTalkCheckmkPulse",
[string]$CheckmkLocalDirectory = "$env:ProgramData\checkmk\agent\local"
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version 2.0
$taskName = "BizTalk Checkmk Pulse Provider"
$runtimeRoot = Join-Path $env:ProgramData "BizTalkCheckmkPulse"
$dataDirectory = Join-Path $runtimeRoot "data"
$logDirectory = Join-Path $runtimeRoot "logs"
$sourceApplication = Join-Path $PSScriptRoot "application"
$sourceWrapper = Join-Path $PSScriptRoot "biztalk_checkmk_pulse.cmd"
$targetExe = Join-Path $InstallDirectory "BizTalkCheckmkPulse.exe"
$targetConfig = "$targetExe.config"
$targetWrapper = Join-Path $CheckmkLocalDirectory "biztalk_checkmk_pulse.cmd"
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "Die Installation muss in einer administrativen Windows PowerShell ausgefuehrt werden."
}
}
function Invoke-Icacls {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string[]]$Arguments
)
& icacls.exe $Path @Arguments | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "icacls ist fuer '$Path' mit Exitcode $LASTEXITCODE fehlgeschlagen."
}
}
Assert-Administrator
if (-not (Test-Path -LiteralPath $sourceApplication -PathType Container)) {
throw "Quellverzeichnis fehlt: $sourceApplication"
}
if (-not (Test-Path -LiteralPath $sourceWrapper -PathType Leaf)) {
throw "Checkmk-Wrapper fehlt: $sourceWrapper"
}
if ($Gmsa -and -not $CollectorAccount.EndsWith('$')) {
throw "Ein gMSA-Kontoname muss mit '$' enden, z.B. DOMAIN\svc_biztalk_cmk$."
}
Write-Host "Installiere Programmdateien nach $InstallDirectory ..."
New-Item -ItemType Directory -Path $InstallDirectory -Force | Out-Null
Copy-Item -Path (Join-Path $sourceApplication "*") -Destination $InstallDirectory -Force
[xml]$configuration = Get-Content -LiteralPath $targetConfig
$environmentSetting = $configuration.configuration.appSettings.add |
Where-Object { $_.key -eq "EnvironmentName" } |
Select-Object -First 1
if ($null -eq $environmentSetting) {
throw "EnvironmentName fehlt in $targetConfig."
}
$environmentSetting.value = $EnvironmentName
$configuration.Save($targetConfig)
Write-Host "Erzeuge Runtime-Verzeichnisse und Least-Privilege-ACLs ..."
New-Item -ItemType Directory -Path $runtimeRoot, $dataDirectory, $logDirectory -Force | Out-Null
# Runtime-Root: Provider lesen, SYSTEM lesen, lokale Administratoren verwalten.
Invoke-Icacls -Path $runtimeRoot -Arguments @(
"/inheritance:r",
"/grant:r",
"*S-1-5-18:(OI)(CI)(RX)",
"*S-1-5-32-544:(OI)(CI)(F)",
"${CollectorAccount}:(OI)(CI)(RX)"
)
# Daten: ausschliesslich Provider schreibt, LocalSystem liest den Snapshot.
Invoke-Icacls -Path $dataDirectory -Arguments @(
"/inheritance:r",
"/grant:r",
"*S-1-5-18:(OI)(CI)(RX)",
"*S-1-5-32-544:(OI)(CI)(F)",
"${CollectorAccount}:(OI)(CI)(M)"
)
# Logs: Provider und LocalSystem-Consumer duerfen getrennte Diagnosezeilen anhaengen.
Invoke-Icacls -Path $logDirectory -Arguments @(
"/inheritance:r",
"/grant:r",
"*S-1-5-18:(OI)(CI)(M)",
"*S-1-5-32-544:(OI)(CI)(F)",
"${CollectorAccount}:(OI)(CI)(M)"
)
Write-Host "Installiere Checkmk Local Check nach $CheckmkLocalDirectory ..."
New-Item -ItemType Directory -Path $CheckmkLocalDirectory -Force | Out-Null
Copy-Item -LiteralPath $sourceWrapper -Destination $targetWrapper -Force
Write-Host "Registriere Scheduled Task '$taskName' ..."
$action = New-ScheduledTaskAction `
-Execute $targetExe `
-Argument "--collect" `
-WorkingDirectory $InstallDirectory
$trigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 1)
$settings = New-ScheduledTaskSettingsSet `
-MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Minutes 5) `
-StartWhenAvailable `
-RestartCount 2 `
-RestartInterval (New-TimeSpan -Minutes 1)
if ($Gmsa) {
$principal = New-ScheduledTaskPrincipal `
-UserId $CollectorAccount `
-LogonType Password `
-RunLevel Limited
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal
Register-ScheduledTask -TaskName $taskName -InputObject $task -Force | Out-Null
}
else {
$credential = Get-Credential `
-UserName $CollectorAccount `
-Message "Kennwort fuer das dedizierte BizTalk-Monitoringkonto eingeben"
if ($credential.UserName -ne $CollectorAccount) {
throw "Das eingegebene Konto stimmt nicht mit CollectorAccount ueberein."
}
$principal = New-ScheduledTaskPrincipal `
-UserId $CollectorAccount `
-LogonType Password `
-RunLevel Limited
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal
Register-ScheduledTask `
-TaskName $taskName `
-InputObject $task `
-User $CollectorAccount `
-Password $credential.GetNetworkCredential().Password `
-Force | Out-Null
}
Write-Host "Fuehre formatseitigen Self-Test aus ..."
$selfTest = & $targetExe --self-test
if ($LASTEXITCODE -ne 0 -or @($selfTest).Count -ne 8) {
throw "Self-Test fehlgeschlagen. Erwartet wurden acht Checkmk-Zeilen."
}
Write-Host "Starte den Provider einmalig ..."
Start-ScheduledTask -TaskName $taskName
Write-Host ""
Write-Host "Installation abgeschlossen."
Write-Host "Naechste Pruefungen:"
Write-Host " Get-ScheduledTaskInfo -TaskName '$taskName'"
Write-Host " Get-Content '$logDirectory\biztalk-checkmk-pulse-*.log' -Tail 50"
Write-Host " & '$targetExe' --consume"
Write-Host " & 'C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe' dump"
@@ -0,0 +1,30 @@
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
param(
[string]$InstallDirectory = "$env:ProgramFiles\BizTalkCheckmkPulse",
[string]$CheckmkLocalDirectory = "$env:ProgramData\checkmk\agent\local",
[switch]$KeepRuntimeData
)
$ErrorActionPreference = "Stop"
$taskName = "BizTalk Checkmk Pulse Provider"
$runtimeRoot = Join-Path $env:ProgramData "BizTalkCheckmkPulse"
$wrapper = Join-Path $CheckmkLocalDirectory "biztalk_checkmk_pulse.cmd"
if ($PSCmdlet.ShouldProcess($taskName, "Scheduled Task entfernen")) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $wrapper) {
if ($PSCmdlet.ShouldProcess($wrapper, "Checkmk Local Check entfernen")) {
Remove-Item -LiteralPath $wrapper -Force
}
}
if (Test-Path -LiteralPath $InstallDirectory) {
if ($PSCmdlet.ShouldProcess($InstallDirectory, "Programmdateien entfernen")) {
Remove-Item -LiteralPath $InstallDirectory -Recurse -Force
}
}
if (-not $KeepRuntimeData -and (Test-Path -LiteralPath $runtimeRoot)) {
if ($PSCmdlet.ShouldProcess($runtimeRoot, "Snapshot und Logs entfernen")) {
Remove-Item -LiteralPath $runtimeRoot -Recurse -Force
}
}
+18 -4
View File
@@ -1,17 +1,31 @@
@echo off
setlocal
set "BASE=%~dp0BizTalkCheckmkPulse"
set "BASE=%ProgramFiles%\BizTalkCheckmkPulse"
set "EXE=%BASE%\BizTalkCheckmkPulse.exe"
if not exist "%EXE%" (
echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen und danach cmk-agent-ctl.exe dump erneut ausfuehren.
echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Install-BizTalkCheckmkPulse.ps1 erneut als Administrator ausfuehren und danach cmk-agent-ctl.exe dump erneut ausfuehren.
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 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
)
"%EXE%" %*
"%EXE%" --consume %*
set "EXITCODE=%ERRORLEVEL%"
if not "%EXITCODE%"=="0" (
echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe konnte nicht erfolgreich gestartet werden, Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, EXE/Config-Dateien und Windows Application Log pruefen.
echo 3 "BizTalk Platform" - Snapshot-Consumer konnte nicht erfolgreich gestartet werden, Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, Installation und Provider-Log pruefen.
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 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
+110
View File
@@ -0,0 +1,110 @@
BizTalk Checkmk Pulse - ACC Berechtigung und Architektur
Stand: 30.07.2026
BEFUND
======
Host: AV23AGPWBIO1
Checkmk-Identitaet: NT AUTHORITY\SYSTEM
Alte Netzwerkidentitaet: BEW\AV23AGPWBIO1$
Fehler: COMException HRESULT=0x80131904
Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$'
Der lokale BizTalk-WMI-Namespace war erreichbar. Datenbankgestuetzte
BizTalk-WMI-Abfragen scheiterten am SQL-Login des Maschinenkontos.
NEUE VERBINDLICHE ARCHITEKTUR
=============================
Das Maschinenkonto erhaelt keine BizTalk-Berechtigung.
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:
%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot
4. Der Checkmk-Agent bleibt LocalSystem und startet nur den Consumer.
5. Der Consumer liest und validiert den Snapshot. Er fuehrt kein WMI und
keinen SQL-Zugriff aus.
BERECHTIGUNG DES PROVIDER-KONTOS
================================
Bevorzugte Rolle fuer BizTalk Server 2020:
BizTalk Server Read Only Users
SQL-Datenbankrolle: BTS_READONLY_USERS
Die exakt konfigurierte Gruppe mit einem bereits berechtigten Konto ermitteln:
Get-CimInstance -Namespace root/MicrosoftBizTalkServer `
-ClassName MSBTS_GroupSetting |
Select-Object Name, BizTalkReadOnlyUserGroup, BizTalkOperatorGroup,
MgmtDbServerName, MgmtDbName
Das Provider-Konto durch AD-Administration in BizTalkReadOnlyUserGroup
aufnehmen. Nicht vom Standardnamen ausgehen.
Die Operator-Gruppe ist nur eine begruendungspflichtige Eskalation, falls eine
konkret benoetigte WMI-Klasse trotz bestaetigter Read-Only-Zuordnung und neuem
Anmeldetoken verweigert wird.
NICHT DURCHFUEHREN
==================
- keinen SQL-Einzellogin fuer Provider- oder Maschinenkonto
- keine ad-hoc BizTalk-Datenbankrollen
- kein db_owner oder sysadmin
- keine pauschalen DCOM-/WMI-ACL-Erweiterungen
- Checkmk-Agentdienst nicht auf das Provider-Konto umstellen
- Maschinenkonto BEW\AV23AGPWBIO1$ nicht fuer diese Loesung berechtigen
INSTALLATION
============
Normales Servicekonto:
.\Install-BizTalkCheckmkPulse.ps1 `
-CollectorAccount 'BEW\svc_biztalk_cmk' `
-EnvironmentName ACC
VERIFIKATION
============
Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider'
Get-Content `
"$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" `
-Tail 100
Get-Item `
"$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot"
& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump |
Select-String -Pattern "BizTalk|UNKNOWN|Permission|snapshot" -Context 0,1
ERWARTET
========
- LastTaskResult = 0
- Snapshot wird minuetlich aktualisiert
- Provider-Log nennt das dedizierte Konto
- BizTalk Platform zeigt read_only_group=<exakte Gruppe>
- 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
QUELLEN
=======
https://learn.microsoft.com/en-us/biztalk/core/managing-biztalk-server-security
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://docs.checkmk.com/latest/en/agent_windows.html
https://docs.checkmk.com/latest/en/localchecks.html
+81 -22
View File
@@ -8,19 +8,24 @@ Alle Services werden als Checkmk Local Checks ausgegeben. Das Format ist:
Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt: zwischen den vier Feldern steht genau ein normales Leerzeichen.
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 acht stabilen Services `UNKNOWN`.
## BizTalk Platform
Zweck:
- Prueft, ob der BizTalk-WMI-Namespace erreichbar ist.
- Zeigt Server, Gruppe, Management-DB und MessageBox-DB, soweit WMI diese Werte liefert.
- Zeigt Server, Gruppe, konfigurierte BizTalk-Read-Only-/Operator-Gruppe, Management-DB und Master-MessageBox-DB aus `MSBTS_GroupSetting`.
Status:
| Status | Bedingung |
| --- | --- |
| `OK` | WMI-Verbindung und beide Plattform-Pflichtklassen erfolgreich. |
| `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, `MSBTS_MessageBoxSetting` oder Programmstart fehlgeschlagen. |
| `OK` | WMI-Verbindung erfolgreich und `MSBTS_GroupSetting` liefert beide vollstaendigen Datenbankziele. |
| `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, ein erforderliches Datenbankziel oder der Programmstart fehlgeschlagen. |
Metriken:
@@ -30,12 +35,13 @@ Metriken:
Zweck:
- Ermittelt Management- und MessageBox-Datenbank ueber BizTalk-WMI.
- Oeffnet mit der Windows-Identitaet des Checkmk-Agentprozesses eine integrierte SQL-Verbindung.
- Ermittelt Management- und Master-MessageBox-Datenbank ueber `MSBTS_GroupSetting`.
- Oeffnet mit der Windows-Identitaet des privilegierten Provider-Tasks eine integrierte SQL-Verbindung.
- Fuehrt die harmlose Abfrage `SELECT 1` aus und schliesst die Verbindung sofort wieder.
- Zeigt lokale Ausfuehrungsidentitaet und erwartete Netzwerkidentitaet an.
Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prueft den fuer das BizTalk-Monitoring relevanten Zugriff des Local Checks.
Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prueft den fuer das
BizTalk-Monitoring relevanten Zugriff des dedizierten Provider-Kontos.
Status:
@@ -53,7 +59,9 @@ Fehlerkategorien:
| `Sql/Connectivity` | DNS, SQL-Dienst, Instanz, TCP-Port oder Firewall nicht erreichbar. |
| `Sql/Timeout` | Verbindungsaufbau oder Testabfrage ueberschreitet das konfigurierte Zeitlimit. |
| `Sql/Provider` | Sonstiger von SQL Server oder SqlClient gemeldeter Providerfehler. |
| `Sql/Configuration` | WMI-Ziele unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. |
| `Sql/Configuration` | WMI-Ziele aus einem Nicht-Berechtigungsgrund unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. |
Ist die Zielermittlung wegen eines eingebetteten SQL-Loginfehlers des BizTalk-WMI-Providers unvollstaendig, wird die Folgediagnose als `Sql/Permission` statt als `Sql/Configuration` ausgegeben.
Metriken:
@@ -69,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:
@@ -78,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:
@@ -87,12 +97,14 @@ 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
Zweck:
- Ueberwacht BizTalk Host Instances auf dem BizTalk-Server.
- Filtert die gruppenweite WMI-Auflistung clientseitig auf den konfigurierten BizTalk-Server; Namen mit Sonderzeichen gelangen nicht in WQL.
- Nicht gestartete Host Instances sind normalerweise betriebsrelevant.
Status:
@@ -114,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 erkannter BizTalk-Anwendungen. |
| `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:
@@ -171,10 +230,10 @@ Metriken:
## Optionale per-Application-Services
Wenn `EmitPerApplicationSuspensionServices=true` gesetzt ist, werden zusaetzliche Services fuer Anwendungen erzeugt, die suspendierte Instanzen haben:
Wenn `EmitPerApplicationSuspensionServices=true` gesetzt ist, werden zusaetzliche Services fuer Anwendungen erzeugt, die suspendierte Instanzen haben und deren Anwendung per WMI tatsaechlich zugeordnet werden konnte:
```text
BizTalk Suspended <ApplicationName>
```
Das ist fuer Anwendungsteams hilfreich, kann aber die Service-Anzahl erhoehen. Fuer den Basisausbau wird empfohlen, diese Option deaktiviert zu lassen.
Nicht zuordenbare Instanzen bleiben im stabilen Aggregat-Service enthalten; ein irrefuehrender per-Application-Service namens `(unknown)` wird nicht erzeugt. Die Option kann die Service-Anzahl erhoehen und bleibt fuer den Basisausbau deaktiviert.
+43 -15
View File
@@ -6,39 +6,67 @@
0 "BizTalk Platform" - Self test OK. Checkmk local check output is valid.
0 "BizTalk SQL Access" biztalk_sql_targets_total=0;;;0 Self test OK. No SQL connection was opened.
0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0 Self test OK.
0 "BizTalk Host Instances" biztalk_host_instances_total=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, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb
0 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=2;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=2, available=2, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=<DOMAIN>\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:OK, MessageBox=SQL01\BizTalkMsgBoxDb:OK
0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|biztalk_suspended_resumable=0;1;;0|biztalk_suspended_nonresumable=0;;1;0 No suspended BizTalk service instances found.
0 "BizTalk Host Instances" biztalk_host_instances_total=4;;;0|biztalk_host_instances_started=4;;;0|biztalk_host_instances_stopped=0;;1;0|biztalk_host_instances_pending=0;1;;0|biztalk_host_instances_unknown=0;;1;0 Host instances total=4, started=4, stopped=0, pending=0, unknown=0
0 "BizTalk Runtime Artifacts" biztalk_applications=8;;;0|biztalk_receive_locations=24;;;0|biztalk_receive_locations_disabled=0;1;;0|biztalk_send_ports=31;;;0|biztalk_send_ports_started=31;;;0|biztalk_send_ports_inactive=0;1;;0|biztalk_send_ports_unknown=0;;1;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=12;;;0|biztalk_orchestrations_inactive=0;1;;0|biztalk_orchestrations_unknown=0;;1;0 Applications=8; receive_locations total=24, disabled=0; send_ports total=31, started=31, inactive=0, unknown=0; orchestrations total=12, started=12, inactive=0, unknown=0
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=NT AUTHORITY\SYSTEM, network_identity=<DOMAIN>\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:FAILED, MessageBox=SQL01\BizTalkMsgBoxDb:FAILED; diagnostics: Sql/Permission [Management SQL01\BizTalkMgmtDb]: SQL Server hat die integrierte Anmeldung abgelehnt. Erwartete Netzwerkidentitaet: <DOMAIN>\BIZTALK01$. Massnahme: <DOMAIN>\BIZTALK01$ in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen. Technik: SqlException Number=18456: Login failed for user
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 auf MSBTS_ServiceInstance wurde im LocalSystem-Kontext verweigert. Massnahme: Agent-Dump pruefen und Computerkonto <DOMAIN>\BIZTALK01$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen. 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)
```
## Fehlendes Deployment
## Begrenzte Detailausgabe
```text
3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter C:\ProgramData\checkmk\agent\local\BizTalkCheckmkPulse. Massnahme: Deployment-Paket und Dateirechte pruefen und danach cmk-agent-ctl.exe dump erneut ausfuehren.
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.
```
## WMI-Schemafehler
Nur der fachlich betroffene Service wird `UNKNOWN`:
```text
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.
```
+108 -151
View File
@@ -1,179 +1,136 @@
# Integration in Checkmk
# Checkmk-Integration und Betriebsuebergabe
## Zielstruktur auf dem BizTalk-Server
## Verantwortungsgrenzen
| Team | Aufgabe |
| --- | --- |
| 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 |
Der Checkmk-Agent bleibt `LocalSystem`. Er greift weder auf BizTalk-WMI noch
auf SQL zu. Der minuetliche Scheduled Task sammelt unter dem privilegierten
Provider-Konto und publiziert einen validierbaren Snapshot.
## Installation
Normales Servicekonto:
```powershell
.\Install-BizTalkCheckmkPulse.ps1 `
-CollectorAccount 'BEW\svc_biztalk_cmk' `
-EnvironmentName ACC
```
Der Installer legt den Local Check hier ab:
```text
%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd
%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe
%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config
```
## Manuelle Installation
1. Release-Paket auf den BizTalk-Server kopieren.
2. Inhalt nach `%ProgramData%\checkmk\agent\local` kopieren.
3. Optional `EnvironmentName` in der `.exe.config` setzen.
4. Test ausfuehren:
```cmd
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd"
```
5. Agent-Ausgabe pruefen:
```cmd
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
```
6. In Checkmk:
- Host oeffnen
- Service Discovery ausfuehren
- gefundene `BizTalk ...` Services aufnehmen
- Changes aktivieren
## Aufgaben in Checkmk
Die Checkmk-Kollegen muessen kein serverseitiges Python-Check-Plugin installieren. Das Plugin liefert bereits fertige Local-Check-Services ueber den Windows Agent. In Checkmk selbst sind diese Schritte erforderlich:
1. BizTalk-Hosts fuer `ACC`, `DEV`, `TST` und `PRD` anlegen oder vorhandene Hosts pruefen.
2. Windows-Agent-Status pruefen: Host muss Agent-Daten liefern.
3. Nach Installation des Local Checks den Agent-Dump pruefen.
4. Service Discovery fuer jeden BizTalk-Host ausfuehren.
5. Gefundene `BizTalk ...` Services aufnehmen.
6. Changes aktivieren.
7. Views oder Dashboards mit Filter `Service starts with: BizTalk` anlegen.
8. Benachrichtigungen und Eskalationen je Umgebung definieren.
Empfohlene Service-Behandlung:
| Service | Empfehlung |
| --- | --- |
| `BizTalk Platform` | `UNKNOWN` immer als Integrationsproblem behandeln. |
| `BizTalk SQL Access` | `UNKNOWN` anhand der Kategorie `Permission`, `Connectivity`, `Timeout`, `Configuration` oder `Provider` bearbeiten. |
| `BizTalk Suspended Instances` | In `PRD` alarmieren; in `ACC`/`TST`/`DEV` nach Teamvereinbarung. |
| `BizTalk Host Instances` | `CRIT` alarmieren. |
| `BizTalk Runtime Artifacts` | Erst beobachten; strenge Alarmierung nur bei klar definiertem Runtime-Sollzustand. |
| `BizTalk Event Log` | Schwellwerte nach Beobachtungsphase feinjustieren. |
## Agent Bakery
Checkmk Managed Services Edition 2.4 enthaelt die kommerziellen Mechanismen fuer Agent Bakery. Fuer einen sauberen Rollout:
1. Deployment-Dateien als Custom Files oder ueber ein spaeteres MKP bereitstellen.
2. Regel nur auf BizTalk-Hosts anwenden, z.B. Host-Tag `application:biztalk`.
3. Gebackenen Windows-Agenten fuer die BizTalk-Hosts installieren.
4. Service Discovery ausfuehren.
Hinweis: Dieses Repository enthaelt bewusst noch kein Bakery-Python-Plugin. Die manuelle bzw. dateibasierte Verteilung ist die risikoarme erste Version. Eine Bakery-Erweiterung ist eine sinnvolle Version-2-Ausbaustufe.
## Berechtigungen fuer WMI und BizTalk-Datenbanken
### Ausfuehrung als LocalSystem
Der Checkmk Windows Agent und der Agent Controller laufen standardmaessig unter `LocalSystem` (`NT AUTHORITY\SYSTEM`). Dadurch werden auch `biztalk_checkmk_pulse.cmd` und `BizTalkCheckmkPulse.exe` in diesem Kontext gestartet.
Das Plugin verbindet sich mit dem lokalen Namespace `\\<eigener-server>\root\MicrosoftBizTalkServer`, setzt keine separaten Anmeldedaten und verwendet kein Remote-WMI. Es fuehrt ausschliesslich WMI-Leseabfragen aus und liest das lokale Windows Application Event Log. Fuer diese lokalen Zugriffe sind normalerweise keine zusaetzlichen DCOM-, Firewall- oder WMI-Namespace-Freigaben erforderlich.
### Besonderheit bei getrenntem SQL Server
Mehrere BizTalk-WMI-Klassen lesen Daten aus der BizTalk Management- oder MessageBox-Datenbank. Bei einem getrennten SQL Server verwendet `LocalSystem` fuer diesen Netzwerkzugriff das Active-Directory-Computerkonto des BizTalk-Servers:
Die EXE liegt zentral hier:
```text
DOMAIN\BIZTALKSERVER$
%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe
```
Lokale Administratorrechte von `LocalSystem` gelten nicht automatisch auf dem entfernten SQL Server. Daher koennen SQL-gestuetzte WMI-Abfragen fehlschlagen, obwohl der lokale WMI-Namespace grundsaetzlich erreichbar ist.
## Checkmk-Agent-Konfiguration
### Verbindlicher Funktionstest
Der Wrapper darf synchron laufen, da er nur einen kleinen lokalen Snapshot
liest. Eine alte Async-Regel mit `cache_age: 300` aus der direkten
WMI-Architektur soll entfernt werden; andernfalls addiert sie eine unnoetige
Verzoegerung zum minuetlichen Provider.
Der direkte Start des Wrappers in einer administrativen Eingabeaufforderung ist nur ein Vorabtest, weil er mit dem angemeldeten Benutzer laeuft. Fuer die Berechtigungspruefung ist die Ausgabe unter dem echten Checkmk-Kontext massgeblich:
Aktive Agentkonfiguration pruefen:
```powershell
& "C:\Program Files (x86)\checkmk\service\check_mk_agent.exe" showconfig local
```
Verbindlicher Agent-Dump:
```powershell
& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump |
Select-String -Pattern "BizTalk|Access denied|Unauthorized|UNKNOWN" -Context 0,1
Select-String -Pattern "BizTalk|UNKNOWN|snapshot|Permission" -Context 0,1
```
Die Ausgabe muss die sechs stabilen `BizTalk ...` Services mit plausiblen Daten enthalten. `Access denied`, `UnauthorizedAccessException`, `Sql/Permission` und ein berechtigungsbedingtes `UNKNOWN` weisen auf eine fehlende Rollenzuordnung hin.
## Service Discovery
Die EXE klassifiziert WMI- und SQL-Probleme und schreibt zu jeder Diagnose `Massnahme:` und `Technik:`. Eine fehlgeschlagene erforderliche WMI-Abfrage erzeugt beim betroffenen Service immer `UNKNOWN`; fehlende Daten werden nicht als Nullbestand und damit nicht als `OK` ausgegeben.
1. Provider dreimal erfolgreich laufen lassen.
2. `LastTaskResult=0`, frischen Snapshot und Log pruefen.
3. Agent-Dump im `LocalSystem`-Kontext pruefen.
4. Service Discovery fuer den BizTalk-Host ausfuehren.
5. acht stabile Services aufnehmen und den alten
`BizTalk Runtime Artifacts`-Service entfernen.
6. Changes aktivieren.
7. Views, Servicegruppen und Benachrichtigungen einrichten.
Fuer den produktiven Betrieb wird empfohlen, den Local Check alle 300 Sekunden asynchron auszufuehren. Dadurch erzeugen fehlende SQL-Rechte nicht bei jedem Agent-Abruf neue fehlgeschlagene Logins. In `%ProgramData%\checkmk\agent\check_mk.user.yml`:
Host-Tags:
```yaml
local:
enabled: yes
execution:
- pattern: $CUSTOM_LOCAL_PATH$\biztalk_checkmk_pulse.cmd
async: yes
run: yes
cache_age: 300
```text
env:ACC|DEV|TST|PRD
app:biztalk
```
Alternativ koennen die Checkmk-Kollegen in der Agent Bakery die Regeln `Set execution mode for plug-ins and local checks` und `Set cache age for plug-ins and local checks` verwenden. Der Cache reduziert Last und SQL-Fehlerlogs; ein Zustandswechsel wird dadurch um maximal die konfigurierte Cache-Zeit verzoegert.
### Least-Privilege-Vorgehen
1. Das verwendete Computerkonto feststellen, normalerweise `DOMAIN\BIZTALKSERVER$`.
2. Die bei der BizTalk-Konfiguration hinterlegte Operator-Gruppe ermitteln. Sie kann anders als `BizTalk Server Operators` benannt sein.
3. Das Computerkonto in diese Operator-Gruppe aufnehmen.
4. Kerberos-Tickets des Systemkontos erneuern oder den Server in einem Wartungsfenster neu starten.
5. `cmk-agent-ctl.exe dump` erneut ausfuehren.
6. Nur bei weiterhin abgelehnten, konkret identifizierten WMI-Klassen mit BizTalk- und SQL-Administration klaeren, ob `BizTalk Server Administrators` erforderlich ist.
Die BizTalk-Operator-Rolle ist fuer grundlegende Administration und Monitoring vorgesehen. Sie kann Zustands- und Message-Flow-Informationen lesen, darf aber keine Konfiguration oder Nachrichteninhalte einsehen. Das entspricht dem lesenden Funktionsumfang dieses Plugins. Keine direkten SQL-Rollen in den BizTalk-Datenbanken hinzufuegen; die von BizTalk konfigurierte Windows-Gruppe soll die Berechtigungen vermitteln.
Entscheidungsmatrix:
| Beobachtung | Bewertung und Massnahme |
| --- | --- |
| Alle sechs stabilen Services liefern plausible Werte | Keine Aenderung erforderlich. |
| `root\MicrosoftBizTalkServer` ist nicht erreichbar | BizTalk-WMI-Provider, WMI-Dienst, Namespace und dessen ACL gezielt pruefen. |
| `BizTalk SQL Access` meldet `Sql/Permission` | Maschinenkonto in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und Agent-Dump wiederholen. |
| `BizTalk SQL Access` meldet `Sql/Connectivity` | SQL-Server-/Instanzname, DNS, Dienst, TCP-Port und Firewall pruefen. |
| `BizTalk SQL Access` meldet `Sql/Timeout` | SQL-/Netzwerkauslastung untersuchen; Timeout nur nach Ursachenanalyse anpassen. |
| Verbindung funktioniert, einzelne SQL-gestuetzte Klassen melden Zugriffsfehler | `DOMAIN\BIZTALKSERVER$` zunaechst in die konfigurierte BizTalk-Operator-Gruppe aufnehmen. |
| Nur `BizTalk Event Log` ist `UNKNOWN` | Lokalen Zugriff auf das Application Event Log pruefen. |
| Fehler bleibt nach Operator-Zuweisung bestehen | Exakte WMI-Klasse anhand der Service-Ausgabe bestimmen und deren BizTalk-/SQL-Rollenanforderung pruefen. |
### Sicherheitsauswirkung und gMSA-Alternative
Die Mitgliedschaft des Computerkontos in der Operator-Gruppe gilt fuer alle Dienste, die auf dem BizTalk-Server als `LocalSystem` laufen und mit dem Computerkonto auf Netzwerkressourcen zugreifen. Diese Auswirkung muss mit Security und BizTalk-Betrieb abgestimmt werden.
Falls das Computerkonto keine BizTalk-Rechte erhalten darf, kann ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten die Checkmk-Ausgabe erzeugen und als Spool-Datei bereitstellen. Das waere eine eigene Betriebsvariante und ist in der aktuellen Local-Check-Version noch nicht implementiert. Den gesamten Checkmk-Agent-Dienst nur fuer dieses Plugin auf ein anderes Konto umzustellen ist nicht empfohlen, da die Identitaetsaenderung alle Agent-Sektionen und Local Checks betrifft.
Quellen:
- Checkmk Windows Agent: https://docs.checkmk.com/latest/en/agent_windows.html
- Checkmk Local Checks: https://docs.checkmk.com/latest/en/localchecks.html
- BizTalk Minimum Security User Rights: https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights
- BizTalk Access Control and Data Security: https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security
- Microsoft LocalSystem und Computerkonten: https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer
- Microsoft WMI Namespace Security: https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces
## Empfohlene Host-Struktur
Host-Tags oder Ordner:
- `env:ACC`
- `env:DEV`
- `env:TST`
- `env:PRD`
- `app:biztalk`
Service-Filter fuer Views:
Servicefilter:
```text
Service starts with: BizTalk
```
Dashboard-Kacheln:
## Bakery-/Softwareverteilung
- Host/Service state fuer BizTalk-Host
- Service state fuer `BizTalk Suspended Instances`
- Service state fuer `BizTalk SQL Access`
- Graph `biztalk_suspended_total`
- Graph `biztalk_host_instances_stopped`
- Graph `biztalk_eventlog_errors`
- MSSQL-Services des SQL-Servers daneben
Die komplette Installation umfasst mehr als eine Dateiablage:
- Programmdateien unter `%ProgramFiles%`,
- Runtimeverzeichnisse und ACLs,
- Scheduled Task mit Providerkonto,
- Local-Check-Wrapper.
Der Wrapper allein kann per Agent Bakery verteilt werden, ersetzt aber nicht
die lokale Providerinstallation und die Kontofreigabe. Fuer den ersten Rollout
ist das signierte/abgenommene Deployment-Paket mit administrativer
Installationsautomation die klarere Variante.
## Healthchecks
```powershell
Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider'
Get-Item "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot"
Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 50
& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
```
Soll:
- Task laeuft jede Minute und endet mit `0`.
- Snapshot ist kleiner als `SnapshotMaxBytes` und juenger als 180 Sekunden.
- 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 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.
Optional kann Windows Task Scheduler zusaetzlich durch vorhandene Checkmk
Task-/Event-Log-Regeln ueberwacht werden. Das ist eine Ergaenzung, kein Ersatz
fuer die eingebaute Stale-Pruefung.
## Rollout
Empfohlene Reihenfolge:
1. ACC: Berechtigung, Task, Snapshot, Stale-Test und Discovery abnehmen.
2. DEV/TST: gleiche Automatisierung und umgebungsspezifische Config.
3. PRD: Change, Wartungsfenster, Healthcheck und fachliche Plausibilitaet.
Ausfuehrliche Architektur, ACL, Fehlerbilder und Abnahmekriterien:
[Dokumentation.md](../Dokumentation.md).
+1 -2
View File
@@ -7,6 +7,5 @@ if errorlevel 1 (
exit /b 1
)
msbuild BizTalkCheckmkPulse.sln /p:Configuration=Release /p:Platform="Any CPU" /m /v:minimal
msbuild BizTalkCheckmkPulse.sln /p:Configuration=Release /p:Platform="Any CPU" /m:1 /v:minimal
exit /b %ERRORLEVEL%
+3 -2
View File
@@ -6,15 +6,16 @@ if errorlevel 1 exit /b 1
set "ROOT=%~dp0.."
set "OUT=%ROOT%\artifacts\BizTalkCheckmkPulse-deploy"
set "APP=%OUT%\BizTalkCheckmkPulse"
set "APP=%OUT%\application"
if exist "%OUT%" rmdir /s /q "%OUT%"
mkdir "%APP%"
copy "%ROOT%\deployment\checkmk\biztalk_checkmk_pulse.cmd" "%OUT%\" >nul
copy "%ROOT%\deployment\Install-BizTalkCheckmkPulse.ps1" "%OUT%\" >nul
copy "%ROOT%\deployment\Uninstall-BizTalkCheckmkPulse.ps1" "%OUT%\" >nul
copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe" "%APP%\" >nul
copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe.config" "%APP%\" >nul
echo Deployment package written to %OUT%
exit /b 0
+8
View File
@@ -0,0 +1,8 @@
@echo off
setlocal
call "%~dp0build-release.cmd"
if errorlevel 1 exit /b 1
"%~dp0..\tests\BizTalkCheckmkPulse.Tests\bin\Release\BizTalkCheckmkPulse.Tests.exe"
exit /b %ERRORLEVEL%
+19 -4
View File
@@ -6,16 +6,31 @@
<add key="ServicePrefix" value="BizTalk" />
<add key="EnvironmentName" value="" />
<!-- Provider/Consumer-Datei. Der Scheduled Task schreibt, LocalSystem liest. -->
<add key="SnapshotPath" value="%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" />
<!-- Bei minuetlicher Provider-Ausfuehrung nach drei Minuten als UNKNOWN bewerten. -->
<add key="SnapshotMaxAgeSeconds" value="180" />
<add key="SnapshotMaxBytes" value="1048576" />
<add key="LogDirectory" value="%ProgramData%\BizTalkCheckmkPulse\logs" />
<add key="LogRetentionDays" value="30" />
<add key="QueryTimeoutSeconds" value="25" />
<!-- Testet integrierte Windows-Anmeldung an den per WMI ermittelten BizTalk-Datenbanken. -->
<!-- Testet die Anmeldung des privilegierten Provider-Kontos an den ermittelten BizTalk-Datenbanken. -->
<add key="ProbeSqlConnectivity" value="true" />
<add key="SqlConnectionTimeoutSeconds" value="5" />
<add key="WarnResumableThreshold" value="1" />
<add key="CritNonResumableThreshold" value="1" />
<add key="MaxSummaryItems" value="12" />
<add key="CritRoutingFailureThreshold" value="1" />
<add key="MaxSummaryItems" value="5" />
<add key="MaxDetailCharacters" value="1600" />
<!-- Gestoppte Artefakte koennen in BizTalk-Landschaften fachlich beabsichtigt sein. -->
<add key="AlertOnArtifactRuntimeIssues" value="false" />
<!-- Unerwartet deaktivierte Receive Locations und inaktive Send Ports sind standardmaessig CRIT. -->
<add key="AlertOnArtifactRuntimeIssues" value="true" />
<!-- Exakte Namen, Pipe-getrennt: Name oder Anwendung\Name. Keine Wildcards. -->
<add key="ExpectedDisabledReceiveLocations" value="" />
<add key="ExpectedInactiveSendPorts" value="" />
<!-- Gestoppte/bound Orchestrations koennen fachlich beabsichtigt sein. -->
<add key="AlertOnInactiveOrchestrations" value="false" />
<add key="EmitPerApplicationSuspensionServices" value="false" />
<add key="ProbeEventLog" value="true" />
@@ -40,12 +40,15 @@
<Reference Include="System.Management" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="CheckmkLocalFormatter.cs" />
<Compile Include="EventLogProbe.cs" />
<Compile Include="FileLogger.cs" />
<Compile Include="MonitoringOptions.cs" />
<Compile Include="Models.cs" />
<Compile Include="Program.cs" />
<Compile Include="SqlConnectivityProbe.cs" />
<Compile Include="SnapshotStore.cs" />
<Compile Include="WmiBizTalkProbe.cs" />
<Compile Include="WmiHelpers.cs" />
</ItemGroup>
+271 -71
View File
@@ -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;
/// <summary>
@@ -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)
@@ -62,6 +67,11 @@ namespace BizTalkCheckmkPulse
yield return BuildLine(CheckState.Ok, _options.ServiceName("Platform"), "-", "Self test OK. Checkmk local check output is valid.");
yield return BuildLine(CheckState.Ok, _options.ServiceName("SQL Access"), "biztalk_sql_targets_total=0;;;0", "Self test OK. No SQL connection was opened.");
yield return BuildLine(CheckState.Ok, _options.ServiceName("Suspended Instances"), "biztalk_suspended_total=0;;;0", "Self test OK.");
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("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.");
}
/// <summary>
@@ -76,10 +86,30 @@ 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);
}
/// <summary>
/// Liefert stabile UNKNOWN-Services, wenn der Consumer keinen gueltigen Provider-Snapshot lesen kann.
/// </summary>
public IEnumerable<string> FormatSnapshotFailure(string reason)
{
var detail = "Privilegierter BizTalk-Datensnapshot nicht verfuegbar: "
+ SanitizeDetail(reason)
+ " Massnahme: Scheduled Task 'BizTalk Checkmk Pulse Provider', Provider-Log, Snapshot-Alter und ACL pruefen.";
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Platform"), "-", detail);
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("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);
}
/// <summary>
/// Formatiert Erreichbarkeit und Basisdaten des BizTalk-WMI-Providers.
/// </summary>
@@ -93,10 +123,12 @@ namespace BizTalkCheckmkPulse
detail.Append(available ? "BizTalk WMI and platform data reachable" : "BizTalk WMI or required platform data not readable");
detail.Append(", server=").Append(EmptyAsUnknown(result.Platform.ServerName));
AppendOptional(detail, "group", result.Platform.GroupName);
AppendOptional(detail, "read_only_group", result.Platform.ReadOnlyUserGroup);
AppendOptional(detail, "operator_group", result.Platform.OperatorGroup);
AppendOptional(detail, "mgmt_db", JoinDb(result.Platform.ManagementDbServer, result.Platform.ManagementDbName));
AppendOptional(detail, "msgbox_db", JoinDb(result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName));
var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting", "MSBTS_MessageBoxSetting");
var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting");
return BuildLine(state, _options.ServiceName("Platform"), "-", AppendDiagnostics(detail.ToString(), diagnostics));
}
@@ -161,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);
}
@@ -224,65 +259,147 @@ namespace BizTalkCheckmkPulse
return BuildLine(state, _options.ServiceName("Host Instances"), metrics, detail.ToString());
}
/// <summary>
/// Formatiert aggregierte Receive-Location-, Send-Port- und Orchestration-Zustaende.
/// </summary>
/// <param name="result">Probe-Ergebnis.</param>
/// <returns>Checkmk-Zeile fuer Runtime Artifacts.</returns>
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
: _options.AlertOnArtifactRuntimeIssues && (receiveLocationsDisabled > 0 || sendPortsInactive > 0 || orchestrationsInactive > 0)
: 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
: 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());
}
/// <summary>
@@ -303,7 +420,7 @@ namespace BizTalkCheckmkPulse
CheckState.Unknown,
_options.ServiceName("Event Log"),
"-",
"Application Event Log konnte nicht gelesen werden: " + result.EventLog.Failure + " Massnahme: lokalen Event-Log-Zugriff des Checkmk-Agentkontos und den Windows Event Log Dienst pruefen.");
"Application Event Log konnte nicht gelesen werden: " + result.EventLog.Failure + " Massnahme: lokalen Event-Log-Zugriff des Provider-Kontos und den Windows Event Log Dienst pruefen.");
}
var state = _options.EventLogCritThreshold > 0 && result.EventLog.Errors >= _options.EventLogCritThreshold
@@ -334,20 +451,31 @@ namespace BizTalkCheckmkPulse
/// <returns>Checkmk-Zeilen je betroffener BizTalk-Anwendung.</returns>
private IEnumerable<string> FormatApplicationSuspensions(ProbeResult result)
{
foreach (var group in result.SuspendedInstances.GroupBy(x => x.ApplicationName).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
foreach (var group in result.SuspendedInstances
.Where(x => !string.Equals(x.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase))
.GroupBy(x => x.ApplicationName)
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
{
var resumable = group.Count(x => x.Kind == SuspendedKind.Resumable);
var nonresumable = group.Count(x => x.Kind == SuspendedKind.NonResumable);
var state = DetermineSuspensionState(resumable, nonresumable);
var 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);
}
}
@@ -357,8 +485,14 @@ namespace BizTalkCheckmkPulse
/// <param name="resumable">Anzahl fortsetzbarer Instanzen.</param>
/// <param name="nonresumable">Anzahl nicht fortsetzbarer Instanzen.</param>
/// <returns>Zustand gemaess konfigurierten Schwellwerten.</returns>
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;
@@ -380,24 +514,32 @@ namespace BizTalkCheckmkPulse
/// <param name="resumable">Anzahl fortsetzbarer Instanzen.</param>
/// <param name="nonresumable">Anzahl nicht fortsetzbarer Instanzen.</param>
/// <returns>Kompakter Detailtext.</returns>
private string BuildSuspensionDetail(IEnumerable<SuspendedInstance> instances, int total, int resumable, int nonresumable)
private string BuildSuspensionDetail(
IEnumerable<SuspendedInstance> 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);
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();
}
@@ -409,13 +551,20 @@ namespace BizTalkCheckmkPulse
/// <param name="metrics">Performance-Daten oder Bindestrich.</param>
/// <param name="detail">Menschenlesbare Service-Ausgabe.</param>
/// <returns>Vollstaendige Local-Check-Zeile.</returns>
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;
}
/// <summary>
@@ -465,6 +614,57 @@ namespace BizTalkCheckmkPulse
}
}
private void AppendLimitedList(StringBuilder builder, string label, IEnumerable<string> 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);
}
}
/// <summary>
/// Haengt einen optionalen benannten Wert an einen Detailtext an.
/// </summary>
+150
View File
@@ -0,0 +1,150 @@
using System;
using System.Globalization;
using System.IO;
using System.Security.Principal;
using System.Text;
using System.Threading;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Kleine, ausfallsichere Tagesdatei-Protokollierung ohne externe Abhaengigkeiten.
/// Logging-Fehler duerfen weder Provider noch Checkmk-Ausgabe blockieren.
/// </summary>
internal sealed class FileLogger
{
private readonly string _directory;
private readonly int _retentionDays;
private readonly string _component;
public FileLogger(string directory, int retentionDays, string component)
{
_directory = directory;
_retentionDays = retentionDays;
_component = string.IsNullOrWhiteSpace(component) ? "application" : component;
}
public void Info(string message)
{
Write("INFO", message, null);
}
public void Warning(string message)
{
Write("WARN", message, null);
}
public void Error(string message, Exception exception)
{
Write("ERROR", message, exception);
}
public void Prune()
{
try
{
if (!Directory.Exists(_directory))
{
return;
}
var cutoff = DateTime.UtcNow.Date.AddDays(-_retentionDays);
foreach (var file in Directory.GetFiles(_directory, "biztalk-checkmk-pulse-*.log"))
{
try
{
if (File.GetLastWriteTimeUtc(file) < cutoff)
{
File.Delete(file);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
private void Write(string level, string message, Exception exception)
{
try
{
Directory.CreateDirectory(_directory);
var path = Path.Combine(
_directory,
"biztalk-checkmk-pulse-" + DateTime.UtcNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture) + ".log");
var line = new StringBuilder()
.Append(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture))
.Append(" level=").Append(level)
.Append(" component=").Append(_component)
.Append(" pid=").Append(System.Diagnostics.Process.GetCurrentProcess().Id)
.Append(" identity=").Append(CurrentIdentity())
.Append(" message=").Append(SingleLine(message));
if (exception != null)
{
line.Append(" exception=").Append(SingleLine(exception.ToString()));
}
line.Append(Environment.NewLine);
var bytes = new UTF8Encoding(false).GetBytes(line.ToString());
for (var attempt = 0; attempt < 3; attempt++)
{
try
{
using (var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read))
{
stream.Write(bytes, 0, bytes.Length);
stream.Flush();
}
return;
}
catch (IOException)
{
if (attempt == 2)
{
return;
}
Thread.Sleep(25 * (attempt + 1));
}
}
}
catch (Exception)
{
// Logging bleibt bewusst best effort.
}
}
private static string CurrentIdentity()
{
try
{
var identity = WindowsIdentity.GetCurrent();
return identity == null ? "(unknown)" : SingleLine(identity.Name);
}
catch (Exception)
{
return "(unknown)";
}
}
private static string SingleLine(string value)
{
return (value ?? string.Empty)
.Replace("\r", "\\r")
.Replace("\n", "\\n")
.Replace("\t", " ");
}
}
}
+36 -1
View File
@@ -43,6 +43,7 @@ namespace BizTalkCheckmkPulse
Connectivity,
Timeout,
Configuration,
Schema,
Provider,
Unexpected
}
@@ -95,6 +96,8 @@ namespace BizTalkCheckmkPulse
Platform = new PlatformState();
HostInstances = new List<HostInstanceState>();
SuspendedInstances = new List<SuspendedInstance>();
ReceiveLocations = new List<ReceiveLocationState>();
SendPorts = new List<SendPortState>();
Applications = new List<ApplicationRuntimeState>();
SqlTargets = new List<SqlAccessState>();
EventLog = new EventLogState();
@@ -104,6 +107,8 @@ namespace BizTalkCheckmkPulse
public PlatformState Platform { get; private set; }
public List<HostInstanceState> HostInstances { get; private set; }
public List<SuspendedInstance> SuspendedInstances { get; private set; }
public List<ReceiveLocationState> ReceiveLocations { get; private set; }
public List<SendPortState> SendPorts { get; private set; }
public List<ApplicationRuntimeState> Applications { get; private set; }
public List<SqlAccessState> SqlTargets { get; private set; }
public string ExecutionIdentity { get; set; }
@@ -120,9 +125,13 @@ 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; }
public string ReadOnlyUserGroup { get; set; }
public string ManagementDbServer { get; set; }
public string ManagementDbName { get; set; }
public string MessageBoxDbServer { get; set; }
@@ -166,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; }
}
}
/// <summary>
/// Konfigurations- und Laufzeitzustand einer Receive Location.
/// </summary>
internal sealed class ReceiveLocationState
{
public string Name { get; set; }
public string ApplicationName { get; set; }
public bool? IsDisabled { get; set; }
}
/// <summary>
/// Laufzeitzustand eines Send Ports.
/// </summary>
internal sealed class SendPortState
{
public string Name { get; set; }
public string ApplicationName { get; set; }
public int Status { get; set; }
}
/// <summary>
+106 -2
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
namespace BizTalkCheckmkPulse
@@ -19,14 +20,25 @@ 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<string> ExpectedDisabledReceiveLocations { get; set; }
public IReadOnlyList<string> ExpectedInactiveSendPorts { get; set; }
public bool EmitPerApplicationSuspensionServices { get; set; }
public bool ProbeEventLog { get; set; }
public int EventLogLookbackMinutes { get; set; }
public int EventLogWarnThreshold { get; set; }
public int EventLogCritThreshold { get; set; }
public IReadOnlyList<string> EventLogSources { get; set; }
public string SnapshotPath { get; set; }
public int SnapshotMaxAgeSeconds { get; set; }
public int SnapshotMaxBytes { get; set; }
public string LogDirectory { get; set; }
public int LogRetentionDays { get; set; }
public bool Collect { get; set; }
public bool SelfTest { get; set; }
/// <summary>
@@ -42,14 +54,25 @@ 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;
EventLogWarnThreshold = 1;
EventLogCritThreshold = 10;
EventLogSources = new[] { "BizTalk Server", "XLANG/s", "ENTSSO", "BizTalk Server Application", "BizTalk Server EDI" };
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
SnapshotPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "biztalk-checkmk-pulse.snapshot");
SnapshotMaxAgeSeconds = 180;
SnapshotMaxBytes = 1048576;
LogDirectory = Path.Combine(commonData, "BizTalkCheckmkPulse", "logs");
LogRetentionDays = 30;
}
/// <summary>
@@ -83,16 +106,30 @@ 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);
options.EventLogWarnThreshold = ReadInt(settings, "EventLogWarnThreshold", options.EventLogWarnThreshold, 0, 1000000);
options.EventLogCritThreshold = ReadInt(settings, "EventLogCritThreshold", options.EventLogCritThreshold, 0, 1000000);
options.EventLogSources = ReadSources(ReadString(settings, "EventLogSources", string.Join("|", options.EventLogSources)));
options.SnapshotPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "SnapshotPath", options.SnapshotPath));
options.SnapshotMaxAgeSeconds = ReadInt(settings, "SnapshotMaxAgeSeconds", options.SnapshotMaxAgeSeconds, 60, 86400);
options.SnapshotMaxBytes = ReadInt(settings, "SnapshotMaxBytes", options.SnapshotMaxBytes, 4096, 16777216);
options.LogDirectory = Environment.ExpandEnvironmentVariables(ReadString(settings, "LogDirectory", options.LogDirectory));
options.LogRetentionDays = ReadInt(settings, "LogRetentionDays", options.LogRetentionDays, 1, 365);
ApplyArguments(options, args ?? new string[0]);
if (!options.SelfTest)
{
Validate(options);
}
return options;
}
@@ -110,6 +147,14 @@ namespace BizTalkCheckmkPulse
{
options.SelfTest = true;
}
else if (EqualsAny(arg, "--collect", "/collect"))
{
options.Collect = true;
}
else if (EqualsAny(arg, "--consume", "/consume"))
{
options.Collect = false;
}
else if (EqualsAny(arg, "--server", "/server") && i + 1 < args.Length)
{
options.Server = args[++i];
@@ -133,6 +178,22 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Verhindert unsichere oder mehrdeutige Laufzeitpfade.
/// </summary>
private static void Validate(MonitoringOptions options)
{
if (string.IsNullOrWhiteSpace(options.SnapshotPath) || !Path.IsPathRooted(options.SnapshotPath))
{
throw new ConfigurationErrorsException("SnapshotPath must be an absolute path.");
}
if (string.IsNullOrWhiteSpace(options.LogDirectory) || !Path.IsPathRooted(options.LogDirectory))
{
throw new ConfigurationErrorsException("LogDirectory must be an absolute path.");
}
}
/// <summary>
/// Liest einen getrimmten Konfigurationswert.
/// </summary>
@@ -199,6 +260,49 @@ namespace BizTalkCheckmkPulse
.ToArray();
}
/// <summary>
/// Liest eine Pipe-getrennte, exakte Allowlist. Wildcards werden bewusst nicht interpretiert.
/// </summary>
private static IReadOnlyList<string> 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<string> 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)));
}
/// <summary>
/// Prueft einen Wert ohne Beachtung der Gross-/Kleinschreibung gegen Kandidaten.
/// </summary>
+160 -25
View File
@@ -1,19 +1,22 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Text;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Einstiegspunkt des Checkmk Local Checks.
/// Einstiegspunkt fuer privilegierten Provider und unprivilegierten Checkmk-Consumer.
/// </summary>
internal static class Program
{
/// <summary>
/// Laedt die Konfiguration, fuehrt die Probes aus und schreibt Checkmk-Zeilen nach STDOUT.
/// Laedt die Konfiguration und fuehrt je nach Modus Provider oder Snapshot-Consumer aus.
/// </summary>
/// <param name="args">Kommandozeilenargumente wie <c>--self-test</c> oder <c>--environment</c>.</param>
/// <returns>Immer 0, damit Diagnosefehler als UNKNOWN-Service statt als kaputte Agent-Sektion erscheinen.</returns>
/// <returns>Consumer immer 0; Provider 0 bei Erfolg und ungleich 0 bei einem Laufzeitfehler.</returns>
private static int Main(string[] args)
{
MonitoringOptions options = null;
@@ -34,38 +37,170 @@ namespace BizTalkCheckmkPulse
return 0;
}
var result = new ProbeResult();
var wmiProbe = new WmiBizTalkProbe(options);
wmiProbe.Query(result);
// Der SQL-Test verwendet bewusst dieselbe Windows-Identitaet wie der Checkmk-Agent.
var sqlProbe = new SqlConnectivityProbe(options);
sqlProbe.Query(result);
if (options.ProbeEventLog)
{
var eventLogProbe = new EventLogProbe(options);
eventLogProbe.Query(result);
}
foreach (var line in formatter.Format(result))
{
Console.WriteLine(line);
}
return 0;
return options.Collect
? RunProvider(options, formatter)
: RunConsumer(options, formatter);
}
catch (Exception ex)
{
var fallbackOptions = options ?? new MonitoringOptions();
var formatter = new CheckmkLocalFormatter(fallbackOptions);
foreach (var line in formatter.FormatFatal("BizTalk Checkmk Pulse failed: " + ex.GetType().Name + ": " + ex.Message))
foreach (var line in formatter.FormatSnapshotFailure(
"Programmstart fehlgeschlagen: " + ex.GetType().Name + ": " + ex.Message))
{
Console.WriteLine(line);
}
return options != null && options.Collect ? 1 : 0;
}
}
private static int RunProvider(MonitoringOptions options, CheckmkLocalFormatter formatter)
{
var logger = new FileLogger(options.LogDirectory, options.LogRetentionDays, "provider");
logger.Prune();
var stopwatch = Stopwatch.StartNew();
var lockPath = options.SnapshotPath + ".provider.lock";
try
{
EnsureProviderIdentity();
Directory.CreateDirectory(Path.GetDirectoryName(options.SnapshotPath));
using (new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
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());
stopwatch.Stop();
logger.Info(
"Collection completed. lines="
+ 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;
}
}
catch (IOException ex)
{
logger.Warning("Collection skipped or snapshot I/O failed: " + ex.Message);
return 2;
}
catch (Exception ex)
{
stopwatch.Stop();
logger.Error("Collection failed after " + stopwatch.ElapsedMilliseconds + " ms.", ex);
TryWriteFatalSnapshot(options, formatter, ex, logger);
return 1;
}
}
private static void EnsureProviderIdentity()
{
using (var identity = WindowsIdentity.GetCurrent())
{
if (identity != null
&& identity.User != null
&& identity.User.IsWellKnown(WellKnownSidType.LocalSystemSid))
{
throw new InvalidOperationException(
"Provider mode must not run as LocalSystem. Configure the dedicated Scheduled Task account.");
}
}
}
private static int RunConsumer(MonitoringOptions options, CheckmkLocalFormatter formatter)
{
var snapshot = new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
.Read(DateTime.UtcNow, TimeSpan.FromSeconds(options.SnapshotMaxAgeSeconds));
if (snapshot.IsSuccess)
{
foreach (var line in snapshot.Lines)
{
Console.WriteLine(line);
}
return 0;
}
new FileLogger(options.LogDirectory, options.LogRetentionDays, "consumer")
.Warning("Snapshot rejected. reason=" + snapshot.Error + " path=" + options.SnapshotPath);
foreach (var line in formatter.FormatSnapshotFailure(snapshot.Error))
{
Console.WriteLine(line);
}
return 0;
}
private static ProbeResult Collect(MonitoringOptions options)
{
var result = new ProbeResult();
new WmiBizTalkProbe(options).Query(result);
new SqlConnectivityProbe(options).Query(result);
if (options.ProbeEventLog)
{
new EventLogProbe(options).Query(result);
}
return result;
}
private static void TryWriteFatalSnapshot(
MonitoringOptions options,
CheckmkLocalFormatter formatter,
Exception exception,
FileLogger logger)
{
try
{
var lines = formatter
.FormatFatal("Privilegierter Datenprovider fehlgeschlagen: " + exception.GetType().Name + ": " + exception.Message)
.ToArray();
new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
.Write(lines, DateTime.UtcNow, CurrentIdentity());
logger.Warning("A current UNKNOWN snapshot was written after the provider failure.");
}
catch (Exception snapshotException)
{
logger.Error("Fatal UNKNOWN snapshot could not be written.", snapshotException);
}
}
private static string CurrentIdentity()
{
try
{
var identity = WindowsIdentity.GetCurrent();
return identity == null ? "(unknown)" : identity.Name;
}
catch (Exception)
{
return "(unknown)";
}
}
}
}
@@ -0,0 +1,10 @@
using System.Runtime.CompilerServices;
using System.Reflection;
[assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")]
[assembly: AssemblyTitle("BizTalk Checkmk Pulse")]
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
[assembly: AssemblyCompany("BEW")]
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
[assembly: AssemblyVersion("2.1.0.0")]
[assembly: AssemblyFileVersion("2.1.0.0")]
+353
View File
@@ -0,0 +1,353 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Diagnostics;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Schreibt und liest einen atomaren, integritaetsgeschuetzten Checkmk-Snapshot.
/// </summary>
internal sealed class SnapshotStore
{
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;
public SnapshotStore(string path, int maxBytes)
{
_path = path;
_maxBytes = maxBytes;
}
public void Write(IReadOnlyCollection<string> lines, DateTime generatedUtc, string identity)
{
ValidatePayload(lines);
var payload = string.Join("\n", lines) + "\n";
var payloadBytes = StrictUtf8.GetBytes(payload);
var content = BuildHeader(generatedUtc, identity, lines.Count, Hash(payloadBytes)) + payload;
var contentBytes = StrictUtf8.GetBytes(content);
if (contentBytes.Length > _maxBytes)
{
throw new InvalidDataException("Snapshot exceeds configured SnapshotMaxBytes.");
}
var directory = Path.GetDirectoryName(_path);
if (string.IsNullOrWhiteSpace(directory))
{
throw new InvalidOperationException("Snapshot path has no parent directory.");
}
Directory.CreateDirectory(directory);
var temporaryPath = Path.Combine(
directory,
Path.GetFileName(_path) + "." + Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture) + "." + Guid.NewGuid().ToString("N") + ".tmp");
try
{
using (var stream = new FileStream(
temporaryPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
4096,
FileOptions.WriteThrough))
{
stream.Write(contentBytes, 0, contentBytes.Length);
stream.Flush(true);
}
if (File.Exists(_path))
{
File.Replace(temporaryPath, _path, null, true);
}
else
{
File.Move(temporaryPath, _path);
}
}
finally
{
try
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
catch (IOException)
{
}
}
}
public SnapshotReadResult Read(DateTime utcNow, TimeSpan maximumAge)
{
try
{
var bytes = ReadBytesWithRetry();
var text = StrictUtf8.GetString(bytes).Replace("\r\n", "\n");
var separator = text.IndexOf("\n\n", StringComparison.Ordinal);
if (separator < 0)
{
return SnapshotReadResult.Failed("Snapshot header separator is missing.");
}
var headerLines = text.Substring(0, separator).Split('\n');
if (headerLines.Length != 6 || !string.Equals(headerLines[0], Magic, StringComparison.Ordinal))
{
return SnapshotReadResult.Failed("Snapshot format or version is invalid.");
}
DateTime generatedUtc;
int expectedLineCount;
if (!TryReadDate(headerLines[1], "generatedUtc=", out generatedUtc)
|| !TryReadInt(headerLines[4], "payloadLines=", out expectedLineCount))
{
return SnapshotReadResult.Failed("Snapshot metadata is invalid.");
}
var expectedMachine = DecodeHeader(headerLines[2], "machineBase64=");
DecodeHeader(headerLines[3], "identityBase64=");
var expectedHash = ReadHeaderValue(headerLines[5], "payloadSha256=");
if (!string.Equals(expectedMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
{
return SnapshotReadResult.Failed("Snapshot was created for a different machine.");
}
var payload = text.Substring(separator + 2);
if (!payload.EndsWith("\n", StringComparison.Ordinal)
|| payload.IndexOf("\n\n", StringComparison.Ordinal) >= 0)
{
return SnapshotReadResult.Failed("Snapshot payload framing is invalid.");
}
var payloadBytes = StrictUtf8.GetBytes(payload);
if (!FixedTimeEquals(expectedHash, Hash(payloadBytes)))
{
return SnapshotReadResult.Failed("Snapshot SHA-256 validation failed.");
}
var lines = payload
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.ToArray();
if (lines.Length != expectedLineCount)
{
return SnapshotReadResult.Failed("Snapshot payload line count is invalid.");
}
ValidatePayload(lines);
var age = utcNow - generatedUtc;
if (age < TimeSpan.FromMinutes(-5))
{
return SnapshotReadResult.Failed("Snapshot timestamp is too far in the future.");
}
if (age > maximumAge)
{
return SnapshotReadResult.Failed(
"Snapshot is stale: age="
+ Math.Floor(age.TotalSeconds).ToString(CultureInfo.InvariantCulture)
+ "s, maximum="
+ Math.Floor(maximumAge.TotalSeconds).ToString(CultureInfo.InvariantCulture)
+ "s.");
}
return SnapshotReadResult.Success(lines, generatedUtc);
}
catch (FileNotFoundException)
{
return SnapshotReadResult.Failed("Snapshot file does not exist.");
}
catch (DirectoryNotFoundException)
{
return SnapshotReadResult.Failed("Snapshot directory does not exist.");
}
catch (UnauthorizedAccessException ex)
{
return SnapshotReadResult.Failed("Snapshot cannot be read: " + ex.Message);
}
catch (IOException ex)
{
return SnapshotReadResult.Failed("Snapshot I/O failed: " + ex.Message);
}
catch (Exception ex)
{
return SnapshotReadResult.Failed("Snapshot validation failed: " + ex.GetType().Name + ": " + ex.Message);
}
}
private byte[] ReadBytesWithRetry()
{
for (var attempt = 0; ; attempt++)
{
try
{
using (var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
{
if (stream.Length <= 0 || stream.Length > _maxBytes)
{
throw new InvalidDataException("Snapshot size is outside the allowed range.");
}
var bytes = new byte[(int)stream.Length];
var offset = 0;
while (offset < bytes.Length)
{
var read = stream.Read(bytes, offset, bytes.Length - offset);
if (read == 0)
{
throw new EndOfStreamException("Unexpected end of snapshot.");
}
offset += read;
}
return bytes;
}
}
catch (IOException)
{
if (attempt >= 2)
{
throw;
}
Thread.Sleep(25 * (attempt + 1));
}
}
}
private static string BuildHeader(DateTime generatedUtc, string identity, int lineCount, string hash)
{
return Magic + "\n"
+ "generatedUtc=" + generatedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture) + "\n"
+ "machineBase64=" + Convert.ToBase64String(StrictUtf8.GetBytes(Environment.MachineName)) + "\n"
+ "identityBase64=" + Convert.ToBase64String(StrictUtf8.GetBytes(identity ?? string.Empty)) + "\n"
+ "payloadLines=" + lineCount.ToString(CultureInfo.InvariantCulture) + "\n"
+ "payloadSha256=" + hash + "\n\n";
}
private static void ValidatePayload(IEnumerable<string> lines)
{
if (lines == null)
{
throw new ArgumentNullException("lines");
}
var count = 0;
foreach (var line in lines)
{
count++;
if (string.IsNullOrWhiteSpace(line)
|| line.IndexOf('\r') >= 0
|| line.IndexOf('\n') >= 0
|| line.Length < 5
|| line[1] != ' '
|| line[2] != '"'
|| line[0] < '0'
|| line[0] > '3')
{
throw new InvalidDataException("Snapshot contains an invalid Checkmk local-check line.");
}
}
if (count < 8)
{
throw new InvalidDataException("Snapshot must contain all eight stable services.");
}
}
private static string Hash(byte[] bytes)
{
using (var sha = SHA256.Create())
{
return string.Concat(sha.ComputeHash(bytes).Select(x => x.ToString("x2", CultureInfo.InvariantCulture)));
}
}
private static bool FixedTimeEquals(string left, string right)
{
if (left == null || right == null || left.Length != right.Length)
{
return false;
}
var difference = 0;
for (var i = 0; i < left.Length; i++)
{
difference |= left[i] ^ right[i];
}
return difference == 0;
}
private static string ReadHeaderValue(string line, string prefix)
{
if (line == null || !line.StartsWith(prefix, StringComparison.Ordinal))
{
throw new InvalidDataException("Missing snapshot header " + prefix);
}
return line.Substring(prefix.Length);
}
private static string DecodeHeader(string line, string prefix)
{
return StrictUtf8.GetString(Convert.FromBase64String(ReadHeaderValue(line, prefix)));
}
private static bool TryReadDate(string line, string prefix, out DateTime result)
{
return DateTime.TryParseExact(
ReadHeaderValue(line, prefix),
"o",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out result);
}
private static bool TryReadInt(string line, string prefix, out int result)
{
return int.TryParse(ReadHeaderValue(line, prefix), NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
}
}
internal sealed class SnapshotReadResult
{
private SnapshotReadResult()
{
Lines = new string[0];
}
public bool IsSuccess { get; private set; }
public string Error { get; private set; }
public IReadOnlyList<string> Lines { get; private set; }
public DateTime GeneratedUtc { get; private set; }
public static SnapshotReadResult Success(IReadOnlyList<string> lines, DateTime generatedUtc)
{
return new SnapshotReadResult
{
IsSuccess = true,
Lines = lines,
GeneratedUtc = generatedUtc
};
}
public static SnapshotReadResult Failed(string error)
{
return new SnapshotReadResult
{
IsSuccess = false,
Error = error
};
}
}
}
+31 -10
View File
@@ -35,18 +35,29 @@ namespace BizTalkCheckmkPulse
}
AddTarget(result, "Management", result.Platform.ManagementDbServer, result.Platform.ManagementDbName);
AddTarget(result, "MessageBox", result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName);
AddTarget(result, "MasterMessageBox", result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName);
if (!result.Platform.PlatformDataAvailable)
{
var wmiPermissionFailure = result.Diagnostics.Any(x =>
x.Area == DiagnosticArea.Wmi
&& x.Category == DiagnosticCategory.Permission
&& x.Required
&& string.Equals(x.Component, "MSBTS_GroupSetting", StringComparison.OrdinalIgnoreCase));
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Sql,
Category = DiagnosticCategory.Configuration,
Category = wmiPermissionFailure
? DiagnosticCategory.Permission
: DiagnosticCategory.Configuration,
Component = "BizTalk database discovery",
Required = true,
Summary = "SQL-Zielermittlung ist unvollstaendig, weil nicht alle erforderlichen BizTalk-Plattformklassen gelesen wurden.",
Action = "Zuerst den Service 'BizTalk Platform' sowie MSBTS_GroupSetting und MSBTS_MessageBoxSetting pruefen. Danach den Agent-Dump erneut ausfuehren."
Summary = wmiPermissionFailure
? "SQL-Zielermittlung ist unvollstaendig, weil der BizTalk-WMI-Provider beim SQL-Zugriff abgewiesen wurde."
: "SQL-Zielermittlung ist unvollstaendig, weil nicht alle erforderlichen BizTalk-Plattformklassen gelesen wurden.",
Action = wmiPermissionFailure
? "Zuerst die Wmi/Permission-Diagnose sowie Provider-Konto, konfigurierte BizTalk-Read-Only-Gruppe und BTS_READONLY_USERS pruefen."
: "Zuerst den Service 'BizTalk Platform' und MSBTS_GroupSetting im Provider-Log pruefen. Danach den Scheduled Task erneut starten."
});
}
@@ -75,9 +86,19 @@ namespace BizTalkCheckmkPulse
var isLocalSystem = identity != null
&& identity.User != null
&& identity.User.IsWellKnown(WellKnownSidType.LocalSystemSid);
result.NetworkIdentityHint = isLocalSystem
? "<DOMAIN>\\" + Environment.MachineName + "$"
: result.ExecutionIdentity;
if (isLocalSystem)
{
if (string.IsNullOrWhiteSpace(result.NetworkIdentityHint)
|| string.Equals(result.NetworkIdentityHint, "unknown", StringComparison.OrdinalIgnoreCase)
|| result.NetworkIdentityHint.StartsWith("<DOMAIN>\\", StringComparison.OrdinalIgnoreCase))
{
result.NetworkIdentityHint = "<DOMAIN>\\" + Environment.MachineName + "$";
}
}
else
{
result.NetworkIdentityHint = result.ExecutionIdentity;
}
}
}
catch (Exception ex)
@@ -91,7 +112,7 @@ namespace BizTalkCheckmkPulse
Component = "Windows identity",
Required = false,
Summary = "Die Windows-Ausfuehrungsidentitaet konnte nicht bestimmt werden.",
Action = "Agent-Dienstkonto mit 'sc.exe qc CheckMKService' beziehungsweise in services.msc pruefen.",
Action = "Konto des Scheduled Tasks 'BizTalk Checkmk Pulse Provider' pruefen.",
TechnicalDetails = ex.GetType().Name + ": " + ex.Message
});
}
@@ -247,7 +268,7 @@ namespace BizTalkCheckmkPulse
case DiagnosticCategory.Timeout:
return "SQL-Verbindungsaufbau oder Testabfrage hat das konfigurierte Zeitlimit ueberschritten.";
case DiagnosticCategory.Connectivity:
return "SQL Server oder die konfigurierte SQL-Instanz ist aus dem Checkmk-Agent-Kontext nicht erreichbar.";
return "SQL Server oder die konfigurierte SQL-Instanz ist aus dem Provider-Kontext nicht erreichbar.";
case DiagnosticCategory.Configuration:
return "SQL-Verbindung scheitert an TLS-, Zertifikats-, SPN- oder SSPI-Konfiguration.";
default:
@@ -267,7 +288,7 @@ namespace BizTalkCheckmkPulse
switch (category)
{
case DiagnosticCategory.Permission:
return networkIdentity + " in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen.";
return "Provider-Konto " + networkIdentity + " und dessen Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe sowie BTS_READONLY_USERS pruefen; keine direkten BizTalk-DB-Rollen vergeben. Danach den Scheduled Task mit neuem Anmeldetoken starten.";
case DiagnosticCategory.Timeout:
return "Netzwerkpfad, DNS, SQL-Port, Firewall und Auslastung fuer " + target.Server + " pruefen; Timeout nur nach Ursachenanalyse erhoehen.";
case DiagnosticCategory.Connectivity:
+278 -75
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
@@ -23,6 +24,10 @@ namespace BizTalkCheckmkPulse
private const int OrchestrationStopped = 3;
private const int OrchestrationStarted = 4;
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;
/// <summary>
@@ -53,9 +58,14 @@ namespace BizTalkCheckmkPulse
result.Platform.WmiConnected = true;
result.Platform.PlatformDataAvailable = QueryPlatform(scope, result);
result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result);
var applicationIndex = BuildApplicationIndex(scope, result);
result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result);
result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result, server);
var applicationIndex = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
QueryApplicationRuntime(scope, result, applicationIndex);
if (_options.EmitPerApplicationSuspensionServices)
{
AddArtifactIndex(scope, result, applicationIndex, "MSBTS_ReceivePort");
}
result.Platform.SuspendedInstancesDataAvailable = QuerySuspendedInstances(scope, result, applicationIndex);
}
@@ -74,7 +84,7 @@ namespace BizTalkCheckmkPulse
}
catch (Exception ex) when (IsExpectedWmiException(ex))
{
AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true);
AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true, null, null);
return false;
}
}
@@ -88,23 +98,21 @@ namespace BizTalkCheckmkPulse
private bool QueryPlatform(ManagementScope scope, ProbeResult result)
{
var groupFound = false;
var groupAvailable = TryQuery(scope, result, "MSBTS_GroupSetting", "SELECT * FROM MSBTS_GroupSetting", item =>
var groupAvailable = TryQuery(scope, result, "MSBTS_GroupSetting", GroupSettingQuery, item =>
{
if (groupFound)
{
return;
}
groupFound = true;
result.Platform.GroupName = FirstNonEmpty(WmiHelpers.GetString(item, "Name"), WmiHelpers.GetString(item, "MgmtDbName"));
result.Platform.ManagementDbServer = FirstNonEmpty(WmiHelpers.GetString(item, "MgmtDbServerName"), WmiHelpers.GetString(item, "DBServerName"));
result.Platform.ManagementDbName = FirstNonEmpty(WmiHelpers.GetString(item, "MgmtDbName"), WmiHelpers.GetString(item, "DatabaseName"));
}, true);
var messageBoxFound = false;
var messageBoxAvailable = TryQuery(scope, result, "MSBTS_MessageBoxSetting", "SELECT * FROM MSBTS_MessageBoxSetting", item =>
{
messageBoxFound = true;
if (string.IsNullOrWhiteSpace(result.Platform.MessageBoxDbServer))
{
result.Platform.MessageBoxDbServer = FirstNonEmpty(WmiHelpers.GetString(item, "DBServerName"), WmiHelpers.GetString(item, "ServerName"));
result.Platform.MessageBoxDbName = FirstNonEmpty(WmiHelpers.GetString(item, "DBName"), WmiHelpers.GetString(item, "DatabaseName"));
}
result.Platform.OperatorGroup = WmiHelpers.GetString(item, "BizTalkOperatorGroup");
result.Platform.ReadOnlyUserGroup = WmiHelpers.GetString(item, "BizTalkReadOnlyUserGroup");
result.Platform.ManagementDbServer = WmiHelpers.GetString(item, "MgmtDbServerName");
result.Platform.ManagementDbName = WmiHelpers.GetString(item, "MgmtDbName");
result.Platform.MessageBoxDbServer = WmiHelpers.GetString(item, "SubscriptionDBServerName");
result.Platform.MessageBoxDbName = WmiHelpers.GetString(item, "SubscriptionDBName");
}, true);
if (groupAvailable && !groupFound)
@@ -112,32 +120,33 @@ namespace BizTalkCheckmkPulse
AddMissingDataDiagnostic(result, "MSBTS_GroupSetting", "Keine BizTalk-Gruppe gefunden.");
}
if (messageBoxAvailable && !messageBoxFound)
var managementTargetComplete = HasDatabaseTarget(
result.Platform.ManagementDbServer,
result.Platform.ManagementDbName);
if (groupAvailable && groupFound && !managementTargetComplete)
{
AddMissingDataDiagnostic(result, "MSBTS_MessageBoxSetting", "Keine BizTalk-MessageBox-Datenbank gefunden.");
AddMissingDataDiagnostic(
result,
"MSBTS_GroupSetting.ManagementDatabase",
"BizTalk Management-Datenbankziel ist in MSBTS_GroupSetting unvollstaendig.");
}
return groupAvailable && groupFound && messageBoxAvailable && messageBoxFound;
var messageBoxTargetComplete = HasDatabaseTarget(
result.Platform.MessageBoxDbServer,
result.Platform.MessageBoxDbName);
if (groupAvailable && groupFound && !messageBoxTargetComplete)
{
AddMissingDataDiagnostic(
result,
"MSBTS_GroupSetting.SubscriptionDatabase",
"Master-MessageBox-Datenbankziel ist in MSBTS_GroupSetting unvollstaendig.");
}
return groupAvailable && groupFound && managementTargetComplete && messageBoxTargetComplete;
}
/// <summary>
/// Erstellt einen Index, um suspendierte Instanzen BizTalk-Anwendungen zuzuordnen.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Ergebnisobjekt fuer optionale Diagnosen.</param>
/// <returns>Index aus moeglichen Artefaktschluesseln und Anwendungsnamen.</returns>
private Dictionary<string, string> BuildApplicationIndex(ManagementScope scope, ProbeResult result)
{
var index = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
AddArtifactIndex(scope, result, index, "MSBTS_SendPort");
AddArtifactIndex(scope, result, index, "MSBTS_ReceivePort");
AddArtifactIndex(scope, result, index, "MSBTS_ReceiveLocation");
AddArtifactIndex(scope, result, index, "MSBTS_Orchestration");
return index;
}
/// <summary>
/// Fuegt die Schluessel einer WMI-Artefaktklasse zum Anwendungsindex hinzu.
/// Fuegt die Schluessel einer optionalen WMI-Artefaktklasse zum Anwendungsindex hinzu.
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Ergebnisobjekt fuer optionale Diagnosen.</param>
@@ -147,14 +156,7 @@ namespace BizTalkCheckmkPulse
{
TryQuery(scope, result, className + " application index", "SELECT * FROM " + className, item =>
{
var app = GetApplicationName(item);
foreach (var key in CandidateKeys(item))
{
if (!string.IsNullOrWhiteSpace(key) && !index.ContainsKey(key))
{
index.Add(key, app);
}
}
AddArtifactIndexEntry(index, item);
}, false);
}
@@ -163,16 +165,28 @@ namespace BizTalkCheckmkPulse
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <param name="server">BizTalk-Server, dessen Host Instances bewertet werden.</param>
/// <returns><c>true</c>, wenn die Pflichtabfrage erfolgreich war.</returns>
private bool QueryHostInstances(ManagementScope scope, ProbeResult result)
private bool QueryHostInstances(ManagementScope scope, ProbeResult result, string server)
{
return TryQuery(scope, result, "MSBTS_HostInstance", "SELECT * FROM MSBTS_HostInstance", item =>
return TryQuery(
scope,
result,
"MSBTS_HostInstance",
"SELECT Name, HostName, RunningServer, ServiceState FROM MSBTS_HostInstance",
item =>
{
var runningServer = WmiHelpers.GetString(item, "RunningServer");
if (!string.IsNullOrWhiteSpace(runningServer) && !IsSameServer(runningServer, server))
{
return;
}
result.HostInstances.Add(new HostInstanceState
{
InstanceName = FirstNonEmpty(WmiHelpers.GetString(item, "InstanceName"), WmiHelpers.GetString(item, "Name")),
HostName = WmiHelpers.GetString(item, "HostName"),
RunningServer = WmiHelpers.GetString(item, "RunningServer"),
RunningServer = runningServer,
ServiceState = WmiHelpers.GetInt32(item, "ServiceState", 0)
});
}, true);
@@ -183,26 +197,53 @@ namespace BizTalkCheckmkPulse
/// </summary>
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
/// <returns><c>true</c>, wenn alle drei Pflichtabfragen erfolgreich waren.</returns>
private bool QueryApplicationRuntime(ManagementScope scope, ProbeResult result)
/// <param name="applicationIndex">Best-Effort-Index fuer Suspensionsdetails.</param>
private void QueryApplicationRuntime(
ManagementScope scope,
ProbeResult result,
Dictionary<string, string> applicationIndex)
{
var apps = new Dictionary<string, ApplicationRuntimeState>(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 =>
{
var app = GetApplication(apps, GetApplicationName(item));
AddArtifactIndexEntry(applicationIndex, 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 =>
{
var app = GetApplication(apps, GetApplicationName(item));
AddArtifactIndexEntry(applicationIndex, 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++;
@@ -219,8 +260,9 @@ 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));
app.OrchestrationTotal++;
switch (WmiHelpers.GetInt32(item, "OrchestrationStatus", 0))
@@ -244,7 +286,6 @@ namespace BizTalkCheckmkPulse
}, true);
result.Applications.AddRange(apps.Values.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase));
return receiveLocationsAvailable && sendPortsAvailable && orchestrationsAvailable;
}
/// <summary>
@@ -256,9 +297,10 @@ namespace BizTalkCheckmkPulse
/// <returns><c>true</c>, wenn die Pflichtabfrage erfolgreich war.</returns>
private bool QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary<string, string> 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),
@@ -268,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);
}
@@ -285,6 +330,7 @@ namespace BizTalkCheckmkPulse
/// <returns><c>true</c>, wenn die Abfrage vollstaendig ausgefuehrt wurde.</returns>
private bool TryQuery(ManagementScope scope, ProbeResult result, string label, string queryText, Action<ManagementObject> action, bool required)
{
var stopwatch = Stopwatch.StartNew();
try
{
ForEachObject(scope, queryText, action);
@@ -292,7 +338,7 @@ namespace BizTalkCheckmkPulse
}
catch (Exception ex) when (IsExpectedWmiException(ex))
{
AddWmiDiagnostic(result, label, ex, required, false);
AddWmiDiagnostic(result, label, ex, required, false, queryText, stopwatch.Elapsed);
return false;
}
}
@@ -344,6 +390,28 @@ namespace BizTalkCheckmkPulse
return app;
}
/// <summary>
/// Nimmt nur tatsaechlich vorhandene Anwendungszuordnungen in den Best-Effort-Index auf.
/// </summary>
/// <param name="index">Zu erweiternder Index.</param>
/// <param name="item">BizTalk-Artefakt aus WMI.</param>
private static void AddArtifactIndexEntry(Dictionary<string, string> index, ManagementBaseObject item)
{
var app = GetApplicationName(item);
if (string.Equals(app, UnknownApplication, StringComparison.OrdinalIgnoreCase))
{
return;
}
foreach (var key in CandidateKeys(item))
{
if (!string.IsNullOrWhiteSpace(key) && !index.ContainsKey(key))
{
index.Add(key, app);
}
}
}
/// <summary>
/// Ermittelt den Anwendungsnamen direkt oder ueber den Artefaktindex.
/// </summary>
@@ -406,6 +474,32 @@ namespace BizTalkCheckmkPulse
return values == null ? null : values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
}
/// <summary>
/// Prueft, ob Server und Datenbank eines SQL-Ziels vorhanden sind.
/// </summary>
private static bool HasDatabaseTarget(string server, string database)
{
return !string.IsNullOrWhiteSpace(server) && !string.IsNullOrWhiteSpace(database);
}
/// <summary>
/// Vergleicht kurzen Namen und FQDN eines BizTalk-Servers ohne WQL-Namensfilter.
/// </summary>
internal static bool IsSameServer(string left, string right)
{
return string.Equals(
NormalizeServerName(left),
NormalizeServerName(right),
StringComparison.OrdinalIgnoreCase);
}
private static string NormalizeServerName(string value)
{
var normalized = (value ?? string.Empty).Trim().TrimStart('\\');
var dotIndex = normalized.IndexOf('.');
return dotIndex > 0 ? normalized.Substring(0, dotIndex) : normalized;
}
/// <summary>
/// Prueft, ob eine Exception aus dem erwarteten WMI-/COM-Fehlerbereich stammt.
/// </summary>
@@ -426,18 +520,44 @@ namespace BizTalkCheckmkPulse
/// <param name="exception">Ausgeloeste Exception.</param>
/// <param name="required">Kennzeichen fuer eine Pflichtabfrage.</param>
/// <param name="connectionFailure">Kennzeichen fuer Fehler beim Namespace-Verbindungsaufbau.</param>
private static void AddWmiDiagnostic(ProbeResult result, string component, Exception exception, bool required, bool connectionFailure)
private static void AddWmiDiagnostic(
ProbeResult result,
string component,
Exception exception,
bool required,
bool connectionFailure,
string queryText,
TimeSpan? duration)
{
var category = ClassifyWmiException(exception);
var sqlLoginPrincipal = ExtractSqlLoginPrincipal(exception.Message);
if (!string.IsNullOrWhiteSpace(sqlLoginPrincipal))
{
result.NetworkIdentityHint = sqlLoginPrincipal;
}
var technicalDetails = exception.GetType().Name
+ " HRESULT=0x" + exception.HResult.ToString("X8")
+ ": " + exception.Message;
if (!string.IsNullOrWhiteSpace(queryText))
{
technicalDetails += " WQL={" + queryText + "}";
}
if (duration.HasValue)
{
technicalDetails += " DurationMs=" + Math.Round(duration.Value.TotalMilliseconds);
}
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Wmi,
Category = category,
Component = component,
Required = required,
Summary = BuildWmiSummary(category, component, connectionFailure),
Action = BuildWmiAction(category, connectionFailure),
TechnicalDetails = exception.GetType().Name + " HRESULT=0x" + exception.HResult.ToString("X8") + ": " + exception.Message
Summary = BuildWmiSummary(category, component, connectionFailure, sqlLoginPrincipal),
Action = BuildWmiAction(category, connectionFailure, sqlLoginPrincipal),
TechnicalDetails = technicalDetails
});
}
@@ -465,7 +585,7 @@ namespace BizTalkCheckmkPulse
/// </summary>
/// <param name="exception">Zu klassifizierende Exception.</param>
/// <returns>Handlungsorientierte Diagnosekategorie.</returns>
private static DiagnosticCategory ClassifyWmiException(Exception exception)
internal static DiagnosticCategory ClassifyWmiException(Exception exception)
{
var managementException = exception as ManagementException;
var comException = exception as COMException;
@@ -477,13 +597,35 @@ namespace BizTalkCheckmkPulse
}
if (managementException != null
&& (managementException.ErrorCode == ManagementStatus.InvalidNamespace
|| managementException.ErrorCode == ManagementStatus.InvalidClass))
&& managementException.ErrorCode == ManagementStatus.InvalidNamespace)
{
return DiagnosticCategory.Configuration;
}
if (managementException != null
&& (managementException.ErrorCode == ManagementStatus.InvalidClass
|| managementException.ErrorCode == ManagementStatus.InvalidProperty
|| managementException.ErrorCode == ManagementStatus.InvalidQuery
|| managementException.ErrorCode == ManagementStatus.UnparsableQuery))
{
return DiagnosticCategory.Schema;
}
if (managementException != null && managementException.ErrorCode == ManagementStatus.Timedout)
{
return DiagnosticCategory.Timeout;
}
var message = exception.Message ?? string.Empty;
if (ContainsAny(
message,
"Login failed for user",
"Cannot open database",
"not associated with a trusted SQL Server connection"))
{
return DiagnosticCategory.Permission;
}
if (ContainsAny(message, "timed out", "timeout", "Zeitlimit"))
{
return DiagnosticCategory.Timeout;
@@ -497,6 +639,45 @@ namespace BizTalkCheckmkPulse
return DiagnosticCategory.Provider;
}
/// <summary>
/// Extrahiert das von SQL Server abgewiesene Konto aus der Provider-Fehlermeldung.
/// </summary>
/// <param name="message">WMI-/OLEDB-Fehlermeldung.</param>
/// <returns>Abgewiesenes Konto oder eine leere Zeichenfolge.</returns>
internal static string ExtractSqlLoginPrincipal(string message)
{
const string marker = "Login failed for user";
if (string.IsNullOrWhiteSpace(message))
{
return string.Empty;
}
var markerIndex = message.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
if (markerIndex < 0)
{
return string.Empty;
}
var valueStart = markerIndex + marker.Length;
while (valueStart < message.Length
&& (char.IsWhiteSpace(message[valueStart]) || message[valueStart] == ':'))
{
valueStart++;
}
if (valueStart >= message.Length
|| (message[valueStart] != '\'' && message[valueStart] != '"'))
{
return string.Empty;
}
var quote = message[valueStart];
var valueEnd = message.IndexOf(quote, valueStart + 1);
return valueEnd > valueStart + 1
? message.Substring(valueStart + 1, valueEnd - valueStart - 1).Trim()
: string.Empty;
}
/// <summary>
/// Erstellt eine kurze Fehlerursache fuer einen WMI-Fehler.
/// </summary>
@@ -504,16 +685,27 @@ namespace BizTalkCheckmkPulse
/// <param name="component">Betroffene Komponente.</param>
/// <param name="connectionFailure">Kennzeichen fuer den Namespace-Verbindungsaufbau.</param>
/// <returns>Menschenlesbare Fehlerzusammenfassung.</returns>
private static string BuildWmiSummary(DiagnosticCategory category, string component, bool connectionFailure)
private static string BuildWmiSummary(
DiagnosticCategory category,
string component,
bool connectionFailure,
string sqlLoginPrincipal)
{
switch (category)
{
case DiagnosticCategory.Permission:
return "Zugriff auf " + component + " wurde im LocalSystem-Kontext verweigert.";
if (!string.IsNullOrWhiteSpace(sqlLoginPrincipal))
{
return "BizTalk-WMI-Provider konnte den SQL-Zugriff fuer " + sqlLoginPrincipal + " nicht anmelden.";
}
return "Zugriff des privilegierten Provider-Kontos auf " + component + " wurde verweigert.";
case DiagnosticCategory.Configuration:
return connectionFailure
? "BizTalk-WMI-Namespace ist nicht vorhanden oder nicht korrekt registriert."
: "BizTalk-WMI-Klasse " + component + " ist nicht vorhanden oder nicht korrekt registriert.";
: "BizTalk-WMI-Konfiguration fuer " + component + " ist unvollstaendig.";
case DiagnosticCategory.Schema:
return "WMI-Klasse oder WQL fuer " + component + " wird vom installierten BizTalk-Provider nicht unterstuetzt.";
case DiagnosticCategory.Timeout:
return "WMI-Abfrage fuer " + component + " hat das konfigurierte Zeitlimit ueberschritten.";
case DiagnosticCategory.Connectivity:
@@ -529,16 +721,27 @@ namespace BizTalkCheckmkPulse
/// <param name="category">Klassifizierte Fehlerart.</param>
/// <param name="connectionFailure">Kennzeichen fuer den Namespace-Verbindungsaufbau.</param>
/// <returns>Empfohlener Diagnoseschritt.</returns>
private static string BuildWmiAction(DiagnosticCategory category, bool connectionFailure)
private static string BuildWmiAction(
DiagnosticCategory category,
bool connectionFailure,
string sqlLoginPrincipal)
{
switch (category)
{
case DiagnosticCategory.Permission:
if (!string.IsNullOrWhiteSpace(sqlLoginPrincipal))
{
return "Provider-Konto " + sqlLoginPrincipal
+ " und dessen Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe sowie BTS_READONLY_USERS pruefen; keine direkten SQL-Logins oder Datenbankrollen vergeben.";
}
return connectionFailure
? "Namespace-ACL fuer root\\MicrosoftBizTalkServer gezielt pruefen. LocalSystem benoetigt lokalen Lesezugriff; keine pauschalen WMI-Rechte vergeben."
: "Agent-Dump pruefen und Computerkonto <DOMAIN>\\" + Environment.MachineName + "$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen.";
? "Lokalen Namespace-Zugriff des Scheduled-Task-Kontos auf root\\MicrosoftBizTalkServer gezielt pruefen; keine pauschalen WMI-Rechte vergeben."
: "Provider-Identitaet und Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe pruefen; Operator-Rechte nur nach klassenspezifischer Analyse erwaegen.";
case DiagnosticCategory.Configuration:
return "BizTalk-WMI-Provider/Namespace auf dem BizTalk-Server pruefen und gegebenenfalls mit dem BizTalk-Setup reparieren.";
case DiagnosticCategory.Schema:
return "Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema und die Microsoft-Dokumentation pruefen. Rechteerhoehung behebt InvalidClass/InvalidQuery nicht.";
case DiagnosticCategory.Timeout:
return "WMI-Dienst, BizTalk-/SQL-Auslastung und Event Logs pruefen; Timeout erst danach erhoehen.";
case DiagnosticCategory.Connectivity:
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" Condition="false" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkCheckmkPulse.Tests</RootNamespace>
<AssemblyName>BizTalkCheckmkPulse.Tests</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BizTalkCheckmkPulse\BizTalkCheckmkPulse.csproj">
<Project>{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}</Project>
<Name>BizTalkCheckmkPulse</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+488
View File
@@ -0,0 +1,488 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace BizTalkCheckmkPulse.Tests
{
internal static class Program
{
private static readonly List<string> Failures = new List<string>();
private static int ExecutedTests;
private static int Main()
{
Run("SelfTestEmitsAllStableServices", SelfTestEmitsAllStableServices);
Run("UnavailableSourcesAreUnknown", UnavailableSourcesAreUnknown);
Run("UnknownApplicationDoesNotCreateService", UnknownApplicationDoesNotCreateService);
Run("KnownApplicationCreatesService", KnownApplicationCreatesService);
Run("ServerNamesAreComparedWithoutWql", ServerNamesAreComparedWithoutWql);
Run("GroupSettingQueryUsesDocumentedSchema", GroupSettingQueryUsesDocumentedSchema);
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);
Run("SnapshotRejectsDifferentMachine", SnapshotRejectsDifferentMachine);
Run("SnapshotReportsMissingFile", SnapshotReportsMissingFile);
Run("SnapshotFailureEmitsStableUnknownServices", SnapshotFailureEmitsStableUnknownServices);
if (Failures.Count == 0)
{
Console.WriteLine("PASS: " + ExecutedTests + " tests");
return 0;
}
foreach (var failure in Failures)
{
Console.Error.WriteLine("FAIL: " + failure);
}
return 1;
}
private static void SelfTestEmitsAllStableServices()
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
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(8).ToArray();
AssertEqual(8, lines.Length, "stable service count");
Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "unavailable sources must be UNKNOWN");
}
private static void UnknownApplicationDoesNotCreateService()
{
var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true };
var result = CreateSuspensionResult("(unknown)");
var lines = new CheckmkLocalFormatter(options).Format(result).ToArray();
AssertEqual(8, lines.Length, "unknown application must not create a dynamic service");
}
private static void KnownApplicationCreatesService()
{
var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true };
var result = CreateSuspensionResult("Orders");
var lines = new CheckmkLocalFormatter(options).Format(result).ToArray();
AssertEqual(9, lines.Length, "known application should create one dynamic service");
Assert(lines.Any(x => x.Contains("\"BizTalk Suspended Orders\"")), "known application service missing");
}
private static void ServerNamesAreComparedWithoutWql()
{
Assert(WmiBizTalkProbe.IsSameServer("BIZTALK01.corp.example", "biztalk01"), "FQDN and short name must match");
Assert(WmiBizTalkProbe.IsSameServer(@"\\BIZ-TALK+01", "biz-talk+01.example"), "special characters must be compared client-side");
Assert(!WmiBizTalkProbe.IsSameServer("BIZTALK01", "BIZTALK02"), "different servers must not match");
}
private static void GroupSettingQueryUsesDocumentedSchema()
{
var query = WmiBizTalkProbe.GroupSettingQuery;
Assert(query.Contains("FROM MSBTS_GroupSetting"), "MSBTS_GroupSetting query missing");
Assert(query.Contains("BizTalkOperatorGroup"), "configured operator group property missing");
Assert(query.Contains("BizTalkReadOnlyUserGroup"), "configured read-only group property missing");
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()
{
var exception = new COMException(
"Internal error from OLEDB provider: 'Login failed for user 'BEW\\AV23AGPWBIO1$'.'",
unchecked((int)0x80131904));
AssertEqual(
DiagnosticCategory.Permission,
WmiBizTalkProbe.ClassifyWmiException(exception),
"provider SQL login classification");
}
private static void RejectedSqlPrincipalIsExtracted()
{
var message = "Internal error from OLEDB provider: 'Login failed for user 'BEW\\AV23AGPWBIO1$'.'";
AssertEqual(
"BEW\\AV23AGPWBIO1$",
WmiBizTalkProbe.ExtractSqlLoginPrincipal(message),
"rejected SQL principal");
}
private static void PlatformPermissionPropagatesToSqlDiscovery()
{
var result = new ProbeResult();
result.Diagnostics.Add(new ProbeDiagnostic
{
Area = DiagnosticArea.Wmi,
Category = DiagnosticCategory.Permission,
Component = "MSBTS_GroupSetting",
Required = true
});
new SqlConnectivityProbe(new MonitoringOptions()).Query(result);
var discovery = result.Diagnostics.Single(x =>
x.Area == DiagnosticArea.Sql
&& x.Component == "BizTalk database discovery");
AssertEqual(
DiagnosticCategory.Permission,
discovery.Category,
"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) =>
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
var generated = DateTime.UtcNow;
store.Write(lines, generated, "DOMAIN\\collector$");
var result = store.Read(generated.AddSeconds(10), TimeSpan.FromMinutes(3));
Assert(result.IsSuccess, "snapshot should be readable: " + result.Error);
AssertEqual(lines.Length, result.Lines.Count, "snapshot line count");
Assert(lines.SequenceEqual(result.Lines), "snapshot payload changed");
var replacement = lines.Select(x => x.Replace("Self test OK", "Replacement OK")).ToArray();
store.Write(replacement, generated.AddSeconds(30), "DOMAIN\\collector$");
var replaced = store.Read(generated.AddSeconds(40), TimeSpan.FromMinutes(3));
Assert(replaced.IsSuccess, "replaced snapshot should be readable");
Assert(replacement.SequenceEqual(replaced.Lines), "atomic replacement payload changed");
});
}
private static void SnapshotRejectsTampering()
{
WithTemporarySnapshot((path, store) =>
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
store.Write(lines, DateTime.UtcNow, "DOMAIN\\collector$");
var content = File.ReadAllText(path, Encoding.UTF8);
File.WriteAllText(path, content.Replace("Self test OK", "Tampered output"), new UTF8Encoding(false));
var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3));
Assert(!result.IsSuccess, "tampered snapshot must be rejected");
Assert(result.Error.IndexOf("SHA-256", StringComparison.OrdinalIgnoreCase) >= 0, "tamper reason should mention SHA-256");
});
}
private static void SnapshotRejectsStaleData()
{
WithTemporarySnapshot((path, store) =>
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
var generated = DateTime.UtcNow.AddMinutes(-10);
store.Write(lines, generated, "DOMAIN\\collector$");
var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3));
Assert(!result.IsSuccess, "stale snapshot must be rejected");
Assert(result.Error.IndexOf("stale", StringComparison.OrdinalIgnoreCase) >= 0, "stale reason missing");
});
}
private static void SnapshotRejectsDifferentMachine()
{
WithTemporarySnapshot((path, store) =>
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
store.Write(lines, DateTime.UtcNow, "DOMAIN\\collector$");
var content = File.ReadAllText(path, Encoding.UTF8);
var currentMachine = Convert.ToBase64String(Encoding.UTF8.GetBytes(Environment.MachineName));
var otherMachine = Convert.ToBase64String(Encoding.UTF8.GetBytes("OTHER-SERVER"));
File.WriteAllText(
path,
content.Replace("machineBase64=" + currentMachine, "machineBase64=" + otherMachine),
new UTF8Encoding(false));
var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3));
Assert(!result.IsSuccess, "snapshot for another machine must be rejected");
Assert(result.Error.IndexOf("different machine", StringComparison.OrdinalIgnoreCase) >= 0, "machine mismatch reason missing");
});
}
private static void SnapshotReportsMissingFile()
{
WithTemporarySnapshot((path, store) =>
{
var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3));
Assert(!result.IsSuccess, "missing snapshot must be rejected");
Assert(result.Error.IndexOf("does not exist", StringComparison.OrdinalIgnoreCase) >= 0, "missing file reason missing");
});
}
private static void SnapshotFailureEmitsStableUnknownServices()
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions())
.FormatSnapshotFailure("Snapshot file does not exist.")
.ToArray();
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");
}
private static void WithTemporarySnapshot(Action<string, SnapshotStore> test)
{
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.Tests." + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, "snapshot.txt");
try
{
test(path, new SnapshotStore(path, 1048576));
}
finally
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, true);
}
}
}
private static ProbeResult CreateSuspensionResult(string applicationName)
{
var result = new ProbeResult();
result.Platform.SuspendedInstancesDataAvailable = true;
result.SuspendedInstances.Add(new SuspendedInstance
{
ApplicationName = applicationName,
ServiceName = "TestService",
Kind = SuspendedKind.Resumable
});
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++;
try
{
test();
}
catch (Exception ex)
{
Failures.Add(name + ": " + ex.Message);
}
}
private static void Assert(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
private static void AssertEqual(int expected, int actual, string label)
{
if (expected != actual)
{
throw new InvalidOperationException(label + ": expected " + expected + ", actual " + actual);
}
}
private static void AssertEqual(DiagnosticCategory expected, DiagnosticCategory actual, string label)
{
if (expected != actual)
{
throw new InvalidOperationException(label + ": expected " + expected + ", actual " + actual);
}
}
private static void AssertEqual(string expected, string actual, string label)
{
if (!string.Equals(expected, actual, StringComparison.Ordinal))
{
throw new InvalidOperationException(label + ": expected " + expected + ", actual " + actual);
}
}
}
}