diff --git a/.vscode/cspell.misc.yaml b/.vscode/cspell.misc.yaml index caf197bdedf..56bc83aa518 100644 --- a/.vscode/cspell.misc.yaml +++ b/.vscode/cspell.misc.yaml @@ -22,6 +22,8 @@ overrides: - msrc - filename: ./docs/specs/metrics-audit/** words: + - apphost + - appdetect - vsrpc - Buildpacks - devdeviceid @@ -117,6 +119,7 @@ overrides: - vsrpc - filename: docs/reference/**/*.md words: + - apphost - appinit - appservice - buildpack diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index ea3a20e0634..d354e75acf1 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -173,6 +173,13 @@ dictionaryDefinitions: dictionaries: - azdProjectDictionary overrides: + - filename: internal/appdetect/aspire_polyglot.go + words: + - pylock + - filename: internal/appdetect/dotnet_apphost.go + words: + - buildpack + - upvote - filename: cmd/mcp.go words: - internalcmd diff --git a/cli/azd/cmd/telemetry_test.go b/cli/azd/cmd/telemetry_test.go index b8bf78b3251..46090d7f076 100644 --- a/cli/azd/cmd/telemetry_test.go +++ b/cli/azd/cmd/telemetry_test.go @@ -208,6 +208,19 @@ func TestTelemetryFieldConstants(t *testing.T) { require.Equal(t, "validation.provision.error.count", string(kvErrors.Key)) require.Equal(t, int64(1), kvErrors.Value.AsInt64()) }) + + // Aspire telemetry fields + t.Run("AspireFields", func(t *testing.T) { + t.Parallel() + kv := fields.AspireAppHostLanguageKey.String("typescript") + require.Equal(t, "aspire.apphost.language", string(kv.Key)) + require.Equal(t, "typescript", kv.Value.AsString()) + + for _, language := range []string{"typescript", "python", "go", "java", "rust"} { + kv := fields.AspireAppHostLanguageKey.String(language) + require.NotEmpty(t, kv.Value.AsString()) + } + }) } // TestCommandTelemetryCoverage ensures every user-facing command is explicitly categorized diff --git a/cli/azd/internal/appdetect/aspire_polyglot.go b/cli/azd/internal/appdetect/aspire_polyglot.go new file mode 100644 index 00000000000..4d0cc8ae4a1 --- /dev/null +++ b/cli/azd/internal/appdetect/aspire_polyglot.go @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package appdetect + +import ( + "encoding/json" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// aspirePolyglotConfigFile is the unified Aspire CLI configuration file. For polyglot (non-C#) +// AppHosts it declares the AppHost entry-point path and language under the "appHost" object. +const aspirePolyglotConfigFile = "aspire.config.json" + +// aspireConfig models the subset of aspire.config.json that azd inspects. +type aspireConfig struct { + AppHost struct { + Path string `json:"path"` + Language string `json:"language"` + } `json:"appHost"` +} + +// polyglotAppHostFileLanguage maps well-known polyglot Aspire AppHost entry-point file names +// (compared case-insensitively) to the normalized language azd reports. These mirror the +// detection patterns used by the Aspire CLI's language discovery +// (microsoft/aspire: src/Aspire.Cli/Projects/DefaultLanguageDiscovery.cs). +var polyglotAppHostFileLanguage = map[string]string{ + "apphost.mts": "typescript", + "apphost.ts": "typescript", + "apphost.py": "python", + "apphost.go": "go", + "apphost.rs": "rust", + "apphost.java": "java", +} + +// pythonAppHostCompanions are additional files that corroborate a Python Aspire AppHost. Because +// "apphost.py" is a fairly generic file name, azd only treats it as an Aspire AppHost (via the +// file-name fallback) when one of these companion markers is also present, to avoid false +// positives. A Python AppHost declared explicitly in aspire.config.json is resolved earlier and +// does not rely on these companions. +var pythonAppHostCompanions = []string{"pylock.apphost.toml", "apphost_requirements.txt"} + +// normalizeAspireLanguage maps an aspire.config.json "appHost.language" value to a normalized +// language identifier. It returns an empty string for C# (or unknown/empty values), since C# +// AppHosts are detected and supported through the regular .NET AppHost path. +func normalizeAspireLanguage(language string) string { + switch strings.ToLower(strings.TrimSpace(language)) { + case "typescript/nodejs", "typescript", "ts", "javascript/nodejs", "javascript", "js": + return "typescript" + case "python", "py": + return "python" + case "go", "golang": + return "go" + case "java": + return "java" + case "rust", "rs": + return "rust" + default: + // csharp, c#, dotnet, empty, or anything azd doesn't recognize as polyglot. + return "" + } +} + +// detectAspirePolyglotAppHost inspects a directory for an Aspire polyglot (non-C#) AppHost. +// It returns the normalized language (e.g. "typescript", "python") and the AppHost file path +// when detected. azd does not yet support these AppHosts; see +// https://github.com/Azure/azure-dev/issues/7138. +// +// Detection prefers the explicit signal from aspire.config.json ("appHost.language" or a +// polyglot "appHost.path"), and otherwise falls back to well-known AppHost file names. +func detectAspirePolyglotAppHost(dir string, entries []fs.DirEntry) (language string, appHostFile string, ok bool) { + present := make(map[string]string, len(entries)) + for _, entry := range entries { + if !entry.IsDir() { + present[strings.ToLower(entry.Name())] = entry.Name() + } + } + + // Strongest signal: aspire.config.json explicitly declares the AppHost language/path. + if configName, has := present[aspirePolyglotConfigFile]; has { + lang, file, declared := languageFromAspireConfig(filepath.Join(dir, configName), present) + if lang != "" { + return lang, filepath.Join(dir, file), true + } + if declared { + // The config authoritatively declares an AppHost that resolves to C# (or an + // otherwise non-polyglot target). Trust it and do NOT run the filename fallback: + // a sibling apphost.ts/apphost.py in a C# Aspire layout must not be misreported as + // polyglot, which would hard-fail `azd init`/`up` on a supported project. + return "", "", false + } + } + + // Fallback: detect by well-known AppHost file names when there is no explicit config signal. + // Only TypeScript file names are trusted on their own; "apphost.py" requires a companion marker, + // and the remaining (experimental) languages are only detected via aspire.config.json above, to + // avoid false positives on generically named files. + for fileName, original := range present { + lang, isAppHostFile := polyglotAppHostFileLanguage[fileName] + if !isAppHostFile { + continue + } + + switch lang { + case "typescript": + return lang, filepath.Join(dir, original), true + case "python": + if hasPythonAppHostCompanion(present) { + return lang, filepath.Join(dir, original), true + } + } + } + + return "", "", false +} + +// languageFromAspireConfig resolves the polyglot language declared in aspire.config.json. It +// returns: +// - language: the normalized polyglot language, or "" for a C# AppHost (handled by the regular +// .NET path) or when no language could be resolved. +// - appHostFile: the relative AppHost path declared in the config (may include subdirectories, +// e.g. "src/apphost.mts"). +// - declared: whether the config authoritatively declares an AppHost (a readable, parsable +// config with a non-empty "appHost.path"). When true but language is "", the config declares a +// C#/non-polyglot AppHost; callers should treat that as authoritative and skip filename-based +// fallback detection. When false, the config was missing/unreadable/malformed and callers may +// fall back to file-name heuristics. +func languageFromAspireConfig( + configPath string, + present map[string]string, +) (language string, appHostFile string, declared bool) { + //nolint:gosec // G304: configPath is derived from a directory listing during app detection. + contents, err := os.ReadFile(configPath) + if err != nil { + return "", "", false + } + + var config aspireConfig + if err := json.Unmarshal(contents, &config); err != nil { + return "", "", false + } + + if strings.TrimSpace(config.AppHost.Path) == "" { + return "", "", false + } + + // Preserve the full relative path (may contain subdirectories). Only case-resolve the path + // against the directory listing when it refers to an immediate child, since `present` only + // contains the top-level entries of the scanned directory. + appHostFile = filepath.Clean(filepath.FromSlash(config.AppHost.Path)) + if !strings.ContainsRune(appHostFile, filepath.Separator) { + if resolved, has := present[strings.ToLower(appHostFile)]; has { + appHostFile = resolved + } + } + + fileName := strings.ToLower(filepath.Base(appHostFile)) + + // Prefer the explicit language declaration. + if lang := normalizeAspireLanguage(config.AppHost.Language); lang != "" { + return lang, appHostFile, true + } + + // No explicit (polyglot) language: infer from the AppHost path's file name. This treats a + // ".csproj"/"apphost.cs" path as C# (empty language), so it is not misreported as polyglot. + if lang, isAppHostFile := polyglotAppHostFileLanguage[fileName]; isAppHostFile { + return lang, appHostFile, true + } + + return "", appHostFile, true +} + +// hasPythonAppHostCompanion reports whether a Python Aspire AppHost companion marker is present. +func hasPythonAppHostCompanion(present map[string]string) bool { + for _, companion := range pythonAppHostCompanions { + if _, has := present[companion]; has { + return true + } + } + return false +} diff --git a/cli/azd/internal/appdetect/aspire_polyglot_test.go b/cli/azd/internal/appdetect/aspire_polyglot_test.go new file mode 100644 index 00000000000..2812e484bda --- /dev/null +++ b/cli/azd/internal/appdetect/aspire_polyglot_test.go @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package appdetect + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "go.opentelemetry.io/otel" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + + "github.com/azure/azure-dev/cli/azd/internal/tracing/events" + "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" + "github.com/azure/azure-dev/cli/azd/pkg/errorhandler" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/stretchr/testify/require" +) + +// writeFiles writes each name/content pair into dir and returns the directory entries, mirroring +// what the appdetect walker passes to detectors. +func writeFiles(t *testing.T, files map[string]string) (string, []fs.DirEntry) { + t.Helper() + dir := t.TempDir() + for name, content := range files { + err := os.WriteFile(filepath.Join(dir, name), []byte(content), osutil.PermissionFile) + require.NoError(t, err) + } + entries, err := os.ReadDir(dir) + require.NoError(t, err) + return dir, entries +} + +func TestDetectAspirePolyglotAppHost(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + files map[string]string + expectOk bool + expectLang string + expectFile string // path relative to the scanned dir (may include subdirectories) + }{ + { + name: "TypeScriptWithConfig", + files: map[string]string{ + "apphost.mts": "await createBuilder();", + "package.json": "{}", + "aspire.config.json": `{"appHost":{"path":"apphost.mts","language":"typescript/nodejs"}}`, + }, + expectOk: true, + expectLang: "typescript", + expectFile: "apphost.mts", + }, + { + name: "TypeScriptByFileNameOnly", + files: map[string]string{ + "apphost.ts": "await createBuilder();", + "package.json": "{}", + }, + expectOk: true, + expectLang: "typescript", + expectFile: "apphost.ts", + }, + { + name: "PythonWithConfig", + files: map[string]string{ + "apphost.py": "create_builder()", + "aspire.config.json": `{"appHost":{"path":"apphost.py","language":"python"}}`, + }, + expectOk: true, + expectLang: "python", + expectFile: "apphost.py", + }, + { + name: "PythonWithCompanionButNoConfig", + files: map[string]string{ + "apphost.py": "create_builder()", + "apphost_requirements.txt": "aspire", + }, + expectOk: true, + expectLang: "python", + expectFile: "apphost.py", + }, + { + name: "PythonAloneIsNotDetected", + files: map[string]string{ + "apphost.py": "print('hello, not aspire')", + }, + expectOk: false, + }, + { + name: "GoDetectedOnlyViaConfig", + files: map[string]string{ + "apphost.go": "package main", + "aspire.config.json": `{"appHost":{"path":"apphost.go","language":"go"}}`, + }, + expectOk: true, + expectLang: "go", + expectFile: "apphost.go", + }, + { + name: "GoFileNameAloneIsNotDetected", + files: map[string]string{ + "apphost.go": "package main", + "go.mod": "module example", + }, + expectOk: false, + }, + { + name: "CSharpConfigIsNotPolyglot", + files: map[string]string{ + "aspire.config.json": `{"appHost":{"path":"AppHost/AppHost.csproj"}}`, + }, + expectOk: false, + }, + { + name: "CSharpConfigWithSiblingPythonFileIsNotPolyglot", + files: map[string]string{ + // A supported C# Aspire layout that also has a root-level apphost.py must not be + // misreported as a Python polyglot AppHost. The config is authoritative. + "aspire.config.json": `{"appHost":{"path":"AppHost/AppHost.csproj"}}`, + "apphost.py": "print('unrelated')", + }, + expectOk: false, + }, + { + name: "CSharpConfigWithSiblingTypeScriptFileIsNotPolyglot", + files: map[string]string{ + // Same class of problem for TypeScript: an apphost.ts next to a C# config must not + // trigger the file-name fallback. + "aspire.config.json": `{"appHost":{"path":"AppHost/AppHost.csproj"}}`, + "apphost.ts": "createBuilder();", + }, + expectOk: false, + }, + { + name: "TypeScriptWithSubdirectoryPath", + files: map[string]string{ + "package.json": "{}", + "aspire.config.json": `{"appHost":{"path":"src/apphost.mts","language":"typescript/nodejs"}}`, + }, + expectOk: true, + expectLang: "typescript", + expectFile: filepath.Join("src", "apphost.mts"), + }, + { + name: "PlainNodeAppIsNotDetected", + files: map[string]string{ + "package.json": "{}", + "index.ts": "console.log('hi')", + }, + expectOk: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + dir, entries := writeFiles(t, tt.files) + lang, appHostFile, ok := detectAspirePolyglotAppHost(dir, entries) + require.Equal(t, tt.expectOk, ok) + if tt.expectOk { + require.Equal(t, tt.expectLang, lang) + // The reported AppHost file preserves any subdirectories from aspire.config.json. + rel, err := filepath.Rel(dir, appHostFile) + require.NoError(t, err) + require.Equal(t, tt.expectFile, rel) + } + }) + } +} + +func TestDotNetAppHostDetector_PolyglotReturnsSuggestionError(t *testing.T) { + t.Parallel() + + dir, entries := writeFiles(t, map[string]string{ + "apphost.mts": "await createBuilder();", + "package.json": "{}", + "aspire.config.json": `{"appHost":{"path":"apphost.mts","language":"typescript/nodejs"}}`, + }) + + detector := &dotNetAppHostDetector{} + project, err := detector.DetectProject(t.Context(), dir, entries) + require.Nil(t, project) + require.Error(t, err) + + var suggestionErr *errorhandler.ErrorWithSuggestion + require.ErrorAs(t, err, &suggestionErr) + require.Contains(t, suggestionErr.Suggestion, "7138") + require.NotEmpty(t, suggestionErr.Links) +} + +// TestDotNetAppHostDetector_EmitsUnsupportedTelemetry verifies that detecting a polyglot AppHost +// emits the aspire.apphost.unsupported span with the aspire.apphost.language attribute, as required +// by cli/azd/AGENTS.md. It installs an in-memory tracer provider so the emitted span is captured. +func TestDotNetAppHostDetector_EmitsUnsupportedTelemetry(t *testing.T) { + // Not parallel: mutates the global OpenTelemetry tracer provider. + sr := tracetest.NewSpanRecorder() + tp := tracesdk.NewTracerProvider(tracesdk.WithSpanProcessor(sr)) + prev := otel.GetTracerProvider() + otel.SetTracerProvider(tp) + t.Cleanup(func() { otel.SetTracerProvider(prev) }) + + dir, entries := writeFiles(t, map[string]string{ + "apphost.mts": "await createBuilder();", + "aspire.config.json": `{"appHost":{"path":"apphost.mts","language":"typescript/nodejs"}}`, + }) + + detector := &dotNetAppHostDetector{} + _, err := detector.DetectProject(t.Context(), dir, entries) + require.Error(t, err) + + var language string + var found bool + for _, span := range sr.Ended() { + if span.Name() != events.AspireUnsupportedAppHostEvent { + continue + } + for _, attr := range span.Attributes() { + if attr.Key == fields.AspireAppHostLanguageKey.Key { + language = attr.Value.AsString() + found = true + } + } + } + + require.True(t, found, "expected %q span with %q attribute", + events.AspireUnsupportedAppHostEvent, fields.AspireAppHostLanguageKey.Key) + require.Equal(t, "typescript", language) +} diff --git a/cli/azd/internal/appdetect/dotnet_apphost.go b/cli/azd/internal/appdetect/dotnet_apphost.go index 999cb264512..5a966093b1f 100644 --- a/cli/azd/internal/appdetect/dotnet_apphost.go +++ b/cli/azd/internal/appdetect/dotnet_apphost.go @@ -5,13 +5,19 @@ package appdetect import ( "context" + "fmt" "io/fs" "log" "path/filepath" "slices" "strings" + "github.com/azure/azure-dev/cli/azd/internal/tracing" + "github.com/azure/azure-dev/cli/azd/internal/tracing/events" + "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" + "github.com/azure/azure-dev/cli/azd/pkg/errorhandler" "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" + "go.opentelemetry.io/otel/trace" ) type dotNetAppHostDetector struct { @@ -61,5 +67,35 @@ func (ad *dotNetAppHostDetector) DetectProject(ctx context.Context, path string, } } + // Finally, check for an Aspire polyglot (non-C#) AppHost (e.g. TypeScript or Python). azd does + // not support these yet, so surface an actionable error instead of letting the AppHost fall + // through to a generic source build (which produces confusing Docker/buildpack failures). + // See https://github.com/Azure/azure-dev/issues/7138. + if language, appHostFile, ok := detectAspirePolyglotAppHost(path, entries); ok { + _, span := tracing.Start( + ctx, + events.AspireUnsupportedAppHostEvent, + trace.WithAttributes(fields.AspireAppHostLanguageKey.String(language))) + span.End() + + return nil, &errorhandler.ErrorWithSuggestion{ + Err: fmt.Errorf( + "detected an Aspire polyglot (%s) AppHost at %q, which azd does not support yet", + language, appHostFile), + Message: "azd does not yet support Aspire polyglot (non-C#) AppHosts, " + + "such as TypeScript or Python AppHosts.", + Suggestion: "Track and upvote support for this scenario at " + + "https://github.com/Azure/azure-dev/issues/7138.\n" + + "In the meantime, use a C# (.NET) Aspire AppHost, or publish with the Aspire CLI " + + "(for example, 'aspire deploy').", + Links: []errorhandler.ErrorLink{ + { + URL: "https://github.com/Azure/azure-dev/issues/7138", + Title: "Support Aspire polyglot (non-C#) AppHost projects in azd", + }, + }, + } + } + return nil, nil } diff --git a/cli/azd/internal/tracing/events/events.go b/cli/azd/internal/tracing/events/events.go index 5e89fb7cbf7..896ac2f058f 100644 --- a/cli/azd/internal/tracing/events/events.go +++ b/cli/azd/internal/tracing/events/events.go @@ -24,6 +24,11 @@ const PackBuildEvent = "tools.pack.build" // AgentTroubleshootEvent is the name of the event which tracks agent troubleshoot operations. const AgentTroubleshootEvent = "agent.troubleshoot" +// AspireUnsupportedAppHostEvent tracks when azd detects an Aspire polyglot (non-C#) AppHost +// (e.g. a TypeScript or Python AppHost) which azd does not yet support. See +// https://github.com/Azure/azure-dev/issues/7138. +const AspireUnsupportedAppHostEvent = "aspire.apphost.unsupported" + // Extension related events. const ( ExtensionRunEvent = "ext.run" diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index f59a0bf8cd0..854fcdc6fb3 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -1025,6 +1025,18 @@ var ( } ) +// Aspire related fields +var ( + // AspireAppHostLanguageKey is the language of a detected Aspire polyglot (non-C#) AppHost + // (e.g. "typescript", "python", "go", "java", "rust"). This is a fixed enum of Aspire-supported + // AppHost languages, so it is emitted raw (not hashed). + AspireAppHostLanguageKey = AttributeKey{ + Key: attribute.Key("aspire.apphost.language"), + Classification: SystemMetadata, + Purpose: FeatureInsight, + } +) + // Mcp related fields var ( // The name of the MCP client. diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index 202e8cce7d5..28cf7cd0563 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -116,6 +116,7 @@ Commands follow the pattern `cmd.` where spaces become dots. | `container.remotebuild` | Remote container build | | `exegraph.run` | Execution graph run (parallel operations) | | `exegraph.step` | Single step within execution graph | +| `aspire.apphost.unsupported` | Detected an unsupported Aspire polyglot (non-C#) AppHost during app detection | ### VS Code Extension Events (`azure-dev.*`) @@ -321,6 +322,14 @@ Set **only when an external command-line tool invocation fails**, during error c | `appinit.lastStep` | string | Last init step reached | +
+Aspire + +| Field Key | Type | Description | +|-----------|------|-------------| +| `aspire.apphost.language` | string | Language of a detected but unsupported Aspire polyglot (non-C#) AppHost. Emitted on `aspire.apphost.unsupported`. Values: `typescript`, `python`, `go`, `java`, `rust`. | +
+
Hooks @@ -707,6 +716,7 @@ How to find telemetry for a given feature area. Start here if you know the featu | **Self-Update** | `cmd.update` | `update.installMethod`, `update.fromVersion` | Update adoption | | **Hooks** | `hooks.exec` | `hooks.name`, `hooks.type`, `hooks.kind` | Hook usage by type | | **Container Build** | `container.publish`, `container.remotebuild`, `tools.pack.build` | `pack.builder.image` | Build method usage, success rates | +| **App Detection (Aspire polyglot)** | `aspire.apphost.unsupported` | `aspire.apphost.language` (`typescript`/`python`/`go`/`java`/`rust`) | How often an unsupported Aspire polyglot (non-C#) AppHost is encountered, by language. **Emitted only during app detection for `init` and fresh `up` (no existing `azure.yaml`)** — not for already-initialized projects, so absence does not mean zero unsupported AppHosts. | | **Tool Management (`azd tool`)** | `cmd.tool.install`, `cmd.tool.upgrade`, `cmd.tool.uninstall`, `cmd.tool.check` | `tool.id`, `tool.install.strategy` | Install/upgrade/uninstall success, upgrade availability | ## See Also diff --git a/docs/specs/metrics-audit/feature-telemetry-matrix.md b/docs/specs/metrics-audit/feature-telemetry-matrix.md index 024971b2d32..0190e010a6a 100644 --- a/docs/specs/metrics-audit/feature-telemetry-matrix.md +++ b/docs/specs/metrics-audit/feature-telemetry-matrix.md @@ -167,3 +167,4 @@ reserved field contracts. | **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` | | **Up-graph performance** | `up` (graph execution) | (none — enriches the `up` command span) | `perf.provision_duration_ms`, `perf.deploy_duration_ms`, `perf.total_duration_ms` | Emitted from `internal/cmd/up_graph.go` after the graph completes; provision/deploy durations set only when those phases run | | **VS RPC** | `vs-server` long-running session | `vsrpc.*` (event prefix) | Per-RPC attributes documented in `telemetry-schema.md` | Long-running RPC server for VS integration | +| **App detection** | `init`, `up` (fresh projects without `azure.yaml`, via `appdetect.Detect`) | `aspire.apphost.unsupported` | `aspire.apphost.language` (fixed enum — `typescript` / `python` / `go` / `java` / `rust`; not hashed) | Emitted from `internal/appdetect/dotnet_apphost.go` when an Aspire polyglot (non-C#) AppHost is detected; azd surfaces an actionable error referencing [#7138](https://github.com/Azure/azure-dev/issues/7138) instead of falling through to a generic source build | diff --git a/docs/specs/metrics-audit/privacy-review-checklist.md b/docs/specs/metrics-audit/privacy-review-checklist.md index 304f1c4b3ab..f12824430f7 100644 --- a/docs/specs/metrics-audit/privacy-review-checklist.md +++ b/docs/specs/metrics-audit/privacy-review-checklist.md @@ -166,7 +166,8 @@ A new field **must** be hashed if any of the following are true: A new field should **not** be hashed if: -- The value is from a fixed enum (e.g., `auth.method` = `"browser"`). +- The value is from a fixed enum (e.g., `auth.method` = `"browser"`, or + `aspire.apphost.language` = `"typescript"` / `"python"` / `"go"` / `"java"` / `"rust"`). - The value is a count or duration (measurements). - The value is system-generated metadata (e.g., OS type). - The value is a hardcoded literal in source code (e.g., `exegraph.step.tags`, which diff --git a/docs/specs/metrics-audit/telemetry-schema.md b/docs/specs/metrics-audit/telemetry-schema.md index 402e4de9f73..feb3fced28f 100644 --- a/docs/specs/metrics-audit/telemetry-schema.md +++ b/docs/specs/metrics-audit/telemetry-schema.md @@ -38,6 +38,7 @@ OpenTelemetry span name or event name. | `ContainerRemoteBuildEvent` | `container.remotebuild` | Azure-side remote container build | | `ExeGraphRunEvent` | `exegraph.run` | Root span for executing an entire graph | | `ExeGraphStepEvent` | `exegraph.step` | Single step execution within the graph | +| `AspireUnsupportedAppHostEvent` | `aspire.apphost.unsupported` | Detected an unsupported Aspire polyglot (non-C#) AppHost during app detection | ## Fields @@ -157,6 +158,12 @@ not emitted by azd spans. | Builder image | `pack.builder.image` | SystemMetadata | FeatureInsight | | Builder tag | `pack.builder.tag` | SystemMetadata | FeatureInsight | +### Aspire + +| Field | OTel Key | Classification | Purpose | Notes | +|-------|----------|----------------|---------|-------| +| AppHost language | `aspire.apphost.language` | SystemMetadata | FeatureInsight | Fixed enum (`typescript`/`python`/`go`/`java`/`rust`); not hashed; not a measurement. Emitted on `aspire.apphost.unsupported`. | + ### MCP | Field | OTel Key | Classification | Purpose |