Skip to content

[otelgrpc] Finalize gRPC status overriding shape #9378

Description

@ps-mir

Stats handlers in otelgrpc use a fixed mapping from gRPC status codes to OTel span status, and the client treats every non-OK code as an error. This was raised in #9189. Overriding this behavior is allowed as per current spec. Check #9189 (comment).

While couple of approaches remain in progress, this issue tracks decision on choice of override option shape and associated trade-offs. At the moment no Otel SDK contrib offers any override option for gRPC.

Proposed Solution

Three mutually exclusive directions under discussion:

1. Full custom callback

otelgrpc.WithSpanStatusFn(func(ctx context.Context, fullMethod string, grpcStatus *status.Status) (codes.Code, string) {
    // arbitrary user logic
})
  • Max flexibility in terms of defining rules for user
  • Replaces the default mapping entirely, forcing callers to re-implement the standard OTel error cases to stay spec-compliant.
  • Runs arbitrary user logic on the hot path for every RPC. Has received push-back because of this.
  • Non representative in declarative config (otelconf), a function value can't be serialized into YAML/JSON config.
  • Shape is not compatible with any future declarative config mechanism.

Implementation Example

2. Per-method non-error code mapping

otelgrpc.WithMethodNonErrorCodes(map[string][]codes.Code{
    "/pkg.Svc/Lookup": {codes.NotFound},
})
  • Method specific mapping of NonErrorCodes.
  • Only exact match is supported. No regex matching support for methods. (New)
  • Allows the same gRPC code to be treated differently per RPC (e.g. NotFound is expected on Lookup but should still be an error on Delete).
  • Shape is compatible with future declarative config mechanism (data-only).

No Implementation example exists yet.

3. Global non-error codes

otelgrpc.WithNonErrorCodes(codes.NotFound)
  • Flat allowlist, applied uniformly to every RPC, the handler process.
  • Simplest option, data-shaped, met favorable reviews from Go SIG;
  • Shape is compatible with future declarative config mechanism (data-only).

Implementation example

4. Composition of Per Method (2) + Global Non Error Code (3)

Based on feedback from SIG

  • Two independent options to configure status code mapping at Per Method and Global level.
  • Precedence If Found (Method level mapping > Global mapping > SDK Default)
  • Chosen mapping is applied as replacement, not merge with existing mapping
  • RPC Metrics must remain untouched, never derived from overridden Span Status.
  • The change is symmetric to client/server.
  • No caller-supplied reference survives into the instrumentation config.

Prior Art (Generated by Claude)

Survey of gRPC status-to-span-status handling across OpenTelemetry language SDKs (as of 2026-07-29), none currently expose any user-facing override:

Language Server classification Override available?
Go (otelgrpc) 6-code allowlist → Error, else Unset No (this issue)
Java (grpc-1.6) identical 6-code allowlist, via internal GrpcSpanStatusExtractor No, the underlying SpanStatusExtractor extension point exists in instrumentation-api but is never exposed publicly by any instrumentation library's builder
Python (opentelemetry-instrumentation-grpc) identical 6-code allowlist No, only a method-instrumentation filter
.NET (opentelemetry-dotnet-contrib) identical 6-code allowlist, static switch No dedicated hook, only the generic Enrich*(Activity, ...) callback shared by all .NET instrumentations
JS (opentelemetry-js) no allowlist, any non-OK is Error, same as client No, only ignoreGrpcMethods
Ruby (opentelemetry-ruby-contrib, grpc/gruf) no code-based logic, relies on SDK default "exception raised inside in_span → Error" No, only a server method-ignore list
Notably, JS and Ruby don't even implement the server-side SHOULD-allowlist from the gRPC semantic conventions; they treat every non-OK code as an error on both client and server. That drift exists independently of this feature request and is worth keeping in mind when deciding how much cross-language consistency to require before merging an override API in Go.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions