Skip to content

feat: add azd monitor --tail for in-terminal log streaming#7331

Open
Copilot wants to merge 3 commits intomainfrom
copilot/add-azure-application-logs-support
Open

feat: add azd monitor --tail for in-terminal log streaming#7331
Copilot wants to merge 3 commits intomainfrom
copilot/add-azure-application-logs-support

Conversation

Copy link
Contributor

Copilot AI commented Mar 25, 2026

azd monitor currently only opens Azure Portal URLs in a browser. This adds --tail for direct CLI log streaming from deployed App Service, Azure Functions, and Container Apps resources.

# Stream logs from a deployed service (auto-discovers resources, prompts if multiple)
azd monitor --tail

Changes

  • cmd/monitor.go — New --tail flag. When set, discovers deployed resources via existing resource group enumeration, filters to streamable types (Microsoft.Web/sites, Microsoft.App/containerApps), prompts for selection if multiple found, and streams logs to console.GetWriter() with Ctrl+C handling.

  • pkg/azapi/appservice_logstream.goAzureClient.GetAppServiceLogStream(): connects to Kudu SCM /api/logstream with bearer token auth. Covers both App Service and Functions targets (both are Microsoft.Web/sites).

  • pkg/containerapps/logstream.gocontainerAppService.GetLogStream(): resolves latest revision → lists replicas → reads ReplicaContainer.LogStreamEndpoint from the SDK → authenticates via GetAuthToken → streams response body.

  • pkg/containerapps/container_app.go — Added GetLogStream to the ContainerAppService interface.

Design notes

  • No azure.yaml / project config required — works purely from the provisioned environment, consistent with existing --live/--logs/--overview flags.
  • Container Apps streaming uses the SDK-provided LogStreamEndpoint on ReplicaContainer + the stable GetAuthToken API, avoiding preview API versions.
  • When --tail is not specified, existing behavior (default to --overview) is preserved.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • aka.ms
    • Triggering command: /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd extension source add -n local -t file -l /home/REDACTED/work/azure-dev/azure-dev/cli/azd/extensions/registry.json -lang=go1.26 x_amd64/vet -I -DTNxMp6M -I x_amd64/vet --gdwarf-5 g/protobuf/types-atomic -o x_amd64/vet (dns block)
    • Triggering command: /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd extension list --source local --output json x_amd64/vet -p g/grpc/grpclog -lang=go1.26 x_amd64/vet -I Wxv7f21s0 om/cenkalti/back-ifaceassert x_amd64/vet --gdwarf-5 go -o x_amd64/vet (dns block)
    • Triggering command: /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd extension install azure.ai.agents --source local --version 0.1.18-preview -p t/message -lang=go1.16 x_amd64/vet -I q48QNS-ah pkg/mod/github.c-ifaceassert x_amd64/vet --gdwarf-5 g/protobuf/proto-atomic -o x_amd64/vet (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 26, 2026 00:12
Add support for streaming application logs directly to the terminal for
App Service, Azure Functions, and Container Apps via `azd monitor --tail`.

- App Service/Functions: stream via Kudu SCM /api/logstream endpoint
- Container Apps: stream via replica container LogStreamEndpoint with auth token
- Auto-discovers deployed resources and prompts for selection when multiple found

Agent-Logs-Url: https://github.com/Azure/azure-dev/sessions/293fdb62-c4b1-470c-946f-0cd5395d6bfe

Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support to retrieve application logs from Azure app feat: add azd monitor --tail for in-terminal log streaming Mar 26, 2026
Copilot AI requested a review from spboyer March 26, 2026 00:18
@spboyer spboyer marked this pull request as ready for review March 26, 2026 23:28
@spboyer spboyer requested a review from wbreza as a code owner March 26, 2026 23:28
Copilot AI review requested due to automatic review settings March 26, 2026 23:28
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds azd monitor --tail to stream logs directly in the terminal by discovering deployed resources in the environment and connecting to App Service (Kudu) and Container Apps log streaming endpoints.

Changes:

  • Introduces --tail flag and log streaming workflow (resource discovery, selection prompt, and streaming output) in azd monitor.
  • Adds App Service/Functions log streaming via Kudu /api/logstream.
  • Adds Container Apps log streaming via latest revision → replicas → LogStreamEndpoint + GetAuthToken.

Reviewed changes

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

Show a summary per file
File Description
cli/azd/cmd/monitor.go Adds --tail flag, resource discovery/prompting, and streaming loop.
cli/azd/pkg/azapi/appservice_logstream.go New AzureClient helper to stream logs from Kudu logstream endpoint.
cli/azd/pkg/containerapps/logstream.go New Container Apps log stream implementation via replicas + auth token.
cli/azd/pkg/containerapps/container_app.go Extends ContainerAppService interface with GetLogStream.
cli/azd/cmd/testdata/TestUsage-azd-monitor.snap Updates usage snapshot for new --tail flag and example.
cli/azd/cmd/testdata/TestFigSpec.ts Adds --tail to shell completion spec.
cli/azd/extensions/microsoft.azd.concurx/go.mod Updates indirect dependency versions (go mod tidy-style changes).
cli/azd/extensions/microsoft.azd.concurx/go.sum Updates corresponding checksums for dependency changes.

Comment on lines 116 to +132
@@ -107,13 +126,20 @@ func (m *monitorAction) Run(ctx context.Context) (*actions.ActionResult, error)
}
}

// Handle --tail: stream application logs directly to the terminal
if m.flags.monitorTail {
return m.runTail(ctx)
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

--tail currently takes precedence over --live/--logs/--overview if users pass multiple flags, silently ignoring the browser actions. It would be clearer to validate flag combinations (e.g., return an error when --tail is combined with the portal flags) so the command behavior is unambiguous.

Copilot uses AI. Check for mistakes.
Comment on lines +341 to +351
// isStreamClosedError reports whether the error indicates the log stream
// connection was closed, which is expected during normal termination.
func isStreamClosedError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "connection reset") ||
strings.Contains(msg, "use of closed network connection") ||
strings.Contains(msg, "EOF")
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

isStreamClosedError relies on substring matching (including any "EOF"), which can swallow real errors (e.g., unexpected EOF) and is platform/locale dependent. Prefer checking structured errors (e.g., errors.Is(err, io.EOF), errors.Is(err, net.ErrClosed), and optionally errors.As for *net.OpError / syscall.ECONNRESET) to avoid false positives.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +66
if len(replicasResp.Value) > 0 {
replica := replicasResp.Value[0]
if replica.Properties != nil && len(replica.Properties.Containers) > 0 {
container := replica.Properties.Containers[0]
if container.LogStreamEndpoint != nil {
logStreamEndpoint = *container.LogStreamEndpoint
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

GetLogStream only inspects the first replica and first container when looking for a LogStreamEndpoint. If the first replica/container has no endpoint (or isn’t running) but a later one does, this will incorrectly fail. Iterate all replicas (and containers within each replica) and pick the first non-empty endpoint (or prefer a running/ready replica if available).

Suggested change
if len(replicasResp.Value) > 0 {
replica := replicasResp.Value[0]
if replica.Properties != nil && len(replica.Properties.Containers) > 0 {
container := replica.Properties.Containers[0]
if container.LogStreamEndpoint != nil {
logStreamEndpoint = *container.LogStreamEndpoint
}
}
for _, replica := range replicasResp.Value {
if replica == nil || replica.Properties == nil {
continue
}
for _, container := range replica.Properties.Containers {
if container.LogStreamEndpoint != nil && *container.LogStreamEndpoint != "" {
logStreamEndpoint = *container.LogStreamEndpoint
break
}
}
if logStreamEndpoint != "" {
break
}

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +96
// Connect to the log stream endpoint with the auth token
// Append follow=true and tailLines query parameters for streaming
streamURL := logStreamEndpoint + "&follow=true&tailLines=300"

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

streamURL := logStreamEndpoint + "&follow=true&tailLines=300" assumes the endpoint already contains a query string. If LogStreamEndpoint is ever returned without existing query params, this produces an invalid URL. Consider parsing with net/url and setting follow/tailLines via url.Values so it works regardless of whether ? is present and avoids duplicating params.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +57
//nolint:gosec // URL is constructed from trusted Azure ARM data (SCM hostname)
resp, err := (&http.Client{}).Do(req)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

This issues an HTTP request with (&http.Client{}).Do(req) which bypasses the repo’s configured policy.Transporter/ARM client options (used for user-agent, correlation, proxies, and test recording). Consider using cli.armClientOptions.Transport.Do(req) when available (and setting the User-Agent header consistently) so behavior matches other Azure calls and is mock/record friendly.

Suggested change
//nolint:gosec // URL is constructed from trusted Azure ARM data (SCM hostname)
resp, err := (&http.Client{}).Do(req)
var resp *http.Response
if cli.armClientOptions != nil && cli.armClientOptions.Transport != nil {
// Use the configured transport so that user-agent, correlation, proxies,
// and test recording behave consistently with other Azure calls.
var doErr error
resp, doErr = cli.armClientOptions.Transport.Do(req)
err = doErr
} else {
// Fallback to a direct HTTP client when no transport is configured.
//nolint:gosec // URL is constructed from trusted Azure ARM data (SCM hostname)
resp, err = (&http.Client{}).Do(req)
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support to easily retrieve application logs from a running Azure application

3 participants