Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
826d87fef7 | ||
|
|
d15bb6539b | ||
|
|
e3d7f6a780 | ||
|
|
0001d7b861 | ||
|
|
8d2934e343 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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%
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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%
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")]
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user