diff --git a/src/spring/HISTORY.md b/src/spring/HISTORY.md index 1ddf3fc0e87..f392cfbbc18 100644 --- a/src/spring/HISTORY.md +++ b/src/spring/HISTORY.md @@ -1,5 +1,10 @@ Release History =============== + +1.19.1 +--- +* Create workspace-based Application Insights instead, since classic Application Insights will be retired on 29 February 2024. + 1.19.0 --- * Add new commands for managed component log streaming `az spring component list`, `az spring component instance list` and `az spring component logs`. diff --git a/src/spring/azext_spring/apm.py b/src/spring/azext_spring/apm.py index 6d20c2d78b0..b152d4cf49e 100644 --- a/src/spring/azext_spring/apm.py +++ b/src/spring/azext_spring/apm.py @@ -13,6 +13,7 @@ from msrestazure.tools import parse_resource_id, is_valid_resource_id from ._utils import get_portal_uri +from .custom import try_create_application_insights from .vendored_sdks.appplatform.v2023_11_01_preview import models logger = get_logger(__name__) @@ -110,7 +111,7 @@ def _get_connection_string(cmd, resource_group, service_name, location, app_insi def _create_app_insights_and_get_connection_string(cmd, resource_group, service_name, location): try: - created_app_insights = _try_create_application_insights(cmd, resource_group, service_name, location) + created_app_insights = try_create_application_insights(cmd, resource_group, service_name, location) if created_app_insights: return created_app_insights.connection_string except Exception: # pylint: disable=broad-except @@ -154,36 +155,3 @@ def _get_app_insights_connection_string(cli_ctx, resource_group, name): "Application Insights {} under resource group {} was not found.".format(name, resource_group)) return appinsights.connection_string - - -def _try_create_application_insights(cmd, resource_group, name, location): - creation_failed_warn = 'Unable to create the Application Insights for the Azure Spring Apps. ' \ - 'Please use the Azure Portal to manually create and configure the Application Insights, ' \ - 'if needed.' - - ai_resource_group_name = resource_group - ai_name = name - ai_location = location - ai_properties = { - "name": ai_name, - "location": ai_location, - "kind": "web", - "properties": { - "Application_Type": "web" - } - } - - app_insights_client = get_mgmt_service_client(cmd.cli_ctx, ApplicationInsightsManagementClient) - appinsights = app_insights_client.components.create_or_update(ai_resource_group_name, ai_name, ai_properties) - - if not appinsights or not appinsights.connection_string: - logger.warning(creation_failed_warn) - return None - - portal_url = get_portal_uri(cmd.cli_ctx) - # We make this success message as a warning to no interfere with regular JSON output in stdout - logger.warning('Application Insights \"%s\" was created for this Azure Spring Apps. ' - 'You can visit %s/#resource%s/overview to view your ' - 'Application Insights component', appinsights.name, portal_url, appinsights.id) - - return appinsights diff --git a/src/spring/azext_spring/buildpack_binding.py b/src/spring/azext_spring/buildpack_binding.py index 3ec51fec812..fb393f065f4 100644 --- a/src/spring/azext_spring/buildpack_binding.py +++ b/src/spring/azext_spring/buildpack_binding.py @@ -7,6 +7,7 @@ from .vendored_sdks.appplatform.v2023_11_01_preview import models from azure.cli.core.util import sdk_no_wait from ._utils import get_portal_uri +from .custom import try_create_application_insights from msrestazure.tools import parse_resource_id, is_valid_resource_id from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.azclierror import InvalidArgumentValueError @@ -112,7 +113,7 @@ def _get_connection_string(cmd, resource_group, service_name, location, app_insi def _create_app_insights_and_get_connection_string(cmd, resource_group, service_name, location): try: - created_app_insights = _try_create_application_insights(cmd, resource_group, service_name, location) + created_app_insights = try_create_application_insights(cmd, resource_group, service_name, location) if created_app_insights: return created_app_insights.connection_string except Exception: # pylint: disable=broad-except @@ -155,36 +156,3 @@ def _get_app_insights_connection_string(cli_ctx, resource_group, name): raise ResourceNotFoundError("App Insights {} under resource group {} was not found.".format(name, resource_group)) return appinsights.connection_string - - -def _try_create_application_insights(cmd, resource_group, name, location): - creation_failed_warn = 'Unable to create the Application Insights for the Azure Spring Apps. ' \ - 'Please use the Azure Portal to manually create and configure the Application Insights, ' \ - 'if needed.' - - ai_resource_group_name = resource_group - ai_name = name - ai_location = location - ai_properties = { - "name": ai_name, - "location": ai_location, - "kind": "web", - "properties": { - "Application_Type": "web" - } - } - - app_insights_client = get_mgmt_service_client(cmd.cli_ctx, ApplicationInsightsManagementClient) - appinsights = app_insights_client.components.create_or_update(ai_resource_group_name, ai_name, ai_properties) - - if not appinsights or not appinsights.connection_string: - logger.warning(creation_failed_warn) - return None - - portal_url = get_portal_uri(cmd.cli_ctx) - # We make this success message as a warning to no interfere with regular JSON output in stdout - logger.warning('Application Insights \"%s\" was created for this Azure Spring Apps. ' - 'You can visit %s/#resource%s/overview to view your ' - 'Application Insights component', appinsights.name, portal_url, appinsights.id) - - return appinsights diff --git a/src/spring/azext_spring/custom.py b/src/spring/azext_spring/custom.py index 5e5358b8068..54637fb44df 100644 --- a/src/spring/azext_spring/custom.py +++ b/src/spring/azext_spring/custom.py @@ -10,6 +10,8 @@ import os import time from azure.cli.core._profile import Profile +from azure.core.exceptions import HttpResponseError +from azure.mgmt.loganalytics import LogAnalyticsManagementClient from ._websocket import WebSocketConnection, recv_remote, send_stdin, EXEC_PROTOCOL_CTRL_C_MSG from azure.mgmt.cosmosdb import CosmosDBManagementClient @@ -49,6 +51,8 @@ NO_PRODUCTION_DEPLOYMENT_SET_ERROR = "This app has no production deployment, use \"az spring app deployment create\" to create a deployment and \"az spring app set-deployment\" to set production deployment." DELETE_PRODUCTION_DEPLOYMENT_WARNING = "You are going to delete production deployment, the app will be inaccessible after this operation." LOG_RUNNING_PROMPT = "This command usually takes minutes to run. Add '--verbose' parameter if needed." +APP_INSIGHTS_CREATION_FAILURE_WARNING = 'Unable to create the Application Insights for the Azure Spring Apps. ' \ + 'Please use the Azure Portal to manually create and configure the Application Insights, if needed.' def _warn_enable_java_agent(enable_java_agent, **_): @@ -1460,26 +1464,26 @@ def _get_connection_string_from_app_insights(cmd, resource_group, app_insights): def try_create_application_insights(cmd, resource_group, name, location): - creation_failed_warn = 'Unable to create the Application Insights for the Azure Spring Apps. ' \ - 'Please use the Azure Portal to manually create and configure the Application Insights, ' \ - 'if needed.' - - ai_resource_group_name = resource_group - ai_name = name - ai_location = location + workspace = try_create_log_analytics_workspace(cmd, resource_group, name, location) + if workspace is None: + logger.warning(APP_INSIGHTS_CREATION_FAILURE_WARNING) + return None - app_insights_client = get_mgmt_service_client(cmd.cli_ctx, ApplicationInsightsManagementClient) + app_insights_client = get_mgmt_service_client(cmd.cli_ctx, ApplicationInsightsManagementClient, + api_version='2020-02-02-preview') ai_properties = { - "name": ai_name, - "location": ai_location, + "location": location, "kind": "web", "properties": { - "Application_Type": "web" + "Application_Type": "web", + "Flow_Type": "Bluefield", + "Request_Source": "rest", + "WorkspaceResourceId": workspace.id } } - appinsights = app_insights_client.components.create_or_update(ai_resource_group_name, ai_name, ai_properties) + appinsights = app_insights_client.components.create_or_update(resource_group, name, ai_properties) if appinsights is None or appinsights.connection_string is None: - logger.warning(creation_failed_warn) + logger.warning(APP_INSIGHTS_CREATION_FAILURE_WARNING) return None portal_url = get_portal_uri(cmd.cli_ctx) @@ -1542,6 +1546,40 @@ def app_insights_show(cmd, client, resource_group, name, no_wait=False): return monitoring_setting_properties +def try_create_log_analytics_workspace(cmd, resource_group, name, location): + client = get_mgmt_service_client(cmd.cli_ctx, LogAnalyticsManagementClient) + workspace = None + + try: + workspace = client.workspaces.get(resource_group, name) + except HttpResponseError as err: + if err.status_code != 404: + raise + + if workspace is not None: + return workspace + + logger.debug("Log Analytics workspace not found. Creating it now...") + properties = { + "location": location, + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30 + } + } + workspace = client.workspaces.begin_create_or_update(resource_group, name, properties).result() + + portal_url = get_portal_uri(cmd.cli_ctx) + # We make this success message as a warning to no interfere with regular JSON output in stdout + logger.warning('Log Analytics workspace \"%s\" was created for this Azure Spring Apps. ' + 'You can visit %s/#resource%s/overview to view your Log Analytics workspace', + workspace.name, portal_url, workspace.id) + + return workspace + + def app_connect(cmd, client, resource_group, service, name, deployment=None, instance=None, shell_cmd='/bin/sh'): diff --git a/src/spring/azext_spring/tests/latest/test_asa_create.py b/src/spring/azext_spring/tests/latest/test_asa_create.py index 7345fc1b811..17dbcff1a20 100644 --- a/src/spring/azext_spring/tests/latest/test_asa_create.py +++ b/src/spring/azext_spring/tests/latest/test_asa_create.py @@ -69,7 +69,7 @@ def _execute(self, resource_group, name, **kwargs): self.created_resource = call_args[0][0][2] -class TestSpringCloudCreateEnerprise(BasicTest): +class TestSpringCloudCreateEnterprise(BasicTest): def test_asc_create_enterprise(self): self._execute('rg', 'asc', sku=self._get_sku('Enterprise'), disable_app_insights=True) resource = self.created_resource @@ -169,8 +169,18 @@ def test_asa_basic_with_acc(self): self.assertIsNone(self.dev_tool) +def _workspaces_get_func(*args, **kwargs): + if args[1] == 'asc-with-existing-workspace': + workspace = mock.MagicMock() + workspace.id = 'workspace-id' + return workspace + else: + return None + + class TestSpringCloudCreateWithAI(BasicTest): - def _get_ai_client(ctx, type): + def _get_ai_client(ctx, type, api_version=None): + free_mock_client.workspaces.get.side_effect = _workspaces_get_func ai_create_resource = mock.MagicMock() ai_create_resource.connection_string = 'fake-connection' free_mock_client.components.create_or_update.return_value = ai_create_resource @@ -205,6 +215,15 @@ def test_asc_create_with_AI_happy_path(self): self.assertEqual('fake-connection', self.monitoring_settings_resource.properties.app_insights_instrumentation_key) self.assertEqual(True, self.monitoring_settings_resource.properties.trace_enabled) + def test_asc_create_with_AI_and_existing_workspace(self): + self._execute('rg', 'asc-with-existing-workspace', sku=self._get_sku()) + resource = self.created_resource + self.assertEqual('S0', resource.sku.name) + self.assertEqual('Standard', resource.sku.tier) + self.assertEqual(False, resource.properties.zone_redundant) + self.assertEqual('fake-connection', self.monitoring_settings_resource.properties.app_insights_instrumentation_key) + self.assertEqual(True, self.monitoring_settings_resource.properties.trace_enabled) + def test_asc_create_with_AI_key(self): self._execute('rg', 'asc', sku=self._get_sku(), app_insights_key='my-key') resource = self.created_resource @@ -224,8 +243,8 @@ def test_asc_create_with_AI_name(self): self.assertEqual(True, self.monitoring_settings_resource.properties.trace_enabled) -class TestSpringCloudCreateEnerpriseWithApplicationInsights(BasicTest): - def _get_application_insights_client(ctx, type): +class TestSpringCloudCreateEnterpriseWithApplicationInsights(BasicTest): + def _get_application_insights_client(ctx, type, api_version=None): application_insights_create_resource = mock.MagicMock() application_insights_create_resource.connection_string = 'fake-create-connection-string' @@ -242,6 +261,7 @@ def __init__(self, methodName: str = ...): self.buildpack_binding_resource = None @mock.patch('azext_spring.buildpack_binding.get_mgmt_service_client', _get_application_insights_client) + @mock.patch('azext_spring.custom.get_mgmt_service_client', _get_application_insights_client) def _execute(self, resource_group, name, **kwargs): client = kwargs.pop('client', None) or _get_basic_mock_client() super()._execute(resource_group, name, client=client, **kwargs) diff --git a/src/spring/setup.py b/src/spring/setup.py index eb74c738294..094794c7651 100644 --- a/src/spring/setup.py +++ b/src/spring/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '1.19.0' +VERSION = '1.19.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers