Compare commits
22
Commits
7275546d6c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8e8034e6 | ||
|
|
951a5e72d1 | ||
|
|
cc1e59916a | ||
|
|
448be8dc49 | ||
|
|
8fe1b57173 | ||
|
|
d408419c5f | ||
|
|
26e6ea5aac | ||
|
|
ac51714a88 | ||
|
|
82584473e8 | ||
|
|
e9143e0e58 | ||
|
|
3310732ba4 | ||
|
|
8888ab97c0 | ||
|
|
22a4725056 | ||
|
|
91e0db5619 | ||
|
|
f296a2de2f | ||
|
|
826d87fef7 | ||
|
|
d15bb6539b | ||
|
|
e3d7f6a780 | ||
|
|
0001d7b861 | ||
|
|
8d2934e343 | ||
|
|
78e8881d38 | ||
|
|
31222ad28f |
+1
-2
@@ -8,6 +8,5 @@ indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
end_of_line = lf
|
||||
end_of_line = crlf
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=crlf
|
||||
+20
-3
@@ -1,9 +1,15 @@
|
||||
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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse.Setup", "src\BizTalkCheckmkPulse.Setup\BizTalkCheckmkPulse.Setup.csproj", "{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse.Packager", "src\BizTalkCheckmkPulse.Packager\BizTalkCheckmkPulse.Packager.csproj", "{B35A04E0-253B-4CEB-9033-F5109F3B9F82}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -14,9 +20,20 @@ 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
|
||||
{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
|
||||
+668
-210
@@ -1,109 +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 überwacht 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 für die allgemeine
|
||||
SQL-Überwachung zuständig; 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` erhält keine BizTalk-/SQL-Gruppenmitgliedschaft.
|
||||
- Ein normales dediziertes AD-Servicekonto sammelt minütlich.
|
||||
- Der Agentpfad führt niemals WMI- oder SQL-Abfragen aus.
|
||||
- Fehler müssen als gültige Checkmk-`UNKNOWN`-Services sichtbar werden.
|
||||
- Schreiben und Lesen dürfen nie einen halben Snapshot exponieren.
|
||||
- Installation und Laufzeit benötigen keine PowerShell und keine BizTalk-DLL-Referenz.
|
||||
- Installation, ACL und Scheduled Task müssen reproduzierbar sein.
|
||||
|
||||
## Technische Bewertung
|
||||
## 2. Auslöser der Architekturänderung
|
||||
|
||||
### 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
|
||||
+-- WMI root\MicrosoftBizTalkServer
|
||||
+-- Windows Application Event Log
|
||||
|
|
||||
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, wenn die EXE fehlt. Die EXE selbst faengt Laufzeitfehler ab und schreibt fuer alle stabilen Services `UNKNOWN`, damit unvollstaendige Deployments oder WMI-Probleme in Checkmk sichtbar bleiben.
|
||||
Lokales `root\MicrosoftBizTalkServer` war erreichbar. Die
|
||||
datenbankgestützten BizTalk-WMI-Klassen scheiterten erst beim Zugriff des
|
||||
Providers auf den getrennten SQL Server. DCOM-, Firewall- oder pauschale
|
||||
WMI-ACL-Änderungen beheben diesen Befund nicht.
|
||||
|
||||
## Datenquellen
|
||||
Eine Berechtigung des Maschinenkontos hätte die Netzwerkrolle allen
|
||||
`LocalSystem`-Diensten des BizTalk-Servers vermittelt. Die jetzt implementierte
|
||||
Producer/Consumer-Trennung beschränkt 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 unverändert nach STDOUT │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Eine gemeinsame EXE implementiert beide Modi. Das reduziert
|
||||
Versionsabweichungen. Die zentrale Config unter `%ProgramFiles%` gilt für
|
||||
Provider und Consumer.
|
||||
|
||||
### 3.1 Provider-Modus
|
||||
|
||||
Aufruf:
|
||||
|
||||
```cmd
|
||||
"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --collect
|
||||
```
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. Config laden und absolute Runtimepfade validieren.
|
||||
2. Tageslogs gemäß `LogRetentionDays` bereinigen.
|
||||
3. exklusives Handle auf `<SnapshotPath>.provider.lock` halten.
|
||||
4. WMI-, SQL- und Event-Log-Probes ausführen.
|
||||
5. neun stabile und optionale dynamische Checkmk-Zeilen formatieren.
|
||||
6. Snapshot in einer eindeutigen Temporärdatei desselben Verzeichnisses
|
||||
schreiben.
|
||||
7. `Flush(true)` ausführen und Temporärdatei 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
|
||||
müssen 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:
|
||||
|
||||
- öffnet nur den Snapshot,
|
||||
- begrenzt die Dateigröße vor dem Einlesen,
|
||||
- validiert striktes UTF-8,
|
||||
- validiert Formatversion und Header,
|
||||
- fordert die lokale Maschinenkennung,
|
||||
- berechnet SHA-256 über den exakten Payload,
|
||||
- prüft Zeilenanzahl und Checkmk-Zeilenform,
|
||||
- verwirft Zeitstempel mehr als fünf Minuten in der Zukunft,
|
||||
- verwirft Snapshots nach `SnapshotMaxAgeSeconds`,
|
||||
- gibt bei Erfolg ausschließlich den validierten Payload aus.
|
||||
|
||||
Er gibt immer Exitcode `0` zurück, damit ein Fehler die komplette
|
||||
Checkmk-Agentsektion nicht zerstört. Jede Ablehnung erzeugt neun
|
||||
`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=9
|
||||
payloadSha256=<64 hex characters>
|
||||
|
||||
0 "BizTalk Platform" - ...
|
||||
...
|
||||
```
|
||||
|
||||
Maschine und Identität sind Base64-kodiert, um Zeilenumbrüche und
|
||||
Trennzeichen im Header auszuschließen. Base64 ist hier keine Verschlüsselung.
|
||||
Die SHA-256-Prüfung schützt vor unbemerkter Teil-/Fehlschreibung, nicht vor
|
||||
einem lokalen Administrator.
|
||||
|
||||
### 4.1 Atomare Publikation
|
||||
|
||||
Die Temporärdatei 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 vollständige Snapshot erhalten. Seine Altersprüfung
|
||||
verhindert unbegrenztes Weiterreichen alter Daten.
|
||||
|
||||
### 4.2 Keine direkte Checkmk-Spooldatei
|
||||
|
||||
Checkmk kann zwar Dateien aus seinem Spoolverzeichnis direkt anhängen, prüft
|
||||
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 übernommen.
|
||||
|
||||
## 5. Datenquellen
|
||||
|
||||
### 5.1 BizTalk WMI
|
||||
|
||||
Namespace:
|
||||
|
||||
@@ -111,155 +182,542 @@ 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-Zustände. |
|
||||
| `MSBTS_ServiceInstance` | suspendierte Instanzen. |
|
||||
| `MSBTS_ReceiveLocation` | Receive-Location-Zustände. |
|
||||
| `MSBTS_SendPort` | Send-Port-Zustände. |
|
||||
| `MSBTS_Orchestration` | Orchestration-Zustände. |
|
||||
|
||||
### 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 für `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` mitgezählt.
|
||||
|
||||
Die Liste ist ueber `EventLogSources` konfigurierbar.
|
||||
### 5.2 Kompaktes Checkmk-Servicebild
|
||||
|
||||
## Resilienz
|
||||
|
||||
Das Plugin ist bewusst defensiv gebaut:
|
||||
|
||||
- WMI-Queries haben ein konfigurierbares Timeout.
|
||||
- Optionale WMI-Klassen erzeugen Diagnosehinweise statt Totalabbruch.
|
||||
- Fehlende Properties werden als leer/unknown behandelt.
|
||||
- Local-Check-Ausgaben verwenden gueltige Checkmk-Zeilen mit genau vier Feldern.
|
||||
- Fatal Errors erzeugen `UNKNOWN` fuer alle stabilen Services.
|
||||
- Event-Log-Auswertung ist isoliert; ein Fehler dort bricht WMI-Monitoring nicht ab.
|
||||
- Service-Namen sind stabil, damit Service Discovery nicht bei jedem Lauf neue Services erzeugt.
|
||||
|
||||
## Statusmodell
|
||||
|
||||
### BizTalk Platform
|
||||
|
||||
- `OK`: BizTalk-WMI erreichbar.
|
||||
- `UNKNOWN`: WMI nicht erreichbar oder kompletter Programmfehler.
|
||||
|
||||
Dieser Service ist der Integrationsindikator. Wenn er `UNKNOWN` ist, sind Berechtigungen, BizTalk-Installation oder WMI-Repository zu pruefen.
|
||||
|
||||
### BizTalk Suspended Instances
|
||||
|
||||
- `OK`: keine suspendierten Instanzen.
|
||||
- `WARN`: mindestens `WarnResumableThreshold` resumable suspended instances.
|
||||
- `CRIT`: mindestens `CritNonResumableThreshold` non-resumable suspended instances.
|
||||
- `UNKNOWN`: Datenquelle nicht lesbar.
|
||||
|
||||
### BizTalk Host Instances
|
||||
|
||||
- `OK`: alle Host-Instanzen sind started.
|
||||
- `WARN`: mindestens eine Host-Instanz ist pending.
|
||||
- `CRIT`: mindestens eine Host-Instanz ist stopped oder unknown.
|
||||
- `UNKNOWN`: keine Host-Instanzen gefunden oder Datenquelle nicht lesbar.
|
||||
|
||||
### BizTalk Runtime Artifacts
|
||||
|
||||
- `OK`: Artefakte lesbar, keine unbekannten Statuswerte.
|
||||
- `WARN`: nur wenn `AlertOnArtifactRuntimeIssues=true` und deaktivierte/inaktive Artefakte vorhanden sind.
|
||||
- `CRIT`: unbekannte Send-Port- oder Orchestration-Statuswerte.
|
||||
|
||||
Deaktivierte Receive Locations und gestoppte Ports koennen in BizTalk fachlich korrekt sein. Deshalb ist die Alarmierung hier standardmaessig informativ.
|
||||
|
||||
### BizTalk Event Log
|
||||
|
||||
- `OK`: keine relevanten Fehler/Warnungen ueber Schwellwert.
|
||||
- `WARN`: Fehler oder Warnungen ab `EventLogWarnThreshold`.
|
||||
- `CRIT`: Fehler ab `EventLogCritThreshold`.
|
||||
- `UNKNOWN`: Event Log nicht lesbar.
|
||||
|
||||
## Dashboard-Empfehlung
|
||||
|
||||
Pro Umgebung sollte der BizTalk-Host in einem eigenen Host-Ordner oder Host-Tag fuer `ACC`, `DEV`, `TST`, `PRD` liegen. Im Dashboard eignen sich:
|
||||
|
||||
- Service State Widgets fuer die fuenf BizTalk-Services
|
||||
- Graphen fuer `biztalk_suspended_total`, `biztalk_host_instances_stopped`, `biztalk_eventlog_errors`
|
||||
- Hostgruppe/Ordner pro Umgebung
|
||||
- Optional eine View gefiltert auf `Service starts with BizTalk`
|
||||
|
||||
Empfohlene Reihenfolge im Dashboard:
|
||||
Die alte Sammelzeile `BizTalk Runtime Artifacts` wurde durch drei kurze
|
||||
Services ersetzt. Insgesamt entstehen neun stabile Services:
|
||||
|
||||
1. `BizTalk Platform`
|
||||
2. `BizTalk Suspended Instances`
|
||||
3. `BizTalk Host Instances`
|
||||
4. `BizTalk Event Log`
|
||||
5. `BizTalk Runtime Artifacts`
|
||||
6. SQL-Server-Services aus dem Checkmk-MSSQL-Plugin
|
||||
2. `BizTalk SQL Access`
|
||||
3. `BizTalk Suspended Instances`
|
||||
4. `BizTalk Host Instances`
|
||||
5. `BizTalk Receive Locations`
|
||||
6. `BizTalk Send Ports`
|
||||
7. `BizTalk Endpoint Reachability`
|
||||
8. `BizTalk Orchestrations`
|
||||
9. `BizTalk Event Log`
|
||||
|
||||
## Rollout-Vorgehen
|
||||
Receive Locations und Send Ports alarmieren nur für unerwartete
|
||||
Aus-Zustände. 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, während
|
||||
alle numerischen Metriken vollständig erhalten bleiben.
|
||||
|
||||
## Troubleshooting
|
||||
### 5.3 Endpoint-Discovery und Netzwerkprobe
|
||||
|
||||
### Service bleibt UNKNOWN
|
||||
Die Endpoint-Funktion erweitert keine WMI-Klassenmenge. Aus den ohnehin
|
||||
minütlich abgefragten Instanzen werden nur dokumentierte Properties gelesen:
|
||||
|
||||
Pruefen:
|
||||
| WMI-Klasse | Properties |
|
||||
| --- | --- |
|
||||
| `MSBTS_SendPort` | `Status`, `IsDynamic`, `PTAddress`, `STAddress`, `PTTransportType`, `STTransportType` |
|
||||
| `MSBTS_ReceiveLocation` | `IsDisabled`, `InboundTransportURL`, `AdapterName` |
|
||||
|
||||
```cmd
|
||||
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd"
|
||||
Nur `Status=Started` beziehungsweise `IsDisabled=false` wird geprüft.
|
||||
Inaktive Artefakte bleiben aus der Probe ausgeschlossen, auch wenn sie noch im
|
||||
Katalog stehen. Dynamische Send Ports, lokale Dateipfade, SMTP-Empfängerlisten
|
||||
und Named Pipes sowie relative HTTP-/WCF-Receive-Listener werden als erwartbar
|
||||
nicht socket-prüfbar behandelt und erzeugen kein `UNKNOWN`. WCF-SQL-Adressen
|
||||
im Format `mssql://server[:port]/instanz/datenbank` werden auf TCP 1433 oder
|
||||
den expliziten Port reduziert. Schemafreie `host:port`-Angaben werden vor der
|
||||
allgemeinen URI-Auswertung erkannt. Bei FTP und SFTP werden zusätzlich Angaben
|
||||
der Form `[benutzer@]host[:port]/pfad` auf Host und expliziten beziehungsweise
|
||||
adaptertypischen Standardport reduziert. Echte Auflösungslücken bleiben
|
||||
sichtbar und nennen Artefakt, Adapter und Grund, aber niemals die vollständige
|
||||
URI.
|
||||
|
||||
Der lokale Katalog
|
||||
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` wird beim ersten
|
||||
erfolgreichen Lauf erzeugt und danach standardmäßig alle 168 Stunden atomar
|
||||
abgeglichen. Er enthält keine vollständigen URIs, Querystrings,
|
||||
Benutzernamen oder Kennwörter, sondern nur Artefaktbezug, Adapter, Host, Port
|
||||
und `TCP`/`UDP`. Automatisch erzeugte Einträge tragen
|
||||
`autoDiscovered="true"`. Ein bewusst manueller Override wird mit
|
||||
`autoDiscovered="false"` markiert und beim Wochenabgleich erhalten; sein
|
||||
`key` muss dem Artefakteintrag entsprechen. Eigenständige manuelle Ziele
|
||||
verwenden `artifactType="Manual"`.
|
||||
|
||||
HTTP(S), SFTP, FTP, UNC/SMB, WCF und `net.tcp` werden ausschließlich per
|
||||
TCP-Connect geprüft. Es findet kein fachlicher Request und keine Anmeldung
|
||||
statt. Explizite `udp://host:port`-Einträge senden ein einzelnes Datagramm;
|
||||
das bestätigt DNS, Route und lokalen Versand, kann ohne
|
||||
applikationsspezifisches Antwortprotokoll aber keinen entfernten Dienstzustand
|
||||
beweisen. Gemeinsame Host/Port-Ziele werden dedupliziert und mit begrenzter
|
||||
Parallelität getestet.
|
||||
|
||||
Der Checkmk-Service nennt im OK-Fall nur die Gesamtzahl. Bei Ausfällen werden
|
||||
ausschließlich die nicht erreichbaren Artefakte in `unavailable=` angezeigt;
|
||||
`MaxSummaryItems` und `MaxDetailCharacters` begrenzen die Darstellung. Alle
|
||||
Einzelfehler bleiben vollständig im Provider-Log.
|
||||
|
||||
### 5.4 SQL-Zugriffsprobe
|
||||
|
||||
Der Provider öffnet für Management- und Master-MessageBox-Datenbank eine
|
||||
kurze `System.Data.SqlClient`-Verbindung mit integrierter
|
||||
Windows-Authentifizierung, führt `SELECT 1` aus und schließt sie. Keine
|
||||
BizTalk-Tabelle wird direkt gelesen oder verändert.
|
||||
|
||||
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.5 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 führte die `BizTalk Server Read Only Users`-Gruppe ein.
|
||||
Sie darf Artefakte, Servicezustand, Message Flow und Trackinginformationen
|
||||
lesen, aber keine administrativen Operationen ausführen. SQL-seitig wird sie
|
||||
über `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
|
||||
```
|
||||
|
||||
Wenn WMI nicht erreichbar ist:
|
||||
Die genaue Gruppe ist aus
|
||||
`MSBTS_GroupSetting.BizTalkReadOnlyUserGroup` beziehungsweise der
|
||||
BizTalk-Administration zu lesen. Nicht vom Standardnamen ausgehen.
|
||||
|
||||
```cmd
|
||||
wmic /namespace:\\root\MicrosoftBizTalkServer path MSBTS_HostInstance get HostName,RunningServer,ServiceState
|
||||
Die Operator-Gruppe (`BTS_OPERATORS`) wird erst geprüft, 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 begründet werden.
|
||||
|
||||
Unzulässig:
|
||||
|
||||
- Checkmk-Dienstkonto ändern,
|
||||
- Maschinenkonto pauschal berechtigen,
|
||||
- individueller SQL-Login für 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 Einträge 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 benötigt, um abgelehnte
|
||||
Snapshots aus dem Consumer-Kontext zu protokollieren.
|
||||
|
||||
## 7. Scheduled Task
|
||||
|
||||
Name:
|
||||
|
||||
```text
|
||||
BizTalk Checkmk Pulse Provider
|
||||
```
|
||||
|
||||
Der Checkmk Windows Agent laeuft normalerweise als LocalSystem. Daher immer auch den Agent-Dump verwenden:
|
||||
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 ausgeführt, also
|
||||
unabhängig von einer interaktiven Anmeldung. Bei Kennwortwechsel oder
|
||||
-ablauf muss das Task-Kennwort aktualisiert werden. Ein gMSA ist weiterhin
|
||||
optional unterstützt, aber nicht die produktive Standardannahme.
|
||||
|
||||
## 8. Installation
|
||||
|
||||
### 8.1 Build
|
||||
|
||||
```cmd
|
||||
scripts\test-release.cmd
|
||||
scripts\package-release.cmd
|
||||
```
|
||||
|
||||
Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen
|
||||
Installer und Packaging-Werkzeug. Danach liegen der Paketordner
|
||||
`artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare
|
||||
`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit.
|
||||
|
||||
### 8.2 Normales Servicekonto
|
||||
|
||||
Das ZIP wird auf dem Zielserver vollständig entpackt. Danach `Setup.exe` als
|
||||
Administrator starten, Konto im Format `DOMAIN\Benutzer` (zum Beispiel
|
||||
`BEW\t231bizmon`), Kennwort und Umgebung eintragen und die Installation
|
||||
starten.
|
||||
|
||||
Der Installer muss als lokaler Administrator laufen. Er vergibt keine
|
||||
AD-/BizTalk-/SQL-Rechte; diese bleiben getrennte administrative Freigaben.
|
||||
|
||||
Beim Update wird zuerst die neue Paketversion im Staging-Verzeichnis mit neun
|
||||
Self-Test-Zeilen validiert. Bestehende AppSettings werden in die neue Config
|
||||
übernommen; neue Keys stammen aus der neuen Standardkonfiguration. Die
|
||||
Runtime-Daten unter `%ProgramData%` bleiben bestehen. Erst danach wird der
|
||||
Task gestoppt und die Programmversion umgeschaltet. Scheitert ein Folgeschritt,
|
||||
versucht der Installer Programmverzeichnis, Checkmk-Wrapper und Scheduled Task
|
||||
auf den vorherigen Stand zurückzusetzen.
|
||||
|
||||
Ab Version 2.2.4 ist die Umschaltung erst nach einer transaktionalen
|
||||
Runtime-Abnahme abgeschlossen. Ein einmaliger, triggerloser Task läuft unter
|
||||
dem echten Collector-Konto und erzwingt einen vollständigen Endpoint-
|
||||
Katalogabgleich. Der Installer wartet begrenzt auf `LastTaskResult=0` und lässt
|
||||
anschließend die installierte Produkt-EXE Snapshot und Katalog mit den normalen
|
||||
Produktparsern validieren. Der Snapshot muss aus diesem Lauf stammen, die
|
||||
erwartete Identität und alle neun eindeutigen Services enthalten. Erst dann
|
||||
wird der normale Minutentask registriert und das Backup
|
||||
gelöscht. Bei Fehler oder Timeout wird die alte Version wiederhergestellt und
|
||||
ein frischer Lauf des alten Tasks abgewartet.
|
||||
|
||||
Version 2.2.5 korrigiert die versionsübergreifende Vorprüfung. Die neue
|
||||
Paket-, Staging- und Zielversion muss weiterhin exakt neun gültige
|
||||
Self-Test-Zeilen liefern. Für die installierte Ausgangsversion wird dagegen
|
||||
deren vollständiger, eindeutiger und ausschließlich grüner Servicevertrag
|
||||
akzeptiert, auch wenn er aus einer älteren Version mit acht Services stammt.
|
||||
Rein additive Services blockieren das Update nicht; entfernte Namen und damit
|
||||
auch Renames bleiben vor der Umschaltung bestätigungspflichtig.
|
||||
|
||||
Parallel erzeugt jeder Installations-/Updatelauf ein separates
|
||||
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`. Protokolliert werden
|
||||
Phase, Setup-/Binary-Version, Pfad und Größe, Self-Test-stdout/-stderr,
|
||||
Exitcodes, Taskstatus, Exception-Kette, letzte Provider-Logzeilen bei einem
|
||||
Abnahmefehler und das Rollback-Ergebnis. Das Collector-Kennwort wird der
|
||||
Logging-Komponente nie übergeben. Ein frisch abgeschlossener Task mit
|
||||
Fehlercode wird sofort erkannt; der Fehler erscheint dezimal und hexadezimal.
|
||||
|
||||
Version 2.2.6 behandelt einen fachlichen UNKNOWN-Zustand der Endpoint-Prüfung
|
||||
nicht mehr als Installationsdefekt. Sind Providerlauf, Snapshot, Identität,
|
||||
neun Services und der erzwungen aktualisierte Katalog technisch valide, darf
|
||||
`BizTalk Endpoint Reachability` als einziger Service `UNKNOWN` bleiben. Die
|
||||
vollständige Local-Check-Zeile wird als Abnahmewarnung ausgegeben und bleibt
|
||||
unverändert für Checkmk erhalten. Damit wird eine unvollständige
|
||||
Endpoint-Auflösung nicht grüngefärbt und zugleich kein funktionsfähiges Update
|
||||
zurückgerollt. UNKNOWN in Platform, SQL Access, Suspensions, Host Instances,
|
||||
Receive Locations, Send Ports, Orchestrations oder Event Log bleibt
|
||||
blockierend, weil dort die Kern-Datenerhebung nicht zuverlässig belegt ist.
|
||||
|
||||
Version 2.2.7 klassifiziert die im RTM-Abnahmelauf identifizierten Adapter
|
||||
präziser. Scheduling-Komponenten sind keine Netzwerkziele und werden komplett
|
||||
ausgenommen. `Database://host[:port]/.../Oracle` wird per TCP auf den
|
||||
expliziten Port oder den Oracle-Standard 1521 geprüft. WCF-SAP verwendet den
|
||||
ersten SAProuter-Hop oder das Paar `ListenerGwHost`/`ListenerGwServ`; dabei
|
||||
entspricht `sapgwNN` dem TCP-Port `33NN`. Diese beiden Adapterklassen laufen
|
||||
als Best-Effort-Diagnose: Ein Fehlschlag bleibt über Metriken sichtbar, erzeugt
|
||||
aber keinen Alarm. Alle regulären und manuellen Ziele behalten ihre bisherige
|
||||
strenge Statuslogik.
|
||||
|
||||
Der installierte Task selbst läuft mit `RunLevel Limited`. Der Installer
|
||||
übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle;
|
||||
es wird weder in einer Prozesskommandozeile noch in Config oder Log
|
||||
geschrieben. Das Konto benötigt `Log on as a batch job`.
|
||||
|
||||
### 8.3 Optionales gMSA
|
||||
|
||||
Im Installer die gMSA-Checkbox aktivieren und ein auf `$` endendes Konto wie
|
||||
`BEW\svc_biztalk_cmk$` eingeben. Ein Kennwort wird dann nicht abgefragt.
|
||||
|
||||
### 8.4 Checkmk
|
||||
|
||||
Der installierte Wrapper liegt unter:
|
||||
|
||||
```text
|
||||
%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd
|
||||
```
|
||||
|
||||
Der Agent darf den Consumer synchron ausführen. Er liest nur maximal 1 MiB
|
||||
lokale Daten; ein Checkmk-Async-Cache ist daher nicht notwendig. Falls alte
|
||||
`check_mk.user.yml`-Regeln für den bisherigen 300-Sekunden-Cache existieren,
|
||||
sollten sie entfernt werden, damit Checkmk den minütlich aktualisierten
|
||||
Snapshot nicht zusätzlich verzögert.
|
||||
|
||||
Nach dem Agent-Dump:
|
||||
|
||||
1. Service Discovery für den BizTalk-Host ausführen.
|
||||
2. neun 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 enthält UTC-Zeit, Level, Komponente, PID, Windows-Identität 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 zusätzlich als eigene `WARN`-Zeile
|
||||
protokolliert und bleibt damit auch außerhalb der gekürzten Checkmk-Summary
|
||||
vollständig nachvollziehbar.
|
||||
|
||||
Logging ist best effort: Ein blockiertes Log darf Checkmk-Ausgabe oder
|
||||
Snapshot-Publikation nicht zerstören. Der Provider entfernt beim Start Dateien
|
||||
älter als `LogRetentionDays`.
|
||||
|
||||
## 10. Diagnose und Runbook
|
||||
|
||||
### 10.1 Grundprüfung
|
||||
|
||||
```cmd
|
||||
schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list
|
||||
dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot"
|
||||
type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
|
||||
"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
|
||||
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
|
||||
```
|
||||
|
||||
### Keine Services in Discovery
|
||||
### 10.2 Kein Snapshot
|
||||
|
||||
Pruefen:
|
||||
Prüfen:
|
||||
|
||||
- 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?
|
||||
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 gültig.
|
||||
5. Provider besitzt Modify auf `data` und `logs`.
|
||||
6. EXE/Config sind ausführbar.
|
||||
|
||||
### Runtime Artifacts zeigt deaktivierte Artefakte
|
||||
### 10.3 Staler Snapshot
|
||||
|
||||
Das ist standardmaessig `OK`, damit gewollt deaktivierte BizTalk-Artefakte nicht alarmieren. Fuer strengere PRD-Standards:
|
||||
Der Consumer nennt Alter und Grenzwert. Ursachen:
|
||||
|
||||
```xml
|
||||
<add key="AlertOnArtifactRuntimeIssues" value="true" />
|
||||
- Task deaktiviert oder Konto/Kennwort ungültig,
|
||||
- vorheriger Lauf hängt,
|
||||
- WMI-/SQL-Timeouts,
|
||||
- Task Scheduler lief während Wartung nicht,
|
||||
- Snapshot-Publikation wird durch AV/EDR oder ACL blockiert.
|
||||
|
||||
Nicht einfach `SnapshotMaxAgeSeconds` stark erhöhen. Zuerst den minütlichen
|
||||
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,
|
||||
Datenträger und AV/EDR prüfen. Wiederholt sich der Fehler, Temporärdateien,
|
||||
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 prüfen.
|
||||
4. neuen Logontoken durch neuen Tasklauf sicherstellen.
|
||||
5. SQL-seitig Gruppenlogin und `BTS_READONLY_USERS` read-only prüfen.
|
||||
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.
|
||||
Berechtigungserhöhung kann eine nicht vorhandene Klasse nicht erzeugen.
|
||||
Installierte BizTalk-Version, WQL und Properties prüfen.
|
||||
|
||||
## 11. Konfiguration
|
||||
|
||||
Alle AppSettings befinden sich in:
|
||||
|
||||
```text
|
||||
%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config
|
||||
```
|
||||
|
||||
## Weiterentwicklung
|
||||
Runtime:
|
||||
|
||||
Sinnvolle naechste Ausbaustufen:
|
||||
| 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 |
|
||||
| `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | absolut |
|
||||
| `EndpointCatalogMaxBytes` | `1048576` | 4096 bis 16777216 |
|
||||
|
||||
- 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.
|
||||
BizTalk-Probes:
|
||||
|
||||
| Key | Default |
|
||||
| --- | --- |
|
||||
| `Server` | `.` |
|
||||
| `EnvironmentName` | leer |
|
||||
| `IncludeEnvironmentInServiceName` | `false` |
|
||||
| `QueryTimeoutSeconds` | `25` |
|
||||
| `ProbeSqlConnectivity` | `true` |
|
||||
| `SqlConnectionTimeoutSeconds` | `5` |
|
||||
| `ProbeEventLog` | `true` |
|
||||
| `EventLogLookbackMinutes` | `60` |
|
||||
| `ProbeEndpointConnectivity` | `true` |
|
||||
| `EndpointDiscoveryIntervalHours` | `168` |
|
||||
| `EndpointProbeTimeoutMilliseconds` | `3000` |
|
||||
| `EndpointProbeMaxConcurrency` | `16` |
|
||||
| `EndpointCatalogMaxEntries` | `1000` |
|
||||
| `EndpointMaxCount` | `100` eindeutige Netzwerkziele |
|
||||
|
||||
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 neun 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 gezählt,
|
||||
- Receive-/Send-Allowlisten trennen bewusste von unerwarteten Aus-Zuständen,
|
||||
- ein fehlender `IsDisabled`-Wert wird `UNKNOWN` statt still als enabled,
|
||||
- betroffene Namen und Gesamtlänge der Summary bleiben begrenzt,
|
||||
- Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert,
|
||||
- manuelle Endpoint-Overrides überleben den automatischen Wochenabgleich,
|
||||
- nur nicht erreichbare oder wirklich unaufgelöste Endpoints erscheinen im
|
||||
Checkmk-Detail,
|
||||
- 70 vollständig timeoutende eindeutige Ziele bleiben mit den Defaults in
|
||||
einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte
|
||||
Maximum von 100 Zielen in etwa 21 Sekunden,
|
||||
- Update-Config-Merge erhält bestehende Werte und fügt neue Defaults hinzu,
|
||||
- erzwungener Installations-Katalogabgleich wird unabhängig vom Katalogalter
|
||||
ausgeführt,
|
||||
- Runtime-Abnahme weist alten/manipulierten Snapshot, falsche Identität,
|
||||
fehlenden Service, Kernservice-`UNKNOWN`, alten/ungültigen Katalog und
|
||||
fehlerhaften oder nicht rechtzeitig beendeten Tasklauf zurück; ein isoliertes
|
||||
Endpoint-Reachability-`UNKNOWN` wird mit vollständigem Detail akzeptiert,
|
||||
- Snapshot-Roundtrip und Ersatz,
|
||||
- SHA-256-Manipulation wird verworfen,
|
||||
- Stale-Snapshot wird verworfen,
|
||||
- Consumer-Fallback enthält neun `UNKNOWN`-Services.
|
||||
|
||||
Windows-/ACC-Abnahme:
|
||||
|
||||
1. `LastTaskResult=0`.
|
||||
2. Snapshot wird mindestens dreimal minütlich 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,
|
||||
einschließlich Routing Failure Reports.
|
||||
8. Eine bewusst deaktivierte Receive Location und einen inaktiven Send Port
|
||||
über die exakten Allowlisten als expected bestätigen.
|
||||
9. Einen Testnamen aus der Allowlist entfernen und den erwarteten CRIT mit
|
||||
kurzem `affected=`-Detail prüfen.
|
||||
10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben.
|
||||
11. `endpoints.xml` wurde erzeugt; sie enthält keine vollständigen URIs oder
|
||||
Zugangsdaten und nur Started-/Enabled-Artefakte.
|
||||
12. `Endpoint Reachability` zeigt `unresolved=0`; erwartbar lokale oder
|
||||
adapterinterne Adressen stehen nur unter `expected_non_socket` und bleiben OK.
|
||||
13. Einen erreichbaren Testport sperren: nur `Endpoint Reachability` wird CRIT
|
||||
und nennt ausschließlich das nicht erreichbare Ziel.
|
||||
14. Ein Artefakt stoppen/deaktivieren: es wird im nächsten Minutenlauf nicht
|
||||
mehr geprüft.
|
||||
15. Katalogzeit auf älter als 168 Stunden setzen beziehungsweise Intervall
|
||||
testweise reduzieren: erfolgreicher atomarer Umgebungsabgleich.
|
||||
16. Task deaktivieren: nach 180 Sekunden neun stale-`UNKNOWN`s.
|
||||
17. Task wieder aktivieren: nächster Snapshot stellt Echtzustand her.
|
||||
18. Agent-Dump und Checkmk Service Discovery erfolgreich.
|
||||
|
||||
Erst nach ACC-Abnahme erfolgt der gestufte Rollout nach DEV/TST/PRD.
|
||||
|
||||
## 13. Deinstallation und Rollback
|
||||
|
||||
`Setup.exe` aus dem entpackten Paket erneut starten und **Deinstallieren**
|
||||
wählen. Der Installer entfernt Task, Wrapper und Programmdateien, behält
|
||||
Logs und Snapshot aber absichtlich bei. Die AD-Gruppenmitgliedschaft wird
|
||||
nicht geändert und 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. Referenzen
|
||||
|
||||
- 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://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sql/create-the-sql-server-connection-uri
|
||||
- https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-oracle-database/create-the-oracle-database-connection-uri
|
||||
- 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,176 +1,480 @@
|
||||
# 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 für Microsoft
|
||||
BizTalk Server 2020 auf Windows Server 2019. Die Anwendung trennt den
|
||||
berechtigten BizTalk-Datenzugriff vollständig 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.
|
||||

|
||||
|
||||
## 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 Identitätswechsel 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:
|
||||
## Endpoint-Erreichbarkeitsprüfung
|
||||
|
||||
- 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 Provider prüft die netzwerktechnische Erreichbarkeit der externen Ziele
|
||||
aktiver BizTalk Send Ports und Receive Locations. Die Funktion ist im eigenen
|
||||
Checkmk-Service `BizTalk Endpoint Reachability` zusammengefasst:
|
||||
|
||||
## Erzeugte Checkmk-Services
|
||||
- Berücksichtigt werden nur Send Ports mit `Status=Started` und Receive
|
||||
Locations mit `IsDisabled=false`. Dynamische Send Ports werden nicht als
|
||||
statisches Ziel interpretiert.
|
||||
- Fehlt der lokale Endpoint-Katalog, wird er aus den bereits gelesenen
|
||||
`MSBTS_SendPort`- und `MSBTS_ReceiveLocation`-Daten aufgebaut. Alle
|
||||
168 Stunden erfolgt ein vollständiger Abgleich mit der BizTalk-Umgebung.
|
||||
- HTTP(S), SFTP, FTP, WCF/`net.tcp` und UNC/SMB werden durch einen reinen
|
||||
TCP-Verbindungsaufbau zu Host und Port geprüft. Explizite
|
||||
`udp://host:port`-Ziele verwenden ein UDP-Datagramm.
|
||||
- Scheduling-Komponenten werden vollständig aus der Endpoint-Prüfung
|
||||
ausgeschlossen. Oracle-`DATABASE`-Ziele werden aus
|
||||
`Database://host[:port]/.../Oracle` auf TCP 1521 beziehungsweise den
|
||||
expliziten Port reduziert. WCF-SAP nutzt den ersten SAProuter-Hop oder
|
||||
`ListenerGwHost`/`ListenerGwServ`; `sapgwNN` wird als TCP `33NN` aufgelöst.
|
||||
- Automatisch abgeleitete DATABASE-/WCF-SAP-Probes sind Best-Effort:
|
||||
erfolgreiche Tests bleiben messbar, nicht ableitbare oder nicht erreichbare
|
||||
Ziele erzeugen jedoch weder `UNKNOWN` noch `WARN`/`CRIT`. Für verbindliche
|
||||
Alarmierung kann weiterhin ein manueller Katalogeintrag verwendet werden.
|
||||
- Es werden weder HTTP-Aufrufe noch Anmeldungen, fachliche Requests oder
|
||||
BizTalk-Nachrichten gesendet. Der Check validiert DNS, Netzwerkpfad,
|
||||
Firewall und bei TCP einen annehmenden Zielport, nicht die fachliche
|
||||
Funktion des entfernten Dienstes.
|
||||
- Im OK-Fall meldet Checkmk nur, dass alle aktiven Endpunkte erreichbar sind.
|
||||
Bei einem Fehler werden ausschließlich die nicht erreichbaren Endpunkte
|
||||
ausgegeben; lange Listen sind begrenzt, die Metriken bleiben vollständig.
|
||||
- Doppelte Protokoll-/Host-/Port-Ziele werden nur einmal verbunden. Mit den Defaults
|
||||
benötigen 70 vollständig timeoutende eindeutige Ziele theoretisch etwa
|
||||
15 Sekunden; die harte Standardgrenze von 100 Zielen liegt bei etwa
|
||||
21 Sekunden.
|
||||
|
||||
Standardmaessig entstehen diese stabilen Services:
|
||||
Der Checkmk-Agent führt diese Netzwerkprobes nicht selbst aus. Er liest als
|
||||
`LocalSystem` weiterhin ausschließlich den validierten lokalen Snapshot.
|
||||
Details zum Katalog und zu manuellen Overrides stehen in
|
||||
[docs/EndpointCatalog.md](docs/EndpointCatalog.md).
|
||||
|
||||
## Warum die Architektur geändert wurde
|
||||
|
||||
Der ACC-Test vom 29.07.2026 zeigte:
|
||||
|
||||
```text
|
||||
Login failed for user 'BEW\AV23AGPWBIO1$'
|
||||
```
|
||||
|
||||
Der Checkmk-Agent lief korrekt als `NT AUTHORITY\SYSTEM` und erreichte lokales
|
||||
BizTalk-WMI. Datenbankgestützte WMI-Abfragen wurden jedoch am SQL Server als
|
||||
Maschinenkonto `BEW\AV23AGPWBIO1$` abgewiesen. Eine Mitgliedschaft des
|
||||
Computerkontos würde die BizTalk-Netzwerkberechtigung allen als `LocalSystem`
|
||||
laufenden Diensten des Servers geben.
|
||||
|
||||
Die neue Trennung reduziert diesen Berechtigungsumfang. Für 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 benötigte WMI-Klasse trotz bestätigter Read-Only-Zuordnung
|
||||
abgewiesen wird. Direkte SQL-Logins, manuelle Datenbankrollen und `sysadmin`
|
||||
sind keine Lösung.
|
||||
|
||||
## Robustheit
|
||||
|
||||
Der Datenaustausch ist bewusst defensiv:
|
||||
|
||||
- Der Provider schreibt zuerst eine eindeutige Temporärdatei im Zielordner,
|
||||
leert Betriebssystempuffer und ersetzt danach den Snapshot atomar.
|
||||
- Der Snapshot enthält Formatversion, UTC-Zeit, Quellmaschine,
|
||||
Provider-Identität, Zeilenanzahl und SHA-256 des Payloads.
|
||||
- Der Consumer akzeptiert nur denselben Rechner, gültiges UTF-8, intakte
|
||||
Checkmk-Zeilen, korrekte SHA-256-Prüfsumme und ein maximales Alter von
|
||||
standardmäßig 180 Sekunden.
|
||||
- Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien
|
||||
ergeben neun gültige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe.
|
||||
- Ein exklusives Lock und die Task-Einstellung `IgnoreNew` verhindern
|
||||
überlappende Providerläufe.
|
||||
- Ein unerwarteter Providerfehler erzeugt nach Möglichkeit einen aktuellen
|
||||
`UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode.
|
||||
- Provider und Consumer protokollieren in tägliche Dateien; die Aufbewahrung
|
||||
ist standardmäßig 30 Tage.
|
||||
- Jeder Installations-/Updatelauf schreibt zusätzlich ein eigenes
|
||||
`setup-*.log` mit Phase, Binary-Version, Self-Test-Ausgabe,
|
||||
Task-Scheduler-Status, Exception-Kette und Rollback-Ergebnis. Kennwörter
|
||||
werden nicht an das Logging übergeben.
|
||||
- Snapshotgröße, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind
|
||||
begrenzt und konfigurierbar.
|
||||
|
||||
## Erzeugte Services
|
||||
|
||||
Standardmäßig entstehen:
|
||||
|
||||
- `BizTalk Platform`
|
||||
- `BizTalk SQL Access`
|
||||
- `BizTalk Suspended Instances`
|
||||
- `BizTalk Host Instances`
|
||||
- `BizTalk Runtime Artifacts`
|
||||
- `BizTalk Receive Locations`
|
||||
- `BizTalk Send Ports`
|
||||
- `BizTalk Endpoint Reachability`
|
||||
- `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
|
||||
- Endpoint Reachability: nur aktive Send-/Receive-Artefakte; im OK-Fall nur
|
||||
eine Gesamtaussage, im Fehlerfall ausschließlich nicht erreichbare oder
|
||||
wirklich unaufgelöste Ziele
|
||||
- Orchestrations: total, started, stopped, bound, unbound und unbekannt
|
||||
|
||||
## Build
|
||||
Der Provider nutzt für die Endpoint-Discovery keine neue WMI-Klasse. Er liest
|
||||
`PTAddress`, `STAddress`, `PTTransportType`, `STTransportType`,
|
||||
`InboundTransportURL` und `AdapterName` aus den bereits vorhandenen
|
||||
`MSBTS_SendPort`-/`MSBTS_ReceiveLocation`-Abfragen. Der Check selbst ist ein
|
||||
reiner Host/Port-Test: HTTP(S), SFTP, FTP, WCF/net.tcp, WCF-SQL/mssql und UNC
|
||||
werden per TCP geprüft; explizite `udp://`-Ziele per UDP-Datagramm. Es werden
|
||||
keine HTTP-Requests, Anmeldungen oder fachlichen Nachrichten gesendet.
|
||||
Auch schemafreie `host:port`-Angaben sowie FTP-/SFTP-Adressen der Form
|
||||
`[benutzer@]host[:port]/pfad` werden erkannt; Benutzer und Pfad werden dabei
|
||||
nicht in den Endpoint-Katalog übernommen.
|
||||
|
||||
Voraussetzungen auf einem Windows-Build-Host:
|
||||
Nicht jede aktive BizTalk-Adresse ist selbst ein Socket-Ziel. Dynamische Send
|
||||
Ports, lokale Pfade, SMTP-Empfänger, Named Pipes, relative HTTP-/WCF-
|
||||
Receive-Listener, Scheduling-Komponenten sowie nicht sicher ableitbare
|
||||
DATABASE-/WCF-SAP-Adressen werden deshalb als erwartbar nicht socket-prüfbar
|
||||
behandelt und erzeugen kein `UNKNOWN`. Nur eine andere aktive statische
|
||||
Adresse, die tatsächlich ein Netzwerkziel enthalten müsste und nicht sicher
|
||||
aufgelöst werden kann, bleibt `UNKNOWN` und wird geheimnisfrei mit Artefakt,
|
||||
Adapter und Grund genannt.
|
||||
|
||||
- Visual Studio 2022 Build Tools oder Visual Studio
|
||||
Der geheimnisfreie Katalog liegt unter
|
||||
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml`. Fehlt er, wird er beim
|
||||
nächsten erfolgreichen Providerlauf erstellt. Alle 168 Stunden wird er gegen
|
||||
die Umgebung abgeglichen. Automatisch verwaltete Einträge für inzwischen
|
||||
inaktive Artefakte verschwinden beim Abgleich; bei jedem Minutenlauf werden
|
||||
sie zusätzlich gegen den aktuellen Started-/Enabled-Zustand gefiltert.
|
||||
|
||||
Pro Service werden standardmäßig maximal fünf betroffene Namen gezeigt.
|
||||
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusätzlich auf
|
||||
1600 Zeichen begrenzt. Metriken bleiben trotzdem vollständig.
|
||||
|
||||
`EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` kennzeichnet Snapshot und
|
||||
Endpoint-Katalog. Die neun Checkmk-Servicenamen bleiben standardmäßig
|
||||
unverändert. Nur das explizite Opt-in
|
||||
`IncludeEnvironmentInServiceName=true` ergänzt die Umgebung im Servicenamen
|
||||
und verlangt anschließend eine Checkmk Service Discovery.
|
||||
|
||||
Statuslogik und Metriken: [docs/CheckmkServices.md](docs/CheckmkServices.md)
|
||||
|
||||
Beispielausgaben: [docs/ExampleOutput.md](docs/ExampleOutput.md)
|
||||
|
||||
Endpoint-Katalog, manuelle Overrides und Protokollgrenzen:
|
||||
[docs/EndpointCatalog.md](docs/EndpointCatalog.md)
|
||||
|
||||
Die RTM-Adapterregeln und ihre Herstellerquellen sind in
|
||||
[docs/RTM-Endpoint-Adapterregeln-2026-08-11.md](docs/RTM-Endpoint-Adapterregeln-2026-08-11.md)
|
||||
festgehalten.
|
||||
|
||||
## 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 für die einmalige Installation
|
||||
- normales dediziertes AD-Servicekonto für den Provider
|
||||
|
||||
Das Provider-Konto benötigt:
|
||||
|
||||
- lokales Recht zur Ausführung als Scheduled Task
|
||||
- lokalen Lese-/Ausführungszugriff 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 läuft 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 für 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:
|
||||
Jeder Release-Build der Solution erzeugt automatisch den fertigen Paketordner
|
||||
und das transportierbare ZIP:
|
||||
|
||||
```text
|
||||
artifacts\BizTalkCheckmkPulse-deploy\
|
||||
artifacts\
|
||||
BizTalkCheckmkPulse-Setup.zip
|
||||
BizTalkCheckmkPulse-Setup\
|
||||
Setup.exe
|
||||
INSTALLATION.md
|
||||
biztalk_checkmk_pulse.cmd
|
||||
BizTalkCheckmkPulse\
|
||||
application\
|
||||
BizTalkCheckmkPulse.exe
|
||||
BizTalkCheckmkPulse.exe.config
|
||||
```
|
||||
|
||||
Validierung nach dem Build:
|
||||
Das Packaging-Projekt ist Teil von `BizTalkCheckmkPulse.sln`. Ein normaler
|
||||
`Release`-Build in Visual Studio 2019 genügt daher; ein separates
|
||||
PowerShell-/WiX-/MSI-Tooling wird nicht benötigt.
|
||||
|
||||
Format-Self-Test ohne WMI, SQL oder Event Log:
|
||||
|
||||
```cmd
|
||||
artifacts\BizTalkCheckmkPulse-deploy\biztalk_checkmk_pulse.cmd --self-test
|
||||
artifacts\BizTalkCheckmkPulse-Setup\application\BizTalkCheckmkPulse.exe --self-test
|
||||
```
|
||||
|
||||
## Installation auf dem BizTalk-Server
|
||||
Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests prüfen
|
||||
zusätzlich Snapshot-Roundtrip, atomaren Ersatz, SHA-256-Manipulation,
|
||||
Stale-Erkennung, stabile Fallbacks, den exakten Servicevertrag, erzwungene
|
||||
Katalogaktualisierung sowie erfolgreiche und abgewiesene
|
||||
Post-Install-Runtime-Abnahmen.
|
||||
Ein Mono-Build ist eine hilfreiche Quellcodeprüfung, ersetzt aber nicht die
|
||||
Windows-/BizTalk-Laufzeitvalidierung.
|
||||
|
||||
Kopiere den Inhalt von `artifacts\BizTalkCheckmkPulse-deploy` nach:
|
||||
## Berechtigung vorbereiten
|
||||
|
||||
```text
|
||||
%ProgramData%\checkmk\agent\local
|
||||
```
|
||||
Die exakte Read-Only-Gruppe wird in der BizTalk Administration Console unter
|
||||
den Eigenschaften der BizTalk-Gruppe abgelesen.
|
||||
|
||||
Zielstruktur:
|
||||
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.
|
||||
|
||||
```text
|
||||
%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd
|
||||
%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe
|
||||
%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config
|
||||
```
|
||||
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 für die Gruppe repariert, nicht als Einzelberechtigung
|
||||
für das Provider-Konto.
|
||||
|
||||
Manueller Test auf dem BizTalk-Server:
|
||||
## Installation mit normalem Servicekonto
|
||||
|
||||
1. `BizTalkCheckmkPulse-Setup.zip` auf dem Zielserver vollständig entpacken.
|
||||
2. `Setup.exe` starten und die UAC-Abfrage bestätigen.
|
||||
3. Collector-Konto, zum Beispiel `BEW\t231bizmon`, Kennwort und Umgebung
|
||||
eingeben.
|
||||
4. **Installieren / aktualisieren** wählen.
|
||||
|
||||
Der Installer übergibt das Kennwort direkt über die Windows-Task-Scheduler-
|
||||
COM-Schnittstelle. Es steht weder in einer Prozesskommandozeile noch in der
|
||||
Konfigurationsdatei oder in Logs. Der Backslash in `DOMAIN\Benutzer` wird als
|
||||
normaler Bestandteil des Kontonamens verarbeitet.
|
||||
|
||||
Wenn das Kennwort rotiert oder abläuft, muss es im Scheduled Task aktualisiert
|
||||
werden. Dazu `Setup.exe` mit dem neuen Kennwort erneut ausführen. Bis dahin
|
||||
wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle neun Services
|
||||
als `UNKNOWN`.
|
||||
|
||||
Ein gMSA kann über die Checkbox ohne Kennworteingabe installiert werden; die
|
||||
produktive Standardbeschreibung geht vom normalen Servicekonto aus.
|
||||
|
||||
Der Installer (ab Version 2.2.6):
|
||||
|
||||
1. prüft Paket und Staging zwingend auf neun Self-Test-Zeilen, bevor der
|
||||
bestehende Task angehalten wird. Der Servicevertrag der installierten
|
||||
Legacy-Version darf weniger gültige Zeilen enthalten,
|
||||
2. baut die neue Version in einem Staging-Verzeichnis auf und übernimmt
|
||||
vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten
|
||||
unveränderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert,
|
||||
3. vergleicht installierten und neuen Checkmk-Servicevertrag. Rein additive
|
||||
Services werden zugelassen und als Discovery-Hinweis gemeldet; entfernte
|
||||
oder umbenannte Services stoppen ohne ausdrückliche Bestätigung vor jeder
|
||||
Umschaltung,
|
||||
4. schaltet die Programmdateien per Verzeichniswechsel um,
|
||||
5. startet einen triggerlosen Provider-Abnahmelauf unter dem echten
|
||||
Collector-Konto und erzwingt einen vollständigen Endpoint-Katalogabgleich,
|
||||
6. wartet höchstens vier Minuten auf `LastTaskResult=0`, bricht bei einem
|
||||
bereits abgeschlossenen Fehlerlauf sofort mit Dezimal-/Hexcode ab und validiert danach
|
||||
mit der Produkt-EXE einen erst nach Installationsbeginn erzeugten Snapshot,
|
||||
die Collector-Identität, alle neun eindeutigen Services sowie den frischen,
|
||||
maschinen- und umgebungsgebundenen Endpoint-Katalog. `UNKNOWN` in einem
|
||||
Kernservice bleibt ein Abnahmefehler. Ein ausschließlich bei
|
||||
`BizTalk Endpoint Reachability` auftretendes `UNKNOWN` wird als sichtbare
|
||||
Betriebswarnung übernommen, weil es eine unvollständige Prüfabdeckung und
|
||||
keinen defekten Installationslauf beschreibt,
|
||||
7. registriert erst nach dieser Abnahme den normalen Minutentask; andernfalls
|
||||
werden vorherige Version, Wrapper und Task samt frischem Altsnapshot
|
||||
wiederhergestellt,
|
||||
8. behält `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot
|
||||
und Logs bei. Das laufbezogene Setup-Log enthält auch die vollständige
|
||||
Self-Test-Ausgabe sowie bei Abnahmefehlern die letzten Provider-Logzeilen,
|
||||
9. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`, setzt
|
||||
explizite ACLs für Administratoren, Provider und `LocalSystem` und
|
||||
installiert nur den kleinen `.cmd`-Consumer unter
|
||||
`%ProgramData%\checkmk\agent\local`,
|
||||
10. registriert `BizTalk Checkmk Pulse Provider` minütlich mit `IgnoreNew`,
|
||||
fünf Minuten Laufzeitlimit und zwei Wiederholungen.
|
||||
|
||||
Installation, Update, Deinstallation und Laufzeit verwenden keine PowerShell.
|
||||
|
||||
## Verifikation auf dem Server
|
||||
|
||||
Task und letzter Lauf:
|
||||
|
||||
```cmd
|
||||
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test
|
||||
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd"
|
||||
schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list
|
||||
```
|
||||
|
||||
Agent-Ausgabe wie Checkmk sie sieht:
|
||||
Provider-Log:
|
||||
|
||||
```cmd
|
||||
dir /o-d "%ProgramData%\BizTalkCheckmkPulse\logs"
|
||||
type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
|
||||
type "%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log"
|
||||
```
|
||||
|
||||
Die Analyse des in Version 2.2.4 beobachteten Abbruchs mit
|
||||
`Exitcode=0, Zeilen=8` ist in
|
||||
[docs/Installer-Self-Test-Analyse-2026-08-11.md](docs/Installer-Self-Test-Analyse-2026-08-11.md)
|
||||
dokumentiert.
|
||||
|
||||
Der nachfolgende ACC-Abbruch mit einem technisch validen Snapshot und
|
||||
`UNKNOWN=[BizTalk Endpoint Reachability]` ist in
|
||||
[docs/Installer-Runtime-Unknown-Analyse-2026-08-11.md](docs/Installer-Runtime-Unknown-Analyse-2026-08-11.md)
|
||||
analysiert. Der Zustand wird nicht grüngefärbt: Setup 2.2.6 schließt ab und
|
||||
gibt die vollständige UNKNOWN-Zeile als Warnung aus; Checkmk behält Status 3.
|
||||
|
||||
Snapshot und Consumer:
|
||||
|
||||
```cmd
|
||||
dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot"
|
||||
"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
|
||||
```
|
||||
|
||||
Verbindlicher Test im echten `LocalSystem`-Kontext:
|
||||
|
||||
```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.
|
||||
Danach in Checkmk eine Service Discovery ausführen, die neun Services
|
||||
aufnehmen und Changes aktivieren. Ein zusätzlicher 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
|
||||
%ProgramFiles%\BizTalkCheckmkPulse\
|
||||
BizTalkCheckmkPulse.exe
|
||||
BizTalkCheckmkPulse.exe.config
|
||||
|
||||
%ProgramData%\BizTalkCheckmkPulse\
|
||||
data\
|
||||
biztalk-checkmk-pulse.snapshot
|
||||
biztalk-checkmk-pulse.snapshot.provider.lock
|
||||
endpoints.xml
|
||||
logs\
|
||||
biztalk-checkmk-pulse-YYYYMMDD.log
|
||||
|
||||
%ProgramData%\checkmk\agent\local\
|
||||
biztalk_checkmk_pulse.cmd
|
||||
```
|
||||
|
||||
ACL-Soll:
|
||||
|
||||
| Pfad | Provider | LocalSystem | Administratoren |
|
||||
| --- | --- | --- | --- |
|
||||
| Programm | Lesen/Ausführen | Lesen/Ausführen | Vollzugriff |
|
||||
| `data` | Ändern | Lesen/Ausführen | Vollzugriff |
|
||||
| `logs` | Ändern | Ändern | Vollzugriff |
|
||||
|
||||
Der Snapshot enthält Monitoringzustand und kompakte Fehlerdetails, aber keine
|
||||
Passwörter oder Nachrichteninhalte. Der Consumer validiert die Datei trotzdem
|
||||
vollständig, 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. |
|
||||
| `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`. |
|
||||
| `IncludeEnvironmentInServiceName` | `false` | Nur mit geplanter Service Discovery aktivieren. |
|
||||
| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | Gemeinsame Provider-/Consumer-Datei. |
|
||||
| `SnapshotMaxAgeSeconds` | `180` | Ab diesem Alter liefert der Consumer `UNKNOWN`. |
|
||||
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegröße für den Consumer. |
|
||||
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. |
|
||||
| `LogRetentionDays` | `30` | Provider bereinigt ältere Logs. |
|
||||
| `ProbeEndpointConnectivity` | `true` | Aktiviert den aggregierten TCP-/UDP-Netzwerkcheck. |
|
||||
| `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollständige URIs/Secrets. |
|
||||
| `EndpointCatalogMaxEntries` | `1000` | Harte Größenbegrenzung für Artefakte im Katalog. |
|
||||
| `EndpointDiscoveryIntervalHours` | `168` | Intervall für den vollständigen Umgebungsabgleich. |
|
||||
| `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. |
|
||||
| `EndpointProbeMaxConcurrency` | `16` | Begrenzte parallele Socket-Probes. |
|
||||
| `EndpointMaxCount` | `100` | Obergrenze eindeutiger Host/Port/Protokoll-Ziele und damit Laufzeitbudget; bei 3 s Timeout theoretisch maximal etwa 21 s. |
|
||||
| `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 für stopped/bound/unbound Orchestrations. |
|
||||
| `MaxSummaryItems` | `5` | Maximal angezeigte betroffene Artefakte je Service. |
|
||||
| `MaxDetailCharacters` | `1600` | Harte Obergrenze für Checkmk-Summary. |
|
||||
| `EmitPerApplicationSuspensionServices` | `false` | Zusätzliche Anwendungsservices. |
|
||||
| `EventLogLookbackMinutes` | `60` | Event-Log-Zeitfenster des Providers. |
|
||||
|
||||
Empfehlung:
|
||||
Nach einer Config-Änderung den Scheduled Task manuell starten. Der Consumer
|
||||
liest den nächsten 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 / Maßnahme |
|
||||
| --- | --- |
|
||||
| Alle neun Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto/Kennwort und ACL prüfen. |
|
||||
| Snapshot ist `stale` | `LastTaskResult`, Laufzeit, WMI-/SQL-Timeout und Log prüfen. |
|
||||
| SHA-256 oder Format ungültig | Datei nicht manuell bearbeiten; Datenträger/AV und Schreibpfad prüfen, Task neu starten. |
|
||||
| Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` prüfen. |
|
||||
| Receive Locations / Send Ports sind CRIT | `affected=` prüfen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. |
|
||||
| Endpoint Reachability ist CRIT | Nur `unavailable=` prüfen; DNS, Zielport, Firewall und externen Dienst kontrollieren. |
|
||||
| Endpoint Reachability ist UNKNOWN | `unresolved_endpoints=`, WMI-Vollständigkeit, `endpoints.xml` und wöchentlichen Refresh prüfen. Schedule sowie nicht ableitbare DATABASE-/WCF-SAP-Ziele sind ab 2.2.7 bereits ausgenommen; andere indirekte Ziele bei Bedarf geheimnisfrei manuell überschreiben. |
|
||||
| `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema prüfen; keine Rechte ausweiten. |
|
||||
| Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos prüfen. |
|
||||
| Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL prüfen. |
|
||||
|
||||
Aufgaben der Checkmk-Kollegen:
|
||||
Ausführliche 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.
|
||||
## Deinstallation
|
||||
|
||||
Manuelle Integration:
|
||||
`Setup.exe` erneut starten und **Deinstallieren** wählen. Snapshot und Logs
|
||||
bleiben dabei unter `%ProgramData%\BizTalkCheckmkPulse` erhalten.
|
||||
|
||||
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.
|
||||
|
||||
Empfohlene Alarmierung:
|
||||
|
||||
- `BizTalk Platform`: `UNKNOWN` immer untersuchen, da dann WMI, Rechte oder Deployment betroffen sind.
|
||||
- `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 AD-Gruppenmitgliedschaft des Provider-Kontos wird bewusst nicht automatisch
|
||||
geändert und muss separat durch AD-/BizTalk-Administration entfernt werden.
|
||||
|
||||
## 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 `MSBTS_GroupSetting.BizTalkReadOnlyUserGroup`
|
||||
- Microsoft: Windows Groups and User Accounts in BizTalk Server
|
||||
- Microsoft: Managing BizTalk Server Security
|
||||
- Microsoft: [`MSBTS_SendPort` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi)
|
||||
- Microsoft: [`MSBTS_ReceiveLocation` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi)
|
||||
- Microsoft: [SQL-Adapter-Verbindungs-URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sql/create-the-sql-server-connection-uri)
|
||||
- Microsoft: [Oracle-Database-Adapter-Verbindungs-URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-oracle-database/create-the-oracle-database-connection-uri)
|
||||
- Checkmk: Windows Agent und Local Checks
|
||||
|
||||
Die genauen Links stehen in [Dokumentation.md](Dokumentation.md).
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Installation auf dem BizTalk-Server
|
||||
|
||||
1. `BizTalkCheckmkPulse-Setup.zip` vollständig in ein lokales Verzeichnis entpacken.
|
||||
2. `Setup.exe` als lokaler Administrator starten und die UAC-Abfrage bestätigen.
|
||||
3. Das Collector-Konto im Format `DOMAIN\Benutzer` eingeben, zum Beispiel
|
||||
`BEW\t231bizmon`.
|
||||
4. Kennwort und Umgebung eingeben und **Installieren / aktualisieren** wählen.
|
||||
5. Im Aufgabenplaner den Task `BizTalk Checkmk Pulse Provider` und danach den
|
||||
Checkmk-Agent-Dump kontrollieren.
|
||||
|
||||
Bei einer bereits vorhandenen ACC-Installation ist dies ein Update. Der
|
||||
Installer erkennt die bestehende Umgebung, behält Endpoint-Katalog, Snapshot
|
||||
und Logs sowie vorhandene AppSettings bei und ergänzt neue Config-Keys aus dem
|
||||
Paket. Das BEW-Konto und sein aktuelles Kennwort müssen erneut eingegeben
|
||||
werden, weil der Scheduled Task mit den bestätigten Zugangsdaten neu
|
||||
registriert wird.
|
||||
|
||||
Die früheren unveränderten Endpoint-Defaults `12` parallele Probes und
|
||||
`EndpointMaxCount=500` werden beim Update auf `16` beziehungsweise `100`
|
||||
migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten.
|
||||
`EndpointMaxCount` begrenzt nun eindeutige Socket-Ziele statt Artefakte; der
|
||||
neue Wert `EndpointCatalogMaxEntries=1000` begrenzt separat die Kataloggröße.
|
||||
|
||||
Version 2.2.2 ergänzt `IncludeEnvironmentInServiceName=false`. Damit bleibt
|
||||
`EnvironmentName=ACC` als Umgebungsmetadatum erhalten, ohne die bestehenden
|
||||
Services von `BizTalk ...` in `BizTalk ACC ...` umzubenennen. Das beseitigt
|
||||
nach dem nächsten Agentenlauf das 2.2.1-Fehlerbild `Item not found in
|
||||
monitoring data`. Nur bei bewusst auf `true` gesetztem Opt-in ist anschließend
|
||||
eine Checkmk Service Discovery erforderlich.
|
||||
|
||||
Ab Version 2.2.5 vergleicht der Installer bei jedem Update die exakten
|
||||
Servicenamen der installierten und der vorbereiteten Version. Rein neue
|
||||
Services sind abwärtskompatibel, blockieren das Update nicht und werden als
|
||||
Hinweis für die anschließende Service Discovery angezeigt. Entfernte oder
|
||||
umbenannte Services führen **vor dem Stoppen des Tasks** zu einem
|
||||
Sicherheitsstopp. Die Installation darf dann nur über die Checkbox
|
||||
**Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant**
|
||||
fortgesetzt werden. Der Installer zeigt dabei die entfernten und neuen Namen
|
||||
an. Eine bloß geänderte Ausgabereihenfolge gilt nicht als Rename.
|
||||
|
||||
Version 2.2.5 akzeptiert außerdem den gültigen Self-Test einer installierten
|
||||
Legacy-Version mit weniger Services. Paket, Staging und aktivierte Zielversion
|
||||
müssen weiterhin exakt neun Services liefern. Damit kann insbesondere eine
|
||||
Version mit acht Services auf die Version mit dem zusätzlichen Service
|
||||
`BizTalk Endpoint Reachability` aktualisiert werden. Der frühere Abbruch
|
||||
`Exitcode=0, Zeilen=8` betraf die Altversion und war kein Fehler im neuen Paket.
|
||||
|
||||
Version 2.2.6 trennt technische Installationsabnahme und fachlichen
|
||||
Endpoint-Zustand. Ein frischer, integritäts- und identitätsgeprüfter Snapshot
|
||||
mit allen neun Services und ein frisch synchronisierter Endpoint-Katalog
|
||||
belegen eine funktionsfähige Installation. Meldet ausschließlich
|
||||
`BizTalk Endpoint Reachability` weiterhin `UNKNOWN`, wird die Installation
|
||||
abgeschlossen und die vollständige Checkmk-Zeile als Warnung im Setup sowie
|
||||
im Setup-Log ausgegeben. Der Status bleibt im Snapshot und in Checkmk
|
||||
unverändert `UNKNOWN`, bis Auflösungslücken, Katalog-/Probeprobleme oder
|
||||
fehlende manuelle Overrides betrieblich geklärt sind. `UNKNOWN` in Platform,
|
||||
SQL oder einem anderen Kernservice bleibt ein blockierender Abnahmefehler.
|
||||
|
||||
Version 2.2.7 beseitigt die bekannten fachlichen Fehlalarme der
|
||||
Endpoint-Prüfung: Scheduling-Komponenten werden ausgeschlossen; Oracle-
|
||||
DATABASE- und WCF-SAP-Ziele werden als TCP-Best-Effort-Probes abgeleitet.
|
||||
Nicht ableitbare oder nicht erreichbare Best-Effort-Ziele lösen weder UNKNOWN
|
||||
noch CRIT aus. Reguläre und manuell konfigurierte Ziele bleiben unverändert
|
||||
streng alarmierend.
|
||||
|
||||
Version 2.2.4 nimmt die umgeschaltete Installation zusätzlich vollständig ab:
|
||||
|
||||
- einmaliger, triggerloser Providerlauf unter dem angegebenen Collector-Konto,
|
||||
- erzwungener vollständiger Abgleich von `endpoints.xml`, wobei gültige
|
||||
manuelle Overrides erhalten bleiben,
|
||||
- maximal vier Minuten Warten auf `LastTaskResult=0`,
|
||||
- Snapshot muss nach Beginn des Abnahmelaufs erzeugt, SHA-256-valid, für die
|
||||
aktuelle Maschine und vom erwarteten Collector-Konto geschrieben sein,
|
||||
- alle neun stabilen Services müssen eindeutig vorhanden sein; `WARN` und
|
||||
`CRIT` sind reale Betriebszustände. `UNKNOWN` in Kernservices blockiert die
|
||||
Installation; ausschließlich `BizTalk Endpoint Reachability = UNKNOWN`
|
||||
wird ab 2.2.6 als nicht blockierende Betriebswarnung übernommen,
|
||||
- bei aktivierter Endpoint-Prüfung muss auch der Katalog nach Beginn des
|
||||
Abnahmelaufs atomar synchronisiert worden sein.
|
||||
|
||||
Erst danach ersetzt der Installer den Abnahmetask durch den normalen
|
||||
Minutentask und löscht das Backup. Bei Fehler oder Timeout wird die vorherige
|
||||
Programmversion samt Task wiederhergestellt und ein frischer Lauf der alten
|
||||
Version abgewartet. Fehler in Paket-, Config- oder Rename-Vorprüfung treten vor
|
||||
jeder Taskänderung auf und lassen die laufende Installation unangetastet.
|
||||
|
||||
Jeder Installations-/Updatelauf schreibt ein separates Diagnoselog nach
|
||||
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`. Es enthält die aktuelle
|
||||
Phase, Pfad und Version jeder geprüften EXE, Exitcode, vollständiges stdout und
|
||||
stderr des Self-Tests, Taskstatus samt Dezimal-/Hexcode, Exception-Kette und
|
||||
Rollback-Ergebnis. Bei einem Runtime-Abnahmefehler werden zusätzlich die
|
||||
letzten Provider-Logzeilen übernommen. Das Collector-Kennwort wird weder an
|
||||
das Logging übergeben noch protokolliert. Ein abgeschlossener Providerlauf mit
|
||||
Fehlercode beendet die Abnahme sofort; nur ein noch laufender Task wird bis zur
|
||||
Timeoutgrenze abgewartet.
|
||||
|
||||
Beim korrigierenden Wechsel von 2.2.1 mit `BizTalk ACC ...` auf stabile
|
||||
`BizTalk ...`-Namen ist die Änderung beabsichtigt: Checkbox aktivieren und den
|
||||
Servicebestand danach per Discovery abgleichen. Sind in Checkmk bereits die
|
||||
unpräfixierten Services vorhanden, werden damit insbesondere eventuell
|
||||
vorhandene falsche `BizTalk ACC ...`-Services bereinigt.
|
||||
|
||||
Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet
|
||||
und ihr Checkmk-Servicevertrag mit der installierten Version verglichen.
|
||||
Bei normalen Servicekonten prüft der Installer außerdem Kennwort und
|
||||
Batch-Anmelderecht vor der Umschaltung.
|
||||
Der Task wird vor dem Dateitausch deaktiviert und sein Prozessende maximal zehn
|
||||
Sekunden abgewartet, damit keine laufende Provider-EXE überschrieben wird.
|
||||
Bei einem Fehler nach der Umschaltung versucht der Installer, vorherige
|
||||
Programmdateien, Wrapper und Task wiederherzustellen.
|
||||
|
||||
PowerShell wird für Installation, Update, Deinstallation und Laufzeit nicht
|
||||
benötigt. Das Kennwort wird direkt an die Windows-Aufgabenplanung übergeben
|
||||
und weder in einer Datei noch in einer Prozesskommandozeile abgelegt.
|
||||
|
||||
Der Installer vergibt keine AD-, BizTalk- oder SQL-Berechtigungen. Das Konto
|
||||
muss separat Mitglied der für die BizTalk-Gruppe konfigurierten Read-Only-
|
||||
Gruppe sein und lokal das Recht `Log on as a batch job` besitzen.
|
||||
|
||||
Bei einer Deinstallation bleiben Snapshot und Logs absichtlich unter
|
||||
`%ProgramData%\BizTalkCheckmkPulse` erhalten. Sie können nach der
|
||||
Betriebsfreigabe manuell entfernt werden.
|
||||
@@ -1,14 +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 not found below %BASE%
|
||||
echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Setup.exe 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" - 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,174 @@
|
||||
# ACC-Analyse: Checkmk-Datenalter und Endpoint Reachability
|
||||
|
||||
> Nachtrag 11.08.2026: Version 2.2.7 behebt die aus der RTM-Ausgabe
|
||||
> identifizierten Schedule-, Oracle-DATABASE- und WCF-SAP-Sonderfälle. Die
|
||||
> verbindliche Nachfolgeregel steht in
|
||||
> [RTM-Endpoint-Adapterregeln-2026-08-11.md](RTM-Endpoint-Adapterregeln-2026-08-11.md).
|
||||
|
||||
## Befund
|
||||
|
||||
Die am 10.08.2026 bereitgestellten ACC-Artefakte zeigen zwei getrennte
|
||||
Fehlerbilder.
|
||||
|
||||
### 1. Checkmk findet die neun Services nicht mehr
|
||||
|
||||
Die Checkmk-Ansicht erwartet die bisherigen Namen ohne Umgebung, zum Beispiel:
|
||||
|
||||
```text
|
||||
BizTalk Endpoint Reachability
|
||||
BizTalk Platform
|
||||
BizTalk SQL Access
|
||||
```
|
||||
|
||||
Der aktuelle Snapshot enthält dagegen ausschließlich umbenannte Services:
|
||||
|
||||
```text
|
||||
BizTalk ACC Endpoint Reachability
|
||||
BizTalk ACC Platform
|
||||
BizTalk ACC SQL Access
|
||||
```
|
||||
|
||||
Damit ist `Item not found in monitoring data` kein Beleg für einen veralteten
|
||||
oder fehlenden Snapshot. Checkmk sucht nach einem anderen Servicenamen als der
|
||||
Agent aktuell liefert. Die unterschiedlichen Alterswerte sind die Zeitpunkte,
|
||||
zu denen Checkmk die jeweiligen alten Namen zuletzt gesehen hat.
|
||||
|
||||
Ursache in Version 2.2.1: `EnvironmentName=ACC` wurde zugleich als
|
||||
Servicenamensbestandteil verwendet. Das Installerfeld „Umgebung“ konnte damit
|
||||
bei einem Update den stabilen Checkmk-Vertrag unbeabsichtigt ändern.
|
||||
|
||||
### 2. Endpoint-Probe ist schnell und erfolgreich, wird aber als UNKNOWN formatiert
|
||||
|
||||
Der Snapshot belegt:
|
||||
|
||||
| Wert | ACC-Befund |
|
||||
| --- | ---: |
|
||||
| aktive Transportkandidaten laut Katalog | 135 |
|
||||
| automatisch konfigurierte/prüfbare Artefakte | 64 |
|
||||
| vollständig getestete Artefakte | 64 |
|
||||
| erreichbare Artefakte | 64 |
|
||||
| fehlgeschlagene Artefakte | 0 |
|
||||
| eindeutige Host-/Port-/Protokoll-Ziele | 17 |
|
||||
| nicht automatisch auflösbare externe Kandidaten | 61 |
|
||||
| Probe-Laufzeit | 266 ms |
|
||||
|
||||
Die Netzwerkprobe selbst ist damit weder veraltet noch langsam. Sie
|
||||
dedupliziert 64 Artefakte auf 17 Netzwerkverbindungen und schließt alle Probes
|
||||
in deutlich unter einer Sekunde ab.
|
||||
|
||||
Version 2.2.1 behandelt jedoch jeden Wert
|
||||
`biztalk_endpoints_unsupported > 0` als unvollständiges Ergebnis. Deshalb wird
|
||||
der Service trotz `available=64` und `failed=0` als `UNKNOWN` ausgegeben. Die
|
||||
61 Kandidaten fehlen erwartungsgemäß in `endpoints.xml`, weil für sie kein
|
||||
sicherer und geheimnisfreier Host/Port ermittelt werden konnte. Das ist eine
|
||||
Abdeckungsinformation, kein Fehler der 64 ausgeführten Probes.
|
||||
|
||||
Aus `135 - 64 - 61` ergeben sich zusätzlich 10 lokale, dynamische oder bewusst
|
||||
nicht netzwerkprüfbare Kandidaten. Version 2.2.2 weist sie explizit als
|
||||
`excluded` aus.
|
||||
|
||||
## Korrektur in Version 2.2.2
|
||||
|
||||
### Stabile Servicenamen
|
||||
|
||||
- `EnvironmentName` bleibt Umgebungsmetadatum für Snapshot und Katalog.
|
||||
- Standard `IncludeEnvironmentInServiceName=false`: Die neun Namen bleiben
|
||||
`BizTalk ...` und passen wieder zu den bereits entdeckten ACC-Services.
|
||||
- Nur ein bewusstes Opt-in mit `true` erzeugt Namen wie `BizTalk ACC ...` und
|
||||
verlangt anschließend eine Checkmk Service Discovery.
|
||||
- Version 2.2.3 ergänzt einen versionsübergreifenden Installer-Schutz: Der
|
||||
tatsächliche Servicevertrag der vorhandenen und der neuen EXE wird vor dem
|
||||
Task-Stopp verglichen. Unbestätigte Renames, neue oder entfernte Services
|
||||
blockieren das Update; die Differenz wird vollständig angezeigt.
|
||||
- Version 2.2.4 erzwingt nach der Umschaltung einen frischen vollständigen
|
||||
Endpoint-Katalogabgleich und akzeptiert die Installation erst nach einem
|
||||
erfolgreichen Providerlauf mit neuem, identitäts- und integritätsgeprüftem
|
||||
Snapshot sowie neun verlässlichen stabilen Services.
|
||||
|
||||
### Korrekte Abdeckungssemantik
|
||||
|
||||
- Erfolgreich geprüfte Ziele bestimmen den Zustand der Endpoint-Probe.
|
||||
- Ein fehlgeschlagenes getestetes Ziel bleibt `CRIT`.
|
||||
- `unsupported` und `excluded` werden als vollständige Metriken ausgegeben,
|
||||
führen neben erfolgreicher Prüfabdeckung aber nicht mehr zu `UNKNOWN`.
|
||||
- Wenn externe `unsupported`-Kandidaten existieren und überhaupt kein Ziel
|
||||
automatisch geprüft werden kann, bleibt der Service bewusst `UNKNOWN`.
|
||||
|
||||
### Sofortige Katalogkonsistenz
|
||||
|
||||
Der Provider vergleicht in jedem Minutenlauf die bereits aus WMI gelesenen
|
||||
aktiven Kandidaten mit dem Katalog. Folgende Änderungen lösen sofort einen
|
||||
atomaren Neuabgleich aus:
|
||||
|
||||
- neuer, deaktivierter oder entfernter prüfbarer Artefaktschlüssel,
|
||||
- geänderter Host, Port oder Protokoll eines vorhandenen Artefakts,
|
||||
- geänderte Zahl aktiver oder nicht automatisch auflösbarer Kandidaten.
|
||||
|
||||
Manuelle Overrides gelten weiterhin als beabsichtigte Abdeckung und lösen
|
||||
keine Refresh-Schleife aus. Der 168-Stunden-Abgleich bleibt als zusätzliche
|
||||
Frischegrenze bestehen.
|
||||
|
||||
### Laufzeitgrenzen
|
||||
|
||||
Die performante Implementierung bleibt unverändert:
|
||||
|
||||
- Deduplizierung nach Protokoll, Host und Port,
|
||||
- maximal 16 parallele Probes,
|
||||
- 3 Sekunden Timeout je eindeutiges Ziel,
|
||||
- maximal 100 automatisch konfigurierte Endpoint-Einträge.
|
||||
|
||||
Der beobachtete ACC-Wert von 266 ms für 17 eindeutige Ziele liegt deutlich im
|
||||
Minutenbudget.
|
||||
|
||||
## Erwartung nach dem Update
|
||||
|
||||
Der Endpoint-Service sollte bei unverändertem ACC-Zustand sinngemäß liefern:
|
||||
|
||||
```text
|
||||
0 "BizTalk Endpoint Reachability" ... candidates=135 ... active=64 ...
|
||||
available=64 ... failed=0 ... unsupported=61 ... excluded=10 ...
|
||||
unique_targets=17 ... probe_ms=<Laufzeit>
|
||||
```
|
||||
|
||||
Die übrigen fachlichen Zustände aus dem gelieferten Snapshot werden danach in
|
||||
Checkmk wieder sichtbar und sind getrennt zu bewerten:
|
||||
|
||||
- 3 resumable Suspensions: `WARN`,
|
||||
- 2 Host Instances mit unbekanntem Status: `CRIT`,
|
||||
- 22 nicht allowlistete deaktivierte Receive Locations: `CRIT`,
|
||||
- 17 nicht allowlistete inaktive Send Ports: `CRIT`,
|
||||
- 2 Errors und 6 Warnings im Event-Log-Zeitfenster: `WARN`.
|
||||
|
||||
Diese Zustände sind nicht die Ursache des Datenalters und werden durch den
|
||||
Endpoint-/Namensfix bewusst nicht grüngefärbt.
|
||||
|
||||
## ACC-Abnahme nach Installation
|
||||
|
||||
1. Setup 2.2.6 als Update ausführen; Umgebung `ACC` kann bestehen bleiben.
|
||||
Beim Wechsel von der fehlerhaften 2.2.1-Ausgabe `BizTalk ACC ...` auf die
|
||||
stabilen Namen die Rename-/Discovery-Checkbox bewusst aktivieren.
|
||||
2. In der installierten Config bestätigen:
|
||||
|
||||
```text
|
||||
EnvironmentName=ACC
|
||||
IncludeEnvironmentInServiceName=false
|
||||
```
|
||||
|
||||
3. Scheduled Task manuell starten und `LastTaskResult=0` prüfen.
|
||||
4. Snapshot zweimal im Abstand von ungefähr einer Minute prüfen; `generatedUtc`
|
||||
muss fortschreiten und die Namen müssen `BizTalk ...` ohne `ACC` lauten.
|
||||
5. `BizTalk Endpoint Reachability` muss bei `failed=0` und vorhandener
|
||||
automatischer Prüfabdeckung `OK` liefern. `unsupported`/`excluded` bleiben
|
||||
als Metriken sichtbar.
|
||||
6. Agent-Dump prüfen:
|
||||
|
||||
```cmd
|
||||
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | findstr /C:"BizTalk "
|
||||
```
|
||||
|
||||
7. Checkmk-Prüfung neu planen. Die bestehenden neun Namen sollten ohne
|
||||
Umbenennung wieder Daten erhalten. Nur falls Checkmk noch alte
|
||||
`BizTalk ACC ...`-Services enthält, einmal Service Discovery durchführen
|
||||
und die bewusst falschen Namen entfernen.
|
||||
8. `biztalk_endpoint_probe_duration_ms` und `elapsed_ms` über mehrere Läufe
|
||||
beobachten; der Task darf nicht in das nächste Minutenintervall laufen.
|
||||
@@ -0,0 +1,96 @@
|
||||
BizTalk Checkmk Pulse - ACC Berechtigung und Architektur
|
||||
Stand: 30.07.2026
|
||||
|
||||
BEFUND
|
||||
======
|
||||
|
||||
Host: AV23AGPWBIO1
|
||||
Checkmk-Identität: NT AUTHORITY\SYSTEM
|
||||
Alte Netzwerkidentität: BEW\AV23AGPWBIO1$
|
||||
Fehler: COMException HRESULT=0x80131904
|
||||
Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$'
|
||||
|
||||
Der lokale BizTalk-WMI-Namespace war erreichbar. Datenbankgestützte
|
||||
BizTalk-WMI-Abfragen scheiterten am SQL-Login des Maschinenkontos.
|
||||
|
||||
NEUE VERBINDLICHE ARCHITEKTUR
|
||||
=============================
|
||||
|
||||
Das Maschinenkonto erhält keine BizTalk-Berechtigung.
|
||||
|
||||
1. Ein normales dediziertes Servicekonto läuft minütlich als
|
||||
Scheduled Task "BizTalk Checkmk Pulse Provider".
|
||||
2. Dieses Konto führt BizTalkCheckmkPulse.exe --collect aus.
|
||||
3. Es schreibt atomar einen SHA-256-geschützten 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 führt kein WMI und
|
||||
keinen SQL-Zugriff aus.
|
||||
|
||||
BERECHTIGUNG DES PROVIDER-KONTOS
|
||||
================================
|
||||
|
||||
Bevorzugte Rolle für BizTalk Server 2020:
|
||||
|
||||
BizTalk Server Read Only Users
|
||||
SQL-Datenbankrolle: BTS_READONLY_USERS
|
||||
|
||||
Die exakt konfigurierte Gruppe in den Eigenschaften der BizTalk-Gruppe in der
|
||||
BizTalk Administration Console ablesen. Das Provider-Konto durch
|
||||
AD-Administration in BizTalkReadOnlyUserGroup aufnehmen. Nicht vom
|
||||
Standardnamen ausgehen.
|
||||
|
||||
Die Operator-Gruppe ist nur eine begründungspflichtige Eskalation, falls eine
|
||||
konkret benötigte WMI-Klasse trotz bestätigter Read-Only-Zuordnung und neuem
|
||||
Anmeldetoken verweigert wird.
|
||||
|
||||
NICHT DURCHFÜHREN
|
||||
==================
|
||||
|
||||
- keinen SQL-Einzellogin für 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 für diese Lösung berechtigen
|
||||
|
||||
INSTALLATION
|
||||
============
|
||||
|
||||
BizTalkCheckmkPulse-Setup.zip vollständig entpacken und Setup.exe als
|
||||
Administrator starten. Konto im Format DOMAIN\Benutzer (zum Beispiel
|
||||
BEW\t231bizmon), Kennwort und Umgebung eingeben. PowerShell wird nicht
|
||||
benötigt.
|
||||
|
||||
VERIFIKATION
|
||||
============
|
||||
|
||||
schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list
|
||||
dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot"
|
||||
type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
|
||||
"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
|
||||
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
|
||||
|
||||
ERWARTET
|
||||
========
|
||||
|
||||
- LastTaskResult = 0
|
||||
- Snapshot wird minütlich 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
|
||||
Binary file not shown.
+174
-23
@@ -8,30 +8,76 @@ 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 minütlich vom privilegierten Scheduled-Task-
|
||||
Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den
|
||||
integritäts- und altersgeprüften Snapshot. Ist dieser Transport nicht
|
||||
verfügbar, liefern alle neun 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.
|
||||
- Prüft, ob der BizTalk-WMI-Namespace erreichbar ist.
|
||||
- Zeigt Server, Gruppe, konfigurierte BizTalk-Read-Only-/Operator-Gruppe, Management-DB und Master-MessageBox-DB aus `MSBTS_GroupSetting`.
|
||||
|
||||
Status:
|
||||
|
||||
| Status | Bedingung |
|
||||
| --- | --- |
|
||||
| `OK` | WMI-Verbindung erfolgreich. |
|
||||
| `UNKNOWN` | WMI-Verbindung oder Programmstart fehlgeschlagen. |
|
||||
| `OK` | WMI-Verbindung erfolgreich und `MSBTS_GroupSetting` liefert beide vollständigen Datenbankziele. |
|
||||
| `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, ein erforderliches Datenbankziel oder der Programmstart fehlgeschlagen. |
|
||||
|
||||
Metriken:
|
||||
|
||||
- keine
|
||||
|
||||
## BizTalk SQL Access
|
||||
|
||||
Zweck:
|
||||
|
||||
- Ermittelt Management- und Master-MessageBox-Datenbank über `MSBTS_GroupSetting`.
|
||||
- Öffnet mit der Windows-Identität des privilegierten Provider-Tasks eine integrierte SQL-Verbindung.
|
||||
- Führt die harmlose Abfrage `SELECT 1` aus und schließt die Verbindung sofort wieder.
|
||||
- Zeigt lokale Ausführungsidentität und erwartete Netzwerkidentität an.
|
||||
|
||||
Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prüft den für das
|
||||
BizTalk-Monitoring relevanten Zugriff des dedizierten Provider-Kontos.
|
||||
|
||||
Status:
|
||||
|
||||
| Status | Bedingung |
|
||||
| --- | --- |
|
||||
| `OK` | WMI-Zielermittlung vollständig und alle ermittelten Datenbanken erreichbar. |
|
||||
| `UNKNOWN` | Zielermittlung unvollständig, Anmeldung verweigert, SQL nicht erreichbar, Timeout oder Providerfehler. |
|
||||
| `OK` mit Hinweis | Nur wenn `ProbeSqlConnectivity=false`; der Test ist dann bewusst deaktiviert. |
|
||||
|
||||
Fehlerkategorien:
|
||||
|
||||
| Kategorie | Typische Bedeutung |
|
||||
| --- | --- |
|
||||
| `Sql/Permission` | Login abgelehnt, Datenbank nicht öffnbar oder fehlende Berechtigung. |
|
||||
| `Sql/Connectivity` | DNS, SQL-Dienst, Instanz, TCP-Port oder Firewall nicht erreichbar. |
|
||||
| `Sql/Timeout` | Verbindungsaufbau oder Testabfrage überschreitet das konfigurierte Zeitlimit. |
|
||||
| `Sql/Provider` | Sonstiger von SQL Server oder SqlClient gemeldeter Providerfehler. |
|
||||
| `Sql/Configuration` | WMI-Ziele aus einem Nicht-Berechtigungsgrund unvollständig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. |
|
||||
|
||||
Ist die Zielermittlung wegen eines eingebetteten SQL-Loginfehlers des BizTalk-WMI-Providers unvollständig, wird die Folgediagnose als `Sql/Permission` statt als `Sql/Configuration` ausgegeben.
|
||||
|
||||
Metriken:
|
||||
|
||||
| Metrik | Bedeutung |
|
||||
| --- | --- |
|
||||
| `biztalk_sql_targets_total` | Anzahl eindeutiger Management-/MessageBox-Ziele. |
|
||||
| `biztalk_sql_targets_available` | Erfolgreich getestete SQL-Ziele. |
|
||||
| `biztalk_sql_targets_failed` | Fehlgeschlagene SQL-Ziele. |
|
||||
|
||||
## BizTalk Suspended Instances
|
||||
|
||||
Zweck:
|
||||
|
||||
- Ermittelt suspendierte BizTalk-Service-Instanzen.
|
||||
- Unterscheidet resumable und non-resumable.
|
||||
- Zählt Routing Failure Reports (`ServiceClass=64`) separat.
|
||||
- Zeigt betroffene Anwendungen und Beispiele in der Summary.
|
||||
|
||||
Status:
|
||||
@@ -41,7 +87,8 @@ Status:
|
||||
| `OK` | keine suspendierten Instanzen. |
|
||||
| `WARN` | `biztalk_suspended_resumable >= WarnResumableThreshold`. |
|
||||
| `CRIT` | `biztalk_suspended_nonresumable >= CritNonResumableThreshold`. |
|
||||
| `UNKNOWN` | WMI oder Programmfehler. |
|
||||
| `CRIT` | `biztalk_routing_failure_reports >= CritRoutingFailureThreshold`. |
|
||||
| `UNKNOWN` | `MSBTS_ServiceInstance` nicht verlässlich lesbar oder Programmfehler. |
|
||||
|
||||
Metriken:
|
||||
|
||||
@@ -50,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.
|
||||
- Überwacht 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:
|
||||
@@ -77,44 +126,147 @@ 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.
|
||||
- Zählt 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` | Programmfehler. |
|
||||
| `OK` | Daten lesbar und keine unerwartet deaktivierte Receive Location. |
|
||||
| `CRIT` | Mindestens eine unerwartet deaktivierte Receive Location bei `AlertOnArtifactRuntimeIssues=true`. |
|
||||
| `UNKNOWN` | `MSBTS_ReceiveLocation` nicht verlässlich 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 verlässlich 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 Endpoint Reachability
|
||||
|
||||
Zweck:
|
||||
|
||||
- Prüft nur Receive Locations mit `IsDisabled=false` und Send Ports mit
|
||||
`Status=Started`.
|
||||
- Reduziert die Adresse auf Host/Port und führt keinen fachlichen
|
||||
HTTP-, FTP-, SFTP-, WCF- oder BizTalk-Aufruf aus.
|
||||
- Erkennt unter anderem Standard-URIs, UNC, MSMQ, `net.tcp`, WCF-SQL-
|
||||
`mssql`-URIs, schemafreie `host:port`-Angaben, FTP-/SFTP-Ziele mit optionalen
|
||||
Benutzerinformationen sowie lokale HTTP-/WCF-Wildcard-Listener.
|
||||
- Schließt Scheduling-Komponenten vollständig aus. Oracle-`DATABASE`- und
|
||||
WCF-SAP-Adressen werden, soweit eindeutig möglich, als TCP-Best-Effort-Ziel
|
||||
geprüft; fehlende Ableitung und fehlgeschlagene Best-Effort-Probes alarmieren
|
||||
nicht.
|
||||
- Behandelt Adressen ohne eigenständiges Socket-Ziel (zum Beispiel lokale
|
||||
Pfade, SMTP-Empfänger, Named Pipes und relative Receive-Listener) als
|
||||
erwartbar ausgenommen und nicht als Abdeckungsfehler.
|
||||
- Dedupliziert gemeinsame Host/Port-Ziele und prüft sie mit begrenzter
|
||||
Parallelität; das Ergebnis wird danach wieder den Artefakten zugeordnet.
|
||||
- Zeigt im gesunden Zustand nur `Alle n ... erreichbar`. Nur bei Fehlern
|
||||
erscheint die begrenzte Liste `unavailable=`.
|
||||
|
||||
Status:
|
||||
|
||||
| Status | Bedingung |
|
||||
| --- | --- |
|
||||
| `OK` | Katalog und Runtimezustand vollständig; alle alarmierenden Ziele erreichbar. Best-Effort-Fehler bei DATABASE/WCF-SAP sind höchstens als Metrik sichtbar. |
|
||||
| `CRIT` | Mindestens ein reguläres oder manuell konfiguriertes TCP-/UDP-Ziel nicht erreichbar. |
|
||||
| `UNKNOWN` | Katalog fehlt/ist ungültig, Wochenabgleich scheitert, Runtimezustand ist unvollständig oder ein aktiver externer Endpoint kann nicht sicher auf Host/Port reduziert werden. |
|
||||
| `OK` mit Hinweis | Nur wenn `ProbeEndpointConnectivity=false`; der Test ist bewusst deaktiviert. |
|
||||
|
||||
Metriken:
|
||||
|
||||
| Metrik | Bedeutung |
|
||||
| --- | --- |
|
||||
| `biztalk_endpoints_configured` | Einträge in `endpoints.xml`, einschließlich derzeit inaktiver. |
|
||||
| `biztalk_endpoints_active` | Im aktuellen Lauf Started/Enabled und für die Probe ausgewählt. |
|
||||
| `biztalk_endpoints_unique_targets` | Nach Host/Port/Protokoll-Deduplizierung tatsächlich verbundene Ziele. |
|
||||
| `biztalk_endpoints_tested` | Vollständig abgeschlossene Artefaktprobes. |
|
||||
| `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. |
|
||||
| `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. |
|
||||
| `biztalk_endpoints_unresolved` | Aktive statische Netzwerkadressen ohne sicher bestimmbaren Host/Port; `UNKNOWN` ab 1, sofern kein manueller Override existiert. |
|
||||
| `biztalk_endpoints_expected_non_socket` | Aktive Artefakte, deren Adresse erwartbar kein eigenständiges TCP-/UDP-Ziel enthält. |
|
||||
| `biztalk_endpoints_best_effort` | Automatisch abgeleitete aktive Oracle-DATABASE-/WCF-SAP-Ziele. |
|
||||
| `biztalk_endpoints_best_effort_ignored` | Fehlgeschlagene Best-Effort-Probes, die vereinbarungsgemäß keinen Alarm auslösen. |
|
||||
| `biztalk_endpoints_manual_overrides` | Aktive Ziele, die bewusst über einen geheimnisfreien manuellen Katalogeintrag aufgelöst werden. |
|
||||
| `biztalk_endpoints_inactive_skipped` | Konfigurierte Einträge, deren BizTalk-Artefakt aktuell nicht aktiv ist. |
|
||||
| `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. |
|
||||
|
||||
Bei expliziten `udp://host:port`-Einträgen kann ein generischer Check ohne
|
||||
applikationsspezifisches Protokoll nur DNS, Route und erfolgreichen lokalen
|
||||
Datagrammversand bestätigen. Er beweist keine Antwort des entfernten
|
||||
UDP-Dienstes. Für alle üblichen streambasierten Adapter wird deshalb TCP
|
||||
verwendet.
|
||||
|
||||
## BizTalk Orchestrations
|
||||
|
||||
Status:
|
||||
|
||||
| Status | Bedingung |
|
||||
| --- | --- |
|
||||
| `OK` | Daten lesbar und kein unbekannter Status; inaktive Orchestrations sind standardmäßig nur Information. |
|
||||
| `WARN` | stopped/bound/unbound bei `AlertOnInactiveOrchestrations=true`. |
|
||||
| `CRIT` | Mindestens ein unbekannter Statuswert. |
|
||||
| `UNKNOWN` | `MSBTS_Orchestration` nicht verlässlich 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 für Receive Locations und Send Ports:
|
||||
|
||||
```text
|
||||
Name|Anwendung\Name|Weiterer Name
|
||||
```
|
||||
|
||||
Die Auswertung ist case-insensitive und exakt. Wildcards werden bewusst nicht
|
||||
unterstützt, damit eine breite Regel keine neuen Artefaktfehler verdeckt.
|
||||
|
||||
## BizTalk Event Log
|
||||
|
||||
Zweck:
|
||||
|
||||
- Erfasst BizTalk-bezogene Errors und Warnings aus dem Windows Application Log.
|
||||
- Ergaenzt WMI-Zustandsdaten um aktuelle Laufzeitereignisse.
|
||||
- Ergänzt WMI-Zustandsdaten um aktuelle Laufzeitereignisse.
|
||||
|
||||
Status:
|
||||
|
||||
@@ -134,11 +286,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 zusätzliche Services für Anwendungen erzeugt, die suspendierte Instanzen haben und deren Anwendung per WMI tatsächlich 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 irreführender per-Application-Service namens `(unknown)` wird nicht erzeugt. Die Option kann die Service-Anzahl erhöhen und bleibt für den Basisausbau deaktiviert.
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# Endpoint-Katalog und Netzwerkprobe
|
||||
|
||||
## Automatischer Betrieb
|
||||
|
||||
Der privilegierte Provider liest bei seiner bereits vorhandenen
|
||||
`MSBTS_SendPort`-/`MSBTS_ReceiveLocation`-Abfrage auch die dokumentierten
|
||||
Transportadressen. Fehlt die Datei
|
||||
|
||||
```text
|
||||
%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml
|
||||
```
|
||||
|
||||
wird sie im ersten erfolgreichen Lauf erzeugt. Danach erfolgt der vollständige
|
||||
Abgleich alle `EndpointDiscoveryIntervalHours` (Default: 168 Stunden). Ein
|
||||
Fehler beim Abgleich ersetzt keinen letzten gültigen Katalog. Checkmk meldet
|
||||
den überfälligen/fehlgeschlagenen Abgleich trotzdem als `UNKNOWN`.
|
||||
|
||||
Bei Installation oder Update ab Version 2.2.4 erzwingt ein einmaliger
|
||||
Provider-Abnahmelauf unabhängig vom Alter einen vollständigen Abgleich. Der
|
||||
Installer akzeptiert den Release erst, wenn der neue Synchronisationszeitpunkt
|
||||
nach dem Start dieses Laufs liegt. Gültige manuelle Overrides werden dabei
|
||||
erhalten; der Katalog wird nicht blind gelöscht.
|
||||
|
||||
Ab Setup 2.2.6 blockiert ein technisch gültiger Abnahmelauf nicht mehr allein
|
||||
deshalb, weil `BizTalk Endpoint Reachability` fachlich `UNKNOWN` meldet. Die
|
||||
Installation wird mit einer sichtbaren Warnung abgeschlossen; Status, Metriken
|
||||
und Detailtext bleiben im Snapshot erhalten. Ein fehlender, ungültiger oder
|
||||
nicht in diesem Abnahmelauf synchronisierter Katalog bleibt dagegen ein harter
|
||||
Installationsfehler.
|
||||
|
||||
Unabhängig vom Wochenabgleich wird bei jedem Minutenlauf gegen den aktuellen
|
||||
BizTalk-Runtimezustand gefiltert:
|
||||
|
||||
- Receive Location nur bei `IsDisabled=false`
|
||||
- Send Port nur bei `Status=Started`
|
||||
- dynamischer Send Port nie als statisches Ziel
|
||||
- Scheduling-Komponente nie als Netzwerkziel
|
||||
|
||||
Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr
|
||||
geprüft. Prüffähige aktuelle WMI-Adressen werden bei jedem Minutenlauf direkt
|
||||
verwendet; der Wochenabgleich pflegt nur den persistenten, geheimnisfreien
|
||||
Katalog. Dadurch wird weder ein neues Ziel verzögert noch eine bis zu sieben
|
||||
Tage alte Auto-Discovery-Adresse weiter geprüft. Manuelle Overrides gewinnen
|
||||
weiterhin anhand ihres stabilen Artefaktschlüssels.
|
||||
|
||||
## Laufzeitbudget
|
||||
|
||||
Die Socket-Probes werden nach Protokoll, Host und Port dedupliziert und mit
|
||||
maximal `EndpointProbeMaxConcurrency=16` parallelen Verbindungen ausgeführt.
|
||||
Bei `EndpointProbeTimeoutMilliseconds=3000` benötigen 70 vollständig
|
||||
timeoutende eindeutige Ziele theoretisch etwa 15 Sekunden. Die Standardgrenze
|
||||
`EndpointMaxCount=100` begrenzt diesen Anteil eines Providerlaufs auf etwa
|
||||
21 Sekunden. Die Grenze gilt für deduplizierte Host/Port/Protokoll-Ziele, nicht
|
||||
für die Anzahl der darauf abgebildeten BizTalk-Artefakte. Der Katalog ist
|
||||
separat über `EndpointCatalogMaxEntries=1000` begrenzt. Erwartbar nicht als
|
||||
Socket prüfbare Artefakte zählen nicht gegen das Laufzeitbudget; echte
|
||||
Auflösungslücken werden als `unresolved` mit einem geheimnisfreien Grund
|
||||
gemeldet.
|
||||
|
||||
Checkmk selbst wartet nicht auf diese Verbindungen, sondern liest nur den
|
||||
zuletzt atomar geschriebenen Snapshot. Die reale ACC-Laufzeit ist nach dem
|
||||
Update an `biztalk_endpoint_probe_duration_ms` sowie an `endpoint_probe_ms` und
|
||||
`elapsed_ms` im Provider-Log zu kontrollieren. Der Scheduled Task verhindert
|
||||
mit `IgnoreNew` überlappende Providerläufe.
|
||||
|
||||
## Sicherheitsmodell
|
||||
|
||||
Der Katalog speichert nicht die vollständige BizTalk-Adresse. Persistiert
|
||||
werden nur:
|
||||
|
||||
- Artefakttyp, Anwendung, Name und Transportrolle
|
||||
- Adaptername
|
||||
- `TCP` oder `UDP`
|
||||
- Host und Port
|
||||
- `enabled`, `autoDiscovered` und die Best-Effort-Kennzeichnung
|
||||
|
||||
URI-Benutzerinformationen, Kennwörter, Pfade und Querystrings werden nicht
|
||||
geschrieben. Der Katalog liegt im bereits ACL-geschützten `data`-Verzeichnis;
|
||||
nur Provider und Administratoren dürfen ihn ändern. `LocalSystem` liest nur
|
||||
den separaten, validierten Checkmk-Snapshot.
|
||||
|
||||
Die automatische Auflösung akzeptiert neben absoluten URIs auch `host:port`
|
||||
und bei FTP/SFTP die schemafreie Form `[benutzer@]host[:port]/pfad`. Die
|
||||
Reihenfolge ist absichtlich eindeutig: `host:port` wird vor der allgemeinen
|
||||
.NET-URI-Auswertung erkannt, damit der Host nicht fälschlich als URI-Schema
|
||||
gedeutet wird. Benutzerinformationen und Pfad bleiben ausschließlich im
|
||||
flüchtigen WMI-Kandidaten und werden nicht persistiert. Eckige URI-Klammern
|
||||
um IPv6-Adressen werden vor der Socket-Probe entfernt.
|
||||
|
||||
## Manuelle Korrektur eines automatisch nicht auflösbaren Endpoints
|
||||
|
||||
Vor einer manuellen Änderung den Scheduled Task kurz stoppen und die Datei
|
||||
sichern. Einen vorhandenen Eintrag auf den gewünschten Host/Port korrigieren
|
||||
und `autoDiscovered="false"` setzen. Der vorhandene `key` bleibt unverändert:
|
||||
|
||||
```xml
|
||||
<Endpoint
|
||||
key="SENDPORT|ORDERS|SP ORDER OUT|PRIMARY"
|
||||
artifactType="SendPort"
|
||||
application="Orders"
|
||||
artifact="SP Order Out"
|
||||
transportRole="Primary"
|
||||
adapter="Custom Adapter"
|
||||
protocol="TCP"
|
||||
host="gateway.example.internal"
|
||||
port="7443"
|
||||
enabled="true"
|
||||
autoDiscovered="false" />
|
||||
```
|
||||
|
||||
Manuelle Einträge werden beim Wochenabgleich erhalten. Stimmen Artefakttyp
|
||||
und `key` mit einem Send-/Receive-Artefakt überein, gilt weiterhin dessen
|
||||
Started-/Enabled-Filter. Ein zusätzliches, nicht an BizTalk gebundenes Ziel
|
||||
kann `artifactType="Manual"` verwenden; es wird bei `enabled="true"` immer
|
||||
geprüft.
|
||||
|
||||
Nach dem Speichern Task wieder starten und prüfen:
|
||||
|
||||
```powershell
|
||||
Start-ScheduledTask -TaskName 'BizTalk Checkmk Pulse Provider'
|
||||
Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 100
|
||||
& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
|
||||
```
|
||||
|
||||
Ungültiges XML, doppelte Keys, falsche Maschine/Umgebung, unbekannte
|
||||
Protokolle, ungültige Ports und übergroße Dateien werden verworfen. Der
|
||||
Provider kann bei vollständig verfügbarem BizTalk-Runtimezustand einen
|
||||
ungültigen rein automatisch erzeugten Katalog neu aufbauen. Deshalb muss ein
|
||||
manueller Eintrag vor dem produktiven Einsatz gesichert und im Provider-Log
|
||||
verifiziert werden.
|
||||
|
||||
## Protokollgrenzen
|
||||
|
||||
TCP-Connect ist für HTTP(S), SFTP, FTP, SMB/UNC, WCF und `net.tcp` der
|
||||
passende nebenwirkungsfreie Netzwerktest. Er bestätigt DNS, Routing,
|
||||
Firewall und einen annehmenden TCP-Port, aber nicht die fachliche Antwort der
|
||||
Zielanwendung.
|
||||
|
||||
Oracle-`DATABASE`-Adressen mit explizitem Anbietersegment `/Oracle` verwenden
|
||||
den angegebenen Port oder den Oracle-Standard TCP 1521. WCF-SAP-Adressen
|
||||
verwenden vorrangig den ersten `SAPROUTER=/H/host[/S/port]`-Hop, andernfalls
|
||||
`ListenerGwHost`/`ListenerGwServ` beziehungsweise `GwHost`/`GwServ`.
|
||||
`sapgwNN` wird als TCP `33NN` abgeleitet; der SAProuter-Standard ist TCP 3299.
|
||||
Diese automatisch erkannten Adapterziele sind `bestEffort="true"`: Die Probe
|
||||
wird ausgeführt und gemessen, ein Fehlschlag aber nur in
|
||||
`biztalk_endpoints_best_effort_ignored` gezählt. Nicht sicher ableitbare
|
||||
DATABASE-/WCF-SAP-Adressen werden erwartbar ausgenommen. Ein manueller
|
||||
Katalogeintrag bleibt standardmäßig streng alarmierend und ist die bewusste
|
||||
Option für verbindliche Überwachung.
|
||||
|
||||
UDP ist verbindungslos. Bei `udp://host:port` bestätigt der Check nur DNS,
|
||||
Route und erfolgreichen lokalen Datagrammversand. Für einen belastbaren
|
||||
UDP-Dienstcheck wäre ein adapter-/protokollspezifisches Request/Response-
|
||||
Payload erforderlich; ein solches fachliches Paket wird bewusst nicht
|
||||
erfunden.
|
||||
+69
-10
@@ -4,28 +4,87 @@
|
||||
|
||||
```text
|
||||
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 Endpoint Reachability" biztalk_endpoints_active=0;;;0 Self test OK. No network connection was opened.
|
||||
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 reachable, server=BIZTALK01, group=BizTalkMgmtDb, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb
|
||||
0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|biztalk_suspended_resumable=0;1;;0|biztalk_suspended_nonresumable=0;;1;0 No suspended BizTalk service instances found.
|
||||
0 "BizTalk Host Instances" biztalk_host_instances_total=4;;;0|biztalk_host_instances_started=4;;;0|biztalk_host_instances_stopped=0;;1;0|biztalk_host_instances_pending=0;1;;0|biztalk_host_instances_unknown=0;;1;0 Host instances total=4, started=4, stopped=0, pending=0, unknown=0
|
||||
0 "BizTalk Runtime Artifacts" biztalk_applications=8;;;0|biztalk_receive_locations=24;;;0|biztalk_receive_locations_disabled=0;1;;0|biztalk_send_ports=31;;;0|biztalk_send_ports_started=31;;;0|biztalk_send_ports_inactive=0;1;;0|biztalk_send_ports_unknown=0;;1;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=12;;;0|biztalk_orchestrations_inactive=0;1;;0|biztalk_orchestrations_unknown=0;;1;0 Applications=8; receive_locations total=24, disabled=0; send_ports total=31, started=31, inactive=0, unknown=0; orchestrations total=12, started=12, inactive=0, unknown=0
|
||||
0 "BizTalk Event Log" biztalk_eventlog_errors=0;1;10;0|biztalk_eventlog_warnings=0;1;;0 BizTalk-related Application log entries in last 60 minutes: errors=0, warnings=0
|
||||
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
|
||||
```
|
||||
|
||||
## Fehlendes Deployment
|
||||
Routing Failure Reports sind eine Teilmenge der non-resumable Instanzen und
|
||||
werden nicht doppelt zum Total addiert.
|
||||
|
||||
## Unerwartet deaktivierte Receive Locations
|
||||
|
||||
```text
|
||||
3 "BizTalk Platform" - BizTalkCheckmkPulse.exe not found below C:\ProgramData\checkmk\agent\local\BizTalkCheckmkPulse
|
||||
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
|
||||
```
|
||||
|
||||
## Unerwartet inaktive Send Ports
|
||||
|
||||
```text
|
||||
2 "BizTalk Send Ports" biztalk_send_ports_total=31;;;0|biztalk_send_ports_started=29;;;0|biztalk_send_ports_stopped=1;;;0|biztalk_send_ports_bound=1;;;0|biztalk_send_ports_unexpected_inactive=1;;1;0|biztalk_send_ports_expected_inactive=1;;;0|biztalk_send_ports_unknown=0;;1;0 Send ports total=31, started=29, stopped=1, bound=1, unexpected_inactive=1, expected_inactive=1, unknown=0; affected=Orders\SP_Order_Out(bound)
|
||||
```
|
||||
|
||||
## Begrenzte Detailausgabe
|
||||
|
||||
```text
|
||||
2 "BizTalk Receive Locations" ... affected=App\RL1, App\RL2, App\RL3, App\RL4, App\RL5 (+17 more)
|
||||
```
|
||||
|
||||
Die Zähler und Performance-Daten enthalten weiterhin alle 22 betroffenen
|
||||
Artefakte. Nur die Textliste ist begrenzt.
|
||||
|
||||
## Endpoint-Erreichbarkeit
|
||||
|
||||
Gesund, ohne Auflistung aller Ziele:
|
||||
|
||||
```text
|
||||
0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=60;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unresolved=0;;1;0|biztalk_endpoints_expected_non_socket=12;;;0|biztalk_endpoints_best_effort=9;;;0|biztalk_endpoints_best_effort_ignored=1;;;0|biztalk_endpoints_manual_overrides=0;;;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle regulaer alarmierenden aktiven Send-/Receive-Endpunkte sind erreichbar (active=61, available=60, unique_targets=44, expected_non_socket=12, best_effort=9, best_effort_ignored=1, manual_overrides=0, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z
|
||||
```
|
||||
|
||||
Fehlerhaft, nur mit nicht erreichbaren Zielen:
|
||||
|
||||
```text
|
||||
2 "BizTalk ACC Endpoint Reachability" ... Endpoint reachability active=61, tested=61, available=58, failed=2, unresolved=0, expected_non_socket=12, best_effort=9, best_effort_ignored=1, manual_overrides=0; unavailable=SendPort:Orders\SP_Order_API[Primary]->orders.example:443/TCP(TCP timeout), ReceiveLocation:Invoices\RL_SFTP[Inbound]->sftp.example:22/TCP(SocketError=ConnectionRefused)
|
||||
```
|
||||
|
||||
Der nicht verfügbare Best-Effort-Endpunkt wird nur gezählt und nicht in
|
||||
`unavailable=` ausgegeben. Die beiden regulären Fehler bleiben CRIT.
|
||||
|
||||
## Fehlender oder veralteter Provider-Snapshot
|
||||
|
||||
Alle neun 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.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Analyse: Runtime-Abnahme mit Endpoint-Reachability `UNKNOWN`
|
||||
|
||||
## Beobachtung
|
||||
|
||||
Setup 2.2.5 schaltete die neue Version um, startete den Provider erfolgreich
|
||||
und validierte danach die Runtime-Artefakte. Die Abnahme endete trotzdem mit:
|
||||
|
||||
```text
|
||||
Runtime validation failed: Snapshot enthaelt unzuverlaessige UNKNOWN-Zustaende
|
||||
in stabilen Services. UNKNOWN=[BizTalk Endpoint Reachability].
|
||||
```
|
||||
|
||||
Anschließend stellte der Installer die vorherige Version samt Task und frischem
|
||||
Snapshot erfolgreich wieder her.
|
||||
|
||||
## Einordnung
|
||||
|
||||
`LastTaskResult=0` belegt, dass der Providerlauf technisch abgeschlossen und
|
||||
ein Snapshot geschrieben wurde. Zusätzlich waren Snapshot-Zeitpunkt,
|
||||
SHA-256, Maschine, Collector-Identität, alle neun Servicenamen und der im
|
||||
Abnahmelauf synchronisierte Endpoint-Katalog valide. Das verbleibende
|
||||
`UNKNOWN` war daher kein Installations- oder Transaktionsfehler, sondern der
|
||||
fachliche Zustand der Endpoint-Prüfung.
|
||||
|
||||
Der Endpoint-Service verwendet `UNKNOWN`, wenn die Prüfabdeckung nicht
|
||||
vollständig verlässlich ist, zum Beispiel bei einer nicht sicher auf
|
||||
Host/Port reduzierbaren aktiven Adresse, einem Katalog-/Probehinweis oder
|
||||
fehlendem manuellem Override. Diesen Zustand auf `OK` zu ändern würde die
|
||||
Monitoringlücke verbergen.
|
||||
|
||||
## Korrektur in Version 2.2.6
|
||||
|
||||
- Snapshot, Erzeugungszeit, Integrität, Maschine und Collector-Identität
|
||||
werden unverändert strikt geprüft.
|
||||
- Alle neun stabilen Services müssen eindeutig vorhanden sein.
|
||||
- Bei aktivierter Endpoint-Prüfung muss der Katalog weiterhin im gestarteten
|
||||
Abnahmelauf erfolgreich synchronisiert worden sein.
|
||||
- `UNKNOWN` in einem Kernservice bleibt ein Abnahmefehler.
|
||||
- Ausschließlich `BizTalk Endpoint Reachability = UNKNOWN` wird als
|
||||
nicht blockierende Betriebswarnung akzeptiert.
|
||||
- Die vollständige UNKNOWN-Zeile erscheint in Setup-Ausgabe und Setup-Log und
|
||||
bleibt unverändert im Snapshot für Checkmk erhalten.
|
||||
|
||||
Damit wird die Installation abgeschlossen, ohne einen ungeklärten
|
||||
Monitoringzustand grünzufärben. Der normale Minutentask läuft weiter und die
|
||||
Endpoint-Ursache kann anhand von `unresolved_endpoints`,
|
||||
`catalog_or_probe_error`, `biztalk_endpoints_unresolved`, Katalog und
|
||||
Provider-Log gezielt bearbeitet werden.
|
||||
|
||||
## Weiteres Vorgehen in ACC
|
||||
|
||||
1. Setup 2.2.6 als Update ausführen und die ausgegebene
|
||||
`RUNTIME_VALIDATION_WARNING` sichern.
|
||||
2. Im Snapshot die vollständige Zeile `BizTalk Endpoint Reachability` prüfen.
|
||||
3. Bei `unresolved_endpoints` Adapter, Artefakt und Grund auswerten und nur
|
||||
bei einem tatsächlich externen Ziel einen geheimnisfreien manuellen
|
||||
Override in `endpoints.xml` ergänzen.
|
||||
4. Bei `catalog_or_probe_error` zuerst Provider-Log und Katalog-XML prüfen.
|
||||
5. Nach der Bereinigung zwei Minutenläufe und den Checkmk-Agent-Dump prüfen.
|
||||
|
||||
## Regressionstests
|
||||
|
||||
- Ein isoliertes Endpoint-Reachability-`UNKNOWN` wird als Warnung akzeptiert
|
||||
und sein vollständiger Detailtext bleibt erhalten.
|
||||
- `BizTalk Platform = UNKNOWN` bleibt blockierend.
|
||||
- Fehlender Service, alter Snapshot, falsche Identität und alter Katalog
|
||||
bleiben blockierend.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Analyse: Update-Abbruch mit `Exitcode=0, Zeilen=8`
|
||||
|
||||
## Beobachtung
|
||||
|
||||
Der Installer meldete nach erfolgreicher Paketprüfung, Anmeldung und
|
||||
Staging-Validierung:
|
||||
|
||||
```text
|
||||
Installation/Update fehlgeschlagen. Rollback erfolgreich.
|
||||
Ursache: Self-Test fehlgeschlagen. Exitcode=0, Zeilen=8.
|
||||
```
|
||||
|
||||
Die Reihenfolge im Statusfenster zeigt, dass das neue Paket und das Staging
|
||||
bereits erfolgreich neun Services geliefert hatten. Der danach ausgeführte
|
||||
Self-Test gehörte zur noch installierten Ausgangsversion.
|
||||
|
||||
## Ursache
|
||||
|
||||
Version 2.2.4 verwendete dieselbe starre Erwartung von exakt neun Zeilen für
|
||||
beide Seiten des Updatevergleichs. Eine ältere, intakte Installation vor
|
||||
Einführung von `BizTalk Endpoint Reachability` liefert jedoch acht gültige
|
||||
Checkmk-Zeilen und Exitcode 0. Der Updater deutete diese erwartete
|
||||
Versionsdifferenz fälschlich als defekten Self-Test. Der Abbruch erfolgte vor
|
||||
dem Stoppen des Tasks und vor dem Austausch von Dateien. Die Meldung
|
||||
`Rollback erfolgreich` war deshalb technisch irreführend; es war keine
|
||||
Systemänderung zurückzusetzen.
|
||||
|
||||
## Korrektur in Version 2.2.5
|
||||
|
||||
- Paket, Staging und aktivierte Zielversion müssen weiterhin exakt neun
|
||||
eindeutige `OK`-Zeilen liefern.
|
||||
- Die installierte Ausgangsversion darf eine andere positive Anzahl
|
||||
eindeutiger `OK`-Zeilen liefern. Damit wird ihr tatsächlicher
|
||||
versionsspezifischer Servicevertrag gelesen.
|
||||
- Rein neu hinzukommende Services sind updatekompatibel. Sie werden als
|
||||
Discovery-Hinweis gemeldet, blockieren aber die Umschaltung nicht.
|
||||
- Entfernte Namen – und damit auch Renames – bleiben ohne aktivierte
|
||||
Discovery-Bestätigung ein Sicherheitsstopp.
|
||||
- Tritt ein Fehler vor der ersten Systemänderung auf, lautet der Status jetzt
|
||||
`kein Rollback erforderlich`.
|
||||
|
||||
## Erweiterte Diagnose
|
||||
|
||||
Jeder Lauf erzeugt
|
||||
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-<UTC>-<PID>.log`. Das Log enthält:
|
||||
|
||||
- aktuelle Installationsphase und Setup-Version,
|
||||
- Pfad, Dateiversion und Größe jeder geprüften EXE,
|
||||
- erwartete und tatsächliche Zeilenzahl, Exitcode sowie vollständiges stdout
|
||||
und stderr jedes Self-Tests,
|
||||
- Task-Scheduler-State und `LastTaskResult` dezimal und hexadezimal,
|
||||
- vollständige Exception-Kette mit HRESULT und Stacktrace,
|
||||
- bei Runtime-Abnahmefehlern bis zu 40 letzte Provider-Logzeilen,
|
||||
- Ergebnis jedes Rollback-Schritts.
|
||||
|
||||
Das Kennwort wird ausschließlich an `LogonUser` und die
|
||||
Task-Scheduler-COM-Schnittstelle übergeben. Die Logging-Komponente erhält es
|
||||
nicht.
|
||||
|
||||
## Regressionstests
|
||||
|
||||
Automatisiert geprüft werden insbesondere:
|
||||
|
||||
- acht gültige Zeilen einer Legacy-Ausgangsversion werden akzeptiert,
|
||||
- acht Zeilen eines neuen Stagings werden weiterhin abgewiesen,
|
||||
- eine reine Service-Erweiterung wird ohne Rename-Bestätigung akzeptiert,
|
||||
- ein frisch beendeter Task mit Fehlercode wird sofort erkannt und der Code
|
||||
enthält zusätzlich die hexadezimale Darstellung.
|
||||
+130
-63
@@ -1,93 +1,160 @@
|
||||
# Integration in Checkmk
|
||||
# Checkmk-Integration und Betriebsübergabe
|
||||
|
||||
## 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` bestätigen; keine Einzelrechte |
|
||||
| Windows | Paket installieren, ACL und Scheduled Task prüfen |
|
||||
| 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 minütliche Scheduled Task sammelt unter dem privilegierten
|
||||
Provider-Konto und publiziert einen validierbaren Snapshot.
|
||||
|
||||
## Installation
|
||||
|
||||
`BizTalkCheckmkPulse-Setup.zip` vollständig entpacken, `Setup.exe` als
|
||||
Administrator starten und Konto, Kennwort sowie Umgebung eingeben. Konten wie
|
||||
`BEW\t231bizmon` werden direkt im Windows-Format `DOMAIN\Benutzer` verarbeitet.
|
||||
Der Installer benötigt keine PowerShell.
|
||||
|
||||
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
|
||||
Die EXE liegt zentral hier:
|
||||
|
||||
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:
|
||||
```text
|
||||
%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe
|
||||
```
|
||||
|
||||
## Checkmk-Agent-Konfiguration
|
||||
|
||||
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 unnötige
|
||||
Verzögerung zum minütlichen Provider.
|
||||
|
||||
Aktive Agentkonfiguration prüfen:
|
||||
|
||||
```cmd
|
||||
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test
|
||||
"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd"
|
||||
"C:\Program Files (x86)\checkmk\service\check_mk_agent.exe" showconfig local
|
||||
```
|
||||
|
||||
5. Agent-Ausgabe pruefen:
|
||||
Verbindlicher Agent-Dump:
|
||||
|
||||
```cmd
|
||||
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
|
||||
```
|
||||
|
||||
6. In Checkmk:
|
||||
## Service Discovery
|
||||
|
||||
- Host oeffnen
|
||||
- Service Discovery ausfuehren
|
||||
- gefundene `BizTalk ...` Services aufnehmen
|
||||
- Changes aktivieren
|
||||
Das Setup ab 2.2.4 schließt erst erfolgreich ab, nachdem der erste Providerlauf
|
||||
unter dem echten Collector-Konto einen frischen Snapshot und – bei aktivierter
|
||||
Endpoint-Prüfung – einen frisch synchronisierten Katalog erzeugt hat. Die neun
|
||||
stabilen Services müssen vollständig vorliegen. `UNKNOWN` in einem Kernservice
|
||||
bleibt blockierend. Ab Version 2.2.6 wird ein isoliertes
|
||||
`BizTalk Endpoint Reachability = UNKNOWN` als fachliche Betriebswarnung
|
||||
akzeptiert, sofern Snapshot, Identität und frisch synchronisierter Katalog
|
||||
valide sind. Die UNKNOWN-Zeile bleibt in Checkmk sichtbar. Diese lokale
|
||||
Runtime-Abnahme ersetzt nicht die zentrale Checkmk Service Discovery.
|
||||
|
||||
## Aufgaben in Checkmk
|
||||
`EnvironmentName` kennzeichnet Snapshot und Katalog, ändert die neun
|
||||
Servicenamen aber standardmäßig nicht. Nur das bewusste Opt-in
|
||||
`IncludeEnvironmentInServiceName=true` erzeugt umgebungsabhängige Namen und
|
||||
verlangt den anschließenden Discovery-Abgleich.
|
||||
|
||||
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:
|
||||
Ab Setup 2.2.5 darf der gültige Servicevertrag der installierten
|
||||
Ausgangsversion weniger als neun Services enthalten. So ist das Update einer
|
||||
Legacy-Version mit acht Services möglich; nur Paket, Staging und Zielversion
|
||||
müssen exakt neun Services liefern. Additive Services werden gemeldet, aber
|
||||
nicht blockiert. Entfernte oder umbenannte Services erfordern weiterhin die
|
||||
bewusste Bestätigung im Setup. Laufbezogene Details stehen in
|
||||
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`.
|
||||
|
||||
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.
|
||||
1. Provider dreimal erfolgreich laufen lassen.
|
||||
2. `LastTaskResult=0`, frischen Snapshot und Log prüfen.
|
||||
3. Agent-Dump im `LocalSystem`-Kontext prüfen.
|
||||
4. Service Discovery für den BizTalk-Host ausführen.
|
||||
5. neun stabile Services aufnehmen und den alten
|
||||
`BizTalk Runtime Artifacts`-Service entfernen.
|
||||
6. Changes aktivieren.
|
||||
7. Views oder Dashboards mit Filter `Service starts with: BizTalk` anlegen.
|
||||
8. Benachrichtigungen und Eskalationen je Umgebung definieren.
|
||||
7. Views, Servicegruppen und Benachrichtigungen einrichten.
|
||||
|
||||
Empfohlene Service-Behandlung:
|
||||
Host-Tags:
|
||||
|
||||
| Service | Empfehlung |
|
||||
| --- | --- |
|
||||
| `BizTalk Platform` | `UNKNOWN` immer als Integrationsproblem behandeln. |
|
||||
| `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. |
|
||||
```text
|
||||
env:ACC|DEV|TST|PRD
|
||||
app:biztalk
|
||||
```
|
||||
|
||||
## Agent Bakery
|
||||
|
||||
Checkmk Managed Services Edition 2.4 enthaelt die kommerziellen Mechanismen fuer Agent Bakery. Fuer einen sauberen Rollout:
|
||||
|
||||
1. Deployment-Dateien als Custom Files oder ueber ein spaeteres MKP bereitstellen.
|
||||
2. Regel nur auf BizTalk-Hosts anwenden, z.B. Host-Tag `application:biztalk`.
|
||||
3. Gebackenen Windows-Agenten fuer die BizTalk-Hosts installieren.
|
||||
4. Service Discovery ausfuehren.
|
||||
|
||||
Hinweis: Dieses Repository enthaelt bewusst noch kein Bakery-Python-Plugin. Die manuelle bzw. dateibasierte Verteilung ist die risikoarme erste Version. Eine Bakery-Erweiterung ist eine sinnvolle Version-2-Ausbaustufe.
|
||||
|
||||
## Empfohlene Host-Struktur
|
||||
|
||||
Host-Tags oder Ordner:
|
||||
|
||||
- `env:ACC`
|
||||
- `env:DEV`
|
||||
- `env:TST`
|
||||
- `env:PRD`
|
||||
- `app:biztalk`
|
||||
|
||||
Service-Filter fuer Views:
|
||||
Servicefilter:
|
||||
|
||||
```text
|
||||
Service starts with: BizTalk
|
||||
```
|
||||
|
||||
Dashboard-Kacheln:
|
||||
## Bakery-/Softwareverteilung
|
||||
|
||||
- Host/Service state fuer BizTalk-Host
|
||||
- Service state fuer `BizTalk Suspended Instances`
|
||||
- Graph `biztalk_suspended_total`
|
||||
- Graph `biztalk_host_instances_stopped`
|
||||
- Graph `biztalk_eventlog_errors`
|
||||
- MSSQL-Services des SQL-Servers daneben
|
||||
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. Für den ersten Rollout
|
||||
ist das signierte/abgenommene Deployment-Paket mit administrativer
|
||||
Installationsautomation die klarere Variante.
|
||||
|
||||
## Healthchecks
|
||||
|
||||
```cmd
|
||||
schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list
|
||||
dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot"
|
||||
type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
|
||||
"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
|
||||
```
|
||||
|
||||
Soll:
|
||||
|
||||
- Task läuft jede Minute und endet mit `0`.
|
||||
- Snapshot ist kleiner als `SnapshotMaxBytes` und jünger 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.
|
||||
- Endpoint Reachability zeigt bei Erfolg nur die Gesamtzahl und bei Fehlern
|
||||
ausschließlich nicht erreichbare oder wirklich unaufgelöste Ziele.
|
||||
- Schedule ist vollständig ausgenommen; Oracle-DATABASE und WCF-SAP laufen
|
||||
als TCP-Best-Effort und können keinen Alarm auslösen. Die zugehörigen
|
||||
Erfolgs-/Ignoriert-Zähler bleiben für die Betriebsanalyse sichtbar.
|
||||
- keine berechtigungsbedingten `UNKNOWN`-Services.
|
||||
|
||||
## Alarmierung der Transportkette
|
||||
|
||||
Ein Ausfall des Providers wird über alle neun Services als `UNKNOWN`
|
||||
sichtbar. Die Summary nennt fehlenden, unlesbaren, ungültigen oder stale
|
||||
Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso
|
||||
eskaliert werden wie ein technischer Monitoringausfall.
|
||||
|
||||
Optional kann Windows Task Scheduler zusätzlich durch vorhandene Checkmk
|
||||
Task-/Event-Log-Regeln überwacht werden. Das ist eine Ergänzung, kein Ersatz
|
||||
für die eingebaute Stale-Prüfung.
|
||||
|
||||
## 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 Plausibilität.
|
||||
|
||||
Ausführliche Architektur, ACL, Fehlerbilder und Abnahmekriterien:
|
||||
[Dokumentation.md](../Dokumentation.md).
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# RTM-Adapterregeln für Endpoint Reachability
|
||||
|
||||
## Anlass
|
||||
|
||||
Die ACC-Ausgabe vom 11.08.2026 zeigte `BizTalk Endpoint Reachability =
|
||||
UNKNOWN`, obwohl die eigentlichen Socket-Probes in wenigen Millisekunden
|
||||
abgeschlossen waren. Die Detailausgabe bestand überwiegend aus drei
|
||||
Adapterklassen, die der generische URI-Parser nicht fachgerecht einordnen
|
||||
konnte:
|
||||
|
||||
- Scheduling-Komponenten ohne externes Netzwerkziel,
|
||||
- kundenspezifische `DATABASE`-Adressen, nahezu ausschließlich Oracle,
|
||||
- WCF-SAP-Verbindungs-URIs mit Gateway- und SAProuter-Parametern.
|
||||
|
||||
Diese Fälle dürfen den RTM-Service weder mit Auflösungslücken noch mit
|
||||
Fehlalarmen belasten.
|
||||
|
||||
## Verbindliche Klassifizierung ab Version 2.2.7
|
||||
|
||||
### Scheduling
|
||||
|
||||
Adaptername oder Adresse mit `Schedule`/`Scheduler` wird vollständig als
|
||||
erwartbar ausgenommen. Es erfolgt keine Netzwerkprobe und kein UNKNOWN.
|
||||
|
||||
### DATABASE / Oracle
|
||||
|
||||
Nur die eindeutig erkennbare Form
|
||||
|
||||
```text
|
||||
Database://host[:port]/.../Oracle
|
||||
```
|
||||
|
||||
wird automatisch aufgelöst. Ein expliziter Port gewinnt; andernfalls wird
|
||||
TCP 1521 verwendet. Andere DATABASE-Anbieter oder nicht eindeutig lesbare
|
||||
Adressen werden erwartbar ausgenommen. Es wird kein UDP-Test erzeugt.
|
||||
|
||||
Oracle dokumentiert TCP 1521 als Standardadresse des Listeners und nimmt bei
|
||||
fehlender Portangabe im Easy-Connect-Format ebenfalls 1521 an:
|
||||
|
||||
- [Oracle: Configuring the Network Environment](https://docs.oracle.com/en/database/oracle/oracle-database/19/admqs/configuring-the-network-environment.html)
|
||||
- [Oracle: Configuring and Administering Oracle Net Listener](https://docs.oracle.com/en/database/oracle/oracle-database/21/netag/configuring-and-administering-oracle-net-listener.html)
|
||||
|
||||
### WCF-SAP
|
||||
|
||||
Microsoft dokumentiert `ListenerGwHost`, `ListenerGwServ` und `SAPROUTER` als
|
||||
Bestandteile der SAP-Verbindungs-URI. Der Parser verwendet in dieser
|
||||
Reihenfolge:
|
||||
|
||||
1. erster SAProuter-Hop aus `/H/host/S/port`,
|
||||
2. `ListenerGwHost` und `ListenerGwServ`,
|
||||
3. `GwHost` und `GwServ` als Fallback.
|
||||
|
||||
Numerische Dienste werden direkt übernommen. `sapgwNN` wird auf TCP `33NN`
|
||||
abgebildet. Fehlt bei einem SAProuter-Hop `/S/`, gilt der dokumentierte
|
||||
Standard TCP 3299. URI-Escapes werden vor der Auswertung dekodiert; Kennwörter,
|
||||
Program-ID, Pfade und Querystrings gelangen weiterhin nicht in Katalog, Log
|
||||
oder Checkmk-Ausgabe.
|
||||
|
||||
Quellen:
|
||||
|
||||
- [Microsoft Learn: Create the SAP system connection URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sap/create-the-sap-system-connection-uri)
|
||||
- [SAP Help: Entries in the Services File](https://help.sap.com/docs/SLTOOLSET/6bc963751d0840f996c5c9d7a7f83473/bb9faaebe0c74a58bbcc259c83f59437.html)
|
||||
- [SAP Help: SAProuter Host and Port](https://help.sap.com/docs/SAP_NETWEAVER_AS_ABAP_FOR_SOH_740/e245703406684d8a81812f4c6334eb2f/486b5b0cb74c07bee10000000a42189d.html)
|
||||
|
||||
## Best-Effort statt Alarm
|
||||
|
||||
Automatisch abgeleitete DATABASE-/WCF-SAP-Ziele erhalten im lokalen Katalog
|
||||
`bestEffort="true"`. Sie werden normal dedupliziert und per TCP verbunden.
|
||||
|
||||
- Erfolg erhöht die getesteten und verfügbaren Zähler.
|
||||
- Timeout, DNS- oder Verbindungsfehler erhöht
|
||||
`biztalk_endpoints_best_effort_ignored`.
|
||||
- Der Fehler erscheint nicht als `unavailable=`, erzeugt keinen Warn-Logeintrag
|
||||
pro Minute und ändert den Checkmk-Status nicht.
|
||||
- Eine nicht ableitbare Adapteradresse zählt zu
|
||||
`biztalk_endpoints_expected_non_socket`, nicht zu `unresolved`.
|
||||
|
||||
Damit erfüllt der Check die gewünschte „versuchen, sonst ignorieren“-Semantik.
|
||||
Soll ein solches Ziel später verbindlich alarmieren, wird ein manueller
|
||||
Katalogeintrag mit `autoDiscovered="false"` und ohne
|
||||
`bestEffort="true"` hinterlegt. Reguläre automatisch erkannte Ziele bleiben
|
||||
ebenfalls streng: Ihre Nichterreichbarkeit ist weiterhin CRIT.
|
||||
|
||||
## Regression und Abnahme
|
||||
|
||||
Die automatisierten Tests decken Scheduler-Ausschluss, Oracle-Standard- und
|
||||
Explizitport, nicht unterstützte DATABASE-Anbieter, WCF-SAP-Gateway,
|
||||
URI-kodierten SAProuter, zielbasierte SAP-Konfiguration ohne ableitbaren Host,
|
||||
Geheimnisschutz und die nicht alarmierende Best-Effort-Ausgabe ab.
|
||||
|
||||
Für die Windows-/ACC-Abnahme nach Installation von 2.2.7:
|
||||
|
||||
1. Im Provider-Log muss der erzwungene Katalogabgleich erfolgreich sein.
|
||||
2. `BizTalk Endpoint Reachability` darf wegen Schedule, DATABASE oder WCF-SAP
|
||||
nicht mehr UNKNOWN werden.
|
||||
3. `biztalk_endpoints_unresolved` darf diese drei Klassen nicht enthalten.
|
||||
4. `biztalk_endpoints_best_effort` zeigt die tatsächlich abgeleiteten Oracle-
|
||||
und SAP-Ziele; nicht erreichbare davon erscheinen nur in
|
||||
`biztalk_endpoints_best_effort_ignored`.
|
||||
5. Ein absichtlich nicht erreichbares reguläres Testziel muss weiterhin CRIT
|
||||
auslösen, damit die strenge Standardalarmierung belegt bleibt.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title description">
|
||||
<title id="title">BizTalk Checkmk Pulse Architektur</title>
|
||||
<desc id="description">Ein privilegierter Scheduled Task liest BizTalk-Daten und prüft aktive Endpunkte. Er schreibt einen validierten Snapshot, den der Checkmk-Agent als LocalSystem nur konsumiert.</desc>
|
||||
<defs>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#2563eb"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#059669"/>
|
||||
</marker>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||
<feDropShadow dx="0" dy="3" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<style>
|
||||
.title { font: 700 28px Arial, sans-serif; fill: #0f172a; }
|
||||
.lane-title { font: 700 17px Arial, sans-serif; fill: #334155; }
|
||||
.box-title { font: 700 17px Arial, sans-serif; fill: #0f172a; }
|
||||
.box-text { font: 14px Arial, sans-serif; fill: #334155; }
|
||||
.small { font: 13px Arial, sans-serif; fill: #475569; }
|
||||
.label { font: 700 13px Arial, sans-serif; fill: #1e40af; }
|
||||
.provider-box { fill: #ffffff; stroke: #2563eb; stroke-width: 2; }
|
||||
.endpoint-box { fill: #ecfdf5; stroke: #059669; stroke-width: 2; }
|
||||
.snapshot-box { fill: #fff7ed; stroke: #ea580c; stroke-width: 2; }
|
||||
.consumer-box { fill: #ffffff; stroke: #64748b; stroke-width: 2; }
|
||||
.blue-arrow { fill: none; stroke: #2563eb; stroke-width: 3; marker-end: url(#arrow-blue); }
|
||||
.green-arrow { fill: none; stroke: #059669; stroke-width: 3; marker-end: url(#arrow-green); }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect width="1200" height="720" rx="18" fill="#f8fafc"/>
|
||||
<text x="60" y="52" class="title">BizTalk Checkmk Pulse</text>
|
||||
<text x="60" y="78" class="small">Privilegierte Datenerfassung und unprivilegierter Checkmk-Consumer</text>
|
||||
|
||||
<rect x="35" y="105" width="1130" height="355" rx="18" fill="#eff6ff" stroke="#bfdbfe" stroke-width="2"/>
|
||||
<text x="60" y="138" class="lane-title">Provider-Zone: dediziertes AD-Servicekonto</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="70" y="175" width="245" height="105" rx="14" class="provider-box"/>
|
||||
<text x="192" y="207" text-anchor="middle" class="box-title">Scheduled Task</text>
|
||||
<text x="192" y="232" text-anchor="middle" class="box-text">jede Minute, RunLevel Limited</text>
|
||||
<text x="192" y="254" text-anchor="middle" class="box-text">IgnoreNew verhindert Überlappung</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="395" y="165" width="310" height="125" rx="14" class="provider-box"/>
|
||||
<text x="550" y="198" text-anchor="middle" class="box-title">BizTalkCheckmkPulse --collect</text>
|
||||
<text x="550" y="225" text-anchor="middle" class="box-text">BizTalk-WMI + BizTalk-SQL</text>
|
||||
<text x="550" y="247" text-anchor="middle" class="box-text">Application Event Log</text>
|
||||
<text x="550" y="269" text-anchor="middle" class="box-text">aktive Send-/Receive-Artefakte</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="795" y="165" width="330" height="125" rx="14" class="endpoint-box"/>
|
||||
<text x="960" y="198" text-anchor="middle" class="box-title">Externe BizTalk-Endpunkte</text>
|
||||
<text x="960" y="225" text-anchor="middle" class="box-text">TCP: HTTP(S), SFTP/FTP, WCF/SQL, SMB</text>
|
||||
<text x="960" y="247" text-anchor="middle" class="box-text">UDP: explizite udp://-Ziele</text>
|
||||
<text x="960" y="269" text-anchor="middle" class="box-text">nur Started / Enabled</text>
|
||||
</g>
|
||||
|
||||
<path d="M315 227 H385" class="blue-arrow"/>
|
||||
<path d="M705 227 H785" class="green-arrow"/>
|
||||
<text x="745" y="211" text-anchor="middle" class="label" fill="#047857">TCP / UDP</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="395" y="335" width="310" height="90" rx="14" class="snapshot-box"/>
|
||||
<text x="550" y="367" text-anchor="middle" class="box-title">Validierter lokaler Snapshot</text>
|
||||
<text x="550" y="392" text-anchor="middle" class="box-text">atomar + versioniert + SHA-256</text>
|
||||
<text x="550" y="413" text-anchor="middle" class="small">Endpoint-Katalog und kompakte Checkmk-Daten</text>
|
||||
</g>
|
||||
<path d="M550 290 V325" class="blue-arrow"/>
|
||||
<text x="565" y="316" class="label">schreiben</text>
|
||||
|
||||
<line x1="35" y1="485" x2="1165" y2="485" stroke="#94a3b8" stroke-width="2" stroke-dasharray="9 7"/>
|
||||
<rect x="470" y="470" width="260" height="30" rx="15" fill="#f8fafc" stroke="#94a3b8"/>
|
||||
<text x="600" y="490" text-anchor="middle" class="small">Berechtigungsgrenze</text>
|
||||
|
||||
<rect x="35" y="510" width="1130" height="175" rx="18" fill="#f1f5f9" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="60" y="543" class="lane-title">Checkmk-Zone: LocalSystem ohne BizTalk-/SQL-Rechte</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="75" y="575" width="245" height="75" rx="14" class="consumer-box"/>
|
||||
<text x="197" y="607" text-anchor="middle" class="box-title">Checkmk Windows Agent</text>
|
||||
<text x="197" y="632" text-anchor="middle" class="box-text">LocalSystem</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="430" y="565" width="300" height="95" rx="14" class="consumer-box"/>
|
||||
<text x="580" y="597" text-anchor="middle" class="box-title">--consume</text>
|
||||
<text x="580" y="622" text-anchor="middle" class="box-text">liest nur den Snapshot</text>
|
||||
<text x="580" y="643" text-anchor="middle" class="box-text">kein WMI, SQL oder Netzwerkzugriff</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="840" y="575" width="285" height="75" rx="14" class="consumer-box"/>
|
||||
<text x="982" y="607" text-anchor="middle" class="box-title">9 kompakte Local Checks</text>
|
||||
<text x="982" y="632" text-anchor="middle" class="box-text">inkl. Endpoint Reachability</text>
|
||||
</g>
|
||||
|
||||
<path d="M320 612 H420" class="blue-arrow"/>
|
||||
<path d="M730 612 H830" class="blue-arrow"/>
|
||||
<path d="M705 380 H765 V545 H580 V555" class="blue-arrow"/>
|
||||
<text x="775" y="463" class="label">nur lesen</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -7,6 +7,5 @@ if errorlevel 1 (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
msbuild BizTalkCheckmkPulse.sln /p:Configuration=Release /p:Platform="Any CPU" /m /v:minimal
|
||||
msbuild "%~dp0..\BizTalkCheckmkPulse.sln" /p:Configuration=Release /p:Platform="Any CPU" /m:1 /v:minimal
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the external BizTalk Checkmk Pulse architecture overview as DOCX."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.section import WD_SECTION
|
||||
from docx.enum.table import WD_ALIGN_VERTICAL, WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Inches, Pt, RGBColor
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCS = ROOT / "docs"
|
||||
OUTPUT = DOCS / "BizTalk_Checkmk_Pulse_Architekturueberblick.docx"
|
||||
ARCHITECTURE_IMAGE = DOCS / "architecture.png"
|
||||
|
||||
NAVY = "17365D"
|
||||
BLUE = "24547C"
|
||||
MID_BLUE = "2B6F9F"
|
||||
LIGHT_BLUE = "EEF6FB"
|
||||
LIGHTER_BLUE = "F4F7FA"
|
||||
GREEN = "16845B"
|
||||
LIGHT_GREEN = "ECFDF5"
|
||||
ORANGE = "DD6B20"
|
||||
LIGHT_ORANGE = "FFF7ED"
|
||||
TEXT = "1F2937"
|
||||
MUTED = "5B6770"
|
||||
GRID = "CBD5E1"
|
||||
WHITE = "FFFFFF"
|
||||
|
||||
|
||||
def set_cell_shading(cell, fill):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
shd = tc_pr.find(qn("w:shd"))
|
||||
if shd is None:
|
||||
shd = OxmlElement("w:shd")
|
||||
tc_pr.append(shd)
|
||||
shd.set(qn("w:fill"), fill)
|
||||
|
||||
|
||||
def set_cell_margins(cell, top=120, start=120, bottom=120, end=120):
|
||||
tc = cell._tc
|
||||
tc_pr = tc.get_or_add_tcPr()
|
||||
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||||
if tc_mar is None:
|
||||
tc_mar = OxmlElement("w:tcMar")
|
||||
tc_pr.append(tc_mar)
|
||||
for margin, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
||||
node = tc_mar.find(qn(f"w:{margin}"))
|
||||
if node is None:
|
||||
node = OxmlElement(f"w:{margin}")
|
||||
tc_mar.append(node)
|
||||
node.set(qn("w:w"), str(value))
|
||||
node.set(qn("w:type"), "dxa")
|
||||
|
||||
|
||||
def set_repeat_table_header(row):
|
||||
tr_pr = row._tr.get_or_add_trPr()
|
||||
repeat = OxmlElement("w:tblHeader")
|
||||
repeat.set(qn("w:val"), "true")
|
||||
tr_pr.append(repeat)
|
||||
|
||||
|
||||
def prevent_row_split(row):
|
||||
tr_pr = row._tr.get_or_add_trPr()
|
||||
cant_split = OxmlElement("w:cantSplit")
|
||||
tr_pr.append(cant_split)
|
||||
|
||||
|
||||
def set_table_fixed(table):
|
||||
table.autofit = False
|
||||
tbl_pr = table._tbl.tblPr
|
||||
layout = tbl_pr.find(qn("w:tblLayout"))
|
||||
if layout is None:
|
||||
layout = OxmlElement("w:tblLayout")
|
||||
tbl_pr.append(layout)
|
||||
layout.set(qn("w:type"), "fixed")
|
||||
|
||||
|
||||
def set_paragraph_shading(paragraph, fill, border=None):
|
||||
p_pr = paragraph._p.get_or_add_pPr()
|
||||
shd = OxmlElement("w:shd")
|
||||
shd.set(qn("w:fill"), fill)
|
||||
p_pr.append(shd)
|
||||
if border:
|
||||
p_bdr = OxmlElement("w:pBdr")
|
||||
left = OxmlElement("w:left")
|
||||
left.set(qn("w:val"), "single")
|
||||
left.set(qn("w:sz"), "20")
|
||||
left.set(qn("w:space"), "8")
|
||||
left.set(qn("w:color"), border)
|
||||
p_bdr.append(left)
|
||||
p_pr.append(p_bdr)
|
||||
|
||||
|
||||
def add_bottom_border(paragraph, color="8FB8DC"):
|
||||
p_pr = paragraph._p.get_or_add_pPr()
|
||||
p_bdr = OxmlElement("w:pBdr")
|
||||
bottom = OxmlElement("w:bottom")
|
||||
bottom.set(qn("w:val"), "single")
|
||||
bottom.set(qn("w:sz"), "8")
|
||||
bottom.set(qn("w:space"), "5")
|
||||
bottom.set(qn("w:color"), color)
|
||||
p_bdr.append(bottom)
|
||||
p_pr.append(p_bdr)
|
||||
|
||||
|
||||
def set_keep(paragraph, keep_next=False, keep_lines=True):
|
||||
p_pr = paragraph._p.get_or_add_pPr()
|
||||
if keep_next:
|
||||
p_pr.append(OxmlElement("w:keepNext"))
|
||||
if keep_lines:
|
||||
p_pr.append(OxmlElement("w:keepLines"))
|
||||
|
||||
|
||||
def add_page_field(paragraph):
|
||||
run = paragraph.add_run()
|
||||
fld_char_1 = OxmlElement("w:fldChar")
|
||||
fld_char_1.set(qn("w:fldCharType"), "begin")
|
||||
instr_text = OxmlElement("w:instrText")
|
||||
instr_text.set(qn("xml:space"), "preserve")
|
||||
instr_text.text = " PAGE "
|
||||
fld_char_2 = OxmlElement("w:fldChar")
|
||||
fld_char_2.set(qn("w:fldCharType"), "end")
|
||||
run._r.extend((fld_char_1, instr_text, fld_char_2))
|
||||
|
||||
|
||||
def format_run(run, *, size=None, bold=None, color=None, italic=None, font="Liberation Sans"):
|
||||
run.font.name = font
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), font)
|
||||
if size is not None:
|
||||
run.font.size = Pt(size)
|
||||
if bold is not None:
|
||||
run.bold = bold
|
||||
if color:
|
||||
run.font.color.rgb = RGBColor.from_string(color)
|
||||
if italic is not None:
|
||||
run.italic = italic
|
||||
return run
|
||||
|
||||
|
||||
def add_rich_paragraph(document, parts, *, style=None, before=0, after=5, align=None):
|
||||
paragraph = document.add_paragraph(style=style)
|
||||
paragraph.paragraph_format.space_before = Pt(before)
|
||||
paragraph.paragraph_format.space_after = Pt(after)
|
||||
paragraph.paragraph_format.line_spacing = 1.12
|
||||
if align is not None:
|
||||
paragraph.alignment = align
|
||||
for text, options in parts:
|
||||
run = paragraph.add_run(text)
|
||||
format_run(run, **options)
|
||||
return paragraph
|
||||
|
||||
|
||||
def add_body(document, text, *, bold_lead=None, after=5):
|
||||
paragraph = document.add_paragraph()
|
||||
paragraph.paragraph_format.space_after = Pt(after)
|
||||
paragraph.paragraph_format.line_spacing = 1.13
|
||||
if bold_lead and text.startswith(bold_lead):
|
||||
format_run(paragraph.add_run(bold_lead), bold=True, color=TEXT)
|
||||
format_run(paragraph.add_run(text[len(bold_lead):]), color=TEXT)
|
||||
else:
|
||||
format_run(paragraph.add_run(text), color=TEXT)
|
||||
return paragraph
|
||||
|
||||
|
||||
def add_heading(document, text, level=1):
|
||||
paragraph = document.add_paragraph()
|
||||
paragraph.paragraph_format.space_before = Pt(12 if level == 1 else 8)
|
||||
paragraph.paragraph_format.space_after = Pt(6 if level == 1 else 4)
|
||||
size = 17 if level == 1 else 12
|
||||
format_run(paragraph.add_run(text), size=size, bold=True, color=NAVY)
|
||||
if level == 1:
|
||||
add_bottom_border(paragraph)
|
||||
set_keep(paragraph, keep_next=True)
|
||||
return paragraph
|
||||
|
||||
|
||||
def add_bullet(document, text, *, level=0):
|
||||
paragraph = document.add_paragraph(style="List Bullet" if level == 0 else "List Bullet 2")
|
||||
paragraph.paragraph_format.left_indent = Cm(0.65 + 0.45 * level)
|
||||
paragraph.paragraph_format.first_line_indent = Cm(-0.25)
|
||||
paragraph.paragraph_format.space_after = Pt(2.5)
|
||||
paragraph.paragraph_format.line_spacing = 1.08
|
||||
format_run(paragraph.add_run(text), color=TEXT)
|
||||
return paragraph
|
||||
|
||||
|
||||
def add_numbered(document, text):
|
||||
paragraph = document.add_paragraph(style="List Number")
|
||||
paragraph.paragraph_format.left_indent = Cm(0.7)
|
||||
paragraph.paragraph_format.first_line_indent = Cm(-0.3)
|
||||
paragraph.paragraph_format.space_after = Pt(3)
|
||||
paragraph.paragraph_format.line_spacing = 1.08
|
||||
format_run(paragraph.add_run(text), color=TEXT)
|
||||
return paragraph
|
||||
|
||||
|
||||
def add_callout(document, label, text, *, fill=LIGHT_BLUE, border=MID_BLUE):
|
||||
table = document.add_table(rows=1, cols=1)
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
table.autofit = False
|
||||
cell = table.cell(0, 0)
|
||||
cell.width = Cm(17.2)
|
||||
set_cell_shading(cell, fill)
|
||||
set_cell_margins(cell, top=170, start=230, bottom=170, end=230)
|
||||
paragraph = cell.paragraphs[0]
|
||||
paragraph.paragraph_format.space_after = Pt(0)
|
||||
paragraph.paragraph_format.line_spacing = 1.12
|
||||
format_run(paragraph.add_run(label), bold=True, color=border)
|
||||
format_run(paragraph.add_run(text), color=TEXT)
|
||||
table.rows[0]._tr.get_or_add_trPr().append(OxmlElement("w:cantSplit"))
|
||||
after = document.add_paragraph()
|
||||
after.paragraph_format.space_after = Pt(1)
|
||||
return table
|
||||
|
||||
|
||||
def add_table(document, headers, rows, widths, *, font_size=8.7):
|
||||
table = document.add_table(rows=1, cols=len(headers))
|
||||
table.style = "Table Grid"
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
set_table_fixed(table)
|
||||
header = table.rows[0]
|
||||
set_repeat_table_header(header)
|
||||
prevent_row_split(header)
|
||||
for index, title in enumerate(headers):
|
||||
cell = header.cells[index]
|
||||
cell.width = Cm(widths[index])
|
||||
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||
set_cell_shading(cell, BLUE)
|
||||
set_cell_margins(cell, top=100, start=110, bottom=100, end=110)
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
format_run(p.add_run(title), bold=True, color=WHITE, size=font_size)
|
||||
for row_index, values in enumerate(rows):
|
||||
row = table.add_row()
|
||||
prevent_row_split(row)
|
||||
for col_index, value in enumerate(values):
|
||||
cell = row.cells[col_index]
|
||||
cell.width = Cm(widths[col_index])
|
||||
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.TOP
|
||||
set_cell_margins(cell, top=90, start=110, bottom=90, end=110)
|
||||
if row_index % 2 == 1:
|
||||
set_cell_shading(cell, LIGHTER_BLUE)
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
p.paragraph_format.line_spacing = 1.03
|
||||
format_run(p.add_run(str(value)), color=TEXT, size=font_size)
|
||||
spacer = document.add_paragraph()
|
||||
spacer.paragraph_format.space_after = Pt(1)
|
||||
return table
|
||||
|
||||
|
||||
def add_code_block(document, lines):
|
||||
paragraph = document.add_paragraph()
|
||||
paragraph.paragraph_format.left_indent = Cm(0.15)
|
||||
paragraph.paragraph_format.right_indent = Cm(0.15)
|
||||
paragraph.paragraph_format.space_before = Pt(3)
|
||||
paragraph.paragraph_format.space_after = Pt(7)
|
||||
paragraph.paragraph_format.line_spacing = 1.0
|
||||
set_paragraph_shading(paragraph, "F3F6F8", MID_BLUE)
|
||||
for index, line in enumerate(lines):
|
||||
run = paragraph.add_run(line)
|
||||
format_run(run, font="Liberation Mono", size=7.8, color="0F3E5E")
|
||||
if index < len(lines) - 1:
|
||||
run.add_break()
|
||||
set_keep(paragraph)
|
||||
return paragraph
|
||||
|
||||
|
||||
def configure_document(document):
|
||||
section = document.sections[0]
|
||||
section.page_width = Cm(21.0)
|
||||
section.page_height = Cm(29.7)
|
||||
section.top_margin = Cm(1.55)
|
||||
section.bottom_margin = Cm(1.55)
|
||||
section.left_margin = Cm(1.7)
|
||||
section.right_margin = Cm(1.7)
|
||||
section.header_distance = Cm(0.65)
|
||||
section.footer_distance = Cm(0.65)
|
||||
section.different_first_page_header_footer = True
|
||||
|
||||
styles = document.styles
|
||||
normal = styles["Normal"]
|
||||
normal.font.name = "Liberation Sans"
|
||||
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Liberation Sans")
|
||||
normal.font.size = Pt(9.6)
|
||||
normal.font.color.rgb = RGBColor.from_string(TEXT)
|
||||
normal.paragraph_format.space_after = Pt(4)
|
||||
|
||||
for name in ("List Bullet", "List Bullet 2", "List Number"):
|
||||
styles[name].font.name = "Liberation Sans"
|
||||
styles[name]._element.rPr.rFonts.set(qn("w:eastAsia"), "Liberation Sans")
|
||||
styles[name].font.size = Pt(9.3)
|
||||
|
||||
header = section.header
|
||||
p = header.paragraphs[0]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
format_run(p.add_run("BizTalk Checkmk Pulse | Architektur- und Lösungsüberblick"), size=7.5, color=MUTED)
|
||||
|
||||
footer = section.footer
|
||||
p = footer.paragraphs[0]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
format_run(p.add_run("Version 2.2.6 | Stand 11.08.2026 | Seite "), size=7.5, color=MUTED)
|
||||
add_page_field(p)
|
||||
|
||||
|
||||
def build_document():
|
||||
if not ARCHITECTURE_IMAGE.exists():
|
||||
raise FileNotFoundError(f"Architecture image not found: {ARCHITECTURE_IMAGE}")
|
||||
|
||||
document = Document()
|
||||
configure_document(document)
|
||||
core = document.core_properties
|
||||
core.title = "BizTalk Checkmk Pulse – Architektur- und Lösungsüberblick"
|
||||
core.subject = "Gesamtarchitektur, Funktionsumfang und Datenaustausch"
|
||||
core.author = "BEW"
|
||||
core.comments = "Externe Lösungsübersicht zum implementierten Stand 2.2.6"
|
||||
|
||||
# Cover
|
||||
banner = document.add_table(rows=1, cols=1)
|
||||
banner.autofit = False
|
||||
banner.cell(0, 0).width = Cm(17.3)
|
||||
set_cell_shading(banner.cell(0, 0), NAVY)
|
||||
set_cell_margins(banner.cell(0, 0), top=170, start=170, bottom=170, end=170)
|
||||
p = banner.cell(0, 0).paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
format_run(p.add_run("LÖSUNGSÜBERSICHT · EXTERNE DARSTELLUNG"), bold=True, color=WHITE, size=9)
|
||||
|
||||
p = document.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(32)
|
||||
p.paragraph_format.space_after = Pt(10)
|
||||
format_run(p.add_run("BizTalk Checkmk Pulse"), size=28, bold=True, color=NAVY)
|
||||
p = document.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(26)
|
||||
format_run(p.add_run("Architektur, Funktionsumfang und Datenaustausch des lokalen Checkmk-Monitorings für Microsoft BizTalk Server"), size=15, color="4B6478")
|
||||
|
||||
add_callout(
|
||||
document,
|
||||
"",
|
||||
"Die Lösung übersetzt den technischen Laufzeitzustand einer BizTalk-Umgebung in neun kompakte Checkmk-Services. Ein dediziertes Konto erfasst die benötigten Daten; der Checkmk-Agent selbst erhält keine BizTalk- oder SQL-Berechtigungen.",
|
||||
fill=BLUE,
|
||||
border=WHITE,
|
||||
)
|
||||
# Callout helper uses dark text; force white on the cover callout.
|
||||
for run in document.tables[-1].cell(0, 0).paragraphs[0].runs:
|
||||
run.font.color.rgb = RGBColor.from_string(WHITE)
|
||||
run.font.size = Pt(12.5)
|
||||
|
||||
facts = document.add_table(rows=1, cols=4)
|
||||
facts.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
set_table_fixed(facts)
|
||||
for cell, value, label in zip(
|
||||
facts.rows[0].cells,
|
||||
("9", "1 min", "180 s", "Read-only"),
|
||||
("stabile Services", "Sammelintervall", "Frischegrenze", "BizTalk-Zugriff"),
|
||||
):
|
||||
cell.width = Cm(4.3)
|
||||
set_cell_shading(cell, "EAF2F8")
|
||||
set_cell_margins(cell, top=180, start=80, bottom=180, end=80)
|
||||
p = cell.paragraphs[0]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
format_run(p.add_run(value), size=15, bold=True, color=NAVY)
|
||||
p = cell.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
format_run(p.add_run(label), size=8.3, color=TEXT)
|
||||
|
||||
document.add_paragraph().paragraph_format.space_after = Pt(3)
|
||||
add_table(
|
||||
document,
|
||||
("Dokumentmerkmal", "Angabe"),
|
||||
(
|
||||
("Lösungsstand", "BizTalk Checkmk Pulse 2.2.6"),
|
||||
("Zielplattform", "Microsoft BizTalk Server 2020 · Windows Server 2019 · Checkmk 2.4"),
|
||||
("Dokumentstand", "11. August 2026"),
|
||||
("Dokumentzweck", "Architektur- und Leistungsüberblick für technische Stakeholder"),
|
||||
),
|
||||
(4.2, 13.0),
|
||||
font_size=8.8,
|
||||
)
|
||||
add_rich_paragraph(
|
||||
document,
|
||||
(("Dieses Dokument beschreibt den implementierten Lösungsstand. Umgebungsspezifische Parameter und die jeweilige Betriebsfreigabe werden im Rollout bestätigt.", {"size": 8.2, "color": MUTED}),),
|
||||
before=10,
|
||||
after=0,
|
||||
)
|
||||
document.add_page_break()
|
||||
|
||||
# 1–2: Summary and Checkmk overview
|
||||
add_heading(document, "1. Zusammenfassung")
|
||||
add_body(document, "BizTalk Checkmk Pulse ergänzt das zentrale Monitoring um eine BizTalk-spezifische Sicht. Die Lösung erkennt typische Betriebsstörungen – zum Beispiel suspendierte Instanzen, gestoppte Host Instances, unerwartet inaktive Ports, nicht erreichbare externe Ziele oder aktuelle BizTalk-Ereignisse – und stellt sie als eigenständige Services in Checkmk dar.")
|
||||
add_body(document, "Die Architektur folgt einer klaren Aufgabentrennung:")
|
||||
add_numbered(document, "Ein minütlicher Windows Scheduled Task sammelt unter einem dedizierten, eingeschränkt berechtigten AD-Servicekonto die BizTalk-Betriebsdaten.")
|
||||
add_numbered(document, "Die Ergebnisse werden in einen kompakten, versionierten und integritätsgeprüften lokalen Snapshot geschrieben.")
|
||||
add_numbered(document, "Der Checkmk Windows Agent läuft weiterhin als LocalSystem und liest ausschließlich diesen Snapshot.")
|
||||
add_numbered(document, "Checkmk übernimmt Status, Metriken und Kurztexte in das zentrale Monitoring.")
|
||||
add_callout(document, "Nutzen: ", "BizTalk-Störungen werden zentral sichtbar, ohne dem Checkmk-Agenten direkte BizTalk- oder SQL-Rechte zu erteilen. Die Ausgabe bleibt kompakt, graphfähig und alarmierbar.", fill=LIGHT_GREEN, border=GREEN)
|
||||
|
||||
add_heading(document, "2. Was ist Checkmk – und was ist ein Local Check?")
|
||||
add_body(document, "Checkmk ist eine zentrale Monitoring-Plattform für IT-Infrastrukturen und Anwendungen. Auf überwachten Servern liefert ein Agent technische Zustände und Messwerte an die Checkmk-Instanz. Dort werden die Daten als Hosts und Services dargestellt, historisiert und für Dashboards, Schwellwerte und Benachrichtigungen verwendet.")
|
||||
add_body(document, "Ein Local Check ist eine bewusst einfache Erweiterungsmöglichkeit des Checkmk-Agenten. Die Prüfung läuft auf dem Zielsystem und gibt pro Service eine Textzeile mit vier Bestandteilen aus:")
|
||||
add_code_block(document, ('0 "BizTalk Send Ports" biztalk_send_ports_total=31;;;0|biztalk_send_ports_started=31;;;0', 'Alle Send Ports sind aktiv.'))
|
||||
add_table(
|
||||
document,
|
||||
("Bestandteil", "Bedeutung"),
|
||||
(
|
||||
("0", "Status: 0 OK, 1 WARN, 2 CRIT, 3 UNKNOWN"),
|
||||
('"BizTalk …"', "Eindeutiger Servicename, der in Checkmk angezeigt wird"),
|
||||
("biztalk_…=Wert", "Metriken für Schwellwerte, Auswertungen und Zeitreihen"),
|
||||
("Kurztext", "Lesbare Zusammenfassung; bei Fehlern mit begrenzter Liste betroffener Objekte"),
|
||||
),
|
||||
(4.0, 13.2),
|
||||
)
|
||||
add_body(document, "Der lokale Check dieser Lösung führt im Agentenpfad keine aufwendige BizTalk-Abfrage aus. Er startet lediglich den Consumer, validiert den vorbereiteten Snapshot und schreibt die bereits erzeugten Checkmk-Zeilen auf die Standardausgabe. Damit bleibt der Agentenaufruf schnell und seine Berechtigungsfläche klein.")
|
||||
add_rich_paragraph(
|
||||
document,
|
||||
(
|
||||
("Referenz: ", {"size": 8.0, "bold": True, "color": MUTED}),
|
||||
("Checkmk User Guide, „Local checks“ – https://docs.checkmk.com/latest/en/localchecks.html", {"size": 8.0, "color": MID_BLUE}),
|
||||
),
|
||||
after=0,
|
||||
)
|
||||
document.add_page_break()
|
||||
|
||||
# 3: Architecture
|
||||
add_heading(document, "3. Gesamtarchitektur")
|
||||
p = document.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
run = p.add_run()
|
||||
run.add_picture(str(ARCHITECTURE_IMAGE), width=Cm(16.9))
|
||||
caption = document.add_paragraph()
|
||||
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
caption.paragraph_format.space_after = Pt(6)
|
||||
format_run(caption.add_run("Abbildung 1: Trennung von privilegierter Datenerfassung, lokalem Datentransport und Checkmk-Consumer"), size=7.8, color=MUTED, italic=True)
|
||||
add_table(
|
||||
document,
|
||||
("Baustein", "Aufgabe", "Berechtigungsprofil"),
|
||||
(
|
||||
("Scheduled Task", "Startet den Collector jede Minute und verhindert parallele Läufe.", "Dediziertes AD-Servicekonto, RunLevel Limited"),
|
||||
("Collector (--collect)", "Liest BizTalk-WMI, prüft integrierten SQL-Zugriff, Event Log und aktive Endpunkte.", "BizTalk Read-only; kein lokaler Administrator, kein SQL-sysadmin"),
|
||||
("Snapshot & Katalog", "Lokaler, ACL-geschützter Übergabepunkt zwischen Collector und Consumer.", "Collector schreibt; LocalSystem liest"),
|
||||
("Consumer (--consume)", "Prüft Format, Maschine, Alter, Zeilenanzahl und SHA-256; gibt gültige Checkmk-Zeilen aus.", "LocalSystem; kein WMI-, SQL- oder Endpoint-Zugriff"),
|
||||
("Checkmk", "Erkennt neun Services, übernimmt Metriken und löst regelbasiert Benachrichtigungen aus.", "Zentraler Monitoring-Betrieb"),
|
||||
),
|
||||
(3.5, 8.1, 5.6),
|
||||
font_size=7.9,
|
||||
)
|
||||
add_callout(document, "Fail-safe-Verhalten: ", "Fehlt der Snapshot, ist er älter als standardmäßig 180 Sekunden oder schlägt eine Integritätsprüfung fehl, liefert der Consumer neun gültige UNKNOWN-Services. Ein Transportfehler wird sichtbar und nicht als gesunder Zustand interpretiert.")
|
||||
add_heading(document, "Architekturprinzipien", level=2)
|
||||
add_bullet(document, "Least Privilege: Nur das Collector-Konto erhält den erforderlichen lesenden BizTalk-Zugriff.")
|
||||
add_bullet(document, "Atomare Übergabe: Der Collector ersetzt den Snapshot erst nach vollständigem Schreiben.")
|
||||
add_bullet(document, "Begrenzte Laufzeit: Timeouts, Parallelität, maximale Endpunktzahl und Textlängen sind konfiguriert.")
|
||||
add_bullet(document, "Stabile Schnittstelle: Die neun Basisservices bleiben auch bei Fehlern vorhanden.")
|
||||
add_bullet(document, "Stabile Namen: Die Umgebungskennzeichnung ändert vorhandene Checkmk-Services standardmäßig nicht.")
|
||||
document.add_page_break()
|
||||
|
||||
# 4: Monitoring scope
|
||||
add_heading(document, "4. Was überwacht die Lösung?")
|
||||
add_body(document, "Die Lösung erzeugt standardmäßig neun kompakte Services. Die einzelnen Zustände können in Checkmk separat visualisiert, alarmiert und historisiert werden.")
|
||||
add_table(
|
||||
document,
|
||||
("Service", "Überwachte Aussage", "Typische Messwerte"),
|
||||
(
|
||||
("BizTalk Platform", "WMI-Zugang, BizTalk-Gruppe und zentrale Datenbankziele sind ermittelbar.", "Plattform- und Zielinformationen"),
|
||||
("BizTalk SQL Access", "Collector-Konto kann die BizTalk-Datenbanken per Windows-Authentifizierung öffnen.", "Ziele gesamt, erreichbar, fehlgeschlagen"),
|
||||
("BizTalk Suspended Instances", "Resumable/non-resumable Instanzen und Routing Failure Reports.", "Anzahlen je Kategorie"),
|
||||
("BizTalk Host Instances", "Lokale Host Instances laufen oder befinden sich in Stop-/Übergangszuständen.", "Started, stopped, pending, unknown"),
|
||||
("BizTalk Receive Locations", "Aktivierungen sowie erwartete oder unerwartete Deaktivierungen.", "Enabled, expected/unexpected disabled"),
|
||||
("BizTalk Send Ports", "Gestartete sowie erwartet oder unerwartet inaktive Send Ports.", "Started, stopped, bound, unknown"),
|
||||
("BizTalk Endpoint Reachability", "Technische Ziele aktiver Send-/Receive-Artefakte sind über Host/Port erreichbar.", "Aktiv, getestet, erreichbar, fehlerhaft, Dauer"),
|
||||
("BizTalk Orchestrations", "Gestartete, gestoppte, gebundene, ungebundene oder unbekannte Zustände.", "Anzahlen je Laufzeitstatus"),
|
||||
("BizTalk Event Log", "Aktuelle BizTalk-bezogene Fehler und Warnungen im Application Log.", "Errors und Warnings im Zeitfenster"),
|
||||
),
|
||||
(4.5, 8.2, 4.5),
|
||||
font_size=8.0,
|
||||
)
|
||||
add_heading(document, "Endpoint-Prüfung", level=2)
|
||||
add_body(document, "Die Erreichbarkeitsprüfung beschränkt sich auf aktuell gestartete Send Ports und aktivierte Receive Locations. Adressen werden auf Protokoll, Host und Port reduziert, dedupliziert und mit begrenzter Parallelität geprüft. Im gesunden Zustand erscheint nur eine Gesamtaussage; bei Fehlern werden ausschließlich die nicht erreichbaren Ziele begrenzt aufgelistet.")
|
||||
add_callout(document, "Wichtige Abgrenzung: ", "Die Endpoint-Prüfung ist ein technischer Netzwerkcheck. Sie bestätigt je nach Protokoll DNS, Route, Firewall und einen annehmenden TCP-Port beziehungsweise den lokalen UDP-Versand. Sie führt keine Anmeldung durch, sendet keine BizTalk-Nachricht und bewertet nicht die fachliche Funktion des Zielsystems.", fill=LIGHT_ORANGE, border=ORANGE)
|
||||
document.add_page_break()
|
||||
|
||||
# 5: Data exchange
|
||||
add_heading(document, "5. Welche Daten werden ausgetauscht?")
|
||||
add_body(document, "Der Datenaustausch besteht aus drei klar getrennten Stufen. Verarbeitet werden Betriebs- und Konfigurationsmetadaten, keine fachlichen Nachrichteninhalte.")
|
||||
add_table(
|
||||
document,
|
||||
("Stufe", "Dateninhalt", "Transport und Schutz"),
|
||||
(
|
||||
("1. Quellen → Collector", "BizTalk-Gruppe und Datenbankziele; Namen und Laufzeitzustände von Host Instances, suspendierten Instanzen, Receive Locations, Send Ports und Orchestrations; Event-Log-Zähler; Host/Port aktiver Endpunkte.", "Lokale WMI-Abfragen, integrierter SQL-Verbindungstest, lokales Event Log und begrenzte TCP-/UDP-Probes."),
|
||||
("2. Collector → Snapshot", "Erzeugungszeit, Quellmaschine, Collector-Identität, Zeilenanzahl, SHA-256 und neun Checkmk-Zeilen mit Status, Metriken und Kurztext.", "Lokale Datei, atomar ersetzt, ACL-geschützt, standardmäßig maximal 1 MiB."),
|
||||
("3. Consumer → Checkmk", "Validierte Local-Check-Zeilen: Status, Zähler, Summaries sowie bei Störungen begrenzte Objekt- oder Ziellisten.", "Standardausgabe des Local Checks als Bestandteil der Checkmk-Agentenausgabe."),
|
||||
),
|
||||
(3.7, 8.2, 5.3),
|
||||
font_size=8.0,
|
||||
)
|
||||
add_heading(document, "Beispiel des lokalen Snapshot-Vertrags", level=2)
|
||||
add_code_block(
|
||||
document,
|
||||
(
|
||||
"BIZTALK_CHECKMK_PULSE_SNAPSHOT_V2",
|
||||
"generatedUtc=2026-08-10T08:15:00.0000000Z",
|
||||
"machineBase64=QlRaLVBSSC0wMQ==",
|
||||
"identityBase64=RE9NQUlOXHN2Y19iaXp0YWxrX21vbml0b3Jpbmc=",
|
||||
"payloadLines=9",
|
||||
"payloadSha256=<64 hexadezimale Zeichen>",
|
||||
"",
|
||||
'0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|... Suspended total=0.',
|
||||
'0 "BizTalk Endpoint Reachability" biztalk_endpoints_active=61;;;0|... Alle 61 aktiven Endpunkte sind erreichbar.',
|
||||
),
|
||||
)
|
||||
add_body(document, "Die Base64-Felder verhindern problematische Trennzeichen im Header; sie sind keine Verschlüsselung. SHA-256 erkennt unvollständige oder veränderte Snapshots. Der Schutz vor unberechtigtem lokalem Zugriff erfolgt über Windows-Dateirechte.")
|
||||
add_heading(document, "Enthaltene und nicht enthaltene Informationen", level=2)
|
||||
add_table(
|
||||
document,
|
||||
("Im Monitoring enthalten", "Nicht erhoben oder übertragen"),
|
||||
(
|
||||
("Technische Status- und Mengeninformationen", "BizTalk-Nachrichten oder fachliche Payloads"),
|
||||
("Relevante BizTalk-Anwendungs- und Artefaktnamen", "Passwörter, Tokens oder Endpoint-Zugangsdaten"),
|
||||
("DB-Server/-Namen und Ausführungsidentität zur Diagnose", "Vollständige Inhalte der BizTalk-Datenbanken"),
|
||||
("Bei Endpoint-Fehlern: Protokoll, Host und Port", "Anmeldungen oder fachliche Requests an Zielsysteme"),
|
||||
("Fehlerkategorien und begrenzte Diagnosetexte", "Dateiinhalte von UNC-/SMB-Freigaben"),
|
||||
),
|
||||
(8.6, 8.6),
|
||||
font_size=8.2,
|
||||
)
|
||||
add_callout(document, "Dateneinordnung: ", "Die Ausgabe enthält technische Betriebsmetadaten. Artefakt-, Server- und Zielnamen können Rückschlüsse auf Integrationen zulassen und sollten innerhalb der bestehenden Zugriffs- und Aufbewahrungsregeln des Monitorings behandelt werden.")
|
||||
document.add_page_break()
|
||||
|
||||
# 6–7: Operation and conclusion
|
||||
add_heading(document, "6. Betrieb, Sicherheit und Grenzen")
|
||||
add_heading(document, "Betriebsmodell", level=2)
|
||||
add_bullet(document, "Collector jede Minute; parallele Läufe werden verhindert.")
|
||||
add_bullet(document, "Endpoint-Katalog übernimmt relevante Kandidatenänderungen im nächsten Minutenlauf und wird spätestens wöchentlich vollständig abgeglichen.")
|
||||
add_bullet(document, "Nicht automatisch prüfbare Adapter bleiben als Abdeckungsmetriken sichtbar, ohne erfolgreiche Netzwerkprobes pauschal zu entwerten.")
|
||||
add_bullet(document, "Getrennte Tageslogs für Provider und Consumer; Standardaufbewahrung 30 Tage.")
|
||||
add_bullet(document, "Erwartet inaktive Receive Locations und Send Ports können exakt allowlisted werden und bleiben als Messwert sichtbar.")
|
||||
add_bullet(document, "Endpoint-Prüfung standardmäßig mit maximal 100 Zielen, 16 parallelen Probes und 3 Sekunden Timeout je Ziel.")
|
||||
add_bullet(document, "Der Installer validiert neue Versionen auf exakt neun Services, akzeptiert vollständige Legacy-Verträge mit weniger Services und stoppt entfernte oder umbenannte Servicenamen vor jeder Umschaltung.")
|
||||
add_bullet(document, "Ein laufbezogenes Setup-Log protokolliert Phase, Binary-Version, Self-Test-Ausgabe, Taskcodes, Exception-Kette und Rollback ohne Kennwort.")
|
||||
add_bullet(document, "Nach der Umschaltung erzwingt das Setup einen frischen Katalogabgleich. Kernservice-UNKNOWN bleibt blockierend; ein isoliertes Endpoint-Reachability-UNKNOWN bleibt als sichtbare Betriebswarnung erhalten und löst keinen Rollback aus.")
|
||||
|
||||
add_heading(document, "Sicherheitsmodell", level=2)
|
||||
add_table(
|
||||
document,
|
||||
("Kontrolle", "Wirkung"),
|
||||
(
|
||||
("Dediziertes Servicekonto", "BizTalk-/SQL-Zugriff ist auf den Collector begrenzt und wird nicht auf alle LocalSystem-Dienste ausgeweitet."),
|
||||
("BizTalk Read-Only-Gruppe", "Lesende BizTalk-Rolle; individuelle SQL-Rechte oder SQL-sysadmin sind nicht vorgesehen."),
|
||||
("ACL-getrennter Snapshot", "Collector schreibt, LocalSystem liest; Konfiguration und Logs werden getrennt behandelt."),
|
||||
("Integritäts- und Altersprüfung", "Falsche Maschine, fehlerhaftes Format, abweichende Prüfsumme oder veraltete Daten führen zu UNKNOWN."),
|
||||
("Begrenzte Ausgabe", "Listen und Diagnosen werden gekürzt; vollständige Zähler bleiben als Metriken erhalten."),
|
||||
),
|
||||
(4.6, 12.6),
|
||||
font_size=8.3,
|
||||
)
|
||||
|
||||
add_heading(document, "Bewusste Grenzen", level=2)
|
||||
add_bullet(document, "Die Lösung ersetzt nicht das allgemeine Checkmk-MSSQL-Plugin und ist kein vollständiges SQL-Monitoring.")
|
||||
add_bullet(document, "Sie bewertet keine fachliche End-to-End-Verarbeitung einer BizTalk-Schnittstelle.")
|
||||
add_bullet(document, "Ein erreichbarer Port beweist nicht, dass Anmeldung, Protokoll oder Zielanwendung fachlich funktionieren.")
|
||||
add_bullet(document, "SHA-256 schützt die Übergabe vor unbemerkten Fehlern, nicht vor einem lokalen Administrator.")
|
||||
add_bullet(document, "Schwellwerte, Allowlist-Einträge und Benachrichtigungen bleiben Teil der betrieblichen Konfiguration.")
|
||||
|
||||
add_heading(document, "7. Fazit")
|
||||
add_body(document, "BizTalk Checkmk Pulse schafft eine klar abgegrenzte Brücke zwischen BizTalk Server und Checkmk. Die Lösung liefert eine verständliche, alarmierbare Sicht auf die wichtigsten Laufzeitkomponenten, hält den Checkmk-Agenten von privilegierten Datenquellen fern und macht auch Fehler im Datentransport selbst sichtbar. Der ausgetauschte Datenumfang bleibt auf technische Betriebsmetadaten beschränkt.")
|
||||
add_callout(document, "Kernaussage: ", "Ein privilegierter Read-only-Collector sammelt, ein unprivilegierter Consumer validiert und übergibt – Checkmk erhält neun stabile Services statt direkten Zugriff auf BizTalk oder SQL.", fill=LIGHT_GREEN, border=GREEN)
|
||||
add_rich_paragraph(
|
||||
document,
|
||||
(("Dokumentbasis: Implementierung und Projektdokumentation von BizTalk Checkmk Pulse 2.2.6 sowie Checkmk User Guide „Local checks“, abgerufen am 10. August 2026.", {"size": 7.8, "color": MUTED}),),
|
||||
before=8,
|
||||
after=0,
|
||||
)
|
||||
|
||||
document.save(OUTPUT)
|
||||
print(OUTPUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_document()
|
||||
@@ -4,17 +4,8 @@ setlocal
|
||||
call "%~dp0build-release.cmd"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
set "ROOT=%~dp0.."
|
||||
set "OUT=%ROOT%\artifacts\BizTalkCheckmkPulse-deploy"
|
||||
set "APP=%OUT%\BizTalkCheckmkPulse"
|
||||
|
||||
if exist "%OUT%" rmdir /s /q "%OUT%"
|
||||
mkdir "%APP%"
|
||||
|
||||
copy "%ROOT%\deployment\checkmk\biztalk_checkmk_pulse.cmd" "%OUT%\" >nul
|
||||
copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe" "%APP%\" >nul
|
||||
copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe.config" "%APP%\" >nul
|
||||
|
||||
echo Deployment package written to %OUT%
|
||||
echo Install package written to artifacts\BizTalkCheckmkPulse-Setup
|
||||
echo Install ZIP written to artifacts\BizTalkCheckmkPulse-Setup.zip
|
||||
echo Certutil Base64 written to artifacts\BizTalkCheckmkPulse-Setup.zip.b64.txt
|
||||
echo SHA-256 written to artifacts\BizTalkCheckmkPulse-Setup.zip.sha256.txt
|
||||
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%
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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>{B35A04E0-253B-4CEB-9033-F5109F3B9F82}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>BizTalkCheckmkPulse.Packager</RootNamespace>
|
||||
<AssemblyName>BizTalkCheckmkPulse.Packager</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" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BizTalkCheckmkPulse\BizTalkCheckmkPulse.csproj">
|
||||
<Project>{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}</Project>
|
||||
<Name>BizTalkCheckmkPulse</Name>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\BizTalkCheckmkPulse.Setup\BizTalkCheckmkPulse.Setup.csproj">
|
||||
<Project>{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}</Project>
|
||||
<Name>BizTalkCheckmkPulse.Setup</Name>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="CreateInstallPackage" AfterTargets="Build" Condition=" '$(Configuration)' == 'Release' ">
|
||||
<Exec Command=""$(TargetPath)" "$(MSBuildProjectDirectory)\..\.." "$(Configuration)"" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkCheckmkPulse.Packager
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length != 2) throw new ArgumentException("Usage: BizTalkCheckmkPulse.Packager.exe <repository-root> <configuration>");
|
||||
var root = Path.GetFullPath(args[0]);
|
||||
var configuration = args[1];
|
||||
var artifacts = Path.Combine(root, "artifacts");
|
||||
var package = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup");
|
||||
var application = Path.Combine(package, "application");
|
||||
var zip = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup.zip");
|
||||
var base64 = zip + ".b64.txt";
|
||||
var checksum = zip + ".sha256.txt";
|
||||
|
||||
if (Directory.Exists(package)) Directory.Delete(package, true);
|
||||
Directory.CreateDirectory(application);
|
||||
|
||||
Copy(
|
||||
Path.Combine(root, "src", "BizTalkCheckmkPulse.Setup", "bin", configuration, "BizTalkCheckmkPulse.Setup.exe"),
|
||||
Path.Combine(package, "Setup.exe"));
|
||||
Copy(
|
||||
Path.Combine(root, "src", "BizTalkCheckmkPulse", "bin", configuration, "BizTalkCheckmkPulse.exe"),
|
||||
Path.Combine(application, "BizTalkCheckmkPulse.exe"));
|
||||
Copy(
|
||||
Path.Combine(root, "src", "BizTalkCheckmkPulse", "bin", configuration, "BizTalkCheckmkPulse.exe.config"),
|
||||
Path.Combine(application, "BizTalkCheckmkPulse.exe.config"));
|
||||
Copy(
|
||||
Path.Combine(root, "deployment", "checkmk", "biztalk_checkmk_pulse.cmd"),
|
||||
Path.Combine(package, "biztalk_checkmk_pulse.cmd"));
|
||||
Copy(
|
||||
Path.Combine(root, "deployment", "INSTALLATION.md"),
|
||||
Path.Combine(package, "INSTALLATION.md"));
|
||||
|
||||
if (File.Exists(zip)) File.Delete(zip);
|
||||
ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false);
|
||||
WriteBase64(zip, base64);
|
||||
File.WriteAllText(
|
||||
checksum,
|
||||
Sha256(zip) + " " + Path.GetFileName(zip) + Environment.NewLine,
|
||||
new UTF8Encoding(false));
|
||||
Console.WriteLine("Install package: " + package);
|
||||
Console.WriteLine("Install ZIP: " + zip);
|
||||
Console.WriteLine("Base64 TXT: " + base64);
|
||||
Console.WriteLine("SHA-256 TXT: " + checksum);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Packaging failed: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Copy(string source, string target)
|
||||
{
|
||||
if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source);
|
||||
File.Copy(source, target, true);
|
||||
}
|
||||
|
||||
private static void WriteBase64(string source, string target)
|
||||
{
|
||||
var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
|
||||
var builder = new StringBuilder(encoded.Length + encoded.Length / 64 * 2 + 2);
|
||||
for (var offset = 0; offset < encoded.Length; offset += 64)
|
||||
{
|
||||
builder.Append(encoded, offset, Math.Min(64, encoded.Length - offset));
|
||||
builder.Append('\n');
|
||||
}
|
||||
File.WriteAllText(target, builder.ToString(), new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
private static string Sha256(string path)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
using (var algorithm = SHA256.Create())
|
||||
{
|
||||
var hash = algorithm.ComputeHash(stream);
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
foreach (var value in hash) builder.Append(value.ToString("x2"));
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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>{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>BizTalkCheckmkPulse.Setup</RootNamespace>
|
||||
<AssemblyName>BizTalkCheckmkPulse.Setup</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</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="Microsoft.CSharp" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="InstallerEngine.cs" />
|
||||
<Compile Include="MainForm.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="SetupOperationLog.cs" />
|
||||
<Compile Include="TaskSchedulerService.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,731 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
namespace BizTalkCheckmkPulse.Setup
|
||||
{
|
||||
internal sealed class InstallerEngine
|
||||
{
|
||||
internal const string TaskName = "BizTalk Checkmk Pulse Provider";
|
||||
private static readonly TimeSpan PostInstallValidationTimeout = TimeSpan.FromMinutes(4);
|
||||
private readonly string packageDirectory;
|
||||
private readonly string installDirectory;
|
||||
private readonly string runtimeDirectory;
|
||||
private readonly string checkmkLocalDirectory;
|
||||
|
||||
public InstallerEngine(string packageDirectory)
|
||||
{
|
||||
this.packageDirectory = Path.GetFullPath(packageDirectory);
|
||||
installDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkCheckmkPulse");
|
||||
runtimeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkCheckmkPulse");
|
||||
checkmkLocalDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "checkmk", "agent", "local");
|
||||
}
|
||||
|
||||
public void Install(
|
||||
string account,
|
||||
string password,
|
||||
bool isGmsa,
|
||||
string environmentName,
|
||||
bool allowServiceNameChange,
|
||||
Action<string> report)
|
||||
{
|
||||
var operationStartedUtc = DateTime.UtcNow;
|
||||
var uiReport = report ?? delegate { };
|
||||
var setupLog = SetupOperationLog.Create(runtimeDirectory, "install-or-update");
|
||||
report = delegate(string message)
|
||||
{
|
||||
setupLog.Write("INFO", message);
|
||||
uiReport(message);
|
||||
};
|
||||
if (setupLog.FilePath.Length > 0)
|
||||
report("Diagnoselog: " + setupLog.FilePath);
|
||||
else
|
||||
uiReport("WARNUNG: Diagnoselog konnte nicht angelegt werden: " + setupLog.CreationError);
|
||||
|
||||
var phase = "Initialisierung";
|
||||
var sourceApplication = Path.Combine(packageDirectory, "application");
|
||||
var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe");
|
||||
var sourceConfig = sourceExe + ".config";
|
||||
var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd");
|
||||
var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe");
|
||||
var targetConfig = targetExe + ".config";
|
||||
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
||||
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
||||
var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
|
||||
byte[] previousWrapper = null;
|
||||
var hadExistingInstallation = false;
|
||||
var hadExistingExecutable = false;
|
||||
var mutationStarted = false;
|
||||
var backupCreated = false;
|
||||
var filesActivated = false;
|
||||
var scheduler = new TaskSchedulerService();
|
||||
|
||||
try
|
||||
{
|
||||
phase = "Eingaben und Paket pruefen";
|
||||
report("Phase: " + phase + ". Paket=" + packageDirectory + ".");
|
||||
Validate(account, password, isGmsa, environmentName);
|
||||
var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier));
|
||||
report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")");
|
||||
RequireFile(sourceExe);
|
||||
RequireFile(sourceConfig);
|
||||
RequireFile(sourceWrapper);
|
||||
previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null;
|
||||
hadExistingInstallation = Directory.Exists(installDirectory);
|
||||
hadExistingExecutable = File.Exists(targetExe);
|
||||
|
||||
RunSelfTest(sourceExe, 9, "Paket", setupLog);
|
||||
report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services.");
|
||||
if (!isGmsa)
|
||||
{
|
||||
phase = "Collector-Anmeldung pruefen";
|
||||
ValidateBatchLogon(account, password);
|
||||
report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt.");
|
||||
}
|
||||
|
||||
phase = "Update-Staging vorbereiten";
|
||||
report("Phase: " + phase + ".");
|
||||
Directory.CreateDirectory(stagingDirectory);
|
||||
var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe");
|
||||
var stagedConfig = stagedExe + ".config";
|
||||
File.Copy(sourceExe, stagedExe, false);
|
||||
var effectiveEnvironment = PrepareConfig(
|
||||
sourceConfig,
|
||||
stagedConfig,
|
||||
File.Exists(targetConfig) ? targetConfig : null,
|
||||
environmentName);
|
||||
var stagedServiceNames = RunSelfTest(stagedExe, 9, "Staging", setupLog);
|
||||
report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + ".");
|
||||
|
||||
if (File.Exists(targetExe))
|
||||
{
|
||||
phase = "Installierten Servicevertrag vergleichen";
|
||||
report("Phase: " + phase + ". Legacy-Versionen mit weniger stabilen Services werden als Updatequelle akzeptiert.");
|
||||
var installedServiceNames = RunSelfTest(targetExe, null, "Installierte Version", setupLog);
|
||||
var serviceNameChange = EnsureServiceNameCompatibility(
|
||||
installedServiceNames,
|
||||
stagedServiceNames,
|
||||
allowServiceNameChange);
|
||||
report(serviceNameChange.Length == 0
|
||||
? "Checkmk-Servicevertrag unveraendert: keine Service Discovery erforderlich."
|
||||
: "Checkmk-Servicevertrag geaendert; Service Discovery erforderlich. " + serviceNameChange);
|
||||
}
|
||||
|
||||
// Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten.
|
||||
phase = "Vorhandenen Scheduled Task anhalten";
|
||||
report("Phase: " + phase + ".");
|
||||
mutationStarted = true;
|
||||
scheduler.DeleteIfExists(TaskName);
|
||||
report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt.");
|
||||
|
||||
if (hadExistingInstallation)
|
||||
{
|
||||
phase = "Vorhandene Programmversion sichern";
|
||||
Directory.Move(installDirectory, backupDirectory);
|
||||
backupCreated = true;
|
||||
}
|
||||
|
||||
phase = "Neue Programmversion aktivieren";
|
||||
Directory.Move(stagingDirectory, installDirectory);
|
||||
filesActivated = true;
|
||||
report(hadExistingInstallation
|
||||
? "Programmdateien atomar auf die neue Version umgestellt."
|
||||
: "Programmdateien installiert: " + installDirectory);
|
||||
|
||||
phase = "Runtime-Verzeichnisse und ACLs einrichten";
|
||||
var dataDirectory = Path.Combine(runtimeDirectory, "data");
|
||||
var logDirectory = Path.Combine(runtimeDirectory, "logs");
|
||||
Directory.CreateDirectory(runtimeDirectory);
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
|
||||
ApplyDirectoryAcl(installDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
|
||||
ApplyDirectoryAcl(runtimeDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
|
||||
ApplyDirectoryAcl(dataDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.ReadAndExecute);
|
||||
ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify);
|
||||
report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten.");
|
||||
|
||||
phase = "Checkmk Local Check installieren";
|
||||
Directory.CreateDirectory(checkmkLocalDirectory);
|
||||
File.Copy(sourceWrapper, targetWrapper, true);
|
||||
report("Checkmk Local Check installiert: " + checkmkLocalDirectory);
|
||||
|
||||
phase = "Installierte Programmversion pruefen";
|
||||
RunSelfTest(targetExe, 9, "Aktivierte Version", setupLog);
|
||||
report("Installierter Self-Test erfolgreich: neun Checkmk-Services.");
|
||||
|
||||
phase = "Provider-Abnahmelauf registrieren und starten";
|
||||
var runtimeValidationStartedUtc = DateTime.UtcNow;
|
||||
scheduler.RegisterValidationAndStart(
|
||||
TaskName,
|
||||
targetExe,
|
||||
installDirectory,
|
||||
account,
|
||||
isGmsa ? null : password,
|
||||
isGmsa);
|
||||
report("Einmaliger Provider-Abnahmelauf mit erzwungenem Endpoint-Katalogabgleich gestartet.");
|
||||
|
||||
phase = "Provider-Abnahmelauf abwarten";
|
||||
var completedRun = scheduler.WaitForSuccessfulRun(
|
||||
TaskName,
|
||||
runtimeValidationStartedUtc,
|
||||
PostInstallValidationTimeout,
|
||||
report);
|
||||
report("Provider-Abnahmelauf erfolgreich: LastTaskResult=" + completedRun.ExitCode + ".");
|
||||
|
||||
phase = "Runtime-Artefakte validieren";
|
||||
var runtimeValidation = RunRuntimeValidation(
|
||||
targetExe,
|
||||
runtimeValidationStartedUtc,
|
||||
account);
|
||||
report("Post-Install-Runtime-Abnahme erfolgreich: " + runtimeValidation);
|
||||
|
||||
phase = "Minuetlichen Scheduled Task registrieren";
|
||||
scheduler.RegisterRecurring(
|
||||
TaskName,
|
||||
targetExe,
|
||||
installDirectory,
|
||||
account,
|
||||
isGmsa ? null : password,
|
||||
isGmsa);
|
||||
report("Validierter minuetlicher Scheduled Task registriert: " + TaskName);
|
||||
|
||||
TryDeleteDirectory(backupDirectory, report);
|
||||
report("Installation/Update erfolgreich abgeschlossen.");
|
||||
}
|
||||
catch (Exception installException)
|
||||
{
|
||||
setupLog.WriteException(phase, installException);
|
||||
if (phase.StartsWith("Provider-", StringComparison.Ordinal)
|
||||
|| phase.StartsWith("Runtime-", StringComparison.Ordinal))
|
||||
{
|
||||
WriteRecentProviderLog(setupLog, runtimeDirectory, operationStartedUtc);
|
||||
}
|
||||
var rollbackFailures = new List<string>();
|
||||
if (mutationStarted)
|
||||
{
|
||||
try
|
||||
{
|
||||
scheduler.DeleteIfExists(TaskName);
|
||||
report("Rollback: Scheduled Task gestoppt und entfernt.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rollbackFailures.Add("Task stoppen: " + ex.Message);
|
||||
setupLog.WriteException("Rollback: Task stoppen", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesActivated || backupCreated)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
|
||||
if (backupCreated && Directory.Exists(backupDirectory)) Directory.Move(backupDirectory, installDirectory);
|
||||
report("Vorherige Programmversion wiederhergestellt.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message);
|
||||
setupLog.WriteException("Rollback: Programmdateien wiederherstellen", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (mutationStarted)
|
||||
{
|
||||
try
|
||||
{
|
||||
RestoreWrapper(targetWrapper, previousWrapper);
|
||||
report("Rollback: Checkmk-Wrapper auf den vorherigen Stand zurueckgesetzt.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
|
||||
setupLog.WriteException("Rollback: Checkmk-Wrapper wiederherstellen", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (mutationStarted && hadExistingExecutable && File.Exists(targetExe))
|
||||
{
|
||||
try
|
||||
{
|
||||
var rollbackRunStartedUtc = DateTime.UtcNow;
|
||||
scheduler.RegisterRecurringAndStart(
|
||||
TaskName,
|
||||
targetExe,
|
||||
installDirectory,
|
||||
account,
|
||||
isGmsa ? null : password,
|
||||
isGmsa);
|
||||
scheduler.WaitForSuccessfulRun(
|
||||
TaskName,
|
||||
rollbackRunStartedUtc,
|
||||
PostInstallValidationTimeout,
|
||||
report);
|
||||
report("Scheduled Task und frischer Snapshot der vorherigen Version wiederhergestellt.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message);
|
||||
setupLog.WriteException("Rollback: Scheduled Task wiederherstellen", ex);
|
||||
}
|
||||
}
|
||||
|
||||
var rollback = !mutationStarted
|
||||
? "Keine Systemaenderung vorgenommen; kein Rollback erforderlich."
|
||||
: rollbackFailures.Count == 0
|
||||
? "Rollback erfolgreich."
|
||||
: "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures);
|
||||
setupLog.Write("ERROR", rollback);
|
||||
var logReference = setupLog.FilePath.Length == 0
|
||||
? "Diagnoselog nicht verfuegbar (" + setupLog.CreationError + ")."
|
||||
: "Diagnoselog: " + setupLog.FilePath;
|
||||
throw new InvalidOperationException(
|
||||
"Installation/Update fehlgeschlagen in Phase '" + phase + "'. "
|
||||
+ rollback + " Ursache: " + installException.Message + " " + logReference,
|
||||
installException);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDeleteDirectory(stagingDirectory, null);
|
||||
setupLog.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Uninstall(bool keepRuntimeData, Action<string> report)
|
||||
{
|
||||
new TaskSchedulerService().DeleteIfExists(TaskName);
|
||||
report("Scheduled Task entfernt.");
|
||||
|
||||
var wrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
|
||||
if (File.Exists(wrapper)) File.Delete(wrapper);
|
||||
if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
|
||||
if (!keepRuntimeData && Directory.Exists(runtimeDirectory)) Directory.Delete(runtimeDirectory, true);
|
||||
}
|
||||
|
||||
private static void Validate(string account, string password, bool isGmsa, string environmentName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(account)) throw new ArgumentException("Collector-Konto fehlt.");
|
||||
if (account.IndexOf('\\') <= 0 || account.EndsWith("\\", StringComparison.Ordinal))
|
||||
throw new ArgumentException(@"Collector-Konto im Format DOMAIN\Benutzer eingeben, z.B. BEW\t231bizmon.");
|
||||
if (isGmsa && !account.EndsWith("$", StringComparison.Ordinal))
|
||||
throw new ArgumentException(@"Ein gMSA-Konto muss mit '$' enden, z.B. BEW\svc_biztalk_cmk$.");
|
||||
if (!isGmsa && string.IsNullOrEmpty(password)) throw new ArgumentException("Kennwort fehlt.");
|
||||
if (!new[] { "", "ACC", "DEV", "TST", "PRD" }.Contains(environmentName))
|
||||
throw new ArgumentException("Ungueltige Umgebung.");
|
||||
}
|
||||
|
||||
private static void RequireFile(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException("Installationspaket ist unvollstaendig. Datei fehlt: " + path, path);
|
||||
}
|
||||
|
||||
internal string GetInstalledEnvironment()
|
||||
{
|
||||
var config = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe.config");
|
||||
if (!File.Exists(config)) return string.Empty;
|
||||
try
|
||||
{
|
||||
var document = LoadXml(config);
|
||||
var setting = FindAppSetting(document, "EnvironmentName");
|
||||
return setting == null ? string.Empty : setting.GetAttribute("value").Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsInstalled
|
||||
{
|
||||
get { return File.Exists(Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe")); }
|
||||
}
|
||||
|
||||
internal static string PrepareConfig(
|
||||
string sourceConfig,
|
||||
string stagedConfig,
|
||||
string existingConfig,
|
||||
string requestedEnvironment)
|
||||
{
|
||||
var document = LoadXml(sourceConfig);
|
||||
if (!string.IsNullOrWhiteSpace(existingConfig) && File.Exists(existingConfig))
|
||||
{
|
||||
var existing = LoadXml(existingConfig);
|
||||
var existingSettings = existing.SelectNodes("/configuration/appSettings/add[@key]");
|
||||
if (existingSettings != null)
|
||||
{
|
||||
foreach (XmlNode node in existingSettings)
|
||||
{
|
||||
var element = node as XmlElement;
|
||||
if (element == null) continue;
|
||||
var key = element.GetAttribute("key");
|
||||
var value = element.GetAttribute("value");
|
||||
if (IsSupersededDefault(key, value)) continue;
|
||||
var target = FindAppSetting(document, key);
|
||||
if (target != null) target.SetAttribute("value", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var environmentSetting = FindAppSetting(document, "EnvironmentName");
|
||||
if (environmentSetting == null)
|
||||
throw new InvalidDataException("EnvironmentName fehlt in " + sourceConfig + ".");
|
||||
if (!string.IsNullOrWhiteSpace(requestedEnvironment))
|
||||
environmentSetting.SetAttribute("value", requestedEnvironment.Trim());
|
||||
|
||||
document.Save(stagedConfig);
|
||||
return environmentSetting.GetAttribute("value").Trim();
|
||||
}
|
||||
|
||||
private static XmlDocument LoadXml(string path)
|
||||
{
|
||||
var document = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
|
||||
using (var reader = XmlReader.Create(path, new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit,
|
||||
XmlResolver = null
|
||||
}))
|
||||
{
|
||||
document.Load(reader);
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlElement FindAppSetting(XmlDocument document, string key)
|
||||
{
|
||||
var nodes = document.SelectNodes("/configuration/appSettings/add[@key]");
|
||||
if (nodes == null) return null;
|
||||
foreach (XmlNode node in nodes)
|
||||
{
|
||||
var element = node as XmlElement;
|
||||
if (element != null && string.Equals(element.GetAttribute("key"), key, StringComparison.Ordinal))
|
||||
return element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsSupersededDefault(string key, string value)
|
||||
{
|
||||
return string.Equals(key, "EndpointProbeMaxConcurrency", StringComparison.Ordinal)
|
||||
&& string.Equals(value, "12", StringComparison.Ordinal)
|
||||
|| string.Equals(key, "EndpointMaxCount", StringComparison.Ordinal)
|
||||
&& string.Equals(value, "500", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void ValidateBatchLogon(string account, string password)
|
||||
{
|
||||
var separator = account.IndexOf('\\');
|
||||
var domain = account.Substring(0, separator);
|
||||
var user = account.Substring(separator + 1);
|
||||
IntPtr token;
|
||||
if (!LogonUser(user, domain, password, 4, 0, out token))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collector-Anmeldung als Batch fehlgeschlagen: "
|
||||
+ new Win32Exception(Marshal.GetLastWin32Error()).Message
|
||||
+ ". Kennwort und lokales Recht 'Log on as a batch job' pruefen.");
|
||||
}
|
||||
|
||||
CloseHandle(token);
|
||||
}
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool LogonUser(
|
||||
string userName,
|
||||
string domain,
|
||||
string password,
|
||||
int logonType,
|
||||
int logonProvider,
|
||||
out IntPtr token);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(IntPtr handle);
|
||||
|
||||
private static void RestoreWrapper(string path, byte[] previousContent)
|
||||
{
|
||||
if (previousContent == null)
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
File.WriteAllBytes(path, previousContent);
|
||||
}
|
||||
|
||||
private static void TryDeleteDirectory(string path, Action<string> report)
|
||||
{
|
||||
if (!Directory.Exists(path)) return;
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (report != null) report("Hinweis: temporaeres Verzeichnis konnte nicht entfernt werden: " + path + " (" + ex.Message + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyDirectoryAcl(
|
||||
string path,
|
||||
SecurityIdentifier collectorSid,
|
||||
FileSystemRights collectorRights,
|
||||
FileSystemRights systemRights)
|
||||
{
|
||||
var inheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
|
||||
var security = new DirectorySecurity();
|
||||
security.SetAccessRuleProtection(true, false);
|
||||
security.AddAccessRule(new FileSystemAccessRule(
|
||||
new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
|
||||
FileSystemRights.FullControl,
|
||||
inheritance,
|
||||
PropagationFlags.None,
|
||||
AccessControlType.Allow));
|
||||
security.AddAccessRule(new FileSystemAccessRule(
|
||||
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
|
||||
systemRights,
|
||||
inheritance,
|
||||
PropagationFlags.None,
|
||||
AccessControlType.Allow));
|
||||
security.AddAccessRule(new FileSystemAccessRule(
|
||||
collectorSid,
|
||||
collectorRights,
|
||||
inheritance,
|
||||
PropagationFlags.None,
|
||||
AccessControlType.Allow));
|
||||
new DirectoryInfo(path).SetAccessControl(security);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> RunSelfTest(
|
||||
string executable,
|
||||
int? expectedServiceCount,
|
||||
string label,
|
||||
SetupOperationLog setupLog)
|
||||
{
|
||||
var version = FileVersionInfo.GetVersionInfo(executable).FileVersion ?? "(unbekannt)";
|
||||
setupLog.Write(
|
||||
"INFO",
|
||||
"Self-Test startet. label=" + label
|
||||
+ ", path=" + executable
|
||||
+ ", file_version=" + version
|
||||
+ ", size_bytes=" + new FileInfo(executable).Length
|
||||
+ ", expected_lines=" + (expectedServiceCount.HasValue ? expectedServiceCount.Value.ToString(CultureInfo.InvariantCulture) : "legacy-compatible") + ".");
|
||||
var start = new ProcessStartInfo(executable, "--self-test")
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
using (var process = Process.Start(start))
|
||||
{
|
||||
if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden.");
|
||||
var outputRead = process.StandardOutput.ReadToEndAsync();
|
||||
var errorRead = process.StandardError.ReadToEndAsync();
|
||||
if (!process.WaitForExit(30000))
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||
var timeoutOutput = outputRead.Result;
|
||||
var timeoutError = errorRead.Result;
|
||||
setupLog.Write("ERROR", "Self-Test Timeout. label=" + label + ", stdout=" + timeoutOutput + ", stderr=" + timeoutError + ".");
|
||||
throw new InvalidOperationException("Self-Test '" + label + "' hat das Zeitlimit von 30 Sekunden ueberschritten.");
|
||||
}
|
||||
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||
var output = outputRead.Result;
|
||||
var error = errorRead.Result;
|
||||
setupLog.Write(
|
||||
"INFO",
|
||||
"Self-Test beendet. label=" + label
|
||||
+ ", exit_code=" + process.ExitCode
|
||||
+ ", stdout=" + output
|
||||
+ ", stderr=" + error + ".");
|
||||
return ParseSelfTestOutput(output, error, process.ExitCode, expectedServiceCount, label);
|
||||
}
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<string> ParseSelfTestOutput(
|
||||
string output,
|
||||
string error,
|
||||
int exitCode,
|
||||
int? expectedServiceCount,
|
||||
string label)
|
||||
{
|
||||
var lines = (output ?? string.Empty).Split(
|
||||
new[] { "\r\n", "\n" },
|
||||
StringSplitOptions.RemoveEmptyEntries);
|
||||
var expected = expectedServiceCount.HasValue
|
||||
? expectedServiceCount.Value.ToString(CultureInfo.InvariantCulture)
|
||||
: "mindestens 1 (Legacy-kompatibel)";
|
||||
if (exitCode != 0
|
||||
|| lines.Length == 0
|
||||
|| expectedServiceCount.HasValue && lines.Length != expectedServiceCount.Value
|
||||
|| lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Self-Test '" + label + "' fehlgeschlagen. Exitcode=" + exitCode
|
||||
+ ", Zeilen=" + lines.Length + ", Erwartet=" + expected
|
||||
+ ", Stdout=" + CompactProcessText(output)
|
||||
+ ", Stderr=" + CompactProcessText(error) + ".");
|
||||
}
|
||||
|
||||
var serviceNames = lines.Select(ExtractServiceName).ToArray();
|
||||
if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length)
|
||||
throw new InvalidOperationException(
|
||||
"Self-Test '" + label + "' fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig. "
|
||||
+ "Services=[" + string.Join(", ", serviceNames) + "].");
|
||||
return serviceNames;
|
||||
}
|
||||
|
||||
private static string CompactProcessText(string value)
|
||||
{
|
||||
var text = (value ?? string.Empty)
|
||||
.Replace("\r", string.Empty)
|
||||
.Replace("\n", " | ")
|
||||
.Trim();
|
||||
const int maxLength = 2000;
|
||||
return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...[gekuerzt]";
|
||||
}
|
||||
|
||||
private static string RunRuntimeValidation(
|
||||
string executable,
|
||||
DateTime notBeforeUtc,
|
||||
string expectedIdentity)
|
||||
{
|
||||
var identityBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(expectedIdentity));
|
||||
var arguments = "--validate-runtime --validation-not-before-utc "
|
||||
+ notBeforeUtc.ToUniversalTime().ToString("o")
|
||||
+ " --expected-identity-base64 " + identityBase64;
|
||||
var start = new ProcessStartInfo(executable, arguments)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
using (var process = Process.Start(start))
|
||||
{
|
||||
if (process == null) throw new InvalidOperationException("Runtime-Abnahme konnte nicht gestartet werden.");
|
||||
var outputRead = process.StandardOutput.ReadToEndAsync();
|
||||
var errorRead = process.StandardError.ReadToEndAsync();
|
||||
if (!process.WaitForExit(30000))
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||
throw new TimeoutException(
|
||||
"Runtime-Abnahme hat das Zeitlimit von 30 Sekunden ueberschritten. Stdout="
|
||||
+ CompactProcessText(outputRead.Result)
|
||||
+ ", Stderr=" + CompactProcessText(errorRead.Result) + ".");
|
||||
}
|
||||
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||
var output = outputRead.Result.Trim();
|
||||
var error = errorRead.Result.Trim();
|
||||
|
||||
if (process.ExitCode != 0
|
||||
|| !output.StartsWith("RUNTIME_VALIDATION_V1 ", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime-Abnahme fehlgeschlagen. Exitcode=" + process.ExitCode
|
||||
+ ". " + (error.Length == 0 ? output : error));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractServiceName(string line)
|
||||
{
|
||||
var firstQuote = line.IndexOf('"');
|
||||
var secondQuote = firstQuote < 0 ? -1 : line.IndexOf('"', firstQuote + 1);
|
||||
if (firstQuote < 0 || secondQuote <= firstQuote + 1)
|
||||
throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicename kann nicht gelesen werden: " + line);
|
||||
return line.Substring(firstQuote + 1, secondQuote - firstQuote - 1);
|
||||
}
|
||||
|
||||
internal static string EnsureServiceNameCompatibility(
|
||||
IEnumerable<string> installedServiceNames,
|
||||
IEnumerable<string> stagedServiceNames,
|
||||
bool allowServiceNameChange)
|
||||
{
|
||||
var installed = new HashSet<string>(installedServiceNames ?? Enumerable.Empty<string>(), StringComparer.Ordinal);
|
||||
var staged = new HashSet<string>(stagedServiceNames ?? Enumerable.Empty<string>(), StringComparer.Ordinal);
|
||||
if (installed.SetEquals(staged)) return string.Empty;
|
||||
|
||||
var removed = installed.Except(staged, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||
var added = staged.Except(installed, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||
var description = "Entfernt=[" + string.Join(", ", removed) + "]; Neu=[" + string.Join(", ", added) + "].";
|
||||
// Reine Erweiterungen lassen bestehende Checkmk-Services unangetastet und
|
||||
// muessen Legacy-Updates deshalb nicht blockieren. Entfernte Namen sind
|
||||
// dagegen ein Rename oder ein Vertragsbruch und bleiben bestaetigungspflichtig.
|
||||
if (removed.Length > 0 && !allowServiceNameChange)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Sicherheitsstopp: Das Update wuerde Checkmk-Servicenamen aendern. "
|
||||
+ description
|
||||
+ " Ohne Service Discovery entstehen verwaiste bzw. fehlende Services. "
|
||||
+ "Nur wenn die Aenderung beabsichtigt ist, im Setup 'Service Discovery ist eingeplant' bestaetigen und anschliessend die Discovery ausfuehren.");
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
private static void WriteRecentProviderLog(
|
||||
SetupOperationLog setupLog,
|
||||
string runtimeRoot,
|
||||
DateTime operationStartedUtc)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.Combine(runtimeRoot, "logs");
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
setupLog.Write("INFO", "Provider-Logverzeichnis existiert nicht: " + directory + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
var file = Directory.GetFiles(directory, "biztalk-checkmk-pulse-*.log")
|
||||
.OrderByDescending(File.GetLastWriteTimeUtc)
|
||||
.FirstOrDefault();
|
||||
if (file == null)
|
||||
{
|
||||
setupLog.Write("INFO", "Kein Provider-Log fuer die Fehlerdiagnose gefunden.");
|
||||
return;
|
||||
}
|
||||
|
||||
var lastWriteUtc = File.GetLastWriteTimeUtc(file);
|
||||
if (lastWriteUtc < operationStartedUtc.AddSeconds(-5))
|
||||
{
|
||||
setupLog.Write(
|
||||
"WARN",
|
||||
"Neuestes Provider-Log ist aelter als dieser Setup-Lauf und wird nicht als Fehlerkontext uebernommen. "
|
||||
+ "path=" + file + ", last_write_utc=" + lastWriteUtc.ToString("o") + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = File.ReadAllLines(file);
|
||||
var tail = lines.Skip(Math.Max(0, lines.Length - 40));
|
||||
setupLog.Write(
|
||||
"ERROR",
|
||||
"Letzte Provider-Logzeilen. path=" + file + ", tail=" + string.Join(" || ", tail) + ".");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
setupLog.Write("WARN", "Provider-Log konnte nicht fuer die Diagnose gelesen werden: " + ex.Message + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkCheckmkPulse.Setup
|
||||
{
|
||||
internal sealed class MainForm : Form
|
||||
{
|
||||
private readonly InstallerEngine engine;
|
||||
private readonly TextBox account = new TextBox();
|
||||
private readonly TextBox password = new TextBox();
|
||||
private readonly ComboBox environment = new ComboBox();
|
||||
private readonly CheckBox gmsa = new CheckBox();
|
||||
private readonly CheckBox allowServiceNameChange = new CheckBox();
|
||||
private readonly Button install = new Button();
|
||||
private readonly Button uninstall = new Button();
|
||||
private readonly TextBox status = new TextBox();
|
||||
|
||||
public MainForm(InstallerEngine engine)
|
||||
{
|
||||
this.engine = engine;
|
||||
Text = "BizTalk Checkmk Pulse Setup";
|
||||
ClientSize = new Size(650, 535);
|
||||
MinimumSize = new Size(666, 574);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Font = new Font("Segoe UI", 9F);
|
||||
|
||||
var title = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Font = new Font(Font.FontFamily, 15F, FontStyle.Bold),
|
||||
Location = new Point(20, 18),
|
||||
Text = "BizTalk Checkmk Pulse installieren"
|
||||
};
|
||||
var description = new Label
|
||||
{
|
||||
AutoSize = false,
|
||||
Location = new Point(22, 58),
|
||||
Size = new Size(605, 48),
|
||||
Text = "Installiert Anwendung, Checkmk Local Check, sichere Verzeichnisrechte und den minuetlichen Scheduled Task. PowerShell wird nicht verwendet."
|
||||
};
|
||||
|
||||
AddLabel("Collector-Konto", 22, 120);
|
||||
account.Location = new Point(190, 116);
|
||||
account.Size = new Size(420, 23);
|
||||
account.Text = @"BEW\t231bizmon";
|
||||
|
||||
AddLabel("Kennwort", 22, 158);
|
||||
password.Location = new Point(190, 154);
|
||||
password.Size = new Size(420, 23);
|
||||
password.UseSystemPasswordChar = true;
|
||||
|
||||
AddLabel("Umgebung", 22, 196);
|
||||
environment.Location = new Point(190, 192);
|
||||
environment.Size = new Size(160, 23);
|
||||
environment.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
environment.Items.AddRange(new object[] { "(keine)", "ACC", "DEV", "TST", "PRD" });
|
||||
var installedEnvironment = engine.GetInstalledEnvironment();
|
||||
var installedIndex = environment.Items.IndexOf(installedEnvironment);
|
||||
environment.SelectedIndex = installedIndex >= 0 ? installedIndex : 0;
|
||||
|
||||
gmsa.Location = new Point(190, 229);
|
||||
gmsa.Size = new Size(420, 24);
|
||||
gmsa.Text = "Konto ist ein gMSA (kein Kennwort erforderlich)";
|
||||
gmsa.CheckedChanged += delegate
|
||||
{
|
||||
password.Enabled = !gmsa.Checked;
|
||||
if (gmsa.Checked) password.Clear();
|
||||
};
|
||||
|
||||
allowServiceNameChange.Location = new Point(190, 262);
|
||||
allowServiceNameChange.Size = new Size(420, 34);
|
||||
allowServiceNameChange.Text = "Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant";
|
||||
allowServiceNameChange.Enabled = engine.IsInstalled;
|
||||
|
||||
install.Location = new Point(190, 306);
|
||||
install.Size = new Size(200, 34);
|
||||
install.Text = "Installieren / aktualisieren";
|
||||
install.Click += async delegate { await InstallAsync(); };
|
||||
|
||||
uninstall.Location = new Point(410, 306);
|
||||
uninstall.Size = new Size(200, 34);
|
||||
uninstall.Text = "Deinstallieren";
|
||||
uninstall.Click += async delegate { await UninstallAsync(); };
|
||||
|
||||
status.Location = new Point(22, 359);
|
||||
status.Size = new Size(588, 146);
|
||||
status.Multiline = true;
|
||||
status.ReadOnly = true;
|
||||
status.ScrollBars = ScrollBars.Vertical;
|
||||
status.Text = engine.IsInstalled
|
||||
? "Bestehende Installation erkannt. Update behaelt Runtime-Daten und vorhandene Konfigurationswerte bei; Konto und Kennwort werden fuer den Scheduled Task neu bestaetigt."
|
||||
: "Bereit. Setup.exe muss aus dem vollstaendig entpackten Installationspaket gestartet werden.";
|
||||
|
||||
Controls.AddRange(new Control[]
|
||||
{
|
||||
title, description, account, password, environment, gmsa, allowServiceNameChange, install, uninstall, status
|
||||
});
|
||||
AcceptButton = install;
|
||||
}
|
||||
|
||||
private void AddLabel(string text, int left, int top)
|
||||
{
|
||||
Controls.Add(new Label { AutoSize = true, Location = new Point(left, top), Text = text });
|
||||
}
|
||||
|
||||
private async Task InstallAsync()
|
||||
{
|
||||
var collectorAccount = account.Text.Trim();
|
||||
var collectorPassword = password.Text;
|
||||
var environmentName = environment.SelectedIndex <= 0 ? string.Empty : environment.SelectedItem.ToString();
|
||||
var collectorIsGmsa = gmsa.Checked;
|
||||
|
||||
SetBusy(true, "Installation laeuft ...");
|
||||
try
|
||||
{
|
||||
await Task.Run(() => engine.Install(
|
||||
collectorAccount,
|
||||
collectorPassword,
|
||||
collectorIsGmsa,
|
||||
environmentName,
|
||||
allowServiceNameChange.Checked,
|
||||
Report));
|
||||
password.Clear();
|
||||
Report("Installation abgeschlossen. Der Provider-Task wurde einmalig gestartet.");
|
||||
MessageBox.Show(this, "Installation erfolgreich abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
password.Clear();
|
||||
Report("FEHLER: " + ex.Message);
|
||||
MessageBox.Show(this, ex.Message, "Installation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UninstallAsync()
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
this,
|
||||
"Scheduled Task, Checkmk-Wrapper und Programmdateien entfernen? Snapshot und Logs bleiben erhalten.",
|
||||
"Deinstallation bestaetigen",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning);
|
||||
if (result != DialogResult.Yes) return;
|
||||
|
||||
SetBusy(true, "Deinstallation laeuft ...");
|
||||
try
|
||||
{
|
||||
await Task.Run(() => engine.Uninstall(true, Report));
|
||||
Report("Deinstallation abgeschlossen. Runtime-Daten wurden beibehalten.");
|
||||
MessageBox.Show(this, "Deinstallation erfolgreich abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Report("FEHLER: " + ex.Message);
|
||||
MessageBox.Show(this, ex.Message, "Deinstallation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBusy(bool busy, string message)
|
||||
{
|
||||
install.Enabled = !busy;
|
||||
uninstall.Enabled = !busy;
|
||||
account.Enabled = !busy;
|
||||
environment.Enabled = !busy;
|
||||
gmsa.Enabled = !busy;
|
||||
allowServiceNameChange.Enabled = !busy && engine.IsInstalled;
|
||||
password.Enabled = !busy && !gmsa.Checked;
|
||||
UseWaitCursor = busy;
|
||||
if (message != null) Report(message);
|
||||
}
|
||||
|
||||
private void Report(string message)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
BeginInvoke(new Action<string>(Report), message);
|
||||
return;
|
||||
}
|
||||
|
||||
status.AppendText(Environment.NewLine + DateTime.Now.ToString("HH:mm:ss") + " " + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkCheckmkPulse.Setup
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")]
|
||||
[assembly: AssemblyTitle("BizTalk Checkmk Pulse Setup")]
|
||||
[assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
||||
[assembly: AssemblyVersion("2.2.7.0")]
|
||||
[assembly: AssemblyFileVersion("2.2.7.0")]
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkCheckmkPulse.Setup
|
||||
{
|
||||
/// <summary>
|
||||
/// Best-effort-Diagnoselog fuer einen einzelnen Setup-Lauf. Das Kennwort
|
||||
/// wird dieser Klasse nie uebergeben und kann deshalb nicht protokolliert werden.
|
||||
/// </summary>
|
||||
internal sealed class SetupOperationLog : IDisposable
|
||||
{
|
||||
private readonly object sync = new object();
|
||||
private StreamWriter writer;
|
||||
|
||||
private SetupOperationLog(string filePath, StreamWriter writer)
|
||||
{
|
||||
FilePath = filePath;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
public string FilePath { get; private set; }
|
||||
|
||||
public static SetupOperationLog Create(string runtimeDirectory, string operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.Combine(runtimeDirectory, "logs");
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(
|
||||
directory,
|
||||
"setup-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmssfff", CultureInfo.InvariantCulture)
|
||||
+ "-" + Process.GetCurrentProcess().Id + ".log");
|
||||
var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
|
||||
var log = new SetupOperationLog(path, new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true });
|
||||
log.Write("INFO", "Setup-Log gestartet. operation=" + operation
|
||||
+ ", setup_version=" + typeof(SetupOperationLog).Assembly.GetName().Version
|
||||
+ ", os=" + Environment.OSVersion.VersionString
|
||||
+ ", process_bitness=" + (Environment.Is64BitProcess ? "64" : "32")
|
||||
+ ", identity=" + CurrentIdentity() + ".");
|
||||
return log;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new SetupOperationLog(string.Empty, null)
|
||||
{
|
||||
CreationError = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string CreationError { get; private set; }
|
||||
|
||||
public void Write(string level, string message)
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
if (writer == null) return;
|
||||
writer.WriteLine(
|
||||
DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
|
||||
+ " level=" + level
|
||||
+ " " + SingleLine(message));
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteException(string phase, Exception exception)
|
||||
{
|
||||
Write("ERROR", "Fehler in Phase '" + phase + "'. " + FormatException(exception));
|
||||
}
|
||||
|
||||
internal static string FormatException(Exception exception)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
var current = exception;
|
||||
var depth = 0;
|
||||
while (current != null && depth < 10)
|
||||
{
|
||||
if (depth > 0) result.Append(" | inner[").Append(depth).Append("] ");
|
||||
result.Append(current.GetType().FullName)
|
||||
.Append(" hresult=0x")
|
||||
.Append(current.HResult.ToString("X8", CultureInfo.InvariantCulture))
|
||||
.Append(": ")
|
||||
.Append(current.Message);
|
||||
if (!string.IsNullOrWhiteSpace(current.StackTrace))
|
||||
result.Append(" stack=").Append(current.StackTrace);
|
||||
current = current.InnerException;
|
||||
depth++;
|
||||
}
|
||||
return SingleLine(result.ToString());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
if (writer == null) return;
|
||||
writer.Dispose();
|
||||
writer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CurrentIdentity()
|
||||
{
|
||||
try
|
||||
{
|
||||
var identity = WindowsIdentity.GetCurrent();
|
||||
return identity == null ? "(unknown)" : identity.Name;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return "(unknown)";
|
||||
}
|
||||
}
|
||||
|
||||
private static string SingleLine(string value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\t", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace BizTalkCheckmkPulse.Setup
|
||||
{
|
||||
internal sealed class TaskSchedulerService
|
||||
{
|
||||
private const int TaskCreateOrUpdate = 6;
|
||||
private const int TaskLogonPassword = 1;
|
||||
private const int TaskTriggerTime = 1;
|
||||
private const int TaskActionExecute = 0;
|
||||
private const int TaskInstancesIgnoreNew = 2;
|
||||
private const int TaskStateReady = 3;
|
||||
private const int TaskStateRunning = 4;
|
||||
|
||||
public void RegisterValidationAndStart(
|
||||
string taskName,
|
||||
string executable,
|
||||
string workingDirectory,
|
||||
string account,
|
||||
string password,
|
||||
bool isGmsa)
|
||||
{
|
||||
Register(
|
||||
taskName,
|
||||
executable,
|
||||
workingDirectory,
|
||||
account,
|
||||
password,
|
||||
isGmsa,
|
||||
"--collect --force-endpoint-refresh",
|
||||
false,
|
||||
true);
|
||||
}
|
||||
|
||||
public void RegisterRecurring(
|
||||
string taskName,
|
||||
string executable,
|
||||
string workingDirectory,
|
||||
string account,
|
||||
string password,
|
||||
bool isGmsa)
|
||||
{
|
||||
Register(taskName, executable, workingDirectory, account, password, isGmsa, "--collect", true, false);
|
||||
}
|
||||
|
||||
public void RegisterRecurringAndStart(
|
||||
string taskName,
|
||||
string executable,
|
||||
string workingDirectory,
|
||||
string account,
|
||||
string password,
|
||||
bool isGmsa)
|
||||
{
|
||||
Register(taskName, executable, workingDirectory, account, password, isGmsa, "--collect", true, true);
|
||||
}
|
||||
|
||||
private static void Register(
|
||||
string taskName,
|
||||
string executable,
|
||||
string workingDirectory,
|
||||
string account,
|
||||
string password,
|
||||
bool isGmsa,
|
||||
string arguments,
|
||||
bool enableRecurringTrigger,
|
||||
bool startImmediately)
|
||||
{
|
||||
dynamic service = null;
|
||||
dynamic root = null;
|
||||
dynamic definition = null;
|
||||
dynamic registeredTask = null;
|
||||
dynamic runningTask = null;
|
||||
try
|
||||
{
|
||||
var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true);
|
||||
service = Activator.CreateInstance(schedulerType);
|
||||
service.Connect();
|
||||
root = service.GetFolder("\\");
|
||||
definition = service.NewTask(0);
|
||||
|
||||
definition.RegistrationInfo.Description = "Collects BizTalk monitoring data for the Checkmk local check.";
|
||||
definition.Principal.UserId = account;
|
||||
// Auch gMSA werden als Password-Logon registriert. Nur das Kennwort
|
||||
// bleibt leer; TASK_LOGON_SERVICE_ACCOUNT ist ausschliesslich fuer
|
||||
// LocalSystem, LocalService und NetworkService vorgesehen.
|
||||
definition.Principal.LogonType = TaskLogonPassword;
|
||||
definition.Principal.RunLevel = 0;
|
||||
|
||||
definition.Settings.Enabled = true;
|
||||
definition.Settings.StartWhenAvailable = true;
|
||||
definition.Settings.MultipleInstances = TaskInstancesIgnoreNew;
|
||||
definition.Settings.ExecutionTimeLimit = "PT5M";
|
||||
definition.Settings.RestartCount = 2;
|
||||
definition.Settings.RestartInterval = "PT1M";
|
||||
definition.Settings.DisallowStartIfOnBatteries = false;
|
||||
definition.Settings.StopIfGoingOnBatteries = false;
|
||||
|
||||
dynamic trigger = definition.Triggers.Create(TaskTriggerTime);
|
||||
trigger.StartBoundary = DateTime.Now.AddMinutes(1).ToString("s");
|
||||
trigger.Enabled = enableRecurringTrigger;
|
||||
trigger.Repetition.Interval = "PT1M";
|
||||
|
||||
dynamic action = definition.Actions.Create(TaskActionExecute);
|
||||
action.Path = executable;
|
||||
action.Arguments = arguments;
|
||||
action.WorkingDirectory = workingDirectory;
|
||||
|
||||
registeredTask = root.RegisterTaskDefinition(
|
||||
taskName,
|
||||
definition,
|
||||
TaskCreateOrUpdate,
|
||||
account,
|
||||
isGmsa ? null : password,
|
||||
TaskLogonPassword,
|
||||
null);
|
||||
if (startImmediately)
|
||||
{
|
||||
runningTask = registeredTask.Run(null);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(runningTask);
|
||||
Release(registeredTask);
|
||||
Release(definition);
|
||||
Release(root);
|
||||
Release(service);
|
||||
}
|
||||
}
|
||||
|
||||
public TaskRunResult WaitForSuccessfulRun(
|
||||
string taskName,
|
||||
DateTime notBeforeUtc,
|
||||
TimeSpan timeout,
|
||||
Action<string> report)
|
||||
{
|
||||
dynamic service = null;
|
||||
dynamic root = null;
|
||||
dynamic task = null;
|
||||
var startedUtc = DateTime.UtcNow;
|
||||
var nextReportUtc = startedUtc.AddSeconds(15);
|
||||
var lastState = -1;
|
||||
var lastResult = int.MinValue;
|
||||
DateTime? lastRunUtc = null;
|
||||
try
|
||||
{
|
||||
var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true);
|
||||
service = Activator.CreateInstance(schedulerType);
|
||||
service.Connect();
|
||||
root = service.GetFolder("\\");
|
||||
task = root.GetTask(taskName);
|
||||
|
||||
while (DateTime.UtcNow - startedUtc < timeout)
|
||||
{
|
||||
lastState = (int)task.State;
|
||||
lastResult = (int)task.LastTaskResult;
|
||||
var lastRun = (DateTime)task.LastRunTime;
|
||||
lastRunUtc = lastRun.Year <= 1900
|
||||
? (DateTime?)null
|
||||
: DateTime.SpecifyKind(lastRun, DateTimeKind.Local).ToUniversalTime();
|
||||
|
||||
if (IsSuccessfulCompletion(lastState, lastResult, lastRunUtc, notBeforeUtc))
|
||||
{
|
||||
return new TaskRunResult(lastRunUtc.Value, lastResult);
|
||||
}
|
||||
|
||||
if (IsFreshCompletedFailure(lastState, lastResult, lastRunUtc, notBeforeUtc))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Providerlauf ist fehlgeschlagen. state=" + DescribeState(lastState)
|
||||
+ ", last_result=" + DescribeResult(lastResult)
|
||||
+ ", last_run=" + lastRunUtc.Value.ToString("o")
|
||||
+ ". Provider- und Setup-Log pruefen.");
|
||||
}
|
||||
|
||||
if (report != null && DateTime.UtcNow >= nextReportUtc)
|
||||
{
|
||||
report(
|
||||
"Warte auf erfolgreichen Providerlauf: state=" + DescribeState(lastState)
|
||||
+ ", last_result=" + DescribeResult(lastResult)
|
||||
+ ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + ".");
|
||||
nextReportUtc = DateTime.UtcNow.AddSeconds(15);
|
||||
}
|
||||
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
throw new TimeoutException(
|
||||
"Providerlauf wurde innerhalb von " + Math.Ceiling(timeout.TotalSeconds)
|
||||
+ " Sekunden nicht erfolgreich abgeschlossen. state=" + DescribeState(lastState)
|
||||
+ ", last_result=" + DescribeResult(lastResult)
|
||||
+ ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + ".");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(task);
|
||||
Release(root);
|
||||
Release(service);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsSuccessfulCompletion(
|
||||
int taskState,
|
||||
int lastTaskResult,
|
||||
DateTime? lastRunUtc,
|
||||
DateTime notBeforeUtc)
|
||||
{
|
||||
return taskState == TaskStateReady
|
||||
&& lastTaskResult == 0
|
||||
&& lastRunUtc.HasValue
|
||||
// Task Scheduler kann LastRunTime mit geringerer Aufloesung als DateTime.UtcNow liefern.
|
||||
&& lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2);
|
||||
}
|
||||
|
||||
internal static bool IsFreshCompletedFailure(
|
||||
int taskState,
|
||||
int lastTaskResult,
|
||||
DateTime? lastRunUtc,
|
||||
DateTime notBeforeUtc)
|
||||
{
|
||||
return taskState == TaskStateReady
|
||||
&& lastTaskResult != 0
|
||||
&& lastRunUtc.HasValue
|
||||
&& lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2);
|
||||
}
|
||||
|
||||
internal static string DescribeResult(int result)
|
||||
{
|
||||
var unsigned = unchecked((uint)result);
|
||||
string description;
|
||||
switch (unsigned)
|
||||
{
|
||||
case 0: description = "Erfolgreich"; break;
|
||||
case 1: description = "Provider-Laufzeitfehler; Provider-Log pruefen"; break;
|
||||
case 2: description = "Provider bereits aktiv oder Snapshot-I/O fehlgeschlagen"; break;
|
||||
case 0x00041300: description = "Task ist bereit"; break;
|
||||
case 0x00041301: description = "Task wird ausgefuehrt"; break;
|
||||
case 0x00041302: description = "Task ist deaktiviert"; break;
|
||||
case 0x00041303: description = "Task wurde noch nicht ausgefuehrt"; break;
|
||||
case 0x00041306: description = "Letzter Tasklauf wurde beendet"; break;
|
||||
default:
|
||||
if ((unsigned & 0xFFFF0000u) == 0x80070000u)
|
||||
{
|
||||
try
|
||||
{
|
||||
description = new Win32Exception((int)(unsigned & 0xFFFFu)).Message;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
description = "Windows-/Task-Scheduler-Fehler";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
description = "Anwendungs- oder Task-Scheduler-Code";
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result.ToString(CultureInfo.InvariantCulture)
|
||||
+ " (0x" + unsigned.ToString("X8", CultureInfo.InvariantCulture) + ")"
|
||||
+ " '" + description + "'";
|
||||
}
|
||||
|
||||
private static string DescribeState(int state)
|
||||
{
|
||||
string name;
|
||||
switch (state)
|
||||
{
|
||||
case 0: name = "Unknown"; break;
|
||||
case 1: name = "Disabled"; break;
|
||||
case 2: name = "Queued"; break;
|
||||
case 3: name = "Ready"; break;
|
||||
case 4: name = "Running"; break;
|
||||
default: name = "Unbekannt"; break;
|
||||
}
|
||||
return state.ToString(CultureInfo.InvariantCulture) + " (" + name + ")";
|
||||
}
|
||||
|
||||
public void DeleteIfExists(string taskName)
|
||||
{
|
||||
dynamic service = null;
|
||||
dynamic root = null;
|
||||
dynamic task = null;
|
||||
try
|
||||
{
|
||||
var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true);
|
||||
service = Activator.CreateInstance(schedulerType);
|
||||
service.Connect();
|
||||
root = service.GetFolder("\\");
|
||||
try
|
||||
{
|
||||
task = root.GetTask(taskName);
|
||||
task.Enabled = false;
|
||||
try
|
||||
{
|
||||
task.Stop(0);
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// Ein nicht laufender Task muss vor dem Loeschen nicht gestoppt werden.
|
||||
}
|
||||
for (var attempt = 0; attempt < 100 && (int)task.State == TaskStateRunning; attempt++)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
if ((int)task.State == TaskStateRunning)
|
||||
{
|
||||
throw new TimeoutException("Scheduled Task konnte innerhalb von 10 Sekunden nicht beendet werden.");
|
||||
}
|
||||
root.DeleteTask(taskName, 0);
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
const int FileNotFound = unchecked((int)0x80070002);
|
||||
if (ex.ErrorCode != FileNotFound) throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(task);
|
||||
Release(root);
|
||||
Release(service);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Release(object value)
|
||||
{
|
||||
if (value != null && Marshal.IsComObject(value)) Marshal.FinalReleaseComObject(value);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TaskRunResult
|
||||
{
|
||||
public TaskRunResult(DateTime lastRunUtc, int exitCode)
|
||||
{
|
||||
LastRunUtc = lastRunUtc;
|
||||
ExitCode = exitCode;
|
||||
}
|
||||
|
||||
public DateTime LastRunUtc { get; private set; }
|
||||
public int ExitCode { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="BizTalkCheckmkPulse.Setup" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -1,18 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<!-- Empty or "." means local BizTalk machine. -->
|
||||
<!-- Leer oder "." bedeutet die lokale BizTalk-Maschine. -->
|
||||
<add key="Server" value="." />
|
||||
<add key="ServicePrefix" value="BizTalk" />
|
||||
<add key="EnvironmentName" value="" />
|
||||
<!-- Stabiler Checkmk-Servicevertrag; nur mit geplanter Service Discovery aktivieren. -->
|
||||
<add key="IncludeEnvironmentInServiceName" value="false" />
|
||||
|
||||
<!-- 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" />
|
||||
|
||||
<!-- Automatisch gepflegter, geheimnisfreier Katalog aktiver Send-/Receive-Endpunkte. -->
|
||||
<add key="ProbeEndpointConnectivity" value="true" />
|
||||
<add key="EndpointCatalogPath" value="%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml" />
|
||||
<add key="EndpointCatalogMaxBytes" value="1048576" />
|
||||
<!-- Artefaktgrenze; das Netzwerkbudget wird separat ueber eindeutige Ziele begrenzt. -->
|
||||
<add key="EndpointCatalogMaxEntries" value="1000" />
|
||||
<!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. -->
|
||||
<add key="EndpointDiscoveryIntervalHours" value="168" />
|
||||
<add key="EndpointProbeTimeoutMilliseconds" value="3000" />
|
||||
<add key="EndpointProbeMaxConcurrency" value="16" />
|
||||
<!-- Bei 3 s Timeout und 16 parallelen Probes maximal ca. 21 s fuer 100 eindeutige Ziele. -->
|
||||
<add key="EndpointMaxCount" value="100" />
|
||||
|
||||
<add key="QueryTimeoutSeconds" value="25" />
|
||||
<!-- 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" />
|
||||
|
||||
<!-- In many BizTalk landscapes stopped artifacts can be intentional. -->
|
||||
<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" />
|
||||
@@ -25,4 +58,3 @@
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||
</startup>
|
||||
</configuration>
|
||||
|
||||
|
||||
@@ -36,14 +36,25 @@
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="CheckmkLocalFormatter.cs" />
|
||||
<Compile Include="EventLogProbe.cs" />
|
||||
<Compile Include="EndpointAddressParser.cs" />
|
||||
<Compile Include="EndpointCatalogStore.cs" />
|
||||
<Compile Include="EndpointConnectivityProbe.cs" />
|
||||
<Compile Include="FileLogger.cs" />
|
||||
<Compile Include="MonitoringOptions.cs" />
|
||||
<Compile Include="Models.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="RuntimeValidator.cs" />
|
||||
<Compile Include="SqlConnectivityProbe.cs" />
|
||||
<Compile Include="SnapshotStore.cs" />
|
||||
<Compile Include="WmiBizTalkProbe.cs" />
|
||||
<Compile Include="WmiHelpers.cs" />
|
||||
</ItemGroup>
|
||||
@@ -52,4 +63,3 @@
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ using System.Text;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Formatiert Probe-Ergebnisse als robuste Checkmk-Local-Check-Zeilen.
|
||||
/// </summary>
|
||||
internal sealed class CheckmkLocalFormatter
|
||||
{
|
||||
private const int HostStopped = 1;
|
||||
@@ -16,22 +19,38 @@ 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>
|
||||
/// Initialisiert den Formatter mit Service-Namen und Schwellwerten.
|
||||
/// </summary>
|
||||
/// <param name="options">Monitoring-Konfiguration.</param>
|
||||
public CheckmkLocalFormatter(MonitoringOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert alle stabilen und optionalen Checkmk-Services.
|
||||
/// </summary>
|
||||
/// <param name="result">Vollstaendiges Ergebnis der Probes.</param>
|
||||
/// <returns>Gueltige Checkmk-Local-Check-Zeilen.</returns>
|
||||
public IEnumerable<string> Format(ProbeResult result)
|
||||
{
|
||||
yield return FormatPlatform(result);
|
||||
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 FormatEndpointConnectivity(result);
|
||||
yield return FormatOrchestrations(result);
|
||||
yield return FormatEventLog(result);
|
||||
|
||||
if (_options.EmitPerApplicationSuspensionServices)
|
||||
if (_options.EmitPerApplicationSuspensionServices && result.Platform.SuspendedInstancesDataAvailable)
|
||||
{
|
||||
foreach (var line in FormatApplicationSuspensions(result))
|
||||
{
|
||||
@@ -40,58 +59,180 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erzeugt minimale Beispielzeilen ohne WMI-, SQL- oder Event-Log-Zugriff.
|
||||
/// </summary>
|
||||
/// <returns>Checkmk-Zeilen zur reinen Format- und Deployment-Pruefung.</returns>
|
||||
public IEnumerable<string> FormatSelfTest()
|
||||
{
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("Platform"), "-", "Self test OK. Checkmk local check output is valid.");
|
||||
yield return BuildLine(CheckState.Ok, _options.ServiceName("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("Endpoint Reachability"), "biztalk_endpoints_active=0;;;0", "Self test OK. No network connection was opened.");
|
||||
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>
|
||||
/// Erzeugt fuer einen nicht abgefangenen Programmfehler UNKNOWN-Zeilen.
|
||||
/// </summary>
|
||||
/// <param name="message">Bereits aufbereitete technische Fehlermeldung.</param>
|
||||
/// <returns>UNKNOWN-Zeilen fuer alle stabilen Services.</returns>
|
||||
public IEnumerable<string> FormatFatal(string message)
|
||||
{
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Platform"), "-", message);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Suspended Instances"), "-", message);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", message);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Runtime Artifacts"), "-", message);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", message);
|
||||
var action = message + " Massnahme: Programmdatei, Konfiguration und Windows Application Log pruefen; anschliessend cmk-agent-ctl.exe dump erneut ausfuehren.";
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Platform"), "-", action);
|
||||
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("Receive Locations"), "-", action);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Send Ports"), "-", action);
|
||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Endpoint Reachability"), "-", 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("Endpoint Reachability"), "-", 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>
|
||||
/// <param name="result">Probe-Ergebnis.</param>
|
||||
/// <returns>Checkmk-Zeile fuer BizTalk Platform.</returns>
|
||||
private string FormatPlatform(ProbeResult result)
|
||||
{
|
||||
var state = result.Platform.WmiConnected ? CheckState.Ok : CheckState.Unknown;
|
||||
var available = result.Platform.WmiConnected && result.Platform.PlatformDataAvailable;
|
||||
var state = available ? CheckState.Ok : CheckState.Unknown;
|
||||
var detail = new StringBuilder();
|
||||
detail.Append(result.Platform.WmiConnected ? "BizTalk WMI reachable" : "BizTalk WMI not reachable");
|
||||
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));
|
||||
|
||||
return BuildLine(state, _options.ServiceName("Platform"), "-", AppendDiagnostics(detail.ToString(), result.Diagnostics));
|
||||
var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting");
|
||||
return BuildLine(state, _options.ServiceName("Platform"), "-", AppendDiagnostics(detail.ToString(), diagnostics));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert die integrierten Anmeldetests an den BizTalk-Datenbanken.
|
||||
/// </summary>
|
||||
/// <param name="result">Probe-Ergebnis mit SQL-Zielen und Identitaeten.</param>
|
||||
/// <returns>Checkmk-Zeile fuer BizTalk SQL Access.</returns>
|
||||
private string FormatSqlAccess(ProbeResult result)
|
||||
{
|
||||
if (!_options.ProbeSqlConnectivity)
|
||||
{
|
||||
return BuildLine(CheckState.Ok, _options.ServiceName("SQL Access"), "-", "SQL connectivity probe disabled by configuration.");
|
||||
}
|
||||
|
||||
var total = result.SqlTargets.Count;
|
||||
var available = result.SqlTargets.Count(x => x.Attempted && x.Available);
|
||||
var discoveryComplete = result.Platform.PlatformDataAvailable;
|
||||
var state = discoveryComplete && total > 0 && available == total ? CheckState.Ok : CheckState.Unknown;
|
||||
var metrics = total == 0
|
||||
? "-"
|
||||
: string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"biztalk_sql_targets_total={0};;;0|biztalk_sql_targets_available={1};;;0|biztalk_sql_targets_failed={2};;1;0",
|
||||
total,
|
||||
available,
|
||||
total - available);
|
||||
|
||||
var detail = new StringBuilder();
|
||||
detail.Append("SQL access targets=").Append(total)
|
||||
.Append(", available=").Append(available)
|
||||
.Append(", discovery_complete=").Append(discoveryComplete)
|
||||
.Append(", execution_identity=").Append(EmptyAsUnknown(result.ExecutionIdentity))
|
||||
.Append(", network_identity=").Append(EmptyAsUnknown(result.NetworkIdentityHint));
|
||||
AppendList(detail, "databases", result.SqlTargets.Select(x =>
|
||||
x.Role + "=" + EmptyAsUnknown(x.Server) + "\\" + EmptyAsUnknown(x.Database) + ":" + (x.Available ? "OK" : "FAILED")));
|
||||
|
||||
return BuildLine(
|
||||
state,
|
||||
_options.ServiceName("SQL Access"),
|
||||
metrics,
|
||||
AppendDiagnostics(detail.ToString(), SelectDiagnostics(result, DiagnosticArea.Sql)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert Anzahl und Kritikalitaet suspendierter Service Instances.
|
||||
/// </summary>
|
||||
/// <param name="result">Probe-Ergebnis.</param>
|
||||
/// <returns>Checkmk-Zeile fuer suspendierte Instanzen.</returns>
|
||||
private string FormatSuspendedInstances(ProbeResult result)
|
||||
{
|
||||
if (!result.Platform.SuspendedInstancesDataAvailable)
|
||||
{
|
||||
var errorDetail = "Suspendierte Instanzen konnten nicht verlaesslich gelesen werden. Ein leerer Datenbestand wird nicht als OK gewertet.";
|
||||
return BuildLine(
|
||||
CheckState.Unknown,
|
||||
_options.ServiceName("Suspended Instances"),
|
||||
"-",
|
||||
AppendDiagnostics(errorDetail, SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_ServiceInstance")));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert den Laufzeitzustand aller BizTalk Host Instances.
|
||||
/// </summary>
|
||||
/// <param name="result">Probe-Ergebnis.</param>
|
||||
/// <returns>Checkmk-Zeile fuer Host Instances.</returns>
|
||||
private string FormatHostInstances(ProbeResult result)
|
||||
{
|
||||
if (!result.Platform.HostInstancesDataAvailable)
|
||||
{
|
||||
var errorDetail = "BizTalk Host Instances konnten nicht verlaesslich gelesen werden.";
|
||||
return BuildLine(
|
||||
CheckState.Unknown,
|
||||
_options.ServiceName("Host Instances"),
|
||||
"-",
|
||||
AppendDiagnostics(errorDetail, SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_HostInstance")));
|
||||
}
|
||||
|
||||
var total = result.HostInstances.Count;
|
||||
var started = result.HostInstances.Count(x => x.ServiceState == HostStarted);
|
||||
var stopped = result.HostInstances.Count(x => x.ServiceState == HostStopped || x.ServiceState == HostPaused);
|
||||
@@ -119,57 +260,260 @@ namespace BizTalkCheckmkPulse
|
||||
.Take(_options.MaxSummaryItems)
|
||||
.Select(x => EmptyAsUnknown(x.HostName) + "@" + EmptyAsUnknown(x.RunningServer) + "=" + HostStateName(x.ServiceState));
|
||||
AppendList(detail, "affected", affected);
|
||||
|
||||
return BuildLine(state, _options.ServiceName("Host Instances"), metrics, detail.ToString());
|
||||
}
|
||||
|
||||
private string FormatRuntimeArtifacts(ProbeResult result)
|
||||
private string FormatReceiveLocations(ProbeResult result)
|
||||
{
|
||||
var receiveLocations = result.Applications.Sum(x => x.ReceiveLocationTotal);
|
||||
var receiveLocationsDisabled = result.Applications.Sum(x => x.ReceiveLocationDisabled);
|
||||
var sendPorts = result.Applications.Sum(x => x.SendPortTotal);
|
||||
var sendPortsStarted = result.Applications.Sum(x => x.SendPortStarted);
|
||||
var sendPortsInactive = result.Applications.Sum(x => x.SendPortStopped + x.SendPortBound);
|
||||
var sendPortsUnknown = result.Applications.Sum(x => x.SendPortUnknown);
|
||||
var orchestrations = result.Applications.Sum(x => x.OrchestrationTotal);
|
||||
var orchestrationsStarted = result.Applications.Sum(x => x.OrchestrationStarted);
|
||||
var orchestrationsInactive = result.Applications.Sum(x => x.OrchestrationStopped + x.OrchestrationBound + x.OrchestrationUnbound);
|
||||
var orchestrationsUnknown = result.Applications.Sum(x => x.OrchestrationUnknown);
|
||||
var state = sendPortsUnknown > 0 || orchestrationsUnknown > 0
|
||||
if (!result.Platform.ReceiveLocationsDataAvailable)
|
||||
{
|
||||
return BuildLine(
|
||||
CheckState.Unknown,
|
||||
_options.ServiceName("Receive Locations"),
|
||||
"-",
|
||||
AppendDiagnostics(
|
||||
"Receive Locations konnten nicht verlaesslich gelesen werden.",
|
||||
SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_ReceiveLocation")));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert die Netzwerk-Erreichbarkeit aller aktuell aktiven, pruefbaren BizTalk-Endpunkte.
|
||||
/// </summary>
|
||||
/// <param name="result">Vollstaendiges Provider-Ergebnis mit Endpoint-Zustand.</param>
|
||||
/// <returns>Eine syntaktisch gueltige Checkmk-Local-Check-Zeile.</returns>
|
||||
private string FormatEndpointConnectivity(ProbeResult result)
|
||||
{
|
||||
var endpointState = result.EndpointConnectivity;
|
||||
if (endpointState.Disabled)
|
||||
{
|
||||
return BuildLine(
|
||||
CheckState.Ok,
|
||||
_options.ServiceName("Endpoint Reachability"),
|
||||
"-",
|
||||
"Endpoint connectivity probe disabled by configuration.");
|
||||
}
|
||||
|
||||
if (!endpointState.RuntimeStateAvailable || !endpointState.CatalogAvailable)
|
||||
{
|
||||
return BuildLine(
|
||||
CheckState.Unknown,
|
||||
_options.ServiceName("Endpoint Reachability"),
|
||||
"-",
|
||||
"Endpoint-Pruefung nicht verlaesslich moeglich. " + EmptyAsUnknown(endpointState.Failure));
|
||||
}
|
||||
|
||||
var failed = endpointState.Results
|
||||
.Where(x => !x.Available && !x.Endpoint.BestEffort)
|
||||
.ToArray();
|
||||
var ignoredBestEffort = endpointState.Results
|
||||
.Where(x => !x.Available && x.Endpoint.BestEffort)
|
||||
.ToArray();
|
||||
var available = endpointState.Results.Count(x => x.Available);
|
||||
var incomplete = endpointState.Results.Count != endpointState.Active
|
||||
|| endpointState.UnresolvedActive > 0
|
||||
|| endpointState.RefreshRequired && !endpointState.RefreshSucceeded
|
||||
|| !string.IsNullOrWhiteSpace(endpointState.Failure);
|
||||
var state = failed.Length > 0
|
||||
? CheckState.Critical
|
||||
: incomplete
|
||||
? CheckState.Unknown
|
||||
: CheckState.Ok;
|
||||
var metrics = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unresolved={6};;1;0|biztalk_endpoints_expected_non_socket={7};;;0|biztalk_endpoints_best_effort={8};;;0|biztalk_endpoints_best_effort_ignored={9};;;0|biztalk_endpoints_manual_overrides={10};;;0|biztalk_endpoints_inactive_skipped={11};;;0|biztalk_endpoint_probe_duration_ms={12};;;0",
|
||||
endpointState.Configured,
|
||||
endpointState.Active,
|
||||
endpointState.UniqueTargets,
|
||||
endpointState.Results.Count,
|
||||
available,
|
||||
failed.Length,
|
||||
endpointState.UnresolvedActive,
|
||||
endpointState.ExpectedNonProbeableActive,
|
||||
endpointState.BestEffortActive,
|
||||
ignoredBestEffort.Length,
|
||||
endpointState.ManualOverridesActive,
|
||||
endpointState.SkippedInactive,
|
||||
endpointState.ProbeDurationMilliseconds);
|
||||
var detail = new StringBuilder();
|
||||
if (failed.Length == 0 && !incomplete)
|
||||
{
|
||||
detail.Append("Alle regulaer alarmierenden aktiven Send-/Receive-Endpunkte sind erreichbar")
|
||||
.Append(" (active=").Append(endpointState.Active)
|
||||
.Append(", available=").Append(available)
|
||||
.Append(", unique_targets=").Append(endpointState.UniqueTargets)
|
||||
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
|
||||
.Append(", best_effort=").Append(endpointState.BestEffortActive)
|
||||
.Append(", best_effort_ignored=").Append(ignoredBestEffort.Length)
|
||||
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
|
||||
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")");
|
||||
}
|
||||
else
|
||||
{
|
||||
detail.Append("Endpoint reachability active=").Append(endpointState.Active)
|
||||
.Append(", tested=").Append(endpointState.Results.Count)
|
||||
.Append(", available=").Append(available)
|
||||
.Append(", failed=").Append(failed.Length)
|
||||
.Append(", unresolved=").Append(endpointState.UnresolvedActive)
|
||||
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
|
||||
.Append(", best_effort=").Append(endpointState.BestEffortActive)
|
||||
.Append(", best_effort_ignored=").Append(ignoredBestEffort.Length)
|
||||
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
|
||||
.Append(", unique_targets=").Append(endpointState.UniqueTargets)
|
||||
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds);
|
||||
AppendLimitedList(detail, "unavailable", failed.Select(x =>
|
||||
EndpointConnectivityProbe.Display(x.Endpoint) + "(" + EmptyAsUnknown(x.Failure) + ")"));
|
||||
if (!string.IsNullOrWhiteSpace(endpointState.Failure))
|
||||
{
|
||||
detail.Append("; catalog_or_probe_error=").Append(endpointState.Failure);
|
||||
}
|
||||
|
||||
AppendLimitedList(detail, "unresolved_endpoints", endpointState.ResolutionIssues);
|
||||
}
|
||||
|
||||
if (endpointState.CatalogSynchronizedUtc.HasValue)
|
||||
{
|
||||
detail.Append("; catalog_utc=")
|
||||
.Append(endpointState.CatalogSynchronizedUtc.Value.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return BuildLine(state, _options.ServiceName("Endpoint Reachability"), 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>
|
||||
/// Formatiert relevante Fehler und Warnungen aus dem Application Event Log.
|
||||
/// </summary>
|
||||
/// <param name="result">Probe-Ergebnis.</param>
|
||||
/// <returns>Checkmk-Zeile fuer das BizTalk Event Log.</returns>
|
||||
private string FormatEventLog(ProbeResult result)
|
||||
{
|
||||
if (!_options.ProbeEventLog)
|
||||
@@ -179,7 +523,11 @@ namespace BizTalkCheckmkPulse
|
||||
|
||||
if (!result.EventLog.Available)
|
||||
{
|
||||
return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", "Application event log could not be read: " + result.EventLog.Failure);
|
||||
return BuildLine(
|
||||
CheckState.Unknown,
|
||||
_options.ServiceName("Event Log"),
|
||||
"-",
|
||||
"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
|
||||
@@ -200,32 +548,58 @@ namespace BizTalkCheckmkPulse
|
||||
_options.EventLogLookbackMinutes,
|
||||
result.EventLog.Errors,
|
||||
result.EventLog.Warnings);
|
||||
|
||||
return BuildLine(state, _options.ServiceName("Event Log"), metrics, detail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erzeugt bei aktivierter Option einen Suspensions-Service je Anwendung.
|
||||
/// </summary>
|
||||
/// <param name="result">Probe-Ergebnis mit suspendierten Instanzen.</param>
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
private CheckState DetermineSuspensionState(int resumable, int nonresumable)
|
||||
/// <summary>
|
||||
/// Bestimmt den Checkmk-Zustand aus den beiden Suspensionszaehlern.
|
||||
/// </summary>
|
||||
/// <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, int routingFailureReports)
|
||||
{
|
||||
if (_options.CritRoutingFailureThreshold > 0
|
||||
&& routingFailureReports >= _options.CritRoutingFailureThreshold)
|
||||
{
|
||||
return CheckState.Critical;
|
||||
}
|
||||
|
||||
if (_options.CritNonResumableThreshold > 0 && nonresumable >= _options.CritNonResumableThreshold)
|
||||
{
|
||||
return CheckState.Critical;
|
||||
@@ -239,47 +613,105 @@ namespace BizTalkCheckmkPulse
|
||||
return CheckState.Ok;
|
||||
}
|
||||
|
||||
private string BuildSuspensionDetail(IEnumerable<SuspendedInstance> instances, int total, int resumable, int nonresumable)
|
||||
/// <summary>
|
||||
/// Erstellt Details mit betroffenen Anwendungen und aktuellen Beispielen.
|
||||
/// </summary>
|
||||
/// <param name="instances">Suspendierte Instanzen.</param>
|
||||
/// <param name="total">Gesamtanzahl.</param>
|
||||
/// <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,
|
||||
int routingFailureReports)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(total).Append(" suspended BizTalk service instance(s): resumable=").Append(resumable)
|
||||
.Append(", nonresumable=").Append(nonresumable);
|
||||
builder.Append("Suspended total=").Append(total)
|
||||
.Append(", resumable=").Append(resumable)
|
||||
.Append(", nonresumable=").Append(nonresumable)
|
||||
.Append(", routing_failure_reports=").Append(routingFailureReports);
|
||||
|
||||
var apps = instances
|
||||
.Where(x => !x.IsRoutingFailureReport
|
||||
&& !string.Equals(x.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase))
|
||||
.GroupBy(x => x.ApplicationName)
|
||||
.OrderByDescending(x => x.Count())
|
||||
.Take(_options.MaxSummaryItems)
|
||||
.Select(x => x.Key + "(R=" + x.Count(y => y.Kind == SuspendedKind.Resumable) + ", NR=" + x.Count(y => y.Kind == SuspendedKind.NonResumable) + ")");
|
||||
AppendList(builder, "applications", apps);
|
||||
.Select(x => x.Key + "(R=" + x.Count(y => y.Kind == SuspendedKind.Resumable) + ",NR=" + x.Count(y => y.Kind == SuspendedKind.NonResumable) + ")");
|
||||
AppendLimitedList(builder, "applications", apps);
|
||||
|
||||
var examples = instances
|
||||
.Where(x => !x.IsRoutingFailureReport)
|
||||
.OrderByDescending(x => x.SuspendTime ?? DateTime.MinValue)
|
||||
.Take(Math.Min(_options.MaxSummaryItems, 5))
|
||||
.Select(x => EmptyAsUnknown(x.ServiceName) + " on " + EmptyAsUnknown(x.HostName));
|
||||
AppendList(builder, "examples", examples);
|
||||
.Select(x => EmptyAsUnknown(x.ServiceName) + "@" + EmptyAsUnknown(x.HostName));
|
||||
AppendLimitedList(builder, "recent", examples);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string BuildLine(CheckState state, string serviceName, string metrics, string detail)
|
||||
/// <summary>
|
||||
/// Baut eine syntaktisch gueltige Checkmk-Local-Check-Zeile.
|
||||
/// </summary>
|
||||
/// <param name="state">Numerischer Checkmk-Zustand.</param>
|
||||
/// <param name="serviceName">Stabiler Service-Name.</param>
|
||||
/// <param name="metrics">Performance-Daten oder Bindestrich.</param>
|
||||
/// <param name="detail">Menschenlesbare Service-Ausgabe.</param>
|
||||
/// <returns>Vollstaendige Local-Check-Zeile.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
private static string AppendDiagnostics(string detail, IEnumerable<string> diagnostics)
|
||||
/// <summary>
|
||||
/// Ergaenzt bis zu drei strukturierte Diagnosen am Detailtext.
|
||||
/// </summary>
|
||||
/// <param name="detail">Vorhandener Service-Detailtext.</param>
|
||||
/// <param name="diagnostics">Relevante Diagnosen.</param>
|
||||
/// <returns>Detailtext mit Ursache, Massnahme und technischen Details.</returns>
|
||||
private static string AppendDiagnostics(string detail, IEnumerable<ProbeDiagnostic> diagnostics)
|
||||
{
|
||||
var visible = diagnostics == null ? new string[0] : diagnostics.Take(3).ToArray();
|
||||
if (visible.Length == 0)
|
||||
var visible = diagnostics == null
|
||||
? new string[0]
|
||||
: diagnostics.Take(3).Select(x => x.ToDisplayText()).ToArray();
|
||||
return visible.Length == 0
|
||||
? detail
|
||||
: detail + "; diagnostics: " + string.Join("; ", visible);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filtert Diagnosen nach Bereich und optionalen Komponentenfragmenten.
|
||||
/// </summary>
|
||||
/// <param name="result">Probe-Ergebnis mit allen Diagnosen.</param>
|
||||
/// <param name="area">Gesuchter Diagnosebereich.</param>
|
||||
/// <param name="components">Optionale Fragmente betroffener Komponenten.</param>
|
||||
/// <returns>Passende Diagnosen in Entstehungsreihenfolge.</returns>
|
||||
private static IEnumerable<ProbeDiagnostic> SelectDiagnostics(ProbeResult result, DiagnosticArea area, params string[] components)
|
||||
{
|
||||
return detail;
|
||||
}
|
||||
|
||||
return detail + "; diagnostics: " + string.Join("; ", visible);
|
||||
return result.Diagnostics.Where(x =>
|
||||
x.Area == area
|
||||
&& (components == null
|
||||
|| components.Length == 0
|
||||
|| components.Any(component => (x.Component ?? string.Empty).IndexOf(component, StringComparison.OrdinalIgnoreCase) >= 0)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Haengt eine nicht leere Werteliste an einen Detailtext an.
|
||||
/// </summary>
|
||||
/// <param name="builder">Zu erweiternder Text.</param>
|
||||
/// <param name="label">Bezeichnung der Liste.</param>
|
||||
/// <param name="values">Zu formatierende Werte.</param>
|
||||
private static void AppendList(StringBuilder builder, string label, IEnumerable<string> values)
|
||||
{
|
||||
var list = values.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
|
||||
@@ -289,6 +721,63 @@ 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>
|
||||
/// <param name="builder">Zu erweiternder Text.</param>
|
||||
/// <param name="label">Bezeichnung des Wertes.</param>
|
||||
/// <param name="value">Optionaler Wert.</param>
|
||||
private static void AppendOptional(StringBuilder builder, string label, string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
@@ -297,6 +786,12 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verbindet SQL-Server und Datenbank zu einer kompakten Anzeige.
|
||||
/// </summary>
|
||||
/// <param name="server">SQL-Server oder Instanz.</param>
|
||||
/// <param name="database">Datenbankname.</param>
|
||||
/// <returns>Kombination aus Server und Datenbank oder leerer Text.</returns>
|
||||
private static string JoinDb(string server, string database)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(server) && string.IsNullOrWhiteSpace(database))
|
||||
@@ -307,6 +802,11 @@ namespace BizTalkCheckmkPulse
|
||||
return EmptyAsUnknown(server) + "\\" + EmptyAsUnknown(database);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uebersetzt den numerischen BizTalk Host ServiceState in einen Namen.
|
||||
/// </summary>
|
||||
/// <param name="state">Numerischer WMI-Zustand.</param>
|
||||
/// <returns>Menschenlesbarer Zustandsname.</returns>
|
||||
private static string HostStateName(int state)
|
||||
{
|
||||
switch (state)
|
||||
@@ -332,16 +832,31 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ersetzt leere Werte durch eine sichtbare Unknown-Markierung.
|
||||
/// </summary>
|
||||
/// <param name="value">Zu normalisierender Text.</param>
|
||||
/// <returns>Getrimmter Wert oder <c>unknown</c>.</returns>
|
||||
private static string EmptyAsUnknown(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "unknown" : value.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entfernt fuer Checkmk problematische Zeichen aus Service-Namen.
|
||||
/// </summary>
|
||||
/// <param name="value">Urspruenglicher Service-Name.</param>
|
||||
/// <returns>Sicherer Service-Name.</returns>
|
||||
private static string SanitizeService(string value)
|
||||
{
|
||||
return SanitizeDetail(value).Replace("\"", "'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalisiert Detailtext auf eine einzelne Checkmk-kompatible Zeile.
|
||||
/// </summary>
|
||||
/// <param name="value">Urspruenglicher Detailtext.</param>
|
||||
/// <returns>Einzeiliger, nicht leerer Detailtext.</returns>
|
||||
private static string SanitizeDetail(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
@@ -0,0 +1,795 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Reduziert adapter-spezifische BizTalk-Adressen auf einen geheimnisfreien Host/Port-Test.
|
||||
/// </summary>
|
||||
internal static class EndpointAddressParser
|
||||
{
|
||||
private static readonly Regex HostPortPattern = new Regex(
|
||||
@"^(?<host>\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):(?<port>\d{1,5})(?:[/\\].*)?$",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly Regex DatabaseProviderPattern = new Regex(
|
||||
@"/(?:Oracle|SQLServer)(?:[/;?]|$)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag.
|
||||
/// </summary>
|
||||
/// <param name="candidate">Aktiver BizTalk-Transport mit Adaptername und Adresse.</param>
|
||||
/// <param name="entry">Geheimnisfreier Katalogeintrag, sofern ein Ziel bestimmt wurde.</param>
|
||||
/// <param name="reason">Begruendung, wenn kein pruefbares Ziel bestimmt wurde.</param>
|
||||
/// <returns><see langword="true"/>, wenn Host, Port und Protokoll sicher bestimmt wurden.</returns>
|
||||
public static bool TryCreate(
|
||||
EndpointCandidate candidate,
|
||||
out EndpointCatalogEntry entry,
|
||||
out string reason)
|
||||
{
|
||||
var resolution = Analyze(candidate);
|
||||
entry = resolution.Entry;
|
||||
reason = resolution.Reason ?? string.Empty;
|
||||
return resolution.Status == EndpointResolutionStatus.Probeable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken.
|
||||
/// </summary>
|
||||
/// <param name="candidate">Zu klassifizierender BizTalk-Transport.</param>
|
||||
/// <returns>Klassifizierung sowie gegebenenfalls das pruefbare Socket-Ziel.</returns>
|
||||
public static EndpointResolution Analyze(EndpointCandidate candidate)
|
||||
{
|
||||
if (candidate == null)
|
||||
{
|
||||
return Unresolved("Endpoint-Kandidat fehlt.");
|
||||
}
|
||||
|
||||
if (candidate.Dynamic)
|
||||
{
|
||||
return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel.");
|
||||
}
|
||||
|
||||
// Scheduling-Adapter bilden zeitliche BizTalk-Steuerung ab und besitzen kein
|
||||
// fachlich relevantes Remoteziel. Sie werden bewusst vollstaendig ausgeklammert.
|
||||
if (IsScheduler(candidate))
|
||||
{
|
||||
return Expected("Scheduling-Komponente ist von der Endpoint-Pruefung ausgenommen.");
|
||||
}
|
||||
|
||||
var address = (candidate.Address ?? string.Empty).Trim();
|
||||
if (address.Length == 0)
|
||||
{
|
||||
return Unresolved("Aktive statische Transportadresse ist leer.");
|
||||
}
|
||||
|
||||
if (Contains(candidate.AdapterName, "SMTP"))
|
||||
{
|
||||
return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server.");
|
||||
}
|
||||
|
||||
// SFTP-/FTP-Adressen duerfen Benutzerinformationen enthalten (user@host).
|
||||
// Deshalb erst nach der adapterspezifischen Aufloesung als Empfaengerliste einstufen.
|
||||
if (LooksLikeEmailRecipients(address)
|
||||
&& !Contains(candidate.AdapterName, "SFTP")
|
||||
&& !Contains(candidate.AdapterName, "FTP"))
|
||||
{
|
||||
return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server.");
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|
||||
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
{
|
||||
return Expected("Lokaler Dateipfad benoetigt keine Netzwerkprobe.");
|
||||
}
|
||||
|
||||
if (address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|
||||
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Expected("Named Pipes besitzen kein TCP-/UDP-Ziel.");
|
||||
}
|
||||
|
||||
if (address.StartsWith("/", StringComparison.Ordinal)
|
||||
&& IsInboundListener(candidate))
|
||||
{
|
||||
return Expected("Relative Receive-Listener-Adresse enthaelt keine eindeutige Site-Bindung.");
|
||||
}
|
||||
|
||||
// DATABASE und WCF-SAP liefern Adapteradressen, deren Socket-Ziel nicht von
|
||||
// System.Uri abgeleitet werden kann. Eine fehlende sichere Ableitung ist bei
|
||||
// diesen Best-Effort-Adaptern kein Abdeckungsfehler.
|
||||
if (IsDatabaseAdapter(candidate))
|
||||
{
|
||||
EndpointCatalogEntry databaseEntry;
|
||||
string databaseReason;
|
||||
if (TryResolveDatabase(candidate, address, out databaseEntry, out databaseReason))
|
||||
{
|
||||
return Probeable(databaseEntry);
|
||||
}
|
||||
|
||||
return Expected(databaseReason);
|
||||
}
|
||||
|
||||
if (IsWcfSapAdapter(candidate))
|
||||
{
|
||||
EndpointCatalogEntry sapEntry;
|
||||
string sapReason;
|
||||
if (TryResolveWcfSap(candidate, address, out sapEntry, out sapReason))
|
||||
{
|
||||
return Probeable(sapEntry);
|
||||
}
|
||||
|
||||
return Expected(sapReason);
|
||||
}
|
||||
|
||||
EndpointCatalogEntry entry;
|
||||
string protocol;
|
||||
string host;
|
||||
int port;
|
||||
string reason;
|
||||
if (!TryResolve(
|
||||
address,
|
||||
candidate.AdapterName,
|
||||
IsInboundListener(candidate),
|
||||
out protocol,
|
||||
out host,
|
||||
out port,
|
||||
out reason))
|
||||
{
|
||||
if (address.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
|
||||
|| address.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Expected(reason);
|
||||
}
|
||||
|
||||
return Unresolved(reason);
|
||||
}
|
||||
|
||||
entry = new EndpointCatalogEntry
|
||||
{
|
||||
Key = candidate.Key,
|
||||
ArtifactType = candidate.ArtifactType,
|
||||
ApplicationName = candidate.ApplicationName,
|
||||
ArtifactName = candidate.ArtifactName,
|
||||
TransportRole = candidate.TransportRole,
|
||||
AdapterName = candidate.AdapterName,
|
||||
Protocol = protocol,
|
||||
Host = host,
|
||||
Port = port,
|
||||
Enabled = true,
|
||||
AutoDiscovered = true,
|
||||
BestEffort = false
|
||||
};
|
||||
return Probeable(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen.
|
||||
/// </summary>
|
||||
/// <param name="candidate">Zu klassifizierender BizTalk-Transport.</param>
|
||||
/// <returns><see langword="true"/> nur bei einer echten Aufloesungsluecke.</returns>
|
||||
public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
|
||||
{
|
||||
return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leitet das Oracle-TCP-Ziel aus der expliziten DATABASE-Adresse ab.
|
||||
/// </summary>
|
||||
/// <param name="candidate">BizTalk-Artefakt, aus dem der Katalogeintrag aufgebaut wird.</param>
|
||||
/// <param name="address">Vollstaendige DATABASE-Transportadresse.</param>
|
||||
/// <param name="entry">Abgeleiteter geheimnisfreier Best-Effort-Eintrag.</param>
|
||||
/// <param name="reason">Begruendung, wenn Host oder Anbieter nicht sicher erkennbar ist.</param>
|
||||
/// <returns><see langword="true"/>, wenn ein Oracle-Host und TCP-Port bestimmt wurden.</returns>
|
||||
private static bool TryResolveDatabase(
|
||||
EndpointCandidate candidate,
|
||||
string address,
|
||||
out EndpointCatalogEntry entry,
|
||||
out string reason)
|
||||
{
|
||||
entry = null;
|
||||
reason = "DATABASE-Adresse enthaelt keinen sicher bestimmbaren Oracle-Listener.";
|
||||
if (!address.StartsWith("database://", StringComparison.OrdinalIgnoreCase)
|
||||
|| !DatabaseProviderPattern.IsMatch(address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(address, UriKind.Absolute, out uri)
|
||||
|| string.IsNullOrWhiteSpace(uri.Host))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var oracle = Regex.IsMatch(
|
||||
address,
|
||||
@"/Oracle(?:[/;?]|$)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
if (!oracle)
|
||||
{
|
||||
reason = "Nicht-Oracle-DATABASE-Ziel wird ohne explizite Betriebsfreigabe nicht automatisch geprueft.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var port = HasExplicitPort(address) ? uri.Port : 1521;
|
||||
string validationReason;
|
||||
var host = NormalizeHost(uri.Host);
|
||||
if (!Validate(host, port, out validationReason))
|
||||
{
|
||||
reason = validationReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = CreateAdapterEntry(candidate, host, port);
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leitet den ersten erreichbaren TCP-Hop einer WCF-SAP-Verbindung ab.
|
||||
/// </summary>
|
||||
/// <param name="candidate">BizTalk-Artefakt, aus dem der Katalogeintrag aufgebaut wird.</param>
|
||||
/// <param name="address">Vollstaendige SAP-Verbindungs-URI.</param>
|
||||
/// <param name="entry">Abgeleiteter geheimnisfreier Best-Effort-Eintrag.</param>
|
||||
/// <param name="reason">Begruendung, wenn Gateway oder SAProuter nicht sicher bestimmbar ist.</param>
|
||||
/// <returns><see langword="true"/>, wenn ein SAProuter- oder Gateway-Ziel bestimmt wurde.</returns>
|
||||
private static bool TryResolveWcfSap(
|
||||
EndpointCandidate candidate,
|
||||
string address,
|
||||
out EndpointCatalogEntry entry,
|
||||
out string reason)
|
||||
{
|
||||
entry = null;
|
||||
reason = "WCF-SAP-Adresse enthaelt keinen sicher bestimmbaren Gateway- oder SAProuter-Port.";
|
||||
var decoded = DecodeAdapterAddress(address);
|
||||
|
||||
string route;
|
||||
if (TryReadParameter(decoded, "SAPROUTER", out route))
|
||||
{
|
||||
string routerHost;
|
||||
int routerPort;
|
||||
if (TryParseSapRouter(route, out routerHost, out routerPort))
|
||||
{
|
||||
entry = CreateAdapterEntry(candidate, routerHost, routerPort);
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
string gatewayHost;
|
||||
string gatewayService;
|
||||
if ((!TryReadParameter(decoded, "ListenerGwHost", out gatewayHost)
|
||||
|| !TryReadParameter(decoded, "ListenerGwServ", out gatewayService))
|
||||
&& (!TryReadParameter(decoded, "GwHost", out gatewayHost)
|
||||
|| !TryReadParameter(decoded, "GwServ", out gatewayService)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int gatewayPort;
|
||||
if (!TryResolveSapService(gatewayService, out gatewayPort))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
gatewayHost = NormalizeHost(gatewayHost);
|
||||
string validationReason;
|
||||
if (!Validate(gatewayHost, gatewayPort, out validationReason))
|
||||
{
|
||||
reason = validationReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = CreateAdapterEntry(candidate, gatewayHost, gatewayPort);
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut einen TCP-Katalogeintrag fuer einen automatisch abgeleiteten Best-Effort-Adapter.
|
||||
/// </summary>
|
||||
/// <param name="candidate">Quellartefakt fuer Metadaten und stabilen Schluessel.</param>
|
||||
/// <param name="host">Bereinigter DNS-Name oder IP-Adresse.</param>
|
||||
/// <param name="port">Gueltiger TCP-Zielport.</param>
|
||||
/// <returns>Automatisch verwalteter, nicht alarmierender Katalogeintrag.</returns>
|
||||
private static EndpointCatalogEntry CreateAdapterEntry(EndpointCandidate candidate, string host, int port)
|
||||
{
|
||||
return new EndpointCatalogEntry
|
||||
{
|
||||
Key = candidate.Key,
|
||||
ArtifactType = candidate.ArtifactType,
|
||||
ApplicationName = candidate.ApplicationName,
|
||||
ArtifactName = candidate.ArtifactName,
|
||||
TransportRole = candidate.TransportRole,
|
||||
AdapterName = candidate.AdapterName,
|
||||
Protocol = "TCP",
|
||||
Host = host,
|
||||
Port = port,
|
||||
Enabled = true,
|
||||
AutoDiscovered = true,
|
||||
BestEffort = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest einen URI-Parameter auch aus SAP-URIs mit wiederholtem Fragezeichen.
|
||||
/// </summary>
|
||||
/// <param name="address">Dekodierte SAP-Verbindungsadresse.</param>
|
||||
/// <param name="name">Name des gesuchten Query-Parameters.</param>
|
||||
/// <param name="value">Bereinigter Parameterwert.</param>
|
||||
/// <returns><see langword="true"/>, wenn ein nicht leerer Wert gefunden wurde.</returns>
|
||||
private static bool TryReadParameter(string address, string name, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
var match = Regex.Match(
|
||||
address ?? string.Empty,
|
||||
@"(?:^|[?&])" + Regex.Escape(name) + @"=(?<value>[^?&]+)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = match.Groups["value"].Value.Trim();
|
||||
return value.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dekodiert URI-Escapes, ohne eine fehlerhafte Adapteradresse zum Laufzeitfehler zu machen.
|
||||
/// </summary>
|
||||
/// <param name="address">Rohe BizTalk-Transportadresse.</param>
|
||||
/// <returns>Bestmoeglich dekodierte Adresse.</returns>
|
||||
private static string DecodeAdapterAddress(string address)
|
||||
{
|
||||
var normalized = (address ?? string.Empty).Replace("&", "&");
|
||||
try
|
||||
{
|
||||
return Uri.UnescapeDataString(normalized);
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bestimmt Host und Port des ersten SAProuter-Hops einer Route.
|
||||
/// </summary>
|
||||
/// <param name="route">SAProuter-Route im Format <c>/H/host/S/port</c>.</param>
|
||||
/// <param name="host">Host des ersten Router-Hops.</param>
|
||||
/// <param name="port">Expliziter Port oder SAProuter-Standardport 3299.</param>
|
||||
/// <returns><see langword="true"/>, wenn der erste Hop gueltig ist.</returns>
|
||||
private static bool TryParseSapRouter(string route, out string host, out int port)
|
||||
{
|
||||
host = string.Empty;
|
||||
port = 0;
|
||||
var match = Regex.Match(
|
||||
route ?? string.Empty,
|
||||
@"/H/(?<host>[^/]+)(?:/S/(?<service>[^/]+))?",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
host = NormalizeHost(match.Groups["host"].Value);
|
||||
port = 3299;
|
||||
var service = match.Groups["service"].Value;
|
||||
if (service.Length > 0 && !TryResolveSapService(service, out port))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string reason;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uebersetzt numerische SAP-Dienste und sapgwNN in einen TCP-Port.
|
||||
/// </summary>
|
||||
/// <param name="service">Numerischer Port oder SAP-Gateway-Dienstname.</param>
|
||||
/// <param name="port">Aufgeloester TCP-Port.</param>
|
||||
/// <returns><see langword="true"/> bei einem gueltigen Port oder <c>sapgwNN</c>.</returns>
|
||||
private static bool TryResolveSapService(string service, out int port)
|
||||
{
|
||||
port = 0;
|
||||
var normalized = (service ?? string.Empty).Trim();
|
||||
if (int.TryParse(normalized, out port))
|
||||
{
|
||||
return port >= 1 && port <= 65535;
|
||||
}
|
||||
|
||||
var match = Regex.Match(
|
||||
normalized,
|
||||
@"^sapgw(?<instance>\d{2})$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
int instance;
|
||||
if (!match.Success || !int.TryParse(match.Groups["instance"].Value, out instance))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
port = 3300 + instance;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erkennt BizTalk-Scheduling-Komponenten, die kein Remoteziel darstellen.
|
||||
/// </summary>
|
||||
/// <param name="candidate">Zu pruefender BizTalk-Transport.</param>
|
||||
/// <returns><see langword="true"/> fuer Schedule-/Scheduler-Konfigurationen.</returns>
|
||||
private static bool IsScheduler(EndpointCandidate candidate)
|
||||
{
|
||||
return Contains(candidate.AdapterName, "SCHEDUL")
|
||||
|| (candidate.Address ?? string.Empty).StartsWith("schedule", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erkennt den kundenspezifischen DATABASE-Adapter und dessen URI-Schema.
|
||||
/// </summary>
|
||||
/// <param name="candidate">Zu pruefender BizTalk-Transport.</param>
|
||||
/// <returns><see langword="true"/> fuer DATABASE-Konfigurationen.</returns>
|
||||
private static bool IsDatabaseAdapter(EndpointCandidate candidate)
|
||||
{
|
||||
return Contains(candidate.AdapterName, "DATABASE")
|
||||
|| (candidate.Address ?? string.Empty).StartsWith("database://", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erkennt WCF-SAP-Adapter, ohne beliebige Adapter mit SAP im Namen einzubeziehen.
|
||||
/// </summary>
|
||||
/// <param name="candidate">Zu pruefender BizTalk-Transport.</param>
|
||||
/// <returns><see langword="true"/> fuer eindeutig erkannte WCF-SAP-Konfigurationen.</returns>
|
||||
private static bool IsWcfSapAdapter(EndpointCandidate candidate)
|
||||
{
|
||||
return Contains(candidate.AdapterName, "WCF-SAP")
|
||||
|| Contains(candidate.AdapterName, "WCF SAP")
|
||||
// BizTalk meldet den SAP Binding-Typ in manchen Umgebungen nur als
|
||||
// WCF-Custom; das eindeutige sap://-Schema bleibt dennoch erhalten.
|
||||
|| (candidate.Address ?? string.Empty).StartsWith("sap://", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verpackt einen gueltigen Katalogeintrag als pruefbare Aufloesung.
|
||||
/// </summary>
|
||||
/// <param name="entry">Vollstaendig validierter Katalogeintrag.</param>
|
||||
/// <returns>Pruefbare Endpoint-Aufloesung ohne Fehlergrund.</returns>
|
||||
private static EndpointResolution Probeable(EndpointCatalogEntry entry)
|
||||
{
|
||||
return new EndpointResolution
|
||||
{
|
||||
Status = EndpointResolutionStatus.Probeable,
|
||||
Entry = entry,
|
||||
Reason = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryResolve(
|
||||
string address,
|
||||
string adapterName,
|
||||
bool allowWildcardListener,
|
||||
out string protocol,
|
||||
out string host,
|
||||
out int port,
|
||||
out string reason)
|
||||
{
|
||||
protocol = "TCP";
|
||||
host = string.Empty;
|
||||
port = 0;
|
||||
reason = string.Empty;
|
||||
|
||||
if (allowWildcardListener
|
||||
&& TryResolveWildcardListener(address, out protocol, out host, out port, out reason))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LooksLikeEmailRecipients(address)
|
||||
&& !Contains(adapterName, "SFTP")
|
||||
&& !Contains(adapterName, "FTP"))
|
||||
{
|
||||
reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
{
|
||||
var end = address.IndexOf('\\', 2);
|
||||
host = end < 0 ? address.Substring(2) : address.Substring(2, end - 2);
|
||||
port = 445;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|
||||
|| Path.IsPathRooted(address) && !address.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
reason = "Lokaler Dateipfad benoetigt keine externe Netzwerkprobe.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryResolveMsmq(address, out host))
|
||||
{
|
||||
port = 1801;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
// Host:Port muss vor Uri.TryCreate ausgewertet werden: .NET interpretiert
|
||||
// beispielsweise "server:2222/path" sonst als URI mit dem Schema "server".
|
||||
var match = HostPortPattern.Match(address);
|
||||
if (match.Success)
|
||||
{
|
||||
host = match.Groups["host"].Value.Trim('[', ']');
|
||||
if (!int.TryParse(match.Groups["port"].Value, out port))
|
||||
{
|
||||
port = 0;
|
||||
}
|
||||
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
// BizTalk-FTP-/SFTP-Konfigurationen kommen auch ohne Schema vor. Durch
|
||||
// die temporaere URI-Normalisierung werden user@host, IPv6 und Ports
|
||||
// sauber getrennt; Benutzername und Pfad gelangen nicht in den Katalog.
|
||||
if (TryResolveAdapterUri(address, adapterName, out host, out port))
|
||||
{
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(address, UriKind.Absolute, out uri)
|
||||
&& !string.IsNullOrWhiteSpace(uri.Scheme))
|
||||
{
|
||||
var scheme = uri.Scheme.ToLowerInvariant();
|
||||
if (scheme == "file")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri.Host))
|
||||
{
|
||||
reason = "Lokaler file-Endpunkt benoetigt keine externe Netzwerkprobe.";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = NormalizeHost(uri.Host);
|
||||
port = 445;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (scheme == "net.pipe" || scheme == "npipe")
|
||||
{
|
||||
reason = "Named Pipes sind keine TCP-/UDP-Endpunkte.";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = NormalizeHost(uri.Host);
|
||||
protocol = scheme == "udp" ? "UDP" : "TCP";
|
||||
port = ResolvePort(uri, scheme);
|
||||
if (port <= 0 && HasExplicitPort(address))
|
||||
{
|
||||
port = uri.Port;
|
||||
}
|
||||
|
||||
if (port <= 0)
|
||||
{
|
||||
reason = "Fuer das Schema '" + scheme + "' konnte kein TCP-/UDP-Port bestimmt werden.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (Contains(adapterName, "SFTP"))
|
||||
{
|
||||
host = ExtractBareAdapterHost(address);
|
||||
port = 22;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
if (Contains(adapterName, "FTP"))
|
||||
{
|
||||
host = ExtractBareAdapterHost(address);
|
||||
port = 21;
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
reason = "Transportadresse enthaelt keinen sicher bestimmbaren Host und Port.";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int ResolvePort(Uri uri, string scheme)
|
||||
{
|
||||
if (uri.Port > 0 && !uri.IsDefaultPort)
|
||||
{
|
||||
return uri.Port;
|
||||
}
|
||||
|
||||
switch (scheme)
|
||||
{
|
||||
case "http": return 80;
|
||||
case "https": return 443;
|
||||
case "ftp": return 21;
|
||||
case "sftp": return 22;
|
||||
case "smb": return 445;
|
||||
case "smtp": return 25;
|
||||
case "ldap": return 389;
|
||||
case "ldaps": return 636;
|
||||
case "pop3": return 110;
|
||||
case "pop3s": return 995;
|
||||
case "imap": return 143;
|
||||
case "imaps": return 993;
|
||||
case "msmq": return 1801;
|
||||
case "net.tcp": return 808;
|
||||
case "mssql": return 1433;
|
||||
// Ohne Pfad ist der Hostteil ein tnsnames.ora-Alias und kein sicher pruefbarer DNS-Host.
|
||||
case "oracledb": return uri.AbsolutePath.Trim('/').Length > 0 ? 1521 : 0;
|
||||
default: return uri.Port > 0 ? uri.Port : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduziert lokale HTTP-/WCF-Wildcard-Listener auf einen nebenwirkungsfreien Loopback-Porttest.
|
||||
/// </summary>
|
||||
private static bool TryResolveWildcardListener(
|
||||
string address,
|
||||
out string protocol,
|
||||
out string host,
|
||||
out int port,
|
||||
out string reason)
|
||||
{
|
||||
protocol = "TCP";
|
||||
host = string.Empty;
|
||||
port = 0;
|
||||
reason = string.Empty;
|
||||
var match = Regex.Match(
|
||||
address,
|
||||
@"^(?<scheme>https?|net\.tcp)://(?:\+|\*)(?::(?<port>\d{1,5}))?(?:[/\\].*)?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var scheme = match.Groups["scheme"].Value.ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(match.Groups["port"].Value)
|
||||
&& !int.TryParse(match.Groups["port"].Value, out port))
|
||||
{
|
||||
port = 0;
|
||||
}
|
||||
|
||||
if (port <= 0)
|
||||
{
|
||||
port = scheme == "https" ? 443 : scheme == "net.tcp" ? 808 : 80;
|
||||
}
|
||||
|
||||
host = "127.0.0.1";
|
||||
return Validate(host, port, out reason);
|
||||
}
|
||||
|
||||
private static string ExtractBareAdapterHost(string address)
|
||||
{
|
||||
var value = (address ?? string.Empty).Trim().Trim('/');
|
||||
var separator = value.IndexOfAny(new[] { '/', '\\' });
|
||||
return separator < 0 ? value : value.Substring(0, separator);
|
||||
}
|
||||
|
||||
private static bool TryResolveAdapterUri(string address, string adapterName, out string host, out int port)
|
||||
{
|
||||
host = string.Empty;
|
||||
port = 0;
|
||||
var scheme = Contains(adapterName, "SFTP")
|
||||
? "sftp"
|
||||
: Contains(adapterName, "FTP") ? "ftp" : string.Empty;
|
||||
if (scheme.Length == 0 || address.IndexOf("://", StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
var normalized = scheme + "://" + address.TrimStart('/');
|
||||
if (!Uri.TryCreate(normalized, UriKind.Absolute, out uri)
|
||||
|| string.IsNullOrWhiteSpace(uri.Host))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
host = NormalizeHost(uri.Host);
|
||||
port = uri.IsDefaultPort || uri.Port <= 0
|
||||
? scheme == "sftp" ? 22 : 21
|
||||
: uri.Port;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erkennt Adapter, deren relative Receive-Adresse eine lokale Listenerbindung statt eines Remoteziels beschreibt.
|
||||
/// </summary>
|
||||
private static bool IsInboundListener(EndpointCandidate candidate)
|
||||
{
|
||||
return string.Equals(candidate.ArtifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
|
||||
&& (Contains(candidate.AdapterName, "HTTP")
|
||||
|| Contains(candidate.AdapterName, "SOAP")
|
||||
|| Contains(candidate.AdapterName, "WCF"));
|
||||
}
|
||||
|
||||
private static EndpointResolution Expected(string reason)
|
||||
{
|
||||
return new EndpointResolution
|
||||
{
|
||||
Status = EndpointResolutionStatus.ExpectedNonProbeable,
|
||||
Reason = reason
|
||||
};
|
||||
}
|
||||
|
||||
private static EndpointResolution Unresolved(string reason)
|
||||
{
|
||||
return new EndpointResolution
|
||||
{
|
||||
Status = EndpointResolutionStatus.Unresolved,
|
||||
Reason = string.IsNullOrWhiteSpace(reason)
|
||||
? "Transportadresse konnte nicht sicher auf Host und Port reduziert werden."
|
||||
: reason
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasExplicitPort(string address)
|
||||
{
|
||||
var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3);
|
||||
var authority = authorityEnd < 0 ? address : address.Substring(0, authorityEnd);
|
||||
return Regex.IsMatch(authority, @":\d{1,5}$", RegexOptions.CultureInvariant);
|
||||
}
|
||||
|
||||
private static bool TryResolveMsmq(string address, out string host)
|
||||
{
|
||||
host = string.Empty;
|
||||
var match = Regex.Match(
|
||||
address,
|
||||
@"(?:FORMATNAME:)?DIRECT=(?:OS|TCP):(?<host>[^\\/;]+)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
host = match.Groups["host"].Value.Trim();
|
||||
return host.Length > 0;
|
||||
}
|
||||
|
||||
private static bool LooksLikeEmailRecipients(string value)
|
||||
{
|
||||
return value.IndexOf('@') > 0
|
||||
&& value.IndexOf("://", StringComparison.Ordinal) < 0
|
||||
&& !value.StartsWith("\\\\", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool Contains(string value, string fragment)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(value)
|
||||
&& value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static string NormalizeHost(string host)
|
||||
{
|
||||
return (host ?? string.Empty).Trim().Trim('[', ']');
|
||||
}
|
||||
|
||||
private static bool Validate(string host, int port, out string reason)
|
||||
{
|
||||
host = NormalizeHost(host);
|
||||
if (host.Length == 0
|
||||
|| host.IndexOfAny(new[] { ' ', '\t', '\r', '\n', '*', '%', '/', '\\', '@', ';', '?', '#' }) >= 0)
|
||||
{
|
||||
reason = "Host ist leer oder enthaelt nicht aufgeloeste Platzhalter.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (port < 1 || port > 65535)
|
||||
{
|
||||
reason = "Port liegt ausserhalb des gueltigen Bereichs.";
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Liest und schreibt die lokale Endpoint-Konfiguration atomar und mit strikter Validierung.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCatalogStore
|
||||
{
|
||||
private const string RootName = "BizTalkEndpointCatalog";
|
||||
private const string Version = "1";
|
||||
private readonly string _path;
|
||||
private readonly int _maxBytes;
|
||||
private readonly int _maxEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert den validierenden XML-Katalogspeicher.
|
||||
/// </summary>
|
||||
/// <param name="path">Absoluter Zielpfad der Katalogdatei.</param>
|
||||
/// <param name="maxBytes">Maximal akzeptierte Dateigroesse.</param>
|
||||
/// <param name="maxEntries">Maximal akzeptierte Anzahl Eintraege.</param>
|
||||
public EndpointCatalogStore(string path, int maxBytes, int maxEntries)
|
||||
{
|
||||
_path = path;
|
||||
_maxBytes = maxBytes;
|
||||
_maxEntries = maxEntries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest und validiert einen maschinen- und umgebungsgebundenen Katalog.
|
||||
/// </summary>
|
||||
/// <param name="environmentName">Erwartete Monitoring-Umgebung.</param>
|
||||
/// <returns>Vollstaendig validierter Endpoint-Katalog.</returns>
|
||||
public EndpointCatalog Read(string environmentName)
|
||||
{
|
||||
var info = new FileInfo(_path);
|
||||
if (!info.Exists)
|
||||
{
|
||||
throw new FileNotFoundException("Endpoint catalog does not exist.", _path);
|
||||
}
|
||||
|
||||
if (info.Length <= 0 || info.Length > _maxBytes)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog size is outside the configured range.");
|
||||
}
|
||||
|
||||
XDocument document;
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit,
|
||||
XmlResolver = null,
|
||||
MaxCharactersInDocument = _maxBytes
|
||||
};
|
||||
using (var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
|
||||
using (var reader = XmlReader.Create(stream, settings))
|
||||
{
|
||||
document = XDocument.Load(reader, LoadOptions.None);
|
||||
}
|
||||
|
||||
var root = document.Root;
|
||||
if (root == null
|
||||
|| root.Name.LocalName != RootName
|
||||
|| ReadAttribute(root, "version") != Version)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog format or version is invalid.");
|
||||
}
|
||||
|
||||
var machine = ReadAttribute(root, "machine");
|
||||
if (!string.Equals(machine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog was created for a different machine.");
|
||||
}
|
||||
|
||||
var catalogEnvironment = ReadAttribute(root, "environment");
|
||||
if (!string.Equals(catalogEnvironment, environmentName ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog was created for a different environment.");
|
||||
}
|
||||
|
||||
DateTime synchronizedUtc;
|
||||
if (!DateTime.TryParseExact(
|
||||
ReadAttribute(root, "synchronizedUtc"),
|
||||
"o",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out synchronizedUtc))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog synchronization timestamp is invalid.");
|
||||
}
|
||||
|
||||
var catalog = new EndpointCatalog
|
||||
{
|
||||
MachineName = machine,
|
||||
EnvironmentName = catalogEnvironment,
|
||||
SynchronizedUtc = synchronizedUtc,
|
||||
ActiveCandidates = ReadInt(root, "activeCandidates", 0, 100000),
|
||||
UnresolvedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000)
|
||||
};
|
||||
|
||||
foreach (var node in root.Elements("Endpoint"))
|
||||
{
|
||||
if (catalog.Entries.Count >= _maxEntries)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog exceeds the configured entry limit.");
|
||||
}
|
||||
|
||||
var entry = new EndpointCatalogEntry
|
||||
{
|
||||
Key = ReadAttribute(node, "key"),
|
||||
ArtifactType = ReadAttribute(node, "artifactType"),
|
||||
ApplicationName = ReadAttribute(node, "application"),
|
||||
ArtifactName = ReadAttribute(node, "artifact"),
|
||||
TransportRole = ReadAttribute(node, "transportRole"),
|
||||
AdapterName = ReadAttribute(node, "adapter"),
|
||||
Protocol = ReadAttribute(node, "protocol").ToUpperInvariant(),
|
||||
Host = ReadAttribute(node, "host"),
|
||||
Port = ReadInt(node, "port", 1, 65535),
|
||||
Enabled = ReadBool(node, "enabled"),
|
||||
AutoDiscovered = ReadBool(node, "autoDiscovered"),
|
||||
// Das optionale Attribut haelt bestehende Kataloge der Version 1 lesbar.
|
||||
BestEffort = ReadOptionalBool(node, "bestEffort", false)
|
||||
};
|
||||
ValidateEntry(entry);
|
||||
catalog.Entries.Add(entry);
|
||||
}
|
||||
|
||||
if (catalog.Entries.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != catalog.Entries.Count)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog contains duplicate keys.");
|
||||
}
|
||||
|
||||
return catalog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validiert und ersetzt den Endpoint-Katalog atomar.
|
||||
/// </summary>
|
||||
/// <param name="catalog">Zu persistierender geheimnisfreier Katalog.</param>
|
||||
public void Write(EndpointCatalog catalog)
|
||||
{
|
||||
if (catalog == null)
|
||||
{
|
||||
throw new ArgumentNullException("catalog");
|
||||
}
|
||||
|
||||
if (catalog.Entries.Count > _maxEntries)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog exceeds the configured entry limit.");
|
||||
}
|
||||
|
||||
foreach (var entry in catalog.Entries)
|
||||
{
|
||||
ValidateEntry(entry);
|
||||
}
|
||||
|
||||
var root = new XElement(
|
||||
RootName,
|
||||
new XAttribute("version", Version),
|
||||
new XAttribute("machine", Environment.MachineName),
|
||||
new XAttribute("environment", catalog.EnvironmentName ?? string.Empty),
|
||||
new XAttribute("synchronizedUtc", catalog.SynchronizedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)),
|
||||
new XAttribute("activeCandidates", catalog.ActiveCandidates),
|
||||
// Der XML-Attributsname bleibt fuer vorhandene Katalogdateien der Version 1 stabil.
|
||||
new XAttribute("unsupportedCandidates", catalog.UnresolvedCandidates));
|
||||
foreach (var entry in catalog.Entries
|
||||
.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.ArtifactType, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.ArtifactName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.TransportRole, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
root.Add(new XElement(
|
||||
"Endpoint",
|
||||
new XAttribute("key", entry.Key),
|
||||
new XAttribute("artifactType", entry.ArtifactType ?? string.Empty),
|
||||
new XAttribute("application", entry.ApplicationName ?? string.Empty),
|
||||
new XAttribute("artifact", entry.ArtifactName ?? string.Empty),
|
||||
new XAttribute("transportRole", entry.TransportRole ?? string.Empty),
|
||||
new XAttribute("adapter", entry.AdapterName ?? string.Empty),
|
||||
new XAttribute("protocol", entry.Protocol),
|
||||
new XAttribute("host", entry.Host),
|
||||
new XAttribute("port", entry.Port),
|
||||
new XAttribute("enabled", entry.Enabled),
|
||||
new XAttribute("autoDiscovered", entry.AutoDiscovered),
|
||||
new XAttribute("bestEffort", entry.BestEffort)));
|
||||
}
|
||||
|
||||
var document = new XDocument(new XDeclaration("1.0", "utf-8", null), root);
|
||||
byte[] bytes;
|
||||
using (var memory = new MemoryStream())
|
||||
using (var writer = XmlWriter.Create(memory, new XmlWriterSettings
|
||||
{
|
||||
Encoding = new UTF8Encoding(false),
|
||||
Indent = true,
|
||||
NewLineChars = "\r\n",
|
||||
NewLineHandling = NewLineHandling.Replace
|
||||
}))
|
||||
{
|
||||
document.Save(writer);
|
||||
writer.Flush();
|
||||
bytes = memory.ToArray();
|
||||
}
|
||||
|
||||
if (bytes.Length > _maxBytes)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog exceeds EndpointCatalogMaxBytes.");
|
||||
}
|
||||
|
||||
AtomicWrite(bytes);
|
||||
}
|
||||
|
||||
private void AtomicWrite(byte[] bytes)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_path);
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
throw new InvalidOperationException("Endpoint catalog path has no parent directory.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
var temporaryPath = Path.Combine(directory, Path.GetFileName(_path) + "." + Guid.NewGuid().ToString("N") + ".tmp");
|
||||
try
|
||||
{
|
||||
using (var stream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
stream.Flush(true);
|
||||
}
|
||||
|
||||
if (File.Exists(_path))
|
||||
{
|
||||
File.Replace(temporaryPath, _path, null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(temporaryPath, _path);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporaryPath))
|
||||
{
|
||||
try { File.Delete(temporaryPath); } catch (IOException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateEntry(EndpointCatalogEntry entry)
|
||||
{
|
||||
if (entry == null
|
||||
|| string.IsNullOrWhiteSpace(entry.Key)
|
||||
|| string.IsNullOrWhiteSpace(entry.ArtifactType)
|
||||
|| string.IsNullOrWhiteSpace(entry.ArtifactName)
|
||||
|| string.IsNullOrWhiteSpace(entry.Host)
|
||||
|| entry.Host.IndexOfAny(new[] { '\r', '\n', '\t', ' ' }) >= 0
|
||||
|| entry.Port < 1
|
||||
|| entry.Port > 65535
|
||||
|| !(string.Equals(entry.Protocol, "TCP", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(entry.Protocol, "UDP", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog contains an invalid entry.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadAttribute(XElement element, string name)
|
||||
{
|
||||
var attribute = element.Attribute(name);
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog attribute is missing: " + name);
|
||||
}
|
||||
|
||||
return attribute.Value.Trim();
|
||||
}
|
||||
|
||||
private static int ReadInt(XElement element, string name, int min, int max)
|
||||
{
|
||||
int value;
|
||||
if (!int.TryParse(ReadAttribute(element, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out value)
|
||||
|| value < min
|
||||
|| value > max)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog integer is invalid: " + name);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool ReadBool(XElement element, string name)
|
||||
{
|
||||
bool value;
|
||||
if (!bool.TryParse(ReadAttribute(element, name), out value))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog boolean is invalid: " + name);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest ein optionales boolesches Attribut mit rueckwaertskompatiblem Standardwert.
|
||||
/// </summary>
|
||||
/// <param name="element">XML-Element mit dem optionalen Attribut.</param>
|
||||
/// <param name="name">Name des Attributs.</param>
|
||||
/// <param name="defaultValue">Rueckgabewert, wenn das Attribut fehlt.</param>
|
||||
/// <returns>Gelesener Wert oder der angegebene Standardwert.</returns>
|
||||
private static bool ReadOptionalBool(XElement element, string name, bool defaultValue)
|
||||
{
|
||||
var attribute = element.Attribute(name);
|
||||
if (attribute == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool value;
|
||||
if (!bool.TryParse(attribute.Value.Trim(), out value))
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog boolean is invalid: " + name);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Pflegt den woechentlichen Endpoint-Katalog und prueft aktive Ziele parallel auf Netzwerkebene.
|
||||
/// </summary>
|
||||
internal sealed class EndpointConnectivityProbe
|
||||
{
|
||||
private readonly MonitoringOptions _options;
|
||||
private readonly FileLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert die Endpoint-Pruefung mit Laufzeitgrenzen und Dateiprotokoll.
|
||||
/// </summary>
|
||||
/// <param name="options">Validierte Monitoring-Konfiguration.</param>
|
||||
/// <param name="logger">Gemeinsames Provider-Dateiprotokoll.</param>
|
||||
public EndpointConnectivityProbe(MonitoringOptions options, FileLogger logger)
|
||||
{
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronisiert den Katalog und ergaenzt das Gesamtergebnis um Socket-Probes.
|
||||
/// </summary>
|
||||
/// <param name="result">Bereits mit aktuellen BizTalk-Artefakten gefuelltes Ergebnis.</param>
|
||||
public void Query(ProbeResult result)
|
||||
{
|
||||
var state = result.EndpointConnectivity;
|
||||
if (!_options.ProbeEndpointConnectivity)
|
||||
{
|
||||
state.Disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state.RuntimeStateAvailable = result.Platform.ReceiveLocationsDataAvailable
|
||||
&& result.Platform.SendPortsDataAvailable;
|
||||
if (!state.RuntimeStateAvailable)
|
||||
{
|
||||
state.Failure = "Aktive Send Ports und Receive Locations konnten nicht vollstaendig bestimmt werden.";
|
||||
return;
|
||||
}
|
||||
|
||||
var store = new EndpointCatalogStore(
|
||||
_options.EndpointCatalogPath,
|
||||
_options.EndpointCatalogMaxBytes,
|
||||
_options.EndpointCatalogMaxEntries);
|
||||
EndpointCatalog catalog = null;
|
||||
string readFailure = null;
|
||||
try
|
||||
{
|
||||
catalog = store.Read(_options.EnvironmentName);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
readFailure = ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
readFailure = ex.GetType().Name + ": " + ex.Message;
|
||||
_logger.Warning("Endpoint catalog rejected. path=" + _options.EndpointCatalogPath + " reason=" + readFailure);
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
state.RefreshRequired = _options.ForceEndpointCatalogRefresh
|
||||
|| catalog == null
|
||||
|| catalog.SynchronizedUtc > now.AddMinutes(5)
|
||||
|| now - catalog.SynchronizedUtc >= TimeSpan.FromHours(_options.EndpointDiscoveryIntervalHours);
|
||||
if (state.RefreshRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
catalog = Synchronize(catalog, result.EndpointCandidates, now);
|
||||
store.Write(catalog);
|
||||
state.RefreshSucceeded = true;
|
||||
_logger.Info(
|
||||
"Endpoint catalog synchronized. path=" + _options.EndpointCatalogPath
|
||||
+ " active_candidates=" + catalog.ActiveCandidates
|
||||
+ " configured=" + catalog.Entries.Count
|
||||
+ " unresolved_candidates=" + catalog.UnresolvedCandidates);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state.RefreshSucceeded = false;
|
||||
state.Failure = "Endpoint-Katalog konnte nicht synchronisiert werden: " + ex.GetType().Name + ": " + ex.Message;
|
||||
_logger.Error("Endpoint catalog synchronization failed.", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.RefreshSucceeded = true;
|
||||
}
|
||||
|
||||
if (catalog == null)
|
||||
{
|
||||
state.CatalogAvailable = false;
|
||||
if (string.IsNullOrWhiteSpace(state.Failure))
|
||||
{
|
||||
state.Failure = "Keine gueltige Endpoint-Konfiguration verfuegbar: " + readFailure;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.CatalogAvailable = true;
|
||||
state.CatalogSynchronizedUtc = catalog.SynchronizedUtc;
|
||||
state.Configured = catalog.Entries.Count;
|
||||
var activeEntries = ResolveActiveEndpoints(catalog, result.EndpointCandidates, state);
|
||||
var activeTargetCount = activeEntries
|
||||
.Select(TargetKey)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Count();
|
||||
if (activeTargetCount > _options.EndpointMaxCount)
|
||||
{
|
||||
state.Failure = "Eindeutige aktive Netzwerkziele=" + activeTargetCount
|
||||
+ " ueberschreiten EndpointMaxCount=" + _options.EndpointMaxCount + ".";
|
||||
return;
|
||||
}
|
||||
|
||||
state.Active = activeEntries.Length;
|
||||
|
||||
// Detailwarnungen nur beim Katalogabgleich schreiben; der Minutensnapshot
|
||||
// enthaelt die aktuelle begrenzte Liste bereits und das Tageslog bleibt kompakt.
|
||||
foreach (var issue in state.RefreshRequired
|
||||
? state.ResolutionIssues.Take(20)
|
||||
: Enumerable.Empty<string>())
|
||||
{
|
||||
_logger.Warning("Endpoint target unresolved. " + issue);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var probeStopwatch = Stopwatch.StartNew();
|
||||
var probeResults = ProbeAllAsync(activeEntries).GetAwaiter().GetResult();
|
||||
probeStopwatch.Stop();
|
||||
state.ProbeDurationMilliseconds = probeStopwatch.ElapsedMilliseconds;
|
||||
state.UniqueTargets = activeEntries
|
||||
.Select(TargetKey)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Count();
|
||||
foreach (var probeResult in probeResults)
|
||||
{
|
||||
state.Results.Add(probeResult);
|
||||
if (!probeResult.Available && probeResult.Endpoint.BestEffort)
|
||||
{
|
||||
// DATABASE-/WCF-SAP-Ziele sind bewusst Diagnose-Probes: Ein
|
||||
// Fehlschlag darf weder Tageslog noch Checkmk mit Alarmen fluten.
|
||||
state.BestEffortIgnoredFailures++;
|
||||
}
|
||||
else if (!probeResult.Available)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Endpoint unavailable. endpoint=" + Display(probeResult.Endpoint)
|
||||
+ " duration_ms=" + probeResult.DurationMilliseconds
|
||||
+ " reason=" + probeResult.Failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state.Failure = "Endpoint-Probes konnten nicht abgeschlossen werden: " + ex.GetType().Name + ": " + ex.Message;
|
||||
_logger.Error("Endpoint probes failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fuehrt manuelle Overrides mit den aktuell automatisch erkannten Zielen zusammen.
|
||||
/// </summary>
|
||||
/// <param name="existing">Vorhandener Katalog oder <see langword="null"/>.</param>
|
||||
/// <param name="candidates">Aktuelle BizTalk-Send-/Receive-Transporte.</param>
|
||||
/// <param name="synchronizedUtc">UTC-Zeitpunkt des vollstaendigen Abgleichs.</param>
|
||||
/// <returns>Neuer, geheimnisfreier Endpoint-Katalog.</returns>
|
||||
internal static EndpointCatalog SynchronizeCatalog(
|
||||
EndpointCatalog existing,
|
||||
IEnumerable<EndpointCandidate> candidates,
|
||||
DateTime synchronizedUtc)
|
||||
{
|
||||
var active = (candidates ?? Enumerable.Empty<EndpointCandidate>())
|
||||
.Where(x => x != null && x.Active)
|
||||
.ToArray();
|
||||
var manual = existing == null
|
||||
? new EndpointCatalogEntry[0]
|
||||
: existing.Entries.Where(x => !x.AutoDiscovered).ToArray();
|
||||
var manualKeys = new HashSet<string>(manual.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
|
||||
var discovered = new List<EndpointCatalogEntry>();
|
||||
var unresolved = 0;
|
||||
foreach (var candidate in active)
|
||||
{
|
||||
var resolution = EndpointAddressParser.Analyze(candidate);
|
||||
if (resolution.Status == EndpointResolutionStatus.Probeable)
|
||||
{
|
||||
discovered.Add(resolution.Entry);
|
||||
}
|
||||
else if (resolution.Status == EndpointResolutionStatus.Unresolved
|
||||
&& !manualKeys.Contains(candidate.Key))
|
||||
{
|
||||
unresolved++;
|
||||
}
|
||||
}
|
||||
|
||||
var merged = manual
|
||||
.Concat(discovered.Where(x => !manualKeys.Contains(x.Key)))
|
||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(x => x.First())
|
||||
.ToArray();
|
||||
|
||||
var catalog = new EndpointCatalog
|
||||
{
|
||||
MachineName = Environment.MachineName,
|
||||
EnvironmentName = existing == null ? string.Empty : existing.EnvironmentName,
|
||||
SynchronizedUtc = synchronizedUtc.ToUniversalTime(),
|
||||
ActiveCandidates = active.Length,
|
||||
// Attributsname bleibt fuer die Rueckwaertskompatibilitaet des Katalogformats bestehen.
|
||||
UnresolvedCandidates = unresolved
|
||||
};
|
||||
catalog.Entries.AddRange(merged);
|
||||
return catalog;
|
||||
}
|
||||
|
||||
private EndpointCatalog Synchronize(
|
||||
EndpointCatalog existing,
|
||||
IEnumerable<EndpointCandidate> candidates,
|
||||
DateTime synchronizedUtc)
|
||||
{
|
||||
var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc);
|
||||
catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty;
|
||||
if (catalog.Entries.Count > _options.EndpointCatalogMaxEntries)
|
||||
{
|
||||
throw new InvalidDataException("Endpoint catalog exceeds EndpointCatalogMaxEntries.");
|
||||
}
|
||||
|
||||
return catalog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ermittelt aus aktuellen WMI-Kandidaten und manuellen Overrides die in diesem Lauf zu pruefenden Ziele.
|
||||
/// Automatisch erkannte Katalogeintraege werden bewusst nicht als veraltete Laufzeitquelle verwendet.
|
||||
/// </summary>
|
||||
/// <param name="catalog">Gueltiger lokaler Endpoint-Katalog.</param>
|
||||
/// <param name="candidates">Aktuelle aktive und inaktive BizTalk-Transporte.</param>
|
||||
/// <param name="state">Zu aktualisierende Laufzeitzaehler und Aufloesungsdiagnosen.</param>
|
||||
/// <returns>Aktuell aktive, deduplizierte Katalogeintraege.</returns>
|
||||
internal static EndpointCatalogEntry[] ResolveActiveEndpoints(
|
||||
EndpointCatalog catalog,
|
||||
IEnumerable<EndpointCandidate> candidates,
|
||||
EndpointConnectivityState state)
|
||||
{
|
||||
if (catalog == null)
|
||||
{
|
||||
throw new ArgumentNullException("catalog");
|
||||
}
|
||||
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException("state");
|
||||
}
|
||||
|
||||
var activeCandidates = (candidates ?? Enumerable.Empty<EndpointCandidate>())
|
||||
.Where(x => x != null && x.Active)
|
||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(x => x.First())
|
||||
.ToArray();
|
||||
var activeKeys = new HashSet<string>(activeCandidates.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
|
||||
var manualOverrides = catalog.Entries
|
||||
.Where(x => x.Enabled && !x.AutoDiscovered)
|
||||
.Where(x => !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
|
||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase);
|
||||
var selected = catalog.Entries
|
||||
.Where(x => x.Enabled && string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
foreach (var candidate in activeCandidates)
|
||||
{
|
||||
EndpointCatalogEntry manual;
|
||||
if (manualOverrides.TryGetValue(candidate.Key, out manual))
|
||||
{
|
||||
selected.Add(manual);
|
||||
state.ManualOverridesActive++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var resolution = EndpointAddressParser.Analyze(candidate);
|
||||
if (resolution.Status == EndpointResolutionStatus.Probeable)
|
||||
{
|
||||
// Immer das aktuelle WMI-Ziel pruefen; der Wochenkatalog darf keine alte Adresse erzwingen.
|
||||
selected.Add(resolution.Entry);
|
||||
}
|
||||
else if (resolution.Status == EndpointResolutionStatus.ExpectedNonProbeable)
|
||||
{
|
||||
state.ExpectedNonProbeableActive++;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.UnresolvedActive++;
|
||||
state.ResolutionIssues.Add(DisplayResolutionIssue(candidate, resolution.Reason));
|
||||
}
|
||||
}
|
||||
|
||||
state.SkippedInactive = catalog.Entries.Count(x =>
|
||||
x.Enabled
|
||||
&& !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
|
||||
&& !activeKeys.Contains(x.Key));
|
||||
var resolved = selected
|
||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(x => x.First())
|
||||
.ToArray();
|
||||
state.BestEffortActive = resolved.Count(x => x.BestEffort);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert eine Aufloesungsluecke ohne Transportadresse, Pfad, Querystring oder Zugangsdaten.
|
||||
/// </summary>
|
||||
private static string DisplayResolutionIssue(EndpointCandidate candidate, string reason)
|
||||
{
|
||||
var artifact = string.IsNullOrWhiteSpace(candidate.ApplicationName)
|
||||
|| string.Equals(candidate.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)
|
||||
? candidate.ArtifactName
|
||||
: candidate.ApplicationName + "\\" + candidate.ArtifactName;
|
||||
return "endpoint=" + CompactText(candidate.ArtifactType) + ":" + CompactText(artifact)
|
||||
+ "[" + CompactText(candidate.TransportRole) + "]"
|
||||
+ " adapter=" + CompactText(candidate.AdapterName)
|
||||
+ " reason=" + CompactText(reason);
|
||||
}
|
||||
|
||||
private static string CompactText(string value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.Replace('\r', ' ')
|
||||
.Replace('\n', ' ')
|
||||
.Replace('|', '/')
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints)
|
||||
{
|
||||
var unique = endpoints
|
||||
.GroupBy(TargetKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var outcomes = new Dictionary<string, NetworkOutcome>(StringComparer.OrdinalIgnoreCase);
|
||||
using (var gate = new SemaphoreSlim(_options.EndpointProbeMaxConcurrency))
|
||||
{
|
||||
var tasks = unique.Select(async group =>
|
||||
{
|
||||
await gate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var endpoint = group.First();
|
||||
var outcome = await ProbeOneAsync(endpoint).ConfigureAwait(false);
|
||||
lock (outcomes)
|
||||
{
|
||||
outcomes[group.Key] = outcome;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}).ToArray();
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return endpoints.Select(endpoint =>
|
||||
{
|
||||
var key = TargetKey(endpoint);
|
||||
var outcome = outcomes[key];
|
||||
return new EndpointProbeResult
|
||||
{
|
||||
Endpoint = endpoint,
|
||||
Available = outcome.Available,
|
||||
DurationMilliseconds = outcome.DurationMilliseconds,
|
||||
Failure = outcome.Failure
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Berechnet die theoretische Obergrenze fuer vollstaendig timeoutende Zielgruppen.
|
||||
/// </summary>
|
||||
/// <param name="targetCount">Anzahl eindeutiger Socket-Ziele.</param>
|
||||
/// <param name="concurrency">Maximale parallele Probes.</param>
|
||||
/// <param name="timeoutMilliseconds">Timeout je Probe in Millisekunden.</param>
|
||||
/// <returns>Theoretische Worst-Case-Dauer in Millisekunden.</returns>
|
||||
internal static long CalculateWorstCaseProbeMilliseconds(int targetCount, int concurrency, int timeoutMilliseconds)
|
||||
{
|
||||
if (targetCount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (concurrency <= 0 || timeoutMilliseconds <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("concurrency");
|
||||
}
|
||||
|
||||
return ((targetCount + concurrency - 1L) / concurrency) * timeoutMilliseconds;
|
||||
}
|
||||
|
||||
private static string TargetKey(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
return endpoint.Protocol.ToUpperInvariant()
|
||||
+ "|" + endpoint.Host.ToUpperInvariant()
|
||||
+ "|" + endpoint.Port;
|
||||
}
|
||||
|
||||
private async Task<NetworkOutcome> ProbeOneAsync(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
return string.Equals(endpoint.Protocol, "UDP", StringComparison.OrdinalIgnoreCase)
|
||||
? await ProbeUdpAsync(endpoint).ConfigureAwait(false)
|
||||
: await ProbeTcpAsync(endpoint).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<NetworkOutcome> ProbeTcpAsync(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
try
|
||||
{
|
||||
var connect = client.ConnectAsync(endpoint.Host, endpoint.Port);
|
||||
var completed = await Task.WhenAny(connect, Task.Delay(_options.EndpointProbeTimeoutMilliseconds)).ConfigureAwait(false);
|
||||
if (completed != connect)
|
||||
{
|
||||
client.Close();
|
||||
ObserveFault(connect);
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "TCP timeout");
|
||||
}
|
||||
|
||||
await connect.ConfigureAwait(false);
|
||||
return client.Connected
|
||||
? NetworkOutcome.Success(stopwatch.ElapsedMilliseconds)
|
||||
: NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "TCP connection not established");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, CompactFailure(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NetworkOutcome> ProbeUdpAsync(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var resolution = Dns.GetHostAddressesAsync(endpoint.Host);
|
||||
var completed = await Task.WhenAny(resolution, Task.Delay(_options.EndpointProbeTimeoutMilliseconds)).ConfigureAwait(false);
|
||||
if (completed != resolution)
|
||||
{
|
||||
ObserveFault(resolution);
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "UDP DNS timeout");
|
||||
}
|
||||
|
||||
var addresses = await resolution.ConfigureAwait(false);
|
||||
var address = addresses.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork)
|
||||
?? addresses.FirstOrDefault();
|
||||
if (address == null)
|
||||
{
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, "UDP host resolved without an address");
|
||||
}
|
||||
|
||||
using (var socket = new Socket(address.AddressFamily, SocketType.Dgram, ProtocolType.Udp))
|
||||
{
|
||||
socket.Connect(new IPEndPoint(address, endpoint.Port));
|
||||
socket.Send(new byte[0]);
|
||||
}
|
||||
|
||||
// UDP ist verbindungslos: Erfolg bestaetigt DNS, Route und lokalen Datagrammversand,
|
||||
// aber ohne applikationsspezifische Antwort nicht den entfernten Dienstzustand.
|
||||
return NetworkOutcome.Success(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return NetworkOutcome.Failed(stopwatch.ElapsedMilliseconds, CompactFailure(ex));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ObserveFault(Task task)
|
||||
{
|
||||
task.ContinueWith(
|
||||
completed => { var ignored = completed.Exception; },
|
||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
|
||||
private static string CompactFailure(Exception exception)
|
||||
{
|
||||
var socket = exception as SocketException;
|
||||
return socket == null
|
||||
? exception.GetType().Name + ": " + exception.Message
|
||||
: "SocketError=" + socket.SocketErrorCode + ": " + socket.Message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatiert ein geheimnisfreies Socket-Ziel fuer Checkmk und Dateilog.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">Zu formatierender Katalogeintrag.</param>
|
||||
/// <returns>Artefakt, Host, Port und Protokoll ohne URI-Geheimnisse.</returns>
|
||||
internal static string Display(EndpointCatalogEntry endpoint)
|
||||
{
|
||||
var artifact = string.IsNullOrWhiteSpace(endpoint.ApplicationName)
|
||||
|| string.Equals(endpoint.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)
|
||||
? endpoint.ArtifactName
|
||||
: endpoint.ApplicationName + "\\" + endpoint.ArtifactName;
|
||||
return endpoint.ArtifactType + ":" + artifact
|
||||
+ "[" + endpoint.TransportRole + "]->"
|
||||
+ endpoint.Host + ":" + endpoint.Port + "/" + endpoint.Protocol.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private sealed class NetworkOutcome
|
||||
{
|
||||
public bool Available { get; private set; }
|
||||
public long DurationMilliseconds { get; private set; }
|
||||
public string Failure { get; private set; }
|
||||
|
||||
public static NetworkOutcome Success(long durationMilliseconds)
|
||||
{
|
||||
return new NetworkOutcome { Available = true, DurationMilliseconds = durationMilliseconds };
|
||||
}
|
||||
|
||||
public static NetworkOutcome Failed(long durationMilliseconds, string failure)
|
||||
{
|
||||
return new NetworkOutcome
|
||||
{
|
||||
Available = false,
|
||||
DurationMilliseconds = durationMilliseconds,
|
||||
Failure = failure
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,26 @@ using System.Linq;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Zaehlt BizTalk-bezogene Fehler und Warnungen im lokalen Application Event Log.
|
||||
/// </summary>
|
||||
internal sealed class EventLogProbe
|
||||
{
|
||||
private readonly MonitoringOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert den Event-Log-Probe mit Zeitraum, Quellen und Schwellwerten.
|
||||
/// </summary>
|
||||
/// <param name="options">Monitoring-Konfiguration.</param>
|
||||
public EventLogProbe(MonitoringOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest das Application Event Log rueckwaerts bis zum Beginn des Zeitfensters.
|
||||
/// </summary>
|
||||
/// <param name="result">Ergebnisobjekt fuer Zaehler und Fehlerstatus.</param>
|
||||
public void Query(ProbeResult result)
|
||||
{
|
||||
result.EventLog.Since = DateTime.Now.AddMinutes(-_options.EventLogLookbackMinutes);
|
||||
@@ -55,6 +66,11 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prueft, ob eine Event-Log-Quelle zu den konfigurierten BizTalk-Quellen gehoert.
|
||||
/// </summary>
|
||||
/// <param name="source">Name der Windows Event-Log-Quelle.</param>
|
||||
/// <returns><c>true</c>, wenn die Quelle ueberwacht werden soll.</returns>
|
||||
private bool IsRelevantSource(string source)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
@@ -66,4 +82,3 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ using System.Collections.Generic;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Definiert die von Checkmk erwarteten numerischen Service-Zustaende.
|
||||
/// </summary>
|
||||
internal enum CheckState
|
||||
{
|
||||
Ok = 0,
|
||||
@@ -11,43 +14,151 @@ namespace BizTalkCheckmkPulse
|
||||
Unknown = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unterscheidet fortsetzbare und nicht fortsetzbare suspendierte Instanzen.
|
||||
/// </summary>
|
||||
internal enum SuspendedKind
|
||||
{
|
||||
Resumable,
|
||||
NonResumable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kennzeichnet den technischen Bereich, in dem eine Diagnose entstanden ist.
|
||||
/// </summary>
|
||||
internal enum DiagnosticArea
|
||||
{
|
||||
Wmi,
|
||||
Sql,
|
||||
EventLog,
|
||||
General
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Klassifiziert Fehler, damit Bediener eine passende Massnahme erhalten.
|
||||
/// </summary>
|
||||
internal enum DiagnosticCategory
|
||||
{
|
||||
Permission,
|
||||
Connectivity,
|
||||
Timeout,
|
||||
Configuration,
|
||||
Schema,
|
||||
Provider,
|
||||
Unexpected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strukturierte Diagnose mit Ursache und konkreter Handlungsempfehlung.
|
||||
/// </summary>
|
||||
internal sealed class ProbeDiagnostic
|
||||
{
|
||||
public DiagnosticArea Area { get; set; }
|
||||
public DiagnosticCategory Category { get; set; }
|
||||
public string Component { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public string Summary { get; set; }
|
||||
public string Action { get; set; }
|
||||
public string TechnicalDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt eine kompakte, fuer Checkmk geeignete Diagnosezeile.
|
||||
/// </summary>
|
||||
/// <returns>Menschenlesbarer Text mit Kategorie, Ursache und Massnahme.</returns>
|
||||
public string ToDisplayText()
|
||||
{
|
||||
var text = Area + "/" + Category + " [" + (Component ?? "unknown") + "]: " + (Summary ?? "Unbekannter Fehler.");
|
||||
if (!string.IsNullOrWhiteSpace(Action))
|
||||
{
|
||||
text += " Massnahme: " + Action;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(TechnicalDetails))
|
||||
{
|
||||
text += " Technik: " + TechnicalDetails;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sammelt alle Ergebnisse eines Programmlaufs.
|
||||
/// </summary>
|
||||
internal sealed class ProbeResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialisiert leere Ergebnislisten und Zustandsobjekte.
|
||||
/// </summary>
|
||||
public ProbeResult()
|
||||
{
|
||||
Diagnostics = new List<string>();
|
||||
Diagnostics = new List<ProbeDiagnostic>();
|
||||
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();
|
||||
EndpointCandidates = new List<EndpointCandidate>();
|
||||
EndpointConnectivity = new EndpointConnectivityState();
|
||||
}
|
||||
|
||||
public List<string> Diagnostics { get; private set; }
|
||||
public List<ProbeDiagnostic> Diagnostics { get; private set; }
|
||||
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; }
|
||||
public string NetworkIdentityHint { get; set; }
|
||||
public EventLogState EventLog { get; private set; }
|
||||
public List<EndpointCandidate> EndpointCandidates { get; private set; }
|
||||
public EndpointConnectivityState EndpointConnectivity { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Beschreibt Erreichbarkeit und Teilverfuegbarkeit der BizTalk-WMI-Daten.
|
||||
/// </summary>
|
||||
internal sealed class PlatformState
|
||||
{
|
||||
public bool WmiConnected { get; set; }
|
||||
public bool PlatformDataAvailable { get; set; }
|
||||
public bool HostInstancesDataAvailable { get; set; }
|
||||
public bool SuspendedInstancesDataAvailable { 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; }
|
||||
public string MessageBoxDbName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ergebnis der integrierten Anmeldung an einer BizTalk-Datenbank.
|
||||
/// </summary>
|
||||
internal sealed class SqlAccessState
|
||||
{
|
||||
public string Role { get; set; }
|
||||
public string Server { get; set; }
|
||||
public string Database { get; set; }
|
||||
public bool Attempted { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public DiagnosticCategory? FailureCategory { get; set; }
|
||||
public string Failure { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Laufzeitzustand einer BizTalk Host Instance.
|
||||
/// </summary>
|
||||
internal sealed class HostInstanceState
|
||||
{
|
||||
public string InstanceName { get; set; }
|
||||
@@ -56,6 +167,9 @@ namespace BizTalkCheckmkPulse
|
||||
public int ServiceState { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Relevante Daten einer suspendierten BizTalk Service Instance.
|
||||
/// </summary>
|
||||
internal sealed class SuspendedInstance
|
||||
{
|
||||
public string ApplicationName { get; set; }
|
||||
@@ -65,9 +179,38 @@ 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>
|
||||
/// Aggregierte Zustandszaehler einer BizTalk-Anwendung.
|
||||
/// </summary>
|
||||
internal sealed class ApplicationRuntimeState
|
||||
{
|
||||
public string ApplicationName { get; set; }
|
||||
@@ -86,6 +229,9 @@ namespace BizTalkCheckmkPulse
|
||||
public int OrchestrationUnknown { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ergebnis der Auswertung des lokalen Windows Application Event Logs.
|
||||
/// </summary>
|
||||
internal sealed class EventLogState
|
||||
{
|
||||
public bool Available { get; set; }
|
||||
@@ -94,5 +240,163 @@ namespace BizTalkCheckmkPulse
|
||||
public DateTime Since { get; set; }
|
||||
public string Failure { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rohdaten eines BizTalk-Transports aus einer bereits ausgefuehrten Runtime-Abfrage.
|
||||
/// Vollstaendige URIs werden nur im Speicher gehalten und weder persistiert noch ausgegeben.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCandidate
|
||||
{
|
||||
public string ArtifactType { get; set; }
|
||||
public string ApplicationName { get; set; }
|
||||
public string ArtifactName { get; set; }
|
||||
public string TransportRole { get; set; }
|
||||
public string AdapterName { get; set; }
|
||||
public string Address { get; set; }
|
||||
public bool Active { get; set; }
|
||||
public bool Dynamic { get; set; }
|
||||
|
||||
public string Key
|
||||
{
|
||||
get
|
||||
{
|
||||
return EndpointCatalogEntry.BuildKey(
|
||||
ArtifactType,
|
||||
ApplicationName,
|
||||
ArtifactName,
|
||||
TransportRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ergebnis der geheimnisfreien Auswertung einer BizTalk-Transportadresse.
|
||||
/// </summary>
|
||||
internal enum EndpointResolutionStatus
|
||||
{
|
||||
Probeable,
|
||||
ExpectedNonProbeable,
|
||||
Unresolved
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Klassifiziert einen Kandidaten und enthaelt nur bei sicherer Aufloesung ein Socket-Ziel.
|
||||
/// </summary>
|
||||
internal sealed class EndpointResolution
|
||||
{
|
||||
public EndpointResolutionStatus Status { get; set; }
|
||||
public EndpointCatalogEntry Entry { get; set; }
|
||||
public string Reason { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ein geheimnisfreier, lokal persistierbarer Netzwerk-Endpunkt.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCatalogEntry
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string ArtifactType { get; set; }
|
||||
public string ApplicationName { get; set; }
|
||||
public string ArtifactName { get; set; }
|
||||
public string TransportRole { get; set; }
|
||||
public string AdapterName { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public bool AutoDiscovered { get; set; }
|
||||
/// <summary>
|
||||
/// Gibt an, dass ein automatisch abgeleiteter Adaptertest nur Diagnosewert besitzt
|
||||
/// und ein Fehlschlag deshalb keinen Checkmk-Alarm ausloest.
|
||||
/// </summary>
|
||||
public bool BestEffort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt den stabilen, adressunabhaengigen Schluessel eines BizTalk-Transports.
|
||||
/// </summary>
|
||||
/// <param name="artifactType">Typ des BizTalk-Artefakts.</param>
|
||||
/// <param name="applicationName">Name der BizTalk-Anwendung.</param>
|
||||
/// <param name="artifactName">Name des Send Ports oder der Receive Location.</param>
|
||||
/// <param name="transportRole">Primaer-, Sekundaer- oder Inbound-Rolle.</param>
|
||||
/// <returns>Normalisierter Katalogschluessel.</returns>
|
||||
public static string BuildKey(string artifactType, string applicationName, string artifactName, string transportRole)
|
||||
{
|
||||
return NormalizeKeyPart(artifactType)
|
||||
+ "|" + NormalizeKeyPart(applicationName)
|
||||
+ "|" + NormalizeKeyPart(artifactName)
|
||||
+ "|" + NormalizeKeyPart(transportRole);
|
||||
}
|
||||
|
||||
private static string NormalizeKeyPart(string value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim().ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Versionierter Inhalt der automatisch gepflegten lokalen Endpoint-Konfiguration.
|
||||
/// </summary>
|
||||
internal sealed class EndpointCatalog
|
||||
{
|
||||
public EndpointCatalog()
|
||||
{
|
||||
Entries = new List<EndpointCatalogEntry>();
|
||||
}
|
||||
|
||||
public DateTime SynchronizedUtc { get; set; }
|
||||
public string MachineName { get; set; }
|
||||
public string EnvironmentName { get; set; }
|
||||
public int ActiveCandidates { get; set; }
|
||||
public int UnresolvedCandidates { get; set; }
|
||||
public List<EndpointCatalogEntry> Entries { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ergebnis einer einzelnen TCP- beziehungsweise UDP-Netzwerkprobe.
|
||||
/// </summary>
|
||||
internal sealed class EndpointProbeResult
|
||||
{
|
||||
public EndpointCatalogEntry Endpoint { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public long DurationMilliseconds { get; set; }
|
||||
public string Failure { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregierter Zustand des Endpoint-Katalogs und aller aktiven Netzwerkprobes.
|
||||
/// </summary>
|
||||
internal sealed class EndpointConnectivityState
|
||||
{
|
||||
public EndpointConnectivityState()
|
||||
{
|
||||
Results = new List<EndpointProbeResult>();
|
||||
ResolutionIssues = new List<string>();
|
||||
}
|
||||
|
||||
public bool Disabled { get; set; }
|
||||
public bool CatalogAvailable { get; set; }
|
||||
public bool RuntimeStateAvailable { get; set; }
|
||||
public bool RefreshRequired { get; set; }
|
||||
public bool RefreshSucceeded { get; set; }
|
||||
public DateTime? CatalogSynchronizedUtc { get; set; }
|
||||
public int Configured { get; set; }
|
||||
public int Active { get; set; }
|
||||
public int SkippedInactive { get; set; }
|
||||
public int UnresolvedActive { get; set; }
|
||||
public int ExpectedNonProbeableActive { get; set; }
|
||||
/// <summary>
|
||||
/// Anzahl der automatisch abgeleiteten, nicht alarmierenden Adapterziele.
|
||||
/// </summary>
|
||||
public int BestEffortActive { get; set; }
|
||||
/// <summary>
|
||||
/// Anzahl fehlgeschlagener Best-Effort-Probes im aktuellen Lauf.
|
||||
/// </summary>
|
||||
public int BestEffortIgnoredFailures { get; set; }
|
||||
public int ManualOverridesActive { get; set; }
|
||||
public int UniqueTargets { get; set; }
|
||||
public long ProbeDurationMilliseconds { get; set; }
|
||||
public string Failure { get; set; }
|
||||
public List<EndpointProbeResult> Results { get; private set; }
|
||||
public List<string> ResolutionIssues { get; private set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,54 +2,122 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Enthaelt alle konfigurierbaren Schwellwerte und Probe-Optionen.
|
||||
/// </summary>
|
||||
internal sealed class MonitoringOptions
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public string ServicePrefix { get; set; }
|
||||
public string EnvironmentName { get; set; }
|
||||
public bool IncludeEnvironmentInServiceName { get; set; }
|
||||
public int QueryTimeoutSeconds { get; set; }
|
||||
public bool ProbeSqlConnectivity { get; set; }
|
||||
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 ProbeEndpointConnectivity { get; set; }
|
||||
public string EndpointCatalogPath { get; set; }
|
||||
public int EndpointCatalogMaxBytes { get; set; }
|
||||
public int EndpointCatalogMaxEntries { get; set; }
|
||||
public int EndpointDiscoveryIntervalHours { get; set; }
|
||||
public int EndpointProbeTimeoutMilliseconds { get; set; }
|
||||
public int EndpointProbeMaxConcurrency { get; set; }
|
||||
public int EndpointMaxCount { get; set; }
|
||||
public bool Collect { get; set; }
|
||||
public bool SelfTest { get; set; }
|
||||
public bool ForceEndpointCatalogRefresh { get; set; }
|
||||
public bool RuntimeValidation { get; set; }
|
||||
public DateTime? RuntimeValidationNotBeforeUtc { get; set; }
|
||||
public string ExpectedRuntimeIdentity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Setzt konservative Standardwerte fuer BizTalk Server 2020.
|
||||
/// </summary>
|
||||
public MonitoringOptions()
|
||||
{
|
||||
Server = ".";
|
||||
ServicePrefix = "BizTalk";
|
||||
EnvironmentName = string.Empty;
|
||||
IncludeEnvironmentInServiceName = false;
|
||||
QueryTimeoutSeconds = 25;
|
||||
ProbeSqlConnectivity = true;
|
||||
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;
|
||||
ProbeEndpointConnectivity = true;
|
||||
EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml");
|
||||
EndpointCatalogMaxBytes = 1048576;
|
||||
EndpointCatalogMaxEntries = 1000;
|
||||
EndpointDiscoveryIntervalHours = 168;
|
||||
EndpointProbeTimeoutMilliseconds = 3000;
|
||||
EndpointProbeMaxConcurrency = 16;
|
||||
EndpointMaxCount = 100;
|
||||
ExpectedRuntimeIdentity = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erzeugt den stabilen Checkmk-Service-Namen. Der Umgebungsname ist nur explizit opt-in,
|
||||
/// damit ein Installer-Update bestehende Checkmk-Services nicht unbemerkt umbenennt.
|
||||
/// </summary>
|
||||
/// <param name="suffix">Fachlicher Suffix des Services.</param>
|
||||
/// <returns>Vollstaendiger Service-Name fuer Checkmk.</returns>
|
||||
public string ServiceName(string suffix)
|
||||
{
|
||||
var prefix = string.IsNullOrWhiteSpace(EnvironmentName)
|
||||
var prefix = !IncludeEnvironmentInServiceName || string.IsNullOrWhiteSpace(EnvironmentName)
|
||||
? ServicePrefix
|
||||
: ServicePrefix + " " + EnvironmentName.Trim();
|
||||
return prefix.Trim() + " " + suffix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Laedt Optionen aus App.config und ueberschreibt sie anschliessend mit Argumenten.
|
||||
/// </summary>
|
||||
/// <param name="args">Beim Programmstart uebergebene Kommandozeilenargumente.</param>
|
||||
/// <returns>Validierte Monitoring-Optionen.</returns>
|
||||
public static MonitoringOptions Load(string[] args)
|
||||
{
|
||||
var options = new MonitoringOptions();
|
||||
@@ -58,22 +126,55 @@ namespace BizTalkCheckmkPulse
|
||||
options.Server = ReadString(settings, "Server", options.Server);
|
||||
options.ServicePrefix = ReadString(settings, "ServicePrefix", options.ServicePrefix);
|
||||
options.EnvironmentName = ReadString(settings, "EnvironmentName", options.EnvironmentName);
|
||||
options.IncludeEnvironmentInServiceName = ReadBool(
|
||||
settings,
|
||||
"IncludeEnvironmentInServiceName",
|
||||
options.IncludeEnvironmentInServiceName);
|
||||
options.QueryTimeoutSeconds = ReadInt(settings, "QueryTimeoutSeconds", options.QueryTimeoutSeconds, 5, 120);
|
||||
options.ProbeSqlConnectivity = ReadBool(settings, "ProbeSqlConnectivity", options.ProbeSqlConnectivity);
|
||||
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);
|
||||
options.ProbeEndpointConnectivity = ReadBool(settings, "ProbeEndpointConnectivity", options.ProbeEndpointConnectivity);
|
||||
options.EndpointCatalogPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "EndpointCatalogPath", options.EndpointCatalogPath));
|
||||
options.EndpointCatalogMaxBytes = ReadInt(settings, "EndpointCatalogMaxBytes", options.EndpointCatalogMaxBytes, 4096, 16777216);
|
||||
options.EndpointCatalogMaxEntries = ReadInt(settings, "EndpointCatalogMaxEntries", options.EndpointCatalogMaxEntries, 1, 100000);
|
||||
options.EndpointDiscoveryIntervalHours = ReadInt(settings, "EndpointDiscoveryIntervalHours", options.EndpointDiscoveryIntervalHours, 1, 8760);
|
||||
options.EndpointProbeTimeoutMilliseconds = ReadInt(settings, "EndpointProbeTimeoutMilliseconds", options.EndpointProbeTimeoutMilliseconds, 250, 30000);
|
||||
options.EndpointProbeMaxConcurrency = ReadInt(settings, "EndpointProbeMaxConcurrency", options.EndpointProbeMaxConcurrency, 1, 64);
|
||||
options.EndpointMaxCount = ReadInt(settings, "EndpointMaxCount", options.EndpointMaxCount, 1, 5000);
|
||||
|
||||
ApplyArguments(options, args ?? new string[0]);
|
||||
if (!options.SelfTest)
|
||||
{
|
||||
Validate(options);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wendet unterstuetzte Kommandozeilenargumente auf die Optionen an.
|
||||
/// </summary>
|
||||
/// <param name="options">Zu aktualisierende Optionen.</param>
|
||||
/// <param name="args">Kommandozeilenargumente.</param>
|
||||
private static void ApplyArguments(MonitoringOptions options, string[] args)
|
||||
{
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
@@ -83,6 +184,41 @@ namespace BizTalkCheckmkPulse
|
||||
{
|
||||
options.SelfTest = true;
|
||||
}
|
||||
else if (EqualsAny(arg, "--collect", "/collect"))
|
||||
{
|
||||
options.Collect = true;
|
||||
}
|
||||
else if (EqualsAny(arg, "--force-endpoint-refresh", "/force-endpoint-refresh"))
|
||||
{
|
||||
options.ForceEndpointCatalogRefresh = true;
|
||||
}
|
||||
else if (EqualsAny(arg, "--validate-runtime", "/validate-runtime"))
|
||||
{
|
||||
options.RuntimeValidation = true;
|
||||
}
|
||||
else if (EqualsAny(arg, "--validation-not-before-utc", "/validation-not-before-utc") && i + 1 < args.Length)
|
||||
{
|
||||
DateTime value;
|
||||
if (!DateTime.TryParseExact(
|
||||
args[++i],
|
||||
"o",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out value))
|
||||
{
|
||||
throw new ConfigurationErrorsException("Runtime validation timestamp must use the round-trip UTC format.");
|
||||
}
|
||||
options.RuntimeValidationNotBeforeUtc = value;
|
||||
}
|
||||
else if (EqualsAny(arg, "--expected-identity-base64", "/expected-identity-base64") && i + 1 < args.Length)
|
||||
{
|
||||
options.ExpectedRuntimeIdentity = new UTF8Encoding(false, true)
|
||||
.GetString(Convert.FromBase64String(args[++i]));
|
||||
}
|
||||
else if (EqualsAny(arg, "--consume", "/consume"))
|
||||
{
|
||||
options.Collect = false;
|
||||
}
|
||||
else if (EqualsAny(arg, "--server", "/server") && i + 1 < args.Length)
|
||||
{
|
||||
options.Server = args[++i];
|
||||
@@ -99,15 +235,76 @@ namespace BizTalkCheckmkPulse
|
||||
{
|
||||
options.AlertOnArtifactRuntimeIssues = true;
|
||||
}
|
||||
else if (EqualsAny(arg, "--no-sql-probe", "/no-sql-probe"))
|
||||
{
|
||||
options.ProbeSqlConnectivity = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verhindert unsichere oder mehrdeutige Laufzeitpfade.
|
||||
/// </summary>
|
||||
private static void Validate(MonitoringOptions options)
|
||||
{
|
||||
if (options.ForceEndpointCatalogRefresh && !options.Collect)
|
||||
{
|
||||
throw new ConfigurationErrorsException("ForceEndpointCatalogRefresh is valid only in provider mode.");
|
||||
}
|
||||
|
||||
if (options.RuntimeValidation
|
||||
&& (!options.RuntimeValidationNotBeforeUtc.HasValue
|
||||
|| string.IsNullOrWhiteSpace(options.ExpectedRuntimeIdentity)))
|
||||
{
|
||||
throw new ConfigurationErrorsException("Runtime validation requires a start timestamp and expected identity.");
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.EndpointCatalogPath) || !Path.IsPathRooted(options.EndpointCatalogPath))
|
||||
{
|
||||
throw new ConfigurationErrorsException("EndpointCatalogPath must be an absolute path.");
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
Path.GetFullPath(options.EndpointCatalogPath),
|
||||
Path.GetFullPath(options.SnapshotPath),
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ConfigurationErrorsException("EndpointCatalogPath and SnapshotPath must be different files.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest einen getrimmten Konfigurationswert.
|
||||
/// </summary>
|
||||
/// <param name="settings">AppSettings-Sammlung.</param>
|
||||
/// <param name="key">Name des Konfigurationswertes.</param>
|
||||
/// <param name="fallback">Ersatzwert bei fehlendem Eintrag.</param>
|
||||
/// <returns>Konfigurierter oder ersetzter Textwert.</returns>
|
||||
private static string ReadString(System.Collections.Specialized.NameValueCollection settings, string key, string fallback)
|
||||
{
|
||||
var value = settings[key];
|
||||
return value == null ? fallback : value.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest eine Ganzzahl und begrenzt sie auf den erlaubten Wertebereich.
|
||||
/// </summary>
|
||||
/// <param name="settings">AppSettings-Sammlung.</param>
|
||||
/// <param name="key">Name des Konfigurationswertes.</param>
|
||||
/// <param name="fallback">Ersatzwert bei ungueltiger Eingabe.</param>
|
||||
/// <param name="min">Kleinster erlaubter Wert.</param>
|
||||
/// <param name="max">Groesster erlaubter Wert.</param>
|
||||
/// <returns>Validierter Zahlenwert.</returns>
|
||||
private static int ReadInt(System.Collections.Specialized.NameValueCollection settings, string key, int fallback, int min, int max)
|
||||
{
|
||||
int parsed;
|
||||
@@ -124,12 +321,24 @@ namespace BizTalkCheckmkPulse
|
||||
return parsed > max ? max : parsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest einen booleschen Konfigurationswert.
|
||||
/// </summary>
|
||||
/// <param name="settings">AppSettings-Sammlung.</param>
|
||||
/// <param name="key">Name des Konfigurationswertes.</param>
|
||||
/// <param name="fallback">Ersatzwert bei ungueltiger Eingabe.</param>
|
||||
/// <returns>Konfigurierter oder ersetzter Wahrheitswert.</returns>
|
||||
private static bool ReadBool(System.Collections.Specialized.NameValueCollection settings, string key, bool fallback)
|
||||
{
|
||||
bool parsed;
|
||||
return bool.TryParse(settings[key], out parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zerlegt die konfigurierten Event-Log-Quellen und entfernt Duplikate.
|
||||
/// </summary>
|
||||
/// <param name="value">Mit Pipe, Semikolon oder Komma getrennte Quellen.</param>
|
||||
/// <returns>Eindeutige Liste relevanter Event-Log-Quellen.</returns>
|
||||
private static IReadOnlyList<string> ReadSources(string value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
@@ -140,10 +349,58 @@ 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>
|
||||
/// <param name="value">Zu pruefender Wert.</param>
|
||||
/// <param name="candidates">Erlaubte Vergleichswerte.</param>
|
||||
/// <returns><c>true</c>, wenn ein Kandidat passt.</returns>
|
||||
private static bool EqualsAny(string value, params string[] candidates)
|
||||
{
|
||||
return candidates.Any(x => string.Equals(value, x, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +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 fuer privilegierten Provider und unprivilegierten Checkmk-Consumer.
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 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>Consumer immer 0; Provider 0 bei Erfolg und ungleich 0 bei einem Laufzeitfehler.</returns>
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
MonitoringOptions options = null;
|
||||
@@ -26,35 +37,231 @@ namespace BizTalkCheckmkPulse
|
||||
return 0;
|
||||
}
|
||||
|
||||
var result = new ProbeResult();
|
||||
var wmiProbe = new WmiBizTalkProbe(options);
|
||||
wmiProbe.Query(result);
|
||||
|
||||
if (options.ProbeEventLog)
|
||||
if (options.RuntimeValidation)
|
||||
{
|
||||
var eventLogProbe = new EventLogProbe(options);
|
||||
eventLogProbe.Query(result);
|
||||
return RunRuntimeValidation(options, formatter);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
var strictFailureMode = options != null
|
||||
? options.Collect || options.RuntimeValidation
|
||||
: (args ?? new string[0]).Any(x =>
|
||||
string.Equals(x, "--collect", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(x, "/collect", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(x, "--validate-runtime", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(x, "/validate-runtime", StringComparison.OrdinalIgnoreCase));
|
||||
return strictFailureMode ? 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, logger);
|
||||
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)
|
||||
+ " endpoint_candidates_active="
|
||||
+ result.EndpointCandidates.Count(x => x.Active)
|
||||
+ " endpoints_active="
|
||||
+ result.EndpointConnectivity.Active
|
||||
+ " endpoints_failed="
|
||||
+ result.EndpointConnectivity.Results.Count(x => !x.Available && !x.Endpoint.BestEffort)
|
||||
+ " endpoints_best_effort_ignored="
|
||||
+ result.EndpointConnectivity.BestEffortIgnoredFailures
|
||||
+ " endpoints_unresolved="
|
||||
+ result.EndpointConnectivity.UnresolvedActive
|
||||
+ " endpoints_expected_non_socket="
|
||||
+ result.EndpointConnectivity.ExpectedNonProbeableActive
|
||||
+ " endpoints_manual_overrides="
|
||||
+ result.EndpointConnectivity.ManualOverridesActive
|
||||
+ " endpoints_unique_targets="
|
||||
+ result.EndpointConnectivity.UniqueTargets
|
||||
+ " endpoint_probe_ms="
|
||||
+ result.EndpointConnectivity.ProbeDurationMilliseconds
|
||||
+ " 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 int RunRuntimeValidation(MonitoringOptions options, CheckmkLocalFormatter formatter)
|
||||
{
|
||||
var validation = RuntimeValidator.Validate(
|
||||
options,
|
||||
formatter,
|
||||
options.RuntimeValidationNotBeforeUtc.Value,
|
||||
options.ExpectedRuntimeIdentity,
|
||||
DateTime.UtcNow);
|
||||
if (!validation.IsSuccess)
|
||||
{
|
||||
Console.Error.WriteLine("Runtime validation failed: " + validation.Error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"RUNTIME_VALIDATION_V1 generatedUtc=" + validation.GeneratedUtc.ToString("o")
|
||||
+ " catalogUtc=" + (validation.CatalogSynchronizedUtc.HasValue
|
||||
? validation.CatalogSynchronizedUtc.Value.ToString("o")
|
||||
: "disabled")
|
||||
+ " stableServices=" + validation.StableServiceCount
|
||||
+ " acceptedUnknown=" + validation.AcceptedUnknownLines.Count);
|
||||
foreach (var line in validation.AcceptedUnknownLines)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"RUNTIME_VALIDATION_WARNING: Installation technisch valide; Monitoringzustand bleibt sichtbar: "
|
||||
+ line);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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, FileLogger logger)
|
||||
{
|
||||
var result = new ProbeResult();
|
||||
new WmiBizTalkProbe(options).Query(result);
|
||||
new SqlConnectivityProbe(options).Query(result);
|
||||
new EndpointConnectivityProbe(options, logger).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.2.7.0")]
|
||||
[assembly: AssemblyFileVersion("2.2.7.0")]
|
||||
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Validiert die vom ersten Providerlauf erzeugten Runtime-Artefakte mit denselben
|
||||
/// Parsern und Grenzen, die auch der laufende Consumer verwendet.
|
||||
/// </summary>
|
||||
internal static class RuntimeValidator
|
||||
{
|
||||
public static RuntimeValidationResult Validate(
|
||||
MonitoringOptions options,
|
||||
CheckmkLocalFormatter formatter,
|
||||
DateTime notBeforeUtc,
|
||||
string expectedIdentity,
|
||||
DateTime utcNow)
|
||||
{
|
||||
if (options == null) throw new ArgumentNullException("options");
|
||||
if (formatter == null) throw new ArgumentNullException("formatter");
|
||||
|
||||
var snapshot = new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
|
||||
.Read(utcNow, TimeSpan.FromSeconds(options.SnapshotMaxAgeSeconds));
|
||||
if (!snapshot.IsSuccess)
|
||||
{
|
||||
return RuntimeValidationResult.Failed("Snapshot ist ungueltig: " + snapshot.Error);
|
||||
}
|
||||
|
||||
var boundary = notBeforeUtc.ToUniversalTime();
|
||||
if (snapshot.GeneratedUtc < boundary)
|
||||
{
|
||||
return RuntimeValidationResult.Failed(
|
||||
"Snapshot stammt nicht aus dem gestarteten Providerlauf. generatedUtc="
|
||||
+ snapshot.GeneratedUtc.ToString("o") + ", requiredUtc=" + boundary.ToString("o") + ".");
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.Identity, expectedIdentity, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RuntimeValidationResult.Failed(
|
||||
"Snapshot wurde von einer unerwarteten Identitaet erzeugt. expected="
|
||||
+ expectedIdentity + ", actual=" + (snapshot.Identity ?? "(leer)") + ".");
|
||||
}
|
||||
|
||||
string serviceError;
|
||||
string[] unknownServiceLines;
|
||||
var expectedServices = formatter.FormatSelfTest().Select(ExtractServiceName).ToArray();
|
||||
if (!ContainsExactStableContract(
|
||||
snapshot.Lines,
|
||||
expectedServices,
|
||||
out unknownServiceLines,
|
||||
out serviceError))
|
||||
{
|
||||
return RuntimeValidationResult.Failed(serviceError);
|
||||
}
|
||||
|
||||
var acceptedEndpointUnknownName = options.ServiceName("Endpoint Reachability");
|
||||
var blockingUnknown = unknownServiceLines
|
||||
.Where(x => !options.ProbeEndpointConnectivity
|
||||
|| !string.Equals(
|
||||
ExtractServiceName(x),
|
||||
acceptedEndpointUnknownName,
|
||||
StringComparison.Ordinal))
|
||||
.Select(ExtractServiceName)
|
||||
.OrderBy(x => x, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (blockingUnknown.Length != 0)
|
||||
{
|
||||
return RuntimeValidationResult.Failed(
|
||||
"Snapshot enthaelt blockierende UNKNOWN-Zustaende in Kernservices. UNKNOWN=["
|
||||
+ string.Join(", ", blockingUnknown) + "].");
|
||||
}
|
||||
|
||||
DateTime? catalogUtc = null;
|
||||
if (options.ProbeEndpointConnectivity)
|
||||
{
|
||||
EndpointCatalog catalog;
|
||||
try
|
||||
{
|
||||
catalog = new EndpointCatalogStore(
|
||||
options.EndpointCatalogPath,
|
||||
options.EndpointCatalogMaxBytes,
|
||||
options.EndpointCatalogMaxEntries)
|
||||
.Read(options.EnvironmentName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RuntimeValidationResult.Failed(
|
||||
"Endpoint-Katalog ist ungueltig: " + ex.GetType().Name + ": " + ex.Message);
|
||||
}
|
||||
|
||||
if (catalog.SynchronizedUtc < boundary)
|
||||
{
|
||||
return RuntimeValidationResult.Failed(
|
||||
"Endpoint-Katalog wurde beim Installationslauf nicht aktualisiert. synchronizedUtc="
|
||||
+ catalog.SynchronizedUtc.ToString("o") + ", requiredUtc=" + boundary.ToString("o") + ".");
|
||||
}
|
||||
|
||||
if (catalog.SynchronizedUtc > utcNow.ToUniversalTime().AddMinutes(5))
|
||||
{
|
||||
return RuntimeValidationResult.Failed("Endpoint-Katalog-Zeitstempel liegt unplausibel in der Zukunft.");
|
||||
}
|
||||
|
||||
catalogUtc = catalog.SynchronizedUtc;
|
||||
}
|
||||
|
||||
return RuntimeValidationResult.Success(
|
||||
snapshot.GeneratedUtc,
|
||||
catalogUtc,
|
||||
expectedServices.Length,
|
||||
unknownServiceLines);
|
||||
}
|
||||
|
||||
internal static bool ContainsExactStableContract(
|
||||
IEnumerable<string> lines,
|
||||
IEnumerable<string> expectedServices,
|
||||
out string[] unknownServiceLines,
|
||||
out string error)
|
||||
{
|
||||
unknownServiceLines = new string[0];
|
||||
string[] actual;
|
||||
try
|
||||
{
|
||||
actual = (lines ?? Enumerable.Empty<string>()).Select(ExtractServiceName).ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = "Snapshot-Servicevertrag kann nicht gelesen werden: " + ex.Message;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actual.Distinct(StringComparer.Ordinal).Count() != actual.Length)
|
||||
{
|
||||
error = "Snapshot enthaelt doppelte Checkmk-Servicenamen.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var actualSet = new HashSet<string>(actual, StringComparer.Ordinal);
|
||||
var expected = (expectedServices ?? Enumerable.Empty<string>()).ToArray();
|
||||
var missing = expected
|
||||
.Where(x => !actualSet.Contains(x))
|
||||
.OrderBy(x => x, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (missing.Length != 0)
|
||||
{
|
||||
error = "Snapshot enthaelt nicht alle stabilen Checkmk-Services. Fehlend=["
|
||||
+ string.Join(", ", missing) + "].";
|
||||
return false;
|
||||
}
|
||||
|
||||
unknownServiceLines = (lines ?? Enumerable.Empty<string>())
|
||||
.Where(x => x != null && x.StartsWith("3 ", StringComparison.Ordinal))
|
||||
.Where(x => expected.Contains(ExtractServiceName(x), StringComparer.Ordinal))
|
||||
.OrderBy(ExtractServiceName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ExtractServiceName(string line)
|
||||
{
|
||||
var firstQuote = (line ?? string.Empty).IndexOf('"');
|
||||
var secondQuote = firstQuote < 0 ? -1 : line.IndexOf('"', firstQuote + 1);
|
||||
if (firstQuote < 0 || secondQuote <= firstQuote + 1)
|
||||
throw new InvalidOperationException("Ungueltige Local-Check-Zeile.");
|
||||
return line.Substring(firstQuote + 1, secondQuote - firstQuote - 1);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RuntimeValidationResult
|
||||
{
|
||||
private RuntimeValidationResult() { }
|
||||
|
||||
public bool IsSuccess { get; private set; }
|
||||
public string Error { get; private set; }
|
||||
public DateTime GeneratedUtc { get; private set; }
|
||||
public DateTime? CatalogSynchronizedUtc { get; private set; }
|
||||
public int StableServiceCount { get; private set; }
|
||||
public IReadOnlyList<string> AcceptedUnknownLines { get; private set; }
|
||||
|
||||
public static RuntimeValidationResult Success(
|
||||
DateTime generatedUtc,
|
||||
DateTime? catalogUtc,
|
||||
int serviceCount,
|
||||
IEnumerable<string> acceptedUnknownLines)
|
||||
{
|
||||
return new RuntimeValidationResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
GeneratedUtc = generatedUtc,
|
||||
CatalogSynchronizedUtc = catalogUtc,
|
||||
StableServiceCount = serviceCount,
|
||||
AcceptedUnknownLines = (acceptedUnknownLines ?? Enumerable.Empty<string>()).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public static RuntimeValidationResult Failed(string error)
|
||||
{
|
||||
return new RuntimeValidationResult
|
||||
{
|
||||
Error = error,
|
||||
AcceptedUnknownLines = new string[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
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=");
|
||||
var identity = 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, identity);
|
||||
}
|
||||
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 < 9)
|
||||
{
|
||||
throw new InvalidDataException("Snapshot must contain all nine 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 string Identity { get; private set; }
|
||||
|
||||
public static SnapshotReadResult Success(IReadOnlyList<string> lines, DateTime generatedUtc, string identity)
|
||||
{
|
||||
return new SnapshotReadResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
Lines = lines,
|
||||
GeneratedUtc = generatedUtc,
|
||||
Identity = identity
|
||||
};
|
||||
}
|
||||
|
||||
public static SnapshotReadResult Failed(string error)
|
||||
{
|
||||
return new SnapshotReadResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Prueft mit der aktuellen Windows-Identitaet die Anmeldung an den von WMI
|
||||
/// ermittelten BizTalk Management- und MessageBox-Datenbanken.
|
||||
/// </summary>
|
||||
internal sealed class SqlConnectivityProbe
|
||||
{
|
||||
private readonly MonitoringOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert die SQL-Diagnose mit den Monitoring-Optionen.
|
||||
/// </summary>
|
||||
/// <param name="options">Konfiguration fuer Aktivierung und Timeout.</param>
|
||||
public SqlConnectivityProbe(MonitoringOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ermittelt die aktuelle Identitaet und prueft alle bekannten SQL-Ziele.
|
||||
/// </summary>
|
||||
/// <param name="result">Ergebnisobjekt, das um SQL-Zustaende und Diagnosen erweitert wird.</param>
|
||||
public void Query(ProbeResult result)
|
||||
{
|
||||
CaptureIdentity(result);
|
||||
if (!_options.ProbeSqlConnectivity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddTarget(result, "Management", result.Platform.ManagementDbServer, result.Platform.ManagementDbName);
|
||||
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 = wmiPermissionFailure
|
||||
? DiagnosticCategory.Permission
|
||||
: DiagnosticCategory.Configuration,
|
||||
Component = "BizTalk database discovery",
|
||||
Required = true,
|
||||
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."
|
||||
});
|
||||
}
|
||||
|
||||
if (result.SqlTargets.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var target in result.SqlTargets)
|
||||
{
|
||||
ProbeTarget(result, target);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Speichert die lokale Prozessidentitaet und einen Hinweis auf die Netzwerkidentitaet.
|
||||
/// </summary>
|
||||
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
|
||||
private static void CaptureIdentity(ProbeResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
result.ExecutionIdentity = identity == null ? "unknown" : identity.Name;
|
||||
var isLocalSystem = identity != null
|
||||
&& identity.User != null
|
||||
&& identity.User.IsWellKnown(WellKnownSidType.LocalSystemSid);
|
||||
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)
|
||||
{
|
||||
result.ExecutionIdentity = "unknown";
|
||||
result.NetworkIdentityHint = "unknown";
|
||||
result.Diagnostics.Add(new ProbeDiagnostic
|
||||
{
|
||||
Area = DiagnosticArea.General,
|
||||
Category = DiagnosticCategory.Unexpected,
|
||||
Component = "Windows identity",
|
||||
Required = false,
|
||||
Summary = "Die Windows-Ausfuehrungsidentitaet konnte nicht bestimmt werden.",
|
||||
Action = "Konto des Scheduled Tasks 'BizTalk Checkmk Pulse Provider' pruefen.",
|
||||
TechnicalDetails = ex.GetType().Name + ": " + ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fuegt ein eindeutiges und vollstaendig beschriebenes SQL-Ziel hinzu.
|
||||
/// </summary>
|
||||
/// <param name="result">Ergebnisobjekt mit der SQL-Zielliste.</param>
|
||||
/// <param name="role">Fachliche Rolle der Datenbank.</param>
|
||||
/// <param name="server">SQL-Server oder Instanzname.</param>
|
||||
/// <param name="database">Name der BizTalk-Datenbank.</param>
|
||||
private static void AddTarget(ProbeResult result, string role, string server, string database)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(server) || string.IsNullOrWhiteSpace(database))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = result.SqlTargets.FirstOrDefault(x =>
|
||||
string.Equals(x.Server, server, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(x.Database, database, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Role += "+" + role;
|
||||
return;
|
||||
}
|
||||
|
||||
result.SqlTargets.Add(new SqlAccessState
|
||||
{
|
||||
Role = role,
|
||||
Server = server.Trim(),
|
||||
Database = database.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Oeffnet eine integrierte SQL-Verbindung und fuehrt eine harmlose Testabfrage aus.
|
||||
/// </summary>
|
||||
/// <param name="result">Ergebnisobjekt fuer eventuell entstehende Diagnosen.</param>
|
||||
/// <param name="target">Zu pruefendes SQL-Ziel.</param>
|
||||
private void ProbeTarget(ProbeResult result, SqlAccessState target)
|
||||
{
|
||||
target.Attempted = true;
|
||||
try
|
||||
{
|
||||
var builder = new SqlConnectionStringBuilder
|
||||
{
|
||||
DataSource = target.Server,
|
||||
InitialCatalog = target.Database,
|
||||
IntegratedSecurity = true,
|
||||
ConnectTimeout = _options.SqlConnectionTimeoutSeconds,
|
||||
ApplicationName = "BizTalkCheckmkPulse",
|
||||
Pooling = false,
|
||||
PersistSecurityInfo = false
|
||||
};
|
||||
|
||||
using (var connection = new SqlConnection(builder.ConnectionString))
|
||||
using (var command = new SqlCommand("SELECT 1", connection))
|
||||
{
|
||||
command.CommandTimeout = _options.SqlConnectionTimeoutSeconds;
|
||||
connection.Open();
|
||||
command.ExecuteScalar();
|
||||
}
|
||||
|
||||
target.Available = true;
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
var category = ClassifySqlException(ex);
|
||||
target.Available = false;
|
||||
target.FailureCategory = category;
|
||||
target.Failure = "SQL error " + ex.Number + ": " + ex.Message;
|
||||
result.Diagnostics.Add(new ProbeDiagnostic
|
||||
{
|
||||
Area = DiagnosticArea.Sql,
|
||||
Category = category,
|
||||
Component = target.Role + " " + target.Server + "\\" + target.Database,
|
||||
Required = true,
|
||||
Summary = BuildSqlSummary(category, result.NetworkIdentityHint),
|
||||
Action = BuildSqlAction(category, result.NetworkIdentityHint, target),
|
||||
TechnicalDetails = "SqlException Number=" + ex.Number + ", State=" + ex.State + ", Class=" + ex.Class + ": " + ex.Message
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
target.Available = false;
|
||||
target.FailureCategory = DiagnosticCategory.Unexpected;
|
||||
target.Failure = ex.GetType().Name + ": " + ex.Message;
|
||||
result.Diagnostics.Add(new ProbeDiagnostic
|
||||
{
|
||||
Area = DiagnosticArea.Sql,
|
||||
Category = DiagnosticCategory.Unexpected,
|
||||
Component = target.Role + " " + target.Server + "\\" + target.Database,
|
||||
Required = true,
|
||||
Summary = "Unerwarteter Fehler beim SQL-Zugriff.",
|
||||
Action = "Technische Details und Windows-/SQL-Event-Logs pruefen. Danach den Test im Agent-Dump wiederholen.",
|
||||
TechnicalDetails = ex.GetType().Name + ": " + ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ordnet bekannte SQL-Fehlernummern einer handlungsorientierten Kategorie zu.
|
||||
/// </summary>
|
||||
/// <param name="exception">Von System.Data.SqlClient gelieferter Fehler.</param>
|
||||
/// <returns>Diagnosekategorie fuer Berechtigung, Timeout oder Verbindung.</returns>
|
||||
private static DiagnosticCategory ClassifySqlException(SqlException exception)
|
||||
{
|
||||
var permissionNumbers = new[] { 229, 297, 4060, 916, 18452, 18456 };
|
||||
var connectivityNumbers = new[] { 2, 53, 64, 121, 233, 258, 10060, 10061, 11001 };
|
||||
if (exception.Errors.Cast<SqlError>().Any(x => permissionNumbers.Contains(x.Number)))
|
||||
{
|
||||
return DiagnosticCategory.Permission;
|
||||
}
|
||||
|
||||
var message = exception.Message ?? string.Empty;
|
||||
if (ContainsAny(message, "certificate", "SSL Provider", "TLS", "SSPI context", "target principal name"))
|
||||
{
|
||||
return DiagnosticCategory.Configuration;
|
||||
}
|
||||
|
||||
if (exception.Number == -2)
|
||||
{
|
||||
return DiagnosticCategory.Timeout;
|
||||
}
|
||||
|
||||
if (exception.Errors.Cast<SqlError>().Any(x => connectivityNumbers.Contains(x.Number)))
|
||||
{
|
||||
return DiagnosticCategory.Connectivity;
|
||||
}
|
||||
|
||||
if (ContainsAny(message, "login failed", "cannot open database", "permission", "denied", "not authorized"))
|
||||
{
|
||||
return DiagnosticCategory.Permission;
|
||||
}
|
||||
|
||||
return DiagnosticCategory.Provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt eine kurze Fehlerursache fuer die Checkmk-Serviceausgabe.
|
||||
/// </summary>
|
||||
/// <param name="category">Klassifizierte SQL-Fehlerart.</param>
|
||||
/// <param name="networkIdentity">Vermutete Identitaet am entfernten SQL Server.</param>
|
||||
/// <returns>Menschenlesbare Fehlerzusammenfassung.</returns>
|
||||
private static string BuildSqlSummary(DiagnosticCategory category, string networkIdentity)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case DiagnosticCategory.Permission:
|
||||
return "SQL Server hat die integrierte Anmeldung abgelehnt. Erwartete Netzwerkidentitaet: " + networkIdentity + ".";
|
||||
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 Provider-Kontext nicht erreichbar.";
|
||||
case DiagnosticCategory.Configuration:
|
||||
return "SQL-Verbindung scheitert an TLS-, Zertifikats-, SPN- oder SSPI-Konfiguration.";
|
||||
default:
|
||||
return "SQL Server hat beim Verbindungstest einen Providerfehler geliefert.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liefert eine konkrete, zur SQL-Fehlerkategorie passende Handlungsempfehlung.
|
||||
/// </summary>
|
||||
/// <param name="category">Klassifizierte SQL-Fehlerart.</param>
|
||||
/// <param name="networkIdentity">Vermutete Identitaet am entfernten SQL Server.</param>
|
||||
/// <param name="target">Betroffenes SQL-Ziel.</param>
|
||||
/// <returns>Empfohlener Diagnoseschritt.</returns>
|
||||
private static string BuildSqlAction(DiagnosticCategory category, string networkIdentity, SqlAccessState target)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case DiagnosticCategory.Permission:
|
||||
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:
|
||||
return "Server-/Instanzname " + target.Server + ", DNS, SQL-Dienst, TCP-Protokoll und Firewall aus Sicht des BizTalk-Servers pruefen.";
|
||||
case DiagnosticCategory.Configuration:
|
||||
return "TLS-Zertifikatskette, SQL-Verschluesselung, SPN und Kerberos/SSPI gemeinsam mit SQL- und Windows-Administration pruefen; Verschluesselung nicht pauschal deaktivieren.";
|
||||
default:
|
||||
return "SQL-Fehlernummer mit der SQL-Administration pruefen und den Test gegen " + target.Server + "\\" + target.Database + " im gleichen Dienstkonto-Kontext wiederholen.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht mehrere Teiltexte ohne Beachtung der Gross-/Kleinschreibung.
|
||||
/// </summary>
|
||||
/// <param name="value">Zu untersuchender Text.</param>
|
||||
/// <param name="candidates">Gesuchte Teiltexte.</param>
|
||||
/// <returns><c>true</c>, wenn mindestens ein Teiltext enthalten ist.</returns>
|
||||
private static bool ContainsAny(string value, params string[] candidates)
|
||||
{
|
||||
return candidates.Any(x => value.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Liest BizTalk Server 2020 ueber den lokalen Microsoft-WMI-Provider aus.
|
||||
/// </summary>
|
||||
internal sealed class WmiBizTalkProbe
|
||||
{
|
||||
private const string NamespacePath = "root\\MicrosoftBizTalkServer";
|
||||
@@ -18,13 +23,26 @@ namespace BizTalkCheckmkPulse
|
||||
private const int OrchestrationBound = 2;
|
||||
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>
|
||||
/// Initialisiert den WMI-Probe mit den konfigurierten Timeouts.
|
||||
/// </summary>
|
||||
/// <param name="options">Monitoring-Konfiguration.</param>
|
||||
public WmiBizTalkProbe(MonitoringOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verbindet sich mit dem BizTalk-WMI-Namespace und liest alle Datenbereiche.
|
||||
/// </summary>
|
||||
/// <param name="result">Ergebnisobjekt fuer Messwerte, Verfuegbarkeiten und Diagnosen.</param>
|
||||
public void Query(ProbeResult result)
|
||||
{
|
||||
var server = string.IsNullOrWhiteSpace(_options.Server) || _options.Server == "."
|
||||
@@ -32,95 +50,238 @@ namespace BizTalkCheckmkPulse
|
||||
: _options.Server.Trim();
|
||||
|
||||
result.Platform.ServerName = server;
|
||||
|
||||
var scope = new ManagementScope("\\\\" + server + "\\" + NamespacePath);
|
||||
scope.Connect();
|
||||
if (!TryConnect(scope, result))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
result.Platform.WmiConnected = true;
|
||||
|
||||
QueryPlatform(scope, result);
|
||||
QueryHostInstances(scope, result);
|
||||
var applicationIndex = BuildApplicationIndex(scope, result);
|
||||
QueryApplicationRuntime(scope, result);
|
||||
QuerySuspendedInstances(scope, result, applicationIndex);
|
||||
result.Platform.PlatformDataAvailable = QueryPlatform(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");
|
||||
}
|
||||
|
||||
private void QueryPlatform(ManagementScope scope, ProbeResult result)
|
||||
result.Platform.SuspendedInstancesDataAvailable = QuerySuspendedInstances(scope, result, applicationIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut die WMI-Verbindung auf und wandelt Fehler in handlungsorientierte Diagnosen um.
|
||||
/// </summary>
|
||||
/// <param name="scope">Zu verbindender WMI-Scope.</param>
|
||||
/// <param name="result">Ergebnisobjekt fuer eine moegliche Diagnose.</param>
|
||||
/// <returns><c>true</c>, wenn der Namespace verbunden wurde.</returns>
|
||||
private static bool TryConnect(ManagementScope scope, ProbeResult result)
|
||||
{
|
||||
TryQuery(scope, result, "MSBTS_GroupSetting", "SELECT * FROM MSBTS_GroupSetting", item =>
|
||||
try
|
||||
{
|
||||
scope.Connect();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (IsExpectedWmiException(ex))
|
||||
{
|
||||
AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true, null, null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest BizTalk-Gruppe sowie Management- und MessageBox-Datenbankziele.
|
||||
/// </summary>
|
||||
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
|
||||
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
|
||||
/// <returns><c>true</c>, wenn beide Pflichtklassen gelesen wurden.</returns>
|
||||
private bool QueryPlatform(ManagementScope scope, ProbeResult result)
|
||||
{
|
||||
var groupFound = false;
|
||||
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"));
|
||||
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);
|
||||
|
||||
TryQuery(scope, result, "MSBTS_MessageBoxSetting", "SELECT * FROM MSBTS_MessageBoxSetting", item =>
|
||||
if (groupAvailable && !groupFound)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(result.Platform.MessageBoxDbServer))
|
||||
{
|
||||
result.Platform.MessageBoxDbServer = FirstNonEmpty(WmiHelpers.GetString(item, "DBServerName"), WmiHelpers.GetString(item, "ServerName"));
|
||||
result.Platform.MessageBoxDbName = FirstNonEmpty(WmiHelpers.GetString(item, "DBName"), WmiHelpers.GetString(item, "DatabaseName"));
|
||||
}
|
||||
}, true);
|
||||
AddMissingDataDiagnostic(result, "MSBTS_GroupSetting", "Keine BizTalk-Gruppe gefunden.");
|
||||
}
|
||||
|
||||
private Dictionary<string, string> BuildApplicationIndex(ManagementScope scope, ProbeResult result)
|
||||
var managementTargetComplete = HasDatabaseTarget(
|
||||
result.Platform.ManagementDbServer,
|
||||
result.Platform.ManagementDbName);
|
||||
if (groupAvailable && groupFound && !managementTargetComplete)
|
||||
{
|
||||
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;
|
||||
AddMissingDataDiagnostic(
|
||||
result,
|
||||
"MSBTS_GroupSetting.ManagementDatabase",
|
||||
"BizTalk Management-Datenbankziel ist in MSBTS_GroupSetting unvollstaendig.");
|
||||
}
|
||||
|
||||
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>
|
||||
/// 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>
|
||||
/// <param name="index">Zu erweiternder Anwendungsindex.</param>
|
||||
/// <param name="className">Name der auszulesenden WMI-Klasse.</param>
|
||||
private void AddArtifactIndex(ManagementScope scope, ProbeResult result, Dictionary<string, string> index, string className)
|
||||
{
|
||||
TryQuery(scope, result, className, "SELECT * FROM " + className, item =>
|
||||
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);
|
||||
}
|
||||
|
||||
private void QueryHostInstances(ManagementScope scope, ProbeResult result)
|
||||
/// <summary>
|
||||
/// Liest alle BizTalk Host Instances und deren Dienstzustand.
|
||||
/// </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, string server)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private void QueryApplicationRuntime(ManagementScope scope, ProbeResult result)
|
||||
/// <summary>
|
||||
/// Aggregiert Receive Locations, Send Ports und Orchestrations pro Anwendung.
|
||||
/// </summary>
|
||||
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
|
||||
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
|
||||
/// <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);
|
||||
|
||||
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
|
||||
});
|
||||
result.EndpointCandidates.Add(new EndpointCandidate
|
||||
{
|
||||
ArtifactType = "ReceiveLocation",
|
||||
ApplicationName = applicationName,
|
||||
ArtifactName = name,
|
||||
TransportRole = "Inbound",
|
||||
AdapterName = WmiHelpers.GetString(item, "AdapterName"),
|
||||
Address = WmiHelpers.GetString(item, "InboundTransportURL"),
|
||||
Active = isDisabled == false,
|
||||
Dynamic = false
|
||||
});
|
||||
var app = GetApplication(apps, applicationName);
|
||||
app.ReceiveLocationTotal++;
|
||||
if (WmiHelpers.GetBoolean(item, "IsDisabled", false))
|
||||
if (isDisabled == true)
|
||||
{
|
||||
app.ReceiveLocationDisabled++;
|
||||
}
|
||||
}, true);
|
||||
|
||||
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 isDynamic = WmiHelpers.GetBoolean(item, "IsDynamic", false);
|
||||
result.EndpointCandidates.Add(new EndpointCandidate
|
||||
{
|
||||
ArtifactType = "SendPort",
|
||||
ApplicationName = applicationName,
|
||||
ArtifactName = string.IsNullOrWhiteSpace(name) ? "(unknown)" : name,
|
||||
TransportRole = "Primary",
|
||||
AdapterName = WmiHelpers.GetString(item, "PTTransportType"),
|
||||
Address = WmiHelpers.GetString(item, "PTAddress"),
|
||||
Active = status == SendPortStarted,
|
||||
Dynamic = isDynamic
|
||||
});
|
||||
var secondaryAddress = WmiHelpers.GetString(item, "STAddress");
|
||||
if (!string.IsNullOrWhiteSpace(secondaryAddress))
|
||||
{
|
||||
result.EndpointCandidates.Add(new EndpointCandidate
|
||||
{
|
||||
ArtifactType = "SendPort",
|
||||
ApplicationName = applicationName,
|
||||
ArtifactName = string.IsNullOrWhiteSpace(name) ? "(unknown)" : name,
|
||||
TransportRole = "Secondary",
|
||||
AdapterName = WmiHelpers.GetString(item, "STTransportType"),
|
||||
Address = secondaryAddress,
|
||||
Active = status == SendPortStarted,
|
||||
Dynamic = isDynamic
|
||||
});
|
||||
}
|
||||
var app = GetApplication(apps, applicationName);
|
||||
app.SendPortTotal++;
|
||||
switch (WmiHelpers.GetInt32(item, "Status", 0))
|
||||
switch (status)
|
||||
{
|
||||
case SendPortStarted:
|
||||
app.SendPortStarted++;
|
||||
@@ -137,8 +298,9 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}, true);
|
||||
|
||||
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))
|
||||
@@ -164,11 +326,19 @@ namespace BizTalkCheckmkPulse
|
||||
result.Applications.AddRange(apps.Values.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private void QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary<string, string> applicationIndex)
|
||||
/// <summary>
|
||||
/// Liest ausschliesslich fortsetzbar und nicht fortsetzbar suspendierte Instanzen.
|
||||
/// </summary>
|
||||
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
|
||||
/// <param name="result">Zu aktualisierendes Ergebnisobjekt.</param>
|
||||
/// <param name="applicationIndex">Index fuer die Zuordnung zur BizTalk-Anwendung.</param>
|
||||
/// <returns><c>true</c>, wenn die Pflichtabfrage erfolgreich war.</returns>
|
||||
private bool QuerySuspendedInstances(ManagementScope scope, ProbeResult result, Dictionary<string, string> applicationIndex)
|
||||
{
|
||||
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),
|
||||
@@ -178,31 +348,45 @@ 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);
|
||||
}
|
||||
|
||||
private void TryQuery(ManagementScope scope, ProbeResult result, string label, string queryText, Action<ManagementObject> action, bool required)
|
||||
/// <summary>
|
||||
/// Fuehrt eine WQL-Abfrage aus und erfasst erwartbare Providerfehler strukturiert.
|
||||
/// </summary>
|
||||
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
|
||||
/// <param name="result">Ergebnisobjekt fuer Diagnosen.</param>
|
||||
/// <param name="label">Menschenlesbare Bezeichnung der Abfrage.</param>
|
||||
/// <param name="queryText">Auszufuehrende WQL-Abfrage.</param>
|
||||
/// <param name="action">Verarbeitung fuer jedes gefundene WMI-Objekt.</param>
|
||||
/// <param name="required">Kennzeichen, ob die Abfrage fuer einen stabilen Service erforderlich ist.</param>
|
||||
/// <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);
|
||||
return true;
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
catch (Exception ex) when (IsExpectedWmiException(ex))
|
||||
{
|
||||
AddDiagnostic(result, label, ex.Message, required);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
AddDiagnostic(result, label, ex.Message, required);
|
||||
}
|
||||
catch (System.Runtime.InteropServices.COMException ex)
|
||||
{
|
||||
AddDiagnostic(result, label, ex.Message, required);
|
||||
AddWmiDiagnostic(result, label, ex, required, false, queryText, stopwatch.Elapsed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumeriert eine WQL-Abfrage mit festem Timeout und gibt jedes Objekt weiter.
|
||||
/// </summary>
|
||||
/// <param name="scope">Verbundener BizTalk-WMI-Scope.</param>
|
||||
/// <param name="queryText">Auszufuehrende WQL-Abfrage.</param>
|
||||
/// <param name="action">Verarbeitung fuer jedes gefundene Objekt.</param>
|
||||
private void ForEachObject(ManagementScope scope, string queryText, Action<ManagementObject> action)
|
||||
{
|
||||
var queryOptions = new EnumerationOptions
|
||||
@@ -225,6 +409,12 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liefert eine vorhandene oder neu angelegte Aggregation fuer eine Anwendung.
|
||||
/// </summary>
|
||||
/// <param name="apps">Aggregation nach Anwendungsname.</param>
|
||||
/// <param name="applicationName">Zu suchender Anwendungsname.</param>
|
||||
/// <returns>Aggregation der angeforderten Anwendung.</returns>
|
||||
private static ApplicationRuntimeState GetApplication(Dictionary<string, ApplicationRuntimeState> apps, string applicationName)
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(applicationName) ? UnknownApplication : applicationName.Trim();
|
||||
@@ -238,6 +428,34 @@ 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>
|
||||
/// <param name="item">Suspendierte WMI-Instanz.</param>
|
||||
/// <param name="applicationIndex">Index bekannter Artefaktschluessel.</param>
|
||||
/// <returns>Ermittelter Anwendungsname oder <c>(unknown)</c>.</returns>
|
||||
private static string ResolveApplication(ManagementBaseObject item, Dictionary<string, string> applicationIndex)
|
||||
{
|
||||
var direct = GetApplicationName(item);
|
||||
@@ -258,6 +476,11 @@ namespace BizTalkCheckmkPulse
|
||||
return UnknownApplication;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liefert moegliche Schluessel fuer die Zuordnung eines WMI-Objekts.
|
||||
/// </summary>
|
||||
/// <param name="item">Auszuwertendes WMI-Objekt.</param>
|
||||
/// <returns>Vorhandene oder leere Kandidatenwerte.</returns>
|
||||
private static IEnumerable<string> CandidateKeys(ManagementBaseObject item)
|
||||
{
|
||||
yield return WmiHelpers.GetString(item, "ApplicationName");
|
||||
@@ -266,6 +489,11 @@ namespace BizTalkCheckmkPulse
|
||||
yield return WmiHelpers.GetString(item, "HostName");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest den Anwendungsnamen aus den je Klasse unterschiedlichen Properties.
|
||||
/// </summary>
|
||||
/// <param name="item">Auszuwertendes WMI-Objekt.</param>
|
||||
/// <returns>Anwendungsname oder <c>(unknown)</c>.</returns>
|
||||
private static string GetApplicationName(ManagementBaseObject item)
|
||||
{
|
||||
return FirstNonEmpty(
|
||||
@@ -274,15 +502,302 @@ namespace BizTalkCheckmkPulse
|
||||
WmiHelpers.GetString(item, "BizTalkApplication")) ?? UnknownApplication;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liefert den ersten nicht leeren Textwert.
|
||||
/// </summary>
|
||||
/// <param name="values">Zu pruefende Werte in Prioritaetsreihenfolge.</param>
|
||||
/// <returns>Erster nicht leerer Wert oder <c>null</c>.</returns>
|
||||
private static string FirstNonEmpty(params string[] values)
|
||||
{
|
||||
return values == null ? null : values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
|
||||
}
|
||||
|
||||
private static void AddDiagnostic(ProbeResult result, string label, string message, bool required)
|
||||
/// <summary>
|
||||
/// Prueft, ob Server und Datenbank eines SQL-Ziels vorhanden sind.
|
||||
/// </summary>
|
||||
private static bool HasDatabaseTarget(string server, string database)
|
||||
{
|
||||
result.Diagnostics.Add((required ? "Required" : "Optional") + " WMI query " + label + " failed: " + message);
|
||||
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>
|
||||
/// <param name="exception">Zu klassifizierende Exception.</param>
|
||||
/// <returns><c>true</c> fuer behandelbare WMI-, Rechte- und COM-Fehler.</returns>
|
||||
private static bool IsExpectedWmiException(Exception exception)
|
||||
{
|
||||
return exception is ManagementException
|
||||
|| exception is UnauthorizedAccessException
|
||||
|| exception is COMException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erzeugt aus einer WMI-Exception eine strukturierte Diagnose.
|
||||
/// </summary>
|
||||
/// <param name="result">Ergebnisobjekt fuer die Diagnose.</param>
|
||||
/// <param name="component">Betroffener Namespace oder Klassenname.</param>
|
||||
/// <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,
|
||||
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, sqlLoginPrincipal),
|
||||
Action = BuildWmiAction(category, connectionFailure, sqlLoginPrincipal),
|
||||
TechnicalDetails = technicalDetails
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erzeugt eine Konfigurationsdiagnose, wenn eine erfolgreiche Pflichtabfrage keine Instanz liefert.
|
||||
/// </summary>
|
||||
/// <param name="result">Ergebnisobjekt fuer die Diagnose.</param>
|
||||
/// <param name="component">Betroffene WMI-Klasse.</param>
|
||||
/// <param name="summary">Beschreibung der fehlenden BizTalk-Konfiguration.</param>
|
||||
private static void AddMissingDataDiagnostic(ProbeResult result, string component, string summary)
|
||||
{
|
||||
result.Diagnostics.Add(new ProbeDiagnostic
|
||||
{
|
||||
Area = DiagnosticArea.Wmi,
|
||||
Category = DiagnosticCategory.Configuration,
|
||||
Component = component,
|
||||
Required = true,
|
||||
Summary = summary,
|
||||
Action = "BizTalk-Gruppenkonfiguration und WMI-Provider pruefen. Danach den Agent-Dump erneut ausfuehren."
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Klassifiziert WMI-Fehler anhand von Exception-Typ, Status und HRESULT.
|
||||
/// </summary>
|
||||
/// <param name="exception">Zu klassifizierende Exception.</param>
|
||||
/// <returns>Handlungsorientierte Diagnosekategorie.</returns>
|
||||
internal static DiagnosticCategory ClassifyWmiException(Exception exception)
|
||||
{
|
||||
var managementException = exception as ManagementException;
|
||||
var comException = exception as COMException;
|
||||
if (exception is UnauthorizedAccessException
|
||||
|| (managementException != null && managementException.ErrorCode == ManagementStatus.AccessDenied)
|
||||
|| (comException != null && comException.ErrorCode == EAccessDenied))
|
||||
{
|
||||
return DiagnosticCategory.Permission;
|
||||
}
|
||||
|
||||
if (managementException != null
|
||||
&& 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;
|
||||
}
|
||||
|
||||
if (ContainsAny(message, "RPC server", "network path", "unavailable", "not available"))
|
||||
{
|
||||
return DiagnosticCategory.Connectivity;
|
||||
}
|
||||
|
||||
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>
|
||||
/// <param name="category">Klassifizierte Fehlerart.</param>
|
||||
/// <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,
|
||||
string sqlLoginPrincipal)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case DiagnosticCategory.Permission:
|
||||
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-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:
|
||||
return "WMI-/RPC-Verbindung zu " + component + " konnte nicht hergestellt werden.";
|
||||
default:
|
||||
return "BizTalk-WMI-Provider konnte " + component + " nicht auslesen.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liefert eine konkrete Handlungsempfehlung fuer einen WMI-Fehler.
|
||||
/// </summary>
|
||||
/// <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,
|
||||
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
|
||||
? "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:
|
||||
return "Bei lokalem Zugriff WMI-Dienst und BizTalk-WMI-Provider pruefen; bei konfiguriertem Remote-WMI zusaetzlich RPC, DNS und Firewall pruefen.";
|
||||
default:
|
||||
return "Technische Details sowie Windows Application Log und BizTalk-Ereignisse pruefen. Betroffene WMI-Klasse separat testen.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht mehrere Teiltexte ohne Beachtung der Gross-/Kleinschreibung.
|
||||
/// </summary>
|
||||
/// <param name="value">Zu untersuchender Text.</param>
|
||||
/// <param name="candidates">Gesuchte Teiltexte.</param>
|
||||
/// <returns><c>true</c>, wenn mindestens ein Teiltext enthalten ist.</returns>
|
||||
private static bool ContainsAny(string value, params string[] candidates)
|
||||
{
|
||||
return candidates.Any(x => value.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,17 @@ using System.Management;
|
||||
|
||||
namespace BizTalkCheckmkPulse
|
||||
{
|
||||
/// <summary>
|
||||
/// Konvertiert optionale WMI-Properties defensiv in .NET-Typen.
|
||||
/// </summary>
|
||||
internal static class WmiHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Liest eine WMI-Property als getrimmten Text.
|
||||
/// </summary>
|
||||
/// <param name="item">WMI-Objekt mit der Property.</param>
|
||||
/// <param name="propertyName">Name der Property.</param>
|
||||
/// <returns>Textwert oder leerer Text bei fehlender Property.</returns>
|
||||
public static string GetString(ManagementBaseObject item, string propertyName)
|
||||
{
|
||||
if (item == null || string.IsNullOrWhiteSpace(propertyName))
|
||||
@@ -24,6 +33,13 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest eine WMI-Property als Ganzzahl.
|
||||
/// </summary>
|
||||
/// <param name="item">WMI-Objekt mit der Property.</param>
|
||||
/// <param name="propertyName">Name der Property.</param>
|
||||
/// <param name="fallback">Ersatzwert bei fehlender oder ungueltiger Property.</param>
|
||||
/// <returns>Konvertierter oder ersetzter Zahlenwert.</returns>
|
||||
public static int GetInt32(ManagementBaseObject item, string propertyName, int fallback)
|
||||
{
|
||||
var value = GetString(item, propertyName);
|
||||
@@ -31,6 +47,13 @@ namespace BizTalkCheckmkPulse
|
||||
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest eine WMI-Property als booleschen Wert.
|
||||
/// </summary>
|
||||
/// <param name="item">WMI-Objekt mit der Property.</param>
|
||||
/// <param name="propertyName">Name der Property.</param>
|
||||
/// <param name="fallback">Ersatzwert bei fehlender oder ungueltiger Property.</param>
|
||||
/// <returns>Konvertierter oder ersetzter Wahrheitswert.</returns>
|
||||
public static bool GetBoolean(ManagementBaseObject item, string propertyName, bool fallback)
|
||||
{
|
||||
var value = GetString(item, propertyName);
|
||||
@@ -38,6 +61,12 @@ namespace BizTalkCheckmkPulse
|
||||
return bool.TryParse(value, out parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Konvertiert einen DMTF-Zeitstempel aus einer WMI-Property.
|
||||
/// </summary>
|
||||
/// <param name="item">WMI-Objekt mit der Property.</param>
|
||||
/// <param name="propertyName">Name der Zeitstempel-Property.</param>
|
||||
/// <returns>Zeitpunkt oder <c>null</c> bei fehlendem beziehungsweise ungueltigem Wert.</returns>
|
||||
public static DateTime? GetDmtfDateTime(ManagementBaseObject item, string propertyName)
|
||||
{
|
||||
var value = GetString(item, propertyName);
|
||||
@@ -57,4 +86,3 @@ namespace BizTalkCheckmkPulse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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>
|
||||
<ProjectReference Include="..\..\src\BizTalkCheckmkPulse.Setup\BizTalkCheckmkPulse.Setup.csproj">
|
||||
<Project>{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}</Project>
|
||||
<Name>BizTalkCheckmkPulse.Setup</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user