-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[AppService] az webapp status: Add new command to show per-instance Site Runtime Status #33632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
ec9195c
1a42ac9
c3e0faf
a10bcbf
d49109c
e6d1aff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -385,10 +385,12 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi | |
|
|
||
| _enable_basic_auth(cmd, name, None, resource_group_name, basic_auth.lower()) | ||
| # Only suggest deployment command when no deployment method is already configured | ||
| if not using_webapp_up and not any([container_image_name, deployment_container_image_name, | ||
| multicontainer_config_type, sitecontainers_app, | ||
| deployment_source_url, deployment_local_git]): | ||
| logger.warning("Webapp '%s' created. Deploy your code with: az webapp deploy", name) | ||
| if not using_webapp_up: | ||
| if not any([container_image_name, deployment_container_image_name, | ||
| multicontainer_config_type, sitecontainers_app, | ||
| deployment_source_url, deployment_local_git]): | ||
| logger.warning("Webapp '%s' created. Deploy your code with: az webapp deploy", name) | ||
| _log_webapp_status_tip(name, resource_group_name, is_linux) | ||
| return webapp | ||
|
|
||
|
|
||
|
|
@@ -2460,6 +2462,81 @@ def show_app(cmd, resource_group_name, name, slot=None): | |
| return app | ||
|
|
||
|
|
||
| def _log_webapp_status_tip(name, resource_group_name, is_linux): | ||
| # Per-instance runtime status (siteStatus) is a Linux App Service feature, | ||
| # so only surface the tip for Linux webapps. | ||
| if not is_linux: | ||
| return | ||
| logger.warning("Tip: run 'az webapp status --name %s --resource-group %s' " | ||
| "to see per-instance runtime status.", | ||
| name, resource_group_name) | ||
|
|
||
|
|
||
| def _extract_webapp_status_items(result): | ||
| # The siteStatus response holds per-instance status under 'properties': | ||
| # a list for /siteStatus, a single object for /siteStatus/{instanceId}. | ||
| # Normalize both shapes into a list for uniform formatting. | ||
| if isinstance(result, dict): | ||
| properties = result.get('properties') | ||
| if isinstance(properties, list): | ||
| return properties | ||
| if isinstance(properties, dict): | ||
| return [properties] | ||
| return [] | ||
|
|
||
|
|
||
| def format_webapp_status_output(result): | ||
| from collections import OrderedDict | ||
|
|
||
| items = _extract_webapp_status_items(result) | ||
| # LastError is a nullable field on the backend SiteRuntimeStatusOnWorker contract, | ||
| # so the error columns (LastError, LastErrorDetails, LastErrorTimestamp) are only | ||
| # surfaced when at least one instance reports a LastError. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the comments! |
||
| show_errors = any(item.get('lastError') for item in items) | ||
|
|
||
| rows = [] | ||
| for item in items: | ||
| row = OrderedDict([ | ||
| ('InstanceId', item.get('instanceId')), | ||
| ('State', item.get('state')), | ||
| ('Action', item.get('action')) | ||
| ]) | ||
| if show_errors: | ||
| row['LastError'] = item.get('lastError') | ||
| row['LastErrorDetails'] = item.get('lastErrorDetails') | ||
| row['LastErrorTimestamp'] = item.get('lastErrorTimestamp') | ||
| row['Details'] = item.get('details') | ||
| row['DetailsLevel'] = item.get('detailsLevel') | ||
| rows.append(row) | ||
| return rows | ||
|
|
||
|
|
||
| def show_webapp_status(cmd, resource_group_name, name, slot=None, instance=None): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This takes a slot, but the actual command does not. I do think the slot param should be present
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this about _params.py and there not being a slot parameter in az webapp status? I thought the slot parameter should be inherited from az webapp the way resource group or name are? Or am I misunderstanding something?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the slot param would still work if unlisted as it is inherited (might want to test this to be 100%) |
||
| from azure.cli.core.commands.client_factory import get_subscription_id | ||
|
|
||
| client = web_client_factory(cmd.cli_ctx) | ||
| subscription_id = get_subscription_id(cmd.cli_ctx) | ||
| api_version = client.DEFAULT_API_VERSION | ||
| resource_manager = cmd.cli_ctx.cloud.endpoints.resource_manager | ||
| slot_segment = f'/slots/{slot}' if slot else '' | ||
| instance_segment = f'/{instance}' if instance else '' | ||
| request_url = ( | ||
| f'{resource_manager}/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just something from copilot, have you confirmed resource_manager does not end in '/' so we dont get resource_manager// ? |
||
| f'/providers/Microsoft.Web/sites/{name}{slot_segment}/siteStatus{instance_segment}' | ||
| f'?api-version={api_version}' | ||
| ) | ||
|
|
||
| try: | ||
| return send_raw_request(cmd.cli_ctx, 'GET', request_url).json() | ||
| except HttpResponseError as ex: | ||
| if instance and ex.status_code == 404: | ||
| scope = 'webapp and slot' if slot else 'webapp' | ||
| raise ResourceNotFoundError( | ||
| f"Instance '{instance}' was not found for this {scope}. " | ||
| "Run 'az webapp list-instances' to see available instance IDs.") | ||
| raise | ||
|
|
||
|
|
||
| def _list_app(cli_ctx, resource_group_name=None, show_details=False): | ||
| client = web_client_factory(cli_ctx) | ||
| if resource_group_name: | ||
|
|
@@ -9931,6 +10008,7 @@ def _poll_deployment_runtime_status(cmd, resource_group_name, webapp_name, slot, | |
| time_elapsed = 0 | ||
| deployment_status = None | ||
| response_body = None | ||
| status_tip_logged = False | ||
| while time_elapsed < max_time_sec: | ||
| try: | ||
| response_body = send_raw_request(cmd.cli_ctx, "GET", deploymentstatusapi_url).json() | ||
|
|
@@ -9945,10 +10023,15 @@ def _poll_deployment_runtime_status(cmd, resource_group_name, webapp_name, slot, | |
| status = deployment_status if status is None else status | ||
| logger.warning("Status: %s Time: %s(s)", status, time_elapsed) | ||
| if deployment_status == "RuntimeStarting": | ||
| if not status_tip_logged: | ||
| _log_webapp_status_tip(webapp_name, resource_group_name, True) | ||
| status_tip_logged = True | ||
| logger.info("InprogressInstances: %s, SuccessfulInstances: %s", | ||
| deployment_properties.get('numberOfInstancesInProgress'), | ||
| deployment_properties.get('numberOfInstancesSuccessful')) | ||
| if deployment_status == "RuntimeSuccessful": | ||
| if not status_tip_logged: | ||
| _log_webapp_status_tip(webapp_name, resource_group_name, True) | ||
| break | ||
| if deployment_status == "RuntimeFailed": | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we not want to log status tip if RuntimeFailed? |
||
| error_text = "" | ||
|
|
@@ -10834,6 +10917,8 @@ def webapp_up(cmd, name=None, resource_group_name=None, plan=None, location=None | |
| logger.warning("You can launch the app at %s", _url) | ||
| create_json.update({'URL': _url}) | ||
|
|
||
| _log_webapp_status_tip(name, rg_name, _is_linux) | ||
|
|
||
| if logs: | ||
| _configure_default_logging(cmd, rg_name, name) | ||
| try: | ||
|
|
@@ -11588,6 +11673,8 @@ def _make_onedeploy_request(params): | |
| logger.warning("Deployment status is: \"%s\"", state) | ||
| response_body = response.json().get("properties", {}) | ||
| logger.warning("Deployment has completed successfully") | ||
| if not (poll_async_deployment_for_debugging and params.track_status): | ||
| _log_webapp_status_tip(params.webapp_name, params.resource_group_name, params.is_linux_webapp) | ||
| logger.warning("You can visit your app at: %s", _get_visit_url(params)) | ||
| return response_body | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see there's a lot of isinstance type checks here, do we also need to check if each item in properties list is a dict? Up to you, just asking because we seem to be checking types a lot.
item.get('lastError') for item in items (line 2495) is assuming item is a dict I believe