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(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(builder.Configuration.GetSection("Gateway:RateLimiting")); var rateLimitOptions = builder.Configuration.GetSection("Gateway:RateLimiting").Get() ?? new(); if (rateLimitOptions.Enabled) { builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.GlobalLimiter = PartitionedRateLimiter.Create(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() ?? 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(); 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().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();