From 9d551a684323fad4fb4919ffa5ec1b21a00c624e Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 1 May 2024 15:16:24 -0400 Subject: [PATCH 1/4] Demo code merge (#7) * Update `IsAspireHost` property in test projects Changed the `IsAspireHost` property from `true` to `false` in `Catalog.FunctionalTests.csproj` and `Ordering.FunctionalTests.csproj` files. This suggests that these projects may no longer be hosted on Aspire. * Added new projects, classes, and updated configurations * add workflow for pipeline and azure.yml for config host * update workflow to add openai ENV items * Configure Azure Developer Pipeline * change useOpenAI to ENV variable * Configure Azure Developer Pipeline * fixing openapi flag * useopenai ENV change * echo env var * minor fix for format * set env at provision step * Configuration naming structure with underscore issues * Configure Azure Developer Pipeline * Configure Azure Developer Pipeline * save program.cs * updates from Victor * Configure Azure Developer Pipeline * Configure Azure Developer Pipeline * Configure Azure Developer Pipeline * Configure Azure Developer Pipeline * ... try again * update ENV workings * Configure Azure Developer Pipeline * update ENV for connectionstring * update more VARS to make it happen * update action for azd env set * update more VARS to make it happen again * update more VARS to make it happen again again * env playground * create env for azd * escape quotes * misspelling * quote vars for escaping characters * Configure Azure Developer Pipeline * add profiler package for webapp workaround * Revert "add profiler package for webapp workaround" This reverts commit ba25534c3ee6728822febce6dbe23815f89bb8ab. * add profiler to extension class --- .github/workflows/azure-dev.yml | 73 +++++++++++++++++++ .gitignore | 1 + Directory.Packages.props | 1 + azure.yaml | 8 ++ eShop.Web.slnf | 3 +- eShop.sln | 12 +++ next-steps.md | 69 ++++++++++++++++++ src/Performance/BackgroudScrubber.cs | 30 ++++++++ src/Performance/Performance.csproj | 9 +++ src/Performance/Program.cs | 34 +++++++++ .../Properties/launchSettings.json | 38 ++++++++++ src/Performance/Scrubber.cs | 23 ++++++ src/Performance/ScrubberHelper.cs | 34 +++++++++ src/Performance/appsettings.Development.json | 8 ++ src/Performance/appsettings.json | 9 +++ src/eShop.AppHost/Program.cs | 8 +- .../Properties/launchSettings.json | 29 -------- src/eShop.AppHost/appsettings.json | 3 +- src/eShop.ServiceDefaults/Extensions.cs | 3 + .../eShop.ServiceDefaults.csproj | 1 + .../Catalog.FunctionalTests.csproj | 2 +- .../Ordering.FunctionalTests.csproj | 2 +- 22 files changed, 362 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/azure-dev.yml create mode 100644 azure.yaml create mode 100644 next-steps.md create mode 100644 src/Performance/BackgroudScrubber.cs create mode 100644 src/Performance/Performance.csproj create mode 100644 src/Performance/Program.cs create mode 100644 src/Performance/Properties/launchSettings.json create mode 100644 src/Performance/Scrubber.cs create mode 100644 src/Performance/ScrubberHelper.cs create mode 100644 src/Performance/appsettings.Development.json create mode 100644 src/Performance/appsettings.json delete mode 100644 src/eShop.AppHost/Properties/launchSettings.json diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 0000000..56452e4 --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,73 @@ +name: eShop Build, Provision, and Deploy to Azure + +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - shboyer + +permissions: + id-token: write + contents: read + +jobs: + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_OPENAI: "Endpoint=${{ vars.OPEN_ENDPOINT }};Key=${{ secrets.OPENKEY }}" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install azd + uses: Azure/setup-azd@v0.1.0 + + - name: Install .NET Aspire workload + run: dotnet workload install aspire + + - name: Log in with Azure (Federated Credentials) + if: ${{ env.AZURE_CLIENT_ID != '' }} + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + - name: Log in with Azure (Client Credentials) + if: ${{ env.AZURE_CREDENTIALS != '' }} + run: | + $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; + Write-Host "::add-mask::$($info.clientSecret)" + + azd auth login ` + --client-id "$($info.clientId)" ` + --client-secret "$($info.clientSecret)" ` + --tenant-id "$($info.tenantId)" + shell: pwsh + + - name: Set azd ENV + run: | + azd env new ${{ vars.AZURE_ENV_NAME }} --location ${{ vars.AZURE_LOCATION }} --subscription ${{ vars.AZURE_SUBSCRIPTION_ID }} --no-prompt + azd env set OPENAI_CONNECTIONSTRING "${{ env.AZURE_OPENAI }}" --no-prompt + + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + #AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + ConnectionStrings__OpenAi: "${{ env.AZURE_OPENAI }}" + + - name: Deploy Application + run: | + azd env set OPENAI_CONNECTIONSTRING "${{ env.AZURE_OPENAI }}" --no-prompt + azd deploy --no-prompt \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8893976..b1bf165 100644 --- a/.gitignore +++ b/.gitignore @@ -491,3 +491,4 @@ $RECYCLE.BIN/ /playwright/.auth/ /user.json .azure +/src/eShop.AppHost/Properties/launchSettings.json diff --git a/Directory.Packages.props b/Directory.Packages.props index 2c890b2..f194c37 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..c58008c --- /dev/null +++ b/azure.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: eShopOnAzure +services: + app: + language: dotnet + project: .\src\eShop.AppHost\eShop.AppHost.csproj + host: containerapp diff --git a/eShop.Web.slnf b/eShop.Web.slnf index 101ca8b..6d4f2d1 100644 --- a/eShop.Web.slnf +++ b/eShop.Web.slnf @@ -8,11 +8,12 @@ "src\\EventBus\\EventBus.csproj", "src\\IntegrationEventLogEF\\IntegrationEventLogEF.csproj", "src\\Mobile.Bff.Shopping\\Mobile.Bff.Shopping.csproj", - "src\\Ordering.API\\Ordering.API.csproj", "src\\OrderProcessor\\OrderProcessor.csproj", + "src\\Ordering.API\\Ordering.API.csproj", "src\\Ordering.Domain\\Ordering.Domain.csproj", "src\\Ordering.Infrastructure\\Ordering.Infrastructure.csproj", "src\\PaymentProcessor\\PaymentProcessor.csproj", + "src\\Performance\\Performance.csproj", "src\\WebAppComponents\\WebAppComponents.csproj", "src\\WebApp\\WebApp.csproj", "src\\WebhookClient\\WebhookClient.csproj", diff --git a/eShop.sln b/eShop.sln index 5f058fb..71055c9 100644 --- a/eShop.sln +++ b/eShop.sln @@ -63,6 +63,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientApp", "src\ClientApp\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientApp.UnitTests", "tests\ClientApp.UnitTests\ClientApp.UnitTests.csproj", "{02878FFB-F4DA-4996-B4A6-308851A837C6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Performance", "src\Performance\Performance.csproj", "{71667539-C845-46D8-8E43-E686D1B184B1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{3673B22C-778B-46B0-AB34-338C6F52BBDF}" + ProjectSection(SolutionItems) = preProject + .github\workflows\azure-dev.yml = .github\workflows\azure-dev.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -169,6 +176,10 @@ Global {02878FFB-F4DA-4996-B4A6-308851A837C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {02878FFB-F4DA-4996-B4A6-308851A837C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {02878FFB-F4DA-4996-B4A6-308851A837C6}.Release|Any CPU.Build.0 = Release|Any CPU + {71667539-C845-46D8-8E43-E686D1B184B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71667539-C845-46D8-8E43-E686D1B184B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71667539-C845-46D8-8E43-E686D1B184B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71667539-C845-46D8-8E43-E686D1B184B1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -198,6 +209,7 @@ Global {66275483-5364-42F9-B7E6-410E6A1B5ECF} = {932D8224-11F6-4D07-B109-DA28AD288A63} {938803BB-4F6F-4108-BDD1-2AD0180BBDC1} = {932D8224-11F6-4D07-B109-DA28AD288A63} {02878FFB-F4DA-4996-B4A6-308851A837C6} = {A857AD10-40FF-4303-BEC2-FF1C58D5735E} + {3673B22C-778B-46B0-AB34-338C6F52BBDF} = {3AF739CD-81D8-428D-A08A-0A58372DEBF6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9} diff --git a/next-steps.md b/next-steps.md new file mode 100644 index 0000000..eb1af36 --- /dev/null +++ b/next-steps.md @@ -0,0 +1,69 @@ +# Next Steps after `azd init` + +## Table of Contents + +1. [Next Steps](#next-steps) +2. [What was added](#what-was-added) +3. [Billing](#billing) +4. [Troubleshooting](#troubleshooting) + +## Next Steps + +### Provision infrastructure and deploy application code + +Run `azd up` to provision your infrastructure and deploy to Azure in one step (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! + +To troubleshoot any issues, see [troubleshooting](#troubleshooting). + +### Configure CI/CD pipeline + +1. Create a workflow pipeline file locally. The following starters are available: + - [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml) + - [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml) +2. Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. + +## What was added + +### Infrastructure configuration + +To describe the infrastructure and application, an `azure.yaml` was added with the following directory structure: + +```yaml +- azure.yaml # azd project configuration +``` + +This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it. + +If you would like to see or modify the infrastructure that `azd` uses, run `azd infra synth` to persist it to disk. + +If you do this, some additional directories will be created: + +```yaml +- infra/ # Infrastructure as Code (bicep) files + - main.bicep # main deployment module + - resources.bicep # resources shared across your application's services +``` + +In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps. + +*Note*: Once you have synthesized your infrastructure to disk, changes made to your App Host will not be reflected in the infrastructure. You can re-generate the infrastructure by running `azd infra synth` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra synth` to overwrite the files without prompting. + +*Note*: `azd infra synth` is currently an alpha feature and must be explicitly enabled by running `azd config set alpha.infraSynth on`. You only need to do this once. + +## Billing + +Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). + +## Troubleshooting + +Q: I visited the service endpoint listed, and I'm seeing a blank or error page. + +A: Your service may have failed to start or misconfigured. To investigate further: + +1. Click on the resource group link shown to visit Azure Portal. +2. Navigate to the specific Azure Container App resource for the service. +3. Select *Monitoring -> Log stream* under the navigation pane. +4. Observe the log output to identify any errors. +5. If logs are written to disk, examine the local logs or debug the application by using the *Console* to connect to a shell within the running container. + +For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). diff --git a/src/Performance/BackgroudScrubber.cs b/src/Performance/BackgroudScrubber.cs new file mode 100644 index 0000000..ff6680a --- /dev/null +++ b/src/Performance/BackgroudScrubber.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Store.Checkout.Services; + +public class BackgroundScrubber : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + TimeSpan waitTime = TimeSpan.FromSeconds(5); + TimeSpan burnTime = TimeSpan.FromSeconds(5); + try + { + // Just wastes CPU + while (true) + { + await Task.Delay(waitTime, stoppingToken); + Stopwatch stopwatch = Stopwatch.StartNew(); + do + { + stoppingToken.ThrowIfCancellationRequested(); + Scrubber.SanitizeData($"The secret word is {stopwatch.ElapsedMilliseconds}", '*', CultureInfo.InvariantCulture); + } while (stopwatch.Elapsed < burnTime); + } + } + catch (OperationCanceledException) + { + } + } +} \ No newline at end of file diff --git a/src/Performance/Performance.csproj b/src/Performance/Performance.csproj new file mode 100644 index 0000000..1b28a01 --- /dev/null +++ b/src/Performance/Performance.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Performance/Program.cs b/src/Performance/Program.cs new file mode 100644 index 0000000..6394141 --- /dev/null +++ b/src/Performance/Program.cs @@ -0,0 +1,34 @@ +using System.Globalization; +namespace Store.Checkout.Services; + +internal class Program +{ + private static int Main(string[] args) + { + + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddHostedService(); + + var app = builder.Build(); + + app.MapGet("/scrub", () => + { + string x = Math.PI.ToString(); + for (int i = 0; i < 1000; i++) + { + x = x + Random.Shared.Next(0, 10).ToString(); + if (i % 50 == 0) + { + Scrubber.SanitizeData("Working...", 'X', CultureInfo.CurrentCulture); + } + } + + return Scrubber.SanitizeData($"PI is {x}", 'X', CultureInfo.CurrentCulture); + }); + + app.MapGet("/", () => "Hello World! V2"); + app.Run(); + + return 0; + } +} \ No newline at end of file diff --git a/src/Performance/Properties/launchSettings.json b/src/Performance/Properties/launchSettings.json new file mode 100644 index 0000000..f2a15f8 --- /dev/null +++ b/src/Performance/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:29073", + "sslPort": 44335 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5097", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7049;http://localhost:5097", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Performance/Scrubber.cs b/src/Performance/Scrubber.cs new file mode 100644 index 0000000..89014af --- /dev/null +++ b/src/Performance/Scrubber.cs @@ -0,0 +1,23 @@ +using System.Globalization; + +namespace Store.Checkout.Services; + +public class Scrubber +{ + internal record LocalizedWord(string Text, CultureInfo Culture); + + private static IEnumerable DisallowedWords { get; } = ScrubberHelpers.LoadDisallowedWords(); + + public static string SanitizeData(string data, char replacementChar, CultureInfo culture) + { + List wordList = DisallowedWords + .Where(word => culture.Equals(CultureInfo.InvariantCulture) || culture.Equals(word.Culture)) + .Select(word => word.Text).ToList(); + + foreach (string word in wordList) + { + data = data.Replace(word, replacementChar.ToString(), ignoreCase: true, culture); + } + return data; + } +} diff --git a/src/Performance/ScrubberHelper.cs b/src/Performance/ScrubberHelper.cs new file mode 100644 index 0000000..b5ae08b --- /dev/null +++ b/src/Performance/ScrubberHelper.cs @@ -0,0 +1,34 @@ +using System.Globalization; +using static Store.Checkout.Services.Scrubber; + +internal static class ScrubberHelpers +{ + + public static IEnumerable LoadDisallowedWords() + { + // Generate fake data. Could be loaded from resources, file on disk or external service. + + var cultures = new CultureInfo[] + { + CultureInfo.GetCultureInfo("en-US"), + CultureInfo.GetCultureInfo("en-GB"), + CultureInfo.GetCultureInfo("fr-FR"), + }; + + for (int i = 0; i < 3000; i++) + { + yield return new LocalizedWord(Text: DeserializeLocalizedTerm(), Culture: cultures[Random.Shared.Next(0, cultures.Length)]); + } + + static string DeserializeLocalizedTerm() + { + Span text = stackalloc char[Random.Shared.Next(4, 12)]; + for (int i = 0; i < text.Length; i++) + { + text[i] = (char)Random.Shared.Next('a', 'z' + 1); + } + + return new string(text); + } + } +} diff --git a/src/Performance/appsettings.Development.json b/src/Performance/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Performance/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Performance/appsettings.json b/src/Performance/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Performance/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/eShop.AppHost/Program.cs b/src/eShop.AppHost/Program.cs index a369031..3cae7f8 100644 --- a/src/eShop.AppHost/Program.cs +++ b/src/eShop.AppHost/Program.cs @@ -78,12 +78,12 @@ .WithReference(appInsights); // set to true if you want to use OpenAI -bool useOpenAI = false; +bool useOpenAI = true; if (useOpenAI) { const string openAIName = "openai"; - const string textEmbeddingName = "text-embedding-ada-002"; - const string chatModelName = "gpt-35-turbo-16k"; + const string textEmbeddingName = "shboyer-text-emdedding-ada-002"; + const string chatModelName = "shboyer-gpt35deployment"; // to use an existing OpenAI resource, add the following to the AppHost user secrets: // "ConnectionStrings": { @@ -114,7 +114,7 @@ webApp .WithReference(openAI) - .WithEnvironment("AI__OPENAI__CHATMODEL", chatModelName); ; + .WithEnvironment("AI__OPENAI__CHATMODEL", chatModelName); } // Wire up the callback urls (self referencing) diff --git a/src/eShop.AppHost/Properties/launchSettings.json b/src/eShop.AppHost/Properties/launchSettings.json deleted file mode 100644 index 6622446..0000000 --- a/src/eShop.AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:19888;http://localhost:18848", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:19076" - } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:18848", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17119" - } - } - } -} diff --git a/src/eShop.AppHost/appsettings.json b/src/eShop.AppHost/appsettings.json index 5739939..839ee10 100644 --- a/src/eShop.AppHost/appsettings.json +++ b/src/eShop.AppHost/appsettings.json @@ -8,7 +8,6 @@ }, "ConnectionStrings": { "AppInsights": "", - "EventBus": ".servicebus.windows.net", - //"OpenAi": "Endpoint=xxxx;Key=xxxx" + "EventBus": ".servicebus.windows.net" } } diff --git a/src/eShop.ServiceDefaults/Extensions.cs b/src/eShop.ServiceDefaults/Extensions.cs index c99cf34..ba81e6d 100644 --- a/src/eShop.ServiceDefaults/Extensions.cs +++ b/src/eShop.ServiceDefaults/Extensions.cs @@ -96,6 +96,9 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) { builder.Services.AddOpenTelemetry().UseAzureMonitor(); + // Add Profiler for Azure Monitor + builder.Services.AddApplicationInsightsTelemetry(); + builder.Services.AddServiceProfiler(); } return builder; diff --git a/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index 9374b39..c9b3565 100644 --- a/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj b/tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj index 43880a7..c9ad577 100644 --- a/tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj +++ b/tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj @@ -4,7 +4,7 @@ net8.0 false false - true + false diff --git a/tests/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj b/tests/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj index bdab7c1..7150320 100644 --- a/tests/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj +++ b/tests/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj @@ -4,7 +4,7 @@ net8.0 false false - true + false From 5c565b91937ce5e649faeab1b4eb5799e6371cb1 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 1 May 2024 15:17:31 -0400 Subject: [PATCH 2/4] Update azure-dev.yml --- .github/workflows/azure-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 56452e4..3982144 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -6,7 +6,7 @@ on: # Run when commits are pushed to mainline branch (main or master) # Set this to the mainline branch you are using branches: - - shboyer + - main permissions: id-token: write @@ -70,4 +70,4 @@ jobs: - name: Deploy Application run: | azd env set OPENAI_CONNECTIONSTRING "${{ env.AZURE_OPENAI }}" --no-prompt - azd deploy --no-prompt \ No newline at end of file + azd deploy --no-prompt From dae9d1eeef3829356fc9312dab24d88c33b06d20 Mon Sep 17 00:00:00 2001 From: Jan Kalis Date: Mon, 6 May 2024 10:38:52 -0700 Subject: [PATCH 3/4] Pushing perf issues in the eShop namespace --- src/Performance/BackgroudScrubber.cs | 8 ++++---- src/Performance/Program.cs | 12 ++++++------ src/Performance/Scrubber.cs | 4 ++-- src/Performance/ScrubberHelper.cs | 2 +- src/Performance/appsettings.json | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Performance/BackgroudScrubber.cs b/src/Performance/BackgroudScrubber.cs index ff6680a..cf29fe9 100644 --- a/src/Performance/BackgroudScrubber.cs +++ b/src/Performance/BackgroudScrubber.cs @@ -1,9 +1,9 @@ using System.Diagnostics; using System.Globalization; -namespace Store.Checkout.Services; +namespace eShop.Store.Reviews; -public class BackgroundScrubber : BackgroundService +public class BackgroundReviewValidation : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -19,7 +19,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) do { stoppingToken.ThrowIfCancellationRequested(); - Scrubber.SanitizeData($"The secret word is {stopwatch.ElapsedMilliseconds}", '*', CultureInfo.InvariantCulture); + ReviewValidation.SanitizeData($"The secret word is {stopwatch.ElapsedMilliseconds}", '*', CultureInfo.InvariantCulture); } while (stopwatch.Elapsed < burnTime); } } @@ -27,4 +27,4 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { } } -} \ No newline at end of file +} diff --git a/src/Performance/Program.cs b/src/Performance/Program.cs index 6394141..d3e1966 100644 --- a/src/Performance/Program.cs +++ b/src/Performance/Program.cs @@ -1,5 +1,5 @@ -using System.Globalization; -namespace Store.Checkout.Services; +using System.Globalization; +namespace eShop.Store.Reviews; internal class Program { @@ -7,7 +7,7 @@ private static int Main(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.Services.AddHostedService(); + builder.Services.AddHostedService(); var app = builder.Build(); @@ -19,11 +19,11 @@ private static int Main(string[] args) x = x + Random.Shared.Next(0, 10).ToString(); if (i % 50 == 0) { - Scrubber.SanitizeData("Working...", 'X', CultureInfo.CurrentCulture); + ReviewValidation.SanitizeData("Working...", 'X', CultureInfo.CurrentCulture); } } - return Scrubber.SanitizeData($"PI is {x}", 'X', CultureInfo.CurrentCulture); + return ReviewValidation.SanitizeData($"PI is {x}", 'X', CultureInfo.CurrentCulture); }); app.MapGet("/", () => "Hello World! V2"); @@ -31,4 +31,4 @@ private static int Main(string[] args) return 0; } -} \ No newline at end of file +} diff --git a/src/Performance/Scrubber.cs b/src/Performance/Scrubber.cs index 89014af..9e80ec0 100644 --- a/src/Performance/Scrubber.cs +++ b/src/Performance/Scrubber.cs @@ -1,8 +1,8 @@ using System.Globalization; -namespace Store.Checkout.Services; +namespace eShop.Store.Reviews; -public class Scrubber +public class ReviewValidation { internal record LocalizedWord(string Text, CultureInfo Culture); diff --git a/src/Performance/ScrubberHelper.cs b/src/Performance/ScrubberHelper.cs index b5ae08b..3d05edc 100644 --- a/src/Performance/ScrubberHelper.cs +++ b/src/Performance/ScrubberHelper.cs @@ -1,5 +1,5 @@ using System.Globalization; -using static Store.Checkout.Services.Scrubber; +using static eShop.Store.Reviews.ReviewValidation; internal static class ScrubberHelpers { diff --git a/src/Performance/appsettings.json b/src/Performance/appsettings.json index 10f68b8..44d718f 100644 --- a/src/Performance/appsettings.json +++ b/src/Performance/appsettings.json @@ -1,8 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Error", + "Microsoft.AspNetCore": "Error" } }, "AllowedHosts": "*" From 3b6e29525632425fc68a445496eb0d67f76933d4 Mon Sep 17 00:00:00 2001 From: Jan Kalis Date: Mon, 6 May 2024 12:46:37 -0700 Subject: [PATCH 4/4] This is push 1/2 - new perfomance issue in the core eShop. Simulates higher load for rendering catalog page. --- src/Catalog.API/Apis/CatalogApi.cs | 99 ++++++++++++++++++------------ 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/src/Catalog.API/Apis/CatalogApi.cs b/src/Catalog.API/Apis/CatalogApi.cs index 582f2eb..e2293cd 100644 --- a/src/Catalog.API/Apis/CatalogApi.cs +++ b/src/Catalog.API/Apis/CatalogApi.cs @@ -8,7 +8,7 @@ public static class CatalogApi public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app) { // Routes for querying catalog items. - app.MapGet("/items", GetAllItems); + app.MapGet("/items", GetAllCatalogItems); app.MapGet("/items/by", GetItemsByIds); app.MapGet("/items/{id:int}", GetItemById); app.MapGet("/items/by/{name:minlength(1)}", GetItemsByName); @@ -31,53 +31,57 @@ public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app return app; } - public static async Task>, BadRequest>> GetAllItems( + + public static Results>, BadRequest> GetAllItems( [AsParameters] PaginationRequest paginationRequest, [AsParameters] CatalogServices services) { var pageSize = paginationRequest.PageSize; var pageIndex = paginationRequest.PageIndex; - var totalItems = await services.Context.CatalogItems - .LongCountAsync(); + var totalItems = services.Context.CatalogItems + .LongCount(); - var itemsOnPage = await services.Context.CatalogItems - .OrderBy(c => c.Name) - .Skip(pageSize * pageIndex) - .Take(pageSize) - .ToListAsync(); + var itemsOnPage = services.Context.CatalogItems + .OrderBy(c => c.Name) + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToList(); return TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage)); } - public static async Task>> GetItemsByIds( + + public static Ok> GetItemsByIds( [AsParameters] CatalogServices services, int[] ids) { - var items = await services.Context.CatalogItems.Where(item => ids.Contains(item.Id)).ToListAsync(); + var items = services.Context.CatalogItems.Where(item => ids.Contains(item.Id)).ToList(); return TypedResults.Ok(items); } - public static async Task, NotFound, BadRequest>> GetItemById( + public static Task, NotFound, BadRequest>> GetItemById( [AsParameters] CatalogServices services, int id) { if (id <= 0) { - return TypedResults.BadRequest("Id is not valid."); + return Task.FromResult, NotFound, BadRequest>>(TypedResults.BadRequest("Id is not valid.")); } - var item = await services.Context.CatalogItems.Include(ci => ci.CatalogBrand).SingleOrDefaultAsync(ci => ci.Id == id); + var items = services.Context.CatalogItems.Include(ci => ci.CatalogBrand).ToList(); + + var item = items.SingleOrDefault(ci => ci.Id == id); if (item == null) { - return TypedResults.NotFound(); + return Task.FromResult, NotFound, BadRequest>>(TypedResults.NotFound()); } - return TypedResults.Ok(item); + return Task.FromResult, NotFound, BadRequest>>(TypedResults.Ok(item)); } - public static async Task>> GetItemsByName( + public static Task>> GetItemsByName( [AsParameters] PaginationRequest paginationRequest, [AsParameters] CatalogServices services, string name) @@ -85,26 +89,28 @@ public static async Task>> GetItemsByName( var pageSize = paginationRequest.PageSize; var pageIndex = paginationRequest.PageIndex; - var totalItems = await services.Context.CatalogItems + var allItems = services.Context.CatalogItems .Where(c => c.Name.StartsWith(name)) - .LongCountAsync(); + .ToList(); - var itemsOnPage = await services.Context.CatalogItems - .Where(c => c.Name.StartsWith(name)) + var totalItems = allItems.Count; + + var itemsOnPage = allItems .Skip(pageSize * pageIndex) .Take(pageSize) - .ToListAsync(); + .ToList(); - return TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage)); + return Task.FromResult(TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage))); } - public static async Task> GetItemPictureById(CatalogContext context, IWebHostEnvironment environment, int catalogItemId) + public static Task> GetItemPictureById(CatalogContext context, IWebHostEnvironment environment, int catalogItemId) { - var item = await context.CatalogItems.FindAsync(catalogItemId); + var allItems = context.CatalogItems.ToList(); + var item = allItems.FirstOrDefault(i => i.Id == catalogItemId); if (item is null) { - return TypedResults.NotFound(); + return Task.FromResult>(TypedResults.NotFound()); } var path = GetFullPath(environment.ContentRootPath, item.PictureFileName); @@ -113,7 +119,7 @@ public static async Task> GetItemPictu string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); DateTime lastModified = File.GetLastWriteTimeUtc(path); - return TypedResults.PhysicalFile(path, mimetype, lastModified: lastModified); + return Task.FromResult>(TypedResults.PhysicalFile(path, mimetype, lastModified: lastModified)); } public static async Task, RedirectToRouteHttpResult, Ok>>> GetItemsBySemanticRelevance( @@ -163,7 +169,7 @@ public static async Task, RedirectToRouteHttpResult, return TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage)); } - public static async Task>> GetItemsByBrandAndTypeId( + public static Task>> GetItemsByBrandAndTypeId( [AsParameters] PaginationRequest paginationRequest, [AsParameters] CatalogServices services, int typeId, @@ -179,18 +185,19 @@ public static async Task>> GetItemsByBrandAndType root = root.Where(c => c.CatalogBrandId == brandId); } - var totalItems = await root - .LongCountAsync(); + var allItems = root.ToList(); - var itemsOnPage = await root + var totalItems = allItems.Count; + + var itemsOnPage = allItems .Skip(pageSize * pageIndex) .Take(pageSize) - .ToListAsync(); + .ToList(); - return TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage)); + return Task.FromResult(TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage))); } - public static async Task>> GetItemsByBrandId( + public static Task>> GetItemsByBrandId( [AsParameters] PaginationRequest paginationRequest, [AsParameters] CatalogServices services, int? brandId) @@ -205,15 +212,16 @@ public static async Task>> GetItemsByBrandId( root = root.Where(ci => ci.CatalogBrandId == brandId); } - var totalItems = await root - .LongCountAsync(); + var allItems = root.ToList(); - var itemsOnPage = await root + var totalItems = allItems.Count; + + var itemsOnPage = allItems .Skip(pageSize * pageIndex) .Take(pageSize) - .ToListAsync(); + .ToList(); - return TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage)); + return Task.FromResult(TypedResults.Ok(new PaginatedItems(pageIndex, pageSize, totalItems, itemsOnPage))); } public static async Task>> UpdateItem( @@ -294,6 +302,19 @@ public static async Task> DeleteItemById( return TypedResults.NoContent(); } + //Deliberately introduce high traffic to simulate a high load scenario. Remove for production. + public static Results>, BadRequest> GetAllCatalogItems( + [AsParameters] PaginationRequest paginationRequest, + [AsParameters] CatalogServices services) + { + + // JANK: Just to simulate a slow query + for (int i = 0; i < 500; i++) + { + GetAllItems(paginationRequest, services); + } + return GetAllItems(paginationRequest, services); + } private static string GetImageMimeTypeFromImageFileExtension(string extension) => extension switch { ".png" => "image/png",