Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions AdaptiveRemote.sln
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.Layo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.LayoutProcessingService.Tests", "test\AdaptiveRemote.Backend.LayoutProcessingService.Tests\AdaptiveRemote.Backend.LayoutProcessingService.Tests.csproj", "{A829A88B-C42D-4D3B-8CDE-621862E4B39C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.Common", "src\AdaptiveRemote.Backend.Common\AdaptiveRemote.Backend.Common.csproj", "{1F36A31B-299C-480C-B974-F4CE67C6F034}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -304,6 +306,18 @@ Global
{A829A88B-C42D-4D3B-8CDE-621862E4B39C}.Release|x64.Build.0 = Release|Any CPU
{A829A88B-C42D-4D3B-8CDE-621862E4B39C}.Release|x86.ActiveCfg = Release|Any CPU
{A829A88B-C42D-4D3B-8CDE-621862E4B39C}.Release|x86.Build.0 = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Debug|x64.ActiveCfg = Debug|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Debug|x64.Build.0 = Debug|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Debug|x86.ActiveCfg = Debug|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Debug|x86.Build.0 = Debug|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|Any CPU.Build.0 = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|x64.ActiveCfg = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|x64.Build.0 = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|x86.ActiveCfg = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -325,6 +339,7 @@ Global
{352E5981-CC33-4474-8203-9CE241F42281} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
{F341B9BA-8517-447F-93B3-7E09AAD0F0E1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A829A88B-C42D-4D3B-8CDE-621862E4B39C} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
{1F36A31B-299C-480C-B974-F4CE67C6F034} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {556A11E4-2F89-4600-9831-8162F067EC3E}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>

</Project>
56 changes: 56 additions & 0 deletions src/AdaptiveRemote.Backend.Common/Logging/FileLoggerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.Extensions.Logging;

namespace AdaptiveRemote.Backend.Common.Logging;

public static class FileLoggerExtensions
{
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filePath)
{
builder.AddProvider(new SimpleFileLoggerProvider(filePath));
return builder;
}
}

internal sealed class SimpleFileLoggerProvider : ILoggerProvider
{
private readonly string _filePath;
private readonly object _lock = new();

public SimpleFileLoggerProvider(string filePath)
{
_filePath = filePath;
}

public ILogger CreateLogger(string categoryName) => new SimpleFileLogger(_filePath, _lock, categoryName);

public void Dispose() { }

private class SimpleFileLogger : ILogger
{
private readonly string _filePath;
private readonly object _lock;
private readonly string _categoryName;

public SimpleFileLogger(string filePath, object lockObj, string categoryName)
{
_filePath = filePath;
_lock = lockObj;
_categoryName = categoryName;
}

IDisposable ILogger.BeginScope<TState>(TState state) => null!;
Comment thread
jodavis marked this conversation as resolved.
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
string message = $"[{DateTime.Now:O}] [{logLevel}] [{_categoryName}] {formatter(state, exception)}";
lock (_lock)
{
File.AppendAllText(_filePath, message + "\n");
if (exception != null)
{
File.AppendAllText(_filePath, exception + "\n");
}
}
}
}
}
131 changes: 131 additions & 0 deletions src/AdaptiveRemote.Backend.Common/Logging/MessageLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using Microsoft.Extensions.Logging;

namespace AdaptiveRemote.Backend.Common.Logging;

/// <summary>
/// Centralized logging messages for CompiledLayoutService.
/// All log messages MUST be defined here as [LoggerMessage] source-generated methods.
/// Event ID ranges:
/// 1100-1199: CompiledLayoutService
/// </summary>
public static partial class MessageLogger
{
// Common service messages
[LoggerMessage(EventId = 1100, Level = LogLevel.Information, Message = "{ServiceName} starting")]
public static partial void ServiceStarting(this ILogger logger, string serviceName);

[LoggerMessage(EventId = 1101, Level = LogLevel.Information, Message = "{ServiceName} started successfully on {ListenAddress}")]
public static partial void ServiceStarted(this ILogger logger, string serviceName, string listenAddress);

[LoggerMessage(EventId = 1102, Level = LogLevel.Information, Message = "{Method} {Path} request received for userId={UserId}")]
public static partial void AuthenticatedRequestStarted(this ILogger logger, string method, string path, string userId);

[LoggerMessage(EventId = 1103, Level = LogLevel.Information, Message = "{Method} {Path} request received")]
public static partial void UnauthenticatedRequestStarted(this ILogger logger, string method, string path);

[LoggerMessage(EventId = 1104, Level = LogLevel.Information, Message = "{Method} {Path} request handled")]
public static partial void RequestHandled(this ILogger logger, string method, string path);

[LoggerMessage(EventId = 1105, Level = LogLevel.Information, Message = "Health check successful")]
public static partial void HealthCheckSuccessful(this ILogger logger);

[LoggerMessage(EventId = 1106, Level = LogLevel.Error, Message = "Error processing health check request")]
public static partial void ErrorProcessingHealthCheck(this ILogger logger, Exception exception);

[LoggerMessage(
EventId = 1107,
Level = LogLevel.Error,
Message = "LocalStack dependency check failed at {HealthUrl}: {FailureReason}. LocalStack is required for local development. See docs/local-dev.md for setup instructions")]
public static partial void LocalStackDependencyUnavailable(this ILogger logger, string healthUrl, string failureReason, Exception? exception);

// CompiledLayoutService-specific messages
[LoggerMessage(EventId = 1301, Level = LogLevel.Information, Message = "Returning active compiled layout Id={LayoutId}")]
public static partial void ReturningActiveLayout(this ILogger logger, Guid layoutId);

[LoggerMessage(EventId = 1303, Level = LogLevel.Error, Message = "Error retrieving active layout for userId={UserId}")]
public static partial void ErrorRetrievingActiveLayout(this ILogger logger, string userId, Exception exception);

// RawLayoutService-specific messages
[LoggerMessage(EventId = 1201, Level = LogLevel.Information, Message = "Raw layout created successfully: Id={LayoutId}")]
public static partial void RawLayoutCreated(this ILogger logger, Guid layoutId);

[LoggerMessage(EventId = 1202, Level = LogLevel.Information, Message = "Raw layout updated successfully: Id={LayoutId}")]
public static partial void RawLayoutUpdated(this ILogger logger, Guid layoutId);

[LoggerMessage(EventId = 1203, Level = LogLevel.Information, Message = "Raw layout deleted successfully: Id={LayoutId}")]
public static partial void RawLayoutDeleted(this ILogger logger, Guid layoutId);

[LoggerMessage(EventId = 1204, Level = LogLevel.Error, Message = "Error retrieving raw layouts for userId={UserId}")]
public static partial void ErrorRetrievingRawLayouts(this ILogger logger, string userId, Exception exception);

[LoggerMessage(EventId = 1205, Level = LogLevel.Error, Message = "Error retrieving raw layout Id={LayoutId} for userId={UserId}")]
public static partial void ErrorRetrievingRawLayout(this ILogger logger, Guid layoutId, string userId, Exception exception);

[LoggerMessage(EventId = 1206, Level = LogLevel.Error, Message = "Error creating raw layout for userId={UserId}")]
public static partial void ErrorCreatingRawLayout(this ILogger logger, string userId, Exception exception);

[LoggerMessage(EventId = 1207, Level = LogLevel.Error, Message = "Error updating raw layout Id={LayoutId} for userId={UserId}")]
public static partial void ErrorUpdatingRawLayout(this ILogger logger, Guid layoutId, string userId, Exception exception);

[LoggerMessage(EventId = 1208, Level = LogLevel.Error, Message = "Error deleting raw layout Id={LayoutId} for userId={UserId}")]
public static partial void ErrorDeletingRawLayout(this ILogger logger, Guid layoutId, string userId, Exception exception);

[LoggerMessage(EventId = 1209, Level = LogLevel.Information, Message = "SQS trigger enqueued; rawLayoutId={RawLayoutId} queueUrl={QueueUrl}")]
public static partial void SqsTriggerEnqueued(this ILogger logger, Guid rawLayoutId, string queueUrl);

[LoggerMessage(EventId = 1210, Level = LogLevel.Error, Message = "Failed to enqueue SQS trigger; rawLayoutId={RawLayoutId}")]
public static partial void ErrorEnqueuingSqsTrigger(this ILogger logger, Guid rawLayoutId, Exception exception);

[LoggerMessage(EventId = 1211, Level = LogLevel.Information, Message = "Validation result updated for raw layout Id={LayoutId}")]
public static partial void ValidationResultUpdated(this ILogger logger, Guid layoutId);

[LoggerMessage(EventId = 1212, Level = LogLevel.Error, Message = "Error updating validation result for raw layout Id={LayoutId}")]
public static partial void ErrorUpdatingValidationResult(this ILogger logger, Guid layoutId, Exception exception);

// LayoutProcessingService-specific messages
[LoggerMessage(EventId = 1706, Level = LogLevel.Information, Message = "SQS polling loop started; queue={QueueUrl}")]
public static partial void SqsPollingStarted(this ILogger logger, string queueUrl);

[LoggerMessage(EventId = 1707, Level = LogLevel.Information, Message = "SQS polling loop stopped")]
public static partial void SqsPollingStopped(this ILogger logger);

[LoggerMessage(EventId = 1708, Level = LogLevel.Information, Message = "SQS message received; rawLayoutId={RawLayoutId} receiptHandle={ReceiptHandle}")]
public static partial void SqsMessageReceived(this ILogger logger, Guid rawLayoutId, string receiptHandle);

[LoggerMessage(EventId = 1709, Level = LogLevel.Information, Message = "Layout compiled successfully; rawLayoutId={RawLayoutId}")]
public static partial void LayoutCompiled(this ILogger logger, Guid rawLayoutId);

[LoggerMessage(EventId = 1710, Level = LogLevel.Information, Message = "Layout validation passed; rawLayoutId={RawLayoutId}")]
public static partial void LayoutValidationPassed(this ILogger logger, Guid rawLayoutId);

[LoggerMessage(EventId = 1711, Level = LogLevel.Warning, Message = "Layout validation failed; rawLayoutId={RawLayoutId} issueCount={IssueCount}")]
public static partial void LayoutValidationFailed(this ILogger logger, Guid rawLayoutId, int issueCount);

[LoggerMessage(EventId = 1712, Level = LogLevel.Information, Message = "Compiled layout stored; rawLayoutId={RawLayoutId} compiledLayoutId={CompiledLayoutId}")]
public static partial void CompiledLayoutStored(this ILogger logger, Guid rawLayoutId, Guid compiledLayoutId);

[LoggerMessage(EventId = 1713, Level = LogLevel.Information, Message = "Layout-ready notification published; userId={UserId} compiledLayoutId={CompiledLayoutId}")]
public static partial void LayoutReadyPublished(this ILogger logger, string userId, Guid compiledLayoutId);

[LoggerMessage(EventId = 1714, Level = LogLevel.Information, Message = "SQS message processed successfully; rawLayoutId={RawLayoutId}")]
public static partial void SqsMessageProcessedSuccessfully(this ILogger logger, Guid rawLayoutId);

[LoggerMessage(EventId = 1715, Level = LogLevel.Error, Message = "Failed to process SQS message; rawLayoutId={RawLayoutId} receiptHandle={ReceiptHandle}")]
public static partial void ErrorProcessingSqsMessage(this ILogger logger, Guid rawLayoutId, string receiptHandle, Exception exception);

[LoggerMessage(EventId = 1716, Level = LogLevel.Warning, Message = "SQS message is being retried; rawLayoutId={RawLayoutId} approximateReceiveCount={ApproximateReceiveCount}")]
public static partial void SqsMessageRetry(this ILogger logger, Guid rawLayoutId, int approximateReceiveCount);

[LoggerMessage(EventId = 1717, Level = LogLevel.Error, Message = "SQS polling error; will retry")]
public static partial void SqsPollingError(this ILogger logger, Exception exception);

[LoggerMessage(EventId = 1718, Level = LogLevel.Warning, Message = "Raw layout not found; rawLayoutId={RawLayoutId}")]
public static partial void RawLayoutNotFound(this ILogger logger, Guid rawLayoutId);

[LoggerMessage(EventId = 1719, Level = LogLevel.Information, Message = "Validation result written back to raw layout; rawLayoutId={RawLayoutId}")]
public static partial void ValidationResultWrittenBack(this ILogger logger, Guid rawLayoutId);

[LoggerMessage(EventId = 1720, Level = LogLevel.Warning, Message = "SQS message unrecognized and deleted; receiptHandle={ReceiptHandle}")]
public static partial void SqsUnrecognizedMessageWarning(this ILogger logger, string receiptHandle, Exception exception);

}
43 changes: 43 additions & 0 deletions src/AdaptiveRemote.Backend.Common/Logging/RequestHandlerScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Microsoft.Extensions.Logging;

namespace AdaptiveRemote.Backend.Common.Logging;

public static class RequestHandlerScopeExtensions
{
public static IDisposable StartRequestScope(this ILogger logger, string method, string path, string? userId = null)
{
if (userId != null)
{
logger.AuthenticatedRequestStarted(method, path, userId);
}
else
{
logger.UnauthenticatedRequestStarted(method, path);
}

return new RequestHandlerScope(logger, method, path);
}
}

internal class RequestHandlerScope : IDisposable
{
private readonly ILogger _logger;
private readonly string _method;
private readonly string _path;
private bool _disposed = false;

public RequestHandlerScope(ILogger logger, string method, string path)
{
_logger = logger;
_method = method;
_path = path;
}

public void Dispose()
{
if (!Interlocked.Exchange(ref _disposed, true))
{
_logger.RequestHandled(_method, _path);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AdaptiveRemote.Backend.Common\AdaptiveRemote.Backend.Common.csproj" />
<ProjectReference Include="..\AdaptiveRemote.Contracts\AdaptiveRemote.Contracts.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Reflection;
using AdaptiveRemote.Backend.CompiledLayoutService.Logging;
using AdaptiveRemote.Backend.Common.Logging;
using AdaptiveRemote.Contracts;
using Microsoft.OpenApi;

namespace AdaptiveRemote.Backend.CompiledLayoutService.Endpoints;

Expand All @@ -15,7 +16,7 @@ public static void MapHealthEndpoints(this IEndpointRouteBuilder app)

private static IResult GetHealth(ILogger<Program> logger)
{
logger.HealthCheckRequested();
using IDisposable scope = logger.StartRequestScope("GET", "/health");

string? version = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Security.Claims;
using AdaptiveRemote.Backend.CompiledLayoutService.Logging;
using AdaptiveRemote.Backend.Common.Logging;
using AdaptiveRemote.Contracts;

namespace AdaptiveRemote.Backend.CompiledLayoutService.Endpoints;
Expand All @@ -12,6 +12,28 @@ public static void MapLayoutEndpoints(this IEndpointRouteBuilder app)
.WithName(nameof(GetActiveLayout))
.Produces<CompiledLayout>(StatusCodes.Status200OK)
.RequireAuthorization();

app.MapPost("/layouts/compiled", CreateOrUpdateLayout)
.WithName(nameof(CreateOrUpdateLayout))
.Produces<CompiledLayout>(StatusCodes.Status201Created);
Comment thread
jodavis marked this conversation as resolved.
}

private static async Task<IResult> CreateOrUpdateLayout(
ILogger<Program> logger,
CompiledLayout layout,
CancellationToken cancellationToken)
{
using IDisposable scope = logger.StartRequestScope("POST", "/layouts/compiled");

// Stub implementation to support E2E testing
if (layout is null)
{
return Results.BadRequest();
}

// Assign a new ID to simulate storage
CompiledLayout stored = layout with { Id = Guid.NewGuid() };
return Results.Created($"/layouts/compiled/{stored.Id}", stored);
}

private static async Task<IResult> GetActiveLayout(
Expand All @@ -28,7 +50,7 @@ private static async Task<IResult> GetActiveLayout(
return Results.Unauthorized();
}

logger.GetActiveLayoutRequested(userId);
using IDisposable scope = logger.StartRequestScope("GET", "/layouts/compiled/active", userId);

CompiledLayout? layout = await repository.GetActiveForUserAsync(userId, cancellationToken);

Expand Down
Loading