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
14 changes: 14 additions & 0 deletions LightQueryProfiler.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightQueryProfiler.Highligh
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightQueryProfiler.WinFormsApp", "src\LightQueryProfiler.WinFormsApp\LightQueryProfiler.WinFormsApp.csproj", "{BA5AD370-10BB-40D7-8FD5-F7055DBA9D29}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightQueryProfiler.JsonRpc", "src\LightQueryProfiler.JsonRpc\LightQueryProfiler.JsonRpc.csproj", "{D1E8F3C4-5B6A-4D9E-8F7C-9A1B2C3D4E5F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightQueryProfiler.JsonRpc.Tests", "tests\LightQueryProfiler.JsonRpc.Tests\LightQueryProfiler.JsonRpc.Tests.csproj", "{E2F9G4D5-6C7B-5E0F-9G8D-0B2C3D4E5F6G}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -37,6 +41,14 @@ Global
{BA5AD370-10BB-40D7-8FD5-F7055DBA9D29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA5AD370-10BB-40D7-8FD5-F7055DBA9D29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA5AD370-10BB-40D7-8FD5-F7055DBA9D29}.Release|Any CPU.Build.0 = Release|Any CPU
{D1E8F3C4-5B6A-4D9E-8F7C-9A1B2C3D4E5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1E8F3C4-5B6A-4D9E-8F7C-9A1B2C3D4E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1E8F3C4-5B6A-4D9E-8F7C-9A1B2C3D4E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1E8F3C4-5B6A-4D9E-8F7C-9A1B2C3D4E5F}.Release|Any CPU.Build.0 = Release|Any CPU
{E2F9G4D5-6C7B-5E0F-9G8D-0B2C3D4E5F6G}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2F9G4D5-6C7B-5E0F-9G8D-0B2C3D4E5F6G}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2F9G4D5-6C7B-5E0F-9G8D-0B2C3D4E5F6G}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2F9G4D5-6C7B-5E0F-9G8D-0B2C3D4E5F6G}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -46,6 +58,8 @@ Global
{50FB44D7-9878-447C-BA1B-829D2BC86B78} = {66D515C8-C16C-4429-A47D-DB296B7DC486}
{BFF93236-CB9F-4780-9B52-C0DB3286451F} = {51FC1EE8-5A57-4BD8-A682-0790A08C0FE7}
{BA5AD370-10BB-40D7-8FD5-F7055DBA9D29} = {51FC1EE8-5A57-4BD8-A682-0790A08C0FE7}
{D1E8F3C4-5B6A-4D9E-8F7C-9A1B2C3D4E5F} = {51FC1EE8-5A57-4BD8-A682-0790A08C0FE7}
{E2F9G4D5-6C7B-5E0F-9G8D-0B2C3D4E5F6G} = {66D515C8-C16C-4429-A47D-DB296B7DC486}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E833AB64-9548-4BE4-ACB5-2059821BDCDB}
Expand Down
235 changes: 235 additions & 0 deletions src/LightQueryProfiler.JsonRpc/JsonRpcServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
using LightQueryProfiler.JsonRpc.Models;
using LightQueryProfiler.Shared.Data;
using LightQueryProfiler.Shared.Enums;
using LightQueryProfiler.Shared.Factories;
using LightQueryProfiler.Shared.Repositories;
using LightQueryProfiler.Shared.Services;
using LightQueryProfiler.Shared.Services.Interfaces;
using Microsoft.Extensions.Logging;

namespace LightQueryProfiler.JsonRpc;

/// <summary>
/// JSON-RPC server that exposes profiling operations for VS Code extension
/// </summary>
public class JsonRpcServer
{
private readonly ILogger<JsonRpcServer> _logger;
private readonly Dictionary<string, IProfilerService> _activeSessions;
private readonly Dictionary<string, IApplicationDbContext> _activeContexts;

public JsonRpcServer(ILogger<JsonRpcServer> logger)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger;
_activeSessions = new Dictionary<string, IProfilerService>();
_activeContexts = new Dictionary<string, IApplicationDbContext>();
}

/// <summary>
/// Starts a profiling session with the specified parameters
/// </summary>
public async Task StartProfilingAsync(StartProfilingRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);

if (string.IsNullOrWhiteSpace(request.SessionName))
{
throw new ArgumentException("SessionName cannot be null or empty", nameof(request));
}

if (string.IsNullOrWhiteSpace(request.ConnectionString))
{
throw new ArgumentException("ConnectionString cannot be null or empty", nameof(request));
}

if (!Enum.IsDefined(typeof(DatabaseEngineType), request.EngineType))
{
throw new ArgumentException($"Invalid EngineType: {request.EngineType}", nameof(request));
}

cancellationToken.ThrowIfCancellationRequested();
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Starting profiling session: {SessionName} with engine type: {EngineType}",
request.SessionName, request.EngineType);
}


try
{
// Create context and services for this session
var dbContext = new ApplicationDbContext(request.ConnectionString);
var xEventRepository = new XEventRepository(dbContext);
xEventRepository.SetEngineType((DatabaseEngineType)request.EngineType);

var xEventService = new XEventService();
var profilerService = new ProfilerService(xEventRepository, xEventService);

// Create template based on engine type
var template = ProfilerSessionTemplateFactory.CreateTemplate((DatabaseEngineType)request.EngineType);

// Start profiling
await Task.Run(() => profilerService.StartProfiling(request.SessionName, template), cancellationToken)
.ConfigureAwait(false);

// Store for later use
_activeSessions[request.SessionName] = profilerService;
_activeContexts[request.SessionName] = dbContext;

if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Profiling session started successfully: {SessionName}", request.SessionName);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start profiling session: {SessionName}", request.SessionName);
throw;
}
}

/// <summary>
/// Stops the specified profiling session
/// </summary>
public async Task StopProfilingAsync(StopProfilingRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);

if (string.IsNullOrWhiteSpace(request.SessionName))
{
throw new ArgumentException("SessionName cannot be null or empty", nameof(request));
}

cancellationToken.ThrowIfCancellationRequested();

if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Stopping profiling session: {SessionName}", request.SessionName);
}

try
{
if (!_activeSessions.TryGetValue(request.SessionName, out var profilerService))
{
throw new InvalidOperationException($"No active profiling session found: {request.SessionName}");
}

await Task.Run(() => profilerService.StopProfiling(request.SessionName), cancellationToken)
.ConfigureAwait(false);

// Clean up
_activeSessions.Remove(request.SessionName);
_activeContexts.Remove(request.SessionName);


if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Profiling session stopped successfully: {SessionName}", request.SessionName);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop profiling session: {SessionName}", request.SessionName);
throw;
}
}

/// <summary>
/// Retrieves the latest events from the specified profiling session
/// </summary>
public async Task<List<ProfilerEventDto>> GetLastEventsAsync(GetEventsRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);

if (string.IsNullOrWhiteSpace(request.SessionName))
{
throw new ArgumentException("SessionName cannot be null or empty", nameof(request));
}

cancellationToken.ThrowIfCancellationRequested();

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Retrieving events for session: {SessionName}", request.SessionName);
}

try
{
if (!_activeSessions.TryGetValue(request.SessionName, out var profilerService))
{
throw new InvalidOperationException($"No active profiling session found: {request.SessionName}");
}

var events = await profilerService.GetLastEventsAsync(request.SessionName)
.ConfigureAwait(false);

// Convert to DTOs
var eventDtos = events.Select(e => new ProfilerEventDto
{
Name = e.Name,
Timestamp = e.Timestamp,
Fields = e.Fields,
Actions = e.Actions
}).ToList();

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Retrieved {Count} events for session: {SessionName}", eventDtos.Count, request.SessionName);
}

return eventDtos;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve events for session: {SessionName}", request.SessionName);
throw;
}
}

/// <summary>
/// Pauses the specified profiling session (not yet implemented in ProfilerService)
/// </summary>
public async Task PauseProfilingAsync(StopProfilingRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);

if (string.IsNullOrWhiteSpace(request.SessionName))
{
throw new ArgumentException("SessionName cannot be null or empty", nameof(request));
}

cancellationToken.ThrowIfCancellationRequested();

if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Pausing profiling session: {SessionName}", request.SessionName);
}

try
{
if (!_activeSessions.TryGetValue(request.SessionName, out var profilerService))
{
throw new InvalidOperationException($"No active profiling session found: {request.SessionName}");
}

await Task.Run(() => profilerService.PauseProfiling(request.SessionName), cancellationToken)
.ConfigureAwait(false);

if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Profiling session paused successfully: {SessionName}", request.SessionName);
}
}
catch (NotImplementedException)
{
_logger.LogWarning("PauseProfiling is not yet implemented");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to pause profiling session: {SessionName}", request.SessionName);
throw;
}
}
}
22 changes: 22 additions & 0 deletions src/LightQueryProfiler.JsonRpc/LightQueryProfiler.JsonRpc.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>LightQueryProfiler.JsonRpc</AssemblyName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="StreamJsonRpc" Version="2.24.84" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\LightQueryProfiler.Shared\LightQueryProfiler.Shared.csproj" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/LightQueryProfiler.JsonRpc/Models/GetEventsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace LightQueryProfiler.JsonRpc.Models;

/// <summary>
/// Request parameters for retrieving profiling events
/// </summary>
public record GetEventsRequest
{
/// <summary>
/// Name of the profiling session to retrieve events from
/// </summary>
public required string SessionName { get; init; }
}
27 changes: 27 additions & 0 deletions src/LightQueryProfiler.JsonRpc/Models/ProfilerEventDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace LightQueryProfiler.JsonRpc.Models;

/// <summary>
/// Data transfer object for profiler events (JSON-RPC serializable)
/// </summary>
public record ProfilerEventDto
{
/// <summary>
/// Event name
/// </summary>
public string? Name { get; init; }

/// <summary>
/// Event timestamp
/// </summary>
public string? Timestamp { get; init; }

/// <summary>
/// Event fields
/// </summary>
public Dictionary<string, object?>? Fields { get; init; }

/// <summary>
/// Event actions
/// </summary>
public Dictionary<string, object?>? Actions { get; init; }
}
22 changes: 22 additions & 0 deletions src/LightQueryProfiler.JsonRpc/Models/StartProfilingRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace LightQueryProfiler.JsonRpc.Models;

/// <summary>
/// Request parameters for starting a profiling session
/// </summary>
public record StartProfilingRequest
{
/// <summary>
/// Name of the profiling session
/// </summary>
public required string SessionName { get; init; }

/// <summary>
/// Database engine type (1 = SqlServer, 2 = AzureSqlDatabase)
/// </summary>
public required int EngineType { get; init; }

/// <summary>
/// SQL Server connection string
/// </summary>
public required string ConnectionString { get; init; }
}
12 changes: 12 additions & 0 deletions src/LightQueryProfiler.JsonRpc/Models/StopProfilingRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace LightQueryProfiler.JsonRpc.Models;

/// <summary>
/// Request parameters for stopping a profiling session
/// </summary>
public record StopProfilingRequest
{
/// <summary>
/// Name of the profiling session to stop
/// </summary>
public required string SessionName { get; init; }
}
Loading
Loading