[Prod] More logging #4

Merged
thomas.nuyken merged 7 commits from main into production 2025-07-21 21:25:22 +02:00
15 changed files with 117 additions and 2029 deletions

View File

@@ -2,7 +2,7 @@
Vegasco (**VE**hicle **GAS** **CO**nsumption) application. Vegasco (**VE**hicle **GAS** **CO**nsumption) application.
Includes the backend (`src/Vegasco.Server.Api`) and the frontend (`src/Vegasco-Web`). Utilizes [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview). Includes the backend (`src/Vegasco.Server.Api`) and the frontend (`src/Vegasco-Web`). Uses [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview).
## Getting Started ## Getting Started
@@ -67,3 +67,17 @@ creates a Postgres database as a docker container, and starts the Api with the c
Ensure you have an identity provider set up, for example Keycloak, and configured the relevant options described above. Ensure you have an identity provider set up, for example Keycloak, and configured the relevant options described above.
Then, to run the application, ensure you have Docker running, then run either the `http` or `https` launch profile of the `Vegasco.Server.AppHost` project. Then, to run the application, ensure you have Docker running, then run either the `http` or `https` launch profile of the `Vegasco.Server.AppHost` project.
## Deployment
Build server by running in project root:
```shell
docker build . -t docker.nuyken.dev/vegasco/api:main
```
Builder web client by running in `src/Vegasco-Web`:
```shell
docker build -t docker.nuyken.dev/vegasco/web:main --build-arg CONFIGURATION=production .
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,7 +3,6 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start:withInstall": "pnpm install && pnpm run start",
"start": "run-script-os", "start": "run-script-os",
"start:win32": "ng serve --port %PORT% --configuration development", "start:win32": "ng serve --port %PORT% --configuration development",
"start:default": "ng serve --port $PORT --configuration development", "start:default": "ng serve --port $PORT --configuration development",

View File

@@ -39,19 +39,41 @@ public static class CreateCar
IEnumerable<IValidator<Request>> validators, IEnumerable<IValidator<Request>> validators,
ApplicationDbContext dbContext, ApplicationDbContext dbContext,
UserAccessor userAccessor, UserAccessor userAccessor,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
ILogger logger = loggerFactory.CreateLogger(nameof(CreateCar));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken); List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
if (failedValidations.Count > 0) if (failedValidations.Count > 0)
{ {
logger.LogDebug(
"Validation failed for request {@Request} with errors {@Errors}",
request,
failedValidations
.Where(x => !x.IsValid)
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary())); return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
} }
bool isDuplicate = await dbContext.Cars
.AnyAsync(x => x.Name.ToUpper() == request.Name.ToUpper(), cancellationToken);
if (isDuplicate)
{
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
return TypedResults.Conflict();
}
string userId = userAccessor.GetUserId(); string userId = userAccessor.GetUserId();
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken); User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
if (user is null) if (user is null)
{ {
logger.LogDebug("User with ID '{UserId}' not found, creating new user", userId);
user = new User user = new User
{ {
Id = userId Id = userId
@@ -65,17 +87,11 @@ public static class CreateCar
UserId = userId UserId = userId
}; };
bool isDuplicate = await dbContext.Cars
.AnyAsync(x => x.Name.ToUpper() == request.Name.ToUpper(), cancellationToken);
if (isDuplicate)
{
return TypedResults.Conflict();
}
await dbContext.Cars.AddAsync(car, cancellationToken); await dbContext.Cars.AddAsync(car, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
logger.LogTrace("Created new car: {@Car}", car);
Response response = new(car.Id.Value, car.Name); Response response = new(car.Id.Value, car.Name);
return TypedResults.Created($"/v1/cars/{car.Id}", response); return TypedResults.Created($"/v1/cars/{car.Id}", response);
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
using Vegasco.Server.Api.Persistence; using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Cars; namespace Vegasco.Server.Api.Cars;
@@ -21,6 +22,9 @@ public static class DeleteCar
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Activity? activity = Activity.Current;
activity?.SetTag("id", id);
int rows = await dbContext.Cars int rows = await dbContext.Cars
.Where(x => x.Id == new CarId(id)) .Where(x => x.Id == new CarId(id))
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
using Vegasco.Server.Api.Persistence; using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Cars; namespace Vegasco.Server.Api.Cars;
@@ -34,11 +35,15 @@ public static class GetCars
ApplicationDbContext dbContext, ApplicationDbContext dbContext,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Activity? activity = Activity.Current;
List<ResponseDto> cars = await dbContext.Cars List<ResponseDto> cars = await dbContext.Cars
.Select(x => new ResponseDto(x.Id.Value, x.Name)) .Select(x => new ResponseDto(x.Id.Value, x.Name))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
ApiResponse response = new ApiResponse activity?.SetTag("carCount", cars.Count);
ApiResponse response = new()
{ {
Cars = cars Cars = cars
}; };

View File

@@ -10,6 +10,7 @@ namespace Vegasco.Server.Api.Cars;
public static class UpdateCar public static class UpdateCar
{ {
public record Request(string Name); public record Request(string Name);
public record Response(Guid Id, string Name); public record Response(Guid Id, string Name);
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder) public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
@@ -40,11 +41,22 @@ public static class UpdateCar
IEnumerable<IValidator<Request>> validators, IEnumerable<IValidator<Request>> validators,
ApplicationDbContext dbContext, ApplicationDbContext dbContext,
UserAccessor userAccessor, UserAccessor userAccessor,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var logger = loggerFactory.CreateLogger(nameof(UpdateCar));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken); List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
if (failedValidations.Count > 0) if (failedValidations.Count > 0)
{ {
logger.LogDebug(
"Validation failed for request {@Request} with errors {@Errors}",
request,
failedValidations
.Where(x => !x.IsValid)
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary())); return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
} }
@@ -60,13 +72,16 @@ public static class UpdateCar
if (isDuplicate) if (isDuplicate)
{ {
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
return TypedResults.Conflict(); return TypedResults.Conflict();
} }
car.Name = request.Name.Trim(); car.Name = request.Name.Trim();
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
logger.LogTrace("Updated car: {@Car}", car);
Response response = new(car.Id.Value, car.Name); Response response = new(car.Id.Value, car.Name);
return TypedResults.Ok(response); return TypedResults.Ok(response);
} }
} }

View File

@@ -1,5 +1,6 @@
using FluentValidation; using FluentValidation;
using FluentValidation.Results; using FluentValidation.Results;
using System.Diagnostics;
using Vegasco.Server.Api.Cars; using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Common; using Vegasco.Server.Api.Common;
using Vegasco.Server.Api.Persistence; using Vegasco.Server.Api.Persistence;
@@ -49,11 +50,22 @@ public static class CreateConsumption
ApplicationDbContext dbContext, ApplicationDbContext dbContext,
Request request, Request request,
IEnumerable<IValidator<Request>> validators, IEnumerable<IValidator<Request>> validators,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
ILogger logger = loggerFactory.CreateLogger(nameof(CreateConsumption));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken); List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
if (failedValidations.Count > 0) if (failedValidations.Count > 0)
{ {
logger.LogDebug(
"Validation failed for request {@Request} with errors {@Errors}",
request,
failedValidations
.Where(x => !x.IsValid)
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary())); return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
} }
@@ -74,6 +86,8 @@ public static class CreateConsumption
dbContext.Consumptions.Add(consumption); dbContext.Consumptions.Add(consumption);
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
logger.LogTrace("Created new consumption: {@Consumption}", consumption);
return TypedResults.Created($"consumptions/{consumption.Id.Value}", return TypedResults.Created($"consumptions/{consumption.Id.Value}",
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount,
consumption.CarId.Value)); consumption.CarId.Value));

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
using Vegasco.Server.Api.Persistence; using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Consumptions; namespace Vegasco.Server.Api.Consumptions;
@@ -21,6 +22,9 @@ public static class DeleteConsumption
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Activity? activity = Activity.Current;
activity?.SetTag("id", id);
int rows = await dbContext.Consumptions int rows = await dbContext.Consumptions
.Where(x => x.Id == new ConsumptionId(id)) .Where(x => x.Id == new ConsumptionId(id))
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
using Vegasco.Server.Api.Cars; using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Persistence; using Vegasco.Server.Api.Persistence;
@@ -49,8 +50,14 @@ public static class GetConsumptions
private static async Task<Ok<ApiResponse>> Endpoint( private static async Task<Ok<ApiResponse>> Endpoint(
[AsParameters] Request request, [AsParameters] Request request,
ApplicationDbContext dbContext, ApplicationDbContext dbContext,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
ILogger logger = loggerFactory.CreateLogger(nameof(GetConsumptions));
logger.LogTrace("Received request to get consumptions with parameters: {@Request}", request);
Activity? activity = Activity.Current;
Dictionary<CarId, List<Consumption>> consumptionsByCar = await dbContext.Consumptions Dictionary<CarId, List<Consumption>> consumptionsByCar = await dbContext.Consumptions
.Include(x => x.Car) .Include(x => x.Car)
.GroupBy(x => x.CarId) .GroupBy(x => x.CarId)
@@ -83,6 +90,8 @@ public static class GetConsumptions
literPer100Km)); literPer100Km));
} }
} }
activity?.SetTag("consumptionCount", responses.Count);
ApiResponse apiResponse = new() ApiResponse apiResponse = new()
{ {

View File

@@ -48,11 +48,22 @@ public static class UpdateConsumption
Guid id, Guid id,
Request request, Request request,
IEnumerable<IValidator<Request>> validators, IEnumerable<IValidator<Request>> validators,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
ILogger logger = loggerFactory.CreateLogger(nameof(UpdateConsumption));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken); List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
if (failedValidations.Count > 0) if (failedValidations.Count > 0)
{ {
logger.LogDebug(
"Validation failed for request {@Request} with errors {@Errors}",
request,
failedValidations
.Where(x => !x.IsValid)
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary())); return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
} }
@@ -68,6 +79,8 @@ public static class UpdateConsumption
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
logger.LogTrace("Updated consumption: {@Consumption}", consumption);
return TypedResults.Ok(new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.CarId.Value)); return TypedResults.Ok(new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.CarId.Value));
} }
} }

View File

@@ -23,10 +23,10 @@ IResourceBuilder<ProjectResource> api = builder
.WaitFor(postgres); .WaitFor(postgres);
builder builder
.AddNpmApp("Vegasco-Web", "../Vegasco-Web", scriptName: "start:withInstall") .AddNpmApp("Vegasco-Web", "../Vegasco-Web")
.WithReference(api) .WithReference(api)
.WaitFor(api) .WaitFor(api)
.WithHttpEndpoint(port: 44200, env: "PORT", isProxied: false) .WithHttpEndpoint(port: 44200, env: "PORT")
.WithExternalHttpEndpoints() .WithExternalHttpEndpoints()
.WithHttpHealthCheck("/", 200); .WithHttpHealthCheck("/", 200);

View File

@@ -25,4 +25,14 @@
<ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" /> <ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="RestoreNpm" BeforeTargets="Build" Condition=" '$(DesignTimeBuild)' != 'true' ">
<ItemGroup>
<PackageJsons Include="..\*\package.json" />
</ItemGroup>
<!-- Install npm packages if node_modules is missing -->
<Message Importance="Normal" Text="Installing npm packages for %(PackageJsons.RelativeDir)" Condition="!Exists('%(PackageJsons.RootDir)%(PackageJsons.Directory)/node_modules')" />
<Exec Command="pnpm install" WorkingDirectory="%(PackageJsons.RootDir)%(PackageJsons.Directory)" Condition="!Exists('%(PackageJsons.RootDir)%(PackageJsons.Directory)/node_modules')" />
</Target>
</Project> </Project>

View File

@@ -10,7 +10,6 @@
<Project Path="src/Vegasco.Server.AppHost.Shared/Vegasco.Server.AppHost.Shared.csproj" /> <Project Path="src/Vegasco.Server.AppHost.Shared/Vegasco.Server.AppHost.Shared.csproj" />
<Project Path="src/Vegasco.Server.AppHost/Vegasco.Server.AppHost.csproj" /> <Project Path="src/Vegasco.Server.AppHost/Vegasco.Server.AppHost.csproj" />
<Project Path="src/Vegasco.Server.ServiceDefaults/Vegasco.Server.ServiceDefaults.csproj" /> <Project Path="src/Vegasco.Server.ServiceDefaults/Vegasco.Server.ServiceDefaults.csproj" />
<Project Path="src\UploadData\UploadData.csproj" Type="Classic C#" />
</Folder> </Folder>
<Folder Name="/tests/"> <Folder Name="/tests/">
<Project Path="tests/Vegasco.Server.Api.Tests.Integration/Vegasco.Server.Api.Tests.Integration.csproj" /> <Project Path="tests/Vegasco.Server.Api.Tests.Integration/Vegasco.Server.Api.Tests.Integration.csproj" />