diff --git a/documentation/configuration/metrics-configuration.md b/documentation/configuration/metrics-configuration.md index c5e3dea8a90..396a71bb830 100644 --- a/documentation/configuration/metrics-configuration.md +++ b/documentation/configuration/metrics-configuration.md @@ -8,7 +8,7 @@ Due to limitations in event counters, `dotnet monitor` supports only **one** refresh interval when collecting metrics. This interval is used for Prometheus metrics, livemetrics, triggers, traces, and trigger actions that collect traces. The default interval is 5 seconds, but can be changed in configuration. -[8.0+] For EventCounter providers, is possible to specify a different interval for each provider. See [Per provider intervals](#per-provider-intervals-71). +[7.1+] For EventCounter providers, is possible to specify a different interval for each provider. See [Per provider intervals](#per-provider-intervals-71).
JSON @@ -39,7 +39,7 @@ Prometheus metrics, livemetrics, triggers, traces, and trigger actions that coll ```
-## Per provider intervals (8.0+) +## Per provider intervals (7.1+) It is possible to override the global interval on a per provider basis. Note this forces all scenarios (triggers, live metrics, prometheus metrics, traces) that use a particular provider to use that interval. Metrics that are `System.Diagnostics.Metrics` based always use global interval. diff --git a/documentation/schema.json b/documentation/schema.json index 2d9334c7d09..e9b2bfdea3c 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -962,6 +962,31 @@ "default": 1000, "maximum": 2147483647.0, "minimum": 1.0 + }, + "Providers": { + "type": [ + "null", + "object" + ], + "description": "Dictionary of provider names and their global configuration.", + "additionalProperties": { + "$ref": "#/definitions/GlobalProviderOptions" + } + } + } + }, + "GlobalProviderOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "IntervalSeconds": { + "type": [ + "null", + "number" + ], + "format": "float", + "maximum": 86400.0, + "minimum": 1.0 } } }, diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs index 54e601769c1..ccb83017e09 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs @@ -2,12 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; namespace Microsoft.Diagnostics.Monitoring.WebApi { - public class GlobalCounterOptions + public partial class GlobalCounterOptions { public const float IntervalMinSeconds = 1; public const float IntervalMaxSeconds = 60 * 60 * 24; // One day @@ -32,6 +35,38 @@ public class GlobalCounterOptions [DefaultValue(GlobalCounterOptionsDefaults.MaxTimeSeries)] [Range(1, int.MaxValue)] public int? MaxTimeSeries { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_GlobalCounterOptions_Providers))] + public System.Collections.Generic.IDictionary Providers { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public class GlobalProviderOptions + { + [Range(GlobalCounterOptions.IntervalMinSeconds, GlobalCounterOptions.IntervalMaxSeconds)] + public float? IntervalSeconds { get; set; } + } + + partial class GlobalCounterOptions : IValidatableObject + { + public IEnumerable Validate(ValidationContext validationContext) + { + var results = new List(); + var providerResults = new List(); + foreach ((string provider, GlobalProviderOptions options) in Providers) + { + providerResults.Clear(); + if (!Validator.TryValidateObject(options, new ValidationContext(options), providerResults, true)) + { + // We prefix the validation error with the provider. + results.AddRange(providerResults.Select(r => new ValidationResult( + string.Format(CultureInfo.CurrentCulture, OptionsDisplayStrings.ErrorMessage_NestedProviderValidationError, provider, r.ErrorMessage)))); + } + } + + return results; + } } internal static class GlobalCounterOptionsExtensions @@ -44,5 +79,8 @@ public static int GetMaxHistograms(this GlobalCounterOptions options) => public static int GetMaxTimeSeries(this GlobalCounterOptions options) => options.MaxTimeSeries.GetValueOrDefault(GlobalCounterOptionsDefaults.MaxTimeSeries); + + public static float GetProviderSpecificInterval(this GlobalCounterOptions options, string providerName) => + options.Providers.TryGetValue(providerName, out GlobalProviderOptions providerOptions) ? providerOptions.IntervalSeconds ?? options.GetIntervalSeconds() : options.GetIntervalSeconds(); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index e2954cc4557..18bce8c4da1 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -1589,6 +1589,15 @@ public static string ErrorMessage_NestedProviderValidationError { } } + /// + /// Looks up a localized string similar to Provider '{0}' validation error: '{1}'. + /// + public static string ErrorMessage_NestedProviderValidationError { + get { + return ResourceManager.GetString("ErrorMessage_NestedProviderValidationError", resourceCulture); + } + } + /// /// Looks up a localized string similar to An egress provider must be specified if there is no default egress provider.. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 9a7f8d16c76..d35db50e6be 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -804,4 +804,7 @@ Names of meters to collect from the System.Diagnostics.Metrics provider. - + + Dictionary of provider names and their global configuration. + + \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs index 564c93d94bc..d79444ad3be 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs @@ -337,7 +337,7 @@ public Task CaptureTrace( { TimeSpan duration = Utilities.ConvertSecondsToTimeSpan(durationSeconds); - var aggregateConfiguration = TraceUtilities.GetTraceConfiguration(profile, _counterOptions.CurrentValue.GetIntervalSeconds()); + var aggregateConfiguration = TraceUtilities.GetTraceConfiguration(profile, _counterOptions.CurrentValue); return StartTrace(processInfo, aggregateConfiguration, duration, egressProvider, tags); }, processKey, Utilities.ArtifactType_Trace); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsSettingsFactory.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsSettingsFactory.cs index 687eb564e1e..26fd59f56a5 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsSettingsFactory.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsSettingsFactory.cs @@ -4,6 +4,7 @@ using Microsoft.Diagnostics.Monitoring.EventPipe; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; @@ -20,6 +21,7 @@ public static MetricsPipelineSettings CreateSettings(GlobalCounterOptions counte return CreateSettings(includeDefaults, durationSeconds, counterOptions.GetIntervalSeconds(), + counterOptions.Providers, counterOptions.GetMaxHistograms(), counterOptions.GetMaxTimeSeries(), () => new List(0)); @@ -29,6 +31,7 @@ public static MetricsPipelineSettings CreateSettings(GlobalCounterOptions counte { return CreateSettings(options.IncludeDefaultProviders.GetValueOrDefault(MetricsOptionsDefaults.IncludeDefaultProviders), Timeout.Infinite, counterOptions.GetIntervalSeconds(), + counterOptions.Providers, counterOptions.GetMaxHistograms(), counterOptions.GetMaxTimeSeries(), () => ConvertCounterGroups(options.Providers, options.Meters)); @@ -40,6 +43,7 @@ public static MetricsPipelineSettings CreateSettings(GlobalCounterOptions counte return CreateSettings(configuration.IncludeDefaultProviders, durationSeconds, counterOptions.GetIntervalSeconds(), + counterOptions.Providers, counterOptions.GetMaxHistograms(), counterOptions.GetMaxTimeSeries(), () => ConvertCounterGroups(configuration.Providers, configuration.Meters)); @@ -48,6 +52,7 @@ public static MetricsPipelineSettings CreateSettings(GlobalCounterOptions counte private static MetricsPipelineSettings CreateSettings(bool includeDefaults, int durationSeconds, float counterInterval, + IDictionary intervalMap, int maxHistograms, int maxTimeSeries, Func> createCounterGroups) @@ -61,6 +66,15 @@ private static MetricsPipelineSettings CreateSettings(bool includeDefaults, eventPipeCounterGroups.Add(new EventPipeCounterGroup { ProviderName = MonitoringSourceConfiguration.GrpcAspNetCoreServer, Type = CounterGroupType.EventCounter }); } + foreach (EventPipeCounterGroup counterGroup in eventPipeCounterGroups) + { + if (intervalMap.TryGetValue(counterGroup.ProviderName, out GlobalProviderOptions providerInterval)) + { + Debug.Assert(counterGroup.IntervalSeconds == null, "Unexpected value for provider interval"); + counterGroup.IntervalSeconds = providerInterval.IntervalSeconds; + } + } + return new MetricsPipelineSettings { CounterGroups = eventPipeCounterGroups.ToArray(), diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs index 1aeb9e1bf36..6a73c585fe8 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs @@ -106,7 +106,7 @@ internal static string ErrorMessage_InvalidMetricCount { } /// - /// Looks up a localized string similar to Custom trace metric provider '{0}' must use the global counter interval '{1}'. + /// Looks up a localized string similar to Custom trace metric provider '{0}' must use the expected counter interval '{1}'.. /// internal static string ErrorMessage_InvalidMetricInterval { get { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx index aef63084a5d..c855827f271 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx @@ -136,7 +136,7 @@ Gets a string similar to "Invalid metric count.". - Custom trace metric provider '{0}' must use the global counter interval '{1}' + Custom trace metric provider '{0}' must use the expected counter interval '{1}'. Metrics was not enabled. diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 33ecebe9f2f..ee4a949fe13 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -13,7 +13,7 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class TraceUtilities { - public static MonitoringSourceConfiguration GetTraceConfiguration(Models.TraceProfile profile, float metricsIntervalSeconds) + public static MonitoringSourceConfiguration GetTraceConfiguration(Models.TraceProfile profile, GlobalCounterOptions options) { var configurations = new List(); if (profile.HasFlag(Models.TraceProfile.Cpu)) @@ -34,7 +34,14 @@ public static MonitoringSourceConfiguration GetTraceConfiguration(Models.TracePr } if (profile.HasFlag(Models.TraceProfile.Metrics)) { - configurations.Add(new MetricSourceConfiguration(metricsIntervalSeconds, Enumerable.Empty())); + IEnumerable defaultProviders = MonitoringSourceConfiguration.DefaultMetricProviders.Select(provider => new MetricEventPipeProvider + { + Provider = provider, + IntervalSeconds = options.GetProviderSpecificInterval(provider), + Type = MetricType.EventCounter + }); + + configurations.Add(new MetricSourceConfiguration(options.GetIntervalSeconds(), defaultProviders)); } return new AggregateSourceConfiguration(configurations.ToArray()); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs index a1f6ad99d65..bae063dd3c3 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs @@ -17,12 +17,12 @@ public static bool ValidateProvider(GlobalCounterOptions counterOptions, if (provider.Arguments?.TryGetValue("EventCounterIntervalSec", out string intervalValue) == true) { if (float.TryParse(intervalValue, out float intervalSeconds) && - intervalSeconds != counterOptions.GetIntervalSeconds()) + intervalSeconds != counterOptions.GetProviderSpecificInterval(provider.Name)) { errorMessage = string.Format(CultureInfo.CurrentCulture, Strings.ErrorMessage_InvalidMetricInterval, provider.Name, - counterOptions.GetIntervalSeconds()); + counterOptions.GetProviderSpecificInterval(provider.Name)); return false; } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs index 0936a44b730..436f3f25ed9 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs @@ -45,6 +45,15 @@ public static RootOptions AddGlobalCounter(this RootOptions options, int interva return options; } + public static RootOptions AddProviderInterval(this RootOptions options, string name, int intervalSeconds) + { + Assert.NotNull(options.GlobalCounter); + + options.GlobalCounter.Providers.Add(name, new GlobalProviderOptions { IntervalSeconds = (float)intervalSeconds }); + + return options; + } + public static CollectionRuleOptions CreateCollectionRule(this RootOptions rootOptions, string name) { CollectionRuleOptions options = new(); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index 4b386f019d1..a8cea62c823 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Diagnostics.Monitoring.TestCommon.Options; using Microsoft.Diagnostics.Monitoring.WebApi.Models; @@ -19,6 +20,7 @@ using System.Diagnostics.Tracing; using System.Globalization; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -1055,6 +1057,65 @@ public Task CollectionRuleOptions_CollectTraceAction_PropertyValidation() }); } + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_ValidateProviderIntervals() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + const int ExpectedInterval = 7; + + return ValidateFailure( + rootOptions => + { + rootOptions.AddGlobalCounter(5); + rootOptions.AddProviderInterval(MonitoringSourceConfiguration.SystemRuntimeEventSourceName, ExpectedInterval); + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(new EventPipeProvider[] { new EventPipeProvider + { + Name = MonitoringSourceConfiguration.SystemRuntimeEventSourceName, + Arguments = new Dictionary{ { "EventCounterIntervalSec", "5" } }, + }}, + ExpectedEgressProvider, null); + }, + ex => + { + string failure = Assert.Single(ex.Failures); + VerifyProviderIntervalMessage(failure, MonitoringSourceConfiguration.SystemRuntimeEventSourceName, ExpectedInterval); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_InvalidProviderInterval() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateFailure( + rootOptions => + { + rootOptions.AddGlobalCounter(5); + rootOptions.AddProviderInterval(MonitoringSourceConfiguration.SystemRuntimeEventSourceName, -2); + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(new EventPipeProvider[] { new EventPipeProvider + { + Name = MonitoringSourceConfiguration.SystemRuntimeEventSourceName, + Arguments = new Dictionary{ { "EventCounterIntervalSec", "5" } }, + }}, + ExpectedEgressProvider, null); + }, + ex => + { + string failure = Assert.Single(ex.Failures); + VerifyNestedGlobalInterval(failure, MonitoringSourceConfiguration.SystemRuntimeEventSourceName); + }); + } + [Fact] public Task CollectionRuleOptions_CollectTraceAction_NoProfileOrProviders() { @@ -1884,5 +1945,23 @@ private static void VerifyMissingStoppingEventProviderMessage(string[] failures, Assert.Equal(message, failures[index]); } + + private static void VerifyProviderIntervalMessage(string failure, string provider, int expectedInterval) + { + string message = string.Format(CultureInfo.CurrentCulture, WebApi.Strings.ErrorMessage_InvalidMetricInterval, provider, expectedInterval); + + Assert.Equal(message, failure); + } + + private static void VerifyNestedGlobalInterval(string failure, string provider) + { + string rangeValidationMessage = typeof(WebApi.GlobalProviderOptions) + .GetProperty(nameof(WebApi.GlobalProviderOptions.IntervalSeconds)) + .GetCustomAttribute() + .FormatErrorMessage(nameof(WebApi.GlobalProviderOptions.IntervalSeconds)); + + string message = string.Format(CultureInfo.CurrentCulture, WebApi.OptionsDisplayStrings.ErrorMessage_NestedProviderValidationError, provider, rangeValidationMessage); + Assert.Equal(message, failure); + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/MetricsSettingsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/MetricsSettingsTests.cs new file mode 100644 index 00000000000..f6a1a9a5a7b --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/MetricsSettingsTests.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.EventPipe; +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + public class MetricsSettingsTests + { + private readonly ITestOutputHelper _outputHelper; + + public MetricsSettingsTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Fact] + public void ValidateDefaultMetricSettings() + { + const int ExpectedGlobalInterval = 5; + int customInterval = ExpectedGlobalInterval + 1; + int[] expectedIntervals = MonitoringSourceConfiguration.DefaultMetricProviders.Select((_, index) => index + ExpectedGlobalInterval + 1).ToArray(); + + using IHost host = TestHostHelper.CreateHost(_outputHelper, (rootOptions) => + { + rootOptions.AddGlobalCounter(ExpectedGlobalInterval); + foreach (string provider in MonitoringSourceConfiguration.DefaultMetricProviders) + { + rootOptions.AddProviderInterval(provider, customInterval++); + } + }, + servicesCallback: null, + loggingCallback: null, + overrideSource: null); + + var options = host.Services.GetRequiredService>(); + + var settings = MetricsSettingsFactory.CreateSettings(options.CurrentValue, includeDefaults: true, durationSeconds: 30); + + Assert.Equal(ExpectedGlobalInterval, settings.CounterIntervalSeconds); + + customInterval = ExpectedGlobalInterval + 1; + foreach (string provider in MonitoringSourceConfiguration.DefaultMetricProviders) + { + Assert.Equal(customInterval++, GetInterval(settings, provider)); + } + } + + [Fact] + public void ValidateApiMetricsSettings() + { + const int ExpectedGlobalInterval = 5; + const int CustomInterval = 6; + const string CustomProvider1 = nameof(CustomProvider1); + const string CustomProvider2 = nameof(CustomProvider2); + + using IHost host = TestHostHelper.CreateHost(_outputHelper, (rootOptions) => + { + rootOptions.AddGlobalCounter(ExpectedGlobalInterval) + .AddProviderInterval(CustomProvider1, CustomInterval); + }, + servicesCallback: null, + loggingCallback: null, + overrideSource: null); + + var options = host.Services.GetRequiredService>(); + + var settings = MetricsSettingsFactory.CreateSettings(options.CurrentValue, 30, new WebApi.Models.EventMetricsConfiguration + { + IncludeDefaultProviders = false, + Providers = new[] { new WebApi.Models.EventMetricsProvider { ProviderName = CustomProvider1 }, new WebApi.Models.EventMetricsProvider { ProviderName = CustomProvider2 } } + }); + + Assert.Equal(ExpectedGlobalInterval, settings.CounterIntervalSeconds); + Assert.Equal(CustomInterval, GetInterval(settings, CustomProvider1)); + Assert.Null(GetInterval(settings, CustomProvider2)); + } + + [Fact] + public void ValidateMetricStoreSettings() + { + const int ExpectedGlobalInterval = 5; + const int CustomInterval = 6; + const string CustomProvider1 = nameof(CustomProvider1); + const string CustomProvider2 = nameof(CustomProvider2); + + using IHost host = TestHostHelper.CreateHost(_outputHelper, (rootOptions) => + { + rootOptions.AddGlobalCounter(ExpectedGlobalInterval) + .AddProviderInterval(CustomProvider1, CustomInterval); + }, + servicesCallback: null, + loggingCallback: null, + overrideSource: null); + + var options = host.Services.GetRequiredService>(); + + var settings = MetricsSettingsFactory.CreateSettings(options.CurrentValue, new MetricsOptions + { + IncludeDefaultProviders = false, + Providers = new List { new MetricProvider { ProviderName = CustomProvider1 }, new MetricProvider { ProviderName = CustomProvider2 } } + }); + + Assert.Equal(ExpectedGlobalInterval, settings.CounterIntervalSeconds); + Assert.Equal(CustomInterval, GetInterval(settings, CustomProvider1)); + Assert.Null(GetInterval(settings, CustomProvider2)); + } + + private static float? GetInterval(MetricsPipelineSettings settings, string provider) + { + EventPipeCounterGroup counterGroup = settings.CounterGroups.FirstOrDefault(g => g.ProviderName == provider); + Assert.NotNull(counterGroup); + return counterGroup.IntervalSeconds; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index 627ae4b52de..408d2e107b0 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -68,9 +68,7 @@ protected override async Task ExecuteCoreAsync( if (Options.Profile.HasValue) { TraceProfile profile = Options.Profile.Value; - float metricsIntervalSeconds = _counterOptions.CurrentValue.GetIntervalSeconds(); - - configuration = TraceUtilities.GetTraceConfiguration(profile, metricsIntervalSeconds); + configuration = TraceUtilities.GetTraceConfiguration(profile, _counterOptions.CurrentValue); } else { diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs index 182973fd250..6ab69e32461 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs @@ -55,6 +55,18 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali } else if (hasProviders) { + GlobalCounterOptions counterOptions = null; + + try + { + // Nested validations have to be handled by catching the exception and converting it to a ValidationResult. + counterOptions = validationContext.GetRequiredService>().CurrentValue; + } + catch (OptionsValidationException e) + { + results.AddRange(e.Failures.Select((string failure) => new ValidationResult(e.Message))); + } + // Validate that each provider is valid. int index = 0; foreach (EventPipeProvider provider in Providers) @@ -64,9 +76,7 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali Validator.TryValidateObject(provider, providerContext, results, validateAllProperties: true); - IOptionsMonitor counterOptions = validationContext.GetRequiredService>(); - if (!CounterValidator.ValidateProvider(counterOptions.CurrentValue, - provider, out string errorMessage)) + if (counterOptions != null && !CounterValidator.ValidateProvider(counterOptions, provider, out string errorMessage)) { results.Add(new ValidationResult(errorMessage, new[] { nameof(EventPipeProvider.Arguments) })); } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Triggers/AspNetRequestDurationTriggerFactory.cs b/src/Tools/dotnet-monitor/CollectionRules/Triggers/AspNetRequestDurationTriggerFactory.cs index 6091b366e95..616862cf5ed 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Triggers/AspNetRequestDurationTriggerFactory.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Triggers/AspNetRequestDurationTriggerFactory.cs @@ -44,7 +44,8 @@ public ICollectionRuleTrigger Create(IEndpointInfo endpointInfo, Action callback SlidingWindowDuration = options.SlidingWindowDuration ?? TimeSpan.Parse(AspNetRequestDurationOptionsDefaults.SlidingWindowDuration, CultureInfo.InvariantCulture), }; - var aspnetTriggerSourceConfiguration = new AspNetTriggerSourceConfiguration(_counterOptions.CurrentValue.GetIntervalSeconds()); + //HACK we get the provider specific interval for the configuration + var aspnetTriggerSourceConfiguration = new AspNetTriggerSourceConfiguration(_counterOptions.CurrentValue.GetProviderSpecificInterval(MonitoringSourceConfiguration.MicrosoftAspNetCoreHostingEventSourceName)); return EventPipeTriggerFactory.Create(endpointInfo, aspnetTriggerSourceConfiguration, _traceEventTriggerFactory, settings, callback); } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Triggers/EventCounterTriggerFactory.cs b/src/Tools/dotnet-monitor/CollectionRules/Triggers/EventCounterTriggerFactory.cs index 690bed92cce..309bfa90933 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Triggers/EventCounterTriggerFactory.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Triggers/EventCounterTriggerFactory.cs @@ -40,7 +40,7 @@ public ICollectionRuleTrigger Create(IEndpointInfo endpointInfo, Action callback EventCounterTriggerSettings settings = new() { ProviderName = options.ProviderName, - CounterIntervalSeconds = _counterOptions.CurrentValue.GetIntervalSeconds(), + CounterIntervalSeconds = _counterOptions.CurrentValue.GetProviderSpecificInterval(options.ProviderName), CounterName = options.CounterName, GreaterThan = options.GreaterThan, LessThan = options.LessThan, diff --git a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs index 348562a7089..672babcdcad 100644 --- a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs +++ b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs @@ -45,7 +45,9 @@ public static IServiceCollection ConfigureCors(this IServiceCollection services, public static IServiceCollection ConfigureGlobalCounter(this IServiceCollection services, IConfiguration configuration) { - return ConfigureOptions(services, configuration, ConfigurationKeys.GlobalCounter); + return ConfigureOptions(services, configuration, ConfigurationKeys.GlobalCounter) + .AddSingleton, DataAnnotationValidateOptions>(); + } public static IServiceCollection ConfigureCollectionRuleDefaults(this IServiceCollection services, IConfiguration configuration)