216 lines
6.8 KiB
C#
216 lines
6.8 KiB
C#
using Microsoft.AspNetCore.Diagnostics;
|
||
using Microsoft.AspNetCore.HttpOverrides;
|
||
using OpenTelemetry.Logs;
|
||
using OpenTelemetry.Metrics;
|
||
using OpenTelemetry.Resources;
|
||
using OpenTelemetry.Trace;
|
||
using Serilog;
|
||
using System.Diagnostics;
|
||
using System.Threading.RateLimiting;
|
||
using TakeoutSaaS.ApiGateway.Configuration;
|
||
|
||
const string CorsPolicyName = "GatewayCors";
|
||
|
||
// 1. 创建构建器并配置 Serilog
|
||
var builder = WebApplication.CreateBuilder(args);
|
||
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
|
||
{
|
||
loggerConfiguration
|
||
.ReadFrom.Configuration(context.Configuration)
|
||
.ReadFrom.Services(services)
|
||
.Enrich.FromLogContext();
|
||
});
|
||
|
||
// 2. 配置 YARP 反向代理
|
||
builder.Services.AddReverseProxy()
|
||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||
|
||
// 3. 转发头部配置
|
||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||
{
|
||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||
options.KnownIPNetworks.Clear();
|
||
options.KnownProxies.Clear();
|
||
});
|
||
|
||
// 4. 配置 CORS
|
||
builder.Services.AddCors(options =>
|
||
{
|
||
options.AddPolicy(CorsPolicyName, policy =>
|
||
{
|
||
policy.AllowAnyOrigin()
|
||
.AllowAnyHeader()
|
||
.AllowAnyMethod();
|
||
});
|
||
});
|
||
|
||
// 5. 配置网关限流
|
||
builder.Services.Configure<GatewayRateLimitOptions>(builder.Configuration.GetSection("Gateway:RateLimiting"));
|
||
var rateLimitOptions = builder.Configuration.GetSection("Gateway:RateLimiting").Get<GatewayRateLimitOptions>() ?? new();
|
||
|
||
if (rateLimitOptions.Enabled)
|
||
{
|
||
builder.Services.AddRateLimiter(options =>
|
||
{
|
||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
|
||
{
|
||
var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||
return RateLimitPartition.GetFixedWindowLimiter(remoteIp, _ => new FixedWindowRateLimiterOptions
|
||
{
|
||
PermitLimit = Math.Max(1, rateLimitOptions.PermitLimit),
|
||
Window = TimeSpan.FromSeconds(Math.Max(1, rateLimitOptions.WindowSeconds)),
|
||
QueueLimit = Math.Max(0, rateLimitOptions.QueueLimit),
|
||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// 6. 配置 OpenTelemetry
|
||
var otelOptions = builder.Configuration.GetSection("OpenTelemetry").Get<GatewayOpenTelemetryOptions>() ?? new();
|
||
if (otelOptions.Enabled)
|
||
{
|
||
builder.Services.AddOpenTelemetry()
|
||
// 1. 配置统一的 Resource,便于追踪定位。
|
||
.ConfigureResource(resource => resource.AddService(otelOptions.ServiceName ?? "TakeoutSaaS.ApiGateway"))
|
||
.WithMetrics(metrics =>
|
||
{
|
||
metrics.AddAspNetCoreInstrumentation()
|
||
.AddHttpClientInstrumentation()
|
||
.AddRuntimeInstrumentation();
|
||
|
||
if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
|
||
{
|
||
metrics.AddOtlpExporter(options =>
|
||
{
|
||
options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
|
||
});
|
||
}
|
||
})
|
||
.WithTracing(tracing =>
|
||
{
|
||
tracing.AddAspNetCoreInstrumentation(options =>
|
||
{
|
||
options.RecordException = true;
|
||
})
|
||
.AddHttpClientInstrumentation();
|
||
|
||
if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
|
||
{
|
||
tracing.AddOtlpExporter(options =>
|
||
{
|
||
options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
|
||
});
|
||
}
|
||
});
|
||
|
||
builder.Logging.AddOpenTelemetry(logging =>
|
||
{
|
||
logging.IncludeScopes = true;
|
||
logging.ParseStateValues = true;
|
||
if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
|
||
{
|
||
logging.AddOtlpExporter(options =>
|
||
{
|
||
options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// 7. 构建应用
|
||
var app = builder.Build();
|
||
|
||
// 8. 转发头中间件
|
||
app.UseForwardedHeaders();
|
||
|
||
// 9. 全局异常处理中间件
|
||
app.UseExceptionHandler(errorApp =>
|
||
{
|
||
// 1. 捕获所有未处理异常并返回统一结构。
|
||
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);
|
||
});
|
||
});
|
||
|
||
// 10. 请求日志
|
||
app.UseSerilogRequestLogging(options =>
|
||
{
|
||
options.MessageTemplate = "网关请求 {RequestMethod} {RequestPath} => {StatusCode} 用时 {Elapsed:0.000} 秒";
|
||
options.GetLevel = (httpContext, elapsed, ex) => ex is not null ? Serilog.Events.LogEventLevel.Error : Serilog.Events.LogEventLevel.Information;
|
||
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
|
||
{
|
||
diagnosticContext.Set("TraceId", Activity.Current?.Id ?? httpContext.TraceIdentifier);
|
||
diagnosticContext.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString());
|
||
};
|
||
});
|
||
|
||
// 11. CORS 与限流
|
||
app.UseCors(CorsPolicyName);
|
||
|
||
if (rateLimitOptions.Enabled)
|
||
{
|
||
app.UseRateLimiter();
|
||
}
|
||
|
||
// 12. 透传请求头并保证 Trace
|
||
app.Use(async (context, next) =>
|
||
{
|
||
// 1. 确保请求拥有可追踪的 ID。
|
||
if (!context.Request.Headers.ContainsKey("X-Request-Id"))
|
||
{
|
||
context.Request.Headers["X-Request-Id"] = Activity.Current?.Id ?? Guid.NewGuid().ToString("N");
|
||
}
|
||
|
||
// 2. 透传租户等标识头,方便下游继续使用。
|
||
var tenantId = context.Request.Headers["X-Tenant-Id"];
|
||
if (!string.IsNullOrWhiteSpace(tenantId))
|
||
{
|
||
context.Request.Headers["X-Tenant-Id"] = tenantId;
|
||
}
|
||
|
||
var tenantCode = context.Request.Headers["X-Tenant-Code"];
|
||
if (!string.IsNullOrWhiteSpace(tenantCode))
|
||
{
|
||
context.Request.Headers["X-Tenant-Code"] = tenantCode;
|
||
}
|
||
|
||
await next(context);
|
||
});
|
||
|
||
// 13. 映射反向代理与健康接口
|
||
app.MapReverseProxy();
|
||
|
||
app.MapGet("/", () => Results.Json(new
|
||
{
|
||
Service = "TakeoutSaaS.ApiGateway",
|
||
Status = "OK",
|
||
Timestamp = DateTimeOffset.UtcNow
|
||
}));
|
||
|
||
app.MapGet("/healthz", () => Results.Json(new
|
||
{
|
||
Service = "TakeoutSaaS.ApiGateway",
|
||
Status = "Healthy",
|
||
Timestamp = DateTimeOffset.UtcNow
|
||
}));
|
||
|
||
app.Run();
|