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,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": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user