Skip to content

Stream Kudu build logs during deployment#33550

Open
Saipriya-1144 wants to merge 4 commits into
Azure:devfrom
Saipriya-1144:user/vchintalapat/stream-build-logs-deployment
Open

Stream Kudu build logs during deployment#33550
Saipriya-1144 wants to merge 4 commits into
Azure:devfrom
Saipriya-1144:user/vchintalapat/stream-build-logs-deployment

Conversation

@Saipriya-1144

@Saipriya-1144 Saipriya-1144 commented Jun 14, 2026

Copy link
Copy Markdown

Description

az webapp deploy currently gives no visibility into the server-side build. This PR streams Kudu's build logs into the terminal during async deployments and prints full logs inline on failure.

A new  --build-logs  flag controls verbosity:
•  summary  (default): phase markers, key milestones, and aggregated package counts; transient output collapses to a single status line.
•  full : raw, unfiltered build output.
•  none : build logs suppressed.

During the build, we poll Kudu's log endpoint every ~5s and print new lines sequentially (de-duplicated). So, logs arrive in 5s batches. Fast builds that finish between polls print everything at once on completion; after the build, polling slows to ~15s for runtime startup.

Changes:

Before this Change:
Successful deployment:
image

Failed Deployment:
image

After this Change:
Successful deployment: (Default is --build-logs summary)
image

All non-milestone output is transient — it streams onto a single self-overwriting status line that updates in place, so progress stays visible without flooding the terminal.

image

Failed Deployment: (Displays full logs)
image
image

With --build-logs full:
image

With --build-logs none:
image

Testing:

Unit tests - test_build_log_formatter.py (24 tests): verbosity modes, pip/npm aggregation, the renderer (status line, truncation, finalize, TTY detection, pacing, and Kudu log-fetch dedup. All passing.

Manual - Verified on a live Linux web app across Python, Node, PHP, and .NET: all three --build-logs modes, async vs. sync (sync streams nothing), and the build-failure auto-expand.

@Saipriya-1144 Saipriya-1144 requested a review from NoriZC as a code owner June 14, 2026 19:34
Copilot AI review requested due to automatic review settings June 14, 2026 19:34
@azure-client-tools-bot-prd

Copy link
Copy Markdown
Validation for Azure CLI Full Test Starting...

Thanks for your contribution!

@azure-client-tools-bot-prd

Copy link
Copy Markdown
Validation for Breaking Change Starting...

Thanks for your contribution!

Copilot AI left a comment

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds configurable build log verbosity for OneDeploy-based deployments and improves deployment UX by streaming/formatting Kudu build output and auto-expanding logs on failure.

Changes:

  • Introduces --build-logs {summary|full|none} and threads it through OneDeploy deploy flows.
  • Adds Kudu build log fetching/streaming, phase-based status output, and failure messages enriched with full build logs.
  • Adds a new build log formatter module for summary/full/none behaviors and final URL formatting.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/azure-cli/azure/cli/command_modules/appservice/custom.py Threads build_logs through deployment tracking, streams Kudu logs, and enriches failure output with build logs.
src/azure-cli/azure/cli/command_modules/appservice/_params.py Adds --build-logs CLI argument (currently only in one argument context).
src/azure-cli/azure/cli/command_modules/appservice/_build_log_formatter.py New formatter for summary/full/none build log filtering and output formatting helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -1,4 +1,4 @@
# --------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------
Comment on lines +10001 to +10024
def _emit_build_log_line(message, log_formatter, timestamp=None, indent=False):
"""Emit a single build log line through the formatter.

If formatter is None or verbosity is 'full', prints directly.
Otherwise, the formatter decides whether to show, suppress, or aggregate.

The formatter classifies based on the raw message text (without timestamp prefix)
to ensure patterns match regardless of how the line is formatted for display.
"""
if indent or not timestamp:
display_line = f" {message}\n"
else:
display_line = f"{timestamp} {message}\n"

if log_formatter is None:
logger.warning("%s", display_line.rstrip('\n'))
return

# Pass message with indent prefix for classification (patterns expect leading whitespace)
classify_line = f" {message}\n"
formatted = log_formatter.format_log_line(classify_line)
if formatted is not None:
# Show the display_line (with timestamp) not the classify_line
logger.warning("%s", display_line.rstrip('\n'))

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.

Addressed this, the code now actually prints the text when the formatter gives a new line.

Comment on lines +9887 to +9937
def _fetch_kudu_log_entries(cmd, resource_group_name, webapp_name, slot, deployment_id):
"""Fetch raw log entries from Kudu deployment log API.

Returns a list of (message, log_time, detail_entries) tuples, or None if fetching fails.
Each detail_entries is a list of (message, log_time) tuples from the details_url.
"""
import requests
from azure.cli.core.util import should_disable_connection_verify

try:
headers = get_scm_site_headers(cmd.cli_ctx, webapp_name, resource_group_name, slot)
scm_url = _get_scm_url(cmd, resource_group_name, webapp_name, slot)
log_url = scm_url + f'/api/deployments/{deployment_id}/log'

response = requests.get(log_url, headers=headers,
verify=not should_disable_connection_verify(),
timeout=15)

if response.status_code != 200:
return None

log_entries = response.json()
if not isinstance(log_entries, list):
return None

results = []
for entry in log_entries:
message = (entry.get('message') or '').rstrip()
log_time = entry.get('log_time')
entry_id = entry.get('id') or log_time or message
details_url = entry.get('details_url')

detail_items = []
if details_url:
try:
detail_response = requests.get(details_url, headers=headers,
verify=not should_disable_connection_verify(),
timeout=10)
if detail_response.status_code == 200:
detail_entries = detail_response.json()
if isinstance(detail_entries, list):
for detail in detail_entries:
detail_msg = (detail.get('message') or '').rstrip()
detail_time = detail.get('log_time')
detail_id = detail.get('id')
if detail_msg:
detail_items.append((detail_msg, detail_time, detail_id))
except Exception: # pylint: disable=broad-except
pass

results.append((message, log_time, entry_id, detail_items))

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.

fixed it. Now it remembers which entries are already final ( details_complete_ids ) and only fetch details once per entry. The only one we re-fetch is the very last entry (because it's still being written).


error_text += "Please check the build logs for more info: {}\n".format(deployment_logs)
raise CLIError(error_text)
time.sleep(15)

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.

Fixed. The pause is now 5s during the build and 15s afterward.

Comment on lines +11524 to +11561
def _display_build_logs_on_sync_failure(params, scm_url):
"""For sync deployment failures, fetch build logs and return formatted error text.

This handles the case where the Kudu POST returns an error (e.g., 400) after the build
completed on the server. Without this, the customer only sees 'Status Code: 400' with no
build output.

Returns a formatted error string with logs included, or None if logs can't be fetched.
"""
import requests
from azure.cli.core.util import should_disable_connection_verify
from azure.cli.command_modules.appservice._build_log_formatter import format_build_failure_with_logs

try:
headers = get_scm_site_headers(params.cmd.cli_ctx, params.webapp_name,
params.resource_group_name, params.slot)

# Get the latest deployment ID
latest_url = scm_url + "/api/deployments/latest"
resp = requests.get(latest_url, headers=headers,
verify=not should_disable_connection_verify(), timeout=15)
if resp.status_code != 200:
return None

deployment_info = resp.json()
deployment_id = deployment_info.get('id')
if not deployment_id:
return None

# Fetch build logs
full_logs = _fetch_full_build_logs(params.cmd, params.resource_group_name,
params.webapp_name, params.slot, deployment_id)
if not full_logs:
return None

# Format in the same style as async BuildFailed
error_text = "Deployment failed because the build process failed\n"
return format_build_failure_with_logs(error_text, full_logs)

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.

#33658 - created a new PR for this one, In the PR we get the deployment id from the response

Comment on lines 1100 to 1110
c.argument('enriched_errors', options_list=['--enriched-errors'],
help='If true, deployment failures will show context-enriched diagnostics with error codes, suggested fixes, and Copilot prompts.',
arg_type=get_three_state_flag())
c.argument('build_logs', options_list=['--build-logs'],
help='Controls verbosity of build log output during deployment. '
'"summary" (default): shows phases, milestones, and aggregated warnings. '
'"full": shows all raw build logs. '
'"none": suppresses build logs entirely (auto-expands on failure).',
choices=['full', 'summary', 'none'], default='summary')

with self.argument_context('functionapp deploy') as c:
Comment on lines +9888 to +9892
"""Fetch raw log entries from Kudu deployment log API.

Returns a list of (message, log_time, detail_entries) tuples, or None if fetching fails.
Each detail_entries is a list of (message, log_time) tuples from the details_url.
"""
@yonzhan yonzhan assigned yanzhudd and unassigned zhoxing-ms Jun 14, 2026
@yonzhan

yonzhan commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

webapp


# --- Patterns for classification ---

# Oryx internal metadata lines (hidden in summary mode)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this something we are going to need to continually maintain as we update oryx?

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.

These patterns are just for prettifying the output - If Oryx changes its wording and a pattern stops matching, that line just shows up as normal output instead of a highlighted milestone.


if _should_enrich_errors and response.status_code >= 400:
logger.error("Deployment failed. Visit %s to get more information about your deployment.",
latest_deploymentinfo_url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For this, should we check to see if we are enrichming errors before we raise the CLIError at line 11686?

latest_deploymentinfo_url = scm_url + "/api/deployments/latest"

# For sync deployment failures, fetch and display build logs in the error message
build_failure_text = _display_build_logs_on_sync_failure(params, scm_url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If this fails, do we know for certain that it was the build which failed and not something else?

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.

This PR handles only async deployments. This is not relevant anymore.

For async deployments, if it fails, we display whole logs

@Saipriya-1144 Saipriya-1144 force-pushed the user/vchintalapat/stream-build-logs-deployment branch from bccd2e2 to d01c651 Compare June 26, 2026 14:44
@Saipriya-1144 Saipriya-1144 marked this pull request as ready for review June 26, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants