feat: add azd monitor --tail for in-terminal log streaming#7331
feat: add azd monitor --tail for in-terminal log streaming#7331
azd monitor --tail for in-terminal log streaming#7331Conversation
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>
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>
azd monitor --tail for in-terminal log streaming
There was a problem hiding this comment.
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
--tailflag and log streaming workflow (resource discovery, selection prompt, and streaming output) inazd 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. |
| @@ -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) | |||
| } | |||
There was a problem hiding this comment.
--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.
| // 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") | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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).
| 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 | |
| } |
| // 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" | ||
|
|
There was a problem hiding this comment.
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.
| //nolint:gosec // URL is constructed from trusted Azure ARM data (SCM hostname) | ||
| resp, err := (&http.Client{}).Do(req) |
There was a problem hiding this comment.
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.
| //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) | |
| } |
azd monitorcurrently only opens Azure Portal URLs in a browser. This adds--tailfor 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 --tailChanges
cmd/monitor.go— New--tailflag. 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 toconsole.GetWriter()with Ctrl+C handling.pkg/azapi/appservice_logstream.go—AzureClient.GetAppServiceLogStream(): connects to Kudu SCM/api/logstreamwith bearer token auth. Covers both App Service and Functions targets (both areMicrosoft.Web/sites).pkg/containerapps/logstream.go—containerAppService.GetLogStream(): resolves latest revision → lists replicas → readsReplicaContainer.LogStreamEndpointfrom the SDK → authenticates viaGetAuthToken→ streams response body.pkg/containerapps/container_app.go— AddedGetLogStreamto theContainerAppServiceinterface.Design notes
azure.yaml/ project config required — works purely from the provisioned environment, consistent with existing--live/--logs/--overviewflags.LogStreamEndpointonReplicaContainer+ the stableGetAuthTokenAPI, avoiding preview API versions.--tailis 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/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)/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)/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.