Updated to .NET Framework 4.7.2 due to platform limitations, added email cc, bcc and some fixes.
This commit is contained in:
@@ -0,0 +1,532 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This repository is organized in a **Gitea-friendly repository structure** and contains two applications:
|
||||||
|
|
||||||
|
### 1. HostAvailabilityMonitor
|
||||||
|
|
||||||
|
The monitor checks technical reachability and writes:
|
||||||
|
|
||||||
|
- rolling file logs
|
||||||
|
- Windows Application Event Log
|
||||||
|
- email notifications for failures, recoveries, and a daily status summary
|
||||||
|
- a state file for transition detection, cooldown handling, and once-per-day summary tracking
|
||||||
|
|
||||||
|
### 2. BizTalkMonitorConfigGenerator
|
||||||
|
|
||||||
|
The generator reads BizTalk artifacts and creates a monitor configuration from them.
|
||||||
|
|
||||||
|
The goal is to avoid manually maintaining the target list and instead derive it directly from BizTalk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository structure for Gitea
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── .editorconfig
|
||||||
|
├── .gitignore
|
||||||
|
├── Documentation.en.md
|
||||||
|
├── Dokumentation.md
|
||||||
|
├── HostAvailabilityMonitor.sln
|
||||||
|
├── README.md
|
||||||
|
├── installers/
|
||||||
|
│ ├── README.md
|
||||||
|
│ ├── powershell/
|
||||||
|
│ │ ├── Install-BizTalkMonitorConfigGenerator.ps1
|
||||||
|
│ │ ├── Install-Both.ps1
|
||||||
|
│ │ ├── Install-HostAvailabilityMonitor.ps1
|
||||||
|
│ │ ├── Uninstall-BizTalkMonitorConfigGenerator.ps1
|
||||||
|
│ │ └── Uninstall-HostAvailabilityMonitor.ps1
|
||||||
|
│ └── wix/
|
||||||
|
│ ├── BizTalkMonitorConfigGenerator.wxs
|
||||||
|
│ ├── HostAvailabilityMonitor.wxs
|
||||||
|
│ └── build-msi.ps1
|
||||||
|
├── scripts/
|
||||||
|
│ ├── build-release.ps1
|
||||||
|
│ ├── generate-monitor-config.ps1
|
||||||
|
│ ├── install-generator-on-server.ps1
|
||||||
|
│ ├── install-on-server.ps1
|
||||||
|
│ ├── package-deployment.ps1
|
||||||
|
│ └── register-event-source.ps1
|
||||||
|
└── src/
|
||||||
|
├── BizTalkMonitorConfigGenerator/
|
||||||
|
│ ├── App.config
|
||||||
|
│ ├── BizTalkMonitorConfigGenerator.csproj
|
||||||
|
│ ├── Program.cs
|
||||||
|
│ └── generator-settings.json
|
||||||
|
└── HostAvailabilityMonitor/
|
||||||
|
├── App.config
|
||||||
|
├── HostAvailabilityMonitor.csproj
|
||||||
|
├── Program.cs
|
||||||
|
└── appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This layout is intentionally simple so it can be committed to **Gitea**, built on Windows, and deployed to BizTalk or application servers with minimal friction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported BizTalk sources
|
||||||
|
|
||||||
|
### Send Ports
|
||||||
|
|
||||||
|
By default, only **started send ports** are included.
|
||||||
|
|
||||||
|
Optionally, the **secondary transport** of a send port can also be included. Secondary transports using the **SMTP** adapter are intentionally excluded from monitor target generation because their BizTalk address usually represents an email recipient list rather than a network endpoint.
|
||||||
|
|
||||||
|
### Receive Locations
|
||||||
|
|
||||||
|
By default, only **enabled receive locations** are included.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoint normalization
|
||||||
|
|
||||||
|
The generator tries to transform BizTalk addresses into monitor-compatible endpoints.
|
||||||
|
|
||||||
|
### Directly supported
|
||||||
|
|
||||||
|
- `\\server\share`
|
||||||
|
- `file://...`
|
||||||
|
- `http://...`
|
||||||
|
- `https://...`
|
||||||
|
- `ftp://...`
|
||||||
|
- `sftp://...`
|
||||||
|
- `tcp://...`
|
||||||
|
- `icmp://...`
|
||||||
|
- plain host / host:port values on a best-effort basis
|
||||||
|
|
||||||
|
### Special handling
|
||||||
|
|
||||||
|
- `net.tcp://host:port/...` is rewritten to `tcp://host:port/...`
|
||||||
|
- local file system paths such as `C:\Drop\In` are only included when `IncludeLocalFilePaths=true`
|
||||||
|
- `net.pipe://...` is skipped because the monitor does not support it
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HostAvailabilityMonitor: mail and status behavior
|
||||||
|
|
||||||
|
The monitor can send three kinds of emails:
|
||||||
|
|
||||||
|
1. **Failure email** when an endpoint changes from available to unavailable
|
||||||
|
2. **Recovery email** when an endpoint changes from unavailable back to available
|
||||||
|
3. **Daily status email** once per day at or after a configured local time, default **14:00 local server time**
|
||||||
|
|
||||||
|
The daily summary is intentionally **not** implemented as a second Windows task or a separate internal scheduler.
|
||||||
|
|
||||||
|
Instead, the behavior is:
|
||||||
|
|
||||||
|
- the normal scheduled monitor run starts as usual
|
||||||
|
- on every run, the monitor checks whether the configured local time has already been reached
|
||||||
|
- the **first run at or after that time** sends the daily summary email
|
||||||
|
- only **one** daily summary email is sent per calendar day
|
||||||
|
- the daily summary is marked as sent **only if SMTP delivery succeeded**
|
||||||
|
|
||||||
|
This keeps the solution resilient and simple for BizTalk server operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitor configuration details
|
||||||
|
|
||||||
|
File: `src\HostAvailabilityMonitor\appsettings.json`
|
||||||
|
|
||||||
|
### Root fields
|
||||||
|
|
||||||
|
- `ApplicationName`: monitor name
|
||||||
|
- `EnvironmentName`: environment label such as `HIP Production`
|
||||||
|
- `Runtime`: runtime behavior
|
||||||
|
- `Logging`: file logging configuration
|
||||||
|
- `EventLog`: Windows Event Log configuration
|
||||||
|
- `Email`: SMTP and notification configuration
|
||||||
|
- `Targets`: endpoints to probe
|
||||||
|
|
||||||
|
### Email section
|
||||||
|
|
||||||
|
Important fields in `Email`:
|
||||||
|
|
||||||
|
- `Enabled`: enable or disable email delivery completely
|
||||||
|
- `SmtpHost`, `SmtpPort`, `UseSsl`, `UseDefaultCredentials`, `Username`, `Password`
|
||||||
|
- `From`, `To`, `Cc`, `Bcc`, `SubjectPrefix`
|
||||||
|
- `SendOnFailure`: send an email on failure
|
||||||
|
- `SendOnRecovery`: send an email on recovery
|
||||||
|
- `SendDailySummary`: send a daily status summary
|
||||||
|
- `DailySummaryHourLocal`: local server hour, for example `14`
|
||||||
|
- `DailySummaryMinuteLocal`: local server minute, for example `0`
|
||||||
|
- `OnlyOnStateChange`: for failure/recovery emails, react only to state changes
|
||||||
|
- `CooldownMinutes`: cooldown for repeated failure emails when `OnlyOnStateChange=false`
|
||||||
|
- `TimeoutSeconds`: SMTP timeout
|
||||||
|
|
||||||
|
Recipient notes:
|
||||||
|
|
||||||
|
- `To`, `Cc`, and `Bcc` are JSON arrays of email addresses.
|
||||||
|
- Configure multiple recipients by adding multiple array entries.
|
||||||
|
- A single semicolon-separated string is not the supported format.
|
||||||
|
- At least one recipient across `To`, `Cc`, or `Bcc` must be configured when `Email.Enabled=true`.
|
||||||
|
|
||||||
|
### Example email block
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Email": {
|
||||||
|
"Enabled": true,
|
||||||
|
"SmtpHost": "smtp.example.local",
|
||||||
|
"SmtpPort": 25,
|
||||||
|
"UseSsl": false,
|
||||||
|
"UseDefaultCredentials": false,
|
||||||
|
"Username": "",
|
||||||
|
"Password": "",
|
||||||
|
"From": "monitor@example.local",
|
||||||
|
"To": [
|
||||||
|
"operations@example.local",
|
||||||
|
"integration-oncall@example.local"
|
||||||
|
],
|
||||||
|
"Cc": [
|
||||||
|
"integration-leads@example.local"
|
||||||
|
],
|
||||||
|
"Bcc": [],
|
||||||
|
"SubjectPrefix": "[HostAvailabilityMonitor]",
|
||||||
|
"SendOnFailure": true,
|
||||||
|
"SendOnRecovery": true,
|
||||||
|
"SendDailySummary": true,
|
||||||
|
"DailySummaryHourLocal": 14,
|
||||||
|
"DailySummaryMinuteLocal": 0,
|
||||||
|
"OnlyOnStateChange": true,
|
||||||
|
"CooldownMinutes": 0,
|
||||||
|
"TimeoutSeconds": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example for multiple recipients
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Email": {
|
||||||
|
"Enabled": true,
|
||||||
|
"From": "monitor@example.local",
|
||||||
|
"To": [
|
||||||
|
"operations@example.local",
|
||||||
|
"integration-oncall@example.local"
|
||||||
|
],
|
||||||
|
"Cc": [
|
||||||
|
"integration-leads@example.local"
|
||||||
|
],
|
||||||
|
"Bcc": [
|
||||||
|
"audit@example.local"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daily summary content
|
||||||
|
|
||||||
|
The daily status email contains:
|
||||||
|
|
||||||
|
- monitor name
|
||||||
|
- environment
|
||||||
|
- machine name
|
||||||
|
- time window from local midnight until send time
|
||||||
|
- today's successful and failed probe counts based on the current day's rolling log
|
||||||
|
- a current snapshot of the latest monitor run
|
||||||
|
- a list of currently unavailable endpoints including:
|
||||||
|
- application
|
||||||
|
- name
|
||||||
|
- endpoint
|
||||||
|
- kind
|
||||||
|
- status
|
||||||
|
- detail
|
||||||
|
- host/port/HTTP status when available
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
The daily counters are derived from the **current day's rolling log file**. If that evaluation fails for any reason, email delivery still continues; the counters fall back to `0` and the incident is logged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generator configuration details
|
||||||
|
|
||||||
|
File: `generator-settings.json`
|
||||||
|
|
||||||
|
### Root fields
|
||||||
|
|
||||||
|
- `GeneratorName`: display name for generator logs and event log entries
|
||||||
|
- `EnvironmentName`: environment label such as `HIP Production`
|
||||||
|
- `Logging`: generator logging settings
|
||||||
|
- `EventLog`: generator event log settings
|
||||||
|
- `BizTalk`: discovery rules
|
||||||
|
- `Output`: output path and generation rules
|
||||||
|
- `GeneratedMonitorConfig`: template for the final monitor JSON
|
||||||
|
|
||||||
|
### BizTalk section
|
||||||
|
|
||||||
|
- `ConnectionString`: connection to BizTalkMgmtDb
|
||||||
|
- `IncludeSendPorts`: read send ports
|
||||||
|
- `IncludeReceiveLocations`: read receive locations
|
||||||
|
- `OnlyStartedSendPorts`: include active send ports only
|
||||||
|
- `OnlyEnabledReceiveLocations`: include active receive locations only
|
||||||
|
- `IncludeSecondaryTransportOnSendPorts`: include secondary send transports
|
||||||
|
- SMTP send ports and SMTP secondary transports are skipped automatically by the generator
|
||||||
|
- `IncludeDynamicSendPorts`: include dynamic send ports
|
||||||
|
- `IncludeLocalFilePaths`: include local file system paths
|
||||||
|
- `IgnoreSystemApplications`: skip system applications
|
||||||
|
- `IgnoreApplications`: ignore applications by wildcard pattern
|
||||||
|
- `IgnoreArtifactNamePatterns`: ignore ports/locations by wildcard pattern
|
||||||
|
- `ApplicationMappings`: map BizTalk application names to business application names
|
||||||
|
- `ArtifactMappings`: map specific artifacts to business application names
|
||||||
|
|
||||||
|
### Output section
|
||||||
|
|
||||||
|
- `Path`: target path of the generated monitor JSON
|
||||||
|
- `OverwriteExisting`: overwrite existing file
|
||||||
|
- `FailIfNoTargetsFound`: return exit code 2 when no targets were found
|
||||||
|
- `TargetTimeoutSeconds`: default timeout for each generated target
|
||||||
|
- `TargetEnabled`: default `Enabled` value for each generated target
|
||||||
|
- `DefaultHttpMethod`: default HTTP method for generated HTTP/HTTPS targets
|
||||||
|
|
||||||
|
### GeneratedMonitorConfig section
|
||||||
|
|
||||||
|
This is the template for the monitor runtime configuration.
|
||||||
|
|
||||||
|
It contains:
|
||||||
|
|
||||||
|
- monitor `ApplicationName`
|
||||||
|
- `EnvironmentName`
|
||||||
|
- `Runtime`
|
||||||
|
- `Logging`
|
||||||
|
- `EventLog`
|
||||||
|
- `Email`
|
||||||
|
|
||||||
|
`Targets` is rebuilt by the generator at runtime.
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
When the generator writes the monitor JSON, the daily summary options are also copied into the generated target file. That means `GeneratedMonitorConfig.Email.SendDailySummary`, `DailySummaryHourLocal`, and `DailySummaryMinuteLocal` flow directly into the output monitor configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to use the generator
|
||||||
|
|
||||||
|
### Option A: run the EXE directly
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\BizTalkMonitorConfigGenerator.exe C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: run the wrapper script
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\generate-monitor-config.ps1 -InstallPath C:\Tools\BizTalkMonitorConfigGenerator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected result
|
||||||
|
|
||||||
|
After the run, you should have:
|
||||||
|
|
||||||
|
- a generated JSON file, for example `C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json`
|
||||||
|
- a generator log file under `logs\`
|
||||||
|
- event log entries in the Windows `Application` log
|
||||||
|
|
||||||
|
### Run the monitor afterwards
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example flow on a BizTalk server
|
||||||
|
|
||||||
|
### 1. Install the generator
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-generator-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\BizTalkMonitorConfigGenerator `
|
||||||
|
-InstallPath C:\Tools\BizTalkMonitorConfigGenerator `
|
||||||
|
-RegisterEventSource
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Adjust the generator settings
|
||||||
|
|
||||||
|
Edit this file:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical values:
|
||||||
|
|
||||||
|
- `EnvironmentName = "HIP Production"`
|
||||||
|
- `Output.Path = "C:\\Tools\\BizTalkMonitorConfigGenerator\\generated-monitor-appsettings.json"`
|
||||||
|
- `BizTalk.ConnectionString = "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI"`
|
||||||
|
- `GeneratedMonitorConfig.Email.SendDailySummary = true`
|
||||||
|
- `GeneratedMonitorConfig.Email.DailySummaryHourLocal = 14`
|
||||||
|
- `GeneratedMonitorConfig.Email.DailySummaryMinuteLocal = 0`
|
||||||
|
|
||||||
|
### 3. Run the generator
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\BizTalkMonitorConfigGenerator.exe C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Check the generated JSON
|
||||||
|
|
||||||
|
Confirm that the file was created and contains `Targets`.
|
||||||
|
|
||||||
|
Also verify:
|
||||||
|
|
||||||
|
- the generated JSON contains `SendDailySummary`
|
||||||
|
- the daily summary time values are correct
|
||||||
|
|
||||||
|
### 5. Install the monitor
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\HostAvailabilityMonitor `
|
||||||
|
-InstallPath C:\Tools\HostAvailabilityMonitor `
|
||||||
|
-RegisterEventSource
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Start the monitor with the generated JSON
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Configure Task Scheduler
|
||||||
|
|
||||||
|
Recommended setup:
|
||||||
|
|
||||||
|
- generator every 30 minutes
|
||||||
|
- monitor every 30 minutes, 1 to 2 minutes after the generator
|
||||||
|
- ensure there is at least one monitor run **after 14:00 local time** so the daily summary can be sent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generated target names
|
||||||
|
|
||||||
|
To make logs and alert emails easy to understand, the generator creates descriptive names such as:
|
||||||
|
|
||||||
|
- `SendPort: SAP_Outbound (Primary) [WCF-SQL]`
|
||||||
|
- `SendPort: PartnerA_SFTP (Secondary) [SFTP]`
|
||||||
|
- `ReceiveLocation: InboundOrders / RL_FileDrop [FILE]`
|
||||||
|
|
||||||
|
The business `ApplicationName` of the generated target is resolved in this order:
|
||||||
|
|
||||||
|
1. `ArtifactMappings`
|
||||||
|
2. `ApplicationMappings`
|
||||||
|
3. BizTalk application name as fallback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generator logging and resilience
|
||||||
|
|
||||||
|
The generator is designed defensively:
|
||||||
|
|
||||||
|
- daily rolling logs
|
||||||
|
- retries when appending to the log file
|
||||||
|
- event log is best effort only
|
||||||
|
- atomic output file writing (`.tmp` + rename)
|
||||||
|
- existing target file is backed up as `.bak` before overwrite
|
||||||
|
- unsupported endpoints are logged and skipped
|
||||||
|
- discovery continues even if individual artifacts are unusable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitor logging and resilience
|
||||||
|
|
||||||
|
The monitor is also defensive:
|
||||||
|
|
||||||
|
- daily rolling logs
|
||||||
|
- default retention of 5 days
|
||||||
|
- event log writes are best effort only
|
||||||
|
- state file is written atomically
|
||||||
|
- failure/recovery emails follow explicit notification rules
|
||||||
|
- daily summary is sent only once per day
|
||||||
|
- the summary is marked as sent only after successful SMTP delivery
|
||||||
|
- targets that look like SMTP/email recipient lists are handled defensively as `Skipped` and do not raise DOWN alerts
|
||||||
|
- log parsing or SMTP errors do not crash the monitor run hard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server installation quick guide
|
||||||
|
|
||||||
|
### Create a deployment package
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\build-release.ps1 -CreateDeploymentPackage
|
||||||
|
```
|
||||||
|
|
||||||
|
or separately:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\package-deployment.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is a ZIP under `artifacts\deploy\net472\`.
|
||||||
|
|
||||||
|
### PowerShell installers (recommended)
|
||||||
|
|
||||||
|
Install both applications from the deployment ZIP:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\powershell\Install-Both.ps1 `
|
||||||
|
-PackageRoot . `
|
||||||
|
-BaseInstallPath "C:\Program Files\JR IT Services\BizTalk Endpoint Monitor" `
|
||||||
|
-BaseConfigPath "C:\ProgramData\JR IT Services\BizTalk Endpoint Monitor" `
|
||||||
|
-RegisterEventSources
|
||||||
|
```
|
||||||
|
|
||||||
|
Install monitor only:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\powershell\Install-HostAvailabilityMonitor.ps1 `
|
||||||
|
-PackageRoot .
|
||||||
|
```
|
||||||
|
|
||||||
|
Install generator only:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\powershell\Install-BizTalkMonitorConfigGenerator.ps1 `
|
||||||
|
-PackageRoot .
|
||||||
|
```
|
||||||
|
|
||||||
|
### WiX MSI sources (optional)
|
||||||
|
|
||||||
|
WiX source files are included for both applications. On a Windows build machine with WiX Toolset v3 you can generate MSI packages:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\wix\build-msi.ps1 `
|
||||||
|
-PublishRoot .\artifacts\publish\net472 `
|
||||||
|
-OutputRoot .\artifacts\msi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Individual installation using the original scripts
|
||||||
|
|
||||||
|
#### Monitor
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\HostAvailabilityMonitor `
|
||||||
|
-InstallPath C:\Tools\HostAvailabilityMonitor `
|
||||||
|
-RegisterEventSource
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Generator
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-generator-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\BizTalkMonitorConfigGenerator `
|
||||||
|
-InstallPath C:\Tools\BizTalkMonitorConfigGenerator `
|
||||||
|
-RegisterEventSource
|
||||||
|
```
|
||||||
|
|
||||||
|
Then typically:
|
||||||
|
|
||||||
|
1. adjust `generator-settings.json`
|
||||||
|
2. run the generator
|
||||||
|
3. verify the generated monitor JSON
|
||||||
|
4. run the monitor against that JSON
|
||||||
|
5. configure Task Scheduler
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build note
|
||||||
|
|
||||||
|
In this environment, no real Windows build against .NET Framework 4.7.2 and BizTalk ExplorerOM could be executed. The same applies to real WiX-built MSI binaries because no Windows/WiX toolchain is available here. The code and repository structure were updated, but the first real build and validation must happen on a Windows/BizTalk system.
|
||||||
+532
-349
@@ -1,349 +1,532 @@
|
|||||||
# Dokumentation – Host Availability Monitor (.NET Framework 4.8)
|
# Dokumentation
|
||||||
|
|
||||||
## 1. Zielbild
|
## Überblick
|
||||||
|
|
||||||
Die Anwendung überwacht auf einem Windows Server 2019 in festen Intervallen die netzwerktechnische Verfügbarkeit definierter Ziele. Typische Einsatzzwecke:
|
Dieses Repository ist für den Betrieb in einer **Gitea-Repo-Struktur** vorbereitet und enthält zwei Anwendungen:
|
||||||
|
|
||||||
- interne Fileshares
|
### 1. HostAvailabilityMonitor
|
||||||
- externe HTTPS-Endpunkte
|
|
||||||
- SFTP-Gegenstellen
|
Der Monitor prüft technische Erreichbarkeit und schreibt:
|
||||||
- TCP-Dienste
|
|
||||||
- Hosts per Ping
|
- Rolling-Dateilog
|
||||||
|
- Windows Application Event Log
|
||||||
Der Betrieb erfolgt typischerweise über den **Windows Task Scheduler** alle 30 Minuten.
|
- E-Mail-Benachrichtigungen bei Ausfall, Recovery und täglichem Status
|
||||||
|
- Statusdatei für Zustandswechsel, Cooldown-Logik und die tägliche Summary-Mail
|
||||||
## 2. Unterstützte Zieltypen
|
|
||||||
|
### 2. BizTalkMonitorConfigGenerator
|
||||||
### 2.1 UNC / SMB
|
|
||||||
|
Der Generator liest BizTalk-Artefakte aus und erzeugt daraus eine Monitor-Konfiguration.
|
||||||
Beispiel:
|
|
||||||
|
Ziel ist, dass die Ziel-Liste nicht manuell gepflegt werden muss, sondern direkt aus BizTalk stammt.
|
||||||
```text
|
|
||||||
\\internerserver1\share1
|
---
|
||||||
```
|
|
||||||
|
## Repository-Struktur für Gitea
|
||||||
Verhalten:
|
|
||||||
|
```text
|
||||||
1. TCP-Check auf Port 445 (oder überschriebenen Port)
|
.
|
||||||
2. Optional zusätzliche Pfadvalidierung mit `Directory.Exists(...)` / `File.Exists(...)`
|
├── .editorconfig
|
||||||
|
├── .gitignore
|
||||||
Hinweis:
|
├── Documentation.en.md
|
||||||
|
├── Dokumentation.md
|
||||||
- `ValidateUncPathAccess=false` ist robuster, wenn es nur um Erreichbarkeit geht.
|
├── HostAvailabilityMonitor.sln
|
||||||
- `ValidateUncPathAccess=true` ist strenger, weil Berechtigungen, Namensauflösung und tatsächlicher Share-Zugriff geprüft werden.
|
├── README.md
|
||||||
|
├── installers/
|
||||||
### 2.2 HTTP / HTTPS
|
│ ├── README.md
|
||||||
|
│ ├── powershell/
|
||||||
Beispiele:
|
│ │ ├── Install-BizTalkMonitorConfigGenerator.ps1
|
||||||
|
│ │ ├── Install-Both.ps1
|
||||||
```text
|
│ │ ├── Install-HostAvailabilityMonitor.ps1
|
||||||
https://host1.de/url
|
│ │ ├── Uninstall-BizTalkMonitorConfigGenerator.ps1
|
||||||
http://intranet.local/health
|
│ │ └── Uninstall-HostAvailabilityMonitor.ps1
|
||||||
```
|
│ └── wix/
|
||||||
|
│ ├── BizTalkMonitorConfigGenerator.wxs
|
||||||
Verhalten:
|
│ ├── HostAvailabilityMonitor.wxs
|
||||||
|
│ └── build-msi.ps1
|
||||||
- Standardmäßig `HEAD`
|
├── scripts/
|
||||||
- Fallback auf `GET`, wenn `HEAD` nicht unterstützt wird (`405` / `501`) oder der HEAD-Request fehlschlägt
|
│ ├── build-release.ps1
|
||||||
- Jede empfangene HTTP-Antwort zählt als **netzwerktechnisch erreichbar**
|
│ ├── generate-monitor-config.ps1
|
||||||
|
│ ├── install-generator-on-server.ps1
|
||||||
### 2.3 SFTP
|
│ ├── install-on-server.ps1
|
||||||
|
│ ├── package-deployment.ps1
|
||||||
Beispiel:
|
│ └── register-event-source.ps1
|
||||||
|
└── src/
|
||||||
```text
|
├── BizTalkMonitorConfigGenerator/
|
||||||
sftp://partner.example.local/inbound
|
│ ├── App.config
|
||||||
```
|
│ ├── BizTalkMonitorConfigGenerator.csproj
|
||||||
|
│ ├── Program.cs
|
||||||
Verhalten:
|
│ └── generator-settings.json
|
||||||
|
└── HostAvailabilityMonitor/
|
||||||
- TCP-Port-Prüfung, standardmäßig Port 22
|
├── App.config
|
||||||
- kein Login, keine Dateioperation
|
├── HostAvailabilityMonitor.csproj
|
||||||
|
├── Program.cs
|
||||||
### 2.4 TCP
|
└── appsettings.json
|
||||||
|
```
|
||||||
Beispiel:
|
|
||||||
|
Diese Struktur ist für ein normales Gitea-Repository ausgelegt. Build und Deployment können aus der Solution oder projektweise erfolgen.
|
||||||
```text
|
|
||||||
tcp://host2.example.local:8443
|
---
|
||||||
```
|
|
||||||
|
## Unterstützte BizTalk-Quellen
|
||||||
Verhalten:
|
|
||||||
|
### Send Ports
|
||||||
- reiner Verbindungsaufbau auf dem Zielport
|
|
||||||
|
Es werden standardmäßig nur **gestartete Send Ports** übernommen.
|
||||||
### 2.5 ICMP
|
|
||||||
|
Zusätzlich kann optional auch der **Secondary Transport** eines Send Ports berücksichtigt werden. Secondary Transports mit dem Adapter **SMTP** werden jedoch bewusst nicht als Monitor-Target übernommen, weil deren Address-Wert in BizTalk typischerweise nur eine Empfängerliste und keine prüfbare Netzwerkadresse darstellt.
|
||||||
Beispiel:
|
|
||||||
|
### Receive Locations
|
||||||
```text
|
|
||||||
icmp://server01
|
Es werden standardmäßig nur **aktivierte Receive Locations** übernommen.
|
||||||
```
|
|
||||||
|
---
|
||||||
Verhalten:
|
|
||||||
|
## Endpunkt-Normalisierung
|
||||||
- Ping über `System.Net.NetworkInformation.Ping`
|
|
||||||
|
Der Generator versucht, BizTalk-Adressen in Monitor-kompatible Endpunkte zu überführen.
|
||||||
### 2.6 Plain Hostnames
|
|
||||||
|
### Direkt unterstützt
|
||||||
Beispiele:
|
|
||||||
|
- `\\server\share`
|
||||||
```text
|
- `file://...`
|
||||||
server01
|
- `http://...`
|
||||||
partner-gateway
|
- `https://...`
|
||||||
```
|
- `ftp://...`
|
||||||
|
- `sftp://...`
|
||||||
Verhalten:
|
- `tcp://...`
|
||||||
|
- `icmp://...`
|
||||||
- wenn `Port` gesetzt ist: TCP-Check
|
- Plain Host / Host:Port (best effort)
|
||||||
- wenn kein `Port` gesetzt ist: ICMP-Check
|
|
||||||
|
### Spezielle Behandlung
|
||||||
## 3. Konfigurationsmodell
|
|
||||||
|
- `net.tcp://host:port/...` wird auf `tcp://host:port/...` umgeschrieben
|
||||||
Die Konfiguration erfolgt in `appsettings.json`.
|
- lokale Dateipfade wie `C:\Drop\In` werden nur übernommen, wenn `IncludeLocalFilePaths=true`
|
||||||
|
- `net.pipe://...` wird übersprungen, weil der Monitor diesen Typ nicht prüft
|
||||||
### 3.1 Runtime
|
|
||||||
|
---
|
||||||
| Feld | Bedeutung |
|
|
||||||
|---|---|
|
## HostAvailabilityMonitor: Mail- und Statusverhalten
|
||||||
| `MaxConcurrency` | Maximale parallele Checks |
|
|
||||||
| `DefaultValidateUncPathAccess` | Default für UNC-Pfadzugriffsprüfung |
|
Der Monitor kann drei Arten von Mail-Benachrichtigungen versenden:
|
||||||
| `NonZeroExitCodeOnAnyFailure` | Exit Code 2 bei mindestens einem fehlgeschlagenen Check |
|
|
||||||
| `NonZeroExitCodeOnExecutionError` | Exit Code 1 bei technischen Fehlern |
|
1. **Failure-Mail**: wenn ein Endpoint von erreichbar auf nicht erreichbar wechselt
|
||||||
| `StateDirectory` | Verzeichnis für Statusdatei |
|
2. **Recovery-Mail**: wenn ein Endpoint von nicht erreichbar auf wieder erreichbar wechselt
|
||||||
|
3. **Tagesstatus-Mail**: einmal pro Tag ab einer konfigurierten lokalen Uhrzeit, standardmäßig **14:00 Uhr**
|
||||||
### 3.2 Logging
|
|
||||||
|
Die Tagesstatus-Mail wird absichtlich **nicht** über einen zweiten Task oder separaten Scheduler innerhalb der Anwendung realisiert.
|
||||||
| Feld | Bedeutung |
|
|
||||||
|---|---|
|
Stattdessen gilt:
|
||||||
| `LogDirectory` | Verzeichnis der Logdateien |
|
|
||||||
| `FilePrefix` | Präfix des Tageslogs |
|
- der normale geplante Task startet die Anwendung wie bisher
|
||||||
| `RetentionDays` | Anzahl Tage zur Aufbewahrung |
|
- die Anwendung prüft bei jedem Lauf, ob die konfigurierte Uhrzeit bereits erreicht ist
|
||||||
|
- die **erste Ausführung ab dieser Uhrzeit** verschickt die Tagesstatus-Mail
|
||||||
### 3.3 EventLog
|
- pro Kalendertag wird **maximal eine** Tagesstatus-Mail gesendet
|
||||||
|
- nur ein **erfolgreich versendeter** Tagesstatus wird im State gespeichert
|
||||||
| Feld | Bedeutung |
|
|
||||||
|---|---|
|
Das ist robust für den Betrieb auf BizTalk-Servern, weil keine zusätzliche Orchestrierung erforderlich ist.
|
||||||
| `Enabled` | Aktiviert Event Log |
|
|
||||||
| `LogName` | Standard: `Application` |
|
---
|
||||||
| `Source` | Event Source |
|
|
||||||
| `CreateSourceIfMissing` | Optionaler Versuch, die Source anzulegen |
|
## Konfiguration des Monitors im Detail
|
||||||
| `WriteSuccessEntries` | Erfolgreiche Einzelchecks schreiben |
|
|
||||||
| `WriteSummaryEntries` | Laufzusammenfassung schreiben |
|
Datei: `src\HostAvailabilityMonitor\appsettings.json`
|
||||||
|
|
||||||
### 3.4 Email
|
### Root-Felder
|
||||||
|
|
||||||
| Feld | Bedeutung |
|
- `ApplicationName`: Name des Monitors
|
||||||
|---|---|
|
- `EnvironmentName`: Umgebungsname wie `HIP Produktion`
|
||||||
| `Enabled` | Aktiviert SMTP-Benachrichtigung |
|
- `Runtime`: Laufzeitverhalten
|
||||||
| `SmtpHost` / `SmtpPort` | SMTP-Ziel |
|
- `Logging`: Dateilog-Konfiguration
|
||||||
| `UseSsl` | SSL/TLS für SMTP |
|
- `EventLog`: Windows Event Log Konfiguration
|
||||||
| `Username` / `Password` | Optionale Credentials |
|
- `Email`: SMTP- und Benachrichtigungskonfiguration
|
||||||
| `From` | Absender |
|
- `Targets`: die zu prüfenden Endpunkte
|
||||||
| `To` | Empfängerliste |
|
|
||||||
| `SubjectPrefix` | Präfix im Betreff |
|
### Email
|
||||||
| `SendOnFailure` | Mail bei Fehler |
|
|
||||||
| `SendOnRecovery` | Mail bei Wiederherstellung |
|
Wichtige Felder im Abschnitt `Email`:
|
||||||
| `OnlyOnStateChange` | Nur bei Statuswechsel |
|
|
||||||
| `CooldownMinutes` | Minimaler Abstand zwischen Benachrichtigungen |
|
- `Enabled`: Mailversand insgesamt ein/aus
|
||||||
| `TimeoutSeconds` | SMTP-Timeout |
|
- `SmtpHost`, `SmtpPort`, `UseSsl`, `UseDefaultCredentials`, `Username`, `Password`
|
||||||
|
- `From`, `To`, `Cc`, `Bcc`, `SubjectPrefix`
|
||||||
### 3.5 Targets
|
- `SendOnFailure`: Mail bei Ausfall senden
|
||||||
|
- `SendOnRecovery`: Mail bei Recovery senden
|
||||||
| Feld | Bedeutung |
|
- `SendDailySummary`: tägliche Status-Mail senden
|
||||||
|---|---|
|
- `DailySummaryHourLocal`: lokale Stunde des Servers, z. B. `14`
|
||||||
| `Name` | Anzeigename |
|
- `DailySummaryMinuteLocal`: lokale Minute des Servers, z. B. `0`
|
||||||
| `Endpoint` | UNC, URL, SFTP, TCP, ICMP oder Hostname |
|
- `OnlyOnStateChange`: bei Failure-/Recovery-Mails nur auf Zustandswechsel reagieren
|
||||||
| `TimeoutSeconds` | Timeout je Ziel |
|
- `CooldownMinutes`: Cooldown für wiederholte Failure-Mails, wenn `OnlyOnStateChange=false`
|
||||||
| `Port` | Optionaler Zielport |
|
- `TimeoutSeconds`: SMTP-Timeout
|
||||||
| `ValidateUncPathAccess` | Optional pro UNC-Ziel |
|
|
||||||
| `HttpMethod` | `HEAD` oder `GET` |
|
Hinweise zu Empfaengern:
|
||||||
| `Enabled` | Ziel aktiv/inaktiv |
|
|
||||||
|
- `To`, `Cc` und `Bcc` sind jeweils JSON-Arrays von Mailadressen.
|
||||||
## 4. Logging-Verhalten
|
- Mehrere Empfaenger werden ueber mehrere Array-Eintraege konfiguriert.
|
||||||
|
- Eine einzelne Zeichenkette mit Semikolon-Liste ist nicht vorgesehen.
|
||||||
### 4.1 Dateilog
|
- Mindestens ein Empfaenger in `To`, `Cc` oder `Bcc` muss vorhanden sein, wenn `Email.Enabled=true`.
|
||||||
|
|
||||||
Pro Tag entsteht genau eine Logdatei:
|
### Beispiel für den Email-Block
|
||||||
|
|
||||||
```text
|
```json
|
||||||
logs\host-availability-monitor-2026-04-16.log
|
"Email": {
|
||||||
```
|
"Enabled": true,
|
||||||
|
"SmtpHost": "smtp.example.local",
|
||||||
Format:
|
"SmtpPort": 25,
|
||||||
|
"UseSsl": false,
|
||||||
```text
|
"UseDefaultCredentials": false,
|
||||||
2026-04-16 08:00:00.123 +02:00 | INFO | Lauf gestartet.
|
"Username": "",
|
||||||
2026-04-16 08:00:00.532 +02:00 | SUCCESS | Name=Externe Webseite | Kind=Https | Endpoint=https://host1.de/url | DurationMs=278 | Status=Reachable | Detail=HTTP-Antwort erhalten (200 OK). | Host=host1.de | Port=443 | HttpStatus=200
|
"Password": "",
|
||||||
2026-04-16 08:00:01.003 +02:00 | FAIL | Name=SFTP Partner A | Kind=Sftp | Endpoint=sftp://partner.example.local/inbound | DurationMs=10005 | Status=Unreachable | Detail=TCP-Verbindung fehlgeschlagen: A connection attempt failed... | Host=partner.example.local | Port=22 | ErrorType=SocketException
|
"From": "monitor@example.local",
|
||||||
```
|
"To": [
|
||||||
|
"operations@example.local",
|
||||||
### 4.2 Retention
|
"integration-oncall@example.local"
|
||||||
|
],
|
||||||
Beim Start eines Laufs werden alte Tageslogs bereinigt.
|
"Cc": [
|
||||||
|
"integration-leads@example.local"
|
||||||
Standard:
|
],
|
||||||
|
"Bcc": [],
|
||||||
- aktueller Tag + 4 vorherige Tage bleiben erhalten
|
"SubjectPrefix": "[HostAvailabilityMonitor]",
|
||||||
- ältere Logs werden gelöscht
|
"SendOnFailure": true,
|
||||||
|
"SendOnRecovery": true,
|
||||||
## 5. Event Log
|
"SendDailySummary": true,
|
||||||
|
"DailySummaryHourLocal": 14,
|
||||||
Es wird in das Windows Application Event Log geschrieben.
|
"DailySummaryMinuteLocal": 0,
|
||||||
|
"OnlyOnStateChange": true,
|
||||||
Typische Events:
|
"CooldownMinutes": 0,
|
||||||
|
"TimeoutSeconds": 30
|
||||||
- Fehler bei Einzelchecks: Warning
|
}
|
||||||
- technische Fehler: Error
|
```
|
||||||
- Laufzusammenfassung: Information oder Warning
|
|
||||||
|
### Beispiel fuer mehrere Empfaenger
|
||||||
Wichtig:
|
|
||||||
|
```json
|
||||||
- Das Erzeugen einer Event Source benötigt erhöhte Rechte.
|
"Email": {
|
||||||
- Im produktiven Betrieb sollte die Source einmalig per Skript registriert werden.
|
"Enabled": true,
|
||||||
- Falls Event Log nicht verfügbar ist, fällt die Anwendung auf Dateilogging zurück und läuft weiter.
|
"From": "monitor@example.local",
|
||||||
|
"To": [
|
||||||
## 6. Benachrichtigungslogik
|
"operations@example.local",
|
||||||
|
"integration-oncall@example.local"
|
||||||
Die Anwendung speichert je Ziel einen Status in `state\monitor-state.json`.
|
],
|
||||||
|
"Cc": [
|
||||||
Gespeicherte Informationen pro Ziel:
|
"integration-leads@example.local"
|
||||||
|
],
|
||||||
- `IsUp`
|
"Bcc": [
|
||||||
- `ConsecutiveFailures`
|
"audit@example.local"
|
||||||
- `LastCheckedUtc`
|
]
|
||||||
- `LastChangedUtc`
|
}
|
||||||
- `LastNotificationUtc`
|
```
|
||||||
- `LastDetail`
|
|
||||||
|
### Inhalt der Tagesstatus-Mail
|
||||||
### 6.1 Benachrichtigung bei Fehler
|
|
||||||
|
Die Tagesstatus-Mail enthält:
|
||||||
Es wird benachrichtigt, wenn:
|
|
||||||
|
- Monitorname
|
||||||
- `Email.Enabled=true`
|
- Umgebung
|
||||||
- `SendOnFailure=true`
|
- Servername
|
||||||
- das Ziel aktuell fehlschlägt
|
- Zeitfenster von Mitternacht bis Versandzeitpunkt
|
||||||
- und je nach Einstellung entweder
|
- Anzahl heutiger erfolgreicher und fehlgeschlagener Checks laut Tageslog
|
||||||
- ein Statuswechsel vorliegt oder
|
- aktueller Snapshot des letzten Monitor-Laufs
|
||||||
- der Cooldown abgelaufen ist
|
- Liste aktuell DOWN befindlicher Endpunkte inklusive:
|
||||||
|
- Anwendung
|
||||||
### 6.2 Benachrichtigung bei Recovery
|
- Name
|
||||||
|
- Endpoint
|
||||||
Es wird benachrichtigt, wenn:
|
- Typ
|
||||||
|
- Status
|
||||||
- `Email.Enabled=true`
|
- Detail
|
||||||
- `SendOnRecovery=true`
|
- Host/Port/HTTP-Status, soweit vorhanden
|
||||||
- das Ziel aktuell erfolgreich ist
|
|
||||||
- und der vorherige bekannte Zustand `down` war
|
Hinweis:
|
||||||
|
|
||||||
### 6.3 Anti-Spam
|
Die Tageszähler werden aus dem **heutigen Rolling-Log** ausgewertet. Sollte diese Auswertung ausnahmsweise fehlschlagen, bleibt die Mail-Funktion funktionsfähig; die Tageszähler werden dann defensiv mit `0` angegeben und der Vorfall wird geloggt.
|
||||||
|
|
||||||
Mit `OnlyOnStateChange=true` werden bei Dauerfehlern nicht bei jedem Lauf erneut Mails versendet.
|
---
|
||||||
|
|
||||||
Mit `CooldownMinutes > 0` kann zusätzlich ein Zeitfenster definiert werden.
|
## Generator-Konfiguration im Detail
|
||||||
|
|
||||||
## 7. Exit Codes
|
Datei: `generator-settings.json`
|
||||||
|
|
||||||
| Exit Code | Bedeutung |
|
### Root-Felder
|
||||||
|---|---|
|
|
||||||
| `0` | Lauf erfolgreich abgeschlossen |
|
- `GeneratorName`: Anzeigename für Logs/Eventlog des Generators
|
||||||
| `1` | technischer Fehler / Konfigurations- oder Laufzeitproblem |
|
- `EnvironmentName`: Umgebung wie `HIP Produktion`
|
||||||
| `2` | mindestens ein Ziel fehlgeschlagen und `NonZeroExitCodeOnAnyFailure=true` |
|
- `Logging`: Logging des Generators
|
||||||
|
- `EventLog`: Eventlog des Generators
|
||||||
## 8. Task Scheduler Empfehlung
|
- `BizTalk`: Discovery-Regeln
|
||||||
|
- `Output`: Zielpfad und Erzeugungsregeln
|
||||||
### 8.1 Allgemein
|
- `GeneratedMonitorConfig`: Vorlage für die endgültige Monitor-JSON
|
||||||
|
|
||||||
- Trigger: alle 30 Minuten
|
### BizTalk
|
||||||
- Benutzerkonto: dediziertes Servicekonto, falls UNC-Zugriffe Berechtigungen benötigen
|
|
||||||
- "Start in": Installationsverzeichnis setzen
|
- `ConnectionString`: Verbindung zur BizTalkMgmtDb
|
||||||
|
- `IncludeSendPorts`: Send Ports auslesen
|
||||||
### 8.2 Beispiel
|
- `IncludeReceiveLocations`: Receive Locations auslesen
|
||||||
|
- `OnlyStartedSendPorts`: nur aktive Send Ports
|
||||||
**Aktion**
|
- `OnlyEnabledReceiveLocations`: nur aktive Receive Locations
|
||||||
|
- `IncludeSecondaryTransportOnSendPorts`: sekundäre Send-Port-Transporte zusätzlich aufnehmen
|
||||||
- Program/script:
|
- SMTP-Sendports bzw. SMTP-Secondary-Transports werden vom Generator automatisch übersprungen
|
||||||
|
- `IncludeDynamicSendPorts`: dynamische Send Ports aufnehmen
|
||||||
```text
|
- `IncludeLocalFilePaths`: lokale Pfade aufnehmen
|
||||||
C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe
|
- `IgnoreSystemApplications`: Systemanwendungen ignorieren
|
||||||
```
|
- `IgnoreApplications`: Anwendungen per Pattern ausblenden
|
||||||
|
- `IgnoreArtifactNamePatterns`: Ports/Locations per Pattern ignorieren
|
||||||
- Add arguments:
|
- `ApplicationMappings`: Mapping BizTalk-App -> fachliche App
|
||||||
|
- `ArtifactMappings`: Mapping einzelner Artefakte -> fachliche App
|
||||||
```text
|
|
||||||
C:\Tools\HostAvailabilityMonitor\appsettings.json
|
### Output
|
||||||
```
|
|
||||||
|
- `Path`: Pfad der erzeugten Monitor-JSON
|
||||||
- Start in:
|
- `OverwriteExisting`: bestehende Datei überschreiben
|
||||||
|
- `FailIfNoTargetsFound`: ExitCode 2, wenn keine Targets gefunden wurden
|
||||||
```text
|
- `TargetTimeoutSeconds`: Standard-Timeout pro generiertem Target
|
||||||
C:\Tools\HostAvailabilityMonitor
|
- `TargetEnabled`: `Enabled`-Default in jedem Target
|
||||||
```
|
- `DefaultHttpMethod`: Standard für generierte HTTP/HTTPS-Ziele
|
||||||
|
|
||||||
## 9. Betriebshinweise
|
### GeneratedMonitorConfig
|
||||||
|
|
||||||
### 9.1 UNC-Prüfung
|
Dies ist die Vorlage für den späteren Monitor.
|
||||||
|
|
||||||
Wenn echte Share-Verfügbarkeit geprüft werden soll:
|
Hier werden gepflegt:
|
||||||
|
|
||||||
- Task mit geeignetem Servicekonto ausführen
|
- `ApplicationName` des Monitors
|
||||||
- `ValidateUncPathAccess=true` für das betreffende Ziel setzen
|
- `EnvironmentName`
|
||||||
|
- `Runtime`
|
||||||
Wenn nur der Zielserver bzw. SMB-Port relevant ist:
|
- `Logging`
|
||||||
|
- `EventLog`
|
||||||
- `ValidateUncPathAccess=false` belassen
|
- `Email`
|
||||||
|
|
||||||
### 9.2 HTTP/HTTPS
|
`Targets` wird vom Generator zur Laufzeit neu erzeugt.
|
||||||
|
|
||||||
Die Anwendung prüft Erreichbarkeit, nicht Business-Logik. Für Applikations-Health kann eine dedizierte Health-URL sinnvoller sein.
|
Wichtig:
|
||||||
|
|
||||||
### 9.3 SFTP
|
Wenn der Generator die Monitor-JSON erzeugt, werden auch die Felder für die Tagesstatus-Mail mit erzeugt. Das heißt: `GeneratedMonitorConfig.Email.SendDailySummary`, `DailySummaryHourLocal` und `DailySummaryMinuteLocal` werden direkt in die Zielkonfiguration übernommen.
|
||||||
|
|
||||||
Da nur der Port geprüft wird, eignet sich der Check für "Ist die Gegenstelle erreichbar?", nicht für "Kann ich mich anmelden und Dateien lesen?".
|
---
|
||||||
|
|
||||||
### 9.4 TLS
|
## Generator verwenden
|
||||||
|
|
||||||
Die Anwendung aktiviert zusätzlich gängige SecurityProtocol-Werte, damit HTTPS-Ziele in älteren .NET-Framework-Umgebungen robuster funktionieren.
|
### Variante A: direkt per EXE
|
||||||
|
|
||||||
## 10. Build und Deployment
|
```powershell
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\BizTalkMonitorConfigGenerator.exe C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
### 10.1 Voraussetzungen
|
```
|
||||||
|
|
||||||
- Windows Build Host
|
### Variante B: per Wrapper-Skript
|
||||||
- Visual Studio oder Build Tools mit MSBuild
|
|
||||||
- .NET Framework 4.8 Targeting/Developer Pack
|
```powershell
|
||||||
|
.\scripts\generate-monitor-config.ps1 -InstallPath C:\Tools\BizTalkMonitorConfigGenerator
|
||||||
### 10.2 Build
|
```
|
||||||
|
|
||||||
```powershell
|
### Erwartetes Ergebnis
|
||||||
msbuild .\HostAvailabilityMonitor.sln /p:Configuration=Release /p:Platform="Any CPU"
|
|
||||||
```
|
Nach dem Lauf sollte vorhanden sein:
|
||||||
|
|
||||||
### 10.3 Artefakte
|
- generierte Datei, z. B. `C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json`
|
||||||
|
- Logdatei des Generators unter `logs\`
|
||||||
Relevant für Deployment:
|
- Eventlog-Einträge im `Application`-Log
|
||||||
|
|
||||||
- `HostAvailabilityMonitor.exe`
|
### Monitor danach starten
|
||||||
- `HostAvailabilityMonitor.exe.config`
|
|
||||||
- `appsettings.json`
|
```powershell
|
||||||
- weitere referenzierte .NET-Assemblies aus `bin\Release`
|
C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json
|
||||||
|
```
|
||||||
## 11. Sicherheits- und Resilienz-Aspekte
|
|
||||||
|
---
|
||||||
Die Anwendung ist bewusst defensiv aufgebaut:
|
|
||||||
|
## Beispielablauf auf einem BizTalk-Server
|
||||||
- Fehler einzelner Checks stoppen nicht den Gesamtlauf
|
|
||||||
- Event Log Ausfälle führen nicht zum Abbruch
|
### 1. Generator installieren
|
||||||
- Statusdatei wird atomar aktualisiert
|
|
||||||
- Log-Bereinigung blockiert den Lauf nicht
|
```powershell
|
||||||
- Mailversand markiert Benachrichtigungen nur bei echtem Erfolg
|
.\scripts\install-generator-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\BizTalkMonitorConfigGenerator `
|
||||||
## 12. Empfohlene nächste Ausbaustufen
|
-InstallPath C:\Tools\BizTalkMonitorConfigGenerator `
|
||||||
|
-RegisterEventSource
|
||||||
Optional könnte das Projekt später erweitert werden um:
|
```
|
||||||
|
|
||||||
- CSV- oder HTML-Report pro Lauf
|
### 2. Generator-Konfiguration anpassen
|
||||||
- pro Target eigene Empfängergruppen
|
|
||||||
- zusätzliche Protokolle wie FTP oder LDAP-Portchecks
|
Datei bearbeiten:
|
||||||
- Performance Counter oder Windows Service statt Task Scheduler
|
|
||||||
- Gitea CI-Workflow für Windows Runner
|
```text
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Typische Werte:
|
||||||
|
|
||||||
|
- `EnvironmentName = "HIP Produktion"`
|
||||||
|
- `Output.Path = "C:\\Tools\\BizTalkMonitorConfigGenerator\\generated-monitor-appsettings.json"`
|
||||||
|
- `BizTalk.ConnectionString = "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI"`
|
||||||
|
- `GeneratedMonitorConfig.Email.SendDailySummary = true`
|
||||||
|
- `GeneratedMonitorConfig.Email.DailySummaryHourLocal = 14`
|
||||||
|
- `GeneratedMonitorConfig.Email.DailySummaryMinuteLocal = 0`
|
||||||
|
|
||||||
|
### 3. Generator ausführen
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\BizTalkMonitorConfigGenerator.exe C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Generierte JSON prüfen
|
||||||
|
|
||||||
|
Prüfen, ob die Datei erzeugt wurde und `Targets` enthält.
|
||||||
|
|
||||||
|
Zusätzlich prüfen:
|
||||||
|
|
||||||
|
- ob `GeneratedMonitorConfig.Email.SendDailySummary` in der erzeugten JSON übernommen wurde
|
||||||
|
- ob die Uhrzeitfelder korrekt gesetzt sind
|
||||||
|
|
||||||
|
### 5. Monitor installieren
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\HostAvailabilityMonitor `
|
||||||
|
-InstallPath C:\Tools\HostAvailabilityMonitor `
|
||||||
|
-RegisterEventSource
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Monitor mit der generierten JSON starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Task Scheduler konfigurieren
|
||||||
|
|
||||||
|
Empfehlung:
|
||||||
|
|
||||||
|
- Generator z. B. alle 30 Minuten
|
||||||
|
- Monitor z. B. 1 bis 2 Minuten später ebenfalls alle 30 Minuten
|
||||||
|
- wichtig ist mindestens ein Monitor-Lauf **nach 14:00 Uhr lokal**, damit die Tagesstatus-Mail gesendet werden kann
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Erzeugte Target-Namen
|
||||||
|
|
||||||
|
Damit im Log und in der Mail eindeutig erkennbar ist, woher ein Ziel stammt, werden sprechende Namen erzeugt, z. B.:
|
||||||
|
|
||||||
|
- `SendPort: SAP_Outbound (Primary) [WCF-SQL]`
|
||||||
|
- `SendPort: PartnerA_SFTP (Secondary) [SFTP]`
|
||||||
|
- `ReceiveLocation: InboundOrders / RL_FileDrop [FILE]`
|
||||||
|
|
||||||
|
Das fachliche `ApplicationName` des Targets kommt aus:
|
||||||
|
|
||||||
|
1. `ArtifactMappings`
|
||||||
|
2. `ApplicationMappings`
|
||||||
|
3. BizTalk-Anwendungsname (Fallback)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging und Resilienz des Generators
|
||||||
|
|
||||||
|
Der Generator ist defensiv gebaut:
|
||||||
|
|
||||||
|
- tägliche Logrotation
|
||||||
|
- Retry beim Schreiben ins Logfile
|
||||||
|
- Eventlog nur best effort
|
||||||
|
- atomisches Schreiben der Ausgabedatei (`.tmp` + Rename)
|
||||||
|
- bestehende Zieldatei wird vor Überschreiben als `.bak` gesichert
|
||||||
|
- nicht unterstützte Endpunkte werden protokolliert und übersprungen
|
||||||
|
- Discovery läuft möglichst weiter, auch wenn einzelne Artefakte unbrauchbar sind
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging und Resilienz des Monitors
|
||||||
|
|
||||||
|
Der Monitor ist ebenfalls defensiv gebaut:
|
||||||
|
|
||||||
|
- Rolling-Dateilog mit täglicher Rotation
|
||||||
|
- Retention standardmäßig 5 Tage
|
||||||
|
- Eventlog-Schreiben nur best effort
|
||||||
|
- Statusdatei mit atomischem Schreiben
|
||||||
|
- Failure-/Recovery-Mails nur bei definierten Bedingungen
|
||||||
|
- Tagesstatus nur einmal pro Tag
|
||||||
|
- Tagesstatus wird nur nach erfolgreichem SMTP-Versand als versendet markiert
|
||||||
|
- Ziele, die wie SMTP-/E-Mail-Empfängerlisten aussehen, werden vom Monitor defensiv als `Skipped` protokolliert und nicht als DOWN alarmiert
|
||||||
|
- Fehler bei der Tageslog-Auswertung oder beim SMTP-Versand stoppen den Monitorlauf nicht hart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server-Installation kompakt
|
||||||
|
|
||||||
|
### Deployment-Paket erzeugen
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\build-release.ps1 -CreateDeploymentPackage
|
||||||
|
```
|
||||||
|
|
||||||
|
oder getrennt:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\package-deployment.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Ergebnis liegt dann unter `artifacts\deploy\net472\` als ZIP.
|
||||||
|
|
||||||
|
### PowerShell-Installer (empfohlen)
|
||||||
|
|
||||||
|
Komplette Installation beider Programme aus dem Deployment-ZIP:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\powershell\Install-Both.ps1 `
|
||||||
|
-PackageRoot . `
|
||||||
|
-BaseInstallPath "C:\Program Files\JR IT Services\BizTalk Endpoint Monitor" `
|
||||||
|
-BaseConfigPath "C:\ProgramData\JR IT Services\BizTalk Endpoint Monitor" `
|
||||||
|
-RegisterEventSources
|
||||||
|
```
|
||||||
|
|
||||||
|
Nur Monitor:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\powershell\Install-HostAvailabilityMonitor.ps1 `
|
||||||
|
-PackageRoot .
|
||||||
|
```
|
||||||
|
|
||||||
|
Nur Generator:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\powershell\Install-BizTalkMonitorConfigGenerator.ps1 `
|
||||||
|
-PackageRoot .
|
||||||
|
```
|
||||||
|
|
||||||
|
### WiX-MSI-Quellen (optional)
|
||||||
|
|
||||||
|
Fuer beide Programme liegen WiX-Quellen bereit. Auf einem Windows-Buildserver mit WiX Toolset v3 kannst du daraus MSI-Dateien erzeugen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installers\wix\build-msi.ps1 `
|
||||||
|
-PublishRoot .\artifacts\publish\net472 `
|
||||||
|
-OutputRoot .\artifacts\msi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Einzelinstallation ueber die bisherigen Skripte
|
||||||
|
|
||||||
|
#### Monitor
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\HostAvailabilityMonitor `
|
||||||
|
-InstallPath C:\Tools\HostAvailabilityMonitor `
|
||||||
|
-RegisterEventSource
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Generator
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-generator-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\BizTalkMonitorConfigGenerator `
|
||||||
|
-InstallPath C:\Tools\BizTalkMonitorConfigGenerator `
|
||||||
|
-RegisterEventSource
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach typischerweise:
|
||||||
|
|
||||||
|
1. `generator-settings.json` anpassen
|
||||||
|
2. Generator ausführen
|
||||||
|
3. generierte Monitor-JSON prüfen
|
||||||
|
4. Monitor mit dieser JSON starten
|
||||||
|
5. Task Scheduler einrichten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hinweis zum Build
|
||||||
|
|
||||||
|
In dieser Arbeitsumgebung konnte kein echter Windows-Build gegen .NET Framework 4.7.2 und BizTalk ExplorerOM ausgefuehrt werden. Ebenso konnte hier kein echtes WiX-MSI gebaut werden, weil keine Windows-/WiX-Toolchain verfuegbar war. Der Code und die Repo-Struktur sind angepasst, der erste echte Build und Test muss auf einem Windows-/BizTalk-System erfolgen.
|
||||||
|
|||||||
+27
-21
@@ -1,21 +1,27 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostAvailabilityMonitor", "src\HostAvailabilityMonitor\HostAvailabilityMonitor.csproj", "{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostAvailabilityMonitor", "src\HostAvailabilityMonitor\HostAvailabilityMonitor.csproj", "{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkMonitorConfigGenerator", "src\BizTalkMonitorConfigGenerator\BizTalkMonitorConfigGenerator.csproj", "{E8C93E45-DA2B-4E30-8891-637A2C7FAE0B}"
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
EndProject
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Global
|
||||||
Release|Any CPU = Release|Any CPU
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
EndGlobalSection
|
Debug|Any CPU = Debug|Any CPU
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
Release|Any CPU = Release|Any CPU
|
||||||
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
EndGlobalSection
|
||||||
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
EndGlobalSection
|
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
HideSolutionNode = FALSE
|
{E8C93E45-DA2B-4E30-8891-637A2C7FAE0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
EndGlobalSection
|
{E8C93E45-DA2B-4E30-8891-637A2C7FAE0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
EndGlobal
|
{E8C93E45-DA2B-4E30-8891-637A2C7FAE0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E8C93E45-DA2B-4E30-8891-637A2C7FAE0B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
|
|||||||
@@ -1,213 +1,400 @@
|
|||||||
# Host Availability Monitor (.NET Framework 4.8)
|
# Host Availability Monitor + BizTalk Monitor Config Generator (.NET Framework 4.7.2)
|
||||||
|
|
||||||
Robuste Windows-Console-Anwendung für **Windows Server 2019** zur Überwachung von UNC-Fileshares, HTTP/HTTPS-Endpunkten, SFTP-Servern, TCP-Ports und optional ICMP/Ping.
|
Dieses Repository ist für eine **Gitea-kompatible Repo-Struktur** aufgebaut und enthält zwei Konsolenanwendungen für **Windows Server 2019 / BizTalk Server 2019** auf **.NET Framework 4.7.2**:
|
||||||
|
|
||||||
Die Anwendung ist für den Einsatz mit dem **Task Scheduler** gedacht und schreibt:
|
1. **HostAvailabilityMonitor**
|
||||||
|
- prüft Endpunkte wie UNC/SMB, `file://`, HTTP/HTTPS, SFTP, FTP, TCP und ICMP
|
||||||
- ein tägliches Rolling-Logfile
|
- schreibt Rolling-Logs, Windows Application Event Log und E-Mails bei DOWN, Recovery und täglichem Status
|
||||||
- Einträge ins **Windows Application Event Log**
|
|
||||||
- Statusinformationen in eine lokale Statusdatei
|
2. **BizTalkMonitorConfigGenerator**
|
||||||
- Benachrichtigungs-E-Mails bei Fehlern und optional bei Recovery
|
- liest aktive **BizTalk Send Ports** und aktivierte **Receive Locations** aus
|
||||||
|
- erzeugt daraus automatisch die JSON-Konfiguration, die der `HostAvailabilityMonitor` direkt verwenden kann
|
||||||
## Warum .NET Framework 4.8?
|
- übernimmt Umgebung, Default-Werte und fachliche Anwendungszuordnung
|
||||||
|
|
||||||
Windows Server 2019 enthält .NET Framework 4.7.2 und unterstützt als separat installierbare Version .NET Framework 4.8. Das macht .NET Framework 4.8 für dieses Umfeld zu einer sehr passenden Zielplattform.
|
## Zielbild
|
||||||
|
|
||||||
## Features
|
Der typische Betrieb auf einer BizTalk-Maschine ist:
|
||||||
|
|
||||||
- **Zieltypen**
|
1. Der **Generator** läuft mit Admin-Rechten auf dem jeweiligen BizTalk-Server.
|
||||||
- UNC / SMB (`\\server\share`)
|
2. Er liest die aktiven BizTalk-Artefakte aus.
|
||||||
- HTTP / HTTPS (`http://...`, `https://...`)
|
3. Er erzeugt eine vollständige Monitor-Konfiguration im JSON-Format.
|
||||||
- SFTP (netzwerktechnisch per TCP-Port 22 oder konfiguriertem Port)
|
4. Der **Monitor** läuft anschließend mit genau dieser JSON-Datei.
|
||||||
- TCP (`tcp://server:8443`)
|
5. Bei DOWN und bei `DOWN -> UP` gehen Mails raus.
|
||||||
- ICMP (`icmp://server`)
|
6. Zusätzlich geht **einmal pro Tag** eine **Status-Mail** mit Tageszusammenfassung raus, standardmäßig um **14:00 Uhr lokal**.
|
||||||
- Optional auch **Plain Hostnames**:
|
|
||||||
- mit `Port` => TCP-Check
|
## Gitea-kompatible Repository-Struktur
|
||||||
- ohne `Port` => ICMP-Check
|
|
||||||
- **Tägliche Logrotation** über Dateiname `host-availability-monitor-yyyy-MM-dd.log`
|
```text
|
||||||
- **Log-Retention** konfigurierbar, Standard: 5 Tage
|
.
|
||||||
- **Event Log** in `Application`
|
├── .editorconfig
|
||||||
- **E-Mail-Benachrichtigung** bei Ausfall und optional bei Recovery
|
├── .gitignore
|
||||||
- **Cooldown / State Change Logik** gegen Mail-Spam
|
├── Documentation.en.md
|
||||||
- **Resiliente Dateiverarbeitung** mit atomarem Schreiben der Statusdatei
|
├── Dokumentation.md
|
||||||
- Für **Task Scheduler** geeignet
|
├── HostAvailabilityMonitor.sln
|
||||||
|
├── README.md
|
||||||
## Projektstruktur
|
├── installers/
|
||||||
|
│ ├── README.md
|
||||||
```text
|
│ ├── powershell/
|
||||||
host-availability-monitor-net48/
|
│ │ ├── Install-BizTalkMonitorConfigGenerator.ps1
|
||||||
├── HostAvailabilityMonitor.sln
|
│ │ ├── Install-Both.ps1
|
||||||
├── README.md
|
│ │ ├── Install-HostAvailabilityMonitor.ps1
|
||||||
├── Dokumentation.md
|
│ │ ├── Uninstall-BizTalkMonitorConfigGenerator.ps1
|
||||||
├── .gitignore
|
│ │ └── Uninstall-HostAvailabilityMonitor.ps1
|
||||||
├── .editorconfig
|
│ └── wix/
|
||||||
├── scripts/
|
│ ├── BizTalkMonitorConfigGenerator.wxs
|
||||||
│ ├── build-release.ps1
|
│ ├── HostAvailabilityMonitor.wxs
|
||||||
│ └── register-event-source.ps1
|
│ └── build-msi.ps1
|
||||||
└── src/
|
├── scripts/
|
||||||
└── HostAvailabilityMonitor/
|
│ ├── build-release.ps1
|
||||||
├── App.config
|
│ ├── generate-monitor-config.ps1
|
||||||
├── appsettings.json
|
│ ├── install-generator-on-server.ps1
|
||||||
├── HostAvailabilityMonitor.csproj
|
│ ├── install-on-server.ps1
|
||||||
├── Program.cs
|
│ ├── package-deployment.ps1
|
||||||
├── Configuration/
|
│ └── register-event-source.ps1
|
||||||
├── Logging/
|
└── src/
|
||||||
├── Monitoring/
|
├── BizTalkMonitorConfigGenerator/
|
||||||
├── Notifications/
|
│ ├── App.config
|
||||||
├── Properties/
|
│ ├── BizTalkMonitorConfigGenerator.csproj
|
||||||
└── State/
|
│ ├── Program.cs
|
||||||
```
|
│ └── generator-settings.json
|
||||||
|
└── HostAvailabilityMonitor/
|
||||||
## Build
|
├── App.config
|
||||||
|
├── HostAvailabilityMonitor.csproj
|
||||||
### Visual Studio
|
├── Program.cs
|
||||||
|
└── appsettings.json
|
||||||
- Solution `HostAvailabilityMonitor.sln` öffnen
|
```
|
||||||
- Configuration `Release`
|
|
||||||
- Build starten
|
Diese Struktur ist bewusst einfach gehalten, damit das Repository sauber in **Gitea** eingecheckt, gebaut und auf Windows-Servern ausgerollt werden kann.
|
||||||
|
|
||||||
### MSBuild
|
## Lösung / Solution
|
||||||
|
|
||||||
```powershell
|
- `src/HostAvailabilityMonitor/`
|
||||||
msbuild .\HostAvailabilityMonitor.sln /p:Configuration=Release /p:Platform="Any CPU"
|
- `src/BizTalkMonitorConfigGenerator/`
|
||||||
```
|
- `scripts/build-release.ps1`
|
||||||
|
- `scripts/install-on-server.ps1`
|
||||||
Alternativ liegt das Skript `scripts/build-release.ps1` bei.
|
- `scripts/install-generator-on-server.ps1`
|
||||||
|
- `scripts/generate-monitor-config.ps1`
|
||||||
## Deployment
|
- `scripts/package-deployment.ps1`
|
||||||
|
- `installers/powershell/*`
|
||||||
1. Projekt im Release-Modus bauen
|
- `installers/wix/*`
|
||||||
2. Den Inhalt von `src\HostAvailabilityMonitor\bin\Release\` auf den Server kopieren
|
|
||||||
3. `appsettings.json` anpassen
|
## Technische Eckpunkte
|
||||||
4. Optional Event Source registrieren:
|
|
||||||
|
### HostAvailabilityMonitor
|
||||||
```powershell
|
|
||||||
.\scripts\register-event-source.ps1 -Source "HostAvailabilityMonitor" -LogName "Application"
|
- Task-Scheduler-tauglich
|
||||||
```
|
- tägliche Logrotation
|
||||||
|
- Log-Retention standardmäßig 5 Tage
|
||||||
5. Task Scheduler anlegen, z. B. alle 30 Minuten
|
- Windows Application Event Log
|
||||||
|
- SMTP-Mails bei DOWN, Recovery (`DOWN -> UP`) und täglichem Status
|
||||||
## Konfiguration
|
- SMTP-Sendports bzw. reine E-Mail-Empfaengerlisten werden vom Generator uebersprungen und vom Monitor defensiv nur als `Skipped` protokolliert, nicht alarmiert
|
||||||
|
- tägliche Status-Mail auf Basis des **ersten Laufs ab der konfigurierten Uhrzeit**
|
||||||
Die Anwendung nutzt **`appsettings.json`** als Anwendungs-Konfiguration.
|
- State-Datei gegen Mail-Spam und für die Einmal-pro-Tag-Logik der Status-Mail
|
||||||
|
- `ApplicationName` pro Target
|
||||||
Wichtig für .NET Framework 4.8 in dieser Umsetzung:
|
- `EnvironmentName` global
|
||||||
|
|
||||||
- JSON muss **strict valid** sein
|
### BizTalkMonitorConfigGenerator
|
||||||
- keine Kommentare
|
|
||||||
- keine trailing commas
|
- liest BizTalk über **Microsoft.BizTalk.ExplorerOM**
|
||||||
|
- berücksichtigt standardmäßig nur:
|
||||||
### Beispiel
|
- **gestartete Send Ports**
|
||||||
|
- **aktivierte Receive Locations**
|
||||||
```json
|
- unterstützt optional:
|
||||||
{
|
- Secondary Transport von Send Ports
|
||||||
"ApplicationName": "HostAvailabilityMonitor",
|
- lokale Pfade
|
||||||
"Runtime": {
|
- Mapping von BizTalk-Anwendungsnamen auf fachliche `ApplicationName`-Werte
|
||||||
"MaxConcurrency": 4,
|
- schreibt Rolling-Log und Event Log
|
||||||
"DefaultValidateUncPathAccess": false,
|
- schreibt die Ziel-JSON **atomar** über Temp-Datei + Rename
|
||||||
"NonZeroExitCodeOnAnyFailure": false,
|
|
||||||
"NonZeroExitCodeOnExecutionError": true,
|
## Build
|
||||||
"StateDirectory": "state"
|
|
||||||
},
|
### Voraussetzung
|
||||||
"Logging": {
|
|
||||||
"LogDirectory": "logs",
|
Für den Generator muss auf dem Build- oder Zielsystem die BizTalk-Assembly **`Microsoft.BizTalk.ExplorerOM.dll`** verfügbar sein, typischerweise durch installierten BizTalk Server bzw. BizTalk Developer/Admin Components.
|
||||||
"FilePrefix": "host-availability-monitor",
|
|
||||||
"RetentionDays": 5
|
### MSBuild
|
||||||
},
|
|
||||||
"EventLog": {
|
```powershell
|
||||||
"Enabled": true,
|
msbuild .\HostAvailabilityMonitor.sln /p:Configuration=Release /p:Platform="Any CPU"
|
||||||
"LogName": "Application",
|
```
|
||||||
"Source": "HostAvailabilityMonitor",
|
|
||||||
"CreateSourceIfMissing": false,
|
### Build-Skript
|
||||||
"WriteSuccessEntries": false,
|
|
||||||
"WriteSummaryEntries": true
|
```powershell
|
||||||
},
|
.\scripts\build-release.ps1
|
||||||
"Email": {
|
```
|
||||||
"Enabled": false,
|
|
||||||
"SmtpHost": "smtp.example.local",
|
Das Skript erstellt anschließend typischerweise:
|
||||||
"SmtpPort": 25,
|
|
||||||
"UseSsl": false,
|
- `artifacts\publish\net472\HostAvailabilityMonitor\`
|
||||||
"Username": "",
|
- `artifacts\publish\net472\BizTalkMonitorConfigGenerator\`
|
||||||
"Password": "",
|
|
||||||
"From": "monitor@example.local",
|
Optional direkt mit Deployment-Paket:
|
||||||
"To": ["ops@example.local"],
|
|
||||||
"SubjectPrefix": "[HostAvailabilityMonitor]",
|
```powershell
|
||||||
"SendOnFailure": true,
|
.\scripts\build-release.ps1 -CreateDeploymentPackage
|
||||||
"SendOnRecovery": true,
|
```
|
||||||
"OnlyOnStateChange": true,
|
|
||||||
"CooldownMinutes": 0,
|
Oder separat:
|
||||||
"TimeoutSeconds": 30
|
|
||||||
},
|
```powershell
|
||||||
"Targets": [
|
.\scripts\package-deployment.ps1
|
||||||
{
|
```
|
||||||
"Name": "Internes Fileshare 1",
|
|
||||||
"Endpoint": "\\\\internerserver1\\share1",
|
Danach liegt ein verteilerfertiges ZIP unter `artifacts\deploy\net472\`.
|
||||||
"TimeoutSeconds": 8,
|
|
||||||
"ValidateUncPathAccess": false,
|
|
||||||
"Enabled": true
|
## Installer / Setup-Optionen
|
||||||
},
|
|
||||||
{
|
Es gibt jetzt **zwei Installer-Wege**:
|
||||||
"Name": "Externe Webseite",
|
|
||||||
"Endpoint": "https://host1.de/url",
|
### 1. PowerShell-Installer (empfohlen fuer BizTalk/Windows Server)
|
||||||
"TimeoutSeconds": 10,
|
|
||||||
"HttpMethod": "HEAD",
|
Diese Variante ist in restriktiven Serverumgebungen meist am praktikabelsten.
|
||||||
"Enabled": true
|
|
||||||
},
|
Wichtige Dateien:
|
||||||
{
|
|
||||||
"Name": "SFTP Partner A",
|
- `installers\powershell\Install-HostAvailabilityMonitor.ps1`
|
||||||
"Endpoint": "sftp://partner.example.local/inbound",
|
- `installers\powershell\Install-BizTalkMonitorConfigGenerator.ps1`
|
||||||
"TimeoutSeconds": 10,
|
- `installers\powershell\Install-Both.ps1`
|
||||||
"Port": 22,
|
- `installers\powershell\Uninstall-HostAvailabilityMonitor.ps1`
|
||||||
"Enabled": true
|
- `installers\powershell\Uninstall-BizTalkMonitorConfigGenerator.ps1`
|
||||||
},
|
|
||||||
{
|
Beispiel fuer ein gemeinsames Setup aus dem Deployment-ZIP:
|
||||||
"Name": "Custom TCP Service",
|
|
||||||
"Endpoint": "tcp://host2.example.local:8443",
|
```powershell
|
||||||
"TimeoutSeconds": 10,
|
.\installers\powershell\Install-Both.ps1 `
|
||||||
"Enabled": true
|
-PackageRoot . `
|
||||||
}
|
-BaseInstallPath "C:\Program Files\JR IT Services\BizTalk Endpoint Monitor" `
|
||||||
]
|
-BaseConfigPath "C:\ProgramData\JR IT Services\BizTalk Endpoint Monitor" `
|
||||||
}
|
-RegisterEventSources
|
||||||
```
|
```
|
||||||
|
|
||||||
## Task Scheduler
|
### 2. WiX-MSI-Quellen (optional)
|
||||||
|
|
||||||
**Program/script**
|
Fuer beide Programme liegen **WiX-Quellen** bei:
|
||||||
|
|
||||||
```text
|
- `installers\wix\HostAvailabilityMonitor.wxs`
|
||||||
C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe
|
- `installers\wix\BizTalkMonitorConfigGenerator.wxs`
|
||||||
```
|
- `installers\wix\build-msi.ps1`
|
||||||
|
|
||||||
**Start in**
|
Damit kannst du auf einem Windows-Buildsystem mit WiX Toolset v3 echte MSI-Pakete erzeugen.
|
||||||
|
|
||||||
```text
|
Beispiel:
|
||||||
C:\Tools\HostAvailabilityMonitor
|
|
||||||
```
|
```powershell
|
||||||
|
.\installers\wix\build-msi.ps1 `
|
||||||
**Optional arguments**
|
-PublishRoot .\artifacts\publish\net472 `
|
||||||
|
-OutputRoot .\artifacts\msi
|
||||||
```text
|
```
|
||||||
C:\Tools\HostAvailabilityMonitor\appsettings.json
|
|
||||||
```
|
Hinweis: In diesem Arbeitsraum konnte ich **keine MSI-Binaries real bauen**, weil hier keine Windows-/WiX-Toolchain bereitsteht. Ich habe dir daher **robuste PowerShell-Installer** und **MSI-Quellen fuer den Windows-Build** bereitgestellt.
|
||||||
|
|
||||||
## Logging
|
## Installation auf dem Server
|
||||||
|
|
||||||
- Dateilog: `logs\host-availability-monitor-yyyy-MM-dd.log`
|
### Monitor
|
||||||
- Retention: Standard 5 Tage
|
|
||||||
- Event Log: `Application`
|
```powershell
|
||||||
- State-Datei: `state\monitor-state.json`
|
.\scripts\install-on-server.ps1 `
|
||||||
|
-SourcePath .\artifacts\publish\net472\HostAvailabilityMonitor `
|
||||||
## Benachrichtigung
|
-InstallPath C:\Tools\HostAvailabilityMonitor `
|
||||||
|
-RegisterEventSource
|
||||||
Benachrichtigung wird nur als erfolgreich markiert, wenn der SMTP-Versand wirklich erfolgreich war. Dadurch bleibt die Cooldown-/State-Change-Logik konsistent.
|
```
|
||||||
|
|
||||||
## Hinweise
|
### Generator
|
||||||
|
|
||||||
- Für **UNC-Pfade** kann optional zusätzlich der Pfadzugriff validiert werden. Standardmäßig wird nur die **SMB-Erreichbarkeit** über TCP 445 geprüft.
|
```powershell
|
||||||
- Für **HTTP/HTTPS** gilt jede empfangene HTTP-Antwort als netzwerktechnisch erreichbar, auch `404` oder `500`.
|
.\scripts\install-generator-on-server.ps1 `
|
||||||
- Für **SFTP** wird bewusst nur die **Port-Erreichbarkeit** geprüft, kein Login und kein Dateizugriff.
|
-SourcePath .\artifacts\publish\net472\BizTalkMonitorConfigGenerator `
|
||||||
- Das Anlegen einer neuen Event Source erfordert Administratorrechte. Deshalb ist dafür ein separates Installationsskript beigefügt.
|
-InstallPath C:\Tools\BizTalkMonitorConfigGenerator `
|
||||||
|
-RegisterEventSource
|
||||||
## Weiterführend
|
```
|
||||||
|
|
||||||
Details zu Logging, Exit Codes, Fehlerfällen und Betriebsmodell stehen in [Dokumentation.md](Dokumentation.md).
|
## Monitor-Konfiguration für tägliche Status-Mail
|
||||||
|
|
||||||
|
In `appsettings.json` bzw. in `GeneratedMonitorConfig.Email` des Generators stehen jetzt zusätzlich diese Felder:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Email": {
|
||||||
|
"Enabled": true,
|
||||||
|
"To": [ "operations@example.local", "integration-oncall@example.local" ],
|
||||||
|
"Cc": [ "integration-leads@example.local" ],
|
||||||
|
"Bcc": [],
|
||||||
|
"SendOnFailure": true,
|
||||||
|
"SendOnRecovery": true,
|
||||||
|
"SendDailySummary": true,
|
||||||
|
"DailySummaryHourLocal": 14,
|
||||||
|
"DailySummaryMinuteLocal": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bedeutung:
|
||||||
|
|
||||||
|
- `SendDailySummary`: schaltet die tägliche Status-Mail ein oder aus
|
||||||
|
- `DailySummaryHourLocal`: lokale Stunde auf dem Server, z. B. `14`
|
||||||
|
- `DailySummaryMinuteLocal`: lokale Minute auf dem Server, z. B. `0`
|
||||||
|
|
||||||
|
Empfaengerfelder:
|
||||||
|
|
||||||
|
- `To`: Hauptempfaenger als JSON-Liste
|
||||||
|
- `Cc`: optionale CC-Empfaenger als JSON-Liste
|
||||||
|
- `Bcc`: optionale BCC-Empfaenger als JSON-Liste
|
||||||
|
- Es muss mindestens ein Empfaenger in `To`, `Cc` oder `Bcc` vorhanden sein.
|
||||||
|
- Mehrere Empfaenger werden **nicht** per Semikolon in einem String konfiguriert, sondern als mehrere Array-Eintraege.
|
||||||
|
|
||||||
|
Wichtig für den Betrieb:
|
||||||
|
|
||||||
|
- Die Mail wird **nicht durch einen separaten zweiten Job** verschickt.
|
||||||
|
- Sie wird vom normalen Monitor-Lauf versendet.
|
||||||
|
- Das heißt: Der **erste erfolgreiche Programmlauf ab 14:00 Uhr lokal** verschickt die Tageszusammenfassung.
|
||||||
|
- Läuft der Task z. B. alle 30 Minuten, dann geht die Status-Mail je nach Startzeit um `14:00`, `14:30`, `15:00` usw. raus — aber **nur einmal pro Tag**.
|
||||||
|
|
||||||
|
Inhalt der Tagesstatus-Mail:
|
||||||
|
|
||||||
|
- Monitorname, Umgebung, Servername
|
||||||
|
- Zeitfenster von Mitternacht bis Versandzeitpunkt
|
||||||
|
- Anzahl aller heutigen Checks
|
||||||
|
- Anzahl heutiger erfolgreicher und fehlgeschlagener Checks
|
||||||
|
- aktueller Snapshot des letzten Laufs
|
||||||
|
- Liste der aktuell DOWN befindlichen Endpunkte mit Anwendung und Umgebung
|
||||||
|
|
||||||
|
## Mehrere Mail-Empfaenger konfigurieren
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Email": {
|
||||||
|
"Enabled": true,
|
||||||
|
"From": "monitor@example.local",
|
||||||
|
"To": [
|
||||||
|
"operations@example.local",
|
||||||
|
"integration-oncall@example.local"
|
||||||
|
],
|
||||||
|
"Cc": [
|
||||||
|
"integration-leads@example.local"
|
||||||
|
],
|
||||||
|
"Bcc": [
|
||||||
|
"audit@example.local"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- pro Adresse ein eigener Eintrag im Array
|
||||||
|
- keine Semikolon-getrennte Empfaengerliste in einem einzelnen String
|
||||||
|
- `Cc` und `Bcc` sind optional
|
||||||
|
|
||||||
|
## Generator verwenden
|
||||||
|
|
||||||
|
### 1. Generator-Einstellungen prüfen oder anpassen
|
||||||
|
|
||||||
|
Datei:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtige Felder:
|
||||||
|
|
||||||
|
- `EnvironmentName`: z. B. `HIP Produktion`
|
||||||
|
- `BizTalk.ConnectionString`: meist `SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI`
|
||||||
|
- `BizTalk.OnlyStartedSendPorts`: nur gestartete Send Ports übernehmen
|
||||||
|
- `BizTalk.OnlyEnabledReceiveLocations`: nur aktivierte Receive Locations übernehmen
|
||||||
|
- `BizTalk.IncludeSecondaryTransportOnSendPorts`: Secondary Transport mit aufnehmen
|
||||||
|
- Hinweis: Secondary Transports mit Adapter `SMTP` werden bewusst nicht als Monitor-Targets erzeugt
|
||||||
|
- `BizTalk.IncludeLocalFilePaths`: lokale Pfade zulassen oder verwerfen
|
||||||
|
- `BizTalk.ApplicationMappings`: Mapping von BizTalk-Anwendungsnamen auf fachliche App-Namen
|
||||||
|
- `Output.Path`: Pfad der zu erzeugenden Monitor-JSON
|
||||||
|
- `GeneratedMonitorConfig`: Vorlage für Logging, Runtime, Event Log und Mail des Monitors
|
||||||
|
- `GeneratedMonitorConfig.Email.SendDailySummary`: tägliche Status-Mail in der generierten Monitor-JSON einschalten
|
||||||
|
- `GeneratedMonitorConfig.Email.DailySummaryHourLocal`: lokale Uhrzeit der Tageszusammenfassung
|
||||||
|
- `GeneratedMonitorConfig.Email.DailySummaryMinuteLocal`: lokale Minute der Tageszusammenfassung
|
||||||
|
|
||||||
|
### 2. Generator direkt starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\BizTalkMonitorConfigGenerator.exe C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Generator per Hilfsskript starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\generate-monitor-config.ps1 -InstallPath C:\Tools\BizTalkMonitorConfigGenerator
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Ergebnis prüfen
|
||||||
|
|
||||||
|
Danach sollte eine generierte Monitor-Datei vorhanden sein, z. B.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Zusätzlich prüfen:
|
||||||
|
|
||||||
|
- Generator-Logdatei unter `logs\`
|
||||||
|
- Einträge im Windows Application Event Log
|
||||||
|
- Inhalt der generierten JSON
|
||||||
|
|
||||||
|
### 5. Monitor mit der erzeugten Datei starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Praxisbeispiel für den Betrieb auf der BizTalk-Maschine
|
||||||
|
|
||||||
|
### Task 1: Generator
|
||||||
|
|
||||||
|
- Programm: `C:\Tools\BizTalkMonitorConfigGenerator\BizTalkMonitorConfigGenerator.exe`
|
||||||
|
- Argumente: `C:\Tools\BizTalkMonitorConfigGenerator\generator-settings.json`
|
||||||
|
- Starten in: `C:\Tools\BizTalkMonitorConfigGenerator`
|
||||||
|
- Konto: BizTalk-Admin / technisches Administratorkonto
|
||||||
|
- Intervall: z. B. alle 30 Minuten oder vor dem Monitor-Lauf
|
||||||
|
|
||||||
|
### Task 2: Monitor
|
||||||
|
|
||||||
|
- Programm: `C:\Tools\HostAvailabilityMonitor\HostAvailabilityMonitor.exe`
|
||||||
|
- Argumente: `C:\Tools\BizTalkMonitorConfigGenerator\generated-monitor-appsettings.json`
|
||||||
|
- Starten in: `C:\Tools\HostAvailabilityMonitor`
|
||||||
|
- Konto: Dienstkonto mit technischem Zugriff auf Shares, HTTP(S), FTP/SFTP und andere Zielsysteme
|
||||||
|
- Intervall: kurz nach dem Generator, z. B. 1 bis 2 Minuten später
|
||||||
|
|
||||||
|
Empfehlung für die Status-Mail um 14:00:
|
||||||
|
|
||||||
|
- den Monitor mindestens alle 15 oder 30 Minuten laufen lassen
|
||||||
|
- sicherstellen, dass in dieser Zeit auch **mindestens ein Lauf nach 14:00 Uhr** stattfindet
|
||||||
|
- kein separater Status-Task notwendig
|
||||||
|
|
||||||
|
## Beispiel für ein Generator-Mapping
|
||||||
|
|
||||||
|
```json
|
||||||
|
"ApplicationMappings": [
|
||||||
|
{
|
||||||
|
"BizTalkApplicationName": "HIP*",
|
||||||
|
"ApplicationName": "HIP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"BizTalkApplicationName": "PartnerA*",
|
||||||
|
"ApplicationName": "HIP Partneranbindung"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- Fuer .NET Framework 4.7.2 ist der Build-Output jetzt unter `artifacts\publish\net472\` vorgesehen.
|
||||||
|
|
||||||
|
- Nicht unterstützte Endpunkte wie `net.pipe://` werden sauber übersprungen und protokolliert.
|
||||||
|
- Für `net.tcp://host:port/...` wird eine Monitor-kompatible `tcp://...`-Adresse erzeugt.
|
||||||
|
- Für lokale Dateipfade ist standardmäßig `IncludeLocalFilePaths=false` gesetzt.
|
||||||
|
- Die erzeugte Monitor-JSON enthält `EnvironmentName` global und `ApplicationName` pro Target.
|
||||||
|
- Der Generator versendet selbst keine E-Mails, sondern erzeugt nur die Konfiguration.
|
||||||
|
- Die Event-Source-Erstellung benötigt Administratorrechte.
|
||||||
|
- Die Tagesstatus-Mail wird nur dann als „versendet“ gespeichert, wenn der SMTP-Versand erfolgreich war.
|
||||||
|
|
||||||
|
## Dokumentation
|
||||||
|
|
||||||
|
- Deutsch: [Dokumentation.md](Dokumentation.md)
|
||||||
|
- English: [Documentation.en.md](Documentation.en.md)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Installers
|
||||||
|
|
||||||
|
This repository contains two installer approaches for both console applications.
|
||||||
|
|
||||||
|
## 1. PowerShell installers
|
||||||
|
|
||||||
|
Recommended for most BizTalk / Windows Server environments.
|
||||||
|
|
||||||
|
Files:
|
||||||
|
- `powershell/Install-HostAvailabilityMonitor.ps1`
|
||||||
|
- `powershell/Install-BizTalkMonitorConfigGenerator.ps1`
|
||||||
|
- `powershell/Install-Both.ps1`
|
||||||
|
- `powershell/Uninstall-HostAvailabilityMonitor.ps1`
|
||||||
|
- `powershell/Uninstall-BizTalkMonitorConfigGenerator.ps1`
|
||||||
|
|
||||||
|
These scripts install the binaries, create required directories, copy a default configuration when missing, and can optionally register event log sources.
|
||||||
|
|
||||||
|
## 2. WiX MSI sources
|
||||||
|
|
||||||
|
Optional MSI source files are provided in `wix/`.
|
||||||
|
|
||||||
|
Files:
|
||||||
|
- `wix/HostAvailabilityMonitor.wxs`
|
||||||
|
- `wix/BizTalkMonitorConfigGenerator.wxs`
|
||||||
|
- `wix/build-msi.ps1`
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Windows build machine
|
||||||
|
- WiX Toolset v3 in PATH (`candle.exe`, `light.exe`)
|
||||||
|
- publish output already present in `artifacts\publish\net472`
|
||||||
|
|
||||||
|
Note: the WiX files are intentionally minimal starter installers and may be extended later with service/task registration, shortcuts, custom actions, or richer upgrade rules.
|
||||||
|
|
||||||
|
|
||||||
|
## Email configuration
|
||||||
|
|
||||||
|
The deployed monitor configuration supports `To`, `Cc`, and `Bcc` arrays for email recipients. Configure one address per array element.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
param(
|
||||||
|
[string]$PackageRoot = (Split-Path -Parent $PSScriptRoot),
|
||||||
|
[string]$InstallPath = "C:\Program Files\JR IT Services\BizTalkMonitorConfigGenerator",
|
||||||
|
[string]$ConfigRoot = "C:\ProgramData\JR IT Services\BizTalkMonitorConfigGenerator",
|
||||||
|
[switch]$RegisterEventSource,
|
||||||
|
[switch]$CreateBackup
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$repoScripts = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'scripts'
|
||||||
|
$generatorSource = Join-Path $PackageRoot 'generator'
|
||||||
|
if (-not (Test-Path -LiteralPath $generatorSource)) {
|
||||||
|
$generatorSource = Join-Path (Split-Path -Parent $PackageRoot) 'generator'
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $generatorSource)) {
|
||||||
|
throw "Generator source folder not found below package root: $PackageRoot"
|
||||||
|
}
|
||||||
|
|
||||||
|
& (Join-Path $repoScripts 'install-generator-on-server.ps1') `
|
||||||
|
-SourcePath $generatorSource `
|
||||||
|
-InstallPath $InstallPath `
|
||||||
|
-RegisterEventSource:$RegisterEventSource `
|
||||||
|
-CreateBackup:$CreateBackup
|
||||||
|
|
||||||
|
New-Item -Path $ConfigRoot -ItemType Directory -Force | Out-Null
|
||||||
|
$configFile = Join-Path $ConfigRoot 'generator-settings.json'
|
||||||
|
$defaultConfig = Join-Path $InstallPath 'generator-settings.json'
|
||||||
|
if ((Test-Path -LiteralPath $defaultConfig) -and (-not (Test-Path -LiteralPath $configFile))) {
|
||||||
|
Copy-Item -Path $defaultConfig -Destination $configFile -Force
|
||||||
|
}
|
||||||
|
Write-Host "[INFO] BizTalkMonitorConfigGenerator installed to $InstallPath"
|
||||||
|
Write-Host "[INFO] Config location: $configFile"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
param(
|
||||||
|
[string]$PackageRoot = (Split-Path -Parent $PSScriptRoot),
|
||||||
|
[string]$BaseInstallPath = "C:\Program Files\JR IT Services\BizTalk Endpoint Monitor",
|
||||||
|
[string]$BaseConfigPath = "C:\ProgramData\JR IT Services\BizTalk Endpoint Monitor",
|
||||||
|
[switch]$RegisterEventSources,
|
||||||
|
[switch]$CreateBackup
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$hostScript = Join-Path $PSScriptRoot 'Install-HostAvailabilityMonitor.ps1'
|
||||||
|
$genScript = Join-Path $PSScriptRoot 'Install-BizTalkMonitorConfigGenerator.ps1'
|
||||||
|
|
||||||
|
& $genScript `
|
||||||
|
-PackageRoot $PackageRoot `
|
||||||
|
-InstallPath (Join-Path $BaseInstallPath 'BizTalkMonitorConfigGenerator') `
|
||||||
|
-ConfigRoot (Join-Path $BaseConfigPath 'BizTalkMonitorConfigGenerator') `
|
||||||
|
-RegisterEventSource:$RegisterEventSources `
|
||||||
|
-CreateBackup:$CreateBackup
|
||||||
|
|
||||||
|
& $hostScript `
|
||||||
|
-PackageRoot $PackageRoot `
|
||||||
|
-InstallPath (Join-Path $BaseInstallPath 'HostAvailabilityMonitor') `
|
||||||
|
-ConfigRoot (Join-Path $BaseConfigPath 'HostAvailabilityMonitor') `
|
||||||
|
-RegisterEventSource:$RegisterEventSources `
|
||||||
|
-CreateBackup:$CreateBackup
|
||||||
|
|
||||||
|
Write-Host "[INFO] Both applications have been installed."
|
||||||
|
Write-Host "[INFO] Suggested next steps:"
|
||||||
|
Write-Host "[INFO] 1. Adjust generator settings in ProgramData."
|
||||||
|
Write-Host "[INFO] 2. Run generator once to create generated-monitor-appsettings.json."
|
||||||
|
Write-Host "[INFO] 3. Start HostAvailabilityMonitor with the generated JSON path."
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
param(
|
||||||
|
[string]$PackageRoot = (Split-Path -Parent $PSScriptRoot),
|
||||||
|
[string]$InstallPath = "C:\Program Files\JR IT Services\HostAvailabilityMonitor",
|
||||||
|
[string]$ConfigRoot = "C:\ProgramData\JR IT Services\HostAvailabilityMonitor",
|
||||||
|
[switch]$RegisterEventSource,
|
||||||
|
[switch]$CreateBackup
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$repoScripts = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'scripts'
|
||||||
|
$monitorSource = Join-Path $PackageRoot 'monitor'
|
||||||
|
if (-not (Test-Path -LiteralPath $monitorSource)) {
|
||||||
|
$monitorSource = Join-Path (Split-Path -Parent $PackageRoot) 'monitor'
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $monitorSource)) {
|
||||||
|
throw "Monitor source folder not found below package root: $PackageRoot"
|
||||||
|
}
|
||||||
|
|
||||||
|
& (Join-Path $repoScripts 'install-on-server.ps1') `
|
||||||
|
-SourcePath $monitorSource `
|
||||||
|
-InstallPath $InstallPath `
|
||||||
|
-RegisterEventSource:$RegisterEventSource `
|
||||||
|
-CreateBackup:$CreateBackup
|
||||||
|
|
||||||
|
New-Item -Path $ConfigRoot -ItemType Directory -Force | Out-Null
|
||||||
|
$configFile = Join-Path $ConfigRoot 'appsettings.json'
|
||||||
|
$defaultConfig = Join-Path $InstallPath 'appsettings.json'
|
||||||
|
if ((Test-Path -LiteralPath $defaultConfig) -and (-not (Test-Path -LiteralPath $configFile))) {
|
||||||
|
Copy-Item -Path $defaultConfig -Destination $configFile -Force
|
||||||
|
}
|
||||||
|
Write-Host "[INFO] HostAvailabilityMonitor installed to $InstallPath"
|
||||||
|
Write-Host "[INFO] Config location: $configFile"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
param(
|
||||||
|
[string]$InstallPath = "C:\Program Files\JR IT Services\BizTalkMonitorConfigGenerator",
|
||||||
|
[string]$ConfigRoot = "C:\ProgramData\JR IT Services\BizTalkMonitorConfigGenerator",
|
||||||
|
[switch]$RemoveConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $InstallPath) {
|
||||||
|
Remove-Item -LiteralPath $InstallPath -Recurse -Force
|
||||||
|
Write-Host "[INFO] Removed $InstallPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RemoveConfig -and (Test-Path -LiteralPath $ConfigRoot)) {
|
||||||
|
Remove-Item -LiteralPath $ConfigRoot -Recurse -Force
|
||||||
|
Write-Host "[INFO] Removed $ConfigRoot"
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
param(
|
||||||
|
[string]$InstallPath = "C:\Program Files\JR IT Services\HostAvailabilityMonitor",
|
||||||
|
[string]$ConfigRoot = "C:\ProgramData\JR IT Services\HostAvailabilityMonitor",
|
||||||
|
[switch]$RemoveConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $InstallPath) {
|
||||||
|
Remove-Item -LiteralPath $InstallPath -Recurse -Force
|
||||||
|
Write-Host "[INFO] Removed $InstallPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RemoveConfig -and (Test-Path -LiteralPath $ConfigRoot)) {
|
||||||
|
Remove-Item -LiteralPath $ConfigRoot -Recurse -Force
|
||||||
|
Write-Host "[INFO] Removed $ConfigRoot"
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||||
|
<Product Id="*" Name="BizTalkMonitorConfigGenerator" Language="1033" Version="1.0.0.0" Manufacturer="JR IT Services" UpgradeCode="6E45A4E8-C0C3-4267-8873-B2CF8173E9B2">
|
||||||
|
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
||||||
|
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
|
||||||
|
<MediaTemplate EmbedCab="yes" />
|
||||||
|
|
||||||
|
<PropertyRef Id="WIX_IS_NETFRAMEWORK_472_OR_LATER_INSTALLED" />
|
||||||
|
<Condition Message=".NET Framework 4.7.2 or later is required."><![CDATA[Installed OR WIX_IS_NETFRAMEWORK_472_OR_LATER_INSTALLED]]></Condition>
|
||||||
|
|
||||||
|
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||||
|
<Directory Id="ProgramFilesFolder">
|
||||||
|
<Directory Id="JRITSERVICES" Name="JR IT Services">
|
||||||
|
<Directory Id="INSTALLDIR" Name="BizTalkMonitorConfigGenerator" />
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Feature Id="MainFeature" Title="BizTalkMonitorConfigGenerator" Level="1">
|
||||||
|
<ComponentGroupRef Id="GeneratorHarvest" />
|
||||||
|
</Feature>
|
||||||
|
</Product>
|
||||||
|
</Wix>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||||
|
<Product Id="*" Name="HostAvailabilityMonitor" Language="1033" Version="1.0.0.0" Manufacturer="JR IT Services" UpgradeCode="A4FC4B18-EA15-4F83-8B19-06D79AA2F2A8">
|
||||||
|
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
||||||
|
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
|
||||||
|
<MediaTemplate EmbedCab="yes" />
|
||||||
|
|
||||||
|
<PropertyRef Id="WIX_IS_NETFRAMEWORK_472_OR_LATER_INSTALLED" />
|
||||||
|
<Condition Message=".NET Framework 4.7.2 or later is required."><![CDATA[Installed OR WIX_IS_NETFRAMEWORK_472_OR_LATER_INSTALLED]]></Condition>
|
||||||
|
|
||||||
|
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||||
|
<Directory Id="ProgramFilesFolder">
|
||||||
|
<Directory Id="JRITSERVICES" Name="JR IT Services">
|
||||||
|
<Directory Id="INSTALLDIR" Name="HostAvailabilityMonitor" />
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Feature Id="MainFeature" Title="HostAvailabilityMonitor" Level="1">
|
||||||
|
<ComponentGroupRef Id="MonitorHarvest" />
|
||||||
|
</Feature>
|
||||||
|
</Product>
|
||||||
|
</Wix>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
param(
|
||||||
|
[string]$PublishRoot = ".\artifacts\publish\net472",
|
||||||
|
[string]$OutputRoot = ".\artifacts\msi"
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-WixTool {
|
||||||
|
param([string]$Name)
|
||||||
|
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { return $cmd.Source }
|
||||||
|
throw "$Name was not found. Install WiX Toolset v3 and ensure heat.exe, candle.exe and light.exe are in PATH."
|
||||||
|
}
|
||||||
|
|
||||||
|
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||||
|
$publishRootFull = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $PublishRoot))
|
||||||
|
$outputRootFull = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputRoot))
|
||||||
|
New-Item -Path $outputRootFull -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
|
$heat = Get-WixTool -Name 'heat.exe'
|
||||||
|
$candle = Get-WixTool -Name 'candle.exe'
|
||||||
|
$light = Get-WixTool -Name 'light.exe'
|
||||||
|
|
||||||
|
$packages = @(
|
||||||
|
@{ Name = 'HostAvailabilityMonitor'; ProductFile = 'HostAvailabilityMonitor.wxs'; HarvestFile = 'Harvest_HostAvailabilityMonitor.wxs'; PublishDir = 'HostAvailabilityMonitor'; ComponentGroup = 'MonitorHarvest' },
|
||||||
|
@{ Name = 'BizTalkMonitorConfigGenerator'; ProductFile = 'BizTalkMonitorConfigGenerator.wxs'; HarvestFile = 'Harvest_BizTalkMonitorConfigGenerator.wxs'; PublishDir = 'BizTalkMonitorConfigGenerator'; ComponentGroup = 'GeneratorHarvest' }
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($package in $packages) {
|
||||||
|
$sourceDir = Join-Path $publishRootFull $package.PublishDir
|
||||||
|
if (-not (Test-Path -LiteralPath $sourceDir)) {
|
||||||
|
throw "Publish directory not found: $sourceDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
$harvestPath = Join-Path $outputRootFull $package.HarvestFile
|
||||||
|
$productPath = Join-Path $PSScriptRoot $package.ProductFile
|
||||||
|
$productObj = Join-Path $outputRootFull ($package.Name + '.wixobj')
|
||||||
|
$harvestObj = Join-Path $outputRootFull ($package.Name + '.harvest.wixobj')
|
||||||
|
$msiPath = Join-Path $outputRootFull ($package.Name + '.msi')
|
||||||
|
|
||||||
|
& $heat dir $sourceDir -cg $package.ComponentGroup -dr INSTALLDIR -gg -scom -sreg -sfrag -var var.SourceDir -out $harvestPath
|
||||||
|
& $candle -ext WixNetFxExtension -dSourceDir=$sourceDir -out $productObj $productPath
|
||||||
|
& $candle -ext WixNetFxExtension -dSourceDir=$sourceDir -out $harvestObj $harvestPath
|
||||||
|
& $light -ext WixNetFxExtension -out $msiPath $productObj $harvestObj
|
||||||
|
Write-Host "Built MSI: $msiPath"
|
||||||
|
}
|
||||||
+60
-47
@@ -1,47 +1,60 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Configuration = "Release"
|
[string]$Configuration = "Release",
|
||||||
)
|
[switch]$CreateDeploymentPackage
|
||||||
|
)
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
||||||
$repoRoot = Split-Path -Parent $scriptDir
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
$solutionPath = Join-Path $repoRoot "HostAvailabilityMonitor.sln"
|
$repoRoot = Split-Path -Parent $scriptDir
|
||||||
|
$solutionPath = Join-Path $repoRoot "HostAvailabilityMonitor.sln"
|
||||||
function Get-MSBuildPath {
|
|
||||||
$msbuildFromPath = Get-Command msbuild.exe -ErrorAction SilentlyContinue
|
function Get-MSBuildPath {
|
||||||
if ($msbuildFromPath) {
|
$msbuildFromPath = Get-Command msbuild.exe -ErrorAction SilentlyContinue
|
||||||
return $msbuildFromPath.Source
|
if ($msbuildFromPath) {
|
||||||
}
|
return $msbuildFromPath.Source
|
||||||
|
}
|
||||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
|
||||||
if (Test-Path $vswhere) {
|
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||||
$installationPath = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -property installationPath
|
if (Test-Path $vswhere) {
|
||||||
if ($installationPath) {
|
$installationPath = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -property installationPath
|
||||||
$candidate = Join-Path $installationPath "MSBuild\Current\Bin\MSBuild.exe"
|
if ($installationPath) {
|
||||||
if (Test-Path $candidate) {
|
$candidate = Join-Path $installationPath "MSBuild\Current\Bin\MSBuild.exe"
|
||||||
return $candidate
|
if (Test-Path $candidate) {
|
||||||
}
|
return $candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
throw "MSBuild.exe wurde nicht gefunden. Bitte Visual Studio Build Tools oder Visual Studio installieren."
|
|
||||||
}
|
throw "MSBuild.exe wurde nicht gefunden. Bitte Visual Studio Build Tools oder Visual Studio installieren."
|
||||||
|
}
|
||||||
$msbuild = Get-MSBuildPath
|
|
||||||
Write-Host "Verwende MSBuild: $msbuild"
|
$msbuild = Get-MSBuildPath
|
||||||
|
Write-Host "Verwende MSBuild: $msbuild"
|
||||||
& $msbuild $solutionPath /t:Build /p:Configuration=$Configuration /p:Platform="Any CPU"
|
|
||||||
|
& $msbuild $solutionPath /t:Build /p:Configuration=$Configuration /p:Platform="Any CPU"
|
||||||
$projectDir = Join-Path $repoRoot "src\HostAvailabilityMonitor"
|
|
||||||
$outputDir = Join-Path $projectDir "bin\$Configuration"
|
$publishRoot = Join-Path $repoRoot "artifacts\publish\net472"
|
||||||
$publishDir = Join-Path $repoRoot "artifacts\publish\net48"
|
if (Test-Path $publishRoot) {
|
||||||
|
Remove-Item -Path $publishRoot -Recurse -Force
|
||||||
if (Test-Path $publishDir) {
|
}
|
||||||
Remove-Item -Path $publishDir -Recurse -Force
|
New-Item -ItemType Directory -Path $publishRoot | Out-Null
|
||||||
}
|
|
||||||
|
$projects = @(
|
||||||
New-Item -ItemType Directory -Path $publishDir | Out-Null
|
@{ Name = "HostAvailabilityMonitor"; ProjectDir = Join-Path $repoRoot "src\HostAvailabilityMonitor" },
|
||||||
Copy-Item -Path (Join-Path $outputDir "*") -Destination $publishDir -Recurse -Force
|
@{ Name = "BizTalkMonitorConfigGenerator"; ProjectDir = Join-Path $repoRoot "src\BizTalkMonitorConfigGenerator" }
|
||||||
|
)
|
||||||
Write-Host "Build abgeschlossen. Publish-Verzeichnis: $publishDir"
|
|
||||||
|
foreach ($project in $projects) {
|
||||||
|
$outputDir = Join-Path $project.ProjectDir "bin\$Configuration"
|
||||||
|
$targetDir = Join-Path $publishRoot $project.Name
|
||||||
|
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||||
|
Copy-Item -Path (Join-Path $outputDir "*") -Destination $targetDir -Recurse -Force
|
||||||
|
Write-Host "Publish-Verzeichnis für $($project.Name): $targetDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Build abgeschlossen. Publish-Root: $publishRoot"
|
||||||
|
|
||||||
|
if ($CreateDeploymentPackage) {
|
||||||
|
& (Join-Path $scriptDir 'package-deployment.ps1') -PublishRoot '.\artifacts\publish\net472'
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
param(
|
||||||
|
[string]$InstallPath = 'C:\Tools\BizTalkMonitorConfigGenerator',
|
||||||
|
[string]$ConfigPath
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if (-not $ConfigPath) {
|
||||||
|
$ConfigPath = Join-Path $InstallPath 'generator-settings.json'
|
||||||
|
}
|
||||||
|
|
||||||
|
$exePath = Join-Path $InstallPath 'BizTalkMonitorConfigGenerator.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $exePath)) {
|
||||||
|
throw "Executable wurde nicht gefunden: $exePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
||||||
|
throw "Konfigurationsdatei wurde nicht gefunden: $ConfigPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
& $exePath $ConfigPath
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
Write-Host "ExitCode: $exitCode"
|
||||||
|
exit $exitCode
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SourcePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$InstallPath,
|
||||||
|
|
||||||
|
[string]$EventSource = "BizTalkMonitorConfigGenerator",
|
||||||
|
[string]$EventLogName = "Application",
|
||||||
|
[switch]$RegisterEventSource,
|
||||||
|
[switch]$CreateBackup
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Write-Info {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[INFO] $Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $SourcePath)) {
|
||||||
|
throw "SourcePath '$SourcePath' existiert nicht."
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceFullPath = (Resolve-Path -LiteralPath $SourcePath).Path
|
||||||
|
$installFullPath = [System.IO.Path]::GetFullPath($InstallPath)
|
||||||
|
|
||||||
|
Write-Info "Quelldateien: $sourceFullPath"
|
||||||
|
Write-Info "Zielverzeichnis: $installFullPath"
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $installFullPath) {
|
||||||
|
if ($CreateBackup) {
|
||||||
|
$backupRoot = Join-Path -Path ([System.IO.Path]::GetDirectoryName($installFullPath)) -ChildPath ((Split-Path -Path $installFullPath -Leaf) + '_backup_' + (Get-Date -Format 'yyyyMMdd_HHmmss'))
|
||||||
|
Write-Info "Erstelle Backup unter $backupRoot"
|
||||||
|
Copy-Item -Path (Join-Path $installFullPath '*') -Destination $backupRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
New-Item -Path $installFullPath -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Kopiere Dateien..."
|
||||||
|
Copy-Item -Path (Join-Path $sourceFullPath '*') -Destination $installFullPath -Recurse -Force
|
||||||
|
New-Item -Path (Join-Path $installFullPath 'logs') -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
|
if ($RegisterEventSource) {
|
||||||
|
$registerScript = Join-Path -Path $PSScriptRoot -ChildPath 'register-event-source.ps1'
|
||||||
|
if (-not (Test-Path -LiteralPath $registerScript)) {
|
||||||
|
throw "register-event-source.ps1 wurde nicht gefunden: $registerScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Registriere Event Source '$EventSource' im Log '$EventLogName'"
|
||||||
|
& $registerScript -Source $EventSource -LogName $EventLogName
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info 'Installation des BizTalkMonitorConfigGenerator abgeschlossen.'
|
||||||
|
Write-Info 'Prüfen Sie nun generator-settings.json und führen Sie einen Testlauf aus.'
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SourcePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$InstallPath,
|
||||||
|
|
||||||
|
[string]$EventSource = "HostAvailabilityMonitor",
|
||||||
|
[string]$EventLogName = "Application",
|
||||||
|
[switch]$RegisterEventSource,
|
||||||
|
[switch]$CreateBackup
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Write-Info {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[INFO] $Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $SourcePath)) {
|
||||||
|
throw "SourcePath '$SourcePath' existiert nicht."
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceFullPath = (Resolve-Path -LiteralPath $SourcePath).Path
|
||||||
|
$installFullPath = [System.IO.Path]::GetFullPath($InstallPath)
|
||||||
|
|
||||||
|
Write-Info "Quelldateien: $sourceFullPath"
|
||||||
|
Write-Info "Zielverzeichnis: $installFullPath"
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $installFullPath) {
|
||||||
|
if ($CreateBackup) {
|
||||||
|
$backupRoot = Join-Path -Path ([System.IO.Path]::GetDirectoryName($installFullPath)) -ChildPath ((Split-Path -Path $installFullPath -Leaf) + '_backup_' + (Get-Date -Format 'yyyyMMdd_HHmmss'))
|
||||||
|
Write-Info "Erstelle Backup unter $backupRoot"
|
||||||
|
Copy-Item -Path (Join-Path $installFullPath '*') -Destination $backupRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
New-Item -Path $installFullPath -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Kopiere Dateien..."
|
||||||
|
Copy-Item -Path (Join-Path $sourceFullPath '*') -Destination $installFullPath -Recurse -Force
|
||||||
|
|
||||||
|
New-Item -Path (Join-Path $installFullPath 'logs') -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item -Path (Join-Path $installFullPath 'state') -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
|
if ($RegisterEventSource) {
|
||||||
|
$registerScript = Join-Path -Path $PSScriptRoot -ChildPath 'register-event-source.ps1'
|
||||||
|
if (-not (Test-Path -LiteralPath $registerScript)) {
|
||||||
|
throw "register-event-source.ps1 wurde nicht gefunden: $registerScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Registriere Event Source '$EventSource' im Log '$EventLogName'"
|
||||||
|
& $registerScript -Source $EventSource -LogName $EventLogName
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info 'Installation abgeschlossen.'
|
||||||
|
Write-Info "Prüfen Sie nun appsettings.json, Task Scheduler und Servicekonto-Berechtigungen."
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
param(
|
||||||
|
[string]$PublishRoot = ".\artifacts\publish\net472",
|
||||||
|
[string]$OutputRoot = ".\artifacts\deploy\net472",
|
||||||
|
[string]$PackageName = "biztalk-endpoint-monitor-net472-deployment"
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Ensure-Path {
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Pfad nicht gefunden: $Path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$publishRootFull = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $PublishRoot))
|
||||||
|
$outputRootFull = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputRoot))
|
||||||
|
$stagingRoot = Join-Path $outputRootFull $PackageName
|
||||||
|
$zipPath = Join-Path $outputRootFull ($PackageName + '.zip')
|
||||||
|
|
||||||
|
Ensure-Path -Path $publishRootFull
|
||||||
|
Ensure-Path -Path (Join-Path $publishRootFull 'HostAvailabilityMonitor')
|
||||||
|
Ensure-Path -Path (Join-Path $publishRootFull 'BizTalkMonitorConfigGenerator')
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $stagingRoot) {
|
||||||
|
Remove-Item -LiteralPath $stagingRoot -Recurse -Force
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $zipPath) {
|
||||||
|
Remove-Item -LiteralPath $zipPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Path $stagingRoot -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item -Path (Join-Path $stagingRoot 'monitor') -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item -Path (Join-Path $stagingRoot 'generator') -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item -Path (Join-Path $stagingRoot 'scripts') -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item -Path (Join-Path $stagingRoot 'installers') -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item -Path (Join-Path $stagingRoot 'docs') -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
|
Copy-Item -Path (Join-Path $publishRootFull 'HostAvailabilityMonitor\*') -Destination (Join-Path $stagingRoot 'monitor') -Recurse -Force
|
||||||
|
Copy-Item -Path (Join-Path $publishRootFull 'BizTalkMonitorConfigGenerator\*') -Destination (Join-Path $stagingRoot 'generator') -Recurse -Force
|
||||||
|
Copy-Item -Path (Join-Path $repoRoot 'scripts\*') -Destination (Join-Path $stagingRoot 'scripts') -Recurse -Force
|
||||||
|
Copy-Item -Path (Join-Path $repoRoot 'installers\*') -Destination (Join-Path $stagingRoot 'installers') -Recurse -Force
|
||||||
|
Copy-Item -Path (Join-Path $repoRoot 'README.md') -Destination (Join-Path $stagingRoot 'docs\README.md') -Force
|
||||||
|
Copy-Item -Path (Join-Path $repoRoot 'Dokumentation.md') -Destination (Join-Path $stagingRoot 'docs\Dokumentation.md') -Force
|
||||||
|
Copy-Item -Path (Join-Path $repoRoot 'Documentation.en.md') -Destination (Join-Path $stagingRoot 'docs\Documentation.en.md') -Force
|
||||||
|
|
||||||
|
$deployNotes = @"
|
||||||
|
Deployment package: $PackageName
|
||||||
|
Created: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
|
||||||
|
|
||||||
|
Typical installation order:
|
||||||
|
1. Install generator
|
||||||
|
2. Adjust generator-settings.json
|
||||||
|
3. Generate monitor JSON
|
||||||
|
4. Install monitor
|
||||||
|
5. Configure Task Scheduler
|
||||||
|
|
||||||
|
PowerShell installers:
|
||||||
|
- installers\powershell\Install-HostAvailabilityMonitor.ps1
|
||||||
|
- installers\powershell\Install-BizTalkMonitorConfigGenerator.ps1
|
||||||
|
- installers\powershell\Install-Both.ps1
|
||||||
|
|
||||||
|
Optional MSI sources:
|
||||||
|
- installers\wix\HostAvailabilityMonitor.wxs
|
||||||
|
- installers\wix\BizTalkMonitorConfigGenerator.wxs
|
||||||
|
- installers\wix\build-msi.ps1
|
||||||
|
"@
|
||||||
|
$deployNotes | Set-Content -Path (Join-Path $stagingRoot 'deploy-notes.txt') -Encoding UTF8
|
||||||
|
|
||||||
|
Compress-Archive -Path (Join-Path $stagingRoot '*') -DestinationPath $zipPath -CompressionLevel Optimal
|
||||||
|
Write-Host "Deployment package created: $zipPath"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<configuration>
|
||||||
|
<startup useLegacyV2RuntimeActivationPolicy="true">
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BizTalkMonitorConfigGenerator.Configuration;
|
||||||
|
using BizTalkMonitorConfigGenerator.Logging;
|
||||||
|
using Microsoft.BizTalk.ExplorerOM;
|
||||||
|
|
||||||
|
namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||||
|
{
|
||||||
|
public sealed class BizTalkCatalogDiscoveryService
|
||||||
|
{
|
||||||
|
private readonly BizTalkOptions _options;
|
||||||
|
private readonly OutputOptions _outputOptions;
|
||||||
|
private readonly GeneratorFileLogger _logger;
|
||||||
|
|
||||||
|
public BizTalkCatalogDiscoveryService(BizTalkOptions options, OutputOptions outputOptions, GeneratorFileLogger logger)
|
||||||
|
{
|
||||||
|
_options = options ?? new BizTalkOptions();
|
||||||
|
_outputOptions = outputOptions ?? new OutputOptions();
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<DiscoveredEndpoint> Discover()
|
||||||
|
{
|
||||||
|
var discovered = new List<DiscoveredEndpoint>();
|
||||||
|
|
||||||
|
var catalog = new BtsCatalogExplorer();
|
||||||
|
catalog.ConnectionString = _options.ConnectionString;
|
||||||
|
|
||||||
|
foreach (Application application in catalog.Applications)
|
||||||
|
{
|
||||||
|
if (application == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldSkipApplication(application))
|
||||||
|
{
|
||||||
|
_logger.Info("BizTalk-Anwendung übersprungen: " + application.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.IncludeSendPorts)
|
||||||
|
{
|
||||||
|
DiscoverSendPorts(application, discovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.IncludeReceiveLocations)
|
||||||
|
{
|
||||||
|
DiscoverReceiveLocations(application, discovered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return discovered
|
||||||
|
.OrderBy(x => x.MappedApplicationName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(x => x.TargetName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(x => x.NormalizedEndpoint, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiscoverSendPorts(Application application, List<DiscoveredEndpoint> discovered)
|
||||||
|
{
|
||||||
|
foreach (SendPort sendPort in application.SendPorts)
|
||||||
|
{
|
||||||
|
if (sendPort == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.OnlyStartedSendPorts && sendPort.Status != PortStatus.Started)
|
||||||
|
{
|
||||||
|
_logger.Discovery("SKIP", "SendPort", application.Name, sendPort.Name, ResolveAdapterName(sendPort.PrimaryTransport), SafeAddress(sendPort.PrimaryTransport), string.Empty, "SendPort ist nicht gestartet. Status=" + sendPort.Status);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendPort.IsDynamic && !_options.IncludeDynamicSendPorts)
|
||||||
|
{
|
||||||
|
_logger.Discovery("SKIP", "SendPort", application.Name, sendPort.Name, ResolveAdapterName(sendPort.PrimaryTransport), SafeAddress(sendPort.PrimaryTransport), string.Empty, "Dynamischer SendPort wird per Konfiguration übersprungen.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTransportIfSupported(discovered, application.Name, "SendPort", sendPort.Name, sendPort.Name, sendPort.PrimaryTransport, false);
|
||||||
|
|
||||||
|
if (_options.IncludeSecondaryTransportOnSendPorts)
|
||||||
|
{
|
||||||
|
AddTransportIfSupported(discovered, application.Name, "SendPortSecondary", sendPort.Name + " (Secondary Transport)", sendPort.Name, sendPort.SecondaryTransport, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiscoverReceiveLocations(Application application, List<DiscoveredEndpoint> discovered)
|
||||||
|
{
|
||||||
|
foreach (ReceivePort receivePort in application.ReceivePorts)
|
||||||
|
{
|
||||||
|
if (receivePort == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ReceiveLocation receiveLocation in receivePort.ReceiveLocations)
|
||||||
|
{
|
||||||
|
if (receiveLocation == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.OnlyEnabledReceiveLocations && !receiveLocation.Enable)
|
||||||
|
{
|
||||||
|
_logger.Discovery("SKIP", "ReceiveLocation", application.Name, receiveLocation.Name, ResolveAdapterName(receiveLocation.TransportType), receiveLocation.Address, string.Empty, "Receive Location ist deaktiviert.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldSkipArtifact(receiveLocation.Name))
|
||||||
|
{
|
||||||
|
_logger.Discovery("SKIP", "ReceiveLocation", application.Name, receiveLocation.Name, ResolveAdapterName(receiveLocation.TransportType), receiveLocation.Address, string.Empty, "Receive Location entspricht einem Ignore-Pattern.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddEndpointIfSupported(
|
||||||
|
discovered,
|
||||||
|
application.Name,
|
||||||
|
"ReceiveLocation",
|
||||||
|
receivePort.Name + " / " + receiveLocation.Name,
|
||||||
|
receiveLocation.Name,
|
||||||
|
ResolveAdapterName(receiveLocation.TransportType),
|
||||||
|
receiveLocation.Address,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTransportIfSupported(List<DiscoveredEndpoint> discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, TransportInfo transport, bool isSecondary)
|
||||||
|
{
|
||||||
|
if (transport == null)
|
||||||
|
{
|
||||||
|
_logger.Discovery("SKIP", artifactType, bizTalkApplicationName, artifactName, string.Empty, string.Empty, string.Empty, "Kein Transport konfiguriert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddEndpointIfSupported(
|
||||||
|
discovered,
|
||||||
|
bizTalkApplicationName,
|
||||||
|
artifactType,
|
||||||
|
targetNameBase + (isSecondary ? " (Secondary)" : " (Primary)"),
|
||||||
|
artifactName,
|
||||||
|
ResolveAdapterName(transport),
|
||||||
|
SafeAddress(transport),
|
||||||
|
isSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddEndpointIfSupported(List<DiscoveredEndpoint> discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, string adapterName, string sourceAddress, bool isSecondary)
|
||||||
|
{
|
||||||
|
if (ShouldSkipArtifact(artifactName))
|
||||||
|
{
|
||||||
|
_logger.Discovery("SKIP", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, string.Empty, "Artefakt entspricht einem Ignore-Pattern.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalizedEndpoint;
|
||||||
|
int? port;
|
||||||
|
string httpMethod;
|
||||||
|
string reason;
|
||||||
|
if (!TryNormalizeEndpoint(sourceAddress, adapterName, out normalizedEndpoint, out port, out httpMethod, out reason))
|
||||||
|
{
|
||||||
|
_logger.Discovery("SKIP", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, string.Empty, reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedApplication = ResolveMappedApplicationName(bizTalkApplicationName, artifactType, artifactName);
|
||||||
|
var targetName = BuildTargetName(artifactType, targetNameBase, adapterName, isSecondary);
|
||||||
|
var endpoint = new DiscoveredEndpoint
|
||||||
|
{
|
||||||
|
ArtifactType = artifactType,
|
||||||
|
BizTalkApplicationName = bizTalkApplicationName,
|
||||||
|
MappedApplicationName = mappedApplication,
|
||||||
|
ArtifactName = artifactName,
|
||||||
|
AdapterName = adapterName,
|
||||||
|
SourceAddress = sourceAddress,
|
||||||
|
NormalizedEndpoint = normalizedEndpoint,
|
||||||
|
TargetName = targetName,
|
||||||
|
HttpMethod = httpMethod,
|
||||||
|
Port = port,
|
||||||
|
ValidateUncPathAccess = null,
|
||||||
|
Detail = "Endpoint erfolgreich aus BizTalk übernommen."
|
||||||
|
};
|
||||||
|
|
||||||
|
discovered.Add(endpoint);
|
||||||
|
_logger.Discovery("ADD", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, normalizedEndpoint, "Zuordnung zu ApplicationName='" + mappedApplication + "'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldSkipApplication(Application application)
|
||||||
|
{
|
||||||
|
if (application == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.IgnoreSystemApplications && application.IsSystem)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _options.IgnoreApplications.Any(pattern => WildcardMatch(application.Name, pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldSkipArtifact(string artifactName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(artifactName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _options.IgnoreArtifactNamePatterns.Any(pattern => WildcardMatch(artifactName, pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveMappedApplicationName(string bizTalkApplicationName, string artifactType, string artifactName)
|
||||||
|
{
|
||||||
|
foreach (var rule in _options.ArtifactMappings.Where(r => r != null && !string.IsNullOrWhiteSpace(r.ApplicationName)))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(rule.ArtifactType) && !string.Equals(rule.ArtifactType.Trim(), artifactType, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WildcardMatch(artifactName, rule.ArtifactNamePattern))
|
||||||
|
{
|
||||||
|
return rule.ApplicationName.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var rule in _options.ApplicationMappings.Where(r => r != null && !string.IsNullOrWhiteSpace(r.ApplicationName)))
|
||||||
|
{
|
||||||
|
if (WildcardMatch(bizTalkApplicationName, rule.BizTalkApplicationName))
|
||||||
|
{
|
||||||
|
return rule.ApplicationName.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(bizTalkApplicationName) ? "Unassigned Application" : bizTalkApplicationName.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildTargetName(string artifactType, string baseName, string adapterName, bool isSecondary)
|
||||||
|
{
|
||||||
|
var prefix = string.Equals(artifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "ReceiveLocation"
|
||||||
|
: "SendPort";
|
||||||
|
|
||||||
|
var name = prefix + ": " + baseName;
|
||||||
|
if (!string.IsNullOrWhiteSpace(adapterName))
|
||||||
|
{
|
||||||
|
name += " [" + adapterName + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSecondary && name.IndexOf("Secondary", StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
{
|
||||||
|
name += " [Secondary]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryNormalizeEndpoint(string sourceAddress, string adapterName, out string normalizedEndpoint, out int? port, out string httpMethod, out string reason)
|
||||||
|
{
|
||||||
|
normalizedEndpoint = string.Empty;
|
||||||
|
port = null;
|
||||||
|
httpMethod = null;
|
||||||
|
reason = string.Empty;
|
||||||
|
|
||||||
|
var address = (sourceAddress ?? string.Empty).Trim();
|
||||||
|
var adapter = (adapterName ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(address))
|
||||||
|
{
|
||||||
|
reason = "Address/URI ist leer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdapterContains(adapter, "SMTP"))
|
||||||
|
{
|
||||||
|
reason = "SMTP-Adapter wird uebersprungen, weil dessen Address-Wert keine pruefbare Netzwerkadresse, sondern eine Empfaengerliste ist.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LooksLikeEmailAddressList(address))
|
||||||
|
{
|
||||||
|
reason = "Adresse sieht wie eine SMTP-/E-Mail-Empfaengerliste aus und wird uebersprungen.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.StartsWith("\\\\", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
normalizedEndpoint = address;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Path.IsPathRooted(address))
|
||||||
|
{
|
||||||
|
if (!_options.IncludeLocalFilePaths)
|
||||||
|
{
|
||||||
|
reason = "Lokaler Dateipfad wird per Konfiguration nicht übernommen.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedEndpoint = address;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri;
|
||||||
|
if (Uri.TryCreate(address, UriKind.Absolute, out uri))
|
||||||
|
{
|
||||||
|
switch ((uri.Scheme ?? string.Empty).ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "http":
|
||||||
|
case "https":
|
||||||
|
normalizedEndpoint = uri.AbsoluteUri;
|
||||||
|
httpMethod = _outputOptions.DefaultHttpMethod;
|
||||||
|
return true;
|
||||||
|
case "ftp":
|
||||||
|
case "sftp":
|
||||||
|
case "tcp":
|
||||||
|
case "icmp":
|
||||||
|
case "file":
|
||||||
|
normalizedEndpoint = uri.IsAbsoluteUri ? uri.AbsoluteUri : address;
|
||||||
|
return true;
|
||||||
|
case "net.tcp":
|
||||||
|
if (string.IsNullOrWhiteSpace(uri.Host) || uri.Port <= 0)
|
||||||
|
{
|
||||||
|
reason = "net.tcp-Adresse konnte nicht auf Host/Port aufgelöst werden.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedEndpoint = "tcp://" + uri.Host + ":" + uri.Port + uri.PathAndQuery;
|
||||||
|
port = uri.Port;
|
||||||
|
return true;
|
||||||
|
case "net.pipe":
|
||||||
|
case "npipe":
|
||||||
|
reason = "Named-Pipe-Endpunkte werden vom Monitor nicht unterstützt.";
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
reason = "Schema '" + uri.Scheme + "' wird vom Monitor nicht unterstützt.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LooksLikeHostPort(address))
|
||||||
|
{
|
||||||
|
if (AdapterContains(adapter, "SFTP"))
|
||||||
|
{
|
||||||
|
normalizedEndpoint = "sftp://" + address;
|
||||||
|
port = ParsePort(address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdapterContains(adapter, "FTP"))
|
||||||
|
{
|
||||||
|
normalizedEndpoint = "ftp://" + address;
|
||||||
|
port = ParsePort(address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedEndpoint = "tcp://" + address;
|
||||||
|
port = ParsePort(address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdapterContains(adapter, "SFTP"))
|
||||||
|
{
|
||||||
|
normalizedEndpoint = "sftp://" + address.TrimStart('/');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdapterContains(adapter, "FTP"))
|
||||||
|
{
|
||||||
|
normalizedEndpoint = "ftp://" + address.TrimStart('/');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdapterContains(adapter, "HTTP") || AdapterContains(adapter, "SOAP") || AdapterContains(adapter, "WCF"))
|
||||||
|
{
|
||||||
|
reason = "HTTP/WCF-Adapteradresse konnte nicht als absolute URI erkannt werden.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedEndpoint = address;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveAdapterName(TransportInfo transport)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return transport == null || transport.TransportType == null ? string.Empty : transport.TransportType.Name;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveAdapterName(ProtocolType transportType)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return transportType == null ? string.Empty : transportType.Name;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SafeAddress(TransportInfo transport)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return transport == null ? string.Empty : transport.Address;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AdapterContains(string adapterName, string fragment)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(adapterName)
|
||||||
|
&& adapterName.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeEmailAddressList(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = value.Trim();
|
||||||
|
if (trimmed.IndexOf('@') < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.IndexOf("http://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("https://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("sftp://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("ftp://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("tcp://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("file://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.StartsWith("\\", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = trimmed.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (entries.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var part = entry.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(part))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.IndexOf('@') <= 0 || part.IndexOf(' ') >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WildcardMatch(string input, string pattern)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(pattern))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
input = input ?? string.Empty;
|
||||||
|
var regex = "^" + Regex.Escape(pattern.Trim()).Replace("\\*", ".*").Replace("\\?", ".") + "$";
|
||||||
|
return Regex.IsMatch(input, regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeHostPort(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Regex.IsMatch(value.Trim(), @"^[a-zA-Z0-9._-]+:\d{1,5}($|/.*$)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? ParsePort(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = Regex.Match(value, @":(?<port>\d{1,5})(?:$|/)");
|
||||||
|
int parsed;
|
||||||
|
if (match.Success && int.TryParse(match.Groups["port"].Value, out parsed))
|
||||||
|
{
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace BizTalkMonitorConfigGenerator.BizTalk
|
||||||
|
{
|
||||||
|
public sealed class DiscoveredEndpoint
|
||||||
|
{
|
||||||
|
public string ArtifactType { get; set; }
|
||||||
|
public string BizTalkApplicationName { get; set; }
|
||||||
|
public string MappedApplicationName { get; set; }
|
||||||
|
public string ArtifactName { get; set; }
|
||||||
|
public string AdapterName { get; set; }
|
||||||
|
public string SourceAddress { get; set; }
|
||||||
|
public string NormalizedEndpoint { get; set; }
|
||||||
|
public string TargetName { get; set; }
|
||||||
|
public string HttpMethod { get; set; }
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public bool? ValidateUncPathAccess { get; set; }
|
||||||
|
public string Detail { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{E8C93E45-DA2B-4E30-8891-637A2C7FAE0B}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>BizTalkMonitorConfigGenerator</RootNamespace>
|
||||||
|
<AssemblyName>BizTalkMonitorConfigGenerator</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.BizTalk.ExplorerOM" />
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Configuration" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Web.Extensions" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="BizTalk\BizTalkCatalogDiscoveryService.cs" />
|
||||||
|
<Compile Include="BizTalk\DiscoveredEndpoint.cs" />
|
||||||
|
<Compile Include="Configuration\GeneratorConfiguration.cs" />
|
||||||
|
<Compile Include="Logging\GeneratorFileLogger.cs" />
|
||||||
|
<Compile Include="Logging\GeneratorEventLogger.cs" />
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Serialization\JsonFileWriter.cs" />
|
||||||
|
<Compile Include="..\HostAvailabilityMonitor\Configuration\MonitorConfiguration.cs">
|
||||||
|
<Link>Shared\MonitorConfiguration.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config" />
|
||||||
|
<Content Include="generator-settings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
|
using HostAvailabilityMonitor.Configuration;
|
||||||
|
|
||||||
|
namespace BizTalkMonitorConfigGenerator.Configuration
|
||||||
|
{
|
||||||
|
public sealed class GeneratorConfiguration
|
||||||
|
{
|
||||||
|
public string GeneratorName { get; set; } = "BizTalkMonitorConfigGenerator";
|
||||||
|
public string EnvironmentName { get; set; } = "Unspecified Environment";
|
||||||
|
public GeneratorLoggingOptions Logging { get; set; } = new GeneratorLoggingOptions();
|
||||||
|
public GeneratorEventLogOptions EventLog { get; set; } = new GeneratorEventLogOptions();
|
||||||
|
public BizTalkOptions BizTalk { get; set; } = new BizTalkOptions();
|
||||||
|
public OutputOptions Output { get; set; } = new OutputOptions();
|
||||||
|
public MonitorConfiguration GeneratedMonitorConfig { get; set; } = new MonitorConfiguration();
|
||||||
|
|
||||||
|
public static GeneratorConfiguration Load(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Konfigurationsdatei wurde nicht gefunden: " + path, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
var serializer = new JavaScriptSerializer();
|
||||||
|
var config = serializer.Deserialize<GeneratorConfiguration>(json);
|
||||||
|
if (config == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Generator-Konfiguration konnte nicht deserialisiert werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Logging = config.Logging ?? new GeneratorLoggingOptions();
|
||||||
|
config.EventLog = config.EventLog ?? new GeneratorEventLogOptions();
|
||||||
|
config.BizTalk = config.BizTalk ?? new BizTalkOptions();
|
||||||
|
config.Output = config.Output ?? new OutputOptions();
|
||||||
|
config.GeneratedMonitorConfig = config.GeneratedMonitorConfig ?? new MonitorConfiguration();
|
||||||
|
config.GeneratedMonitorConfig.Runtime = config.GeneratedMonitorConfig.Runtime ?? new RuntimeOptions();
|
||||||
|
config.GeneratedMonitorConfig.Logging = config.GeneratedMonitorConfig.Logging ?? new LoggingOptions();
|
||||||
|
config.GeneratedMonitorConfig.EventLog = config.GeneratedMonitorConfig.EventLog ?? new EventLogOptions();
|
||||||
|
config.GeneratedMonitorConfig.Email = config.GeneratedMonitorConfig.Email ?? new EmailOptions();
|
||||||
|
config.GeneratedMonitorConfig.Email.To = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.To);
|
||||||
|
config.GeneratedMonitorConfig.Email.Cc = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.Cc);
|
||||||
|
config.GeneratedMonitorConfig.Email.Bcc = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.Bcc);
|
||||||
|
config.GeneratedMonitorConfig.Targets = config.GeneratedMonitorConfig.Targets ?? new List<TargetOptions>();
|
||||||
|
config.BizTalk.IgnoreApplications = config.BizTalk.IgnoreApplications ?? new List<string>();
|
||||||
|
config.BizTalk.IgnoreArtifactNamePatterns = config.BizTalk.IgnoreArtifactNamePatterns ?? new List<string>();
|
||||||
|
config.BizTalk.ApplicationMappings = config.BizTalk.ApplicationMappings ?? new List<ApplicationMappingRule>();
|
||||||
|
config.BizTalk.ArtifactMappings = config.BizTalk.ArtifactMappings ?? new List<ArtifactMappingRule>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(config.GeneratorName))
|
||||||
|
{
|
||||||
|
config.GeneratorName = "BizTalkMonitorConfigGenerator";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(config.EnvironmentName))
|
||||||
|
{
|
||||||
|
config.EnvironmentName = "Unspecified Environment";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(config.GeneratedMonitorConfig.ApplicationName))
|
||||||
|
{
|
||||||
|
config.GeneratedMonitorConfig.ApplicationName = "HostAvailabilityMonitor";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(config.GeneratedMonitorConfig.EnvironmentName))
|
||||||
|
{
|
||||||
|
config.GeneratedMonitorConfig.EnvironmentName = config.EnvironmentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(BizTalk.ConnectionString))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("BizTalk.ConnectionString ist leer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Output.Path))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Output.Path ist leer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BizTalk.IncludeSendPorts && !BizTalk.IncludeReceiveLocations)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Mindestens eine Quelle muss aktiviert sein: IncludeSendPorts oder IncludeReceiveLocations.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Logging.RetentionDays < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Logging.RetentionDays darf nicht negativ sein.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Output.TargetTimeoutSeconds <= 0)
|
||||||
|
{
|
||||||
|
Output.TargetTimeoutSeconds = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Output.DefaultHttpMethod))
|
||||||
|
{
|
||||||
|
Output.DefaultHttpMethod = "HEAD";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GeneratedMonitorConfig.Targets == null)
|
||||||
|
{
|
||||||
|
GeneratedMonitorConfig.Targets = new List<TargetOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratedMonitorConfig.ValidateTemplateWithoutTargets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class RecipientListHelper
|
||||||
|
{
|
||||||
|
internal static List<string> SanitizeRecipientList(List<string> values)
|
||||||
|
{
|
||||||
|
if (values == null)
|
||||||
|
{
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Select(x => x.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int GetConfiguredRecipientCount(EmailOptions email)
|
||||||
|
{
|
||||||
|
if (email == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SanitizeRecipientList(email.To).Count
|
||||||
|
+ SanitizeRecipientList(email.Cc).Count
|
||||||
|
+ SanitizeRecipientList(email.Bcc).Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GeneratorLoggingOptions
|
||||||
|
{
|
||||||
|
public string LogDirectory { get; set; } = "logs";
|
||||||
|
public string FilePrefix { get; set; } = "biztalk-monitor-config-generator";
|
||||||
|
public int RetentionDays { get; set; } = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GeneratorEventLogOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public string LogName { get; set; } = "Application";
|
||||||
|
public string Source { get; set; } = "BizTalkMonitorConfigGenerator";
|
||||||
|
public bool CreateSourceIfMissing { get; set; } = false;
|
||||||
|
public bool WriteSuccessEntries { get; set; } = false;
|
||||||
|
public bool WriteSummaryEntries { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BizTalkOptions
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; } = "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI";
|
||||||
|
public bool IncludeSendPorts { get; set; } = true;
|
||||||
|
public bool IncludeReceiveLocations { get; set; } = true;
|
||||||
|
public bool OnlyStartedSendPorts { get; set; } = true;
|
||||||
|
public bool OnlyEnabledReceiveLocations { get; set; } = true;
|
||||||
|
public bool IncludeSecondaryTransportOnSendPorts { get; set; } = true;
|
||||||
|
public bool IncludeDynamicSendPorts { get; set; } = false;
|
||||||
|
public bool IncludeLocalFilePaths { get; set; } = false;
|
||||||
|
public bool IgnoreSystemApplications { get; set; } = true;
|
||||||
|
public List<string> IgnoreApplications { get; set; } = new List<string>();
|
||||||
|
public List<string> IgnoreArtifactNamePatterns { get; set; } = new List<string>();
|
||||||
|
public List<ApplicationMappingRule> ApplicationMappings { get; set; } = new List<ApplicationMappingRule>();
|
||||||
|
public List<ArtifactMappingRule> ArtifactMappings { get; set; } = new List<ArtifactMappingRule>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class OutputOptions
|
||||||
|
{
|
||||||
|
public string Path { get; set; } = "generated-monitor-appsettings.json";
|
||||||
|
public bool OverwriteExisting { get; set; } = true;
|
||||||
|
public bool FailIfNoTargetsFound { get; set; } = false;
|
||||||
|
public int TargetTimeoutSeconds { get; set; } = 10;
|
||||||
|
public bool TargetEnabled { get; set; } = true;
|
||||||
|
public string DefaultHttpMethod { get; set; } = "HEAD";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ApplicationMappingRule
|
||||||
|
{
|
||||||
|
public string BizTalkApplicationName { get; set; } = string.Empty;
|
||||||
|
public string ApplicationName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ArtifactMappingRule
|
||||||
|
{
|
||||||
|
public string ArtifactType { get; set; } = string.Empty;
|
||||||
|
public string ArtifactNamePattern { get; set; } = string.Empty;
|
||||||
|
public string ApplicationName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MonitorConfigurationTemplateExtensions
|
||||||
|
{
|
||||||
|
public static void ValidateTemplateWithoutTargets(this MonitorConfiguration configuration)
|
||||||
|
{
|
||||||
|
if (configuration == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("GeneratedMonitorConfig ist null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration.Runtime = configuration.Runtime ?? new RuntimeOptions();
|
||||||
|
configuration.Logging = configuration.Logging ?? new LoggingOptions();
|
||||||
|
configuration.EventLog = configuration.EventLog ?? new EventLogOptions();
|
||||||
|
configuration.Email = configuration.Email ?? new EmailOptions();
|
||||||
|
configuration.Targets = configuration.Targets ?? new List<TargetOptions>();
|
||||||
|
|
||||||
|
if (configuration.Runtime.MaxConcurrency <= 0)
|
||||||
|
{
|
||||||
|
configuration.Runtime.MaxConcurrency = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.Logging.RetentionDays < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("GeneratedMonitorConfig.Logging.RetentionDays darf nicht negativ sein.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.Email.Enabled)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(configuration.Email.SmtpHost))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("GeneratedMonitorConfig.Email.Enabled=true, aber SmtpHost ist leer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(configuration.Email.From))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("GeneratedMonitorConfig.Email.Enabled=true, aber From ist leer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RecipientListHelper.GetConfiguredRecipientCount(configuration.Email) <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("GeneratedMonitorConfig.Email.Enabled=true, aber es ist kein Empfänger in Email.To, Email.Cc oder Email.Bcc definiert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.Email.DailySummaryHourLocal < 0 || configuration.Email.DailySummaryHourLocal > 23)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("GeneratedMonitorConfig.Email.DailySummaryHourLocal muss zwischen 0 und 23 liegen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.Email.DailySummaryMinuteLocal < 0 || configuration.Email.DailySummaryMinuteLocal > 59)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("GeneratedMonitorConfig.Email.DailySummaryMinuteLocal muss zwischen 0 und 59 liegen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MonitorConfiguration CloneWithoutTargets(this MonitorConfiguration configuration)
|
||||||
|
{
|
||||||
|
var clone = new MonitorConfiguration
|
||||||
|
{
|
||||||
|
ApplicationName = configuration.ApplicationName,
|
||||||
|
EnvironmentName = configuration.EnvironmentName,
|
||||||
|
Runtime = new RuntimeOptions
|
||||||
|
{
|
||||||
|
MaxConcurrency = configuration.Runtime.MaxConcurrency,
|
||||||
|
DefaultValidateUncPathAccess = configuration.Runtime.DefaultValidateUncPathAccess,
|
||||||
|
NonZeroExitCodeOnAnyFailure = configuration.Runtime.NonZeroExitCodeOnAnyFailure,
|
||||||
|
NonZeroExitCodeOnExecutionError = configuration.Runtime.NonZeroExitCodeOnExecutionError,
|
||||||
|
StateDirectory = configuration.Runtime.StateDirectory
|
||||||
|
},
|
||||||
|
Logging = new LoggingOptions
|
||||||
|
{
|
||||||
|
LogDirectory = configuration.Logging.LogDirectory,
|
||||||
|
FilePrefix = configuration.Logging.FilePrefix,
|
||||||
|
RetentionDays = configuration.Logging.RetentionDays
|
||||||
|
},
|
||||||
|
EventLog = new EventLogOptions
|
||||||
|
{
|
||||||
|
Enabled = configuration.EventLog.Enabled,
|
||||||
|
LogName = configuration.EventLog.LogName,
|
||||||
|
Source = configuration.EventLog.Source,
|
||||||
|
CreateSourceIfMissing = configuration.EventLog.CreateSourceIfMissing,
|
||||||
|
WriteSuccessEntries = configuration.EventLog.WriteSuccessEntries,
|
||||||
|
WriteSummaryEntries = configuration.EventLog.WriteSummaryEntries
|
||||||
|
},
|
||||||
|
Email = new EmailOptions
|
||||||
|
{
|
||||||
|
Enabled = configuration.Email.Enabled,
|
||||||
|
SmtpHost = configuration.Email.SmtpHost,
|
||||||
|
SmtpPort = configuration.Email.SmtpPort,
|
||||||
|
UseSsl = configuration.Email.UseSsl,
|
||||||
|
UseDefaultCredentials = configuration.Email.UseDefaultCredentials,
|
||||||
|
Username = configuration.Email.Username,
|
||||||
|
Password = configuration.Email.Password,
|
||||||
|
From = configuration.Email.From,
|
||||||
|
To = configuration.Email.To == null ? new List<string>() : configuration.Email.To.ToList(),
|
||||||
|
Cc = configuration.Email.Cc == null ? new List<string>() : configuration.Email.Cc.ToList(),
|
||||||
|
Bcc = configuration.Email.Bcc == null ? new List<string>() : configuration.Email.Bcc.ToList(),
|
||||||
|
SubjectPrefix = configuration.Email.SubjectPrefix,
|
||||||
|
SendOnFailure = configuration.Email.SendOnFailure,
|
||||||
|
SendOnRecovery = configuration.Email.SendOnRecovery,
|
||||||
|
SendDailySummary = configuration.Email.SendDailySummary,
|
||||||
|
DailySummaryHourLocal = configuration.Email.DailySummaryHourLocal,
|
||||||
|
DailySummaryMinuteLocal = configuration.Email.DailySummaryMinuteLocal,
|
||||||
|
OnlyOnStateChange = configuration.Email.OnlyOnStateChange,
|
||||||
|
CooldownMinutes = configuration.Email.CooldownMinutes,
|
||||||
|
TimeoutSeconds = configuration.Email.TimeoutSeconds
|
||||||
|
},
|
||||||
|
Targets = new List<TargetOptions>()
|
||||||
|
};
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security;
|
||||||
|
using BizTalkMonitorConfigGenerator.Configuration;
|
||||||
|
|
||||||
|
namespace BizTalkMonitorConfigGenerator.Logging
|
||||||
|
{
|
||||||
|
public sealed class GeneratorEventLogger
|
||||||
|
{
|
||||||
|
private readonly GeneratorEventLogOptions _options;
|
||||||
|
private readonly GeneratorFileLogger _fallbackLogger;
|
||||||
|
private bool _disabled;
|
||||||
|
|
||||||
|
public GeneratorEventLogger(GeneratorEventLogOptions options, GeneratorFileLogger fallbackLogger)
|
||||||
|
{
|
||||||
|
_options = options ?? new GeneratorEventLogOptions();
|
||||||
|
_fallbackLogger = fallbackLogger;
|
||||||
|
_disabled = !_options.Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TryInitialize()
|
||||||
|
{
|
||||||
|
if (_disabled || !_options.CreateSourceIfMissing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!EventLog.SourceExists(_options.Source))
|
||||||
|
{
|
||||||
|
EventLog.CreateEventSource(new EventSourceCreationData(_options.Source, _options.LogName));
|
||||||
|
_fallbackLogger.Info("EventLog-Quelle '" + _options.Source + "' wurde neu angelegt.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (SecurityException ex)
|
||||||
|
{
|
||||||
|
_fallbackLogger.Warn("EventLog-Quelle konnte nicht geprüft/angelegt werden (fehlende Rechte). " + ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_fallbackLogger.Warn("EventLog-Initialisierung fehlgeschlagen. " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string message, int eventId)
|
||||||
|
{
|
||||||
|
Write(message, EventLogEntryType.Information, eventId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InfoIfSuccessEnabled(string message, int eventId)
|
||||||
|
{
|
||||||
|
if (_options.WriteSuccessEntries)
|
||||||
|
{
|
||||||
|
Write(message, EventLogEntryType.Information, eventId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warning(string message, int eventId)
|
||||||
|
{
|
||||||
|
Write(message, EventLogEntryType.Warning, eventId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string message, int eventId)
|
||||||
|
{
|
||||||
|
Write(message, EventLogEntryType.Error, eventId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Summary(string message, bool hasWarnings)
|
||||||
|
{
|
||||||
|
if (!_options.WriteSummaryEntries)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(message, hasWarnings ? EventLogEntryType.Warning : EventLogEntryType.Information, hasWarnings ? 2100 : 1100, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Write(string message, EventLogEntryType entryType, int eventId, bool forceWrite)
|
||||||
|
{
|
||||||
|
if (_disabled || !forceWrite)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EventLog.WriteEntry(_options.Source, message, entryType, eventId);
|
||||||
|
}
|
||||||
|
catch (SecurityException ex)
|
||||||
|
{
|
||||||
|
DisableAndFallback("EventLog-Schreiben fehlgeschlagen (SecurityException). " + ex.Message);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
DisableAndFallback("EventLog-Schreiben fehlgeschlagen. Vermutlich fehlt die Event Source '" + _options.Source + "'. " + ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
DisableAndFallback("EventLog-Schreiben fehlgeschlagen. " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisableAndFallback(string reason)
|
||||||
|
{
|
||||||
|
if (_disabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disabled = true;
|
||||||
|
_fallbackLogger.Warn(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace BizTalkMonitorConfigGenerator.Logging
|
||||||
|
{
|
||||||
|
public sealed class GeneratorFileLogger
|
||||||
|
{
|
||||||
|
private readonly object _sync = new object();
|
||||||
|
private readonly string _directory;
|
||||||
|
private readonly string _filePrefix;
|
||||||
|
private readonly int _retentionDays;
|
||||||
|
private readonly string _generatorName;
|
||||||
|
private readonly string _environmentName;
|
||||||
|
|
||||||
|
public GeneratorFileLogger(string directory, string filePrefix, int retentionDays, string generatorName, string environmentName)
|
||||||
|
{
|
||||||
|
_directory = directory;
|
||||||
|
_filePrefix = filePrefix;
|
||||||
|
_retentionDays = retentionDays;
|
||||||
|
_generatorName = string.IsNullOrWhiteSpace(generatorName) ? "BizTalkMonitorConfigGenerator" : generatorName.Trim();
|
||||||
|
_environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim();
|
||||||
|
Directory.CreateDirectory(_directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanupOldFiles()
|
||||||
|
{
|
||||||
|
if (_retentionDays <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cutoff = DateTime.Today.AddDays(-Math.Max(0, _retentionDays - 1));
|
||||||
|
foreach (var file in Directory.EnumerateFiles(_directory, _filePrefix + "-*.log", SearchOption.TopDirectoryOnly))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DateTime fileDate;
|
||||||
|
if (TryGetDateFromFileName(file, out fileDate))
|
||||||
|
{
|
||||||
|
if (fileDate < cutoff)
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (File.GetLastWriteTime(file).Date < cutoff)
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string message)
|
||||||
|
{
|
||||||
|
Write("INFO", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warn(string message)
|
||||||
|
{
|
||||||
|
Write("WARN", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string message)
|
||||||
|
{
|
||||||
|
Write("ERROR", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Discovery(string action, string artifactType, string applicationName, string artifactName, string adapterName, string sourceAddress, string normalizedEndpoint, string detail)
|
||||||
|
{
|
||||||
|
Write("DISCOVERY", string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Action={0} | ArtifactType={1} | BizTalkApplication={2} | ArtifactName={3} | Adapter={4} | SourceAddress={5} | NormalizedEndpoint={6} | Detail={7}",
|
||||||
|
Escape(action),
|
||||||
|
Escape(artifactType),
|
||||||
|
Escape(applicationName),
|
||||||
|
Escape(artifactName),
|
||||||
|
Escape(adapterName),
|
||||||
|
Escape(sourceAddress),
|
||||||
|
Escape(normalizedEndpoint),
|
||||||
|
Escape(detail)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Write(string level, string message)
|
||||||
|
{
|
||||||
|
var line = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"{0:yyyy-MM-dd HH:mm:ss.fff zzz} | {1} | Generator={2} | Environment={3} | {4}{5}",
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
level,
|
||||||
|
Escape(_generatorName),
|
||||||
|
Escape(_environmentName),
|
||||||
|
message,
|
||||||
|
Environment.NewLine);
|
||||||
|
|
||||||
|
var path = GetCurrentLogFilePath();
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
AppendWithRetries(path, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendWithRetries(string path, string line)
|
||||||
|
{
|
||||||
|
Exception lastException = null;
|
||||||
|
for (var attempt = 1; attempt <= 3; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.AppendAllText(path, line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
Thread.Sleep(100 * attempt);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
Thread.Sleep(100 * attempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastException != null)
|
||||||
|
{
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCurrentLogFilePath()
|
||||||
|
{
|
||||||
|
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
|
||||||
|
return Path.Combine(_directory, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetDateFromFileName(string path, out DateTime date)
|
||||||
|
{
|
||||||
|
date = DateTime.MinValue;
|
||||||
|
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
|
||||||
|
var prefix = _filePrefix + "-";
|
||||||
|
if (!fileNameWithoutExtension.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.TryParseExact(
|
||||||
|
fileNameWithoutExtension.Substring(prefix.Length),
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None,
|
||||||
|
out date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Escape(string value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using BizTalkMonitorConfigGenerator.BizTalk;
|
||||||
|
using BizTalkMonitorConfigGenerator.Configuration;
|
||||||
|
using BizTalkMonitorConfigGenerator.Logging;
|
||||||
|
using BizTalkMonitorConfigGenerator.Serialization;
|
||||||
|
using HostAvailabilityMonitor.Configuration;
|
||||||
|
|
||||||
|
namespace BizTalkMonitorConfigGenerator
|
||||||
|
{
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
GeneratorFileLogger logger = null;
|
||||||
|
GeneratorEventLogger eventLogger = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
|
||||||
|
? args[0]
|
||||||
|
: Path.Combine(baseDirectory, "generator-settings.json");
|
||||||
|
|
||||||
|
var configuration = GeneratorConfiguration.Load(configPath);
|
||||||
|
configuration.Validate();
|
||||||
|
|
||||||
|
var logDirectory = ResolvePath(baseDirectory, configuration.Logging.LogDirectory);
|
||||||
|
logger = new GeneratorFileLogger(logDirectory, configuration.Logging.FilePrefix, configuration.Logging.RetentionDays, configuration.GeneratorName, configuration.EnvironmentName);
|
||||||
|
logger.CleanupOldFiles();
|
||||||
|
logger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + ".");
|
||||||
|
logger.Info("Generator-Konfiguration geladen: " + Path.GetFullPath(configPath));
|
||||||
|
|
||||||
|
eventLogger = new GeneratorEventLogger(configuration.EventLog, logger);
|
||||||
|
eventLogger.TryInitialize();
|
||||||
|
eventLogger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000);
|
||||||
|
|
||||||
|
var discoveryService = new BizTalkCatalogDiscoveryService(configuration.BizTalk, configuration.Output, logger);
|
||||||
|
var discovered = discoveryService.Discover();
|
||||||
|
logger.Info("Discovery abgeschlossen. Gefundene unterstützte Targets=" + discovered.Count + ".");
|
||||||
|
|
||||||
|
var outputConfiguration = BuildMonitorConfiguration(configuration, discovered);
|
||||||
|
var outputPath = ResolvePath(baseDirectory, configuration.Output.Path);
|
||||||
|
JsonFileWriter.WritePrettyJsonAtomic(outputPath, outputConfiguration, configuration.Output.OverwriteExisting);
|
||||||
|
logger.Info("Monitor-Konfiguration geschrieben: " + outputPath);
|
||||||
|
eventLogger.InfoIfSuccessEnabled("Monitor-Konfiguration erfolgreich generiert: " + outputPath, 1001);
|
||||||
|
|
||||||
|
if (discovered.Count == 0)
|
||||||
|
{
|
||||||
|
var message = "Es wurden keine aktiven und unterstützten BizTalk-Endpunkte gefunden. Ausgabe wurde dennoch geschrieben: " + outputPath;
|
||||||
|
logger.Warn(message);
|
||||||
|
eventLogger.Summary(message, true);
|
||||||
|
return configuration.Output.FailIfNoTargetsFound ? 2 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = string.Format(
|
||||||
|
"Generatorlauf erfolgreich beendet. Umgebung={0}, GesamtTargets={1}, Ausgabedatei={2}",
|
||||||
|
configuration.EnvironmentName,
|
||||||
|
discovered.Count,
|
||||||
|
outputPath);
|
||||||
|
logger.Info(summary);
|
||||||
|
eventLogger.Summary(summary, false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (logger != null)
|
||||||
|
{
|
||||||
|
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventLogger != null)
|
||||||
|
{
|
||||||
|
eventLogger.Error("Fataler Fehler: " + ex.Message, 9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MonitorConfiguration BuildMonitorConfiguration(GeneratorConfiguration configuration, IReadOnlyCollection<DiscoveredEndpoint> discovered)
|
||||||
|
{
|
||||||
|
var result = configuration.GeneratedMonitorConfig.CloneWithoutTargets();
|
||||||
|
result.EnvironmentName = string.IsNullOrWhiteSpace(configuration.GeneratedMonitorConfig.EnvironmentName)
|
||||||
|
? configuration.EnvironmentName
|
||||||
|
: configuration.GeneratedMonitorConfig.EnvironmentName;
|
||||||
|
|
||||||
|
var targets = new List<TargetOptions>();
|
||||||
|
foreach (var item in discovered)
|
||||||
|
{
|
||||||
|
var target = new TargetOptions
|
||||||
|
{
|
||||||
|
Name = item.TargetName,
|
||||||
|
ApplicationName = item.MappedApplicationName,
|
||||||
|
Endpoint = item.NormalizedEndpoint,
|
||||||
|
TimeoutSeconds = configuration.Output.TargetTimeoutSeconds,
|
||||||
|
Port = item.Port,
|
||||||
|
ValidateUncPathAccess = item.ValidateUncPathAccess,
|
||||||
|
HttpMethod = item.HttpMethod,
|
||||||
|
Enabled = configuration.Output.TargetEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
targets.Add(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Targets = targets
|
||||||
|
.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(x => x.Endpoint, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolvePath(string baseDirectory, string configuredPath)
|
||||||
|
{
|
||||||
|
if (Path.IsPathRooted(configuredPath))
|
||||||
|
{
|
||||||
|
return configuredPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
[assembly: AssemblyTitle("BizTalkMonitorConfigGenerator")]
|
||||||
|
[assembly: AssemblyDescription("Generates HostAvailabilityMonitor JSON config from active BizTalk send ports and receive locations.")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("JR IT Services")]
|
||||||
|
[assembly: AssemblyProduct("BizTalkMonitorConfigGenerator")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © JR IT Services")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
[assembly: Guid("f03c8d6d-1619-4cb6-b8a2-19d96473fddd")]
|
||||||
|
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
|
|
||||||
|
namespace BizTalkMonitorConfigGenerator.Serialization
|
||||||
|
{
|
||||||
|
public static class JsonFileWriter
|
||||||
|
{
|
||||||
|
public static void WritePrettyJsonAtomic(string path, object value, bool overwriteExisting)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Pfad ist leer.", nameof(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
var fullPath = Path.GetFullPath(path);
|
||||||
|
var directory = Path.GetDirectoryName(fullPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Aus dem Zielpfad konnte kein Verzeichnis aufgelöst werden: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
if (File.Exists(fullPath) && !overwriteExisting)
|
||||||
|
{
|
||||||
|
throw new IOException("Zieldatei existiert bereits und OverwriteExisting=false: " + fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializer = new JavaScriptSerializer();
|
||||||
|
serializer.MaxJsonLength = int.MaxValue;
|
||||||
|
var compact = serializer.Serialize(value);
|
||||||
|
var pretty = PrettyPrint(compact);
|
||||||
|
|
||||||
|
var tempPath = fullPath + ".tmp";
|
||||||
|
File.WriteAllText(tempPath, pretty, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||||
|
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
File.Copy(fullPath, fullPath + ".bak", true);
|
||||||
|
File.Delete(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(tempPath, fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PrettyPrint(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var indent = 0;
|
||||||
|
var quoted = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < json.Length; i++)
|
||||||
|
{
|
||||||
|
var ch = json[i];
|
||||||
|
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case '{':
|
||||||
|
case '[':
|
||||||
|
sb.Append(ch);
|
||||||
|
if (!quoted)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
indent++;
|
||||||
|
AppendIndent(sb, indent);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '}':
|
||||||
|
case ']':
|
||||||
|
if (!quoted)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
indent--;
|
||||||
|
AppendIndent(sb, indent);
|
||||||
|
sb.Append(ch);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
sb.Append(ch);
|
||||||
|
if (i > 0 && json[i - 1] == '\\')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
quoted = !quoted;
|
||||||
|
break;
|
||||||
|
case ',':
|
||||||
|
sb.Append(ch);
|
||||||
|
if (!quoted)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
AppendIndent(sb, indent);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
if (quoted)
|
||||||
|
{
|
||||||
|
sb.Append(ch);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(": ");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!quoted && char.IsWhiteSpace(ch))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(ch);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendIndent(StringBuilder sb, int indent)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < indent; i++)
|
||||||
|
{
|
||||||
|
sb.Append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"GeneratorName": "BizTalkMonitorConfigGenerator",
|
||||||
|
"EnvironmentName": "HIP Produktion",
|
||||||
|
"Logging": {
|
||||||
|
"LogDirectory": "logs",
|
||||||
|
"FilePrefix": "biztalk-monitor-config-generator",
|
||||||
|
"RetentionDays": 5
|
||||||
|
},
|
||||||
|
"EventLog": {
|
||||||
|
"Enabled": true,
|
||||||
|
"LogName": "Application",
|
||||||
|
"Source": "BizTalkMonitorConfigGenerator",
|
||||||
|
"CreateSourceIfMissing": false,
|
||||||
|
"WriteSuccessEntries": false,
|
||||||
|
"WriteSummaryEntries": true
|
||||||
|
},
|
||||||
|
"BizTalk": {
|
||||||
|
"ConnectionString": "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI",
|
||||||
|
"IncludeSendPorts": true,
|
||||||
|
"IncludeReceiveLocations": true,
|
||||||
|
"OnlyStartedSendPorts": true,
|
||||||
|
"OnlyEnabledReceiveLocations": true,
|
||||||
|
"IncludeSecondaryTransportOnSendPorts": true,
|
||||||
|
"IncludeDynamicSendPorts": false,
|
||||||
|
"IncludeLocalFilePaths": false,
|
||||||
|
"IgnoreSystemApplications": true,
|
||||||
|
"IgnoreApplications": [
|
||||||
|
"BizTalk.System"
|
||||||
|
],
|
||||||
|
"IgnoreArtifactNamePatterns": [],
|
||||||
|
"ApplicationMappings": [
|
||||||
|
{
|
||||||
|
"BizTalkApplicationName": "HIP*",
|
||||||
|
"ApplicationName": "HIP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ArtifactMappings": []
|
||||||
|
},
|
||||||
|
"Output": {
|
||||||
|
"Path": "generated-monitor-appsettings.json",
|
||||||
|
"OverwriteExisting": true,
|
||||||
|
"FailIfNoTargetsFound": false,
|
||||||
|
"TargetTimeoutSeconds": 10,
|
||||||
|
"TargetEnabled": true,
|
||||||
|
"DefaultHttpMethod": "HEAD"
|
||||||
|
},
|
||||||
|
"GeneratedMonitorConfig": {
|
||||||
|
"ApplicationName": "HostAvailabilityMonitor",
|
||||||
|
"EnvironmentName": "HIP Produktion",
|
||||||
|
"Runtime": {
|
||||||
|
"MaxConcurrency": 4,
|
||||||
|
"DefaultValidateUncPathAccess": false,
|
||||||
|
"NonZeroExitCodeOnAnyFailure": false,
|
||||||
|
"NonZeroExitCodeOnExecutionError": true,
|
||||||
|
"StateDirectory": "state"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogDirectory": "logs",
|
||||||
|
"FilePrefix": "host-availability-monitor",
|
||||||
|
"RetentionDays": 5
|
||||||
|
},
|
||||||
|
"EventLog": {
|
||||||
|
"Enabled": true,
|
||||||
|
"LogName": "Application",
|
||||||
|
"Source": "HostAvailabilityMonitor",
|
||||||
|
"CreateSourceIfMissing": false,
|
||||||
|
"WriteSuccessEntries": false,
|
||||||
|
"WriteSummaryEntries": true
|
||||||
|
},
|
||||||
|
"Email": {
|
||||||
|
"Enabled": false,
|
||||||
|
"SmtpHost": "smtp.example.local",
|
||||||
|
"SmtpPort": 25,
|
||||||
|
"UseSsl": false,
|
||||||
|
"UseDefaultCredentials": false,
|
||||||
|
"Username": "",
|
||||||
|
"Password": "",
|
||||||
|
"From": "monitor@example.local",
|
||||||
|
"To": [
|
||||||
|
"operations@example.local",
|
||||||
|
"integration-oncall@example.local"
|
||||||
|
],
|
||||||
|
"Cc": [
|
||||||
|
"integration-leads@example.local"
|
||||||
|
],
|
||||||
|
"Bcc": [],
|
||||||
|
"SubjectPrefix": "[HostAvailabilityMonitor]",
|
||||||
|
"SendOnFailure": true,
|
||||||
|
"SendOnRecovery": true,
|
||||||
|
"SendDailySummary": true,
|
||||||
|
"DailySummaryHourLocal": 14,
|
||||||
|
"DailySummaryMinuteLocal": 0,
|
||||||
|
"OnlyOnStateChange": true,
|
||||||
|
"CooldownMinutes": 0,
|
||||||
|
"TimeoutSeconds": 30
|
||||||
|
},
|
||||||
|
"Targets": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<startup>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||||
</startup>
|
</startup>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -1,179 +1,272 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Web.Script.Serialization;
|
using System.Linq;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
namespace HostAvailabilityMonitor.Configuration
|
|
||||||
{
|
namespace HostAvailabilityMonitor.Configuration
|
||||||
public sealed class MonitorConfiguration
|
{
|
||||||
{
|
public sealed class MonitorConfiguration
|
||||||
public string ApplicationName { get; set; } = "HostAvailabilityMonitor";
|
{
|
||||||
public RuntimeOptions Runtime { get; set; } = new RuntimeOptions();
|
public string ApplicationName { get; set; } = "HostAvailabilityMonitor";
|
||||||
public LoggingOptions Logging { get; set; } = new LoggingOptions();
|
public string EnvironmentName { get; set; } = "Unspecified Environment";
|
||||||
public EventLogOptions EventLog { get; set; } = new EventLogOptions();
|
public RuntimeOptions Runtime { get; set; } = new RuntimeOptions();
|
||||||
public EmailOptions Email { get; set; } = new EmailOptions();
|
public LoggingOptions Logging { get; set; } = new LoggingOptions();
|
||||||
public List<TargetOptions> Targets { get; set; } = new List<TargetOptions>();
|
public EventLogOptions EventLog { get; set; } = new EventLogOptions();
|
||||||
|
public EmailOptions Email { get; set; } = new EmailOptions();
|
||||||
public static MonitorConfiguration Load(string path)
|
public List<TargetOptions> Targets { get; set; } = new List<TargetOptions>();
|
||||||
{
|
|
||||||
if (!File.Exists(path))
|
public static MonitorConfiguration Load(string path)
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException("Konfigurationsdatei wurde nicht gefunden: " + path, path);
|
if (!File.Exists(path))
|
||||||
}
|
{
|
||||||
|
throw new FileNotFoundException("Konfigurationsdatei wurde nicht gefunden: " + path, path);
|
||||||
var json = File.ReadAllText(path);
|
}
|
||||||
var serializer = new JavaScriptSerializer();
|
|
||||||
var config = serializer.Deserialize<MonitorConfiguration>(json);
|
var json = File.ReadAllText(path);
|
||||||
|
var serializer = new JavaScriptSerializer();
|
||||||
if (config == null)
|
var config = serializer.Deserialize<MonitorConfiguration>(json);
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Konfigurationsdatei konnte nicht deserialisiert werden.");
|
if (config == null)
|
||||||
}
|
{
|
||||||
|
throw new InvalidOperationException("Konfigurationsdatei konnte nicht deserialisiert werden.");
|
||||||
config.Runtime = config.Runtime ?? new RuntimeOptions();
|
}
|
||||||
config.Logging = config.Logging ?? new LoggingOptions();
|
|
||||||
config.EventLog = config.EventLog ?? new EventLogOptions();
|
config.Runtime = config.Runtime ?? new RuntimeOptions();
|
||||||
config.Email = config.Email ?? new EmailOptions();
|
config.Logging = config.Logging ?? new LoggingOptions();
|
||||||
config.Targets = config.Targets ?? new List<TargetOptions>();
|
config.EventLog = config.EventLog ?? new EventLogOptions();
|
||||||
|
config.Email = config.Email ?? new EmailOptions();
|
||||||
foreach (var target in config.Targets)
|
config.Email.To = SanitizeRecipientList(config.Email.To);
|
||||||
{
|
config.Email.Cc = SanitizeRecipientList(config.Email.Cc);
|
||||||
if (target == null)
|
config.Email.Bcc = SanitizeRecipientList(config.Email.Bcc);
|
||||||
{
|
config.Targets = config.Targets ?? new List<TargetOptions>();
|
||||||
continue;
|
|
||||||
}
|
if (string.IsNullOrWhiteSpace(config.ApplicationName))
|
||||||
|
{
|
||||||
if (target.TimeoutSeconds <= 0)
|
config.ApplicationName = "HostAvailabilityMonitor";
|
||||||
{
|
}
|
||||||
target.TimeoutSeconds = 10;
|
|
||||||
}
|
if (string.IsNullOrWhiteSpace(config.EnvironmentName))
|
||||||
}
|
{
|
||||||
|
config.EnvironmentName = "Unspecified Environment";
|
||||||
return config;
|
}
|
||||||
}
|
|
||||||
|
foreach (var target in config.Targets)
|
||||||
public void Validate()
|
{
|
||||||
{
|
if (target == null)
|
||||||
if (Targets == null || Targets.Count == 0)
|
{
|
||||||
{
|
continue;
|
||||||
throw new InvalidOperationException("Es wurde kein Ziel in der Konfiguration definiert.");
|
}
|
||||||
}
|
|
||||||
|
if (target.TimeoutSeconds <= 0)
|
||||||
foreach (var target in Targets)
|
{
|
||||||
{
|
target.TimeoutSeconds = 10;
|
||||||
if (target == null)
|
}
|
||||||
{
|
}
|
||||||
throw new InvalidOperationException("Die Konfiguration enthaelt ein leeres Target-Objekt.");
|
|
||||||
}
|
if (config.Email.TimeoutSeconds <= 0)
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(target.Endpoint))
|
config.Email.TimeoutSeconds = 30;
|
||||||
{
|
}
|
||||||
throw new InvalidOperationException("Mindestens ein Target hat keinen Endpoint-Wert.");
|
|
||||||
}
|
if (config.Email.DailySummaryHourLocal < 0 || config.Email.DailySummaryHourLocal > 23)
|
||||||
|
{
|
||||||
if (target.TimeoutSeconds <= 0)
|
config.Email.DailySummaryHourLocal = 14;
|
||||||
{
|
}
|
||||||
throw new InvalidOperationException("Target '" + target.DisplayName + "' hat einen ungueltigen TimeoutSeconds-Wert.");
|
|
||||||
}
|
if (config.Email.DailySummaryMinuteLocal < 0 || config.Email.DailySummaryMinuteLocal > 59)
|
||||||
}
|
{
|
||||||
|
config.Email.DailySummaryMinuteLocal = 0;
|
||||||
if (Runtime.MaxConcurrency <= 0)
|
}
|
||||||
{
|
|
||||||
Runtime.MaxConcurrency = 1;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Logging.RetentionDays < 0)
|
public void Validate()
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Logging.RetentionDays darf nicht negativ sein.");
|
if (Targets == null || Targets.Count == 0)
|
||||||
}
|
{
|
||||||
|
throw new InvalidOperationException("Es wurde kein Ziel in der Konfiguration definiert.");
|
||||||
if (Email.Enabled)
|
}
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(Email.SmtpHost))
|
foreach (var target in Targets)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Email.Enabled=true, aber SmtpHost ist leer.");
|
if (target == null)
|
||||||
}
|
{
|
||||||
|
throw new InvalidOperationException("Die Konfiguration enthaelt ein leeres Target-Objekt.");
|
||||||
if (string.IsNullOrWhiteSpace(Email.From))
|
}
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Email.Enabled=true, aber From ist leer.");
|
if (string.IsNullOrWhiteSpace(target.Endpoint))
|
||||||
}
|
{
|
||||||
|
throw new InvalidOperationException("Mindestens ein Target hat keinen Endpoint-Wert.");
|
||||||
if (Email.To == null || Email.To.Count == 0)
|
}
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Email.Enabled=true, aber es sind keine Empfaenger in Email.To konfiguriert.");
|
if (target.TimeoutSeconds <= 0)
|
||||||
}
|
{
|
||||||
|
throw new InvalidOperationException("Target '" + target.DisplayName + "' hat einen ungueltigen TimeoutSeconds-Wert.");
|
||||||
if (Email.TimeoutSeconds <= 0)
|
}
|
||||||
{
|
}
|
||||||
Email.TimeoutSeconds = 30;
|
|
||||||
}
|
if (Runtime.MaxConcurrency <= 0)
|
||||||
}
|
{
|
||||||
}
|
Runtime.MaxConcurrency = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class RuntimeOptions
|
if (Logging.RetentionDays < 0)
|
||||||
{
|
{
|
||||||
public int MaxConcurrency { get; set; } = 4;
|
throw new InvalidOperationException("Logging.RetentionDays darf nicht negativ sein.");
|
||||||
public bool DefaultValidateUncPathAccess { get; set; } = false;
|
}
|
||||||
public bool NonZeroExitCodeOnAnyFailure { get; set; } = false;
|
|
||||||
public bool NonZeroExitCodeOnExecutionError { get; set; } = true;
|
if (Email.Enabled)
|
||||||
public string StateDirectory { get; set; } = "state";
|
{
|
||||||
}
|
if (string.IsNullOrWhiteSpace(Email.SmtpHost))
|
||||||
|
{
|
||||||
public sealed class LoggingOptions
|
throw new InvalidOperationException("Email.Enabled=true, aber SmtpHost ist leer.");
|
||||||
{
|
}
|
||||||
public string LogDirectory { get; set; } = "logs";
|
|
||||||
public string FilePrefix { get; set; } = "host-availability-monitor";
|
if (string.IsNullOrWhiteSpace(Email.From))
|
||||||
public int RetentionDays { get; set; } = 5;
|
{
|
||||||
}
|
throw new InvalidOperationException("Email.Enabled=true, aber From ist leer.");
|
||||||
|
}
|
||||||
public sealed class EventLogOptions
|
|
||||||
{
|
if (GetConfiguredRecipientCount(Email) <= 0)
|
||||||
public bool Enabled { get; set; } = true;
|
{
|
||||||
public string LogName { get; set; } = "Application";
|
throw new InvalidOperationException("Email.Enabled=true, aber es ist kein Empfaenger in Email.To, Email.Cc oder Email.Bcc konfiguriert.");
|
||||||
public string Source { get; set; } = "HostAvailabilityMonitor";
|
}
|
||||||
public bool CreateSourceIfMissing { get; set; } = false;
|
|
||||||
public bool WriteSuccessEntries { get; set; } = false;
|
if (Email.TimeoutSeconds <= 0)
|
||||||
public bool WriteSummaryEntries { get; set; } = true;
|
{
|
||||||
}
|
Email.TimeoutSeconds = 30;
|
||||||
|
}
|
||||||
public sealed class EmailOptions
|
|
||||||
{
|
if (Email.DailySummaryHourLocal < 0 || Email.DailySummaryHourLocal > 23)
|
||||||
public bool Enabled { get; set; } = false;
|
{
|
||||||
public string SmtpHost { get; set; } = string.Empty;
|
throw new InvalidOperationException("Email.DailySummaryHourLocal muss zwischen 0 und 23 liegen.");
|
||||||
public int SmtpPort { get; set; } = 25;
|
}
|
||||||
public bool UseSsl { get; set; } = false;
|
|
||||||
public string Username { get; set; } = string.Empty;
|
if (Email.DailySummaryMinuteLocal < 0 || Email.DailySummaryMinuteLocal > 59)
|
||||||
public string Password { get; set; } = string.Empty;
|
{
|
||||||
public string From { get; set; } = string.Empty;
|
throw new InvalidOperationException("Email.DailySummaryMinuteLocal muss zwischen 0 und 59 liegen.");
|
||||||
public List<string> To { get; set; } = new List<string>();
|
}
|
||||||
public string SubjectPrefix { get; set; } = "[HostAvailabilityMonitor]";
|
}
|
||||||
public bool SendOnFailure { get; set; } = true;
|
}
|
||||||
public bool SendOnRecovery { get; set; } = true;
|
private static List<string> SanitizeRecipientList(List<string> values)
|
||||||
public bool OnlyOnStateChange { get; set; } = true;
|
{
|
||||||
public int CooldownMinutes { get; set; } = 0;
|
if (values == null)
|
||||||
public int TimeoutSeconds { get; set; } = 30;
|
{
|
||||||
}
|
return new List<string>();
|
||||||
|
}
|
||||||
public sealed class TargetOptions
|
|
||||||
{
|
return values
|
||||||
public string Name { get; set; } = string.Empty;
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
public string Endpoint { get; set; } = string.Empty;
|
.Select(x => x.Trim())
|
||||||
public int TimeoutSeconds { get; set; } = 10;
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
public int? Port { get; set; }
|
.ToList();
|
||||||
public bool? ValidateUncPathAccess { get; set; }
|
}
|
||||||
public string HttpMethod { get; set; }
|
|
||||||
public bool Enabled { get; set; } = true;
|
private static int GetConfiguredRecipientCount(EmailOptions email)
|
||||||
|
{
|
||||||
public string DisplayName
|
if (email == null)
|
||||||
{
|
{
|
||||||
get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name; }
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string StateKey
|
return SanitizeRecipientList(email.To).Count
|
||||||
{
|
+ SanitizeRecipientList(email.Cc).Count
|
||||||
get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name.Trim(); }
|
+ SanitizeRecipientList(email.Bcc).Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public sealed class RuntimeOptions
|
||||||
|
{
|
||||||
|
public int MaxConcurrency { get; set; } = 4;
|
||||||
|
public bool DefaultValidateUncPathAccess { get; set; } = false;
|
||||||
|
public bool NonZeroExitCodeOnAnyFailure { get; set; } = false;
|
||||||
|
public bool NonZeroExitCodeOnExecutionError { get; set; } = true;
|
||||||
|
public string StateDirectory { get; set; } = "state";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LoggingOptions
|
||||||
|
{
|
||||||
|
public string LogDirectory { get; set; } = "logs";
|
||||||
|
public string FilePrefix { get; set; } = "host-availability-monitor";
|
||||||
|
public int RetentionDays { get; set; } = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EventLogOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public string LogName { get; set; } = "Application";
|
||||||
|
public string Source { get; set; } = "HostAvailabilityMonitor";
|
||||||
|
public bool CreateSourceIfMissing { get; set; } = false;
|
||||||
|
public bool WriteSuccessEntries { get; set; } = false;
|
||||||
|
public bool WriteSummaryEntries { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EmailOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = false;
|
||||||
|
public string SmtpHost { get; set; } = string.Empty;
|
||||||
|
public int SmtpPort { get; set; } = 25;
|
||||||
|
public bool UseSsl { get; set; } = false;
|
||||||
|
public bool UseDefaultCredentials { get; set; } = false;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
public string From { get; set; } = string.Empty;
|
||||||
|
public List<string> To { get; set; } = new List<string>();
|
||||||
|
public List<string> Cc { get; set; } = new List<string>();
|
||||||
|
public List<string> Bcc { get; set; } = new List<string>();
|
||||||
|
public string SubjectPrefix { get; set; } = "[HostAvailabilityMonitor]";
|
||||||
|
public bool SendOnFailure { get; set; } = true;
|
||||||
|
public bool SendOnRecovery { get; set; } = true;
|
||||||
|
public bool SendDailySummary { get; set; } = false;
|
||||||
|
public int DailySummaryHourLocal { get; set; } = 14;
|
||||||
|
public int DailySummaryMinuteLocal { get; set; } = 0;
|
||||||
|
public bool OnlyOnStateChange { get; set; } = true;
|
||||||
|
public int CooldownMinutes { get; set; } = 0;
|
||||||
|
public int TimeoutSeconds { get; set; } = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TargetOptions
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string ApplicationName { get; set; } = string.Empty;
|
||||||
|
public string Endpoint { get; set; } = string.Empty;
|
||||||
|
public int TimeoutSeconds { get; set; } = 10;
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public bool? ValidateUncPathAccess { get; set; }
|
||||||
|
public string HttpMethod { get; set; }
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get { return string.IsNullOrWhiteSpace(Name) ? Endpoint : Name; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EffectiveApplicationName
|
||||||
|
{
|
||||||
|
get { return string.IsNullOrWhiteSpace(ApplicationName) ? "Unassigned Application" : ApplicationName.Trim(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StateKey
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var parts = new[]
|
||||||
|
{
|
||||||
|
Normalize(ApplicationName),
|
||||||
|
Normalize(Name),
|
||||||
|
Normalize(Endpoint)
|
||||||
|
}.Where(x => !string.IsNullOrWhiteSpace(x));
|
||||||
|
|
||||||
|
var key = string.Join("|", parts);
|
||||||
|
return string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("N") : key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
<ProjectGuid>{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}</ProjectGuid>
|
<ProjectGuid>{0E9D281E-8E18-4A97-BFA6-ED0F0B6A242B}</ProjectGuid>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<RootNamespace>HostAvailabilityMonitor</RootNamespace>
|
<RootNamespace>HostAvailabilityMonitor</RootNamespace>
|
||||||
<AssemblyName>HostAvailabilityMonitor</AssemblyName>
|
<AssemblyName>HostAvailabilityMonitor</AssemblyName>
|
||||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
<FileAlignment>512</FileAlignment>
|
<FileAlignment>512</FileAlignment>
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
<LangVersion>7.3</LangVersion>
|
<LangVersion>7.3</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>full</DebugType>
|
<DebugType>full</DebugType>
|
||||||
<Optimize>false</Optimize>
|
<Optimize>false</Optimize>
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<ErrorReport>prompt</ErrorReport>
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
<DebugType>pdbonly</DebugType>
|
<DebugType>pdbonly</DebugType>
|
||||||
<Optimize>true</Optimize>
|
<Optimize>true</Optimize>
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<ErrorReport>prompt</ErrorReport>
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Configuration" />
|
<Reference Include="System.Configuration" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
<Reference Include="System.Web.Extensions" />
|
<Reference Include="System.Web.Extensions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Configuration\MonitorConfiguration.cs" />
|
<Compile Include="Configuration\MonitorConfiguration.cs" />
|
||||||
<Compile Include="Logging\FileLogger.cs" />
|
<Compile Include="Logging\FileLogger.cs" />
|
||||||
<Compile Include="Logging\WindowsEventLogger.cs" />
|
<Compile Include="Logging\WindowsEventLogger.cs" />
|
||||||
<Compile Include="Monitoring\EndpointProbeService.cs" />
|
<Compile Include="Monitoring\EndpointProbeService.cs" />
|
||||||
<Compile Include="Monitoring\ProbeResult.cs" />
|
<Compile Include="Monitoring\ProbeResult.cs" />
|
||||||
<Compile Include="Notifications\EmailNotifier.cs" />
|
<Compile Include="Notifications\EmailNotifier.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="State\MonitorStateStore.cs" />
|
<Compile Include="State\MonitorStateStore.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="App.config" />
|
<None Include="App.config" />
|
||||||
<Content Include="appsettings.json">
|
<Content Include="appsettings.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,185 +1,192 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Threading;
|
||||||
using System.Threading;
|
using HostAvailabilityMonitor.Monitoring;
|
||||||
using HostAvailabilityMonitor.Monitoring;
|
|
||||||
|
namespace HostAvailabilityMonitor.Logging
|
||||||
namespace HostAvailabilityMonitor.Logging
|
{
|
||||||
{
|
public sealed class FileLogger
|
||||||
public sealed class FileLogger
|
{
|
||||||
{
|
private readonly object _sync = new object();
|
||||||
private readonly object _sync = new object();
|
private readonly string _directory;
|
||||||
private readonly string _directory;
|
private readonly string _filePrefix;
|
||||||
private readonly string _filePrefix;
|
private readonly int _retentionDays;
|
||||||
private readonly int _retentionDays;
|
private readonly string _monitorName;
|
||||||
|
private readonly string _environmentName;
|
||||||
public FileLogger(string directory, string filePrefix, int retentionDays)
|
|
||||||
{
|
public FileLogger(string directory, string filePrefix, int retentionDays, string monitorName, string environmentName)
|
||||||
_directory = directory;
|
{
|
||||||
_filePrefix = filePrefix;
|
_directory = directory;
|
||||||
_retentionDays = retentionDays;
|
_filePrefix = filePrefix;
|
||||||
|
_retentionDays = retentionDays;
|
||||||
Directory.CreateDirectory(_directory);
|
_monitorName = string.IsNullOrWhiteSpace(monitorName) ? "HostAvailabilityMonitor" : monitorName.Trim();
|
||||||
}
|
_environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim();
|
||||||
|
|
||||||
public void CleanupOldFiles()
|
Directory.CreateDirectory(_directory);
|
||||||
{
|
}
|
||||||
if (_retentionDays <= 0)
|
|
||||||
{
|
public void CleanupOldFiles()
|
||||||
return;
|
{
|
||||||
}
|
if (_retentionDays <= 0)
|
||||||
|
{
|
||||||
var cutoff = DateTime.Today.AddDays(-Math.Max(0, _retentionDays - 1));
|
return;
|
||||||
foreach (var file in Directory.EnumerateFiles(_directory, _filePrefix + "-*.log", SearchOption.TopDirectoryOnly))
|
}
|
||||||
{
|
|
||||||
try
|
var cutoff = DateTime.Today.AddDays(-Math.Max(0, _retentionDays - 1));
|
||||||
{
|
foreach (var file in Directory.EnumerateFiles(_directory, _filePrefix + "-*.log", SearchOption.TopDirectoryOnly))
|
||||||
if (TryGetDateFromFileName(file, out var fileDate))
|
{
|
||||||
{
|
try
|
||||||
if (fileDate < cutoff)
|
{
|
||||||
{
|
DateTime fileDate;
|
||||||
File.Delete(file);
|
if (TryGetDateFromFileName(file, out fileDate))
|
||||||
}
|
{
|
||||||
}
|
if (fileDate < cutoff)
|
||||||
else
|
{
|
||||||
{
|
File.Delete(file);
|
||||||
var info = new FileInfo(file);
|
}
|
||||||
if (info.LastWriteTime.Date < cutoff)
|
}
|
||||||
{
|
else
|
||||||
info.Delete();
|
{
|
||||||
}
|
var info = new FileInfo(file);
|
||||||
}
|
if (info.LastWriteTime.Date < cutoff)
|
||||||
}
|
{
|
||||||
catch
|
info.Delete();
|
||||||
{
|
}
|
||||||
// Cleanup darf den Lauf nicht blockieren.
|
}
|
||||||
}
|
}
|
||||||
}
|
catch
|
||||||
}
|
{
|
||||||
|
// Cleanup darf den Lauf nicht blockieren.
|
||||||
public void Info(string message)
|
}
|
||||||
{
|
}
|
||||||
Write("INFO", message);
|
}
|
||||||
}
|
|
||||||
|
public void Info(string message)
|
||||||
public void Warn(string message)
|
{
|
||||||
{
|
Write("INFO", message);
|
||||||
Write("WARN", message);
|
}
|
||||||
}
|
|
||||||
|
public void Warn(string message)
|
||||||
public void Error(string message)
|
{
|
||||||
{
|
Write("WARN", message);
|
||||||
Write("ERROR", message);
|
}
|
||||||
}
|
|
||||||
|
public void Error(string message)
|
||||||
public void Probe(ProbeResult result)
|
{
|
||||||
{
|
Write("ERROR", message);
|
||||||
var level = result.IsSuccess ? "SUCCESS" : "FAIL";
|
}
|
||||||
var parts = new List<string>
|
|
||||||
{
|
public void Probe(ProbeResult result)
|
||||||
"Name=" + Escape(result.Name),
|
{
|
||||||
"Kind=" + result.Kind,
|
var level = result.IsSkipped ? "SKIP" : (result.IsSuccess ? "SUCCESS" : "FAIL");
|
||||||
"Endpoint=" + Escape(result.Endpoint),
|
var parts = new List<string>
|
||||||
"DurationMs=" + Math.Max(0L, (long)result.Duration.TotalMilliseconds),
|
{
|
||||||
"Status=" + Escape(result.StatusText),
|
"Monitor=" + Escape(_monitorName),
|
||||||
"Detail=" + Escape(result.Detail)
|
"Environment=" + Escape(_environmentName),
|
||||||
};
|
"Application=" + Escape(result.ApplicationName),
|
||||||
|
"Name=" + Escape(result.Name),
|
||||||
if (!string.IsNullOrWhiteSpace(result.Host))
|
"Kind=" + result.Kind,
|
||||||
{
|
"Endpoint=" + Escape(result.Endpoint),
|
||||||
parts.Add("Host=" + Escape(result.Host));
|
"DurationMs=" + Math.Max(0L, (long)result.Duration.TotalMilliseconds),
|
||||||
}
|
"Status=" + Escape(result.StatusText),
|
||||||
|
"Detail=" + Escape(result.Detail)
|
||||||
if (result.Port.HasValue)
|
};
|
||||||
{
|
|
||||||
parts.Add("Port=" + result.Port.Value);
|
if (!string.IsNullOrWhiteSpace(result.Host))
|
||||||
}
|
{
|
||||||
|
parts.Add("Host=" + Escape(result.Host));
|
||||||
if (result.HttpStatusCode.HasValue)
|
}
|
||||||
{
|
|
||||||
parts.Add("HttpStatus=" + result.HttpStatusCode.Value);
|
if (result.Port.HasValue)
|
||||||
}
|
{
|
||||||
|
parts.Add("Port=" + result.Port.Value);
|
||||||
if (!string.IsNullOrWhiteSpace(result.ErrorType))
|
}
|
||||||
{
|
|
||||||
parts.Add("ErrorType=" + Escape(result.ErrorType));
|
if (result.HttpStatusCode.HasValue)
|
||||||
}
|
{
|
||||||
|
parts.Add("HttpStatus=" + result.HttpStatusCode.Value);
|
||||||
Write(level, string.Join(" | ", parts));
|
}
|
||||||
}
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(result.ErrorType))
|
||||||
private void Write(string level, string message)
|
{
|
||||||
{
|
parts.Add("ErrorType=" + Escape(result.ErrorType));
|
||||||
var line = string.Format(
|
}
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0:yyyy-MM-dd HH:mm:ss.fff zzz} | {1} | {2}{3}",
|
Write(level, string.Join(" | ", parts));
|
||||||
DateTimeOffset.Now,
|
}
|
||||||
level,
|
|
||||||
message,
|
private void Write(string level, string message)
|
||||||
Environment.NewLine);
|
{
|
||||||
|
var line = string.Format(
|
||||||
var path = GetCurrentLogFilePath();
|
CultureInfo.InvariantCulture,
|
||||||
|
"{0:yyyy-MM-dd HH:mm:ss.fff zzz} | {1} | {2}{3}",
|
||||||
lock (_sync)
|
DateTimeOffset.Now,
|
||||||
{
|
level,
|
||||||
AppendWithRetries(path, line);
|
message,
|
||||||
}
|
Environment.NewLine);
|
||||||
}
|
|
||||||
|
var path = GetCurrentLogFilePath();
|
||||||
private void AppendWithRetries(string path, string line)
|
|
||||||
{
|
lock (_sync)
|
||||||
Exception lastException = null;
|
{
|
||||||
|
AppendWithRetries(path, line);
|
||||||
for (var attempt = 1; attempt <= 3; attempt++)
|
}
|
||||||
{
|
}
|
||||||
try
|
|
||||||
{
|
private void AppendWithRetries(string path, string line)
|
||||||
File.AppendAllText(path, line);
|
{
|
||||||
return;
|
Exception lastException = null;
|
||||||
}
|
|
||||||
catch (IOException ex)
|
for (var attempt = 1; attempt <= 3; attempt++)
|
||||||
{
|
{
|
||||||
lastException = ex;
|
try
|
||||||
Thread.Sleep(100 * attempt);
|
{
|
||||||
}
|
File.AppendAllText(path, line);
|
||||||
catch (UnauthorizedAccessException ex)
|
return;
|
||||||
{
|
}
|
||||||
lastException = ex;
|
catch (IOException ex)
|
||||||
Thread.Sleep(100 * attempt);
|
{
|
||||||
}
|
lastException = ex;
|
||||||
}
|
Thread.Sleep(100 * attempt);
|
||||||
|
}
|
||||||
if (lastException != null)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
throw lastException;
|
lastException = ex;
|
||||||
}
|
Thread.Sleep(100 * attempt);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private string GetCurrentLogFilePath()
|
|
||||||
{
|
if (lastException != null)
|
||||||
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
|
{
|
||||||
return Path.Combine(_directory, fileName);
|
throw lastException;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private bool TryGetDateFromFileName(string path, out DateTime date)
|
|
||||||
{
|
private string GetCurrentLogFilePath()
|
||||||
date = DateTime.MinValue;
|
{
|
||||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
|
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
|
||||||
var prefix = _filePrefix + "-";
|
return Path.Combine(_directory, fileName);
|
||||||
if (!fileNameWithoutExtension.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
}
|
||||||
{
|
|
||||||
return false;
|
private bool TryGetDateFromFileName(string path, out DateTime date)
|
||||||
}
|
{
|
||||||
|
date = DateTime.MinValue;
|
||||||
var datePart = fileNameWithoutExtension.Substring(prefix.Length);
|
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
|
||||||
return DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
|
var prefix = _filePrefix + "-";
|
||||||
}
|
if (!fileNameWithoutExtension.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
private static string Escape(string value)
|
return false;
|
||||||
{
|
}
|
||||||
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
|
|
||||||
}
|
var datePart = fileNameWithoutExtension.Substring(prefix.Length);
|
||||||
}
|
return DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string Escape(string value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,357 +1,515 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using HostAvailabilityMonitor.Configuration;
|
using HostAvailabilityMonitor.Configuration;
|
||||||
|
|
||||||
namespace HostAvailabilityMonitor.Monitoring
|
namespace HostAvailabilityMonitor.Monitoring
|
||||||
{
|
{
|
||||||
public sealed class EndpointProbeService
|
public sealed class EndpointProbeService
|
||||||
{
|
{
|
||||||
private static readonly HttpClient HttpClient = CreateHttpClient();
|
private static readonly HttpClient HttpClient = CreateHttpClient();
|
||||||
private readonly RuntimeOptions _runtimeOptions;
|
private readonly RuntimeOptions _runtimeOptions;
|
||||||
|
|
||||||
public EndpointProbeService(RuntimeOptions runtimeOptions)
|
public EndpointProbeService(RuntimeOptions runtimeOptions)
|
||||||
{
|
{
|
||||||
_runtimeOptions = runtimeOptions;
|
_runtimeOptions = runtimeOptions ?? new RuntimeOptions();
|
||||||
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
|
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ProbeResult> ProbeAsync(TargetOptions target, CancellationToken cancellationToken)
|
public async Task<ProbeResult> ProbeAsync(TargetOptions target, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var startedAt = DateTimeOffset.Now;
|
var startedAt = DateTimeOffset.Now;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var endpoint = (target.Endpoint ?? string.Empty).Trim();
|
var endpoint = (target.Endpoint ?? string.Empty).Trim();
|
||||||
if (string.IsNullOrWhiteSpace(endpoint))
|
if (string.IsNullOrWhiteSpace(endpoint))
|
||||||
{
|
{
|
||||||
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Leerer Endpoint.", null, null, null);
|
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Leerer Endpoint.", null, null, null);
|
||||||
}
|
}
|
||||||
|
ProbeResult skippedResult;
|
||||||
if (endpoint.StartsWith("\\\\", StringComparison.Ordinal))
|
if (TryCreateSkipResult(target, endpoint, startedAt, out skippedResult))
|
||||||
{
|
{
|
||||||
return await CheckUncAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
|
return skippedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri uri;
|
|
||||||
if (Uri.TryCreate(endpoint, UriKind.Absolute, out uri))
|
if (endpoint.StartsWith("\\\\", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var scheme = uri.Scheme.ToLowerInvariant();
|
return await CheckUncAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
|
||||||
switch (scheme)
|
}
|
||||||
{
|
|
||||||
case "http":
|
if (Path.IsPathRooted(endpoint))
|
||||||
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Http, cancellationToken).ConfigureAwait(false);
|
{
|
||||||
case "https":
|
return await CheckLocalPathAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
|
||||||
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Https, cancellationToken).ConfigureAwait(false);
|
}
|
||||||
case "sftp":
|
|
||||||
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 22), startedAt, TargetKind.Sftp, endpoint, cancellationToken).ConfigureAwait(false);
|
Uri uri;
|
||||||
case "tcp":
|
if (Uri.TryCreate(endpoint, UriKind.Absolute, out uri))
|
||||||
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, null), startedAt, TargetKind.Tcp, endpoint, cancellationToken).ConfigureAwait(false);
|
{
|
||||||
case "icmp":
|
var scheme = uri.Scheme.ToLowerInvariant();
|
||||||
return await CheckIcmpAsync(target, uri.Host, startedAt, endpoint).ConfigureAwait(false);
|
switch (scheme)
|
||||||
case "ftp":
|
{
|
||||||
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 21), startedAt, TargetKind.Ftp, endpoint, cancellationToken).ConfigureAwait(false);
|
case "http":
|
||||||
default:
|
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Http, cancellationToken).ConfigureAwait(false);
|
||||||
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Nicht unterstütztes Schema '" + uri.Scheme + "'. Unterstützt: UNC, http, https, sftp, ftp, tcp, icmp.", null, uri.Host, uri.IsDefaultPort ? (int?)null : uri.Port);
|
case "https":
|
||||||
}
|
return await CheckHttpAsync(target, uri, startedAt, TargetKind.Https, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
case "sftp":
|
||||||
|
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 22), startedAt, TargetKind.Sftp, endpoint, cancellationToken).ConfigureAwait(false);
|
||||||
return await CheckPlainHostAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
|
case "tcp":
|
||||||
}
|
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, null), startedAt, TargetKind.Tcp, endpoint, cancellationToken).ConfigureAwait(false);
|
||||||
catch (OperationCanceledException ex)
|
case "icmp":
|
||||||
{
|
return await CheckIcmpAsync(target, uri.Host, startedAt, endpoint, cancellationToken).ConfigureAwait(false);
|
||||||
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, "Timeout oder Abbruch während des Checks.", ex, null, null);
|
case "ftp":
|
||||||
}
|
return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 21), startedAt, TargetKind.Ftp, endpoint, cancellationToken).ConfigureAwait(false);
|
||||||
catch (Exception ex)
|
case "file":
|
||||||
{
|
return await CheckFileUriAsync(target, uri, startedAt, cancellationToken).ConfigureAwait(false);
|
||||||
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, ex.Message, ex, null, null);
|
default:
|
||||||
}
|
return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Nicht unterstütztes Schema '" + uri.Scheme + "'. Unterstützt: UNC, file, http, https, sftp, ftp, tcp, icmp.", null, uri.Host, uri.IsDefaultPort ? (int?)null : uri.Port);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async Task<ProbeResult> CheckPlainHostAsync(TargetOptions target, string host, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
|
||||||
{
|
return await CheckPlainHostAsync(target, endpoint, startedAt, cancellationToken).ConfigureAwait(false);
|
||||||
if (target.Port.HasValue)
|
}
|
||||||
{
|
catch (OperationCanceledException ex)
|
||||||
return await CheckTcpAsync(target, host, target.Port.Value, startedAt, TargetKind.Host, host, cancellationToken).ConfigureAwait(false);
|
{
|
||||||
}
|
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, "Timeout oder Abbruch während des Checks.", ex, null, null);
|
||||||
|
}
|
||||||
return await CheckIcmpAsync(target, host, startedAt, host).ConfigureAwait(false);
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
|
return Fail(target, TargetKind.Unknown, target.Endpoint, startedAt, ex.Message, ex, null, null);
|
||||||
private async Task<ProbeResult> CheckUncAsync(TargetOptions target, string endpoint, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
}
|
||||||
{
|
}
|
||||||
var host = ExtractUncServer(endpoint);
|
|
||||||
var port = target.Port ?? 445;
|
private async Task<ProbeResult> CheckPlainHostAsync(TargetOptions target, string host, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
var tcpResult = await CheckTcpAsync(target, host, port, startedAt, TargetKind.Unc, endpoint, cancellationToken).ConfigureAwait(false);
|
if (target.Port.HasValue)
|
||||||
if (!tcpResult.IsSuccess)
|
{
|
||||||
{
|
return await CheckTcpAsync(target, host, target.Port.Value, startedAt, TargetKind.Host, host, cancellationToken).ConfigureAwait(false);
|
||||||
return tcpResult;
|
}
|
||||||
}
|
|
||||||
|
return await CheckIcmpAsync(target, host, startedAt, host, cancellationToken).ConfigureAwait(false);
|
||||||
var validatePath = target.ValidateUncPathAccess ?? _runtimeOptions.DefaultValidateUncPathAccess;
|
}
|
||||||
if (!validatePath)
|
|
||||||
{
|
private async Task<ProbeResult> CheckFileUriAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
||||||
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfadvalidierung übersprungen.", null);
|
{
|
||||||
}
|
if (uri.IsUnc || !string.IsNullOrWhiteSpace(uri.Host))
|
||||||
|
{
|
||||||
try
|
var uncPath = uri.LocalPath;
|
||||||
{
|
if (!uncPath.StartsWith("\\\\", StringComparison.Ordinal))
|
||||||
var existsTask = Task.Run(function: () => Directory.Exists(endpoint) || File.Exists(endpoint), cancellationToken);
|
{
|
||||||
var timed = await CompleteWithinAsync(existsTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
|
uncPath = "\\\\" + uri.Host + uri.LocalPath.Replace('/', '\\');
|
||||||
if (!timed.Completed)
|
}
|
||||||
{
|
|
||||||
return Fail(target, TargetKind.Unc, endpoint, startedAt, "Timeout während der UNC-Pfadvalidierung.", null, host, port);
|
return await CheckUncAsync(target, uncPath, startedAt, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timed.Result)
|
var localPath = Uri.UnescapeDataString(uri.LocalPath ?? string.Empty);
|
||||||
{
|
return await CheckLocalPathAsync(target, localPath, startedAt, cancellationToken).ConfigureAwait(false);
|
||||||
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfad wurde erfolgreich validiert.", null);
|
}
|
||||||
}
|
|
||||||
|
private async Task<ProbeResult> CheckLocalPathAsync(TargetOptions target, string path, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
||||||
return Fail(target, TargetKind.Unc, endpoint, startedAt, "SMB-Port erreichbar, aber UNC-Pfadvalidierung war nicht erfolgreich.", null, host, port);
|
{
|
||||||
}
|
try
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
var existsTask = Task.Run(function: () => Directory.Exists(path) || File.Exists(path), cancellationToken);
|
||||||
return Fail(target, TargetKind.Unc, endpoint, startedAt, "UNC-Pfadvalidierung fehlgeschlagen: " + ex.Message, ex, host, port);
|
var timed = await CompleteWithinAsync(existsTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
|
||||||
}
|
if (!timed.Completed)
|
||||||
}
|
{
|
||||||
|
return Fail(target, TargetKind.File, path, startedAt, "Timeout während der Datei-/Pfadprüfung.", null, null, null);
|
||||||
private async Task<ProbeResult> CheckHttpAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken)
|
}
|
||||||
{
|
|
||||||
var timeout = TimeSpan.FromSeconds(target.TimeoutSeconds);
|
if (timed.Result)
|
||||||
var methodText = string.IsNullOrWhiteSpace(target.HttpMethod) ? "HEAD" : target.HttpMethod.Trim().ToUpperInvariant();
|
{
|
||||||
var method = new HttpMethod(methodText);
|
return Success(target, TargetKind.File, path, startedAt, Environment.MachineName, null, "Datei- oder Pfadprüfung erfolgreich.", null);
|
||||||
|
}
|
||||||
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
|
||||||
using (var request = new HttpRequestMessage(method, uri))
|
return Fail(target, TargetKind.File, path, startedAt, "Datei oder Verzeichnis ist nicht vorhanden oder nicht zugreifbar.", null, Environment.MachineName, null);
|
||||||
{
|
}
|
||||||
timeoutCts.CancelAfter(timeout);
|
catch (Exception ex)
|
||||||
try
|
{
|
||||||
{
|
return Fail(target, TargetKind.File, path, startedAt, "Datei-/Pfadprüfung fehlgeschlagen: " + ex.Message, ex, Environment.MachineName, null);
|
||||||
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
|
}
|
||||||
{
|
}
|
||||||
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& (response.StatusCode == HttpStatusCode.MethodNotAllowed || response.StatusCode == HttpStatusCode.NotImplemented))
|
private async Task<ProbeResult> CheckUncAsync(TargetOptions target, string endpoint, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, new InvalidOperationException("HEAD nicht unterstützt (" + (int)response.StatusCode + " " + response.ReasonPhrase + ")."), cancellationToken).ConfigureAwait(false);
|
var host = ExtractUncServer(endpoint);
|
||||||
}
|
var port = target.Port ?? 445;
|
||||||
|
|
||||||
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HTTP-Antwort erhalten (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
|
var tcpResult = await CheckTcpAsync(target, host, port, startedAt, TargetKind.Unc, endpoint, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
if (!tcpResult.IsSuccess)
|
||||||
}
|
{
|
||||||
catch (HttpRequestException ex)
|
return tcpResult;
|
||||||
{
|
}
|
||||||
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
var validatePath = target.ValidateUncPathAccess ?? _runtimeOptions.DefaultValidateUncPathAccess;
|
||||||
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, ex, cancellationToken).ConfigureAwait(false);
|
if (!validatePath)
|
||||||
}
|
{
|
||||||
|
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfadvalidierung übersprungen.", null);
|
||||||
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
}
|
||||||
}
|
|
||||||
catch (TaskCanceledException ex)
|
try
|
||||||
{
|
{
|
||||||
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check ist in einen Timeout gelaufen.", ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
var existsTask = Task.Run(function: () => Directory.Exists(endpoint) || File.Exists(endpoint), cancellationToken);
|
||||||
}
|
var timed = await CompleteWithinAsync(existsTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
|
||||||
catch (Exception ex)
|
if (!timed.Completed)
|
||||||
{
|
{
|
||||||
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
return Fail(target, TargetKind.Unc, endpoint, startedAt, "Timeout während der UNC-Pfadvalidierung.", null, host, port);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
if (timed.Result)
|
||||||
|
{
|
||||||
private async Task<ProbeResult> RetryHttpWithGetAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, Exception headException, CancellationToken cancellationToken)
|
return Success(target, TargetKind.Unc, endpoint, startedAt, host, port, "SMB-Port erreichbar; UNC-Pfad wurde erfolgreich validiert.", null);
|
||||||
{
|
}
|
||||||
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
|
||||||
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
|
return Fail(target, TargetKind.Unc, endpoint, startedAt, "SMB-Port erreichbar, aber UNC-Pfadvalidierung war nicht erfolgreich.", null, host, port);
|
||||||
{
|
}
|
||||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(target.TimeoutSeconds));
|
catch (Exception ex)
|
||||||
try
|
{
|
||||||
{
|
return Fail(target, TargetKind.Unc, endpoint, startedAt, "UNC-Pfadvalidierung fehlgeschlagen: " + ex.Message, ex, host, port);
|
||||||
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
|
}
|
||||||
{
|
}
|
||||||
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HEAD nicht erfolgreich (" + headException.GetType().Name + "), GET erfolgreich (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
|
|
||||||
}
|
private async Task<ProbeResult> CheckHttpAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken)
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
var methodText = string.IsNullOrWhiteSpace(target.HttpMethod) ? "HEAD" : target.HttpMethod.Trim().ToUpperInvariant();
|
||||||
{
|
var method = new HttpMethod(methodText);
|
||||||
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen. HEAD: " + headException.Message + "; GET: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
|
||||||
}
|
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||||
}
|
using (var request = new HttpRequestMessage(method, uri))
|
||||||
}
|
{
|
||||||
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(target.TimeoutSeconds));
|
||||||
private async Task<ProbeResult> CheckTcpAsync(TargetOptions target, string host, int? port, DateTimeOffset startedAt, TargetKind kind, string endpoint, CancellationToken cancellationToken)
|
try
|
||||||
{
|
{
|
||||||
if (!port.HasValue || port.Value <= 0)
|
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return Fail(target, kind, endpoint, startedAt, "Für diesen Endpoint konnte kein gültiger TCP-Port ermittelt werden.", null, host, port);
|
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase)
|
||||||
}
|
&& (response.StatusCode == HttpStatusCode.MethodNotAllowed || response.StatusCode == HttpStatusCode.NotImplemented))
|
||||||
|
{
|
||||||
var timeout = TimeSpan.FromSeconds(target.TimeoutSeconds);
|
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
using (var client = new TcpClient())
|
|
||||||
{
|
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HTTP-Antwort erhalten (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
|
||||||
try
|
}
|
||||||
{
|
}
|
||||||
var connectTask = client.ConnectAsync(host, port.Value);
|
catch (HttpRequestException ex)
|
||||||
var completedTask = await Task.WhenAny(connectTask, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false);
|
{
|
||||||
if (completedTask != connectTask)
|
if (string.Equals(methodText, "HEAD", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
try
|
return await RetryHttpWithGetAsync(target, uri, startedAt, kind, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
}
|
||||||
client.Close();
|
|
||||||
}
|
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
||||||
catch
|
}
|
||||||
{
|
catch (TaskCanceledException ex)
|
||||||
}
|
{
|
||||||
|
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check ist in einen Timeout gelaufen.", ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
}
|
||||||
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: Timeout.", null, host, port);
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
|
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
||||||
await connectTask.ConfigureAwait(false);
|
}
|
||||||
return Success(target, kind, endpoint, startedAt, host, port, "TCP-Verbindung erfolgreich aufgebaut.", null);
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
private async Task<ProbeResult> RetryHttpWithGetAsync(TargetOptions target, Uri uri, DateTimeOffset startedAt, TargetKind kind, CancellationToken cancellationToken)
|
||||||
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: " + ex.Message, ex, host, port);
|
{
|
||||||
}
|
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||||
}
|
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
|
||||||
}
|
{
|
||||||
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(target.TimeoutSeconds));
|
||||||
private async Task<ProbeResult> CheckIcmpAsync(TargetOptions target, string host, DateTimeOffset startedAt, string endpoint)
|
try
|
||||||
{
|
{
|
||||||
try
|
using (var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
using (var ping = new Ping())
|
return Success(target, kind, uri.ToString(), startedAt, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80), "HTTP-Antwort nach GET-Fallback erhalten (" + (int)response.StatusCode + " " + response.ReasonPhrase + ").", (int)response.StatusCode);
|
||||||
{
|
}
|
||||||
var reply = await ping.SendPingAsync(host, (int)TimeSpan.FromSeconds(target.TimeoutSeconds).TotalMilliseconds).ConfigureAwait(false);
|
}
|
||||||
if (reply.Status == IPStatus.Success)
|
catch (TaskCanceledException ex)
|
||||||
{
|
{
|
||||||
return Success(target, TargetKind.Icmp, endpoint, startedAt, host, null, "ICMP erfolgreich, RTT=" + reply.RoundtripTime + "ms.", null);
|
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check ist in einen Timeout gelaufen.", ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP fehlgeschlagen: " + reply.Status + ".", null, host, null);
|
{
|
||||||
}
|
return Fail(target, kind, uri.ToString(), startedAt, "HTTP-Check fehlgeschlagen: " + ex.Message, ex, uri.Host, ResolvePort(uri, kind == TargetKind.Https ? 443 : 80));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
}
|
||||||
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check fehlgeschlagen: " + ex.Message, ex, host, null);
|
|
||||||
}
|
private async Task<ProbeResult> CheckTcpAsync(TargetOptions target, string host, int? port, DateTimeOffset startedAt, TargetKind kind, string endpoint, CancellationToken cancellationToken)
|
||||||
}
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
private static ProbeResult Success(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string host, int? port, string detail, int? httpStatusCode)
|
{
|
||||||
{
|
return Fail(target, kind, endpoint, startedAt, "Host ist leer.", null, host, port);
|
||||||
return new ProbeResult
|
}
|
||||||
{
|
|
||||||
StateKey = target.StateKey,
|
if (!port.HasValue || port.Value <= 0 || port.Value > 65535)
|
||||||
Name = target.DisplayName,
|
{
|
||||||
Endpoint = endpoint,
|
return Fail(target, kind, endpoint, startedAt, "Es wurde kein gültiger Port aufgelöst.", null, host, port);
|
||||||
Kind = kind,
|
}
|
||||||
IsSuccess = true,
|
|
||||||
StatusText = "Reachable",
|
using (var client = new TcpClient())
|
||||||
Detail = detail,
|
using (cancellationToken.Register(() =>
|
||||||
Host = host,
|
{
|
||||||
Port = port,
|
try
|
||||||
HttpStatusCode = httpStatusCode,
|
{
|
||||||
ErrorType = string.Empty,
|
client.Close();
|
||||||
StartedAtLocal = startedAt,
|
}
|
||||||
FinishedAtLocal = DateTimeOffset.Now
|
catch
|
||||||
};
|
{
|
||||||
}
|
}
|
||||||
|
}))
|
||||||
private static ProbeResult Fail(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string detail, Exception exception, string host, int? port)
|
{
|
||||||
{
|
try
|
||||||
return new ProbeResult
|
{
|
||||||
{
|
var connectTask = client.ConnectAsync(host, port.Value);
|
||||||
StateKey = target.StateKey,
|
var timed = await CompleteWithinAsync(connectTask, TimeSpan.FromSeconds(target.TimeoutSeconds), cancellationToken).ConfigureAwait(false);
|
||||||
Name = target.DisplayName,
|
if (!timed.Completed)
|
||||||
Endpoint = endpoint,
|
{
|
||||||
Kind = kind,
|
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung ist in einen Timeout gelaufen.", null, host, port);
|
||||||
IsSuccess = false,
|
}
|
||||||
StatusText = "Unreachable",
|
|
||||||
Detail = detail,
|
if (!client.Connected)
|
||||||
Host = host,
|
{
|
||||||
Port = port,
|
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung konnte nicht aufgebaut werden.", null, host, port);
|
||||||
ErrorType = exception == null ? string.Empty : exception.GetType().Name,
|
}
|
||||||
StartedAtLocal = startedAt,
|
|
||||||
FinishedAtLocal = DateTimeOffset.Now
|
return Success(target, kind, endpoint, startedAt, host, port, "TCP-Verbindung erfolgreich aufgebaut.", null);
|
||||||
};
|
}
|
||||||
}
|
catch (SocketException ex)
|
||||||
|
{
|
||||||
private static HttpClient CreateHttpClient()
|
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: " + ex.Message, ex, host, port);
|
||||||
{
|
}
|
||||||
var handler = new HttpClientHandler
|
catch (ObjectDisposedException ex)
|
||||||
{
|
{
|
||||||
AllowAutoRedirect = false,
|
return Fail(target, kind, endpoint, startedAt, "TCP-Check wurde abgebrochen oder ist in einen Timeout gelaufen.", ex, host, port);
|
||||||
UseCookies = false,
|
}
|
||||||
UseProxy = false
|
catch (Exception ex)
|
||||||
};
|
{
|
||||||
|
return Fail(target, kind, endpoint, startedAt, "TCP-Verbindung fehlgeschlagen: " + ex.Message, ex, host, port);
|
||||||
var client = new HttpClient(handler);
|
}
|
||||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
}
|
||||||
return client;
|
}
|
||||||
}
|
|
||||||
|
private async Task<ProbeResult> CheckIcmpAsync(TargetOptions target, string host, DateTimeOffset startedAt, string endpoint, CancellationToken cancellationToken)
|
||||||
private static int? ResolvePort(Uri uri, int? fallback)
|
{
|
||||||
{
|
using (var ping = new Ping())
|
||||||
if (uri == null)
|
{
|
||||||
{
|
try
|
||||||
return fallback;
|
{
|
||||||
}
|
var replyTask = ping.SendPingAsync(host, Math.Max(1000, target.TimeoutSeconds * 1000));
|
||||||
|
var timed = await CompleteWithinAsync(replyTask, TimeSpan.FromSeconds(target.TimeoutSeconds + 1), cancellationToken).ConfigureAwait(false);
|
||||||
if (!uri.IsDefaultPort && uri.Port > 0)
|
if (!timed.Completed)
|
||||||
{
|
{
|
||||||
return uri.Port;
|
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check ist in einen Timeout gelaufen.", null, host, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallback;
|
var reply = timed.Result;
|
||||||
}
|
if (reply != null && reply.Status == IPStatus.Success)
|
||||||
|
{
|
||||||
private static string ExtractUncServer(string uncPath)
|
return Success(target, TargetKind.Icmp, endpoint, startedAt, host, null, "Ping erfolgreich.", null);
|
||||||
{
|
}
|
||||||
if (string.IsNullOrWhiteSpace(uncPath))
|
|
||||||
{
|
var status = reply == null ? "Unbekannt" : reply.Status.ToString();
|
||||||
return string.Empty;
|
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "Ping fehlgeschlagen: " + status + ".", null, host, null);
|
||||||
}
|
}
|
||||||
|
catch (PingException ex)
|
||||||
var trimmed = uncPath.TrimStart('\\');
|
{
|
||||||
var firstSeparator = trimmed.IndexOf('\\');
|
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check fehlgeschlagen: " + ex.Message, ex, host, null);
|
||||||
if (firstSeparator < 0)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
return trimmed;
|
{
|
||||||
}
|
return Fail(target, TargetKind.Icmp, endpoint, startedAt, "ICMP-Check fehlgeschlagen: " + ex.Message, ex, host, null);
|
||||||
|
}
|
||||||
return trimmed.Substring(0, firstSeparator);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<TimedResult<T>> CompleteWithinAsync<T>(Task<T> task, TimeSpan timeout, CancellationToken cancellationToken)
|
private static int? ResolvePort(Uri uri, int? defaultPort)
|
||||||
{
|
{
|
||||||
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false);
|
if (uri == null)
|
||||||
if (completedTask == task)
|
{
|
||||||
{
|
return defaultPort;
|
||||||
return new TimedResult<T>(true, await task.ConfigureAwait(false));
|
}
|
||||||
}
|
|
||||||
|
if (!uri.IsDefaultPort)
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
{
|
||||||
return new TimedResult<T>(false, default(T));
|
return uri.Port;
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct TimedResult<T>
|
return defaultPort;
|
||||||
{
|
}
|
||||||
public TimedResult(bool completed, T result)
|
|
||||||
{
|
private static string ExtractUncServer(string endpoint)
|
||||||
Completed = completed;
|
{
|
||||||
Result = result;
|
if (string.IsNullOrWhiteSpace(endpoint))
|
||||||
}
|
{
|
||||||
|
return string.Empty;
|
||||||
public bool Completed { get; private set; }
|
}
|
||||||
public T Result { get; private set; }
|
|
||||||
}
|
var trimmed = endpoint.TrimStart('\\');
|
||||||
}
|
var firstSeparator = trimmed.IndexOf('\\');
|
||||||
}
|
return firstSeparator >= 0 ? trimmed.Substring(0, firstSeparator) : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateHttpClient()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
var client = new HttpClient(handler, disposeHandler: true);
|
||||||
|
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProbeResult Success(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string host, int? port, string detail, int? httpStatusCode)
|
||||||
|
{
|
||||||
|
return BuildResult(target, kind, endpoint, startedAt, true, false, "Reachable", detail, host, port, httpStatusCode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProbeResult Fail(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string detail, Exception ex, string host, int? port)
|
||||||
|
{
|
||||||
|
return BuildResult(target, kind, endpoint, startedAt, false, false, "Unreachable", detail, host, port, null, ex == null ? null : ex.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProbeResult Skip(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, string detail)
|
||||||
|
{
|
||||||
|
return BuildResult(target, kind, endpoint, startedAt, false, true, "Skipped", detail, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProbeResult BuildResult(TargetOptions target, TargetKind kind, string endpoint, DateTimeOffset startedAt, bool isSuccess, bool isSkipped, string statusText, string detail, string host, int? port, int? httpStatusCode, string errorType)
|
||||||
|
{
|
||||||
|
return new ProbeResult
|
||||||
|
{
|
||||||
|
StateKey = target.StateKey,
|
||||||
|
Name = target.DisplayName,
|
||||||
|
ApplicationName = target.EffectiveApplicationName,
|
||||||
|
Endpoint = endpoint,
|
||||||
|
Kind = kind,
|
||||||
|
IsSuccess = isSuccess,
|
||||||
|
IsSkipped = isSkipped,
|
||||||
|
StatusText = statusText,
|
||||||
|
Detail = detail,
|
||||||
|
Host = host,
|
||||||
|
Port = port,
|
||||||
|
HttpStatusCode = httpStatusCode,
|
||||||
|
ErrorType = errorType,
|
||||||
|
StartedAtLocal = startedAt,
|
||||||
|
FinishedAtLocal = DateTimeOffset.Now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCreateSkipResult(TargetOptions target, string endpoint, DateTimeOffset startedAt, out ProbeResult result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
if (LooksLikeSmtpAdapterTarget(target, endpoint))
|
||||||
|
{
|
||||||
|
result = Skip(target, TargetKind.Unknown, endpoint, startedAt, "Target wurde als SMTP-/E-Mail-Empfängerliste erkannt und wird nicht netzwerktechnisch geprüft.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeSmtpAdapterTarget(TargetOptions target, string endpoint)
|
||||||
|
{
|
||||||
|
var name = target == null ? string.Empty : (target.Name ?? string.Empty);
|
||||||
|
var value = endpoint ?? string.Empty;
|
||||||
|
|
||||||
|
if (name.IndexOf("[SMTP]", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LooksLikeEmailAddressList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeEmailAddressList(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = value.Trim();
|
||||||
|
if (trimmed.IndexOf('@') < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.IndexOf("http://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("https://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("sftp://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("ftp://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("tcp://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("file://", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.StartsWith("\\", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = trimmed.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (entries.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var part = entry.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(part))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.IndexOf('@') <= 0 || part.IndexOf(' ') >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<CompletionResult<T>> CompleteWithinAsync<T>(Task<T> task, TimeSpan timeout, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var delayTask = Task.Delay(timeout, cancellationToken);
|
||||||
|
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
||||||
|
if (completedTask != task)
|
||||||
|
{
|
||||||
|
return new CompletionResult<T>(false, default(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CompletionResult<T>(true, await task.ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<CompletionResult<bool>> CompleteWithinAsync(Task task, TimeSpan timeout, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var delayTask = Task.Delay(timeout, cancellationToken);
|
||||||
|
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
||||||
|
if (completedTask != task)
|
||||||
|
{
|
||||||
|
return new CompletionResult<bool>(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
return new CompletionResult<bool>(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CompletionResult<T>
|
||||||
|
{
|
||||||
|
public CompletionResult(bool completed, T result)
|
||||||
|
{
|
||||||
|
Completed = completed;
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Completed { get; private set; }
|
||||||
|
public T Result { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,38 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace HostAvailabilityMonitor.Monitoring
|
namespace HostAvailabilityMonitor.Monitoring
|
||||||
{
|
{
|
||||||
public enum TargetKind
|
public enum TargetKind
|
||||||
{
|
{
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
Unc = 1,
|
Unc = 1,
|
||||||
Http = 2,
|
Http = 2,
|
||||||
Https = 3,
|
Https = 3,
|
||||||
Sftp = 4,
|
Sftp = 4,
|
||||||
Tcp = 5,
|
Tcp = 5,
|
||||||
Icmp = 6,
|
Icmp = 6,
|
||||||
Host = 7,
|
Host = 7,
|
||||||
Ftp = 8
|
Ftp = 8,
|
||||||
}
|
File = 9
|
||||||
|
}
|
||||||
public sealed class ProbeResult
|
|
||||||
{
|
public sealed class ProbeResult
|
||||||
public string StateKey { get; set; }
|
{
|
||||||
public string Name { get; set; }
|
public string StateKey { get; set; }
|
||||||
public string Endpoint { get; set; }
|
public string Name { get; set; }
|
||||||
public TargetKind Kind { get; set; }
|
public string ApplicationName { get; set; }
|
||||||
public bool IsSuccess { get; set; }
|
public string Endpoint { get; set; }
|
||||||
public string StatusText { get; set; }
|
public TargetKind Kind { get; set; }
|
||||||
public string Detail { get; set; }
|
public bool IsSuccess { get; set; }
|
||||||
public string Host { get; set; }
|
public bool IsSkipped { get; set; }
|
||||||
public int? Port { get; set; }
|
public string StatusText { get; set; }
|
||||||
public int? HttpStatusCode { get; set; }
|
public string Detail { get; set; }
|
||||||
public string ErrorType { get; set; }
|
public string Host { get; set; }
|
||||||
public DateTimeOffset StartedAtLocal { get; set; }
|
public int? Port { get; set; }
|
||||||
public DateTimeOffset FinishedAtLocal { get; set; }
|
public int? HttpStatusCode { get; set; }
|
||||||
public TimeSpan Duration
|
public string ErrorType { get; set; }
|
||||||
{
|
public DateTimeOffset StartedAtLocal { get; set; }
|
||||||
get { return FinishedAtLocal - StartedAtLocal; }
|
public DateTimeOffset FinishedAtLocal { get; set; }
|
||||||
}
|
public TimeSpan Duration
|
||||||
}
|
{
|
||||||
}
|
get { return FinishedAtLocal - StartedAtLocal; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,196 +1,334 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mail;
|
using System.Net.Mail;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using HostAvailabilityMonitor.Configuration;
|
using HostAvailabilityMonitor.Configuration;
|
||||||
using HostAvailabilityMonitor.Monitoring;
|
using HostAvailabilityMonitor.Monitoring;
|
||||||
using HostAvailabilityMonitor.State;
|
using HostAvailabilityMonitor.State;
|
||||||
|
|
||||||
namespace HostAvailabilityMonitor.Notifications
|
namespace HostAvailabilityMonitor.Notifications
|
||||||
{
|
{
|
||||||
public sealed class EmailNotifier
|
public sealed class EmailNotifier
|
||||||
{
|
{
|
||||||
private readonly EmailOptions _options;
|
private readonly EmailOptions _options;
|
||||||
|
private readonly string _monitorName;
|
||||||
public EmailNotifier(EmailOptions options)
|
private readonly string _environmentName;
|
||||||
{
|
|
||||||
_options = options ?? new EmailOptions();
|
public EmailNotifier(EmailOptions options, string monitorName, string environmentName)
|
||||||
}
|
{
|
||||||
|
_options = options ?? new EmailOptions();
|
||||||
public bool IsEnabled
|
_monitorName = string.IsNullOrWhiteSpace(monitorName) ? "HostAvailabilityMonitor" : monitorName.Trim();
|
||||||
{
|
_environmentName = string.IsNullOrWhiteSpace(environmentName) ? "Unspecified Environment" : environmentName.Trim();
|
||||||
get { return _options.Enabled; }
|
}
|
||||||
}
|
|
||||||
|
public bool IsEnabled
|
||||||
public bool ShouldNotify(ProbeResult result, TargetState previousState)
|
{
|
||||||
{
|
get { return _options.Enabled; }
|
||||||
if (!IsEnabled || result == null)
|
}
|
||||||
{
|
|
||||||
return false;
|
public bool ShouldNotify(ProbeResult result, TargetState previousState)
|
||||||
}
|
{
|
||||||
|
if (!IsEnabled || result == null)
|
||||||
if (result.IsSuccess && !_options.SendOnRecovery)
|
{
|
||||||
{
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
if (result.IsSkipped)
|
||||||
if (!result.IsSuccess && !_options.SendOnFailure)
|
{
|
||||||
{
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
var nowUtc = DateTime.UtcNow;
|
var cooldown = TimeSpan.FromMinutes(Math.Max(0, _options.CooldownMinutes));
|
||||||
var cooldown = TimeSpan.FromMinutes(Math.Max(0, _options.CooldownMinutes));
|
var cooldownPassed = previousState == null
|
||||||
var cooldownPassed = previousState == null
|
|| !previousState.LastNotificationUtc.HasValue
|
||||||
|| !previousState.LastNotificationUtc.HasValue
|
|| cooldown == TimeSpan.Zero
|
||||||
|| cooldown == TimeSpan.Zero
|
|| (nowUtc - previousState.LastNotificationUtc.Value) >= cooldown;
|
||||||
|| (nowUtc - previousState.LastNotificationUtc.Value) >= cooldown;
|
|
||||||
|
if (IsRecoveryTransition(result, previousState))
|
||||||
if (_options.OnlyOnStateChange)
|
{
|
||||||
{
|
return _options.SendOnRecovery && cooldownPassed;
|
||||||
if (previousState == null)
|
}
|
||||||
{
|
|
||||||
return !result.IsSuccess && cooldownPassed;
|
if (result.IsSuccess)
|
||||||
}
|
{
|
||||||
|
return false;
|
||||||
if (previousState.IsUp == result.IsSuccess)
|
}
|
||||||
{
|
|
||||||
return false;
|
if (!_options.SendOnFailure)
|
||||||
}
|
{
|
||||||
|
return false;
|
||||||
return cooldownPassed;
|
}
|
||||||
}
|
|
||||||
|
if (_options.OnlyOnStateChange)
|
||||||
if (result.IsSuccess && previousState == null)
|
{
|
||||||
{
|
return previousState == null || previousState.IsUp;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
return cooldownPassed;
|
||||||
return cooldownPassed;
|
}
|
||||||
}
|
|
||||||
|
public bool ShouldSendDailySummary(MonitorStateMetadata metadata, DateTime nowLocal)
|
||||||
public async Task SendAsync(
|
{
|
||||||
IReadOnlyCollection<ProbeResult> failures,
|
if (!IsEnabled || !_options.SendDailySummary)
|
||||||
IReadOnlyCollection<ProbeResult> recoveries,
|
{
|
||||||
IDictionary<string, TargetState> currentState,
|
return false;
|
||||||
CancellationToken cancellationToken)
|
}
|
||||||
{
|
|
||||||
if (!IsEnabled)
|
var trigger = new TimeSpan(Math.Max(0, _options.DailySummaryHourLocal), Math.Max(0, _options.DailySummaryMinuteLocal), 0);
|
||||||
{
|
if (nowLocal.TimeOfDay < trigger)
|
||||||
return;
|
{
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
var failureCount = failures == null ? 0 : failures.Count;
|
|
||||||
var recoveryCount = recoveries == null ? 0 : recoveries.Count;
|
var localDate = nowLocal.ToString("yyyy-MM-dd");
|
||||||
if (failureCount == 0 && recoveryCount == 0)
|
return metadata == null || !string.Equals(metadata.LastDailySummarySentLocalDate, localDate, StringComparison.Ordinal);
|
||||||
{
|
}
|
||||||
return;
|
|
||||||
}
|
public async Task SendAsync(
|
||||||
|
IReadOnlyCollection<ProbeResult> failures,
|
||||||
var subject = BuildSubject(failureCount, recoveryCount);
|
IReadOnlyCollection<ProbeResult> recoveries,
|
||||||
var body = BuildBody(failures, recoveries, currentState);
|
IDictionary<string, TargetState> currentState,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
using (var message = new MailMessage())
|
{
|
||||||
{
|
if (!IsEnabled)
|
||||||
message.From = new MailAddress(_options.From);
|
{
|
||||||
foreach (var recipient in _options.To.Where(x => !string.IsNullOrWhiteSpace(x)))
|
return;
|
||||||
{
|
}
|
||||||
message.To.Add(recipient);
|
|
||||||
}
|
var failureCount = failures == null ? 0 : failures.Count;
|
||||||
|
var recoveryCount = recoveries == null ? 0 : recoveries.Count;
|
||||||
message.Subject = subject;
|
if (failureCount == 0 && recoveryCount == 0)
|
||||||
message.Body = body;
|
{
|
||||||
message.IsBodyHtml = false;
|
return;
|
||||||
message.BodyEncoding = Encoding.UTF8;
|
}
|
||||||
message.SubjectEncoding = Encoding.UTF8;
|
|
||||||
|
var subject = BuildSubject(failureCount, recoveryCount);
|
||||||
using (var client = new SmtpClient(_options.SmtpHost, _options.SmtpPort))
|
var body = BuildBody(failures, recoveries, currentState);
|
||||||
{
|
await SendMailAsync(subject, body, cancellationToken).ConfigureAwait(false);
|
||||||
client.EnableSsl = _options.UseSsl;
|
}
|
||||||
client.Timeout = Math.Max(1, _options.TimeoutSeconds) * 1000;
|
|
||||||
|
public async Task SendDailySummaryAsync(DailySummaryEmailData summary, CancellationToken cancellationToken)
|
||||||
if (!string.IsNullOrWhiteSpace(_options.Username))
|
{
|
||||||
{
|
if (!IsEnabled || summary == null)
|
||||||
client.Credentials = new NetworkCredential(_options.Username, _options.Password);
|
{
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
await client.SendMailAsync(message).ConfigureAwait(false);
|
var subject = BuildDailySummarySubject(summary);
|
||||||
}
|
var body = BuildDailySummaryBody(summary);
|
||||||
}
|
await SendMailAsync(subject, body, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildSubject(int failureCount, int recoveryCount)
|
private async Task SendMailAsync(string subject, string body, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (failureCount > 0 && recoveryCount > 0)
|
using (var message = new MailMessage())
|
||||||
{
|
{
|
||||||
return string.Format("{0} {1} Ausfall/Ausfälle, {2} Recovery/Recoveries", _options.SubjectPrefix, failureCount, recoveryCount);
|
message.From = new MailAddress(_options.From);
|
||||||
}
|
AddRecipients(message.To, _options.To);
|
||||||
|
AddRecipients(message.CC, _options.Cc);
|
||||||
if (failureCount > 0)
|
AddRecipients(message.Bcc, _options.Bcc);
|
||||||
{
|
|
||||||
return string.Format("{0} {1} Ausfall/Ausfälle erkannt", _options.SubjectPrefix, failureCount);
|
if (message.To.Count + message.CC.Count + message.Bcc.Count <= 0)
|
||||||
}
|
{
|
||||||
|
throw new InvalidOperationException("Es ist kein gueltiger Empfaenger in To, Cc oder Bcc konfiguriert.");
|
||||||
return string.Format("{0} {1} Recovery/Recoveries erkannt", _options.SubjectPrefix, recoveryCount);
|
}
|
||||||
}
|
|
||||||
|
message.Subject = subject;
|
||||||
private string BuildBody(IReadOnlyCollection<ProbeResult> failures, IReadOnlyCollection<ProbeResult> recoveries, IDictionary<string, TargetState> currentState)
|
message.Body = body;
|
||||||
{
|
message.IsBodyHtml = false;
|
||||||
var sb = new StringBuilder();
|
message.BodyEncoding = Encoding.UTF8;
|
||||||
sb.AppendLine("Host Availability Monitor");
|
message.SubjectEncoding = Encoding.UTF8;
|
||||||
sb.AppendLine("Zeitpunkt: " + DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"));
|
|
||||||
sb.AppendLine();
|
using (var client = new SmtpClient(_options.SmtpHost, _options.SmtpPort))
|
||||||
|
{
|
||||||
if (failures != null && failures.Count > 0)
|
client.EnableSsl = _options.UseSsl;
|
||||||
{
|
client.Timeout = Math.Max(1, _options.TimeoutSeconds) * 1000;
|
||||||
sb.AppendLine("Fehlgeschlagene Checks:");
|
client.UseDefaultCredentials = _options.UseDefaultCredentials;
|
||||||
foreach (var failure in failures.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
if (!_options.UseDefaultCredentials && !string.IsNullOrWhiteSpace(_options.Username))
|
||||||
AppendResult(sb, failure, currentState);
|
{
|
||||||
}
|
client.Credentials = new NetworkCredential(_options.Username, _options.Password);
|
||||||
sb.AppendLine();
|
}
|
||||||
}
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
if (recoveries != null && recoveries.Count > 0)
|
await client.SendMailAsync(message).ConfigureAwait(false);
|
||||||
{
|
}
|
||||||
sb.AppendLine("Wiederhergestellte Checks:");
|
}
|
||||||
foreach (var recovery in recoveries.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
|
}
|
||||||
{
|
|
||||||
AppendResult(sb, recovery, currentState);
|
private static void AddRecipients(MailAddressCollection collection, IEnumerable<string> recipients)
|
||||||
}
|
{
|
||||||
sb.AppendLine();
|
if (collection == null || recipients == null)
|
||||||
}
|
{
|
||||||
|
return;
|
||||||
return sb.ToString();
|
}
|
||||||
}
|
|
||||||
|
foreach (var recipient in recipients.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
private static void AppendResult(StringBuilder sb, ProbeResult result, IDictionary<string, TargetState> currentState)
|
{
|
||||||
{
|
collection.Add(recipient);
|
||||||
sb.AppendLine("- Name: " + result.Name);
|
}
|
||||||
sb.AppendLine(" Endpoint: " + result.Endpoint);
|
}
|
||||||
sb.AppendLine(" Status: " + result.StatusText);
|
|
||||||
sb.AppendLine(" Detail: " + result.Detail);
|
private static bool IsRecoveryTransition(ProbeResult result, TargetState previousState)
|
||||||
if (!string.IsNullOrWhiteSpace(result.Host))
|
{
|
||||||
{
|
return result != null
|
||||||
sb.AppendLine(" Host: " + result.Host);
|
&& result.IsSuccess
|
||||||
}
|
&& previousState != null
|
||||||
if (result.Port.HasValue)
|
&& !previousState.IsUp;
|
||||||
{
|
}
|
||||||
sb.AppendLine(" Port: " + result.Port.Value);
|
|
||||||
}
|
private string BuildSubject(int failureCount, int recoveryCount)
|
||||||
sb.AppendLine(" Dauer(ms): " + Math.Max(0L, (long)result.Duration.TotalMilliseconds));
|
{
|
||||||
|
var prefix = string.IsNullOrWhiteSpace(_options.SubjectPrefix) ? "[HostAvailabilityMonitor]" : _options.SubjectPrefix.Trim();
|
||||||
TargetState state;
|
var envPart = "[" + _environmentName + "]";
|
||||||
if (currentState != null && currentState.TryGetValue(result.StateKey, out state) && state != null)
|
|
||||||
{
|
if (failureCount > 0 && recoveryCount > 0)
|
||||||
sb.AppendLine(" ConsecutiveFailures: " + state.ConsecutiveFailures);
|
{
|
||||||
sb.AppendLine(" LastChangedUtc: " + state.LastChangedUtc.ToString("yyyy-MM-dd HH:mm:ss") + "Z");
|
return string.Format("{0} {1} {2} Ausfall/Ausfälle, {3} Recovery/Recoveries", prefix, envPart, failureCount, recoveryCount);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
if (failureCount > 0)
|
||||||
}
|
{
|
||||||
|
return string.Format("{0} {1} {2} Ausfall/Ausfälle erkannt", prefix, envPart, failureCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format("{0} {1} {2} Endpoint(s) wieder erreichbar", prefix, envPart, recoveryCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildDailySummarySubject(DailySummaryEmailData summary)
|
||||||
|
{
|
||||||
|
var prefix = string.IsNullOrWhiteSpace(_options.SubjectPrefix) ? "[HostAvailabilityMonitor]" : _options.SubjectPrefix.Trim();
|
||||||
|
var envPart = "[" + _environmentName + "]";
|
||||||
|
return string.Format("{0} {1} Tagesstatus {2:yyyy-MM-dd}", prefix, envPart, summary.GeneratedAtLocal.LocalDateTime.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildBody(IReadOnlyCollection<ProbeResult> failures, IReadOnlyCollection<ProbeResult> recoveries, IDictionary<string, TargetState> currentState)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine(_monitorName);
|
||||||
|
sb.AppendLine("Umgebung / Environment: " + _environmentName);
|
||||||
|
sb.AppendLine("Server / Machine: " + Environment.MachineName);
|
||||||
|
sb.AppendLine("Zeitpunkt / Timestamp: " + DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
if (failures != null && failures.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("Fehlgeschlagene Checks / Failed checks:");
|
||||||
|
foreach (var failure in failures.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
AppendResult(sb, failure, currentState, "DOWN");
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recoveries != null && recoveries.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("Wieder erreichbare Checks / Recovered checks:");
|
||||||
|
foreach (var recovery in recoveries.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
AppendResult(sb, recovery, currentState, "DOWN -> UP");
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildDailySummaryBody(DailySummaryEmailData summary)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine(_monitorName + " - Tagesstatus / Daily Summary");
|
||||||
|
sb.AppendLine("Umgebung / Environment: " + _environmentName);
|
||||||
|
sb.AppendLine("Server / Machine: " + Environment.MachineName);
|
||||||
|
sb.AppendLine("Zusammenfassung bis / Summary until: " + summary.GeneratedAtLocal.ToString("yyyy-MM-dd HH:mm:ss zzz"));
|
||||||
|
sb.AppendLine("Zeitfenster / Window: " + summary.WindowStartLocal.ToString("yyyy-MM-dd HH:mm:ss zzz") + " bis " + summary.GeneratedAtLocal.ToString("yyyy-MM-dd HH:mm:ss zzz"));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("Checks des heutigen Tages / Today's checks:");
|
||||||
|
sb.AppendLine("- Gesamt / Total: " + summary.ProbesTodayTotal);
|
||||||
|
sb.AppendLine("- Erfolgreich / Successful: " + summary.ProbesTodaySuccess);
|
||||||
|
sb.AppendLine("- Fehlgeschlagen / Failed: " + summary.ProbesTodayFailure);
|
||||||
|
sb.AppendLine("- Übersprungen / Skipped: " + summary.ProbesTodaySkipped);
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("Aktueller Snapshot des letzten Laufs / Current snapshot of latest run:");
|
||||||
|
sb.AppendLine("- Targets gesamt / Total targets: " + summary.CurrentTargetsTotal);
|
||||||
|
sb.AppendLine("- Aktuell UP: " + summary.CurrentTargetsUp);
|
||||||
|
sb.AppendLine("- Aktuell DOWN: " + summary.CurrentTargetsDown);
|
||||||
|
sb.AppendLine("- Aktuell übersprungen / Currently skipped: " + summary.CurrentTargetsSkipped);
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
if (summary.CurrentFailures != null && summary.CurrentFailures.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("Aktuell nicht erreichbare Endpunkte / Currently unavailable endpoints:");
|
||||||
|
foreach (var failure in summary.CurrentFailures.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
AppendResult(sb, failure, summary.CurrentState, "Aktuell DOWN / Currently DOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine("Aktuell sind keine Endpunkte down / There are currently no unavailable endpoints.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendResult(StringBuilder sb, ProbeResult result, IDictionary<string, TargetState> currentState, string transitionText)
|
||||||
|
{
|
||||||
|
sb.AppendLine("- Anwendung / Application: " + Safe(result.ApplicationName));
|
||||||
|
sb.AppendLine(" Name: " + Safe(result.Name));
|
||||||
|
sb.AppendLine(" Endpoint: " + Safe(result.Endpoint));
|
||||||
|
sb.AppendLine(" Typ / Kind: " + result.Kind);
|
||||||
|
sb.AppendLine(" Transition: " + Safe(transitionText));
|
||||||
|
sb.AppendLine(" Status: " + Safe(result.StatusText));
|
||||||
|
sb.AppendLine(" Detail: " + Safe(result.Detail));
|
||||||
|
if (!string.IsNullOrWhiteSpace(result.Host))
|
||||||
|
{
|
||||||
|
sb.AppendLine(" Host: " + result.Host);
|
||||||
|
}
|
||||||
|
if (result.Port.HasValue)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" Port: " + result.Port.Value);
|
||||||
|
}
|
||||||
|
if (result.HttpStatusCode.HasValue)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" HTTP Status: " + result.HttpStatusCode.Value);
|
||||||
|
}
|
||||||
|
sb.AppendLine(" Dauer(ms) / Duration(ms): " + Math.Max(0L, (long)result.Duration.TotalMilliseconds));
|
||||||
|
|
||||||
|
TargetState state;
|
||||||
|
if (currentState != null && currentState.TryGetValue(result.StateKey, out state) && state != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" ConsecutiveFailures: " + state.ConsecutiveFailures);
|
||||||
|
sb.AppendLine(" LastChangedUtc: " + state.LastChangedUtc.ToString("yyyy-MM-dd HH:mm:ss") + "Z");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Safe(string value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? "n/a" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DailySummaryEmailData
|
||||||
|
{
|
||||||
|
public DateTimeOffset GeneratedAtLocal { get; set; }
|
||||||
|
public DateTimeOffset WindowStartLocal { get; set; }
|
||||||
|
public int ProbesTodayTotal { get; set; }
|
||||||
|
public int ProbesTodaySuccess { get; set; }
|
||||||
|
public int ProbesTodayFailure { get; set; }
|
||||||
|
public int ProbesTodaySkipped { get; set; }
|
||||||
|
public int CurrentTargetsTotal { get; set; }
|
||||||
|
public int CurrentTargetsUp { get; set; }
|
||||||
|
public int CurrentTargetsDown { get; set; }
|
||||||
|
public int CurrentTargetsSkipped { get; set; }
|
||||||
|
public List<ProbeResult> CurrentFailures { get; set; } = new List<ProbeResult>();
|
||||||
|
public IDictionary<string, TargetState> CurrentState { get; set; } = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,251 +1,387 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using HostAvailabilityMonitor.Configuration;
|
using HostAvailabilityMonitor.Configuration;
|
||||||
using HostAvailabilityMonitor.Logging;
|
using HostAvailabilityMonitor.Logging;
|
||||||
using HostAvailabilityMonitor.Monitoring;
|
using HostAvailabilityMonitor.Monitoring;
|
||||||
using HostAvailabilityMonitor.Notifications;
|
using HostAvailabilityMonitor.Notifications;
|
||||||
using HostAvailabilityMonitor.State;
|
using HostAvailabilityMonitor.State;
|
||||||
|
|
||||||
namespace HostAvailabilityMonitor
|
namespace HostAvailabilityMonitor
|
||||||
{
|
{
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
{
|
{
|
||||||
return MainAsync(args).GetAwaiter().GetResult();
|
return MainAsync(args).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<int> MainAsync(string[] args)
|
private static async Task<int> MainAsync(string[] args)
|
||||||
{
|
{
|
||||||
FileLogger logger = null;
|
FileLogger logger = null;
|
||||||
WindowsEventLogger eventLogger = null;
|
WindowsEventLogger eventLogger = null;
|
||||||
var nonZeroExitCodeOnExecutionError = true;
|
var nonZeroExitCodeOnExecutionError = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
|
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
|
||||||
? args[0]
|
? args[0]
|
||||||
: Path.Combine(baseDirectory, "appsettings.json");
|
: Path.Combine(baseDirectory, "appsettings.json");
|
||||||
|
|
||||||
var config = MonitorConfiguration.Load(configPath);
|
var config = MonitorConfiguration.Load(configPath);
|
||||||
config.Validate();
|
config.Validate();
|
||||||
nonZeroExitCodeOnExecutionError = config.Runtime.NonZeroExitCodeOnExecutionError;
|
nonZeroExitCodeOnExecutionError = config.Runtime.NonZeroExitCodeOnExecutionError;
|
||||||
|
|
||||||
var logDirectory = ResolvePath(baseDirectory, config.Logging.LogDirectory);
|
var logDirectory = ResolvePath(baseDirectory, config.Logging.LogDirectory);
|
||||||
var stateDirectory = ResolvePath(baseDirectory, config.Runtime.StateDirectory);
|
var stateDirectory = ResolvePath(baseDirectory, config.Runtime.StateDirectory);
|
||||||
var stateFilePath = Path.Combine(stateDirectory, "monitor-state.json");
|
var stateFilePath = Path.Combine(stateDirectory, "monitor-state.json");
|
||||||
|
|
||||||
logger = new FileLogger(logDirectory, config.Logging.FilePrefix, config.Logging.RetentionDays);
|
logger = new FileLogger(logDirectory, config.Logging.FilePrefix, config.Logging.RetentionDays, config.ApplicationName, config.EnvironmentName);
|
||||||
logger.CleanupOldFiles();
|
logger.CleanupOldFiles();
|
||||||
logger.Info(config.ApplicationName + " gestartet.");
|
logger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + ".");
|
||||||
logger.Info("Konfiguration geladen: " + Path.GetFullPath(configPath));
|
logger.Info("Konfiguration geladen: " + Path.GetFullPath(configPath));
|
||||||
|
|
||||||
eventLogger = new WindowsEventLogger(config.EventLog, logger);
|
eventLogger = new WindowsEventLogger(config.EventLog, logger);
|
||||||
eventLogger.TryInitialize();
|
eventLogger.TryInitialize();
|
||||||
eventLogger.Info(config.ApplicationName + " gestartet.", 1000);
|
eventLogger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000);
|
||||||
|
|
||||||
Directory.CreateDirectory(stateDirectory);
|
Directory.CreateDirectory(stateDirectory);
|
||||||
var stateStore = new MonitorStateStore(stateFilePath);
|
var stateStore = new MonitorStateStore(stateFilePath);
|
||||||
Dictionary<string, TargetState> previousState;
|
MonitorStateDocument stateDocument;
|
||||||
try
|
Dictionary<string, TargetState> previousState;
|
||||||
{
|
try
|
||||||
previousState = stateStore.Load();
|
{
|
||||||
}
|
stateDocument = stateStore.LoadDocument();
|
||||||
catch (Exception ex)
|
previousState = stateDocument.TargetStates;
|
||||||
{
|
}
|
||||||
logger.Warn("Statusdatei konnte nicht geladen werden. Es wird mit leerem Zustand fortgefahren. " + ex.GetType().Name + ": " + ex.Message);
|
catch (Exception ex)
|
||||||
previousState = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
|
{
|
||||||
}
|
logger.Warn("Statusdatei konnte nicht geladen werden. Es wird mit leerem Zustand fortgefahren. " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
stateDocument = new MonitorStateDocument();
|
||||||
var currentState = new Dictionary<string, TargetState>(previousState, StringComparer.OrdinalIgnoreCase);
|
previousState = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
|
||||||
var probeService = new EndpointProbeService(config.Runtime);
|
}
|
||||||
var enabledTargets = config.Targets.Where(t => t != null && t.Enabled).ToList();
|
|
||||||
|
var currentState = new Dictionary<string, TargetState>(previousState, StringComparer.OrdinalIgnoreCase);
|
||||||
if (enabledTargets.Count == 0)
|
var probeService = new EndpointProbeService(config.Runtime);
|
||||||
{
|
var enabledTargets = config.Targets.Where(t => t != null && t.Enabled).ToList();
|
||||||
logger.Warn("Es sind keine aktivierten Targets vorhanden. Der Lauf endet ohne Checks.");
|
|
||||||
eventLogger.Summary("HostAvailabilityMonitor ausgeführt, aber es waren keine aktivierten Targets vorhanden.", false);
|
if (enabledTargets.Count == 0)
|
||||||
return 0;
|
{
|
||||||
}
|
logger.Warn("Es sind keine aktivierten Targets vorhanden. Der Lauf endet ohne Checks.");
|
||||||
|
eventLogger.Summary(config.ApplicationName + " ausgeführt, aber es waren keine aktivierten Targets vorhanden. Umgebung=" + config.EnvironmentName + ".", false);
|
||||||
var results = await ExecuteChecksAsync(enabledTargets, probeService, config.Runtime.MaxConcurrency).ConfigureAwait(false);
|
return 0;
|
||||||
|
}
|
||||||
foreach (var result in results.OrderBy(r => r.Name, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
var results = await ExecuteChecksAsync(enabledTargets, probeService, config.Runtime.MaxConcurrency).ConfigureAwait(false);
|
||||||
logger.Probe(result);
|
|
||||||
TargetState previous;
|
foreach (var result in results.OrderBy(r => r.ApplicationName, StringComparer.OrdinalIgnoreCase).ThenBy(r => r.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
previousState.TryGetValue(result.StateKey, out previous);
|
{
|
||||||
UpdateState(currentState, result, previous);
|
logger.Probe(result);
|
||||||
|
TargetState previous;
|
||||||
if (!result.IsSuccess)
|
previousState.TryGetValue(result.StateKey, out previous);
|
||||||
{
|
var isRecovery = previous != null && !previous.IsUp && result.IsSuccess;
|
||||||
eventLogger.Warning("Check fehlgeschlagen: " + result.Name + " | " + result.Endpoint + " | " + result.Detail, 2000);
|
if (!result.IsSkipped)
|
||||||
}
|
{
|
||||||
else
|
UpdateState(currentState, result, previous);
|
||||||
{
|
}
|
||||||
eventLogger.InfoIfSuccessEnabled("Check erfolgreich: " + result.Name + " | " + result.Endpoint + " | " + result.Detail, 1001);
|
|
||||||
}
|
var eventMessage = BuildEventMessage(config, result);
|
||||||
}
|
if (result.IsSkipped)
|
||||||
|
{
|
||||||
var notifier = new EmailNotifier(config.Email);
|
logger.Warn("Target uebersprungen. Umgebung=" + config.EnvironmentName + ", Anwendung=" + result.ApplicationName + ", Name=" + result.Name + ", Endpoint=" + result.Endpoint + ", Detail=" + result.Detail + ".");
|
||||||
var failuresToNotify = new List<ProbeResult>();
|
eventLogger.Info("Target uebersprungen. " + eventMessage, 1100);
|
||||||
var recoveriesToNotify = new List<ProbeResult>();
|
}
|
||||||
var notifiedKeys = new List<string>();
|
else if (!result.IsSuccess)
|
||||||
|
{
|
||||||
foreach (var result in results)
|
eventLogger.Warning(eventMessage, 2000);
|
||||||
{
|
}
|
||||||
TargetState oldState;
|
else if (isRecovery)
|
||||||
previousState.TryGetValue(result.StateKey, out oldState);
|
{
|
||||||
if (!notifier.ShouldNotify(result, oldState))
|
logger.Info("Recovery erkannt. Umgebung=" + config.EnvironmentName + ", Anwendung=" + result.ApplicationName + ", Name=" + result.Name + ", Endpoint=" + result.Endpoint + ".");
|
||||||
{
|
eventLogger.Info("Recovery erkannt. " + eventMessage, 1201);
|
||||||
continue;
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
if (result.IsSuccess)
|
eventLogger.InfoIfSuccessEnabled(eventMessage, 1001);
|
||||||
{
|
}
|
||||||
recoveriesToNotify.Add(result);
|
}
|
||||||
}
|
|
||||||
else
|
stateDocument.TargetStates = currentState;
|
||||||
{
|
stateDocument.Metadata = stateDocument.Metadata ?? new MonitorStateMetadata();
|
||||||
failuresToNotify.Add(result);
|
|
||||||
}
|
var notifier = new EmailNotifier(config.Email, config.ApplicationName, config.EnvironmentName);
|
||||||
|
var failuresToNotify = new List<ProbeResult>();
|
||||||
notifiedKeys.Add(result.StateKey);
|
var recoveriesToNotify = new List<ProbeResult>();
|
||||||
}
|
var notifiedKeys = new List<string>();
|
||||||
|
|
||||||
if (notifier.IsEnabled)
|
foreach (var result in results)
|
||||||
{
|
{
|
||||||
try
|
TargetState oldState;
|
||||||
{
|
previousState.TryGetValue(result.StateKey, out oldState);
|
||||||
await notifier.SendAsync(failuresToNotify, recoveriesToNotify, currentState, CancellationToken.None).ConfigureAwait(false);
|
if (!notifier.ShouldNotify(result, oldState))
|
||||||
if (failuresToNotify.Count > 0 || recoveriesToNotify.Count > 0)
|
{
|
||||||
{
|
continue;
|
||||||
foreach (var key in notifiedKeys)
|
}
|
||||||
{
|
|
||||||
TargetState stateEntry;
|
if (result.IsSuccess)
|
||||||
if (currentState.TryGetValue(key, out stateEntry) && stateEntry != null)
|
{
|
||||||
{
|
recoveriesToNotify.Add(result);
|
||||||
stateEntry.LastNotificationUtc = DateTime.UtcNow;
|
}
|
||||||
}
|
else
|
||||||
}
|
{
|
||||||
|
failuresToNotify.Add(result);
|
||||||
logger.Info("Benachrichtigungs-E-Mail versendet. Failures=" + failuresToNotify.Count + ", Recoveries=" + recoveriesToNotify.Count);
|
}
|
||||||
}
|
|
||||||
}
|
notifiedKeys.Add(result.StateKey);
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
|
||||||
logger.Error("E-Mail-Versand fehlgeschlagen: " + ex.GetType().Name + ": " + ex.Message);
|
if (notifier.IsEnabled)
|
||||||
eventLogger.Error("E-Mail-Versand fehlgeschlagen: " + ex.Message, 9001);
|
{
|
||||||
}
|
try
|
||||||
}
|
{
|
||||||
|
await notifier.SendAsync(failuresToNotify, recoveriesToNotify, currentState, CancellationToken.None).ConfigureAwait(false);
|
||||||
stateStore.Save(currentState);
|
if (failuresToNotify.Count > 0 || recoveriesToNotify.Count > 0)
|
||||||
|
{
|
||||||
var total = results.Count;
|
foreach (var key in notifiedKeys)
|
||||||
var ok = results.Count(r => r.IsSuccess);
|
{
|
||||||
var failed = total - ok;
|
TargetState stateEntry;
|
||||||
var summary = "Lauf beendet. Gesamt=" + total + ", Erfolgreich=" + ok + ", Fehlgeschlagen=" + failed + ".";
|
if (currentState.TryGetValue(key, out stateEntry) && stateEntry != null)
|
||||||
logger.Info(summary);
|
{
|
||||||
eventLogger.Summary(summary, failed > 0);
|
stateEntry.LastNotificationUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
return failed > 0 && config.Runtime.NonZeroExitCodeOnAnyFailure ? 2 : 0;
|
}
|
||||||
}
|
|
||||||
catch (Exception ex)
|
logger.Info("Benachrichtigungs-E-Mail versendet. Umgebung=" + config.EnvironmentName + ", Failures=" + failuresToNotify.Count + ", Recoveries=" + recoveriesToNotify.Count + ".");
|
||||||
{
|
}
|
||||||
if (logger != null)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
|
{
|
||||||
}
|
logger.Error("E-Mail-Versand fehlgeschlagen: " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
eventLogger.Error("E-Mail-Versand fehlgeschlagen. Umgebung=" + config.EnvironmentName + ". Fehler=" + ex.Message, 9001);
|
||||||
if (eventLogger != null)
|
}
|
||||||
{
|
}
|
||||||
eventLogger.Error("Fataler Fehler: " + ex.Message, 9000);
|
|
||||||
}
|
await TrySendDailySummaryAsync(config, notifier, stateDocument, currentState, results, logDirectory, logger, eventLogger).ConfigureAwait(false);
|
||||||
|
stateStore.Save(stateDocument);
|
||||||
return nonZeroExitCodeOnExecutionError ? 1 : 0;
|
|
||||||
}
|
var total = results.Count(r => !r.IsSkipped);
|
||||||
}
|
var ok = results.Count(r => r.IsSuccess);
|
||||||
|
var failed = results.Count(r => !r.IsSkipped && !r.IsSuccess);
|
||||||
private static async Task<List<ProbeResult>> ExecuteChecksAsync(IReadOnlyCollection<TargetOptions> targets, EndpointProbeService probeService, int maxConcurrency)
|
var skipped = results.Count(r => r.IsSkipped);
|
||||||
{
|
var summary = config.ApplicationName + " Lauf beendet. Umgebung=" + config.EnvironmentName + ", Gesamt=" + total + ", Erfolgreich=" + ok + ", Fehlgeschlagen=" + failed + ", Uebersprungen=" + skipped + ".";
|
||||||
var results = new List<ProbeResult>();
|
logger.Info(summary);
|
||||||
using (var semaphore = new SemaphoreSlim(Math.Max(1, maxConcurrency)))
|
eventLogger.Summary(summary, failed > 0);
|
||||||
{
|
|
||||||
var tasks = targets.Select(async target =>
|
return failed > 0 && config.Runtime.NonZeroExitCodeOnAnyFailure ? 2 : 0;
|
||||||
{
|
}
|
||||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
catch (Exception ex)
|
||||||
try
|
{
|
||||||
{
|
if (logger != null)
|
||||||
return await probeService.ProbeAsync(target, CancellationToken.None).ConfigureAwait(false);
|
{
|
||||||
}
|
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
|
||||||
finally
|
}
|
||||||
{
|
|
||||||
semaphore.Release();
|
if (eventLogger != null)
|
||||||
}
|
{
|
||||||
}).ToArray();
|
eventLogger.Error("Fataler Fehler: " + ex.Message, 9000);
|
||||||
|
}
|
||||||
var completed = await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
||||||
results.AddRange(completed);
|
return nonZeroExitCodeOnExecutionError ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return results;
|
|
||||||
}
|
private static async Task<List<ProbeResult>> ExecuteChecksAsync(IReadOnlyCollection<TargetOptions> targets, EndpointProbeService probeService, int maxConcurrency)
|
||||||
|
{
|
||||||
private static void UpdateState(Dictionary<string, TargetState> currentState, ProbeResult result, TargetState previousState)
|
var results = new List<ProbeResult>();
|
||||||
{
|
using (var semaphore = new SemaphoreSlim(Math.Max(1, maxConcurrency)))
|
||||||
var nowUtc = DateTime.UtcNow;
|
{
|
||||||
TargetState state;
|
var tasks = targets.Select(async target =>
|
||||||
|
{
|
||||||
if (previousState == null)
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
{
|
try
|
||||||
state = new TargetState
|
{
|
||||||
{
|
return await probeService.ProbeAsync(target, CancellationToken.None).ConfigureAwait(false);
|
||||||
IsUp = result.IsSuccess,
|
}
|
||||||
LastCheckedUtc = nowUtc,
|
finally
|
||||||
LastChangedUtc = nowUtc,
|
{
|
||||||
ConsecutiveFailures = result.IsSuccess ? 0 : 1,
|
semaphore.Release();
|
||||||
LastDetail = result.Detail
|
}
|
||||||
};
|
}).ToArray();
|
||||||
}
|
|
||||||
else
|
var completed = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
{
|
results.AddRange(completed);
|
||||||
state = new TargetState
|
}
|
||||||
{
|
|
||||||
IsUp = previousState.IsUp,
|
return results;
|
||||||
LastCheckedUtc = nowUtc,
|
}
|
||||||
LastChangedUtc = previousState.LastChangedUtc,
|
|
||||||
ConsecutiveFailures = previousState.ConsecutiveFailures,
|
private static async Task TrySendDailySummaryAsync(
|
||||||
LastNotificationUtc = previousState.LastNotificationUtc,
|
MonitorConfiguration config,
|
||||||
LastDetail = result.Detail
|
EmailNotifier notifier,
|
||||||
};
|
MonitorStateDocument stateDocument,
|
||||||
}
|
IDictionary<string, TargetState> currentState,
|
||||||
|
IReadOnlyCollection<ProbeResult> results,
|
||||||
if (previousState == null || previousState.IsUp != result.IsSuccess)
|
string logDirectory,
|
||||||
{
|
FileLogger logger,
|
||||||
state.LastChangedUtc = nowUtc;
|
WindowsEventLogger eventLogger)
|
||||||
}
|
{
|
||||||
|
if (config == null || notifier == null || stateDocument == null)
|
||||||
state.IsUp = result.IsSuccess;
|
{
|
||||||
state.LastCheckedUtc = nowUtc;
|
return;
|
||||||
state.LastDetail = result.Detail;
|
}
|
||||||
state.ConsecutiveFailures = result.IsSuccess ? 0 : Math.Max(1, (previousState == null ? 0 : previousState.ConsecutiveFailures) + 1);
|
|
||||||
|
var nowLocal = DateTime.Now;
|
||||||
currentState[result.StateKey] = state;
|
if (!notifier.ShouldSendDailySummary(stateDocument.Metadata, nowLocal))
|
||||||
}
|
{
|
||||||
|
return;
|
||||||
private static string ResolvePath(string baseDirectory, string configuredPath)
|
}
|
||||||
{
|
|
||||||
if (Path.IsPathRooted(configuredPath))
|
try
|
||||||
{
|
{
|
||||||
return configuredPath;
|
var counters = ReadTodayProbeCounters(logDirectory, config.Logging.FilePrefix, nowLocal, logger);
|
||||||
}
|
var latestResults = results == null ? new List<ProbeResult>() : results.ToList();
|
||||||
|
var currentFailures = latestResults.Where(r => !r.IsSkipped && !r.IsSuccess).ToList();
|
||||||
return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath));
|
var summary = new DailySummaryEmailData
|
||||||
}
|
{
|
||||||
}
|
GeneratedAtLocal = DateTimeOffset.Now,
|
||||||
}
|
WindowStartLocal = new DateTimeOffset(nowLocal.Date),
|
||||||
|
ProbesTodaySuccess = counters.SuccessCount,
|
||||||
|
ProbesTodayFailure = counters.FailureCount,
|
||||||
|
ProbesTodaySkipped = counters.SkipCount,
|
||||||
|
ProbesTodayTotal = counters.SuccessCount + counters.FailureCount + counters.SkipCount,
|
||||||
|
CurrentTargetsTotal = latestResults.Count,
|
||||||
|
CurrentTargetsUp = latestResults.Count(r => r.IsSuccess),
|
||||||
|
CurrentTargetsDown = latestResults.Count(r => !r.IsSkipped && !r.IsSuccess),
|
||||||
|
CurrentTargetsSkipped = latestResults.Count(r => r.IsSkipped),
|
||||||
|
CurrentFailures = currentFailures,
|
||||||
|
CurrentState = currentState
|
||||||
|
};
|
||||||
|
|
||||||
|
await notifier.SendDailySummaryAsync(summary, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
stateDocument.Metadata.LastDailySummarySentLocalDate = nowLocal.ToString("yyyy-MM-dd");
|
||||||
|
logger.Info("Tagesstatus-E-Mail versendet. Umgebung=" + config.EnvironmentName + ", Versandzeitpunkt=" + nowLocal.ToString("yyyy-MM-dd HH:mm:ss") + ", AktuellDown=" + summary.CurrentTargetsDown + ", AktuellUebersprungen=" + summary.CurrentTargetsSkipped + ", TagesChecks=" + summary.ProbesTodayTotal + ".");
|
||||||
|
eventLogger.Info("Tagesstatus-E-Mail versendet. Umgebung=" + config.EnvironmentName + ", AktuellDown=" + summary.CurrentTargetsDown + ", AktuellUebersprungen=" + summary.CurrentTargetsSkipped + ", TagesChecks=" + summary.ProbesTodayTotal + ".", 1300);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error("Tagesstatus-E-Mail konnte nicht versendet werden: " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
eventLogger.Error("Tagesstatus-E-Mail fehlgeschlagen. Umgebung=" + config.EnvironmentName + ". Fehler=" + ex.Message, 9002);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProbeLogCounters ReadTodayProbeCounters(string logDirectory, string filePrefix, DateTime nowLocal, FileLogger logger)
|
||||||
|
{
|
||||||
|
var counters = new ProbeLogCounters();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var logPath = Path.Combine(logDirectory, filePrefix + "-" + nowLocal.ToString("yyyy-MM-dd") + ".log");
|
||||||
|
if (!File.Exists(logPath))
|
||||||
|
{
|
||||||
|
return counters;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var line in File.ReadLines(logPath))
|
||||||
|
{
|
||||||
|
if (line.IndexOf("Monitor=", StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.IndexOf("| SUCCESS |", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
counters.SuccessCount++;
|
||||||
|
}
|
||||||
|
else if (line.IndexOf("| FAIL |", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
counters.FailureCount++;
|
||||||
|
}
|
||||||
|
else if (line.IndexOf("| SKIP |", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
counters.SkipCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Warn("Tageslog konnte für die Status-Mail nicht ausgewertet werden. Es wird mit 0 Tageszählern fortgefahren. " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return counters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateState(Dictionary<string, TargetState> currentState, ProbeResult result, TargetState previousState)
|
||||||
|
{
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
TargetState state;
|
||||||
|
|
||||||
|
if (previousState == null)
|
||||||
|
{
|
||||||
|
state = new TargetState
|
||||||
|
{
|
||||||
|
IsUp = result.IsSuccess,
|
||||||
|
LastCheckedUtc = nowUtc,
|
||||||
|
LastChangedUtc = nowUtc,
|
||||||
|
ConsecutiveFailures = result.IsSuccess ? 0 : 1,
|
||||||
|
LastDetail = result.Detail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state = new TargetState
|
||||||
|
{
|
||||||
|
IsUp = previousState.IsUp,
|
||||||
|
LastCheckedUtc = nowUtc,
|
||||||
|
LastChangedUtc = previousState.LastChangedUtc,
|
||||||
|
ConsecutiveFailures = previousState.ConsecutiveFailures,
|
||||||
|
LastNotificationUtc = previousState.LastNotificationUtc,
|
||||||
|
LastDetail = result.Detail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousState == null || previousState.IsUp != result.IsSuccess)
|
||||||
|
{
|
||||||
|
state.LastChangedUtc = nowUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.IsUp = result.IsSuccess;
|
||||||
|
state.LastCheckedUtc = nowUtc;
|
||||||
|
state.LastDetail = result.Detail;
|
||||||
|
state.ConsecutiveFailures = result.IsSuccess ? 0 : Math.Max(1, (previousState == null ? 0 : previousState.ConsecutiveFailures) + 1);
|
||||||
|
|
||||||
|
currentState[result.StateKey] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolvePath(string baseDirectory, string configuredPath)
|
||||||
|
{
|
||||||
|
if (Path.IsPathRooted(configuredPath))
|
||||||
|
{
|
||||||
|
return configuredPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildEventMessage(MonitorConfiguration config, ProbeResult result)
|
||||||
|
{
|
||||||
|
return string.Format(
|
||||||
|
"Umgebung={0} | Anwendung={1} | Name={2} | Endpoint={3} | Status={4} | Detail={5}",
|
||||||
|
config.EnvironmentName,
|
||||||
|
result.ApplicationName,
|
||||||
|
result.Name,
|
||||||
|
result.Endpoint,
|
||||||
|
result.StatusText,
|
||||||
|
result.Detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ProbeLogCounters
|
||||||
|
{
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
public int FailureCount { get; set; }
|
||||||
|
public int SkipCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
[assembly: AssemblyTitle("HostAvailabilityMonitor")]
|
[assembly: AssemblyTitle("HostAvailabilityMonitor")]
|
||||||
[assembly: AssemblyDescription("Windows Server 2019 kompatibler Host Availability Monitor auf .NET Framework 4.8")]
|
[assembly: AssemblyDescription("Windows Server 2019 kompatibler Host Availability Monitor auf .NET Framework 4.7.2")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: AssemblyCompany("JR IT Services")]
|
[assembly: AssemblyCompany("JR IT Services")]
|
||||||
[assembly: AssemblyProduct("HostAvailabilityMonitor")]
|
[assembly: AssemblyProduct("HostAvailabilityMonitor")]
|
||||||
[assembly: AssemblyCopyright("Copyright © JR IT Services")]
|
[assembly: AssemblyCopyright("Copyright © JR IT Services")]
|
||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
[assembly: AssemblyCulture("")]
|
[assembly: AssemblyCulture("")]
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
[assembly: Guid("8f55f0bf-6041-4367-93a6-777387f436bf")]
|
[assembly: Guid("8f55f0bf-6041-4367-93a6-777387f436bf")]
|
||||||
[assembly: AssemblyVersion("1.0.0.0")]
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
|
|||||||
@@ -1,76 +1,139 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Web.Script.Serialization;
|
using System.Web.Script.Serialization;
|
||||||
|
|
||||||
namespace HostAvailabilityMonitor.State
|
namespace HostAvailabilityMonitor.State
|
||||||
{
|
{
|
||||||
public sealed class MonitorStateStore
|
public sealed class MonitorStateStore
|
||||||
{
|
{
|
||||||
private readonly string _filePath;
|
private readonly string _filePath;
|
||||||
|
|
||||||
public MonitorStateStore(string filePath)
|
public MonitorStateStore(string filePath)
|
||||||
{
|
{
|
||||||
_filePath = filePath;
|
_filePath = filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, TargetState> Load()
|
public MonitorStateDocument LoadDocument()
|
||||||
{
|
{
|
||||||
if (!File.Exists(_filePath))
|
if (!File.Exists(_filePath))
|
||||||
{
|
{
|
||||||
return new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
|
return new MonitorStateDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = File.ReadAllText(_filePath);
|
var json = File.ReadAllText(_filePath);
|
||||||
var serializer = new JavaScriptSerializer();
|
var serializer = new JavaScriptSerializer();
|
||||||
var states = serializer.Deserialize<Dictionary<string, TargetState>>(json);
|
|
||||||
|
try
|
||||||
if (states == null)
|
{
|
||||||
{
|
var document = serializer.Deserialize<MonitorStateDocument>(json);
|
||||||
return new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
|
if (document != null && document.TargetStates != null)
|
||||||
}
|
{
|
||||||
|
return Normalize(document);
|
||||||
return new Dictionary<string, TargetState>(states, StringComparer.OrdinalIgnoreCase);
|
}
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
public void Save(Dictionary<string, TargetState> states)
|
{
|
||||||
{
|
// Fall back to legacy format.
|
||||||
var directory = Path.GetDirectoryName(_filePath);
|
}
|
||||||
if (string.IsNullOrWhiteSpace(directory))
|
|
||||||
{
|
try
|
||||||
throw new InvalidOperationException("Das Verzeichnis der Statusdatei konnte nicht ermittelt werden.");
|
{
|
||||||
}
|
var states = serializer.Deserialize<Dictionary<string, TargetState>>(json);
|
||||||
|
return new MonitorStateDocument
|
||||||
Directory.CreateDirectory(directory);
|
{
|
||||||
|
TargetStates = states == null
|
||||||
var serializer = new JavaScriptSerializer();
|
? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase)
|
||||||
var json = serializer.Serialize(states ?? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase));
|
: new Dictionary<string, TargetState>(states, StringComparer.OrdinalIgnoreCase),
|
||||||
var tempFile = _filePath + ".tmp";
|
Metadata = new MonitorStateMetadata()
|
||||||
File.WriteAllText(tempFile, json);
|
};
|
||||||
|
}
|
||||||
if (File.Exists(_filePath))
|
catch
|
||||||
{
|
{
|
||||||
var backupFile = _filePath + ".bak";
|
return new MonitorStateDocument();
|
||||||
File.Replace(tempFile, _filePath, backupFile, true);
|
}
|
||||||
if (File.Exists(backupFile))
|
}
|
||||||
{
|
|
||||||
File.Delete(backupFile);
|
public Dictionary<string, TargetState> Load()
|
||||||
}
|
{
|
||||||
}
|
return LoadDocument().TargetStates;
|
||||||
else
|
}
|
||||||
{
|
|
||||||
File.Move(tempFile, _filePath);
|
public void Save(MonitorStateDocument document)
|
||||||
}
|
{
|
||||||
}
|
var directory = Path.GetDirectoryName(_filePath);
|
||||||
}
|
if (string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
public sealed class TargetState
|
throw new InvalidOperationException("Das Verzeichnis der Statusdatei konnte nicht ermittelt werden.");
|
||||||
{
|
}
|
||||||
public bool IsUp { get; set; }
|
|
||||||
public int ConsecutiveFailures { get; set; }
|
Directory.CreateDirectory(directory);
|
||||||
public DateTime LastCheckedUtc { get; set; }
|
|
||||||
public DateTime LastChangedUtc { get; set; }
|
var normalized = Normalize(document ?? new MonitorStateDocument());
|
||||||
public DateTime? LastNotificationUtc { get; set; }
|
var serializer = new JavaScriptSerializer();
|
||||||
public string LastDetail { get; set; } = string.Empty;
|
var json = serializer.Serialize(normalized);
|
||||||
}
|
var tempFile = _filePath + ".tmp";
|
||||||
}
|
File.WriteAllText(tempFile, json);
|
||||||
|
|
||||||
|
if (File.Exists(_filePath))
|
||||||
|
{
|
||||||
|
var backupFile = _filePath + ".bak";
|
||||||
|
File.Replace(tempFile, _filePath, backupFile, true);
|
||||||
|
if (File.Exists(backupFile))
|
||||||
|
{
|
||||||
|
File.Delete(backupFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(tempFile, _filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(Dictionary<string, TargetState> states)
|
||||||
|
{
|
||||||
|
Save(new MonitorStateDocument
|
||||||
|
{
|
||||||
|
TargetStates = states == null
|
||||||
|
? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
: new Dictionary<string, TargetState>(states, StringComparer.OrdinalIgnoreCase),
|
||||||
|
Metadata = new MonitorStateMetadata()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MonitorStateDocument Normalize(MonitorStateDocument document)
|
||||||
|
{
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
document = new MonitorStateDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.TargetStates = document.TargetStates == null
|
||||||
|
? new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
: new Dictionary<string, TargetState>(document.TargetStates, StringComparer.OrdinalIgnoreCase);
|
||||||
|
document.Metadata = document.Metadata ?? new MonitorStateMetadata();
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MonitorStateDocument
|
||||||
|
{
|
||||||
|
public Dictionary<string, TargetState> TargetStates { get; set; } = new Dictionary<string, TargetState>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public MonitorStateMetadata Metadata { get; set; } = new MonitorStateMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MonitorStateMetadata
|
||||||
|
{
|
||||||
|
public string LastDailySummarySentLocalDate { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TargetState
|
||||||
|
{
|
||||||
|
public bool IsUp { get; set; }
|
||||||
|
public int ConsecutiveFailures { get; set; }
|
||||||
|
public DateTime LastCheckedUtc { get; set; }
|
||||||
|
public DateTime LastChangedUtc { get; set; }
|
||||||
|
public DateTime? LastNotificationUtc { get; set; }
|
||||||
|
public string LastDetail { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,70 +1,92 @@
|
|||||||
{
|
{
|
||||||
"ApplicationName": "HostAvailabilityMonitor",
|
"ApplicationName": "HostAvailabilityMonitor",
|
||||||
"Runtime": {
|
"EnvironmentName": "HIP Produktion",
|
||||||
"MaxConcurrency": 4,
|
"Runtime": {
|
||||||
"DefaultValidateUncPathAccess": false,
|
"MaxConcurrency": 4,
|
||||||
"NonZeroExitCodeOnAnyFailure": false,
|
"DefaultValidateUncPathAccess": false,
|
||||||
"NonZeroExitCodeOnExecutionError": true,
|
"NonZeroExitCodeOnAnyFailure": false,
|
||||||
"StateDirectory": "state"
|
"NonZeroExitCodeOnExecutionError": true,
|
||||||
},
|
"StateDirectory": "state"
|
||||||
"Logging": {
|
},
|
||||||
"LogDirectory": "logs",
|
"Logging": {
|
||||||
"FilePrefix": "host-availability-monitor",
|
"LogDirectory": "logs",
|
||||||
"RetentionDays": 5
|
"FilePrefix": "host-availability-monitor",
|
||||||
},
|
"RetentionDays": 5
|
||||||
"EventLog": {
|
},
|
||||||
"Enabled": true,
|
"EventLog": {
|
||||||
"LogName": "Application",
|
"Enabled": true,
|
||||||
"Source": "HostAvailabilityMonitor",
|
"LogName": "Application",
|
||||||
"CreateSourceIfMissing": false,
|
"Source": "HostAvailabilityMonitor",
|
||||||
"WriteSuccessEntries": false,
|
"CreateSourceIfMissing": false,
|
||||||
"WriteSummaryEntries": true
|
"WriteSuccessEntries": false,
|
||||||
},
|
"WriteSummaryEntries": true
|
||||||
"Email": {
|
},
|
||||||
"Enabled": false,
|
"Email": {
|
||||||
"SmtpHost": "smtp.example.local",
|
"Enabled": false,
|
||||||
"SmtpPort": 25,
|
"SmtpHost": "smtp.example.local",
|
||||||
"UseSsl": false,
|
"SmtpPort": 25,
|
||||||
"Username": "",
|
"UseSsl": false,
|
||||||
"Password": "",
|
"UseDefaultCredentials": false,
|
||||||
"From": "monitor@example.local",
|
"Username": "",
|
||||||
"To": [
|
"Password": "",
|
||||||
"operations@example.local"
|
"From": "monitor@example.local",
|
||||||
],
|
"To": [
|
||||||
"SubjectPrefix": "[HostAvailabilityMonitor]",
|
"operations@example.local",
|
||||||
"SendOnFailure": true,
|
"integration-oncall@example.local"
|
||||||
"SendOnRecovery": true,
|
],
|
||||||
"OnlyOnStateChange": true,
|
"Cc": [
|
||||||
"CooldownMinutes": 0,
|
"integration-leads@example.local"
|
||||||
"TimeoutSeconds": 30
|
],
|
||||||
},
|
"Bcc": [],
|
||||||
"Targets": [
|
"SubjectPrefix": "[HostAvailabilityMonitor]",
|
||||||
{
|
"SendOnFailure": true,
|
||||||
"Name": "Internes Fileshare 1",
|
"SendOnRecovery": true,
|
||||||
"Endpoint": "\\\\internerserver1\\share1",
|
"SendDailySummary": true,
|
||||||
"TimeoutSeconds": 8,
|
"DailySummaryHourLocal": 14,
|
||||||
"ValidateUncPathAccess": false,
|
"DailySummaryMinuteLocal": 0,
|
||||||
"Enabled": true
|
"OnlyOnStateChange": true,
|
||||||
},
|
"CooldownMinutes": 0,
|
||||||
{
|
"TimeoutSeconds": 30
|
||||||
"Name": "Externe Webseite",
|
},
|
||||||
"Endpoint": "https://host1.de/url",
|
"Targets": [
|
||||||
"TimeoutSeconds": 10,
|
{
|
||||||
"HttpMethod": "HEAD",
|
"Name": "Internes Fileshare 1",
|
||||||
"Enabled": true
|
"ApplicationName": "HIP FileTransfer",
|
||||||
},
|
"Endpoint": "\\\\internerserver1\\share1",
|
||||||
{
|
"TimeoutSeconds": 8,
|
||||||
"Name": "SFTP Partner A",
|
"ValidateUncPathAccess": false,
|
||||||
"Endpoint": "sftp://partner-sftp.example.local/inbound",
|
"Enabled": true
|
||||||
"TimeoutSeconds": 10,
|
},
|
||||||
"Port": 22,
|
{
|
||||||
"Enabled": true
|
"Name": "Externe Webseite",
|
||||||
},
|
"ApplicationName": "HIP WebPortal",
|
||||||
{
|
"Endpoint": "https://host1.de/url",
|
||||||
"Name": "Custom TCP Service",
|
"TimeoutSeconds": 10,
|
||||||
"Endpoint": "tcp://host2.example.local:8443",
|
"HttpMethod": "HEAD",
|
||||||
"TimeoutSeconds": 10,
|
"Enabled": true
|
||||||
"Enabled": true
|
},
|
||||||
}
|
{
|
||||||
]
|
"Name": "SFTP Partner A",
|
||||||
}
|
"ApplicationName": "HIP Partneranbindung",
|
||||||
|
"Endpoint": "sftp://partner-sftp.example.local/inbound",
|
||||||
|
"TimeoutSeconds": 10,
|
||||||
|
"Port": 22,
|
||||||
|
"Enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Network File URI",
|
||||||
|
"ApplicationName": "HIP Dokumentenservice",
|
||||||
|
"Endpoint": "file://fileserver01/share/export",
|
||||||
|
"TimeoutSeconds": 10,
|
||||||
|
"ValidateUncPathAccess": false,
|
||||||
|
"Enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Custom TCP Service",
|
||||||
|
"ApplicationName": "HIP Adapterdienst",
|
||||||
|
"Endpoint": "tcp://host2.example.local:8443",
|
||||||
|
"TimeoutSeconds": 10,
|
||||||
|
"Enabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user