Files
2026-05-11 08:17:27 +02:00

178 lines
6.5 KiB
C#

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<string>("ServerConf:RoutePath") ?? "/api/IOB";
_forwarderConfig = new ForwarderRequestConfig
{
ActivityTimeout = TimeSpan.FromSeconds(60),
// Parse della versione (es. "1.1")
Version = Version.Parse(_config.GetValue<string>("ServerConf:HttpVersion") ?? "1.1"),
// Policy per la versione
VersionPolicy = _config.GetValue<HttpVersionPolicy?>("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
}
}