From e39cc9576c8ac32d0a4ec04e1169c55f9760ca46 Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Mon, 9 Aug 2021 11:24:14 -0700 Subject: [PATCH] Initial collection rule options, validation, and unit tests. (#688) --- documentation/schema.json | 250 +++-- dotnet-monitor.sln | 9 +- eng/Versions.props | 1 + .../IEgressService.cs | 6 +- .../OptionsDisplayStrings.Designer.cs | 102 +- .../OptionsDisplayStrings.resx | 44 + ...oring.ConfigurationSchema.UnitTests.csproj | 2 +- ...tics.Monitoring.ConfigurationSchema.csproj | 2 +- ...ics.Monitoring.Tool.FunctionalTests.csproj | 18 + .../Options/OptionsExtensions.cs | 58 +- .../CollectionRuleOptionsTests.cs | 874 ++++++++++++++++++ ...agnostics.Monitoring.Tool.UnitTests.csproj | 16 + .../CollectionRuleOptionsExtensions.cs | 272 ++++++ .../CollectionRuleActionOptionsProvider.cs | 39 + .../CollectionRuleActionProvider.cs | 22 + .../CollectionRuleConfigureNamedOptions.cs | 94 ++ .../CollectionRuleTriggerOptionsProvider.cs | 39 + .../CollectionRuleTriggerProvider.cs | 36 + .../ICollectionRuleActionOptionsProvider.cs | 13 + .../ICollectionRuleActionProvider.cs | 15 + .../ICollectionRuleTriggerOptionsProvider.cs | 13 + .../ICollectionRuleTriggerProvider.cs | 15 + .../KnownCollectionRuleActions.cs | 15 + .../KnownCollectionRuleTriggers.cs | 18 + .../Options/Actions/ActionOptionsConstants.cs | 29 + .../Options/Actions/CollectDumpOptions.cs | 30 + .../Options/Actions/CollectGCDumpOptions.cs | 26 + .../Actions/CollectLogsOptions.Validate.cs | 50 + .../Options/Actions/CollectLogsOptions.cs | 39 + .../Actions/CollectTraceOptions.Validate.cs | 70 ++ .../Options/Actions/CollectTraceOptions.cs | 45 + .../Options/Actions/ExecuteOptions.cs | 25 + .../ValidateEgressProviderAttribute.cs | 32 + .../CollectionRuleActionOptions.Validate.cs | 41 + .../Options/CollectionRuleActionOptions.cs | 32 + .../Options/CollectionRuleLimitsOptions.cs | 37 + .../Options/CollectionRuleOptions.Validate.cs | 34 + .../Options/CollectionRuleOptions.cs | 41 + .../Options/CollectionRuleOptionsConstants.cs | 19 + .../CollectionRuleTriggerOptions.Validate.cs | 41 + .../Options/CollectionRuleTriggerOptions.cs | 32 + .../Triggers/AspNetRequestCountOptions.cs | 29 + .../Triggers/AspNetRequestDurationOptions.cs | 31 + .../Triggers/AspNetResponseStatusOptions.cs | 32 + .../Triggers/EventCounterOptions.Validate.cs | 38 + .../Options/Triggers/EventCounterOptions.cs | 35 + .../Triggers/TriggerOptionsConstants.cs | 24 + .../Options/ValidationHelper.cs | 60 ++ src/Tools/dotnet-monitor/ConfigurationKeys.cs | 2 + ...ns.cs => DataAnnotationValidateOptions.cs} | 28 +- .../DiagnosticsMonitorCommandHandler.cs | 7 +- .../dotnet-monitor/Egress/EgressService.cs | 12 + src/Tools/dotnet-monitor/LoggingExtensions.cs | 22 + .../dotnet-monitor}/RootOptions.cs | 15 +- .../ServiceCollectionExtensions.cs | 55 +- src/Tools/dotnet-monitor/Strings.Designer.cs | 54 ++ src/Tools/dotnet-monitor/Strings.resx | 30 + .../dotnet-monitor/dotnet-monitor.csproj | 1 + 58 files changed, 2976 insertions(+), 95 deletions(-) create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionOptionsProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleConfigureNamedOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerOptionsProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionOptionsProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerOptionsProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerProvider.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleActions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleTriggers.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ActionOptionsConstants.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectDumpOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectGCDumpOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.Validate.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ExecuteOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ValidateEgressProviderAttribute.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.Validate.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleLimitsOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.Validate.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptionsConstants.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.Validate.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestCountOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestDurationOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetResponseStatusOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.Validate.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/TriggerOptionsConstants.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/ValidationHelper.cs rename src/Tools/dotnet-monitor/{Egress/Configuration/EgressProviderValidateOptions.cs => DataAnnotationValidateOptions.cs} (57%) rename src/{Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options => Tools/dotnet-monitor}/RootOptions.cs (62%) diff --git a/documentation/schema.json b/documentation/schema.json index a2990e09fa6..607ba0d97c7 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -16,6 +16,16 @@ } ] }, + "CollectionRules": { + "type": [ + "null", + "object" + ], + "default": {}, + "additionalProperties": { + "$ref": "#/definitions/CollectionRuleOptions" + } + }, "CorsConfiguration": { "default": {}, "oneOf": [ @@ -105,6 +115,188 @@ } } }, + "CollectionRuleOptions": { + "type": "object", + "additionalProperties": false, + "required": [ + "Trigger" + ], + "properties": { + "Filters": { + "type": [ + "array", + "null" + ], + "description": "Process filters used to determine to which process(es) the collection rule is applied. All filters must match. If no filters are specified, the rule is applied to all discovered processes.", + "items": { + "$ref": "#/definitions/ProcessFilterDescriptor" + } + }, + "Trigger": { + "description": "The trigger to use to monitor for a condition in the target process.", + "oneOf": [ + { + "$ref": "#/definitions/CollectionRuleTriggerOptions" + } + ] + }, + "Actions": { + "type": [ + "array", + "null" + ], + "description": "The list of actions to be executed when the trigger raises its notification.", + "items": { + "$ref": "#/definitions/CollectionRuleActionOptions" + } + }, + "Limits": { + "description": "The set of limits to constrain the execution of the rule and its components.", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/CollectionRuleLimitsOptions" + } + ] + } + } + }, + "ProcessFilterDescriptor": { + "type": "object", + "additionalProperties": false, + "required": [ + "Key", + "Value" + ], + "properties": { + "Key": { + "description": "The criteria used to compare against the target process.", + "oneOf": [ + { + "$ref": "#/definitions/ProcessFilterKey" + } + ] + }, + "Value": { + "type": "string", + "description": "The value of the criteria used to compare against the target process.", + "minLength": 1 + }, + "MatchType": { + "description": "Type of match to use against the process criteria.", + "default": "Exact", + "oneOf": [ + { + "$ref": "#/definitions/ProcessFilterType" + } + ] + } + } + }, + "ProcessFilterKey": { + "type": "string", + "description": "", + "x-enumNames": [ + "ProcessId", + "ProcessName", + "CommandLine" + ], + "enum": [ + "ProcessId", + "ProcessName", + "CommandLine" + ] + }, + "ProcessFilterType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Exact", + "Contains" + ], + "enum": [ + "Exact", + "Contains" + ] + }, + "CollectionRuleTriggerOptions": { + "type": "object", + "additionalProperties": false, + "required": [ + "Type" + ], + "properties": { + "Type": { + "type": "string", + "description": "The type of trigger used to monitor for a condition in the target process.", + "minLength": 1 + }, + "Settings": { + "description": "The settings to pass to the trigger when it is executed. Settings may be optional if the trigger doesn't require settings or its settings are all optional.", + "oneOf": [ + {}, + { + "type": "null" + } + ] + } + } + }, + "CollectionRuleActionOptions": { + "type": "object", + "additionalProperties": false, + "required": [ + "Type" + ], + "properties": { + "Type": { + "type": "string", + "description": "The type of action to execute.", + "minLength": 1 + }, + "Settings": { + "description": "The settings to pass to the action when it is executed. Settings may be optional if the action doesn't require settings or its settings are all optional.", + "oneOf": [ + {}, + { + "type": "null" + } + ] + } + } + }, + "CollectionRuleLimitsOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "ActionCount": { + "type": [ + "integer", + "null" + ], + "description": "The number of times the action list may be executed before being throttled.", + "format": "int32" + }, + "ActionCountSlidingWindowDuration": { + "type": [ + "null", + "string" + ], + "description": "The sliding window of time to consider whether the action list should be throttled based on the number of times the action list was executed. Executions that fall outside the window will not count toward the limit specified in the ActionCount setting.", + "format": "time-span" + }, + "RuleDuration": { + "type": [ + "null", + "string" + ], + "description": "The amount of time before the rule will stop monitoring a process after it has been applied to a process. If not specified, the rule will monitor the process with the trigger indefinitely.", + "format": "time-span" + } + } + }, "CorsConfiguration": { "type": "object", "additionalProperties": false, @@ -405,64 +597,6 @@ } } } - }, - "ProcessFilterDescriptor": { - "type": "object", - "additionalProperties": false, - "required": [ - "Key", - "Value" - ], - "properties": { - "Key": { - "description": "The criteria used to compare against the target process.", - "oneOf": [ - { - "$ref": "#/definitions/ProcessFilterKey" - } - ] - }, - "Value": { - "type": "string", - "description": "The value of the criteria used to compare against the target process.", - "minLength": 1 - }, - "MatchType": { - "description": "Type of match to use against the process criteria.", - "default": "Exact", - "oneOf": [ - { - "$ref": "#/definitions/ProcessFilterType" - } - ] - } - } - }, - "ProcessFilterKey": { - "type": "string", - "description": "", - "x-enumNames": [ - "ProcessId", - "ProcessName", - "CommandLine" - ], - "enum": [ - "ProcessId", - "ProcessName", - "CommandLine" - ] - }, - "ProcessFilterType": { - "type": "string", - "description": "", - "x-enumNames": [ - "Exact", - "Contains" - ], - "enum": [ - "Exact", - "Contains" - ] } } } \ No newline at end of file diff --git a/dotnet-monitor.sln b/dotnet-monitor.sln index 54314e01c62..644cc47df1d 100644 --- a/dotnet-monitor.sln +++ b/dotnet-monitor.sln @@ -30,10 +30,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monit EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests", "src\Tests\Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests\Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests.csproj", "{886D1FD5-125F-435B-934C-30DF2938E7F6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Diagnostics.Monitoring.ConfigurationSchema", "src\Tests\Microsoft.Diagnostics.Monitoring.ConfigurationSchema\Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj", "{422ABBF6-6236-4042-AACA-09531DBDFBAA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.ConfigurationSchema", "src\Tests\Microsoft.Diagnostics.Monitoring.ConfigurationSchema\Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj", "{422ABBF6-6236-4042-AACA-09531DBDFBAA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.WebApi.UnitTests", "src\Tests\Microsoft.Diagnostics.Monitoring.WebApi.UnitTests\Microsoft.Diagnostics.Monitoring.WebApi.UnitTests.csproj", "{3AD0A40B-C569-4712-9764-7A788B9CD811}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.Tool.UnitTests", "src\Tests\Microsoft.Diagnostics.Monitoring.Tool.UnitTests\Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj", "{0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +82,10 @@ Global {3AD0A40B-C569-4712-9764-7A788B9CD811}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AD0A40B-C569-4712-9764-7A788B9CD811}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AD0A40B-C569-4712-9764-7A788B9CD811}.Release|Any CPU.Build.0 = Release|Any CPU + {0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -98,6 +104,7 @@ Global {886D1FD5-125F-435B-934C-30DF2938E7F6} = {C7568468-1C79-4944-8136-18812A7F9EA7} {422ABBF6-6236-4042-AACA-09531DBDFBAA} = {C7568468-1C79-4944-8136-18812A7F9EA7} {3AD0A40B-C569-4712-9764-7A788B9CD811} = {C7568468-1C79-4944-8136-18812A7F9EA7} + {0DBE362D-82F1-4740-AE6A-40C1A82EDCDB} = {C7568468-1C79-4944-8136-18812A7F9EA7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0} diff --git a/eng/Versions.props b/eng/Versions.props index 9f89ebb28d8..7bc5cb8ca4e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -53,6 +53,7 @@ 2.1.3 2.1.7 1.1.0 + 5.0.0 5.0.2 5.0.0 5.0.1 diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs index 8bb1a19531b..3062ba57e27 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs @@ -11,8 +11,10 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { internal interface IEgressService { + bool CheckProvider(string providerName); + Task EgressAsync( - string endpointName, + string providerName, Func> action, string fileName, string contentType, @@ -20,7 +22,7 @@ Task EgressAsync( CancellationToken token); Task EgressAsync( - string endpointName, + string providerName, Func action, string fileName, string contentType, diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.Designer.cs index d6a99d1d3f5..e1110f60912 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class OptionsDisplayStrings { @@ -142,6 +142,106 @@ public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_ } } + /// + /// Looks up a localized string similar to The settings to pass to the action when it is executed. Settings may be optional if the action doesn't require settings or its settings are all optional.. + /// + public static string DisplayAttributeDescription_CollectionRuleActionOptions_Settings { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleActionOptions_Settings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type of action to execute.. + /// + public static string DisplayAttributeDescription_CollectionRuleActionOptions_Type { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleActionOptions_Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The number of times the action list may be executed before being throttled.. + /// + public static string DisplayAttributeDescription_CollectionRuleLimitsOptions_ActionCount { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleLimitsOptions_ActionCount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The sliding window of time to consider whether the action list should be throttled based on the number of times the action list was executed. Executions that fall outside the window will not count toward the limit specified in the ActionCount setting.. + /// + public static string DisplayAttributeDescription_CollectionRuleLimitsOptions_ActionCountSlidingWindowDuration { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleLimitsOptions_ActionCountSlidingWindowD" + + "uration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The amount of time before the rule will stop monitoring a process after it has been applied to a process. If not specified, the rule will monitor the process with the trigger indefinitely.. + /// + public static string DisplayAttributeDescription_CollectionRuleLimitsOptions_RuleDuration { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleLimitsOptions_RuleDuration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The list of actions to be executed when the trigger raises its notification.. + /// + public static string DisplayAttributeDescription_CollectionRuleOptions_Actions { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleOptions_Actions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Process filters used to determine to which process(es) the collection rule is applied. All filters must match. If no filters are specified, the rule is applied to all discovered processes.. + /// + public static string DisplayAttributeDescription_CollectionRuleOptions_Filters { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleOptions_Filters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The set of limits to constrain the execution of the rule and its components.. + /// + public static string DisplayAttributeDescription_CollectionRuleOptions_Limits { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleOptions_Limits", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The trigger to use to monitor for a condition in the target process.. + /// + public static string DisplayAttributeDescription_CollectionRuleOptions_Trigger { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleOptions_Trigger", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The settings to pass to the trigger when it is executed. Settings may be optional if the trigger doesn't require settings or its settings are all optional.. + /// + public static string DisplayAttributeDescription_CollectionRuleTriggerOptions_Settings { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleTriggerOptions_Settings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type of trigger used to monitor for a condition in the target process.. + /// + public static string DisplayAttributeDescription_CollectionRuleTriggerOptions_Type { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectionRuleTriggerOptions_Type", resourceCulture); + } + } + /// /// Looks up a localized string similar to Buffer size used when copying data from an egress callback returning a stream to the egress callback that is provided a stream to which data is written.. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.resx index aa836e9700a..aa6c3a630d4 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/OptionsDisplayStrings.resx @@ -153,6 +153,50 @@ The name of the shared access signature (SAS) used to look up the value from the Egress options Properties map. The description provided for the SharedAccessSignatureName parameter on AzureBlobEgressProviderOptions. + + The settings to pass to the action when it is executed. Settings may be optional if the action doesn't require settings or its settings are all optional. + The description provided for the Settings parameter on CollectionRuleActionOptions. + + + The type of action to execute. + The description provided for the Type parameter on CollectionRuleActionOptions. + + + The number of times the action list may be executed before being throttled. + The description provided for the ActionCount parameter on CollectionRuleLimitsOptions. + + + The sliding window of time to consider whether the action list should be throttled based on the number of times the action list was executed. Executions that fall outside the window will not count toward the limit specified in the ActionCount setting. + The description provided for the ActionCountSlidingWindowDuration parameter on CollectionRuleLimitsOptions. + + + The amount of time before the rule will stop monitoring a process after it has been applied to a process. If not specified, the rule will monitor the process with the trigger indefinitely. + The description provided for the RuleDuration parameter on CollectionRuleLimitsOptions. + + + The list of actions to be executed when the trigger raises its notification. + The description provided for the Actions parameter on CollectionRuleOptions. + + + Process filters used to determine to which process(es) the collection rule is applied. All filters must match. If no filters are specified, the rule is applied to all discovered processes. + The description provided for the Filters parameter on CollectionRuleOptions. + + + The set of limits to constrain the execution of the rule and its components. + The description provided for the Limits parameter on CollectionRuleOptions. + + + The trigger to use to monitor for a condition in the target process. + The description provided for the Trigger parameter on CollectionRuleOptions. + + + The settings to pass to the trigger when it is executed. Settings may be optional if the trigger doesn't require settings or its settings are all optional. + The description provided for the Settings parameter on CollectionRuleTriggerOptions. + + + The type of trigger used to monitor for a condition in the target process. + The description provided for the Type parameter on CollectionRuleTriggerOptions. + Buffer size used when copying data from an egress callback returning a stream to the egress callback that is provided a stream to which data is written. The description provided for the CopyBufferSize parameter on all egress provider options. diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests.csproj index 9d8af698f3c..dbf0280b427 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests.csproj @@ -1,6 +1,6 @@ - netcoreapp3.1 + net50 Library diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj index b9c3f6c63b8..bb497d5b441 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net50 diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj index 745f1d5d614..ddee7a4c05c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj @@ -24,13 +24,31 @@ + + + + + + + + + + + + + + + + + + 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 e9b087df993..a334c5217a5 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Extensions.Configuration; using System; using System.Collections; using System.Collections.Generic; @@ -11,10 +12,19 @@ using System.Security.Cryptography; using System.Text; +#if !UNITTEST +using Microsoft.Diagnostics.Tools.Monitor; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Diagnostics.Tools.Monitor.Egress.FileSystem; +#endif + namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options { internal static class OptionsExtensions { + private const string EnvironmentVariablePrefix = "DotnetMonitor_"; + private const string KeySegmentSeparator = "__"; + /// /// Generates an environment variable map of the options. /// @@ -24,7 +34,20 @@ internal static class OptionsExtensions public static IDictionary ToEnvironmentConfiguration(this RootOptions options) { Dictionary variables = new(StringComparer.OrdinalIgnoreCase); - MapObject(options, "DotNetMonitor_", variables); + MapObject(options, EnvironmentVariablePrefix, KeySegmentSeparator, variables); + return variables; + } + + /// + /// Generates a map of options that can be passed directly to configuration via an in-memory collection. + /// + /// + /// Each key is the configuration path; each value is the configuration path value. + /// + public static IDictionary ToConfigurationValues(this RootOptions options) + { + Dictionary variables = new(StringComparer.OrdinalIgnoreCase); + MapObject(options, string.Empty, ConfigurationPath.KeyDelimiter, variables); return variables; } @@ -37,7 +60,7 @@ public static IDictionary ToEnvironmentConfiguration(this RootOp public static IDictionary ToKeyPerFileConfiguration(this RootOptions options) { Dictionary variables = new(StringComparer.OrdinalIgnoreCase); - MapObject(options, string.Empty, variables); + MapObject(options, string.Empty, KeySegmentSeparator, variables); return variables; } @@ -78,7 +101,14 @@ public static RootOptions AddFileSystemEgress(this RootOptions options, string n return options; } - private static void MapDictionary(IDictionary dictionary, string prefix, IDictionary map) + public static CollectionRuleOptions CreateCollectionRule(this RootOptions rootOptions, string name) + { + CollectionRuleOptions options = new(); + rootOptions.CollectionRules.Add(name, options); + return options; + } + + private static void MapDictionary(IDictionary dictionary, string prefix, string separator, IDictionary map) { foreach (var key in dictionary.Keys) { @@ -89,12 +119,13 @@ private static void MapDictionary(IDictionary dictionary, string prefix, IDictio MapValue( value, FormattableString.Invariant($"{prefix}{keyString}"), + separator, map); } } } - private static void MapList(IList list, string prefix, IDictionary map) + private static void MapList(IList list, string prefix, string separator, IDictionary map) { for (int index = 0; index < list.Count; index++) { @@ -104,12 +135,13 @@ private static void MapList(IList list, string prefix, IDictionary map) + private static void MapObject(object obj, string prefix, string separator, IDictionary map) { foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { @@ -118,17 +150,21 @@ private static void MapObject(object obj, string prefix, IDictionary map) + private static void MapValue(object value, string valueName, string separator, IDictionary map) { if (null != value) { Type valueType = value.GetType(); - if (valueType.IsPrimitive || typeof(string) == valueType) + if (valueType.IsPrimitive || + valueType.IsEnum || + typeof(string) == valueType || + typeof(TimeSpan) == valueType) { map.Add( valueName, @@ -136,18 +172,18 @@ private static void MapValue(object value, string valueName, IDictionary + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger(); + }, + ruleOptions => + { + Assert.Empty(ruleOptions.Filters); + + ruleOptions.VerifyStartupTrigger(); + + Assert.Empty(ruleOptions.Actions); + + Assert.Null(ruleOptions.Limits); + }); + } + + [Fact] + public Task CollectionRuleOptions_NoTrigger() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .AddExecuteAction("cmd.exe"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyRequiredMessage(failures, 0, nameof(CollectionRuleOptions.Trigger)); + }); + } + + [Fact] + public Task CollectionRuleOptions_UnknownTrigger() + { + const string ExpectedTriggerType = "UnknownTrigger"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetTrigger(ExpectedTriggerType); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyUnknownTriggerTypeMessage(failures, 0, ExpectedTriggerType); + }); + } + + [Fact] + public Task CollectionRuleOptions_UnknownAction() + { + const string ExpectedActionType = "UnknownAction"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddAction(ExpectedActionType); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyUnknownActionTypeMessage(failures, 0, ExpectedActionType); + }); + } + + [Fact] + public Task CollectionRuleOptions_EventCounterTrigger_MinimumOptions() + { + const string ExpectedProviderName = "System.Runtime"; + const string ExpectedCounterName = "cpu-usage"; + const double ExpectedGreaterThan = 0.5; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetEventCounterTrigger(out EventCounterOptions eventCounterOptions); + + eventCounterOptions.ProviderName = ExpectedProviderName; + eventCounterOptions.CounterName = ExpectedCounterName; + eventCounterOptions.GreaterThan = ExpectedGreaterThan; + }, + ruleOptions => + { + EventCounterOptions eventCounterOptions = ruleOptions.VerifyEventCounterTrigger(); + Assert.Equal(ExpectedProviderName, eventCounterOptions.ProviderName); + Assert.Equal(ExpectedCounterName, eventCounterOptions.CounterName); + Assert.Equal(ExpectedGreaterThan, eventCounterOptions.GreaterThan); + }); + } + + [Fact] + public Task CollectionRuleOptions_EventCounterTrigger_RoundTrip() + { + const string ExpectedProviderName = "System.Runtime"; + const string ExpectedCounterName = "cpu-usage"; + const double ExpectedGreaterThan = 0.5; + const double ExpectedLessThan = 0.75; + TimeSpan ExpectedDuration = TimeSpan.FromSeconds(30); + const int ExpectedFrequency = 5; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetEventCounterTrigger(out EventCounterOptions eventCounterOptions); + + eventCounterOptions.ProviderName = ExpectedProviderName; + eventCounterOptions.CounterName = ExpectedCounterName; + eventCounterOptions.GreaterThan = ExpectedGreaterThan; + eventCounterOptions.LessThan = ExpectedLessThan; + eventCounterOptions.SlidingWindowDuration = ExpectedDuration; + eventCounterOptions.Frequency = ExpectedFrequency; + }, + ruleOptions => + { + EventCounterOptions eventCounterOptions = ruleOptions.VerifyEventCounterTrigger(); + Assert.Equal(ExpectedProviderName, eventCounterOptions.ProviderName); + Assert.Equal(ExpectedCounterName, eventCounterOptions.CounterName); + Assert.Equal(ExpectedGreaterThan, eventCounterOptions.GreaterThan); + Assert.Equal(ExpectedLessThan, eventCounterOptions.LessThan); + Assert.Equal(ExpectedDuration, eventCounterOptions.SlidingWindowDuration); + Assert.Equal(ExpectedFrequency, eventCounterOptions.Frequency); + }); + } + + [Fact] + public Task CollectionRuleOptions_EventCounterTrigger_PropertyValidation() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetEventCounterTrigger(out EventCounterOptions eventCounterOptions); + + eventCounterOptions.SlidingWindowDuration = TimeSpan.FromSeconds(-1); + eventCounterOptions.Frequency = -1; + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + // Property validation failures will short-circuit the remainder of the validation + // rules, thus only observe 4 errors when one might expect 5 (the fifth being that + // either GreaterThan or LessThan should be specified). + Assert.Equal(4, failures.Length); + VerifyRequiredMessage(failures, 0, nameof(EventCounterOptions.ProviderName)); + VerifyRequiredMessage(failures, 1, nameof(EventCounterOptions.CounterName)); + VerifyRangeMessage(failures, 2, nameof(EventCounterOptions.SlidingWindowDuration), + TriggerOptionsConstants.SlidingWindowDuration_MinValue, TriggerOptionsConstants.SlidingWindowDuration_MaxValue); + VerifyRangeMessage(failures, 3, nameof(EventCounterOptions.Frequency), + TriggerOptionsConstants.Frequency_MinValue_String, TriggerOptionsConstants.Frequency_MaxValue_String); + }); + } + + [Fact] + public Task CollectionRuleOptions_EventCounterTrigger_NoGreaterThanOrLessThan() + { + const string ExpectedProviderName = "System.Runtime"; + const string ExpectedCounterName = "cpu-usage"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetEventCounterTrigger(out EventCounterOptions eventCounterOptions); + + eventCounterOptions.ProviderName = ExpectedProviderName; + eventCounterOptions.CounterName = ExpectedCounterName; + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyEitherRequiredMessage(failures, 0, + nameof(EventCounterOptions.GreaterThan), nameof(EventCounterOptions.LessThan)); + }); + } + + [Fact] + public Task CollectionRuleOptions_EventCounterTrigger_GreaterThanLargerThanLessThan() + { + const string ExpectedProviderName = "System.Runtime"; + const string ExpectedCounterName = "cpu-usage"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetEventCounterTrigger(out EventCounterOptions eventCounterOptions); + + eventCounterOptions.ProviderName = ExpectedProviderName; + eventCounterOptions.CounterName = ExpectedCounterName; + eventCounterOptions.GreaterThan = 0.75; + eventCounterOptions.LessThan = 0.5; + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyFieldLessThanOtherFieldMessage(failures, 0, nameof(EventCounterOptions.GreaterThan), nameof(EventCounterOptions.LessThan)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectDumpAction_RoundTrip() + { + const DumpType ExpectedDumpType = DumpType.Mini; + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectDumpAction(ExpectedDumpType, ExpectedEgressProvider); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + ruleOptions.VerifyCollectDumpAction(0, ExpectedDumpType, ExpectedEgressProvider); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectDumpAction_PropertyValidation() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectDumpAction((DumpType)20, UnknownEgressName); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Equal(2, failures.Length); + VerifyEnumDataTypeMessage(failures, 0, nameof(CollectDumpOptions.Type)); + VerifyEgressNotExistMessage(failures, 1, UnknownEgressName); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectGCDumpAction_RoundTrip() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectGCDumpAction(ExpectedEgressProvider); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + ruleOptions.VerifyCollectGCDumpAction(0, ExpectedEgressProvider); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectGCDumpAction_PropertyValidation() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectGCDumpAction(UnknownEgressName); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyEgressNotExistMessage(failures, 0, UnknownEgressName); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectLogsAction_MinimumOptions() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectLogsAction(ExpectedEgressProvider); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + ruleOptions.VerifyCollectLogsAction(0, ExpectedEgressProvider); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectLogsAction_RoundTrip() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + const bool ExpectedUseAppFilters = true; + const LogLevel ExpectedLogLevel = LogLevel.Debug; + Dictionary ExpectedFilterSpecs = new() + { + { "CategoryA", LogLevel.Information }, + { "CategoryA.SubCategoryA", LogLevel.Warning } + }; + TimeSpan ExpectedDuration = TimeSpan.FromMinutes(2); + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectLogsAction(ExpectedEgressProvider, out CollectLogsOptions collectLogsOptions); + + collectLogsOptions.UseAppFilters = ExpectedUseAppFilters; + collectLogsOptions.LogLevel = ExpectedLogLevel; + collectLogsOptions.FilterSpecs = ExpectedFilterSpecs; + collectLogsOptions.Duration = ExpectedDuration; + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + CollectLogsOptions collectLogsOptions = ruleOptions.VerifyCollectLogsAction(0, ExpectedEgressProvider); + Assert.Equal(ExpectedUseAppFilters, collectLogsOptions.UseAppFilters); + Assert.Equal(ExpectedLogLevel, collectLogsOptions.LogLevel); + Assert.NotNull(collectLogsOptions.FilterSpecs); + Assert.Equal(ExpectedFilterSpecs.Count, collectLogsOptions.FilterSpecs.Count); + foreach ((string expectedCategory, LogLevel? expectedLogLevel) in ExpectedFilterSpecs) + { + Assert.True(collectLogsOptions.FilterSpecs.TryGetValue(expectedCategory, out LogLevel? actualLogLevel)); + Assert.Equal(expectedLogLevel, actualLogLevel); + } + Assert.Equal(ExpectedDuration, collectLogsOptions.Duration); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectLogsAction_PropertyValidation() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectLogsAction(egress: null, out CollectLogsOptions collectLogsOptions); + + collectLogsOptions.LogLevel = (LogLevel)100; + collectLogsOptions.Duration = TimeSpan.FromDays(3); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Equal(3, failures.Length); + VerifyEnumDataTypeMessage(failures, 0, nameof(CollectLogsOptions.LogLevel)); + VerifyRangeMessage(failures, 1, nameof(CollectLogsOptions.Duration), + ActionOptionsConstants.Duration_MinValue, ActionOptionsConstants.Duration_MaxValue); + VerifyRequiredMessage(failures, 2, nameof(CollectLogsOptions.Egress)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectLogsAction_FilterSpecValidation() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectLogsAction(ExpectedEgressProvider, out CollectLogsOptions collectLogsOptions); + + collectLogsOptions.FilterSpecs = new Dictionary() + { + { "CategoryA", (LogLevel)50 } + }; + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyEnumDataTypeMessage(failures, 0, nameof(CollectLogsOptions.FilterSpecs)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_MinimumOptions_Profile() + { + const TraceProfile ExpectedProfile = TraceProfile.Http; + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProfile, ExpectedEgressProvider); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + ruleOptions.VerifyCollectTraceAction(0, ExpectedProfile, ExpectedEgressProvider); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_MinimumOptions_Providers() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + List ExpectedProviders = new() + { + new() { Name = "Microsoft-Extensions-Logging" } + }; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + ruleOptions.VerifyCollectTraceAction(0, ExpectedProviders, ExpectedEgressProvider); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_RoundTrip_Profile() + { + const TraceProfile ExpectedProfile = TraceProfile.Logs; + const string ExpectedEgressProvider = "TmpEgressProvider"; + TimeSpan ExpectedDuration = TimeSpan.FromSeconds(45); + const int ExpectedMetricsIntervalSeconds = 3; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProfile, ExpectedEgressProvider, out CollectTraceOptions collectTraceOptions); + + collectTraceOptions.Duration = ExpectedDuration; + collectTraceOptions.MetricsIntervalSeconds = ExpectedMetricsIntervalSeconds; + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + CollectTraceOptions collectTraceOptions = ruleOptions.VerifyCollectTraceAction(0, ExpectedProfile, ExpectedEgressProvider); + + Assert.Equal(ExpectedDuration, collectTraceOptions.Duration); + Assert.Equal(ExpectedMetricsIntervalSeconds, collectTraceOptions.MetricsIntervalSeconds); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_RoundTrip_Providers() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + List ExpectedProviders = new() + { + new() + { + Name = "Microsoft-Extensions-Logging", + EventLevel = EventLevel.Warning, + Keywords = "0xC", + Arguments = new Dictionary() + { + { "FilterSpecs", "UseAppFilters" } + } + } + }; + TimeSpan ExpectedDuration = TimeSpan.FromSeconds(20); + const int ExpectedBufferSizeMegabytes = 128; + const bool ExpectedRequestRundown = false; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider, out CollectTraceOptions collectTraceOptions); + + collectTraceOptions.BufferSizeMegabytes = ExpectedBufferSizeMegabytes; + collectTraceOptions.Duration = ExpectedDuration; + collectTraceOptions.RequestRundown = ExpectedRequestRundown; + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + CollectTraceOptions collectTraceOptions = ruleOptions.VerifyCollectTraceAction(0, ExpectedProviders, ExpectedEgressProvider); + + Assert.Equal(ExpectedBufferSizeMegabytes, collectTraceOptions.BufferSizeMegabytes); + Assert.Equal(ExpectedDuration, collectTraceOptions.Duration); + Assert.Equal(ExpectedRequestRundown, collectTraceOptions.RequestRundown); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_PropertyValidation() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction((TraceProfile)75, egress: null, out CollectTraceOptions collectTraceOptions); + + collectTraceOptions.BufferSizeMegabytes = 2048; + collectTraceOptions.Duration = TimeSpan.FromDays(7); + collectTraceOptions.MetricsIntervalSeconds = (int)TimeSpan.FromDays(3).TotalSeconds; + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Equal(5, failures.Length); + VerifyEnumDataTypeMessage(failures, 0, nameof(CollectTraceOptions.Profile)); + VerifyRangeMessage(failures, 1, nameof(CollectTraceOptions.MetricsIntervalSeconds), + ActionOptionsConstants.MetricsIntervalSeconds_MinValue_String, ActionOptionsConstants.MetricsIntervalSeconds_MaxValue_String); + VerifyRangeMessage(failures, 2, nameof(CollectTraceOptions.BufferSizeMegabytes), + ActionOptionsConstants.BufferSizeMegabytes_MinValue_String, ActionOptionsConstants.BufferSizeMegabytes_MaxValue_String); + VerifyRangeMessage(failures, 3, nameof(CollectTraceOptions.Duration), + ActionOptionsConstants.Duration_MinValue, ActionOptionsConstants.Duration_MaxValue); + VerifyRequiredMessage(failures, 4, nameof(CollectTraceOptions.Egress)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_NoProfileOrProviders() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(Enumerable.Empty(), ExpectedEgressProvider); + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyEitherRequiredMessage(failures, 0, + nameof(CollectTraceOptions.Profile), nameof(CollectTraceOptions.Providers)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_BothProfileAndProviders() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(TraceProfile.Metrics, ExpectedEgressProvider, out CollectTraceOptions collectTraceOptions); + + collectTraceOptions.Providers = new List() + { + new() { Name = "Microsoft-Extensions-Logging" } + }; + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyBothCannotBeSpecifiedMessage(failures, 0, + nameof(CollectTraceOptions.Profile), nameof(CollectTraceOptions.Providers)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_ProviderPropertyValidation() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + List ExpectedProviders = new() + { + new() { Keywords = null } + }; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider); + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyRequiredMessage(failures, 0, nameof(EventPipeProvider.Name)); + }); + } + + [Fact] + public Task CollectionRuleOptions_ExecuteAction_MinimumOptions() + { + const string ExpectedExePath = "cmd.exe"; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddExecuteAction(ExpectedExePath); + }, + ruleOptions => + { + Assert.Single(ruleOptions.Actions); + ruleOptions.VerifyExecuteAction(0, ExpectedExePath); + }); + } + + [Fact] + public Task CollectionRuleOptions_ExecuteAction_RoundTrip() + { + const string ExpectedExePath = "cmd.exe"; + const string ExpectedArguments = "/c \"echo Hello\""; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddExecuteAction(ExpectedExePath, ExpectedArguments); + }, + ruleOptions => + { + Assert.Single(ruleOptions.Actions); + ruleOptions.VerifyExecuteAction(0, ExpectedExePath, ExpectedArguments); + }); + } + + [Fact] + public Task CollectionRuleOptions_ExecuteAction_PropertyValidation() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddExecuteAction(path: null, "arg1 arg2"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyRequiredMessage(failures, 0, nameof(ExecuteOptions.Path)); + }); + } + + private async Task Validate( + Action setup, + Action> validate) + { + IHost host = new HostBuilder() + .ConfigureAppConfiguration(builder => + { + RootOptions options = new(); + setup(options); + + IDictionary configurationValues = options.ToConfigurationValues(); + _outputHelper.WriteLine("Begin Configuration:"); + foreach ((string key, string value) in configurationValues) + { + _outputHelper.WriteLine("{0} = {1}", key, value); + } + _outputHelper.WriteLine("End Configuration"); + + builder.AddInMemoryCollection(configurationValues); + }) + .ConfigureServices(services => + { + services.ConfigureCollectionRules(); + services.ConfigureEgress(); + }) + .Build(); + + try + { + validate(host.Services.GetRequiredService>()); + } + finally + { + if (host is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + host.Dispose(); + } + } + } + + private Task ValidateSuccess( + Action setup, + Action validate) + { + return Validate( + setup, + monitor => validate(monitor.Get(DefaultRuleName))); + } + + private Task ValidateFailure( + Action setup, + Action validate) + { + return Validate( + setup, + monitor => + { + OptionsValidationException ex = Assert.Throws(() => monitor.Get(DefaultRuleName)); + _outputHelper.WriteLine("Exception: {0}", ex.Message); + validate(ex); + }); + } + + private static void VerifyUnknownActionTypeMessage(string[] failures, int index, string actionType) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_UnknownActionType, + actionType); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyUnknownTriggerTypeMessage(string[] failures, int index, string triggerType) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_UnknownTriggerType, + triggerType); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyRequiredMessage(string[] failures, int index, string fieldName) + { + string message = (new RequiredAttribute()).FormatErrorMessage(fieldName); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyEnumDataTypeMessage(string[] failures, int index, string fieldName) + { + string message = (new EnumDataTypeAttribute(typeof(T))).FormatErrorMessage(fieldName); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyRangeMessage(string[] failures, int index, string fieldName, string min, string max) + { + string message = (new RangeAttribute(typeof(T), min, max)).FormatErrorMessage(fieldName); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyEitherRequiredMessage(string[] failures, int index, string fieldName1, string fieldName2) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_TwoFieldsMissing, + fieldName1, + fieldName2); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyBothCannotBeSpecifiedMessage(string[] failures, int index, string fieldName1, string fieldName2) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_TwoFieldsCannotBeSpecified, + fieldName1, + fieldName2); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyFieldLessThanOtherFieldMessage(string[] failures, int index, string fieldName1, string fieldName2) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_FieldMustBeLessThanOtherField, + fieldName1, + fieldName2); + + Assert.Equal(message, failures[index]); + } + + private static void VerifyEgressNotExistMessage(string[] failures, int index, string egressProvider) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_EgressProviderDoesNotExist, + egressProvider); + + Assert.Equal(message, failures[index]); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj new file mode 100644 index 00000000000..d94753730c3 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs new file mode 100644 index 00000000000..4eddca14727 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs @@ -0,0 +1,272 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +{ + internal static class CollectionRuleOptionsExtensions + { + public static CollectionRuleOptions AddAction(this CollectionRuleOptions options, string type) + { + return options.AddAction(type, out _); + } + + public static CollectionRuleOptions AddAction(this CollectionRuleOptions options, string type, out CollectionRuleActionOptions actionOptions) + { + actionOptions = new(); + actionOptions.Type = type; + + options.Actions.Add(actionOptions); + + return options; + } + + public static CollectionRuleOptions AddCollectDumpAction(this CollectionRuleOptions options, DumpType type, string egress) + { + options.AddAction(KnownCollectionRuleActions.CollectDump, out CollectionRuleActionOptions actionOptions); + + CollectDumpOptions collectDumpOptions = new(); + collectDumpOptions.Egress = egress; + collectDumpOptions.Type = type; + + actionOptions.Settings = collectDumpOptions; + + return options; + } + + public static CollectionRuleOptions AddCollectGCDumpAction(this CollectionRuleOptions options, string egress) + { + options.AddAction(KnownCollectionRuleActions.CollectGCDump, out CollectionRuleActionOptions actionOptions); + + CollectGCDumpOptions collectGCDumpOptions = new(); + collectGCDumpOptions.Egress = egress; + + actionOptions.Settings = collectGCDumpOptions; + + return options; + } + + public static CollectionRuleOptions AddCollectLogsAction(this CollectionRuleOptions options, string egress) + { + return options.AddCollectLogsAction(egress, out _); + } + + public static CollectionRuleOptions AddCollectLogsAction(this CollectionRuleOptions options, string egress, out CollectLogsOptions collectLogsOptions) + { + options.AddAction(KnownCollectionRuleActions.CollectLogs, out CollectionRuleActionOptions actionOptions); + + collectLogsOptions = new(); + collectLogsOptions.Egress = egress; + + actionOptions.Settings = collectLogsOptions; + + return options; + } + + public static CollectionRuleOptions AddCollectTraceAction(this CollectionRuleOptions options, TraceProfile profile, string egress) + { + return options.AddCollectTraceAction(profile, egress, out _); + } + + public static CollectionRuleOptions AddCollectTraceAction(this CollectionRuleOptions options, TraceProfile profile, string egress, out CollectTraceOptions collectTraceOptions) + { + options.AddAction(KnownCollectionRuleActions.CollectTrace, out CollectionRuleActionOptions actionOptions); + + collectTraceOptions = new(); + collectTraceOptions.Profile = profile; + collectTraceOptions.Egress = egress; + + actionOptions.Settings = collectTraceOptions; + + return options; + } + + public static CollectionRuleOptions AddCollectTraceAction(this CollectionRuleOptions options, IEnumerable providers, string egress) + { + return options.AddCollectTraceAction(providers, egress, out _); + } + + public static CollectionRuleOptions AddCollectTraceAction(this CollectionRuleOptions options, IEnumerable providers, string egress, out CollectTraceOptions collectTraceOptions) + { + options.AddAction(KnownCollectionRuleActions.CollectTrace, out CollectionRuleActionOptions actionOptions); + + collectTraceOptions = new(); + collectTraceOptions.Providers = new List(providers); + collectTraceOptions.Egress = egress; + + actionOptions.Settings = collectTraceOptions; + + return options; + } + + public static CollectionRuleOptions AddExecuteAction(this CollectionRuleOptions options, string path, string arguments = null) + { + options.AddAction(KnownCollectionRuleActions.Execute, out CollectionRuleActionOptions actionOptions); + + ExecuteOptions executeOptions = new(); + executeOptions.Arguments = arguments; + executeOptions.Path = path; + + actionOptions.Settings = executeOptions; + + return options; + } + + public static CollectionRuleOptions SetEventCounterTrigger(this CollectionRuleOptions options, out EventCounterOptions settings) + { + SetTrigger(options, KnownCollectionRuleTriggers.EventCounter, out CollectionRuleTriggerOptions triggerOptions); + + settings = new(); + + triggerOptions.Settings = settings; + + return options; + } + + public static CollectionRuleOptions SetStartupTrigger(this CollectionRuleOptions options) + { + return SetTrigger(options, KnownCollectionRuleTriggers.Startup, out _); + } + + public static CollectionRuleOptions SetTrigger(this CollectionRuleOptions options, string type) + { + return options.SetTrigger(type, out _); + } + + public static CollectionRuleOptions SetTrigger(this CollectionRuleOptions options, string type, out CollectionRuleTriggerOptions triggerOptions) + { + triggerOptions = new(); + triggerOptions.Type = type; + + options.Trigger = triggerOptions; + + return options; + } + + public static EventCounterOptions VerifyEventCounterTrigger(this CollectionRuleOptions ruleOptions) + { + ruleOptions.VerifyTrigger(KnownCollectionRuleTriggers.EventCounter); + return Assert.IsType(ruleOptions.Trigger.Settings); + } + + public static void VerifyStartupTrigger(this CollectionRuleOptions ruleOptions) + { + ruleOptions.VerifyTrigger(KnownCollectionRuleTriggers.Startup); + Assert.Null(ruleOptions.Trigger.Settings); + } + + private static void VerifyTrigger(this CollectionRuleOptions ruleOptions, string triggerType) + { + Assert.NotNull(ruleOptions.Trigger); + Assert.Equal(triggerType, ruleOptions.Trigger.Type); + } + + public static CollectDumpOptions VerifyCollectDumpAction(this CollectionRuleOptions ruleOptions, int actionIndex, DumpType expectedDumpType, string expectedEgress) + { + CollectDumpOptions collectDumpOptions = ruleOptions.VerifyAction( + actionIndex, KnownCollectionRuleActions.CollectDump); + + Assert.Equal(expectedDumpType, collectDumpOptions.Type); + Assert.Equal(expectedEgress, collectDumpOptions.Egress); + + return collectDumpOptions; + } + + public static CollectGCDumpOptions VerifyCollectGCDumpAction(this CollectionRuleOptions ruleOptions, int actionIndex, string expectedEgress) + { + CollectGCDumpOptions collectGCDumpOptions = ruleOptions.VerifyAction( + actionIndex, KnownCollectionRuleActions.CollectGCDump); + + Assert.Equal(expectedEgress, collectGCDumpOptions.Egress); + + return collectGCDumpOptions; + } + + public static CollectLogsOptions VerifyCollectLogsAction(this CollectionRuleOptions ruleOptions, int actionIndex, string expectedEgress) + { + CollectLogsOptions collectLogsOptions = ruleOptions.VerifyAction( + actionIndex, KnownCollectionRuleActions.CollectLogs); + + Assert.Equal(expectedEgress, collectLogsOptions.Egress); + + return collectLogsOptions; + } + + public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, TraceProfile expectedProfile, string expectedEgress) + { + CollectTraceOptions collectTraceOptions = ruleOptions.VerifyAction( + actionIndex, KnownCollectionRuleActions.CollectTrace); + + Assert.Equal(expectedProfile, collectTraceOptions.Profile); + Assert.Equal(expectedEgress, collectTraceOptions.Egress); + + return collectTraceOptions; + } + + public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, IEnumerable providers, string expectedEgress) + { + CollectTraceOptions collectTraceOptions = ruleOptions.VerifyAction( + actionIndex, KnownCollectionRuleActions.CollectTrace); + + Assert.Equal(expectedEgress, collectTraceOptions.Egress); + Assert.NotNull(collectTraceOptions.Providers); + Assert.Equal(providers.Count(), collectTraceOptions.Providers.Count); + + int index = 0; + foreach (EventPipeProvider expectedProvider in providers) + { + EventPipeProvider actualProvider = collectTraceOptions.Providers[index]; + Assert.Equal(expectedProvider.Name, actualProvider.Name); + Assert.Equal(expectedProvider.Keywords, actualProvider.Keywords); + Assert.Equal(expectedProvider.EventLevel, actualProvider.EventLevel); + if (null == expectedProvider.Arguments) + { + Assert.Null(actualProvider.Arguments); + } + else + { + Assert.NotNull(actualProvider.Arguments); + Assert.Equal(expectedProvider.Arguments.Count, actualProvider.Arguments.Count); + foreach ((string expectedKey, string expectedValue) in expectedProvider.Arguments) + { + Assert.True(actualProvider.Arguments.TryGetValue(expectedKey, out string actualValue)); + Assert.Equal(expectedValue, actualValue); + } + } + + index++; + } + + return collectTraceOptions; + } + + public static ExecuteOptions VerifyExecuteAction(this CollectionRuleOptions ruleOptions, int actionIndex, string expectedPath, string expectedArguments = null) + { + ExecuteOptions executeOptions = ruleOptions.VerifyAction( + actionIndex, KnownCollectionRuleActions.Execute); + + Assert.Equal(expectedPath, executeOptions.Path); + Assert.Equal(expectedArguments, executeOptions.Arguments); + + return executeOptions; + } + + private static TOptions VerifyAction(this CollectionRuleOptions ruleOptions, int actionIndex, string actionType) + { + CollectionRuleActionOptions actionOptions = ruleOptions.Actions[actionIndex]; + + Assert.Equal(actionType, actionOptions.Type); + + return Assert.IsType(actionOptions.Settings); + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionOptionsProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionOptionsProvider.cs new file mode 100644 index 00000000000..1bf74b99f31 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionOptionsProvider.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal sealed class CollectionRuleActionOptionsProvider : + ICollectionRuleActionOptionsProvider + { + private readonly IDictionary _optionsMap = + new Dictionary(StringComparer.Ordinal); + + public CollectionRuleActionOptionsProvider( + ILogger logger, + IEnumerable providers) + { + foreach (ICollectionRuleActionProvider provider in providers) + { + if (_optionsMap.ContainsKey(provider.ActionType)) + { + logger.DuplicateCollectionRuleActionIgnored(provider.ActionType); + } + else + { + _optionsMap.Add(provider.ActionType, provider.OptionsType); + } + } + } + + public bool TryGetOptionsType(string actionType, out Type optionsType) + { + return _optionsMap.TryGetValue(actionType, out optionsType); + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionProvider.cs new file mode 100644 index 00000000000..bfb5772c8e2 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleActionProvider.cs @@ -0,0 +1,22 @@ +// 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; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal sealed class CollectionRuleActionProvider : + ICollectionRuleActionProvider + { + public CollectionRuleActionProvider(string actionType) + { + ActionType = actionType; + OptionsType = typeof(TOptions); + } + + public string ActionType { get; } + + public Type OptionsType { get; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleConfigureNamedOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleConfigureNamedOptions.cs new file mode 100644 index 00000000000..d8daa0a09dd --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleConfigureNamedOptions.cs @@ -0,0 +1,94 @@ +// 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 Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal sealed class CollectionRuleConfigureNamedOptions : + IConfigureNamedOptions + { + private readonly ICollectionRuleActionOptionsProvider _actionOptionsProvider; + private readonly IConfigurationSection _collectionRulesSection; + private readonly ICollectionRuleTriggerOptionsProvider _triggerOptionsProvider; + + public CollectionRuleConfigureNamedOptions( + IConfiguration configuration, + ICollectionRuleActionOptionsProvider actionOptionsProvider, + ICollectionRuleTriggerOptionsProvider triggerOptionsProvider) + { + _actionOptionsProvider = actionOptionsProvider; + _collectionRulesSection = configuration.GetSection(nameof(ConfigurationKeys.CollectionRules)); + _triggerOptionsProvider = triggerOptionsProvider; + } + + public void Configure(string name, CollectionRuleOptions options) + { + IConfigurationSection ruleSection = _collectionRulesSection.GetSection(name); + Debug.Assert(ruleSection.Exists()); + if (ruleSection.Exists()) + { + ruleSection.Bind(options); + + BindTriggerSettings(ruleSection, options); + + for (int i = 0; i < options.Actions.Count; i++) + { + BindActionSettings(ruleSection, options, i); + } + } + } + + public void Configure(CollectionRuleOptions options) + { + throw new NotSupportedException(); + } + + private void BindActionSettings(IConfigurationSection ruleSection, CollectionRuleOptions ruleOptions, int actionIndex) + { + CollectionRuleActionOptions actionOptions = ruleOptions.Actions[actionIndex]; + + if (null != actionOptions && + _actionOptionsProvider.TryGetOptionsType(actionOptions.Type, out Type optionsType) && + null != optionsType) + { + object actionSettings = Activator.CreateInstance(optionsType); + + IConfigurationSection settingsSection = ruleSection.GetSection(ConfigurationPath.Combine( + nameof(CollectionRuleOptions.Actions), + actionIndex.ToString(CultureInfo.InvariantCulture), + nameof(CollectionRuleActionOptions.Settings))); + + settingsSection.Bind(actionSettings); + + actionOptions.Settings = actionSettings; + } + } + + private void BindTriggerSettings(IConfigurationSection ruleSection, CollectionRuleOptions ruleOptions) + { + CollectionRuleTriggerOptions triggerOptions = ruleOptions.Trigger; + + if (null != triggerOptions && + _triggerOptionsProvider.TryGetOptionsType(triggerOptions.Type, out Type optionsType) && + null != optionsType) + { + object triggerSettings = Activator.CreateInstance(optionsType); + + IConfigurationSection settingsSection = ruleSection.GetSection(ConfigurationPath.Combine( + nameof(CollectionRuleOptions.Trigger), + nameof(CollectionRuleTriggerOptions.Settings))); + + settingsSection.Bind(triggerSettings); + + triggerOptions.Settings = triggerSettings; + } + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerOptionsProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerOptionsProvider.cs new file mode 100644 index 00000000000..25d15b08cf1 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerOptionsProvider.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal sealed class CollectionRuleTriggerOptionsProvider : + ICollectionRuleTriggerOptionsProvider + { + private readonly IDictionary _optionsMap = + new Dictionary(StringComparer.Ordinal); + + public CollectionRuleTriggerOptionsProvider( + ILogger logger, + IEnumerable providers) + { + foreach (ICollectionRuleTriggerProvider provider in providers) + { + if (_optionsMap.ContainsKey(provider.TriggerType)) + { + logger.DuplicateCollectionRuleTriggerIgnored(provider.TriggerType); + } + else + { + _optionsMap.Add(provider.TriggerType, provider.OptionsType); + } + } + } + + public bool TryGetOptionsType(string triggerType, out Type optionsType) + { + return _optionsMap.TryGetValue(triggerType, out optionsType); + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerProvider.cs new file mode 100644 index 00000000000..af18571a41e --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/CollectionRuleTriggerProvider.cs @@ -0,0 +1,36 @@ +// 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; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal class CollectionRuleTriggerProvider : + ICollectionRuleTriggerProvider + { + public CollectionRuleTriggerProvider(string triggerType) : + this (triggerType, null) + { + } + + public CollectionRuleTriggerProvider(string triggerType, Type optionsType) + { + TriggerType = triggerType; + OptionsType = optionsType; + } + + public string TriggerType { get; } + + public Type OptionsType { get; } + } + + internal sealed class CollectionRuleTriggerProvider : + CollectionRuleTriggerProvider + { + public CollectionRuleTriggerProvider(string triggerType) + : base(triggerType, typeof(TOptions)) + { + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionOptionsProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionOptionsProvider.cs new file mode 100644 index 00000000000..b00b9547863 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionOptionsProvider.cs @@ -0,0 +1,13 @@ +// 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; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal interface ICollectionRuleActionOptionsProvider + { + bool TryGetOptionsType(string actionType, out Type optionsType); + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionProvider.cs new file mode 100644 index 00000000000..e3535eca117 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleActionProvider.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; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal interface ICollectionRuleActionProvider + { + string ActionType { get; } + + Type OptionsType { get; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerOptionsProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerOptionsProvider.cs new file mode 100644 index 00000000000..959340aead5 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerOptionsProvider.cs @@ -0,0 +1,13 @@ +// 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; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + internal interface ICollectionRuleTriggerOptionsProvider + { + bool TryGetOptionsType(string actionType, out Type optionsType); + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerProvider.cs b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerProvider.cs new file mode 100644 index 00000000000..12c6b81a27d --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Configuration/ICollectionRuleTriggerProvider.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; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration +{ + interface ICollectionRuleTriggerProvider + { + string TriggerType { get; } + + Type OptionsType { get; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleActions.cs b/src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleActions.cs new file mode 100644 index 00000000000..9f5e8e8e9cc --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleActions.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. + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules +{ + internal static class KnownCollectionRuleActions + { + public const string CollectDump = nameof(CollectDump); + public const string CollectGCDump = nameof(CollectGCDump); + public const string CollectLogs = nameof(CollectLogs); + public const string CollectTrace = nameof(CollectTrace); + public const string Execute = nameof(Execute); + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleTriggers.cs b/src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleTriggers.cs new file mode 100644 index 00000000000..4931918e94f --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/KnownCollectionRuleTriggers.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules +{ + internal static class KnownCollectionRuleTriggers + { + // Startup Triggers + public const string Startup = nameof(Startup); + + // Event Source Triggers + public const string AspNetRequestCount = nameof(AspNetRequestCount); + public const string AspNetRequestDuration = nameof(AspNetRequestDuration); + public const string AspNetResponseStatus = nameof(AspNetResponseStatus); + public const string EventCounter = nameof(EventCounter); + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ActionOptionsConstants.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ActionOptionsConstants.cs new file mode 100644 index 00000000000..e025ff92c13 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ActionOptionsConstants.cs @@ -0,0 +1,29 @@ +// 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.Globalization; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + // Constants for action options allowing reuse among multiple actions and for tests to verify ranges. + internal static class ActionOptionsConstants + { + public const int BufferSizeMegabytes_MaxValue = 1024; + public static readonly string BufferSizeMegabytes_MaxValue_String = BufferSizeMegabytes_MaxValue.ToString(CultureInfo.InvariantCulture); + public const int BufferSizeMegabytes_MinValue = 1; + public static readonly string BufferSizeMegabytes_MinValue_String = BufferSizeMegabytes_MinValue.ToString(CultureInfo.InvariantCulture); + + public const string Duration_MaxValue = "1.00:00:00"; // 1 day + public const string Duration_MinValue = "00:00:01"; // 1 second + + public const int MetricsIntervalSeconds_MaxValue = 24 * 60 * 60; // 1 day + public static readonly string MetricsIntervalSeconds_MaxValue_String = MetricsIntervalSeconds_MaxValue.ToString(CultureInfo.InvariantCulture); + public const int MetricsIntervalSeconds_MinValue = 1; // 1 second + public static readonly string MetricsIntervalSeconds_MinValue_String = MetricsIntervalSeconds_MinValue.ToString(CultureInfo.InvariantCulture); + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectDumpOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectDumpOptions.cs new file mode 100644 index 00000000000..a544e50873e --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectDumpOptions.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi.Models; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + /// + /// Options for the CollectDump action. + /// + [DebuggerDisplay("CollectDump")] + internal sealed partial class CollectDumpOptions + { + [EnumDataType(typeof(DumpType))] + public DumpType? Type { get; set; } + + [Required] +#if !UNITTEST + [ValidateEgressProvider] +#endif + public string Egress { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectGCDumpOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectGCDumpOptions.cs new file mode 100644 index 00000000000..e0ed12a8bff --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectGCDumpOptions.cs @@ -0,0 +1,26 @@ +// 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.ComponentModel.DataAnnotations; +using System.Diagnostics; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + /// + /// Options for the CollectGCDump action. + /// + [DebuggerDisplay("CollectGCDump")] + internal sealed partial class CollectGCDumpOptions + { + [Required] +#if !UNITTEST + [ValidateEgressProvider] +#endif + public string Egress { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.Validate.cs new file mode 100644 index 00000000000..fee846ef081 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.Validate.cs @@ -0,0 +1,50 @@ +// 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 Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + partial class CollectLogsOptions : + IValidatableObject + { + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + List results = new(); + + if (null != FilterSpecs) + { + RequiredAttribute requiredAttribute = new(); + EnumDataTypeAttribute enumValidationAttribute = new(typeof(LogLevel)); + + ValidationContext filterSpecsContext = new(FilterSpecs, validationContext, validationContext.Items); + filterSpecsContext.MemberName = nameof(FilterSpecs); + + // Validate that the category is not null and that the level is a valid level value. + foreach ((string category, LogLevel? level) in FilterSpecs) + { + ValidationResult result = requiredAttribute.GetValidationResult(category, filterSpecsContext); + if (result != ValidationResult.Success) + { + results.Add(result); + } + + result = enumValidationAttribute.GetValidationResult(level, filterSpecsContext); + if (result != ValidationResult.Success) + { + results.Add(result); + } + } + } + + return results; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.cs new file mode 100644 index 00000000000..a312fa16f00 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectLogsOptions.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + /// + /// Options for the CollectLogs action. + /// + [DebuggerDisplay("CollectLogs")] + internal sealed partial class CollectLogsOptions + { + [EnumDataType(typeof(LogLevel))] + public LogLevel? LogLevel { get; set; } + + public Dictionary FilterSpecs { get; set; } + + public bool? UseAppFilters { get; set; } + + [Range(typeof(TimeSpan), ActionOptionsConstants.Duration_MinValue, ActionOptionsConstants.Duration_MaxValue)] + public TimeSpan? Duration { get; set; } + + [Required] +#if !UNITTEST + [ValidateEgressProvider] +#endif + public string Egress { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs new file mode 100644 index 00000000000..b8bae3c05af --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs @@ -0,0 +1,70 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi.Models; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + partial class CollectTraceOptions : + IValidatableObject + { + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + List results = new(); + + bool hasProfile = Profile.HasValue; + bool hasProviders = null != Providers && Providers.Any(); + + if (hasProfile) + { + if (hasProviders) + { + // Both Profile and Providers cannot be specified at the same time, otherwise + // cannot determine whether to use providers from the profile or the custom + // specified providers. + results.Add(new ValidationResult( + string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_TwoFieldsCannotBeSpecified, + nameof(Profile), + nameof(Providers)))); + } + } + else if (hasProviders) + { + // Validate that each provider is valid. + int index = 0; + foreach (EventPipeProvider provider in Providers) + { + ValidationContext providerContext = new(provider, validationContext, validationContext.Items); + providerContext.MemberName = nameof(Providers) + "[" + index.ToString(CultureInfo.InvariantCulture) + "]"; + + Validator.TryValidateObject(provider, providerContext, results, validateAllProperties: true); + + index++; + } + } + else + { + // Either Profile or Providers must be specified + results.Add(new ValidationResult( + string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_TwoFieldsMissing, + nameof(Profile), + nameof(Providers)))); + } + + return results; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs new file mode 100644 index 00000000000..31f0a6649bb --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + /// + /// Options for the CollectTrace action. + /// + [DebuggerDisplay("CollectTrace")] + internal sealed partial class CollectTraceOptions + { + [EnumDataType(typeof(TraceProfile))] + public TraceProfile? Profile { get; set; } + + [Range(ActionOptionsConstants.MetricsIntervalSeconds_MinValue, ActionOptionsConstants.MetricsIntervalSeconds_MaxValue)] + public int? MetricsIntervalSeconds { get; set; } + + public List Providers { get; set; } + + public bool? RequestRundown { get; set; } + + [Range(ActionOptionsConstants.BufferSizeMegabytes_MinValue, ActionOptionsConstants.BufferSizeMegabytes_MaxValue)] + public int? BufferSizeMegabytes { get; set; } + + [Range(typeof(TimeSpan), ActionOptionsConstants.Duration_MinValue, ActionOptionsConstants.Duration_MaxValue)] + public TimeSpan? Duration { get; set; } + + [Required] +#if !UNITTEST + [ValidateEgressProvider] +#endif + public string Egress { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ExecuteOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ExecuteOptions.cs new file mode 100644 index 00000000000..f3bbd4c9615 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ExecuteOptions.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. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +#endif +{ + /// + /// Options for the Execute action. + /// + [DebuggerDisplay("Execute: Path = {Path}")] + internal sealed class ExecuteOptions + { + [Required] + public string Path { get; set; } + + public string Arguments { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ValidateEgressProviderAttribute.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ValidateEgressProviderAttribute.cs new file mode 100644 index 00000000000..bddbf6bdf93 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ValidateEgressProviderAttribute.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.DependencyInjection; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +{ + internal sealed class ValidateEgressProviderAttribute : + ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + string egressProvider = (string)value; + + IEgressService egressService = validationContext.GetRequiredService(); + if (!egressService.CheckProvider(egressProvider)) + { + return new ValidationResult( + string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_EgressProviderDoesNotExist, + egressProvider)); + } + + return ValidationResult.Success; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.Validate.cs new file mode 100644 index 00000000000..ff5184344fc --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.Validate.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +{ + partial class CollectionRuleActionOptions : IValidatableObject + { + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + ICollectionRuleActionOptionsProvider actionOptionsProvider = validationContext.GetRequiredService(); + + List results = new(); + + if (!string.IsNullOrEmpty(Type)) + { + if (actionOptionsProvider.TryGetOptionsType(Type, out Type optionsType)) + { + if (null != optionsType) + { + ValidationHelper.TryValidateOptions(optionsType, Settings, validationContext, results); + } + } + else + { + results.Add(new ValidationResult(string.Format(CultureInfo.InvariantCulture, Strings.ErrorMessage_UnknownActionType, Type))); + } + } + + return results; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.cs new file mode 100644 index 00000000000..586e71743a4 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleActionOptions.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +#endif +{ + /// + /// Options for describing the type of action to execute and the settings to pass to that action. + /// + [DebuggerDisplay("Action: Type = {Type}")] + internal sealed partial class CollectionRuleActionOptions + { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleActionOptions_Type))] + [Required] + public string Type { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleActionOptions_Settings))] + public object Settings { get; internal set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleLimitsOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleLimitsOptions.cs new file mode 100644 index 00000000000..f4c32ee41eb --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleLimitsOptions.cs @@ -0,0 +1,37 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi; +using System; +using System.ComponentModel.DataAnnotations; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +#endif +{ + /// + /// Options for limiting the execution of a collection rule. + /// + internal sealed class CollectionRuleLimitsOptions + { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleLimitsOptions_ActionCount))] + public int? ActionCount { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleLimitsOptions_ActionCountSlidingWindowDuration))] + [Range(typeof(TimeSpan), CollectionRuleOptionsConstants.ActionCountSlidingWindowDuration_MinValue, CollectionRuleOptionsConstants.ActionCountSlidingWindowDuration_MaxValue)] + public TimeSpan? ActionCountSlidingWindowDuration { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleLimitsOptions_RuleDuration))] + [Range(typeof(TimeSpan), CollectionRuleOptionsConstants.RuleDuration_MinValue, CollectionRuleOptionsConstants.RuleDuration_MaxValue)] + public TimeSpan? RuleDuration { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.Validate.cs new file mode 100644 index 00000000000..6c73f811f81 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.Validate.cs @@ -0,0 +1,34 @@ +// 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; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +{ + partial class CollectionRuleOptions : IValidatableObject + { + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + List results = new(); + + ValidationContext filtersContext = new(Filters, validationContext, validationContext.Items); + filtersContext.MemberName = nameof(Filters); + ValidationHelper.TryValidateItems(Filters, filtersContext, results); + + if (null != Trigger) + { + ValidationContext triggerContext = new(Trigger, validationContext, validationContext.Items); + triggerContext.MemberName = nameof(Trigger); + Validator.TryValidateObject(Trigger, triggerContext, results); + } + + ValidationContext actionsContext = new(Actions, validationContext, validationContext.Items); + actionsContext.MemberName = nameof(Actions); + ValidationHelper.TryValidateItems(Actions, actionsContext, results); + + return results; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.cs new file mode 100644 index 00000000000..1815e03d0a6 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptions.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +#endif +{ + /// + /// Options for describing an entire collection rule. + /// + internal sealed partial class CollectionRuleOptions + { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleOptions_Filters))] + public List Filters { get; } = new List(0); + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleOptions_Trigger))] + [Required] + public CollectionRuleTriggerOptions Trigger { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleOptions_Actions))] + public List Actions { get; } = new List(0); + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleOptions_Limits))] + public CollectionRuleLimitsOptions Limits { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptionsConstants.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptionsConstants.cs new file mode 100644 index 00000000000..4ad48e6ca44 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleOptionsConstants.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. +// See the LICENSE file in the project root for more information. + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +#endif +{ + internal static class CollectionRuleOptionsConstants + { + public const string ActionCountSlidingWindowDuration_MaxValue = "1.00:00:00"; // 1 day + public const string ActionCountSlidingWindowDuration_MinValue = "00:00:01"; // 1 second + + public const string RuleDuration_MaxValue = "365.00:00:00"; // 1 day + public const string RuleDuration_MinValue = "00:00:01"; // 1 second + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.Validate.cs new file mode 100644 index 00000000000..ffdf4e365b2 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.Validate.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +{ + partial class CollectionRuleTriggerOptions : IValidatableObject + { + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + ICollectionRuleTriggerOptionsProvider triggerOptionsProvider = validationContext.GetRequiredService(); + + List results = new(); + + if (!string.IsNullOrEmpty(Type)) + { + if (triggerOptionsProvider.TryGetOptionsType(Type, out Type optionsType)) + { + if (null != optionsType) + { + ValidationHelper.TryValidateOptions(optionsType, Settings, validationContext, results); + } + } + else + { + results.Add(new ValidationResult(string.Format(CultureInfo.InvariantCulture, Strings.ErrorMessage_UnknownTriggerType, Type))); + } + } + + return results; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.cs new file mode 100644 index 00000000000..c820bd87174 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/CollectionRuleTriggerOptions.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +#endif +{ + /// + /// Options for describing the type of trigger and the settings to pass to that trigger. + /// + [DebuggerDisplay("Trigger: Type = {Type}")] + internal sealed partial class CollectionRuleTriggerOptions + { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleTriggerOptions_Type))] + [Required] + public string Type { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectionRuleTriggerOptions_Settings))] + public object Settings { get; internal set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestCountOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestCountOptions.cs new file mode 100644 index 00000000000..c5d8d0d7538 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestCountOptions.cs @@ -0,0 +1,29 @@ +// 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; +using System.ComponentModel.DataAnnotations; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers +#endif +{ + /// + /// Options for the AspNetRequestCount trigger. + /// + internal sealed class AspNetRequestCountOptions + { + [Required] + public int RequestCount { get; set; } + + [Range(typeof(TimeSpan), TriggerOptionsConstants.SlidingWindowDuration_MinValue, TriggerOptionsConstants.SlidingWindowDuration_MaxValue)] + public TimeSpan? SlidingWindowDuration { get; set; } + + public string IncludePaths { get; set; } + + public string ExcludePaths { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestDurationOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestDurationOptions.cs new file mode 100644 index 00000000000..fd4c81d824b --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetRequestDurationOptions.cs @@ -0,0 +1,31 @@ +// 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; +using System.ComponentModel.DataAnnotations; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers +#endif +{ + /// + /// Options for the AspNetRequestDuration trigger. + /// + internal sealed class AspNetRequestDurationOptions + { + [Required] + public int RequestCount { get; set; } + + public TimeSpan? RequestDuration { get; set; } + + [Range(typeof(TimeSpan), TriggerOptionsConstants.SlidingWindowDuration_MinValue, TriggerOptionsConstants.SlidingWindowDuration_MaxValue)] + public TimeSpan? SlidingWindowDuration { get; set; } + + public string IncludePaths { get; set; } + + public string ExcludePaths { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetResponseStatusOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetResponseStatusOptions.cs new file mode 100644 index 00000000000..f67868476fb --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/AspNetResponseStatusOptions.cs @@ -0,0 +1,32 @@ +// 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; +using System.ComponentModel.DataAnnotations; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers +#endif +{ + /// + /// Options for the AspNetResponseStatus trigger. + /// + internal sealed class AspNetResponseStatusOptions + { + [Required] + public string StatusCodes { get; set; } + + [Required] + public int ResponseCount { get; set; } + + [Range(typeof(TimeSpan), TriggerOptionsConstants.SlidingWindowDuration_MinValue, TriggerOptionsConstants.SlidingWindowDuration_MaxValue)] + public TimeSpan? SlidingWindowDuration { get; set; } + + public string IncludePaths { get; set; } + + public string ExcludePaths { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.Validate.cs new file mode 100644 index 00000000000..f48199c4072 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.Validate.cs @@ -0,0 +1,38 @@ +// 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; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers +{ + partial class EventCounterOptions : IValidatableObject + { + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + List results = new(); + + if (!GreaterThan.HasValue && !LessThan.HasValue) + { + // Either GreaterThan or LessThan must be specified. + results.Add(new ValidationResult( + string.Format( + Strings.ErrorMessage_TwoFieldsMissing, + nameof(GreaterThan), + nameof(LessThan)))); + } + else if (GreaterThan.HasValue && LessThan.HasValue && LessThan.Value < GreaterThan.Value) + { + // The GreaterThan must be lower than LessThan if both are specified. + results.Add(new ValidationResult( + string.Format( + Strings.ErrorMessage_FieldMustBeLessThanOtherField, + nameof(GreaterThan), + nameof(LessThan)))); + } + + return results; + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.cs new file mode 100644 index 00000000000..375f54be58d --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/EventCounterOptions.cs @@ -0,0 +1,35 @@ +// 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; +using System.ComponentModel.DataAnnotations; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers +#endif +{ + /// + /// Options for the EventCounter trigger. + /// + internal sealed partial class EventCounterOptions + { + [Required] + public string ProviderName { get; set; } + + [Required] + public string CounterName { get; set; } + + public double? GreaterThan { get; set; } + + public double? LessThan { get; set; } + + [Range(typeof(TimeSpan), TriggerOptionsConstants.SlidingWindowDuration_MinValue, TriggerOptionsConstants.SlidingWindowDuration_MaxValue)] + public TimeSpan? SlidingWindowDuration { get; set; } + + [Range(TriggerOptionsConstants.CounterFrequency_MinValue, TriggerOptionsConstants.CounterFrequency_MaxValue)] + public int? Frequency { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/TriggerOptionsConstants.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/TriggerOptionsConstants.cs new file mode 100644 index 00000000000..b0a38e02bf3 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Triggers/TriggerOptionsConstants.cs @@ -0,0 +1,24 @@ +// 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.Globalization; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers +#endif +{ + // Constants for trigger options allowing reuse among multiple trigger and for tests to verify ranges. + internal static class TriggerOptionsConstants + { + public const int CounterFrequency_MaxValue = 24*60*60; // 1 day + public static readonly string Frequency_MaxValue_String = CounterFrequency_MaxValue.ToString(CultureInfo.InvariantCulture); + public const int CounterFrequency_MinValue = 1; // 1 second + public static readonly string Frequency_MinValue_String = CounterFrequency_MinValue.ToString(CultureInfo.InvariantCulture); + + public const string SlidingWindowDuration_MaxValue = "1.00:00:00"; // 1 day + public const string SlidingWindowDuration_MinValue = "00:00:01"; // 1 second + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/ValidationHelper.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/ValidationHelper.cs new file mode 100644 index 00000000000..96d29da7658 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/ValidationHelper.cs @@ -0,0 +1,60 @@ +// 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 Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Reflection; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options +{ + internal static class ValidationHelper + { + public static void TryValidateItems(IEnumerable items, ValidationContext validationContext, ICollection results) + { + int index = 0; + foreach (object item in items) + { + ValidationContext itemContext = new(item, validationContext, validationContext.Items); + itemContext.MemberName = validationContext.MemberName + "[" + index.ToString() + "]"; + + Validator.TryValidateObject(item, itemContext, results); + + index++; + } + } + + public static void TryValidateOptions(Type optionsType, object options, ValidationContext validationContext, ICollection results) + { + RequiredAttribute requiredAttribute = new(); + ValidationResult requiredResult = requiredAttribute.GetValidationResult(options, validationContext); + if (requiredResult == ValidationResult.Success) + { + Type validateOptionsType = typeof(IValidateOptions<>).MakeGenericType(optionsType); + MethodInfo validateMethod = validateOptionsType.GetMethod(nameof(IValidateOptions.Validate)); + + IEnumerable validateOptionsImpls = validationContext.GetServices(validateOptionsType); + foreach (object validateOptionsImpl in validateOptionsImpls) + { + ValidateOptionsResult validateResult = (ValidateOptionsResult)validateMethod.Invoke(validateOptionsImpl, new object[] { null, options }); + if (validateResult.Failed) + { + foreach (string failure in validateResult.Failures) + { + results.Add(new ValidationResult(failure)); + } + } + } + } + else + { + results.Add(requiredResult); + } + } + } +} diff --git a/src/Tools/dotnet-monitor/ConfigurationKeys.cs b/src/Tools/dotnet-monitor/ConfigurationKeys.cs index c052e7b0297..fb803229a8b 100644 --- a/src/Tools/dotnet-monitor/ConfigurationKeys.cs +++ b/src/Tools/dotnet-monitor/ConfigurationKeys.cs @@ -8,6 +8,8 @@ internal static class ConfigurationKeys { public const string ApiAuthentication = nameof(ApiAuthentication); + public const string CollectionRules = nameof(CollectionRules); + public const string CorsConfiguration = nameof(CorsConfiguration); public const string DiagnosticPort = nameof(DiagnosticPort); diff --git a/src/Tools/dotnet-monitor/Egress/Configuration/EgressProviderValidateOptions.cs b/src/Tools/dotnet-monitor/DataAnnotationValidateOptions.cs similarity index 57% rename from src/Tools/dotnet-monitor/Egress/Configuration/EgressProviderValidateOptions.cs rename to src/Tools/dotnet-monitor/DataAnnotationValidateOptions.cs index 9972d2f5bbd..9fc3206baca 100644 --- a/src/Tools/dotnet-monitor/Egress/Configuration/EgressProviderValidateOptions.cs +++ b/src/Tools/dotnet-monitor/DataAnnotationValidateOptions.cs @@ -1,25 +1,25 @@ -// 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 Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; -namespace Microsoft.Diagnostics.Tools.Monitor.Egress.Configuration +namespace Microsoft.Diagnostics.Tools.Monitor { - /// - /// Validates the settings within a instance - /// using data annotation validation. - /// - internal sealed class EgressProviderValidateOptions : - IValidateOptions where TOptions : class + internal sealed class DataAnnotationValidateOptions : + IValidateOptions + where TOptions : class { + private readonly IServiceProvider _serviceProvider; + + public DataAnnotationValidateOptions(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + public ValidateOptionsResult Validate(string name, TOptions options) { - ValidationContext validationContext = new ValidationContext(options); + ValidationContext validationContext = new(options, _serviceProvider, null); ICollection results = new Collection(); if (!Validator.TryValidateObject(options, validationContext, results, validateAllProperties: true)) { diff --git a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs index 861ef53ce66..ac4eb4ee2c3 100644 --- a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs +++ b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs @@ -25,7 +25,7 @@ namespace Microsoft.Diagnostics.Tools.Monitor { internal sealed class DiagnosticsMonitorCommandHandler { - private const string ConfigPrefix = "DotnetMonitor_"; + public const string ConfigPrefix = "DotnetMonitor_"; private const string SettingsFileName = "settings.json"; private const string ProductFolderName = "dotnet-monitor"; @@ -68,7 +68,7 @@ public async Task Start(CancellationToken token, IConsole console, string[] //CONSIDER The console logger uses the standard AddConsole, and therefore disregards IConsole. try { - using IHost host = CreateHostBuilder(console, urls, metricUrls, metrics, diagnosticPort, noAuth, tempApiKey, noHttpEgress, configOnly: false).Build(); + IHost host = CreateHostBuilder(console, urls, metricUrls, metrics, diagnosticPort, noAuth, tempApiKey, noHttpEgress, configOnly: false).Build(); try { await host.StartAsync(token); @@ -247,10 +247,11 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st services.AddSingleton(); services.AddSingleton(); services.ConfigureOperationStore(); - services.ConfigureEgress(context.Configuration); + services.ConfigureEgress(); services.ConfigureMetrics(context.Configuration); services.ConfigureStorage(context.Configuration); services.ConfigureDefaultProcess(context.Configuration); + services.ConfigureCollectionRules(); }); } diff --git a/src/Tools/dotnet-monitor/Egress/EgressService.cs b/src/Tools/dotnet-monitor/Egress/EgressService.cs index c94e12581ec..bf57343e7aa 100644 --- a/src/Tools/dotnet-monitor/Egress/EgressService.cs +++ b/src/Tools/dotnet-monitor/Egress/EgressService.cs @@ -55,6 +55,18 @@ public void Dispose() _changeRegistration.Dispose(); } + public bool CheckProvider(string providerName) + { + try + { + return null != GetProvider(providerName); + } + catch (EgressException) + { + return false; + } + } + public async Task EgressAsync(string providerName, Func> action, string fileName, string contentType, IEndpointInfo source, CancellationToken token) { string value = await GetProvider(providerName).EgressAsync( diff --git a/src/Tools/dotnet-monitor/LoggingExtensions.cs b/src/Tools/dotnet-monitor/LoggingExtensions.cs index 8d4f8e9e859..8c5d91a20eb 100644 --- a/src/Tools/dotnet-monitor/LoggingExtensions.cs +++ b/src/Tools/dotnet-monitor/LoggingExtensions.cs @@ -133,6 +133,18 @@ internal static class LoggingExtensions logLevel: LogLevel.Warning, formatString: Strings.LogFormatString_DuplicateEgressProviderIgnored); + private static readonly Action _duplicateCollectionRuleActionIgnored = + LoggerMessage.Define( + eventId: new EventId(25, "DuplicateCollectionRuleActionIgnored"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_DuplicateCollectionRuleActionIgnored); + + private static readonly Action _duplicateCollectionRuleTriggerIgnored = + LoggerMessage.Define( + eventId: new EventId(26, "DuplicateCollectionRuleTriggerIgnored"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_DuplicateCollectionRuleTriggerIgnored); + public static void EgressProviderInvalidOptions(this ILogger logger, string providerName) { _egressProviderInvalidOptions(logger, providerName, null); @@ -225,5 +237,15 @@ public static void DuplicateEgressProviderIgnored(this ILogger logger, string pr { _duplicateEgressProviderIgnored(logger, providerName, providerType, existingProviderType, null); } + + public static void DuplicateCollectionRuleActionIgnored(this ILogger logger, string actionType) + { + _duplicateCollectionRuleActionIgnored(logger, actionType, null); + } + + public static void DuplicateCollectionRuleTriggerIgnored(this ILogger logger, string triggerType) + { + _duplicateCollectionRuleTriggerIgnored(logger, triggerType, null); + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/RootOptions.cs b/src/Tools/dotnet-monitor/RootOptions.cs similarity index 62% rename from src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/RootOptions.cs rename to src/Tools/dotnet-monitor/RootOptions.cs index bcfd8417d23..fdf2f00b696 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/RootOptions.cs +++ b/src/Tools/dotnet-monitor/RootOptions.cs @@ -2,12 +2,25 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#if !UNITTEST +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +#endif +using System.Collections.Generic; + +#if UNITTEST namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor +#endif { - internal class RootOptions + internal sealed class RootOptions { public ApiAuthenticationOptions ApiAuthentication { get; set; } + public IDictionary CollectionRules { get; } + = new Dictionary(0); + public CorsConfiguration CorsConfiguration { get; set; } public DiagnosticPortOptions DiagnosticPort { get; set; } diff --git a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs index d44058a28ab..63e0aa61919 100644 --- a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs +++ b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs @@ -3,6 +3,11 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Configuration; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers; using Microsoft.Diagnostics.Tools.Monitor.Egress; using Microsoft.Diagnostics.Tools.Monitor.Egress.AzureBlob; using Microsoft.Diagnostics.Tools.Monitor.Egress.Configuration; @@ -31,6 +36,52 @@ public static IServiceCollection ConfigureApiKeyConfiguration(this IServiceColle .AddSingleton, ApiKeyAuthenticationOptionsChangeTokenSource>(); } + public static IServiceCollection ConfigureCollectionRules(this IServiceCollection services) + { + services.RegisterCollectionRuleAction(KnownCollectionRuleActions.CollectDump); + services.RegisterCollectionRuleAction(KnownCollectionRuleActions.CollectGCDump); + services.RegisterCollectionRuleAction(KnownCollectionRuleActions.CollectLogs); + services.RegisterCollectionRuleAction(KnownCollectionRuleActions.CollectTrace); + services.RegisterCollectionRuleAction(KnownCollectionRuleActions.Execute); + + services.RegisterCollectionRuleTrigger(KnownCollectionRuleTriggers.AspNetRequestCount); + services.RegisterCollectionRuleTrigger(KnownCollectionRuleTriggers.AspNetRequestDuration); + services.RegisterCollectionRuleTrigger(KnownCollectionRuleTriggers.AspNetResponseStatus); + services.RegisterCollectionRuleTrigger(KnownCollectionRuleTriggers.EventCounter); + services.RegisterCollectionRuleTrigger(KnownCollectionRuleTriggers.Startup); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton, CollectionRuleConfigureNamedOptions>(); + services.AddSingleton, DataAnnotationValidateOptions>(); + + return services; + } + + private static IServiceCollection RegisterCollectionRuleAction(this IServiceCollection services, string actionType) where TOptions : class, new() + { + services.TryAddSingletonEnumerable>(sp => new CollectionRuleActionProvider(actionType)); + // NOTE: When opening colletion rule actions for extensibility, this should not be added for all registered actions. + // Each action should register its own IValidateOptions<> implementation (if it needs one). + services.AddSingleton, DataAnnotationValidateOptions>(); + return services; + } + + private static IServiceCollection RegisterCollectionRuleTrigger(this IServiceCollection services, string triggerType) + { + services.TryAddSingletonEnumerable(sp => new CollectionRuleTriggerProvider(triggerType)); + return services; + } + + private static IServiceCollection RegisterCollectionRuleTrigger(this IServiceCollection services, string triggerType) where TOptions : class, new() + { + services.TryAddSingletonEnumerable>(sp => new CollectionRuleTriggerProvider(triggerType)); + // NOTE: When opening colletion rule triggers for extensibility, this should not be added for all registered triggers. + // Each trigger should register its own IValidateOptions<> implementation (if it needs one). + services.AddSingleton, DataAnnotationValidateOptions>(); + return services; + } + public static IServiceCollection ConfigureStorage(this IServiceCollection services, IConfiguration configuration) { return ConfigureOptions(services, configuration, ConfigurationKeys.Storage); @@ -46,7 +97,7 @@ private static IServiceCollection ConfigureOptions(IServiceCollection service return services.Configure(configuration.GetSection(key)); } - public static IServiceCollection ConfigureEgress(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection ConfigureEgress(this IServiceCollection services) { // Register IEgressService implementation that provides egressing // of artifacts for the REST server. @@ -76,7 +127,7 @@ private static IServiceCollection RegisterProvider(this ISe // Add options services for configuring the options type services.AddSingleton, EgressProviderConfigureNamedOptions>(); - services.AddSingleton, EgressProviderValidateOptions>(); + services.AddSingleton, DataAnnotationValidateOptions>(); // Register change sources for the options type services.AddSingleton, EgressPropertiesConfigurationChangeTokenSource>(); diff --git a/src/Tools/dotnet-monitor/Strings.Designer.cs b/src/Tools/dotnet-monitor/Strings.Designer.cs index ea55e5f86a7..770dd51d3f9 100644 --- a/src/Tools/dotnet-monitor/Strings.Designer.cs +++ b/src/Tools/dotnet-monitor/Strings.Designer.cs @@ -150,6 +150,15 @@ internal static string ErrorMessage_EgressUnableToCreateIntermediateFile { } } + /// + /// Looks up a localized string similar to The value of field {0} must be less than the value of field {1}.. + /// + internal static string ErrorMessage_FieldMustBeLessThanOtherField { + get { + return ResourceManager.GetString("ErrorMessage_FieldMustBeLessThanOtherField", resourceCulture); + } + } + /// /// Looks up a localized string similar to The {0} field value '{1}' is not allowed.. /// @@ -222,6 +231,15 @@ internal static string ErrorMessage_ParameterNotAllowedByteRange { } } + /// + /// Looks up a localized string similar to Both the {0} field and the {1} field cannot be specified.. + /// + internal static string ErrorMessage_TwoFieldsCannotBeSpecified { + get { + return ResourceManager.GetString("ErrorMessage_TwoFieldsCannotBeSpecified", resourceCulture); + } + } + /// /// Looks up a localized string similar to The {0} field or the {1} field is required.. /// @@ -249,6 +267,24 @@ internal static string ErrorMessage_UnhandledConnectionMode { } } + /// + /// Looks up a localized string similar to '{0}' is not a known action type.. + /// + internal static string ErrorMessage_UnknownActionType { + get { + return ResourceManager.GetString("ErrorMessage_UnknownActionType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is not a known trigger type.. + /// + internal static string ErrorMessage_UnknownTriggerType { + get { + return ResourceManager.GetString("ErrorMessage_UnknownTriggerType", resourceCulture); + } + } + /// /// Looks up a localized string similar to Monitor logs and metrics in a .NET application send the results to a chosen destination.. /// @@ -420,6 +456,24 @@ internal static string LogFormatString_DisabledNegotiateWhileElevated { } } + /// + /// Looks up a localized string similar to Collection rule action '{actionType}' was previously registered; the new registration will be ignored.. + /// + internal static string LogFormatString_DuplicateCollectionRuleActionIgnored { + get { + return ResourceManager.GetString("LogFormatString_DuplicateCollectionRuleActionIgnored", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Collection rule trigger '{triggerType}' was previously registered; the new registration will be ignored.. + /// + internal static string LogFormatString_DuplicateCollectionRuleTriggerIgnored { + get { + return ResourceManager.GetString("LogFormatString_DuplicateCollectionRuleTriggerIgnored", resourceCulture); + } + } + /// /// Looks up a localized string similar to New provider '{providerName}' under type '{providerType}' was already registered with type '{existingProviderType}' and will be ignored.. /// diff --git a/src/Tools/dotnet-monitor/Strings.resx b/src/Tools/dotnet-monitor/Strings.resx index 37fed5da520..f6281812710 100644 --- a/src/Tools/dotnet-monitor/Strings.resx +++ b/src/Tools/dotnet-monitor/Strings.resx @@ -166,6 +166,11 @@ Gets the format string for file system egress provider failure due to inability to create an intermediate file. 1 Format Parameter: 0. intermediateFileDirectory: The directory where the intermediate file was attempted to be created. + + + The value of field {0} must be less than the value of field {1}. + {0} = Field name that must be lower. +{1} = Field name that must be higher. The {0} field value '{1}' is not allowed. @@ -213,6 +218,11 @@ 1. parameterValue: The value in the parameter that was rejected 2. minBytes: The minimum number of bytes acceptable 3. maxBytes: The maximum number of bytes acceptable + + + Both the {0} field and the {1} field cannot be specified. + {0} = Name of first field that must not be specified if second field is specified. +{1} = Name of second field that must not be specified if first field is specified. The {0} field or the {1} field is required. @@ -231,6 +241,14 @@ 1 Format Parameter: 0. connectionMode: The provided DiagnosticPortConnectionMode that could not be parsed. + + '{0}' is not a known action type. + {0} = The type of action that is unknown. + + + '{0}' is not a known trigger type. + {0} = The type of trigger that is unknown. + Monitor logs and metrics in a .NET application send the results to a chosen destination. Gets the string to display in help that explains what the 'collect' command does. @@ -316,6 +334,18 @@ Negotiate, Kerberos, and NTLM authentication are not enabled when running with elevated permissions. Gets the format string that is printed in the 20:DisabledNegotiateWhileElevated event. 0 Format Parameters + + + Collection rule action '{actionType}' was previously registered; the new registration will be ignored. + Gets the format string that is printed in the 25:DuplicateCollectionRuleActionIgnored event. +1 Format Parameter: +1. actionType: The registration name of the collection rule action. + + + Collection rule trigger '{triggerType}' was previously registered; the new registration will be ignored. + Gets the format string that is printed in the 26:DuplicateCollectionRuleTriggerIgnored event. +1 Format Parameter: +1. triggerType: The registration name of the collection rule trigger. New provider '{providerName}' under type '{providerType}' was already registered with type '{existingProviderType}' and will be ignored. diff --git a/src/Tools/dotnet-monitor/dotnet-monitor.csproj b/src/Tools/dotnet-monitor/dotnet-monitor.csproj index 9e43c325595..9b063e2d0fe 100644 --- a/src/Tools/dotnet-monitor/dotnet-monitor.csproj +++ b/src/Tools/dotnet-monitor/dotnet-monitor.csproj @@ -55,6 +55,7 @@ +