diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Properties/launchSettings.json b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Properties/launchSettings.json index 16aca777b1e..c5147e7d168 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Properties/launchSettings.json +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Properties/launchSettings.json @@ -10,19 +10,19 @@ "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" // Display dashboard resource in console for local development - } - }, - "generate-manifest": { - "commandName": "Project", - "launchBrowser": true, - "dotnetRunMessages": true, - "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json", - "applicationUrl": "http://localhost:15888", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175" + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" + }, + "generate-manifest": { + "commandName": "Project", + "launchBrowser": true, + "dotnetRunMessages": true, + "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json", + "applicationUrl": "http://localhost:15888", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175" + } } } } diff --git a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs index e2dbe5f854c..d1b8583be4f 100644 --- a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs +++ b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs @@ -13,6 +13,6 @@ // to test end developer dashboard launch experience. Refer to Directory.Build.props // for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output // in the artifacts dir). -//builder.AddProject(KnownResourceNames.AspireDashboard); +builder.AddProject(KnownResourceNames.AspireDashboard); builder.Build().Run(); diff --git a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Properties/launchSettings.json b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Properties/launchSettings.json index 16aca777b1e..32514f6378b 100644 --- a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Properties/launchSettings.json +++ b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Properties/launchSettings.json @@ -10,7 +10,7 @@ "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" // Display dashboard resource in console for local development + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" } }, "generate-manifest": { diff --git a/playground/dapr/AppHost/Properties/launchSettings.json b/playground/dapr/AppHost/Properties/launchSettings.json index 28be2a8997c..eac35bd52bf 100644 --- a/playground/dapr/AppHost/Properties/launchSettings.json +++ b/playground/dapr/AppHost/Properties/launchSettings.json @@ -9,7 +9,7 @@ "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16031", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" // Display dashboard resource in console for local development + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" } }, "generate-manifest": { diff --git a/playground/eShopLite/AppHost/Properties/launchSettings.json b/playground/eShopLite/AppHost/Properties/launchSettings.json index 10744f258fa..f607729fb5b 100644 --- a/playground/eShopLite/AppHost/Properties/launchSettings.json +++ b/playground/eShopLite/AppHost/Properties/launchSettings.json @@ -9,7 +9,7 @@ "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16031", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" // Display dashboard resource in console for local development + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" } }, "generate-manifest": { diff --git a/playground/eShopLite/AppHost/appsettings.json b/playground/eShopLite/AppHost/appsettings.json index 0c208ae9181..31c092aa450 100644 --- a/playground/eShopLite/AppHost/appsettings.json +++ b/playground/eShopLite/AppHost/appsettings.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" } } } diff --git a/playground/mongo/Mongo.AppHost/Properties/launchSettings.json b/playground/mongo/Mongo.AppHost/Properties/launchSettings.json index 16aca777b1e..32514f6378b 100644 --- a/playground/mongo/Mongo.AppHost/Properties/launchSettings.json +++ b/playground/mongo/Mongo.AppHost/Properties/launchSettings.json @@ -10,7 +10,7 @@ "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" // Display dashboard resource in console for local development + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" } }, "generate-manifest": { diff --git a/playground/orleans/OrleansAppHost/Properties/launchSettings.json b/playground/orleans/OrleansAppHost/Properties/launchSettings.json index f752c9e26b6..e5c69689a79 100644 --- a/playground/orleans/OrleansAppHost/Properties/launchSettings.json +++ b/playground/orleans/OrleansAppHost/Properties/launchSettings.json @@ -9,7 +9,7 @@ "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16031", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" // Display dashboard resource in console for local development + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" } }, "generate-manifest": { diff --git a/src/Aspire.Hosting/Dashboard/DashboardManifestExclusionHook.cs b/src/Aspire.Hosting/Dashboard/DashboardManifestExclusionHook.cs new file mode 100644 index 00000000000..33e3a3c3beb --- /dev/null +++ b/src/Aspire.Hosting/Dashboard/DashboardManifestExclusionHook.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Lifecycle; + +namespace Aspire.Hosting.Dashboard; +internal sealed class DashboardManifestExclusionHook : IDistributedApplicationLifecycleHook +{ + public Task BeforeStartAsync(DistributedApplicationModel model, CancellationToken cancellationToken) + { + if (model.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) is { } dashboardResource) + { + dashboardResource.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); + } + + return Task.CompletedTask; + } +} diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index c55a261698b..0d604ea457c 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -1,12 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Dashboard; using Aspire.Hosting.Dcp.Model; using Aspire.Hosting.Lifecycle; +using Aspire.Hosting.Utils; using k8s; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -50,6 +50,7 @@ public ServiceAppResource(IResource modelResource, Service service, EndpointAnno } internal sealed class ApplicationExecutor(ILogger logger, + ILoggerFactory loggerFactory, DistributedApplicationModel model, DistributedApplicationOptions distributedApplicationOptions, KubernetesService kubernetesService, @@ -138,12 +139,12 @@ private async Task ConfigureAspireDashboardResource(IResource dashboardResource, { if (Environment.GetEnvironmentVariable("ASPNETCORE_URLS") is not { } appHostApplicationUrl) { - throw new DistributedApplicationException("Dashboard inner loop hook failed to configure resource because ASPNETCORE_URLS environment variable was not set."); + throw new DistributedApplicationException("Failed to configure dashboard resource because ASPNETCORE_URLS environment variable was not set."); } if (Environment.GetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL") is not { } otlpEndpointUrl) { - throw new DistributedApplicationException("Dashboard inner loop hook failed to configure resource because DOTNET_DASHBOARD_OTLP_ENDPOINT_URL environment variable was not set."); + throw new DistributedApplicationException("Failed to configure dashboard resource because DOTNET_DASHBOARD_OTLP_ENDPOINT_URL environment variable was not set."); } // Grab the resource service URL. We need to inject this into the resource. @@ -254,9 +255,12 @@ private static TimeSpan DashboardAvailabilityTimeoutDuration private async Task CheckDashboardAvailabilityAsync(string delimitedUrlList, CancellationToken cancellationToken) { - if (TryGetUriFromDelimitedString(delimitedUrlList, ";", out var firstDashboardUrl)) + if (StringUtils.TryGetUriFromDelimitedString(delimitedUrlList, ";", out var firstDashboardUrl)) { await WaitForHttpSuccessOrThrow(firstDashboardUrl, DashboardAvailabilityTimeoutDuration, cancellationToken).ConfigureAwait(false); + + var distributedApplicationLogger = loggerFactory.CreateLogger(); + distributedApplicationLogger.LogInformation("Now listening on: {DashboardUrl}", firstDashboardUrl.ToString().TrimEnd('/')); } else { @@ -669,21 +673,6 @@ private async Task CreateExecutablesAsync(IEnumerable executableRes } } - private static bool TryGetUriFromDelimitedString(string input, string delimiter, [NotNullWhen(true)]out Uri? uri) - { - if (!string.IsNullOrEmpty(input) - && input.Split(delimiter) is { Length: > 0} splitInput - && Uri.TryCreate(splitInput[0], UriKind.Absolute, out uri)) - { - return true; - } - else - { - uri = null; - return false; - } - } - private static void ApplyLaunchProfile(AppResource executableResource, Dictionary config, string launchProfileName, LaunchSettings launchSettings) { // Populate DOTNET_LAUNCH_PROFILE environment variable for consistency with "dotnet run" and "dotnet watch". diff --git a/src/Aspire.Hosting/DistributedApplication.cs b/src/Aspire.Hosting/DistributedApplication.cs index c70615292cf..211eb62fc46 100644 --- a/src/Aspire.Hosting/DistributedApplication.cs +++ b/src/Aspire.Hosting/DistributedApplication.cs @@ -5,9 +5,9 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Publishing; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Aspire.Hosting; @@ -46,6 +46,7 @@ public class DistributedApplication : IHost, IAsyncDisposable { private readonly IHost _host; private readonly string[] _args; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -55,6 +56,7 @@ public class DistributedApplication : IHost, IAsyncDisposable public DistributedApplication(IHost host, string[] args) { _host = host; + _logger = host.Services.GetRequiredService>(); _args = args; } @@ -111,8 +113,9 @@ public ValueTask DisposeAsync() /// public async Task StartAsync(CancellationToken cancellationToken = default) { - await ExecuteBeforeStartHooksAsync(cancellationToken).ConfigureAwait(false); + WriteStartingLog(); await _host.StartAsync(cancellationToken).ConfigureAwait(false); + await ExecuteBeforeStartHooksAsync(cancellationToken).ConfigureAwait(false); } /// @@ -121,30 +124,25 @@ public async Task StopAsync(CancellationToken cancellationToken = default) await _host.StopAsync(cancellationToken).ConfigureAwait(false); } - private void SuppressLifetimeLogsDuringManifestPublishing() + /// + public async Task RunAsync(CancellationToken cancellationToken = default) + { + WriteStartingLog(); + await ExecuteBeforeStartHooksAsync(cancellationToken).ConfigureAwait(false); + await _host.RunAsync(cancellationToken).ConfigureAwait(false); + } + + private void WriteStartingLog() { - var config = (IConfigurationRoot)_host.Services.GetRequiredService(); var options = _host.Services.GetRequiredService>(); - if (options.Value?.Publisher != "manifest") + if (options.Value?.Publisher == "manifest") { - // If we aren't doing manifest publishing we want the logs - // to be produced as normal. + // If we are producing the manifest, don't write startup messages. return; } - var hostingLifetimeLoggingLevelSection = config.GetSection("Logging:LogLevel:Microsoft.Hosting.Lifetime"); - hostingLifetimeLoggingLevelSection.Value = "Warning"; - - config.Reload(); - } - - /// - public async Task RunAsync(CancellationToken cancellationToken = default) - { - SuppressLifetimeLogsDuringManifestPublishing(); - await ExecuteBeforeStartHooksAsync(cancellationToken).ConfigureAwait(false); - await _host.RunAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Distributed application starting."); } /// diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index f7259cb5a16..6a73e1f26d8 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -44,6 +45,9 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _args = options.Args ?? []; _innerBuilder = new HostApplicationBuilder(); + _innerBuilder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning); + _innerBuilder.Logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel", LogLevel.None); + AppHostDirectory = options.ProjectDirectory ?? _innerBuilder.Environment.ContentRootPath; // Make the app host directory available to the application via configuration @@ -61,6 +65,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // Dashboard _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); + _innerBuilder.Services.AddLifecycleHook(); // DCP stuff _innerBuilder.Services.AddLifecycleHook(); diff --git a/src/Aspire.Hosting/Utils/StringUtils.cs b/src/Aspire.Hosting/Utils/StringUtils.cs new file mode 100644 index 00000000000..bb8a154e3a7 --- /dev/null +++ b/src/Aspire.Hosting/Utils/StringUtils.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Aspire.Hosting.Utils; + +internal static class StringUtils +{ + public static bool TryGetUriFromDelimitedString(string input, string delimiter, [NotNullWhen(true)] out Uri? uri) + { + if (!string.IsNullOrEmpty(input) + && input.Split(delimiter) is { Length: > 0 } splitInput + && Uri.TryCreate(splitInput[0], UriKind.Absolute, out uri)) + { + return true; + } + else + { + uri = null; + return false; + } + } + +} diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs index 13a99ea0ffc..04b61d6fdf5 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs @@ -26,7 +26,7 @@ public void BuilderAddsDefaultServices() var lifecycles = app.Services.GetServices(); Assert.Single(lifecycles.Where(h => h.GetType().Name == "DcpDistributedApplicationLifecycleHook")); - Assert.Equal(3, lifecycles.Count()); + Assert.Equal(4, lifecycles.Count()); var options = app.Services.GetRequiredService>(); Assert.Null(options.Value.Publisher);