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..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 @@ -45,6 +45,14 @@ import ( // Reference implementation +const ( + agentDeploymentModeArtifactMetadataKey = "azure.ai.agents.deploymentMode" + agentDeploymentModeCode = "code" + agentDeploymentModeContainer = "container" + agentDeploymentModeImage = "byo_image" + fromPackageArtifactMetadataKey = "azd.fromPackage" +) + // displayableProtocolEntry defines a protocol that produces user-visible invocation endpoints. type displayableProtocolEntry struct { Protocol agent_api.AgentProtocol @@ -526,8 +534,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, }, }, }, @@ -600,6 +609,8 @@ func (p *AgentServiceTargetProvider) Package( newArtifacts = append(newArtifacts, packageResponse.Result.Artifacts...) } + setAgentDeploymentMode(newArtifacts, agentDeploymentModeContainer) + return &azdext.ServicePackageResult{ Artifacts: newArtifacts, }, nil @@ -651,11 +662,41 @@ func (p *AgentServiceTargetProvider) Publish( return nil, classifyContainerPublishError(err) } + setAgentDeploymentMode( + publishResponse.Result.Artifacts, + agentDeploymentModeForPublish(serviceContext.Package), + ) + return &azdext.ServicePublishResult{ Artifacts: publishResponse.Result.Artifacts, }, nil } +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 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( @@ -916,7 +957,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 af7f0f8cc3a..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 { @@ -1148,9 +1167,49 @@ 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") } +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() @@ -1180,6 +1239,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) { @@ -1205,6 +1269,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") } @@ -1239,6 +1308,52 @@ 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_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) { 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/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/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 5f70ead81a9..05d0268251c 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,93 @@ 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 || + !isDeploymentOperation(ctx) { + return + } + + mode := hostedAgentFromPackageDeploymentMode(packageArtifacts) + if mode != "" { + tracing.SetAttributesInContext( + ctx, + fields.AgentDeploymentModeKey.String(mode), + ) + return + } + + mode, hasMetadata := hostedAgentDeploymentMode(resultArtifacts) + if !hasMetadata { + mode, _ = hostedAgentDeploymentMode(packageArtifacts) + } + + if mode != "" { + tracing.SetAttributesInContext(ctx, fields.AgentDeploymentModeKey.String(mode)) + } +} + +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 { + 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..682478a1912 --- /dev/null +++ b/cli/azd/pkg/project/service_target_external_test.go @@ -0,0 +1,186 @@ +// 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: "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}, + 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") + ctx = ContextWithDeploymentOperation(ctx) + 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)) + }) + } +} + +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 47f001122e0..87897849786 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -180,6 +180,14 @@ 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 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 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..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 | 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; 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,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** | `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 d4bd317c0a2..b2547ac1089 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 on hosted-agent `azd deploy` package or publish step spans: `code`, `container`, or `byo_image`; includes explicit `--from-package` artifacts | + ### Config and Environment | Field | OTel Key | Classification | Purpose | Notes |