From 7de6442ccc691365967f3fe38a5ebf1081376806 Mon Sep 17 00:00:00 2001 From: Alexander Kaplan Date: Tue, 27 Jan 2026 15:00:17 -0800 Subject: [PATCH] {AKS} Add support for Azure Monitor Application Monitoring auto-instrumentation --- .../azure/cli/command_modules/acs/_help.py | 9 ++ .../azure/cli/command_modules/acs/_params.py | 5 +- .../azure/cli/command_modules/acs/custom.py | 3 + .../acs/managed_cluster_decorator.py | 79 +++++++++++ .../latest/test_managed_cluster_decorator.py | 134 ++++++++++++++++++ 5 files changed, 229 insertions(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index 092b9eb8d52..76497ef5743 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -534,6 +534,9 @@ - name: --enable-windows-recording-rules type: bool short-summary: Enable Windows Recording Rules when enabling the Azure Monitor Metrics addon + - name: --enable-azure-monitor-app-monitoring + type: bool + short-summary: Enable Azure Monitor Application Monitoring auto-instrumentation for a Kubernetes cluster. - name: --nodepool-taints type: string short-summary: The node taints for all node pool. @@ -1047,6 +1050,12 @@ - name: --disable-azure-monitor-metrics type: bool short-summary: Disable Azure Monitor Metrics Profile. This will delete all DCRA's associated with the cluster, any linked DCRs with the data stream = prometheus-stream and the recording rule groups created by the addon for this AKS cluster. + - name: --enable-azure-monitor-app-monitoring + type: bool + short-summary: Enable Azure Monitor Application Monitoring auto-instrumentation for a Kubernetes cluster. + - name: --disable-azure-monitor-app-monitoring + type: bool + short-summary: Disable Azure Monitor Application Monitoring auto-instrumentation for a Kubernetes cluster. - name: --nodepool-taints type: string short-summary: The node taints for all node pool. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index 6e47f7ddb3d..9255c76339e 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -553,6 +553,7 @@ def load_arguments(self, _): c.argument('ksm_metric_annotations_allow_list') c.argument('grafana_resource_id', validator=validate_grafanaresourceid) c.argument('enable_windows_recording_rules', action='store_true') + c.argument('enable_azure_monitor_app_monitoring', action='store_true') c.argument('node_public_ip_tags', arg_type=tags_type, validator=validate_node_public_ip_tags, help='space-separated tags: key[=value] [key[=value] ...].') # azure container storage @@ -761,6 +762,8 @@ def load_arguments(self, _): c.argument('grafana_resource_id', validator=validate_grafanaresourceid) c.argument('enable_windows_recording_rules', action='store_true') c.argument('disable_azure_monitor_metrics', action='store_true') + c.argument('enable_azure_monitor_app_monitoring', action='store_true') + c.argument('disable_azure_monitor_app_monitoring', action='store_true') # azure container storage c.argument( "enable_azure_container_storage", @@ -1315,4 +1318,4 @@ def __call__(self, parser, namespace, values, option_string=None): return CLIArgumentType( nargs='*', # Allow multiple values action=AzureContainerStorageAction, - ) + ) \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/acs/custom.py b/src/azure-cli/azure/cli/command_modules/acs/custom.py index 0abc58c38ec..89767844a08 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -1005,6 +1005,7 @@ def aks_create( ksm_metric_annotations_allow_list=None, grafana_resource_id=None, enable_windows_recording_rules=False, + enable_azure_monitor_app_monitoring=False, # azure container storage enable_azure_container_storage=None, container_storage_version=None, @@ -1189,6 +1190,8 @@ def aks_update( grafana_resource_id=None, enable_windows_recording_rules=False, disable_azure_monitor_metrics=False, + enable_azure_monitor_app_monitoring=False, + disable_azure_monitor_app_monitoring=False, # azure container storage enable_azure_container_storage=None, disable_azure_container_storage=None, diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index 86953e7cc4c..e38eaebbbdc 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -5566,6 +5566,54 @@ def get_disable_azure_monitor_metrics(self) -> bool: """ return self._get_disable_azure_monitor_metrics(enable_validation=True) + def _get_enable_azure_monitor_app_monitoring(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of enable_azure_monitor_app_monitoring. + This function supports the option of enable_validation. When enabled, if both + enable_azure_monitor_app_monitoring and disable_azure_monitor_app_monitoring are specified, + raise a MutuallyExclusiveArgumentError. + :return: bool + """ + enable_azure_monitor_app_monitoring = self.raw_param.get("enable_azure_monitor_app_monitoring") + if enable_validation: + if enable_azure_monitor_app_monitoring and self._get_disable_azure_monitor_app_monitoring(False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-azure-monitor-app-monitoring and " + "--disable-azure-monitor-app-monitoring at the same time." + ) + return enable_azure_monitor_app_monitoring + + def get_enable_azure_monitor_app_monitoring(self) -> bool: + """Obtain the value of enable_azure_monitor_app_monitoring. + If both enable_azure_monitor_app_monitoring and disable_azure_monitor_app_monitoring are specified, + raise a MutuallyExclusiveArgumentError. + :return: bool + """ + return self._get_enable_azure_monitor_app_monitoring(enable_validation=True) + + def _get_disable_azure_monitor_app_monitoring(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of disable_azure_monitor_app_monitoring. + This function supports the option of enable_validation. When enabled, if both + enable_azure_monitor_app_monitoring and disable_azure_monitor_app_monitoring are specified, + raise a MutuallyExclusiveArgumentError. + :return: bool + """ + disable_azure_monitor_app_monitoring = self.raw_param.get("disable_azure_monitor_app_monitoring") + if enable_validation: + if disable_azure_monitor_app_monitoring and self._get_enable_azure_monitor_app_monitoring(False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-azure-monitor-app-monitoring and " + "--disable-azure-monitor-app-monitoring at the same time." + ) + return disable_azure_monitor_app_monitoring + + def get_disable_azure_monitor_app_monitoring(self) -> bool: + """Obtain the value of disable_azure_monitor_app_monitoring. + If both enable_azure_monitor_app_monitoring and disable_azure_monitor_app_monitoring are specified, + raise a MutuallyExclusiveArgumentError. + :return: bool + """ + return self._get_disable_azure_monitor_app_monitoring(enable_validation=True) + def _get_enable_vpa(self, enable_validation: bool = False) -> bool: """Internal function to obtain the value of enable_vpa. This function supports the option of enable_vpa. When enabled, if both enable_vpa and enable_vpa are @@ -7109,6 +7157,15 @@ def set_up_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: metric_annotations_allow_list=str(ksm_metric_annotations_allow_list)) # set intermediate self.context.set_intermediate("azuremonitormetrics_addon_enabled", True, overwrite_exists=True) + if self.context.get_enable_azure_monitor_app_monitoring(): + if mc.azure_monitor_profile is None: + mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring() + ) + mc.azure_monitor_profile.app_monitoring.auto_instrumentation = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringAutoInstrumentation(enabled=True) + ) return mc def set_up_ingress_web_app_routing(self, mc: ManagedCluster) -> ManagedCluster: @@ -8921,6 +8978,28 @@ def update_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: self.context.get_disable_azure_monitor_metrics(), False) + + if self.context.get_enable_azure_monitor_app_monitoring(): + if mc.azure_monitor_profile is None: + mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() + if mc.azure_monitor_profile.app_monitoring is None: + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring() + ) + mc.azure_monitor_profile.app_monitoring.auto_instrumentation = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringAutoInstrumentation(enabled=True) + ) + + if self.context.get_disable_azure_monitor_app_monitoring(): + if mc.azure_monitor_profile is None: + mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() + if mc.azure_monitor_profile.app_monitoring is None: + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring() + ) + mc.azure_monitor_profile.app_monitoring.auto_instrumentation = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringAutoInstrumentation(enabled=False) + ) return mc # pylint: disable=too-many-statements,too-many-locals diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index 62d6225cbd2..d1fb2a9d112 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -6246,6 +6246,92 @@ def test_get_custom_ca_trust_certificates(self): self.assertEqual(certs_empty, []) + def test_get_enable_azure_monitor_app_monitoring(self): + # default value + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_monitor_app_monitoring": False, + } + ), + self.models, + DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_enable_azure_monitor_app_monitoring(), False) + + # custom value - enabled + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_monitor_app_monitoring": True, + } + ), + self.models, + DecoratorMode.CREATE, + ) + self.assertEqual(ctx_2.get_enable_azure_monitor_app_monitoring(), True) + + def test_get_disable_azure_monitor_app_monitoring(self): + # default value + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "disable_azure_monitor_app_monitoring": False, + } + ), + self.models, + DecoratorMode.UPDATE, + ) + self.assertEqual(ctx_1.get_disable_azure_monitor_app_monitoring(), False) + + # custom value - disabled + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "disable_azure_monitor_app_monitoring": True, + } + ), + self.models, + DecoratorMode.UPDATE, + ) + self.assertEqual(ctx_2.get_disable_azure_monitor_app_monitoring(), True) + + def test_get_enable_disable_azure_monitor_app_monitoring_mutually_exclusive(self): + # test mutually exclusive - both enabled raises error + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_monitor_app_monitoring": True, + "disable_azure_monitor_app_monitoring": True, + } + ), + self.models, + DecoratorMode.UPDATE, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + ctx_1.get_enable_azure_monitor_app_monitoring() + + # test mutually exclusive from disable side + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_monitor_app_monitoring": True, + "disable_azure_monitor_app_monitoring": True, + } + ), + self.models, + DecoratorMode.UPDATE, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + ctx_2.get_disable_azure_monitor_app_monitoring() + + class AKSManagedClusterCreateDecoratorTestCase(unittest.TestCase): def setUp(self): self.cli_ctx = MockCLI() @@ -14143,5 +14229,53 @@ def test_update_node_provisioning_profile(self): self.assertEqual(dec_mc_2, ground_truth_mc_2) + + def test_update_azure_monitor_profile_enable_app_monitoring(self): + # Test enabling app monitoring on a cluster without existing azure_monitor_profile + dec_1 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_azure_monitor_app_monitoring": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_1 = self.models.ManagedCluster(location="test_location") + dec_1.context.attach_mc(mc_1) + dec_mc_1 = dec_1.update_azure_monitor_profile(mc_1) + + self.assertIsNotNone(dec_mc_1.azure_monitor_profile) + self.assertIsNotNone(dec_mc_1.azure_monitor_profile.app_monitoring) + self.assertIsNotNone(dec_mc_1.azure_monitor_profile.app_monitoring.auto_instrumentation) + self.assertTrue(dec_mc_1.azure_monitor_profile.app_monitoring.auto_instrumentation.enabled) + + def test_update_azure_monitor_profile_disable_app_monitoring(self): + # Test disabling app monitoring on a cluster with existing azure_monitor_profile + dec_1 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "disable_azure_monitor_app_monitoring": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_1 = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + app_monitoring=self.models.ManagedClusterAzureMonitorProfileAppMonitoring( + auto_instrumentation=self.models.ManagedClusterAzureMonitorProfileAppMonitoringAutoInstrumentation( + enabled=True + ) + ) + ) + ) + dec_1.context.attach_mc(mc_1) + dec_mc_1 = dec_1.update_azure_monitor_profile(mc_1) + + self.assertIsNotNone(dec_mc_1.azure_monitor_profile) + self.assertIsNotNone(dec_mc_1.azure_monitor_profile.app_monitoring) + self.assertIsNotNone(dec_mc_1.azure_monitor_profile.app_monitoring.auto_instrumentation) + self.assertFalse(dec_mc_1.azure_monitor_profile.app_monitoring.auto_instrumentation.enabled) + if __name__ == "__main__": unittest.main()