From dd4e3b962faf36193436a0a07d621b8d35f40a52 Mon Sep 17 00:00:00 2001 From: huimiu Date: Thu, 16 Jul 2026 16:35:04 +0800 Subject: [PATCH 1/6] feat: record hosted agent deployment mode --- cli/azd/cmd/telemetry_test.go | 7 ++++ .../internal/project/service_target_agent.go | 20 +++++++++++ .../project/service_target_agent_test.go | 33 +++++++++++++++++++ cli/azd/internal/tracing/fields/fields.go | 6 ++++ docs/reference/telemetry-data.md | 6 ++++ .../metrics-audit/feature-telemetry-matrix.md | 3 +- docs/specs/metrics-audit/telemetry-schema.md | 6 ++++ 7 files changed, 80 insertions(+), 1 deletion(-) diff --git a/cli/azd/cmd/telemetry_test.go b/cli/azd/cmd/telemetry_test.go index 18328955e78..8a86f70f2a6 100644 --- a/cli/azd/cmd/telemetry_test.go +++ b/cli/azd/cmd/telemetry_test.go @@ -83,6 +83,13 @@ func TestTelemetryFieldConstants(t *testing.T) { } }) + t.Run("HostedAgentFields", func(t *testing.T) { + t.Parallel() + kv := fields.AgentDeploymentModeKey.String("code") + require.Equal(t, "agent.deploy.mode", string(kv.Key)) + require.Equal(t, "code", kv.Value.AsString()) + }) + // Tool command telemetry fields t.Run("ToolFields", func(t *testing.T) { t.Parallel() diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index 610a6d46e22..965ce53303e 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -40,11 +40,21 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/fatih/color" "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/types/known/structpb" ) // Reference implementation +const agentDeploymentModeKey = "agent.deploy.mode" + +const ( + agentDeploymentModeCode = "code" + agentDeploymentModeContainer = "container" + agentDeploymentModePrebuilt = "prebuilt" +) + // displayableProtocolEntry defines a protocol that produces user-visible invocation endpoints. type displayableProtocolEntry struct { Protocol agent_api.AgentProtocol @@ -513,6 +523,7 @@ func (p *AgentServiceTargetProvider) Package( } // Code deploy: ZIP the source directory if p.isCodeDeployAgent() { + recordAgentDeploymentMode(ctx, agentDeploymentModeCode) progress("Packaging code") zipPath, sha256Hex, err := p.packageCodeDeploy(ctx, serviceConfig) if err != nil { @@ -547,6 +558,7 @@ func (p *AgentServiceTargetProvider) Package( return nil, err } if usePreBuiltImage { + recordAgentDeploymentMode(ctx, agentDeploymentModePrebuilt) progress("Using pre-built container image, skipping package") return &azdext.ServicePackageResult{ Artifacts: []*azdext.Artifact{preBuiltImageArtifact(agentDef.Image)}, @@ -556,6 +568,7 @@ func (p *AgentServiceTargetProvider) Package( var packageArtifact *azdext.Artifact var newArtifacts []*azdext.Artifact + recordAgentDeploymentMode(ctx, agentDeploymentModeContainer) progress("Packaging container") for _, artifact := range serviceContext.Package { if artifact.Kind == azdext.ArtifactKind_ARTIFACT_KIND_CONTAINER { @@ -617,6 +630,7 @@ func (p *AgentServiceTargetProvider) Publish( // Pre-built image: nothing to package or push. Skip deploy-context // resolution so this path stays cheap and doesn't require agent.yaml. if preBuiltArtifact := findPreBuiltImageArtifact(serviceContext.Package); preBuiltArtifact != nil { + recordAgentDeploymentMode(ctx, agentDeploymentModePrebuilt) progress("Using pre-built container image, skipping publish") return &azdext.ServicePublishResult{ Artifacts: []*azdext.Artifact{preBuiltArtifact}, @@ -628,6 +642,7 @@ func (p *AgentServiceTargetProvider) Publish( } // Code deploy skips Publish (no ACR needed) if p.isCodeDeployAgent() { + recordAgentDeploymentMode(ctx, agentDeploymentModeCode) return &azdext.ServicePublishResult{}, nil } @@ -639,6 +654,7 @@ func (p *AgentServiceTargetProvider) Publish( return &azdext.ServicePublishResult{}, nil } + recordAgentDeploymentMode(ctx, agentDeploymentModeContainer) progress("Publishing container") publishResponse, err := p.azdClient. Container(). @@ -656,6 +672,10 @@ func (p *AgentServiceTargetProvider) Publish( }, nil } +func recordAgentDeploymentMode(ctx context.Context, mode string) { + trace.SpanFromContext(ctx).SetAttributes(attribute.String(agentDeploymentModeKey, mode)) +} + func classifyContainerPublishError(err error) error { if isPrivateACRNetworkAccessError(err) { return exterrors.Dependency( diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go index af7f0f8cc3a..a3d90f2e150 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go @@ -21,6 +21,9 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -72,6 +75,36 @@ type fakeProjectAgentChecker struct { err error } +func TestRecordAgentDeploymentMode(t *testing.T) { + t.Parallel() + + recorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) + t.Cleanup(func() { + require.NoError(t, provider.Shutdown(context.WithoutCancel(t.Context()))) + }) + + for _, mode := range []string{ + agentDeploymentModeCode, + agentDeploymentModeContainer, + agentDeploymentModePrebuilt, + } { + t.Run(mode, func(t *testing.T) { + ctx, span := provider.Tracer("test").Start(t.Context(), "test") + recordAgentDeploymentMode(ctx, mode) + span.End() + + spans := recorder.Ended() + require.NotEmpty(t, spans) + require.Contains( + t, + spans[len(spans)-1].Attributes(), + attribute.String(agentDeploymentModeKey, mode), + ) + }) + } +} + func (f fakeProjectAgentChecker) GetAgent( context.Context, string, diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index de360f23974..641bb3ae351 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -202,6 +202,12 @@ var ( Classification: SystemMetadata, Purpose: PerformanceAndHealth, } + // The deployment mode selected for a hosted agent. + AgentDeploymentModeKey = AttributeKey{ + Key: attribute.Key("agent.deploy.mode"), + Classification: SystemMetadata, + Purpose: FeatureInsight, + } ) // Platform related attributes for integrations like devcenter / ADE diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index 47f001122e0..dd941ce5d91 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -180,6 +180,12 @@ These are set once at process startup and attached to **every** span. | `project.service.language` | string | ❌ | Language of specific service being executed — see [Service Languages](#service-languages) | | `platform.type` | string | ❌ | Platform integration (e.g., `aca`, `aks`) | +### Hosted Agent Fields + +| Field Key | Type | Hashed? | Description | +|-----------|------|---------|-------------| +| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, or `prebuilt` | + #### Service Targets Valid values for `project.service.hosts` and `project.service.targets`: diff --git a/docs/specs/metrics-audit/feature-telemetry-matrix.md b/docs/specs/metrics-audit/feature-telemetry-matrix.md index 5e9fea1358a..15c24cccec1 100644 --- a/docs/specs/metrics-audit/feature-telemetry-matrix.md +++ b/docs/specs/metrics-audit/feature-telemetry-matrix.md @@ -35,7 +35,7 @@ These commands emit attributes or events beyond the global middleware span. | `tool install` / `tool upgrade` / `tool uninstall` / `tool check` / `tool list` / `tool show` | `tool.id`, `tool.ids`, `tool.dry_run`, `tool.install.strategy`, `tool.install.success`, `tool.install.success_count`, `tool.install.failure_count`, `tool.install.failed_ids`, `tool.install.duration_ms`, `tool.upgrade.from_version`, `tool.upgrade.to_version`, `tool.check.updates_available` | Comprehensive coverage in `cli/azd/cmd/tool.go`; install/upgrade emit `tools.pack.build` spans for pack-based tools | | `copilot` (agent) | `copilot.initialize` event (model + reasoning config), `copilot.session` event (session create/resume) | Emitted from `internal/agent/copilot_agent.go`; covers the experimental copilot agent surface | | `provision` | `validation.provision` event (provision validation outcome + 6 fields), 8 `arm.*` events (subscription / resource-group deploy / stack-deploy / what-if / validate), `aks.postprovision.skip`, per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) when multi-layer infra is used | Telemetry added across `internal/cmd/provision_*.go` and the ARM deployment client | -| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services | Per-service-target instrumentation; container events emitted from container-app and ACR push paths | +| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services, `agent.deploy.mode` for hosted agents | Per-service-target instrumentation; hosted-agent mode distinguishes code, container, and prebuilt-image deployments | | `hooks run` (and all hook-running commands) | `hooks.exec` event with `hooks.name` (hashed unless built-in lifecycle name), `hooks.type` (project / service / **layer**), `hooks.kind` (script runtime — `sh` / `pwsh` / `js` / `ts` / `python` / `dotnet`) | `hooks.type=layer` was added with multi-layer provision; pre/post is encoded in `hooks.name` (e.g., `prebuild` / `postbuild`); emitted from the hooks runner on every lifecycle command | ## Full Inventory Matrix @@ -161,6 +161,7 @@ privacy review covers every emission point. | **Multi-layer provision** | `provision` (when `infra.layers[]` is configured in `azure.yaml`) | (none — enriches the `provision` span) | `provision.layer.count`, `provision.layer.max_parallel`, `provision.layer.safe_fallback_count`, `provision.layer.explicit_dependson_count` | All four are integer measurements emitted from `internal/cmd/provision_graph.go`; no per-layer duration or outcome attribute is emitted | | **Execution graph (scheduler)** | `up`, `provision`, `deploy`, `package`, `publish`, `down` | `exegraph.run`, `exegraph.step` | `exegraph.step.count`, `exegraph.max_concurrency`, `exegraph.error_policy`, `exegraph.step.name` (hashed), `exegraph.step.deps` (hashed slice), `exegraph.step.tags` (raw — hardcoded literals only), `exegraph.step.timeout_s` | Step names embed user-defined service / layer names from `azure.yaml`; both `name` and `deps` use `fields.StringHashed` / `fields.StringSliceHashed` | | **Container lifecycle** | `package`, `deploy` (container service targets) | `container.credentials`, `container.publish`, `container.remotebuild` | `container.publish` sets `container.remotebuild` (bool) only; `container.credentials` and `container.remotebuild` set no attributes (span status carries success/failure and duration) | The hashed `pack.builder.image` / `pack.builder.tag` attributes are emitted on the separate `tools.pack.build` span, not the `container.*` spans | +| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `prebuilt`) | The mode is determined from the loaded agent definition and selected image path | | **App Service deploy** | `deploy`, `publish` (App Service targets) | `deploy.appservice.zip` | `deploy.appservice.linux` (bool), `deploy.appservice.attempt` (retry attempt number) | Zip-deploy path only; outcome / duration are carried by the span status and span timing, not by dedicated attributes | | **AKS service target** | `provision` (AKS preprovision/postprovision) | `aks.postprovision.skip` | Skip reason | Recorded when cluster is not yet available for context setup | | **Agent troubleshoot middleware** | Triggered on command failure when troubleshooting is engaged | `agent.troubleshoot` | Error chain attributes, hashed error fields | Emitted from `cmd/middleware/error.go` | diff --git a/docs/specs/metrics-audit/telemetry-schema.md b/docs/specs/metrics-audit/telemetry-schema.md index d4bd317c0a2..759755be78e 100644 --- a/docs/specs/metrics-audit/telemetry-schema.md +++ b/docs/specs/metrics-audit/telemetry-schema.md @@ -89,6 +89,12 @@ These are set once at process startup via `resource.New()` and attached to every | Service language | `project.service.language` | SystemMetadata | PerformanceAndHealth | Single service language | | Platform type | `platform.type` | SystemMetadata | FeatureInsight | e.g. `aca`, `aks` | +### Hosted Agent Deployment + +| Field | OTel Key | Classification | Purpose | Notes | +|-------|----------|----------------|---------|-------| +| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, or `prebuilt` | + ### Config and Environment | Field | OTel Key | Classification | Purpose | Notes | From c79b14edfbd2b67d5ff5080f600a112d310ca945 Mon Sep 17 00:00:00 2001 From: huimiu Date: Thu, 16 Jul 2026 17:59:01 +0800 Subject: [PATCH 2/6] feat: distinguish prebuilt agent registries --- .../internal/project/service_target_agent.go | 16 ++++++-- .../project/service_target_agent_test.go | 40 ++++++++++++++++++- docs/reference/telemetry-data.md | 2 +- .../metrics-audit/feature-telemetry-matrix.md | 4 +- docs/specs/metrics-audit/telemetry-schema.md | 2 +- 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index 965ce53303e..e102f80ac91 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -52,7 +52,8 @@ const agentDeploymentModeKey = "agent.deploy.mode" const ( agentDeploymentModeCode = "code" agentDeploymentModeContainer = "container" - agentDeploymentModePrebuilt = "prebuilt" + agentDeploymentModeACR = "prebuilt_acr" + agentDeploymentModeImage = "prebuilt_image" ) // displayableProtocolEntry defines a protocol that produces user-visible invocation endpoints. @@ -558,7 +559,7 @@ func (p *AgentServiceTargetProvider) Package( return nil, err } if usePreBuiltImage { - recordAgentDeploymentMode(ctx, agentDeploymentModePrebuilt) + recordAgentDeploymentMode(ctx, agentDeploymentModeForPreBuiltImage(agentDef.Image)) progress("Using pre-built container image, skipping package") return &azdext.ServicePackageResult{ Artifacts: []*azdext.Artifact{preBuiltImageArtifact(agentDef.Image)}, @@ -630,7 +631,7 @@ func (p *AgentServiceTargetProvider) Publish( // Pre-built image: nothing to package or push. Skip deploy-context // resolution so this path stays cheap and doesn't require agent.yaml. if preBuiltArtifact := findPreBuiltImageArtifact(serviceContext.Package); preBuiltArtifact != nil { - recordAgentDeploymentMode(ctx, agentDeploymentModePrebuilt) + recordAgentDeploymentMode(ctx, agentDeploymentModeForPreBuiltImage(preBuiltArtifact.Location)) progress("Using pre-built container image, skipping publish") return &azdext.ServicePublishResult{ Artifacts: []*azdext.Artifact{preBuiltArtifact}, @@ -676,6 +677,15 @@ func recordAgentDeploymentMode(ctx context.Context, mode string) { trace.SpanFromContext(ctx).SetAttributes(attribute.String(agentDeploymentModeKey, mode)) } +func agentDeploymentModeForPreBuiltImage(image string) string { + registry, _, _ := strings.Cut(image, "/") + if strings.HasSuffix(strings.ToLower(registry), ".azurecr.io") { + return agentDeploymentModeACR + } + + return agentDeploymentModeImage +} + func classifyContainerPublishError(err error) error { if isPrivateACRNetworkAccessError(err) { return exterrors.Dependency( diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go index a3d90f2e150..ddfa65883a4 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go @@ -87,7 +87,8 @@ func TestRecordAgentDeploymentMode(t *testing.T) { for _, mode := range []string{ agentDeploymentModeCode, agentDeploymentModeContainer, - agentDeploymentModePrebuilt, + agentDeploymentModeACR, + agentDeploymentModeImage, } { t.Run(mode, func(t *testing.T) { ctx, span := provider.Tracer("test").Start(t.Context(), "test") @@ -105,6 +106,43 @@ func TestRecordAgentDeploymentMode(t *testing.T) { } } +func TestAgentDeploymentModeForPreBuiltImage(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + image string + expected string + }{ + { + name: "Azure Container Registry", + image: "myregistry.azurecr.io/agent:v1", + expected: agentDeploymentModeACR, + }, + { + name: "Azure Container Registry case insensitive", + image: "myregistry.AZURECR.IO/agent:v1", + expected: agentDeploymentModeACR, + }, + { + name: "other registry", + image: "ghcr.io/contoso/agent:v1", + expected: agentDeploymentModeImage, + }, + { + name: "registry omitted", + image: "agent:v1", + expected: agentDeploymentModeImage, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, agentDeploymentModeForPreBuiltImage(tt.image)) + }) + } +} + func (f fakeProjectAgentChecker) GetAgent( context.Context, string, diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index dd941ce5d91..11620079316 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -184,7 +184,7 @@ These are set once at process startup and attached to **every** span. | Field Key | Type | Hashed? | Description | |-----------|------|---------|-------------| -| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, or `prebuilt` | +| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, `prebuilt_acr`, or `prebuilt_image` | #### Service Targets diff --git a/docs/specs/metrics-audit/feature-telemetry-matrix.md b/docs/specs/metrics-audit/feature-telemetry-matrix.md index 15c24cccec1..05e6da233c8 100644 --- a/docs/specs/metrics-audit/feature-telemetry-matrix.md +++ b/docs/specs/metrics-audit/feature-telemetry-matrix.md @@ -35,7 +35,7 @@ These commands emit attributes or events beyond the global middleware span. | `tool install` / `tool upgrade` / `tool uninstall` / `tool check` / `tool list` / `tool show` | `tool.id`, `tool.ids`, `tool.dry_run`, `tool.install.strategy`, `tool.install.success`, `tool.install.success_count`, `tool.install.failure_count`, `tool.install.failed_ids`, `tool.install.duration_ms`, `tool.upgrade.from_version`, `tool.upgrade.to_version`, `tool.check.updates_available` | Comprehensive coverage in `cli/azd/cmd/tool.go`; install/upgrade emit `tools.pack.build` spans for pack-based tools | | `copilot` (agent) | `copilot.initialize` event (model + reasoning config), `copilot.session` event (session create/resume) | Emitted from `internal/agent/copilot_agent.go`; covers the experimental copilot agent surface | | `provision` | `validation.provision` event (provision validation outcome + 6 fields), 8 `arm.*` events (subscription / resource-group deploy / stack-deploy / what-if / validate), `aks.postprovision.skip`, per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) when multi-layer infra is used | Telemetry added across `internal/cmd/provision_*.go` and the ARM deployment client | -| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services, `agent.deploy.mode` for hosted agents | Per-service-target instrumentation; hosted-agent mode distinguishes code, container, and prebuilt-image deployments | +| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services, `agent.deploy.mode` for hosted agents | Per-service-target instrumentation; hosted-agent mode distinguishes code, container, ACR image, and other prebuilt-image deployments | | `hooks run` (and all hook-running commands) | `hooks.exec` event with `hooks.name` (hashed unless built-in lifecycle name), `hooks.type` (project / service / **layer**), `hooks.kind` (script runtime — `sh` / `pwsh` / `js` / `ts` / `python` / `dotnet`) | `hooks.type=layer` was added with multi-layer provision; pre/post is encoded in `hooks.name` (e.g., `prebuild` / `postbuild`); emitted from the hooks runner on every lifecycle command | ## Full Inventory Matrix @@ -161,7 +161,7 @@ privacy review covers every emission point. | **Multi-layer provision** | `provision` (when `infra.layers[]` is configured in `azure.yaml`) | (none — enriches the `provision` span) | `provision.layer.count`, `provision.layer.max_parallel`, `provision.layer.safe_fallback_count`, `provision.layer.explicit_dependson_count` | All four are integer measurements emitted from `internal/cmd/provision_graph.go`; no per-layer duration or outcome attribute is emitted | | **Execution graph (scheduler)** | `up`, `provision`, `deploy`, `package`, `publish`, `down` | `exegraph.run`, `exegraph.step` | `exegraph.step.count`, `exegraph.max_concurrency`, `exegraph.error_policy`, `exegraph.step.name` (hashed), `exegraph.step.deps` (hashed slice), `exegraph.step.tags` (raw — hardcoded literals only), `exegraph.step.timeout_s` | Step names embed user-defined service / layer names from `azure.yaml`; both `name` and `deps` use `fields.StringHashed` / `fields.StringSliceHashed` | | **Container lifecycle** | `package`, `deploy` (container service targets) | `container.credentials`, `container.publish`, `container.remotebuild` | `container.publish` sets `container.remotebuild` (bool) only; `container.credentials` and `container.remotebuild` set no attributes (span status carries success/failure and duration) | The hashed `pack.builder.image` / `pack.builder.tag` attributes are emitted on the separate `tools.pack.build` span, not the `container.*` spans | -| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `prebuilt`) | The mode is determined from the loaded agent definition and selected image path | +| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `prebuilt_acr`, `prebuilt_image`) | The mode is determined from the loaded agent definition and selected image path; ACR is recognized from an `.azurecr.io` registry hostname | | **App Service deploy** | `deploy`, `publish` (App Service targets) | `deploy.appservice.zip` | `deploy.appservice.linux` (bool), `deploy.appservice.attempt` (retry attempt number) | Zip-deploy path only; outcome / duration are carried by the span status and span timing, not by dedicated attributes | | **AKS service target** | `provision` (AKS preprovision/postprovision) | `aks.postprovision.skip` | Skip reason | Recorded when cluster is not yet available for context setup | | **Agent troubleshoot middleware** | Triggered on command failure when troubleshooting is engaged | `agent.troubleshoot` | Error chain attributes, hashed error fields | Emitted from `cmd/middleware/error.go` | diff --git a/docs/specs/metrics-audit/telemetry-schema.md b/docs/specs/metrics-audit/telemetry-schema.md index 759755be78e..c30a750fc10 100644 --- a/docs/specs/metrics-audit/telemetry-schema.md +++ b/docs/specs/metrics-audit/telemetry-schema.md @@ -93,7 +93,7 @@ These are set once at process startup via `resource.New()` and attached to every | Field | OTel Key | Classification | Purpose | Notes | |-------|----------|----------------|---------|-------| -| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, or `prebuilt` | +| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, `prebuilt_acr`, or `prebuilt_image` | ### Config and Environment From d1f3de694d6bb24763da14f693241c25b03e6541 Mon Sep 17 00:00:00 2001 From: huimiu Date: Thu, 16 Jul 2026 18:05:28 +0800 Subject: [PATCH 3/6] feat: label bring-your-own agent images --- .../azure.ai.agents/internal/project/service_target_agent.go | 4 ++-- docs/reference/telemetry-data.md | 2 +- docs/specs/metrics-audit/feature-telemetry-matrix.md | 2 +- docs/specs/metrics-audit/telemetry-schema.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index e102f80ac91..3b6de7a16b8 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -52,8 +52,8 @@ const agentDeploymentModeKey = "agent.deploy.mode" const ( agentDeploymentModeCode = "code" agentDeploymentModeContainer = "container" - agentDeploymentModeACR = "prebuilt_acr" - agentDeploymentModeImage = "prebuilt_image" + agentDeploymentModeACR = "byo_acr" + agentDeploymentModeImage = "byo_image" ) // displayableProtocolEntry defines a protocol that produces user-visible invocation endpoints. diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index 11620079316..053a587d53e 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -184,7 +184,7 @@ These are set once at process startup and attached to **every** span. | Field Key | Type | Hashed? | Description | |-----------|------|---------|-------------| -| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, `prebuilt_acr`, or `prebuilt_image` | +| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, `byo_acr`, or `byo_image` | #### Service Targets diff --git a/docs/specs/metrics-audit/feature-telemetry-matrix.md b/docs/specs/metrics-audit/feature-telemetry-matrix.md index 05e6da233c8..cead54f51ad 100644 --- a/docs/specs/metrics-audit/feature-telemetry-matrix.md +++ b/docs/specs/metrics-audit/feature-telemetry-matrix.md @@ -161,7 +161,7 @@ privacy review covers every emission point. | **Multi-layer provision** | `provision` (when `infra.layers[]` is configured in `azure.yaml`) | (none — enriches the `provision` span) | `provision.layer.count`, `provision.layer.max_parallel`, `provision.layer.safe_fallback_count`, `provision.layer.explicit_dependson_count` | All four are integer measurements emitted from `internal/cmd/provision_graph.go`; no per-layer duration or outcome attribute is emitted | | **Execution graph (scheduler)** | `up`, `provision`, `deploy`, `package`, `publish`, `down` | `exegraph.run`, `exegraph.step` | `exegraph.step.count`, `exegraph.max_concurrency`, `exegraph.error_policy`, `exegraph.step.name` (hashed), `exegraph.step.deps` (hashed slice), `exegraph.step.tags` (raw — hardcoded literals only), `exegraph.step.timeout_s` | Step names embed user-defined service / layer names from `azure.yaml`; both `name` and `deps` use `fields.StringHashed` / `fields.StringSliceHashed` | | **Container lifecycle** | `package`, `deploy` (container service targets) | `container.credentials`, `container.publish`, `container.remotebuild` | `container.publish` sets `container.remotebuild` (bool) only; `container.credentials` and `container.remotebuild` set no attributes (span status carries success/failure and duration) | The hashed `pack.builder.image` / `pack.builder.tag` attributes are emitted on the separate `tools.pack.build` span, not the `container.*` spans | -| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `prebuilt_acr`, `prebuilt_image`) | The mode is determined from the loaded agent definition and selected image path; ACR is recognized from an `.azurecr.io` registry hostname | +| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `byo_acr`, `byo_image`) | The mode is determined from the loaded agent definition and selected image path; ACR is recognized from an `.azurecr.io` registry hostname | | **App Service deploy** | `deploy`, `publish` (App Service targets) | `deploy.appservice.zip` | `deploy.appservice.linux` (bool), `deploy.appservice.attempt` (retry attempt number) | Zip-deploy path only; outcome / duration are carried by the span status and span timing, not by dedicated attributes | | **AKS service target** | `provision` (AKS preprovision/postprovision) | `aks.postprovision.skip` | Skip reason | Recorded when cluster is not yet available for context setup | | **Agent troubleshoot middleware** | Triggered on command failure when troubleshooting is engaged | `agent.troubleshoot` | Error chain attributes, hashed error fields | Emitted from `cmd/middleware/error.go` | diff --git a/docs/specs/metrics-audit/telemetry-schema.md b/docs/specs/metrics-audit/telemetry-schema.md index c30a750fc10..c4a00dc4cfb 100644 --- a/docs/specs/metrics-audit/telemetry-schema.md +++ b/docs/specs/metrics-audit/telemetry-schema.md @@ -93,7 +93,7 @@ These are set once at process startup via `resource.New()` and attached to every | Field | OTel Key | Classification | Purpose | Notes | |-------|----------|----------------|---------|-------| -| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, `prebuilt_acr`, or `prebuilt_image` | +| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, `byo_acr`, or `byo_image` | ### Config and Environment From ea7649ad2edc7999946745edd266bb6199b68532 Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 21 Jul 2026 16:35:09 +0800 Subject: [PATCH 4/6] fix: classify all prebuilt agent images as byo_image --- .../internal/project/service_target_agent.go | 14 +------ .../project/service_target_agent_test.go | 38 ------------------- docs/reference/telemetry-data.md | 2 +- .../metrics-audit/feature-telemetry-matrix.md | 4 +- docs/specs/metrics-audit/telemetry-schema.md | 2 +- 5 files changed, 6 insertions(+), 54 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index 3b6de7a16b8..35947e6b289 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -52,7 +52,6 @@ const agentDeploymentModeKey = "agent.deploy.mode" const ( agentDeploymentModeCode = "code" agentDeploymentModeContainer = "container" - agentDeploymentModeACR = "byo_acr" agentDeploymentModeImage = "byo_image" ) @@ -559,7 +558,7 @@ func (p *AgentServiceTargetProvider) Package( return nil, err } if usePreBuiltImage { - recordAgentDeploymentMode(ctx, agentDeploymentModeForPreBuiltImage(agentDef.Image)) + recordAgentDeploymentMode(ctx, agentDeploymentModeImage) progress("Using pre-built container image, skipping package") return &azdext.ServicePackageResult{ Artifacts: []*azdext.Artifact{preBuiltImageArtifact(agentDef.Image)}, @@ -631,7 +630,7 @@ func (p *AgentServiceTargetProvider) Publish( // Pre-built image: nothing to package or push. Skip deploy-context // resolution so this path stays cheap and doesn't require agent.yaml. if preBuiltArtifact := findPreBuiltImageArtifact(serviceContext.Package); preBuiltArtifact != nil { - recordAgentDeploymentMode(ctx, agentDeploymentModeForPreBuiltImage(preBuiltArtifact.Location)) + recordAgentDeploymentMode(ctx, agentDeploymentModeImage) progress("Using pre-built container image, skipping publish") return &azdext.ServicePublishResult{ Artifacts: []*azdext.Artifact{preBuiltArtifact}, @@ -677,15 +676,6 @@ func recordAgentDeploymentMode(ctx context.Context, mode string) { trace.SpanFromContext(ctx).SetAttributes(attribute.String(agentDeploymentModeKey, mode)) } -func agentDeploymentModeForPreBuiltImage(image string) string { - registry, _, _ := strings.Cut(image, "/") - if strings.HasSuffix(strings.ToLower(registry), ".azurecr.io") { - return agentDeploymentModeACR - } - - return agentDeploymentModeImage -} - func classifyContainerPublishError(err error) error { if isPrivateACRNetworkAccessError(err) { return exterrors.Dependency( diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go index ddfa65883a4..a3e8e72ee8a 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go @@ -87,7 +87,6 @@ func TestRecordAgentDeploymentMode(t *testing.T) { for _, mode := range []string{ agentDeploymentModeCode, agentDeploymentModeContainer, - agentDeploymentModeACR, agentDeploymentModeImage, } { t.Run(mode, func(t *testing.T) { @@ -106,43 +105,6 @@ func TestRecordAgentDeploymentMode(t *testing.T) { } } -func TestAgentDeploymentModeForPreBuiltImage(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - image string - expected string - }{ - { - name: "Azure Container Registry", - image: "myregistry.azurecr.io/agent:v1", - expected: agentDeploymentModeACR, - }, - { - name: "Azure Container Registry case insensitive", - image: "myregistry.AZURECR.IO/agent:v1", - expected: agentDeploymentModeACR, - }, - { - name: "other registry", - image: "ghcr.io/contoso/agent:v1", - expected: agentDeploymentModeImage, - }, - { - name: "registry omitted", - image: "agent:v1", - expected: agentDeploymentModeImage, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, agentDeploymentModeForPreBuiltImage(tt.image)) - }) - } -} - func (f fakeProjectAgentChecker) GetAgent( context.Context, string, diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index 053a587d53e..265c30d9567 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -184,7 +184,7 @@ These are set once at process startup and attached to **every** span. | Field Key | Type | Hashed? | Description | |-----------|------|---------|-------------| -| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, `byo_acr`, or `byo_image` | +| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, or `byo_image` | #### Service Targets diff --git a/docs/specs/metrics-audit/feature-telemetry-matrix.md b/docs/specs/metrics-audit/feature-telemetry-matrix.md index cead54f51ad..316403825f7 100644 --- a/docs/specs/metrics-audit/feature-telemetry-matrix.md +++ b/docs/specs/metrics-audit/feature-telemetry-matrix.md @@ -35,7 +35,7 @@ These commands emit attributes or events beyond the global middleware span. | `tool install` / `tool upgrade` / `tool uninstall` / `tool check` / `tool list` / `tool show` | `tool.id`, `tool.ids`, `tool.dry_run`, `tool.install.strategy`, `tool.install.success`, `tool.install.success_count`, `tool.install.failure_count`, `tool.install.failed_ids`, `tool.install.duration_ms`, `tool.upgrade.from_version`, `tool.upgrade.to_version`, `tool.check.updates_available` | Comprehensive coverage in `cli/azd/cmd/tool.go`; install/upgrade emit `tools.pack.build` spans for pack-based tools | | `copilot` (agent) | `copilot.initialize` event (model + reasoning config), `copilot.session` event (session create/resume) | Emitted from `internal/agent/copilot_agent.go`; covers the experimental copilot agent surface | | `provision` | `validation.provision` event (provision validation outcome + 6 fields), 8 `arm.*` events (subscription / resource-group deploy / stack-deploy / what-if / validate), `aks.postprovision.skip`, per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) when multi-layer infra is used | Telemetry added across `internal/cmd/provision_*.go` and the ARM deployment client | -| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services, `agent.deploy.mode` for hosted agents | Per-service-target instrumentation; hosted-agent mode distinguishes code, container, ACR image, and other prebuilt-image deployments | +| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services, `agent.deploy.mode` for hosted agents | Per-service-target instrumentation; hosted-agent mode distinguishes code, container, and prebuilt-image deployments | | `hooks run` (and all hook-running commands) | `hooks.exec` event with `hooks.name` (hashed unless built-in lifecycle name), `hooks.type` (project / service / **layer**), `hooks.kind` (script runtime — `sh` / `pwsh` / `js` / `ts` / `python` / `dotnet`) | `hooks.type=layer` was added with multi-layer provision; pre/post is encoded in `hooks.name` (e.g., `prebuild` / `postbuild`); emitted from the hooks runner on every lifecycle command | ## Full Inventory Matrix @@ -161,7 +161,7 @@ privacy review covers every emission point. | **Multi-layer provision** | `provision` (when `infra.layers[]` is configured in `azure.yaml`) | (none — enriches the `provision` span) | `provision.layer.count`, `provision.layer.max_parallel`, `provision.layer.safe_fallback_count`, `provision.layer.explicit_dependson_count` | All four are integer measurements emitted from `internal/cmd/provision_graph.go`; no per-layer duration or outcome attribute is emitted | | **Execution graph (scheduler)** | `up`, `provision`, `deploy`, `package`, `publish`, `down` | `exegraph.run`, `exegraph.step` | `exegraph.step.count`, `exegraph.max_concurrency`, `exegraph.error_policy`, `exegraph.step.name` (hashed), `exegraph.step.deps` (hashed slice), `exegraph.step.tags` (raw — hardcoded literals only), `exegraph.step.timeout_s` | Step names embed user-defined service / layer names from `azure.yaml`; both `name` and `deps` use `fields.StringHashed` / `fields.StringSliceHashed` | | **Container lifecycle** | `package`, `deploy` (container service targets) | `container.credentials`, `container.publish`, `container.remotebuild` | `container.publish` sets `container.remotebuild` (bool) only; `container.credentials` and `container.remotebuild` set no attributes (span status carries success/failure and duration) | The hashed `pack.builder.image` / `pack.builder.tag` attributes are emitted on the separate `tools.pack.build` span, not the `container.*` spans | -| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `byo_acr`, `byo_image`) | The mode is determined from the loaded agent definition and selected image path; ACR is recognized from an `.azurecr.io` registry hostname | +| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `byo_image`) | The mode is determined from the loaded agent definition and selected image path | | **App Service deploy** | `deploy`, `publish` (App Service targets) | `deploy.appservice.zip` | `deploy.appservice.linux` (bool), `deploy.appservice.attempt` (retry attempt number) | Zip-deploy path only; outcome / duration are carried by the span status and span timing, not by dedicated attributes | | **AKS service target** | `provision` (AKS preprovision/postprovision) | `aks.postprovision.skip` | Skip reason | Recorded when cluster is not yet available for context setup | | **Agent troubleshoot middleware** | Triggered on command failure when troubleshooting is engaged | `agent.troubleshoot` | Error chain attributes, hashed error fields | Emitted from `cmd/middleware/error.go` | diff --git a/docs/specs/metrics-audit/telemetry-schema.md b/docs/specs/metrics-audit/telemetry-schema.md index c4a00dc4cfb..5cee9f85e8b 100644 --- a/docs/specs/metrics-audit/telemetry-schema.md +++ b/docs/specs/metrics-audit/telemetry-schema.md @@ -93,7 +93,7 @@ These are set once at process startup via `resource.New()` and attached to every | Field | OTel Key | Classification | Purpose | Notes | |-------|----------|----------------|---------|-------| -| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, `byo_acr`, or `byo_image` | +| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, or `byo_image` | ### Config and Environment From 7ac15d8c8109f3a62df74f33acdc3f038848dc69 Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 21 Jul 2026 18:15:16 +0800 Subject: [PATCH 5/6] fix: record hosted agent telemetry in azd core --- .../internal/project/service_target_agent.go | 38 +++--- .../project/service_target_agent_test.go | 53 +++----- .../pkg/project/service_target_external.go | 66 ++++++++++ .../project/service_target_external_test.go | 123 ++++++++++++++++++ 4 files changed, 229 insertions(+), 51 deletions(-) create mode 100644 cli/azd/pkg/project/service_target_external_test.go diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index 35947e6b289..ced57bb1593 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -40,19 +40,16 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/fatih/color" "github.com/google/uuid" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/types/known/structpb" ) // Reference implementation -const agentDeploymentModeKey = "agent.deploy.mode" - const ( - agentDeploymentModeCode = "code" - agentDeploymentModeContainer = "container" - agentDeploymentModeImage = "byo_image" + agentDeploymentModeArtifactMetadataKey = "azure.ai.agents.deploymentMode" + agentDeploymentModeCode = "code" + agentDeploymentModeContainer = "container" + agentDeploymentModeImage = "byo_image" ) // displayableProtocolEntry defines a protocol that produces user-visible invocation endpoints. @@ -523,7 +520,6 @@ func (p *AgentServiceTargetProvider) Package( } // Code deploy: ZIP the source directory if p.isCodeDeployAgent() { - recordAgentDeploymentMode(ctx, agentDeploymentModeCode) progress("Packaging code") zipPath, sha256Hex, err := p.packageCodeDeploy(ctx, serviceConfig) if err != nil { @@ -537,8 +533,9 @@ func (p *AgentServiceTargetProvider) Package( Location: zipPath, LocationKind: azdext.LocationKind_LOCATION_KIND_LOCAL, Metadata: map[string]string{ - "type": "code-zip", - "sha256": sha256Hex, + "type": "code-zip", + "sha256": sha256Hex, + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeCode, }, }, }, @@ -558,7 +555,6 @@ func (p *AgentServiceTargetProvider) Package( return nil, err } if usePreBuiltImage { - recordAgentDeploymentMode(ctx, agentDeploymentModeImage) progress("Using pre-built container image, skipping package") return &azdext.ServicePackageResult{ Artifacts: []*azdext.Artifact{preBuiltImageArtifact(agentDef.Image)}, @@ -568,7 +564,6 @@ func (p *AgentServiceTargetProvider) Package( var packageArtifact *azdext.Artifact var newArtifacts []*azdext.Artifact - recordAgentDeploymentMode(ctx, agentDeploymentModeContainer) progress("Packaging container") for _, artifact := range serviceContext.Package { if artifact.Kind == azdext.ArtifactKind_ARTIFACT_KIND_CONTAINER { @@ -613,6 +608,8 @@ func (p *AgentServiceTargetProvider) Package( newArtifacts = append(newArtifacts, packageResponse.Result.Artifacts...) } + setAgentDeploymentMode(newArtifacts, agentDeploymentModeContainer) + return &azdext.ServicePackageResult{ Artifacts: newArtifacts, }, nil @@ -630,7 +627,6 @@ func (p *AgentServiceTargetProvider) Publish( // Pre-built image: nothing to package or push. Skip deploy-context // resolution so this path stays cheap and doesn't require agent.yaml. if preBuiltArtifact := findPreBuiltImageArtifact(serviceContext.Package); preBuiltArtifact != nil { - recordAgentDeploymentMode(ctx, agentDeploymentModeImage) progress("Using pre-built container image, skipping publish") return &azdext.ServicePublishResult{ Artifacts: []*azdext.Artifact{preBuiltArtifact}, @@ -642,7 +638,6 @@ func (p *AgentServiceTargetProvider) Publish( } // Code deploy skips Publish (no ACR needed) if p.isCodeDeployAgent() { - recordAgentDeploymentMode(ctx, agentDeploymentModeCode) return &azdext.ServicePublishResult{}, nil } @@ -654,7 +649,6 @@ func (p *AgentServiceTargetProvider) Publish( return &azdext.ServicePublishResult{}, nil } - recordAgentDeploymentMode(ctx, agentDeploymentModeContainer) progress("Publishing container") publishResponse, err := p.azdClient. Container(). @@ -667,13 +661,20 @@ func (p *AgentServiceTargetProvider) Publish( return nil, classifyContainerPublishError(err) } + setAgentDeploymentMode(publishResponse.Result.Artifacts, agentDeploymentModeContainer) + return &azdext.ServicePublishResult{ Artifacts: publishResponse.Result.Artifacts, }, nil } -func recordAgentDeploymentMode(ctx context.Context, mode string) { - trace.SpanFromContext(ctx).SetAttributes(attribute.String(agentDeploymentModeKey, mode)) +func setAgentDeploymentMode(artifacts []*azdext.Artifact, mode string) { + for _, artifact := range artifacts { + if artifact.Metadata == nil { + artifact.Metadata = map[string]string{} + } + artifact.Metadata[agentDeploymentModeArtifactMetadataKey] = mode + } } func classifyContainerPublishError(err error) error { @@ -936,7 +937,8 @@ func preBuiltImageArtifact(imageURL string) *azdext.Artifact { Location: imageURL, LocationKind: azdext.LocationKind_LOCATION_KIND_REMOTE, Metadata: map[string]string{ - preBuiltImageArtifactSourceKey: preBuiltImageArtifactSource, + preBuiltImageArtifactSourceKey: preBuiltImageArtifactSource, + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeImage, }, } } diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go index a3e8e72ee8a..598e783d9a6 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go @@ -21,9 +21,6 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/attribute" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/sdk/trace/tracetest" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -75,36 +72,6 @@ type fakeProjectAgentChecker struct { err error } -func TestRecordAgentDeploymentMode(t *testing.T) { - t.Parallel() - - recorder := tracetest.NewSpanRecorder() - provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) - t.Cleanup(func() { - require.NoError(t, provider.Shutdown(context.WithoutCancel(t.Context()))) - }) - - for _, mode := range []string{ - agentDeploymentModeCode, - agentDeploymentModeContainer, - agentDeploymentModeImage, - } { - t.Run(mode, func(t *testing.T) { - ctx, span := provider.Tracer("test").Start(t.Context(), "test") - recordAgentDeploymentMode(ctx, mode) - span.End() - - spans := recorder.Ended() - require.NotEmpty(t, spans) - require.Contains( - t, - spans[len(spans)-1].Attributes(), - attribute.String(agentDeploymentModeKey, mode), - ) - }) - } -} - func (f fakeProjectAgentChecker) GetAgent( context.Context, string, @@ -1181,6 +1148,11 @@ func TestPackage_SkipsWhenPreBuiltImageChosen(t *testing.T) { require.Equal(t, imageURL, result.Artifacts[0].Location) require.Equal(t, azdext.LocationKind_LOCATION_KIND_REMOTE, result.Artifacts[0].LocationKind) require.Equal(t, preBuiltImageArtifactSource, result.Artifacts[0].Metadata[preBuiltImageArtifactSourceKey]) + require.Equal( + t, + agentDeploymentModeImage, + result.Artifacts[0].Metadata[agentDeploymentModeArtifactMetadataKey], + ) require.Contains(t, progressMessages, "Using pre-built container image, skipping package") } @@ -1213,6 +1185,11 @@ func TestPackage_BuildsWhenUserChoseDockerfile(t *testing.T) { require.Equal(t, int32(1), promptStub.selectCalls.Load()) require.Equal(t, int32(1), containerStub.buildCalls.Load()) require.Equal(t, int32(1), containerStub.packageCalls.Load()) + require.Equal( + t, + agentDeploymentModeContainer, + result.Artifacts[0].Metadata[agentDeploymentModeArtifactMetadataKey], + ) } func TestPublish_SkipsWhenPreBuiltImageChosen(t *testing.T) { @@ -1238,6 +1215,11 @@ func TestPublish_SkipsWhenPreBuiltImageChosen(t *testing.T) { require.NotNil(t, result) require.Len(t, result.Artifacts, 1) require.Equal(t, imageURL, result.Artifacts[0].Location) + require.Equal( + t, + agentDeploymentModeImage, + result.Artifacts[0].Metadata[agentDeploymentModeArtifactMetadataKey], + ) require.Contains(t, progressMessages, "Using pre-built container image, skipping publish") } @@ -1272,6 +1254,11 @@ func TestPublish_PublishesWhenPackageBuiltFromDockerfile(t *testing.T) { require.NotNil(t, result) require.NotEmpty(t, result.Artifacts, "expected published container artifacts") require.Equal(t, int32(1), containerStub.publishCalls.Load()) + require.Equal( + t, + agentDeploymentModeContainer, + result.Artifacts[0].Metadata[agentDeploymentModeArtifactMetadataKey], + ) } func TestPublish_PrivateACRNetworkAccessGuidance(t *testing.T) { diff --git a/cli/azd/pkg/project/service_target_external.go b/cli/azd/pkg/project/service_target_external.go index 5f70ead81a9..203d3dc942f 100644 --- a/cli/azd/pkg/project/service_target_external.go +++ b/cli/azd/pkg/project/service_target_external.go @@ -10,6 +10,8 @@ import ( "log" "github.com/azure/azure-dev/cli/azd/internal/mapper" + "github.com/azure/azure-dev/cli/azd/internal/tracing" + "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" "github.com/azure/azure-dev/cli/azd/pkg/async" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/environment" @@ -125,6 +127,8 @@ func (est *ExternalServiceTarget) Publish( return nil, fmt.Errorf("failed to convert publish result: %w", err) } + recordHostedAgentDeploymentMode(ctx, est.extension, result.Artifacts, serviceContext.Package) + return result, nil } @@ -204,6 +208,8 @@ func (est *ExternalServiceTarget) Package( return nil, err } + recordHostedAgentDeploymentMode(ctx, est.extension, convertedResult.Artifacts, serviceContext.Package) + return convertedResult, nil } @@ -412,3 +418,63 @@ func createProgressFunc(progress *async.Progress[ServiceProgress]) func(string) } } } + +const ( + hostedAgentExtensionID = "azure.ai.agents" + agentDeploymentModeArtifactMetadataKey = "azure.ai.agents.deploymentMode" + agentDeploymentModeCode = "code" + agentDeploymentModeContainer = "container" + agentDeploymentModeImage = "byo_image" +) + +func recordHostedAgentDeploymentMode( + ctx context.Context, + extension *extensions.Extension, + resultArtifacts ArtifactCollection, + packageArtifacts ArtifactCollection, +) { + if extension == nil || extension.Id != hostedAgentExtensionID { + return + } + + mode, hasMetadata := hostedAgentDeploymentMode(resultArtifacts) + if !hasMetadata { + mode, _ = hostedAgentDeploymentMode(packageArtifacts) + } + + if mode != "" { + tracing.SetAttributesInContext(ctx, fields.AgentDeploymentModeKey.String(mode)) + } +} + +func hostedAgentDeploymentMode(artifacts ArtifactCollection) (string, bool) { + hasModeMetadata := false + for _, artifact := range artifacts { + if artifact == nil { + continue + } + + mode, ok := artifact.Metadata[agentDeploymentModeArtifactMetadataKey] + if !ok { + continue + } + hasModeMetadata = true + + switch mode { + case agentDeploymentModeCode, agentDeploymentModeContainer, agentDeploymentModeImage: + return mode, true + } + } + + if hasModeMetadata { + return "", true + } + + for _, artifact := range artifacts { + if artifact != nil && artifact.Kind == ArtifactKindContainer { + return agentDeploymentModeContainer, true + } + } + + return "", false +} diff --git a/cli/azd/pkg/project/service_target_external_test.go b/cli/azd/pkg/project/service_target_external_test.go new file mode 100644 index 00000000000..6aa54c1a091 --- /dev/null +++ b/cli/azd/pkg/project/service_target_external_test.go @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package project + +import ( + "context" + "testing" + + "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" + "github.com/azure/azure-dev/cli/azd/pkg/extensions" + "github.com/stretchr/testify/require" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +func TestRecordHostedAgentDeploymentMode(t *testing.T) { + tests := []struct { + name string + extension *extensions.Extension + resultArtifacts ArtifactCollection + packageArtifacts ArtifactCollection + wantMode string + }{ + { + name: "code", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + resultArtifacts: ArtifactCollection{{ + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeCode, + }, + }}, + wantMode: agentDeploymentModeCode, + }, + { + name: "container", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + resultArtifacts: ArtifactCollection{{ + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeContainer, + }, + }}, + wantMode: agentDeploymentModeContainer, + }, + { + name: "bring your own image", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + resultArtifacts: ArtifactCollection{{ + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeImage, + }, + }}, + wantMode: agentDeploymentModeImage, + }, + { + name: "package artifact", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + packageArtifacts: ArtifactCollection{{ + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeImage, + }, + }}, + wantMode: agentDeploymentModeImage, + }, + { + name: "unknown mode", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + resultArtifacts: ArtifactCollection{{ + Kind: ArtifactKindContainer, + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: "unknown", + }, + }}, + }, + { + name: "unknown key", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + resultArtifacts: ArtifactCollection{{ + Metadata: map[string]string{ + "unknown": agentDeploymentModeCode, + }, + }}, + }, + { + name: "other extension", + extension: &extensions.Extension{Id: "other"}, + resultArtifacts: ArtifactCollection{{ + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeCode, + }, + }}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + recorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) + t.Cleanup(func() { + require.NoError(t, provider.Shutdown(context.WithoutCancel(t.Context()))) + }) + + ctx, span := provider.Tracer("test").Start(t.Context(), "exegraph.step") + recordHostedAgentDeploymentMode( + ctx, + tt.extension, + tt.resultArtifacts, + tt.packageArtifacts, + ) + span.End() + + spans := recorder.Ended() + require.Len(t, spans, 1) + + if tt.wantMode == "" { + require.Empty(t, spans[0].Attributes()) + return + } + + require.Contains(t, spans[0].Attributes(), fields.AgentDeploymentModeKey.String(tt.wantMode)) + }) + } +} From 7f6883c94d88c746d27976efc0b253377344db73 Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 21 Jul 2026 21:12:40 +0800 Subject: [PATCH 6/6] fix: classify hosted agent from-package deployments --- .../internal/project/service_target_agent.go | 22 ++++- .../project/service_target_agent_test.go | 95 +++++++++++++++++++ cli/azd/internal/cmd/deploy.go | 3 +- cli/azd/internal/cmd/service_graph.go | 3 + cli/azd/internal/cmd/service_graph_test.go | 22 +++++ cli/azd/pkg/project/artifact.go | 3 + cli/azd/pkg/project/deployment_context.go | 20 ++++ .../pkg/project/service_target_external.go | 32 ++++++- .../project/service_target_external_test.go | 63 ++++++++++++ docs/reference/telemetry-data.md | 4 +- .../metrics-audit/feature-telemetry-matrix.md | 4 +- docs/specs/metrics-audit/telemetry-schema.md | 2 +- 12 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 cli/azd/pkg/project/deployment_context.go diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index ced57bb1593..85a0e92a231 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -50,6 +50,7 @@ const ( agentDeploymentModeCode = "code" agentDeploymentModeContainer = "container" agentDeploymentModeImage = "byo_image" + fromPackageArtifactMetadataKey = "azd.fromPackage" ) // displayableProtocolEntry defines a protocol that produces user-visible invocation endpoints. @@ -661,7 +662,10 @@ func (p *AgentServiceTargetProvider) Publish( return nil, classifyContainerPublishError(err) } - setAgentDeploymentMode(publishResponse.Result.Artifacts, agentDeploymentModeContainer) + setAgentDeploymentMode( + publishResponse.Result.Artifacts, + agentDeploymentModeForPublish(serviceContext.Package), + ) return &azdext.ServicePublishResult{ Artifacts: publishResponse.Result.Artifacts, @@ -677,6 +681,22 @@ func setAgentDeploymentMode(artifacts []*azdext.Artifact, mode string) { } } +func agentDeploymentModeForPublish( + artifacts []*azdext.Artifact, +) string { + for _, artifact := range artifacts { + if artifact == nil || + artifact.Metadata[fromPackageArtifactMetadataKey] != "true" { + continue + } + if artifact.Kind == azdext.ArtifactKind_ARTIFACT_KIND_CONTAINER { + return agentDeploymentModeImage + } + } + + return agentDeploymentModeContainer +} + func classifyContainerPublishError(err error) error { if isPrivateACRNetworkAccessError(err) { return exterrors.Dependency( diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go index 598e783d9a6..1a339bf280e 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go @@ -133,6 +133,25 @@ func writeHostedAgentYAML(t *testing.T, dir string) string { return p } +func writeHostedCodeAgentYAML(t *testing.T, dir string) string { + t.Helper() + p := filepath.Join(dir, "agent.yaml") + content := "kind: hosted\nname: test-agent\n" + + "code_configuration:\n" + + " runtime: python_3_13\n" + + " entry_point: main.py\n" + require.NoError(t, os.WriteFile(p, []byte(content), 0o600)) + require.NoError( + t, + os.WriteFile( + filepath.Join(dir, "main.py"), + []byte("print('hello')\n"), + 0o600, + ), + ) + return p +} + // stubContainerServer is a minimal ContainerServiceServer that returns // success responses for Build, Package, and Publish. type stubContainerServer struct { @@ -1156,6 +1175,41 @@ func TestPackage_SkipsWhenPreBuiltImageChosen(t *testing.T) { require.Contains(t, progressMessages, "Using pre-built container image, skipping package") } +func TestPackage_LabelsCodeDeploy(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + agentPath := writeHostedCodeAgentYAML(t, dir) + provider := &AgentServiceTargetProvider{ + agentDefinitionPath: agentPath, + env: &azdext.Environment{Name: "test-env"}, + } + + result, err := provider.Package( + t.Context(), + &azdext.ServiceConfig{Name: "test-svc"}, + &azdext.ServiceContext{}, + func(string) {}, + ) + + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Artifacts, 1) + t.Cleanup(func() { + require.NoError(t, os.Remove(result.Artifacts[0].Location)) + }) + require.Equal( + t, + azdext.ArtifactKind_ARTIFACT_KIND_ARCHIVE, + result.Artifacts[0].Kind, + ) + require.Equal( + t, + agentDeploymentModeCode, + result.Artifacts[0].Metadata[agentDeploymentModeArtifactMetadataKey], + ) +} + func TestPackage_BuildsWhenUserChoseDockerfile(t *testing.T) { t.Parallel() @@ -1261,6 +1315,47 @@ func TestPublish_PublishesWhenPackageBuiltFromDockerfile(t *testing.T) { ) } +func TestPublish_LabelsFromPackageImage(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + agentPath := writeHostedAgentYAML(t, dir) + containerStub := &stubContainerServer{} + client := newContainerTestClient(t, containerStub) + + provider := &AgentServiceTargetProvider{ + azdClient: client, + agentDefinitionPath: agentPath, + env: &azdext.Environment{Name: "test-env"}, + } + + result, err := provider.Publish( + t.Context(), + &azdext.ServiceConfig{Name: "test-svc"}, + &azdext.ServiceContext{Package: []*azdext.Artifact{{ + Kind: azdext.ArtifactKind_ARTIFACT_KIND_CONTAINER, + Location: "test-image:latest", + LocationKind: azdext.LocationKind_LOCATION_KIND_LOCAL, + Metadata: map[string]string{ + fromPackageArtifactMetadataKey: "true", + }, + }}}, + &azdext.TargetResource{}, + &azdext.PublishOptions{}, + func(string) {}, + ) + + require.NoError(t, err) + require.NotNil(t, result) + require.NotEmpty(t, result.Artifacts) + require.Equal(t, int32(1), containerStub.publishCalls.Load()) + require.Equal( + t, + agentDeploymentModeImage, + result.Artifacts[0].Metadata[agentDeploymentModeArtifactMetadataKey], + ) +} + func TestPublish_PrivateACRNetworkAccessGuidance(t *testing.T) { t.Parallel() diff --git a/cli/azd/internal/cmd/deploy.go b/cli/azd/internal/cmd/deploy.go index e1c5648bd7e..77d7fcbfb0d 100644 --- a/cli/azd/internal/cmd/deploy.go +++ b/cli/azd/internal/cmd/deploy.go @@ -405,7 +405,8 @@ func (da *DeployAction) deployServicesGraph( } err = da.projectConfig.Invoke(ctx, project.ProjectEventDeploy, projectEventArgs, func() error { - result := exegraph.RunWithResult(ctx, g, opts) + deployCtx := project.ContextWithDeploymentOperation(ctx) + result := exegraph.RunWithResult(deployCtx, g, opts) // Log per-step timing for diagnostics and benchmarking. for _, st := range result.Steps { log.Printf("deploy-graph step %-30s %s %s", st.Name, st.Status, st.Duration.Round(time.Millisecond)) diff --git a/cli/azd/internal/cmd/service_graph.go b/cli/azd/internal/cmd/service_graph.go index f8adcbfa3aa..a1c0c13e56f 100644 --- a/cli/azd/internal/cmd/service_graph.go +++ b/cli/azd/internal/cmd/service_graph.go @@ -378,6 +378,9 @@ func addServiceStepsToGraph(g *exegraph.Graph, opts serviceGraphOptions) (*servi Kind: determineArtifactKind(opts.fromPackage), Location: opts.fromPackage, LocationKind: project.LocationKindLocal, + Metadata: map[string]string{ + project.MetadataKeyFromPackage: "true", + }, }); pkgErr != nil { return fmt.Errorf("packaging service %s: %w", pkgSvc.Name, pkgErr) } diff --git a/cli/azd/internal/cmd/service_graph_test.go b/cli/azd/internal/cmd/service_graph_test.go index e56145d3ebe..04548a73dfc 100644 --- a/cli/azd/internal/cmd/service_graph_test.go +++ b/cli/azd/internal/cmd/service_graph_test.go @@ -245,6 +245,28 @@ func TestDeployGraphState_StoreLoadContext(t *testing.T) { require.Same(t, sc, state.LoadContext("svc")) } +func TestServiceGraphFromPackageMarksArtifact(t *testing.T) { + t.Parallel() + + services := []*project.ServiceConfig{{Name: "svc"}} + opts, g := newGraphOpts(services) + opts.fromPackage = "example.azurecr.io/agent:v1" + + _, err := addServiceStepsToGraph(g, opts) + require.NoError(t, err) + require.NoError(t, exegraph.Run(t.Context(), g, exegraph.RunOptions{})) + + serviceContext := opts.state.LoadContext("svc") + require.NotNil(t, serviceContext) + require.Empty(t, serviceContext.Build) + require.Len(t, serviceContext.Package, 1) + require.Equal( + t, + "true", + serviceContext.Package[0].Metadata[project.MetadataKeyFromPackage], + ) +} + // TestBuildGateParallelWithArtifactsPath verifies that when a buildGateKey // is set, deploy steps still execute in PARALLEL at the graph level (no chain // edges). The build-race prevention is handled at runtime via --artifacts-path diff --git a/cli/azd/pkg/project/artifact.go b/cli/azd/pkg/project/artifact.go index a365c629296..933f69f5b42 100644 --- a/cli/azd/pkg/project/artifact.go +++ b/cli/azd/pkg/project/artifact.go @@ -22,6 +22,9 @@ const ( // MetadataKeyNote adds a note line below the artifact output. MetadataKeyNote = "note" + + // MetadataKeyFromPackage marks artifacts supplied by --from-package. + MetadataKeyFromPackage = "azd.fromPackage" ) // ArtifactKind represents well-known artifact types in the Azure Developer CLI diff --git a/cli/azd/pkg/project/deployment_context.go b/cli/azd/pkg/project/deployment_context.go new file mode 100644 index 00000000000..cd8e3f0ec32 --- /dev/null +++ b/cli/azd/pkg/project/deployment_context.go @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package project + +import "context" + +type deploymentOperationContextKey struct{} + +// ContextWithDeploymentOperation marks calls made by azd deploy. +func ContextWithDeploymentOperation(ctx context.Context) context.Context { + return context.WithValue(ctx, deploymentOperationContextKey{}, true) +} + +func isDeploymentOperation(ctx context.Context) bool { + deploymentOperation, _ := ctx.Value( + deploymentOperationContextKey{}, + ).(bool) + return deploymentOperation +} diff --git a/cli/azd/pkg/project/service_target_external.go b/cli/azd/pkg/project/service_target_external.go index 203d3dc942f..05d0268251c 100644 --- a/cli/azd/pkg/project/service_target_external.go +++ b/cli/azd/pkg/project/service_target_external.go @@ -433,7 +433,17 @@ func recordHostedAgentDeploymentMode( resultArtifacts ArtifactCollection, packageArtifacts ArtifactCollection, ) { - if extension == nil || extension.Id != hostedAgentExtensionID { + if extension == nil || extension.Id != hostedAgentExtensionID || + !isDeploymentOperation(ctx) { + return + } + + mode := hostedAgentFromPackageDeploymentMode(packageArtifacts) + if mode != "" { + tracing.SetAttributesInContext( + ctx, + fields.AgentDeploymentModeKey.String(mode), + ) return } @@ -447,6 +457,26 @@ func recordHostedAgentDeploymentMode( } } +func hostedAgentFromPackageDeploymentMode( + artifacts ArtifactCollection, +) string { + for _, artifact := range artifacts { + if artifact == nil || + artifact.Metadata[MetadataKeyFromPackage] != "true" { + continue + } + + switch artifact.Kind { + case ArtifactKindArchive: + return agentDeploymentModeCode + case ArtifactKindContainer: + return agentDeploymentModeImage + } + } + + return "" +} + func hostedAgentDeploymentMode(artifacts ArtifactCollection) (string, bool) { hasModeMetadata := false for _, artifact := range artifacts { diff --git a/cli/azd/pkg/project/service_target_external_test.go b/cli/azd/pkg/project/service_target_external_test.go index 6aa54c1a091..682478a1912 100644 --- a/cli/azd/pkg/project/service_target_external_test.go +++ b/cli/azd/pkg/project/service_target_external_test.go @@ -62,6 +62,33 @@ func TestRecordHostedAgentDeploymentMode(t *testing.T) { }}, wantMode: agentDeploymentModeImage, }, + { + name: "from package code", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + packageArtifacts: ArtifactCollection{{ + Kind: ArtifactKindArchive, + Metadata: map[string]string{ + MetadataKeyFromPackage: "true", + }, + }}, + wantMode: agentDeploymentModeCode, + }, + { + name: "from package image overrides result", + extension: &extensions.Extension{Id: hostedAgentExtensionID}, + resultArtifacts: ArtifactCollection{{ + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeContainer, + }, + }}, + packageArtifacts: ArtifactCollection{{ + Kind: ArtifactKindContainer, + Metadata: map[string]string{ + MetadataKeyFromPackage: "true", + }, + }}, + wantMode: agentDeploymentModeImage, + }, { name: "unknown mode", extension: &extensions.Extension{Id: hostedAgentExtensionID}, @@ -101,6 +128,7 @@ func TestRecordHostedAgentDeploymentMode(t *testing.T) { }) ctx, span := provider.Tracer("test").Start(t.Context(), "exegraph.step") + ctx = ContextWithDeploymentOperation(ctx) recordHostedAgentDeploymentMode( ctx, tt.extension, @@ -121,3 +149,38 @@ func TestRecordHostedAgentDeploymentMode(t *testing.T) { }) } } + +func TestRecordHostedAgentDeploymentModeIgnoresStandalonePublish( + t *testing.T, +) { + recorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider( + sdktrace.WithSpanProcessor(recorder), + ) + t.Cleanup(func() { + require.NoError( + t, + provider.Shutdown(context.WithoutCancel(t.Context())), + ) + }) + + ctx, span := provider.Tracer("test").Start( + t.Context(), + "cmd.publish", + ) + recordHostedAgentDeploymentMode( + ctx, + &extensions.Extension{Id: hostedAgentExtensionID}, + ArtifactCollection{{ + Metadata: map[string]string{ + agentDeploymentModeArtifactMetadataKey: agentDeploymentModeCode, + }, + }}, + nil, + ) + span.End() + + spans := recorder.Ended() + require.Len(t, spans, 1) + require.Empty(t, spans[0].Attributes()) +} diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index 265c30d9567..87897849786 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -184,7 +184,9 @@ These are set once at process startup and attached to **every** span. | Field Key | Type | Hashed? | Description | |-----------|------|---------|-------------| -| `agent.deploy.mode` | string | ❌ | Selected hosted-agent deployment path: `code`, `container`, or `byo_image` | +| `agent.deploy.mode` | string | ❌ | Selected hosted-agent path during `azd deploy`: `code`, `container`, or `byo_image`; recorded on the deploy graph's package or publish step | + +For `azd deploy --from-package`, an archive is classified as `code` and an image reference as `byo_image`. Standalone `azd package` and `azd publish` commands do not record this field. #### Service Targets diff --git a/docs/specs/metrics-audit/feature-telemetry-matrix.md b/docs/specs/metrics-audit/feature-telemetry-matrix.md index 316403825f7..ed12e2edc6a 100644 --- a/docs/specs/metrics-audit/feature-telemetry-matrix.md +++ b/docs/specs/metrics-audit/feature-telemetry-matrix.md @@ -35,7 +35,7 @@ These commands emit attributes or events beyond the global middleware span. | `tool install` / `tool upgrade` / `tool uninstall` / `tool check` / `tool list` / `tool show` | `tool.id`, `tool.ids`, `tool.dry_run`, `tool.install.strategy`, `tool.install.success`, `tool.install.success_count`, `tool.install.failure_count`, `tool.install.failed_ids`, `tool.install.duration_ms`, `tool.upgrade.from_version`, `tool.upgrade.to_version`, `tool.check.updates_available` | Comprehensive coverage in `cli/azd/cmd/tool.go`; install/upgrade emit `tools.pack.build` spans for pack-based tools | | `copilot` (agent) | `copilot.initialize` event (model + reasoning config), `copilot.session` event (session create/resume) | Emitted from `internal/agent/copilot_agent.go`; covers the experimental copilot agent surface | | `provision` | `validation.provision` event (provision validation outcome + 6 fields), 8 `arm.*` events (subscription / resource-group deploy / stack-deploy / what-if / validate), `aks.postprovision.skip`, per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) when multi-layer infra is used | Telemetry added across `internal/cmd/provision_*.go` and the ARM deployment client | -| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services, `agent.deploy.mode` for hosted agents | Per-service-target instrumentation; hosted-agent mode distinguishes code, container, and prebuilt-image deployments | +| `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services; hosted-agent `azd deploy` also records `agent.deploy.mode` | Per-service-target instrumentation; hosted-agent mode distinguishes code, built-container, and prebuilt-image deployments and is not recorded by standalone `publish` or `package` commands | | `hooks run` (and all hook-running commands) | `hooks.exec` event with `hooks.name` (hashed unless built-in lifecycle name), `hooks.type` (project / service / **layer**), `hooks.kind` (script runtime — `sh` / `pwsh` / `js` / `ts` / `python` / `dotnet`) | `hooks.type=layer` was added with multi-layer provision; pre/post is encoded in `hooks.name` (e.g., `prebuild` / `postbuild`); emitted from the hooks runner on every lifecycle command | ## Full Inventory Matrix @@ -161,7 +161,7 @@ privacy review covers every emission point. | **Multi-layer provision** | `provision` (when `infra.layers[]` is configured in `azure.yaml`) | (none — enriches the `provision` span) | `provision.layer.count`, `provision.layer.max_parallel`, `provision.layer.safe_fallback_count`, `provision.layer.explicit_dependson_count` | All four are integer measurements emitted from `internal/cmd/provision_graph.go`; no per-layer duration or outcome attribute is emitted | | **Execution graph (scheduler)** | `up`, `provision`, `deploy`, `package`, `publish`, `down` | `exegraph.run`, `exegraph.step` | `exegraph.step.count`, `exegraph.max_concurrency`, `exegraph.error_policy`, `exegraph.step.name` (hashed), `exegraph.step.deps` (hashed slice), `exegraph.step.tags` (raw — hardcoded literals only), `exegraph.step.timeout_s` | Step names embed user-defined service / layer names from `azure.yaml`; both `name` and `deps` use `fields.StringHashed` / `fields.StringSliceHashed` | | **Container lifecycle** | `package`, `deploy` (container service targets) | `container.credentials`, `container.publish`, `container.remotebuild` | `container.publish` sets `container.remotebuild` (bool) only; `container.credentials` and `container.remotebuild` set no attributes (span status carries success/failure and duration) | The hashed `pack.builder.image` / `pack.builder.tag` attributes are emitted on the separate `tools.pack.build` span, not the `container.*` spans | -| **Hosted agent deployment** | `package`, `deploy`, `publish` (`azure.ai.agent`) | (none — enriches the active span) | `agent.deploy.mode` (`code`, `container`, `byo_image`) | The mode is determined from the loaded agent definition and selected image path | +| **Hosted agent deployment** | `deploy` (`azure.ai.agents`) | (none — enriches the active package or publish step span) | `agent.deploy.mode` (`code`, `container`, `byo_image`) | The mode is determined from the loaded agent definition or the explicit `--from-package` artifact; standalone `package` and `publish` commands do not record it | | **App Service deploy** | `deploy`, `publish` (App Service targets) | `deploy.appservice.zip` | `deploy.appservice.linux` (bool), `deploy.appservice.attempt` (retry attempt number) | Zip-deploy path only; outcome / duration are carried by the span status and span timing, not by dedicated attributes | | **AKS service target** | `provision` (AKS preprovision/postprovision) | `aks.postprovision.skip` | Skip reason | Recorded when cluster is not yet available for context setup | | **Agent troubleshoot middleware** | Triggered on command failure when troubleshooting is engaged | `agent.troubleshoot` | Error chain attributes, hashed error fields | Emitted from `cmd/middleware/error.go` | diff --git a/docs/specs/metrics-audit/telemetry-schema.md b/docs/specs/metrics-audit/telemetry-schema.md index 5cee9f85e8b..b2547ac1089 100644 --- a/docs/specs/metrics-audit/telemetry-schema.md +++ b/docs/specs/metrics-audit/telemetry-schema.md @@ -93,7 +93,7 @@ These are set once at process startup via `resource.New()` and attached to every | Field | OTel Key | Classification | Purpose | Notes | |-------|----------|----------------|---------|-------| -| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum: `code`, `container`, or `byo_image` | +| Deployment mode | `agent.deploy.mode` | SystemMetadata | FeatureInsight | Raw fixed enum on hosted-agent `azd deploy` package or publish step spans: `code`, `container`, or `byo_image`; includes explicit `--from-package` artifacts | ### Config and Environment