From 08f7426a5e439e74b03bf2cb461cb1cc6eb2f056 Mon Sep 17 00:00:00 2001 From: Sheuly Debnath Date: Thu, 2 Jul 2026 20:45:33 +0200 Subject: [PATCH] feat(logging): add structured logging to reliability snapshot generation --- src/TransitPulse.API/Program.cs | 32 +++---- src/TransitPulse.API/TransitPulse.API.csproj | 2 +- .../DependencyInjection.cs | 23 +++++ .../GenerateReliabilitySnapshotHandler.cs | 23 ++++- .../TransitPulse.Application.csproj | 2 + .../DependencyInjection.cs | 17 ++-- .../Services/ReliabilityCalculator.cs | 2 +- ...GenerateReliabilitySnapshotHandlerTests.cs | 14 ++- .../GetReliabilitySnapshotHandlerTests.cs | 88 +++++++++++++++++++ .../Reliability/ReliabiltyCalculatorTests.cs | 4 +- 10 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 src/TransitPulse.Application/DependencyInjection.cs create mode 100644 tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs diff --git a/src/TransitPulse.API/Program.cs b/src/TransitPulse.API/Program.cs index 28a7414..1e3e2a9 100644 --- a/src/TransitPulse.API/Program.cs +++ b/src/TransitPulse.API/Program.cs @@ -1,32 +1,20 @@ -using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; -using TransitPulse.Application.Interfaces; -using TransitPulse.Infrastructure.Repositories; -using TransitPulse.Infrastructure.Services; +using TransitPulse.Application; +using TransitPulse.Infrastructure; using TransitPulse.API.Middleware; -using Microsoft.EntityFrameworkCore; -using TransitPulse.Infrastructure.Persistence; -using TransitPulse.Domain.Entities; -using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshots; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddControllers(); +// ASP.NET Core framework +builder.Services.AddControllers(); //Registers the Web API framework. +builder.Services.AddOpenApi(); //Registers the OpenAPI documentation services. -builder.Services.AddOpenApi(); -builder.Services.AddDbContext( - options => options.UseNpgsql(builder.Configuration.GetConnectionString("TransitPulseDb"))); - -builder.Services.AddScoped(); - -builder.Services.AddScoped(); - - -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +// Application layer +builder.Services.AddApplicationServices(); // Registers your business logic +// Infrastructure layer +builder.Services.AddInfrastructureServices(builder.Configuration); //Registers database, repositories, and infrastructure services. var app = builder.Build(); + app.UseMiddleware(); if (app.Environment.IsDevelopment()) diff --git a/src/TransitPulse.API/TransitPulse.API.csproj b/src/TransitPulse.API/TransitPulse.API.csproj index 5884349..bdb4fc7 100644 --- a/src/TransitPulse.API/TransitPulse.API.csproj +++ b/src/TransitPulse.API/TransitPulse.API.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/TransitPulse.Application/DependencyInjection.cs b/src/TransitPulse.Application/DependencyInjection.cs new file mode 100644 index 0000000..3c43632 --- /dev/null +++ b/src/TransitPulse.Application/DependencyInjection.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; +using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshot; +using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshots; + +namespace TransitPulse.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplicationServices( + this IServiceCollection services) + { + services.AddScoped(); + + services.AddScoped(); + + services.AddScoped(); + + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs b/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs index 1d2c435..6481775 100644 --- a/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs +++ b/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs @@ -2,6 +2,7 @@ using TransitPulse.Application.Exceptions; using TransitPulse.Application.Interfaces; using TransitPulse.Domain.Entities; +using Microsoft.Extensions.Logging; namespace TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; @@ -12,23 +13,30 @@ public class GenerateReliabilitySnapshotHandler private readonly IReliabilityCalculator _reliabilityCalculator; private readonly IReliabilitySnapshotRepository _snapshotRepository; private readonly GenerateReliabilitySnapshotValidator _validator; + private readonly ILogger _logger; public GenerateReliabilitySnapshotHandler( IRouteEventRepository routeEventRepository, IReliabilityCalculator reliabilityCalculator, IReliabilitySnapshotRepository snapshotRepository, - GenerateReliabilitySnapshotValidator validator) + GenerateReliabilitySnapshotValidator validator, + ILogger logger + ) { _routeEventRepository = routeEventRepository; _reliabilityCalculator = reliabilityCalculator; _snapshotRepository = snapshotRepository; _validator = validator; + _logger = logger; } public async Task HandleAsync( GenerateReliabilitySnapshotCommand command, CancellationToken cancellationToken) { + _logger.LogInformation( + "Starting reliability snapshot generation for route {RouteId}.", command.RouteId); + _validator.ValidateAndThrow(command); var routeEvents = @@ -41,8 +49,14 @@ await _routeEventRepository if (!routeEvents.Any()) { + _logger.LogWarning( + "No route events found for route {RouteId} between {PeriodStart} and {PeriodEnd}.", + command.RouteId, + command.PeriodStart, + command.PeriodEnd); + throw new NotFoundException( - "No route events found for the specified period."); + $"No route events found for route {command.RouteId} between {command.PeriodStart:d} and {command.PeriodEnd:d}."); } //Single Responsibility Principle in IReliabilityCalculator calculate var metrics = @@ -64,6 +78,11 @@ await _snapshotRepository.AddAsync( snapshot, cancellationToken); + _logger.LogInformation( + "Reliability snapshot {SnapshotId} generated successfully for route {RouteId}.", + snapshot.Id, + command.RouteId); + return new GenerateReliabilitySnapshotResult( snapshot.Id, metrics.Score, diff --git a/src/TransitPulse.Application/TransitPulse.Application.csproj b/src/TransitPulse.Application/TransitPulse.Application.csproj index 0ae40f4..390c585 100644 --- a/src/TransitPulse.Application/TransitPulse.Application.csproj +++ b/src/TransitPulse.Application/TransitPulse.Application.csproj @@ -12,6 +12,8 @@ + + diff --git a/src/TransitPulse.Infrastructure/DependencyInjection.cs b/src/TransitPulse.Infrastructure/DependencyInjection.cs index 7b0501b..deca58a 100644 --- a/src/TransitPulse.Infrastructure/DependencyInjection.cs +++ b/src/TransitPulse.Infrastructure/DependencyInjection.cs @@ -1,6 +1,9 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; using TransitPulse.Application.Interfaces; +using TransitPulse.Infrastructure.Persistence; using TransitPulse.Infrastructure.Repositories; using TransitPulse.Infrastructure.Services; @@ -10,16 +13,16 @@ namespace TransitPulse.Infrastructure; public static class DependencyInjection { public static IServiceCollection AddInfrastructureServices( - this IServiceCollection services) + this IServiceCollection services, IConfiguration configuration) { - services.AddSingleton(); + services.AddDbContext(options => + options.UseNpgsql(configuration.GetConnectionString("TransitPulseDb"))); - services.AddSingleton(); + services.AddScoped(); - services.AddSingleton(); + services.AddScoped(); + + services.AddSingleton(); return services; } diff --git a/src/TransitPulse.Infrastructure/Services/ReliabilityCalculator.cs b/src/TransitPulse.Infrastructure/Services/ReliabilityCalculator.cs index aedf0e9..723e5d1 100644 --- a/src/TransitPulse.Infrastructure/Services/ReliabilityCalculator.cs +++ b/src/TransitPulse.Infrastructure/Services/ReliabilityCalculator.cs @@ -4,7 +4,7 @@ namespace TransitPulse.Infrastructure.Services; -public class ReliabiltyCalculator : IReliabilityCalculator +public class ReliabilityCalculator : IReliabilityCalculator { public ReliabilityMetrics Calculate( IEnumerable routeEvents) diff --git a/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs b/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs index 9924161..8b960a4 100644 --- a/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs +++ b/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs @@ -5,6 +5,8 @@ using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; using TransitPulse.Application.Interfaces; using TransitPulse.Domain.Entities; +using Microsoft.Extensions.Logging; + public class GenerateReliabilitySnapshotHandlerTests { @@ -25,6 +27,9 @@ public async Task HandleAsync_WithNoRouteEvents_ShouldThrowInvalidOperationExcep var validator = new GenerateReliabilitySnapshotValidator(); + var logger = + new Mock>(); + routeEventRepository .Setup(repository => repository.GetByRouteAndPeriodAsync( @@ -39,7 +44,8 @@ public async Task HandleAsync_WithNoRouteEvents_ShouldThrowInvalidOperationExcep routeEventRepository.Object, reliabilityCalculator.Object, snapshotRepository.Object, - validator); + validator, + logger.Object); var command = new GenerateReliabilitySnapshotCommand( @@ -72,6 +78,9 @@ public async Task HandleAsync_WithValidRouteEvents_ShouldSaveSnapshotAndReturnRe var validator = new GenerateReliabilitySnapshotValidator(); + var logger = + new Mock>(); + var routeEvents = new List { @@ -116,7 +125,8 @@ public async Task HandleAsync_WithValidRouteEvents_ShouldSaveSnapshotAndReturnRe routeEventRepository.Object, reliabilityCalculator.Object, snapshotRepository.Object, - validator); + validator, + logger.Object); var command = new GenerateReliabilitySnapshotCommand( diff --git a/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs b/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs new file mode 100644 index 0000000..7130f0f --- /dev/null +++ b/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs @@ -0,0 +1,88 @@ +using FluentAssertions; +using FluentValidation; +using Moq; +using TransitPulse.Application.Exceptions; +using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; +using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshot; +using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshots; +using TransitPulse.Application.Interfaces; +using TransitPulse.Domain.Entities; + +namespace TransitPulse.Application.Tests.Reliability; + +public class GetReliabilitySnapshotHandlerTests +{ + + [Fact] + public async Task HandleAsync_WithSnapshotNotFound_ShouldThrowNotFoundException() + { + // Arrange + + var snapshotRepository = new Mock(); + + snapshotRepository.Setup(repository => + repository.GetByIdAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync((ReliabilitySnapshot?)null); + + var handler = new GetReliabilitySnapshotHandler(snapshotRepository.Object); + + // Act + + Func action = () => handler.HandleAsync( + Guid.NewGuid(), + CancellationToken.None); + + // Assert + + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithSnapshot_ShouldReturnMappedResult() + { + // Arrange + + var routeId = Guid.NewGuid(); + + var snapshot = + new ReliabilitySnapshot( + routeId, + 92, + 2.5, + 10, + 90, + new DateTime(2026, 1, 1), + new DateTime(2026, 1, 2), + new DateTime(2026, 1, 3)); + + var snapshotRepository = + new Mock(); + + snapshotRepository.Setup(repository => + repository.GetByIdAsync( + snapshot.Id, + It.IsAny())).ReturnsAsync(snapshot); + + var handler = new GetReliabilitySnapshotHandler(snapshotRepository.Object); + + // Act + + var result = await handler.HandleAsync(snapshot.Id, CancellationToken.None); + + // Assert + + result.Score.Should().Be(92); + + result.AverageDelay.Should().Be(2.5); + + result.CancellationRate.Should().Be(10); + + result.OnTimePercentage.Should().Be(90); + + result.CalculatedAt.Should().Be(snapshot.CalculatedAt); + + result.SnapshotId.Should().Be(snapshot.Id); + } +} \ No newline at end of file diff --git a/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs b/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs index 47a2712..ed6411b 100644 --- a/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs +++ b/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs @@ -10,7 +10,7 @@ public class ReliabiltyCalculatorTests public void Calculate_ShouldReturnExpectedMetrics() { // Arrange - var calculator = new ReliabiltyCalculator(); + var calculator = new ReliabilityCalculator(); var routeEvents = new List { @@ -55,7 +55,7 @@ public void Calculate_ShouldReturnExpectedMetrics() public void Calculate_WithNoEvents_ShouldThrowInvalidOperationException() { // Arrange - var calculator = new ReliabiltyCalculator(); + var calculator = new ReliabilityCalculator(); // Act Action action =