Updated logging plus more resilient coding to allow better run experience

This commit is contained in:
2026-06-29 13:34:05 +02:00
parent 105f5ac7d5
commit 2dab6f970a
19 changed files with 2052 additions and 60 deletions
@@ -9,12 +9,32 @@ using Microsoft.BizTalk.ExplorerOM;
namespace BizTalkMonitorConfigGenerator.BizTalk
{
/// <summary>
/// Discovers monitorable endpoints from the BizTalk management catalog.
/// </summary>
public sealed class BizTalkCatalogDiscoveryService
{
/// <summary>
/// BizTalk discovery options.
/// </summary>
private readonly BizTalkOptions _options;
/// <summary>
/// Output defaults used while normalizing endpoints.
/// </summary>
private readonly OutputOptions _outputOptions;
/// <summary>
/// Logger used for discovery decisions.
/// </summary>
private readonly GeneratorFileLogger _logger;
/// <summary>
/// Initializes a new BizTalk catalog discovery service.
/// </summary>
/// <param name="options">BizTalk discovery options.</param>
/// <param name="outputOptions">Output defaults for generated targets.</param>
/// <param name="logger">Discovery logger.</param>
public BizTalkCatalogDiscoveryService(BizTalkOptions options, OutputOptions outputOptions, GeneratorFileLogger logger)
{
_options = options ?? new BizTalkOptions();
@@ -22,10 +42,17 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
_logger = logger;
}
/// <summary>
/// Discovers all configured BizTalk artifact endpoints and returns monitor-compatible endpoints.
/// </summary>
/// <returns>The discovered endpoints sorted by application, target name and endpoint.</returns>
public IReadOnlyCollection<DiscoveredEndpoint> Discover()
{
var discovered = new List<DiscoveredEndpoint>();
var inspectedApplications = 0;
var skippedApplications = 0;
_logger.Info("BizTalk-Katalog wird geöffnet. IncludeSendPorts=" + _options.IncludeSendPorts + ", IncludeReceiveLocations=" + _options.IncludeReceiveLocations + ".");
var catalog = new BtsCatalogExplorer();
catalog.ConnectionString = _options.ConnectionString;
@@ -38,10 +65,14 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
if (ShouldSkipApplication(application))
{
skippedApplications++;
_logger.Info("BizTalk-Anwendung übersprungen: " + application.Name);
continue;
}
inspectedApplications++;
_logger.Info("BizTalk-Anwendung wird analysiert: " + application.Name);
if (_options.IncludeSendPorts)
{
DiscoverSendPorts(application, discovered);
@@ -53,6 +84,8 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
}
}
_logger.Info("BizTalk-Discovery beendet. AnalysierteAnwendungen=" + inspectedApplications + ", UebersprungeneAnwendungen=" + skippedApplications + ", UnterstuetzteTargets=" + discovered.Count + ".");
return discovered
.OrderBy(x => x.MappedApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.TargetName, StringComparer.OrdinalIgnoreCase)
@@ -60,6 +93,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
.ToList();
}
/// <summary>
/// Discovers send port transports from one BizTalk application.
/// </summary>
/// <param name="application">The BizTalk application to inspect.</param>
/// <param name="discovered">The target collection receiving supported endpoints.</param>
private void DiscoverSendPorts(Application application, List<DiscoveredEndpoint> discovered)
{
foreach (SendPort sendPort in application.SendPorts)
@@ -90,6 +128,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
}
}
/// <summary>
/// Discovers receive locations from one BizTalk application.
/// </summary>
/// <param name="application">The BizTalk application to inspect.</param>
/// <param name="discovered">The target collection receiving supported endpoints.</param>
private void DiscoverReceiveLocations(Application application, List<DiscoveredEndpoint> discovered)
{
foreach (ReceivePort receivePort in application.ReceivePorts)
@@ -131,6 +174,16 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
}
}
/// <summary>
/// Adds a send port transport when it exists and can be normalized.
/// </summary>
/// <param name="discovered">The target collection receiving supported endpoints.</param>
/// <param name="bizTalkApplicationName">The source BizTalk application name.</param>
/// <param name="artifactType">The artifact type label.</param>
/// <param name="targetNameBase">The base name for the generated target.</param>
/// <param name="artifactName">The source artifact name.</param>
/// <param name="transport">The BizTalk transport information.</param>
/// <param name="isSecondary">True when the transport is a secondary send port transport.</param>
private void AddTransportIfSupported(List<DiscoveredEndpoint> discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, TransportInfo transport, bool isSecondary)
{
if (transport == null)
@@ -150,6 +203,17 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
isSecondary);
}
/// <summary>
/// Adds a discovered endpoint when artifact filters and endpoint normalization allow it.
/// </summary>
/// <param name="discovered">The target collection receiving supported endpoints.</param>
/// <param name="bizTalkApplicationName">The source BizTalk application name.</param>
/// <param name="artifactType">The artifact type label.</param>
/// <param name="targetNameBase">The base name for the generated target.</param>
/// <param name="artifactName">The source artifact name.</param>
/// <param name="adapterName">The BizTalk adapter name.</param>
/// <param name="sourceAddress">The raw address read from BizTalk.</param>
/// <param name="isSecondary">True when the endpoint is from a secondary transport.</param>
private void AddEndpointIfSupported(List<DiscoveredEndpoint> discovered, string bizTalkApplicationName, string artifactType, string targetNameBase, string artifactName, string adapterName, string sourceAddress, bool isSecondary)
{
if (ShouldSkipArtifact(artifactName))
@@ -190,6 +254,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
_logger.Discovery("ADD", artifactType, bizTalkApplicationName, artifactName, adapterName, sourceAddress, normalizedEndpoint, "Zuordnung zu ApplicationName='" + mappedApplication + "'.");
}
/// <summary>
/// Determines whether a BizTalk application should be skipped.
/// </summary>
/// <param name="application">The BizTalk application to inspect.</param>
/// <returns>True when the application should be skipped.</returns>
private bool ShouldSkipApplication(Application application)
{
if (application == null)
@@ -205,6 +274,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return _options.IgnoreApplications.Any(pattern => WildcardMatch(application.Name, pattern));
}
/// <summary>
/// Determines whether a BizTalk artifact name matches a configured ignore pattern.
/// </summary>
/// <param name="artifactName">The artifact name to inspect.</param>
/// <returns>True when the artifact should be skipped.</returns>
private bool ShouldSkipArtifact(string artifactName)
{
if (string.IsNullOrWhiteSpace(artifactName))
@@ -215,6 +289,13 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return _options.IgnoreArtifactNamePatterns.Any(pattern => WildcardMatch(artifactName, pattern));
}
/// <summary>
/// Resolves the generated monitor application name for a BizTalk artifact.
/// </summary>
/// <param name="bizTalkApplicationName">The source BizTalk application name.</param>
/// <param name="artifactType">The source artifact type.</param>
/// <param name="artifactName">The source artifact name.</param>
/// <returns>The generated monitor application name.</returns>
private string ResolveMappedApplicationName(string bizTalkApplicationName, string artifactType, string artifactName)
{
foreach (var rule in _options.ArtifactMappings.Where(r => r != null && !string.IsNullOrWhiteSpace(r.ApplicationName)))
@@ -241,6 +322,14 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return string.IsNullOrWhiteSpace(bizTalkApplicationName) ? "Unassigned Application" : bizTalkApplicationName.Trim();
}
/// <summary>
/// Builds a generated monitor target name from BizTalk artifact metadata.
/// </summary>
/// <param name="artifactType">The source artifact type.</param>
/// <param name="baseName">The base artifact display name.</param>
/// <param name="adapterName">The BizTalk adapter name.</param>
/// <param name="isSecondary">True when the endpoint is from a secondary transport.</param>
/// <returns>The generated target name.</returns>
private string BuildTargetName(string artifactType, string baseName, string adapterName, bool isSecondary)
{
var prefix = string.Equals(artifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
@@ -261,6 +350,16 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return name;
}
/// <summary>
/// Normalizes a BizTalk adapter address into a monitor-compatible endpoint.
/// </summary>
/// <param name="sourceAddress">The raw address read from BizTalk.</param>
/// <param name="adapterName">The BizTalk adapter name.</param>
/// <param name="normalizedEndpoint">The normalized monitor endpoint when successful.</param>
/// <param name="port">The resolved explicit port when available.</param>
/// <param name="httpMethod">The generated HTTP method when applicable.</param>
/// <param name="reason">The skip reason when normalization fails.</param>
/// <returns>True when the address can be monitored; otherwise false.</returns>
private bool TryNormalizeEndpoint(string sourceAddress, string adapterName, out string normalizedEndpoint, out int? port, out string httpMethod, out string reason)
{
normalizedEndpoint = string.Empty;
@@ -386,6 +485,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return true;
}
/// <summary>
/// Safely resolves the adapter name from a BizTalk transport.
/// </summary>
/// <param name="transport">The BizTalk transport information.</param>
/// <returns>The adapter name or an empty string.</returns>
private static string ResolveAdapterName(TransportInfo transport)
{
try
@@ -398,6 +502,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
}
}
/// <summary>
/// Safely resolves the adapter name from a BizTalk protocol type.
/// </summary>
/// <param name="transportType">The BizTalk protocol type.</param>
/// <returns>The adapter name or an empty string.</returns>
private static string ResolveAdapterName(ProtocolType transportType)
{
try
@@ -410,6 +519,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
}
}
/// <summary>
/// Safely reads the address from a BizTalk transport.
/// </summary>
/// <param name="transport">The BizTalk transport information.</param>
/// <returns>The transport address or an empty string.</returns>
private static string SafeAddress(TransportInfo transport)
{
try
@@ -422,12 +536,23 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
}
}
/// <summary>
/// Checks whether an adapter name contains a fragment.
/// </summary>
/// <param name="adapterName">The adapter name to inspect.</param>
/// <param name="fragment">The fragment to search for.</param>
/// <returns>True when the adapter name contains the fragment.</returns>
private static bool AdapterContains(string adapterName, string fragment)
{
return !string.IsNullOrWhiteSpace(adapterName)
&& adapterName.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0;
}
/// <summary>
/// Detects semicolon-separated email recipient lists that are not monitorable endpoints.
/// </summary>
/// <param name="value">The address value to inspect.</param>
/// <returns>True when the value looks like an email address list.</returns>
private static bool LooksLikeEmailAddressList(string value)
{
if (string.IsNullOrWhiteSpace(value))
@@ -475,6 +600,12 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return true;
}
/// <summary>
/// Applies simple wildcard matching with asterisks and question marks.
/// </summary>
/// <param name="input">The input value.</param>
/// <param name="pattern">The wildcard pattern.</param>
/// <returns>True when the input matches the pattern.</returns>
private static bool WildcardMatch(string input, string pattern)
{
if (string.IsNullOrWhiteSpace(pattern))
@@ -487,6 +618,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return Regex.IsMatch(input, regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
/// <summary>
/// Determines whether a value looks like a host:port endpoint.
/// </summary>
/// <param name="value">The value to inspect.</param>
/// <returns>True when the value contains a host and numeric port.</returns>
private static bool LooksLikeHostPort(string value)
{
if (string.IsNullOrWhiteSpace(value))
@@ -497,6 +633,11 @@ namespace BizTalkMonitorConfigGenerator.BizTalk
return Regex.IsMatch(value.Trim(), @"^[a-zA-Z0-9._-]+:\d{1,5}($|/.*$)");
}
/// <summary>
/// Parses a port from a host:port value.
/// </summary>
/// <param name="value">The value containing a port.</param>
/// <returns>The parsed port, or null when no port is found.</returns>
private static int? ParsePort(string value)
{
if (string.IsNullOrWhiteSpace(value))
@@ -1,18 +1,68 @@
namespace BizTalkMonitorConfigGenerator.BizTalk
{
/// <summary>
/// Represents one BizTalk artifact endpoint that can be written as a monitor target.
/// </summary>
public sealed class DiscoveredEndpoint
{
/// <summary>
/// Gets or sets the BizTalk artifact type.
/// </summary>
public string ArtifactType { get; set; }
/// <summary>
/// Gets or sets the source BizTalk application name.
/// </summary>
public string BizTalkApplicationName { get; set; }
/// <summary>
/// Gets or sets the generated monitor application name.
/// </summary>
public string MappedApplicationName { get; set; }
/// <summary>
/// Gets or sets the source BizTalk artifact name.
/// </summary>
public string ArtifactName { get; set; }
/// <summary>
/// Gets or sets the BizTalk adapter name.
/// </summary>
public string AdapterName { get; set; }
/// <summary>
/// Gets or sets the raw address read from BizTalk.
/// </summary>
public string SourceAddress { get; set; }
/// <summary>
/// Gets or sets the monitor-compatible endpoint.
/// </summary>
public string NormalizedEndpoint { get; set; }
/// <summary>
/// Gets or sets the generated monitor target name.
/// </summary>
public string TargetName { get; set; }
/// <summary>
/// Gets or sets the HTTP method for generated HTTP targets.
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// Gets or sets the explicit target port, when resolved.
/// </summary>
public int? Port { get; set; }
/// <summary>
/// Gets or sets an optional UNC path validation override.
/// </summary>
public bool? ValidateUncPathAccess { get; set; }
/// <summary>
/// Gets or sets discovery detail text for diagnostics.
/// </summary>
public string Detail { get; set; }
}
}
@@ -7,16 +7,51 @@ using HostAvailabilityMonitor.Configuration;
namespace BizTalkMonitorConfigGenerator.Configuration
{
/// <summary>
/// Root configuration for the BizTalk monitor configuration generator.
/// </summary>
public sealed class GeneratorConfiguration
{
/// <summary>
/// Gets or sets the generator name shown in logs and Event Log entries.
/// </summary>
public string GeneratorName { get; set; } = "BizTalkMonitorConfigGenerator";
/// <summary>
/// Gets or sets the environment label used in logs and generated output.
/// </summary>
public string EnvironmentName { get; set; } = "Unspecified Environment";
/// <summary>
/// Gets or sets rolling file log settings.
/// </summary>
public GeneratorLoggingOptions Logging { get; set; } = new GeneratorLoggingOptions();
/// <summary>
/// Gets or sets Windows Event Log settings.
/// </summary>
public GeneratorEventLogOptions EventLog { get; set; } = new GeneratorEventLogOptions();
/// <summary>
/// Gets or sets BizTalk discovery settings.
/// </summary>
public BizTalkOptions BizTalk { get; set; } = new BizTalkOptions();
/// <summary>
/// Gets or sets output file and target defaults.
/// </summary>
public OutputOptions Output { get; set; } = new OutputOptions();
/// <summary>
/// Gets or sets the monitor configuration template used for generated output.
/// </summary>
public MonitorConfiguration GeneratedMonitorConfig { get; set; } = new MonitorConfiguration();
/// <summary>
/// Loads and normalizes generator configuration from a JSON file.
/// </summary>
/// <param name="path">The JSON configuration path.</param>
/// <returns>The normalized generator configuration.</returns>
public static GeneratorConfiguration Load(string path)
{
if (!File.Exists(path))
@@ -74,6 +109,9 @@ namespace BizTalkMonitorConfigGenerator.Configuration
return config;
}
/// <summary>
/// Validates generator settings and applies safe defaults where possible.
/// </summary>
public void Validate()
{
if (string.IsNullOrWhiteSpace(BizTalk.ConnectionString))
@@ -115,8 +153,16 @@ namespace BizTalkMonitorConfigGenerator.Configuration
}
}
/// <summary>
/// Provides recipient list normalization helpers shared by generator configuration validation.
/// </summary>
internal static class RecipientListHelper
{
/// <summary>
/// Removes null, empty and duplicate recipient entries.
/// </summary>
/// <param name="values">The recipient list from configuration.</param>
/// <returns>A normalized recipient list.</returns>
internal static List<string> SanitizeRecipientList(List<string> values)
{
if (values == null)
@@ -131,6 +177,11 @@ namespace BizTalkMonitorConfigGenerator.Configuration
.ToList();
}
/// <summary>
/// Counts all configured email recipients across To, Cc and Bcc.
/// </summary>
/// <param name="email">The email options to inspect.</param>
/// <returns>The number of configured recipients.</returns>
internal static int GetConfiguredRecipientCount(EmailOptions email)
{
if (email == null)
@@ -144,65 +195,216 @@ namespace BizTalkMonitorConfigGenerator.Configuration
}
}
/// <summary>
/// Defines rolling file log settings for the generator.
/// </summary>
public sealed class GeneratorLoggingOptions
{
/// <summary>
/// Gets or sets the directory where generator log files are written.
/// </summary>
public string LogDirectory { get; set; } = "logs";
/// <summary>
/// Gets or sets the daily generator log file prefix.
/// </summary>
public string FilePrefix { get; set; } = "biztalk-monitor-config-generator";
/// <summary>
/// Gets or sets the number of daily generator log files to retain.
/// </summary>
public int RetentionDays { get; set; } = 5;
}
/// <summary>
/// Defines Windows Event Log behavior for the generator.
/// </summary>
public sealed class GeneratorEventLogOptions
{
/// <summary>
/// Gets or sets a value indicating whether Event Log writes are enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Gets or sets the Windows Event Log name.
/// </summary>
public string LogName { get; set; } = "Application";
/// <summary>
/// Gets or sets the Event Log source name.
/// </summary>
public string Source { get; set; } = "BizTalkMonitorConfigGenerator";
/// <summary>
/// Gets or sets whether the generator should try to create the Event Log source.
/// </summary>
public bool CreateSourceIfMissing { get; set; } = false;
/// <summary>
/// Gets or sets whether successful generator events should be written.
/// </summary>
public bool WriteSuccessEntries { get; set; } = false;
/// <summary>
/// Gets or sets whether generator summary events should be written.
/// </summary>
public bool WriteSummaryEntries { get; set; } = true;
}
/// <summary>
/// Defines BizTalk artifact discovery behavior.
/// </summary>
public sealed class BizTalkOptions
{
/// <summary>
/// Gets or sets the BizTalk management database connection string.
/// </summary>
public string ConnectionString { get; set; } = "SERVER=.;DATABASE=BizTalkMgmtDb;Integrated Security=SSPI";
/// <summary>
/// Gets or sets whether send ports should be discovered.
/// </summary>
public bool IncludeSendPorts { get; set; } = true;
/// <summary>
/// Gets or sets whether receive locations should be discovered.
/// </summary>
public bool IncludeReceiveLocations { get; set; } = true;
/// <summary>
/// Gets or sets whether only started send ports should be included.
/// </summary>
public bool OnlyStartedSendPorts { get; set; } = true;
/// <summary>
/// Gets or sets whether only enabled receive locations should be included.
/// </summary>
public bool OnlyEnabledReceiveLocations { get; set; } = true;
/// <summary>
/// Gets or sets whether secondary send port transports should be included.
/// </summary>
public bool IncludeSecondaryTransportOnSendPorts { get; set; } = true;
/// <summary>
/// Gets or sets whether dynamic send ports should be included.
/// </summary>
public bool IncludeDynamicSendPorts { get; set; } = false;
/// <summary>
/// Gets or sets whether local file paths should be emitted as monitor targets.
/// </summary>
public bool IncludeLocalFilePaths { get; set; } = false;
/// <summary>
/// Gets or sets whether BizTalk system applications should be skipped.
/// </summary>
public bool IgnoreSystemApplications { get; set; } = true;
/// <summary>
/// Gets or sets application name wildcard patterns to skip.
/// </summary>
public List<string> IgnoreApplications { get; set; } = new List<string>();
/// <summary>
/// Gets or sets artifact name wildcard patterns to skip.
/// </summary>
public List<string> IgnoreArtifactNamePatterns { get; set; } = new List<string>();
/// <summary>
/// Gets or sets application-level mapping rules for generated ApplicationName values.
/// </summary>
public List<ApplicationMappingRule> ApplicationMappings { get; set; } = new List<ApplicationMappingRule>();
/// <summary>
/// Gets or sets artifact-level mapping rules for generated ApplicationName values.
/// </summary>
public List<ArtifactMappingRule> ArtifactMappings { get; set; } = new List<ArtifactMappingRule>();
}
/// <summary>
/// Defines output file behavior and defaults for generated monitor targets.
/// </summary>
public sealed class OutputOptions
{
/// <summary>
/// Gets or sets the generated monitor configuration output path.
/// </summary>
public string Path { get; set; } = "generated-monitor-appsettings.json";
/// <summary>
/// Gets or sets whether an existing output file may be overwritten.
/// </summary>
public bool OverwriteExisting { get; set; } = true;
/// <summary>
/// Gets or sets whether no discovered targets should produce a failing exit code.
/// </summary>
public bool FailIfNoTargetsFound { get; set; } = false;
/// <summary>
/// Gets or sets the timeout assigned to generated targets.
/// </summary>
public int TargetTimeoutSeconds { get; set; } = 10;
/// <summary>
/// Gets or sets whether generated targets are enabled by default.
/// </summary>
public bool TargetEnabled { get; set; } = true;
/// <summary>
/// Gets or sets the HTTP method assigned to generated HTTP targets.
/// </summary>
public string DefaultHttpMethod { get; set; } = "HEAD";
}
/// <summary>
/// Maps a BizTalk application name pattern to a monitor application name.
/// </summary>
public sealed class ApplicationMappingRule
{
/// <summary>
/// Gets or sets the BizTalk application wildcard pattern.
/// </summary>
public string BizTalkApplicationName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the generated monitor application name.
/// </summary>
public string ApplicationName { get; set; } = string.Empty;
}
/// <summary>
/// Maps a BizTalk artifact pattern to a monitor application name.
/// </summary>
public sealed class ArtifactMappingRule
{
/// <summary>
/// Gets or sets the optional BizTalk artifact type filter.
/// </summary>
public string ArtifactType { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the BizTalk artifact name wildcard pattern.
/// </summary>
public string ArtifactNamePattern { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the generated monitor application name.
/// </summary>
public string ApplicationName { get; set; } = string.Empty;
}
/// <summary>
/// Provides monitor configuration template helpers used by the generator.
/// </summary>
internal static class MonitorConfigurationTemplateExtensions
{
/// <summary>
/// Validates the monitor template without requiring generated targets.
/// </summary>
/// <param name="configuration">The monitor template to validate.</param>
public static void ValidateTemplateWithoutTargets(this MonitorConfiguration configuration)
{
if (configuration == null)
@@ -274,6 +476,11 @@ namespace BizTalkMonitorConfigGenerator.Configuration
}
}
/// <summary>
/// Clones the monitor template while leaving the target list empty for generated output.
/// </summary>
/// <param name="configuration">The monitor template to clone.</param>
/// <returns>A monitor configuration clone without targets.</returns>
public static MonitorConfiguration CloneWithoutTargets(this MonitorConfiguration configuration)
{
var clone = new MonitorConfiguration
@@ -5,12 +5,31 @@ using BizTalkMonitorConfigGenerator.Configuration;
namespace BizTalkMonitorConfigGenerator.Logging
{
/// <summary>
/// Best-effort Windows Event Log writer for generator lifecycle and summary events.
/// </summary>
public sealed class GeneratorEventLogger
{
/// <summary>
/// Event Log settings from generator configuration.
/// </summary>
private readonly GeneratorEventLogOptions _options;
/// <summary>
/// File logger used when Event Log writes fail.
/// </summary>
private readonly GeneratorFileLogger _fallbackLogger;
/// <summary>
/// Indicates that Event Log writing has been disabled for this process.
/// </summary>
private bool _disabled;
/// <summary>
/// Initializes a new generator Event Log writer.
/// </summary>
/// <param name="options">The Event Log configuration.</param>
/// <param name="fallbackLogger">The fallback file logger.</param>
public GeneratorEventLogger(GeneratorEventLogOptions options, GeneratorFileLogger fallbackLogger)
{
_options = options ?? new GeneratorEventLogOptions();
@@ -18,6 +37,9 @@ namespace BizTalkMonitorConfigGenerator.Logging
_disabled = !_options.Enabled;
}
/// <summary>
/// Optionally creates the configured Event Log source.
/// </summary>
public void TryInitialize()
{
if (_disabled || !_options.CreateSourceIfMissing)
@@ -43,11 +65,21 @@ namespace BizTalkMonitorConfigGenerator.Logging
}
}
/// <summary>
/// Writes an informational event.
/// </summary>
/// <param name="message">The event message.</param>
/// <param name="eventId">The event identifier.</param>
public void Info(string message, int eventId)
{
Write(message, EventLogEntryType.Information, eventId, true);
}
/// <summary>
/// Writes an informational event only when success entries are enabled.
/// </summary>
/// <param name="message">The event message.</param>
/// <param name="eventId">The event identifier.</param>
public void InfoIfSuccessEnabled(string message, int eventId)
{
if (_options.WriteSuccessEntries)
@@ -56,16 +88,31 @@ namespace BizTalkMonitorConfigGenerator.Logging
}
}
/// <summary>
/// Writes a warning event.
/// </summary>
/// <param name="message">The event message.</param>
/// <param name="eventId">The event identifier.</param>
public void Warning(string message, int eventId)
{
Write(message, EventLogEntryType.Warning, eventId, true);
}
/// <summary>
/// Writes an error event.
/// </summary>
/// <param name="message">The event message.</param>
/// <param name="eventId">The event identifier.</param>
public void Error(string message, int eventId)
{
Write(message, EventLogEntryType.Error, eventId, true);
}
/// <summary>
/// Writes a generator summary event when summary entries are enabled.
/// </summary>
/// <param name="message">The summary message.</param>
/// <param name="hasWarnings">True when the summary should be written as a warning.</param>
public void Summary(string message, bool hasWarnings)
{
if (!_options.WriteSummaryEntries)
@@ -76,6 +123,13 @@ namespace BizTalkMonitorConfigGenerator.Logging
Write(message, hasWarnings ? EventLogEntryType.Warning : EventLogEntryType.Information, hasWarnings ? 2100 : 1100, true);
}
/// <summary>
/// Writes one Event Log entry and disables future Event Log writes on failure.
/// </summary>
/// <param name="message">The event message.</param>
/// <param name="entryType">The Windows Event Log entry type.</param>
/// <param name="eventId">The event identifier.</param>
/// <param name="forceWrite">True to write the event when the logger is enabled.</param>
private void Write(string message, EventLogEntryType entryType, int eventId, bool forceWrite)
{
if (_disabled || !forceWrite)
@@ -101,6 +155,10 @@ namespace BizTalkMonitorConfigGenerator.Logging
}
}
/// <summary>
/// Disables Event Log writes and records the reason in the fallback log.
/// </summary>
/// <param name="reason">The reason Event Log writes were disabled.</param>
private void DisableAndFallback(string reason)
{
if (_disabled)
@@ -5,15 +5,49 @@ using System.Threading;
namespace BizTalkMonitorConfigGenerator.Logging
{
/// <summary>
/// Writes rolling log files for BizTalk monitor configuration generation.
/// </summary>
public sealed class GeneratorFileLogger
{
/// <summary>
/// Synchronizes file writes within the current process.
/// </summary>
private readonly object _sync = new object();
/// <summary>
/// Directory where generator logs are stored.
/// </summary>
private readonly string _directory;
/// <summary>
/// Prefix used for daily generator log files.
/// </summary>
private readonly string _filePrefix;
/// <summary>
/// Number of daily log files to retain.
/// </summary>
private readonly int _retentionDays;
/// <summary>
/// Generator name written to log lines.
/// </summary>
private readonly string _generatorName;
/// <summary>
/// Environment name written to log lines.
/// </summary>
private readonly string _environmentName;
/// <summary>
/// Initializes a new generator file logger.
/// </summary>
/// <param name="directory">The target log directory.</param>
/// <param name="filePrefix">The daily log file prefix.</param>
/// <param name="retentionDays">The number of daily log files to retain.</param>
/// <param name="generatorName">The generator name written to log lines.</param>
/// <param name="environmentName">The environment name written to log lines.</param>
public GeneratorFileLogger(string directory, string filePrefix, int retentionDays, string generatorName, string environmentName)
{
_directory = directory;
@@ -24,6 +58,9 @@ namespace BizTalkMonitorConfigGenerator.Logging
Directory.CreateDirectory(_directory);
}
/// <summary>
/// Deletes old rolling log files according to the retention setting.
/// </summary>
public void CleanupOldFiles()
{
if (_retentionDays <= 0)
@@ -55,21 +92,44 @@ namespace BizTalkMonitorConfigGenerator.Logging
}
}
/// <summary>
/// Writes an informational log entry.
/// </summary>
/// <param name="message">The message to write.</param>
public void Info(string message)
{
Write("INFO", message);
}
/// <summary>
/// Writes a warning log entry.
/// </summary>
/// <param name="message">The message to write.</param>
public void Warn(string message)
{
Write("WARN", message);
}
/// <summary>
/// Writes an error log entry.
/// </summary>
/// <param name="message">The message to write.</param>
public void Error(string message)
{
Write("ERROR", message);
}
/// <summary>
/// Writes one structured discovery decision.
/// </summary>
/// <param name="action">The discovery action, such as ADD or SKIP.</param>
/// <param name="artifactType">The BizTalk artifact type.</param>
/// <param name="applicationName">The BizTalk application name.</param>
/// <param name="artifactName">The artifact name.</param>
/// <param name="adapterName">The adapter name.</param>
/// <param name="sourceAddress">The source address read from BizTalk.</param>
/// <param name="normalizedEndpoint">The generated monitor endpoint, if any.</param>
/// <param name="detail">The discovery detail or skip reason.</param>
public void Discovery(string action, string artifactType, string applicationName, string artifactName, string adapterName, string sourceAddress, string normalizedEndpoint, string detail)
{
Write("DISCOVERY", string.Format(
@@ -85,6 +145,11 @@ namespace BizTalkMonitorConfigGenerator.Logging
Escape(detail)));
}
/// <summary>
/// Formats and appends a generator log line.
/// </summary>
/// <param name="level">The log level text.</param>
/// <param name="message">The formatted message payload.</param>
private void Write(string level, string message)
{
var line = string.Format(
@@ -104,6 +169,11 @@ namespace BizTalkMonitorConfigGenerator.Logging
}
}
/// <summary>
/// Appends a line to a file and retries transient file access failures.
/// </summary>
/// <param name="path">The log file path.</param>
/// <param name="line">The full line to append.</param>
private void AppendWithRetries(string path, string line)
{
Exception lastException = null;
@@ -132,12 +202,22 @@ namespace BizTalkMonitorConfigGenerator.Logging
}
}
/// <summary>
/// Builds the daily generator log file path for the current local date.
/// </summary>
/// <returns>The current log file path.</returns>
private string GetCurrentLogFilePath()
{
var fileName = _filePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
return Path.Combine(_directory, fileName);
}
/// <summary>
/// Extracts the date encoded in a rolling generator log file name.
/// </summary>
/// <param name="path">The log file path to inspect.</param>
/// <param name="date">The parsed file date when parsing succeeds.</param>
/// <returns>True when the date could be parsed; otherwise false.</returns>
private bool TryGetDateFromFileName(string path, out DateTime date)
{
date = DateTime.MinValue;
@@ -156,6 +236,11 @@ namespace BizTalkMonitorConfigGenerator.Logging
out date);
}
/// <summary>
/// Removes line breaks from values before they are written to one-line logs.
/// </summary>
/// <param name="value">The value to escape.</param>
/// <returns>The log-safe value.</returns>
private static string Escape(string value)
{
return (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
+143 -2
View File
@@ -10,48 +10,71 @@ using HostAvailabilityMonitor.Configuration;
namespace BizTalkMonitorConfigGenerator
{
/// <summary>
/// Console entry point and orchestration workflow for the BizTalk monitor config generator.
/// </summary>
internal static class Program
{
/// <summary>
/// Runs the generator once.
/// </summary>
/// <param name="args">Optional command-line arguments. The first argument may be the generator configuration path.</param>
/// <returns>The process exit code.</returns>
private static int Main(string[] args)
{
GeneratorFileLogger logger = null;
GeneratorEventLogger eventLogger = null;
string baseDirectory = null;
string configPath = null;
try
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])
? args[0]
: Path.Combine(baseDirectory, "generator-settings.json");
configPath = Path.GetFullPath(configPath);
WriteConsoleBanner("BizTalk Monitor Config Generator", configPath);
WriteConsoleInfo("Lade Generator-Konfiguration...");
var configuration = GeneratorConfiguration.Load(configPath);
configuration.Validate();
WriteConsoleInfo("Konfiguration geladen. Umgebung=" + configuration.EnvironmentName + ".");
var logDirectory = ResolvePath(baseDirectory, configuration.Logging.LogDirectory);
WriteConsoleInfo("Initialisiere Dateilog: " + BuildLogFilePreview(logDirectory, configuration.Logging.FilePrefix));
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));
logger.Info("Ausgabeziel: " + ResolvePath(baseDirectory, configuration.Output.Path));
WriteConsoleInfo("Initialisiere Windows Event Log. Quelle=" + configuration.EventLog.Source + ", Log=" + configuration.EventLog.LogName + ", Aktiv=" + configuration.EventLog.Enabled + ".");
eventLogger = new GeneratorEventLogger(configuration.EventLog, logger);
eventLogger.TryInitialize();
eventLogger.Info(configuration.GeneratorName + " gestartet. Umgebung=" + configuration.EnvironmentName + ", Server=" + Environment.MachineName + ".", 1000);
WriteConsoleInfo("Starte BizTalk-Discovery. SendPorts=" + configuration.BizTalk.IncludeSendPorts + ", ReceiveLocations=" + configuration.BizTalk.IncludeReceiveLocations + ".");
var discoveryService = new BizTalkCatalogDiscoveryService(configuration.BizTalk, configuration.Output, logger);
var discovered = discoveryService.Discover();
logger.Info("Discovery abgeschlossen. Gefundene unterstützte Targets=" + discovered.Count + ".");
WriteConsoleInfo("Discovery abgeschlossen. Gefundene unterstuetzte Targets=" + discovered.Count + ".");
var outputConfiguration = BuildMonitorConfiguration(configuration, discovered);
var outputPath = ResolvePath(baseDirectory, configuration.Output.Path);
WriteConsoleInfo("Schreibe Monitor-Konfiguration: " + outputPath);
JsonFileWriter.WritePrettyJsonAtomic(outputPath, outputConfiguration, configuration.Output.OverwriteExisting);
logger.Info("Monitor-Konfiguration geschrieben: " + outputPath);
eventLogger.InfoIfSuccessEnabled("Monitor-Konfiguration erfolgreich generiert: " + outputPath, 1001);
WriteConsoleInfo("Monitor-Konfiguration geschrieben.");
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);
WriteConsoleWarn(message);
return configuration.Output.FailIfNoTargetsFound ? 2 : 0;
}
@@ -62,13 +85,21 @@ namespace BizTalkMonitorConfigGenerator
outputPath);
logger.Info(summary);
eventLogger.Summary(summary, false);
WriteConsoleInfo(summary);
return 0;
}
catch (Exception ex)
{
WriteConsoleFatal("Generatorlauf fehlgeschlagen.", configPath, ex);
if (logger != null)
{
logger.Error("Fataler Fehler: " + ex.GetType().Name + ": " + ex.Message);
logger.Error("Details: " + ex);
}
else
{
TryWriteFallbackFailureLog(baseDirectory, configPath, ex);
}
if (eventLogger != null)
@@ -80,6 +111,12 @@ namespace BizTalkMonitorConfigGenerator
}
}
/// <summary>
/// Builds the final monitor configuration from the template and discovered endpoints.
/// </summary>
/// <param name="configuration">The generator configuration.</param>
/// <param name="discovered">The discovered BizTalk endpoints.</param>
/// <returns>The monitor configuration to write.</returns>
private static MonitorConfiguration BuildMonitorConfiguration(GeneratorConfiguration configuration, IReadOnlyCollection<DiscoveredEndpoint> discovered)
{
var result = configuration.GeneratedMonitorConfig.CloneWithoutTargets();
@@ -114,6 +151,12 @@ namespace BizTalkMonitorConfigGenerator
return result;
}
/// <summary>
/// Resolves a configured path against the application base directory.
/// </summary>
/// <param name="baseDirectory">The application base directory.</param>
/// <param name="configuredPath">The configured relative or absolute path.</param>
/// <returns>The resolved path.</returns>
private static string ResolvePath(string baseDirectory, string configuredPath)
{
if (Path.IsPathRooted(configuredPath))
@@ -123,5 +166,103 @@ namespace BizTalkMonitorConfigGenerator
return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath));
}
/// <summary>
/// Writes the initial console banner before configuration and file logging are available.
/// </summary>
/// <param name="title">The program title.</param>
/// <param name="configPath">The resolved configuration path.</param>
private static void WriteConsoleBanner(string title, string configPath)
{
TryWriteConsoleLine(Console.Out, "============================================================");
TryWriteConsoleLine(Console.Out, title);
TryWriteConsoleLine(Console.Out, "Server: " + Environment.MachineName);
TryWriteConsoleLine(Console.Out, "Start: " + DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"));
TryWriteConsoleLine(Console.Out, "Config: " + (configPath ?? "(nicht aufgeloest)"));
TryWriteConsoleLine(Console.Out, "============================================================");
}
/// <summary>
/// Writes a console information message.
/// </summary>
/// <param name="message">The message to write.</param>
private static void WriteConsoleInfo(string message)
{
TryWriteConsoleLine(Console.Out, "[INFO] " + message);
}
/// <summary>
/// Writes a console warning message.
/// </summary>
/// <param name="message">The message to write.</param>
private static void WriteConsoleWarn(string message)
{
TryWriteConsoleLine(Console.Out, "[WARN] " + message);
}
/// <summary>
/// Writes a fatal console error with actionable context.
/// </summary>
/// <param name="message">The user-facing failure message.</param>
/// <param name="configPath">The configuration path used for the run.</param>
/// <param name="exception">The exception that ended the run.</param>
private static void WriteConsoleFatal(string message, string configPath, Exception exception)
{
TryWriteConsoleLine(Console.Error, "[ERROR] " + message);
TryWriteConsoleLine(Console.Error, "[ERROR] Konfiguration: " + (configPath ?? "(nicht aufgeloest)"));
TryWriteConsoleLine(Console.Error, "[ERROR] Ursache: " + exception.GetType().Name + ": " + exception.Message);
TryWriteConsoleLine(Console.Error, "[ERROR] Details: " + exception);
}
/// <summary>
/// Writes one line to a console writer and ignores secondary console failures.
/// </summary>
/// <param name="writer">The console writer.</param>
/// <param name="message">The line to write.</param>
private static void TryWriteConsoleLine(TextWriter writer, string message)
{
try
{
writer.WriteLine(message);
}
catch
{
}
}
/// <summary>
/// Writes a fallback fatal log when configured logging could not be initialized.
/// </summary>
/// <param name="baseDirectory">The application base directory.</param>
/// <param name="configPath">The configuration path used for the run.</param>
/// <param name="exception">The exception that ended the run.</param>
private static void TryWriteFallbackFailureLog(string baseDirectory, string configPath, Exception exception)
{
try
{
var directory = ResolvePath(baseDirectory ?? AppDomain.CurrentDomain.BaseDirectory, "logs");
var fallbackLogger = new GeneratorFileLogger(directory, "biztalk-monitor-config-generator-startup", 5, "BizTalkMonitorConfigGenerator", "Startup");
fallbackLogger.Error("Generator konnte nicht gestartet werden. Konfiguration=" + (configPath ?? "(nicht aufgeloest)") + ". Fehler=" + exception.GetType().Name + ": " + exception.Message);
fallbackLogger.Error("Details: " + exception);
WriteConsoleInfo("Fallback-Log geschrieben: " + BuildLogFilePreview(directory, "biztalk-monitor-config-generator-startup"));
}
catch (Exception fallbackEx)
{
TryWriteConsoleLine(Console.Error, "[ERROR] Fallback-Log konnte nicht geschrieben werden: " + fallbackEx.GetType().Name + ": " + fallbackEx.Message);
}
}
/// <summary>
/// Builds the expected daily log file path for console diagnostics.
/// </summary>
/// <param name="directory">The log directory.</param>
/// <param name="filePrefix">The rolling file prefix.</param>
/// <returns>The expected log file path for the current local date.</returns>
private static string BuildLogFilePreview(string directory, string filePrefix)
{
var safeDirectory = string.IsNullOrWhiteSpace(directory) ? AppDomain.CurrentDomain.BaseDirectory : directory;
var safePrefix = string.IsNullOrWhiteSpace(filePrefix) ? "biztalk-monitor-config-generator" : filePrefix;
return Path.Combine(safeDirectory, safePrefix + "-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log");
}
}
}
@@ -1,6 +1,7 @@
using System.Reflection;
using System.Runtime.InteropServices;
// Assembly metadata for the BizTalkMonitorConfigGenerator console application.
[assembly: AssemblyTitle("BizTalkMonitorConfigGenerator")]
[assembly: AssemblyDescription("Generates HostAvailabilityMonitor JSON config from active BizTalk send ports and receive locations.")]
[assembly: AssemblyConfiguration("")]
@@ -5,8 +5,17 @@ using System.Web.Script.Serialization;
namespace BizTalkMonitorConfigGenerator.Serialization
{
/// <summary>
/// Writes generator output JSON files with stable formatting and atomic replacement.
/// </summary>
public static class JsonFileWriter
{
/// <summary>
/// Serializes an object as pretty JSON and replaces the target file atomically.
/// </summary>
/// <param name="path">The target JSON file path.</param>
/// <param name="value">The value to serialize.</param>
/// <param name="overwriteExisting">True to replace an existing file; otherwise false.</param>
public static void WritePrettyJsonAtomic(string path, object value, bool overwriteExisting)
{
if (string.IsNullOrWhiteSpace(path))
@@ -44,6 +53,11 @@ namespace BizTalkMonitorConfigGenerator.Serialization
File.Move(tempPath, fullPath);
}
/// <summary>
/// Formats compact JavaScriptSerializer JSON with indentation.
/// </summary>
/// <param name="json">The compact JSON string to format.</param>
/// <returns>The formatted JSON string.</returns>
private static string PrettyPrint(string json)
{
if (string.IsNullOrWhiteSpace(json))
@@ -126,6 +140,11 @@ namespace BizTalkMonitorConfigGenerator.Serialization
return sb.ToString();
}
/// <summary>
/// Appends indentation spaces for the current JSON nesting level.
/// </summary>
/// <param name="sb">The output string builder.</param>
/// <param name="indent">The indentation level.</param>
private static void AppendIndent(StringBuilder sb, int indent)
{
for (var i = 0; i < indent; i++)