diff --git a/documentation/configuration.md b/documentation/configuration.md index 744c8557355..3c8fcdfa179 100644 --- a/documentation/configuration.md +++ b/documentation/configuration.md @@ -4,7 +4,7 @@ ## Configuration Sources -`dotnet monitor` can read and combine configuration from multiple sources. The configuration sources are listed below in the order in which they are read (Environment variables are highest precedence) : +`dotnet monitor` can read and combine configuration from multiple sources. The configuration sources are listed below in the order in which they are read (User-specified json file is highest precedence) : - Command line parameters - User settings path @@ -16,6 +16,8 @@ - On \*nix, `/etc/dotnet-monitor` - Environment variables +- User-Specified json file + - Use the `--configuration-file-path` flag from the command line to specify your own configuration file (using its full path). ### Translating configuration between providers diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs index 6ece8ac544e..edd194331aa 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs @@ -43,7 +43,8 @@ public static void Main(string[] args) metricUrls: null, metrics: false, diagnosticPort: null, - authConfiguration: HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false)); + authConfiguration: HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false), + userProvidedConfigFilePath: null); // Create all of the same services as dotnet-monitor and add // OpenAPI generation in order to have it inspect the ASP.NET Core diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs index f994fccba17..ba8da386406 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs @@ -54,6 +54,11 @@ public sealed class ConfigurationTests { WebHostDefaults.ServerUrlsKey, nameof(ConfigurationLevel.UserSettings) } }; + private static readonly Dictionary UserProvidedFileSettingsContent = new(StringComparer.Ordinal) + { + { WebHostDefaults.ServerUrlsKey, nameof(ConfigurationLevel.UserProvidedFileSettings) } + }; + // This needs to be updated and kept in order for any future configuration sections private static readonly List OrderedConfigurationKeys = new() { @@ -79,6 +84,8 @@ public sealed class ConfigurationTests private const string SampleConfigurationsDirectory = "SampleConfigurations"; + private const string UserProvidedSettingsFileName = "UserSpecifiedFile.json"; // Note: if this name is updated, it must also be updated in the expected show sources configuration files + private readonly ITestOutputHelper _outputHelper; public ConfigurationTests(ITestOutputHelper outputHelper) @@ -101,11 +108,15 @@ public ConfigurationTests(ITestOutputHelper outputHelper) [InlineData(ConfigurationLevel.SharedSettings)] [InlineData(ConfigurationLevel.SharedKeyPerFile)] [InlineData(ConfigurationLevel.MonitorEnvironment)] + [InlineData(ConfigurationLevel.UserProvidedFileSettings)] public void ConfigurationOrderingTest(ConfigurationLevel level) { using TemporaryDirectory contentRootDirectory = new(_outputHelper); using TemporaryDirectory sharedConfigDir = new(_outputHelper); using TemporaryDirectory userConfigDir = new(_outputHelper); + using TemporaryDirectory userProvidedConfigDir = new(_outputHelper); + + string userProvidedConfigFullPath = Path.Combine(userProvidedConfigDir.FullName, UserProvidedSettingsFileName); // Set up the initial settings used to create the host builder. HostBuilderSettings settings = new() @@ -113,7 +124,8 @@ public void ConfigurationOrderingTest(ConfigurationLevel level) Authentication = HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false), ContentRootDirectory = contentRootDirectory.FullName, SharedConfigDirectory = sharedConfigDir.FullName, - UserConfigDirectory = userConfigDir.FullName + UserConfigDirectory = userConfigDir.FullName, + UserProvidedConfigFilePath = level >= ConfigurationLevel.UserProvidedFileSettings ? new FileInfo(userProvidedConfigFullPath) : null }; if (level >= ConfigurationLevel.HostBuilderSettingsUrl) { @@ -147,6 +159,12 @@ public void ConfigurationOrderingTest(ConfigurationLevel level) // is typically used when mounting secrets from a Docker volume. File.WriteAllText(Path.Combine(sharedConfigDir.FullName, WebHostDefaults.ServerUrlsKey), nameof(ConfigurationLevel.SharedKeyPerFile)); } + if (level >= ConfigurationLevel.UserProvidedFileSettings) + { + // This is the user-provided file in the directory specified on the command-line + string userSpecifiedFileSettingsContent = JsonSerializer.Serialize(UserProvidedFileSettingsContent); + File.WriteAllText(userProvidedConfigFullPath, userSpecifiedFileSettingsContent); + } // Create the initial host builder. IHostBuilder builder = HostBuilderHelper.CreateHostBuilder(settings); @@ -232,6 +250,9 @@ public void FullConfigurationWithSourcesTest(bool redact) using TemporaryDirectory contentRootDirectory = new(_outputHelper); using TemporaryDirectory sharedConfigDir = new(_outputHelper); using TemporaryDirectory userConfigDir = new(_outputHelper); + using TemporaryDirectory userProvidedConfigDir = new(_outputHelper); + + string userProvidedConfigFullPath = Path.Combine(userProvidedConfigDir.FullName, UserProvidedSettingsFileName); // Set up the initial settings used to create the host builder. HostBuilderSettings settings = new() @@ -239,13 +260,17 @@ public void FullConfigurationWithSourcesTest(bool redact) Authentication = HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false), ContentRootDirectory = contentRootDirectory.FullName, SharedConfigDirectory = sharedConfigDir.FullName, - UserConfigDirectory = userConfigDir.FullName + UserConfigDirectory = userConfigDir.FullName, + UserProvidedConfigFilePath = new FileInfo(userProvidedConfigFullPath) }; settings.Urls = new[] { "https://localhost:44444" }; // This corresponds to the value in SampleConfigurations/URLs.json + // This is the user-provided file in the directory specified on the command-line + File.WriteAllText(userProvidedConfigFullPath, ConstructSettingsJson("Egress.json")); + // This is the settings.json file in the user profile directory. - File.WriteAllText(Path.Combine(userConfigDir.FullName, "settings.json"), ConstructSettingsJson("Egress.json", "CollectionRules.json", "CollectionRuleDefaults.json", "Templates.json")); + File.WriteAllText(Path.Combine(userConfigDir.FullName, "settings.json"), ConstructSettingsJson("CollectionRules.json", "CollectionRuleDefaults.json", "Templates.json")); // This is the appsettings.json file that is normally next to the entrypoint assembly. // The location of the appsettings.json is determined by the content root in configuration. @@ -434,7 +459,8 @@ public enum ConfigurationLevel UserSettings, SharedSettings, SharedKeyPerFile, - MonitorEnvironment + MonitorEnvironment, + UserProvidedFileSettings } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json index dc4c1337989..771418e4df4 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json @@ -1,18 +1,18 @@ { "AzureBlobStorage": { "monitorBlob": { - "accountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "accountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "blobPrefix": "artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "containerName": "dotnet-monitor" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "accountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "accountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "blobPrefix": "artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "containerName": "dotnet-monitor" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ } }, "FileSystem": { "artifacts": { - "directoryPath": "/artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "directoryPath": "/artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ } }, "Properties": { - "MonitorBlobAccountKey": "accountKey" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "MonitorBlobAccountKey": "accountKey" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json index 44dd1093cda..7af971d6599 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json @@ -1,25 +1,25 @@ { "Properties": { - "MonitorBlobAccountKey": ":REDACTED:" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "MonitorBlobAccountKey": ":REDACTED:" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ }, "AzureBlobStorage": { "monitorBlob": { - "AccountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "BlobPrefix": "artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "ContainerName": "dotnet-monitor" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, + "AccountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "BlobPrefix": "artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "ContainerName": "dotnet-monitor" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, "CopyBufferSize": ":NOT PRESENT:", "QueueName": ":NOT PRESENT:", "QueueAccountUri": ":NOT PRESENT:", "SharedAccessSignature": ":NOT PRESENT:", "AccountKey": ":NOT PRESENT:", "SharedAccessSignatureName": ":NOT PRESENT:", - "AccountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, + "AccountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, "ManagedIdentityClientId": ":NOT PRESENT:" } }, "FileSystem": { "artifacts": { - "DirectoryPath": " /artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, + "DirectoryPath": " /artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, "IntermediateDirectoryPath": ":NOT PRESENT:", "CopyBufferSize": ":NOT PRESENT:" } diff --git a/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs b/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs index 7ac1bfc9861..583d42dee7c 100644 --- a/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs +++ b/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -19,12 +20,12 @@ namespace Microsoft.Diagnostics.Tools.Monitor.Commands { internal static class CollectCommandHandler { - public static async Task Invoke(CancellationToken token, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth, bool tempApiKey, bool noHttpEgress) + public static async Task Invoke(CancellationToken token, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth, bool tempApiKey, bool noHttpEgress, FileInfo configurationFilePath) { try { AuthConfiguration authConfiguration = HostBuilderHelper.CreateAuthConfiguration(noAuth, tempApiKey); - HostBuilderSettings settings = HostBuilderSettings.CreateMonitor(urls, metricUrls, metrics, diagnosticPort, authConfiguration); + HostBuilderSettings settings = HostBuilderSettings.CreateMonitor(urls, metricUrls, metrics, diagnosticPort, authConfiguration, configurationFilePath); IHost host = HostBuilderHelper.CreateHostBuilder(settings) .ConfigureServices(authConfiguration, noHttpEgress) diff --git a/src/Tools/dotnet-monitor/Commands/ConfigShowCommandHandler.cs b/src/Tools/dotnet-monitor/Commands/ConfigShowCommandHandler.cs index cf8273d5efc..29029b84e78 100644 --- a/src/Tools/dotnet-monitor/Commands/ConfigShowCommandHandler.cs +++ b/src/Tools/dotnet-monitor/Commands/ConfigShowCommandHandler.cs @@ -17,7 +17,7 @@ internal sealed class ConfigShowCommandHandler // Although the "noHttpEgress" parameter is unused, it keeps the entire command parameter set a superset // of the "collect" command so that users can take the same arguments from "collect" and use it on "config show" // to get the same configuration without telling them to drop specific command line arguments. - public static void Invoke(string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth, bool tempApiKey, bool noHttpEgress, ConfigDisplayLevel level, bool showSources) + public static void Invoke(string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth, bool tempApiKey, bool noHttpEgress, FileInfo configurationFilePath, ConfigDisplayLevel level, bool showSources) { Stream stream = Console.OpenStandardOutput(); @@ -26,13 +26,13 @@ public static void Invoke(string[] urls, string[] metricUrls, bool metrics, stri writer.WriteLine(); writer.Flush(); - Write(stream, urls, metricUrls, metrics, diagnosticPort, noAuth, tempApiKey, level, showSources); + Write(stream, urls, metricUrls, metrics, diagnosticPort, noAuth, tempApiKey, configurationFilePath, level, showSources); } - public static void Write(Stream stream, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth, bool tempApiKey, ConfigDisplayLevel level, bool showSources) + public static void Write(Stream stream, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth, bool tempApiKey, FileInfo configurationFilePath, ConfigDisplayLevel level, bool showSources) { IAuthConfiguration authConfiguration = HostBuilderHelper.CreateAuthConfiguration(noAuth, tempApiKey); - HostBuilderSettings settings = HostBuilderSettings.CreateMonitor(urls, metricUrls, metrics, diagnosticPort, authConfiguration); + HostBuilderSettings settings = HostBuilderSettings.CreateMonitor(urls, metricUrls, metrics, diagnosticPort, authConfiguration, configurationFilePath); IHost host = HostBuilderHelper.CreateHostBuilder(settings).Build(); IConfiguration configuration = host.Services.GetRequiredService(); using ConfigurationJsonWriter jsonWriter = new ConfigurationJsonWriter(stream); diff --git a/src/Tools/dotnet-monitor/HostBuilderHelper.cs b/src/Tools/dotnet-monitor/HostBuilderHelper.cs index 9a6111f5449..6bd908d6734 100644 --- a/src/Tools/dotnet-monitor/HostBuilderHelper.cs +++ b/src/Tools/dotnet-monitor/HostBuilderHelper.cs @@ -49,7 +49,7 @@ public static IHostBuilder CreateHostBuilder(HostBuilderSettings settings) }) .ConfigureDefaults(args: null) .UseContentRoot(settings.ContentRootDirectory) - .ConfigureAppConfiguration((IConfigurationBuilder builder) => + .ConfigureAppConfiguration((HostBuilderContext context, IConfigurationBuilder builder) => { string userSettingsPath = Path.Combine(settings.UserConfigDirectory, SettingsFileName); builder.AddJsonFile(userSettingsPath, optional: true, reloadOnChange: true); @@ -80,6 +80,35 @@ public static IHostBuilder CreateHostBuilder(HostBuilderSettings settings) { ConfigureTempApiHashKey(builder, settings.Authentication); } + + // User-specified configuration file path is considered highest precedence, but does NOT override other configuration sources + FileInfo userFilePath = settings.UserProvidedConfigFilePath; + + if (null != userFilePath) + { + HostBuilderResults hostBuilderResults = new HostBuilderResults(); + context.Properties.Add(HostBuilderResults.ResultKey, hostBuilderResults); + + if (!userFilePath.Exists) + { + hostBuilderResults.Warnings.Add(Strings.Message_ConfigurationFileDoesNotExist); + } + else if (!".json".Equals(userFilePath.Extension, StringComparison.OrdinalIgnoreCase)) + { + hostBuilderResults.Warnings.Add(Strings.Message_ConfigurationFileNotJson); + } + else + { + try + { + builder.AddJsonFile(userFilePath.FullName, optional: true, reloadOnChange: true); + } + catch (Exception ex) + { + hostBuilderResults.Warnings.Add(ex.Message); + } + } + } }) //Note this is necessary for config only because Kestrel configuration //is not added until WebHostDefaults are added. diff --git a/src/Tools/dotnet-monitor/HostBuilderResults.cs b/src/Tools/dotnet-monitor/HostBuilderResults.cs new file mode 100644 index 00000000000..b18deace1af --- /dev/null +++ b/src/Tools/dotnet-monitor/HostBuilderResults.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + public class HostBuilderResults + { + public const string ResultKey = "DotnetMonitorHostBuilderResults"; + + public List Warnings { get; } = new(); + } +} diff --git a/src/Tools/dotnet-monitor/HostBuilderSettings.cs b/src/Tools/dotnet-monitor/HostBuilderSettings.cs index 2f4540e2ea7..1247e126d22 100644 --- a/src/Tools/dotnet-monitor/HostBuilderSettings.cs +++ b/src/Tools/dotnet-monitor/HostBuilderSettings.cs @@ -58,6 +58,8 @@ private const string UserConfigDirectoryOverrideEnvironmentVariable public string UserConfigDirectory { get; set; } + public FileInfo UserProvidedConfigFilePath { get; set; } + /// /// Create settings for dotnet-monitor hosting. /// @@ -66,7 +68,8 @@ public static HostBuilderSettings CreateMonitor( string[] metricUrls, bool metrics, string diagnosticPort, - IAuthConfiguration authConfiguration) + IAuthConfiguration authConfiguration, + FileInfo userProvidedConfigFilePath) { return new HostBuilderSettings() { @@ -77,7 +80,8 @@ public static HostBuilderSettings CreateMonitor( Authentication = authConfiguration, ContentRootDirectory = AppContext.BaseDirectory, SharedConfigDirectory = SharedConfigDirectoryPath, - UserConfigDirectory = UserConfigDirectoryPath + UserConfigDirectory = UserConfigDirectoryPath, + UserProvidedConfigFilePath = userProvidedConfigFilePath }; } diff --git a/src/Tools/dotnet-monitor/Program.cs b/src/Tools/dotnet-monitor/Program.cs index f6b4714f601..7513b3ee752 100644 --- a/src/Tools/dotnet-monitor/Program.cs +++ b/src/Tools/dotnet-monitor/Program.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using System.Linq; using System.CommandLine.Binding; +using System.IO; namespace Microsoft.Diagnostics.Tools.Monitor { @@ -40,7 +41,7 @@ private static Command CollectCommand() SharedOptions() }; - command.SetHandler(CollectCommandHandler.Invoke, command.Children.OfType().ToArray()); + command.SetHandler(CollectCommandHandler.Invoke, command.Children.OfType().ToArray()); return command; } @@ -56,7 +57,7 @@ private static Command ConfigCommand() ShowSources() }; - showCommand.SetHandler(ConfigShowCommandHandler.Invoke, showCommand.Children.OfType().ToArray()); + showCommand.SetHandler(ConfigShowCommandHandler.Invoke, showCommand.Children.OfType().ToArray()); Command configCommand = new Command( name: "config", @@ -70,7 +71,7 @@ private static Command ConfigCommand() private static IEnumerable