feat: finalize core modules and gateway
This commit is contained in:
@@ -1,67 +1,110 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var routes = new[]
|
||||
{
|
||||
new RouteConfig
|
||||
{
|
||||
RouteId = "admin-route",
|
||||
ClusterId = "admin",
|
||||
Match = new() { Path = "/api/admin/{**catch-all}" }
|
||||
},
|
||||
new RouteConfig
|
||||
{
|
||||
RouteId = "mini-route",
|
||||
ClusterId = "mini",
|
||||
Match = new() { Path = "/api/mini/{**catch-all}" }
|
||||
},
|
||||
new RouteConfig
|
||||
{
|
||||
RouteId = "user-route",
|
||||
ClusterId = "user",
|
||||
Match = new() { Path = "/api/user/{**catch-all}" }
|
||||
}
|
||||
};
|
||||
|
||||
var clusters = new[]
|
||||
{
|
||||
new ClusterConfig
|
||||
{
|
||||
ClusterId = "admin",
|
||||
Destinations = new Dictionary<string, DestinationConfig>
|
||||
{
|
||||
["d1"] = new() { Address = "http://localhost:5001/" }
|
||||
}
|
||||
},
|
||||
new ClusterConfig
|
||||
{
|
||||
ClusterId = "mini",
|
||||
Destinations = new Dictionary<string, DestinationConfig>
|
||||
{
|
||||
["d1"] = new() { Address = "http://localhost:5002/" }
|
||||
}
|
||||
},
|
||||
new ClusterConfig
|
||||
{
|
||||
ClusterId = "user",
|
||||
Destinations = new Dictionary<string, DestinationConfig>
|
||||
{
|
||||
["d1"] = new() { Address = "http://localhost:5003/" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
builder.Services.AddReverseProxy()
|
||||
.LoadFromMemory(routes, clusters);
|
||||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
||||
{
|
||||
const string partitionKey = "proxy-default";
|
||||
return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 100,
|
||||
Window = TimeSpan.FromSeconds(1),
|
||||
QueueLimit = 50,
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapReverseProxy();
|
||||
app.UseExceptionHandler(errorApp =>
|
||||
{
|
||||
errorApp.Run(async context =>
|
||||
{
|
||||
var feature = context.Features.Get<IExceptionHandlerFeature>();
|
||||
var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var payload = new
|
||||
{
|
||||
success = false,
|
||||
code = 500,
|
||||
message = "Gateway internal error",
|
||||
traceId
|
||||
};
|
||||
|
||||
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("Gateway");
|
||||
logger.LogError(feature?.Error, "网关异常 {TraceId}", traceId);
|
||||
await context.Response.WriteAsJsonAsync(payload, cancellationToken: context.RequestAborted);
|
||||
});
|
||||
});
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("Gateway");
|
||||
var start = DateTime.UtcNow;
|
||||
await next(context);
|
||||
var elapsed = DateTime.UtcNow - start;
|
||||
logger.LogInformation("Gateway {Method} {Path} => {Status} ({Elapsed} ms)",
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
context.Response.StatusCode,
|
||||
(int)elapsed.TotalMilliseconds);
|
||||
});
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
// 确保存在请求 ID,便于上下游链路追踪。
|
||||
if (!context.Request.Headers.ContainsKey("X-Request-Id"))
|
||||
{
|
||||
context.Request.Headers["X-Request-Id"] = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
// 透传租户与认证头。
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"];
|
||||
var tenantCode = context.Request.Headers["X-Tenant-Code"];
|
||||
if (!string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
context.Request.Headers["X-Tenant-Id"] = tenantId;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
context.Request.Headers["X-Tenant-Code"] = tenantCode;
|
||||
}
|
||||
|
||||
await next(context);
|
||||
});
|
||||
|
||||
app.MapReverseProxy(proxyPipeline =>
|
||||
{
|
||||
proxyPipeline.Use(async (context, next) =>
|
||||
{
|
||||
await next().ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
|
||||
app.MapGet("/", () => Results.Json(new
|
||||
{
|
||||
Service = "TakeoutSaaS.ApiGateway",
|
||||
Status = "OK",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
}));
|
||||
|
||||
app.Run();
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"ReverseProxy": {
|
||||
"Routes": [
|
||||
{
|
||||
"RouteId": "admin-route",
|
||||
"ClusterId": "admin",
|
||||
"Match": { "Path": "/api/admin/{**catch-all}" }
|
||||
},
|
||||
{
|
||||
"RouteId": "mini-route",
|
||||
"ClusterId": "mini",
|
||||
"Match": { "Path": "/api/mini/{**catch-all}" }
|
||||
},
|
||||
{
|
||||
"RouteId": "user-route",
|
||||
"ClusterId": "user",
|
||||
"Match": { "Path": "/api/user/{**catch-all}" }
|
||||
}
|
||||
],
|
||||
"Clusters": {
|
||||
"admin": {
|
||||
"Destinations": {
|
||||
"d1": { "Address": "http://localhost:5001/" }
|
||||
}
|
||||
},
|
||||
"mini": {
|
||||
"Destinations": {
|
||||
"d1": { "Address": "http://localhost:5002/" }
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"Destinations": {
|
||||
"d1": { "Address": "http://localhost:5003/" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user