From 0f8e8034e6586ee24ab944b658fb566f12198f0c Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Tue, 11 Aug 2026 13:56:30 +0200 Subject: [PATCH] Harden RTM endpoint adapter handling --- Dokumentation.md | 10 + README.md | 27 +- deployment/INSTALLATION.md | 7 + ...ndpoint-Reachability-Analyse-2026-08-10.md | 5 + docs/CheckmkServices.md | 10 +- docs/EndpointCatalog.md | 15 +- docs/ExampleOutput.md | 7 +- docs/Integration.md | 3 + docs/RTM-Endpoint-Adapterregeln-2026-08-11.md | 101 +++++ .../Properties/AssemblyInfo.cs | 4 +- .../CheckmkLocalFormatter.cs | 23 +- .../EndpointAddressParser.cs | 352 +++++++++++++++++- .../EndpointCatalogStore.cs | 46 ++- .../EndpointConnectivityProbe.cs | 44 ++- src/BizTalkCheckmkPulse/Models.cs | 21 ++ src/BizTalkCheckmkPulse/Program.cs | 4 +- .../Properties/AssemblyInfo.cs | 4 +- tests/BizTalkCheckmkPulse.Tests/Program.cs | 110 +++++- 18 files changed, 759 insertions(+), 34 deletions(-) create mode 100644 docs/RTM-Endpoint-Adapterregeln-2026-08-11.md diff --git a/Dokumentation.md b/Dokumentation.md index 5f0cb37..b0c1d7b 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -446,6 +446,16 @@ zurückgerollt. UNKNOWN in Platform, SQL Access, Suspensions, Host Instances, Receive Locations, Send Ports, Orchestrations oder Event Log bleibt blockierend, weil dort die Kern-Datenerhebung nicht zuverlässig belegt ist. +Version 2.2.7 klassifiziert die im RTM-Abnahmelauf identifizierten Adapter +präziser. Scheduling-Komponenten sind keine Netzwerkziele und werden komplett +ausgenommen. `Database://host[:port]/.../Oracle` wird per TCP auf den +expliziten Port oder den Oracle-Standard 1521 geprüft. WCF-SAP verwendet den +ersten SAProuter-Hop oder das Paar `ListenerGwHost`/`ListenerGwServ`; dabei +entspricht `sapgwNN` dem TCP-Port `33NN`. Diese beiden Adapterklassen laufen +als Best-Effort-Diagnose: Ein Fehlschlag bleibt über Metriken sichtbar, erzeugt +aber keinen Alarm. Alle regulären und manuellen Ziele behalten ihre bisherige +strenge Statuslogik. + Der installierte Task selbst läuft mit `RunLevel Limited`. Der Installer übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle; es wird weder in einer Prozesskommandozeile noch in Config oder Log diff --git a/README.md b/README.md index 18e6206..428f52b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,15 @@ Checkmk-Service `BizTalk Endpoint Reachability` zusammengefasst: - HTTP(S), SFTP, FTP, WCF/`net.tcp` und UNC/SMB werden durch einen reinen TCP-Verbindungsaufbau zu Host und Port geprüft. Explizite `udp://host:port`-Ziele verwenden ein UDP-Datagramm. +- Scheduling-Komponenten werden vollständig aus der Endpoint-Prüfung + ausgeschlossen. Oracle-`DATABASE`-Ziele werden aus + `Database://host[:port]/.../Oracle` auf TCP 1521 beziehungsweise den + expliziten Port reduziert. WCF-SAP nutzt den ersten SAProuter-Hop oder + `ListenerGwHost`/`ListenerGwServ`; `sapgwNN` wird als TCP `33NN` aufgelöst. +- Automatisch abgeleitete DATABASE-/WCF-SAP-Probes sind Best-Effort: + erfolgreiche Tests bleiben messbar, nicht ableitbare oder nicht erreichbare + Ziele erzeugen jedoch weder `UNKNOWN` noch `WARN`/`CRIT`. Für verbindliche + Alarmierung kann weiterhin ein manueller Katalogeintrag verwendet werden. - Es werden weder HTTP-Aufrufe noch Anmeldungen, fachliche Requests oder BizTalk-Nachrichten gesendet. Der Check validiert DNS, Netzwerkpfad, Firewall und bei TCP einen annehmenden Zielport, nicht die fachliche @@ -128,11 +137,13 @@ Auch schemafreie `host:port`-Angaben sowie FTP-/SFTP-Adressen der Form nicht in den Endpoint-Katalog übernommen. 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. +Ports, lokale Pfade, SMTP-Empfänger, Named Pipes, relative HTTP-/WCF- +Receive-Listener, Scheduling-Komponenten sowie nicht sicher ableitbare +DATABASE-/WCF-SAP-Adressen werden deshalb als erwartbar nicht socket-prüfbar +behandelt und erzeugen kein `UNKNOWN`. Nur eine andere aktive statische +Adresse, die tatsächlich ein Netzwerkziel enthalten müsste und nicht sicher +aufgelöst werden kann, bleibt `UNKNOWN` und wird geheimnisfrei mit Artefakt, +Adapter und Grund genannt. Der geheimnisfreie Katalog liegt unter `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml`. Fehlt er, wird er beim @@ -158,6 +169,10 @@ Beispielausgaben: [docs/ExampleOutput.md](docs/ExampleOutput.md) Endpoint-Katalog, manuelle Overrides und Protokollgrenzen: [docs/EndpointCatalog.md](docs/EndpointCatalog.md) +Die RTM-Adapterregeln und ihre Herstellerquellen sind in +[docs/RTM-Endpoint-Adapterregeln-2026-08-11.md](docs/RTM-Endpoint-Adapterregeln-2026-08-11.md) +festgehalten. + ## Voraussetzungen Build-Host: @@ -435,7 +450,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. | | Receive Locations / Send Ports sind CRIT | `affected=` prüfen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. | | Endpoint Reachability ist CRIT | Nur `unavailable=` prüfen; DNS, Zielport, Firewall und externen Dienst kontrollieren. | -| Endpoint Reachability ist UNKNOWN | `unresolved_endpoints=`, WMI-Vollständigkeit, `endpoints.xml` und wöchentlichen Refresh prüfen; indirekte Adapterziele bei Bedarf geheimnisfrei manuell überschreiben. | +| Endpoint Reachability ist UNKNOWN | `unresolved_endpoints=`, WMI-Vollständigkeit, `endpoints.xml` und wöchentlichen Refresh prüfen. Schedule sowie nicht ableitbare DATABASE-/WCF-SAP-Ziele sind ab 2.2.7 bereits ausgenommen; andere indirekte Ziele bei Bedarf geheimnisfrei manuell überschreiben. | | `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema prüfen; keine Rechte ausweiten. | | Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos prüfen. | | Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL prüfen. | diff --git a/deployment/INSTALLATION.md b/deployment/INSTALLATION.md index 70ce1e9..51c1145 100644 --- a/deployment/INSTALLATION.md +++ b/deployment/INSTALLATION.md @@ -56,6 +56,13 @@ unverändert `UNKNOWN`, bis Auflösungslücken, Katalog-/Probeprobleme oder fehlende manuelle Overrides betrieblich geklärt sind. `UNKNOWN` in Platform, SQL oder einem anderen Kernservice bleibt ein blockierender Abnahmefehler. +Version 2.2.7 beseitigt die bekannten fachlichen Fehlalarme der +Endpoint-Prüfung: Scheduling-Komponenten werden ausgeschlossen; Oracle- +DATABASE- und WCF-SAP-Ziele werden als TCP-Best-Effort-Probes abgeleitet. +Nicht ableitbare oder nicht erreichbare Best-Effort-Ziele lösen weder UNKNOWN +noch CRIT aus. Reguläre und manuell konfigurierte Ziele bleiben unverändert +streng alarmierend. + Version 2.2.4 nimmt die umgeschaltete Installation zusätzlich vollständig ab: - einmaliger, triggerloser Providerlauf unter dem angegebenen Collector-Konto, diff --git a/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md b/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md index 8b1aad4..a8a7bba 100644 --- a/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md +++ b/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md @@ -1,5 +1,10 @@ # ACC-Analyse: Checkmk-Datenalter und Endpoint Reachability +> Nachtrag 11.08.2026: Version 2.2.7 behebt die aus der RTM-Ausgabe +> identifizierten Schedule-, Oracle-DATABASE- und WCF-SAP-Sonderfälle. Die +> verbindliche Nachfolgeregel steht in +> [RTM-Endpoint-Adapterregeln-2026-08-11.md](RTM-Endpoint-Adapterregeln-2026-08-11.md). + ## Befund Die am 10.08.2026 bereitgestellten ACC-Artefakte zeigen zwei getrennte diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md index ae8cb82..670a0ad 100644 --- a/docs/CheckmkServices.md +++ b/docs/CheckmkServices.md @@ -185,6 +185,10 @@ Zweck: - Erkennt unter anderem Standard-URIs, UNC, MSMQ, `net.tcp`, WCF-SQL- `mssql`-URIs, schemafreie `host:port`-Angaben, FTP-/SFTP-Ziele mit optionalen Benutzerinformationen sowie lokale HTTP-/WCF-Wildcard-Listener. +- Schließt Scheduling-Komponenten vollständig aus. Oracle-`DATABASE`- und + WCF-SAP-Adressen werden, soweit eindeutig möglich, als TCP-Best-Effort-Ziel + geprüft; fehlende Ableitung und fehlgeschlagene Best-Effort-Probes alarmieren + nicht. - Behandelt Adressen ohne eigenständiges Socket-Ziel (zum Beispiel lokale Pfade, SMTP-Empfänger, Named Pipes und relative Receive-Listener) als erwartbar ausgenommen und nicht als Abdeckungsfehler. @@ -197,8 +201,8 @@ Status: | Status | Bedingung | | --- | --- | -| `OK` | Katalog und Runtimezustand vollständig; alle aktiven prüfbaren Ziele erreichbar. | -| `CRIT` | Mindestens ein getestetes TCP-/UDP-Ziel nicht erreichbar. | +| `OK` | Katalog und Runtimezustand vollständig; alle alarmierenden Ziele erreichbar. Best-Effort-Fehler bei DATABASE/WCF-SAP sind höchstens als Metrik sichtbar. | +| `CRIT` | Mindestens ein reguläres oder manuell konfiguriertes TCP-/UDP-Ziel nicht erreichbar. | | `UNKNOWN` | Katalog fehlt/ist ungültig, Wochenabgleich scheitert, Runtimezustand ist unvollständig oder ein aktiver externer Endpoint kann nicht sicher auf Host/Port reduziert werden. | | `OK` mit Hinweis | Nur wenn `ProbeEndpointConnectivity=false`; der Test ist bewusst deaktiviert. | @@ -214,6 +218,8 @@ Metriken: | `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. | | `biztalk_endpoints_unresolved` | Aktive statische Netzwerkadressen ohne sicher bestimmbaren Host/Port; `UNKNOWN` ab 1, sofern kein manueller Override existiert. | | `biztalk_endpoints_expected_non_socket` | Aktive Artefakte, deren Adresse erwartbar kein eigenständiges TCP-/UDP-Ziel enthält. | +| `biztalk_endpoints_best_effort` | Automatisch abgeleitete aktive Oracle-DATABASE-/WCF-SAP-Ziele. | +| `biztalk_endpoints_best_effort_ignored` | Fehlgeschlagene Best-Effort-Probes, die vereinbarungsgemäß keinen Alarm auslösen. | | `biztalk_endpoints_manual_overrides` | Aktive Ziele, die bewusst über einen geheimnisfreien manuellen Katalogeintrag aufgelöst werden. | | `biztalk_endpoints_inactive_skipped` | Konfigurierte Einträge, deren BizTalk-Artefakt aktuell nicht aktiv ist. | | `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. | diff --git a/docs/EndpointCatalog.md b/docs/EndpointCatalog.md index 2ebcdc6..ded7f96 100644 --- a/docs/EndpointCatalog.md +++ b/docs/EndpointCatalog.md @@ -34,6 +34,7 @@ BizTalk-Runtimezustand gefiltert: - Receive Location nur bei `IsDisabled=false` - Send Port nur bei `Status=Started` - dynamischer Send Port nie als statisches Ziel +- Scheduling-Komponente nie als Netzwerkziel Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr geprüft. Prüffähige aktuelle WMI-Adressen werden bei jedem Minutenlauf direkt @@ -71,7 +72,7 @@ werden nur: - Adaptername - `TCP` oder `UDP` - Host und Port -- `enabled` und `autoDiscovered` +- `enabled`, `autoDiscovered` und die Best-Effort-Kennzeichnung URI-Benutzerinformationen, Kennwörter, Pfade und Querystrings werden nicht geschrieben. Der Katalog liegt im bereits ACL-geschützten `data`-Verzeichnis; @@ -135,6 +136,18 @@ passende nebenwirkungsfreie Netzwerktest. Er bestätigt DNS, Routing, Firewall und einen annehmenden TCP-Port, aber nicht die fachliche Antwort der Zielanwendung. +Oracle-`DATABASE`-Adressen mit explizitem Anbietersegment `/Oracle` verwenden +den angegebenen Port oder den Oracle-Standard TCP 1521. WCF-SAP-Adressen +verwenden vorrangig den ersten `SAPROUTER=/H/host[/S/port]`-Hop, andernfalls +`ListenerGwHost`/`ListenerGwServ` beziehungsweise `GwHost`/`GwServ`. +`sapgwNN` wird als TCP `33NN` abgeleitet; der SAProuter-Standard ist TCP 3299. +Diese automatisch erkannten Adapterziele sind `bestEffort="true"`: Die Probe +wird ausgeführt und gemessen, ein Fehlschlag aber nur in +`biztalk_endpoints_best_effort_ignored` gezählt. Nicht sicher ableitbare +DATABASE-/WCF-SAP-Adressen werden erwartbar ausgenommen. Ein manueller +Katalogeintrag bleibt standardmäßig streng alarmierend und ist die bewusste +Option für verbindliche Überwachung. + UDP ist verbindungslos. Bei `udp://host:port` bestätigt der Check nur DNS, Route und erfolgreichen lokalen Datagrammversand. Für einen belastbaren UDP-Dienstcheck wäre ein adapter-/protokollspezifisches Request/Response- diff --git a/docs/ExampleOutput.md b/docs/ExampleOutput.md index f161040..e74d76b 100644 --- a/docs/ExampleOutput.md +++ b/docs/ExampleOutput.md @@ -61,15 +61,18 @@ Artefakte. Nur die Textliste ist begrenzt. Gesund, ohne Auflistung aller Ziele: ```text -0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=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 +0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=60;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unresolved=0;;1;0|biztalk_endpoints_expected_non_socket=12;;;0|biztalk_endpoints_best_effort=9;;;0|biztalk_endpoints_best_effort_ignored=1;;;0|biztalk_endpoints_manual_overrides=0;;;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle regulaer alarmierenden aktiven Send-/Receive-Endpunkte sind erreichbar (active=61, available=60, unique_targets=44, expected_non_socket=12, best_effort=9, best_effort_ignored=1, manual_overrides=0, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z ``` Fehlerhaft, nur mit nicht erreichbaren Zielen: ```text -2 "BizTalk ACC Endpoint Reachability" ... Endpoint reachability active=61, tested=61, available=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) +2 "BizTalk ACC Endpoint Reachability" ... Endpoint reachability active=61, tested=61, available=58, failed=2, unresolved=0, expected_non_socket=12, best_effort=9, best_effort_ignored=1, manual_overrides=0; unavailable=SendPort:Orders\SP_Order_API[Primary]->orders.example:443/TCP(TCP timeout), ReceiveLocation:Invoices\RL_SFTP[Inbound]->sftp.example:22/TCP(SocketError=ConnectionRefused) ``` +Der nicht verfügbare Best-Effort-Endpunkt wird nur gezählt und nicht in +`unavailable=` ausgegeben. Die beiden regulären Fehler bleiben CRIT. + ## Fehlender oder veralteter Provider-Snapshot Alle neun Services werden `UNKNOWN`, zum Beispiel: diff --git a/docs/Integration.md b/docs/Integration.md index 5f80433..b7d1e2c 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -132,6 +132,9 @@ Soll: - Receive Locations und Send Ports zeigen getrennte expected/unexpected Werte. - Endpoint Reachability zeigt bei Erfolg nur die Gesamtzahl und bei Fehlern ausschließlich nicht erreichbare oder wirklich unaufgelöste Ziele. +- Schedule ist vollständig ausgenommen; Oracle-DATABASE und WCF-SAP laufen + als TCP-Best-Effort und können keinen Alarm auslösen. Die zugehörigen + Erfolgs-/Ignoriert-Zähler bleiben für die Betriebsanalyse sichtbar. - keine berechtigungsbedingten `UNKNOWN`-Services. ## Alarmierung der Transportkette diff --git a/docs/RTM-Endpoint-Adapterregeln-2026-08-11.md b/docs/RTM-Endpoint-Adapterregeln-2026-08-11.md new file mode 100644 index 0000000..e261c05 --- /dev/null +++ b/docs/RTM-Endpoint-Adapterregeln-2026-08-11.md @@ -0,0 +1,101 @@ +# RTM-Adapterregeln für Endpoint Reachability + +## Anlass + +Die ACC-Ausgabe vom 11.08.2026 zeigte `BizTalk Endpoint Reachability = +UNKNOWN`, obwohl die eigentlichen Socket-Probes in wenigen Millisekunden +abgeschlossen waren. Die Detailausgabe bestand überwiegend aus drei +Adapterklassen, die der generische URI-Parser nicht fachgerecht einordnen +konnte: + +- Scheduling-Komponenten ohne externes Netzwerkziel, +- kundenspezifische `DATABASE`-Adressen, nahezu ausschließlich Oracle, +- WCF-SAP-Verbindungs-URIs mit Gateway- und SAProuter-Parametern. + +Diese Fälle dürfen den RTM-Service weder mit Auflösungslücken noch mit +Fehlalarmen belasten. + +## Verbindliche Klassifizierung ab Version 2.2.7 + +### Scheduling + +Adaptername oder Adresse mit `Schedule`/`Scheduler` wird vollständig als +erwartbar ausgenommen. Es erfolgt keine Netzwerkprobe und kein UNKNOWN. + +### DATABASE / Oracle + +Nur die eindeutig erkennbare Form + +```text +Database://host[:port]/.../Oracle +``` + +wird automatisch aufgelöst. Ein expliziter Port gewinnt; andernfalls wird +TCP 1521 verwendet. Andere DATABASE-Anbieter oder nicht eindeutig lesbare +Adressen werden erwartbar ausgenommen. Es wird kein UDP-Test erzeugt. + +Oracle dokumentiert TCP 1521 als Standardadresse des Listeners und nimmt bei +fehlender Portangabe im Easy-Connect-Format ebenfalls 1521 an: + +- [Oracle: Configuring the Network Environment](https://docs.oracle.com/en/database/oracle/oracle-database/19/admqs/configuring-the-network-environment.html) +- [Oracle: Configuring and Administering Oracle Net Listener](https://docs.oracle.com/en/database/oracle/oracle-database/21/netag/configuring-and-administering-oracle-net-listener.html) + +### WCF-SAP + +Microsoft dokumentiert `ListenerGwHost`, `ListenerGwServ` und `SAPROUTER` als +Bestandteile der SAP-Verbindungs-URI. Der Parser verwendet in dieser +Reihenfolge: + +1. erster SAProuter-Hop aus `/H/host/S/port`, +2. `ListenerGwHost` und `ListenerGwServ`, +3. `GwHost` und `GwServ` als Fallback. + +Numerische Dienste werden direkt übernommen. `sapgwNN` wird auf TCP `33NN` +abgebildet. Fehlt bei einem SAProuter-Hop `/S/`, gilt der dokumentierte +Standard TCP 3299. URI-Escapes werden vor der Auswertung dekodiert; Kennwörter, +Program-ID, Pfade und Querystrings gelangen weiterhin nicht in Katalog, Log +oder Checkmk-Ausgabe. + +Quellen: + +- [Microsoft Learn: Create the SAP system connection URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sap/create-the-sap-system-connection-uri) +- [SAP Help: Entries in the Services File](https://help.sap.com/docs/SLTOOLSET/6bc963751d0840f996c5c9d7a7f83473/bb9faaebe0c74a58bbcc259c83f59437.html) +- [SAP Help: SAProuter Host and Port](https://help.sap.com/docs/SAP_NETWEAVER_AS_ABAP_FOR_SOH_740/e245703406684d8a81812f4c6334eb2f/486b5b0cb74c07bee10000000a42189d.html) + +## Best-Effort statt Alarm + +Automatisch abgeleitete DATABASE-/WCF-SAP-Ziele erhalten im lokalen Katalog +`bestEffort="true"`. Sie werden normal dedupliziert und per TCP verbunden. + +- Erfolg erhöht die getesteten und verfügbaren Zähler. +- Timeout, DNS- oder Verbindungsfehler erhöht + `biztalk_endpoints_best_effort_ignored`. +- Der Fehler erscheint nicht als `unavailable=`, erzeugt keinen Warn-Logeintrag + pro Minute und ändert den Checkmk-Status nicht. +- Eine nicht ableitbare Adapteradresse zählt zu + `biztalk_endpoints_expected_non_socket`, nicht zu `unresolved`. + +Damit erfüllt der Check die gewünschte „versuchen, sonst ignorieren“-Semantik. +Soll ein solches Ziel später verbindlich alarmieren, wird ein manueller +Katalogeintrag mit `autoDiscovered="false"` und ohne +`bestEffort="true"` hinterlegt. Reguläre automatisch erkannte Ziele bleiben +ebenfalls streng: Ihre Nichterreichbarkeit ist weiterhin CRIT. + +## Regression und Abnahme + +Die automatisierten Tests decken Scheduler-Ausschluss, Oracle-Standard- und +Explizitport, nicht unterstützte DATABASE-Anbieter, WCF-SAP-Gateway, +URI-kodierten SAProuter, zielbasierte SAP-Konfiguration ohne ableitbaren Host, +Geheimnisschutz und die nicht alarmierende Best-Effort-Ausgabe ab. + +Für die Windows-/ACC-Abnahme nach Installation von 2.2.7: + +1. Im Provider-Log muss der erzwungene Katalogabgleich erfolgreich sein. +2. `BizTalk Endpoint Reachability` darf wegen Schedule, DATABASE oder WCF-SAP + nicht mehr UNKNOWN werden. +3. `biztalk_endpoints_unresolved` darf diese drei Klassen nicht enthalten. +4. `biztalk_endpoints_best_effort` zeigt die tatsächlich abgeleiteten Oracle- + und SAP-Ziele; nicht erreichbare davon erscheinen nur in + `biztalk_endpoints_best_effort_ignored`. +5. Ein absichtlich nicht erreichbares reguläres Testziel muss weiterhin CRIT + auslösen, damit die strenge Standardalarmierung belegt bleibt. diff --git a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs index d9b8ed6..fca359e 100644 --- a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")] [assembly: AssemblyCompany("BEW")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")] -[assembly: AssemblyVersion("2.2.6.0")] -[assembly: AssemblyFileVersion("2.2.6.0")] +[assembly: AssemblyVersion("2.2.7.0")] +[assembly: AssemblyFileVersion("2.2.7.0")] diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs index 8c2a1e9..2856025 100644 --- a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs +++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs @@ -357,6 +357,8 @@ namespace BizTalkCheckmkPulse /// /// Formatiert die Netzwerk-Erreichbarkeit aller aktuell aktiven, pruefbaren BizTalk-Endpunkte. /// + /// Vollstaendiges Provider-Ergebnis mit Endpoint-Zustand. + /// Eine syntaktisch gueltige Checkmk-Local-Check-Zeile. private string FormatEndpointConnectivity(ProbeResult result) { var endpointState = result.EndpointConnectivity; @@ -378,7 +380,12 @@ namespace BizTalkCheckmkPulse "Endpoint-Pruefung nicht verlaesslich moeglich. " + EmptyAsUnknown(endpointState.Failure)); } - var failed = endpointState.Results.Where(x => !x.Available).ToArray(); + var failed = endpointState.Results + .Where(x => !x.Available && !x.Endpoint.BestEffort) + .ToArray(); + var ignoredBestEffort = endpointState.Results + .Where(x => !x.Available && x.Endpoint.BestEffort) + .ToArray(); var available = endpointState.Results.Count(x => x.Available); var incomplete = endpointState.Results.Count != endpointState.Active || endpointState.UnresolvedActive > 0 @@ -391,7 +398,7 @@ namespace BizTalkCheckmkPulse : CheckState.Ok; var metrics = string.Format( CultureInfo.InvariantCulture, - "biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unresolved={6};;1;0|biztalk_endpoints_expected_non_socket={7};;;0|biztalk_endpoints_manual_overrides={8};;;0|biztalk_endpoints_inactive_skipped={9};;;0|biztalk_endpoint_probe_duration_ms={10};;;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_best_effort={8};;;0|biztalk_endpoints_best_effort_ignored={9};;;0|biztalk_endpoints_manual_overrides={10};;;0|biztalk_endpoints_inactive_skipped={11};;;0|biztalk_endpoint_probe_duration_ms={12};;;0", endpointState.Configured, endpointState.Active, endpointState.UniqueTargets, @@ -400,15 +407,21 @@ namespace BizTalkCheckmkPulse failed.Length, endpointState.UnresolvedActive, endpointState.ExpectedNonProbeableActive, + endpointState.BestEffortActive, + ignoredBestEffort.Length, endpointState.ManualOverridesActive, endpointState.SkippedInactive, endpointState.ProbeDurationMilliseconds); var detail = new StringBuilder(); if (failed.Length == 0 && !incomplete) { - detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar") - .Append(" (unique_targets=").Append(endpointState.UniqueTargets) + detail.Append("Alle regulaer alarmierenden aktiven Send-/Receive-Endpunkte sind erreichbar") + .Append(" (active=").Append(endpointState.Active) + .Append(", available=").Append(available) + .Append(", unique_targets=").Append(endpointState.UniqueTargets) .Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive) + .Append(", best_effort=").Append(endpointState.BestEffortActive) + .Append(", best_effort_ignored=").Append(ignoredBestEffort.Length) .Append(", manual_overrides=").Append(endpointState.ManualOverridesActive) .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")"); } @@ -420,6 +433,8 @@ namespace BizTalkCheckmkPulse .Append(", failed=").Append(failed.Length) .Append(", unresolved=").Append(endpointState.UnresolvedActive) .Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive) + .Append(", best_effort=").Append(endpointState.BestEffortActive) + .Append(", best_effort_ignored=").Append(ignoredBestEffort.Length) .Append(", manual_overrides=").Append(endpointState.ManualOverridesActive) .Append(", unique_targets=").Append(endpointState.UniqueTargets) .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds); diff --git a/src/BizTalkCheckmkPulse/EndpointAddressParser.cs b/src/BizTalkCheckmkPulse/EndpointAddressParser.cs index a361e3f..eb511ff 100644 --- a/src/BizTalkCheckmkPulse/EndpointAddressParser.cs +++ b/src/BizTalkCheckmkPulse/EndpointAddressParser.cs @@ -13,9 +13,17 @@ namespace BizTalkCheckmkPulse @"^(?\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):(?\d{1,5})(?:[/\\].*)?$", RegexOptions.CultureInvariant); + private static readonly Regex DatabaseProviderPattern = new Regex( + @"/(?:Oracle|SQLServer)(?:[/;?]|$)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + /// /// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag. /// + /// Aktiver BizTalk-Transport mit Adaptername und Adresse. + /// Geheimnisfreier Katalogeintrag, sofern ein Ziel bestimmt wurde. + /// Begruendung, wenn kein pruefbares Ziel bestimmt wurde. + /// , wenn Host, Port und Protokoll sicher bestimmt wurden. public static bool TryCreate( EndpointCandidate candidate, out EndpointCatalogEntry entry, @@ -30,6 +38,8 @@ namespace BizTalkCheckmkPulse /// /// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken. /// + /// Zu klassifizierender BizTalk-Transport. + /// Klassifizierung sowie gegebenenfalls das pruefbare Socket-Ziel. public static EndpointResolution Analyze(EndpointCandidate candidate) { if (candidate == null) @@ -42,6 +52,13 @@ namespace BizTalkCheckmkPulse return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel."); } + // Scheduling-Adapter bilden zeitliche BizTalk-Steuerung ab und besitzen kein + // fachlich relevantes Remoteziel. Sie werden bewusst vollstaendig ausgeklammert. + if (IsScheduler(candidate)) + { + return Expected("Scheduling-Komponente ist von der Endpoint-Pruefung ausgenommen."); + } + var address = (candidate.Address ?? string.Empty).Trim(); if (address.Length == 0) { @@ -80,6 +97,33 @@ namespace BizTalkCheckmkPulse return Expected("Relative Receive-Listener-Adresse enthaelt keine eindeutige Site-Bindung."); } + // DATABASE und WCF-SAP liefern Adapteradressen, deren Socket-Ziel nicht von + // System.Uri abgeleitet werden kann. Eine fehlende sichere Ableitung ist bei + // diesen Best-Effort-Adaptern kein Abdeckungsfehler. + if (IsDatabaseAdapter(candidate)) + { + EndpointCatalogEntry databaseEntry; + string databaseReason; + if (TryResolveDatabase(candidate, address, out databaseEntry, out databaseReason)) + { + return Probeable(databaseEntry); + } + + return Expected(databaseReason); + } + + if (IsWcfSapAdapter(candidate)) + { + EndpointCatalogEntry sapEntry; + string sapReason; + if (TryResolveWcfSap(candidate, address, out sapEntry, out sapReason)) + { + return Probeable(sapEntry); + } + + return Expected(sapReason); + } + EndpointCatalogEntry entry; string protocol; string host; @@ -115,8 +159,306 @@ namespace BizTalkCheckmkPulse Host = host, Port = port, Enabled = true, - AutoDiscovered = true + AutoDiscovered = true, + BestEffort = false }; + return Probeable(entry); + } + + /// + /// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen. + /// + /// Zu klassifizierender BizTalk-Transport. + /// nur bei einer echten Aufloesungsluecke. + public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate) + { + return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved; + } + + /// + /// Leitet das Oracle-TCP-Ziel aus der expliziten DATABASE-Adresse ab. + /// + /// BizTalk-Artefakt, aus dem der Katalogeintrag aufgebaut wird. + /// Vollstaendige DATABASE-Transportadresse. + /// Abgeleiteter geheimnisfreier Best-Effort-Eintrag. + /// Begruendung, wenn Host oder Anbieter nicht sicher erkennbar ist. + /// , wenn ein Oracle-Host und TCP-Port bestimmt wurden. + private static bool TryResolveDatabase( + EndpointCandidate candidate, + string address, + out EndpointCatalogEntry entry, + out string reason) + { + entry = null; + reason = "DATABASE-Adresse enthaelt keinen sicher bestimmbaren Oracle-Listener."; + if (!address.StartsWith("database://", StringComparison.OrdinalIgnoreCase) + || !DatabaseProviderPattern.IsMatch(address)) + { + return false; + } + + Uri uri; + if (!Uri.TryCreate(address, UriKind.Absolute, out uri) + || string.IsNullOrWhiteSpace(uri.Host)) + { + return false; + } + + var oracle = Regex.IsMatch( + address, + @"/Oracle(?:[/;?]|$)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + if (!oracle) + { + reason = "Nicht-Oracle-DATABASE-Ziel wird ohne explizite Betriebsfreigabe nicht automatisch geprueft."; + return false; + } + + var port = HasExplicitPort(address) ? uri.Port : 1521; + string validationReason; + var host = NormalizeHost(uri.Host); + if (!Validate(host, port, out validationReason)) + { + reason = validationReason; + return false; + } + + entry = CreateAdapterEntry(candidate, host, port); + reason = string.Empty; + return true; + } + + /// + /// Leitet den ersten erreichbaren TCP-Hop einer WCF-SAP-Verbindung ab. + /// + /// BizTalk-Artefakt, aus dem der Katalogeintrag aufgebaut wird. + /// Vollstaendige SAP-Verbindungs-URI. + /// Abgeleiteter geheimnisfreier Best-Effort-Eintrag. + /// Begruendung, wenn Gateway oder SAProuter nicht sicher bestimmbar ist. + /// , wenn ein SAProuter- oder Gateway-Ziel bestimmt wurde. + private static bool TryResolveWcfSap( + EndpointCandidate candidate, + string address, + out EndpointCatalogEntry entry, + out string reason) + { + entry = null; + reason = "WCF-SAP-Adresse enthaelt keinen sicher bestimmbaren Gateway- oder SAProuter-Port."; + var decoded = DecodeAdapterAddress(address); + + string route; + if (TryReadParameter(decoded, "SAPROUTER", out route)) + { + string routerHost; + int routerPort; + if (TryParseSapRouter(route, out routerHost, out routerPort)) + { + entry = CreateAdapterEntry(candidate, routerHost, routerPort); + reason = string.Empty; + return true; + } + } + + string gatewayHost; + string gatewayService; + if ((!TryReadParameter(decoded, "ListenerGwHost", out gatewayHost) + || !TryReadParameter(decoded, "ListenerGwServ", out gatewayService)) + && (!TryReadParameter(decoded, "GwHost", out gatewayHost) + || !TryReadParameter(decoded, "GwServ", out gatewayService))) + { + return false; + } + + int gatewayPort; + if (!TryResolveSapService(gatewayService, out gatewayPort)) + { + return false; + } + + gatewayHost = NormalizeHost(gatewayHost); + string validationReason; + if (!Validate(gatewayHost, gatewayPort, out validationReason)) + { + reason = validationReason; + return false; + } + + entry = CreateAdapterEntry(candidate, gatewayHost, gatewayPort); + reason = string.Empty; + return true; + } + + /// + /// Baut einen TCP-Katalogeintrag fuer einen automatisch abgeleiteten Best-Effort-Adapter. + /// + /// Quellartefakt fuer Metadaten und stabilen Schluessel. + /// Bereinigter DNS-Name oder IP-Adresse. + /// Gueltiger TCP-Zielport. + /// Automatisch verwalteter, nicht alarmierender Katalogeintrag. + private static EndpointCatalogEntry CreateAdapterEntry(EndpointCandidate candidate, string host, int port) + { + return new EndpointCatalogEntry + { + Key = candidate.Key, + ArtifactType = candidate.ArtifactType, + ApplicationName = candidate.ApplicationName, + ArtifactName = candidate.ArtifactName, + TransportRole = candidate.TransportRole, + AdapterName = candidate.AdapterName, + Protocol = "TCP", + Host = host, + Port = port, + Enabled = true, + AutoDiscovered = true, + BestEffort = true + }; + } + + /// + /// Liest einen URI-Parameter auch aus SAP-URIs mit wiederholtem Fragezeichen. + /// + /// Dekodierte SAP-Verbindungsadresse. + /// Name des gesuchten Query-Parameters. + /// Bereinigter Parameterwert. + /// , wenn ein nicht leerer Wert gefunden wurde. + private static bool TryReadParameter(string address, string name, out string value) + { + value = string.Empty; + var match = Regex.Match( + address ?? string.Empty, + @"(?:^|[?&])" + Regex.Escape(name) + @"=(?[^?&]+)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + if (!match.Success) + { + return false; + } + + value = match.Groups["value"].Value.Trim(); + return value.Length > 0; + } + + /// + /// Dekodiert URI-Escapes, ohne eine fehlerhafte Adapteradresse zum Laufzeitfehler zu machen. + /// + /// Rohe BizTalk-Transportadresse. + /// Bestmoeglich dekodierte Adresse. + private static string DecodeAdapterAddress(string address) + { + var normalized = (address ?? string.Empty).Replace("&", "&"); + try + { + return Uri.UnescapeDataString(normalized); + } + catch (UriFormatException) + { + return normalized; + } + } + + /// + /// Bestimmt Host und Port des ersten SAProuter-Hops einer Route. + /// + /// SAProuter-Route im Format /H/host/S/port. + /// Host des ersten Router-Hops. + /// Expliziter Port oder SAProuter-Standardport 3299. + /// , wenn der erste Hop gueltig ist. + private static bool TryParseSapRouter(string route, out string host, out int port) + { + host = string.Empty; + port = 0; + var match = Regex.Match( + route ?? string.Empty, + @"/H/(?[^/]+)(?:/S/(?[^/]+))?", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + if (!match.Success) + { + return false; + } + + host = NormalizeHost(match.Groups["host"].Value); + port = 3299; + var service = match.Groups["service"].Value; + if (service.Length > 0 && !TryResolveSapService(service, out port)) + { + return false; + } + + string reason; + return Validate(host, port, out reason); + } + + /// + /// Uebersetzt numerische SAP-Dienste und sapgwNN in einen TCP-Port. + /// + /// Numerischer Port oder SAP-Gateway-Dienstname. + /// Aufgeloester TCP-Port. + /// bei einem gueltigen Port oder sapgwNN. + private static bool TryResolveSapService(string service, out int port) + { + port = 0; + var normalized = (service ?? string.Empty).Trim(); + if (int.TryParse(normalized, out port)) + { + return port >= 1 && port <= 65535; + } + + var match = Regex.Match( + normalized, + @"^sapgw(?\d{2})$", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + int instance; + if (!match.Success || !int.TryParse(match.Groups["instance"].Value, out instance)) + { + return false; + } + + port = 3300 + instance; + return true; + } + + /// + /// Erkennt BizTalk-Scheduling-Komponenten, die kein Remoteziel darstellen. + /// + /// Zu pruefender BizTalk-Transport. + /// fuer Schedule-/Scheduler-Konfigurationen. + private static bool IsScheduler(EndpointCandidate candidate) + { + return Contains(candidate.AdapterName, "SCHEDUL") + || (candidate.Address ?? string.Empty).StartsWith("schedule", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Erkennt den kundenspezifischen DATABASE-Adapter und dessen URI-Schema. + /// + /// Zu pruefender BizTalk-Transport. + /// fuer DATABASE-Konfigurationen. + private static bool IsDatabaseAdapter(EndpointCandidate candidate) + { + return Contains(candidate.AdapterName, "DATABASE") + || (candidate.Address ?? string.Empty).StartsWith("database://", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Erkennt WCF-SAP-Adapter, ohne beliebige Adapter mit SAP im Namen einzubeziehen. + /// + /// Zu pruefender BizTalk-Transport. + /// fuer eindeutig erkannte WCF-SAP-Konfigurationen. + private static bool IsWcfSapAdapter(EndpointCandidate candidate) + { + return Contains(candidate.AdapterName, "WCF-SAP") + || Contains(candidate.AdapterName, "WCF SAP") + // BizTalk meldet den SAP Binding-Typ in manchen Umgebungen nur als + // WCF-Custom; das eindeutige sap://-Schema bleibt dennoch erhalten. + || (candidate.Address ?? string.Empty).StartsWith("sap://", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Verpackt einen gueltigen Katalogeintrag als pruefbare Aufloesung. + /// + /// Vollstaendig validierter Katalogeintrag. + /// Pruefbare Endpoint-Aufloesung ohne Fehlergrund. + private static EndpointResolution Probeable(EndpointCatalogEntry entry) + { return new EndpointResolution { Status = EndpointResolutionStatus.Probeable, @@ -125,14 +467,6 @@ namespace BizTalkCheckmkPulse }; } - /// - /// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen. - /// - public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate) - { - return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved; - } - private static bool TryResolve( string address, string adapterName, diff --git a/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs b/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs index 8bcb369..2b4cc9c 100644 --- a/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs +++ b/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs @@ -20,6 +20,12 @@ namespace BizTalkCheckmkPulse private readonly int _maxBytes; private readonly int _maxEntries; + /// + /// Initialisiert den validierenden XML-Katalogspeicher. + /// + /// Absoluter Zielpfad der Katalogdatei. + /// Maximal akzeptierte Dateigroesse. + /// Maximal akzeptierte Anzahl Eintraege. public EndpointCatalogStore(string path, int maxBytes, int maxEntries) { _path = path; @@ -27,6 +33,11 @@ namespace BizTalkCheckmkPulse _maxEntries = maxEntries; } + /// + /// Liest und validiert einen maschinen- und umgebungsgebundenen Katalog. + /// + /// Erwartete Monitoring-Umgebung. + /// Vollstaendig validierter Endpoint-Katalog. public EndpointCatalog Read(string environmentName) { var info = new FileInfo(_path); @@ -112,7 +123,9 @@ namespace BizTalkCheckmkPulse Host = ReadAttribute(node, "host"), Port = ReadInt(node, "port", 1, 65535), Enabled = ReadBool(node, "enabled"), - AutoDiscovered = ReadBool(node, "autoDiscovered") + AutoDiscovered = ReadBool(node, "autoDiscovered"), + // Das optionale Attribut haelt bestehende Kataloge der Version 1 lesbar. + BestEffort = ReadOptionalBool(node, "bestEffort", false) }; ValidateEntry(entry); catalog.Entries.Add(entry); @@ -126,6 +139,10 @@ namespace BizTalkCheckmkPulse return catalog; } + /// + /// Validiert und ersetzt den Endpoint-Katalog atomar. + /// + /// Zu persistierender geheimnisfreier Katalog. public void Write(EndpointCatalog catalog) { if (catalog == null) @@ -170,7 +187,8 @@ namespace BizTalkCheckmkPulse new XAttribute("host", entry.Host), new XAttribute("port", entry.Port), new XAttribute("enabled", entry.Enabled), - new XAttribute("autoDiscovered", entry.AutoDiscovered))); + new XAttribute("autoDiscovered", entry.AutoDiscovered), + new XAttribute("bestEffort", entry.BestEffort))); } var document = new XDocument(new XDeclaration("1.0", "utf-8", null), root); @@ -284,5 +302,29 @@ namespace BizTalkCheckmkPulse return value; } + + /// + /// Liest ein optionales boolesches Attribut mit rueckwaertskompatiblem Standardwert. + /// + /// XML-Element mit dem optionalen Attribut. + /// Name des Attributs. + /// Rueckgabewert, wenn das Attribut fehlt. + /// Gelesener Wert oder der angegebene Standardwert. + private static bool ReadOptionalBool(XElement element, string name, bool defaultValue) + { + var attribute = element.Attribute(name); + if (attribute == null) + { + return defaultValue; + } + + bool value; + if (!bool.TryParse(attribute.Value.Trim(), out value)) + { + throw new InvalidDataException("Endpoint catalog boolean is invalid: " + name); + } + + return value; + } } } diff --git a/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs b/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs index 27a575f..4b78bc3 100644 --- a/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs +++ b/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs @@ -18,12 +18,21 @@ namespace BizTalkCheckmkPulse private readonly MonitoringOptions _options; private readonly FileLogger _logger; + /// + /// Initialisiert die Endpoint-Pruefung mit Laufzeitgrenzen und Dateiprotokoll. + /// + /// Validierte Monitoring-Konfiguration. + /// Gemeinsames Provider-Dateiprotokoll. public EndpointConnectivityProbe(MonitoringOptions options, FileLogger logger) { _options = options; _logger = logger; } + /// + /// Synchronisiert den Katalog und ergaenzt das Gesamtergebnis um Socket-Probes. + /// + /// Bereits mit aktuellen BizTalk-Artefakten gefuelltes Ergebnis. public void Query(ProbeResult result) { var state = result.EndpointConnectivity; @@ -141,7 +150,13 @@ namespace BizTalkCheckmkPulse foreach (var probeResult in probeResults) { state.Results.Add(probeResult); - if (!probeResult.Available) + if (!probeResult.Available && probeResult.Endpoint.BestEffort) + { + // DATABASE-/WCF-SAP-Ziele sind bewusst Diagnose-Probes: Ein + // Fehlschlag darf weder Tageslog noch Checkmk mit Alarmen fluten. + state.BestEffortIgnoredFailures++; + } + else if (!probeResult.Available) { _logger.Warning( "Endpoint unavailable. endpoint=" + Display(probeResult.Endpoint) @@ -157,6 +172,13 @@ namespace BizTalkCheckmkPulse } } + /// + /// Fuehrt manuelle Overrides mit den aktuell automatisch erkannten Zielen zusammen. + /// + /// Vorhandener Katalog oder . + /// Aktuelle BizTalk-Send-/Receive-Transporte. + /// UTC-Zeitpunkt des vollstaendigen Abgleichs. + /// Neuer, geheimnisfreier Endpoint-Katalog. internal static EndpointCatalog SynchronizeCatalog( EndpointCatalog existing, IEnumerable candidates, @@ -223,6 +245,10 @@ namespace BizTalkCheckmkPulse /// 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. /// + /// Gueltiger lokaler Endpoint-Katalog. + /// Aktuelle aktive und inaktive BizTalk-Transporte. + /// Zu aktualisierende Laufzeitzaehler und Aufloesungsdiagnosen. + /// Aktuell aktive, deduplizierte Katalogeintraege. internal static EndpointCatalogEntry[] ResolveActiveEndpoints( EndpointCatalog catalog, IEnumerable candidates, @@ -284,10 +310,12 @@ namespace BizTalkCheckmkPulse x.Enabled && !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase) && !activeKeys.Contains(x.Key)); - return selected + var resolved = selected .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .Select(x => x.First()) .ToArray(); + state.BestEffortActive = resolved.Count(x => x.BestEffort); + return resolved; } /// @@ -356,6 +384,13 @@ namespace BizTalkCheckmkPulse }).ToArray(); } + /// + /// Berechnet die theoretische Obergrenze fuer vollstaendig timeoutende Zielgruppen. + /// + /// Anzahl eindeutiger Socket-Ziele. + /// Maximale parallele Probes. + /// Timeout je Probe in Millisekunden. + /// Theoretische Worst-Case-Dauer in Millisekunden. internal static long CalculateWorstCaseProbeMilliseconds(int targetCount, int concurrency, int timeoutMilliseconds) { if (targetCount <= 0) @@ -465,6 +500,11 @@ namespace BizTalkCheckmkPulse : "SocketError=" + socket.SocketErrorCode + ": " + socket.Message; } + /// + /// Formatiert ein geheimnisfreies Socket-Ziel fuer Checkmk und Dateilog. + /// + /// Zu formatierender Katalogeintrag. + /// Artefakt, Host, Port und Protokoll ohne URI-Geheimnisse. internal static string Display(EndpointCatalogEntry endpoint) { var artifact = string.IsNullOrWhiteSpace(endpoint.ApplicationName) diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs index 87f40ab..f015f8d 100644 --- a/src/BizTalkCheckmkPulse/Models.cs +++ b/src/BizTalkCheckmkPulse/Models.cs @@ -305,7 +305,20 @@ namespace BizTalkCheckmkPulse public int Port { get; set; } public bool Enabled { get; set; } public bool AutoDiscovered { get; set; } + /// + /// Gibt an, dass ein automatisch abgeleiteter Adaptertest nur Diagnosewert besitzt + /// und ein Fehlschlag deshalb keinen Checkmk-Alarm ausloest. + /// + public bool BestEffort { get; set; } + /// + /// Erstellt den stabilen, adressunabhaengigen Schluessel eines BizTalk-Transports. + /// + /// Typ des BizTalk-Artefakts. + /// Name der BizTalk-Anwendung. + /// Name des Send Ports oder der Receive Location. + /// Primaer-, Sekundaer- oder Inbound-Rolle. + /// Normalisierter Katalogschluessel. public static string BuildKey(string artifactType, string applicationName, string artifactName, string transportRole) { return NormalizeKeyPart(artifactType) @@ -371,6 +384,14 @@ namespace BizTalkCheckmkPulse public int SkippedInactive { get; set; } public int UnresolvedActive { get; set; } public int ExpectedNonProbeableActive { get; set; } + /// + /// Anzahl der automatisch abgeleiteten, nicht alarmierenden Adapterziele. + /// + public int BestEffortActive { get; set; } + /// + /// Anzahl fehlgeschlagener Best-Effort-Probes im aktuellen Lauf. + /// + public int BestEffortIgnoredFailures { get; set; } public int ManualOverridesActive { get; set; } public int UniqueTargets { get; set; } public long ProbeDurationMilliseconds { get; set; } diff --git a/src/BizTalkCheckmkPulse/Program.cs b/src/BizTalkCheckmkPulse/Program.cs index 3a71b8a..b10e395 100644 --- a/src/BizTalkCheckmkPulse/Program.cs +++ b/src/BizTalkCheckmkPulse/Program.cs @@ -115,7 +115,9 @@ namespace BizTalkCheckmkPulse + " endpoints_active=" + result.EndpointConnectivity.Active + " endpoints_failed=" - + result.EndpointConnectivity.Results.Count(x => !x.Available) + + result.EndpointConnectivity.Results.Count(x => !x.Available && !x.Endpoint.BestEffort) + + " endpoints_best_effort_ignored=" + + result.EndpointConnectivity.BestEffortIgnoredFailures + " endpoints_unresolved=" + result.EndpointConnectivity.UnresolvedActive + " endpoints_expected_non_socket=" diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs index 5f17c6f..81c0ee1 100644 --- a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ using System.Reflection; [assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")] [assembly: AssemblyCompany("BEW")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")] -[assembly: AssemblyVersion("2.2.6.0")] -[assembly: AssemblyFileVersion("2.2.6.0")] +[assembly: AssemblyVersion("2.2.7.0")] +[assembly: AssemblyFileVersion("2.2.7.0")] diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs index 092b4ce..4789f7d 100644 --- a/tests/BizTalkCheckmkPulse.Tests/Program.cs +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -30,11 +30,13 @@ namespace BizTalkCheckmkPulse.Tests Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded); Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets); Run("AdapterSpecificEndpointAddressesResolve", AdapterSpecificEndpointAddressesResolve); + Run("SchedulerDatabaseAndSapEndpointsAreClassifiedSafely", SchedulerDatabaseAndSapEndpointsAreClassifiedSafely); Run("EndpointClassificationSeparatesExpectedAndUnresolved", EndpointClassificationSeparatesExpectedAndUnresolved); Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides); Run("EndpointRuntimeUsesCurrentAddressAndManualOverrides", EndpointRuntimeUsesCurrentAddressAndManualOverrides); Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip); Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets); + Run("BestEffortEndpointFailuresRemainNonAlerting", BestEffortEndpointFailuresRemainNonAlerting); Run("EndpointOutputExplainsOnlyRealResolutionGaps", EndpointOutputExplainsOnlyRealResolutionGaps); Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval); Run("ForcedEndpointRefreshRewritesFreshCatalog", ForcedEndpointRefreshRewritesFreshCatalog); @@ -470,6 +472,73 @@ namespace BizTalkCheckmkPulse.Tests AssertEqual(8080, endpoint.Port, "wildcard listener port"); } + private static void SchedulerDatabaseAndSapEndpointsAreClassifiedSafely() + { + var candidate = new EndpointCandidate + { + ArtifactType = "ReceiveLocation", + ApplicationName = "Integration", + ArtifactName = "RL Scheduler", + TransportRole = "Inbound", + AdapterName = "Schedule", + Address = "schedule://weekday/06:00", + Active = true + }; + + AssertEqual( + EndpointResolutionStatus.ExpectedNonProbeable, + EndpointAddressParser.Analyze(candidate).Status, + "scheduler must be excluded"); + + candidate.ArtifactType = "SendPort"; + candidate.ArtifactName = "SP Oracle"; + candidate.TransportRole = "Primary"; + candidate.AdapterName = "DATABASE"; + candidate.Address = "Database://oracle01/SAPUSER/Oracle"; + var resolution = EndpointAddressParser.Analyze(candidate); + AssertEqual(EndpointResolutionStatus.Probeable, resolution.Status, "Oracle DATABASE classification"); + AssertEqual("oracle01", resolution.Entry.Host, "Oracle DATABASE host"); + AssertEqual(1521, resolution.Entry.Port, "Oracle DATABASE default port"); + Assert(resolution.Entry.BestEffort, "Oracle DATABASE probe must be best effort"); + + candidate.Address = "Database://oracle01:1526/SAPUSER/Oracle"; + resolution = EndpointAddressParser.Analyze(candidate); + AssertEqual(1526, resolution.Entry.Port, "Oracle DATABASE explicit port"); + + candidate.Address = "Database://sql01/Lookup/SQLServer"; + resolution = EndpointAddressParser.Analyze(candidate); + AssertEqual( + EndpointResolutionStatus.ExpectedNonProbeable, + resolution.Status, + "non-Oracle DATABASE endpoint must not cause UNKNOWN"); + + candidate.ArtifactType = "ReceiveLocation"; + candidate.ArtifactName = "RL SAP"; + candidate.TransportRole = "Inbound"; + // WMI kann einen WCF-SAP-Port lediglich als WCF-Custom ausweisen; + // das sap://-Schema muss deshalb fuer die Klassifizierung genuegen. + candidate.AdapterName = "WCF-Custom"; + candidate.Address = "sap://Client=800;lang=DE@A/sap-app/00?ListenerGwHost=sap-gateway.example&ListenerGwServ=SAPGW00&ListenerProgramId=SECRET"; + resolution = EndpointAddressParser.Analyze(candidate); + AssertEqual(EndpointResolutionStatus.Probeable, resolution.Status, "WCF-SAP listener classification"); + AssertEqual("sap-gateway.example", resolution.Entry.Host, "WCF-SAP gateway host"); + AssertEqual(3300, resolution.Entry.Port, "WCF-SAP sapgw00 port"); + Assert(resolution.Entry.BestEffort, "WCF-SAP probe must be best effort"); + Assert(resolution.Entry.Host.IndexOf("SECRET", StringComparison.OrdinalIgnoreCase) < 0, "SAP Program ID must not enter catalog"); + + candidate.Address = "sap://Client=800;lang=DE@A/sap-app/00?SAPROUTER=%2fH%2fsap-router.example%2fS%2f3400%2fH%2fsap-gateway.example&ListenerGwHost=sap-gateway.example&ListenerGwServ=sapgw00"; + resolution = EndpointAddressParser.Analyze(candidate); + AssertEqual("sap-router.example", resolution.Entry.Host, "SAProuter first hop host"); + AssertEqual(3400, resolution.Entry.Port, "SAProuter explicit port"); + + candidate.Address = "sap://Client=800;lang=DE@D/DESTINATION"; + resolution = EndpointAddressParser.Analyze(candidate); + AssertEqual( + EndpointResolutionStatus.ExpectedNonProbeable, + resolution.Status, + "destination-based WCF-SAP endpoint must not cause UNKNOWN"); + } + private static void EndpointClassificationSeparatesExpectedAndUnresolved() { var candidate = new EndpointCandidate @@ -617,6 +686,33 @@ namespace BizTalkCheckmkPulse.Tests Assert(line.IndexOf("up.example.test", StringComparison.Ordinal) < 0, "reachable host must not clutter output"); } + private static void BestEffortEndpointFailuresRemainNonAlerting() + { + var result = new ProbeResult(); + result.EndpointConnectivity.RuntimeStateAvailable = true; + result.EndpointConnectivity.CatalogAvailable = true; + result.EndpointConnectivity.RefreshSucceeded = true; + result.EndpointConnectivity.Configured = 1; + result.EndpointConnectivity.Active = 1; + result.EndpointConnectivity.BestEffortActive = 1; + var endpoint = TestEndpoint("SAP Listener", "sap-gateway.example"); + endpoint.AdapterName = "WCF-SAP"; + endpoint.Port = 3300; + endpoint.BestEffort = true; + result.EndpointConnectivity.Results.Add(new EndpointProbeResult + { + Endpoint = endpoint, + Available = false, + Failure = "TCP timeout" + }); + + var line = FindServiceLine(result, "Endpoint Reachability"); + Assert(line.StartsWith("0 \"BizTalk Endpoint Reachability\"", StringComparison.Ordinal), "best-effort failure must remain OK"); + Assert(line.Contains("biztalk_endpoints_failed=0"), "best-effort failure must not increment failed metric"); + Assert(line.Contains("biztalk_endpoints_best_effort_ignored=1"), "ignored best-effort metric missing"); + Assert(line.IndexOf("unavailable=", StringComparison.OrdinalIgnoreCase) < 0, "best-effort target must not appear as unavailable"); + } + private static void EndpointOutputExplainsOnlyRealResolutionGaps() { var result = new ProbeResult(); @@ -652,7 +748,9 @@ namespace BizTalkCheckmkPulse.Tests ActiveCandidates = 150, UnresolvedCandidates = 7 }; - catalog.Entries.Add(TestEndpoint("Orders", "api.example.test")); + var storedEndpoint = TestEndpoint("Orders", "api.example.test"); + storedEndpoint.BestEffort = true; + catalog.Entries.Add(storedEndpoint); var store = new EndpointCatalogStore(path, 1048576, 100); store.Write(catalog); var loaded = store.Read("ACC"); @@ -660,8 +758,18 @@ namespace BizTalkCheckmkPulse.Tests 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(loaded.Entries[0].BestEffort, "catalog best-effort marker"); AssertEqual(7, loaded.UnresolvedCandidates, "catalog unresolved candidate count"); Assert(File.ReadAllText(path).IndexOf("top-secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog must not contain URI secrets"); + + // Vorhandene Kataloge der Formatversion 1 besitzen das neue optionale + // Attribut noch nicht und muessen beim Update weiterhin lesbar bleiben. + var legacyXml = File.ReadAllText(path) + .Replace(" bestEffort=\"true\"", string.Empty) + .Replace(" bestEffort=\"True\"", string.Empty); + File.WriteAllText(path, legacyXml, new UTF8Encoding(false)); + loaded = store.Read("ACC"); + Assert(!loaded.Entries[0].BestEffort, "legacy catalog best-effort default"); } finally {