From 3d987e9a58abf9c723f525598c8953b32113ecc3 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Mon, 30 Mar 2026 13:23:22 -0400 Subject: [PATCH 01/18] dont mix up the environment and current config, refactor --- .../Config/AppConfigBootstrap.cs | 42 +-- src/SimpleL7Proxy/Config/BackendOptions.cs | 112 ++++++++ .../Config/BackendOptionsBuilder.cs | 159 ++++++++++- .../Config/BackendOptionsExtensions.cs | 153 ----------- .../{WarmOptions.cs => ConfigOptions.cs} | 142 +--------- src/SimpleL7Proxy/Config/ConfigParser.cs | 250 ++++++++++-------- src/SimpleL7Proxy/Constants.cs | 2 +- src/SimpleL7Proxy/User/UserProfile.cs | 9 +- 8 files changed, 416 insertions(+), 453 deletions(-) delete mode 100644 src/SimpleL7Proxy/Config/BackendOptionsExtensions.cs rename src/SimpleL7Proxy/Config/{WarmOptions.cs => ConfigOptions.cs} (51%) diff --git a/src/SimpleL7Proxy/Config/AppConfigBootstrap.cs b/src/SimpleL7Proxy/Config/AppConfigBootstrap.cs index 396434b9..c089ad42 100644 --- a/src/SimpleL7Proxy/Config/AppConfigBootstrap.cs +++ b/src/SimpleL7Proxy/Config/AppConfigBootstrap.cs @@ -18,7 +18,7 @@ public class AppConfigBootstrap : BackgroundService private readonly string? _endpoint; private readonly string? _connectionString; private readonly string? _labelFilter; - private readonly BackendOptions _options; + private BackendOptions _options; private readonly DefaultCredential _defaultCredential; private readonly TimeSpan _refreshInterval; private bool _isInitialized = false; @@ -31,9 +31,6 @@ public class AppConfigBootstrap : BackgroundService public ConfigChangeNotifier? Notifier { get; set; } public IHostHealthCollection? HostCollection { get; set; } - // Snapshot of the last-downloaded warm keys, used for change detection. - private Dictionary _snapshot = new(StringComparer.OrdinalIgnoreCase); - /// The downloaded settings (merged warm + cold), available after has been awaited. public Dictionary? Settings { get; private set; } @@ -43,11 +40,12 @@ public class AppConfigBootstrap : BackgroundService /// Only cold-prefixed settings, available after has been awaited. public Dictionary? ColdSettings { get; private set; } + public static BackendOptions DEFAULT_OPTIONS { get; set; } + public AppConfigBootstrap(ILogger logger, BackendOptions backendOptions, DefaultCredential defaultCredential) { _logger = logger; - _options = backendOptions; _defaultCredential = defaultCredential; _endpoint = backendOptions.AppConfigEndpoint; _connectionString = backendOptions.AppConfigConnectionString; @@ -136,8 +134,9 @@ private void CommitDownload(Dictionary warm, Dictionary - public void RegisterServices(IServiceCollection services) + public void RegisterServices(IServiceCollection services, BackendOptions options) { + _options = options; if (!_isInitialized) return; services.AddHostedService(sp => this); @@ -166,7 +165,6 @@ private async Task ProcessRefreshAsync(CancellationToken ct) { var sentinel = await ReadSentinelAsync(ct); - // Console.WriteLine($"Comparing the sentinel value: {sentinel} with the last seen value: {_lastSentinel}"); if (string.Equals(sentinel, _lastSentinel, StringComparison.Ordinal)) return; _logger.LogInformation("[APP-CONFIG] Sentinel changed ({Old} → {New}), re-downloading...", @@ -179,35 +177,9 @@ private async Task ProcessRefreshAsync(CancellationToken ct) if (result == null || Notifier == null) return; - // TODO: MERGE INTO THE WAY BOOTSTRAP WORKS - - // Detect what changed between the new snapshot and current live options. - var (changes, parsedValues, hostChanges) = ConfigOptions.DetectWarmChanges(_options, warm, _logger); - if (changes.Count == 0 && hostChanges.Count == 0) - return; - // Apply changed properties to the live BackendOptions instance. - var fields = ConfigOptions.GetFieldsByConfigName(); - var changedProps = new List(changes.Count); - foreach (var change in changes) - { - if (!fields.TryGetValue(change.PropertyName, out var prop)) continue; - if (!parsedValues.TryGetValue(change.PropertyName, out var value)) continue; - prop.SetValue(_options, value); - changedProps.Add(prop); - } - if (changedProps.Count > 0) - ConfigParser.ApplyDerivedSettings(_options, [.. changedProps]); - - if (hostChanges.Count > 0) - BackendOptionsBuilder.RegisterBackends(_options, null, hostChanges, HostCollection); - - if (changes.Count > 0) - { - _logger.LogInformation("[BOOTSTRAP] Applied {Count} warm change(s): {Names}", - changes.Count, string.Join(", ", changes.Select(c => c.PropertyName))); - await Notifier.NotifyAsync(changes, _options, ct); - } + await BackendOptionsBuilder.ApplyRefresh( + _options, DEFAULT_OPTIONS, warm, Notifier, HostCollection, _logger, ct); } private async Task ReadSentinelAsync(CancellationToken ct) diff --git a/src/SimpleL7Proxy/Config/BackendOptions.cs b/src/SimpleL7Proxy/Config/BackendOptions.cs index ec77007f..21934fad 100644 --- a/src/SimpleL7Proxy/Config/BackendOptions.cs +++ b/src/SimpleL7Proxy/Config/BackendOptions.cs @@ -1,3 +1,4 @@ +using System.Reflection; using SimpleL7Proxy.Backend; using SimpleL7Proxy.Backend.Iterators; @@ -297,4 +298,115 @@ public class BackendOptions public List Hosts { get; set; } = []; public Dictionary PriorityWorkers { get; set; } = new() { { 2, 1 }, { 3, 1 } }; public bool TrackWorkers { get; set; } = true; + + /// + /// Creates a deep copy of this instance. Scalar properties are copied directly; + /// collections (List, Dictionary, array) are cloned so the copy is fully independent. + /// Note: (HttpClient) is shared, not cloned. + /// + public BackendOptions DeepClone() + { + var clone = (BackendOptions)MemberwiseClone(); + + // Clone collection properties so mutations don't leak between instances. + clone.AcceptableStatusCodes = (int[])AcceptableStatusCodes.Clone(); + clone.DependancyHeaders = new List(DependancyHeaders); + clone.DisallowedHeaders = new List(DisallowedHeaders); + clone.LogAllRequestHeadersExcept = new List(LogAllRequestHeadersExcept); + clone.LogAllResponseHeadersExcept = new List(LogAllResponseHeadersExcept); + clone.LogHeaders = new List(LogHeaders); + clone.LogToConsole = new List(LogToConsole); + clone.LogToEvents = new List(LogToEvents); + clone.LogToAI = new List(LogToAI); + clone.PriorityKeys = new List(PriorityKeys); + clone.PriorityValues = new List(PriorityValues); + clone.RequiredHeaders = new List(RequiredHeaders); + clone.StripRequestHeaders = new List(StripRequestHeaders); + clone.StripResponseHeaders = new List(StripResponseHeaders); + clone.UniqueUserHeaders = new List(UniqueUserHeaders); + clone.ValidateHeaders = new Dictionary(ValidateHeaders); + clone.PriorityWorkers = new Dictionary(PriorityWorkers); + clone.Hosts = new List(Hosts); + + return clone; + } + + /// + /// Applies a single configuration field from the environment dictionary to this instance. + /// Uses reflection to set the named property, falling back to the corresponding default value when the + /// environment variable is absent or set to the default placeholder. + /// + public void ApplyFieldFromEnv(Dictionary env, BackendOptions defaults, string envVar, string property) + { + var pi = typeof(BackendOptions).GetProperty(property) ?? throw new InvalidOperationException($"Unknown BackendOptions property: {property}"); + var defVal = pi.GetValue(defaults); + var type = pi.PropertyType; + + var envValue = env.GetValueOrDefault(envVar)?.Trim(); + bool envVarPresent = !string.IsNullOrEmpty(envValue) && envValue != ConfigOptions.DefaultPlaceholder; + if (!envVarPresent) + { + var currentVal = pi.GetValue(this); + bool alreadyChanged = !Equals(currentVal, defVal); + if (alreadyChanged) + { + return; + } + } + + if (type == typeof(int) || type == typeof(double)) + { + var val = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, Convert.ToInt32(defVal)); + pi.SetValue(this, Convert.ChangeType(val, type)); + } + else if (type == typeof(float)) + { + var val = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, Convert.ToSingle(defVal)); + pi.SetValue(this, Convert.ChangeType(val, type)); + } + else if (type == typeof(string)) + { + pi.SetValue(this, ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, (string)defVal!)); + } + else if (type == typeof(bool)) + { + pi.SetValue(this, ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, (bool)defVal!)); + } + else if (type == typeof(List)) + { + var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, string.Join(",", (List)defVal!)); + pi.SetValue(this, ConfigParser.ToListOfString(value)); + } + else if (type == typeof(List)) + { + var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, string.Join(",", (List)defVal!)); + pi.SetValue(this, ConfigParser.ToListOfInt(value)); + } + else if (type == typeof(int[])) + { + pi.SetValue(this, ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, (int[])defVal!)); + } + else if (type == typeof(Dictionary)) + { + var defaultValue = string.Join(",", ((Dictionary)defVal!).Select(kvp => $"{kvp.Key}={kvp.Value}")); + var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, defaultValue); + pi.SetValue(this, ConfigParser.KVStringPairs(ConfigParser.ToListOfString(value))); + } + else if (type.IsEnum) + { + var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, defVal!.ToString()!); + if (Enum.TryParse(type, value, true, out var parsed)) + { + pi.SetValue(this, parsed); + } + else + { + pi.SetValue(this, defVal); + } + } + else + { + throw new NotSupportedException($"ApplyFieldFromEnv: unsupported property type {type.Name} for {property}"); + } + } } \ No newline at end of file diff --git a/src/SimpleL7Proxy/Config/BackendOptionsBuilder.cs b/src/SimpleL7Proxy/Config/BackendOptionsBuilder.cs index 7cce59bc..9f0ea474 100644 --- a/src/SimpleL7Proxy/Config/BackendOptionsBuilder.cs +++ b/src/SimpleL7Proxy/Config/BackendOptionsBuilder.cs @@ -30,8 +30,6 @@ public static class BackendOptionsBuilder { private static ILogger? _logger; - static BackendOptions s_options = new BackendOptions(); - /// /// Collects all OS env vars and seeds the Hostname identity fallback chain: /// explicit Hostname > ReplicaID > CONTAINER_APP_REPLICA_NAME > HOSTNAME > MachineName. @@ -58,19 +56,22 @@ public static Dictionary EffectiveEnvironment() } /// - /// Builds the initial BackendOptions: env vars first, then App Config warm+cold - /// overrides merged on top (warm wins on collision). + /// Builds and returns two BackendOptions instances: + /// + /// baseOptions — env vars only (pristine defaults snapshot, never mutated after startup). + /// envOptions — env vars + App Config warm/cold overrides merged on top (warm wins on collision). This is the live singleton. + /// /// - public static async Task CreateOptions(AppConfigBootstrap appConfigBootstrap) + public static async Task<(BackendOptions baseOptions, BackendOptions envOptions)> CreateOptions(AppConfigBootstrap appConfigBootstrap) { - s_options = s_options.Apply(EffectiveEnvironment()); + var baseOptions = ConfigParser.ApplyEnv(EffectiveEnvironment(), new BackendOptions()); var (warmSettings, coldSettings) = await appConfigBootstrap.GetSettingsAsync().ConfigureAwait(false); BackendOptions envOptions; if (warmSettings == null && coldSettings == null) { - envOptions = s_options; + envOptions = baseOptions.DeepClone(); } else { @@ -78,12 +79,150 @@ public static async Task CreateOptions(AppConfigBootstrap appCon if ( warmSettings != null) foreach (var kvp in warmSettings) merged[kvp.Key] = kvp.Value; - envOptions = ConfigParser.ApplyEnv(merged, s_options); + envOptions = ConfigParser.ApplyEnv(merged, baseOptions); } - envOptions.ConfigureHttpClient(); + ConfigParser.ConfigureHttpClient(envOptions); + + return (baseOptions, envOptions); + } + + /// + /// Applies a warm-refresh: detects changes, updates the live options, re-registers + /// backends, derives dependent settings, and notifies subscribers. + /// Called by AppConfigBootstrap when the sentinel value changes. + /// + public static async Task ApplyRefresh( + BackendOptions liveOptions, + BackendOptions defaultOptions, + Dictionary warm, + ConfigChangeNotifier? notifier, + IHostHealthCollection? hostCollection, + ILogger logger, + CancellationToken ct) + { + var (changes, parsedValues, hostChanges) = DetectWarmChanges(liveOptions, warm, logger); + if (changes.Count == 0 && hostChanges.Count == 0) + return; + + var fields = ConfigOptions.GetFieldsByConfigName(); + var ds = new Dictionary(1); + + foreach (var kvp in parsedValues) + { + var field = fields[kvp.Key]; + var before = field.GetValue(liveOptions); // TODO: remove debug + ds.Clear(); + ds[kvp.Key] = kvp.Value?.ToString() ?? ""; + liveOptions.ApplyFieldFromEnv(ds, defaultOptions, kvp.Key, field.Name); + var after = field.GetValue(liveOptions); // TODO: remove debug + Console.WriteLine($"[WARM] Applied {kvp.Key} ({field.Name}): '{before}' -> '{after}'"); // TODO: remove debug + } - return envOptions; + // Collect changed PropertyInfos for derived-settings recalculation. + var changedProps = new List(changes.Count); + foreach (var change in changes) + { + if (fields.TryGetValue(change.PropertyName, out var prop)) + changedProps.Add(prop); + } + Console.WriteLine($"[WARM] {changedProps.Count} derived-settings prop(s) to recalculate"); // TODO: remove debug + if (changedProps.Count > 0) + ConfigParser.ApplyDerivedSettings(liveOptions, [.. changedProps]); + + if (hostChanges.Count > 0) + { + Console.WriteLine($"[WARM] {hostChanges.Count} host change(s) detected, re-registering backends"); // TODO: remove debug + RegisterBackends(liveOptions, null, hostChanges, hostCollection); + } + + if (changes.Count > 0) + { + Console.WriteLine($"[WARM] Notifying {changes.Count} change(s): {string.Join(", ", changes.Select(c => c.PropertyName))}"); // TODO: remove debug + logger.LogInformation("[BOOTSTRAP] Applied {Count} warm change(s): {Names}", + changes.Count, string.Join(", ", changes.Select(c => c.PropertyName))); + if (notifier != null) + await notifier.NotifyAsync(changes, liveOptions, ct); + } + } + + /// + /// Diffs bare-keyed warm settings against live options. Does not mutate liveOptions. + /// Host keys (Host*, Probe*, IP*) are returned separately in HostChanges. + /// + private static (List Changes, Dictionary ParsedValues, Dictionary HostChanges) DetectWarmChanges( + BackendOptions liveOptions, + Dictionary warmSettings, + ILogger? logger = null) + { + var changeList = new List(); + var updates = new Dictionary(StringComparer.OrdinalIgnoreCase); + var hostChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + var defaultTarget = new BackendOptions(); + var env = new Dictionary(1, StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in warmSettings) + { + var rawValue = kvp.Value; + if (string.IsNullOrEmpty(rawValue)) continue; + + var key = kvp.Key; + if (key.StartsWith("Host") || key.StartsWith("Probe") || key.StartsWith("IP")) + { + hostChanges[key] = rawValue; + continue; + } + + if (!ConfigOptions.WarmDescriptorsByKeyPath.TryGetValue(key, out var descriptor) + && !ConfigOptions.WarmDescriptorsByConfigName.TryGetValue(key, out descriptor)) + continue; + + var configName = descriptor.ConfigName; + if (!ConfigOptions.TryGetFieldByConfigName(configName, out var field) || field == null) + continue; + + var currentValue = field.GetValue(liveOptions); + + // Parse the raw value via ApplyFieldFromEnv on a throwaway target. + env.Clear(); + env[configName] = rawValue; + + defaultTarget.ApplyFieldFromEnv(env, liveOptions, configName, field.Name); + var newValue = field.GetValue(defaultTarget); + + if (DeepEquals(currentValue, newValue)) continue; + + updates[configName] = newValue; + changeList.Add(new ConfigChange + { + PropertyName = configName, + KeyPath = descriptor.Attribute.KeyPath, + RawOldValue = currentValue, + RawNewValue = newValue + }); + } + + return (changeList, updates, hostChanges); + } + + /// Deep-compares two values, handling List, array, and Dictionary types. + private static bool DeepEquals(object? a, object? b) + { + if (ReferenceEquals(a, b)) return true; + if (a is null || b is null) return false; + if (a.GetType() != b.GetType()) return false; + + return (a, b) switch + { + (int[] la, int[] lb) => la.SequenceEqual(lb), + (IList la, IList lb) => la.SequenceEqual(lb), + (IList la, IList lb) => la.SequenceEqual(lb), + (IDictionary da, IDictionary db) => + da.Count == db.Count && da.All(kvp => db.TryGetValue(kvp.Key, out var v) && v == kvp.Value), + (IDictionary da, IDictionary db) => + da.Count == db.Count && da.All(kvp => db.TryGetValue(kvp.Key, out var v) && v == kvp.Value), + _ => Equals(a, b) + }; } /// diff --git a/src/SimpleL7Proxy/Config/BackendOptionsExtensions.cs b/src/SimpleL7Proxy/Config/BackendOptionsExtensions.cs deleted file mode 100644 index ef51520f..00000000 --- a/src/SimpleL7Proxy/Config/BackendOptionsExtensions.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using SimpleL7Proxy.Events; - -namespace SimpleL7Proxy.Config; - -/// -/// Extension methods for that provide a cleaner -/// calling syntax for configuration parsing operations. -/// -public static class BackendOptionsExtensions -{ - /// - /// Applies a single configuration field from the environment dictionary to this - /// instance, falling back to the corresponding - /// default value when the environment variable is absent or set to the - /// default placeholder. - /// - /// The instance to update. - /// Dictionary of environment/configuration key-value pairs. - /// A default instance providing fallback values. - /// The environment variable (dictionary key) to look up. - /// The name of the property to set. - public static void ApplyFieldFromEnv(this BackendOptions target, Dictionary env, BackendOptions defaults, string envVar, string property) - { - ConfigParser.ApplyFieldFromEnv(env, target, defaults, envVar, property); - } - - public static BackendOptions Apply(this BackendOptions defaults, Dictionary dict) - { - return ConfigParser.ApplyEnv(dict, defaults); - } - - /// - /// Creates a new instance by applying environment variable overrides - /// from the provided dictionary on top of the given defaults. - /// - public static BackendOptions ApplyTo(BackendOptions defaults, Dictionary dict) - { - return ConfigParser.ApplyEnv(dict, defaults); - } - /// - /// Creates and assigns an on this - /// instance, configured from the transport-related properties (keep-alive, HTTP/2, SSL). - /// - public static void ConfigureHttpClient(this BackendOptions backendOptions) - { - var safeKeepAliveInitialDelaySecs = Math.Max(1, backendOptions.KeepAliveInitialDelaySecs); - var safeKeepAlivePingIntervalSecs = Math.Max(1, backendOptions.KeepAlivePingIntervalSecs); - - var retryCount = Math.Max(1, backendOptions.KeepAliveIdleTimeoutSecs / safeKeepAlivePingIntervalSecs); - var handler = CreateSocketsHandler(safeKeepAliveInitialDelaySecs, safeKeepAlivePingIntervalSecs, retryCount); - - if (backendOptions.EnableMultipleHttp2Connections) - { - handler.EnableMultipleHttp2Connections = true; - handler.PooledConnectionLifetime = TimeSpan.FromSeconds(backendOptions.MultiConnLifetimeSecs); - handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(backendOptions.MultiConnIdleTimeoutSecs); - handler.MaxConnectionsPerServer = backendOptions.MultiConnMaxConns; - handler.ResponseDrainTimeout = TimeSpan.FromSeconds(backendOptions.KeepAliveIdleTimeoutSecs); - Console.WriteLine("Multiple HTTP/2 connections enabled."); - } - else - { - handler.EnableMultipleHttp2Connections = false; - Console.WriteLine("Multiple HTTP/2 connections disabled."); - } - - // Configure SSL handling - if (backendOptions.IgnoreSSLCert) - { - handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions - { - RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true - }; - Console.WriteLine("Ignoring SSL certificate validation errors."); - } - - HttpClient client = new HttpClient(handler) - { - // set timeout to large to disable it at HttpClient level. Will use token cancellation for timeout instead. - Timeout = Timeout.InfiniteTimeSpan - }; - - backendOptions.Client = client; - } - - private static SocketsHttpHandler CreateSocketsHandler(int initialDelaySecs, int intervalSecs, int linuxRetryCount) - { - SocketsHttpHandler handler = new SocketsHttpHandler(); - handler.ConnectCallback = async (ctx, ct) => - { - DnsEndPoint dnsEndPoint = ctx.DnsEndPoint; - IPAddress[] addresses = await Dns.GetHostAddressesAsync(dnsEndPoint.Host, dnsEndPoint.AddressFamily, ct).ConfigureAwait(false); - var s = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; - try - { - bool linuxKeepAliveConfigured = false; - - // Basic keep-alive setting - should work on all platforms - s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - try - { - if (OperatingSystem.IsWindows()) - { - // Windows-specific approach using IOControl - byte[] keepAliveValues = new byte[12]; - BitConverter.GetBytes((uint)1).CopyTo(keepAliveValues, 0); // Turn keep-alive on - BitConverter.GetBytes((uint)(initialDelaySecs * 1000)).CopyTo(keepAliveValues, 4); - BitConverter.GetBytes((uint)(intervalSecs * 1000)).CopyTo(keepAliveValues, 8); - - s.IOControl(IOControlCode.KeepAliveValues, keepAliveValues, null); - } - else if (OperatingSystem.IsLinux()) - { - s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, initialDelaySecs); - s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, intervalSecs); - s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, linuxRetryCount); - linuxKeepAliveConfigured = true; - } - } - catch (Exception ex) - { - ProxyEvent pe = new() - { - Type = EventType.Exception, - Exception = ex, - ["Message"] = "Failed to set TCP keep-alive parameters", - ["Host"] = dnsEndPoint.Host, - ["Port"] = dnsEndPoint.Port.ToString(), - ["InitialDelaySecs"] = initialDelaySecs.ToString(), - ["IntervalSecs"] = intervalSecs.ToString(), - ["LinuxRetryCount"] = linuxRetryCount.ToString(), - ["linuxKeepAliveConfigured"] = linuxKeepAliveConfigured.ToString() - }; - pe.SendEvent(); - } - - // Connect to the endpoint - await s.ConnectAsync(addresses, dnsEndPoint.Port, ct).ConfigureAwait(false); - return new NetworkStream(s, ownsSocket: true); - } - catch (Exception ex) - { - Console.Error.WriteLine($"Socket connection error: {ex.Message}"); - s.Dispose(); - throw; - } - }; - - return handler; - } -} diff --git a/src/SimpleL7Proxy/Config/WarmOptions.cs b/src/SimpleL7Proxy/Config/ConfigOptions.cs similarity index 51% rename from src/SimpleL7Proxy/Config/WarmOptions.cs rename to src/SimpleL7Proxy/Config/ConfigOptions.cs index 732d2d63..30821dc5 100644 --- a/src/SimpleL7Proxy/Config/WarmOptions.cs +++ b/src/SimpleL7Proxy/Config/ConfigOptions.cs @@ -1,6 +1,4 @@ using System.Reflection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; namespace SimpleL7Proxy.Config; @@ -86,6 +84,14 @@ public static class ConfigOptions new(() => _warmDescriptors.Value.ToDictionary(d => d.ConfigName, d => d, StringComparer.OrdinalIgnoreCase)); private static readonly Lazy> _warmDescriptorsByKeyPath = new(() => _warmDescriptors.Value.ToDictionary(d => d.Attribute.KeyPath, d => d, StringComparer.OrdinalIgnoreCase)); + + /// Warm descriptors keyed by config name. + public static IReadOnlyDictionary WarmDescriptorsByConfigName => + _warmDescriptorsByConfigName.Value; + + /// Warm descriptors keyed by key path. + public static IReadOnlyDictionary WarmDescriptorsByKeyPath => + _warmDescriptorsByKeyPath.Value; private static readonly Lazy> _fieldsByConfigName = new(() => Descriptors.ToDictionary(d => d.ConfigName, d => d.Property, StringComparer.OrdinalIgnoreCase)); @@ -117,138 +123,6 @@ public static IReadOnlyList GetPublishableDescriptors() /// public const string DefaultPlaceholder = "-"; - // /// - // /// Applies warm-mode config values from the given configuration section - // /// to the target instance. - // /// Only properties with are applied. - // /// Values equal to are ignored, - // /// leaving the built-in code default in place. - // /// - // public static List ApplyWarmTo(BackendOptions target, IConfiguration warmSection, ILogger? logger = null) - // { - // var (changes, parsedValues) = DetectWarmChanges(target, warmSection, logger); - - // foreach (var change in changes) - // { - // if (!parsedValues.TryGetValue(change.PropertyName, out var newValue)) - // continue; - - // if (!_warmDescriptorsByConfigName.Value.TryGetValue(change.PropertyName, out var descriptor)) - // continue; - - // descriptor.Property.SetValue(target, newValue); - // logger?.LogInformation("[CONFIG] Updated {Property}: {Old} → {New}", - // descriptor.ConfigName, change.OldValue, change.NewValue); - // } - - // return changes; - // } - - /// - /// Diffs bare-keyed warm settings against live options. Does not mutate liveOptions. - /// Host keys (Host*, Probe*, IP*) are returned separately in HostChanges. - /// Caller must strip the "Warm:" prefix and exclude the sentinel before calling. - /// - public static (List Changes, Dictionary ParsedValues, Dictionary HostChanges) DetectWarmChanges( - BackendOptions liveOptions, - Dictionary warmSettings, - ILogger? logger = null) - { - var changeList = new List(); - var updates = new Dictionary(StringComparer.OrdinalIgnoreCase); - var hostChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); - var defaultTarget = new BackendOptions(); - var env = new Dictionary(1, StringComparer.OrdinalIgnoreCase); - - foreach (var kvp in warmSettings) - { - var rawValue = kvp.Value; - - if (string.IsNullOrEmpty(rawValue)) - continue; - - var key = kvp.Key; - if (key.StartsWith("Host") || key.StartsWith("Probe") || key.StartsWith("IP")) - { - hostChanges[key] = rawValue; - continue; - } - - if (!_warmDescriptorsByKeyPath.Value.TryGetValue(key, out var descriptor) - && !_warmDescriptorsByConfigName.Value.TryGetValue(key, out descriptor)) - continue; - - var configName = descriptor.ConfigName; - if (!TryGetFieldByConfigName(configName, out var field) || field == null) - continue; - - var currentValue = field.GetValue(liveOptions); - - // Parse the raw value via ApplyFieldFromEnv on a throwaway target. - // Single-entry dict keyed by configName so the lookup matches. - env.Clear(); - env[configName] = rawValue; - - defaultTarget.ApplyFieldFromEnv( - env, - liveOptions, - configName, - field.Name); - - var newValue = field.GetValue(defaultTarget); - - if (DeepEquals(currentValue, newValue)) - continue; - - updates[configName] = newValue; - changeList.Add(new ConfigChange - { - PropertyName = configName, - KeyPath = descriptor.Attribute.KeyPath, - RawOldValue = currentValue, - RawNewValue = newValue - }); - } - - return (changeList, updates, hostChanges); - } - - /// Formats a value for logging, expanding collections to strings. - private static string FormatValue(object? rawValue) - { - if (rawValue == null) return ""; - return rawValue switch - { - string s => s, - int[] arr => string.Join(", ", arr), - IEnumerable list => string.Join(", ", list), - IEnumerable list => string.Join(", ", list), - IDictionary dict => string.Join(", ", dict.Select(kvp => $"{kvp.Key}={kvp.Value}")), - IDictionary dict => string.Join(", ", dict.Select(kvp => $"{kvp.Key}:{kvp.Value}")), - _ => rawValue.ToString() ?? "" - }; - } - - /// Deep-compares two values, handling List, array, and Dictionary types. - private static bool DeepEquals(object? a, object? b) - { - if (ReferenceEquals(a, b)) return true; - if (a is null || b is null) return false; - if (a.GetType() != b.GetType()) return false; - - return (a, b) switch - { - (int[] la, int[] lb) => la.SequenceEqual(lb), - (IList la, IList lb) => la.SequenceEqual(lb), - (IList la, IList lb) => la.SequenceEqual(lb), - (IDictionary da, IDictionary db) => - da.Count == db.Count && da.All(kvp => db.TryGetValue(kvp.Key, out var v) && v == kvp.Value), - (IDictionary da, IDictionary db) => - da.Count == db.Count && da.All(kvp => db.TryGetValue(kvp.Key, out var v) && v == kvp.Value), - _ => Equals(a, b) - }; - } - /// Reflects over BackendOptions to discover all [ConfigOption] properties. private static IReadOnlyList DiscoverDescriptors() { diff --git a/src/SimpleL7Proxy/Config/ConfigParser.cs b/src/SimpleL7Proxy/Config/ConfigParser.cs index 36b23370..dc27ba3f 100644 --- a/src/SimpleL7Proxy/Config/ConfigParser.cs +++ b/src/SimpleL7Proxy/Config/ConfigParser.cs @@ -1,17 +1,18 @@ +using System.Net; +using System.Net.Sockets; using SimpleL7Proxy.Backend.Iterators; +using SimpleL7Proxy.Events; using System.Reflection; namespace SimpleL7Proxy.Config; public static class ConfigParser { - private static readonly Dictionary EnvVars = new(StringComparer.OrdinalIgnoreCase); private static readonly BackendOptions s_defaults = new(); private static readonly System.Data.DataTable s_mathTable = new(); private static readonly (string envVar, string property)[] SimpleFields = - new (string envVar, string property)[] - { + [ ("AsyncBlobWorkerCount", "AsyncBlobWorkerCount"), ("AsyncTimeout", "AsyncTimeout"), ("AsyncTTLSecs", "AsyncTTLSecs"), @@ -110,14 +111,12 @@ private static readonly (string envVar, string property)[] SimpleFields = // ── Security ── ("IgnoreSSLCert", "IgnoreSSLCert"), - }; + ]; // Creates a BackendOptions instance by applying environment variable overrides on top of the defaults public static BackendOptions ApplyEnv(Dictionary dict, BackendOptions defaults) { - EnvVars.Clear(); - // calculated values based on logic var opts = new BackendOptions(); @@ -203,93 +202,12 @@ public static BackendOptions ApplyEnv(Dictionary dict, BackendOp // } /// - /// Applies a single configuration field from the environment dictionary to the target instance. - /// Uses reflection to set the named property, falling back to the corresponding default value when the - /// environment variable is absent or set to the default placeholder. Supports int, double, float, string, - /// bool, List<string>, List<int>, int[], Dictionary<string, string>, and enum property types. + /// + /// Forwards to . Kept for backward compatibility. /// - /// Dictionary of environment/configuration key-value pairs to read from. - /// The instance whose property will be set. - /// A default instance providing fallback values. - /// The environment variable (dictionary key) to look up. - /// The name of the property to set. - /// Thrown when does not exist on . - /// Thrown when the property type is not handled. public static void ApplyFieldFromEnv(Dictionary env, BackendOptions target, BackendOptions defaults, string envVar, string property) { - var pi = typeof(BackendOptions).GetProperty(property) ?? throw new InvalidOperationException($"Unknown BackendOptions property: {property}"); - var defVal = pi.GetValue(defaults); - var type = pi.PropertyType; - - // If the env var is not explicitly provided, check whether the property - // has already been set (by a prior alias). If so, skip — don't overwrite - // a previously resolved value with the default. - var envValue = env.GetValueOrDefault(envVar)?.Trim(); - bool envVarPresent = !string.IsNullOrEmpty(envValue) && envValue != ConfigOptions.DefaultPlaceholder; - if (!envVarPresent) - { - var currentVal = pi.GetValue(target); - bool alreadyChanged = !Equals(currentVal, defVal); - if (alreadyChanged) - { - return; - } - } - - if (type == typeof(int) || type == typeof(double)) - { - var val = ReadEnvironmentVariableOrDefault(env, envVar, Convert.ToInt32(defVal)); - pi.SetValue(target, Convert.ChangeType(val, type)); - } - else if (type == typeof(float)) - { - var val = ReadEnvironmentVariableOrDefault(env, envVar, Convert.ToSingle(defVal)); - pi.SetValue(target, Convert.ChangeType(val, type)); - } - else if (type == typeof(string)) - { - pi.SetValue(target, ReadEnvironmentVariableOrDefault(env, envVar, (string)defVal!)); - } - else if (type == typeof(bool)) - { - pi.SetValue(target, ReadEnvironmentVariableOrDefault(env, envVar, (bool)defVal!)); - } - else if (type == typeof(List)) - { - var value = ReadEnvironmentVariableOrDefault(env, envVar, string.Join(",", (List)defVal!)); - pi.SetValue(target, ToListOfString(value)); - } - else if (type == typeof(List)) - { - var value = ReadEnvironmentVariableOrDefault(env, envVar, string.Join(",", (List)defVal!)); - pi.SetValue(target, ToListOfInt(value)); - } - else if (type == typeof(int[])) - { - pi.SetValue(target, ReadEnvironmentVariableOrDefault(env, envVar, (int[])defVal!)); - } - else if (type == typeof(Dictionary)) - { - var defaultValue = string.Join(",", ((Dictionary)defVal!).Select(kvp => $"{kvp.Key}={kvp.Value}")); - var value = ReadEnvironmentVariableOrDefault(env, envVar, defaultValue); - pi.SetValue(target, KVStringPairs(ToListOfString(value))); - } - else if (type.IsEnum) - { - var value = ReadEnvironmentVariableOrDefault(env, envVar, defVal!.ToString()!); - if (Enum.TryParse(type, value, true, out var parsed)) - { - pi.SetValue(target, parsed); - } - else - { - pi.SetValue(target, defVal); - } - } - else - { - throw new NotSupportedException($"ApplyFieldFromEnv: unsupported property type {type.Name} for {property}"); - } + target.ApplyFieldFromEnv(env, defaults, envVar, property); } public static void ApplyDerivedSettings(BackendOptions backendOptions, params PropertyInfo[] changedProperties) @@ -510,52 +428,40 @@ private static bool TryEvaluateMathExpression(string expression, out double resu } } - private static int ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, int defaultValue) + public static int ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, int defaultValue) { - int value = ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); - EnvVars[variableName] = value.ToString(); - return value; + return ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); } - private static int[] ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, int[] defaultValues) + public static int[] ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, int[] defaultValues) { - int[] value = ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValues); - EnvVars[variableName] = string.Join(",", value); - return value; + return ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValues); } - private static float ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, float defaultValue) + public static float ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, float defaultValue) { - float value = ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); - EnvVars[variableName] = value.ToString(); - return value; + return ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); } - private static string ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, string defaultValue) + public static string ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, string defaultValue) { - string value = ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); - EnvVars[variableName] = value; - return value; + return ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); } - private static IterationModeEnum ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, IterationModeEnum defaultValue) + public static IterationModeEnum ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, IterationModeEnum defaultValue) { string? envValue = env.GetValueOrDefault(variableName)?.Trim(); if (string.IsNullOrEmpty(envValue) || envValue == ConfigOptions.DefaultPlaceholder || !Enum.TryParse(envValue, true, out IterationModeEnum value)) { - EnvVars[variableName] = defaultValue.ToString(); return defaultValue; } - EnvVars[variableName] = value.ToString(); return value; } - private static bool ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, bool defaultValue) + public static bool ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, bool defaultValue) { - bool value = ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); - EnvVars[variableName] = value.ToString(); - return value; + return ReadEnvironmentVariableOrDefaultCore(env, variableName, defaultValue); } private static int ReadEnvironmentVariableOrDefaultCore(Dictionary env, string variableName, int defaultValue) @@ -654,7 +560,7 @@ private static Dictionary KVIntPairs(List list) return keyValuePairs; } - private static Dictionary KVStringPairs(List list, char delimiter = '=') + public static Dictionary KVStringPairs(List list, char delimiter = '=') { char fallback = delimiter == '=' ? ':' : '='; Dictionary keyValuePairs = []; @@ -678,7 +584,7 @@ private static Dictionary KVStringPairs(List list, char return keyValuePairs; } - private static List ToListOfString(string s) + public static List ToListOfString(string s) { if (string.IsNullOrEmpty(s)) { @@ -694,7 +600,7 @@ private static List ToListOfString(string s) return [.. trimmed.Split(',').Select(p => p.Trim().Trim('"')).Where(p => p.Length > 0)]; } - private static List ToListOfInt(string s) + public static List ToListOfInt(string s) { if (string.IsNullOrEmpty(s)) { @@ -847,4 +753,116 @@ private static (string connectionString, string accountUri, bool useMI) ParseBlo return (connectionString, accountUri, useMI); } + + /// + /// Creates and assigns an on this + /// instance, configured from the transport-related properties (keep-alive, HTTP/2, SSL). + /// + public static void ConfigureHttpClient(BackendOptions backendOptions) + { + var safeKeepAliveInitialDelaySecs = Math.Max(1, backendOptions.KeepAliveInitialDelaySecs); + var safeKeepAlivePingIntervalSecs = Math.Max(1, backendOptions.KeepAlivePingIntervalSecs); + + var retryCount = Math.Max(1, backendOptions.KeepAliveIdleTimeoutSecs / safeKeepAlivePingIntervalSecs); + var handler = CreateSocketsHandler(safeKeepAliveInitialDelaySecs, safeKeepAlivePingIntervalSecs, retryCount); + + if (backendOptions.EnableMultipleHttp2Connections) + { + handler.EnableMultipleHttp2Connections = true; + handler.PooledConnectionLifetime = TimeSpan.FromSeconds(backendOptions.MultiConnLifetimeSecs); + handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(backendOptions.MultiConnIdleTimeoutSecs); + handler.MaxConnectionsPerServer = backendOptions.MultiConnMaxConns; + handler.ResponseDrainTimeout = TimeSpan.FromSeconds(backendOptions.KeepAliveIdleTimeoutSecs); + Console.WriteLine("Multiple HTTP/2 connections enabled."); + } + else + { + handler.EnableMultipleHttp2Connections = false; + Console.WriteLine("Multiple HTTP/2 connections disabled."); + } + + // Configure SSL handling + if (backendOptions.IgnoreSSLCert) + { + handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true + }; + Console.WriteLine("Ignoring SSL certificate validation errors."); + } + + HttpClient client = new HttpClient(handler) + { + // set timeout to large to disable it at HttpClient level. Will use token cancellation for timeout instead. + Timeout = Timeout.InfiniteTimeSpan + }; + + backendOptions.Client = client; + } + + private static SocketsHttpHandler CreateSocketsHandler(int initialDelaySecs, int intervalSecs, int linuxRetryCount) + { + SocketsHttpHandler handler = new SocketsHttpHandler(); + handler.ConnectCallback = async (ctx, ct) => + { + DnsEndPoint dnsEndPoint = ctx.DnsEndPoint; + IPAddress[] addresses = await Dns.GetHostAddressesAsync(dnsEndPoint.Host, dnsEndPoint.AddressFamily, ct).ConfigureAwait(false); + var s = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + try + { + bool linuxKeepAliveConfigured = false; + + // Basic keep-alive setting - should work on all platforms + s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + try + { + if (OperatingSystem.IsWindows()) + { + // Windows-specific approach using IOControl + byte[] keepAliveValues = new byte[12]; + BitConverter.GetBytes((uint)1).CopyTo(keepAliveValues, 0); // Turn keep-alive on + BitConverter.GetBytes((uint)(initialDelaySecs * 1000)).CopyTo(keepAliveValues, 4); + BitConverter.GetBytes((uint)(intervalSecs * 1000)).CopyTo(keepAliveValues, 8); + + s.IOControl(IOControlCode.KeepAliveValues, keepAliveValues, null); + } + else if (OperatingSystem.IsLinux()) + { + s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, initialDelaySecs); + s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, intervalSecs); + s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, linuxRetryCount); + linuxKeepAliveConfigured = true; + } + } + catch (Exception ex) + { + ProxyEvent pe = new() + { + Type = EventType.Exception, + Exception = ex, + ["Message"] = "Failed to set TCP keep-alive parameters", + ["Host"] = dnsEndPoint.Host, + ["Port"] = dnsEndPoint.Port.ToString(), + ["InitialDelaySecs"] = initialDelaySecs.ToString(), + ["IntervalSecs"] = intervalSecs.ToString(), + ["LinuxRetryCount"] = linuxRetryCount.ToString(), + ["linuxKeepAliveConfigured"] = linuxKeepAliveConfigured.ToString() + }; + pe.SendEvent(); + } + + // Connect to the endpoint + await s.ConnectAsync(addresses, dnsEndPoint.Port, ct).ConfigureAwait(false); + return new NetworkStream(s, ownsSocket: true); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Socket connection error: {ex.Message}"); + s.Dispose(); + throw; + } + }; + + return handler; + } } diff --git a/src/SimpleL7Proxy/Constants.cs b/src/SimpleL7Proxy/Constants.cs index 00425c09..4487e03c 100644 --- a/src/SimpleL7Proxy/Constants.cs +++ b/src/SimpleL7Proxy/Constants.cs @@ -13,7 +13,7 @@ public static class Constants public const string RoundRobin = "roundrobin"; public const string Random = "random"; public const string Server = "simplel7proxy"; - public const string VERSION = "2.2.10.4"; + public const string VERSION = "2.2.10.5-D1"; public const int AnyPriority = -1; diff --git a/src/SimpleL7Proxy/User/UserProfile.cs b/src/SimpleL7Proxy/User/UserProfile.cs index f035b35c..cb7b9ad5 100644 --- a/src/SimpleL7Proxy/User/UserProfile.cs +++ b/src/SimpleL7Proxy/User/UserProfile.cs @@ -152,6 +152,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { DateTime startTime = DateTime.UtcNow; sb.Clear(); + sb.Append("[PROFILE-READER] "); bool success = false; bool localIsInitialized = true; @@ -191,14 +192,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (!profileConfigStatus.HasData) localIsInitialized = false; - sb.Append($"Profile- {profileConfigStatus}"); + sb.Append($"(Profiles) {profileConfigStatus}"); } if (suspendedTask != null) { var suspendedStatus = await suspendedTask.ConfigureAwait(false); success = success && suspendedStatus.Success; - sb.Append($", Suspended- {suspendedUserConfigStatus}"); + sb.Append($", (Suspended) {suspendedUserConfigStatus}"); } if (authTask != null) @@ -220,7 +221,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (!authAppIDsConfigStatus.HasData) localIsInitialized = false; - sb.Append($", AuthAppIDs- {authAppIDsConfigStatus}"); + sb.Append($", (AuthAppIDs) {authAppIDsConfigStatus}"); } if (sb.Length > 0) @@ -377,7 +378,7 @@ private void LogFailedLoads(CurrentRequestStatus profileStatus, CurrentRequestSt } } - // TODO make all three the same call + // TODO make all three the same private async Task ProfileReader(ConfigStatus status, string url, ParsingMode mode) { From b6487582cc86b03e6505c4cfcaff4679debc0467 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Mon, 30 Mar 2026 13:52:06 -0400 Subject: [PATCH 02/18] ren: BackenOptions,BackendOptionsBuilder,ConfigOptions,AppConfigBootstrap => ProxyConfig,ConfigFactory,ConfigMetadata,AppConfigService --- src/SimpleL7Proxy/Backend/Backends.cs | 4 +- src/SimpleL7Proxy/Backend/CircuitBreaker.cs | 2 +- .../BlobStorage/BlobWriterFactory.cs | 4 +- ...ConfigBootstrap.cs => AppConfigService.cs} | 16 ++--- src/SimpleL7Proxy/Config/ConfigChange.cs | 2 +- .../Config/ConfigChangeNotifer.cs | 22 +++---- ...kendOptionsBuilder.cs => ConfigFactory.cs} | 42 ++++++------ .../{ConfigOptions.cs => ConfigMetadata.cs} | 4 +- src/SimpleL7Proxy/Config/ConfigParser.cs | 64 +++++++++---------- src/SimpleL7Proxy/Config/DefaultCredential.cs | 2 +- .../Config/IConfigChangeSubscriber.cs | 4 +- .../{BackendOptions.cs => ProxyConfig.cs} | 12 ++-- .../Events/BackupAPI/BackupStatService.cs | 4 +- .../Events/CommonEventHeaders.cs | 2 +- src/SimpleL7Proxy/Events/EventDataBuilder.cs | 4 +- src/SimpleL7Proxy/Events/EventHubClient.cs | 2 +- src/SimpleL7Proxy/Events/EventHubConfig.cs | 2 +- .../Events/LogFileEventClient.cs | 2 +- src/SimpleL7Proxy/Events/ProxyEvent.cs | 12 ++-- .../Events/ServiceBus/ServiceBusFactory.cs | 4 +- .../ServiceBus/ServiceBusRequestService.cs | 4 +- src/SimpleL7Proxy/Feeder/AsyncFeeder.cs | 4 +- src/SimpleL7Proxy/Feeder/NormalRequest.cs | 4 +- .../Feeder/OpenAIBackgroundRequest.cs | 4 +- src/SimpleL7Proxy/ProbeServer.cs | 6 +- src/SimpleL7Proxy/Program.cs | 37 ++++++----- src/SimpleL7Proxy/Proxy/AsyncWorker.cs | 4 +- src/SimpleL7Proxy/Proxy/AsyncWorkerFactory.cs | 4 +- src/SimpleL7Proxy/Proxy/HealthCheckService.cs | 4 +- src/SimpleL7Proxy/Proxy/ProxyWorker.cs | 4 +- .../Proxy/RequestLifecycleManager.cs | 4 +- src/SimpleL7Proxy/Proxy/WorkerContext.cs | 4 +- src/SimpleL7Proxy/Proxy/WorkerFactory.cs | 2 +- src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs | 4 +- src/SimpleL7Proxy/RequestData.cs | 4 +- src/SimpleL7Proxy/User/UserPriority.cs | 4 +- src/SimpleL7Proxy/User/UserProfile.cs | 6 +- src/SimpleL7Proxy/server.cs | 6 +- 38 files changed, 162 insertions(+), 157 deletions(-) rename src/SimpleL7Proxy/Config/{AppConfigBootstrap.cs => AppConfigService.cs} (95%) rename src/SimpleL7Proxy/Config/{BackendOptionsBuilder.cs => ConfigFactory.cs} (92%) rename src/SimpleL7Proxy/Config/{ConfigOptions.cs => ConfigMetadata.cs} (98%) rename src/SimpleL7Proxy/Config/{BackendOptions.cs => ProxyConfig.cs} (98%) diff --git a/src/SimpleL7Proxy/Backend/Backends.cs b/src/SimpleL7Proxy/Backend/Backends.cs index 8c22e792..175fde4e 100644 --- a/src/SimpleL7Proxy/Backend/Backends.cs +++ b/src/SimpleL7Proxy/Backend/Backends.cs @@ -29,7 +29,7 @@ public class Backends : IBackendService /// private List _backendHosts => _backendHostCollection.Current.Hosts; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private static readonly bool _debug = false; private static double _successRate; @@ -57,7 +57,7 @@ public class Backends : IBackendService private Task? PollerTask; //public Backends(List hosts, HttpClient client, int interval, int successRate) public Backends( - IOptions options, + IOptions options, ICircuitBreaker circuitBreaker, IHostHealthCollection backendHostCollection, // IHostApplicationLifetime appLifetime, // diff --git a/src/SimpleL7Proxy/Backend/CircuitBreaker.cs b/src/SimpleL7Proxy/Backend/CircuitBreaker.cs index 2d96d15c..bf49f54f 100644 --- a/src/SimpleL7Proxy/Backend/CircuitBreaker.cs +++ b/src/SimpleL7Proxy/Backend/CircuitBreaker.cs @@ -40,7 +40,7 @@ public class CircuitBreaker : ICircuitBreaker public string ID { get; set; } = ""; - public CircuitBreaker(IOptions options, ILogger logger) + public CircuitBreaker(IOptions options, ILogger logger) { ArgumentNullException.ThrowIfNull(options?.Value, nameof(options)); ArgumentNullException.ThrowIfNull(logger, nameof(logger)); diff --git a/src/SimpleL7Proxy/BlobStorage/BlobWriterFactory.cs b/src/SimpleL7Proxy/BlobStorage/BlobWriterFactory.cs index 7a18ed79..2a65dc7d 100644 --- a/src/SimpleL7Proxy/BlobStorage/BlobWriterFactory.cs +++ b/src/SimpleL7Proxy/BlobStorage/BlobWriterFactory.cs @@ -18,13 +18,13 @@ public interface IBlobWriterFactory public class BlobWriterFactory : IBlobWriterFactory { private readonly DefaultCredential _defaultCredential; - private readonly IOptionsMonitor _optionsMonitor; + private readonly IOptionsMonitor _optionsMonitor; private readonly ILogger _logger; private readonly ILogger _nullBlobWriterLogger; public BlobWriterFactory( DefaultCredential defaultCredential, - IOptionsMonitor optionsMonitor, + IOptionsMonitor optionsMonitor, ILogger logger, ILogger nullBlobWriterLogger) { diff --git a/src/SimpleL7Proxy/Config/AppConfigBootstrap.cs b/src/SimpleL7Proxy/Config/AppConfigService.cs similarity index 95% rename from src/SimpleL7Proxy/Config/AppConfigBootstrap.cs rename to src/SimpleL7Proxy/Config/AppConfigService.cs index c089ad42..73755ddc 100644 --- a/src/SimpleL7Proxy/Config/AppConfigBootstrap.cs +++ b/src/SimpleL7Proxy/Config/AppConfigService.cs @@ -11,14 +11,14 @@ namespace SimpleL7Proxy.Config; /// initial download before DI, then periodic sentinel-based refresh /// running as a BackgroundService inside the host. /// -public class AppConfigBootstrap : BackgroundService +public class AppConfigService : BackgroundService { private Task<(Dictionary warm, Dictionary cold)?>? _downloadTask; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly string? _endpoint; private readonly string? _connectionString; private readonly string? _labelFilter; - private BackendOptions _options; + private ProxyConfig _options; private readonly DefaultCredential _defaultCredential; private readonly TimeSpan _refreshInterval; private bool _isInitialized = false; @@ -40,10 +40,10 @@ public class AppConfigBootstrap : BackgroundService /// Only cold-prefixed settings, available after has been awaited. public Dictionary? ColdSettings { get; private set; } - public static BackendOptions DEFAULT_OPTIONS { get; set; } + public static ProxyConfig DEFAULT_OPTIONS { get; set; } - public AppConfigBootstrap(ILogger logger, BackendOptions backendOptions, DefaultCredential defaultCredential) + public AppConfigService(ILogger logger, ProxyConfig backendOptions, DefaultCredential defaultCredential) { _logger = logger; _defaultCredential = defaultCredential; @@ -134,7 +134,7 @@ private void CommitDownload(Dictionary warm, Dictionary - public void RegisterServices(IServiceCollection services, BackendOptions options) + public void RegisterServices(IServiceCollection services, ProxyConfig options) { _options = options; if (!_isInitialized) return; @@ -178,7 +178,7 @@ private async Task ProcessRefreshAsync(CancellationToken ct) if (result == null || Notifier == null) return; - await BackendOptionsBuilder.ApplyRefresh( + await ConfigFactory.ApplyRefresh( _options, DEFAULT_OPTIONS, warm, Notifier, HostCollection, _logger, ct); } @@ -222,7 +222,7 @@ private ConfigurationClient GetConfigurationClient() // Build a lookup from App Config key path → env var name using the descriptors. // e.g. "Logging:LogConsole" → "LogConsole", "Async:Timeout" → "AsyncTimeout" - var keyPathToEnvVar = ConfigOptions.Descriptors + var keyPathToEnvVar = ConfigMetadata.Descriptors .ToDictionary(d => d.Attribute.KeyPath, d => d.ConfigName, StringComparer.OrdinalIgnoreCase); var warm = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/SimpleL7Proxy/Config/ConfigChange.cs b/src/SimpleL7Proxy/Config/ConfigChange.cs index e88720fe..eac07a7f 100644 --- a/src/SimpleL7Proxy/Config/ConfigChange.cs +++ b/src/SimpleL7Proxy/Config/ConfigChange.cs @@ -10,7 +10,7 @@ public readonly record struct ConfigChange private readonly object? _oldValue; private readonly object? _newValue; - /// Property name on (e.g. "LogConsole"). + /// Property name on (e.g. "LogConsole"). public string PropertyName { get; init; } /// The App Configuration key path (e.g. "Logging:LogConsole"). diff --git a/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs b/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs index ee44d198..08966380 100644 --- a/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs +++ b/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs @@ -67,7 +67,7 @@ public void Subscribe(IConfigChangeSubscriber subscriber, params string[] fields /// Returns a handle that can be passed to . /// public IConfigChangeSubscriber Subscribe( - Func, BackendOptions, CancellationToken, Task> callback, + Func, ProxyConfig, CancellationToken, Task> callback, params string[] fields) { var wrapper = new DelegateSubscriber(callback); @@ -76,24 +76,24 @@ public IConfigChangeSubscriber Subscribe( } /// - /// Register a subscriber for specific properties. + /// Register a subscriber for specific properties. /// This avoids callers needing to know config/env field names. /// public void Subscribe( IConfigChangeSubscriber subscriber, - params Expression>[] fields) + params Expression>[] fields) { var configNames = ResolveConfigNames(fields); Subscribe(subscriber, configNames); } /// - /// Register a callback for specific properties. + /// Register a callback for specific properties. /// Returns a handle that can be passed to . /// public IConfigChangeSubscriber Subscribe( - Func, BackendOptions, CancellationToken, Task> callback, - params Expression>[] fields) + Func, ProxyConfig, CancellationToken, Task> callback, + params Expression>[] fields) { var configNames = ResolveConfigNames(fields); return Subscribe(callback, configNames); @@ -147,7 +147,7 @@ public void Unsubscribe(IConfigChangeSubscriber subscriber) /// internal async Task NotifyAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken) { if (changes.Count == 0) return; @@ -209,14 +209,14 @@ internal async Task NotifyAsync( return merged; } - private static string[] ResolveConfigNames(Expression>[] fields) + private static string[] ResolveConfigNames(Expression>[] fields) { if (fields.Length == 0) { return []; } - var descriptorByPropertyName = ConfigOptions.GetDescriptors() + var descriptorByPropertyName = ConfigMetadata.GetDescriptors() .ToDictionary(d => d.Property.Name, d => d.ConfigName, StringComparer.OrdinalIgnoreCase); var configNames = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -258,12 +258,12 @@ private sealed record Subscription(IConfigChangeSubscriber Subscriber, HashSetWraps a lambda/delegate as an . private sealed class DelegateSubscriber( - Func, BackendOptions, CancellationToken, Task> callback) + Func, ProxyConfig, CancellationToken, Task> callback) : IConfigChangeSubscriber { public Task OnConfigChangedAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken) => callback(changes, backendOptions, cancellationToken); } } diff --git a/src/SimpleL7Proxy/Config/BackendOptionsBuilder.cs b/src/SimpleL7Proxy/Config/ConfigFactory.cs similarity index 92% rename from src/SimpleL7Proxy/Config/BackendOptionsBuilder.cs rename to src/SimpleL7Proxy/Config/ConfigFactory.cs index 9f0ea474..3f2f06fd 100644 --- a/src/SimpleL7Proxy/Config/BackendOptionsBuilder.cs +++ b/src/SimpleL7Proxy/Config/ConfigFactory.cs @@ -26,7 +26,7 @@ namespace SimpleL7Proxy.Config; /// Handles environment variable collection, App Config merging, /// backend host discovery, and DI registration. /// -public static class BackendOptionsBuilder +public static class ConfigFactory { private static ILogger? _logger; @@ -62,13 +62,13 @@ public static Dictionary EffectiveEnvironment() /// envOptions — env vars + App Config warm/cold overrides merged on top (warm wins on collision). This is the live singleton. /// /// - public static async Task<(BackendOptions baseOptions, BackendOptions envOptions)> CreateOptions(AppConfigBootstrap appConfigBootstrap) + public static async Task<(ProxyConfig baseOptions, ProxyConfig envOptions)> CreateOptions(AppConfigService appConfigBootstrap) { - var baseOptions = ConfigParser.ApplyEnv(EffectiveEnvironment(), new BackendOptions()); + var baseOptions = ConfigParser.ApplyEnv(EffectiveEnvironment(), new ProxyConfig()); var (warmSettings, coldSettings) = await appConfigBootstrap.GetSettingsAsync().ConfigureAwait(false); - BackendOptions envOptions; + ProxyConfig envOptions; if (warmSettings == null && coldSettings == null) { envOptions = baseOptions.DeepClone(); @@ -93,8 +93,8 @@ public static Dictionary EffectiveEnvironment() /// Called by AppConfigBootstrap when the sentinel value changes. /// public static async Task ApplyRefresh( - BackendOptions liveOptions, - BackendOptions defaultOptions, + ProxyConfig liveOptions, + ProxyConfig defaultOptions, Dictionary warm, ConfigChangeNotifier? notifier, IHostHealthCollection? hostCollection, @@ -105,7 +105,7 @@ public static async Task ApplyRefresh( if (changes.Count == 0 && hostChanges.Count == 0) return; - var fields = ConfigOptions.GetFieldsByConfigName(); + var fields = ConfigMetadata.GetFieldsByConfigName(); var ds = new Dictionary(1); foreach (var kvp in parsedValues) @@ -151,14 +151,14 @@ public static async Task ApplyRefresh( /// Host keys (Host*, Probe*, IP*) are returned separately in HostChanges. /// private static (List Changes, Dictionary ParsedValues, Dictionary HostChanges) DetectWarmChanges( - BackendOptions liveOptions, + ProxyConfig liveOptions, Dictionary warmSettings, ILogger? logger = null) { var changeList = new List(); var updates = new Dictionary(StringComparer.OrdinalIgnoreCase); var hostChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); - var defaultTarget = new BackendOptions(); + var defaultTarget = new ProxyConfig(); var env = new Dictionary(1, StringComparer.OrdinalIgnoreCase); foreach (var kvp in warmSettings) @@ -173,12 +173,12 @@ private static (List Changes, Dictionary ParsedVa continue; } - if (!ConfigOptions.WarmDescriptorsByKeyPath.TryGetValue(key, out var descriptor) - && !ConfigOptions.WarmDescriptorsByConfigName.TryGetValue(key, out descriptor)) + if (!ConfigMetadata.WarmDescriptorsByKeyPath.TryGetValue(key, out var descriptor) + && !ConfigMetadata.WarmDescriptorsByConfigName.TryGetValue(key, out descriptor)) continue; var configName = descriptor.ConfigName; - if (!ConfigOptions.TryGetFieldByConfigName(configName, out var field) || field == null) + if (!ConfigMetadata.TryGetFieldByConfigName(configName, out var field) || field == null) continue; var currentValue = field.GetValue(liveOptions); @@ -229,7 +229,7 @@ private static bool DeepEquals(object? a, object? b) /// Emits a telemetry event with all resolved config values, /// masking sensitive keys (connection strings, secrets, etc.). /// - public static void OutputEnvVars(BackendOptions backendOptions) + public static void OutputEnvVars(ProxyConfig backendOptions) { ProxyEvent pe = new() @@ -242,7 +242,7 @@ public static void OutputEnvVars(BackendOptions backendOptions) var cold = new SortedDictionary(StringComparer.OrdinalIgnoreCase); var hidden = new SortedDictionary(StringComparer.OrdinalIgnoreCase); - foreach (var prop in typeof(BackendOptions).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + foreach (var prop in typeof(ProxyConfig).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var attr = prop.GetCustomAttribute(); if (attr == null) continue; @@ -300,14 +300,14 @@ static string FormatValue(object? rawValue) /// Registers BackendOptions as a singleton for IOptions, IOptionsMonitor, and direct injection. /// All resolve to the same instance so in-place warm-refresh is visible everywhere. /// - public static IServiceCollection RegisterBackendOptions(this IServiceCollection services, ILogger logger, BackendOptions backendOptions) + public static IServiceCollection RegisterBackendOptions(this IServiceCollection services, ILogger logger, ProxyConfig backendOptions) { _logger = logger; - var wrapper = new OptionsWrapper(backendOptions); + var wrapper = new OptionsWrapper(backendOptions); services.AddSingleton(backendOptions); - services.AddSingleton>(wrapper); - services.AddSingleton>(new SingletonOptionsMonitor(backendOptions)); + services.AddSingleton>(wrapper); + services.AddSingleton>(new SingletonOptionsMonitor(backendOptions)); return services; } @@ -352,7 +352,7 @@ private static bool TryEvaluateMathExpression(string expression, out double resu private static int _ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, int defaultValue) { var envValue = env.GetValueOrDefault(variableName); - if (envValue?.Trim() == ConfigOptions.DefaultPlaceholder) envValue = null; + if (envValue?.Trim() == ConfigMetadata.DefaultPlaceholder) envValue = null; if (!int.TryParse(envValue, out var value)) { if (TryEvaluateMathExpression(envValue!, out var mathResult)) @@ -366,7 +366,7 @@ private static int _ReadEnvironmentVariableOrDefault(Dictionary private static bool _ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, bool defaultValue) { var envValue = env.GetValueOrDefault(variableName); - if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigOptions.DefaultPlaceholder) + if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigMetadata.DefaultPlaceholder) { _logger?.LogWarning($"Using default: {variableName}: {defaultValue}"); return defaultValue; @@ -380,7 +380,7 @@ private static bool _ReadEnvironmentVariableOrDefault(Dictionary /// Optionally appends to /etc/hosts for Linux container deployments. /// public static void RegisterBackends( - BackendOptions backendOptions, + ProxyConfig backendOptions, IConfiguration? fallbackConfig = null, Dictionary? appConfigSettings = null, IHostHealthCollection? hostCollection = null) diff --git a/src/SimpleL7Proxy/Config/ConfigOptions.cs b/src/SimpleL7Proxy/Config/ConfigMetadata.cs similarity index 98% rename from src/SimpleL7Proxy/Config/ConfigOptions.cs rename to src/SimpleL7Proxy/Config/ConfigMetadata.cs index 30821dc5..84dde2bf 100644 --- a/src/SimpleL7Proxy/Config/ConfigOptions.cs +++ b/src/SimpleL7Proxy/Config/ConfigMetadata.cs @@ -75,7 +75,7 @@ public sealed class ConfigOptionDescriptor /// Discovers [ConfigOption] descriptors on BackendOptions and provides /// warm change detection for hot-reload. /// -public static class ConfigOptions +public static class ConfigMetadata { private static readonly Lazy> _descriptors = new(DiscoverDescriptors); private static readonly Lazy> _warmDescriptors = @@ -126,7 +126,7 @@ public static IReadOnlyList GetPublishableDescriptors() /// Reflects over BackendOptions to discover all [ConfigOption] properties. private static IReadOnlyList DiscoverDescriptors() { - return typeof(BackendOptions) + return typeof(ProxyConfig) .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(prop => prop.CanRead && prop.CanWrite) .Select(prop => new diff --git a/src/SimpleL7Proxy/Config/ConfigParser.cs b/src/SimpleL7Proxy/Config/ConfigParser.cs index dc27ba3f..89951e0e 100644 --- a/src/SimpleL7Proxy/Config/ConfigParser.cs +++ b/src/SimpleL7Proxy/Config/ConfigParser.cs @@ -8,7 +8,7 @@ namespace SimpleL7Proxy.Config; public static class ConfigParser { - private static readonly BackendOptions s_defaults = new(); + private static readonly ProxyConfig s_defaults = new(); private static readonly System.Data.DataTable s_mathTable = new(); private static readonly (string envVar, string property)[] SimpleFields = @@ -115,10 +115,10 @@ private static readonly (string envVar, string property)[] SimpleFields = // Creates a BackendOptions instance by applying environment variable overrides on top of the defaults - public static BackendOptions ApplyEnv(Dictionary dict, BackendOptions defaults) + public static ProxyConfig ApplyEnv(Dictionary dict, ProxyConfig defaults) { // calculated values based on logic - var opts = new BackendOptions(); + var opts = new ProxyConfig(); foreach (var (envVarName, propertyName) in SimpleFields) { @@ -143,11 +143,11 @@ public static BackendOptions ApplyEnv(Dictionary dict, BackendOp ApplyDerivedSettingsFromConfigNames( opts, - nameof(BackendOptions.HealthProbeSidecar), - nameof(BackendOptions.LoadBalanceMode), - nameof(BackendOptions.PriorityKeys), - nameof(BackendOptions.PriorityValues), - nameof(BackendOptions.ValidateHeaders)); + nameof(ProxyConfig.HealthProbeSidecar), + nameof(ProxyConfig.LoadBalanceMode), + nameof(ProxyConfig.PriorityKeys), + nameof(ProxyConfig.PriorityValues), + nameof(ProxyConfig.ValidateHeaders)); return opts; } @@ -205,12 +205,12 @@ public static BackendOptions ApplyEnv(Dictionary dict, BackendOp /// /// Forwards to . Kept for backward compatibility. /// - public static void ApplyFieldFromEnv(Dictionary env, BackendOptions target, BackendOptions defaults, string envVar, string property) + public static void ApplyFieldFromEnv(Dictionary env, ProxyConfig target, ProxyConfig defaults, string envVar, string property) { target.ApplyFieldFromEnv(env, defaults, envVar, property); } - public static void ApplyDerivedSettings(BackendOptions backendOptions, params PropertyInfo[] changedProperties) + public static void ApplyDerivedSettings(ProxyConfig backendOptions, params PropertyInfo[] changedProperties) { if (changedProperties.Length == 0) { @@ -221,30 +221,30 @@ public static void ApplyDerivedSettings(BackendOptions backendOptions, params Pr changedProperties.Select(p => p.Name), StringComparer.OrdinalIgnoreCase); - if (changedPropertyNames.Contains(nameof(BackendOptions.HealthProbeSidecar))) + if (changedPropertyNames.Contains(nameof(ProxyConfig.HealthProbeSidecar))) { ParseHealthProbeSidecarSettings(backendOptions); } - if (changedPropertyNames.Contains(nameof(BackendOptions.LoadBalanceMode))) + if (changedPropertyNames.Contains(nameof(ProxyConfig.LoadBalanceMode))) { ValidateLoadBalanceMode(backendOptions); } - if (changedPropertyNames.Contains(nameof(BackendOptions.PriorityKeys)) - || changedPropertyNames.Contains(nameof(BackendOptions.PriorityValues))) + if (changedPropertyNames.Contains(nameof(ProxyConfig.PriorityKeys)) + || changedPropertyNames.Contains(nameof(ProxyConfig.PriorityValues))) { ValidatePrioritySettings(backendOptions, s_defaults); } - if (changedPropertyNames.Contains(nameof(BackendOptions.ValidateHeaders))) + if (changedPropertyNames.Contains(nameof(ProxyConfig.ValidateHeaders))) { ValidateHeaderSettings(backendOptions); } } public static void ApplyDerivedSettingsFromConfigNames( - BackendOptions backendOptions, + ProxyConfig backendOptions, params string[] changedConfigNames) { if (changedConfigNames.Length == 0) @@ -252,7 +252,7 @@ public static void ApplyDerivedSettingsFromConfigNames( return; } - var descriptorByConfigName = ConfigOptions.GetDescriptors() + var descriptorByConfigName = ConfigMetadata.GetDescriptors() .ToDictionary(d => d.ConfigName, d => d.Property, StringComparer.OrdinalIgnoreCase); var changedProperties = new List(changedConfigNames.Length); @@ -311,7 +311,7 @@ static bool IsIndexedKey(string value, string prefix) || normalized.Equals("AppendHostsFile", StringComparison.OrdinalIgnoreCase); } - private static void ApplyAsyncServiceBusOverrides(Dictionary env, BackendOptions opts, BackendOptions defaults) + private static void ApplyAsyncServiceBusOverrides(Dictionary env, ProxyConfig opts, ProxyConfig defaults) { var configStr = ReadEnvironmentVariableOrDefault(env, "AsyncSBConfig", defaults.AsyncSBConfig); var (connStr, ns, queue, useMi) = ParseServiceBusConfig(configStr); @@ -322,7 +322,7 @@ private static void ApplyAsyncServiceBusOverrides(Dictionary env opts.AsyncSBUseMI = ReadEnvironmentVariableOrDefault(env, "AsyncSBUseMI", useMi); } - private static void ApplyAsyncBlobStorageOverrides(Dictionary env, BackendOptions opts, BackendOptions defaults) + private static void ApplyAsyncBlobStorageOverrides(Dictionary env, ProxyConfig opts, ProxyConfig defaults) { var configStr = ReadEnvironmentVariableOrDefault(env, "AsyncBlobStorageConfig", defaults.AsyncBlobStorageConfig); var (connStr, accountUri, useMi) = ParseBlobStorageConfig(configStr); @@ -332,13 +332,13 @@ private static void ApplyAsyncBlobStorageOverrides(Dictionary en opts.AsyncBlobStorageUseMI = ReadEnvironmentVariableOrDefault(env, "AsyncBlobStorageUseMI", useMi); } - private static void ApplyReplicaIdentitySettings(Dictionary env, BackendOptions opts, string replicaId) + private static void ApplyReplicaIdentitySettings(Dictionary env, ProxyConfig opts, string replicaId) { opts.HostName = ReadEnvironmentVariableOrDefault(env, "Hostname", replicaId); opts.IDStr = $"{ReadEnvironmentVariableOrDefault(env, "RequestIDPrefix", "S7P")}-{replicaId}-"; } - private static void ParseHealthProbeSidecarSettings(BackendOptions backendOptions) + private static void ParseHealthProbeSidecarSettings(ProxyConfig backendOptions) { var healthSettings = backendOptions.HealthProbeSidecar.Split(';', StringSplitOptions.RemoveEmptyEntries); foreach (var setting in healthSettings) @@ -359,7 +359,7 @@ private static void ParseHealthProbeSidecarSettings(BackendOptions backendOption } } - private static void ValidatePrioritySettings(BackendOptions backendOptions, BackendOptions defaults) + private static void ValidatePrioritySettings(ProxyConfig backendOptions, ProxyConfig defaults) { if (backendOptions.PriorityKeys.Count != backendOptions.PriorityValues.Count) { @@ -378,7 +378,7 @@ private static void ValidatePrioritySettings(BackendOptions backendOptions, Back } } - private static void ValidateHeaderSettings(BackendOptions backendOptions) + private static void ValidateHeaderSettings(ProxyConfig backendOptions) { if (backendOptions.ValidateHeaders.Count > 0) { @@ -400,7 +400,7 @@ private static void ValidateHeaderSettings(BackendOptions backendOptions) } } - private static void ValidateLoadBalanceMode(BackendOptions backendOptions) + private static void ValidateLoadBalanceMode(ProxyConfig backendOptions) { backendOptions.LoadBalanceMode = backendOptions.LoadBalanceMode.Trim().ToLowerInvariant(); if (backendOptions.LoadBalanceMode != Constants.Latency && @@ -451,7 +451,7 @@ public static string ReadEnvironmentVariableOrDefault(Dictionary public static IterationModeEnum ReadEnvironmentVariableOrDefault(Dictionary env, string variableName, IterationModeEnum defaultValue) { string? envValue = env.GetValueOrDefault(variableName)?.Trim(); - if (string.IsNullOrEmpty(envValue) || envValue == ConfigOptions.DefaultPlaceholder || !Enum.TryParse(envValue, true, out IterationModeEnum value)) + if (string.IsNullOrEmpty(envValue) || envValue == ConfigMetadata.DefaultPlaceholder || !Enum.TryParse(envValue, true, out IterationModeEnum value)) { return defaultValue; } @@ -467,7 +467,7 @@ public static bool ReadEnvironmentVariableOrDefault(Dictionary e private static int ReadEnvironmentVariableOrDefaultCore(Dictionary env, string variableName, int defaultValue) { var envValue = env.GetValueOrDefault(variableName); - if (envValue?.Trim() == ConfigOptions.DefaultPlaceholder) envValue = null; + if (envValue?.Trim() == ConfigMetadata.DefaultPlaceholder) envValue = null; if (!int.TryParse(envValue, out var value)) { @@ -484,7 +484,7 @@ private static int ReadEnvironmentVariableOrDefaultCore(Dictionary env, string variableName, int[] defaultValues) { var envValue = env.GetValueOrDefault(variableName); - if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigOptions.DefaultPlaceholder) + if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigMetadata.DefaultPlaceholder) { return defaultValues; } @@ -508,7 +508,7 @@ private static int[] ReadEnvironmentVariableOrDefaultCore(Dictionary env, string variableName, float defaultValue) { var envValue = env.GetValueOrDefault(variableName); - if (envValue?.Trim() == ConfigOptions.DefaultPlaceholder) envValue = null; + if (envValue?.Trim() == ConfigMetadata.DefaultPlaceholder) envValue = null; if (!float.TryParse(envValue, out var value)) { @@ -525,7 +525,7 @@ private static float ReadEnvironmentVariableOrDefaultCore(Dictionary env, string variableName, string defaultValue) { var envValue = env.GetValueOrDefault(variableName); - if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigOptions.DefaultPlaceholder) + if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigMetadata.DefaultPlaceholder) { return defaultValue; } @@ -536,7 +536,7 @@ private static string ReadEnvironmentVariableOrDefaultCore(Dictionary env, string variableName, bool defaultValue) { var envValue = env.GetValueOrDefault(variableName); - if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigOptions.DefaultPlaceholder) + if (string.IsNullOrEmpty(envValue) || envValue.Trim() == ConfigMetadata.DefaultPlaceholder) { return defaultValue; } @@ -755,10 +755,10 @@ private static (string connectionString, string accountUri, bool useMI) ParseBlo } /// - /// Creates and assigns an on this + /// Creates and assigns an on this /// instance, configured from the transport-related properties (keep-alive, HTTP/2, SSL). /// - public static void ConfigureHttpClient(BackendOptions backendOptions) + public static void ConfigureHttpClient(ProxyConfig backendOptions) { var safeKeepAliveInitialDelaySecs = Math.Max(1, backendOptions.KeepAliveInitialDelaySecs); var safeKeepAlivePingIntervalSecs = Math.Max(1, backendOptions.KeepAlivePingIntervalSecs); diff --git a/src/SimpleL7Proxy/Config/DefaultCredential.cs b/src/SimpleL7Proxy/Config/DefaultCredential.cs index d1cfabd4..d571956e 100644 --- a/src/SimpleL7Proxy/Config/DefaultCredential.cs +++ b/src/SimpleL7Proxy/Config/DefaultCredential.cs @@ -3,7 +3,7 @@ namespace SimpleL7Proxy.Config; -public class DefaultCredential(BackendOptions options) +public class DefaultCredential(ProxyConfig options) { public DefaultAzureCredential Credential { get; } = new(options.UseOAuthGov == true diff --git a/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs b/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs index f364319e..5d0a9248 100644 --- a/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs +++ b/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs @@ -10,10 +10,10 @@ public interface IConfigChangeSubscriber /// Called when one or more warm configuration settings have changed. /// /// The list of settings that changed in this refresh cycle. - /// The current instance (already updated). + /// The current instance (already updated). /// Cancellation token tied to the host lifetime. Task OnConfigChangedAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken); } diff --git a/src/SimpleL7Proxy/Config/BackendOptions.cs b/src/SimpleL7Proxy/Config/ProxyConfig.cs similarity index 98% rename from src/SimpleL7Proxy/Config/BackendOptions.cs rename to src/SimpleL7Proxy/Config/ProxyConfig.cs index 21934fad..021e6419 100644 --- a/src/SimpleL7Proxy/Config/BackendOptions.cs +++ b/src/SimpleL7Proxy/Config/ProxyConfig.cs @@ -4,7 +4,7 @@ namespace SimpleL7Proxy.Config; -public class BackendOptions +public class ProxyConfig { // ════════════════════════════════════════════════════════════════════ // Warm — published to App Configuration, hot-reloaded (~30 s) @@ -304,9 +304,9 @@ public class BackendOptions /// collections (List, Dictionary, array) are cloned so the copy is fully independent. /// Note: (HttpClient) is shared, not cloned. /// - public BackendOptions DeepClone() + public ProxyConfig DeepClone() { - var clone = (BackendOptions)MemberwiseClone(); + var clone = (ProxyConfig)MemberwiseClone(); // Clone collection properties so mutations don't leak between instances. clone.AcceptableStatusCodes = (int[])AcceptableStatusCodes.Clone(); @@ -336,14 +336,14 @@ public BackendOptions DeepClone() /// Uses reflection to set the named property, falling back to the corresponding default value when the /// environment variable is absent or set to the default placeholder. /// - public void ApplyFieldFromEnv(Dictionary env, BackendOptions defaults, string envVar, string property) + public void ApplyFieldFromEnv(Dictionary env, ProxyConfig defaults, string envVar, string property) { - var pi = typeof(BackendOptions).GetProperty(property) ?? throw new InvalidOperationException($"Unknown BackendOptions property: {property}"); + var pi = typeof(ProxyConfig).GetProperty(property) ?? throw new InvalidOperationException($"Unknown BackendOptions property: {property}"); var defVal = pi.GetValue(defaults); var type = pi.PropertyType; var envValue = env.GetValueOrDefault(envVar)?.Trim(); - bool envVarPresent = !string.IsNullOrEmpty(envValue) && envValue != ConfigOptions.DefaultPlaceholder; + bool envVarPresent = !string.IsNullOrEmpty(envValue) && envValue != ConfigMetadata.DefaultPlaceholder; if (!envVarPresent) { var currentVal = pi.GetValue(this); diff --git a/src/SimpleL7Proxy/Events/BackupAPI/BackupStatService.cs b/src/SimpleL7Proxy/Events/BackupAPI/BackupStatService.cs index 73f798c1..4493afe0 100644 --- a/src/SimpleL7Proxy/Events/BackupAPI/BackupStatService.cs +++ b/src/SimpleL7Proxy/Events/BackupAPI/BackupStatService.cs @@ -19,7 +19,7 @@ public class BackupAPIService : IHostedService, IBackupAPIService, IShutdownPart { public int ShutdownOrder => 20; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly ILogger _logger; private static readonly ConcurrentQueue _statusQueue = new(); private readonly SemaphoreSlim _queueSignal = new SemaphoreSlim(0); @@ -42,7 +42,7 @@ public class BackupAPIService : IHostedService, IBackupAPIService, IShutdownPart private const int MaxDrainPerCycle = 50; // max messages to drain from queue per cycle private static readonly TimeSpan FlushIntervalMs = TimeSpan.FromMilliseconds(1000); // small delay to coalesce bursts (when not shutting down) - public BackupAPIService(IOptions options, ServiceBusFactory senderFactory,ILogger logger) + public BackupAPIService(IOptions options, ServiceBusFactory senderFactory,ILogger logger) { _options = options.Value; _senderFactory = senderFactory; diff --git a/src/SimpleL7Proxy/Events/CommonEventHeaders.cs b/src/SimpleL7Proxy/Events/CommonEventHeaders.cs index 99849708..f728424e 100644 --- a/src/SimpleL7Proxy/Events/CommonEventHeaders.cs +++ b/src/SimpleL7Proxy/Events/CommonEventHeaders.cs @@ -4,7 +4,7 @@ namespace SimpleL7Proxy.Events; -public class CommonEventHeaders(IOptions options) : ICommonEventData +public class CommonEventHeaders(IOptions options) : ICommonEventData { private readonly FrozenDictionary _defaultEventData = new Dictionary diff --git a/src/SimpleL7Proxy/Events/EventDataBuilder.cs b/src/SimpleL7Proxy/Events/EventDataBuilder.cs index bbdb0d40..851ce956 100644 --- a/src/SimpleL7Proxy/Events/EventDataBuilder.cs +++ b/src/SimpleL7Proxy/Events/EventDataBuilder.cs @@ -13,9 +13,9 @@ namespace SimpleL7Proxy.Events; public class EventDataBuilder { private readonly ILogger _logger; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; - public EventDataBuilder(ILogger logger, IOptions options) + public EventDataBuilder(ILogger logger, IOptions options) { _logger = logger; _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); diff --git a/src/SimpleL7Proxy/Events/EventHubClient.cs b/src/SimpleL7Proxy/Events/EventHubClient.cs index 6682fd7c..9e8847f5 100644 --- a/src/SimpleL7Proxy/Events/EventHubClient.cs +++ b/src/SimpleL7Proxy/Events/EventHubClient.cs @@ -38,7 +38,7 @@ public class EventHubClient : IEventClient, IHostedService, IDisposable //public EventHubClient(string connectionString, string eventHubName, ILogger? logger = null) public EventHubClient(CompositeEventClient composite, - IOptions options, + IOptions options, ILogger logger, DefaultCredential defaultCredential) { diff --git a/src/SimpleL7Proxy/Events/EventHubConfig.cs b/src/SimpleL7Proxy/Events/EventHubConfig.cs index d2ca1118..6b67b6c1 100644 --- a/src/SimpleL7Proxy/Events/EventHubConfig.cs +++ b/src/SimpleL7Proxy/Events/EventHubConfig.cs @@ -9,7 +9,7 @@ public class EventHubConfig { public int StartupSeconds { get; } = 10; public int MaxReconnectAttempts { get; } = 5; - public EventHubConfig(BackendOptions options) { + public EventHubConfig(ProxyConfig options) { ConnectionString = options.EventHubConnectionString; EventHubName = options.EventHubName; EventHubNamespace = options.EventHubNamespace; diff --git a/src/SimpleL7Proxy/Events/LogFileEventClient.cs b/src/SimpleL7Proxy/Events/LogFileEventClient.cs index b45181b6..60a845d6 100644 --- a/src/SimpleL7Proxy/Events/LogFileEventClient.cs +++ b/src/SimpleL7Proxy/Events/LogFileEventClient.cs @@ -32,7 +32,7 @@ public class LogFileEventClient : IEventClient, IHostedService private static Stream log = null!; private static StreamWriter writer = null!; - public LogFileEventClient(string filename, CompositeEventClient composite, IOptions options ) + public LogFileEventClient(string filename, CompositeEventClient composite, IOptions options ) { _composite = composite ?? throw new ArgumentNullException(nameof(composite)); // create file stream to a log file diff --git a/src/SimpleL7Proxy/Events/ProxyEvent.cs b/src/SimpleL7Proxy/Events/ProxyEvent.cs index 8629b01d..d0bb4bd8 100644 --- a/src/SimpleL7Proxy/Events/ProxyEvent.cs +++ b/src/SimpleL7Proxy/Events/ProxyEvent.cs @@ -16,7 +16,7 @@ namespace SimpleL7Proxy.Events public class ProxyEvent : ConcurrentDictionary, IConfigChangeSubscriber { - private static IOptions _options = null!; + private static IOptions _options = null!; private static IEventClient? _eventClient; private static TelemetryClient? _telemetryClient; @@ -44,7 +44,7 @@ public class ProxyEvent : ConcurrentDictionary, IConfigChangeSub public static FrozenDictionary DefaultParams { get; private set; } = FrozenDictionary.Empty; public static void Initialize( - IOptions backendOptions, + IOptions backendOptions, IEventClient? eventClient = null, ICommonEventData? commonEventData = null, TelemetryClient? telemetryClient = null) @@ -75,7 +75,7 @@ public static void SubscribeToConfigChanges(ConfigChangeNotifier notifier) /// public Task OnConfigChangedAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken) { UpdateLogTargets(backendOptions); @@ -83,12 +83,12 @@ public Task OnConfigChangedAsync( } /// - /// Parses , , - /// and into , , + /// Parses , , + /// and into , , /// . A list containing "*" enables all event types for that destination. /// Safe to call on config hot-reload. /// - public static void UpdateLogTargets(BackendOptions options) + public static void UpdateLogTargets(ProxyConfig options) { ConAttr = LogTargetAttr.From(options.LogToConsole); EventAttr = LogTargetAttr.From(options.LogToEvents); diff --git a/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusFactory.cs b/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusFactory.cs index fef5ada7..540068b9 100644 --- a/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusFactory.cs +++ b/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusFactory.cs @@ -13,7 +13,7 @@ namespace SimpleL7Proxy.ServiceBus public class ServiceBusFactory { private readonly DefaultCredential _defaultCredential; - private readonly IOptionsMonitor _optionsMonitor; + private readonly IOptionsMonitor _optionsMonitor; private readonly ILogger _logger; private readonly ServiceBusClient _client = null!; @@ -29,7 +29,7 @@ public class ServiceBusFactory /// The factory initializes the ServiceBusClient only when AsyncModeEnabled is true in the backend options. /// This prevents unnecessary connection creation when async processing is not required. /// - public ServiceBusFactory(IOptionsMonitor optionsMonitor, ILogger logger, DefaultCredential defaultCredential) + public ServiceBusFactory(IOptionsMonitor optionsMonitor, ILogger logger, DefaultCredential defaultCredential) { _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); diff --git a/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusRequestService.cs b/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusRequestService.cs index 82edd6b9..f39c43b2 100644 --- a/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusRequestService.cs +++ b/src/SimpleL7Proxy/Events/ServiceBus/ServiceBusRequestService.cs @@ -17,7 +17,7 @@ namespace SimpleL7Proxy.ServiceBus public class ServiceBusRequestService : IHostedService, IServiceBusRequestService { - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly ServiceBusFactory _senderFactory; private readonly ILogger _logger; public static readonly ConcurrentQueue _statusQueue = new ConcurrentQueue(); @@ -35,7 +35,7 @@ public class ServiceBusRequestService : IHostedService, IServiceBusRequestServic private int _totalMessagesProcessed = 0; private int _totalBatchesSent = 0; - public ServiceBusRequestService(IOptions options, ServiceBusFactory senderFactory, ILogger logger) + public ServiceBusRequestService(IOptions options, ServiceBusFactory senderFactory, ILogger logger) { _options = options.Value; _senderFactory = senderFactory ?? throw new ArgumentNullException(nameof(senderFactory)); diff --git a/src/SimpleL7Proxy/Feeder/AsyncFeeder.cs b/src/SimpleL7Proxy/Feeder/AsyncFeeder.cs index 99a9ff16..e9e51bb4 100644 --- a/src/SimpleL7Proxy/Feeder/AsyncFeeder.cs +++ b/src/SimpleL7Proxy/Feeder/AsyncFeeder.cs @@ -34,7 +34,7 @@ public Task StopAsync(CancellationToken cancellationToken) public class AsyncFeeder : IHostedService, IAsyncFeeder { - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly ILogger _logger; private readonly IUserProfileService _userProfile; private readonly IRequestDataBackupService _requestBackupService; @@ -77,7 +77,7 @@ public class AsyncFeeder : IHostedService, IAsyncFeeder // Batch tuning private static readonly TimeSpan FlushIntervalMs = TimeSpan.FromMilliseconds(1000); // small delay to coalesce bursts (when not shutting down) - public AsyncFeeder(IOptions options, + public AsyncFeeder(IOptions options, IUserPriorityService userPriority, IUserProfileService userProfile, IRequestDataBackupService requestBackupService, diff --git a/src/SimpleL7Proxy/Feeder/NormalRequest.cs b/src/SimpleL7Proxy/Feeder/NormalRequest.cs index 7213050e..12296cbc 100644 --- a/src/SimpleL7Proxy/Feeder/NormalRequest.cs +++ b/src/SimpleL7Proxy/Feeder/NormalRequest.cs @@ -24,13 +24,13 @@ namespace SimpleL7Proxy.Feeder { public class NormalRequest : IRequestProcessor { - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly ILogger _logger; private readonly IRequestDataBackupService _backupService; private readonly IAsyncWorkerFactory _asyncWorkerFactory; - public NormalRequest(IOptions options, + public NormalRequest(IOptions options, IRequestDataBackupService backupService, IAsyncWorkerFactory asyncWorkerFactory, ILogger logger) diff --git a/src/SimpleL7Proxy/Feeder/OpenAIBackgroundRequest.cs b/src/SimpleL7Proxy/Feeder/OpenAIBackgroundRequest.cs index 8535cc54..5dbc0d9b 100644 --- a/src/SimpleL7Proxy/Feeder/OpenAIBackgroundRequest.cs +++ b/src/SimpleL7Proxy/Feeder/OpenAIBackgroundRequest.cs @@ -24,13 +24,13 @@ namespace SimpleL7Proxy.Feeder public class OpenAIBackgroundRequest : IRequestProcessor { - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly ILogger _logger; private readonly IRequestDataBackupService _backupService; private readonly IAsyncWorkerFactory _asyncWorkerFactory; - public OpenAIBackgroundRequest(IOptions options, + public OpenAIBackgroundRequest(IOptions options, IRequestDataBackupService backupService, IAsyncWorkerFactory asyncWorkerFactory, ILogger logger) diff --git a/src/SimpleL7Proxy/ProbeServer.cs b/src/SimpleL7Proxy/ProbeServer.cs index 8b5bbf59..2100b10d 100644 --- a/src/SimpleL7Proxy/ProbeServer.cs +++ b/src/SimpleL7Proxy/ProbeServer.cs @@ -39,7 +39,7 @@ public class ProbeServer : BackgroundService, IConfigChangeSubscriber // Active snapshots published to readers (use Volatile.Read/Write for memory ordering) private Timer? _probeTimer; - private readonly BackendOptions _backendOptions; + private readonly ProxyConfig _backendOptions; private HttpClient? _selfCheckClient; private IEventClient? _eventClient; @@ -57,7 +57,7 @@ public ProbeServer( IBackendService backends, HealthCheckService healthService, ILogger logger, - IOptions backendOptions, + IOptions backendOptions, ConfigChangeNotifier configChangeNotifier, IEventClient eventClient) { @@ -277,7 +277,7 @@ public override Task StopAsync(CancellationToken cancellationToken) /// public Task OnConfigChangedAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken) { _logger.LogInformation("[CONFIG] HealthProbeSidecar changed — restarting probe server"); diff --git a/src/SimpleL7Proxy/Program.cs b/src/SimpleL7Proxy/Program.cs index 395f7ba5..b00f034b 100644 --- a/src/SimpleL7Proxy/Program.cs +++ b/src/SimpleL7Proxy/Program.cs @@ -55,7 +55,7 @@ public static async Task Main(string[] args) // Bootstrap the bootstrapper !!!! // We can't even connect to App Config unless we know this - BackendOptions defaultBackendOptions = new BackendOptions + ProxyConfig defaultBackendOptions = new ProxyConfig { UseOAuthGov = string.Equals( Environment.GetEnvironmentVariable("UseOAuthGov"), "true", StringComparison.OrdinalIgnoreCase), @@ -66,7 +66,7 @@ public static async Task Main(string[] args) }; DefaultCredential defaultCredential = new DefaultCredential(defaultBackendOptions); - var appConfigBootstrap = new AppConfigBootstrap(startupLoggerFactory.CreateLogger(), defaultBackendOptions, defaultCredential); + var appConfigBootstrap = new AppConfigService(startupLoggerFactory.CreateLogger(), defaultBackendOptions, defaultCredential); // Fire off the download — CreateBackendOptions will await completion before reading Settings. appConfigBootstrap.Start(); @@ -78,7 +78,6 @@ public static async Task Main(string[] args) }) .ConfigureServices((hostContext, services) => { - ConfigureAppInsights(services); ConfigureDI(services, startupLogger, appConfigBootstrap, defaultCredential); }); @@ -87,10 +86,10 @@ public static async Task Main(string[] args) // Perform static initialization after building the host to ensure correct singleton usage var serviceProvider = frameworkHost.Services; + var options = serviceProvider.GetRequiredService>(); appConfigBootstrap.Notifier = serviceProvider.GetRequiredService(); appConfigBootstrap.HostCollection = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>(); var eventClient = serviceProvider.GetService(); var telemetryClient = serviceProvider.GetService(); var backendTokenProvider = serviceProvider.GetRequiredService(); @@ -113,7 +112,7 @@ public static async Task Main(string[] args) // Register backends after DI container is built and HostConfig is initialized var hostCollection = serviceProvider.GetRequiredService(); - BackendOptionsBuilder.RegisterBackends(options.Value, null, appConfigBootstrap.WarmSettings, hostCollection); + ConfigFactory.RegisterBackends(options.Value, null, appConfigBootstrap.WarmSettings, hostCollection); // Async try @@ -155,7 +154,7 @@ public static async Task Main(string[] args) appLifetime.ApplicationStarted.Register(() => { var composite = serviceProvider.GetRequiredService(); - BackendOptionsBuilder.OutputEnvVars(options.Value); + ConfigFactory.OutputEnvVars(options.Value); startupLogger.LogInformation("[INIT] ✓ All hosted services started — active event loggers: {Loggers}", composite.ClientType); @@ -192,9 +191,9 @@ private static void ConfigureLogging(ILoggingBuilder logging) logging.SetMinimumLevel(logLevel); } - private static void ConfigureAppInsights(IServiceCollection services) + private static void ConfigureAppInsights(IServiceCollection services, ProxyConfig options, ILogger startupLogger) { - var aiConnectionString = Environment.GetEnvironmentVariable("APPINSIGHTS_CONNECTIONSTRING") ?? ""; + var aiConnectionString = options.AppInsightsConnectionString; if (!string.IsNullOrEmpty(aiConnectionString)) { // Register Application Insights @@ -212,18 +211,24 @@ private static void ConfigureAppInsights(IServiceCollection services) }); // Note: logging isn't fully configured yet - Console.WriteLine("[INIT] ✓ AppInsights initialized with custom request tracking"); + startupLogger.LogInformation("[INIT] ✓ AppInsights initialized with custom request tracking"); } } - private static void ConfigureDI(IServiceCollection services, ILogger startupLogger, AppConfigBootstrap appConfigBootstrap, DefaultCredential defaultCredential) + private static void ConfigureDI(IServiceCollection services, ILogger startupLogger, AppConfigService appConfigBootstrap, DefaultCredential defaultCredential) { services.AddSingleton(appConfigBootstrap); services.AddSingleton(defaultCredential); TryAddCompositeEventClient(services); // register the backend options - var backendOptions = BackendOptionsBuilder.CreateOptions(appConfigBootstrap).GetAwaiter().GetResult(); + var result = ConfigFactory.CreateOptions(appConfigBootstrap).GetAwaiter().GetResult(); + + // a copy of the defaults + AppConfigService.DEFAULT_OPTIONS = result.baseOptions; + var backendOptions = result.envOptions; + + ConfigureAppInsights(services, backendOptions, startupLogger); services.RegisterBackendOptions(startupLogger, backendOptions); // Register event headers and event loggers .. needed for AWS @@ -231,7 +236,7 @@ private static void ConfigureDI(IServiceCollection services, ILogger startupLogg RegisterEventLoggers(services, startupLogger, backendOptions, backendOptions.EventLoggers); // Register refresh services only if App Configuration was reachable. - appConfigBootstrap.RegisterServices(services); + appConfigBootstrap.RegisterServices(services, backendOptions); services.AddSingleton(); @@ -355,7 +360,7 @@ private static void ConfigureDI(IServiceCollection services, ILogger startupLogg // When enabled, requests to the same path share the same iterator for fair distribution services.AddSingleton(sp => { - var options = sp.GetRequiredService>().Value; + var options = sp.GetRequiredService>().Value; if (!options.UseSharedIterators) { // Return null - WorkerFactory handles null gracefully @@ -379,7 +384,7 @@ private static void ConfigureDI(IServiceCollection services, ILogger startupLogg services.AddHostedService(); } - private static void RegisterEventHeaders(IServiceCollection services, ILogger startupLogger, BackendOptions backendOptions) + private static void RegisterEventHeaders(IServiceCollection services, ILogger startupLogger, ProxyConfig backendOptions) { var registered = false; var eventdataclass = backendOptions.EventHeaders; @@ -411,7 +416,7 @@ private static void RegisterEventHeaders(IServiceCollection services, ILogger st } } - private static void RegisterEventLoggers(IServiceCollection services, ILogger startupLogger, BackendOptions backendOptions, string? eventLoggersRaw) + private static void RegisterEventLoggers(IServiceCollection services, ILogger startupLogger, ProxyConfig backendOptions, string? eventLoggersRaw) { HashSet enabledLoggers; if (!string.IsNullOrWhiteSpace(eventLoggersRaw)) @@ -435,7 +440,7 @@ private static void RegisterEventLoggers(IServiceCollection services, ILogger st if (loggername == "file") { services.AddSingleton(svc => - new LogFileEventClient(backendOptions.LogFileName, svc.GetRequiredService(), svc.GetRequiredService>())); + new LogFileEventClient(backendOptions.LogFileName, svc.GetRequiredService(), svc.GetRequiredService>())); services.AddSingleton(svc => (IHostedService)svc.GetRequiredService()); } else if (loggername == "eventhub") diff --git a/src/SimpleL7Proxy/Proxy/AsyncWorker.cs b/src/SimpleL7Proxy/Proxy/AsyncWorker.cs index e7e90a16..3a33cce8 100644 --- a/src/SimpleL7Proxy/Proxy/AsyncWorker.cs +++ b/src/SimpleL7Proxy/Proxy/AsyncWorker.cs @@ -40,7 +40,7 @@ public class AsyncWorker : IAsyncDisposable private readonly IBlobWriter _blobWriter; private readonly ILogger _logger; private readonly IRequestDataBackupService _requestBackupService; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; // private readonly IBackupAPIService _backupAPIService; public bool ShouldReprocess { get; set; } = false; public string ErrorMessage { get; set; } = ""; @@ -70,7 +70,7 @@ public class AsyncWorker : IAsyncDisposable public AsyncWorker(RequestData data, int AsyncTriggerTimeout, IBlobWriter blobWriter, ILogger logger, - IRequestDataBackupService requestBackupService, BackendOptions backendOptions) + IRequestDataBackupService requestBackupService, ProxyConfig backendOptions) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _requestData = data ?? throw new ArgumentNullException(nameof(data)); diff --git a/src/SimpleL7Proxy/Proxy/AsyncWorkerFactory.cs b/src/SimpleL7Proxy/Proxy/AsyncWorkerFactory.cs index ffaf209e..a08312e6 100644 --- a/src/SimpleL7Proxy/Proxy/AsyncWorkerFactory.cs +++ b/src/SimpleL7Proxy/Proxy/AsyncWorkerFactory.cs @@ -15,14 +15,14 @@ public class AsyncWorkerFactory : IAsyncWorkerFactory private readonly IRequestDataBackupService _requestBackupService; private readonly IBackupAPIService _backupAPIService; - private readonly BackendOptions _backendOptions; + private readonly ProxyConfig _backendOptions; private readonly SemaphoreSlim _initLock = new(1, 1); private bool _initialized; public AsyncWorkerFactory(IBlobWriter blobWriter, ILogger logger, IRequestDataBackupService requestBackupService, - IOptions backendOptions, + IOptions backendOptions, IBackupAPIService backupAPIService) { _blobWriter = blobWriter; diff --git a/src/SimpleL7Proxy/Proxy/HealthCheckService.cs b/src/SimpleL7Proxy/Proxy/HealthCheckService.cs index 8286317e..28422cc4 100644 --- a/src/SimpleL7Proxy/Proxy/HealthCheckService.cs +++ b/src/SimpleL7Proxy/Proxy/HealthCheckService.cs @@ -24,7 +24,7 @@ namespace SimpleL7Proxy.Proxy; public class HealthCheckService { private readonly IBackendService _backends; - private static BackendOptions _options=null!; + private static ProxyConfig _options=null!; private readonly IConcurrentPriQueue? _requestsQueue; private readonly IUserPriorityService? _userPriority; private readonly IEventClient? _eventClient; @@ -59,7 +59,7 @@ public class HealthCheckService public HealthCheckService( IBackendService backends, - IOptions options, + IOptions options, IConcurrentPriQueue? requestsQueue, IUserPriorityService? userPriority, IEventClient? eventClient, diff --git a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs index ab7a88ea..1dd09978 100644 --- a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs +++ b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs @@ -36,7 +36,7 @@ public class ProxyWorker : IConfigChangeSubscriber private static bool s_debug = false; // dev time debug flag private static IConcurrentPriQueue? s_requestsQueue; private readonly IBackendService _backends; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly ILogger _logger; private readonly RequestLifecycleManager _lifecycleManager; private readonly EventDataBuilder _eventDataBuilder; @@ -103,7 +103,7 @@ public ProxyWorker( public Task OnConfigChangedAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken) { s_backendKeys = backendOptions.DependancyHeaders; diff --git a/src/SimpleL7Proxy/Proxy/RequestLifecycleManager.cs b/src/SimpleL7Proxy/Proxy/RequestLifecycleManager.cs index 51e6a012..3105f5c9 100644 --- a/src/SimpleL7Proxy/Proxy/RequestLifecycleManager.cs +++ b/src/SimpleL7Proxy/Proxy/RequestLifecycleManager.cs @@ -16,9 +16,9 @@ namespace SimpleL7Proxy.Proxy; public class RequestLifecycleManager { private readonly ILogger _logger; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; - public RequestLifecycleManager(ILogger logger, IOptions options) + public RequestLifecycleManager(ILogger logger, IOptions options) { _logger = logger; _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); diff --git a/src/SimpleL7Proxy/Proxy/WorkerContext.cs b/src/SimpleL7Proxy/Proxy/WorkerContext.cs index 320fb81d..43ef5348 100644 --- a/src/SimpleL7Proxy/Proxy/WorkerContext.cs +++ b/src/SimpleL7Proxy/Proxy/WorkerContext.cs @@ -13,7 +13,7 @@ namespace SimpleL7Proxy.Proxy; public class WorkerContext { //public ProxyStreamWriter ProxyStreamWriter { get; } - public BackendOptions BackendOptions { get; } + public ProxyConfig BackendOptions { get; } public ConfigChangeNotifier ConfigChangeNotifier { get; } public EventDataBuilder EventDataBuilder { get; } public HealthCheckService HealthCheckService { get; } @@ -30,7 +30,7 @@ public class WorkerContext public StreamProcessorFactory StreamProcessorFactory { get; } public WorkerContext( - BackendOptions backendOptions, + ProxyConfig backendOptions, IConcurrentPriQueue queue, IBackendService backends, IUserPriorityService userPriorityService, diff --git a/src/SimpleL7Proxy/Proxy/WorkerFactory.cs b/src/SimpleL7Proxy/Proxy/WorkerFactory.cs index 8c512c76..c89269ae 100644 --- a/src/SimpleL7Proxy/Proxy/WorkerFactory.cs +++ b/src/SimpleL7Proxy/Proxy/WorkerFactory.cs @@ -11,7 +11,7 @@ namespace SimpleL7Proxy.Proxy; public class WorkerFactory : BackgroundService { - private readonly BackendOptions _backendOptions; + private readonly ProxyConfig _backendOptions; private readonly WorkerContext _context; private readonly ILogger _logger; //private readonly ProxyStreamWriter _proxyStreamWriter; diff --git a/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs b/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs index bd541528..8de00f53 100644 --- a/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs +++ b/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs @@ -14,10 +14,10 @@ public class ConcurrentPriQueue : IConcurrentPriQueue //private int insertions = 0; //private int extractions = 0; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; - public ConcurrentPriQueue(IOptions backendOptions, ILogger> logger) + public ConcurrentPriQueue(IOptions backendOptions, ILogger> logger) { ArgumentNullException.ThrowIfNull(backendOptions); _options = backendOptions.Value; diff --git a/src/SimpleL7Proxy/RequestData.cs b/src/SimpleL7Proxy/RequestData.cs index c1ee9fa9..4fcb7a3b 100644 --- a/src/SimpleL7Proxy/RequestData.cs +++ b/src/SimpleL7Proxy/RequestData.cs @@ -23,7 +23,7 @@ public class RequestData : IDisposable, IAsyncDisposable public static IServiceBusRequestService? SBRequestService { get; private set; } public static IBackupAPIService? BackupAPIService { get; private set; } public static IUserPriorityService? UserPriorityService { get; private set; } - public static BackendOptions? BackendOptionsStatic { get; private set; } + public static ProxyConfig? BackendOptionsStatic { get; private set; } // -- ASYNC RELATED PARAMS -- @@ -166,7 +166,7 @@ public ServiceBusMessageStatusEnum SBStatus public static void InitializeServiceBusRequestService(IServiceBusRequestService serviceBusRequestService, IBackupAPIService backupAPIService, IUserPriorityService userPriorityService, - BackendOptions backendOptions) + ProxyConfig backendOptions) { SBRequestService ??= serviceBusRequestService; BackupAPIService ??= backupAPIService; diff --git a/src/SimpleL7Proxy/User/UserPriority.cs b/src/SimpleL7Proxy/User/UserPriority.cs index bc235ed1..47f134c7 100644 --- a/src/SimpleL7Proxy/User/UserPriority.cs +++ b/src/SimpleL7Proxy/User/UserPriority.cs @@ -12,13 +12,13 @@ public class UserPriority : IUserPriorityService { private readonly ConcurrentDictionary> userRequests = new ConcurrentDictionary>(); - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly ILogger _logger; private int total = 0; public float threshold { get; set; } - public UserPriority(IOptions options, ILogger logger) + public UserPriority(IOptions options, ILogger logger) { _options = options.Value; _logger = logger; diff --git a/src/SimpleL7Proxy/User/UserProfile.cs b/src/SimpleL7Proxy/User/UserProfile.cs index cb7b9ad5..9fc296e7 100644 --- a/src/SimpleL7Proxy/User/UserProfile.cs +++ b/src/SimpleL7Proxy/User/UserProfile.cs @@ -16,7 +16,7 @@ namespace SimpleL7Proxy.User; public class UserProfile : BackgroundService, IUserProfileService, IConfigChangeSubscriber { - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private volatile Dictionary> userProfiles = new Dictionary>(); private volatile List suspendedUserProfiles = new List(); @@ -54,7 +54,7 @@ public class UserProfile : BackgroundService, IUserProfileService, IConfigChange private const int s_ErrorDelayMs = 3000; // 3 seconds private bool _configRequired = false; - public UserProfile(BackendOptions options, ConfigChangeNotifier configChangeNotifier, ILogger logger) + public UserProfile(ProxyConfig options, ConfigChangeNotifier configChangeNotifier, ILogger logger) { ArgumentNullException.ThrowIfNull(options, nameof(options)); ArgumentNullException.ThrowIfNull(logger, nameof(logger)); @@ -94,7 +94,7 @@ public void initVars() public Task OnConfigChangedAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken) { _logger.LogInformation("Received config change notification for UserProfile service. Changes: {Changes}", diff --git a/src/SimpleL7Proxy/server.cs b/src/SimpleL7Proxy/server.cs index 7c2e4431..0559acf2 100644 --- a/src/SimpleL7Proxy/server.cs +++ b/src/SimpleL7Proxy/server.cs @@ -28,7 +28,7 @@ namespace SimpleL7Proxy; public class Server : BackgroundService, IConfigChangeSubscriber { // private readonly IBackendOptions? _options; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly HttpListener _httpListener; private readonly IBackendService _backends; @@ -61,7 +61,7 @@ public class Server : BackgroundService, IConfigChangeSubscriber // Constructor to initialize the server with backend options and telemetry client. public Server( IConcurrentPriQueue requestsQueue, - IOptions backendOptions, + IOptions backendOptions, IHostApplicationLifetime appLifetime, IUserPriorityService userPriority, IUserProfileService userProfile, @@ -156,7 +156,7 @@ public Server( public Task OnConfigChangedAsync( IReadOnlyList changes, - BackendOptions backendOptions, + ProxyConfig backendOptions, CancellationToken cancellationToken) { _logger.LogInformation("[CONFIG] Server changed — Settings live updated without restart"); From 63f1197e7ce1eecd5981c2e3fe7445a754228515 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Mon, 30 Mar 2026 13:57:19 -0400 Subject: [PATCH 03/18] ren: BackenOptions,BackendOptionsBuilder,ConfigOptions,AppConfigBootstrap => ProxyConfig,ConfigFactory,ConfigMetadata,AppConfigService --- deployment/AppConfiguration/deploy.sh | 12 ++++++------ src/SimpleL7Proxy/CoordinatedShutdownService.cs | 4 ++-- test/ProxyWorkerTests/Helpers/TestHostFactory.cs | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/deployment/AppConfiguration/deploy.sh b/deployment/AppConfiguration/deploy.sh index c1a30ba1..9ba9ed8b 100644 --- a/deployment/AppConfiguration/deploy.sh +++ b/deployment/AppConfiguration/deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Deploy/Update Azure App Configuration for BackendOptions +# Deploy/Update Azure App Configuration for ProxyConfig # # Goals: # 1. Migration – seed App Configuration from a live Container App's @@ -8,12 +8,12 @@ # 2. Catalog – every publishable setting is always written so that # operators can see the full list in the portal. # When no env value exists, the C# default from -# BackendOptions.cs is used. If even that is empty, +# ProxyConfig.cs is used. If even that is empty, # a "-" placeholder is written, meaning "use the # built-in code default". # # Discovers publishable keys dynamically from [ConfigOption("...")] -# decorations in BackendOptions.cs. +# decorations in ProxyConfig.cs. # # Three modes (ConfigMode enum): # Warm – published under "Warm:" prefix, hot-reloaded (~30 s) @@ -60,7 +60,7 @@ APPCONFIG_LABEL="${APPCONFIG_LABEL:-}" AZURE_APPCONFIG_REFRESH_SECONDS="${AZURE_APPCONFIG_REFRESH_SECONDS:-30}" UPDATE_CONTAINER_APP_ENV="${UPDATE_CONTAINER_APP_ENV:-true}" -BACKEND_OPTIONS_FILE="${BACKEND_OPTIONS_FILE:-${REPO_ROOT}/src/SimpleL7Proxy/Config/BackendOptions.cs}" +BACKEND_OPTIONS_FILE="${BACKEND_OPTIONS_FILE:-${REPO_ROOT}/src/SimpleL7Proxy/Config/ProxyConfig.cs}" GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -76,7 +76,7 @@ if ! command -v az >/dev/null 2>&1; then fi if [ ! -f "${BACKEND_OPTIONS_FILE}" ]; then - echo -e "${RED}Error: BackendOptions file not found: ${BACKEND_OPTIONS_FILE}${NC}" + echo -e "${RED}Error: ProxyConfig file not found: ${BACKEND_OPTIONS_FILE}${NC}" exit 1 fi @@ -310,7 +310,7 @@ for entry in "${CONFIG_ENTRIES[@]}"; do [ -n "${VALUE}" ] && SOURCE="local-env" fi - # 3) Fallback to C# default from BackendOptions.cs + # 3) Fallback to C# default from ProxyConfig.cs if [ -z "${VALUE}" ] && [ -n "${CS_DEFAULT}" ]; then VALUE="${CS_DEFAULT}" SOURCE="cs-default" diff --git a/src/SimpleL7Proxy/CoordinatedShutdownService.cs b/src/SimpleL7Proxy/CoordinatedShutdownService.cs index c2b6ce89..73dee7de 100644 --- a/src/SimpleL7Proxy/CoordinatedShutdownService.cs +++ b/src/SimpleL7Proxy/CoordinatedShutdownService.cs @@ -22,7 +22,7 @@ public class CoordinatedShutdownService : IHostedService private readonly ILogger _logger; private readonly Server _server; private readonly BackendTokenProvider _backendTokenProvider; - private readonly BackendOptions _options; + private readonly ProxyConfig _options; private readonly IEventClient? _eventClient; private readonly IServiceBusRequestService _serviceBusRequestService; private readonly IBackupAPIService _backupAPIService; @@ -38,7 +38,7 @@ public class CoordinatedShutdownService : IHostedService public CoordinatedShutdownService(IHostApplicationLifetime appLifetime, - IOptions backendOptions, + IOptions backendOptions, IConcurrentPriQueue queue, BackendTokenProvider backendTokenProvider, IBackendService backends, diff --git a/test/ProxyWorkerTests/Helpers/TestHostFactory.cs b/test/ProxyWorkerTests/Helpers/TestHostFactory.cs index 190791af..54b95bf7 100644 --- a/test/ProxyWorkerTests/Helpers/TestHostFactory.cs +++ b/test/ProxyWorkerTests/Helpers/TestHostFactory.cs @@ -31,7 +31,7 @@ public static void EnsureInitialized() var services = new ServiceCollection(); services.AddLogging(b => b.AddProvider(NullLoggerProvider.Instance)); - services.Configure(opts => + services.Configure(opts => { opts.CircuitBreakerErrorThreshold = 100; // high threshold so CB never trips in tests opts.CircuitBreakerTimeslice = 60; @@ -61,6 +61,7 @@ public static NonProbeableHostHealth CreateHost(string hostname) // Use direct-mode by default so no probe URL is needed var configStr = hostname.Contains(';') ? hostname : $"host={hostname};mode=direct"; var config = new HostConfig(configStr); + config.Activate(); return new NonProbeableHostHealth(config, logger); } From e6f7492ff6c764c416027afa13d312c57e1fc33d Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Mon, 30 Mar 2026 15:13:19 -0400 Subject: [PATCH 04/18] fix probe logging --- src/SimpleL7Proxy/Config/ProxyConfig.cs | 4 +- src/SimpleL7Proxy/Events/ProxyEvent.cs | 53 +++++++++++++------------ src/SimpleL7Proxy/ProbeServer.cs | 19 +++++++-- src/SimpleL7Proxy/server.cs | 46 ++++++++++++--------- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/src/SimpleL7Proxy/Config/ProxyConfig.cs b/src/SimpleL7Proxy/Config/ProxyConfig.cs index 021e6419..f71cdcbf 100644 --- a/src/SimpleL7Proxy/Config/ProxyConfig.cs +++ b/src/SimpleL7Proxy/Config/ProxyConfig.cs @@ -49,8 +49,6 @@ public class ProxyConfig public List LogToEvents { get; set; } = ["async","backend","circuitbreaker","custom","exception","profile","proxy","enqueued","auth"]; [ConfigOption("Logging:LogToAI")] public List LogToAI { get; set; } = ["*"]; - - public bool LogProbes { get; set; } = true; [ConfigOption("Logging:LogHeaders")] public List LogHeaders { get; set; } = []; [ConfigOption("Logging:LogAllRequestHeaders")] @@ -343,7 +341,7 @@ public void ApplyFieldFromEnv(Dictionary env, ProxyConfig defaul var type = pi.PropertyType; var envValue = env.GetValueOrDefault(envVar)?.Trim(); - bool envVarPresent = !string.IsNullOrEmpty(envValue) && envValue != ConfigMetadata.DefaultPlaceholder; + bool envVarPresent = !string.IsNullOrEmpty(envValue) && envValue != ConfigOptions.DefaultPlaceholder; if (!envVarPresent) { var currentVal = pi.GetValue(this); diff --git a/src/SimpleL7Proxy/Events/ProxyEvent.cs b/src/SimpleL7Proxy/Events/ProxyEvent.cs index d0bb4bd8..789ae63c 100644 --- a/src/SimpleL7Proxy/Events/ProxyEvent.cs +++ b/src/SimpleL7Proxy/Events/ProxyEvent.cs @@ -160,6 +160,7 @@ public void SendEvent() case EventType.ProxyRequest: case EventType.ProxyRequestExpired: case EventType.ProxyRequestRequeued: + case EventType.Probe: TrackRequest(); break; @@ -186,32 +187,32 @@ public void SendEvent() public static string ConvertToJson(ProxyEvent proxyEvent, IDictionary? extraProperties = null) { - // Use Utf8JsonWriter to merge proxyEvent + extraProperties into one JSON object - // without allocating an intermediate merged dictionary - var buffer = new ArrayBufferWriter(512); - using (var writer = new Utf8JsonWriter(buffer)) + // Use Utf8JsonWriter to merge proxyEvent + extraProperties into one JSON object + // without allocating an intermediate merged dictionary + var buffer = new ArrayBufferWriter(512); + using (var writer = new Utf8JsonWriter(buffer)) + { + writer.WriteStartObject(); + + foreach (var kvp in proxyEvent) { - writer.WriteStartObject(); - - foreach (var kvp in proxyEvent) - { - writer.WriteString(kvp.Key, kvp.Value); - } - - if (extraProperties is not null) - { - foreach (var kvp in extraProperties) - { - writer.WriteString(kvp.Key, kvp.Value); - } - } - - writer.WriteEndObject(); + writer.WriteString(kvp.Key, kvp.Value); } - return Encoding.UTF8.GetString(buffer.WrittenSpan); + if (extraProperties is not null) + { + foreach (var kvp in extraProperties) + { + writer.WriteString(kvp.Key, kvp.Value); + } + } + + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(buffer.WrittenSpan); } - + private void TrackEvent() { @@ -294,10 +295,9 @@ private void TrackRequest() Url = Uri, ResponseCode = Status.ToString(), Success = success, - Id = requestId // Set a consistent ID to help identify duplicates + Id = requestId, // Set a consistent ID to help identify duplicates + Timestamp = DateTimeOffset.UtcNow.Subtract(Duration) }; - - requestTelemetry.Timestamp = DateTimeOffset.UtcNow.Subtract(Duration); requestTelemetry.Properties["HttpMethod"] = Method ?? "GET"; requestTelemetry.Source = "S7P"; // Custom source identifier requestTelemetry.Duration = Duration; @@ -312,6 +312,7 @@ private void TrackRequest() { requestTelemetry.Properties.Add(kvp); } + Console.WriteLine("Logging request"); _telemetryClient?.TrackRequest(requestTelemetry); } @@ -373,7 +374,7 @@ public void WriteOutput(string data = "") Type = EventType.Console; // Only send to event client if this event type is enabled for it - if ( EventAttr.Console ) + if (EventAttr.Console) { _eventClient?.SendData(ConvertToJson(this)); } diff --git a/src/SimpleL7Proxy/ProbeServer.cs b/src/SimpleL7Proxy/ProbeServer.cs index 2100b10d..4a4fee8b 100644 --- a/src/SimpleL7Proxy/ProbeServer.cs +++ b/src/SimpleL7Proxy/ProbeServer.cs @@ -156,7 +156,7 @@ private async Task PushStatusToSidecarAsync(HttpClient selfCheckClient) public int EventCount => _activeUndrainedEvents; // TODO: no need for stopwatch any longer - public async Task LivenessResponseAsync(HttpListenerContext lc) + public async Task LivenessResponseAsync(HttpListenerContext lc) { // Liveness probe check - use pre-allocated objects try @@ -186,10 +186,12 @@ public async Task LivenessResponseAsync(HttpListenerContext lc) catch { } } + return HttpStatusCode.OK; } - public async Task ReadinessResponseAsync(HttpListenerContext lc) + public async Task ReadinessResponseAsync(HttpListenerContext lc) { + HttpStatusCode statusCode = HttpStatusCode.OK; // default to 200, may be overridden in switch try { lc.Response.ContentType = "text/plain"; @@ -202,16 +204,20 @@ public async Task ReadinessResponseAsync(HttpListenerContext lc) lc.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; lc.Response.ContentLength64 = s_zeroHostsLength; await lc.Response.OutputStream.WriteAsync(s_zeroHosts, 0, s_zeroHostsLength); + statusCode = HttpStatusCode.ServiceUnavailable; break; case HealthStatusEnum.ReadinessFailedHosts: lc.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; lc.Response.ContentLength64 = s_failedHostsLength; await lc.Response.OutputStream.WriteAsync(s_failedHosts, 0, s_failedHostsLength); + statusCode = HttpStatusCode.ServiceUnavailable; break; case HealthStatusEnum.ReadinessReady: lc.Response.StatusCode = (int)HttpStatusCode.OK; lc.Response.ContentLength64 = s_okLength; await lc.Response.OutputStream.WriteAsync(s_okBytes, 0, s_okLength); + statusCode = HttpStatusCode.OK; + break; } } finally { @@ -221,10 +227,12 @@ public async Task ReadinessResponseAsync(HttpListenerContext lc) } catch { } } + return statusCode; } - public async Task StartupResponseAsync(HttpListenerContext lc) + public async Task StartupResponseAsync(HttpListenerContext lc) { + HttpStatusCode statusCode = HttpStatusCode.OK; // default to 200, may be overridden in switch try { lc.Response.ContentType = "text/plain"; @@ -237,16 +245,19 @@ public async Task StartupResponseAsync(HttpListenerContext lc) lc.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; lc.Response.ContentLength64 = s_zeroHostsLength; await lc.Response.OutputStream.WriteAsync(s_zeroHosts, 0, s_zeroHostsLength); + statusCode = HttpStatusCode.ServiceUnavailable; break; case HealthStatusEnum.StartupFailedHosts: lc.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; lc.Response.ContentLength64 = s_failedHostsLength; await lc.Response.OutputStream.WriteAsync(s_failedHosts, 0, s_failedHostsLength); + statusCode = HttpStatusCode.ServiceUnavailable; break; case HealthStatusEnum.StartupReady: lc.Response.StatusCode = (int)HttpStatusCode.OK; lc.Response.ContentLength64 = s_okLength; await lc.Response.OutputStream.WriteAsync(s_okBytes, 0, s_okLength); + statusCode = HttpStatusCode.OK; break; } } @@ -257,8 +268,8 @@ public async Task StartupResponseAsync(HttpListenerContext lc) lc.Response.Close(); } catch { } - } + return statusCode; } /// diff --git a/src/SimpleL7Proxy/server.cs b/src/SimpleL7Proxy/server.cs index 0559acf2..45ba10e7 100644 --- a/src/SimpleL7Proxy/server.cs +++ b/src/SimpleL7Proxy/server.cs @@ -47,7 +47,7 @@ public class Server : BackgroundService, IConfigChangeSubscriber private readonly IEventClient? _eventHubClient; private static ProxyEvent _staticEvent = new ProxyEvent(); - + private static ProxyEvent _probe = new ProxyEvent(); private readonly ProbeServer _probeServer; // Precomputed frozen collections for O(1) hot-path lookups, recomputed on config change @@ -281,6 +281,8 @@ public async Task Run(CancellationToken cancellationToken) int maxEvents_60Percent = (int)(maxEvents * .6); int maxEvents_50Percent = (int)(maxEvents * .5); + _probe.Type = EventType.Probe; + while (!cancellationToken.IsCancellationRequested) { ProxyEvent ed = null!; @@ -305,26 +307,32 @@ public async Task Run(CancellationToken cancellationToken) continue; } var isprobe = false; - - // if it's a probe, then bypass all the below checks and enqueue the request var probePath = lc.Request.Url?.PathAndQuery; - switch (probePath) + + // Liveness/Readiness/Startup: respond immediately, don't enqueue + if (probePath is Constants.Liveness or Constants.Readiness or Constants.Startup) + { + var (probeType, code) = probePath switch + { + Constants.Liveness => ("Liveness", await _probeServer.LivenessResponseAsync(lc)), + Constants.Readiness => ("Readiness", await _probeServer.ReadinessResponseAsync(lc)), + _ => ("Startup", await _probeServer.StartupResponseAsync(lc)), + }; + _probe.Uri = lc.Request.Url!; + _probe["ProbeType"] = probeType; + _probe["StatusCode"] = ((int)code).ToString(); + _probe.SendEvent(); + continue; + } + + // Health/ForceGC: log probe event, then fall through to enqueue + if (probePath is Constants.Health or Constants.ForceGC) { - case Constants.Liveness: - await _probeServer.LivenessResponseAsync(lc); - continue; - case Constants.Readiness: - await _probeServer.ReadinessResponseAsync(lc); - continue; - case Constants.Startup: - await _probeServer.StartupResponseAsync(lc); - continue; - case Constants.Health: - case Constants.ForceGC: - isprobe = true; - break; // fall through to queue path - default: - break; // not a probe, fall through + isprobe = true; + _probe.Uri = lc.Request.Url!; + _probe["ProbeType"] = probePath == Constants.Health ? "Health" : "ForceGC"; + _probe["StatusCode"] = ((int)HttpStatusCode.OK).ToString(); + _probe.SendEvent(); } int priority = _options.DefaultPriority; From 1684b826ef47fd9d423dae6764f72a2db91234df Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 13:12:30 -0400 Subject: [PATCH 05/18] add InitVars() to config changes --- src/SimpleL7Proxy/Backend/CircuitBreaker.cs | 32 +++++---- .../Config/ConfigChangeNotifer.cs | 5 ++ .../Config/IConfigChangeSubscriber.cs | 4 ++ src/SimpleL7Proxy/ProbeServer.cs | 4 ++ src/SimpleL7Proxy/Proxy/ProxyWorker.cs | 18 ++--- src/SimpleL7Proxy/User/UserProfile.cs | 6 +- src/SimpleL7Proxy/server.cs | 65 +++++++++---------- 7 files changed, 75 insertions(+), 59 deletions(-) diff --git a/src/SimpleL7Proxy/Backend/CircuitBreaker.cs b/src/SimpleL7Proxy/Backend/CircuitBreaker.cs index bf49f54f..9fc5a949 100644 --- a/src/SimpleL7Proxy/Backend/CircuitBreaker.cs +++ b/src/SimpleL7Proxy/Backend/CircuitBreaker.cs @@ -11,10 +11,11 @@ namespace SimpleL7Proxy.Backend; public class CircuitBreaker : ICircuitBreaker { + static ProxyConfig _options = null!; private ConcurrentQueue hostFailureTimes2 = new(); - private readonly int _failureThreshold; - private readonly int _failureTimeFrame; - private readonly HashSet _allowableCodes; + private int _failureThreshold; + private int _failureTimeFrame; + private HashSet _allowableCodes = null!; private readonly ILogger _logger; // Global counters using Interlocked operations @@ -46,16 +47,10 @@ public CircuitBreaker(IOptions options, ILogger log ArgumentNullException.ThrowIfNull(logger, nameof(logger)); var backendOptions = options.Value; - _failureThreshold = backendOptions.CircuitBreakerErrorThreshold; - _failureTimeFrame = backendOptions.CircuitBreakerTimeslice; - _allowableCodes = new HashSet(backendOptions.AcceptableStatusCodes ?? new[] { 200, 401, 403, 408, 410, 412, 417, 400 }); - - count_50percent = (int)(_failureThreshold * 0.5); - count_60percent = (int)(_failureThreshold * 0.6); - count_70percent = (int)(_failureThreshold * 0.7); - count_80percent = (int)(_failureThreshold * 0.8); - count_90percent = (int)(_failureThreshold * 0.9); _logger = logger; + _options = backendOptions; + + InitVars(); // Register this circuit breaker globally Interlocked.Increment(ref _totalCircuitBreakersCount); @@ -69,6 +64,19 @@ public CircuitBreaker(IOptions options, ILogger log ID, _failureThreshold, _failureTimeFrame, _totalCircuitBreakersCount); } + public void InitVars() + { + _failureThreshold = _options.CircuitBreakerErrorThreshold; + _failureTimeFrame = _options.CircuitBreakerTimeslice; + _allowableCodes = new HashSet(_options.AcceptableStatusCodes ?? new[] { 200, 401, 403, 408, 410, 412, 417, 400 }); + + count_50percent = (int)(_failureThreshold * 0.5); + count_60percent = (int)(_failureThreshold * 0.6); + count_70percent = (int)(_failureThreshold * 0.7); + count_80percent = (int)(_failureThreshold * 0.8); + count_90percent = (int)(_failureThreshold * 0.9); + } + public void TrackStatus(int code, bool wasFailure, string state) { if (_allowableCodes.Contains(code) && !wasFailure) diff --git a/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs b/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs index 08966380..eeaf6b14 100644 --- a/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs +++ b/src/SimpleL7Proxy/Config/ConfigChangeNotifer.cs @@ -261,6 +261,11 @@ private sealed class DelegateSubscriber( Func, ProxyConfig, CancellationToken, Task> callback) : IConfigChangeSubscriber { + public void InitVars() + { + throw new NotImplementedException(); + } + public Task OnConfigChangedAsync( IReadOnlyList changes, ProxyConfig backendOptions, diff --git a/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs b/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs index 5d0a9248..f8a588e2 100644 --- a/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs +++ b/src/SimpleL7Proxy/Config/IConfigChangeSubscriber.cs @@ -16,4 +16,8 @@ Task OnConfigChangedAsync( IReadOnlyList changes, ProxyConfig backendOptions, CancellationToken cancellationToken); + + // Share this method between the constructor and the OnConfigChangedAsync to avoid + // code duplication and potential bugs where the two get out of sync. + void InitVars(); } diff --git a/src/SimpleL7Proxy/ProbeServer.cs b/src/SimpleL7Proxy/ProbeServer.cs index 4a4fee8b..d2910168 100644 --- a/src/SimpleL7Proxy/ProbeServer.cs +++ b/src/SimpleL7Proxy/ProbeServer.cs @@ -281,6 +281,10 @@ public override Task StopAsync(CancellationToken cancellationToken) return base.StopAsync(cancellationToken); } + public void InitVars() + { + // no config-dependent variables to init for now, but this is a placeholder for future ones + } /// /// Called when HealthProbeSidecar config changes at runtime. diff --git a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs index 1dd09978..1952723a 100644 --- a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs +++ b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs @@ -80,10 +80,8 @@ public ProxyWorker( _lifecycleManager = context.LifecycleManager; _eventDataBuilder = context.EventDataBuilder; _options = context.BackendOptions; - s_backendKeys = _options.DependancyHeaders; - s_stripRequestHeaders = _options.StripRequestHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); - s_stripResponseHeaders = _options.StripResponseHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + InitVars(); _wrkCntxt.ConfigChangeNotifier.Subscribe( this, @@ -99,17 +97,21 @@ public ProxyWorker( options => options.Timeout, options => options.AsyncTimeout, options => options.AsyncTriggerTimeout); -} + } + + public void InitVars(){ + s_backendKeys = _options.DependancyHeaders; + s_stripRequestHeaders = _options.StripRequestHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + s_stripResponseHeaders = _options.StripResponseHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + } + public Task OnConfigChangedAsync( IReadOnlyList changes, ProxyConfig backendOptions, CancellationToken cancellationToken) { - s_backendKeys = backendOptions.DependancyHeaders; - s_stripRequestHeaders = backendOptions.StripRequestHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); - s_stripResponseHeaders = backendOptions.StripResponseHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); - + InitVars(); return Task.CompletedTask; } diff --git a/src/SimpleL7Proxy/User/UserProfile.cs b/src/SimpleL7Proxy/User/UserProfile.cs index 9fc296e7..9a024953 100644 --- a/src/SimpleL7Proxy/User/UserProfile.cs +++ b/src/SimpleL7Proxy/User/UserProfile.cs @@ -66,7 +66,7 @@ public UserProfile(ProxyConfig options, ConfigChangeNotifier configChangeNotifie _logger.LogInformation("[INIT] UserProfile service starting. Lookup header: {Header}, Config URL: {ConfigUrl}, Refresh interval: {RefreshInterval}s, Soft-delete TTL: {SoftDeleteTTL} minutes, Required: {Required}", options.UserIDFieldName, options.UserConfigUrl, options.UserConfigRefreshIntervalSecs, options.UserSoftDeleteTTLMinutes, options.UserConfigRequired); - initVars(); + InitVars(); // Subscribe to config change notifications configChangeNotifier.Subscribe(this, @@ -80,7 +80,7 @@ public UserProfile(ProxyConfig options, ConfigChangeNotifier configChangeNotifie options => options.AsyncClientConfigFieldName]); } - public void initVars() + public void InitVars() { doUserConfig = _options.UseProfiles && !string.IsNullOrEmpty(_options.UserConfigUrl); @@ -100,7 +100,7 @@ public Task OnConfigChangedAsync( _logger.LogInformation("Received config change notification for UserProfile service. Changes: {Changes}", string.Join(", ", changes.Select(c => c.ToString()))); - initVars(); + InitVars(); if (!doUserConfig) { diff --git a/src/SimpleL7Proxy/server.cs b/src/SimpleL7Proxy/server.cs index 45ba10e7..f7975fc0 100644 --- a/src/SimpleL7Proxy/server.cs +++ b/src/SimpleL7Proxy/server.cs @@ -51,12 +51,12 @@ public class Server : BackgroundService, IConfigChangeSubscriber private readonly ProbeServer _probeServer; // Precomputed frozen collections for O(1) hot-path lookups, recomputed on config change - private volatile FrozenSet _disallowedHeaders; - private volatile FrozenDictionary _priorityKeyToValue; + private volatile FrozenSet _disallowedHeaders = null!; + private volatile FrozenDictionary _priorityKeyToValue = null!; // Precomputed validation rules to avoid dictionary iteration and string ops per request private readonly record struct ValidateHeaderRule(string SourceHeader, string AllowedValuesHeader, string DisplayName); - private ValidateHeaderRule[] _validateHeaderRules; + private ValidateHeaderRule[] _validateHeaderRules = null!; // Constructor to initialize the server with backend options and telemetry client. public Server( @@ -125,18 +125,7 @@ public Server( options => options.PollInterval ]); - // Precompute frozen sets and validation rules at startup - _disallowedHeaders = _options.DisallowedHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); - _priorityKeyToValue = _options.PriorityKeys - .Zip(_options.PriorityValues) - .ToFrozenDictionary(x => x.First, x => x.Second, StringComparer.OrdinalIgnoreCase); - - _validateHeaderRules = _options.ValidateHeaders - .Select(kvp => new ValidateHeaderRule( - kvp.Key, - kvp.Value, - kvp.Key.StartsWith("S7", StringComparison.Ordinal) ? kvp.Key[2..] : kvp.Key)) - .ToArray(); + InitVars(); var _listeningUrl = $"http://+:{_options.Port}/"; @@ -154,25 +143,30 @@ public Server( _logger.LogInformation($"[CONFIG] Server configuration - Port: {_options.Port} | Timeout: {timeoutTime} | Workers: {_options.Workers}"); } - public Task OnConfigChangedAsync( - IReadOnlyList changes, - ProxyConfig backendOptions, - CancellationToken cancellationToken) + public void InitVars() { - _logger.LogInformation("[CONFIG] Server changed — Settings live updated without restart"); - // Recompute frozen sets from updated options - _disallowedHeaders = backendOptions.DisallowedHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + _disallowedHeaders = _options.DisallowedHeaders.ToFrozenSet(StringComparer.OrdinalIgnoreCase); _priorityKeyToValue = _options.PriorityKeys .Zip(_options.PriorityValues) .ToFrozenDictionary(x => x.First, x => x.Second, StringComparer.OrdinalIgnoreCase); - _validateHeaderRules = backendOptions.ValidateHeaders + _validateHeaderRules = _options.ValidateHeaders .Select(kvp => new ValidateHeaderRule( kvp.Key, kvp.Value, kvp.Key.StartsWith("S7", StringComparison.Ordinal) ? kvp.Key[2..] : kvp.Key)) .ToArray(); + } + + public Task OnConfigChangedAsync( + IReadOnlyList changes, + ProxyConfig backendOptions, + CancellationToken cancellationToken) + { + _logger.LogInformation("[CONFIG] Server changed — Settings live updated without restart"); + + InitVars(); return Task.CompletedTask; } @@ -250,7 +244,7 @@ protected override Task ExecuteAsync(CancellationToken cancellationToken) { if (x.IsFaulted && x.Exception != null) { - Console.WriteLine($"[BACKENDS-STARTUP-ERROR] {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} {x.Exception.Flatten()}"); + // Console.WriteLine($"[BACKENDS-STARTUP-ERROR] {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} {x.Exception.Flatten()}"); throw x.Exception.Flatten(); } @@ -322,6 +316,7 @@ public async Task Run(CancellationToken cancellationToken) _probe["ProbeType"] = probeType; _probe["StatusCode"] = ((int)code).ToString(); _probe.SendEvent(); + Console.WriteLine($"[PROBE] {probeType} probe received, responded with {code}"); continue; } @@ -427,12 +422,12 @@ public async Task Run(CancellationToken cancellationToken) if (!string.IsNullOrEmpty(authAppID) && _userProfile.IsAuthAppIDValid(authAppID)) { if (rd.Debug) - Console.WriteLine($"AuthAppID {rd.Headers[_options.ValidateAuthAppIDHeader]} is valid."); + _logger.LogInformation("AuthAppID {AuthAppID} is valid.", rd.Headers[_options.ValidateAuthAppIDHeader]); } else { if (rd.Debug) - Console.WriteLine($"AuthAppID {rd.Headers[_options.ValidateAuthAppIDHeader]} is invalid."); + _logger.LogInformation("AuthAppID {AuthAppID} is invalid.", rd.Headers[_options.ValidateAuthAppIDHeader]); throw new ProxyErrorException( ProxyErrorException.ErrorType.DisallowedAppID, @@ -446,7 +441,7 @@ public async Task Run(CancellationToken cancellationToken) foreach (var header in _disallowedHeaders) { if (rd.Debug && !string.IsNullOrEmpty(rd.Headers.Get(header))) - Console.WriteLine($"Disallowed header {header} removed from request."); + _logger.LogInformation("Disallowed header {Header} removed from request.", header); rd.Headers.Remove(header); } @@ -474,14 +469,14 @@ public async Task Run(CancellationToken cancellationToken) { rd.Headers.Set(header.Key, header.Value); if (rd.Debug) - Console.WriteLine($"Add Header: {header.Key} = {header.Value}"); + _logger.LogInformation("Add Header: {Header} = {Value}", header.Key, header.Value); } } } else { if (rd.Debug) - Console.WriteLine($"User profile for {requestUser} not found."); + _logger.LogInformation("User profile for {User} not found.", requestUser); throw new ProxyErrorException( ProxyErrorException.ErrorType.UnknownProfile, HttpStatusCode.Forbidden, @@ -499,7 +494,7 @@ public async Task Run(CancellationToken cancellationToken) if (!string.IsNullOrEmpty(missing)) { if (rd.Debug) - Console.WriteLine($"Required header {missing} is missing from request."); + _logger.LogInformation("Required header {Header} is missing from request.", missing); throw new ProxyErrorException( ProxyErrorException.ErrorType.IncompleteHeaders, @@ -539,7 +534,7 @@ public async Task Run(CancellationToken cancellationToken) if (!matched) { if (rd.Debug) - Console.WriteLine($"Validation check failed for {rule.DisplayName}: {lookup}"); + _logger.LogInformation("Validation check failed for {DisplayName}: {Lookup}", rule.DisplayName, lookup.ToString()); throw new ProxyErrorException( ProxyErrorException.ErrorType.InvalidHeader, HttpStatusCode.ExpectationFailed, @@ -548,7 +543,7 @@ public async Task Run(CancellationToken cancellationToken) } } if (rd.Debug) - Console.WriteLine($"Validation check passed for all headers."); + _logger.LogInformation("Validation check passed for all headers."); } // Determine priority boost based on the UserID @@ -569,7 +564,7 @@ public async Task Run(CancellationToken cancellationToken) ed["S7P-ID"] = rd.MID; if (rd.Debug) - Console.WriteLine($"UserID: {rd.UserID}"); + _logger.LogInformation("UserID: {UserID}", rd.UserID); // ASYNC: Determine if the request is allowed async operation if (doAsync && bool.TryParse(rd.Headers[_options.AsyncClientRequestHeader], out var asyncEnabled) && asyncEnabled) @@ -590,7 +585,7 @@ public async Task Run(CancellationToken cancellationToken) if (rd.Debug) { - Console.WriteLine($"AsyncEnabled: {rd.runAsync}"); + _logger.LogInformation("AsyncEnabled: {AsyncEnabled}", rd.runAsync); } } @@ -760,8 +755,6 @@ public async Task Run(CancellationToken cancellationToken) } catch (OperationCanceledException) { - // Handle the cancellation request (e.g., break the loop, log the cancellation, etc.) - _staticEvent.WriteOutput("[SHUTDOWN] ⏹ HTTP server shutdown initiated"); break; // Exit the loop } catch (Exception e) From 9cf57b71852b3019a5ea0fda102d6df94ab26916 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 13:17:57 -0400 Subject: [PATCH 06/18] wait for shutdown to complete before exiting --- .../CoordinatedShutdownService.cs | 204 ++++++++++-------- 1 file changed, 114 insertions(+), 90 deletions(-) diff --git a/src/SimpleL7Proxy/CoordinatedShutdownService.cs b/src/SimpleL7Proxy/CoordinatedShutdownService.cs index 73dee7de..50d504a6 100644 --- a/src/SimpleL7Proxy/CoordinatedShutdownService.cs +++ b/src/SimpleL7Proxy/CoordinatedShutdownService.cs @@ -17,6 +17,11 @@ namespace SimpleL7Proxy; public class CoordinatedShutdownService : IHostedService { + private readonly TaskCompletionSource _shutdownComplete = new(TaskCreationOptions.RunContinuationsAsynchronously); + + /// Completes when has fully finished. + public Task ShutdownComplete => _shutdownComplete.Task; + private readonly IHostApplicationLifetime _appLifetime; // Inject other services if needed private readonly ILogger _logger; @@ -36,6 +41,7 @@ public class CoordinatedShutdownService : IHostedService private readonly ProbeServer _probeServer; private readonly CompositeEventClient _compositeEventClient; + private int _stopped = 0; public CoordinatedShutdownService(IHostApplicationLifetime appLifetime, IOptions backendOptions, @@ -78,114 +84,132 @@ public async Task StartAsync(CancellationToken cancellationToken) if (_blobWriteQueue != null) await _blobWriteQueue.StartAsync(cancellationToken).ConfigureAwait(false); await _probeServer.StartAsync(cancellationToken).ConfigureAwait(false); - + if (_serviceBusRequestService is IHostedService sbHosted) await sbHosted.StartAsync(cancellationToken).ConfigureAwait(false); } public async Task StopAsync(CancellationToken cancellationToken) { - await _server.StopListening(cancellationToken).ConfigureAwait(false); - var timeoutTask = Task.Delay(_options.TerminationGracePeriodSeconds * 1000, CancellationToken.None); - _logger.LogInformation($"[SHUTDOWN] ⏳============ Begin Shutdown - Maximum {_options.TerminationGracePeriodSeconds}s"); - _compositeEventClient?.BeginShutdown(); // signal EventHubClient to begin agressively flushing - - // Stop AsyncFeeder and server first to prevent it from generating new work - await _asyncFeeder.StopAsync(cancellationToken).ConfigureAwait(false); - Task requeueTask = _requeueWorker.CancelAllCancelableTasks(); // cancel all cancellable delays - - var allTasksComplete = Task.WhenAll(WorkerFactory.GetAllTasks()); - WorkerFactory.ExpelAsyncRequests(); // backup all async requests - WorkerFactory.RequestWorkerShutdown(); - - // Logger task to periodically log the shutdown status - Task watcherTask = new Task(async () => + if (Interlocked.Exchange(ref _stopped, 1) != 0) + { + await ShutdownComplete; + return; + } + + try { - int counter=0; - while ( ! allTasksComplete.IsCompleted ) + var httplistenerTask = _server.StopListening(cancellationToken); + var timeoutTask = Task.Delay(_options.TerminationGracePeriodSeconds * 1000, CancellationToken.None); + _logger.LogInformation($"[SHUTDOWN] ⏳============ Begin Shutdown - Maximum {_options.TerminationGracePeriodSeconds}s"); + _compositeEventClient?.BeginShutdown(); // signal EventHubClient to begin agressively flushing + + // Stop AsyncFeeder and server first to prevent it from generating new work + var feederTask = _asyncFeeder.StopAsync(cancellationToken); + var requeueTask = _requeueWorker.CancelAllCancelableTasks(); // cancel all cancellable delays + + var allTasksComplete = Task.WhenAll(WorkerFactory.GetAllTasks()); + WorkerFactory.ExpelAsyncRequests(); // backup all async requests + + await Task.WhenAll(httplistenerTask, feederTask, requeueTask).ConfigureAwait(false); // wait for server to stop accepting requests and feeder to finish + WorkerFactory.RequestWorkerShutdown(); + + + // Logger task to periodically log the shutdown status + Task watcherTask = new Task(async () => { - int queueCount = _queue?.thrdSafeCount ?? 0; - if ( counter++ % 5 == 0) // Log every 5 iterations to avoid spamming - Console.WriteLine($"[SHUTDOWN] Queue: {queueCount}, Workers: { HealthCheckService.GetWorkerState() }"); - if (queueCount == 0 ) + int counter = 0; + while (!allTasksComplete.IsCompleted) { - await _queue!.StopAsync().ConfigureAwait(false); - // no more work to do - break; + int queueCount = _queue?.thrdSafeCount ?? 0; + if (counter++ % 5 == 0) // Log every 5 iterations to avoid spamming + _logger.LogInformation($"[SHUTDOWN] Queue: {queueCount}, Workers: {HealthCheckService.GetWorkerState()}"); + if (queueCount == 0) + { + await _queue!.StopAsync().ConfigureAwait(false); + // no more work to do + break; + } + await Task.Delay(200).ConfigureAwait(false); } - await Task.Delay(200).ConfigureAwait(false); + }); + watcherTask.Start(); + + await requeueTask.ConfigureAwait(false); // wait for all cancellable delays to be cancelled + + var completedTask = await Task.WhenAny(allTasksComplete, timeoutTask).ConfigureAwait(false); + if (completedTask == timeoutTask) + { + _logger.LogInformation("[SHUTDOWN] ⚠ Grace period expired ({GracePeriod}s) — queue: {Queue}, workers: {States}", + _options.TerminationGracePeriodSeconds, _queue.thrdSafeCount, HealthCheckService.GetWorkerState()); + } + else + { + _logger.LogInformation("[SHUTDOWN] ✓ All tasks completed"); } - }); - watcherTask.Start(); + await _queue!.StopAsync().ConfigureAwait(false); - await requeueTask.ConfigureAwait(false); // wait for all cancellable delays to be cancelled + ProxyEvent data = new() + { + ["EventType"] = "S7P-Shutdown", + ["Message"] = "Coordinated shutdown in process.", + ["Timestamp"] = DateTime.UtcNow.ToString("o"), + ["BackendStatus"] = _backends.HostStatus, + ["QueueCount"] = _queue.thrdSafeCount.ToString(), + ["WorkerStates"] = HealthCheckService.GetWorkerState() + }; + data.SendEvent(); + + Task? t = _backends.Stop(); + if (t != null) + await t.ConfigureAwait(false); // Stop the backend pollers + + // Discover and invoke all IShutdownParticipant implementations, ordered by ShutdownOrder. + // Same pattern as IHostedService — register as IShutdownParticipant in DI, get discovered here. + foreach (var participant in _shutdownParticipants.OrderBy(p => p.ShutdownOrder)) + { + _logger.LogInformation("[SHUTDOWN] ⏹ Shutting down {Service} (order {Order})", + participant.GetType().Name, participant.ShutdownOrder); + await participant.ShutdownAsync(cancellationToken).ConfigureAwait(false); + } - var completedTask = await Task.WhenAny(allTasksComplete, timeoutTask).ConfigureAwait(false); - if (completedTask == timeoutTask) - { - _logger.LogInformation("[SHUTDOWN] ⚠ Grace period expired ({GracePeriod}s) — queue: {Queue}, workers: {States}", - _options.TerminationGracePeriodSeconds, _queue.thrdSafeCount, HealthCheckService.GetWorkerState()); - } - else - { - _logger.LogInformation("[SHUTDOWN] ✓ All tasks completed"); - } - await _queue!.StopAsync().ConfigureAwait(false); + if (_backendTokenProvider != null) + await _backendTokenProvider.StopAsync(cancellationToken).ConfigureAwait(false); - ProxyEvent data=new() - { - ["EventType"] = "S7P-Shutdown", - ["Message"] = "Coordinated shutdown in process.", - ["Timestamp"] = DateTime.UtcNow.ToString("o"), - ["BackendStatus"] = _backends.HostStatus, - ["QueueCount"] = _queue.thrdSafeCount.ToString(), - ["WorkerStates"] = HealthCheckService.GetWorkerState() - }; - data.SendEvent(); - - Task? t = _backends.Stop(); - if (t != null) - await t.ConfigureAwait(false); // Stop the backend pollers - - // Discover and invoke all IShutdownParticipant implementations, ordered by ShutdownOrder. - // Same pattern as IHostedService — register as IShutdownParticipant in DI, get discovered here. - foreach (var participant in _shutdownParticipants.OrderBy(p => p.ShutdownOrder)) + // // BackupAPIService is NOT registered as IHostedService - we control its shutdown explicitly here + // // to ensure it stops AFTER all proxy workers have completed and flushed their status updates + // if (_backupAPIService != null) + // await _backupAPIService.StopAsync(cancellationToken).ConfigureAwait(false); + + // ServiceBusRequestService is stopped explicitly here for ordering control + if (_serviceBusRequestService != null) + await _serviceBusRequestService.StopAsync(cancellationToken).ConfigureAwait(false); + + // BlobWriteQueue is stopped LAST before probes - all producers (proxy workers, async workers, backup service) + // are guaranteed to be done at this point, so no more enqueues will happen + if (_blobWriteQueue != null) + { + _logger.LogInformation("[SHUTDOWN] ⏹ Stopping BlobWriteQueue (final flush)"); + await _blobWriteQueue.StopAsync(CancellationToken.None).ConfigureAwait(false); + // Dispose underlying BlobWriter after the queue has flushed + _blobWriter?.Dispose(); + } + + // Health probes are stopped at the VERY END so the container orchestrator + // (e.g. Kubernetes, Container Apps) continues to see healthy probes while + // other services drain. If probes fail early, the orchestrator may kill the pod. + await _probeServer.StopAsync(CancellationToken.None).ConfigureAwait(false); + await _server.StopProbes(CancellationToken.None).ConfigureAwait(false); + await _compositeEventClient!.StopTimerAsync().ConfigureAwait(false); + } + catch (Exception ex) { - _logger.LogInformation("[SHUTDOWN] ⏹ Shutting down {Service} (order {Order})", - participant.GetType().Name, participant.ShutdownOrder); - await participant.ShutdownAsync(cancellationToken).ConfigureAwait(false); + _logger.LogError(ex, "[SHUTDOWN] ❌ Shutdown failed"); } - - if (_backendTokenProvider != null) - await _backendTokenProvider.StopAsync(cancellationToken).ConfigureAwait(false); - - // // BackupAPIService is NOT registered as IHostedService - we control its shutdown explicitly here - // // to ensure it stops AFTER all proxy workers have completed and flushed their status updates - // if (_backupAPIService != null) - // await _backupAPIService.StopAsync(cancellationToken).ConfigureAwait(false); - - // ServiceBusRequestService is stopped explicitly here for ordering control - if (_serviceBusRequestService != null) - await _serviceBusRequestService.StopAsync(cancellationToken).ConfigureAwait(false); - - // BlobWriteQueue is stopped LAST before probes - all producers (proxy workers, async workers, backup service) - // are guaranteed to be done at this point, so no more enqueues will happen - if (_blobWriteQueue != null) + finally { - _logger.LogInformation("[SHUTDOWN] ⏹ Stopping BlobWriteQueue (final flush)"); - await _blobWriteQueue.StopAsync(CancellationToken.None).ConfigureAwait(false); - // Dispose underlying BlobWriter after the queue has flushed - _blobWriter?.Dispose(); + _shutdownComplete.TrySetResult(); } - - // Health probes are stopped at the VERY END so the container orchestrator - // (e.g. Kubernetes, Container Apps) continues to see healthy probes while - // other services drain. If probes fail early, the orchestrator may kill the pod. - Console.WriteLine("[SHUTDOWN] ⏹ Stopping health probes"); - await _probeServer.StopAsync(CancellationToken.None).ConfigureAwait(false); - await _server.StopProbes(CancellationToken.None).ConfigureAwait(false); - await _compositeEventClient!.StopTimerAsync().ConfigureAwait(false); - Console.WriteLine("[SHUTDOWN] ✅ Service Stopped"); } } \ No newline at end of file From 5504d707d9111dedcc350e53bc6b4c81c10ac69e Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 13:28:38 -0400 Subject: [PATCH 07/18] clean up noisy console logs, fix double time logs in ACA --- src/SimpleL7Proxy/Backend/Backends.cs | 14 ++------- .../Backend/HostCollectionManager.cs | 1 - src/SimpleL7Proxy/Backend/HostConfig.cs | 2 +- src/SimpleL7Proxy/Banner.cs | 6 ++-- src/SimpleL7Proxy/CustomConsoleFormatter.cs | 30 ++++++++++++++----- .../Events/CompositeEventClient.cs | 2 -- src/SimpleL7Proxy/Events/EventHubClient.cs | 6 ++-- src/SimpleL7Proxy/Events/EventHubConfig.cs | 2 -- src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs | 1 - 9 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/SimpleL7Proxy/Backend/Backends.cs b/src/SimpleL7Proxy/Backend/Backends.cs index 175fde4e..5dadbccc 100644 --- a/src/SimpleL7Proxy/Backend/Backends.cs +++ b/src/SimpleL7Proxy/Backend/Backends.cs @@ -119,13 +119,9 @@ public void Start() { if (task.Exception != null) { - Console.WriteLine($"[BACKENDS-POLLER-ERROR] {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} {task.Exception.Flatten()}"); + _logger.LogError(task.Exception, "[BACKENDS-POLLER-ERROR] {Time} {Exception}", DateTime.UtcNow, task.Exception.Flatten()); } }, TaskContinuationOptions.OnlyOnFaulted); - - // If OAuth is enabled, start token refresh - - _logger.LogInformation("[SERVICE] ✓ Backend service started"); } @@ -165,13 +161,10 @@ public async Task WaitForStartup(int timeout) var completedTask = await Task.WhenAny(allTasks, delayTask).ConfigureAwait(false); if (completedTask == delayTask) { - Console.WriteLine($"[BACKENDS-STARTUP-ERROR] {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} Backend Token Provider did not initialize tokens in the last {timeout} seconds."); - _logger.LogError($"Backend Token Provider did not initialize tokens in the last {timeout} seconds."); + // Console.WriteLine($"[BACKENDS-STARTUP-ERROR] {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} Backend Token Provider did not initialize tokens in the last {timeout} seconds."); + _logger.LogError("Backend Token Provider did not initialize tokens in the last {Timeout} seconds.", timeout); throw new Exception("Backend Token Provider did not initialize tokens in time."); } - - _logger.LogInformation($"[SERVICE] ✓ Backend Poller started in {(DateTime.Now - start).TotalSeconds:F3}s"); - } private readonly Dictionary currentHostStatus = []; @@ -221,7 +214,6 @@ private async Task Run() catch (Exception ex) { // Catch any unhandled exceptions to prevent background service from crashing the host - Console.WriteLine($"[BACKENDS-RUN-ERROR] {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} {ex}"); _logger.LogError(ex, "[BACKENDS] CRITICAL: Unhandled exception in backend poller - service stopping"); throw; // Rethrow to let the host know the background service failed, but at least we logged it } diff --git a/src/SimpleL7Proxy/Backend/HostCollectionManager.cs b/src/SimpleL7Proxy/Backend/HostCollectionManager.cs index 827224df..c236701a 100644 --- a/src/SimpleL7Proxy/Backend/HostCollectionManager.cs +++ b/src/SimpleL7Proxy/Backend/HostCollectionManager.cs @@ -91,7 +91,6 @@ public void Activate() } // Activate circuit breakers only for configs that survived dedup - _logger.LogInformation("[HOST-MANAGER] Activating {Count} host(s)...", uniqueConfigs.Count); foreach (var config in uniqueConfigs) { config.Activate(); diff --git a/src/SimpleL7Proxy/Backend/HostConfig.cs b/src/SimpleL7Proxy/Backend/HostConfig.cs index 789eabca..9ec0b4a1 100644 --- a/src/SimpleL7Proxy/Backend/HostConfig.cs +++ b/src/SimpleL7Proxy/Backend/HostConfig.cs @@ -307,7 +307,7 @@ private static ParsedConfig TryParseConfig(string hostname, string? probepath, s result.Hostname = result.Host; } else { - Console.WriteLine($"Direct mode host detected: {result.Host}"); + // Console.WriteLine($"Direct mode host detected: {result.Host}"); result.Hostname = new Uri(result.Host).Host; if (string.IsNullOrEmpty(result.Processor)) { diff --git a/src/SimpleL7Proxy/Banner.cs b/src/SimpleL7Proxy/Banner.cs index 14736feb..8aaeffd4 100644 --- a/src/SimpleL7Proxy/Banner.cs +++ b/src/SimpleL7Proxy/Banner.cs @@ -5,13 +5,14 @@ using System.Threading.Tasks; using SimpleL7Proxy; +using SimpleL7Proxy.Config; namespace SimpleL7Proxy; public static class Banner { public const string VERSION = Constants.VERSION; - public static void Display() + public static void Display(ProxyConfig options) { Console.WriteLine("======================================================================================="); Console.WriteLine(" ##### # ####### "); @@ -22,7 +23,6 @@ public static void Display() Console.WriteLine("# # # # # # # # # # # # # # # # # #"); Console.WriteLine(" ##### # # # # ###### ###### ####### # # # # #### # # #"); Console.WriteLine("======================================================================================="); - Console.WriteLine($"Version: {VERSION}"); - + Console.WriteLine($"Version: {VERSION} Log_Level: {options.LogLevel} Container App: {options.ContainerApp} Replica: {options.ReplicaName}"); } } diff --git a/src/SimpleL7Proxy/CustomConsoleFormatter.cs b/src/SimpleL7Proxy/CustomConsoleFormatter.cs index eef67165..45849c4a 100644 --- a/src/SimpleL7Proxy/CustomConsoleFormatter.cs +++ b/src/SimpleL7Proxy/CustomConsoleFormatter.cs @@ -1,21 +1,37 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; using System; using System.IO; +using SimpleL7Proxy.Config; + public class CustomConsoleFormatter : ConsoleFormatter { - public CustomConsoleFormatter() : base("custom") { } + private readonly bool _logDateTime; + + public CustomConsoleFormatter(IOptions options) : base("custom") + { + _logDateTime = options?.Value?.LogDateTime ?? false; + Console.WriteLine($"[CONFIG] CustomConsoleFormatter initialized with LogDateTime={_logDateTime}"); + } public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) { - var logLevel = logEntry.LogLevel; - var eventId = logEntry.EventId; - var state = logEntry.State; - var exception = logEntry.Exception; - var message = logEntry.Formatter(state, exception); + var message = logEntry.Formatter(logEntry.State, logEntry.Exception); + if (message is null) return; - textWriter.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {message}"); + if (_logDateTime) + { + Span buf = stackalloc char[19]; // "MM-dd HH:mm:ss.fff" + DateTime.Now.TryFormat(buf, out _, "MM-dd HH:mm:ss.fff "); + textWriter.Write(buf); + textWriter.WriteLine(message); + } + else + { + textWriter.WriteLine(message); + } } } \ No newline at end of file diff --git a/src/SimpleL7Proxy/Events/CompositeEventClient.cs b/src/SimpleL7Proxy/Events/CompositeEventClient.cs index 633703d7..3e0e07d5 100644 --- a/src/SimpleL7Proxy/Events/CompositeEventClient.cs +++ b/src/SimpleL7Proxy/Events/CompositeEventClient.cs @@ -29,7 +29,6 @@ public void Add(IEventClient client) _mutable[client] = 0; _frozen = _mutable.ToFrozenDictionary(); } - Console.WriteLine($"[CompositeEventClient] Added {client.ClientType}"); } public async Task StopTimerAsync() @@ -38,7 +37,6 @@ public async Task StopTimerAsync() foreach (var client in _frozen.Keys) { - Console.WriteLine($"Stopping timer for {client.ClientType}"); stopTasks.Add(client.StopTimerAsync()); } await Task.WhenAll(stopTasks).ConfigureAwait(false); diff --git a/src/SimpleL7Proxy/Events/EventHubClient.cs b/src/SimpleL7Proxy/Events/EventHubClient.cs index 9e8847f5..37d6337d 100644 --- a/src/SimpleL7Proxy/Events/EventHubClient.cs +++ b/src/SimpleL7Proxy/Events/EventHubClient.cs @@ -89,7 +89,9 @@ public async Task StartAsync(CancellationToken cancellationToken) { workerCancelToken = cancellationTokenSource.Token; _composite.Add(this); - _logger.LogCritical("[SERVICE] ✓ EventHub Client started successfully"); + var ConnString = string.IsNullOrEmpty(_config.ConnectionString) ? "Not Set" : "Set"; + _logger.LogCritical("[SERVICE] ✓ EventHub Client started: ConnectionString: {ConnString}, Name: {EventHubName}, Namespace: {EventHubNamespace}", ConnString, _config.EventHubName, _config.EventHubNamespace); + writerTask = Task.Run(() => EventWriter(workerCancelToken), workerCancelToken); } catch (OperationCanceledException) { @@ -135,7 +137,6 @@ public async Task EventWriter(CancellationToken token) try { - _logger.LogInformation("EventHubClient: EventWriter running."); while (!token.IsCancellationRequested) { if (GetNextBatch(99) > 0) @@ -251,7 +252,6 @@ async Task ConnectAsync() await ConnectAsync().ConfigureAwait(false); _batchData?.Dispose(); _batchData = await _producerClient!.CreateBatchAsync().ConfigureAwait(false); - Console.WriteLine("EventHubClient: Reconnected successfully."); Interlocked.Exchange(ref ReconnectCount, 0); return; diff --git a/src/SimpleL7Proxy/Events/EventHubConfig.cs b/src/SimpleL7Proxy/Events/EventHubConfig.cs index 6b67b6c1..45154a8c 100644 --- a/src/SimpleL7Proxy/Events/EventHubConfig.cs +++ b/src/SimpleL7Proxy/Events/EventHubConfig.cs @@ -25,7 +25,5 @@ public EventHubConfig(ProxyConfig options) { Console.WriteLine("[CONFIG] EventHubConfig incomplete — need (EVENTHUB_CONNECTIONSTRING + EVENTHUB_NAME) or (EVENTHUB_NAMESPACE + EVENTHUB_NAME). EventHub logging will be disabled."); throw new InvalidOperationException("Incomplete EventHub configuration. Check logs for details."); } - - Console.WriteLine($"[CONFIG] EventHubConfig initialized. ConnectionString: {(string.IsNullOrEmpty(ConnectionString) ? "Not Set" : "Set")}, EventHubName: {EventHubName}, EventHubNamespace: {EventHubNamespace}, StartupSeconds: {StartupSeconds}"); } } \ No newline at end of file diff --git a/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs b/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs index 8de00f53..7e44630e 100644 --- a/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs +++ b/src/SimpleL7Proxy/Queue/ConcurrentPriQueue.cs @@ -153,7 +153,6 @@ public async Task SignalWorker(CancellationToken cancellationToken) } } - _logger.LogInformation("SignalWorker: Exiting - queue is empty: " + (_priorityQueue.Count == 0)); // Shutdown _taskSignaler.CancelAllTasks(); From fd8ef6f0e11645139a95fd53a5a56e6f41d8d4db Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 13:29:35 -0400 Subject: [PATCH 08/18] add InitVars() to config changes --- .../Iterators/SharedIteratorRegistry.cs | 39 ++++++--- src/SimpleL7Proxy/Events/ProxyEvent.cs | 81 +++++++++---------- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/src/SimpleL7Proxy/Backend/Iterators/SharedIteratorRegistry.cs b/src/SimpleL7Proxy/Backend/Iterators/SharedIteratorRegistry.cs index 6b56c30b..5ce87710 100644 --- a/src/SimpleL7Proxy/Backend/Iterators/SharedIteratorRegistry.cs +++ b/src/SimpleL7Proxy/Backend/Iterators/SharedIteratorRegistry.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.Logging; +using SimpleL7Proxy.Config; namespace SimpleL7Proxy.Backend.Iterators; @@ -27,7 +28,7 @@ namespace SimpleL7Proxy.Backend.Iterators; /// │ │ /// └─────────────────────────────────────────────────────────────────────────────┘ /// -public sealed class SharedIteratorRegistry : ISharedIteratorRegistry, IShutdownParticipant, IDisposable +public sealed class SharedIteratorRegistry : ISharedIteratorRegistry, IShutdownParticipant, IDisposable, IConfigChangeSubscriber { public int ShutdownOrder => 200; @@ -40,26 +41,26 @@ public Task ShutdownAsync(CancellationToken cancellationToken) private readonly Dictionary _iterators = new(); private readonly object _lock = new(); private readonly ILogger _logger; + private readonly ProxyConfig _options; private readonly Timer _cleanupTimer; - private readonly TimeSpan _iteratorTTL; - private readonly TimeSpan _cleanupInterval; + private TimeSpan _iteratorTTL; + private TimeSpan _cleanupInterval; private volatile bool _disposed; /// /// Creates a new SharedIteratorRegistry with the specified TTL and cleanup interval. /// /// Logger for diagnostic output - /// How long an unused iterator lives before cleanup (default: 60 seconds) - /// How often to run cleanup (default: 30 seconds) + /// Backend configuration options public SharedIteratorRegistry( ILogger logger, - int iteratorTTLSeconds = 60, - int cleanupIntervalSeconds = 30) + ProxyConfig backendOptions, + ConfigChangeNotifier configChangeNotifier) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _iteratorTTL = TimeSpan.FromSeconds(Math.Max(10, iteratorTTLSeconds)); - _cleanupInterval = TimeSpan.FromSeconds(Math.Max(5, cleanupIntervalSeconds)); + _options = backendOptions ?? throw new ArgumentNullException(nameof(backendOptions)); + InitVars(); // Start cleanup timer _cleanupTimer = new Timer( CleanupStaleIterators, @@ -67,11 +68,26 @@ public SharedIteratorRegistry( _cleanupInterval, _cleanupInterval); - _logger.LogInformation( + _logger.LogDebug( "[SharedIteratorRegistry] Initialized with TTL={TTL}s, CleanupInterval={Interval}s", _iteratorTTL.TotalSeconds, _cleanupInterval.TotalSeconds); + + configChangeNotifier.Subscribe(this, + o => o.SharedIteratorTTLSeconds, + o => o.SharedIteratorCleanupIntervalSeconds); + } + + public Task OnConfigChangedAsync(IReadOnlyList changes, ProxyConfig backendOptions, CancellationToken cancellationToken) + { + InitVars(); + return Task.CompletedTask; } + public void InitVars() + { + _iteratorTTL = TimeSpan.FromSeconds(Math.Max(10, _options.SharedIteratorTTLSeconds)); + _cleanupInterval = TimeSpan.FromSeconds(Math.Max(5, _options.SharedIteratorCleanupIntervalSeconds)); + } /// public int Count { @@ -265,7 +281,6 @@ public void Dispose() { iterator.Dispose(); } - - _logger.LogInformation("[SharedIteratorRegistry] Disposed"); } + } diff --git a/src/SimpleL7Proxy/Events/ProxyEvent.cs b/src/SimpleL7Proxy/Events/ProxyEvent.cs index 789ae63c..c0be099e 100644 --- a/src/SimpleL7Proxy/Events/ProxyEvent.cs +++ b/src/SimpleL7Proxy/Events/ProxyEvent.cs @@ -14,17 +14,52 @@ namespace SimpleL7Proxy.Events { - public class ProxyEvent : ConcurrentDictionary, IConfigChangeSubscriber + public class ProxyEventInitializer : IConfigChangeSubscriber + { + public ProxyEventInitializer( + IOptions backendOptions, + IEventClient eventClient, + ICommonEventData commonEventData, + ConfigChangeNotifier configChangeNotifier, + TelemetryClient? telemetryClient = null) + { + ProxyEvent.Initialize(backendOptions, eventClient, commonEventData, telemetryClient); + + configChangeNotifier.Subscribe(this, + o => o.LogToConsole, + o => o.LogToEvents, + o => o.LogToAI); + InitVars(); + } + + public void InitVars() + { + var options = ProxyEvent.Options; + ProxyEvent.ConAttr = LogTargetAttr.From(options.Value.LogToConsole); + ProxyEvent.EventAttr = LogTargetAttr.From(options.Value.LogToEvents); + ProxyEvent.AIAttr = LogTargetAttr.From(options.Value.LogToAI); + } + + public Task OnConfigChangedAsync( + IReadOnlyList changes, + ProxyConfig backendOptions, + CancellationToken cancellationToken) + { + InitVars(); + return Task.CompletedTask; + } + } + + public class ProxyEvent : ConcurrentDictionary { private static IOptions _options = null!; private static IEventClient? _eventClient; private static TelemetryClient? _telemetryClient; /// - /// Singleton instance used for config-change subscription. - /// Created by . + /// Exposes the options for to read during config refresh. /// - private static ProxyEvent? _subscriberInstance; + internal static IOptions Options => _options; private static readonly Uri LOCALHOSTURI = new Uri("http://localhost"); // ── Per-event-type log routing ── @@ -55,44 +90,6 @@ public static void Initialize( // Set default parameters that should be included with every event (frozen = immutable + optimized reads) DefaultParams = commonEventData?.DefaultEventData() ?? DefaultParams; - - UpdateLogTargets(backendOptions.Value); - } - - /// - /// Subscribes to warm config changes for LogToConsole, LogToEvents, and LogToAI. - /// Call once after when the is available. - /// - public static void SubscribeToConfigChanges(ConfigChangeNotifier notifier) - { - _subscriberInstance ??= new ProxyEvent(); - notifier.Subscribe(_subscriberInstance, - o => o.LogToConsole, - o => o.LogToEvents, - o => o.LogToAI); - } - - /// - public Task OnConfigChangedAsync( - IReadOnlyList changes, - ProxyConfig backendOptions, - CancellationToken cancellationToken) - { - UpdateLogTargets(backendOptions); - return Task.CompletedTask; - } - - /// - /// Parses , , - /// and into , , - /// . A list containing "*" enables all event types for that destination. - /// Safe to call on config hot-reload. - /// - public static void UpdateLogTargets(ProxyConfig options) - { - ConAttr = LogTargetAttr.From(options.LogToConsole); - EventAttr = LogTargetAttr.From(options.LogToEvents); - AIAttr = LogTargetAttr.From(options.LogToAI); } /// From ce10215e6413ce54e3f728fee1a893845b73a028 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 13:31:52 -0400 Subject: [PATCH 09/18] streamline configuration processing --- src/SimpleL7Proxy/Config/AppConfigService.cs | 128 ++++++++----------- src/SimpleL7Proxy/Config/ConfigFactory.cs | 104 +++++++++++++-- src/SimpleL7Proxy/Config/ConfigParser.cs | 54 ++++---- src/SimpleL7Proxy/Config/ProxyConfig.cs | 128 ++++++++----------- 4 files changed, 234 insertions(+), 180 deletions(-) diff --git a/src/SimpleL7Proxy/Config/AppConfigService.cs b/src/SimpleL7Proxy/Config/AppConfigService.cs index 73755ddc..17fd1197 100644 --- a/src/SimpleL7Proxy/Config/AppConfigService.cs +++ b/src/SimpleL7Proxy/Config/AppConfigService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Azure.Data.AppConfiguration; +using System.Collections.Frozen; using SimpleL7Proxy.Backend; namespace SimpleL7Proxy.Config; @@ -18,29 +19,30 @@ public class AppConfigService : BackgroundService private readonly string? _endpoint; private readonly string? _connectionString; private readonly string? _labelFilter; - private ProxyConfig _options; private readonly DefaultCredential _defaultCredential; private readonly TimeSpan _refreshInterval; private bool _isInitialized = false; - // Warm-keyed snapshot: "Warm:KeyPath" → value, used by DetectWarmChanges. - private Dictionary _warmSnapshot = new(StringComparer.OrdinalIgnoreCase); + // App Config key path → config name (e.g. "Logging:LogConsole" → "LogConsole"). + // Built once from static descriptors; never changes. + private static readonly FrozenDictionary s_keyPathToConfigName = + ConfigMetadata.Descriptors.ToFrozenDictionary( + d => d.Attribute.KeyPath, d => d.ConfigName, StringComparer.OrdinalIgnoreCase); + private string? _lastSentinel; // Set from Program.cs after the DI container is built. public ConfigChangeNotifier? Notifier { get; set; } public IHostHealthCollection? HostCollection { get; set; } - /// The downloaded settings (merged warm + cold), available after has been awaited. - public Dictionary? Settings { get; private set; } - /// Only warm-prefixed settings, available after has been awaited. public Dictionary? WarmSettings { get; private set; } /// Only cold-prefixed settings, available after has been awaited. public Dictionary? ColdSettings { get; private set; } - public static ProxyConfig DEFAULT_OPTIONS { get; set; } + private ProxyConfig _options = null!; + public static ProxyConfig DEFAULT_OPTIONS { get; set; } = null!; public AppConfigService(ILogger logger, ProxyConfig backendOptions, DefaultCredential defaultCredential) @@ -76,7 +78,6 @@ public void Start() return; } - _logger.LogInformation("[BOOTSTRAP] Starting App Configuration bootstrap download..."); _downloadTask = Task.Run(DownloadConfig); } @@ -107,27 +108,15 @@ public void Start() return (null, null); } - _logger.LogInformation("[BOOTSTRAP] Retrieved {WarmCount} warm and {ColdCount} cold App Configuration value(s)", warm.Count, cold.Count); - CommitDownload(warm, cold); - _isInitialized = true; - return (WarmSettings, ColdSettings); - } - - public (Dictionary?, Dictionary?) WaitForDownload() => (Settings, ColdSettings); - - /// - /// Commits a freshly downloaded config: stores Settings, swaps snapshot, extracts sentinel. - /// - private void CommitDownload(Dictionary warm, Dictionary cold) - { + _logger.LogInformation("[BOOTSTRAP] App Configuration- Warm: {WarmCount}, Cold: {ColdCount} Refresh: {interval} secs", warm.Count, cold.Count, _refreshInterval.TotalSeconds); WarmSettings = warm; ColdSettings = cold; - // Merge: cold first, then warm overwrites (warm takes precedence) - // var merged = new Dictionary(cold, StringComparer.OrdinalIgnoreCase); - // foreach (var kvp in warm) merged[kvp.Key] = kvp.Value; - // Settings = merged; - // _snapshot = new Dictionary(_warmSnapshot, StringComparer.OrdinalIgnoreCase); - _lastSentinel = _warmSnapshot.TryGetValue("Warm:Sentinel", out var s) ? s : null; + warm.TryGetValue("Sentinel", out _lastSentinel); + if (_lastSentinel == null) + _logger.LogInformation("[APP-CONFIG] Sentinel missing"); + _isInitialized = true; + + return (WarmSettings, ColdSettings); } /// @@ -148,13 +137,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (string.IsNullOrEmpty(_endpoint) && string.IsNullOrEmpty(_connectionString)) return; // No App Config — nothing to refresh. - _logger.LogInformation("[BOOTSTRAP] App Configuration refresh: {Interval} seconds", _refreshInterval.TotalSeconds); - + DateTime nextRefreshTime = DateTime.UtcNow.Add(_refreshInterval); while (!stoppingToken.IsCancellationRequested) { - await Task.Delay(_refreshInterval, stoppingToken); + var delay = nextRefreshTime - DateTime.UtcNow; + if (delay > TimeSpan.Zero) + { + await Task.Delay(delay, stoppingToken); + } try { await ProcessRefreshAsync(stoppingToken); + nextRefreshTime = nextRefreshTime.Add(_refreshInterval); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogWarning(ex, "[BOOTSTRAP] Refresh failed, will retry"); } @@ -173,12 +166,14 @@ private async Task ProcessRefreshAsync(CancellationToken ct) var result = await Task.Run(DownloadConfig, ct); var (warm, cold) = result.Value; - CommitDownload(warm, cold); + WarmSettings = warm; + ColdSettings = cold; + warm.TryGetValue("Sentinel", out _lastSentinel); if (result == null || Notifier == null) return; - await ConfigFactory.ApplyRefresh( + await ConfigFactory.ApplyRefreshV2( _options, DEFAULT_OPTIONS, warm, Notifier, HostCollection, _logger, ct); } @@ -214,59 +209,48 @@ private ConfigurationClient GetConfigurationClient() return _cachedClient; } + /// + /// Downloads all settings from Azure App Configuration in a single round-trip, + /// filtered by label. Each key is expected to have a "Warm:" or "Cold:" prefix; + /// unrecognised prefixes default to cold. The prefix is stripped and the remaining + /// key path is resolved to a config name via + /// or passed through directly for backend host keys. + /// Returns null if the download fails. + /// private (Dictionary warm, Dictionary cold)? DownloadConfig() { try { var client = GetConfigurationClient(); - // Build a lookup from App Config key path → env var name using the descriptors. - // e.g. "Logging:LogConsole" → "LogConsole", "Async:Timeout" → "AsyncTimeout" - var keyPathToEnvVar = ConfigMetadata.Descriptors - .ToDictionary(d => d.Attribute.KeyPath, d => d.ConfigName, StringComparer.OrdinalIgnoreCase); - var warm = new Dictionary(StringComparer.OrdinalIgnoreCase); var cold = new Dictionary(StringComparer.OrdinalIgnoreCase); - var warmSnapshot = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var prefix in new[] { "Warm:", "Cold:" }) + var selector = new SettingSelector { KeyFilter = "*", LabelFilter = _labelFilter }; + foreach (var setting in client.GetConfigurationSettings(selector)) { - var isWarm = prefix == "Warm:"; - var target = isWarm ? warm : cold; - var selector = new SettingSelector { KeyFilter = $"{prefix}*", LabelFilter = _labelFilter }; - foreach (var setting in client.GetConfigurationSettings(selector)) + var (key, value) = (setting.Key, setting.Value ?? ""); + + if (key.Length < 6) continue; + + var keyPath = key[5..]; + Dictionary? target = + key.StartsWith("Warm:", StringComparison.OrdinalIgnoreCase) ? warm : + key.StartsWith("Cold:", StringComparison.OrdinalIgnoreCase) ? cold : + cold; + + var resolvedKey = s_keyPathToConfigName.TryGetValue(keyPath, out var envVarName) ? envVarName + : ConfigParser.IsBackendHostConfigName(keyPath) ? keyPath + : null; + + if (resolvedKey == null) { - var keyPath = setting.Key.Substring(prefix.Length); - if (string.IsNullOrEmpty(keyPath)) continue; - - var value = setting.Value ?? ""; - - if (isWarm && keyPath.Equals("Sentinel", StringComparison.OrdinalIgnoreCase)) - { - warmSnapshot[setting.Key] = value; - continue; - } - - if (keyPathToEnvVar.TryGetValue(keyPath, out var envVarName)) - { - target[envVarName] = value; - if (isWarm) warmSnapshot[setting.Key] = value; - _logger.LogDebug("[CONFIG] {Key} → {EnvVar}", setting.Key, envVarName); - } - else if (ConfigParser.IsBackendHostConfigName(keyPath)) - { - target[keyPath] = value; - if (isWarm) warmSnapshot[setting.Key] = value; - _logger.LogDebug("[CONFIG] {Key} → {KeyPath}", setting.Key, keyPath); - } - else - { - _logger.LogDebug("[CONFIG] No descriptor for key {Key}, skipping", setting.Key); - } + _logger.LogInformation("[CONFIG] No descriptor for key {Key}, skipping", key); + continue; } - } - _warmSnapshot = warmSnapshot; + target[resolvedKey] = value; + } return (warm, cold); } diff --git a/src/SimpleL7Proxy/Config/ConfigFactory.cs b/src/SimpleL7Proxy/Config/ConfigFactory.cs index 3f2f06fd..7f7b8936 100644 --- a/src/SimpleL7Proxy/Config/ConfigFactory.cs +++ b/src/SimpleL7Proxy/Config/ConfigFactory.cs @@ -75,11 +75,11 @@ public static Dictionary EffectiveEnvironment() } else { - var merged = new Dictionary(coldSettings ?? new Dictionary()); + var incoming = new Dictionary(coldSettings ?? new Dictionary()); if ( warmSettings != null) - foreach (var kvp in warmSettings) merged[kvp.Key] = kvp.Value; + foreach (var kvp in warmSettings) incoming[kvp.Key] = kvp.Value; - envOptions = ConfigParser.ApplyEnv(merged, baseOptions); + envOptions = ConfigParser.ApplyEnv(incoming, baseOptions); } ConfigParser.ConfigureHttpClient(envOptions); @@ -114,9 +114,9 @@ public static async Task ApplyRefresh( var before = field.GetValue(liveOptions); // TODO: remove debug ds.Clear(); ds[kvp.Key] = kvp.Value?.ToString() ?? ""; - liveOptions.ApplyFieldFromEnv(ds, defaultOptions, kvp.Key, field.Name); + liveOptions.ApplyFieldFromEnv(ds, kvp.Key, field.Name); var after = field.GetValue(liveOptions); // TODO: remove debug - Console.WriteLine($"[WARM] Applied {kvp.Key} ({field.Name}): '{before}' -> '{after}'"); // TODO: remove debug + // Console.WriteLine($"[WARM] Applied {kvp.Key} ({field.Name}): '{before}' -> '{after}'"); // TODO: remove debug } // Collect changed PropertyInfos for derived-settings recalculation. @@ -126,19 +126,19 @@ public static async Task ApplyRefresh( if (fields.TryGetValue(change.PropertyName, out var prop)) changedProps.Add(prop); } - Console.WriteLine($"[WARM] {changedProps.Count} derived-settings prop(s) to recalculate"); // TODO: remove debug + // Console.WriteLine($"[WARM] {changedProps.Count} derived-settings prop(s) to recalculate"); // TODO: remove debug if (changedProps.Count > 0) ConfigParser.ApplyDerivedSettings(liveOptions, [.. changedProps]); if (hostChanges.Count > 0) { - Console.WriteLine($"[WARM] {hostChanges.Count} host change(s) detected, re-registering backends"); // TODO: remove debug + // Console.WriteLine($"[WARM] {hostChanges.Count} host change(s) detected, re-registering backends"); // TODO: remove debug RegisterBackends(liveOptions, null, hostChanges, hostCollection); } if (changes.Count > 0) { - Console.WriteLine($"[WARM] Notifying {changes.Count} change(s): {string.Join(", ", changes.Select(c => c.PropertyName))}"); // TODO: remove debug + // Console.WriteLine($"[WARM] Notifying {changes.Count} change(s): {string.Join(", ", changes.Select(c => c.PropertyName))}"); // TODO: remove debug logger.LogInformation("[BOOTSTRAP] Applied {Count} warm change(s): {Names}", changes.Count, string.Join(", ", changes.Select(c => c.PropertyName))); if (notifier != null) @@ -146,6 +146,92 @@ public static async Task ApplyRefresh( } } + /// + /// Single-pass warm-refresh: detects changes, applies them to liveOptions, derives + /// dependent settings, re-registers backends, and notifies subscribers — all without + /// throwaway ProxyConfig allocations or double-parsing. + /// Parallel replacement for ApplyRefresh + DetectWarmChanges for comparison. + /// + public static async Task ApplyRefreshV2( + ProxyConfig liveOptions, + ProxyConfig defaultOptions, + Dictionary warm, + ConfigChangeNotifier? notifier, + IHostHealthCollection? hostCollection, + ILogger logger, + CancellationToken ct) + { + var changeList = new List(); + var changedProps = new List(); + var hostChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + var env = new Dictionary(1, StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in warm) + { + var (key, rawValue) = (kvp.Key, kvp.Value); + if (string.IsNullOrEmpty(rawValue)) continue; + + // Host keys are handled separately via RegisterBackends. + if (key.StartsWith("Host") || key.StartsWith("Probe") || key.StartsWith("IP")) + { + hostChanges[key] = rawValue; + continue; + } + + // Resolve the warm descriptor (try key-path first, then config name). + if (!ConfigMetadata.WarmDescriptorsByKeyPath.TryGetValue(key, out var descriptor) + && !ConfigMetadata.WarmDescriptorsByConfigName.TryGetValue(key, out descriptor)) + continue; + + var configName = descriptor.ConfigName; + if (!ConfigMetadata.TryGetFieldByConfigName(configName, out var field) || field == null) + continue; + + // Capture the current live value before applying. + var currentValue = field.GetValue(liveOptions); + + // Parse the raw value by applying directly to liveOptions (single parse). + env.Clear(); + env[configName] = rawValue; + liveOptions.ApplyFieldFromEnv(env, configName, field.Name); + var newValue = field.GetValue(liveOptions); + + // If unchanged, skip. + if (DeepEquals(currentValue, newValue)) continue; + + Console.WriteLine($"[WARM-V2] Applied {configName} ({field.Name}): '{currentValue}' -> '{newValue}'"); // TODO: remove debug + + changedProps.Add(field); + changeList.Add(new ConfigChange + { + PropertyName = configName, + KeyPath = descriptor.Attribute.KeyPath, + RawOldValue = currentValue, + RawNewValue = newValue + }); + } + + if (changeList.Count == 0 && hostChanges.Count == 0) + return; + + // Recalculate derived settings for any changed properties. + if (changedProps.Count > 0) + ConfigParser.ApplyDerivedSettings(liveOptions, [.. changedProps]); + + // Re-register backends if host keys changed. + if (hostChanges.Count > 0) + RegisterBackends(liveOptions, null, hostChanges, hostCollection); + + // Notify subscribers. + if (changeList.Count > 0) + { + logger.LogInformation("[BOOTSTRAP] Applied {Count} warm change(s): {Names}", + changeList.Count, string.Join(", ", changeList.Select(c => c.PropertyName))); + if (notifier != null) + await notifier.NotifyAsync(changeList, liveOptions, ct); + } + } + /// /// Diffs bare-keyed warm settings against live options. Does not mutate liveOptions. /// Host keys (Host*, Probe*, IP*) are returned separately in HostChanges. @@ -187,7 +273,7 @@ private static (List Changes, Dictionary ParsedVa env.Clear(); env[configName] = rawValue; - defaultTarget.ApplyFieldFromEnv(env, liveOptions, configName, field.Name); + defaultTarget.ApplyFieldFromEnv(env, configName, field.Name); var newValue = field.GetValue(defaultTarget); if (DeepEquals(currentValue, newValue)) continue; diff --git a/src/SimpleL7Proxy/Config/ConfigParser.cs b/src/SimpleL7Proxy/Config/ConfigParser.cs index 89951e0e..7b7cc805 100644 --- a/src/SimpleL7Proxy/Config/ConfigParser.cs +++ b/src/SimpleL7Proxy/Config/ConfigParser.cs @@ -40,12 +40,10 @@ private static readonly (string envVar, string property)[] SimpleFields = ("AsyncClientRequestHeader", "AsyncClientRequestHeader"), ("AsyncClientConfigFieldName", "AsyncClientConfigFieldName"), - ("CONTAINER_APP_NAME", "ContainerApp"), ("HealthProbeSidecar", "HealthProbeSidecar"), ("LoadBalanceMode", "LoadBalanceMode"), ("OAuthAudience", "OAuthAudience"), ("PriorityKeyHeader", "PriorityKeyHeader"), - ("CONTAINER_APP_REVISION", "Revision"), ("StorageDbContainerName", "StorageDbContainerName"), ("SuspendedUserConfigUrl", "SuspendedUserConfigUrl"), ("TimeoutHeader", "TimeoutHeader"), @@ -91,6 +89,7 @@ private static readonly (string envVar, string property)[] SimpleFields = ("EVENT_HEADERS", "EventHeaders"), ("LOGTOFILE", "LogToFile"), ("LOGFILE_NAME", "LogFileName"), + ("LOGDATETIME", "LogDateTime"), // ── EventHub ── ("EVENTHUB_CONNECTIONSTRING", "EventHubConnectionString"), @@ -111,35 +110,44 @@ private static readonly (string envVar, string property)[] SimpleFields = // ── Security ── ("IgnoreSSLCert", "IgnoreSSLCert"), + + // ── Identity ── + ("CONTAINER_APP_NAME", "ContainerApp"), + ("CONTAINER_APP_REVISION", "Revision"), + ("CONTAINER_APP_REPLICA_NAME", "ReplicaName"), + ("Hostname", "HostName"), + ("RequestIDPrefix", "IDStr"), ]; // Creates a BackendOptions instance by applying environment variable overrides on top of the defaults - public static ProxyConfig ApplyEnv(Dictionary dict, ProxyConfig defaults) + public static ProxyConfig ApplyEnv(Dictionary incoming, ProxyConfig defaults) { - // calculated values based on logic - var opts = new ProxyConfig(); + // Start from a copy of the defaults; the loop only overwrites keys present in incoming. + var opts = defaults.DeepClone(); foreach (var (envVarName, propertyName) in SimpleFields) { - // for all options, uses either the environment or default value - opts.ApplyFieldFromEnv(dict, defaults, envVarName, propertyName); + opts.ApplyFieldFromEnv(incoming, envVarName, propertyName); } - opts.AcceptableStatusCodes = ReadEnvironmentVariableOrDefault(dict, "AcceptableStatusCodes", defaults.AcceptableStatusCodes); - opts.IterationMode = ReadEnvironmentVariableOrDefault(dict, "IterationMode", defaults.IterationMode); + opts.AcceptableStatusCodes = ReadEnvironmentVariableOrDefault(incoming, "AcceptableStatusCodes", defaults.AcceptableStatusCodes); + opts.IterationMode = ReadEnvironmentVariableOrDefault(incoming, "IterationMode", defaults.IterationMode); var defaultPriorityWorkers = string.Join(",", defaults.PriorityWorkers.Select(kvp => $"{kvp.Key}:{kvp.Value}")); - opts.PriorityWorkers = KVIntPairs(ToListOfString(ReadEnvironmentVariableOrDefault(dict, "PriorityWorkers", defaultPriorityWorkers))); + opts.PriorityWorkers = KVIntPairs(ToListOfString(ReadEnvironmentVariableOrDefault(incoming, "PriorityWorkers", defaultPriorityWorkers))); var defaultValidateHeaders = string.Join(",", defaults.ValidateHeaders.Select(kvp => $"{kvp.Key}={kvp.Value}")); - opts.ValidateHeaders = KVStringPairs(ToListOfString(ReadEnvironmentVariableOrDefault(dict, "ValidateHeaders", defaultValidateHeaders))); + opts.ValidateHeaders = KVStringPairs(ToListOfString(ReadEnvironmentVariableOrDefault(incoming, "ValidateHeaders", defaultValidateHeaders))); - ApplyAsyncServiceBusOverrides(dict, opts, defaults); - ApplyAsyncBlobStorageOverrides(dict, opts, defaults); + ApplyAsyncServiceBusOverrides(incoming, opts, defaults); + ApplyAsyncBlobStorageOverrides(incoming, opts, defaults); - // IDStr is derived from the prefix + HostName (already resolved via SimpleFields). - opts.IDStr = $"{opts.IDStr}-{opts.HostName}-"; + // IDStr uses the replica identity for request tracing. + // Prefer the container-app replica ID over the resolved HostName, + // since Hostname may be explicitly overridden to a user-friendly value. + var replicaId = !string.IsNullOrEmpty(opts.ReplicaName) ? opts.ReplicaName : opts.HostName; + opts.IDStr = $"{opts.IDStr}-{replicaId}-"; ApplyDerivedSettingsFromConfigNames( opts, @@ -203,12 +211,12 @@ public static ProxyConfig ApplyEnv(Dictionary dict, ProxyConfig /// /// - /// Forwards to . Kept for backward compatibility. + /// Forwards to . Kept for backward compatibility. /// - public static void ApplyFieldFromEnv(Dictionary env, ProxyConfig target, ProxyConfig defaults, string envVar, string property) - { - target.ApplyFieldFromEnv(env, defaults, envVar, property); - } + // public static void ApplyFieldFromEnv(Dictionary incomingSettings, ProxyConfig target, string configKey, string propertyName) + // { + // target.ApplyFieldFromEnv(incomingSettings, configKey, propertyName); + // } public static void ApplyDerivedSettings(ProxyConfig backendOptions, params PropertyInfo[] changedProperties) { @@ -332,12 +340,6 @@ private static void ApplyAsyncBlobStorageOverrides(Dictionary en opts.AsyncBlobStorageUseMI = ReadEnvironmentVariableOrDefault(env, "AsyncBlobStorageUseMI", useMi); } - private static void ApplyReplicaIdentitySettings(Dictionary env, ProxyConfig opts, string replicaId) - { - opts.HostName = ReadEnvironmentVariableOrDefault(env, "Hostname", replicaId); - opts.IDStr = $"{ReadEnvironmentVariableOrDefault(env, "RequestIDPrefix", "S7P")}-{replicaId}-"; - } - private static void ParseHealthProbeSidecarSettings(ProxyConfig backendOptions) { var healthSettings = backendOptions.HealthProbeSidecar.Split(';', StringSplitOptions.RemoveEmptyEntries); diff --git a/src/SimpleL7Proxy/Config/ProxyConfig.cs b/src/SimpleL7Proxy/Config/ProxyConfig.cs index f71cdcbf..40cc902f 100644 --- a/src/SimpleL7Proxy/Config/ProxyConfig.cs +++ b/src/SimpleL7Proxy/Config/ProxyConfig.cs @@ -94,6 +94,11 @@ public class ProxyConfig [ConfigOption("Response:StripResponseHeaders")] public List StripResponseHeaders { get; set; } = []; + // Sentinel -- + [ConfigOption("Sentinel")] + public string Sentinel { get; set; } = ""; // used for app configrefresh + + // ── User ── [ConfigOption("User:SuspendedUserConfigUrl")] public string SuspendedUserConfigUrl { get; set; } = ""; // e.g. "file:suspended.json" or "http://configservice/suspended" @@ -246,6 +251,9 @@ public class ProxyConfig [ConfigOption("Logging:LogToFile", ConfigName = "LOGTOFILE", Mode = ConfigMode.Hidden)] public bool LogToFile { get; set; } = false; + [ConfigOption("Logging:LogDateTime", ConfigName = "LOGDATETIME", Mode = ConfigMode.Cold)] + public bool LogDateTime { get; set; } = false; + // ── Shared Iterators ── /// /// When true, requests to the same path share the same host iterator, @@ -283,8 +291,12 @@ public class ProxyConfig // ── Metadata ── [ConfigOption("Metadata:ContainerApp", ConfigName = "CONTAINER_APP_NAME", Mode = ConfigMode.Hidden)] public string ContainerApp { get; set; } = "ContainerAppName"; + [ConfigOption("Metadata:HostName", ConfigName = "Hostname", Mode = ConfigMode.Hidden)] + public string HostName { get; set; } = ""; [ConfigOption("Metadata:IDStr", ConfigName = "RequestIDPrefix", Mode = ConfigMode.Hidden)] public string IDStr { get; set; } = "S7P"; + [ConfigOption("Metadata:ReplicaName", ConfigName = "CONTAINER_APP_REPLICA_NAME", Mode = ConfigMode.Hidden)] + public string ReplicaName { get; set; } = ""; [ConfigOption("Metadata:Revision", ConfigName = "CONTAINER_APP_REVISION", Mode = ConfigMode.Hidden)] public string Revision { get; set; } = "revisionID"; @@ -292,7 +304,6 @@ public class ProxyConfig public HttpClient? Client { get; set; } public bool HealthProbeSidecarEnabled { get; set; } = false; public string HealthProbeSidecarUrl { get; set; } = "http://localhost/9000"; - public string HostName { get; set; } = ""; public List Hosts { get; set; } = []; public Dictionary PriorityWorkers { get; set; } = new() { { 2, 1 }, { 3, 1 } }; public bool TrackWorkers { get; set; } = true; @@ -330,81 +341,52 @@ public ProxyConfig DeepClone() } /// - /// Applies a single configuration field from the environment dictionary to this instance. - /// Uses reflection to set the named property, falling back to the corresponding default value when the - /// environment variable is absent or set to the default placeholder. + /// Applies a single configuration field from the incoming settings to this instance. + /// Looks up in ; when absent + /// or set to the default placeholder, the property is left unchanged. + /// The caller is expected to seed this instance (e.g. via DeepClone) before calling. /// - public void ApplyFieldFromEnv(Dictionary env, ProxyConfig defaults, string envVar, string property) + public void ApplyFieldFromEnv(Dictionary incomingSettings, string configKey, string propertyName) { - var pi = typeof(ProxyConfig).GetProperty(property) ?? throw new InvalidOperationException($"Unknown BackendOptions property: {property}"); - var defVal = pi.GetValue(defaults); - var type = pi.PropertyType; + var prop = typeof(ProxyConfig).GetProperty(propertyName) ?? throw new InvalidOperationException($"Unknown ProxyConfig property: {propertyName}"); + var currentValue = prop.GetValue(this); + var type = prop.PropertyType; - var envValue = env.GetValueOrDefault(envVar)?.Trim(); - bool envVarPresent = !string.IsNullOrEmpty(envValue) && envValue != ConfigOptions.DefaultPlaceholder; - if (!envVarPresent) - { - var currentVal = pi.GetValue(this); - bool alreadyChanged = !Equals(currentVal, defVal); - if (alreadyChanged) - { - return; - } - } - - if (type == typeof(int) || type == typeof(double)) - { - var val = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, Convert.ToInt32(defVal)); - pi.SetValue(this, Convert.ChangeType(val, type)); - } - else if (type == typeof(float)) - { - var val = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, Convert.ToSingle(defVal)); - pi.SetValue(this, Convert.ChangeType(val, type)); - } - else if (type == typeof(string)) - { - pi.SetValue(this, ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, (string)defVal!)); - } - else if (type == typeof(bool)) - { - pi.SetValue(this, ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, (bool)defVal!)); - } - else if (type == typeof(List)) - { - var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, string.Join(",", (List)defVal!)); - pi.SetValue(this, ConfigParser.ToListOfString(value)); - } - else if (type == typeof(List)) - { - var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, string.Join(",", (List)defVal!)); - pi.SetValue(this, ConfigParser.ToListOfInt(value)); - } - else if (type == typeof(int[])) - { - pi.SetValue(this, ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, (int[])defVal!)); - } - else if (type == typeof(Dictionary)) - { - var defaultValue = string.Join(",", ((Dictionary)defVal!).Select(kvp => $"{kvp.Key}={kvp.Value}")); - var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, defaultValue); - pi.SetValue(this, ConfigParser.KVStringPairs(ConfigParser.ToListOfString(value))); - } - else if (type.IsEnum) - { - var value = ConfigParser.ReadEnvironmentVariableOrDefault(env, envVar, defVal!.ToString()!); - if (Enum.TryParse(type, value, true, out var parsed)) - { - pi.SetValue(this, parsed); - } - else - { - pi.SetValue(this, defVal); - } - } - else + var rawIncoming = incomingSettings.GetValueOrDefault(configKey)?.Trim(); + bool hasIncomingValue = !string.IsNullOrEmpty(rawIncoming) && rawIncoming != ConfigMetadata.DefaultPlaceholder; + if (!hasIncomingValue) + return; + + object? resolved = type switch { - throw new NotSupportedException($"ApplyFieldFromEnv: unsupported property type {type.Name} for {property}"); - } + _ when type == typeof(int) + => ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, Convert.ToInt32(currentValue)), + _ when type == typeof(double) + => ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, Convert.ToInt32(currentValue)), + _ when type == typeof(float) + => ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, Convert.ToSingle(currentValue)), + _ when type == typeof(string) + => ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, (string)currentValue!), + _ when type == typeof(bool) + => ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, (bool)currentValue!), + _ when type == typeof(List) + => ConfigParser.ToListOfString( + ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, string.Join(",", (List)currentValue!))), + _ when type == typeof(List) + => ConfigParser.ToListOfInt( + ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, string.Join(",", (List)currentValue!))), + _ when type == typeof(int[]) + => ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, (int[])currentValue!), + _ when type == typeof(Dictionary) + => ConfigParser.KVStringPairs(ConfigParser.ToListOfString( + ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, + string.Join(",", ((Dictionary)currentValue!).Select(kvp => $"{kvp.Key}={kvp.Value}"))))), + _ when type.IsEnum + => Enum.TryParse(type, ConfigParser.ReadEnvironmentVariableOrDefault(incomingSettings, configKey, currentValue!.ToString()!), true, out var parsed) + ? parsed : currentValue, + _ => throw new NotSupportedException($"ApplyFieldFromEnv: unsupported property type {type.Name} for {propertyName}") + }; + + prop.SetValue(this, resolved); } } \ No newline at end of file From 0a855a2b1268269f56a062f241f9518a3f573d21 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 13:32:45 -0400 Subject: [PATCH 10/18] wait for shutdown to complete before exiting --- src/SimpleL7Proxy/Program.cs | 80 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/src/SimpleL7Proxy/Program.cs b/src/SimpleL7Proxy/Program.cs index b00f034b..f0fdb527 100644 --- a/src/SimpleL7Proxy/Program.cs +++ b/src/SimpleL7Proxy/Program.cs @@ -48,8 +48,8 @@ public class Program public static async Task Main(string[] args) { - Banner.Display(); + DateTime StartTime= DateTime.UtcNow; var startupLoggerFactory = LoggerFactory.Create(ConfigureLogging); var startupLogger = startupLoggerFactory.CreateLogger(); @@ -78,7 +78,7 @@ public static async Task Main(string[] args) }) .ConfigureServices((hostContext, services) => { - ConfigureDI(services, startupLogger, appConfigBootstrap, defaultCredential); + ConfigureDI(services, startupLoggerFactory, appConfigBootstrap, defaultCredential); }); var frameworkHost = hostBuilder.Build(); @@ -87,6 +87,8 @@ public static async Task Main(string[] args) var serviceProvider = frameworkHost.Services; var options = serviceProvider.GetRequiredService>(); + Banner.Display(options.Value); + appConfigBootstrap.Notifier = serviceProvider.GetRequiredService(); appConfigBootstrap.HostCollection = serviceProvider.GetRequiredService(); @@ -98,13 +100,11 @@ public static async Task Main(string[] args) var loggerFactory = serviceProvider.GetRequiredService(); var streamProcessorLogger = loggerFactory.CreateLogger("StreamProcessor"); BaseStreamProcessor.SetLogger(streamProcessorLogger); - startupLogger.LogInformation("[INIT] ✓ Stream processor logger initialized"); // Initialize ProxyEvent with BackendOptions var commonEventData = serviceProvider.GetRequiredService(); - ProxyEvent.Initialize(options, eventClient, commonEventData, telemetryClient); - ProxyEvent.SubscribeToConfigChanges(serviceProvider.GetRequiredService()); + serviceProvider.GetRequiredService(); // Initialize HostConfig with all required dependencies including service provider for circuit breaker DI HostConfig.Initialize(backendTokenProvider, startupLogger, serviceProvider); @@ -138,10 +138,6 @@ public static async Task Main(string[] args) // serviceProvider.GetRequiredService>() // ); } - else - { - startupLogger.LogInformation("[INIT] ⚠ Async mode disabled - Running in synchronous mode only"); - } } catch (Exception ex) { @@ -171,20 +167,37 @@ public static async Task Main(string[] args) catch (Exception e) { // Log full exception details including inner exceptions - Console.WriteLine(e.ToString()); - startupLogger.LogError(e, "[ERROR] ✗ Unexpected startup error: {Message}", e.Message); + var pe = new ProxyEvent(); pe.Type = EventType.Exception; pe.SendEvent(); } + + // await for the coordinated shutdown to complete before exiting Main, ensuring all cleanup logic runs + // (RunAsync may return early if the host's ShutdownTimeout expires before StopAsync finishes) + try { + var shutdownService = serviceProvider.GetRequiredService(); + await shutdownService.ShutdownComplete; + } + catch (ObjectDisposedException) + { + //nop - Shutdown may have completed already or may have timed out, in which case we just exit + } + + catch (Exception ex) + { + //nop - Shutdown may have completed already or may have timed out, in which case we just exit + startupLogger.LogWarning(ex, "[SHUTDOWN] Coordinated shutdown did not complete gracefully"); + } + + startupLogger.LogInformation("[SHUTDOWN] ✅ SimpleL7Proxy Service Stopped. Version: {Version} Runtime: {Runtime}", Banner.VERSION, DateTime.UtcNow - StartTime); } private static void ConfigureLogging(ILoggingBuilder logging) { var logLevelString = Environment.GetEnvironmentVariable("LOG_LEVEL") ?? "Information"; var logLevel = Enum.TryParse(logLevelString, true, out var l) ? l : LogLevel.Information; - Console.WriteLine($"[CONFIG] Log level: {logLevel}"); logging.AddConsole(options => options.FormatterName = "custom"); logging.AddConsoleFormatter(); @@ -214,8 +227,7 @@ private static void ConfigureAppInsights(IServiceCollection services, ProxyConfi startupLogger.LogInformation("[INIT] ✓ AppInsights initialized with custom request tracking"); } } - - private static void ConfigureDI(IServiceCollection services, ILogger startupLogger, AppConfigService appConfigBootstrap, DefaultCredential defaultCredential) + private static void ConfigureDI(IServiceCollection services, ILoggerFactory startupLoggerFactory, AppConfigService appConfigBootstrap, DefaultCredential defaultCredential) { services.AddSingleton(appConfigBootstrap); services.AddSingleton(defaultCredential); @@ -224,10 +236,16 @@ private static void ConfigureDI(IServiceCollection services, ILogger startupLogg // register the backend options var result = ConfigFactory.CreateOptions(appConfigBootstrap).GetAwaiter().GetResult(); + // create a new logger based on configs loaded from App Config + var startupLogger = startupLoggerFactory.CreateLogger(); + + // a copy of the defaults AppConfigService.DEFAULT_OPTIONS = result.baseOptions; var backendOptions = result.envOptions; + Console.Out.Flush(); + ConfigureAppInsights(services, backendOptions, startupLogger); services.RegisterBackendOptions(startupLogger, backendOptions); @@ -315,6 +333,7 @@ private static void ConfigureDI(IServiceCollection services, ILogger startupLogg services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton>(); @@ -356,32 +375,15 @@ private static void ConfigureDI(IServiceCollection services, ILogger startupLogg // Stream processor factory - optimized singleton for high-throughput scenarios services.AddSingleton(); - // Shared Iterator Registry - conditionally registered based on UseSharedIterators option - // When enabled, requests to the same path share the same iterator for fair distribution - services.AddSingleton(sp => - { - var options = sp.GetRequiredService>().Value; - if (!options.UseSharedIterators) - { - // Return null - WorkerFactory handles null gracefully - return null!; - } - var logger = sp.GetRequiredService>(); - return new SharedIteratorRegistry( - logger, - options.SharedIteratorTTLSeconds, - options.SharedIteratorCleanupIntervalSeconds); - }); - services.AddSingleton(sp => - { - var registry = sp.GetRequiredService(); - return registry as IShutdownParticipant ?? throw new InvalidOperationException( - "ISharedIteratorRegistry implementation does not implement IShutdownParticipant"); - }); + // Shared Iterator Registry — requests to the same path share the same iterator for fair distribution + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); services.AddHostedService(); services.AddTransient(source => new CancellationTokenSource()); - services.AddHostedService(); + services.AddSingleton(); + services.AddHostedService(sp => sp.GetRequiredService()); } private static void RegisterEventHeaders(IServiceCollection services, ILogger startupLogger, ProxyConfig backendOptions) @@ -424,7 +426,7 @@ private static void RegisterEventLoggers(IServiceCollection services, ILogger st enabledLoggers = new HashSet( eventLoggersRaw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), StringComparer.OrdinalIgnoreCase); - Console.WriteLine($"[CONFIG] EVENT_LOGGERS: {string.Join(", ", enabledLoggers)}"); + startupLogger.LogInformation("[CONFIG] EVENT_LOGGERS: {EventLoggers}", string.Join(", ", enabledLoggers)); } else { @@ -432,7 +434,7 @@ private static void RegisterEventLoggers(IServiceCollection services, ILogger st { backendOptions.LogToFile ? "file" : "eventhub" }; - Console.WriteLine($"[CONFIG] EVENT_LOGGERS not set, falling back to legacy: {string.Join(", ", enabledLoggers)}"); + startupLogger.LogInformation("[CONFIG] EVENT_LOGGERS not set, falling back to legacy: {EventLoggers}", string.Join(", ", enabledLoggers)); } foreach (var loggername in enabledLoggers) From 7df56a69325b5367f24e4e4fed8f0d333912451b Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 16:41:29 -0400 Subject: [PATCH 11/18] cleanup startup console messages --- src/SimpleL7Proxy/Banner.cs | 2 +- src/SimpleL7Proxy/User/UserProfile.cs | 2 +- src/SimpleL7Proxy/server.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SimpleL7Proxy/Banner.cs b/src/SimpleL7Proxy/Banner.cs index 8aaeffd4..e1330aac 100644 --- a/src/SimpleL7Proxy/Banner.cs +++ b/src/SimpleL7Proxy/Banner.cs @@ -23,6 +23,6 @@ public static void Display(ProxyConfig options) Console.WriteLine("# # # # # # # # # # # # # # # # # #"); Console.WriteLine(" ##### # # # # ###### ###### ####### # # # # #### # # #"); Console.WriteLine("======================================================================================="); - Console.WriteLine($"Version: {VERSION} Log_Level: {options.LogLevel} Container App: {options.ContainerApp} Replica: {options.ReplicaName}"); + Console.WriteLine($"Version: {VERSION} LogLevel: {options.LogLevel} ContainerApp: {options.ContainerApp} Replica: {options.ReplicaName}"); } } diff --git a/src/SimpleL7Proxy/User/UserProfile.cs b/src/SimpleL7Proxy/User/UserProfile.cs index 9a024953..c70d82a2 100644 --- a/src/SimpleL7Proxy/User/UserProfile.cs +++ b/src/SimpleL7Proxy/User/UserProfile.cs @@ -63,7 +63,7 @@ public UserProfile(ProxyConfig options, ConfigChangeNotifier configChangeNotifie _options = options; _logger = logger; _userInformation = CreateDefaultAsyncClientInfoCache(); - _logger.LogInformation("[INIT] UserProfile service starting. Lookup header: {Header}, Config URL: {ConfigUrl}, Refresh interval: {RefreshInterval}s, Soft-delete TTL: {SoftDeleteTTL} minutes, Required: {Required}", + _logger.LogInformation("[INIT] UserProfile service starting. Lookup header: {Header}, Config URL: {ConfigUrl}, Refresh interval: {RefreshInterval}s, Soft-delete TTL: {SoftDeleteTTL} minutes, Required: {Required}", options.UserIDFieldName, options.UserConfigUrl, options.UserConfigRefreshIntervalSecs, options.UserSoftDeleteTTLMinutes, options.UserConfigRequired); InitVars(); diff --git a/src/SimpleL7Proxy/server.cs b/src/SimpleL7Proxy/server.cs index f7975fc0..b1dac01f 100644 --- a/src/SimpleL7Proxy/server.cs +++ b/src/SimpleL7Proxy/server.cs @@ -316,7 +316,6 @@ public async Task Run(CancellationToken cancellationToken) _probe["ProbeType"] = probeType; _probe["StatusCode"] = ((int)code).ToString(); _probe.SendEvent(); - Console.WriteLine($"[PROBE] {probeType} probe received, responded with {code}"); continue; } From c018a2e591f52162e446bab0c86e095b557a5aeb Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 16:41:53 -0400 Subject: [PATCH 12/18] add probes to /health --- src/SimpleL7Proxy/Proxy/HealthCheckService.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/SimpleL7Proxy/Proxy/HealthCheckService.cs b/src/SimpleL7Proxy/Proxy/HealthCheckService.cs index 28422cc4..a7fec0c9 100644 --- a/src/SimpleL7Proxy/Proxy/HealthCheckService.cs +++ b/src/SimpleL7Proxy/Proxy/HealthCheckService.cs @@ -336,6 +336,16 @@ public void BuildHealthResponse(string path, int hostCount, bool hasFailedHosts, .Append(hasFailedHosts ? "FAILED HOSTS" : "All Hosts Operational") .Append('\n'); + // Probe status snapshot + var (startupStatus, readinessStatus, undrainedEvents) = GetStatus(); + _stringBuilder.Append("Probes:\n /startup: ") + .Append(startupStatus == HealthStatusEnum.StartupReady ? "200 OK" : "503 " + startupStatus) + .Append("\n /readiness: ") + .Append(readinessStatus == HealthStatusEnum.ReadinessReady ? "200 OK" : "503 " + readinessStatus) + .Append("\n Undrained Events: ") + .Append(undrainedEvents) + .Append('\n'); + // ThreadPool availability snapshot ThreadPool.GetAvailableThreads(out int workersAvailable, out int ioAvailable); ThreadPool.GetMinThreads(out int workersMin, out int ioMin); From a9cdcba7eedc4f4cdfd21076b36b3f59f162bdd3 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 16:43:11 -0400 Subject: [PATCH 13/18] consolidate response paths for 404 into streaming add loop_once to shared iterator --- src/SimpleL7Proxy/Proxy/ProxyWorker.cs | 84 +++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs index 1952723a..3341c5b7 100644 --- a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs +++ b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs @@ -821,8 +821,27 @@ public async Task ProxyToBackEndAsync(RequestData request) // Try the request on each active host, stop if it worked // Use helper method to abstract over shared vs per-request iterators + + // TODO: Replace dummy parameters with request header lookup (e.g. request.Headers["x-S7P-IterationMode"]) + bool loop_once = true; + bool loop_for_max_attempts = false; + + // For shared iterators, compute the max attempts for this request so the + // circular iterator doesn't spin forever. Per-request iterators already + // track their own limits internally, so we use int.MaxValue for them. + int maxSharedAttempts = int.MaxValue; + if (sharedIterator != null) + { + if (loop_once) + maxSharedAttempts = matchingHostCount; // SinglePass: try each host once + else if (loop_for_max_attempts) + maxSharedAttempts = _options.MaxAttempts; // MultiPass: use configured max + // else: no limit (original circular behaviour) + } + BaseHostHealth? host; - while (TryGetNextHost(hostIterator, sharedIterator, out host) && host != null) + while (request.BackendAttempts < maxSharedAttempts + && TryGetNextHost(hostIterator, sharedIterator, out host) && host != null) { DateTime proxyStartDate = DateTime.UtcNow; @@ -1253,7 +1272,7 @@ public async Task ProxyToBackEndAsync(RequestData request) ProxyHelperUtils.GenerateErrorMessage(incompleteRequests, out sb, out statusMatches, out currentStatusCode); // 502 Bad Gateway or call status code form all attempts ( if they are the same ) - lastStatusCode = (statusMatches) ? (HttpStatusCode)currentStatusCode : HttpStatusCode.BadGateway; + lastStatusCode = statusMatches ? (HttpStatusCode)currentStatusCode : HttpStatusCode.BadGateway; // requestSummary.Type = EventType.ProxyError; // ASYNC: Synchronize with AsyncWorker if it was started, even for error responses @@ -1274,19 +1293,68 @@ public async Task ProxyToBackEndAsync(RequestData request) } } - // Write error response to client (sync HTTP) or blob (async) - await WriteExhaustedHostsErrorAsync(request, lastStatusCode, sb.ToString()).ConfigureAwait(false); + var errorBodyStr = sb.ToString(); + var errorBytes = Encoding.UTF8.GetBytes(errorBodyStr); + var recordedStatusCode = ProxyHelperUtils.RecordIncompleteRequests(requestSummary, lastStatusCode, "No active hosts were able to handle the request", incompleteRequests); + + if (request.AsyncTriggered) + { + // TODO: Unify async error path to flow through WriteResponseAsync/StreamResponseAsync + // like the non-async path below. For now, write directly via WriteExhaustedHostsErrorAsync. + await WriteExhaustedHostsErrorAsync(request, lastStatusCode, errorBodyStr).ConfigureAwait(false); + + return new ProxyData + { + FullURL = request.FullURL, + CalculatedHostLatency = (DateTime.UtcNow - request.EnqueueTime).TotalMilliseconds, + BackendHostname = "No Active Hosts Available", + ResponseDate = DateTime.UtcNow, + StatusCode = recordedStatusCode, + Body = errorBytes, + }; + } + + // Non-async path: build a ProxyData that flows through the normal + // WriteResponseAsync → StreamResponseAsync pipeline, so error responses + // use the same code path as success responses. + var errorContent = new ByteArrayContent(errorBytes); + errorContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }; + errorContent.Headers.ContentLength = errorBytes.Length; + + var errorHeaders = new WebHeaderCollection + { + ["x-Request-Queue-Duration"] = (request.DequeueTime - request.EnqueueTime).TotalMilliseconds.ToString("F3") + " ms", + ["x-Total-Latency"] = (DateTime.UtcNow - request.EnqueueTime).TotalMilliseconds.ToString("F3") + " ms", + ["x-ProxyHost"] = _options.HostName, + ["x-MID"] = request.MID, + ["Attempts"] = request.BackendAttempts.ToString() + }; + var errorResponse = new HttpResponseMessage(lastStatusCode) { Content = errorContent }; + + // Set headers on the HTTP context so WriteResponseAsync finds them already applied + if (request.Context != null) + { + request.Context.Response.StatusCode = (int)lastStatusCode; + request.Context.Response.Headers = errorHeaders; + } return new ProxyData { FullURL = request.FullURL, - CalculatedHostLatency = (DateTime.UtcNow - request.EnqueueTime).TotalMilliseconds, BackendHostname = "No Active Hosts Available", ResponseDate = DateTime.UtcNow, - StatusCode = ProxyHelperUtils.RecordIncompleteRequests(requestSummary, lastStatusCode, "No active hosts were able to handle the request", incompleteRequests), - Body = Encoding.UTF8.GetBytes(sb.ToString()) + StatusCode = recordedStatusCode, + Body = errorBytes, + BodyResponseMessage = errorResponse, + StreamingProcessor = StreamProcessorFactory.DEFAULT_PROCESSOR, + Headers = errorHeaders, + ContentHeaders = new WebHeaderCollection + { + ["Content-Type"] = "application/json; charset=utf-8", + ["Content-Length"] = errorBytes.Length.ToString() + } }; } @@ -1544,7 +1612,7 @@ private async Task WriteExhaustedHostsErrorAsync(RequestData request, HttpStatus } else if (!request.AsyncTriggered && request.Context != null) { - _logger.LogInformation("Response Status Code: {StatusCode} for request {Guid}", statusCode, request.Guid); + _logger.LogInformation("Response Status Code: '{StatusCode}' for request {Guid}", statusCode, request.Guid); request.Context.Response.StatusCode = (int)statusCode; request.Context.Response.KeepAlive = false; From 0de79980dee64d60b3475cc91e14e20518196d09 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 16:43:47 -0400 Subject: [PATCH 14/18] use [] rather than .Add to prevent exceptions --- src/SimpleL7Proxy/Events/ProxyEvent.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SimpleL7Proxy/Events/ProxyEvent.cs b/src/SimpleL7Proxy/Events/ProxyEvent.cs index c0be099e..3b817816 100644 --- a/src/SimpleL7Proxy/Events/ProxyEvent.cs +++ b/src/SimpleL7Proxy/Events/ProxyEvent.cs @@ -134,7 +134,6 @@ public void SendEvent() { try { - bool logToConsole = ConAttr.IsEnabled(Type); bool logToEventClient = EventAttr.IsEnabled(Type); bool logToAI = AIAttr.IsEnabled(Type); @@ -179,6 +178,7 @@ public void SendEvent() catch (Exception ex) { Console.Error.WriteLine($"Error sending telemetry: {ex.Message}"); + Console.WriteLine("Stack: " + ex.StackTrace); } } @@ -307,9 +307,8 @@ private void TrackRequest() foreach (var kvp in this) { - requestTelemetry.Properties.Add(kvp); + requestTelemetry.Properties[kvp.Key] = kvp.Value; } - Console.WriteLine("Logging request"); _telemetryClient?.TrackRequest(requestTelemetry); } From a4e1f29fd936260e0e2815bb593f01bfa5b10ec0 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 16:44:18 -0400 Subject: [PATCH 15/18] use constants.Latency --- src/SimpleL7Proxy/Config/ProxyConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SimpleL7Proxy/Config/ProxyConfig.cs b/src/SimpleL7Proxy/Config/ProxyConfig.cs index 40cc902f..deacc9ff 100644 --- a/src/SimpleL7Proxy/Config/ProxyConfig.cs +++ b/src/SimpleL7Proxy/Config/ProxyConfig.cs @@ -34,7 +34,7 @@ public class ProxyConfig // ── Load Balancing ── [ConfigOption("LoadBalancing:Mode")] - public string LoadBalanceMode { get; set; } = "latency"; // "latency", "roundrobin", "random" + public string LoadBalanceMode { get; set; } = Constants.Latency; // ── Server ── [ConfigOption("Server:IterationMode")] From 454d8494dc6c5ac19d32fa78ef93ca61d0f668f4 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 16:44:54 -0400 Subject: [PATCH 16/18] cleanup startup console messages, dont invalidate shared iterator unless latency changed --- src/SimpleL7Proxy/Backend/Backends.cs | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/SimpleL7Proxy/Backend/Backends.cs b/src/SimpleL7Proxy/Backend/Backends.cs index 5dadbccc..752762b7 100644 --- a/src/SimpleL7Proxy/Backend/Backends.cs +++ b/src/SimpleL7Proxy/Backend/Backends.cs @@ -44,6 +44,7 @@ public class Backends : IBackendService private readonly IEventClient _eventClient; private readonly ISharedIteratorRegistry? _sharedIteratorRegistry; + private List _lastLatencyOrder = new(); // Reusable ProxyEvent instances for backend poller to reduce allocations private readonly ProxyEvent _statusEvent = new ProxyEvent(25); // 4 fixed (Timestamp, LoadBalanceMode, ActiveHostsCount, SuccessRate) + 7*N per host (assumes ~3 hosts) @@ -98,13 +99,13 @@ public Backends( // Hosts are staged and activated by ConfigBootstrapper.RegisterBackends - _logger.LogDebug("[INIT] Backends service starting"); + _logger.LogDebug("[INIT] Backend health-polling service created"); } public Task Stop() { - _logger.LogInformation("[SHUTDOWN] ⏹ Backend stopping"); + _logger.LogInformation("[SHUTDOWN] ⏹ Backend health poller stopping"); _cancellationTokenSource.Cancel(); return PollerTask ?? Task.CompletedTask; @@ -119,7 +120,7 @@ public void Start() { if (task.Exception != null) { - _logger.LogError(task.Exception, "[BACKENDS-POLLER-ERROR] {Time} {Exception}", DateTime.UtcNow, task.Exception.Flatten()); + _logger.LogError(task.Exception, "[SERVICE] ✗ Backend health poller task faulted {Time} {Exception}", DateTime.UtcNow, task.Exception.Flatten()); } }, TaskContinuationOptions.OnlyOnFaulted); } @@ -176,7 +177,7 @@ private async Task Run() { var intervalTime = TimeSpan.FromMilliseconds(_options.PollInterval).ToString(@"hh\:mm\:ss"); var timeoutTime = TimeSpan.FromMilliseconds(_options.PollTimeout).ToString(@"hh\:mm\:ss\.fff"); - _logger.LogInformation($"[SERVICE] ✓ Backend Poller starting - Interval: {intervalTime} | Success Rate: {_successRate} | Timeout: {timeoutTime}"); + _logger.LogInformation($"[SERVICE] ✓ Backend health poller started — polling every {intervalTime}, healthy threshold: {_successRate}, probe timeout: {timeoutTime}"); _client.Timeout = TimeSpan.FromMilliseconds(_options.PollTimeout); @@ -198,23 +199,23 @@ private async Task Run() } catch (OperationCanceledException) { - _logger.LogInformation("[SHUTDOWN] ⏹ Backend Poller stopping"); + _logger.LogInformation("[SHUTDOWN] ⏹ Backend health poller cancelled — draining"); break; } catch (Exception e) { - _logger.LogError(e, "[BACKENDS] Unexpected error in poller loop - continuing"); + _logger.LogError(e, "[SERVICE] ⚠ Backend health poller hit an error — retrying next cycle"); } } } - _logger.LogInformation("[SHUTDOWN] ✓ Backend Poller stopped"); + _logger.LogInformation("[SHUTDOWN] ✓ Backend health poller stopped"); } } catch (Exception ex) { // Catch any unhandled exceptions to prevent background service from crashing the host - _logger.LogError(ex, "[BACKENDS] CRITICAL: Unhandled exception in backend poller - service stopping"); + _logger.LogError(ex, "[SERVICE] ✗ Backend health poller crashed — service stopping"); throw; // Rethrow to let the host know the background service failed, but at least we logged it } } @@ -317,6 +318,7 @@ private async Task GetHostStatus(BaseHostHealth host, HttpClient client) } // If the response is successful, add the host to the active hosts + _probeEvent["Success"] = response.IsSuccessStatusCode.ToString(); return response.IsSuccessStatusCode; } finally @@ -401,17 +403,22 @@ private void FilterActiveHosts() _activeHosts = newActiveHosts; - // Invalidate iterator cache only if hosts actually changed if (hostsChanged) { InvalidateIteratorCache(); + _lastLatencyOrder = newActiveHosts.OrderBy(h => h.CalculatedAverageLatency).Select(h => h.guid).ToList(); } - else + else if (string.Equals(_options.LoadBalanceMode, Constants.Latency, StringComparison.OrdinalIgnoreCase)) { - // Even if host list didn't change, invalidate shared iterators - // so they get fresh latency ordering on next request - _sharedIteratorRegistry?.InvalidateAll(); + // Only invalidate shared iterators when the latency-based ordering actually changed + var newOrder = newActiveHosts.OrderBy(h => h.CalculatedAverageLatency).Select(h => h.guid).ToList(); + if (!newOrder.SequenceEqual(_lastLatencyOrder)) + { + _sharedIteratorRegistry?.InvalidateAll(); + _lastLatencyOrder = newOrder; + } } + // For roundrobin/random modes with unchanged host list, no invalidation needed } /// @@ -451,7 +458,7 @@ private void DisplayHostStatus() if (_backendHosts != null) foreach (var host in _backendHosts.OrderBy(h => h.AverageLatency())) { - statusIndicator = host.SuccessRate() >= _successRate ? "Good " : "Errors"; + statusIndicator = host.SuccessRate() >= _successRate ? "✓ Active" : "✗ Below threshold"; var roundedLatency = Math.Round(host.AverageLatency(), 3); var successRatePercentage = Math.Round(host.SuccessRate() * 100, 2); var hoststatus = host.GetStatus(out int calls, out int errors, out double average); From 5c4fadc18c9aac0a70ead617832d9140d33ede45 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 16:45:58 -0400 Subject: [PATCH 17/18] rename Revision => Replica, console message cleanup --- src/SimpleL7Proxy/Events/CommonEventHeaders.cs | 2 +- src/SimpleL7Proxy/Events/EventHubClient.cs | 2 +- src/SimpleL7Proxy/Program.cs | 6 +++--- .../Proxy/StreamProcessor/DefaultStreamProcessor.cs | 3 --- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/SimpleL7Proxy/Events/CommonEventHeaders.cs b/src/SimpleL7Proxy/Events/CommonEventHeaders.cs index f728424e..c4b13043 100644 --- a/src/SimpleL7Proxy/Events/CommonEventHeaders.cs +++ b/src/SimpleL7Proxy/Events/CommonEventHeaders.cs @@ -10,7 +10,7 @@ public class CommonEventHeaders(IOptions options) : ICommonEventDat new Dictionary { ["Ver"] = Constants.VERSION, - ["Revision"] = options.Value.Revision, + ["Replica"] = options.Value.ReplicaName, ["ContainerApp"] = options.Value.ContainerApp }.ToFrozenDictionary(); diff --git a/src/SimpleL7Proxy/Events/EventHubClient.cs b/src/SimpleL7Proxy/Events/EventHubClient.cs index 37d6337d..22b97d01 100644 --- a/src/SimpleL7Proxy/Events/EventHubClient.cs +++ b/src/SimpleL7Proxy/Events/EventHubClient.cs @@ -90,7 +90,7 @@ public async Task StartAsync(CancellationToken cancellationToken) { _composite.Add(this); var ConnString = string.IsNullOrEmpty(_config.ConnectionString) ? "Not Set" : "Set"; - _logger.LogCritical("[SERVICE] ✓ EventHub Client started: ConnectionString: {ConnString}, Name: {EventHubName}, Namespace: {EventHubNamespace}", ConnString, _config.EventHubName, _config.EventHubNamespace); + _logger.LogInformation("[SERVICE] ✓ EventHub Client started: ConnectionString: {ConnString}, Name: {EventHubName}, Namespace: {EventHubNamespace}", ConnString, _config.EventHubName, _config.EventHubNamespace); writerTask = Task.Run(() => EventWriter(workerCancelToken), workerCancelToken); } diff --git a/src/SimpleL7Proxy/Program.cs b/src/SimpleL7Proxy/Program.cs index f0fdb527..8c5f63b3 100644 --- a/src/SimpleL7Proxy/Program.cs +++ b/src/SimpleL7Proxy/Program.cs @@ -209,21 +209,21 @@ private static void ConfigureAppInsights(IServiceCollection services, ProxyConfi var aiConnectionString = options.AppInsightsConnectionString; if (!string.IsNullOrEmpty(aiConnectionString)) { - // Register Application Insights + // Register Application Insights — also adds the ILogger → App Insights provider, + // so all ILogger output flows to both console and App Insights once the host starts. services.AddApplicationInsightsTelemetryWorkerService(options => { options.ConnectionString = aiConnectionString; options.EnableAdaptiveSampling = false; // Disable sampling to ensure all your custom telemetry is sent }); - // Configure telemetry to filter out duplicate logs + // Filter out duplicate request telemetry services.Configure(config => { config.TelemetryProcessorChainBuilder.Use(next => new RequestFilterTelemetryProcessor(next)); config.TelemetryProcessorChainBuilder.Build(); }); - // Note: logging isn't fully configured yet startupLogger.LogInformation("[INIT] ✓ AppInsights initialized with custom request tracking"); } } diff --git a/src/SimpleL7Proxy/Proxy/StreamProcessor/DefaultStreamProcessor.cs b/src/SimpleL7Proxy/Proxy/StreamProcessor/DefaultStreamProcessor.cs index 1afa9d54..870e12d8 100644 --- a/src/SimpleL7Proxy/Proxy/StreamProcessor/DefaultStreamProcessor.cs +++ b/src/SimpleL7Proxy/Proxy/StreamProcessor/DefaultStreamProcessor.cs @@ -14,9 +14,6 @@ public class DefaultStreamProcessor : BaseStreamProcessor /// public override async Task CopyToAsync(System.Net.Http.HttpContent sourceContent, Stream outputStream) { - // if (cancellationToken != null) - // await sourceContent.CopyToAsync(outputStream, cancellationToken.Value).ConfigureAwait(false); - // else await sourceContent.CopyToAsync(outputStream).ConfigureAwait(false); } From 14790119ba7be5c1d49e1b1eae3a5be91d47a2d6 Mon Sep 17 00:00:00 2001 From: Nagendra Mishr Date: Wed, 1 Apr 2026 17:04:57 -0400 Subject: [PATCH 18/18] console cleanup --- ReleaseNotes/version2.2.md | 11 +++++++++++ src/SimpleL7Proxy/Program.cs | 1 + src/SimpleL7Proxy/server.cs | 15 +++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ReleaseNotes/version2.2.md b/ReleaseNotes/version2.2.md index 7898d974..8eee1a23 100644 --- a/ReleaseNotes/version2.2.md +++ b/ReleaseNotes/version2.2.md @@ -1,5 +1,16 @@ # Release Notes # +2.2.10.5-d1 + +* Bug Fix: Start App Insights with app config setting +* Bug Fix: environment defaults and BackendOptions were getting mixedup +* Bug Fix: Probes not getting logged +* Bug Fix: Environment variables not being defaulted to +* Bug Fix: Make sure program does not exit until shutdown completes +* Make shared_iterator use either singlePass or maxAttempts, default to singlePass. +* Don't invalidate shared iterators unless using latency load balancer and the order changed +* In the case of a 404, use the same response path as a normal response + 2.2.10.4 Proxy: diff --git a/src/SimpleL7Proxy/Program.cs b/src/SimpleL7Proxy/Program.cs index 8c5f63b3..8a5a1f26 100644 --- a/src/SimpleL7Proxy/Program.cs +++ b/src/SimpleL7Proxy/Program.cs @@ -202,6 +202,7 @@ private static void ConfigureLogging(ILoggingBuilder logging) logging.AddConsole(options => options.FormatterName = "custom"); logging.AddConsoleFormatter(); logging.SetMinimumLevel(logLevel); + logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning); } private static void ConfigureAppInsights(IServiceCollection services, ProxyConfig options, ILogger startupLogger) diff --git a/src/SimpleL7Proxy/server.cs b/src/SimpleL7Proxy/server.cs index b1dac01f..a06de143 100644 --- a/src/SimpleL7Proxy/server.cs +++ b/src/SimpleL7Proxy/server.cs @@ -139,8 +139,7 @@ public Server( // _probeDataPool[i] = new ProbeData(); // } - var timeoutTime = TimeSpan.FromMilliseconds(_options.Timeout).ToString(@"hh\:mm\:ss\.fff"); - _logger.LogInformation($"[CONFIG] Server configuration - Port: {_options.Port} | Timeout: {timeoutTime} | Workers: {_options.Workers}"); + // Server config is logged at startup in ExecuteAsync alongside the listening message } public void InitVars() @@ -209,6 +208,7 @@ public async Task StopProbes(CancellationToken cancellationToken) protected override Task ExecuteAsync(CancellationToken cancellationToken) { Task backendStartTask; + string serverInfo = $"Port: {_options.Port}, Timeout: {_options.Timeout}ms, Workers: {_options.Workers}, LoadBalanceMode: {_options.LoadBalanceMode}, ValidateAuthAppID: {_options.ValidateAuthAppID}, AsyncModeEnabled: {_options.AsyncModeEnabled}"; try { _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -218,21 +218,20 @@ protected override Task ExecuteAsync(CancellationToken cancellationToken) backendStartTask = _backends.WaitForStartup(20); _httpListener.Start(); - _logger.LogInformation($"[SERVICE] ✓ Server listening on port {_options?.Port}"); + var timeoutTime = TimeSpan.FromMilliseconds(_options.Timeout).ToString(@"hh\:mm\:ss\.fff"); + _logger.LogInformation($"[SERVICE] ✓ Server listening: {serverInfo}"); // Additional setup or async start operations can be performed here _requestsQueue.StartSignaler(cancellationToken); } catch (HttpListenerException ex) { - // Handle specific errors, e.g., port already in use - _staticEvent.WriteOutput($"Failed to start HttpListener: {ex.Message}"); - throw new Exception("Failed to start the server due to an HttpListener exception.", ex); + _logger.LogError(ex, "[SERVICE] ✗ HttpListener failed to start {info}, {Message}", serverInfo, ex.Message); + throw new Exception($"Failed to start the server on port {_options.Port}.", ex); } catch (Exception ex) { - // Handle other potential errors - _staticEvent.WriteErrorOutput($"An error occurred: {ex.Message}"); + _logger.LogError(ex, "[SERVICE] ✗ Server failed to start — {info}, {Message}", serverInfo, ex.Message); throw new Exception("An error occurred while starting the server.", ex); }