Skip to content

Commit df10e4d

Browse files
committed
Monitoring updates to match replat
Signed-off-by: trangevi <trangevi@microsoft.com>
1 parent 5822d68 commit df10e4d

File tree

2 files changed

+159
-21
lines changed

2 files changed

+159
-21
lines changed

cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11+
"io"
12+
"os"
13+
"strconv"
1114

1215
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
1316
"github.com/spf13/cobra"
@@ -18,6 +21,7 @@ type monitorFlags struct {
1821
projectName string
1922
name string
2023
version string
24+
sessionID string
2125
follow bool
2226
tail int
2327
logType string
@@ -34,23 +38,24 @@ func newMonitorCommand() *cobra.Command {
3438

3539
cmd := &cobra.Command{
3640
Use: "monitor",
37-
Short: "Monitor logs from a hosted agent container.",
38-
Long: `Monitor logs from a hosted agent container.
41+
Short: "Monitor logs from a hosted agent.",
42+
Long: `Monitor logs from a hosted agent.
3943
40-
Streams console output (stdout/stderr) or system events from an agent container.
44+
Streams console output (stdout/stderr) or system events from an agent session or container.
45+
Use --session to stream logs for a specific session, or omit it to use the container logstream.
4146
Use --follow to stream logs in real-time, or omit it to fetch recent logs and exit.
4247
This is useful for troubleshooting agent startup issues or monitoring agent behavior.`,
43-
Example: ` # Fetch the last 50 lines of console logs
44-
azd ai agent monitor --name my-agent --version 1
48+
Example: ` # Stream session logs
49+
azd ai agent monitor --name my-agent --version 1 --session <session-id>
4550
46-
# Stream console logs in real-time
47-
azd ai agent monitor --name my-agent --version 1 --follow
51+
# Stream session logs in real-time
52+
azd ai agent monitor --name my-agent --version 1 --session <session-id> --follow
4853
49-
# Fetch system event logs
50-
azd ai agent monitor --name my-agent --version 1 --type system
54+
# Fetch container console logs (legacy)
55+
azd ai agent monitor --name my-agent --version 1
5156
52-
# Fetch last 100 lines with explicit account
53-
azd ai agent monitor --name my-agent --version 1 --tail 100 --account-name myAccount --project-name myProject`,
57+
# Fetch system event logs from container (legacy)
58+
azd ai agent monitor --name my-agent --version 1 --type system`,
5459
RunE: func(cmd *cobra.Command, args []string) error {
5560
if err := validateMonitorFlags(flags); err != nil {
5661
return err
@@ -64,6 +69,14 @@ This is useful for troubleshooting agent startup issues or monitoring agent beha
6469
return err
6570
}
6671

72+
// When vnext is enabled, resolve session ID for session-based logstream.
73+
if flags.sessionID == "" {
74+
sessionID, vnext := resolveMonitorSession(ctx, flags.name)
75+
if vnext {
76+
flags.sessionID = sessionID
77+
}
78+
}
79+
6780
action := &MonitorAction{
6881
AgentContext: agentContext,
6982
flags: flags,
@@ -77,6 +90,7 @@ This is useful for troubleshooting agent startup issues or monitoring agent beha
7790
cmd.Flags().StringVarP(&flags.projectName, "project-name", "p", "", "AI Foundry project name")
7891
cmd.Flags().StringVarP(&flags.name, "name", "n", "", "Name of the hosted agent (required)")
7992
cmd.Flags().StringVarP(&flags.version, "version", "v", "", "Version of the hosted agent (required)")
93+
cmd.Flags().StringVarP(&flags.sessionID, "session", "s", "", "Session ID to stream logs for")
8094
cmd.Flags().BoolVarP(&flags.follow, "follow", "f", false, "Stream logs in real-time")
8195
cmd.Flags().IntVarP(&flags.tail, "tail", "l", 50, "Number of trailing log lines to fetch (1-300)")
8296
cmd.Flags().StringVarP(&flags.logType, "type", "t", "console", "Type of logs: 'console' (stdout/stderr) or 'system' (container events)")
@@ -94,15 +108,28 @@ func (a *MonitorAction) Run(ctx context.Context) error {
94108
return err
95109
}
96110

97-
body, err := agentClient.GetAgentContainerLogStream(
98-
ctx,
99-
a.Name,
100-
a.Version,
101-
DefaultAgentAPIVersion,
102-
a.flags.logType,
103-
a.flags.tail,
104-
a.flags.follow,
105-
)
111+
var body io.ReadCloser
112+
if a.flags.sessionID != "" {
113+
fmt.Fprintf(os.Stderr, "Streaming session logs for %s (session: %s)...\n", a.Name, a.flags.sessionID)
114+
body, err = agentClient.GetAgentSessionLogStream(
115+
ctx,
116+
a.Name,
117+
a.Version,
118+
a.flags.sessionID,
119+
"2025-11-15-preview",
120+
a.flags.follow,
121+
)
122+
} else {
123+
body, err = agentClient.GetAgentContainerLogStream(
124+
ctx,
125+
a.Name,
126+
a.Version,
127+
DefaultAgentAPIVersion,
128+
a.flags.logType,
129+
a.flags.tail,
130+
a.flags.follow,
131+
)
132+
}
106133
if err != nil {
107134
// Suppress context deadline/cancellation errors (expected in non-follow timeout and Ctrl+C)
108135
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
@@ -141,3 +168,42 @@ func validateMonitorFlags(flags *monitorFlags) error {
141168

142169
return nil
143170
}
171+
172+
// resolveMonitorSession checks if vnext is enabled and resolves the session ID
173+
// from the .foundry-agent.json file. Returns the session ID and whether vnext is enabled.
174+
// If vnext is not enabled or the session cannot be resolved, returns empty string and false.
175+
func resolveMonitorSession(ctx context.Context, agentName string) (string, bool) {
176+
azdClient, err := azdext.NewAzdClient()
177+
if err != nil {
178+
return "", false
179+
}
180+
defer azdClient.Close()
181+
182+
// Check if vnext is enabled
183+
vnextValue := ""
184+
azdEnv, err := loadAzdEnvironment(ctx, azdClient)
185+
if err == nil {
186+
vnextValue = azdEnv["enableHostedAgentVNext"]
187+
}
188+
if vnextValue == "" {
189+
vnextValue = os.Getenv("enableHostedAgentVNext")
190+
}
191+
enabled, err := strconv.ParseBool(vnextValue)
192+
if err != nil || !enabled {
193+
return "", false
194+
}
195+
196+
// Resolve session ID from .foundry-agent.json
197+
configPath, err := resolveConfigPath(ctx, azdClient)
198+
if err != nil {
199+
return "", true
200+
}
201+
agentCtx := loadLocalContext(configPath)
202+
if agentCtx.Sessions != nil {
203+
if sid, ok := agentCtx.Sessions[agentName]; ok {
204+
return sid, true
205+
}
206+
}
207+
208+
return "", true
209+
}

cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ func (c *AgentClient) GetAgentContainerLogStream(
802802
query.Set("api-version", apiVersion)
803803
query.Set("kind", kind)
804804
query.Set("tail", strconv.Itoa(tail))
805+
query.Set("follow", strconv.FormatBool(follow))
805806
u.RawQuery = query.Encode()
806807

807808
requestURL := u.String()
@@ -845,11 +846,12 @@ func (c *AgentClient) GetAgentContainerLogStream(
845846
}
846847

847848
if resp.StatusCode != http.StatusOK {
849+
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
848850
_ = resp.Body.Close()
849851
if cancel != nil {
850852
cancel()
851853
}
852-
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
854+
return nil, fmt.Errorf("unexpected status code: %d — %s", resp.StatusCode, string(body))
853855
}
854856

855857
// Wrap the body to cancel the context timeout when closed.
@@ -871,6 +873,76 @@ func (r *cancelOnCloseReader) Close() error {
871873
return r.ReadCloser.Close()
872874
}
873875

876+
// GetAgentSessionLogStream streams logs from an agent session.
877+
// This uses the session-based logstream endpoint for vnext agent configurations.
878+
func (c *AgentClient) GetAgentSessionLogStream(
879+
ctx context.Context,
880+
agentName, agentVersion, sessionID, apiVersion string,
881+
follow bool,
882+
) (io.ReadCloser, error) {
883+
u, err := url.Parse(c.endpoint)
884+
if err != nil {
885+
return nil, fmt.Errorf("invalid endpoint URL: %w", err)
886+
}
887+
888+
u.Path += fmt.Sprintf("/agents/%s/versions/%s/sessions/%s:logstream", agentName, agentVersion, sessionID)
889+
890+
query := u.Query()
891+
query.Set("api-version", apiVersion)
892+
query.Set("follow", strconv.FormatBool(follow))
893+
u.RawQuery = query.Encode()
894+
895+
requestURL := u.String()
896+
token, err := c.credential.GetToken(ctx, policy.TokenRequestOptions{
897+
Scopes: []string{"https://ai.azure.com/.default"},
898+
})
899+
if err != nil {
900+
return nil, fmt.Errorf("failed to get auth token: %w", err)
901+
}
902+
903+
requestCtx := ctx
904+
var cancel context.CancelFunc
905+
if !follow {
906+
requestCtx, cancel = context.WithTimeout(ctx, 5*time.Second)
907+
}
908+
909+
req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, requestURL, nil)
910+
if err != nil {
911+
if cancel != nil {
912+
cancel()
913+
}
914+
return nil, fmt.Errorf("failed to create request: %w", err)
915+
}
916+
917+
req.Header.Set("Authorization", "Bearer "+token.Token)
918+
req.Header.Set("User-Agent", fmt.Sprintf("azd-ext-azure-ai-agents/%s", version.Version))
919+
920+
httpClient := &http.Client{}
921+
//nolint:gosec // request URL is built from trusted SDK endpoint + path components
922+
resp, err := httpClient.Do(req)
923+
if err != nil {
924+
if cancel != nil {
925+
cancel()
926+
}
927+
return nil, fmt.Errorf("HTTP request failed: %w", err)
928+
}
929+
930+
if resp.StatusCode != http.StatusOK {
931+
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
932+
_ = resp.Body.Close()
933+
if cancel != nil {
934+
cancel()
935+
}
936+
return nil, fmt.Errorf("unexpected status code: %d — %s", resp.StatusCode, string(body))
937+
}
938+
939+
if cancel != nil {
940+
return &cancelOnCloseReader{ReadCloser: resp.Body, cancel: cancel}, nil
941+
}
942+
943+
return resp.Body, nil
944+
}
945+
874946
// GetAgentContainerOperation retrieves the status of a container operation
875947
func (c *AgentClient) GetAgentContainerOperation(ctx context.Context, agentName, operationID, apiVersion string) (*AgentContainerOperationObject, error) {
876948
url := fmt.Sprintf("%s/agents/%s/operations/%s?api-version=%s", c.endpoint, agentName, operationID, apiVersion)

0 commit comments

Comments
 (0)