Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22a4725056 | ||
|
|
91e0db5619 |
+71
-13
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user