Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/azure-cli/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Release History
* Fix #33180: `az functionapp plan create`: Simplify reserved parameter assignment in AppServicePlan (#33202)
* `az webapp sitecontainers convert`: Add support for converting Docker Compose multi-container apps to Sitecontainers mode (#33131)
* `az webapp up/deploy`: Add `--enriched-errors` parameter to see detailed deployment failure log (#32940)
* `az webapp status`: Add new command to show per-instance Site Runtime Status (#33632)
* `az webapp create`: Add error message that clearly lists all valid options and specifies how to discover available runtimes (#33252)
* `az appservice plan create`: Make `P0V3` as default SKU when `--sku` is omitted for linux webapp (#33237)
* `az appservice plan create`: Add `PREMIUM0V3` tier for elastic scale (#33237)
Expand Down
14 changes: 14 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -2478,6 +2478,20 @@
crafted: true
"""

helps['webapp status'] = """
type: command
short-summary: Show per-instance Site Runtime Status for a web app.
long-summary: >
Returns the runtime status of each instance.
examples:
- name: Show runtime status for all instances in the production slot.
text: az webapp status --name MyWebapp --resource-group MyResourceGroup
- name: Show runtime status for a deployment slot.
text: az webapp status --name MyWebapp --resource-group MyResourceGroup --slot staging
- name: Show runtime status for a specific instance.
text: az webapp status --name MyWebapp --resource-group MyResourceGroup --instance 6d3f0a2b8e5c4d1fb97a3c6e2f4a1b09
"""

helps['webapp ssh'] = """
type: command
short-summary: SSH command establishes a ssh session to the web container and developer would get a shell terminal remotely.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ def load_arguments(self, _):
help="the name of the slot. Default to the productions slot if not specified")
c.argument('name', arg_type=webapp_name_arg_type)

with self.argument_context('webapp status') as c:
c.argument('instance', options_list=['--instance'],
help='show runtime status for a specific instance only. '
"Run 'az webapp list-instances' to discover instance IDs.")
with self.argument_context('functionapp') as c:
c.ignore('app_instance')
c.argument('resource_group_name', arg_type=resource_group_name_type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ def transform_runtime_list_output(result):
]) for r in result]


def transform_webapp_status_output(result):
from .custom import format_webapp_status_output
return format_webapp_status_output(result)


def ex_handler_factory(creating_plan=False):
def _ex_handler(ex):
ex = _polish_bad_errors(ex, creating_plan)
Expand Down Expand Up @@ -142,6 +147,7 @@ def load_command_table(self, _):
g.custom_command('restart', 'restart_webapp')
g.custom_command('browse', 'view_in_browser')
g.custom_command('list-instances', 'list_instances')
g.custom_command('status', 'show_webapp_status', table_transformer=transform_webapp_status_output)
g.custom_command('list-runtimes', 'list_runtimes', table_transformer=transform_runtime_list_output)
g.custom_command('identity assign', 'assign_identity')
g.custom_show_command('identity show', 'show_identity')
Expand Down
95 changes: 91 additions & 4 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):

@vageorge00 vageorge00 Jun 24, 2026

Copy link
Copy Markdown
Contributor

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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%)
But I also see some other functions in _params.py, do list it, maybe as a style?

    with self.argument_context('webapp list-instances') as c:
        c.argument('name', arg_type=webapp_name_arg_type, id_part=None)
        c.argument('slot', options_list=['--slot', '-s'], help='Name of the web app slot. Default to the productions slot if not specified.')

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}'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -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()
Expand All @@ -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":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want to log status tip if RuntimeFailed?

error_text = ""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
Loading
Loading