[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.
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
@@ -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.
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",
"scripts": {
"ng": "ng",
"start:withInstall": "pnpm install && pnpm run start",
"start": "run-script-os",
"start:win32": "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,
ApplicationDbContext dbContext,
UserAccessor userAccessor,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken)
{
ILogger logger = loggerFactory.CreateLogger(nameof(CreateCar));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
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()));
}
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();
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
if (user is null)
{
logger.LogDebug("User with ID '{UserId}' not found, creating new user", userId);
user = new User
{
Id = userId
@@ -65,17 +87,11 @@ public static class CreateCar
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.SaveChangesAsync(cancellationToken);
logger.LogTrace("Created new car: {@Car}", car);
Response response = new(car.Id.Value, car.Name);
return TypedResults.Created($"/v1/cars/{car.Id}", response);
}

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ namespace Vegasco.Server.Api.Cars;
public static class UpdateCar
{
public record Request(string Name);
public record Response(Guid Id, string Name);
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
@@ -40,11 +41,22 @@ public static class UpdateCar
IEnumerable<IValidator<Request>> validators,
ApplicationDbContext dbContext,
UserAccessor userAccessor,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken)
{
var logger = loggerFactory.CreateLogger(nameof(UpdateCar));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
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()));
}
@@ -60,12 +72,15 @@ public static class UpdateCar
if (isDuplicate)
{
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
return TypedResults.Conflict();
}
car.Name = request.Name.Trim();
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogTrace("Updated car: {@Car}", car);
Response response = new(car.Id.Value, car.Name);
return TypedResults.Ok(response);
}

View File

@@ -1,5 +1,6 @@
using FluentValidation;
using FluentValidation.Results;
using System.Diagnostics;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Common;
using Vegasco.Server.Api.Persistence;
@@ -49,11 +50,22 @@ public static class CreateConsumption
ApplicationDbContext dbContext,
Request request,
IEnumerable<IValidator<Request>> validators,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken)
{
ILogger logger = loggerFactory.CreateLogger(nameof(CreateConsumption));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
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()));
}
@@ -74,6 +86,8 @@ public static class CreateConsumption
dbContext.Consumptions.Add(consumption);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogTrace("Created new consumption: {@Consumption}", consumption);
return TypedResults.Created($"consumptions/{consumption.Id.Value}",
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount,
consumption.CarId.Value));

View File

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

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
using Vegasco.Server.Api.Cars;
using Vegasco.Server.Api.Persistence;
@@ -49,8 +50,14 @@ public static class GetConsumptions
private static async Task<Ok<ApiResponse>> Endpoint(
[AsParameters] Request request,
ApplicationDbContext dbContext,
ILoggerFactory loggerFactory,
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
.Include(x => x.Car)
.GroupBy(x => x.CarId)
@@ -84,6 +91,8 @@ public static class GetConsumptions
}
}
activity?.SetTag("consumptionCount", responses.Count);
ApiResponse apiResponse = new()
{
Consumptions = responses

View File

@@ -48,11 +48,22 @@ public static class UpdateConsumption
Guid id,
Request request,
IEnumerable<IValidator<Request>> validators,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken)
{
ILogger logger = loggerFactory.CreateLogger(nameof(UpdateConsumption));
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
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()));
}
@@ -68,6 +79,8 @@ public static class UpdateConsumption
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));
}
}

View File

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

View File

@@ -25,4 +25,14 @@
<ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
</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>

View File

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