Compare commits

..
2 Commits
Author SHA1 Message Date
admin 22a4725056 Merge origin/main into main 2026-08-04 10:09:45 +02:00
admin 91e0db5619 Add BizTalk endpoint reachability monitoring 2026-08-04 09:48:00 +02:00
19 changed files with 1711 additions and 35 deletions
+71 -13
View File
@@ -89,7 +89,7 @@ Ablauf:
2. Tageslogs gemaess `LogRetentionDays` bereinigen.
3. exklusives Handle auf `<SnapshotPath>.provider.lock` halten.
4. WMI-, SQL- und Event-Log-Probes ausfuehren.
5. acht stabile und optionale dynamische Checkmk-Zeilen formatieren.
5. neun stabile und optionale dynamische Checkmk-Zeilen formatieren.
6. Snapshot in einer eindeutigen Temporaerdatei desselben Verzeichnisses
schreiben.
7. `Flush(true)` ausfuehren und Temporaerdatei atomar publizieren.
@@ -132,7 +132,7 @@ Der Consumer:
- gibt bei Erfolg ausschliesslich den validierten Payload aus.
Er gibt immer Exitcode `0` zurueck, damit ein Fehler die komplette
Checkmk-Agentsektion nicht zerstoert. Jede Ablehnung erzeugt acht
Checkmk-Agentsektion nicht zerstoert. Jede Ablehnung erzeugt neun
`UNKNOWN`-Zeilen und einen Eintrag im Consumer-Log.
## 4. Snapshot-Vertrag
@@ -210,7 +210,7 @@ Metrik ausgewiesen und zugleich in `suspended_nonresumable` mitgezaehlt.
### 5.2 Kompaktes Checkmk-Servicebild
Die alte Sammelzeile `BizTalk Runtime Artifacts` wurde durch drei kurze
Services ersetzt. Insgesamt entstehen acht stabile Services:
Services ersetzt. Insgesamt entstehen neun stabile Services:
1. `BizTalk Platform`
2. `BizTalk SQL Access`
@@ -218,8 +218,9 @@ Services ersetzt. Insgesamt entstehen acht stabile Services:
4. `BizTalk Host Instances`
5. `BizTalk Receive Locations`
6. `BizTalk Send Ports`
7. `BizTalk Orchestrations`
8. `BizTalk Event Log`
7. `BizTalk Endpoint Reachability`
8. `BizTalk Orchestrations`
9. `BizTalk Event Log`
Receive Locations und Send Ports alarmieren nur fuer unerwartete
Aus-Zustaende. Fachlich bewusst deaktivierte Namen werden exakt, ohne
@@ -232,7 +233,46 @@ Artefakte werden als `(+n more)` zusammengefasst. `MaxDetailCharacters`
begrenzt jede Detailausgabe, damit Checkmk-Ansichten kompakt bleiben, waehrend
alle numerischen Metriken vollstaendig erhalten bleiben.
### 5.3 SQL-Zugriffsprobe
### 5.3 Endpoint-Discovery und Netzwerkprobe
Die Endpoint-Funktion erweitert keine WMI-Klassenmenge. Aus den ohnehin
minuetlich abgefragten Instanzen werden nur dokumentierte Properties gelesen:
| WMI-Klasse | Properties |
| --- | --- |
| `MSBTS_SendPort` | `Status`, `IsDynamic`, `PTAddress`, `STAddress`, `PTTransportType`, `STTransportType` |
| `MSBTS_ReceiveLocation` | `IsDisabled`, `InboundTransportURL`, `AdapterName` |
Nur `Status=Started` beziehungsweise `IsDisabled=false` wird geprueft.
Inaktive Artefakte bleiben aus der Probe ausgeschlossen, auch wenn sie noch im
Katalog stehen. Dynamische Send Ports, lokale Dateipfade, SMTP-Empfaengerlisten
und Named Pipes werden nicht als externe Host/Port-Ziele behandelt.
Der lokale Katalog
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` wird beim ersten
erfolgreichen Lauf erzeugt und danach standardmaessig alle 168 Stunden atomar
abgeglichen. Er enthaelt keine vollstaendigen URIs, Querystrings,
Benutzernamen oder Kennwoerter, sondern nur Artefaktbezug, Adapter, Host, Port
und `TCP`/`UDP`. Automatisch erzeugte Eintraege tragen
`autoDiscovered="true"`. Ein bewusst manueller Override wird mit
`autoDiscovered="false"` markiert und beim Wochenabgleich erhalten; sein
`key` muss dem Artefakteintrag entsprechen. Eigenstaendige manuelle Ziele
verwenden `artifactType="Manual"`.
HTTP(S), SFTP, FTP, UNC/SMB, WCF und `net.tcp` werden ausschliesslich per
TCP-Connect geprueft. Es findet kein fachlicher Request und keine Anmeldung
statt. Explizite `udp://host:port`-Eintraege senden ein einzelnes Datagramm;
das bestaetigt DNS, Route und lokalen Versand, kann ohne
applikationsspezifisches Antwortprotokoll aber keinen entfernten Dienstzustand
beweisen. Gemeinsame Host/Port-Ziele werden dedupliziert und mit begrenzter
Parallelitaet getestet.
Der Checkmk-Service nennt im OK-Fall nur die Gesamtzahl. Bei Ausfaellen werden
ausschliesslich die nicht erreichbaren Artefakte in `unavailable=` angezeigt;
`MaxSummaryItems` und `MaxDetailCharacters` begrenzen die Darstellung. Alle
Einzelfehler bleiben vollstaendig im Provider-Log.
### 5.4 SQL-Zugriffsprobe
Der Provider oeffnet fuer Management- und Master-MessageBox-Datenbank eine
kurze `System.Data.SqlClient`-Verbindung mit integrierter
@@ -243,7 +283,7 @@ Die Ausgabe `execution_identity=` zeigt deshalb das Provider-Konto, nicht mehr
`NT AUTHORITY\SYSTEM`. Der Check beweist, dass genau das Scheduled-Task-Konto
die Ziele erreichen kann.
### 5.4 Event Log
### 5.5 Event Log
Der Provider liest das lokale Application Log im konfigurierten Zeitfenster
und filtert Quellen wie `BizTalk Server`, `XLANG/s`, `ENTSSO`,
@@ -379,7 +419,7 @@ Snapshot nicht zusaetzlich verzoegert.
Nach dem Agent-Dump:
1. Service Discovery fuer den BizTalk-Host ausfuehren.
2. acht stabile Services aufnehmen; den alten Service
2. neun stabile Services aufnehmen; den alten Service
`BizTalk Runtime Artifacts` nach erfolgreicher Discovery entfernen.
3. Changes aktivieren.
4. Views/Benachrichtigungen nach Umgebung konfigurieren.
@@ -486,6 +526,8 @@ Runtime:
| `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 |
BizTalk-Probes:
@@ -497,6 +539,11 @@ BizTalk-Probes:
| `SqlConnectionTimeoutSeconds` | `5` |
| `ProbeEventLog` | `true` |
| `EventLogLookbackMinutes` | `60` |
| `ProbeEndpointConnectivity` | `true` |
| `EndpointDiscoveryIntervalHours` | `168` |
| `EndpointProbeTimeoutMilliseconds` | `3000` |
| `EndpointProbeMaxConcurrency` | `12` |
| `EndpointMaxCount` | `500` |
Alarmierung:
@@ -520,7 +567,7 @@ Alarmierung:
Automatisiert:
- Release-Build .NET Framework 4.7.2,
- exakt acht Self-Test-Services,
- exakt neun Self-Test-Services,
- unbekannte Quellen werden `UNKNOWN`,
- dynamische Anwendungsservices nur bei bekannter Anwendung,
- WMI-Queryvertrag nutzt dokumentierte Properties,
@@ -530,10 +577,13 @@ Automatisiert:
- Receive-/Send-Allowlisten trennen bewusste von unerwarteten Aus-Zustaenden,
- ein fehlender `IsDisabled`-Wert wird `UNKNOWN` statt still als enabled,
- betroffene Namen und Gesamtlaenge der Summary bleiben begrenzt,
- Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert,
- manuelle Endpoint-Overrides ueberleben den automatischen Wochenabgleich,
- nur nicht erreichbare Endpoints erscheinen im Checkmk-Detail,
- Snapshot-Roundtrip und Ersatz,
- SHA-256-Manipulation wird verworfen,
- Stale-Snapshot wird verworfen,
- Consumer-Fallback enthaelt acht `UNKNOWN`-Services.
- Consumer-Fallback enthaelt neun `UNKNOWN`-Services.
Windows-/ACC-Abnahme:
@@ -550,9 +600,17 @@ Windows-/ACC-Abnahme:
9. Einen Testnamen aus der Allowlist entfernen und den erwarteten CRIT mit
kurzem `affected=`-Detail pruefen.
10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben.
11. Task deaktivieren: nach 180 Sekunden acht stale-`UNKNOWN`s.
12. Task wieder aktivieren: naechster Snapshot stellt Echtzustand her.
13. Agent-Dump und Checkmk Service Discovery erfolgreich.
11. `endpoints.xml` wurde erzeugt; sie enthaelt keine vollstaendigen URIs oder
Zugangsdaten und nur Started-/Enabled-Artefakte.
12. Einen erreichbaren Testport sperren: nur `Endpoint Reachability` wird CRIT
und nennt ausschliesslich das nicht erreichbare Ziel.
13. Ein Artefakt stoppen/deaktivieren: es wird im naechsten Minutenlauf nicht
mehr geprueft.
14. Katalogzeit auf aelter als 168 Stunden setzen beziehungsweise Intervall
testweise reduzieren: erfolgreicher atomarer Umgebungsabgleich.
15. Task deaktivieren: nach 180 Sekunden neun stale-`UNKNOWN`s.
16. Task wieder aktivieren: naechster Snapshot stellt Echtzustand her.
17. Agent-Dump und Checkmk Service Discovery erfolgreich.
Erst nach ACC-Abnahme erfolgt der gestufte Rollout nach DEV/TST/PRD.
+37 -5
View File
@@ -20,7 +20,7 @@ lokales BizTalk-WMI + BizTalk-SQL + Application Event Log
Checkmk Windows Agent (LocalSystem)
|
v
acht kompakte Checkmk Local Checks
neun kompakte Checkmk Local Checks
```
Damit bekommt `LocalSystem` keine BizTalk- oder SQL-Berechtigung. Nur das
@@ -62,7 +62,7 @@ Der Datenaustausch ist bewusst defensiv:
Checkmk-Zeilen, korrekte SHA-256-Pruefsumme und ein maximales Alter von
standardmaessig 180 Sekunden.
- Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien
ergeben acht gueltige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe.
ergeben neun gueltige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe.
- Ein exklusives Lock und die Task-Einstellung `IgnoreNew` verhindern
ueberlappende Providerlaeufe.
- Ein unerwarteter Providerfehler erzeugt nach Moeglichkeit einen aktuellen
@@ -82,6 +82,7 @@ Standardmaessig entstehen:
- `BizTalk Host Instances`
- `BizTalk Receive Locations`
- `BizTalk Send Ports`
- `BizTalk Endpoint Reachability`
- `BizTalk Orchestrations`
- `BizTalk Event Log`
@@ -91,8 +92,25 @@ Zustand und Graphen direkt erkennbar, ohne eine lange Sammelzeile zu lesen:
- Suspensions: total, resumable, non-resumable und Routing Failure Reports
- Receive Locations: total, enabled, unerwartet/bewusst disabled und unbekannt
- Send Ports: total, started, stopped, bound, unbekannt sowie bewusst inactive
- Endpoint Reachability: nur aktive Send-/Receive-Artefakte; im OK-Fall nur
eine Gesamtaussage, im Fehlerfall ausschliesslich nicht erreichbare Ziele
- Orchestrations: total, started, stopped, bound, unbound und unbekannt
Der Provider nutzt fuer 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 und UNC werden per TCP
geprueft; explizite `udp://`-Ziele per UDP-Datagramm. Es werden keine
HTTP-Requests, Anmeldungen oder fachlichen Nachrichten gesendet.
Der geheimnisfreie Katalog liegt unter
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml`. Fehlt er, wird er beim
naechsten erfolgreichen Providerlauf erstellt. Alle 168 Stunden wird er gegen
die Umgebung abgeglichen. Automatisch verwaltete Eintraege fuer inzwischen
inaktive Artefakte verschwinden beim Abgleich; bei jedem Minutenlauf werden
sie zusaetzlich gegen den aktuellen Started-/Enabled-Zustand gefiltert.
Pro Service werden standardmaessig maximal fuenf betroffene Namen gezeigt.
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusaetzlich auf
1600 Zeichen begrenzt. Metriken bleiben trotzdem vollstaendig.
@@ -104,6 +122,9 @@ 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)
## Voraussetzungen
Build-Host:
@@ -168,7 +189,7 @@ Format-Self-Test ohne WMI, SQL oder Event Log:
artifacts\BizTalkCheckmkPulse-Setup\application\BizTalkCheckmkPulse.exe --self-test
```
Erwartet werden exakt acht `OK`-Zeilen. Die Regressionstests pruefen
Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests pruefen
zusaetzlich Snapshot-Roundtrip, atomaren Ersatz, SHA-256-Manipulation,
Stale-Erkennung, stabile Fallbacks und die bestehenden BizTalk-WMI-Diagnosen.
Ein Mono-Build ist eine hilfreiche Quellcodepruefung, ersetzt aber nicht die
@@ -253,7 +274,7 @@ Verbindlicher Test im echten `LocalSystem`-Kontext:
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
```
Danach in Checkmk eine Service Discovery ausfuehren, die acht Services
Danach in Checkmk eine Service Discovery ausfuehren, die neun Services
aufnehmen und Changes aktivieren. Ein zusaetzlicher Checkmk-Async-Cache ist
nicht erforderlich: Der Consumer liest nur eine kleine lokale Datei und der
Provider besitzt bereits seinen eigenen Minutentakt.
@@ -269,6 +290,7 @@ Provider besitzt bereits seinen eigenen Minutentakt.
data\
biztalk-checkmk-pulse.snapshot
biztalk-checkmk-pulse.snapshot.provider.lock
endpoints.xml
logs\
biztalk-checkmk-pulse-YYYYMMDD.log
@@ -306,6 +328,12 @@ Wichtige Werte:
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegroesse fuer den Consumer. |
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. |
| `LogRetentionDays` | `30` | Provider bereinigt aeltere Logs. |
| `ProbeEndpointConnectivity` | `true` | Aktiviert den aggregierten TCP-/UDP-Netzwerkcheck. |
| `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollstaendige URIs/Secrets. |
| `EndpointDiscoveryIntervalHours` | `168` | Intervall fuer den vollstaendigen Umgebungsabgleich. |
| `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. |
| `EndpointProbeMaxConcurrency` | `12` | Begrenzte parallele Socket-Probes. |
| `EndpointMaxCount` | `500` | Harte Obergrenze gegen fehlerhafte/uebergrosse Konfiguration. |
| `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. |
| `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. |
| `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. |
@@ -327,11 +355,13 @@ liest den naechsten atomar publizierten Snapshot.
| Beobachtung | Ursache / Massnahme |
| --- | --- |
| Alle acht Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto/Kennwort und ACL pruefen. |
| Alle neun Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto/Kennwort und ACL pruefen. |
| Snapshot ist `stale` | `LastTaskResult`, Laufzeit, WMI-/SQL-Timeout und Log pruefen. |
| SHA-256 oder Format ungueltig | Datei nicht manuell bearbeiten; Datentraeger/AV und Schreibpfad pruefen, Task neu starten. |
| Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` pruefen. |
| Receive Locations / Send Ports sind CRIT | `affected=` pruefen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. |
| Endpoint Reachability ist CRIT | Nur `unavailable=` pruefen; DNS, Zielport, Firewall und externen Dienst kontrollieren. |
| Endpoint Reachability ist UNKNOWN | WMI-Vollstaendigkeit, `endpoints.xml`, woechentlichen Refresh und nicht automatisch aufloesbare externe Adapteradresse pruefen. |
| `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema pruefen; keine Rechte ausweiten. |
| Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos pruefen. |
| Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL pruefen. |
@@ -352,6 +382,8 @@ geaendert und muss separat durch AD-/BizTalk-Administration entfernt werden.
- 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)
- Checkmk: Windows Agent und Local Checks
Die genauen Links stehen in [Dokumentation.md](Dokumentation.md).
+41 -1
View File
@@ -11,7 +11,7 @@ Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt:
Die fachlichen Daten werden minuetlich vom privilegierten Scheduled-Task-
Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den
integritaets- und altersgeprueften Snapshot. Ist dieser Transport nicht
verfuegbar, liefern alle acht stabilen Services `UNKNOWN`.
verfuegbar, liefern alle neun stabilen Services `UNKNOWN`.
## BizTalk Platform
@@ -174,6 +174,46 @@ Metriken:
| `biztalk_send_ports_expected_inactive` | Bewusst inactive und exakt allowlisted. |
| `biztalk_send_ports_unknown` | Nicht interpretierbarer Send-Port-Status. |
## BizTalk Endpoint Reachability
Zweck:
- Prueft nur Receive Locations mit `IsDisabled=false` und Send Ports mit
`Status=Started`.
- Reduziert die Adresse auf Host/Port und fuehrt keinen fachlichen
HTTP-, FTP-, SFTP-, WCF- oder BizTalk-Aufruf aus.
- Dedupliziert gemeinsame Host/Port-Ziele und prueft sie mit begrenzter
Parallelitaet; 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 vollstaendig; alle aktiven pruefbaren Ziele erreichbar. |
| `CRIT` | Mindestens ein getestetes TCP-/UDP-Ziel nicht erreichbar. |
| `UNKNOWN` | Katalog fehlt/ist ungueltig, Wochenabgleich scheitert, Runtimezustand ist unvollstaendig 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` | Eintraege in `endpoints.xml`, einschliesslich derzeit inaktiver. |
| `biztalk_endpoints_active` | Im aktuellen Lauf Started/Enabled und fuer die Probe ausgewaehlt. |
| `biztalk_endpoints_tested` | Vollstaendig abgeschlossene Artefaktprobes. |
| `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. |
| `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. |
| `biztalk_endpoints_unsupported` | Aktive externe Adressen ohne sicher bestimmbaren Host/Port. |
| `biztalk_endpoints_inactive_skipped` | Konfigurierte Eintraege, deren BizTalk-Artefakt aktuell nicht aktiv ist. |
Bei expliziten `udp://host:port`-Eintraegen kann ein generischer Check ohne
applikationsspezifisches Protokoll nur DNS, Route und erfolgreichen lokalen
Datagrammversand bestaetigen. Er beweist keine Antwort des entfernten
UDP-Dienstes. Fuer alle ueblichen streambasierten Adapter wird deshalb TCP
verwendet.
## BizTalk Orchestrations
Status:
+98
View File
@@ -0,0 +1,98 @@
# 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 vollstaendige
Abgleich alle `EndpointDiscoveryIntervalHours` (Default: 168 Stunden). Ein
Fehler beim Abgleich ersetzt keinen letzten gueltigen Katalog. Checkmk meldet
den ueberfaelligen/fehlgeschlagenen Abgleich trotzdem als `UNKNOWN`.
Unabhaengig 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
Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr
geprueft. Neue Artefakte werden spaetestens beim naechsten Wochenabgleich
aufgenommen.
## Sicherheitsmodell
Der Katalog speichert nicht die vollstaendige BizTalk-Adresse. Persistiert
werden nur:
- Artefakttyp, Anwendung, Name und Transportrolle
- Adaptername
- `TCP` oder `UDP`
- Host und Port
- `enabled` und `autoDiscovered`
URI-Benutzerinformationen, Kennwoerter, Pfade und Querystrings werden nicht
geschrieben. Der Katalog liegt im bereits ACL-geschuetzten `data`-Verzeichnis;
nur Provider und Administratoren duerfen ihn aendern. `LocalSystem` liest nur
den separaten, validierten Checkmk-Snapshot.
## Manuelle Korrektur eines automatisch nicht aufloesbaren Endpoints
Vor einer manuellen Aenderung den Scheduled Task kurz stoppen und die Datei
sichern. Einen vorhandenen Eintrag auf den gewuenschten Host/Port korrigieren
und `autoDiscovered="false"` setzen. Der vorhandene `key` bleibt unveraendert:
```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 Eintraege werden beim Wochenabgleich erhalten. Stimmen Artefakttyp
und `key` mit einem Send-/Receive-Artefakt ueberein, gilt weiterhin dessen
Started-/Enabled-Filter. Ein zusaetzliches, nicht an BizTalk gebundenes Ziel
kann `artifactType="Manual"` verwenden; es wird bei `enabled="true"` immer
geprueft.
Nach dem Speichern Task wieder starten und pruefen:
```powershell
Start-ScheduledTask -TaskName 'BizTalk Checkmk Pulse Provider'
Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 100
& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
```
Ungueltiges XML, doppelte Keys, falsche Maschine/Umgebung, unbekannte
Protokolle, ungueltige Ports und uebergrosse Dateien werden verworfen. Der
Provider kann bei vollstaendig verfuegbarem BizTalk-Runtimezustand einen
ungueltigen 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 fuer HTTP(S), SFTP, FTP, SMB/UNC, WCF und `net.tcp` der
passende nebenwirkungsfreie Netzwerktest. Er bestaetigt DNS, Routing,
Firewall und einen annehmenden TCP-Port, aber nicht die fachliche Antwort der
Zielanwendung.
UDP ist verbindungslos. Bei `udp://host:port` bestaetigt der Check nur DNS,
Route und erfolgreichen lokalen Datagrammversand. Fuer einen belastbaren
UDP-Dienstcheck waere ein adapter-/protokollspezifisches Request/Response-
Payload erforderlich; ein solches fachliches Paket wird bewusst nicht
erfunden.
+16 -1
View File
@@ -9,6 +9,7 @@
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.
```
@@ -55,9 +56,23 @@ werden nicht doppelt zum Total addiert.
Die Zaehler 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_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unsupported=0;;1;0|biztalk_endpoints_inactive_skipped=2;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar; 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=59, failed=2, unsupported_external=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)
```
## Fehlender oder veralteter Provider-Snapshot
Alle acht Services werden `UNKNOWN`, zum Beispiel:
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.
+4 -2
View File
@@ -57,7 +57,7 @@ Verbindlicher Agent-Dump:
2. `LastTaskResult=0`, frischen Snapshot und Log pruefen.
3. Agent-Dump im `LocalSystem`-Kontext pruefen.
4. Service Discovery fuer den BizTalk-Host ausfuehren.
5. acht stabile Services aufnehmen und den alten
5. neun stabile Services aufnehmen und den alten
`BizTalk Runtime Artifacts`-Service entfernen.
6. Changes aktivieren.
7. Views, Servicegruppen und Benachrichtigungen einrichten.
@@ -107,11 +107,13 @@ Soll:
- 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
ausschliesslich nicht erreichbare Ziele.
- keine berechtigungsbedingten `UNKNOWN`-Services.
## Alarmierung der Transportkette
Ein Ausfall des Providers wird ueber alle acht Services als `UNKNOWN`
Ein Ausfall des Providers wird ueber alle neun Services als `UNKNOWN`
sichtbar. Die Summary nennt fehlenden, unlesbaren, ungueltigen oder stale
Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso
eskaliert werden wie ein technischer Monitoringausfall.
+10
View File
@@ -14,6 +14,16 @@
<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" />
<!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. -->
<add key="EndpointDiscoveryIntervalHours" value="168" />
<add key="EndpointProbeTimeoutMilliseconds" value="3000" />
<add key="EndpointProbeMaxConcurrency" value="12" />
<add key="EndpointMaxCount" value="500" />
<add key="QueryTimeoutSeconds" value="25" />
<!-- Testet die Anmeldung des privilegierten Provider-Kontos an den ermittelten BizTalk-Datenbanken. -->
<add key="ProbeSqlConnectivity" value="true" />
@@ -38,11 +38,16 @@
<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" />
@@ -46,6 +46,7 @@ namespace BizTalkCheckmkPulse
yield return FormatHostInstances(result);
yield return FormatReceiveLocations(result);
yield return FormatSendPorts(result);
yield return FormatEndpointConnectivity(result);
yield return FormatOrchestrations(result);
yield return FormatEventLog(result);
@@ -70,6 +71,7 @@ namespace BizTalkCheckmkPulse
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.");
}
@@ -88,6 +90,7 @@ namespace BizTalkCheckmkPulse
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);
}
@@ -106,6 +109,7 @@ namespace BizTalkCheckmkPulse
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);
}
@@ -350,6 +354,80 @@ namespace BizTalkCheckmkPulse
return BuildLine(state, _options.ServiceName("Send Ports"), metrics, detail.ToString());
}
/// <summary>
/// Formatiert die Netzwerk-Erreichbarkeit aller aktuell aktiven, pruefbaren BizTalk-Endpunkte.
/// </summary>
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).ToArray();
var available = endpointState.Results.Count(x => x.Available);
var incomplete = endpointState.Results.Count != endpointState.Active
|| endpointState.UnsupportedActive > 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_tested={2};;;0|biztalk_endpoints_available={3};;;0|biztalk_endpoints_failed={4};;1;0|biztalk_endpoints_unsupported={5};;1;0|biztalk_endpoints_inactive_skipped={6};;;0",
endpointState.Configured,
endpointState.Active,
endpointState.Results.Count,
available,
failed.Length,
endpointState.UnsupportedActive,
endpointState.SkippedInactive);
var detail = new StringBuilder();
if (failed.Length == 0 && !incomplete)
{
detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar");
}
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(", unsupported_external=").Append(endpointState.UnsupportedActive);
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);
}
}
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)
@@ -0,0 +1,310 @@
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);
/// <summary>
/// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag.
/// </summary>
public static bool TryCreate(
EndpointCandidate candidate,
out EndpointCatalogEntry entry,
out string reason)
{
entry = null;
reason = string.Empty;
if (candidate == null)
{
reason = "Endpoint-Kandidat fehlt.";
return false;
}
if (candidate.Dynamic)
{
reason = "Dynamischer Send Port besitzt kein statisch pruefbares Ziel.";
return false;
}
var address = (candidate.Address ?? string.Empty).Trim();
if (address.Length == 0)
{
reason = "Transportadresse ist leer.";
return false;
}
string protocol;
string host;
int port;
if (!TryResolve(address, candidate.AdapterName, out protocol, out host, out port, out reason))
{
return false;
}
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
};
return true;
}
/// <summary>
/// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen.
/// </summary>
public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
{
if (candidate == null || candidate.Dynamic)
{
return false;
}
var address = (candidate.Address ?? string.Empty).Trim();
if (address.Length == 0
|| LooksLikeEmailRecipients(address)
|| Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|| address.StartsWith("/", StringComparison.Ordinal)
|| address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (Contains(candidate.AdapterName, "SMTP")
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
{
return false;
}
if (address.StartsWith("\\\\", StringComparison.Ordinal)
|| HostPortPattern.IsMatch(address)
|| Regex.IsMatch(address, @"(?:FORMATNAME:)?DIRECT=(?:OS|TCP):", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
{
return true;
}
Uri uri;
if (Uri.TryCreate(address, UriKind.Absolute, out uri))
{
return !string.Equals(uri.Scheme, "file", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(uri.Scheme, "mailto", StringComparison.OrdinalIgnoreCase);
}
return Contains(candidate.AdapterName, "SFTP")
|| Contains(candidate.AdapterName, "FTP")
|| Contains(candidate.AdapterName, "HTTP")
|| Contains(candidate.AdapterName, "SOAP")
|| Contains(candidate.AdapterName, "WCF");
}
private static bool TryResolve(
string address,
string adapterName,
out string protocol,
out string host,
out int port,
out string reason)
{
protocol = "TCP";
host = string.Empty;
port = 0;
reason = string.Empty;
if (LooksLikeEmailRecipients(address))
{
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);
}
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 = 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 = 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);
}
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);
}
if (Contains(adapterName, "SFTP"))
{
host = address.Trim('/');
port = 22;
return Validate(host, port, out reason);
}
if (Contains(adapterName, "FTP"))
{
host = address.Trim('/');
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;
default: return uri.Port > 0 ? uri.Port : 0;
}
}
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 bool Validate(string host, int port, out string reason)
{
host = (host ?? string.Empty).Trim();
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,287 @@
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;
public EndpointCatalogStore(string path, int maxBytes, int maxEntries)
{
_path = path;
_maxBytes = maxBytes;
_maxEntries = maxEntries;
}
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, _maxEntries),
UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, _maxEntries)
};
foreach (var node in root.Elements("Endpoint"))
{
if (catalog.Entries.Count >= _maxEntries)
{
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount.");
}
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")
};
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;
}
public void Write(EndpointCatalog catalog)
{
if (catalog == null)
{
throw new ArgumentNullException("catalog");
}
if (catalog.Entries.Count > _maxEntries)
{
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount.");
}
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),
new XAttribute("unsupportedCandidates", catalog.UnsupportedCandidates));
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)));
}
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;
}
}
}
@@ -0,0 +1,383 @@
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;
public EndpointConnectivityProbe(MonitoringOptions options, FileLogger logger)
{
_options = options;
_logger = logger;
}
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.EndpointMaxCount);
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 = 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
+ " unsupported_external=" + catalog.UnsupportedCandidates);
}
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;
state.UnsupportedActive = CountUnsupportedExternal(result.EndpointCandidates);
var activeKeys = new HashSet<string>(
result.EndpointCandidates.Where(x => x.Active).Select(x => x.Key),
StringComparer.OrdinalIgnoreCase);
var activeEntries = catalog.Entries
.Where(x => x.Enabled)
.Where(x => string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
|| activeKeys.Contains(x.Key))
.Take(_options.EndpointMaxCount)
.ToArray();
state.Active = activeEntries.Length;
state.SkippedInactive = catalog.Entries.Count(x => x.Enabled) - activeEntries.Length;
try
{
foreach (var probeResult in ProbeAllAsync(activeEntries).GetAwaiter().GetResult())
{
state.Results.Add(probeResult);
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);
}
}
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 discovered = new List<EndpointCatalogEntry>();
var unsupported = 0;
foreach (var candidate in active)
{
EndpointCatalogEntry entry;
string reason;
if (EndpointAddressParser.TryCreate(candidate, out entry, out reason))
{
discovered.Add(entry);
}
else if (EndpointAddressParser.IsPotentialExternalEndpoint(candidate))
{
unsupported++;
}
}
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 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,
UnsupportedCandidates = unsupported
};
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.EndpointMaxCount
|| catalog.ActiveCandidates > _options.EndpointMaxCount)
{
throw new InvalidDataException("Discovered endpoint candidates exceed EndpointMaxCount.");
}
return catalog;
}
private static int CountUnsupportedExternal(IEnumerable<EndpointCandidate> candidates)
{
var count = 0;
foreach (var candidate in candidates.Where(x => x.Active))
{
EndpointCatalogEntry ignored;
string reason;
if (!EndpointAddressParser.TryCreate(candidate, out ignored, out reason)
&& EndpointAddressParser.IsPotentialExternalEndpoint(candidate))
{
count++;
}
}
return count;
}
private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints)
{
var unique = endpoints
.GroupBy(x => x.Protocol.ToUpperInvariant() + "|" + x.Host.ToUpperInvariant() + "|" + x.Port)
.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 = endpoint.Protocol.ToUpperInvariant() + "|" + endpoint.Host.ToUpperInvariant() + "|" + endpoint.Port;
var outcome = outcomes[key];
return new EndpointProbeResult
{
Endpoint = endpoint,
Available = outcome.Available,
DurationMilliseconds = outcome.DurationMilliseconds,
Failure = outcome.Failure
};
}).ToArray();
}
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;
}
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
};
}
}
}
}
+116
View File
@@ -101,6 +101,8 @@ namespace BizTalkCheckmkPulse
Applications = new List<ApplicationRuntimeState>();
SqlTargets = new List<SqlAccessState>();
EventLog = new EventLogState();
EndpointCandidates = new List<EndpointCandidate>();
EndpointConnectivity = new EndpointConnectivityState();
}
public List<ProbeDiagnostic> Diagnostics { get; private set; }
@@ -114,6 +116,8 @@ namespace BizTalkCheckmkPulse
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>
@@ -236,4 +240,116 @@ 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>
/// 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; }
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 UnsupportedCandidates { 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>();
}
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 UnsupportedActive { get; set; }
public string Failure { get; set; }
public List<EndpointProbeResult> Results { get; private set; }
}
}
@@ -38,6 +38,13 @@ namespace BizTalkCheckmkPulse
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 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; }
@@ -73,6 +80,13 @@ namespace BizTalkCheckmkPulse
SnapshotMaxBytes = 1048576;
LogDirectory = Path.Combine(commonData, "BizTalkCheckmkPulse", "logs");
LogRetentionDays = 30;
ProbeEndpointConnectivity = true;
EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml");
EndpointCatalogMaxBytes = 1048576;
EndpointDiscoveryIntervalHours = 168;
EndpointProbeTimeoutMilliseconds = 3000;
EndpointProbeMaxConcurrency = 12;
EndpointMaxCount = 500;
}
/// <summary>
@@ -124,6 +138,13 @@ namespace BizTalkCheckmkPulse
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.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)
@@ -192,6 +213,19 @@ namespace BizTalkCheckmkPulse
{
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>
+7 -2
View File
@@ -69,7 +69,7 @@ namespace BizTalkCheckmkPulse
using (new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
logger.Info("Collection started. snapshot=" + options.SnapshotPath);
var result = Collect(options);
var result = Collect(options, logger);
foreach (var diagnostic in result.Diagnostics)
{
logger.Warning("Probe diagnostic: " + diagnostic.ToDisplayText());
@@ -98,6 +98,10 @@ namespace BizTalkCheckmkPulse
+ result.SendPorts.Count
+ " send_ports_not_started="
+ result.SendPorts.Count(x => x.Status != 3)
+ " endpoints_active="
+ result.EndpointConnectivity.Active
+ " endpoints_failed="
+ result.EndpointConnectivity.Results.Count(x => !x.Available)
+ " elapsed_ms="
+ stopwatch.ElapsedMilliseconds);
return 0;
@@ -155,11 +159,12 @@ namespace BizTalkCheckmkPulse
return 0;
}
private static ProbeResult Collect(MonitoringOptions options)
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)
{
@@ -6,5 +6,5 @@ using System.Reflection;
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
[assembly: AssemblyCompany("BEW")]
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
[assembly: AssemblyVersion("2.1.0.0")]
[assembly: AssemblyFileVersion("2.1.0.0")]
[assembly: AssemblyVersion("2.2.0.0")]
[assembly: AssemblyFileVersion("2.2.0.0")]
+2 -2
View File
@@ -258,9 +258,9 @@ namespace BizTalkCheckmkPulse
}
}
if (count < 8)
if (count < 9)
{
throw new InvalidDataException("Snapshot must contain all eight stable services.");
throw new InvalidDataException("Snapshot must contain all nine stable services.");
}
}
@@ -221,6 +221,17 @@ namespace BizTalkCheckmkPulse
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 (isDisabled == true)
@@ -241,6 +252,33 @@ namespace BizTalkCheckmkPulse
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 (status)
+172 -7
View File
@@ -28,6 +28,10 @@ namespace BizTalkCheckmkPulse.Tests
Run("UnknownReceiveLocationStateIsNotGreen", UnknownReceiveLocationStateIsNotGreen);
Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState);
Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded);
Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets);
Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides);
Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip);
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
@@ -52,19 +56,20 @@ namespace BizTalkCheckmkPulse.Tests
private static void SelfTestEmitsAllStableServices()
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
AssertEqual(8, lines.Length, "self-test line count");
AssertEqual(8, lines.Distinct(StringComparer.Ordinal).Count(), "unique self-test lines");
AssertEqual(9, lines.Length, "self-test line count");
AssertEqual(9, lines.Distinct(StringComparer.Ordinal).Count(), "unique self-test lines");
Assert(lines.All(x => x.StartsWith("0 \"BizTalk ", StringComparison.Ordinal)), "every self-test line must be OK");
Assert(lines.Any(x => x.Contains("\"BizTalk Event Log\"")), "Event Log service missing");
Assert(lines.Any(x => x.Contains("\"BizTalk Receive Locations\"")), "Receive Locations service missing");
Assert(lines.Any(x => x.Contains("\"BizTalk Send Ports\"")), "Send Ports service missing");
Assert(lines.Any(x => x.Contains("\"BizTalk Endpoint Reachability\"")), "Endpoint Reachability service missing");
Assert(lines.Any(x => x.Contains("\"BizTalk Orchestrations\"")), "Orchestrations service missing");
}
private static void UnavailableSourcesAreUnknown()
{
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).Format(new ProbeResult()).Take(8).ToArray();
AssertEqual(8, lines.Length, "stable service count");
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).Format(new ProbeResult()).Take(9).ToArray();
AssertEqual(9, lines.Length, "stable service count");
Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "unavailable sources must be UNKNOWN");
}
@@ -73,7 +78,7 @@ namespace BizTalkCheckmkPulse.Tests
var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true };
var result = CreateSuspensionResult("(unknown)");
var lines = new CheckmkLocalFormatter(options).Format(result).ToArray();
AssertEqual(8, lines.Length, "unknown application must not create a dynamic service");
AssertEqual(9, lines.Length, "unknown application must not create a dynamic service");
}
private static void KnownApplicationCreatesService()
@@ -81,7 +86,7 @@ namespace BizTalkCheckmkPulse.Tests
var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true };
var result = CreateSuspensionResult("Orders");
var lines = new CheckmkLocalFormatter(options).Format(result).ToArray();
AssertEqual(9, lines.Length, "known application should create one dynamic service");
AssertEqual(10, lines.Length, "known application should create one dynamic service");
Assert(lines.Any(x => x.Contains("\"BizTalk Suspended Orders\"")), "known application service missing");
}
@@ -328,6 +333,166 @@ namespace BizTalkCheckmkPulse.Tests
});
}
private static void EndpointAddressesResolveWithoutSecrets()
{
EndpointCatalogEntry endpoint;
string reason;
var candidate = new EndpointCandidate
{
ArtifactType = "SendPort",
ApplicationName = "Orders",
ArtifactName = "SP Orders",
TransportRole = "Primary",
AdapterName = "WCF-BasicHttp",
Address = "https://api-user:top-secret@example.test/orders?q=secret",
Active = true
};
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "HTTPS endpoint should resolve: " + reason);
AssertEqual("example.test", endpoint.Host, "HTTPS host");
AssertEqual(443, endpoint.Port, "HTTPS port");
Assert(endpoint.Key.IndexOf("secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog key must not contain URI secrets");
Assert(endpoint.Host.IndexOf("secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog host must not contain URI secrets");
candidate.Address = @"\\fileserver\drop\%MessageID%.xml";
candidate.AdapterName = "FILE";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "UNC endpoint should resolve: " + reason);
AssertEqual("fileserver", endpoint.Host, "UNC host");
AssertEqual(445, endpoint.Port, "UNC SMB port");
candidate.Address = "recipient@example.test";
candidate.AdapterName = "SMTP";
Assert(!EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "SMTP recipient must not become a host probe");
Assert(!EndpointAddressParser.IsPotentialExternalEndpoint(candidate), "SMTP recipient must not cause UNKNOWN");
candidate.Address = "/Orders/Receive.svc";
candidate.AdapterName = "WCF-CustomIsolated";
Assert(!EndpointAddressParser.IsPotentialExternalEndpoint(candidate), "relative local receive address must not cause UNKNOWN");
}
private static void EndpointCatalogPreservesManualOverrides()
{
var candidate = new EndpointCandidate
{
ArtifactType = "SendPort",
ApplicationName = "Orders",
ArtifactName = "SP Orders",
TransportRole = "Primary",
AdapterName = "SFTP",
Address = "sftp://discovered.example.test/out",
Active = true
};
var inactive = new EndpointCandidate
{
ArtifactType = "ReceiveLocation",
ApplicationName = "Orders",
ArtifactName = "RL Disabled",
TransportRole = "Inbound",
AdapterName = "HTTPS",
Address = "https://disabled.example.test/in",
Active = false
};
var existing = new EndpointCatalog { EnvironmentName = "ACC" };
existing.Entries.Add(new EndpointCatalogEntry
{
Key = candidate.Key,
ArtifactType = candidate.ArtifactType,
ApplicationName = candidate.ApplicationName,
ArtifactName = candidate.ArtifactName,
TransportRole = candidate.TransportRole,
AdapterName = candidate.AdapterName,
Protocol = "TCP",
Host = "manual.example.test",
Port = 2222,
Enabled = true,
AutoDiscovered = false
});
var synchronized = EndpointConnectivityProbe.SynchronizeCatalog(existing, new[] { candidate, inactive }, DateTime.UtcNow);
AssertEqual(1, synchronized.Entries.Count, "manual override count");
AssertEqual("manual.example.test", synchronized.Entries[0].Host, "manual override host");
AssertEqual(2222, synchronized.Entries[0].Port, "manual override port");
Assert(!synchronized.Entries[0].AutoDiscovered, "manual override marker");
Assert(synchronized.Entries.All(x => x.ArtifactName != "RL Disabled"), "inactive receive location must not be added");
}
private static void EndpointOutputListsOnlyUnavailableTargets()
{
var result = new ProbeResult();
result.EndpointConnectivity.RuntimeStateAvailable = true;
result.EndpointConnectivity.CatalogAvailable = true;
result.EndpointConnectivity.RefreshSucceeded = true;
result.EndpointConnectivity.Configured = 2;
result.EndpointConnectivity.Active = 2;
result.EndpointConnectivity.Results.Add(new EndpointProbeResult
{
Endpoint = TestEndpoint("Reachable", "up.example.test"),
Available = true
});
result.EndpointConnectivity.Results.Add(new EndpointProbeResult
{
Endpoint = TestEndpoint("Unavailable", "down.example.test"),
Available = false,
Failure = "TCP timeout"
});
var line = FindServiceLine(result, "Endpoint Reachability");
Assert(line.StartsWith("2 \"BizTalk Endpoint Reachability\"", StringComparison.Ordinal), "failed endpoint must be CRIT");
Assert(line.Contains("Unavailable"), "unavailable endpoint name missing");
Assert(line.Contains("down.example.test:443/TCP"), "unavailable host missing");
Assert(line.IndexOf("Reachable", StringComparison.Ordinal) < 0, "reachable endpoint must not clutter output");
Assert(line.IndexOf("up.example.test", StringComparison.Ordinal) < 0, "reachable host must not clutter output");
}
private static void EndpointCatalogRoundTrip()
{
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.CatalogTests." + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, "endpoints.xml");
try
{
var catalog = new EndpointCatalog
{
EnvironmentName = "ACC",
SynchronizedUtc = DateTime.UtcNow,
ActiveCandidates = 1,
UnsupportedCandidates = 0
};
catalog.Entries.Add(TestEndpoint("Orders", "api.example.test"));
var store = new EndpointCatalogStore(path, 1048576, 500);
store.Write(catalog);
var loaded = store.Read("ACC");
AssertEqual(1, loaded.Entries.Count, "catalog entry count");
AssertEqual("api.example.test", loaded.Entries[0].Host, "catalog host");
AssertEqual(443, loaded.Entries[0].Port, "catalog port");
Assert(File.ReadAllText(path).IndexOf("top-secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog must not contain URI secrets");
}
finally
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, true);
}
}
}
private static EndpointCatalogEntry TestEndpoint(string name, string host)
{
return new EndpointCatalogEntry
{
Key = name,
ArtifactType = "SendPort",
ApplicationName = "Orders",
ArtifactName = name,
TransportRole = "Primary",
Protocol = "TCP",
Host = host,
Port = 443,
Enabled = true,
AutoDiscovered = true
};
}
private static void SnapshotRejectsTampering()
{
WithTemporarySnapshot((path, store) =>
@@ -392,7 +557,7 @@ namespace BizTalkCheckmkPulse.Tests
var lines = new CheckmkLocalFormatter(new MonitoringOptions())
.FormatSnapshotFailure("Snapshot file does not exist.")
.ToArray();
AssertEqual(8, lines.Length, "snapshot failure stable service count");
AssertEqual(9, lines.Length, "snapshot failure stable service count");
Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "snapshot failure must be UNKNOWN");
Assert(lines.All(x => x.Contains("Scheduled Task")), "snapshot failure must contain provider action");
}