Skip to content

Emit gen_ai.client.token.usage and operation.duration metrics from the OTel middleware - #666

Open
PratikDhanave (PratikDhanave) wants to merge 4 commits into
microsoft:mainfrom
PratikDhanaveFork:otel-emit-token-usage-duration-metrics
Open

Emit gen_ai.client.token.usage and operation.duration metrics from the OTel middleware#666
PratikDhanave (PratikDhanave) wants to merge 4 commits into
microsoft:mainfrom
PratikDhanaveFork:otel-emit-token-usage-duration-metrics

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

What

The OTel middleware previously recorded token counts purely as span attributes (gen_ai.usage.*) via setUsage, and never measured run duration at all. It imported only otel/{attribute,codes,trace} and held a lone tracer — no Meter, no histograms.

Span attributes describe a single run and are not aggregatable across runs, so a spend/latency dashboard summing across many invocations saw nothing from the Go port. This change adds a Meter alongside the tracer and emits the two histograms the OTel GenAI semantic conventions define:

  • gen_ai.client.token.usage (unit {token}) — two data points per run, one tagged gen_ai.token.type=input (InputTokenCount) and one output (OutputTokenCount).
  • gen_ai.client.operation.duration (unit s) — one sample per run, measured from before span.Start.

Every data point carries the run's identifying attributes (gen_ai.operation.name, gen_ai.provider.name, gen_ai.agent.name), plus error.type when the run faulted. Recording is factored into a recordMetrics helper (mirroring setUsage) shared by both the normal post-loop path and the early-return path. Histogram construction errors are tolerated: NewMiddleware still returns a working tracer-only middleware and recordMetrics no-ops on a nil histogram.

Why (cross-SDK parity)

This aligns the Go port with the OTel GenAI semantic conventions and the Python/.NET references, which both emit these two histograms. Cross-language dashboards can now aggregate Go runs alongside the rest. Span-attribute accounting stays exactly as-is; metrics complement it (single-run detail vs. cross-run aggregation) rather than replacing it. Not a duplicate of the span PRs (#655/#646) or usage-accounting PRs (#594/#609/#551-554).

Tests

provider/otelprovider/otel_test.go gains two black-box tests driving an in-memory sdkmetric.ManualReader:

  • TestOtel_Run_RecordsUsageAndDurationMetrics — runs the middleware over a fake RunFunc yielding UsageContent (100 input / 50 output), collects ResourceMetrics, and asserts the two gen_ai.client.token.usage data points (input=100, output=50 by gen_ai.token.type) with the shared attributes, plus one gen_ai.client.operation.duration sample.
  • TestOtel_Run_MetricsIncludeErrorType — asserts error.type is present on the duration metric when the run faults.

Both fail before the change and pass after. go build ./..., go vet ./provider/otelprovider/..., and go test ./provider/otelprovider/... are green.

Open design questions

  • Scope: should these live in the existing tracing middleware (as here) or a separate metrics middleware/config toggle? Kept together since they share the accumulated usage and the run boundary.
  • API shape: MiddlewareConfig is unchanged — the Meter reuses SourceName for its instrumentation scope, matching the tracer. Open to a separate meter name if preferred.
  • Follow-ups: AdditionalCounts and cache/reasoning sub-counters are not yet emitted as metric data points (the conventions only define input/output token-type dimensions); could be added if there's appetite.

@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the otel-emit-token-usage-duration-metrics branch from 026998e to 5773147 Compare July 23, 2026 15:40
@github-actions

This comment has been minimized.

@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the otel-emit-token-usage-duration-metrics branch from 5773147 to 23dfb1a Compare July 24, 2026 01:40
@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Jul 24, 2026
@github-actions

This comment has been minimized.

…el middleware

The OTel middleware recorded token counts only as span attributes, which are
not aggregatable across runs, so a spend/latency dashboard saw nothing from the
Go port. Add a Meter alongside the tracer and record the two histograms the
GenAI semantic conventions define and the Python/.NET SDKs emit:
gen_ai.client.token.usage ({token}) split by gen_ai.token.type input/output, and
gen_ai.client.operation.duration (s). Both are recorded on the normal and
early-return exit paths, tagged with operation/provider/agent name plus
error.type when the run faults. Histogram-construction failures leave a working
tracer-only middleware.
@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the otel-emit-token-usage-duration-metrics branch from 23dfb1a to 0076565 Compare July 24, 2026 09:34
@github-actions

This comment has been minimized.

# Conflicts:
#	go.mod
#	provider/otelprovider/otel.go
@github-actions

This comment has been minimized.

@PratikDhanave
PratikDhanave (PratikDhanave) marked this pull request as ready for review August 4, 2026 06:06
@PratikDhanave
PratikDhanave (PratikDhanave) requested a review from a team as a code owner August 4, 2026 06:06
Copilot AI lite review requested due to automatic review settings August 4, 2026 06:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the provider/otelprovider OpenTelemetry middleware to emit GenAI semantic-convention metrics in addition to existing span attributes, enabling cross-run aggregation for token usage and operation duration (dashboards/alerts) while preserving per-run span detail.

Changes:

  • Add OTel Metrics instruments (gen_ai.client.token.usage, gen_ai.client.operation.duration) to the existing tracing middleware and record them per run.
  • Capture and attach shared identifying attributes (and error.type on failures) to emitted metric data points.
  • Add black-box tests using an in-memory sdkmetric.ManualReader to assert emitted histograms and error attribution.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.

File Description
provider/otelprovider/otel.go Adds Meter-based histograms and records token-usage + duration metrics from the middleware.
provider/otelprovider/otel_test.go Adds metric-focused tests validating histogram emission and error tagging.
go.mod Promotes OTel metric and sdk/metric dependencies to direct requirements.
go.sum Records new module checksums introduced by metric dependencies.

Comment on lines +166 to +173
attrs := []attribute.KeyValue{
attribute.String(attrKeyOperationName, opInvoke),
attribute.String(attrKeyProviderName, cmp.Or(a.ProviderName(), "unknown")),
attribute.String(attrKeyAgentName, a.Name()),
}
if errorType != "" {
attrs = append(attrs, attribute.String(attrKeyErrorType, errorType))
}
@gdams

Copy link
Copy Markdown
Member

PratikDhanave (@PratikDhanave) conflicts

@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/otel Changes files in the provider / otel area size:large At most 300 changed lines across at most 10 files pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added failed-auto-risk Automatic risk classification was inconclusive or failed and removed pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress risk:medium Contained production impact requiring normal review depth and removed failed-auto-risk Automatic risk classification was inconclusive or failed pending-auto-risk Automatic risk classification is in progress labels Aug 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Parity Review — ✅ Approved

This PR adds gen_ai.client.token.usage and gen_ai.client.operation.duration histogram metrics to the Go OTel middleware. Parity check against upstream:

Python

python/packages/core/agent_framework/observability.py defines OtelAttr.LLM_TOKEN_USAGE = "gen_ai.client.token.usage" and OtelAttr.LLM_OPERATION_DURATION = "gen_ai.client.operation.duration", and emits both histograms with gen_ai.token.type=input/output data points — identical to this PR.

.NET

dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs delegates to OpenTelemetryChatClient which emits these same two histograms per the OTel GenAI semantic conventions.

Go contract

MiddlewareConfig (the only exported type) is unchanged. The histograms are wired internally in NewMiddleware. No new exported API surface — public-api-change label is not required.

Attribute keys used (gen_ai.operation.name, gen_ai.provider.name, gen_ai.agent.name, gen_ai.token.type, error.type) match the upstream Python and .NET usages. The parity-approved label is correct.

Generated by Go API Consistency Review Agent · sonnet46 · 36.3 AIC · ⌖ 4.73 AIC · ⊞ 6K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider/otel Changes files in the provider / otel area area:provider Changes files in the provider area parity-approved Go API consistency review found no parity issues risk:medium Contained production impact requiring normal review depth size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants