-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[App Service] az webapp deploy, az webapp up: Add enriched deployment failure logs for quicker resolution
#32940
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
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
0f35993
added detailed failure prompt
135f9fc
added enriched failure prompt for az webapp up
5ef568c
Merge branch 'dev' of https://github.com/Shi1810/azure-cli into user/…
7667348
incorporate design review feedback
5d5fa5d
Merge branch 'dev' of https://github.com/Shi1810/azure-cli into user/…
7adff12
added --enriched-error feature flag
e5b5020
fixes
0651557
remove deprecated command from suggestion
89476b4
Merge remote-tracking branch 'origin/dev' into user/shikhajha/errorco…
6de7583
fix
3d4f96e
more fix
9a29b20
add enriched-error in help entry
98be2b0
add test and recordings
e5e7a4d
Merge branch 'dev' of https://github.com/Shi1810/azure-cli into user/…
146e279
Merge branch 'dev' of https://github.com/Shi1810/azure-cli into user/…
cccb7a9
Remove unnecessary code
2c29443
minor fix
00ba39d
PR comments
48b69ad
PR comments
563929e
indentation
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
233 changes: 233 additions & 0 deletions
233
src/azure-cli/azure/cli/command_modules/appservice/_deployment_context_engine.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| """ | ||
| Context-enriched error builder for az webapp deploy / az webapp up. | ||
| Enabled via the --enriched-errors flag on az webapp deploy / az webapp up. | ||
| """ | ||
|
|
||
| import re | ||
|
|
||
| from knack.log import get_logger | ||
| from knack.util import CLIError | ||
|
|
||
| from ._deployment_failure_patterns import match_failure_pattern | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| class EnrichedDeploymentError(CLIError): | ||
| # A CLIError subclass for context-enriched deployment failures. | ||
| pass | ||
|
|
||
|
|
||
| _STATUS_CODE_PATTERNS = [ | ||
| re.compile(r'Status\s*Code[:\s]+(\d{3})', re.IGNORECASE), # "Status Code: 400" | ||
| re.compile(r'\(([45]\d{2})\)'), # "Bad Request(400)" | ||
| re.compile(r'HTTP\s+(\d{3})', re.IGNORECASE), # "HTTP 504" | ||
| re.compile( | ||
| r'\b([45]\d{2})\s+(?:Bad|Unauthorized|Forbidden|Not\s+Found|Conflict' | ||
| r'|Too\s+Many|Internal|Gateway|Service)', re.IGNORECASE), # "400 Bad Request" | ||
| ] | ||
|
|
||
|
|
||
| def extract_status_code_from_message(message): | ||
| if not message: | ||
| return None | ||
| for pattern in _STATUS_CODE_PATTERNS: | ||
| m = pattern.search(message) | ||
| if m: | ||
| code = int(m.group(1)) | ||
| if 400 <= code <= 599: | ||
| return code | ||
| return None | ||
|
|
||
|
|
||
| def _get_app_runtime(cmd, resource_group_name, webapp_name, slot=None): | ||
| try: | ||
| from ._client_factory import web_client_factory | ||
| client = web_client_factory(cmd.cli_ctx) | ||
| if slot: | ||
| config = client.web_apps.get_configuration_slot(resource_group_name, webapp_name, slot) | ||
| else: | ||
| config = client.web_apps.get_configuration(resource_group_name, webapp_name) | ||
| if config.linux_fx_version: | ||
| return config.linux_fx_version | ||
| return "Unknown" | ||
| except Exception: # pylint: disable=broad-except | ||
| return "Unknown" | ||
|
|
||
|
|
||
| def _get_app_region_and_plan_sku(cmd, resource_group_name, webapp_name): | ||
| try: | ||
| from ._client_factory import web_client_factory | ||
| from azure.mgmt.core.tools import parse_resource_id | ||
| client = web_client_factory(cmd.cli_ctx) | ||
| app = client.web_apps.get(resource_group_name, webapp_name) | ||
| region = app.location if app else "Unknown" | ||
| sku = "Unknown" | ||
| if app and app.server_farm_id: | ||
| plan_parts = parse_resource_id(app.server_farm_id) | ||
| plan = client.app_service_plans.get(plan_parts['resource_group'], plan_parts['name']) | ||
| if plan and plan.sku: | ||
| sku = plan.sku.name | ||
| return region, sku | ||
| except Exception: # pylint: disable=broad-except | ||
| return "Unknown", "Unknown" | ||
|
|
||
|
|
||
| _ARTIFACT_TYPE_MAP = { | ||
| 'zip': 'ZipDeploy', 'war': 'WarDeploy', 'jar': 'JarDeploy', | ||
| 'ear': 'EarDeploy', 'startup': 'StartupFile', 'static': 'StaticDeploy' | ||
| } | ||
|
|
||
|
|
||
| def _determine_deployment_type(params=None, *, src_url=None, artifact_type=None): | ||
| _src_url = src_url if src_url is not None else (getattr(params, 'src_url', None) if params else None) | ||
| _artifact = artifact_type if artifact_type is not None else ( | ||
| getattr(params, 'artifact_type', None) if params else None) | ||
|
|
||
| if _src_url: | ||
| return "OneDeploy (URL-based)" | ||
|
Shi1810 marked this conversation as resolved.
|
||
|
|
||
| return _ARTIFACT_TYPE_MAP.get(_artifact, "OneDeploy") | ||
|
|
||
|
|
||
| def build_enriched_error_context(params=None, *, cmd=None, resource_group_name=None, # pylint: disable=too-many-locals | ||
| webapp_name=None, slot=None, src_url=None, | ||
| artifact_type=None, status_code=None, error_message=None, | ||
| deployment_status=None, | ||
| last_known_step=None, kudu_status=None): | ||
| _cmd = cmd or (params.cmd if params else None) | ||
| _rg = resource_group_name or (params.resource_group_name if params else None) | ||
| _name = webapp_name or (params.webapp_name if params else None) | ||
| _slot = slot if slot is not None else ( | ||
| getattr(params, 'slot', None) if params else None) | ||
| _src_url = src_url if src_url is not None else ( | ||
| getattr(params, 'src_url', None) if params else None) | ||
| _artifact = artifact_type if artifact_type is not None else ( | ||
| getattr(params, 'artifact_type', None) if params else None) | ||
|
|
||
| pattern = match_failure_pattern( | ||
| status_code=status_code, | ||
| error_message=error_message, | ||
| ) | ||
|
|
||
| # Build base context | ||
| context = {} | ||
|
|
||
| if pattern: | ||
| context["errorCode"] = pattern["errorCode"] | ||
| context["stage"] = pattern["stage"] | ||
| else: | ||
| context["errorCode"] = f"HTTP_{status_code}" if status_code else "UnknownDeploymentError" | ||
| context["stage"] = deployment_status or "Unknown" | ||
|
|
||
| # App metadata (best-effort) | ||
| if _cmd and _rg and _name: | ||
| context["runtime"] = _get_app_runtime(_cmd, _rg, _name, _slot) | ||
| region, plan_sku = _get_app_region_and_plan_sku(_cmd, _rg, _name) | ||
| context["region"] = region | ||
| context["planSku"] = plan_sku | ||
| else: | ||
| context["runtime"] = "Unknown" | ||
| context["region"] = "Unknown" | ||
| context["planSku"] = "Unknown" | ||
|
|
||
| context["deploymentType"] = _determine_deployment_type( | ||
| params, src_url=_src_url, artifact_type=_artifact | ||
| ) | ||
|
|
||
| # Suggested fixes | ||
| if pattern: | ||
| context["suggestedFixes"] = pattern["suggestedFixes"] | ||
| else: | ||
| context["suggestedFixes"] = [ | ||
| "Check deployment logs: 'az webapp log deployment show -n {} -g {}'".format( | ||
| _name or '<app>', _rg or '<rg>'), | ||
| "Check runtime logs: 'az webapp log tail -n {} -g {}'".format( | ||
| _name or '<app>', _rg or '<rg>') | ||
| ] | ||
|
|
||
| # Extra diagnostics | ||
| if last_known_step: | ||
| context["lastKnownStep"] = last_known_step | ||
| if kudu_status: | ||
| context["kuduStatus"] = str(kudu_status) | ||
|
|
||
| # Raw details | ||
| if error_message: | ||
| if len(error_message) > 500: | ||
| context["rawError"] = error_message[:500] + "... [truncated]" | ||
| else: | ||
| context["rawError"] = error_message | ||
|
|
||
| return context | ||
|
|
||
|
|
||
| def format_enriched_error_message(context): | ||
| lines = [] | ||
| lines.append("") | ||
| lines.append("=" * 72) | ||
| lines.append("DEPLOYMENT FAILED: Context-Enriched Diagnostics") | ||
| lines.append("=" * 72) | ||
| lines.append("") | ||
|
|
||
| lines.append(f"Error Code : {context.get('errorCode', 'Unknown')}") | ||
| lines.append(f"Stage : {context.get('stage', 'Unknown')}") | ||
| lines.append(f"Runtime : {context.get('runtime', 'Unknown')}") | ||
| lines.append(f"Deploy Type : {context.get('deploymentType', 'Unknown')}") | ||
| lines.append(f"Region : {context.get('region', 'Unknown')}") | ||
| lines.append(f"Plan SKU : {context.get('planSku', 'Unknown')}") | ||
| if context.get("lastKnownStep"): | ||
| lines.append(f"Last Step : {context['lastKnownStep']}") | ||
| if context.get("kuduStatus"): | ||
| lines.append(f"Kudu Status : {context['kuduStatus']}") | ||
| lines.append("") | ||
|
|
||
| if context.get("rawError"): | ||
| lines.append(f"Raw Error : {context['rawError']}") | ||
| lines.append("") | ||
|
|
||
| fixes = context.get("suggestedFixes", []) | ||
| if fixes: | ||
| lines.append("Suggested Fixes:") | ||
| for f in fixes: | ||
| lines.append(f" - {f}") | ||
| lines.append("") | ||
|
|
||
| # Copilot prompt | ||
| lines.append("-" * 72) | ||
| lines.append(" Copy the full error output above and paste it into GitHub Copilot Chat") | ||
| lines.append(" with the prompt: 'Why did my Linux App Service deployment fail and how do I fix it?'") | ||
| lines.append("-" * 72) | ||
|
|
||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def raise_enriched_deployment_error(params=None, *, cmd=None, resource_group_name=None, | ||
| webapp_name=None, slot=None, src_url=None, | ||
| artifact_type=None, status_code=None, error_message=None, | ||
| deployment_status=None, | ||
| last_known_step=None, kudu_status=None): | ||
| context = build_enriched_error_context( | ||
| params=params, | ||
| cmd=cmd, | ||
| resource_group_name=resource_group_name, | ||
| webapp_name=webapp_name, | ||
| slot=slot, | ||
| src_url=src_url, | ||
| artifact_type=artifact_type, | ||
| status_code=status_code, | ||
| error_message=error_message, | ||
| deployment_status=deployment_status, | ||
| last_known_step=last_known_step, | ||
| kudu_status=kudu_status | ||
| ) | ||
|
|
||
| logger.debug("Deployment failure context: %s", context) | ||
|
|
||
| message = format_enriched_error_message(context) | ||
|
Shi1810 marked this conversation as resolved.
|
||
| raise EnrichedDeploymentError(message) | ||
146 changes: 146 additions & 0 deletions
146
src/azure-cli/azure/cli/command_modules/appservice/_deployment_failure_patterns.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| DEPLOYMENT_FAILURE_PATTERNS = [ | ||
| # 400 Bad Request — OneDeploy / general request validation | ||
| { | ||
| "errorCode": "DeploymentFailed", | ||
|
Shi1810 marked this conversation as resolved.
|
||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Check the deployment request body and packageUri for correctness", | ||
| "Verify the artifact is a valid deployment package", | ||
| "Check deployment logs: 'az webapp log deployment show'" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "InvalidArtifactType", | ||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Use a supported artifact type: zip, war, jar, ear, lib, startup, static, script", | ||
| "Check the 'type' query parameter in the deploy request" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "ArtifactStackMismatch", | ||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Ensure the artifact type matches the app's runtime stack (e.g., war requires Tomcat)", | ||
| "Check 'az webapp config show' for the current linuxFxVersion", | ||
| "Update the runtime stack via 'az webapp config set --linux-fx-version'" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "MissingDeployPath", | ||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Provide the 'path' query parameter for type=lib, type=script, or type=static", | ||
| "Review the OneDeploy API documentation for required parameters" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "InvalidDeployPath", | ||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Remove trailing '/' from the deploy path", | ||
| "Use an absolute path; do not include '..' path segments", | ||
| "Review the deploy path for correct format" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "InvalidPackageUri", | ||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Verify the packageUri is a valid, accessible URL", | ||
| "Ensure the packageUri is not empty or null in the JSON request body", | ||
| "Test the package URL is reachable from your network" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "CleanDeployForbidden", | ||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Do not use clean=true when deploying to /home or /home/site", | ||
| "Change the deploy path to a subdirectory (e.g., /home/site/wwwroot)", | ||
| "Remove the 'clean=true' parameter from the deploy request" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "UnsupportedArtifactType", | ||
| "stage": "Deployment", | ||
| "httpStatus": 400, | ||
| "suggestedFixes": [ | ||
| "Use a supported artifact type: zip, war, jar, ear, lib, startup, static, script", | ||
| "Check 'az webapp deploy --help' for valid type values" | ||
| ] | ||
| }, | ||
| # 409 Conflict | ||
| { | ||
| "errorCode": "DeploymentInProgress", | ||
| "stage": "Deployment", | ||
| "httpStatus": 409, | ||
| "suggestedFixes": [ | ||
| "Wait for the current deployment to complete before starting a new one", | ||
| "Check deployment status: 'az webapp deployment show'", | ||
| "If stuck, restart the SCM site to release the deployment lock" | ||
| ] | ||
| }, | ||
| { | ||
| "errorCode": "RunFromRemoteZipConfigured", | ||
| "stage": "Deployment", | ||
| "httpStatus": 409, | ||
| "suggestedFixes": [ | ||
| "Remove WEBSITE_RUN_FROM_PACKAGE (or legacy WEBSITE_RUN_FROM_ZIP) app setting pointing to a remote URL", | ||
| "Use 'az webapp config appsettings delete --setting-names WEBSITE_RUN_FROM_PACKAGE'", | ||
| "Set WEBSITE_RUN_FROM_PACKAGE to 1 instead of a URL" | ||
| ] | ||
| }, | ||
| ] | ||
|
|
||
| # Index for O(1) lookup by error code | ||
| _PATTERN_INDEX = {p["errorCode"]: p for p in DEPLOYMENT_FAILURE_PATTERNS} | ||
|
|
||
|
|
||
| def get_failure_pattern(error_code): | ||
| return _PATTERN_INDEX.get(error_code) | ||
|
|
||
|
|
||
| def match_failure_pattern(status_code=None, error_message=None): # pylint: disable=too-many-return-statements,too-many-branches | ||
| if error_message is None: | ||
| error_message = "" | ||
|
|
||
| error_lower = error_message.lower() | ||
|
|
||
| if status_code == 400: | ||
| if "not recognized" in error_lower and "type=" in error_lower: | ||
| return get_failure_pattern("InvalidArtifactType") | ||
| if "cannot be deployed to stack" in error_lower: | ||
| return get_failure_pattern("ArtifactStackMismatch") | ||
| if "artifact type" in error_lower and "not supported" in error_lower: | ||
| return get_failure_pattern("UnsupportedArtifactType") | ||
| if "path must be defined" in error_lower: | ||
| return get_failure_pattern("MissingDeployPath") | ||
| if "path cannot end with" in error_lower or "path cannot contain" in error_lower: | ||
| return get_failure_pattern("InvalidDeployPath") | ||
| if "invalid packageurl" in error_lower: | ||
| return get_failure_pattern("InvalidPackageUri") | ||
| if "clean deployments cannot be performed" in error_lower: | ||
| return get_failure_pattern("CleanDeployForbidden") | ||
| # Generic 400 - deployment failed pattern | ||
| return get_failure_pattern("DeploymentFailed") | ||
| if status_code == 409: | ||
| if ("run-from-zip" in error_lower or | ||
| "website_run_from_package" in error_lower or | ||
| "website_use_zip" in error_lower): | ||
| return get_failure_pattern("RunFromRemoteZipConfigured") | ||
| # Generic 409 - deployment lock conflict | ||
| return get_failure_pattern("DeploymentInProgress") | ||
| return None | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.