diff --git a/README.md b/README.md index 5160c5c1..deea441f 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,21 @@ The documentation sources are located in the `docs` directory. See the [docs rea ## Development -This section describes how to set up the development environment. First, you need to install the following tools: +This section describes how to set up the development environment. First, you need to install the following tools installed on your machine: +- Docker - .NET 10.0 SDK -- node.js v24.x and npm -- your favourite IDE +- Node.js v24.x and npm +- Your preferred IDE such as [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio](https://visualstudio.microsoft.com/vs/) To run the application from source, follow these steps: -1. Open the `src/Turnierplan.slnx` solution and navigate to the docker compose file located under `Solution Items`. Run the `turnierplan.database` docker compose service. This will start up the PostgreSQL database for local development. -2. Navigate to the `Turnierplan.App` project and run the `Turnierplan.App` launch configuration. This will start the backend using port `45000`. -3. Open a terminal and navigate to the `src/Turnierplan.App/Client` directory. Run `npm install` to install the node dependencies. Next, you can start the client application by typing `npm run start`. -4. Access the client application using [http://localhost:45001](http://localhost:45001) and log in using default credentials. The user name is `admin` and the password is `P@ssw0rd`. +1. Open the `src/Turnierplan.slnx` solution. +2. Run the `Turnierplan.AppHost` project. Make sure to use the configuration *with client*. This will start the Aspire AppHost which will do the following steps: + - Download the postgres container image and run a local database + - Install npm dependencies and run the client app + - Run the backend `Turnierplan.App` +3. The Aspire dashboard will open up from which you can navigate to the client application ([http://localhost:45001](http://localhost:45001)). +4. Now you can log in using default credentials: The username is `admin` and the password is `P@ssw0rd`. When running locally, the API documentation can be viewed by opening [http://localhost:45000/scalar](http://localhost:45000/scalar). - -> [!NOTE] -> The solution must be built first before the client application can be started. This is because the client application startup depends on OpenAPI files generated during the solution build. diff --git a/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs index 94a6ddb3..6ec999a4 100644 --- a/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.App/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ using Azure.Monitor.OpenTelemetry.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; +using Npgsql; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; using Turnierplan.App.Security; using Turnierplan.Core.ApiKey; using Turnierplan.Core.User; @@ -14,17 +18,45 @@ internal static class ServiceCollectionExtensions { public static void AddTurnierplanMonitoring(this IServiceCollection services, IConfiguration configuration) { - var connectionString = configuration.GetSection("ApplicationInsights").GetValue("ConnectionString"); + var applicationInsightsConnectionString = configuration.GetSection("ApplicationInsights").GetValue("ConnectionString"); + var hasApplicationInsights = !string.IsNullOrEmpty(applicationInsightsConnectionString); + var hasOltpExporter = !string.IsNullOrWhiteSpace(configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - if (!string.IsNullOrWhiteSpace(connectionString)) + if (!hasApplicationInsights && !hasOltpExporter) { - services.AddOpenTelemetry() - .WithTracing(tracing => - { - tracing.AddTurnierplanDataAccessLayer(); - tracing.AddTurnierplanDocumentRendering(); - }) - .UseAzureMonitor(opt => opt.ConnectionString = connectionString); + return; + } + + var openTelemetryBuilder = services.AddOpenTelemetry(); + + openTelemetryBuilder.WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation(); + metrics.AddHttpClientInstrumentation(); + metrics.AddRuntimeInstrumentation(); + metrics.AddNpgsqlInstrumentation(); + }); + + openTelemetryBuilder.WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation(options => + { + // Exclude health check requests from tracing + options.Filter = context => !context.Request.Path.StartsWithSegments("/health"); + }); + tracing.AddHttpClientInstrumentation(); + tracing.AddTurnierplanDataAccessLayer(); + tracing.AddTurnierplanDocumentRendering(); + }); + + if (hasApplicationInsights) + { + openTelemetryBuilder.UseAzureMonitor(opt => opt.ConnectionString = applicationInsightsConnectionString); + } + + if (hasOltpExporter) + { + openTelemetryBuilder.UseOtlpExporter(); } } diff --git a/src/Turnierplan.App/Properties/launchSettings.json b/src/Turnierplan.App/Properties/launchSettings.json index 3610cf4f..9b5e92c4 100644 --- a/src/Turnierplan.App/Properties/launchSettings.json +++ b/src/Turnierplan.App/Properties/launchSettings.json @@ -1,6 +1,7 @@ { + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "Turnierplan.App": { + "postgres": { "commandName": "Project", "launchBrowser": false, "applicationUrl": "http://localhost:45000", @@ -8,7 +9,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Turnierplan.App (in-memory)": { + "in-memory": { "commandName": "Project", "launchBrowser": false, "applicationUrl": "http://localhost:45000", diff --git a/src/Turnierplan.App/Turnierplan.App.csproj b/src/Turnierplan.App/Turnierplan.App.csproj index e2fd2686..e145d3d5 100644 --- a/src/Turnierplan.App/Turnierplan.App.csproj +++ b/src/Turnierplan.App/Turnierplan.App.csproj @@ -24,6 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Turnierplan.App/appsettings.Development.json b/src/Turnierplan.App/appsettings.Development.json index 68dff27e..702029d6 100644 --- a/src/Turnierplan.App/appsettings.Development.json +++ b/src/Turnierplan.App/appsettings.Development.json @@ -4,9 +4,6 @@ "Default": "Information" } }, - "Database": { - "ConnectionString": "Host=localhost;Database=turnierplan;Username=postgres;Password=P@ssw0rd" - }, "Turnierplan": { "ApplicationUrl": "http://localhost:45000", "InitialUserPassword": "P@ssw0rd" diff --git a/src/Turnierplan.AppHost/AppHost.cs b/src/Turnierplan.AppHost/AppHost.cs new file mode 100644 index 00000000..700e6337 --- /dev/null +++ b/src/Turnierplan.AppHost/AppHost.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Configuration; +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var database = builder.AddPostgres("turnierplan-postgres") + .WithDataVolume() + .WithLifetime(ContainerLifetime.Persistent) + .AddDatabase("turnierplan-database"); + +builder.AddProject("turnierplan-backend") + .WaitFor(database) + .WithHttpHealthCheck("/health") + .WithEnvironment("Database__ConnectionString", database.Resource.ConnectionStringExpression); + +if (builder.Configuration.GetValue("TURNIERPLAN_ASPIRE_RUN_CLIENT", defaultValue: false)) +{ + builder.AddJavaScriptApp("turnierplan-client", "../Turnierplan.App/Client") + .WithRunScript("start") + .WithHttpEndpoint(45001, isProxied: false) + .WithHttpHealthCheck("/index.html") + .WithNpm(install: true, installCommand: "ci"); +} +else +{ + // When starting locally without client app, add an external resource with the client app URL + // and health check so that the client app is still visible and accessible in the dashboard. + builder.AddExternalService("turnierplan-client", "http://localhost:45001") + .WithHttpHealthCheck("/index.html"); +} + +builder.Build().Run(); diff --git a/src/Turnierplan.AppHost/Properties/launchSettings.json b/src/Turnierplan.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..f9bc36b0 --- /dev/null +++ b/src/Turnierplan.AppHost/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "with client": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:15285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19065", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18247", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20277", + "TURNIERPLAN_ASPIRE_RUN_CLIENT": "true" + } + }, + "without client": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:15285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19065", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18247", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20277" + } + } + } +} diff --git a/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj b/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj new file mode 100644 index 00000000..ee8ce96b --- /dev/null +++ b/src/Turnierplan.AppHost/Turnierplan.AppHost.csproj @@ -0,0 +1,20 @@ + + + + + net10.0 + enable + enable + Exe + 523f324d-1695-4d45-a000-a79765a1468a + + + + + + + + + + + diff --git a/src/Turnierplan.AppHost/appsettings.Development.json b/src/Turnierplan.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/src/Turnierplan.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Turnierplan.AppHost/appsettings.json b/src/Turnierplan.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/src/Turnierplan.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/docker-compose.yml b/src/docker-compose.yml deleted file mode 100644 index 31727155..00000000 --- a/src/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - turnierplan.database: - image: postgres:17.0 - environment: - - POSTGRES_PASSWORD=P@ssw0rd - - POSTGRES_DB=turnierplan - volumes: - - turnierplan-data:/var/lib/postgresql/data - ports: - - "5432:5432" - restart: unless-stopped - -volumes: - turnierplan-data: - diff --git a/src/turnierplan.NET.slnx b/src/turnierplan.NET.slnx index ba0e0a96..03a88477 100644 --- a/src/turnierplan.NET.slnx +++ b/src/turnierplan.NET.slnx @@ -1,6 +1,5 @@ - @@ -15,6 +14,7 @@ +