Add BizTalk endpoint reachability monitoring
This commit is contained in:
+71
-13
@@ -89,7 +89,7 @@ Ablauf:
|
|||||||
2. Tageslogs gemaess `LogRetentionDays` bereinigen.
|
2. Tageslogs gemaess `LogRetentionDays` bereinigen.
|
||||||
3. exklusives Handle auf `<SnapshotPath>.provider.lock` halten.
|
3. exklusives Handle auf `<SnapshotPath>.provider.lock` halten.
|
||||||
4. WMI-, SQL- und Event-Log-Probes ausfuehren.
|
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
|
6. Snapshot in einer eindeutigen Temporaerdatei desselben Verzeichnisses
|
||||||
schreiben.
|
schreiben.
|
||||||
7. `Flush(true)` ausfuehren und Temporaerdatei atomar publizieren.
|
7. `Flush(true)` ausfuehren und Temporaerdatei atomar publizieren.
|
||||||
@@ -132,7 +132,7 @@ Der Consumer:
|
|||||||
- gibt bei Erfolg ausschliesslich den validierten Payload aus.
|
- gibt bei Erfolg ausschliesslich den validierten Payload aus.
|
||||||
|
|
||||||
Er gibt immer Exitcode `0` zurueck, damit ein Fehler die komplette
|
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.
|
`UNKNOWN`-Zeilen und einen Eintrag im Consumer-Log.
|
||||||
|
|
||||||
## 4. Snapshot-Vertrag
|
## 4. Snapshot-Vertrag
|
||||||
@@ -210,7 +210,7 @@ Metrik ausgewiesen und zugleich in `suspended_nonresumable` mitgezaehlt.
|
|||||||
### 5.2 Kompaktes Checkmk-Servicebild
|
### 5.2 Kompaktes Checkmk-Servicebild
|
||||||
|
|
||||||
Die alte Sammelzeile `BizTalk Runtime Artifacts` wurde durch drei kurze
|
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`
|
1. `BizTalk Platform`
|
||||||
2. `BizTalk SQL Access`
|
2. `BizTalk SQL Access`
|
||||||
@@ -218,8 +218,9 @@ Services ersetzt. Insgesamt entstehen acht stabile Services:
|
|||||||
4. `BizTalk Host Instances`
|
4. `BizTalk Host Instances`
|
||||||
5. `BizTalk Receive Locations`
|
5. `BizTalk Receive Locations`
|
||||||
6. `BizTalk Send Ports`
|
6. `BizTalk Send Ports`
|
||||||
7. `BizTalk Orchestrations`
|
7. `BizTalk Endpoint Reachability`
|
||||||
8. `BizTalk Event Log`
|
8. `BizTalk Orchestrations`
|
||||||
|
9. `BizTalk Event Log`
|
||||||
|
|
||||||
Receive Locations und Send Ports alarmieren nur fuer unerwartete
|
Receive Locations und Send Ports alarmieren nur fuer unerwartete
|
||||||
Aus-Zustaende. Fachlich bewusst deaktivierte Namen werden exakt, ohne
|
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
|
begrenzt jede Detailausgabe, damit Checkmk-Ansichten kompakt bleiben, waehrend
|
||||||
alle numerischen Metriken vollstaendig erhalten bleiben.
|
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
|
Der Provider oeffnet fuer Management- und Master-MessageBox-Datenbank eine
|
||||||
kurze `System.Data.SqlClient`-Verbindung mit integrierter
|
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
|
`NT AUTHORITY\SYSTEM`. Der Check beweist, dass genau das Scheduled-Task-Konto
|
||||||
die Ziele erreichen kann.
|
die Ziele erreichen kann.
|
||||||
|
|
||||||
### 5.4 Event Log
|
### 5.5 Event Log
|
||||||
|
|
||||||
Der Provider liest das lokale Application Log im konfigurierten Zeitfenster
|
Der Provider liest das lokale Application Log im konfigurierten Zeitfenster
|
||||||
und filtert Quellen wie `BizTalk Server`, `XLANG/s`, `ENTSSO`,
|
und filtert Quellen wie `BizTalk Server`, `XLANG/s`, `ENTSSO`,
|
||||||
@@ -378,7 +418,7 @@ Snapshot nicht zusaetzlich verzoegert.
|
|||||||
Nach dem Agent-Dump:
|
Nach dem Agent-Dump:
|
||||||
|
|
||||||
1. Service Discovery fuer den BizTalk-Host ausfuehren.
|
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.
|
`BizTalk Runtime Artifacts` nach erfolgreicher Discovery entfernen.
|
||||||
3. Changes aktivieren.
|
3. Changes aktivieren.
|
||||||
4. Views/Benachrichtigungen nach Umgebung konfigurieren.
|
4. Views/Benachrichtigungen nach Umgebung konfigurieren.
|
||||||
@@ -485,6 +525,8 @@ Runtime:
|
|||||||
| `SnapshotMaxBytes` | `1048576` | 4096 bis 16777216 |
|
| `SnapshotMaxBytes` | `1048576` | 4096 bis 16777216 |
|
||||||
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | absolut |
|
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | absolut |
|
||||||
| `LogRetentionDays` | `30` | 1 bis 365 |
|
| `LogRetentionDays` | `30` | 1 bis 365 |
|
||||||
|
| `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | absolut |
|
||||||
|
| `EndpointCatalogMaxBytes` | `1048576` | 4096 bis 16777216 |
|
||||||
|
|
||||||
BizTalk-Probes:
|
BizTalk-Probes:
|
||||||
|
|
||||||
@@ -496,6 +538,11 @@ BizTalk-Probes:
|
|||||||
| `SqlConnectionTimeoutSeconds` | `5` |
|
| `SqlConnectionTimeoutSeconds` | `5` |
|
||||||
| `ProbeEventLog` | `true` |
|
| `ProbeEventLog` | `true` |
|
||||||
| `EventLogLookbackMinutes` | `60` |
|
| `EventLogLookbackMinutes` | `60` |
|
||||||
|
| `ProbeEndpointConnectivity` | `true` |
|
||||||
|
| `EndpointDiscoveryIntervalHours` | `168` |
|
||||||
|
| `EndpointProbeTimeoutMilliseconds` | `3000` |
|
||||||
|
| `EndpointProbeMaxConcurrency` | `12` |
|
||||||
|
| `EndpointMaxCount` | `500` |
|
||||||
|
|
||||||
Alarmierung:
|
Alarmierung:
|
||||||
|
|
||||||
@@ -519,7 +566,7 @@ Alarmierung:
|
|||||||
Automatisiert:
|
Automatisiert:
|
||||||
|
|
||||||
- Release-Build .NET Framework 4.7.2,
|
- Release-Build .NET Framework 4.7.2,
|
||||||
- exakt acht Self-Test-Services,
|
- exakt neun Self-Test-Services,
|
||||||
- unbekannte Quellen werden `UNKNOWN`,
|
- unbekannte Quellen werden `UNKNOWN`,
|
||||||
- dynamische Anwendungsservices nur bei bekannter Anwendung,
|
- dynamische Anwendungsservices nur bei bekannter Anwendung,
|
||||||
- WMI-Queryvertrag nutzt dokumentierte Properties,
|
- WMI-Queryvertrag nutzt dokumentierte Properties,
|
||||||
@@ -529,10 +576,13 @@ Automatisiert:
|
|||||||
- Receive-/Send-Allowlisten trennen bewusste von unerwarteten Aus-Zustaenden,
|
- Receive-/Send-Allowlisten trennen bewusste von unerwarteten Aus-Zustaenden,
|
||||||
- ein fehlender `IsDisabled`-Wert wird `UNKNOWN` statt still als enabled,
|
- ein fehlender `IsDisabled`-Wert wird `UNKNOWN` statt still als enabled,
|
||||||
- betroffene Namen und Gesamtlaenge der Summary bleiben begrenzt,
|
- 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,
|
- Snapshot-Roundtrip und Ersatz,
|
||||||
- SHA-256-Manipulation wird verworfen,
|
- SHA-256-Manipulation wird verworfen,
|
||||||
- Stale-Snapshot wird verworfen,
|
- Stale-Snapshot wird verworfen,
|
||||||
- Consumer-Fallback enthaelt acht `UNKNOWN`-Services.
|
- Consumer-Fallback enthaelt neun `UNKNOWN`-Services.
|
||||||
|
|
||||||
Windows-/ACC-Abnahme:
|
Windows-/ACC-Abnahme:
|
||||||
|
|
||||||
@@ -549,9 +599,17 @@ Windows-/ACC-Abnahme:
|
|||||||
9. Einen Testnamen aus der Allowlist entfernen und den erwarteten CRIT mit
|
9. Einen Testnamen aus der Allowlist entfernen und den erwarteten CRIT mit
|
||||||
kurzem `affected=`-Detail pruefen.
|
kurzem `affected=`-Detail pruefen.
|
||||||
10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben.
|
10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben.
|
||||||
11. Task deaktivieren: nach 180 Sekunden acht stale-`UNKNOWN`s.
|
11. `endpoints.xml` wurde erzeugt; sie enthaelt keine vollstaendigen URIs oder
|
||||||
12. Task wieder aktivieren: naechster Snapshot stellt Echtzustand her.
|
Zugangsdaten und nur Started-/Enabled-Artefakte.
|
||||||
13. Agent-Dump und Checkmk Service Discovery erfolgreich.
|
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.
|
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)
|
Checkmk Windows Agent (LocalSystem)
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
acht kompakte Checkmk Local Checks
|
neun kompakte Checkmk Local Checks
|
||||||
```
|
```
|
||||||
|
|
||||||
Damit bekommt `LocalSystem` keine BizTalk- oder SQL-Berechtigung. Nur das
|
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
|
Checkmk-Zeilen, korrekte SHA-256-Pruefsumme und ein maximales Alter von
|
||||||
standardmaessig 180 Sekunden.
|
standardmaessig 180 Sekunden.
|
||||||
- Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien
|
- 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
|
- Ein exklusives Lock und die Task-Einstellung `IgnoreNew` verhindern
|
||||||
ueberlappende Providerlaeufe.
|
ueberlappende Providerlaeufe.
|
||||||
- Ein unerwarteter Providerfehler erzeugt nach Moeglichkeit einen aktuellen
|
- Ein unerwarteter Providerfehler erzeugt nach Moeglichkeit einen aktuellen
|
||||||
@@ -82,6 +82,7 @@ Standardmaessig entstehen:
|
|||||||
- `BizTalk Host Instances`
|
- `BizTalk Host Instances`
|
||||||
- `BizTalk Receive Locations`
|
- `BizTalk Receive Locations`
|
||||||
- `BizTalk Send Ports`
|
- `BizTalk Send Ports`
|
||||||
|
- `BizTalk Endpoint Reachability`
|
||||||
- `BizTalk Orchestrations`
|
- `BizTalk Orchestrations`
|
||||||
- `BizTalk Event Log`
|
- `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
|
- Suspensions: total, resumable, non-resumable und Routing Failure Reports
|
||||||
- Receive Locations: total, enabled, unerwartet/bewusst disabled und unbekannt
|
- Receive Locations: total, enabled, unerwartet/bewusst disabled und unbekannt
|
||||||
- Send Ports: total, started, stopped, bound, unbekannt sowie bewusst inactive
|
- 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
|
- 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.
|
Pro Service werden standardmaessig maximal fuenf betroffene Namen gezeigt.
|
||||||
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusaetzlich auf
|
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusaetzlich auf
|
||||||
1600 Zeichen begrenzt. Metriken bleiben trotzdem vollstaendig.
|
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)
|
Beispielausgaben: [docs/ExampleOutput.md](docs/ExampleOutput.md)
|
||||||
|
|
||||||
|
Endpoint-Katalog, manuelle Overrides und Protokollgrenzen:
|
||||||
|
[docs/EndpointCatalog.md](docs/EndpointCatalog.md)
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
Build-Host:
|
Build-Host:
|
||||||
@@ -161,7 +182,7 @@ Format-Self-Test ohne WMI, SQL oder Event Log:
|
|||||||
artifacts\BizTalkCheckmkPulse-deploy\application\BizTalkCheckmkPulse.exe --self-test
|
artifacts\BizTalkCheckmkPulse-deploy\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,
|
zusaetzlich Snapshot-Roundtrip, atomaren Ersatz, SHA-256-Manipulation,
|
||||||
Stale-Erkennung, stabile Fallbacks und die bestehenden BizTalk-WMI-Diagnosen.
|
Stale-Erkennung, stabile Fallbacks und die bestehenden BizTalk-WMI-Diagnosen.
|
||||||
Ein Mono-Build ist eine hilfreiche Quellcodepruefung, ersetzt aber nicht die
|
Ein Mono-Build ist eine hilfreiche Quellcodepruefung, ersetzt aber nicht die
|
||||||
@@ -207,7 +228,7 @@ Konfigurationsdatei noch in den Logs.
|
|||||||
|
|
||||||
Wenn das Kennwort rotiert oder ablaeuft, muss es im Scheduled Task aktualisiert
|
Wenn das Kennwort rotiert oder ablaeuft, muss es im Scheduled Task aktualisiert
|
||||||
werden. Bis dahin wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt
|
werden. Bis dahin wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt
|
||||||
alle acht Services als `UNKNOWN`.
|
alle neun Services als `UNKNOWN`.
|
||||||
|
|
||||||
Ein gMSA kann weiterhin optional mit `-Gmsa` installiert werden; die
|
Ein gMSA kann weiterhin optional mit `-Gmsa` installiert werden; die
|
||||||
produktive Standardbeschreibung geht vom normalen Servicekonto aus.
|
produktive Standardbeschreibung geht vom normalen Servicekonto aus.
|
||||||
@@ -268,7 +289,7 @@ Verbindlicher Test im echten `LocalSystem`-Kontext:
|
|||||||
Select-String -Pattern "BizTalk|UNKNOWN|Snapshot|Permission" -Context 0,1
|
Select-String -Pattern "BizTalk|UNKNOWN|Snapshot|Permission" -Context 0,1
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
aufnehmen und Changes aktivieren. Ein zusaetzlicher Checkmk-Async-Cache ist
|
||||||
nicht erforderlich: Der Consumer liest nur eine kleine lokale Datei und der
|
nicht erforderlich: Der Consumer liest nur eine kleine lokale Datei und der
|
||||||
Provider besitzt bereits seinen eigenen Minutentakt.
|
Provider besitzt bereits seinen eigenen Minutentakt.
|
||||||
@@ -284,6 +305,7 @@ Provider besitzt bereits seinen eigenen Minutentakt.
|
|||||||
data\
|
data\
|
||||||
biztalk-checkmk-pulse.snapshot
|
biztalk-checkmk-pulse.snapshot
|
||||||
biztalk-checkmk-pulse.snapshot.provider.lock
|
biztalk-checkmk-pulse.snapshot.provider.lock
|
||||||
|
endpoints.xml
|
||||||
logs\
|
logs\
|
||||||
biztalk-checkmk-pulse-YYYYMMDD.log
|
biztalk-checkmk-pulse-YYYYMMDD.log
|
||||||
|
|
||||||
@@ -321,6 +343,12 @@ Wichtige Werte:
|
|||||||
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegroesse fuer den Consumer. |
|
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegroesse fuer den Consumer. |
|
||||||
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. |
|
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. |
|
||||||
| `LogRetentionDays` | `30` | Provider bereinigt aeltere Logs. |
|
| `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. |
|
| `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. |
|
||||||
| `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. |
|
| `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. |
|
||||||
| `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. |
|
| `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. |
|
||||||
@@ -342,11 +370,13 @@ liest den naechsten atomar publizierten Snapshot.
|
|||||||
|
|
||||||
| Beobachtung | Ursache / Massnahme |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema pruefen; keine Rechte ausweiten. |
|
||||||
| Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos pruefen. |
|
| 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. |
|
| Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL pruefen. |
|
||||||
@@ -388,6 +418,8 @@ Die konkrete Datei und SHA-256-Summe werden bei der Uebergabe genannt.
|
|||||||
- Microsoft: BizTalk `MSBTS_GroupSetting.BizTalkReadOnlyUserGroup`
|
- Microsoft: BizTalk `MSBTS_GroupSetting.BizTalkReadOnlyUserGroup`
|
||||||
- Microsoft: Windows Groups and User Accounts in BizTalk Server
|
- Microsoft: Windows Groups and User Accounts in BizTalk Server
|
||||||
- Microsoft: Managing BizTalk Server Security
|
- 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
|
- Checkmk: Windows Agent und Local Checks
|
||||||
|
|
||||||
Die genauen Links stehen in [Dokumentation.md](Dokumentation.md).
|
Die genauen Links stehen in [Dokumentation.md](Dokumentation.md).
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ else {
|
|||||||
|
|
||||||
Write-Host "Fuehre formatseitigen Self-Test aus ..."
|
Write-Host "Fuehre formatseitigen Self-Test aus ..."
|
||||||
$selfTest = & $targetExe --self-test
|
$selfTest = & $targetExe --self-test
|
||||||
if ($LASTEXITCODE -ne 0 -or @($selfTest).Count -ne 8) {
|
if ($LASTEXITCODE -ne 0 -or @($selfTest).Count -ne 9) {
|
||||||
throw "Self-Test fehlgeschlagen. Erwartet wurden acht Checkmk-Zeilen."
|
throw "Self-Test fehlgeschlagen. Erwartet wurden neun Checkmk-Zeilen."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Starte den Provider einmalig ..."
|
Write-Host "Starte den Provider einmalig ..."
|
||||||
|
|||||||
+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-
|
Die fachlichen Daten werden minuetlich vom privilegierten Scheduled-Task-
|
||||||
Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den
|
Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den
|
||||||
integritaets- und altersgeprueften Snapshot. Ist dieser Transport nicht
|
integritaets- und altersgeprueften Snapshot. Ist dieser Transport nicht
|
||||||
verfuegbar, liefern alle acht stabilen Services `UNKNOWN`.
|
verfuegbar, liefern alle neun stabilen Services `UNKNOWN`.
|
||||||
|
|
||||||
## BizTalk Platform
|
## BizTalk Platform
|
||||||
|
|
||||||
@@ -174,6 +174,46 @@ Metriken:
|
|||||||
| `biztalk_send_ports_expected_inactive` | Bewusst inactive und exakt allowlisted. |
|
| `biztalk_send_ports_expected_inactive` | Bewusst inactive und exakt allowlisted. |
|
||||||
| `biztalk_send_ports_unknown` | Nicht interpretierbarer Send-Port-Status. |
|
| `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
|
## BizTalk Orchestrations
|
||||||
|
|
||||||
Status:
|
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 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 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 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 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.
|
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
|
Die Zaehler und Performance-Daten enthalten weiterhin alle 22 betroffenen
|
||||||
Artefakte. Nur die Textliste ist begrenzt.
|
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
|
## Fehlender oder veralteter Provider-Snapshot
|
||||||
|
|
||||||
Alle acht Services werden `UNKNOWN`, zum Beispiel:
|
Alle neun Services werden `UNKNOWN`, zum Beispiel:
|
||||||
|
|
||||||
```text
|
```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.
|
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
@@ -61,7 +61,7 @@ Verbindlicher Agent-Dump:
|
|||||||
2. `LastTaskResult=0`, frischen Snapshot und Log pruefen.
|
2. `LastTaskResult=0`, frischen Snapshot und Log pruefen.
|
||||||
3. Agent-Dump im `LocalSystem`-Kontext pruefen.
|
3. Agent-Dump im `LocalSystem`-Kontext pruefen.
|
||||||
4. Service Discovery fuer den BizTalk-Host ausfuehren.
|
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.
|
`BizTalk Runtime Artifacts`-Service entfernen.
|
||||||
6. Changes aktivieren.
|
6. Changes aktivieren.
|
||||||
7. Views, Servicegruppen und Benachrichtigungen einrichten.
|
7. Views, Servicegruppen und Benachrichtigungen einrichten.
|
||||||
@@ -111,11 +111,13 @@ Soll:
|
|||||||
- SQL Access zeigt `targets=2`, `available=2`.
|
- SQL Access zeigt `targets=2`, `available=2`.
|
||||||
- Suspended Instances zeigt total/resumable/non-resumable/routing failures.
|
- Suspended Instances zeigt total/resumable/non-resumable/routing failures.
|
||||||
- Receive Locations und Send Ports zeigen getrennte expected/unexpected Werte.
|
- 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.
|
- keine berechtigungsbedingten `UNKNOWN`-Services.
|
||||||
|
|
||||||
## Alarmierung der Transportkette
|
## 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
|
sichtbar. Die Summary nennt fehlenden, unlesbaren, ungueltigen oder stale
|
||||||
Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso
|
Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso
|
||||||
eskaliert werden wie ein technischer Monitoringausfall.
|
eskaliert werden wie ein technischer Monitoringausfall.
|
||||||
|
|||||||
@@ -14,6 +14,16 @@
|
|||||||
<add key="LogDirectory" value="%ProgramData%\BizTalkCheckmkPulse\logs" />
|
<add key="LogDirectory" value="%ProgramData%\BizTalkCheckmkPulse\logs" />
|
||||||
<add key="LogRetentionDays" value="30" />
|
<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" />
|
<add key="QueryTimeoutSeconds" value="25" />
|
||||||
<!-- Testet die Anmeldung des privilegierten Provider-Kontos an den ermittelten BizTalk-Datenbanken. -->
|
<!-- Testet die Anmeldung des privilegierten Provider-Kontos an den ermittelten BizTalk-Datenbanken. -->
|
||||||
<add key="ProbeSqlConnectivity" value="true" />
|
<add key="ProbeSqlConnectivity" value="true" />
|
||||||
|
|||||||
@@ -38,11 +38,16 @@
|
|||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
<Reference Include="System.Management" />
|
<Reference Include="System.Management" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="CheckmkLocalFormatter.cs" />
|
<Compile Include="CheckmkLocalFormatter.cs" />
|
||||||
<Compile Include="EventLogProbe.cs" />
|
<Compile Include="EventLogProbe.cs" />
|
||||||
|
<Compile Include="EndpointAddressParser.cs" />
|
||||||
|
<Compile Include="EndpointCatalogStore.cs" />
|
||||||
|
<Compile Include="EndpointConnectivityProbe.cs" />
|
||||||
<Compile Include="FileLogger.cs" />
|
<Compile Include="FileLogger.cs" />
|
||||||
<Compile Include="MonitoringOptions.cs" />
|
<Compile Include="MonitoringOptions.cs" />
|
||||||
<Compile Include="Models.cs" />
|
<Compile Include="Models.cs" />
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ namespace BizTalkCheckmkPulse
|
|||||||
yield return FormatHostInstances(result);
|
yield return FormatHostInstances(result);
|
||||||
yield return FormatReceiveLocations(result);
|
yield return FormatReceiveLocations(result);
|
||||||
yield return FormatSendPorts(result);
|
yield return FormatSendPorts(result);
|
||||||
|
yield return FormatEndpointConnectivity(result);
|
||||||
yield return FormatOrchestrations(result);
|
yield return FormatOrchestrations(result);
|
||||||
yield return FormatEventLog(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("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("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("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("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.");
|
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("Host Instances"), "-", action);
|
||||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Receive Locations"), "-", 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("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("Orchestrations"), "-", action);
|
||||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", 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("Host Instances"), "-", detail);
|
||||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Receive Locations"), "-", 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("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("Orchestrations"), "-", detail);
|
||||||
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", 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());
|
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)
|
private string FormatOrchestrations(ProbeResult result)
|
||||||
{
|
{
|
||||||
if (!result.Platform.OrchestrationsDataAvailable)
|
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>();
|
Applications = new List<ApplicationRuntimeState>();
|
||||||
SqlTargets = new List<SqlAccessState>();
|
SqlTargets = new List<SqlAccessState>();
|
||||||
EventLog = new EventLogState();
|
EventLog = new EventLogState();
|
||||||
|
EndpointCandidates = new List<EndpointCandidate>();
|
||||||
|
EndpointConnectivity = new EndpointConnectivityState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ProbeDiagnostic> Diagnostics { get; private set; }
|
public List<ProbeDiagnostic> Diagnostics { get; private set; }
|
||||||
@@ -114,6 +116,8 @@ namespace BizTalkCheckmkPulse
|
|||||||
public string ExecutionIdentity { get; set; }
|
public string ExecutionIdentity { get; set; }
|
||||||
public string NetworkIdentityHint { get; set; }
|
public string NetworkIdentityHint { get; set; }
|
||||||
public EventLogState EventLog { get; private set; }
|
public EventLogState EventLog { get; private set; }
|
||||||
|
public List<EndpointCandidate> EndpointCandidates { get; private set; }
|
||||||
|
public EndpointConnectivityState EndpointConnectivity { get; private set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -236,4 +240,116 @@ namespace BizTalkCheckmkPulse
|
|||||||
public DateTime Since { get; set; }
|
public DateTime Since { get; set; }
|
||||||
public string Failure { 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 int SnapshotMaxBytes { get; set; }
|
||||||
public string LogDirectory { get; set; }
|
public string LogDirectory { get; set; }
|
||||||
public int LogRetentionDays { 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 Collect { get; set; }
|
||||||
public bool SelfTest { get; set; }
|
public bool SelfTest { get; set; }
|
||||||
|
|
||||||
@@ -73,6 +80,13 @@ namespace BizTalkCheckmkPulse
|
|||||||
SnapshotMaxBytes = 1048576;
|
SnapshotMaxBytes = 1048576;
|
||||||
LogDirectory = Path.Combine(commonData, "BizTalkCheckmkPulse", "logs");
|
LogDirectory = Path.Combine(commonData, "BizTalkCheckmkPulse", "logs");
|
||||||
LogRetentionDays = 30;
|
LogRetentionDays = 30;
|
||||||
|
ProbeEndpointConnectivity = true;
|
||||||
|
EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml");
|
||||||
|
EndpointCatalogMaxBytes = 1048576;
|
||||||
|
EndpointDiscoveryIntervalHours = 168;
|
||||||
|
EndpointProbeTimeoutMilliseconds = 3000;
|
||||||
|
EndpointProbeMaxConcurrency = 12;
|
||||||
|
EndpointMaxCount = 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -124,6 +138,13 @@ namespace BizTalkCheckmkPulse
|
|||||||
options.SnapshotMaxBytes = ReadInt(settings, "SnapshotMaxBytes", options.SnapshotMaxBytes, 4096, 16777216);
|
options.SnapshotMaxBytes = ReadInt(settings, "SnapshotMaxBytes", options.SnapshotMaxBytes, 4096, 16777216);
|
||||||
options.LogDirectory = Environment.ExpandEnvironmentVariables(ReadString(settings, "LogDirectory", options.LogDirectory));
|
options.LogDirectory = Environment.ExpandEnvironmentVariables(ReadString(settings, "LogDirectory", options.LogDirectory));
|
||||||
options.LogRetentionDays = ReadInt(settings, "LogRetentionDays", options.LogRetentionDays, 1, 365);
|
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]);
|
ApplyArguments(options, args ?? new string[0]);
|
||||||
if (!options.SelfTest)
|
if (!options.SelfTest)
|
||||||
@@ -192,6 +213,19 @@ namespace BizTalkCheckmkPulse
|
|||||||
{
|
{
|
||||||
throw new ConfigurationErrorsException("LogDirectory must be an absolute path.");
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace BizTalkCheckmkPulse
|
|||||||
using (new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
|
using (new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
|
||||||
{
|
{
|
||||||
logger.Info("Collection started. snapshot=" + options.SnapshotPath);
|
logger.Info("Collection started. snapshot=" + options.SnapshotPath);
|
||||||
var result = Collect(options);
|
var result = Collect(options, logger);
|
||||||
foreach (var diagnostic in result.Diagnostics)
|
foreach (var diagnostic in result.Diagnostics)
|
||||||
{
|
{
|
||||||
logger.Warning("Probe diagnostic: " + diagnostic.ToDisplayText());
|
logger.Warning("Probe diagnostic: " + diagnostic.ToDisplayText());
|
||||||
@@ -98,6 +98,10 @@ namespace BizTalkCheckmkPulse
|
|||||||
+ result.SendPorts.Count
|
+ result.SendPorts.Count
|
||||||
+ " send_ports_not_started="
|
+ " send_ports_not_started="
|
||||||
+ result.SendPorts.Count(x => x.Status != 3)
|
+ result.SendPorts.Count(x => x.Status != 3)
|
||||||
|
+ " endpoints_active="
|
||||||
|
+ result.EndpointConnectivity.Active
|
||||||
|
+ " endpoints_failed="
|
||||||
|
+ result.EndpointConnectivity.Results.Count(x => !x.Available)
|
||||||
+ " elapsed_ms="
|
+ " elapsed_ms="
|
||||||
+ stopwatch.ElapsedMilliseconds);
|
+ stopwatch.ElapsedMilliseconds);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -155,11 +159,12 @@ namespace BizTalkCheckmkPulse
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProbeResult Collect(MonitoringOptions options)
|
private static ProbeResult Collect(MonitoringOptions options, FileLogger logger)
|
||||||
{
|
{
|
||||||
var result = new ProbeResult();
|
var result = new ProbeResult();
|
||||||
new WmiBizTalkProbe(options).Query(result);
|
new WmiBizTalkProbe(options).Query(result);
|
||||||
new SqlConnectivityProbe(options).Query(result);
|
new SqlConnectivityProbe(options).Query(result);
|
||||||
|
new EndpointConnectivityProbe(options, logger).Query(result);
|
||||||
|
|
||||||
if (options.ProbeEventLog)
|
if (options.ProbeEventLog)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ using System.Reflection;
|
|||||||
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
|
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
|
||||||
[assembly: AssemblyCompany("BEW")]
|
[assembly: AssemblyCompany("BEW")]
|
||||||
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
||||||
[assembly: AssemblyVersion("2.1.0.0")]
|
[assembly: AssemblyVersion("2.2.0.0")]
|
||||||
[assembly: AssemblyFileVersion("2.1.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,
|
ApplicationName = applicationName,
|
||||||
IsDisabled = isDisabled
|
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);
|
var app = GetApplication(apps, applicationName);
|
||||||
app.ReceiveLocationTotal++;
|
app.ReceiveLocationTotal++;
|
||||||
if (isDisabled == true)
|
if (isDisabled == true)
|
||||||
@@ -241,6 +252,33 @@ namespace BizTalkCheckmkPulse
|
|||||||
ApplicationName = applicationName,
|
ApplicationName = applicationName,
|
||||||
Status = status
|
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);
|
var app = GetApplication(apps, applicationName);
|
||||||
app.SendPortTotal++;
|
app.SendPortTotal++;
|
||||||
switch (status)
|
switch (status)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
Run("UnknownReceiveLocationStateIsNotGreen", UnknownReceiveLocationStateIsNotGreen);
|
Run("UnknownReceiveLocationStateIsNotGreen", UnknownReceiveLocationStateIsNotGreen);
|
||||||
Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState);
|
Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState);
|
||||||
Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded);
|
Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded);
|
||||||
|
Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets);
|
||||||
|
Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides);
|
||||||
|
Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip);
|
||||||
|
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
|
||||||
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
|
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
|
||||||
Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
|
Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
|
||||||
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
|
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
|
||||||
@@ -52,19 +56,20 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
private static void SelfTestEmitsAllStableServices()
|
private static void SelfTestEmitsAllStableServices()
|
||||||
{
|
{
|
||||||
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
|
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
|
||||||
AssertEqual(8, lines.Length, "self-test line count");
|
AssertEqual(9, lines.Length, "self-test line count");
|
||||||
AssertEqual(8, lines.Distinct(StringComparer.Ordinal).Count(), "unique self-test lines");
|
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.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 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 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 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");
|
Assert(lines.Any(x => x.Contains("\"BizTalk Orchestrations\"")), "Orchestrations service missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UnavailableSourcesAreUnknown()
|
private static void UnavailableSourcesAreUnknown()
|
||||||
{
|
{
|
||||||
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).Format(new ProbeResult()).Take(8).ToArray();
|
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).Format(new ProbeResult()).Take(9).ToArray();
|
||||||
AssertEqual(8, lines.Length, "stable service count");
|
AssertEqual(9, lines.Length, "stable service count");
|
||||||
Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "unavailable sources must be UNKNOWN");
|
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 options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true };
|
||||||
var result = CreateSuspensionResult("(unknown)");
|
var result = CreateSuspensionResult("(unknown)");
|
||||||
var lines = new CheckmkLocalFormatter(options).Format(result).ToArray();
|
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()
|
private static void KnownApplicationCreatesService()
|
||||||
@@ -81,7 +86,7 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true };
|
var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true };
|
||||||
var result = CreateSuspensionResult("Orders");
|
var result = CreateSuspensionResult("Orders");
|
||||||
var lines = new CheckmkLocalFormatter(options).Format(result).ToArray();
|
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");
|
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()
|
private static void SnapshotRejectsTampering()
|
||||||
{
|
{
|
||||||
WithTemporarySnapshot((path, store) =>
|
WithTemporarySnapshot((path, store) =>
|
||||||
@@ -392,7 +557,7 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
var lines = new CheckmkLocalFormatter(new MonitoringOptions())
|
var lines = new CheckmkLocalFormatter(new MonitoringOptions())
|
||||||
.FormatSnapshotFailure("Snapshot file does not exist.")
|
.FormatSnapshotFailure("Snapshot file does not exist.")
|
||||||
.ToArray();
|
.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.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "snapshot failure must be UNKNOWN");
|
||||||
Assert(lines.All(x => x.Contains("Scheduled Task")), "snapshot failure must contain provider action");
|
Assert(lines.All(x => x.Contains("Scheduled Task")), "snapshot failure must contain provider action");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user