diff --git a/src/azure-cli/HISTORY.rst b/src/azure-cli/HISTORY.rst index 59fae05291c..239942c14b9 100644 --- a/src/azure-cli/HISTORY.rst +++ b/src/azure-cli/HISTORY.rst @@ -33,6 +33,7 @@ Release History * `az webapp list-runtimes`: Add `--runtime` and `--support` filter parameters (#32903) * [BREAKING CHANGE] `az webapp list-runtimes`: Remove deprecated `--linux` and `--show-runtime-details` parameters (#32903) * `az webapp log startup`: Add commands to list and view Linux container startup logs (#33256) +* `az webapp troubleshoot config`: Add preview command that runs built-in configuration checks against a Linux web app and surfaces the last runtime error from ARM (#33709) * `az webapp create`: Add `--site-scoped-certs` parameter to support enabling or disabling site-scoped certificates (#33306) * `az webapp up`: Add warning message for future deprecation (#33410) * `az functionapp deployment source config-zip`: Fix `KeyError` `'FUNCTIONS_WORKER_RUNTIME'` for Go function apps on Flex Consumption (#33404) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index eea863ae35d..361f731ec2a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -2432,6 +2432,35 @@ text: az webapp log startup show --name MyWebApp --resource-group MyResourceGroup --instance lw0sdlwk000002 """ +helps['webapp troubleshoot config'] = """ +type: command +short-summary: Validate configuration for a Linux web app and surface the last runtime error. +long-summary: > + Aggregates two data sources into a single report: + + (1) Built-in configuration checks — a set of common + Linux App Service settings (linuxFxVersion, port binding, startup + command, alwaysOn, health check path, ...) evaluated against the + running site's configuration snapshot. + + (2) The most recent site runtime status error reported by App Service. + The runtime error recommendation section is only surfaced when the + error occurred within the last 15 minutes; older errors are still + included in the structured payload but are hidden from the `--report` + view. + + By default the command returns a structured payload so the standard + `-o json/yaml/table` formatters handle output. Pass `--report` to + print a human-readable two-section report to stdout instead. +examples: + - name: Run the built-in configuration checks and show the runtime error, if any (JSON by default) + text: az webapp troubleshoot config --name MyWebApp --resource-group MyResourceGroup + - name: Print the human-readable report + text: az webapp troubleshoot config --name MyWebApp --resource-group MyResourceGroup --report + - name: Target a deployment slot + text: az webapp troubleshoot config --name MyWebApp --resource-group MyResourceGroup --slot staging +""" + helps['functionapp log'] = """ type: group short-summary: Manage function app logs. diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 7958d119c36..b4dd8f809e8 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -849,6 +849,13 @@ def load_arguments(self, _): with self.argument_context('webapp log startup show') as c: c.argument('filename', options_list=['--filename', '-f'], help='Name of a specific startup log file to display. If not specified, shows the latest log (preferring failures).') + with self.argument_context('webapp troubleshoot config') as c: + c.argument('name', arg_type=webapp_name_arg_type, id_part=None) + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('slot', options_list=['--slot', '-s'], help="the name of the slot. Defaults to the production slot if not specified") + c.argument('report', options_list=['--report'], arg_type=get_three_state_flag(), + help='Print a human-readable report instead of the structured payload.') + with self.argument_context('functionapp log deployment show') as c: c.argument('name', arg_type=functionapp_name_arg_type, id_part=None) c.argument('resource_group', arg_type=resource_group_name_type) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_troubleshoot_config_report.py b/src/azure-cli/azure/cli/command_modules/appservice/_troubleshoot_config_report.py new file mode 100644 index 00000000000..a48fc32de32 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/appservice/_troubleshoot_config_report.py @@ -0,0 +1,245 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Human-readable report rendering for 'az webapp troubleshoot config --report'. + +Extracted from ``custom.py`` to keep the command's control flow separate from +its presentation layer. The command builds a structured payload; this module +renders it. ``render_report(payload)`` is the sole public entry point. +""" + +import shutil +import sys +import textwrap +from datetime import datetime, timezone + +from azure.cli.core.style import Style, print_styled_text + + +def _format_dt(value): + """Human-readable timestamp: 'YYYY-MM-DD HH:MM:SS UTC' (or best-effort).""" + if not value: + return None + if isinstance(value, str): + v = value.replace('T', ' ') + is_utc = v.endswith('Z') + if '.' in v: + v = v.split('.', 1)[0] + if is_utc: + if v.endswith('Z'): + v = v[:-1] + v = v + ' UTC' + elif '+' in v: + v = v.split('+', 1)[0] + return v + return str(value) + + +def _short_id(instance_id): + """Truncate a long hex ARM instanceId to 10 characters for display.""" + if not instance_id: + return None + if len(instance_id) > 12: + return instance_id[:10] + return instance_id + + +def _relative_age(iso_value): + """Return a short 'Nh Mm ago' / 'Nm ago' / 'just now' / 'in the future' string + for an ISO-8601 UTC timestamp, or None if the input is unparseable/missing.""" + if not iso_value or not isinstance(iso_value, str): + return None + v = iso_value + if '.' in v: + head, _, tail = v.partition('.') + tz = '' + for suffix in ('Z', '+', '-'): + if suffix in tail: + idx = tail.find(suffix) + tz = tail[idx:] + break + v = head + tz + v = v.replace('Z', '+00:00') + try: + dt = datetime.fromisoformat(v) + except ValueError: + return None + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + delta = datetime.now(timezone.utc) - dt + total_seconds = int(delta.total_seconds()) + if total_seconds < 0: + return 'in the future' + if total_seconds < 60: + return 'just now' + minutes = total_seconds // 60 + if minutes < 60: + return '{}m ago'.format(minutes) + hours = minutes // 60 + rem_min = minutes % 60 + if hours < 24: + return '{}h {}m ago'.format(hours, rem_min) if rem_min else '{}h ago'.format(hours) + days = hours // 24 + rem_hours = hours % 24 + return '{}d {}h ago'.format(days, rem_hours) if rem_hours else '{}d ago'.format(days) + + +def render_report(payload): + """Print the human-readable report: BUILT-IN CHECKS + SITE RUNTIME ERROR + RECOMMENDATION. Invoked when ``--report`` is passed.""" + + def _out(*objs): + print_styled_text(*objs, file=sys.stdout) + + def _row(*objs): + _out(list(objs)) + + def _labeled(label, value): + """Emit '