otel_grpc 0.2.0
otel_grpc: ^0.2.0 copied to clipboard
OpenTelemetry instrumentation for `package:grpc`. Client and server interceptors emitting rpc.* spans, propagating W3C trace context via gRPC metadata, with recursion suppression.
otel_grpc #
OpenTelemetry instrumentation for package:grpc,
built on the Dartastic OpenTelemetry SDK.
Two interceptors:
OTelGrpcClientInterceptor— attach to aClientChannelto get aCLIENT-kind span per outbound RPC, with W3C trace context injected into the call metadata.OTelGrpcServerInterceptor— add to aServer'sserverInterceptors:list to get aSERVER-kind span per inbound RPC, with W3C trace context extracted from the metadata so the span joins the caller's trace. The span wraps the whole handler invocation, so durations and status codes are real.
final channel = ClientChannel(
'api.example.com',
options: ChannelOptions(credentials: ChannelCredentials.secure()),
interceptors: [
OTelGrpcClientInterceptor(
// Optional: interceptors can't see the channel, so pass the
// host/port you gave the channel to get server.address/port
// on the spans.
serverAddress: 'api.example.com',
),
],
);
final server = Server.create(
services: [MyService()],
serverInterceptors: [OTelGrpcServerInterceptor()],
);
⚠️ Self-recursion: don't instrument your OTLP/gRPC export channel #
gRPC export over OTLP is itself gRPC. If you put
OTelGrpcClientInterceptor on a ClientChannel that's also used
by your OTLP/gRPC exporter, every span you export creates another
span (the export call), which gets exported, which creates another
span — instrumentation recursion until you blow the stack or
saturate your backend.
You're safe by default — the dartastic SDK's built-in
OTLP/gRPC exporter creates its own private ClientChannel that
your interceptor isn't attached to. The risk only appears if you
manually wire OTLP export over a ClientChannel you control. If
that's your setup, do one of:
- Don't attach the interceptor to that channel. Use a separate, dedicated channel for OTLP traffic.
- Wrap export calls in the suppression helper:
The interceptor checks a zone-scoped flag and bails before creating the span. Sync variant:import 'package:otel_grpc/otel_grpc.dart'; await runWithoutGrpcInstrumentationAsync(() async { await myOtlpClient.export(spans); });runWithoutGrpcInstrumentation.
Span shape #
Per the OTel RPC semantic conventions and gRPC sub-spec.
| Attribute | Source | Set on |
|---|---|---|
rpc.system.name |
constant grpc |
client + server |
rpc.method |
fully-qualified <service>/<method> on the client; ServiceMethod.name on the server (ServiceMethod doesn't carry the parent Service) |
client + server |
rpc.response.status_code |
canonical status name (OK, NOT_FOUND, DEADLINE_EXCEEDED, ...) |
both, on completion |
server.address / server.port |
constructor parameters (interceptors can't see the channel) | client, when provided |
error.type |
exception's runtime class | client + server, on error |
The registry-deprecated keys are not emitted: rpc.system became
rpc.system.name, rpc.service folds into the fully-qualified
rpc.method, and the numeric rpc.grpc.status_code became the string
form on rpc.response.status_code.
- Span name:
<service>/<method>on the client (e.g.com.example.UserService/GetUser);<method>on the server. - Span kind:
CLIENTfor outbound,SERVERfor inbound. - Span status:
Errorfor any non-OK gRPC status or thrown exception; otherwise unset.
W3C trace context propagation #
- Outbound:
traceparent/tracestate/baggageare added toCallOptions.metadataso the receiving service joins the same trace. - Inbound: those same headers are extracted from
ServiceCall.clientMetadataand used to parent the server span.
If the receiving service also runs this package's server interceptor (or any OTel-aware gRPC server), spans on both sides join into a single distributed trace automatically.
Caveats #
- Both interceptors call
OTel.tracerProvider().getTracer(...)in their constructors —OTel.initialize()must have run first. - The server interceptor is the class-based
ServerInterceptor(package:grpc>= 4.1.0), which wraps the whole invocation — the span ends when the response stream completes or errors, so streaming-call durations are real. It goes inserverInterceptors:, not the legacy function-typedefinterceptors:list. - Streaming client calls hang completion off
ResponseStream.trailers, which fires after the stream is closed (success) or errors (failure) — that's the correct end-of-call signal perpackage:grpc's contract.
License #
Apache 2.0 — see LICENSE.