Tracing in azd

July 22, 2026 · View on GitHub

Overview

azd uses OpenTelemetry to collect anonymous usage information and success metrics. Internally, azd defines its own wrapper package — cli/azd/internal/tracing — which simplifies working with the standard OpenTelemetry library.

All azd commands automatically create a root command event with a namespace prefix of cmd. (for example, cmd.provision, cmd.up, etc.).


Background Concepts

  • Trace – Represents an entire operation or command (e.g., running azd up).
  • Span (also Event) – Represents a single unit of work within that operation (e.g., deploying resources).
  • Attribute – Metadata attached to a span (e.g., environment name, subscription ID).

For general OpenTelemetry background, see the official Traces documentation.


How to Add New Attributes or Events

1. Identify What You Want to Add

Decide whether you are adding:

  • A new attribute (metadata attached to a span), or
  • A new event (a time-stamped span representing an operation or milestone).
TypeFile to UpdateDescription
Attributefields.goDefines standardized attribute keys.
Eventevents.goDefines standardized event names.

2. Recording a New Event

Use the tracing.Start helper to start a new span:

ctx, span := tracing.Start(ctx, events.MyNewEvent)
// ... your logic here
//  don't forget to span.SetStatus(codes.Err, "<description>) to mark success or failure
span.End()
  • span.End() marks completion of the event.
  • span.EndWithStatus(err) sets an appropriate error code if something failed.
  • Timing and duration are automatically recorded.

Note: Traces are automatically exported and uploaded asynchronously. Simply logging the span will be sufficient.

3. Adding Attributes

You can set attributes in different ways depending on scope:

MethodScopeUsage
tracing.*UsageAttributeRoot command spanFor attributes that apply to the entire command (e.g., environment, service).
span.SetAttributes(...)Current spanFor attributes specific to the current operation.
tracing.SetGlobalAttributes(...)All spansTo attach a property globally across all spans.
tracing.SetBaggageInContext(ctx, key, value)Current and child spansPropagates an attribute across child contexts.

Example:

tracing.SetUsageAttributes(fields.EnvName.StringHashed(envName))

This example sets a usage attribute to be included in the root command event.


Existing Event Taxonomy

The following events already exist in events.go. Use them instead of adding new events for extension and hook lifecycle telemetry.

EventLifecycleAttributes to expectSample row
ext.runRunning an installed extension command through azd.Command attributes such as cmd.entry, cmd.flags, cmd.args.count, plus extension.installed on the root span.name=ext.run, cmd.entry=cmd.ai.chat, cmd.flags=["model"], cmd.args.count=0
ext.installInstalling one extension version.extension.id (set as soon as installation begins); extension.version (set after the version is resolved). On failure the span uses OpenTelemetry status Error; EndWithStatus derives the status description from the error type.name=ext.install, extension.id=microsoft.azd.ai, extension.version=1.2.0, status=Ok
ext.upgradeUpgrading one extension attempt.extension.id, extension.version.from, extension.version.to, extension.source, extension.upgrade.duration_ms, extension.upgrade.outcome.name=ext.upgrade, extension.id=microsoft.azd.ai, extension.version.from=1.1.0, extension.version.to=1.2.0, extension.upgrade.outcome=upgraded
ext.promotePromoting an extension registry entry, such as dev to main.extension.id, extension.version.from, extension.version.to, extension.source.from, extension.source.to.name=ext.promote, extension.id=microsoft.azd.ai, extension.source.from=dev, extension.source.to=main, status=Ok
hooks.execExecuting a project, layer, or service lifecycle hook.hooks.name, hooks.type, hooks.kind; status description uses hook-specific codes such as hook.validation_failed.name=hooks.exec, hooks.name=predeploy, hooks.type=service, hooks.kind=sh, status=Ok

Extension Attributes

Extension telemetry attributes are defined in fields.go.

AttributeDescriptionExample
extension.idExtension identifier.microsoft.azd.ai
extension.versionInstalled extension version.1.2.0
extension.installedInstalled extensions on a command span, each formatted as id@version.["microsoft.azd.ai@1.2.0"]
extension.version.fromVersion before an upgrade or promotion.1.1.0
extension.version.toVersion after an upgrade or promotion.1.2.0
extension.sourceRegistry source used for an upgrade.main
extension.source.fromRegistry source before a promotion.dev
extension.source.toRegistry source after a promotion.main
extension.upgrade.duration_msUpgrade duration in milliseconds.1532
extension.upgrade.outcomeUpgrade result status.upgraded

Hook Attributes

hooks.exec spans should include the hook name and scope as soon as they are known, then add the executor kind after hook validation succeeds.

AttributeDescriptionExample
hooks.nameHook name. The azd hooks run root command hashes unknown hook names before recording usage attributes; hooks.exec child spans record the resolved hook name.predeploy
hooks.typeHook run scope.project, layer, or service
hooks.kindExecutor kind used to run the hook.sh, pwsh, python, js, ts, or dotnet

Error Attribute Conventions

Use MapError from internal/cmd/errors.go for command, extension, JSON-RPC, MCP, and agent spans so that error status and attributes stay consistent.

ConventionDescriptionExample
Span statusFailed spans mapped through MapError set OpenTelemetry status Error; the status description is the primary error code. Codes use stable families such as auth.*, ext.*, internal.*, service.*, tool.*, or user.*.ext.run.failed, service.arm.deployment.failed, user.canceled
error.categoryBroad local error category, used when the error is local rather than returned by an external service.auth
error.codeNormalized local or extension error code.invalid_payload
error.typeGo error type for unclassified or suggestion-wrapped errors.*os.PathError
error.service.nameExternal service name after fields.ErrorKey prefixes service.name for error details. Only set this when an external service returned the error.arm, aad, storage
error.service.errorCodeError code returned by an external service, after fields.ErrorKey prefixes service.errorCode. For ARM deployment errors this is a JSON array describing the nested error chain (see below).AuthorizationFailed
error.service.statusCodeStatus code returned by an external service, after fields.ErrorKey prefixes service.statusCode.403

For nested ARM deployment failures, MapError walks the inner error tree and encodes each level as an entry in the JSON array stored on error.service.errorCode. Each entry has the shape {"error.code": "<code>", "error.arm.frame_index": <n>}, where error.arm.frame_index is the depth in the nested chain (0 for the outermost error). For example:

[
  {"error.code": "InvalidTemplateDeployment", "error.arm.frame_index": 0},
  {"error.code": "AuthorizationFailed", "error.arm.frame_index": 1}
]

Do not attach arbitrary user input or secrets to error.* attributes. Prefer the standardized field constants from fields.go; if you need to include a service-related field on an error span, pass it through fields.ErrorKey so it is reported under the error. namespace.


🧪 Observing New Traces

1. Local Observation

You can log traces to a file during local development:

azd up --trace-log-file trace.json

Then open the file in your favorite text editor.

2. Remote Observation (Released Builds)

When using a released (daily or official) build of azd, traces are automatically uploaded to the shared DevCli Kusto cluster.

Note: Newly added attributes or events may require classification in the Data Catalog portal before they appear in dashboards or reports.


Examples - Previous Work

These example PRs include adding both new spans and events and can be used as reference.


Quick Reference

TaskAPINotes
Start a spantracing.Start(ctx, events.MyEvent)Returns a Span object.
End a spanspan.End()Marks completion of span.
End with statusspan.EndWithStatus(err)Records success/failure automatically.
Set attributespan.SetAttributes(...)Attach metadata to the span.
Set global attributestracing.SetGlobalAttributes(...)Affects all spans.
Add root attributestracing.*UsageAttributeFor top-level command attributes.

General Tips

  • Keep attribute names consistent and descriptive. Follow OpenTelemetry naming best practices. Use constants from fields.go.
  • Always end spans and set status for the ones you start; otherwise, they appear as incomplete traces.
  • Avoid logging any user data. Traces are collected anonymously.
  • Use trace logs locally to verify correctness before merging changes.

Next Steps:


Tool Command Telemetry

The azd tool command group emits telemetry that captures per-operation outcomes for install, upgrade, check, and show. The first-run telemetry contract remains defined for a possible future experience but is not currently emitted. All active attributes are attached as usage attributes via tracing.SetUsageAttributes, which means they appear on the user's actual command span (e.g. cmd.tool.install) rather than on a separate child span.

Dormant First-Run Experience

The first-run middleware (cmd/middleware/tool_first_run.go) is not registered in the command middleware chain, so the following reserved attributes are not currently emitted. The definitions remain documented to support a future redesign without changing the telemetry contract.

AttributeTypeEmitted whenNotes
tool.firstrun.skip_reasonstringFirst-run was bypassedOne of env_var, no_prompt, ci_cd, non_interactive, already_completed, config_error. The alpha-disabled and child-action skip paths are intentionally silent because the user has no opportunity to opt in.
tool.firstrun.opt_inboolUser answered the welcome prompttrue = accepted, false = declined.
tool.firstrun.tools_detectedintOpt-in = trueCount of built-in tools already installed locally.
tool.firstrun.tools_offeredintOpt-in = trueCount of recommended tools offered for installation. 0 when nothing is missing.
tool.firstrun.tools_selectedintAt least one tool was offeredCount of tools the user kept selected.
tool.firstrun.tools_selected_namesstringAt least one tool selectedComma-separated sorted built-in tool IDs (low-cardinality, no PII). Sorting collapses permutations of the same set into a single canonical value.
tool.firstrun.tools_deselected_namesstringUser deselected at least one offered toolComma-separated sorted built-in tool IDs.
tool.firstrun.outcomestringFirst-run flow ran to a terminal stateOne of completed (full flow succeeded), declined (user declined the opt-in), cancelled (user interrupted a prompt), detect_failed (tool detection failed), install_failed (install batch errored). Mutually exclusive with tool.firstrun.skip_reasonskip_reason is set only when the flow never ran. Replaces the prior tool.firstrun.completed bool, which was not filterable for the failure and decline cases.
tool.firstrun.install_success_countintFirst-run installed toolsNumber of tools that succeeded during the first-run batch install. Mirrors tool.install.success_count but is namespaced so a subsequent azd tool install action on the same span does not overwrite it.
tool.firstrun.install_failure_countintFirst-run installed toolsNumber of tools that failed during the first-run batch install.
tool.firstrun.install_failed_idsstringFirst-run had at least one failureComma-separated sorted tool IDs whose first-run install failed.
tool.firstrun.install_duration_msintFirst-run installed toolsWall-clock duration of the first-run install batch in milliseconds.

Per-Operation Attributes

The tool install / tool upgrade / tool check / tool show actions emit:

AttributeTypeEmitted byNotes
tool.idstringSingle-target install / upgrade / show (len(ids) == 1)The built-in tool identifier. Mutually exclusive with tool.ids.
tool.idsstringMulti-target batch install / upgrade (len(ids) > 1)Comma-separated sorted built-in tool IDs. Mutually exclusive with tool.id — single-tool operations emit only tool.id. Sorting keeps attribute cardinality bounded (set vs permutation).
tool.dry_runboolinstall / upgradeReflects the --dry-run flag.
tool.install.strategystringSingle-target install / upgradeE.g. winget, brew, manual.
tool.install.successboolSingle-target install / upgradeWhether the per-tool operation succeeded.
tool.install.success_countintBatch install / upgradeNumber of tools that succeeded.
tool.install.failure_countintBatch install / upgradeNumber of tools that failed.
tool.install.failed_idsstringAt least one failureComma-separated sorted tool IDs whose operation failed. Only tool IDs are recorded — error messages flow through the global error middleware (error.message). When the batch call itself errors before any per-tool result is produced, the count of failed IDs may be less than failure_count (failures are synthesized from the requested set, but a synthesized entry with no Tool reference is omitted from the ID list to avoid emitting "unknown").
tool.install.duration_msintBatch install / upgradeWall-clock duration of the operation in milliseconds. Per-tool durations are intentionally not emitted: the cardinality cost would be O(tools × installs) for limited diagnostic value, and the aggregate captures the only number actionable at fleet scale. Per-tool durations remain available in the in-process InstallResult.Duration for local logging / dry-run reporting.
tool.upgrade.from_versionstringSingle-target upgradePre-upgrade installed version. Captured via detection on both the explicit-args (azd tool upgrade <id>) and auto-detect (azd tool upgrade) paths. Unset only when detection failed or the tool was not previously installed.
tool.upgrade.to_versionstringSingle-target upgrade succeededPost-upgrade installed version. Only emitted when the upgrade succeeded — on failure InstalledVersion is either the unchanged pre-upgrade value or empty, which would be ambiguous against from_version.
tool.check.updates_availableinttool checkCount of tools whose UpdateAvailable is true.

PII rule: Never include free-form error strings, file paths, or user input in tool telemetry. Stick to built-in tool IDs and semver-style version strings. Error messages are already captured by the global error middleware on the same span.

Coverage Test

Every new tool subcommand must be classified in cmd/telemetry_test.go (either commandsWithSpecificTelemetry or commandsWithOnlyGlobalTelemetry), and any new field constant added to internal/tracing/fields/fields.go should have a sub-assertion in TestTelemetryFieldConstants.