using NLog; using System.Diagnostics; using Yarp.ReverseProxy.Forwarder; namespace MP.RIOC.Services { public class RouteManager { #region Public Constructors public RouteManager( IHttpForwarder forwarder, HttpMessageInvoker httpClientInvoker, PreserveBodyTransformer transformer, RouteStatsManager stats, IWeightProvider weightProvider, IConfiguration config) { _forwarder = forwarder; _httpClientInvoker = httpClientInvoker; _transformer = transformer; _stats = stats; _weightProvider = weightProvider; _config = config; _routePath = _config.GetValue("ServerConf:RoutePath") ?? "/api/IOB"; _forwarderConfig = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(60), // Parse della versione (es. "1.1") Version = Version.Parse(_config.GetValue("ServerConf:HttpVersion") ?? "1.1"), // Policy per la versione VersionPolicy = _config.GetValue("ServerConf:HttpVersionPolicy") ?? HttpVersionPolicy.RequestVersionExact }; } #endregion Public Constructors #region Public Methods public async Task HandleAsync(HttpContext context) { var sw = Stopwatch.StartNew(); var routePrefix = new PathString(_routePath); // 1. ESTRAZIONE PATH SEMPLIFICATA // Se la richiesta è /api/IOB/metodo, 'remaining' sarà /metodo if (!context.Request.Path.StartsWithSegments(routePrefix, out var remaining)) { // Se non inizia con il prefisso, è una chiamata errata al router context.Response.StatusCode = 404; return; } // Togliamo lo slash iniziale per avere il metodo pulito string relativePath = remaining.Value.TrimStart('/'); // 2. LOGICA METODO / ID (Per statistiche) string metodo = "/"; string id = "ALL"; if (!string.IsNullOrEmpty(relativePath)) { var pathOnly = relativePath.Split('?')[0]; var parts = pathOnly.Split('/', StringSplitOptions.RemoveEmptyEntries); if (parts.Length > 0) metodo = parts[0]; if (parts.Length > 1) id = parts[1]; } // 3. DECISIONE TARGET var (oldW, newW) = _weightProvider.GetWeightsFor(metodo); var pickNew = DecideByWeights(oldW, newW); var targetLabel = pickNew ? "IOC" : "IO"; string sKey = $"{targetLabel}|{metodo}|{id}"; var destBase = pickNew ? _config["ServerConf:NewApiUrl"] : _config["ServerConf:OldApiUrl"]; if (string.IsNullOrEmpty(destBase)) { context.Response.StatusCode = 502; await context.Response.WriteAsync("Destination not configured"); return; } // Verifica destinazione // per evitare il "doppio slash" (es. .../api/IOB//metodo) if (!destBase.EndsWith("/")) destBase += "/"; // avvio registrazione statistice _stats.Record(sKey); // 4. PREPARAZIONE FORWARDING var originalPath = context.Request.Path; var originalPathBase = context.Request.PathBase; try { context.Request.Path = new PathString("/" + relativePath); context.Request.PathBase = PathString.Empty; // ESECUZIONE FORWARDING var error = await _forwarder.SendAsync(context, destBase, _httpClientInvoker, _forwarderConfig, HttpTransformer.Default, context.RequestAborted); // commento transformer custom //var error = await _forwarder.SendAsync(context, destBase, _httpClientInvoker, _forwarderConfig, _transformer, context.RequestAborted); sw.Stop(); _stats.RecordDuration(sKey, sw.Elapsed); // REGISTRAZIONE STATUS CODE (Sempre, se disponibile) _stats.RecordStatusCode(sKey, context.Response.StatusCode); if (error != ForwarderError.None) { var feat = context.GetForwarderErrorFeature(); var errorMsg = feat?.Exception?.Message ?? error.ToString(); // REGISTRAZIONE ERRORE DETTAGLIATO _stats.RecordError(sKey, errorMsg); Log.Error(feat?.Exception, "Forwarder error to {DestBase} for {Method}: {Msg}", destBase, metodo, errorMsg); if (!context.Response.HasStarted) { context.Response.StatusCode = 502; await context.Response.WriteAsync($"Forward error: {errorMsg}"); } } } catch (Exception ex) { sw.Stop(); _stats.RecordError(sKey, ex.Message); Log.Fatal(ex, "Critical error in RouteManager"); } finally { context.Request.Path = originalPath; context.Request.PathBase = originalPathBase; } } #endregion Public Methods #region Private Fields private static Logger Log = LogManager.GetCurrentClassLogger(); private readonly IConfiguration _config; private readonly IHttpForwarder _forwarder; private readonly ForwarderRequestConfig _forwarderConfig; private readonly HttpMessageInvoker _httpClientInvoker; private readonly RouteStatsManager _stats; private readonly PreserveBodyTransformer _transformer; private readonly IWeightProvider _weightProvider; private string _routePath = ""; #endregion Private Fields #region Private Methods private bool DecideByWeights(int oldW, int newW) { bool result = false; // se entrambi zero -> prefer legacy var total = oldW + newW; if (total <= 0) { result = false; } else { var rnd = Random.Shared.NextDouble(); // 0..1 result = rnd < (double)newW / total; } return result; } #endregion Private Methods } }