Skip to content

Add a cross-client telemetry-names contract (+ Rust reference adoption)#237

Closed
joshmouch wants to merge 1 commit into
microsoft:mainfrom
joshmouch:pr/telemetry-names-contract
Closed

Add a cross-client telemetry-names contract (+ Rust reference adoption)#237
joshmouch wants to merge 1 commit into
microsoft:mainfrom
joshmouch:pr/telemetry-names-contract

Conversation

@joshmouch

@joshmouch joshmouch commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Why this belongs at the protocol layer

The self-instrumentation names (spans, metrics, attributes) an AHP client emits about its own operation are an interop surface, the same way the wire types are. They have to be byte-identical across clients or an operator running a fleet of mixed-language agents cannot write one OpenTelemetry dashboard or alert that works across all of them. That requirement is what makes the name contract a protocol concern rather than a per-client detail; the instrumentation code stays per-client and idiomatic, and only the names are shared. This is separate from the existing "OpenTelemetry over AHP" channel (docs/specification/telemetry-channel.md), which carries OTel data over the protocol. The scope question is open in #239.

What

Adds types/telemetry/registry.ts as the single source of truth for those names, generates an idiomatic holder per client, and wires the Rust client to emit the metrics as a reference adopter.

Client Generated holder Self-instrumentation today
.NET AhpTelemetryNames (static class) ActivitySource + Meter (in the .NET-client draft, #206)
Rust ahp_types::telemetry (pub const &str) metrics facade (this PR) plus tracing diagnostics
Go telemetry.generated.go (consts) available
Swift AhpTelemetryNames (caseless enum) available
Kotlin AhpTelemetryNames (object) available
TypeScript the telemetry enums re-exported from the package root available

Full rationale and the per-client OTel-export story live in docs/specification/self-instrumentation.md.

How the names are represented

The names are string enums (TelemetrySpan, TelemetryMetric, TelemetryAttribute, TelemetryOutcome), the same shape as the protocol enums (e.g. ChangesetOperationStatus). Each name's description is a JSDoc comment on its enum member, extracted by the generators the same way as any other enum (member.getJsDocs(), via the shared scripts/read-telemetry.ts helper), so the description lives next to the name with no separate lookup table:

export enum TelemetryMetric {
  /** Round-trip duration of a JSON-RPC request, tagged by rpc.method and ahp.outcome (ok|error|cancelled|timeout). */
  RequestDuration = 'ahp.client.request.duration',
  ...
}

Metric units are a small Record<TelemetryMetric, string> map. The generators emit a flat string holder (telemetry names are consumed as raw strings by Meter and metrics, not as language enums), carrying each member's description through as a doc comment on the generated constant. types/telemetry/registry.test.ts guards the invariants (dotted names, unique, every metric has a unit), and a verify-generated gate fails CI if a holder drifts from the contract.

Rust reference adoption

The ahp client emits self-instrumentation metrics through the metrics facade (messages sent and received, request duration and in-flight, reconnects, dropped events per stream, malformed frames, with rpc.* and ahp.* attributes), all named from ahp_types::telemetry. The facade is a no-op until the host installs a recorder, so it is zero-cost when unobserved. A recorder-based integration test drives a request and asserts the metrics emit with the right attributes and that the in-flight gauge returns to zero on completion; a separate test asserts a cancelled request is tagged ahp.outcome=cancelled.

Verified

go build plus gofmt -l, cargo build plus cargo fmt --check, cargo test, cargo clippy, swift build, kotlin compileKotlin, the TypeScript tsc --noEmit, and the npm test suite (typecheck, lint, the verify-generated freshness gate, registry.test.ts, and the read-telemetry test) all pass.

Status

Draft until the scope question in #239 is decided. Open to adjusting the naming scheme, the per-language holder shape, which client adopts first, or dropping the Rust metrics dependency in favor of holders-only.

@joshmouch joshmouch force-pushed the pr/telemetry-names-contract branch from ea706e5 to 45034db Compare June 17, 2026 22:07
@joshmouch joshmouch changed the title Add a cross-client telemetry-names contract + generated per-client holders Add a cross-client telemetry-names contract (+ Rust reference adoption) Jun 17, 2026
@joshmouch joshmouch force-pushed the pr/telemetry-names-contract branch 3 times, most recently from 07b7794 to ab8250c Compare June 18, 2026 00:23
joshmouch added a commit to joshmouch/agent-host-protocol that referenced this pull request Jun 18, 2026
…to .NET holder

Bring the shared cross-client telemetry source-of-truth files to the SAME
bytes as the telemetry-names-contract PR (microsoft#237) so the two PRs stay
byte-identical for cross-PR dedup, then regenerate the .NET telemetry holder.

Byte-identical to microsoft#237:
  types/telemetry/registry.ts, registry.test.ts
  scripts/read-telemetry.test.ts, generate-kotlin.ts, generate-swift.ts

package.json: hand-applied the shared additions (verify:generated script +
test-glob types/telemetry/*.test.ts scripts/*.test.ts) while keeping this
branch's own generate:dotnet entry.

clients/dotnet .NET holder: regenerated via 'npm run generate:dotnet' so it
gains the host-* stream constants (StreamHostEvent/Subscription/Resource/
Snapshot/Summaries) emitted from the synced registry.

scripts/verify-generated.ts: body byte-identical to microsoft#237; HOLDERS list scoped
to the .NET holder this branch actually ships (the go/rust/kotlin/swift
telemetry holders are introduced by microsoft#237 and are not on this branch's
lineage, so a byte-identical 4-holder list would fail 'npm test' here).
joshmouch added a commit to joshmouch/agent-host-protocol that referenced this pull request Jun 18, 2026
…l example, DI interface

Land the .NET telemetry best-practices audit follow-ups on the microsoft#237 self-instrumentation base:

- MultiHostClient: route the 5 per-host drop tags (host-event/subscription/
  resource/snapshot/summaries) through the generated AhpTelemetryNames.StreamHost*
  constants instead of raw literals, cached as static KeyValuePairs (no per-callback
  alloc). Regression-guarded by HostStreamDropTagTests (source gate + runtime proof).
- MultiHostClient reconnect supervisor: emit the existing AhpTelemetry.Reconnects
  counter tagged ahp.outcome=ok on a successful reconnect and =error per failed
  attempt + on MaxAttempts exhaustion (previously uninstrumented).
- TelemetryTests: add reconnect / dropped-events / malformed-frames coverage and
  tag-VALUE assertions (rpc.method, ahp.outcome, ahp.message.kind, ahp.stream);
  reference AhpTelemetryNames constants instead of "ahp.client.*" literals.
- Extract IMultiHostClient interface; register it in AddAgentHostProtocol (same
  singleton). Add AhpServiceCollectionExtensions.TelemetrySourceName one-liner.
- De-duplicate instrument descriptions: generate AhpTelemetryNames.*Description
  constants (generate-csharp.ts) as the single source; Telemetry.cs references them.
- examples/OtelExport: Shape C consumer wiring OpenTelemetry (.AddSource/.AddMeter)
  with a console exporter over a self-contained loopback transport.

All 356 dotnet tests pass (Debug + Release); verify:generated + verify:changelog +
telemetry registry tests green; example builds and prints spans+metrics.
@joshmouch joshmouch force-pushed the pr/telemetry-names-contract branch from 3e623af to cea7757 Compare June 18, 2026 04:13
A single source of truth (types/telemetry/registry.ts) for the self-
instrumentation span / metric / attribute names every AHP client emits about
its own operation, consumed by the existing scripts/generate-*.ts codegen to
emit an idiomatic generated name-holder per client (Go / Rust / Swift / Kotlin
/ TypeScript), plus a generated-output freshness gate so the holders can't
drift.

The Rust client is the reference adopter: it emits the metrics through the
metrics facade (a no-op until the host installs a recorder), named entirely
from the generated holder, with a recorder-based emission test. Client self-
instrumentation is distinct from the protocol's OpenTelemetry-over-AHP channel;
see docs/specification/self-instrumentation.md.
@joshmouch joshmouch force-pushed the pr/telemetry-names-contract branch from cea7757 to aab017e Compare June 18, 2026 04:28
@joshmouch joshmouch marked this pull request as ready for review June 18, 2026 04:33
@joshmouch joshmouch marked this pull request as draft June 18, 2026 04:50
@connor4312

Copy link
Copy Markdown
Member

#239 (comment)

@connor4312 connor4312 closed this Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants