Files
TakeoutSaaS.Gateway/Program.cs
2026-01-29 05:25:02 +00:00

216 lines
6.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();