diff --git a/src/dotnet/aspire-session-host/Connection.cs b/src/dotnet/aspire-session-host/Connection.cs index 0803defd..c4734bfb 100644 --- a/src/dotnet/aspire-session-host/Connection.cs +++ b/src/dotnet/aspire-session-host/Connection.cs @@ -6,7 +6,7 @@ namespace AspireSessionHost; -internal class Connection : IDisposable +internal sealed class Connection : IDisposable { private readonly LifetimeDefinition _lifetimeDef = new(); private readonly Lifetime _lifetime; diff --git a/src/dotnet/aspire-session-host/Otel/OtelEndpoints.cs b/src/dotnet/aspire-session-host/Otel/OtelEndpoints.cs new file mode 100644 index 00000000..9a2837b8 --- /dev/null +++ b/src/dotnet/aspire-session-host/Otel/OtelEndpoints.cs @@ -0,0 +1,11 @@ +namespace AspireSessionHost.Otel; + +internal static class OtelEndpoints +{ + internal static void MapOtelEndpoints(this IEndpointRouteBuilder routes) + { + routes.MapGrpcService(); + routes.MapGrpcService(); + routes.MapGrpcService(); + } +} \ No newline at end of file diff --git a/src/dotnet/aspire-session-host/Otel/OtelLogService.cs b/src/dotnet/aspire-session-host/Otel/OtelLogService.cs new file mode 100644 index 00000000..0c4d5b66 --- /dev/null +++ b/src/dotnet/aspire-session-host/Otel/OtelLogService.cs @@ -0,0 +1,20 @@ +using Grpc.Core; +using OpenTelemetry.Proto.Collector.Logs.V1; + +namespace AspireSessionHost.Otel; + +internal sealed class OtelLogService : LogsService.LogsServiceBase +{ + public override Task Export( + ExportLogsServiceRequest request, + ServerCallContext context) + { + return Task.FromResult(new ExportLogsServiceResponse + { + PartialSuccess = new ExportLogsPartialSuccess + { + RejectedLogRecords = 0 + } + }); + } +} \ No newline at end of file diff --git a/src/dotnet/aspire-session-host/Otel/OtelMetricService.cs b/src/dotnet/aspire-session-host/Otel/OtelMetricService.cs new file mode 100644 index 00000000..85db0120 --- /dev/null +++ b/src/dotnet/aspire-session-host/Otel/OtelMetricService.cs @@ -0,0 +1,20 @@ +using Grpc.Core; +using OpenTelemetry.Proto.Collector.Metrics.V1; + +namespace AspireSessionHost.Otel; + +internal sealed class OtelMetricService : MetricsService.MetricsServiceBase +{ + public override Task Export( + ExportMetricsServiceRequest request, + ServerCallContext context) + { + return Task.FromResult(new ExportMetricsServiceResponse + { + PartialSuccess = new ExportMetricsPartialSuccess + { + RejectedDataPoints = 0 + } + }); + } +} \ No newline at end of file diff --git a/src/dotnet/aspire-session-host/Otel/OtelTraceService.cs b/src/dotnet/aspire-session-host/Otel/OtelTraceService.cs new file mode 100644 index 00000000..dd92386b --- /dev/null +++ b/src/dotnet/aspire-session-host/Otel/OtelTraceService.cs @@ -0,0 +1,20 @@ +using Grpc.Core; +using OpenTelemetry.Proto.Collector.Trace.V1; + +namespace AspireSessionHost.Otel; + +internal sealed class OtelTraceService : TraceService.TraceServiceBase +{ + public override Task Export( + ExportTraceServiceRequest request, + ServerCallContext context) + { + return Task.FromResult(new ExportTraceServiceResponse + { + PartialSuccess = new ExportTracePartialSuccess + { + RejectedSpans = 0 + } + }); + } +} \ No newline at end of file diff --git a/src/dotnet/aspire-session-host/Program.cs b/src/dotnet/aspire-session-host/Program.cs index a1ef24cb..55aadf98 100644 --- a/src/dotnet/aspire-session-host/Program.cs +++ b/src/dotnet/aspire-session-host/Program.cs @@ -1,18 +1,34 @@ using System.Globalization; using System.Text.Json; using AspireSessionHost; +using AspireSessionHost.Otel; +using Microsoft.AspNetCore.Server.Kestrel.Core; ParentProcessWatchdog.StartNewIfAvailable(); -var port = Environment.GetEnvironmentVariable("RIDER_RD_PORT"); -if (port == null) throw new ApplicationException("Unable to find RIDER_RD_PORT variable"); +var aspNetCoreUrlValue = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); +if (aspNetCoreUrlValue == null) throw new ApplicationException("Unable to find ASPNETCORE_URLS variable"); +if (!Uri.TryCreate(aspNetCoreUrlValue, UriKind.Absolute, out var aspNetCoreUrl)) + throw new ApplicationException("ASPNETCORE_URLS is not a valid URI"); -var connection = new Connection(int.Parse(port, CultureInfo.InvariantCulture)); +var rdPortValue = Environment.GetEnvironmentVariable("RIDER_RD_PORT"); +if (rdPortValue == null) throw new ApplicationException("Unable to find RIDER_RD_PORT variable"); +if (!int.TryParse(rdPortValue, CultureInfo.InvariantCulture, out var rdPort)) + throw new ApplicationException("RIDER_RD_PORT is not a valid port"); + +var otelPortValue = Environment.GetEnvironmentVariable("RIDER_OTEL_PORT"); +if (otelPortValue == null) throw new ApplicationException("Unable to find RIDER_OTEL_PORT variable"); +if (!int.TryParse(otelPortValue, CultureInfo.InvariantCulture, out var otelPort)) + throw new ApplicationException("RIDER_OTEL_PORT is not a valid port"); + +var connection = new Connection(rdPort); var sessionEventService = new SessionEventService(); await sessionEventService.Subscribe(connection); var builder = WebApplication.CreateBuilder(args); +builder.Services.AddGrpc(); + builder.Services.AddSingleton(connection); builder.Services.AddSingleton(sessionEventService); builder.Services.AddSingleton(); @@ -22,10 +38,21 @@ it.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; }); +builder.WebHost.ConfigureKestrel(it => +{ + it.ListenLocalhost(aspNetCoreUrl.Port); + it.ListenLocalhost(otelPort, options => + { + options.Protocols = HttpProtocols.Http2; + options.UseHttps(); + }); +}); + var app = builder.Build(); app.UseWebSockets(); app.MapSessionEndpoints(); +app.MapOtelEndpoints(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/dotnet/aspire-session-host/SessionEventService.cs b/src/dotnet/aspire-session-host/SessionEventService.cs index d43277c7..85463090 100644 --- a/src/dotnet/aspire-session-host/SessionEventService.cs +++ b/src/dotnet/aspire-session-host/SessionEventService.cs @@ -4,7 +4,7 @@ namespace AspireSessionHost; -internal class SessionEventService : IDisposable +internal sealed class SessionEventService : IDisposable { private readonly LifetimeDefinition _lifetimeDef = new(); diff --git a/src/dotnet/aspire-session-host/SessionService.cs b/src/dotnet/aspire-session-host/SessionService.cs index 454de133..6a9d30da 100644 --- a/src/dotnet/aspire-session-host/SessionService.cs +++ b/src/dotnet/aspire-session-host/SessionService.cs @@ -4,7 +4,7 @@ namespace AspireSessionHost; -internal class SessionService(Connection connection) +internal sealed class SessionService(Connection connection) { private const string TelemetryServiceName = "OTEL_SERVICE_NAME"; diff --git a/src/dotnet/aspire-session-host/aspire-session-host.csproj b/src/dotnet/aspire-session-host/aspire-session-host.csproj index 94db6481..117205f1 100644 --- a/src/dotnet/aspire-session-host/aspire-session-host.csproj +++ b/src/dotnet/aspire-session-host/aspire-session-host.csproj @@ -8,8 +8,13 @@ - - + + + + + + + diff --git a/src/main/kotlin/me/rafaelldi/aspire/run/AspireHostProgramRunner.kt b/src/main/kotlin/me/rafaelldi/aspire/run/AspireHostProgramRunner.kt index aad6c1a5..8457d280 100644 --- a/src/main/kotlin/me/rafaelldi/aspire/run/AspireHostProgramRunner.kt +++ b/src/main/kotlin/me/rafaelldi/aspire/run/AspireHostProgramRunner.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.rd.util.lifetime import com.intellij.openapi.rd.util.startOnUiAsync import com.jetbrains.rider.debugger.DotNetProgramRunner import com.jetbrains.rider.run.DotNetProcessRunProfileState +import com.jetbrains.rider.util.NetUtils import me.rafaelldi.aspire.sessionHost.AspireSessionHostConfig import me.rafaelldi.aspire.sessionHost.AspireSessionHostRunner import org.jetbrains.concurrency.Promise @@ -38,12 +39,12 @@ class AspireHostProgramRunner : DotNetProgramRunner() { val dotnetProcessState = state as? DotNetProcessRunProfileState ?: throw CantRunException("Unable to execute RunProfileState: $state") val token = dotnetProcessState.dotNetExecutable.environmentVariables[DEBUG_SESSION_TOKEN] - val port = dotnetProcessState.dotNetExecutable.environmentVariables[DEBUG_SESSION_PORT] + val aspNetPort = dotnetProcessState.dotNetExecutable.environmentVariables[DEBUG_SESSION_PORT] ?.substringAfter(':') ?.toInt() - if (token == null || port == null) + if (token == null || aspNetPort == null) throw CantRunException("Unable to find token or port") - LOG.trace("Found token $token and port $port") + LOG.trace("Found token $token and port $aspNetPort") val runProfileName = environment.runProfile.name val isDebug = environment.executor.id == DefaultDebugExecutor.EXECUTOR_ID @@ -51,11 +52,13 @@ class AspireHostProgramRunner : DotNetProgramRunner() { val sessionHostLifetime = environment.project.lifetime.createNested() val sessionHostRunner = AspireSessionHostRunner.getInstance() + val otelPort = NetUtils.findFreePort(77800) val config = AspireSessionHostConfig( token, runProfileName, isDebug, - port + aspNetPort, + otelPort ) LOG.trace("Aspire session host config: $config") diff --git a/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostConfig.kt b/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostConfig.kt index 5f488658..599835b4 100644 --- a/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostConfig.kt +++ b/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostConfig.kt @@ -4,5 +4,6 @@ data class AspireSessionHostConfig( val id: String, val projectName: String, val isDebug: Boolean, - val aspNetPort: Int + val aspNetPort: Int, + val otelPort: Int ) \ No newline at end of file diff --git a/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostRunner.kt b/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostRunner.kt index b11fbbd4..a4dddde0 100644 --- a/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostRunner.kt +++ b/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionHostRunner.kt @@ -35,6 +35,7 @@ class AspireSessionHostRunner { private val LOG = logger() private const val ASPNETCORE_URLS = "ASPNETCORE_URLS" + private const val RIDER_OTEL_PORT = "RIDER_OTEL_PORT" private const val RIDER_PARENT_PROCESS_PID = "RIDER_PARENT_PROCESS_PID" private const val RIDER_RD_PORT = "RIDER_RD_PORT" } @@ -72,6 +73,7 @@ class AspireSessionHostRunner { .withEnvironment( mapOf( ASPNETCORE_URLS to "http://localhost:${hostConfig.aspNetPort}/", + RIDER_OTEL_PORT to hostConfig.otelPort.toString(), RIDER_RD_PORT to "${protocol.wire.serverPort}", RIDER_PARENT_PROCESS_PID to ProcessHandle.current().pid().toString() ) @@ -153,7 +155,8 @@ class AspireSessionHostRunner { sessionLifetime, sessionEvents, hostConfig.projectName, - hostConfig.isDebug + hostConfig.isDebug, + hostConfig.otelPort ) ) } diff --git a/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionRunner.kt b/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionRunner.kt index 45deb97a..a0cbb2f4 100644 --- a/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionRunner.kt +++ b/src/main/kotlin/me/rafaelldi/aspire/sessionHost/AspireSessionRunner.kt @@ -44,6 +44,7 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { private val LOG = logger() private const val ASPIRE_SUFFIX = "Aspire" + private const val OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" } private val commandChannel = Channel(Channel.UNLIMITED) @@ -55,7 +56,8 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { val sessionLifetime: Lifetime, val sessionEvents: Channel, val hostName: String, - val isHostDebug: Boolean + val isHostDebug: Boolean, + val otelPort: Int ) init { @@ -67,7 +69,8 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { it.sessionLifetime, it.sessionEvents, it.hostName, - it.isHostDebug + it.isHostDebug, + it.otelPort ) } } @@ -84,7 +87,8 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { sessionLifetime: Lifetime, sessionEvents: Channel, hostName: String, - isHostDebug: Boolean + isHostDebug: Boolean, + otelPort: Int ) { LOG.info("Starting a session for the project ${sessionModel.projectPath}") @@ -93,7 +97,7 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { return } - val configuration = getOrCreateConfiguration(sessionModel, hostName) + val configuration = getOrCreateConfiguration(sessionModel, hostName, otelPort) if (configuration == null) { LOG.warn("Unable to find or create run configuration for the project ${sessionModel.projectPath}") return @@ -178,7 +182,11 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { } } - private fun getOrCreateConfiguration(session: SessionModel, hostName: String): RunnerAndConfigurationSettings? { + private fun getOrCreateConfiguration( + session: SessionModel, + hostName: String, + otelPort: Int + ): RunnerAndConfigurationSettings? { val projects = project.solution.runnableProjectsModel.projects.valueOrNull if (projects == null) { LOG.warn("Runnable projects model doesn't contain projects") @@ -205,24 +213,40 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { if (existingConfiguration != null) { LOG.info("Found existing run configuration ${existingConfiguration.name}") - return updateConfiguration(existingConfiguration, runnableProject, session) + return updateConfiguration( + existingConfiguration, + runnableProject, + session, + otelPort + ) + } else { + LOG.info("Creating a new run configuration $configurationName") + return createConfiguration( + configurationType, + configurationName, + runManager, + runnableProject, + session, + hostName, + otelPort + ) } - - LOG.info("Creating a new run configuration $configurationName") - return createConfiguration(configurationType, configurationName, runManager, runnableProject, session, hostName) } private fun updateConfiguration( existingConfiguration: RunnerAndConfigurationSettings, runnableProject: RunnableProject, session: SessionModel, + otelPort: Int ): RunnerAndConfigurationSettings { existingConfiguration.apply { (configuration as DotNetProjectConfiguration).apply { parameters.projectFilePath = runnableProject.projectFilePath parameters.projectKind = runnableProject.kind parameters.programParameters = ParametersListUtil.join(session.args?.toList() ?: emptyList()) - parameters.envs = session.envs?.associate { it.key to it.value } ?: emptyMap() + val envs = session.envs?.associate { it.key to it.value }?.toMutableMap() ?: mutableMapOf() +// envs[OTEL_EXPORTER_OTLP_ENDPOINT] = "https://localhost:$otelPort" + parameters.envs = envs } } @@ -237,7 +261,8 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { runManager: RunManager, runnableProject: RunnableProject, session: SessionModel, - hostName: String + hostName: String, + otelPort: Int ): RunnerAndConfigurationSettings { val factory = configurationType.factory val defaultConfiguration = @@ -246,7 +271,9 @@ class AspireSessionRunner(private val project: Project, scope: CoroutineScope) { parameters.projectFilePath = runnableProject.projectFilePath parameters.projectKind = runnableProject.kind parameters.programParameters = ParametersListUtil.join(session.args?.toList() ?: emptyList()) - parameters.envs = session.envs?.associate { it.key to it.value } ?: emptyMap() + val envs = session.envs?.associate { it.key to it.value }?.toMutableMap() ?: mutableMapOf() +// envs[OTEL_EXPORTER_OTLP_ENDPOINT] = "https://localhost:$otelPort" + parameters.envs = envs } isActivateToolWindowBeforeRun = false isFocusToolWindowBeforeRun = false