From 21facc3cd71fede1967c74491864288db8eaf131 Mon Sep 17 00:00:00 2001 From: thangchung Date: Mon, 9 Sep 2024 19:34:47 +0700 Subject: [PATCH 1/4] upgrade aspire and add trace-id --- Directory.Build.props | 1 + Directory.Packages.props | 156 +++++++++--------- app-host/CoffeeShop.AppHost.csproj | 4 +- app-host/Program.cs | 18 +- app-host/app1.http | 4 +- app-host/appsettings.json | 60 ------- coffeeshop-aspire.sln | 6 + product-api/UseCases/ItemTypesQuery.cs | 7 +- shared/CoffeeShop.Shared/Aspire/Extensions.cs | 11 +- yarp/CoffeeShop.Yarp/CoffeeShop.Yarp.csproj | 21 +++ yarp/CoffeeShop.Yarp/Program.cs | 39 +++++ .../Properties/launchSettings.json | 13 ++ .../appsettings.Development.json | 8 + yarp/CoffeeShop.Yarp/appsettings.json | 77 +++++++++ 14 files changed, 277 insertions(+), 148 deletions(-) create mode 100644 yarp/CoffeeShop.Yarp/CoffeeShop.Yarp.csproj create mode 100644 yarp/CoffeeShop.Yarp/Program.cs create mode 100644 yarp/CoffeeShop.Yarp/Properties/launchSettings.json create mode 100644 yarp/CoffeeShop.Yarp/appsettings.Development.json create mode 100644 yarp/CoffeeShop.Yarp/appsettings.json diff --git a/Directory.Build.props b/Directory.Build.props index b06c95a..d3aca69 100755 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,5 +6,6 @@ enable preview $(NoWarn);NU1507 + 1 \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index b5530dc..09e8c41 100755 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,78 +1,84 @@ - - true - true - 8.0.5 - 8.4.0 - 8.0.4 - 8.1.0 - 1.9.0 - 0.0.4 - 8.0.1 - 8.1.0 - 8.2.4 - 7.10.1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + 8.0.6 + 8.4.0 + 8.0.8 + 8.2.0 + 1.9.0 + 0.0.4 + 8.0.1 + 8.1.0 + 8.2.4 + 7.10.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-host/CoffeeShop.AppHost.csproj b/app-host/CoffeeShop.AppHost.csproj index 1a988c0..4b6523c 100644 --- a/app-host/CoffeeShop.AppHost.csproj +++ b/app-host/CoffeeShop.AppHost.csproj @@ -9,14 +9,13 @@ - - + @@ -32,6 +31,7 @@ + diff --git a/app-host/Program.cs b/app-host/Program.cs index 6be3f8d..29c901e 100755 --- a/app-host/Program.cs +++ b/app-host/Program.cs @@ -34,14 +34,22 @@ .WaitFor(rabbitmq) .WithSwaggerUI(); -var isHttps = builder.Configuration["DOTNET_LAUNCH_PROFILE"] == "https"; -var ingressPort = int.TryParse(builder.Configuration["Ingress:Port"], out var port) ? port : (int?)null; +//var isHttps = builder.Configuration["DOTNET_LAUNCH_PROFILE"] == "https"; +//var ingressPort = int.TryParse(builder.Configuration["Ingress:Port"], out var port) ? port : (int?)null; -builder.AddYarp("ingress") - .WithEndpoint(scheme: isHttps ? "https" : "http", port: ingressPort) +//builder.AddYarp("ingress") +// .WithEndpoint(scheme: isHttps ? "https" : "http", port: ingressPort) +// .WithReference(productApi) +// .WithReference(counterApi) +// .WithReference(orderSummaryApi) +// .LoadFromConfiguration("ReverseProxy"); + +builder.AddProject("yarp") .WithReference(productApi) .WithReference(counterApi) .WithReference(orderSummaryApi) - .LoadFromConfiguration("ReverseProxy"); + .WaitFor(productApi) + .WaitFor(counterApi) + .WaitFor(orderSummaryApi); builder.Build().Run(); diff --git a/app-host/app1.http b/app-host/app1.http index 8a600ff..aeeaaad 100644 --- a/app-host/app1.http +++ b/app-host/app1.http @@ -31,12 +31,14 @@ GET https://{{hostname}}/c/api/v1/fulfillment-orders content-type: application/json ### -GET https://{{hostname}}/p/api/v1/item-types +GET http://{{hostname}}/p/api/v1/item-types content-type: application/json +trace-id: "ff71c44ca162820de23468bb8c1b024b" ### GET https://{{hostname}}/p/api/v1/items-by-types/1,2,3 content-type: application/json +trace-id: "ff71c44ca162820de23468bb8c1b024b" ### @orderId = 8cf20000-8d12-00ff-acd0-08dc7cc27ccd diff --git a/app-host/appsettings.json b/app-host/appsettings.json index e088c3f..033be33 100755 --- a/app-host/appsettings.json +++ b/app-host/appsettings.json @@ -8,65 +8,5 @@ "AllowedHosts": "*", "Ingress": { "Port": 5000 - }, - "ReverseProxy": { - "Routes": { - "productapi": { - "ClusterId": "productapi", - "Match": { - "Path": "/p/{**remainder}" - }, - "Transforms": [ - { "PathRemovePrefix": "/p" }, - { "PathPrefix": "/" }, - { "RequestHeaderOriginalHost": "true" } - ] - }, - "counterApi": { - "ClusterId": "counterApi", - "Match": { - "Path": "/c/{**remainder}" - }, - "Transforms": [ - { "PathRemovePrefix": "/c" }, - { "PathPrefix": "/" }, - { "RequestHeaderOriginalHost": "true" } - ] - }, - "orderSummaryApi": { - "ClusterId": "orderSummaryApi", - "Match": { - "Path": "/audit/{**remainder}" - }, - "Transforms": [ - { "PathRemovePrefix": "/audit" }, - { "PathPrefix": "/" }, - { "RequestHeaderOriginalHost": "true" } - ] - } - }, - "Clusters": { - "productapi": { - "Destinations": { - "base_destination": { - "Address": "http://product-api" - } - } - }, - "counterApi": { - "Destinations": { - "base_destination": { - "Address": "http://counter-api" - } - } - }, - "orderSummaryApi": { - "Destinations": { - "base_destination": { - "Address": "http://order-summary" - } - } - } - } } } diff --git a/coffeeshop-aspire.sln b/coffeeshop-aspire.sln index 0d475c2..8069ecc 100755 --- a/coffeeshop-aspire.sln +++ b/coffeeshop-aspire.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.OrderSummary", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.CounterApi.IntegrationTests", "counter-api-tests\CoffeeShop.CounterApi.IntegrationTests.csproj", "{5DB71E8E-BF52-4918-895D-CD3BB373D6A0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoffeeShop.Yarp", "yarp\CoffeeShop.Yarp\CoffeeShop.Yarp.csproj", "{B64CA4AA-AE04-4C84-AADE-704FDA69F06E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {5DB71E8E-BF52-4918-895D-CD3BB373D6A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DB71E8E-BF52-4918-895D-CD3BB373D6A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {5DB71E8E-BF52-4918-895D-CD3BB373D6A0}.Release|Any CPU.Build.0 = Release|Any CPU + {B64CA4AA-AE04-4C84-AADE-704FDA69F06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B64CA4AA-AE04-4C84-AADE-704FDA69F06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B64CA4AA-AE04-4C84-AADE-704FDA69F06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B64CA4AA-AE04-4C84-AADE-704FDA69F06E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/product-api/UseCases/ItemTypesQuery.cs b/product-api/UseCases/ItemTypesQuery.cs index eb1a74c..d0facb8 100755 --- a/product-api/UseCases/ItemTypesQuery.cs +++ b/product-api/UseCases/ItemTypesQuery.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + using CoffeeShop.Shared.Endpoint; using ProductApi.Domain; @@ -21,12 +23,15 @@ internal class ItemTypesQueryValidator : AbstractValidator { } -internal class ItemTypesQueryHandler(ILogger logger) : IRequestHandler> +internal class ItemTypesQueryHandler(IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler> { public Task> Handle(ItemTypesQuery request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); + // todo: only for debugging purposes, remove later + var traceId = Activity.Current?.Id ?? httpContextAccessor?.HttpContext?.TraceIdentifier; + var results = new List { // beverages diff --git a/shared/CoffeeShop.Shared/Aspire/Extensions.cs b/shared/CoffeeShop.Shared/Aspire/Extensions.cs index cd53b42..cfb3fa0 100644 --- a/shared/CoffeeShop.Shared/Aspire/Extensions.cs +++ b/shared/CoffeeShop.Shared/Aspire/Extensions.cs @@ -24,6 +24,8 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu builder.AddDefaultHealthChecks(); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddServiceDiscovery(); builder.Services.ConfigureHttpClientDefaults(http => @@ -62,10 +64,11 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati .WithTracing(tracing => { tracing.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddSource(DiagnosticHeaders.DefaultListenerName) - .AddSource("Marten") - .AddSource(ActivitySourceProvider.DefaultSourceName); + .AddHttpClientInstrumentation() + .AddSource(DiagnosticHeaders.DefaultListenerName) + .AddSource("Marten") + .AddSource(ActivitySourceProvider.DefaultSourceName) + .AddSource("Yarp.ReverseProxy"); }); builder.AddOpenTelemetryExporters(); diff --git a/yarp/CoffeeShop.Yarp/CoffeeShop.Yarp.csproj b/yarp/CoffeeShop.Yarp/CoffeeShop.Yarp.csproj new file mode 100644 index 0000000..672a569 --- /dev/null +++ b/yarp/CoffeeShop.Yarp/CoffeeShop.Yarp.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/yarp/CoffeeShop.Yarp/Program.cs b/yarp/CoffeeShop.Yarp/Program.cs new file mode 100644 index 0000000..d897196 --- /dev/null +++ b/yarp/CoffeeShop.Yarp/Program.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) + .AddServiceDiscoveryDestinationResolver(); + +var app = builder.Build(); + +app.MapReverseProxy(); + +// todo: only for testing purposes, remove later +// https://stackoverflow.com/questions/70306118/set-traceid-on-activity +app.Use((context, next) => +{ + var traceIdFromProxy = context.Request.Headers["trace-id"].FirstOrDefault(); + if (traceIdFromProxy != null) + { + traceIdFromProxy = traceIdFromProxy.Replace("-", ""); + } + + if (context.Request.Headers.TryGetValue("traceparent", out var traceParent)) + { + if (context.Request.Headers.Remove("traceparent")) + { + var p = traceParent.SelectMany(x => x!.Split('-')).ToArray(); + + Activity.Current = new Activity("Yarp.ReverseProxy") + .SetParentId($"{p[0]}-{traceIdFromProxy}-{p[2]}-{p[3]}") + .Start(); + } + } + + return next(context); +}); + +app.Run(); diff --git a/yarp/CoffeeShop.Yarp/Properties/launchSettings.json b/yarp/CoffeeShop.Yarp/Properties/launchSettings.json new file mode 100644 index 0000000..52cfe19 --- /dev/null +++ b/yarp/CoffeeShop.Yarp/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/yarp/CoffeeShop.Yarp/appsettings.Development.json b/yarp/CoffeeShop.Yarp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/yarp/CoffeeShop.Yarp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/yarp/CoffeeShop.Yarp/appsettings.json b/yarp/CoffeeShop.Yarp/appsettings.json new file mode 100644 index 0000000..82ec688 --- /dev/null +++ b/yarp/CoffeeShop.Yarp/appsettings.json @@ -0,0 +1,77 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ReverseProxy": { + "Routes": { + "productapi": { + "ClusterId": "productapi", + "Match": { + "Path": "/p/{**remainder}" + }, + "Transforms": [ + { "PathRemovePrefix": "/p" }, + { "PathPrefix": "/" }, + { "RequestHeaderOriginalHost": "true" }, + { + "X-Forwarded": "Append", + "HeaderPrefix": "trace-id" + }, + { + "X-Forwarded": "Append", + "HeaderPrefix": "TraceId" + } + ] + }, + "counterApi": { + "ClusterId": "counterApi", + "Match": { + "Path": "/c/{**remainder}" + }, + "Transforms": [ + { "PathRemovePrefix": "/c" }, + { "PathPrefix": "/" }, + { "RequestHeaderOriginalHost": "true" } + ] + }, + "orderSummaryApi": { + "ClusterId": "orderSummaryApi", + "Match": { + "Path": "/audit/{**remainder}" + }, + "Transforms": [ + { "PathRemovePrefix": "/audit" }, + { "PathPrefix": "/" }, + { "RequestHeaderOriginalHost": "true" } + ] + } + }, + "Clusters": { + "productapi": { + "Destinations": { + "base_destination": { + "Address": "http://product-api" + } + } + }, + "counterApi": { + "Destinations": { + "base_destination": { + "Address": "http://counter-api" + } + } + }, + "orderSummaryApi": { + "Destinations": { + "base_destination": { + "Address": "http://order-summary" + } + } + } + } + } +} From 8bb1c7f3dde5f9d756cb90c67e5d3e5e9edb83b0 Mon Sep 17 00:00:00 2001 From: thangchung Date: Wed, 11 Sep 2024 19:16:05 +0700 Subject: [PATCH 2/4] updated --- app-host/app1.http | 2 +- yarp/CoffeeShop.Yarp/Program.cs | 60 +++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/app-host/app1.http b/app-host/app1.http index aeeaaad..92370f9 100644 --- a/app-host/app1.http +++ b/app-host/app1.http @@ -33,7 +33,7 @@ content-type: application/json ### GET http://{{hostname}}/p/api/v1/item-types content-type: application/json -trace-id: "ff71c44ca162820de23468bb8c1b024b" +trace-id: "{{$guid}}" ### GET https://{{hostname}}/p/api/v1/items-by-types/1,2,3 diff --git a/yarp/CoffeeShop.Yarp/Program.cs b/yarp/CoffeeShop.Yarp/Program.cs index d897196..56a2d96 100644 --- a/yarp/CoffeeShop.Yarp/Program.cs +++ b/yarp/CoffeeShop.Yarp/Program.cs @@ -1,39 +1,49 @@ using System.Diagnostics; +using Yarp.ReverseProxy.Transforms; + var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) + .AddTransforms(b => + { + // todo: only for testing purposes, remove later + // https://stackoverflow.com/questions/70306118/set-traceid-on-activity + b.AddRequestTransform(async transformContext => + { + var context = transformContext.HttpContext; + var traceIdFromProxy = context.Request.Headers["trace-id"].FirstOrDefault()?.Trim('"'); + if (Guid.TryParse(traceIdFromProxy, out var parsedGuid)) + { + traceIdFromProxy = parsedGuid.ToString("N"); + } + else + { + // Handle the case where the traceId is not a valid GUID + traceIdFromProxy = Guid.NewGuid().ToString("N"); + } + + if (context.Request.Headers.TryGetValue("traceparent", out var traceParent)) + { + if (context.Request.Headers.Remove("traceparent")) + { + var p = traceParent.SelectMany(x => x!.Split('-')).ToArray(); + + Activity.Current = new Activity("Yarp.ReverseProxy") + .SetParentId($"{p[0]}-{traceIdFromProxy}-{p[2]}-{p[3]}") + .Start(); + } + } + + await ValueTask.CompletedTask; + }); + }) .AddServiceDiscoveryDestinationResolver(); var app = builder.Build(); app.MapReverseProxy(); -// todo: only for testing purposes, remove later -// https://stackoverflow.com/questions/70306118/set-traceid-on-activity -app.Use((context, next) => -{ - var traceIdFromProxy = context.Request.Headers["trace-id"].FirstOrDefault(); - if (traceIdFromProxy != null) - { - traceIdFromProxy = traceIdFromProxy.Replace("-", ""); - } - - if (context.Request.Headers.TryGetValue("traceparent", out var traceParent)) - { - if (context.Request.Headers.Remove("traceparent")) - { - var p = traceParent.SelectMany(x => x!.Split('-')).ToArray(); - - Activity.Current = new Activity("Yarp.ReverseProxy") - .SetParentId($"{p[0]}-{traceIdFromProxy}-{p[2]}-{p[3]}") - .Start(); - } - } - - return next(context); -}); - app.Run(); From e6f8f5ed85d4cba02d09d82e542962c93f155e13 Mon Sep 17 00:00:00 2001 From: thangchung Date: Mon, 16 Sep 2024 19:36:04 +0700 Subject: [PATCH 3/4] change to regex replace --- yarp/CoffeeShop.Yarp/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarp/CoffeeShop.Yarp/Program.cs b/yarp/CoffeeShop.Yarp/Program.cs index 56a2d96..905c661 100644 --- a/yarp/CoffeeShop.Yarp/Program.cs +++ b/yarp/CoffeeShop.Yarp/Program.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Text.RegularExpressions; using Yarp.ReverseProxy.Transforms; @@ -29,10 +30,9 @@ { if (context.Request.Headers.Remove("traceparent")) { - var p = traceParent.SelectMany(x => x!.Split('-')).ToArray(); - + var traceParentReplaced = Regex.Replace(traceParent, "-.*?-", $"-${traceIdFromProxy}-"); Activity.Current = new Activity("Yarp.ReverseProxy") - .SetParentId($"{p[0]}-{traceIdFromProxy}-{p[2]}-{p[3]}") + .SetParentId(traceParentReplaced) .Start(); } } From 661d6039700490280f92c4cfa59356cb76451539 Mon Sep 17 00:00:00 2001 From: thangchung Date: Mon, 30 Sep 2024 17:42:56 +0700 Subject: [PATCH 4/4] refactor code --- Directory.Packages.props | 4 +- app-host/CoffeeShop.AppHost.csproj | 3 +- app-host/Program.cs | 38 +- app-host/SwaggerUi/SwaggerUiExtensions.cs | 333 +++++++++--------- app-host/app1.http | 2 - product-api/UseCases/ItemTypesQuery.cs | 3 - yarp/CoffeeShop.Yarp/Program.cs | 37 -- .../Properties/launchSettings.json | 18 +- yarp/CoffeeShop.Yarp/appsettings.json | 6 +- 9 files changed, 197 insertions(+), 247 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 09e8c41..c21c14b 100755 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ 8.0.6 8.4.0 8.0.8 - 8.2.0 + 8.2.1 1.9.0 0.0.4 8.0.1 @@ -32,7 +32,7 @@ - + diff --git a/app-host/CoffeeShop.AppHost.csproj b/app-host/CoffeeShop.AppHost.csproj index 4b6523c..f775d03 100644 --- a/app-host/CoffeeShop.AppHost.csproj +++ b/app-host/CoffeeShop.AppHost.csproj @@ -14,8 +14,7 @@ - - + diff --git a/app-host/Program.cs b/app-host/Program.cs index 29c901e..4211920 100755 --- a/app-host/Program.cs +++ b/app-host/Program.cs @@ -11,38 +11,28 @@ var rabbitmq = builder.AddRabbitMQ("rabbitmq").WithHealthCheck().WithManagementPlugin(); var productApi = builder.AddProject("product-api") - .WithSwaggerUI(); + .WithSwaggerUI(); var counterApi = builder.AddProject("counter-api") - .WithReference(productApi) - .WithReference(rabbitmq) - .WaitFor(rabbitmq) - .WithSwaggerUI(); + .WithReference(productApi) + .WithReference(rabbitmq) + .WaitFor(rabbitmq) + .WithSwaggerUI(); builder.AddProject("barista-api") - .WithReference(rabbitmq) - .WaitFor(rabbitmq); + .WithReference(rabbitmq) + .WaitFor(rabbitmq); builder.AddProject("kitchen-api") - .WithReference(rabbitmq) - .WaitFor(rabbitmq); + .WithReference(rabbitmq) + .WaitFor(rabbitmq); var orderSummaryApi = builder.AddProject("order-summary") - .WithReference(postgres) - .WithReference(rabbitmq) - .WaitFor(postgres) - .WaitFor(rabbitmq) - .WithSwaggerUI(); - -//var isHttps = builder.Configuration["DOTNET_LAUNCH_PROFILE"] == "https"; -//var ingressPort = int.TryParse(builder.Configuration["Ingress:Port"], out var port) ? port : (int?)null; - -//builder.AddYarp("ingress") -// .WithEndpoint(scheme: isHttps ? "https" : "http", port: ingressPort) -// .WithReference(productApi) -// .WithReference(counterApi) -// .WithReference(orderSummaryApi) -// .LoadFromConfiguration("ReverseProxy"); + .WithReference(postgres) + .WithReference(rabbitmq) + .WaitFor(postgres) + .WaitFor(rabbitmq) + .WithSwaggerUI(); builder.AddProject("yarp") .WithReference(productApi) diff --git a/app-host/SwaggerUi/SwaggerUiExtensions.cs b/app-host/SwaggerUi/SwaggerUiExtensions.cs index f3d1b47..a333257 100644 --- a/app-host/SwaggerUi/SwaggerUiExtensions.cs +++ b/app-host/SwaggerUi/SwaggerUiExtensions.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; + using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -9,173 +11,174 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using Yarp.ReverseProxy.Forwarder; public static class SwaggerUIExtensions { - /// - /// Maps the swagger ui endpoint to the application. - /// - /// The resource builder. - /// The list of open api documents. Defaults to "v1" if null. - /// The path to the open api document. - /// The endpoint name - public static IResourceBuilder WithSwaggerUI(this IResourceBuilder builder, - string[]? documentNames = null, string path = "swagger/v1/swagger.json", string endpointName = "http") - { - if (!builder.ApplicationBuilder.Resources.OfType().Any()) - { - // Add the swagger ui code hook and resource - builder.ApplicationBuilder.Services.TryAddLifecycleHook(); - builder.ApplicationBuilder.AddResource(new SwaggerUIResource("swagger-ui")) - .WithInitialState(new CustomResourceSnapshot - { - ResourceType = "swagger-ui", - Properties = [], - State = "Starting" - }) - .ExcludeFromManifest(); - } - - return builder.WithAnnotation(new SwaggerUIAnnotation(documentNames ?? ["v1"], path, builder.GetEndpoint(endpointName))); - } - - class SwaggerUiHook(ResourceNotificationService notificationService, - ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook - { - public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) - { - var openApiResource = appModel.Resources.OfType().SingleOrDefault(); - - if (openApiResource is null) - { - return; - } - - // We host a single webserver that will manage the swagger ui endpoints for all resources - var builder = WebApplication.CreateSlimBuilder(); - - builder.Services.AddHttpForwarder(); - builder.Logging.ClearProviders(); - - builder.Logging.AddProvider(new ResourceLoggerProvider(resourceLoggerService.GetLogger(openApiResource.Name))); - - var app = builder.Build(); - - // openapi/resourcename/documentname.json - app.MapSwaggerUI(); - - var resourceToEndpoint = new Dictionary(); - var portToResourceMap = new Dictionary)>(); - - foreach (var r in appModel.Resources) - { - if (!r.TryGetLastAnnotation(out var annotation)) - { - continue; - } - - // We store the url and path for each resource so we can hit the open api endpoint - resourceToEndpoint[r.Name] = (annotation.EndpointReference.Url, annotation.Path); - - var paths = new List(); - // To avoid cors issues, we expose URLs that send requests to the apphost and then forward them to the actual resource - foreach (var documentName in annotation.DocumentNames) - { - paths.Add($"swagger/{r.Name}/{documentName}"); - } - - // We store the URL for the resource on the host so we can map it back to the actual address once they are allocated - portToResourceMap[app.Urls.Count] = (annotation.EndpointReference.Url, paths); - - // We add a new URL for each resource that has a swagger ui annotation - // This is because swagger ui takes over the entire url space - app.Urls.Add("http://127.0.0.1:0"); - } - - var client = new HttpMessageInvoker(new SocketsHttpHandler()); - - // Swagger UI will make requests to the apphost so we can avoid doing any CORS configuration. - app.Map("/openapi/{resourceName}/{documentName}.json", - async (string resourceName, string documentName, IHttpForwarder forwarder, HttpContext context) => - { - var (endpoint, path) = resourceToEndpoint[resourceName]; - - await forwarder.SendAsync(context, endpoint, client, (c, r) => - { - r.RequestUri = new($"{endpoint}/{path}"); - return ValueTask.CompletedTask; - }); - }); - - app.Map("{*path}", async (HttpContext context, IHttpForwarder forwarder, string? path) => - { - var (endpoint, _) = portToResourceMap[context.Connection.LocalPort]; - - await forwarder.SendAsync(context, endpoint, client, (c, r) => - { - r.RequestUri = path is null ? new(endpoint) : new($"{endpoint}/{path}"); - return ValueTask.CompletedTask; - }); - }); - - await app.StartAsync(cancellationToken); - - var addresses = app.Services.GetRequiredService().Features.GetRequiredFeature().Addresses; - - var urls = ImmutableArray.CreateBuilder(); - - // Map our index back to the actual address - var index = 0; - foreach (var rawAddress in addresses) - { - var address = BindingAddress.Parse(rawAddress); - - // We map the bound port to the resource URL. This lets us forward requests to the correct resource - var (_, paths) = portToResourceMap[address.Port] = portToResourceMap[index++]; - - // We add the swagger ui endpoint for each resource - foreach (var p in paths) - { - urls.Add(new UrlSnapshot(rawAddress, $"{rawAddress}/{p}", IsInternal: false)); - } - } - - await notificationService.PublishUpdateAsync(openApiResource, s => s with - { - State = "Running", - Urls = urls.ToImmutableArray() - }); - } - } - - private class ResourceLoggerProvider(ILogger logger) : ILoggerProvider - { - public ILogger CreateLogger(string categoryName) - { - return new ResourceLogger(logger); - } - - public void Dispose() - { - } - - private class ResourceLogger(ILogger logger) : ILogger - { - public IDisposable? BeginScope(TState state) where TState : notnull - { - return logger.BeginScope(state); - } - - public bool IsEnabled(LogLevel logLevel) - { - return logger.IsEnabled(logLevel); - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - logger.Log(logLevel, eventId, state, exception, formatter); - } - } - } + /// + /// Maps the swagger ui endpoint to the application. + /// + /// The resource builder. + /// The list of open api documents. Defaults to "v1" if null. + /// The path to the open api document. + /// The endpoint name + public static IResourceBuilder WithSwaggerUI(this IResourceBuilder builder, + string[]? documentNames = null, string path = "swagger/v1/swagger.json", string endpointName = "http") + { + if (!builder.ApplicationBuilder.Resources.OfType().Any()) + { + // Add the swagger ui code hook and resource + builder.ApplicationBuilder.Services.TryAddLifecycleHook(); + builder.ApplicationBuilder.AddResource(new SwaggerUIResource("swagger-ui")) + .WithInitialState(new CustomResourceSnapshot + { + ResourceType = "swagger-ui", + Properties = [], + State = "Starting" + }) + .ExcludeFromManifest(); + } + + return builder.WithAnnotation(new SwaggerUIAnnotation(documentNames ?? ["v1"], path, builder.GetEndpoint(endpointName))); + } + + class SwaggerUiHook(ResourceNotificationService notificationService, + ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook + { + public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + { + var openApiResource = appModel.Resources.OfType().SingleOrDefault(); + + if (openApiResource is null) + { + return; + } + + // We host a single webserver that will manage the swagger ui endpoints for all resources + var builder = WebApplication.CreateSlimBuilder(); + + builder.Services.AddHttpForwarder(); + builder.Logging.ClearProviders(); + + builder.Logging.AddProvider(new ResourceLoggerProvider(resourceLoggerService.GetLogger(openApiResource.Name))); + + var app = builder.Build(); + + // openapi/resourcename/documentname.json + app.MapSwaggerUI(); + + var resourceToEndpoint = new Dictionary(); + var portToResourceMap = new Dictionary)>(); + + foreach (var r in appModel.Resources) + { + if (!r.TryGetLastAnnotation(out var annotation)) + { + continue; + } + + // We store the url and path for each resource so we can hit the open api endpoint + resourceToEndpoint[r.Name] = (annotation.EndpointReference.Url, annotation.Path); + + var paths = new List(); + // To avoid cors issues, we expose URLs that send requests to the apphost and then forward them to the actual resource + foreach (var documentName in annotation.DocumentNames) + { + paths.Add($"swagger/{r.Name}/{documentName}"); + } + + // We store the URL for the resource on the host so we can map it back to the actual address once they are allocated + portToResourceMap[app.Urls.Count] = (annotation.EndpointReference.Url, paths); + + // We add a new URL for each resource that has a swagger ui annotation + // This is because swagger ui takes over the entire url space + app.Urls.Add("http://127.0.0.1:0"); + } + + var client = new HttpMessageInvoker(new SocketsHttpHandler()); + + // Swagger UI will make requests to the apphost so we can avoid doing any CORS configuration. + app.Map("/openapi/{resourceName}/{documentName}.json", + async (string resourceName, string documentName, IHttpForwarder forwarder, HttpContext context) => + { + var (endpoint, path) = resourceToEndpoint[resourceName]; + + await forwarder.SendAsync(context, endpoint, client, (c, r) => + { + r.RequestUri = new($"{endpoint}/{path}"); + return ValueTask.CompletedTask; + }); + }); + + app.Map("{*path}", async (HttpContext context, IHttpForwarder forwarder, string? path) => + { + var (endpoint, _) = portToResourceMap[context.Connection.LocalPort]; + + await forwarder.SendAsync(context, endpoint, client, (c, r) => + { + r.RequestUri = path is null ? new(endpoint) : new($"{endpoint}/{path}"); + return ValueTask.CompletedTask; + }); + }); + + await app.StartAsync(cancellationToken); + + var addresses = app.Services.GetRequiredService().Features.GetRequiredFeature().Addresses; + + var urls = ImmutableArray.CreateBuilder(); + + // Map our index back to the actual address + var index = 0; + foreach (var rawAddress in addresses) + { + var address = BindingAddress.Parse(rawAddress); + + // We map the bound port to the resource URL. This lets us forward requests to the correct resource + var (_, paths) = portToResourceMap[address.Port] = portToResourceMap[index++]; + + // We add the swagger ui endpoint for each resource + foreach (var p in paths) + { + urls.Add(new UrlSnapshot(rawAddress, $"{rawAddress}/{p}", IsInternal: false)); + } + } + + await notificationService.PublishUpdateAsync(openApiResource, s => s with + { + State = "Running", + Urls = urls.ToImmutableArray() + }); + } + } + + private class ResourceLoggerProvider(ILogger logger) : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + return new ResourceLogger(logger); + } + + public void Dispose() + { + } + + private class ResourceLogger(ILogger logger) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull + { + return logger.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logger.IsEnabled(logLevel); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + logger.Log(logLevel, eventId, state, exception, formatter); + } + } + } } \ No newline at end of file diff --git a/app-host/app1.http b/app-host/app1.http index 92370f9..21a21cb 100644 --- a/app-host/app1.http +++ b/app-host/app1.http @@ -33,12 +33,10 @@ content-type: application/json ### GET http://{{hostname}}/p/api/v1/item-types content-type: application/json -trace-id: "{{$guid}}" ### GET https://{{hostname}}/p/api/v1/items-by-types/1,2,3 content-type: application/json -trace-id: "ff71c44ca162820de23468bb8c1b024b" ### @orderId = 8cf20000-8d12-00ff-acd0-08dc7cc27ccd diff --git a/product-api/UseCases/ItemTypesQuery.cs b/product-api/UseCases/ItemTypesQuery.cs index d0facb8..6f5e059 100755 --- a/product-api/UseCases/ItemTypesQuery.cs +++ b/product-api/UseCases/ItemTypesQuery.cs @@ -29,9 +29,6 @@ public Task> Handle(ItemTypesQuery request, Cancellatio { ArgumentNullException.ThrowIfNull(request); - // todo: only for debugging purposes, remove later - var traceId = Activity.Current?.Id ?? httpContextAccessor?.HttpContext?.TraceIdentifier; - var results = new List { // beverages diff --git a/yarp/CoffeeShop.Yarp/Program.cs b/yarp/CoffeeShop.Yarp/Program.cs index 905c661..bacac1d 100644 --- a/yarp/CoffeeShop.Yarp/Program.cs +++ b/yarp/CoffeeShop.Yarp/Program.cs @@ -1,45 +1,8 @@ -using System.Diagnostics; -using System.Text.RegularExpressions; - -using Yarp.ReverseProxy.Transforms; - var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) - .AddTransforms(b => - { - // todo: only for testing purposes, remove later - // https://stackoverflow.com/questions/70306118/set-traceid-on-activity - b.AddRequestTransform(async transformContext => - { - var context = transformContext.HttpContext; - var traceIdFromProxy = context.Request.Headers["trace-id"].FirstOrDefault()?.Trim('"'); - if (Guid.TryParse(traceIdFromProxy, out var parsedGuid)) - { - traceIdFromProxy = parsedGuid.ToString("N"); - } - else - { - // Handle the case where the traceId is not a valid GUID - traceIdFromProxy = Guid.NewGuid().ToString("N"); - } - - if (context.Request.Headers.TryGetValue("traceparent", out var traceParent)) - { - if (context.Request.Headers.Remove("traceparent")) - { - var traceParentReplaced = Regex.Replace(traceParent, "-.*?-", $"-${traceIdFromProxy}-"); - Activity.Current = new Activity("Yarp.ReverseProxy") - .SetParentId(traceParentReplaced) - .Start(); - } - } - - await ValueTask.CompletedTask; - }); - }) .AddServiceDiscoveryDestinationResolver(); var app = builder.Build(); diff --git a/yarp/CoffeeShop.Yarp/Properties/launchSettings.json b/yarp/CoffeeShop.Yarp/Properties/launchSettings.json index 52cfe19..03b7535 100644 --- a/yarp/CoffeeShop.Yarp/Properties/launchSettings.json +++ b/yarp/CoffeeShop.Yarp/Properties/launchSettings.json @@ -1,13 +1,13 @@ { "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } } } diff --git a/yarp/CoffeeShop.Yarp/appsettings.json b/yarp/CoffeeShop.Yarp/appsettings.json index 82ec688..f5c6507 100644 --- a/yarp/CoffeeShop.Yarp/appsettings.json +++ b/yarp/CoffeeShop.Yarp/appsettings.json @@ -54,21 +54,21 @@ "productapi": { "Destinations": { "base_destination": { - "Address": "http://product-api" + "Address": "http+https://product-api" } } }, "counterApi": { "Destinations": { "base_destination": { - "Address": "http://counter-api" + "Address": "http+https://counter-api" } } }, "orderSummaryApi": { "Destinations": { "base_destination": { - "Address": "http://order-summary" + "Address": "http+https://order-summary" } } }