Harden endpoint reachability resolution

This commit is contained in:
2026-08-06 15:58:18 +02:00
parent 82584473e8
commit ac51714a88
19 changed files with 556 additions and 152 deletions
+1 -2
View File
@@ -8,6 +8,5 @@ indent_style = space
indent_size = 4 indent_size = 4
[*.md] [*.md]
end_of_line = lf end_of_line = crlf
trim_trailing_whitespace = false trim_trailing_whitespace = false
+1
View File
@@ -0,0 +1 @@
* text=auto eol=crlf
+20 -12
View File
@@ -246,7 +246,11 @@ minütlich abgefragten Instanzen werden nur dokumentierte Properties gelesen:
Nur `Status=Started` beziehungsweise `IsDisabled=false` wird geprüft. Nur `Status=Started` beziehungsweise `IsDisabled=false` wird geprüft.
Inaktive Artefakte bleiben aus der Probe ausgeschlossen, auch wenn sie noch im Inaktive Artefakte bleiben aus der Probe ausgeschlossen, auch wenn sie noch im
Katalog stehen. Dynamische Send Ports, lokale Dateipfade, SMTP-Empfängerlisten Katalog stehen. Dynamische Send Ports, lokale Dateipfade, SMTP-Empfängerlisten
und Named Pipes werden nicht als externe Host/Port-Ziele behandelt. und Named Pipes sowie relative HTTP-/WCF-Receive-Listener werden als erwartbar
nicht socket-prüfbar behandelt und erzeugen kein `UNKNOWN`. WCF-SQL-Adressen
im Format `mssql://server[:port]/instanz/datenbank` werden auf TCP 1433 oder
den expliziten Port reduziert. Echte Auflösungslücken bleiben sichtbar und
nennen Artefakt, Adapter und Grund, aber niemals die vollständige URI.
Der lokale Katalog Der lokale Katalog
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` wird beim ersten `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` wird beim ersten
@@ -380,9 +384,7 @@ scripts\package-release.cmd
Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen
Installer und Packaging-Werkzeug. Danach liegen der Paketordner Installer und Packaging-Werkzeug. Danach liegen der Paketordner
`artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare `artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare
`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit. Zusätzlich entstehen `artifacts\BizTalkCheckmkPulse-Setup.zip` bereit.
`BizTalkCheckmkPulse-Setup.zip.b64.txt` für `certutil -decode` und eine
SHA-256-Textdatei.
### 8.2 Normales Servicekonto ### 8.2 Normales Servicekonto
@@ -553,7 +555,8 @@ BizTalk-Probes:
| `EndpointDiscoveryIntervalHours` | `168` | | `EndpointDiscoveryIntervalHours` | `168` |
| `EndpointProbeTimeoutMilliseconds` | `3000` | | `EndpointProbeTimeoutMilliseconds` | `3000` |
| `EndpointProbeMaxConcurrency` | `16` | | `EndpointProbeMaxConcurrency` | `16` |
| `EndpointMaxCount` | `100` | | `EndpointCatalogMaxEntries` | `1000` |
| `EndpointMaxCount` | `100` eindeutige Netzwerkziele |
Alarmierung: Alarmierung:
@@ -589,7 +592,8 @@ Automatisiert:
- betroffene Namen und Gesamtlänge der Summary bleiben begrenzt, - betroffene Namen und Gesamtlänge der Summary bleiben begrenzt,
- Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert, - Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert,
- manuelle Endpoint-Overrides überleben den automatischen Wochenabgleich, - manuelle Endpoint-Overrides überleben den automatischen Wochenabgleich,
- nur nicht erreichbare Endpoints erscheinen im Checkmk-Detail, - nur nicht erreichbare oder wirklich unaufgelöste Endpoints erscheinen im
Checkmk-Detail,
- 70 vollständig timeoutende eindeutige Ziele bleiben mit den Defaults in - 70 vollständig timeoutende eindeutige Ziele bleiben mit den Defaults in
einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte
Maximum von 100 Zielen in etwa 21 Sekunden, Maximum von 100 Zielen in etwa 21 Sekunden,
@@ -616,15 +620,17 @@ Windows-/ACC-Abnahme:
10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben. 10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben.
11. `endpoints.xml` wurde erzeugt; sie enthält keine vollständigen URIs oder 11. `endpoints.xml` wurde erzeugt; sie enthält keine vollständigen URIs oder
Zugangsdaten und nur Started-/Enabled-Artefakte. Zugangsdaten und nur Started-/Enabled-Artefakte.
12. Einen erreichbaren Testport sperren: nur `Endpoint Reachability` wird CRIT 12. `Endpoint Reachability` zeigt `unresolved=0`; erwartbar lokale oder
adapterinterne Adressen stehen nur unter `expected_non_socket` und bleiben OK.
13. Einen erreichbaren Testport sperren: nur `Endpoint Reachability` wird CRIT
und nennt ausschließlich das nicht erreichbare Ziel. und nennt ausschließlich das nicht erreichbare Ziel.
13. Ein Artefakt stoppen/deaktivieren: es wird im nächsten Minutenlauf nicht 14. Ein Artefakt stoppen/deaktivieren: es wird im nächsten Minutenlauf nicht
mehr geprüft. mehr geprüft.
14. Katalogzeit auf älter als 168 Stunden setzen beziehungsweise Intervall 15. Katalogzeit auf älter als 168 Stunden setzen beziehungsweise Intervall
testweise reduzieren: erfolgreicher atomarer Umgebungsabgleich. testweise reduzieren: erfolgreicher atomarer Umgebungsabgleich.
15. Task deaktivieren: nach 180 Sekunden neun stale-`UNKNOWN`s. 16. Task deaktivieren: nach 180 Sekunden neun stale-`UNKNOWN`s.
16. Task wieder aktivieren: nächster Snapshot stellt Echtzustand her. 17. Task wieder aktivieren: nächster Snapshot stellt Echtzustand her.
17. Agent-Dump und Checkmk Service Discovery erfolgreich. 18. 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.
@@ -650,6 +656,8 @@ Collector-Konto umgestellt werden.
- https://learn.microsoft.com/en-us/biztalk/core/types-of-message-failures - https://learn.microsoft.com/en-us/biztalk/core/types-of-message-failures
- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-isdisabled-property-wmi - https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-isdisabled-property-wmi
- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi - https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi
- https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sql/create-the-sql-server-connection-uri
- https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-oracle-database/create-the-oracle-database-connection-uri
- https://docs.checkmk.com/latest/en/agent_windows.html - https://docs.checkmk.com/latest/en/agent_windows.html
- https://docs.checkmk.com/latest/en/localchecks.html - https://docs.checkmk.com/latest/en/localchecks.html
- https://docs.checkmk.com/latest/en/spool_directory.html - https://docs.checkmk.com/latest/en/spool_directory.html
+17 -16
View File
@@ -108,16 +108,24 @@ Zustand und Graphen direkt erkennbar, ohne eine lange Sammelzeile zu lesen:
- 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 - Endpoint Reachability: nur aktive Send-/Receive-Artefakte; im OK-Fall nur
eine Gesamtaussage, im Fehlerfall ausschließlich nicht erreichbare Ziele eine Gesamtaussage, im Fehlerfall ausschließlich nicht erreichbare oder
wirklich unaufgelöste Ziele
- Orchestrations: total, started, stopped, bound, unbound und unbekannt - Orchestrations: total, started, stopped, bound, unbound und unbekannt
Der Provider nutzt für die Endpoint-Discovery keine neue WMI-Klasse. Er liest Der Provider nutzt für die Endpoint-Discovery keine neue WMI-Klasse. Er liest
`PTAddress`, `STAddress`, `PTTransportType`, `STTransportType`, `PTAddress`, `STAddress`, `PTTransportType`, `STTransportType`,
`InboundTransportURL` und `AdapterName` aus den bereits vorhandenen `InboundTransportURL` und `AdapterName` aus den bereits vorhandenen
`MSBTS_SendPort`-/`MSBTS_ReceiveLocation`-Abfragen. Der Check selbst ist ein `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 reiner Host/Port-Test: HTTP(S), SFTP, FTP, WCF/net.tcp, WCF-SQL/mssql und UNC
geprüft; explizite `udp://`-Ziele per UDP-Datagramm. Es werden keine werden per TCP geprüft; explizite `udp://`-Ziele per UDP-Datagramm. Es werden
HTTP-Requests, Anmeldungen oder fachlichen Nachrichten gesendet. keine HTTP-Requests, Anmeldungen oder fachlichen Nachrichten gesendet.
Nicht jede aktive BizTalk-Adresse ist selbst ein Socket-Ziel. Dynamische Send
Ports, lokale Pfade, SMTP-Empfänger, Named Pipes und relative HTTP-/WCF-
Receive-Listener werden deshalb als erwartbar nicht socket-prüfbar behandelt
und erzeugen kein `UNKNOWN`. Nur eine aktive statische Adresse, die tatsächlich
ein Netzwerkziel enthalten müsste und nicht sicher aufgelöst werden kann,
bleibt `UNKNOWN` und wird geheimnisfrei mit Artefakt, Adapter und Grund genannt.
Der geheimnisfreie Katalog liegt unter Der geheimnisfreie Katalog liegt unter
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml`. Fehlt er, wird er beim `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml`. Fehlt er, wird er beim
@@ -185,8 +193,6 @@ und das transportierbare ZIP:
```text ```text
artifacts\ artifacts\
BizTalkCheckmkPulse-Setup.zip BizTalkCheckmkPulse-Setup.zip
BizTalkCheckmkPulse-Setup.zip.b64.txt
BizTalkCheckmkPulse-Setup.zip.sha256.txt
BizTalkCheckmkPulse-Setup\ BizTalkCheckmkPulse-Setup\
Setup.exe Setup.exe
INSTALLATION.md INSTALLATION.md
@@ -200,14 +206,6 @@ Das Packaging-Projekt ist Teil von `BizTalkCheckmkPulse.sln`. Ein normaler
`Release`-Build in Visual Studio 2019 genügt daher; ein separates `Release`-Build in Visual Studio 2019 genügt daher; ein separates
PowerShell-/WiX-/MSI-Tooling wird nicht benötigt. PowerShell-/WiX-/MSI-Tooling wird nicht benötigt.
Die Base64-TXT kann auf dem Windows-Zielsystem ohne Zusatzwerkzeug dekodiert
werden:
```cmd
certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip
certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256
```
Format-Self-Test ohne WMI, SQL oder Event Log: Format-Self-Test ohne WMI, SQL oder Event Log:
```cmd ```cmd
@@ -362,10 +360,11 @@ Wichtige Werte:
| `LogRetentionDays` | `30` | Provider bereinigt ältere Logs. | | `LogRetentionDays` | `30` | Provider bereinigt ältere Logs. |
| `ProbeEndpointConnectivity` | `true` | Aktiviert den aggregierten TCP-/UDP-Netzwerkcheck. | | `ProbeEndpointConnectivity` | `true` | Aktiviert den aggregierten TCP-/UDP-Netzwerkcheck. |
| `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollständige URIs/Secrets. | | `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollständige URIs/Secrets. |
| `EndpointCatalogMaxEntries` | `1000` | Harte Größenbegrenzung für Artefakte im Katalog. |
| `EndpointDiscoveryIntervalHours` | `168` | Intervall für den vollständigen Umgebungsabgleich. | | `EndpointDiscoveryIntervalHours` | `168` | Intervall für den vollständigen Umgebungsabgleich. |
| `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. | | `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. |
| `EndpointProbeMaxConcurrency` | `16` | Begrenzte parallele Socket-Probes. | | `EndpointProbeMaxConcurrency` | `16` | Begrenzte parallele Socket-Probes. |
| `EndpointMaxCount` | `100` | Harte Obergrenze und Laufzeitbudget; bei 3 s Timeout theoretisch maximal etwa 21 s. | | `EndpointMaxCount` | `100` | Obergrenze eindeutiger Host/Port/Protokoll-Ziele und damit Laufzeitbudget; bei 3 s Timeout theoretisch maximal etwa 21 s. |
| `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. | | `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. |
@@ -393,7 +392,7 @@ liest den nächsten atomar publizierten Snapshot.
| Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` prüfen. | | Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` prüfen. |
| Receive Locations / Send Ports sind CRIT | `affected=` prüfen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. | | Receive Locations / Send Ports sind CRIT | `affected=` prüfen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. |
| Endpoint Reachability ist CRIT | Nur `unavailable=` prüfen; DNS, Zielport, Firewall und externen Dienst kontrollieren. | | Endpoint Reachability ist CRIT | Nur `unavailable=` prüfen; DNS, Zielport, Firewall und externen Dienst kontrollieren. |
| Endpoint Reachability ist UNKNOWN | WMI-Vollständigkeit, `endpoints.xml`, wöchentlichen Refresh und nicht automatisch auflösbare externe Adapteradresse prüfen. | | Endpoint Reachability ist UNKNOWN | `unresolved_endpoints=`, WMI-Vollständigkeit, `endpoints.xml` und wöchentlichen Refresh prüfen; indirekte Adapterziele bei Bedarf geheimnisfrei manuell überschreiben. |
| `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema prüfen; keine Rechte ausweiten. | | `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema prüfen; keine Rechte ausweiten. |
| Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos prüfen. | | Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos prüfen. |
| Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL prüfen. | | Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL prüfen. |
@@ -416,6 +415,8 @@ geändert und muss separat durch AD-/BizTalk-Administration entfernt werden.
- 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_SendPort` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi)
- Microsoft: [`MSBTS_ReceiveLocation` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi) - Microsoft: [`MSBTS_ReceiveLocation` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi)
- Microsoft: [SQL-Adapter-Verbindungs-URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sql/create-the-sql-server-connection-uri)
- Microsoft: [Oracle-Database-Adapter-Verbindungs-URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-oracle-database/create-the-oracle-database-connection-uri)
- Checkmk: Windows Agent und Local Checks - Checkmk: Windows Agent und Local Checks
Die genauen Links stehen in [Dokumentation.md](Dokumentation.md). Die genauen Links stehen in [Dokumentation.md](Dokumentation.md).
+2 -7
View File
@@ -1,12 +1,5 @@
# Installation auf dem BizTalk-Server # Installation auf dem BizTalk-Server
Falls nur die TXT-Datei übertragen wurde:
```cmd
certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip
certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256
```
1. `BizTalkCheckmkPulse-Setup.zip` vollständig in ein lokales Verzeichnis entpacken. 1. `BizTalkCheckmkPulse-Setup.zip` vollständig in ein lokales Verzeichnis entpacken.
2. `Setup.exe` als lokaler Administrator starten und die UAC-Abfrage bestätigen. 2. `Setup.exe` als lokaler Administrator starten und die UAC-Abfrage bestätigen.
3. Das Collector-Konto im Format `DOMAIN\Benutzer` eingeben, zum Beispiel 3. Das Collector-Konto im Format `DOMAIN\Benutzer` eingeben, zum Beispiel
@@ -25,6 +18,8 @@ registriert wird.
Die früheren unveränderten Endpoint-Defaults `12` parallele Probes und Die früheren unveränderten Endpoint-Defaults `12` parallele Probes und
`EndpointMaxCount=500` werden beim Update auf `16` beziehungsweise `100` `EndpointMaxCount=500` werden beim Update auf `16` beziehungsweise `100`
migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten. migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten.
`EndpointMaxCount` begrenzt nun eindeutige Socket-Ziele statt Artefakte; der
neue Wert `EndpointCatalogMaxEntries=1000` begrenzt separat die Kataloggröße.
Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet. Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet.
Bei normalen Servicekonten prüft der Installer außerdem Kennwort und Bei normalen Servicekonten prüft der Installer außerdem Kennwort und
+8 -1
View File
@@ -182,6 +182,11 @@ Zweck:
`Status=Started`. `Status=Started`.
- Reduziert die Adresse auf Host/Port und führt keinen fachlichen - Reduziert die Adresse auf Host/Port und führt keinen fachlichen
HTTP-, FTP-, SFTP-, WCF- oder BizTalk-Aufruf aus. HTTP-, FTP-, SFTP-, WCF- oder BizTalk-Aufruf aus.
- Erkennt unter anderem Standard-URIs, UNC, MSMQ, `net.tcp`, WCF-SQL-
`mssql`-URIs und lokale HTTP-/WCF-Wildcard-Listener.
- Behandelt Adressen ohne eigenständiges Socket-Ziel (zum Beispiel lokale
Pfade, SMTP-Empfänger, Named Pipes und relative Receive-Listener) als
erwartbar ausgenommen und nicht als Abdeckungsfehler.
- Dedupliziert gemeinsame Host/Port-Ziele und prüft sie mit begrenzter - Dedupliziert gemeinsame Host/Port-Ziele und prüft sie mit begrenzter
Parallelität; das Ergebnis wird danach wieder den Artefakten zugeordnet. Parallelität; das Ergebnis wird danach wieder den Artefakten zugeordnet.
- Zeigt im gesunden Zustand nur `Alle n ... erreichbar`. Nur bei Fehlern - Zeigt im gesunden Zustand nur `Alle n ... erreichbar`. Nur bei Fehlern
@@ -206,7 +211,9 @@ Metriken:
| `biztalk_endpoints_tested` | Vollständig abgeschlossene Artefaktprobes. | | `biztalk_endpoints_tested` | Vollständig abgeschlossene Artefaktprobes. |
| `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. | | `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. |
| `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. | | `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. |
| `biztalk_endpoints_unsupported` | Aktive externe Adressen ohne sicher bestimmbaren Host/Port. | | `biztalk_endpoints_unresolved` | Aktive statische Netzwerkadressen ohne sicher bestimmbaren Host/Port; `UNKNOWN` ab 1, sofern kein manueller Override existiert. |
| `biztalk_endpoints_expected_non_socket` | Aktive Artefakte, deren Adresse erwartbar kein eigenständiges TCP-/UDP-Ziel enthält. |
| `biztalk_endpoints_manual_overrides` | Aktive Ziele, die bewusst über einen geheimnisfreien manuellen Katalogeintrag aufgelöst werden. |
| `biztalk_endpoints_inactive_skipped` | Konfigurierte Einträge, deren BizTalk-Artefakt aktuell nicht aktiv ist. | | `biztalk_endpoints_inactive_skipped` | Konfigurierte Einträge, deren BizTalk-Artefakt aktuell nicht aktiv ist. |
| `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. | | `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. |
+11 -4
View File
@@ -23,8 +23,11 @@ BizTalk-Runtimezustand gefiltert:
- dynamischer Send Port nie als statisches Ziel - dynamischer Send Port nie als statisches Ziel
Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr
geprüft. Neue Artefakte werden spätestens beim nächsten Wochenabgleich geprüft. Prüffähige aktuelle WMI-Adressen werden bei jedem Minutenlauf direkt
aufgenommen. verwendet; der Wochenabgleich pflegt nur den persistenten, geheimnisfreien
Katalog. Dadurch wird weder ein neues Ziel verzögert noch eine bis zu sieben
Tage alte Auto-Discovery-Adresse weiter geprüft. Manuelle Overrides gewinnen
weiterhin anhand ihres stabilen Artefaktschlüssels.
## Laufzeitbudget ## Laufzeitbudget
@@ -33,8 +36,12 @@ maximal `EndpointProbeMaxConcurrency=16` parallelen Verbindungen ausgeführt.
Bei `EndpointProbeTimeoutMilliseconds=3000` benötigen 70 vollständig Bei `EndpointProbeTimeoutMilliseconds=3000` benötigen 70 vollständig
timeoutende eindeutige Ziele theoretisch etwa 15 Sekunden. Die Standardgrenze timeoutende eindeutige Ziele theoretisch etwa 15 Sekunden. Die Standardgrenze
`EndpointMaxCount=100` begrenzt diesen Anteil eines Providerlaufs auf etwa `EndpointMaxCount=100` begrenzt diesen Anteil eines Providerlaufs auf etwa
21 Sekunden. Nicht prüfbare aktive BizTalk-Artefakte zählen nicht gegen diese 21 Sekunden. Die Grenze gilt für deduplizierte Host/Port/Protokoll-Ziele, nicht
Grenze; sie werden separat als `unsupported` gemeldet. für die Anzahl der darauf abgebildeten BizTalk-Artefakte. Der Katalog ist
separat über `EndpointCatalogMaxEntries=1000` begrenzt. Erwartbar nicht als
Socket prüfbare Artefakte zählen nicht gegen das Laufzeitbudget; echte
Auflösungslücken werden als `unresolved` mit einem geheimnisfreien Grund
gemeldet.
Checkmk selbst wartet nicht auf diese Verbindungen, sondern liest nur den Checkmk selbst wartet nicht auf diese Verbindungen, sondern liest nur den
zuletzt atomar geschriebenen Snapshot. Die reale ACC-Laufzeit ist nach dem zuletzt atomar geschriebenen Snapshot. Die reale ACC-Laufzeit ist nach dem
+2 -2
View File
@@ -61,13 +61,13 @@ Artefakte. Nur die Textliste ist begrenzt.
Gesund, ohne Auflistung aller Ziele: Gesund, ohne Auflistung aller Ziele:
```text ```text
0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unsupported=0;;1;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar (unique_targets=44, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z 0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unresolved=0;;1;0|biztalk_endpoints_expected_non_socket=12;;;0|biztalk_endpoints_manual_overrides=0;;;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar (unique_targets=44, expected_non_socket=12, manual_overrides=0, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z
``` ```
Fehlerhaft, nur mit nicht erreichbaren Zielen: Fehlerhaft, nur mit nicht erreichbaren Zielen:
```text ```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) 2 "BizTalk ACC Endpoint Reachability" ... Endpoint reachability active=61, tested=61, available=59, failed=2, unresolved=0, expected_non_socket=12, manual_overrides=0; unavailable=SendPort:Orders\SP_Order_API[Primary]->orders.example:443/TCP(TCP timeout), ReceiveLocation:Invoices\RL_SFTP[Inbound]->sftp.example:22/TCP(SocketError=ConnectionRefused)
``` ```
## Fehlender oder veralteter Provider-Snapshot ## Fehlender oder veralteter Provider-Snapshot
+1 -1
View File
@@ -108,7 +108,7 @@ Soll:
- 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 - Endpoint Reachability zeigt bei Erfolg nur die Gesamtzahl und bei Fehlern
ausschließlich nicht erreichbare Ziele. ausschließlich nicht erreichbare oder wirklich unaufgelöste Ziele.
- keine berechtigungsbedingten `UNKNOWN`-Services. - keine berechtigungsbedingten `UNKNOWN`-Services.
## Alarmierung der Transportkette ## Alarmierung der Transportkette
+1 -1
View File
@@ -52,7 +52,7 @@
<g filter="url(#shadow)"> <g filter="url(#shadow)">
<rect x="795" y="165" width="330" height="125" rx="14" class="endpoint-box"/> <rect x="795" y="165" width="330" height="125" rx="14" class="endpoint-box"/>
<text x="960" y="198" text-anchor="middle" class="box-title">Externe BizTalk-Endpunkte</text> <text x="960" y="198" text-anchor="middle" class="box-title">Externe BizTalk-Endpunkte</text>
<text x="960" y="225" text-anchor="middle" class="box-text">TCP: HTTP(S), SFTP, FTP, WCF, SMB</text> <text x="960" y="225" text-anchor="middle" class="box-text">TCP: HTTP(S), SFTP/FTP, WCF/SQL, SMB</text>
<text x="960" y="247" text-anchor="middle" class="box-text">UDP: explizite udp://-Ziele</text> <text x="960" y="247" text-anchor="middle" class="box-text">UDP: explizite udp://-Ziele</text>
<text x="960" y="269" text-anchor="middle" class="box-text">nur Started / Enabled</text> <text x="960" y="269" text-anchor="middle" class="box-text">nur Started / Enabled</text>
</g> </g>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

+2
View File
@@ -18,6 +18,8 @@
<add key="ProbeEndpointConnectivity" value="true" /> <add key="ProbeEndpointConnectivity" value="true" />
<add key="EndpointCatalogPath" value="%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml" /> <add key="EndpointCatalogPath" value="%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml" />
<add key="EndpointCatalogMaxBytes" value="1048576" /> <add key="EndpointCatalogMaxBytes" value="1048576" />
<!-- Artefaktgrenze; das Netzwerkbudget wird separat ueber eindeutige Ziele begrenzt. -->
<add key="EndpointCatalogMaxEntries" value="1000" />
<!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. --> <!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. -->
<add key="EndpointDiscoveryIntervalHours" value="168" /> <add key="EndpointDiscoveryIntervalHours" value="168" />
<add key="EndpointProbeTimeoutMilliseconds" value="3000" /> <add key="EndpointProbeTimeoutMilliseconds" value="3000" />
@@ -381,7 +381,7 @@ namespace BizTalkCheckmkPulse
var failed = endpointState.Results.Where(x => !x.Available).ToArray(); var failed = endpointState.Results.Where(x => !x.Available).ToArray();
var available = endpointState.Results.Count(x => x.Available); var available = endpointState.Results.Count(x => x.Available);
var incomplete = endpointState.Results.Count != endpointState.Active var incomplete = endpointState.Results.Count != endpointState.Active
|| endpointState.UnsupportedActive > 0 || endpointState.UnresolvedActive > 0
|| endpointState.RefreshRequired && !endpointState.RefreshSucceeded || endpointState.RefreshRequired && !endpointState.RefreshSucceeded
|| !string.IsNullOrWhiteSpace(endpointState.Failure); || !string.IsNullOrWhiteSpace(endpointState.Failure);
var state = failed.Length > 0 var state = failed.Length > 0
@@ -391,14 +391,16 @@ namespace BizTalkCheckmkPulse
: CheckState.Ok; : CheckState.Ok;
var metrics = string.Format( var metrics = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unsupported={6};;1;0|biztalk_endpoints_inactive_skipped={7};;;0|biztalk_endpoint_probe_duration_ms={8};;;0", "biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unresolved={6};;1;0|biztalk_endpoints_expected_non_socket={7};;;0|biztalk_endpoints_manual_overrides={8};;;0|biztalk_endpoints_inactive_skipped={9};;;0|biztalk_endpoint_probe_duration_ms={10};;;0",
endpointState.Configured, endpointState.Configured,
endpointState.Active, endpointState.Active,
endpointState.UniqueTargets, endpointState.UniqueTargets,
endpointState.Results.Count, endpointState.Results.Count,
available, available,
failed.Length, failed.Length,
endpointState.UnsupportedActive, endpointState.UnresolvedActive,
endpointState.ExpectedNonProbeableActive,
endpointState.ManualOverridesActive,
endpointState.SkippedInactive, endpointState.SkippedInactive,
endpointState.ProbeDurationMilliseconds); endpointState.ProbeDurationMilliseconds);
var detail = new StringBuilder(); var detail = new StringBuilder();
@@ -406,6 +408,8 @@ namespace BizTalkCheckmkPulse
{ {
detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar") detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar")
.Append(" (unique_targets=").Append(endpointState.UniqueTargets) .Append(" (unique_targets=").Append(endpointState.UniqueTargets)
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")"); .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")");
} }
else else
@@ -414,7 +418,9 @@ namespace BizTalkCheckmkPulse
.Append(", tested=").Append(endpointState.Results.Count) .Append(", tested=").Append(endpointState.Results.Count)
.Append(", available=").Append(available) .Append(", available=").Append(available)
.Append(", failed=").Append(failed.Length) .Append(", failed=").Append(failed.Length)
.Append(", unsupported_external=").Append(endpointState.UnsupportedActive) .Append(", unresolved=").Append(endpointState.UnresolvedActive)
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
.Append(", unique_targets=").Append(endpointState.UniqueTargets) .Append(", unique_targets=").Append(endpointState.UniqueTargets)
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds); .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds);
AppendLimitedList(detail, "unavailable", failed.Select(x => AppendLimitedList(detail, "unavailable", failed.Select(x =>
@@ -423,6 +429,8 @@ namespace BizTalkCheckmkPulse
{ {
detail.Append("; catalog_or_probe_error=").Append(endpointState.Failure); detail.Append("; catalog_or_probe_error=").Append(endpointState.Failure);
} }
AppendLimitedList(detail, "unresolved_endpoints", endpointState.ResolutionIssues);
} }
if (endpointState.CatalogSynchronizedUtc.HasValue) if (endpointState.CatalogSynchronizedUtc.HasValue)
+151 -54
View File
@@ -21,33 +21,77 @@ namespace BizTalkCheckmkPulse
out EndpointCatalogEntry entry, out EndpointCatalogEntry entry,
out string reason) out string reason)
{ {
entry = null; var resolution = Analyze(candidate);
reason = string.Empty; entry = resolution.Entry;
reason = resolution.Reason ?? string.Empty;
return resolution.Status == EndpointResolutionStatus.Probeable;
}
/// <summary>
/// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken.
/// </summary>
public static EndpointResolution Analyze(EndpointCandidate candidate)
{
if (candidate == null) if (candidate == null)
{ {
reason = "Endpoint-Kandidat fehlt."; return Unresolved("Endpoint-Kandidat fehlt.");
return false;
} }
if (candidate.Dynamic) if (candidate.Dynamic)
{ {
reason = "Dynamischer Send Port besitzt kein statisch pruefbares Ziel."; return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel.");
return false;
} }
var address = (candidate.Address ?? string.Empty).Trim(); var address = (candidate.Address ?? string.Empty).Trim();
if (address.Length == 0) if (address.Length == 0)
{ {
reason = "Transportadresse ist leer."; return Unresolved("Aktive statische Transportadresse ist leer.");
return false;
} }
if (LooksLikeEmailRecipients(address) || Contains(candidate.AdapterName, "SMTP"))
{
return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server.");
}
if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
{
return Expected("Lokaler Dateipfad benoetigt keine Netzwerkprobe.");
}
if (address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
{
return Expected("Named Pipes besitzen kein TCP-/UDP-Ziel.");
}
if (address.StartsWith("/", StringComparison.Ordinal)
&& IsInboundListener(candidate))
{
return Expected("Relative Receive-Listener-Adresse enthaelt keine eindeutige Site-Bindung.");
}
EndpointCatalogEntry entry;
string protocol; string protocol;
string host; string host;
int port; int port;
if (!TryResolve(address, candidate.AdapterName, out protocol, out host, out port, out reason)) string reason;
if (!TryResolve(
address,
candidate.AdapterName,
IsInboundListener(candidate),
out protocol,
out host,
out port,
out reason))
{ {
return false; if (address.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase))
{
return Expected(reason);
}
return Unresolved(reason);
} }
entry = new EndpointCatalogEntry entry = new EndpointCatalogEntry
@@ -64,7 +108,12 @@ namespace BizTalkCheckmkPulse
Enabled = true, Enabled = true,
AutoDiscovered = true AutoDiscovered = true
}; };
return true; return new EndpointResolution
{
Status = EndpointResolutionStatus.Probeable,
Entry = entry,
Reason = string.Empty
};
} }
/// <summary> /// <summary>
@@ -72,52 +121,13 @@ namespace BizTalkCheckmkPulse
/// </summary> /// </summary>
public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate) public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
{ {
if (candidate == null || candidate.Dynamic) return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved;
{
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( private static bool TryResolve(
string address, string address,
string adapterName, string adapterName,
bool allowWildcardListener,
out string protocol, out string protocol,
out string host, out string host,
out int port, out int port,
@@ -128,6 +138,12 @@ namespace BizTalkCheckmkPulse
port = 0; port = 0;
reason = string.Empty; reason = string.Empty;
if (allowWildcardListener
&& TryResolveWildcardListener(address, out protocol, out host, out port, out reason))
{
return true;
}
if (LooksLikeEmailRecipients(address)) if (LooksLikeEmailRecipients(address))
{ {
reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt."; reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt.";
@@ -210,14 +226,14 @@ namespace BizTalkCheckmkPulse
if (Contains(adapterName, "SFTP")) if (Contains(adapterName, "SFTP"))
{ {
host = address.Trim('/'); host = ExtractBareAdapterHost(address);
port = 22; port = 22;
return Validate(host, port, out reason); return Validate(host, port, out reason);
} }
if (Contains(adapterName, "FTP")) if (Contains(adapterName, "FTP"))
{ {
host = address.Trim('/'); host = ExtractBareAdapterHost(address);
port = 21; port = 21;
return Validate(host, port, out reason); return Validate(host, port, out reason);
} }
@@ -248,10 +264,91 @@ namespace BizTalkCheckmkPulse
case "imap": return 143; case "imap": return 143;
case "imaps": return 993; case "imaps": return 993;
case "msmq": return 1801; case "msmq": return 1801;
case "net.tcp": return 808;
case "mssql": return 1433;
// Ohne Pfad ist der Hostteil ein tnsnames.ora-Alias und kein sicher pruefbarer DNS-Host.
case "oracledb": return uri.AbsolutePath.Trim('/').Length > 0 ? 1521 : 0;
default: return uri.Port > 0 ? uri.Port : 0; default: return uri.Port > 0 ? uri.Port : 0;
} }
} }
/// <summary>
/// Reduziert lokale HTTP-/WCF-Wildcard-Listener auf einen nebenwirkungsfreien Loopback-Porttest.
/// </summary>
private static bool TryResolveWildcardListener(
string address,
out string protocol,
out string host,
out int port,
out string reason)
{
protocol = "TCP";
host = string.Empty;
port = 0;
reason = string.Empty;
var match = Regex.Match(
address,
@"^(?<scheme>https?|net\.tcp)://(?:\+|\*)(?::(?<port>\d{1,5}))?(?:[/\\].*)?$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
{
return false;
}
var scheme = match.Groups["scheme"].Value.ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(match.Groups["port"].Value)
&& !int.TryParse(match.Groups["port"].Value, out port))
{
port = 0;
}
if (port <= 0)
{
port = scheme == "https" ? 443 : scheme == "net.tcp" ? 808 : 80;
}
host = "127.0.0.1";
return Validate(host, port, out reason);
}
private static string ExtractBareAdapterHost(string address)
{
var value = (address ?? string.Empty).Trim().Trim('/');
var separator = value.IndexOfAny(new[] { '/', '\\' });
return separator < 0 ? value : value.Substring(0, separator);
}
/// <summary>
/// Erkennt Adapter, deren relative Receive-Adresse eine lokale Listenerbindung statt eines Remoteziels beschreibt.
/// </summary>
private static bool IsInboundListener(EndpointCandidate candidate)
{
return string.Equals(candidate.ArtifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
&& (Contains(candidate.AdapterName, "HTTP")
|| Contains(candidate.AdapterName, "SOAP")
|| Contains(candidate.AdapterName, "WCF"));
}
private static EndpointResolution Expected(string reason)
{
return new EndpointResolution
{
Status = EndpointResolutionStatus.ExpectedNonProbeable,
Reason = reason
};
}
private static EndpointResolution Unresolved(string reason)
{
return new EndpointResolution
{
Status = EndpointResolutionStatus.Unresolved,
Reason = string.IsNullOrWhiteSpace(reason)
? "Transportadresse konnte nicht sicher auf Host und Port reduziert werden."
: reason
};
}
private static bool HasExplicitPort(string address) private static bool HasExplicitPort(string address)
{ {
var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3); var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3);
@@ -90,14 +90,14 @@ namespace BizTalkCheckmkPulse
EnvironmentName = catalogEnvironment, EnvironmentName = catalogEnvironment,
SynchronizedUtc = synchronizedUtc, SynchronizedUtc = synchronizedUtc,
ActiveCandidates = ReadInt(root, "activeCandidates", 0, 100000), ActiveCandidates = ReadInt(root, "activeCandidates", 0, 100000),
UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000) UnresolvedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000)
}; };
foreach (var node in root.Elements("Endpoint")) foreach (var node in root.Elements("Endpoint"))
{ {
if (catalog.Entries.Count >= _maxEntries) if (catalog.Entries.Count >= _maxEntries)
{ {
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount."); throw new InvalidDataException("Endpoint catalog exceeds the configured entry limit.");
} }
var entry = new EndpointCatalogEntry var entry = new EndpointCatalogEntry
@@ -135,7 +135,7 @@ namespace BizTalkCheckmkPulse
if (catalog.Entries.Count > _maxEntries) if (catalog.Entries.Count > _maxEntries)
{ {
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount."); throw new InvalidDataException("Endpoint catalog exceeds the configured entry limit.");
} }
foreach (var entry in catalog.Entries) foreach (var entry in catalog.Entries)
@@ -150,7 +150,8 @@ namespace BizTalkCheckmkPulse
new XAttribute("environment", catalog.EnvironmentName ?? string.Empty), new XAttribute("environment", catalog.EnvironmentName ?? string.Empty),
new XAttribute("synchronizedUtc", catalog.SynchronizedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)), new XAttribute("synchronizedUtc", catalog.SynchronizedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)),
new XAttribute("activeCandidates", catalog.ActiveCandidates), new XAttribute("activeCandidates", catalog.ActiveCandidates),
new XAttribute("unsupportedCandidates", catalog.UnsupportedCandidates)); // Der XML-Attributsname bleibt fuer vorhandene Katalogdateien der Version 1 stabil.
new XAttribute("unsupportedCandidates", catalog.UnresolvedCandidates));
foreach (var entry in catalog.Entries foreach (var entry in catalog.Entries
.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase) .OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.ArtifactType, StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.ArtifactType, StringComparer.OrdinalIgnoreCase)
@@ -44,7 +44,7 @@ namespace BizTalkCheckmkPulse
var store = new EndpointCatalogStore( var store = new EndpointCatalogStore(
_options.EndpointCatalogPath, _options.EndpointCatalogPath,
_options.EndpointCatalogMaxBytes, _options.EndpointCatalogMaxBytes,
_options.EndpointMaxCount); _options.EndpointCatalogMaxEntries);
EndpointCatalog catalog = null; EndpointCatalog catalog = null;
string readFailure = null; string readFailure = null;
try try
@@ -76,7 +76,7 @@ namespace BizTalkCheckmkPulse
"Endpoint catalog synchronized. path=" + _options.EndpointCatalogPath "Endpoint catalog synchronized. path=" + _options.EndpointCatalogPath
+ " active_candidates=" + catalog.ActiveCandidates + " active_candidates=" + catalog.ActiveCandidates
+ " configured=" + catalog.Entries.Count + " configured=" + catalog.Entries.Count
+ " unsupported_external=" + catalog.UnsupportedCandidates); + " unresolved_candidates=" + catalog.UnresolvedCandidates);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -104,19 +104,28 @@ namespace BizTalkCheckmkPulse
state.CatalogAvailable = true; state.CatalogAvailable = true;
state.CatalogSynchronizedUtc = catalog.SynchronizedUtc; state.CatalogSynchronizedUtc = catalog.SynchronizedUtc;
state.Configured = catalog.Entries.Count; state.Configured = catalog.Entries.Count;
state.UnsupportedActive = CountUnsupportedExternal(result.EndpointCandidates); var activeEntries = ResolveActiveEndpoints(catalog, result.EndpointCandidates, state);
var activeTargetCount = activeEntries
.Select(TargetKey)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
if (activeTargetCount > _options.EndpointMaxCount)
{
state.Failure = "Eindeutige aktive Netzwerkziele=" + activeTargetCount
+ " ueberschreiten EndpointMaxCount=" + _options.EndpointMaxCount + ".";
return;
}
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.Active = activeEntries.Length;
state.SkippedInactive = catalog.Entries.Count(x => x.Enabled) - activeEntries.Length;
// Detailwarnungen nur beim Katalogabgleich schreiben; der Minutensnapshot
// enthaelt die aktuelle begrenzte Liste bereits und das Tageslog bleibt kompakt.
foreach (var issue in state.RefreshRequired
? state.ResolutionIssues.Take(20)
: Enumerable.Empty<string>())
{
_logger.Warning("Endpoint target unresolved. " + issue);
}
try try
{ {
@@ -155,26 +164,26 @@ namespace BizTalkCheckmkPulse
var active = (candidates ?? Enumerable.Empty<EndpointCandidate>()) var active = (candidates ?? Enumerable.Empty<EndpointCandidate>())
.Where(x => x != null && x.Active) .Where(x => x != null && x.Active)
.ToArray(); .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 var manual = existing == null
? new EndpointCatalogEntry[0] ? new EndpointCatalogEntry[0]
: existing.Entries.Where(x => !x.AutoDiscovered).ToArray(); : existing.Entries.Where(x => !x.AutoDiscovered).ToArray();
var manualKeys = new HashSet<string>(manual.Select(x => x.Key), StringComparer.OrdinalIgnoreCase); var manualKeys = new HashSet<string>(manual.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
var discovered = new List<EndpointCatalogEntry>();
var unresolved = 0;
foreach (var candidate in active)
{
var resolution = EndpointAddressParser.Analyze(candidate);
if (resolution.Status == EndpointResolutionStatus.Probeable)
{
discovered.Add(resolution.Entry);
}
else if (resolution.Status == EndpointResolutionStatus.Unresolved
&& !manualKeys.Contains(candidate.Key))
{
unresolved++;
}
}
var merged = manual var merged = manual
.Concat(discovered.Where(x => !manualKeys.Contains(x.Key))) .Concat(discovered.Where(x => !manualKeys.Contains(x.Key)))
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
@@ -187,7 +196,8 @@ namespace BizTalkCheckmkPulse
EnvironmentName = existing == null ? string.Empty : existing.EnvironmentName, EnvironmentName = existing == null ? string.Empty : existing.EnvironmentName,
SynchronizedUtc = synchronizedUtc.ToUniversalTime(), SynchronizedUtc = synchronizedUtc.ToUniversalTime(),
ActiveCandidates = active.Length, ActiveCandidates = active.Length,
UnsupportedCandidates = unsupported // Attributsname bleibt fuer die Rueckwaertskompatibilitaet des Katalogformats bestehen.
UnresolvedCandidates = unresolved
}; };
catalog.Entries.AddRange(merged); catalog.Entries.AddRange(merged);
return catalog; return catalog;
@@ -200,29 +210,107 @@ namespace BizTalkCheckmkPulse
{ {
var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc); var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc);
catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty; catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty;
if (catalog.Entries.Count > _options.EndpointMaxCount) if (catalog.Entries.Count > _options.EndpointCatalogMaxEntries)
{ {
throw new InvalidDataException("Probeable endpoints exceed EndpointMaxCount."); throw new InvalidDataException("Endpoint catalog exceeds EndpointCatalogMaxEntries.");
} }
return catalog; return catalog;
} }
private static int CountUnsupportedExternal(IEnumerable<EndpointCandidate> candidates) /// <summary>
/// Ermittelt aus aktuellen WMI-Kandidaten und manuellen Overrides die in diesem Lauf zu pruefenden Ziele.
/// Automatisch erkannte Katalogeintraege werden bewusst nicht als veraltete Laufzeitquelle verwendet.
/// </summary>
internal static EndpointCatalogEntry[] ResolveActiveEndpoints(
EndpointCatalog catalog,
IEnumerable<EndpointCandidate> candidates,
EndpointConnectivityState state)
{ {
var count = 0; if (catalog == null)
foreach (var candidate in candidates.Where(x => x.Active))
{ {
EndpointCatalogEntry ignored; throw new ArgumentNullException("catalog");
string reason; }
if (!EndpointAddressParser.TryCreate(candidate, out ignored, out reason)
&& EndpointAddressParser.IsPotentialExternalEndpoint(candidate)) if (state == null)
{ {
count++; throw new ArgumentNullException("state");
}
var activeCandidates = (candidates ?? Enumerable.Empty<EndpointCandidate>())
.Where(x => x != null && x.Active)
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
.ToArray();
var activeKeys = new HashSet<string>(activeCandidates.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
var manualOverrides = catalog.Entries
.Where(x => x.Enabled && !x.AutoDiscovered)
.Where(x => !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase);
var selected = catalog.Entries
.Where(x => x.Enabled && string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var candidate in activeCandidates)
{
EndpointCatalogEntry manual;
if (manualOverrides.TryGetValue(candidate.Key, out manual))
{
selected.Add(manual);
state.ManualOverridesActive++;
continue;
}
var resolution = EndpointAddressParser.Analyze(candidate);
if (resolution.Status == EndpointResolutionStatus.Probeable)
{
// Immer das aktuelle WMI-Ziel pruefen; der Wochenkatalog darf keine alte Adresse erzwingen.
selected.Add(resolution.Entry);
}
else if (resolution.Status == EndpointResolutionStatus.ExpectedNonProbeable)
{
state.ExpectedNonProbeableActive++;
}
else
{
state.UnresolvedActive++;
state.ResolutionIssues.Add(DisplayResolutionIssue(candidate, resolution.Reason));
} }
} }
return count; state.SkippedInactive = catalog.Entries.Count(x =>
x.Enabled
&& !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
&& !activeKeys.Contains(x.Key));
return selected
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
.ToArray();
}
/// <summary>
/// Formatiert eine Aufloesungsluecke ohne Transportadresse, Pfad, Querystring oder Zugangsdaten.
/// </summary>
private static string DisplayResolutionIssue(EndpointCandidate candidate, string reason)
{
var artifact = string.IsNullOrWhiteSpace(candidate.ApplicationName)
|| string.Equals(candidate.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)
? candidate.ArtifactName
: candidate.ApplicationName + "\\" + candidate.ArtifactName;
return "endpoint=" + CompactText(candidate.ArtifactType) + ":" + CompactText(artifact)
+ "[" + CompactText(candidate.TransportRole) + "]"
+ " adapter=" + CompactText(candidate.AdapterName)
+ " reason=" + CompactText(reason);
}
private static string CompactText(string value)
{
return (value ?? string.Empty)
.Replace('\r', ' ')
.Replace('\n', ' ')
.Replace('|', '/')
.Trim();
} }
private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints) private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints)
+26 -2
View File
@@ -269,6 +269,26 @@ namespace BizTalkCheckmkPulse
} }
} }
/// <summary>
/// Ergebnis der geheimnisfreien Auswertung einer BizTalk-Transportadresse.
/// </summary>
internal enum EndpointResolutionStatus
{
Probeable,
ExpectedNonProbeable,
Unresolved
}
/// <summary>
/// Klassifiziert einen Kandidaten und enthaelt nur bei sicherer Aufloesung ein Socket-Ziel.
/// </summary>
internal sealed class EndpointResolution
{
public EndpointResolutionStatus Status { get; set; }
public EndpointCatalogEntry Entry { get; set; }
public string Reason { get; set; }
}
/// <summary> /// <summary>
/// Ein geheimnisfreier, lokal persistierbarer Netzwerk-Endpunkt. /// Ein geheimnisfreier, lokal persistierbarer Netzwerk-Endpunkt.
/// </summary> /// </summary>
@@ -314,7 +334,7 @@ namespace BizTalkCheckmkPulse
public string MachineName { get; set; } public string MachineName { get; set; }
public string EnvironmentName { get; set; } public string EnvironmentName { get; set; }
public int ActiveCandidates { get; set; } public int ActiveCandidates { get; set; }
public int UnsupportedCandidates { get; set; } public int UnresolvedCandidates { get; set; }
public List<EndpointCatalogEntry> Entries { get; private set; } public List<EndpointCatalogEntry> Entries { get; private set; }
} }
@@ -337,6 +357,7 @@ namespace BizTalkCheckmkPulse
public EndpointConnectivityState() public EndpointConnectivityState()
{ {
Results = new List<EndpointProbeResult>(); Results = new List<EndpointProbeResult>();
ResolutionIssues = new List<string>();
} }
public bool Disabled { get; set; } public bool Disabled { get; set; }
@@ -348,10 +369,13 @@ namespace BizTalkCheckmkPulse
public int Configured { get; set; } public int Configured { get; set; }
public int Active { get; set; } public int Active { get; set; }
public int SkippedInactive { get; set; } public int SkippedInactive { get; set; }
public int UnsupportedActive { get; set; } public int UnresolvedActive { get; set; }
public int ExpectedNonProbeableActive { get; set; }
public int ManualOverridesActive { get; set; }
public int UniqueTargets { get; set; } public int UniqueTargets { get; set; }
public long ProbeDurationMilliseconds { get; set; } public long ProbeDurationMilliseconds { get; set; }
public string Failure { get; set; } public string Failure { get; set; }
public List<EndpointProbeResult> Results { get; private set; } public List<EndpointProbeResult> Results { get; private set; }
public List<string> ResolutionIssues { get; private set; }
} }
} }
@@ -41,6 +41,7 @@ namespace BizTalkCheckmkPulse
public bool ProbeEndpointConnectivity { get; set; } public bool ProbeEndpointConnectivity { get; set; }
public string EndpointCatalogPath { get; set; } public string EndpointCatalogPath { get; set; }
public int EndpointCatalogMaxBytes { get; set; } public int EndpointCatalogMaxBytes { get; set; }
public int EndpointCatalogMaxEntries { get; set; }
public int EndpointDiscoveryIntervalHours { get; set; } public int EndpointDiscoveryIntervalHours { get; set; }
public int EndpointProbeTimeoutMilliseconds { get; set; } public int EndpointProbeTimeoutMilliseconds { get; set; }
public int EndpointProbeMaxConcurrency { get; set; } public int EndpointProbeMaxConcurrency { get; set; }
@@ -83,6 +84,7 @@ namespace BizTalkCheckmkPulse
ProbeEndpointConnectivity = true; ProbeEndpointConnectivity = true;
EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml"); EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml");
EndpointCatalogMaxBytes = 1048576; EndpointCatalogMaxBytes = 1048576;
EndpointCatalogMaxEntries = 1000;
EndpointDiscoveryIntervalHours = 168; EndpointDiscoveryIntervalHours = 168;
EndpointProbeTimeoutMilliseconds = 3000; EndpointProbeTimeoutMilliseconds = 3000;
EndpointProbeMaxConcurrency = 16; EndpointProbeMaxConcurrency = 16;
@@ -141,6 +143,7 @@ namespace BizTalkCheckmkPulse
options.ProbeEndpointConnectivity = ReadBool(settings, "ProbeEndpointConnectivity", options.ProbeEndpointConnectivity); options.ProbeEndpointConnectivity = ReadBool(settings, "ProbeEndpointConnectivity", options.ProbeEndpointConnectivity);
options.EndpointCatalogPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "EndpointCatalogPath", options.EndpointCatalogPath)); options.EndpointCatalogPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "EndpointCatalogPath", options.EndpointCatalogPath));
options.EndpointCatalogMaxBytes = ReadInt(settings, "EndpointCatalogMaxBytes", options.EndpointCatalogMaxBytes, 4096, 16777216); options.EndpointCatalogMaxBytes = ReadInt(settings, "EndpointCatalogMaxBytes", options.EndpointCatalogMaxBytes, 4096, 16777216);
options.EndpointCatalogMaxEntries = ReadInt(settings, "EndpointCatalogMaxEntries", options.EndpointCatalogMaxEntries, 1, 100000);
options.EndpointDiscoveryIntervalHours = ReadInt(settings, "EndpointDiscoveryIntervalHours", options.EndpointDiscoveryIntervalHours, 1, 8760); options.EndpointDiscoveryIntervalHours = ReadInt(settings, "EndpointDiscoveryIntervalHours", options.EndpointDiscoveryIntervalHours, 1, 8760);
options.EndpointProbeTimeoutMilliseconds = ReadInt(settings, "EndpointProbeTimeoutMilliseconds", options.EndpointProbeTimeoutMilliseconds, 250, 30000); options.EndpointProbeTimeoutMilliseconds = ReadInt(settings, "EndpointProbeTimeoutMilliseconds", options.EndpointProbeTimeoutMilliseconds, 250, 30000);
options.EndpointProbeMaxConcurrency = ReadInt(settings, "EndpointProbeMaxConcurrency", options.EndpointProbeMaxConcurrency, 1, 64); options.EndpointProbeMaxConcurrency = ReadInt(settings, "EndpointProbeMaxConcurrency", options.EndpointProbeMaxConcurrency, 1, 64);
+6
View File
@@ -102,6 +102,12 @@ namespace BizTalkCheckmkPulse
+ result.EndpointConnectivity.Active + result.EndpointConnectivity.Active
+ " endpoints_failed=" + " endpoints_failed="
+ result.EndpointConnectivity.Results.Count(x => !x.Available) + result.EndpointConnectivity.Results.Count(x => !x.Available)
+ " endpoints_unresolved="
+ result.EndpointConnectivity.UnresolvedActive
+ " endpoints_expected_non_socket="
+ result.EndpointConnectivity.ExpectedNonProbeableActive
+ " endpoints_manual_overrides="
+ result.EndpointConnectivity.ManualOverridesActive
+ " endpoints_unique_targets=" + " endpoints_unique_targets="
+ result.EndpointConnectivity.UniqueTargets + result.EndpointConnectivity.UniqueTargets
+ " endpoint_probe_ms=" + " endpoint_probe_ms="
+158 -1
View File
@@ -29,9 +29,13 @@ namespace BizTalkCheckmkPulse.Tests
Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState); Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState);
Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded); Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded);
Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets); Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets);
Run("AdapterSpecificEndpointAddressesResolve", AdapterSpecificEndpointAddressesResolve);
Run("EndpointClassificationSeparatesExpectedAndUnresolved", EndpointClassificationSeparatesExpectedAndUnresolved);
Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides); Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides);
Run("EndpointRuntimeUsesCurrentAddressAndManualOverrides", EndpointRuntimeUsesCurrentAddressAndManualOverrides);
Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip); Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip);
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets); Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
Run("EndpointOutputExplainsOnlyRealResolutionGaps", EndpointOutputExplainsOnlyRealResolutionGaps);
Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval); Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval);
Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings); Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings);
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines); Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
@@ -368,9 +372,94 @@ namespace BizTalkCheckmkPulse.Tests
candidate.Address = "/Orders/Receive.svc"; candidate.Address = "/Orders/Receive.svc";
candidate.AdapterName = "WCF-CustomIsolated"; candidate.AdapterName = "WCF-CustomIsolated";
candidate.ArtifactType = "ReceiveLocation";
candidate.TransportRole = "Inbound";
Assert(!EndpointAddressParser.IsPotentialExternalEndpoint(candidate), "relative local receive address must not cause UNKNOWN"); Assert(!EndpointAddressParser.IsPotentialExternalEndpoint(candidate), "relative local receive address must not cause UNKNOWN");
} }
private static void AdapterSpecificEndpointAddressesResolve()
{
EndpointCatalogEntry endpoint;
string reason;
var candidate = new EndpointCandidate
{
ArtifactType = "SendPort",
ApplicationName = "Orders",
ArtifactName = "SP SQL",
TransportRole = "Primary",
AdapterName = "WCF-Custom",
Address = "mssql://sql01/INSTANCE/Orders",
Active = true
};
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "WCF-SQL URI should resolve: " + reason);
AssertEqual("sql01", endpoint.Host, "WCF-SQL host");
AssertEqual(1433, endpoint.Port, "WCF-SQL default port");
candidate.Address = "mssql://sql01:15433/INSTANCE/Orders";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "WCF-SQL explicit port should resolve: " + reason);
AssertEqual(15433, endpoint.Port, "WCF-SQL explicit port");
candidate.Address = "net.tcp://service01/Orders";
candidate.AdapterName = "WCF-NetTcp";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "net.tcp URI should resolve: " + reason);
AssertEqual(808, endpoint.Port, "net.tcp default port");
candidate.Address = "oracledb://oracle01/ORDERS/Dedicated";
candidate.AdapterName = "WCF-OracleDB";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "direct Oracle DB URI should resolve: " + reason);
AssertEqual("oracle01", endpoint.Host, "Oracle DB host");
AssertEqual(1521, endpoint.Port, "Oracle DB default port");
candidate.Address = "oracledb://ORDERS_TNS";
Assert(!EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "tnsnames alias must not be guessed as DNS host");
candidate.ArtifactType = "ReceiveLocation";
candidate.TransportRole = "Inbound";
candidate.Address = "http://+:8080/Orders";
candidate.AdapterName = "WCF-WebHttp";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "wildcard listener should resolve: " + reason);
AssertEqual("127.0.0.1", endpoint.Host, "wildcard listener loopback host");
AssertEqual(8080, endpoint.Port, "wildcard listener port");
}
private static void EndpointClassificationSeparatesExpectedAndUnresolved()
{
var candidate = new EndpointCandidate
{
ArtifactType = "ReceiveLocation",
ApplicationName = "Orders",
ArtifactName = "RL HTTP",
TransportRole = "Inbound",
AdapterName = "WCF-CustomIsolated",
Address = "/Orders/Receive.svc",
Active = true
};
AssertEqual(
EndpointResolutionStatus.ExpectedNonProbeable,
EndpointAddressParser.Analyze(candidate).Status,
"relative listener classification");
candidate.ArtifactType = "SendPort";
candidate.TransportRole = "Primary";
AssertEqual(
EndpointResolutionStatus.Unresolved,
EndpointAddressParser.Analyze(candidate).Status,
"relative outbound address classification");
candidate.Address = "http://+:8080/Orders";
AssertEqual(
EndpointResolutionStatus.Unresolved,
EndpointAddressParser.Analyze(candidate).Status,
"outbound wildcard must not become a loopback target");
candidate.Dynamic = true;
AssertEqual(
EndpointResolutionStatus.ExpectedNonProbeable,
EndpointAddressParser.Analyze(candidate).Status,
"dynamic send port classification");
}
private static void EndpointCatalogPreservesManualOverrides() private static void EndpointCatalogPreservesManualOverrides()
{ {
var candidate = new EndpointCandidate var candidate = new EndpointCandidate
@@ -417,6 +506,42 @@ namespace BizTalkCheckmkPulse.Tests
Assert(synchronized.Entries.All(x => x.ArtifactName != "RL Disabled"), "inactive receive location must not be added"); Assert(synchronized.Entries.All(x => x.ArtifactName != "RL Disabled"), "inactive receive location must not be added");
} }
private static void EndpointRuntimeUsesCurrentAddressAndManualOverrides()
{
var candidate = new EndpointCandidate
{
ArtifactType = "SendPort",
ApplicationName = "Orders",
ArtifactName = "SP Orders",
TransportRole = "Primary",
AdapterName = "WCF-Custom",
Address = "https://new.example.test/orders",
Active = true
};
var catalog = new EndpointCatalog();
var stale = TestEndpoint("SP Orders", "old.example.test");
stale.Key = candidate.Key;
catalog.Entries.Add(stale);
var state = new EndpointConnectivityState();
var selected = EndpointConnectivityProbe.ResolveActiveEndpoints(catalog, new[] { candidate }, state);
AssertEqual(1, selected.Length, "current endpoint selection count");
AssertEqual("new.example.test", selected[0].Host, "current WMI address must replace stale catalog target");
candidate.Address = "adapter-specific-target-without-port";
var manual = TestEndpoint("SP Orders", "manual.example.test");
manual.Key = candidate.Key;
manual.AutoDiscovered = false;
manual.Port = 7443;
catalog.Entries.Clear();
catalog.Entries.Add(manual);
state = new EndpointConnectivityState();
selected = EndpointConnectivityProbe.ResolveActiveEndpoints(catalog, new[] { candidate }, state);
AssertEqual(1, selected.Length, "manual endpoint selection count");
AssertEqual("manual.example.test", selected[0].Host, "manual override host");
AssertEqual(0, state.UnresolvedActive, "manual override must close resolution gap");
AssertEqual(1, state.ManualOverridesActive, "manual override metric");
}
private static void EndpointOutputListsOnlyUnavailableTargets() private static void EndpointOutputListsOnlyUnavailableTargets()
{ {
var result = new ProbeResult(); var result = new ProbeResult();
@@ -445,6 +570,27 @@ namespace BizTalkCheckmkPulse.Tests
Assert(line.IndexOf("up.example.test", StringComparison.Ordinal) < 0, "reachable host must not clutter output"); Assert(line.IndexOf("up.example.test", StringComparison.Ordinal) < 0, "reachable host must not clutter output");
} }
private static void EndpointOutputExplainsOnlyRealResolutionGaps()
{
var result = new ProbeResult();
result.EndpointConnectivity.RuntimeStateAvailable = true;
result.EndpointConnectivity.CatalogAvailable = true;
result.EndpointConnectivity.RefreshSucceeded = true;
result.EndpointConnectivity.ExpectedNonProbeableActive = 4;
var line = FindServiceLine(result, "Endpoint Reachability");
Assert(line.StartsWith("0 \"BizTalk Endpoint Reachability\"", StringComparison.Ordinal), "expected non-socket endpoints must remain OK");
Assert(line.Contains("biztalk_endpoints_expected_non_socket=4"), "expected non-socket metric missing");
Assert(line.IndexOf("unsupported", StringComparison.OrdinalIgnoreCase) < 0, "legacy unsupported wording must not be emitted");
result.EndpointConnectivity.UnresolvedActive = 1;
result.EndpointConnectivity.ResolutionIssues.Add(
"endpoint=SendPort:Orders\\SP Custom[Primary] adapter=WCF-Custom reason=Schema ohne Port");
line = FindServiceLine(result, "Endpoint Reachability");
Assert(line.StartsWith("3 \"BizTalk Endpoint Reachability\"", StringComparison.Ordinal), "real resolution gap must be UNKNOWN");
Assert(line.Contains("unresolved_endpoints="), "resolution issue list missing");
Assert(line.Contains("SP Custom"), "resolution issue artifact missing");
}
private static void EndpointCatalogRoundTrip() private static void EndpointCatalogRoundTrip()
{ {
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.CatalogTests." + Guid.NewGuid().ToString("N")); var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.CatalogTests." + Guid.NewGuid().ToString("N"));
@@ -457,7 +603,7 @@ namespace BizTalkCheckmkPulse.Tests
EnvironmentName = "ACC", EnvironmentName = "ACC",
SynchronizedUtc = DateTime.UtcNow, SynchronizedUtc = DateTime.UtcNow,
ActiveCandidates = 150, ActiveCandidates = 150,
UnsupportedCandidates = 0 UnresolvedCandidates = 7
}; };
catalog.Entries.Add(TestEndpoint("Orders", "api.example.test")); catalog.Entries.Add(TestEndpoint("Orders", "api.example.test"));
var store = new EndpointCatalogStore(path, 1048576, 100); var store = new EndpointCatalogStore(path, 1048576, 100);
@@ -467,6 +613,7 @@ namespace BizTalkCheckmkPulse.Tests
AssertEqual(1, loaded.Entries.Count, "catalog entry count"); AssertEqual(1, loaded.Entries.Count, "catalog entry count");
AssertEqual("api.example.test", loaded.Entries[0].Host, "catalog host"); AssertEqual("api.example.test", loaded.Entries[0].Host, "catalog host");
AssertEqual(443, loaded.Entries[0].Port, "catalog port"); AssertEqual(443, loaded.Entries[0].Port, "catalog port");
AssertEqual(7, loaded.UnresolvedCandidates, "catalog unresolved candidate count");
Assert(File.ReadAllText(path).IndexOf("top-secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog must not contain URI secrets"); Assert(File.ReadAllText(path).IndexOf("top-secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog must not contain URI secrets");
} }
finally finally
@@ -526,6 +673,7 @@ namespace BizTalkCheckmkPulse.Tests
+ "<add key=\"EnvironmentName\" value=\"\" />" + "<add key=\"EnvironmentName\" value=\"\" />"
+ "<add key=\"EndpointProbeMaxConcurrency\" value=\"16\" />" + "<add key=\"EndpointProbeMaxConcurrency\" value=\"16\" />"
+ "<add key=\"EndpointMaxCount\" value=\"100\" />" + "<add key=\"EndpointMaxCount\" value=\"100\" />"
+ "<add key=\"EndpointCatalogMaxEntries\" value=\"1000\" />"
+ "<add key=\"NewSetting\" value=\"new-default\" />" + "<add key=\"NewSetting\" value=\"new-default\" />"
+ "</appSettings></configuration>", + "</appSettings></configuration>",
new UTF8Encoding(false)); new UTF8Encoding(false));
@@ -548,6 +696,7 @@ namespace BizTalkCheckmkPulse.Tests
AssertEqual("ACC", effectiveEnvironment, "preserved installer environment"); AssertEqual("ACC", effectiveEnvironment, "preserved installer environment");
Assert(merged.Contains("key=\"EndpointProbeMaxConcurrency\" value=\"7\""), "existing operational value must be preserved"); Assert(merged.Contains("key=\"EndpointProbeMaxConcurrency\" value=\"7\""), "existing operational value must be preserved");
Assert(merged.Contains("key=\"EndpointMaxCount\" value=\"100\""), "superseded old default must migrate to new bounded default"); Assert(merged.Contains("key=\"EndpointMaxCount\" value=\"100\""), "superseded old default must migrate to new bounded default");
Assert(merged.Contains("key=\"EndpointCatalogMaxEntries\" value=\"1000\""), "new catalog entry limit must be added");
Assert(merged.Contains("key=\"NewSetting\" value=\"new-default\""), "new source setting must be added"); Assert(merged.Contains("key=\"NewSetting\" value=\"new-default\""), "new source setting must be added");
Assert(merged.IndexOf("RemovedLegacySetting", StringComparison.Ordinal) < 0, "removed legacy key must not be resurrected"); Assert(merged.IndexOf("RemovedLegacySetting", StringComparison.Ordinal) < 0, "removed legacy key must not be resurrected");
@@ -713,6 +862,14 @@ namespace BizTalkCheckmkPulse.Tests
} }
} }
private static void AssertEqual(EndpointResolutionStatus expected, EndpointResolutionStatus actual, string label)
{
if (expected != actual)
{
throw new InvalidOperationException(label + ": expected " + expected + ", actual " + actual);
}
}
private static void AssertEqual(string expected, string actual, string label) private static void AssertEqual(string expected, string actual, string label)
{ {
if (!string.Equals(expected, actual, StringComparison.Ordinal)) if (!string.Equals(expected, actual, StringComparison.Ordinal))