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
4 changes: 3 additions & 1 deletion documentation/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public sealed class ConfigurationTests
{ WebHostDefaults.ServerUrlsKey, nameof(ConfigurationLevel.UserSettings) }
};

private static readonly Dictionary<string, string> 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<string> OrderedConfigurationKeys = new()
{
Expand All @@ -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)
Expand All @@ -101,19 +108,24 @@ 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()
{
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)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -232,20 +250,27 @@ 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()
{
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.
Expand Down Expand Up @@ -434,7 +459,8 @@ public enum ConfigurationLevel
UserSettings,
SharedSettings,
SharedKeyPerFile,
MonitorEnvironment
MonitorEnvironment,
UserProvidedFileSettings
}
}
}
Original file line number Diff line number Diff line change
@@ -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)*/
}
}
Original file line number Diff line number Diff line change
@@ -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:"
}
Expand Down
5 changes: 3 additions & 2 deletions src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Diagnostics.Tools.Monitor.Commands
{
internal static class CollectCommandHandler
{
public static async Task<int> Invoke(CancellationToken token, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth, bool tempApiKey, bool noHttpEgress)
public static async Task<int> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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<IConfiguration>();
using ConfigurationJsonWriter jsonWriter = new ConfigurationJsonWriter(stream);
Expand Down
31 changes: 30 additions & 1 deletion src/Tools/dotnet-monitor/HostBuilderHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions src/Tools/dotnet-monitor/HostBuilderResults.cs
Original file line number Diff line number Diff line change
@@ -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<string> Warnings { get; } = new();
}
}
8 changes: 6 additions & 2 deletions src/Tools/dotnet-monitor/HostBuilderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ private const string UserConfigDirectoryOverrideEnvironmentVariable

public string UserConfigDirectory { get; set; }

public FileInfo UserProvidedConfigFilePath { get; set; }

/// <summary>
/// Create settings for dotnet-monitor hosting.
/// </summary>
Expand All @@ -66,7 +68,8 @@ public static HostBuilderSettings CreateMonitor(
string[] metricUrls,
bool metrics,
string diagnosticPort,
IAuthConfiguration authConfiguration)
IAuthConfiguration authConfiguration,
FileInfo userProvidedConfigFilePath)
{
return new HostBuilderSettings()
{
Expand All @@ -77,7 +80,8 @@ public static HostBuilderSettings CreateMonitor(
Authentication = authConfiguration,
ContentRootDirectory = AppContext.BaseDirectory,
SharedConfigDirectory = SharedConfigDirectoryPath,
UserConfigDirectory = UserConfigDirectoryPath
UserConfigDirectory = UserConfigDirectoryPath,
UserProvidedConfigFilePath = userProvidedConfigFilePath
};
}

Expand Down
Loading