-
-
Notifications
You must be signed in to change notification settings - Fork 287
feat(grpc): Add integration support for GRPC #3721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e0269c3
9a83568
94651a7
ba3e62b
53925f2
05509c1
247fbe6
5e7efa0
f5d1d92
c84cccc
cf2eeaf
1ea8728
3932a15
a4d676d
0cb3cb8
a0666ee
311bc70
166b609
0623fa7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -414,6 +414,61 @@ | |||||
| /// The database name. | ||||||
| // TODO: deprecated, needs to be replaced later by db.namespace | ||||||
| static const dbName = 'db.name'; | ||||||
|
|
||||||
| /// The RPC system identifier. For gRPC, always `'grpc'`. | ||||||
| /// | ||||||
| /// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/ | ||||||
| static const rpcSystemName = 'rpc.system.name'; | ||||||
|
Check warning on line 421 in packages/dart/lib/src/constants.dart
|
||||||
|
|
||||||
| /// The fully-qualified logical name of the method (`ServiceName/MethodName`). | ||||||
| /// | ||||||
| /// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/ | ||||||
| static const rpcMethod = 'rpc.method'; | ||||||
|
|
||||||
| /// The string representation of the gRPC status code (e.g., `'OK'`, `'NOT_FOUND'`). | ||||||
| /// | ||||||
| /// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/ | ||||||
| static const rpcResponseStatusCode = 'rpc.response.status_code'; | ||||||
|
Check warning on line 431 in packages/dart/lib/src/constants.dart
|
||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The constant value is Evidence
Suggested fix: Change the string value to the correct OTel attribute name.
Suggested change
Identified by Warden find-bugs ยท GCT-9GL
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
|
|
||||||
| /// Prefix for RPC request metadata. Append the lowercase metadata key. | ||||||
| /// | ||||||
| /// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/ | ||||||
|
Check warning on line 435 in packages/dart/lib/src/constants.dart
|
||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rpcRequestMetadataPrefix uses 'rpc.request.metadata.' instead of OTel-specified 'rpc.grpc.request.metadata.' The OTel gRPC spec (linked in the doc comment) scopes request metadata under Evidence
Also found at 1 additional location
Identified by Warden code-review ยท B83-A56 |
||||||
| static const rpcRequestMetadataPrefix = 'rpc.request.metadata.'; | ||||||
|
|
||||||
| /// The reason string from a gRPC ErrorInfo error detail. | ||||||
| static const grpcErrorInfoReason = 'grpc.error_info.reason'; | ||||||
|
|
||||||
| /// The domain string from a gRPC ErrorInfo error detail. | ||||||
| static const grpcErrorInfoDomain = 'grpc.error_info.domain'; | ||||||
|
|
||||||
| /// The metadata map from a gRPC ErrorInfo error detail. | ||||||
| static const grpcErrorInfoMetadata = 'grpc.error_info.metadata'; | ||||||
|
|
||||||
| /// The field violations from a gRPC BadRequest error detail. | ||||||
| static const grpcBadRequestFieldViolations = | ||||||
| 'grpc.bad_request.field_violations'; | ||||||
|
|
||||||
| /// The retry delay from a gRPC RetryInfo error detail. | ||||||
| static const grpcRetryInfoRetryDelay = 'grpc.retry_info.retry_delay'; | ||||||
|
|
||||||
| /// The detail string from a gRPC DebugInfo error detail. | ||||||
| static const grpcDebugInfoDetail = 'grpc.debug_info.detail'; | ||||||
|
|
||||||
| /// The violations from a gRPC PreconditionFailure error detail. | ||||||
| static const grpcPreconditionFailureViolations = | ||||||
| 'grpc.precondition_failure.violations'; | ||||||
|
|
||||||
| /// The resource type from a gRPC ResourceInfo error detail. | ||||||
| static const grpcResourceInfoType = 'grpc.resource_info.type'; | ||||||
|
|
||||||
| /// The resource name from a gRPC ResourceInfo error detail. | ||||||
| static const grpcResourceInfoName = 'grpc.resource_info.name'; | ||||||
|
|
||||||
| /// The description from a gRPC ResourceInfo error detail. | ||||||
| static const grpcResourceInfoDescription = 'grpc.resource_info.description'; | ||||||
|
|
||||||
| /// The violations from a gRPC QuotaFailure error detail. | ||||||
| static const grpcQuotaFailureViolations = 'grpc.quota_failure.violations'; | ||||||
| } | ||||||
|
|
||||||
| /// Attribute keys emitted by the SDK that are not (yet) codified in | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Changelog |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| include: package:lints/recommended.yaml | ||
|
|
||
| analyzer: | ||
| language: | ||
| strict-casts: true | ||
| strict-inference: true | ||
| strict-raw-types: true | ||
| errors: | ||
| missing_required_param: error | ||
| missing_return: error | ||
| todo: ignore | ||
| deprecated_member_use_from_same_package: warning | ||
| invalid_dependency: ignore | ||
| unnecessary_import: ignore | ||
| exclude: | ||
| - example/** | ||
|
|
||
| linter: | ||
| rules: | ||
| - prefer_final_locals | ||
| - public_member_api_docs | ||
| - prefer_single_quotes | ||
| - prefer_relative_imports | ||
| - unnecessary_brace_in_string_interps | ||
| - implementation_imports | ||
| - require_trailing_commas | ||
| - unawaited_futures |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Miscellaneous | ||
| *.class | ||
| *.log | ||
| *.pyc | ||
| *.swp | ||
| .DS_Store | ||
| .atom/ | ||
| .build/ | ||
| .buildlog/ | ||
| .history | ||
| .svn/ | ||
| .swiftpm/ | ||
| migrate_working_dir/ | ||
|
|
||
| # IntelliJ related | ||
| *.iml | ||
| *.ipr | ||
| *.iws | ||
| .idea/ | ||
|
|
||
| # The .vscode folder contains launch configuration and tasks you configure in | ||
| # VS Code which you may wish to be included in version control, so this line | ||
| # is commented out by default. | ||
| #.vscode/ | ||
|
|
||
| # Flutter/Dart/Pub related | ||
| **/doc/api/ | ||
| **/ios/Flutter/.last_build_id | ||
| .dart_tool/ | ||
| .flutter-plugins-dependencies | ||
| .pub-cache/ | ||
| .pub/ | ||
| /build/ | ||
| /coverage/ | ||
|
|
||
| # Symbolication related | ||
| app.*.symbols | ||
|
|
||
| # Obfuscation related | ||
| app.*.map.json | ||
|
|
||
| # Platform folders (Dart-only example, no platform targets committed) | ||
| /android/ | ||
| /ios/ | ||
| /linux/ | ||
| /macos/ | ||
| /web/ | ||
| /windows/ | ||
|
|
||
| # Breadcrumb and Event cache. | ||
| .sentry-native | ||
|
|
||
| # Flutter tool metadata (not needed for building). | ||
| .metadata |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # grpc4 โ sentry_grpc Dart example | ||
|
|
||
| Dart CLI example demonstrating `SentryGrpcInterceptor` with grpc 4.x. | ||
|
|
||
| ## What it shows | ||
|
|
||
| - `Sentry.init` with `tracesSampleRate` and `captureFailedRequests` | ||
| - `SentryGrpcInterceptor` attached to a `ClientChannel` targeting `grpcb.in:9001` | ||
| - Manual transaction wrapping around each RPC call | ||
|
|
||
| ## Calls | ||
|
|
||
| | Call | Endpoint | Purpose | | ||
| |------|----------|---------| | ||
| | Empty | `GRPCBin/Empty` | Successful unary RPC; creates a span | | ||
| | DummyUnary | `GRPCBin/DummyUnary` | Sends a string, echoes it back (hand-encoded proto) | | ||
| | RandomError | `GRPCBin/RandomError` | Randomly fails; tests error span + capture | | ||
|
|
||
| ## Differences from grpc5 | ||
|
|
||
| | | grpc4 | grpc5 | | ||
| |-|-------|-------| | ||
| | grpc version | 4.x | 5.x | | ||
| | Proto encoding | Inline byte helpers | Typed `DummyMessage` class | | ||
| | WithHeaders call | No | Yes | | ||
|
|
||
| ## Run | ||
|
|
||
| ```sh | ||
| cd packages/grpc/example/grpc4 | ||
| dart run lib/main.dart | ||
| ``` | ||
|
|
||
| Set your DSN in `lib/app_config.dart` before running. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| include: package:lints/recommended.yaml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io | ||
| const String exampleDsn = | ||
| 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import 'dart:convert'; | ||
|
|
||
| import 'package:grpc/grpc.dart'; | ||
| import 'package:sentry/sentry.dart'; | ||
| import 'package:sentry_grpc/sentry_grpc.dart'; | ||
|
|
||
| import 'app_config.dart' as config; | ||
|
|
||
| Future<void> main() async { | ||
| await Sentry.init( | ||
| (options) { | ||
| options.dsn = config.exampleDsn; | ||
| options.tracesSampleRate = 1.0; | ||
| options.captureFailedRequests = true; | ||
| options.debug = true; | ||
| }, | ||
| appRunner: _runApp, | ||
| ); | ||
| } | ||
|
|
||
| Future<void> _runApp() async { | ||
| final channel = ClientChannel( | ||
| 'grpcb.in', | ||
| port: 9001, | ||
| options: const ChannelOptions(credentials: ChannelCredentials.secure()), | ||
| ); | ||
| final client = _GrpcBinClient( | ||
| channel, | ||
| interceptors: [SentryGrpcInterceptor(captureFailedRequests: true)], | ||
| ); | ||
|
|
||
| try { | ||
| await _emptyCall(client); | ||
| await _dummyUnaryCall(client); | ||
| await _randomErrorCall(client); | ||
| } finally { | ||
| await channel.shutdown(); | ||
| await Sentry.close(); | ||
| } | ||
| } | ||
|
|
||
| Future<void> _emptyCall(_GrpcBinClient client) async { | ||
| print('--- GRPCBin/Empty ---'); | ||
| final tr = Sentry.startTransaction( | ||
| 'grpcb-empty', | ||
| 'grpc.client', | ||
| bindToScope: true, | ||
| ); | ||
| try { | ||
| await client.empty(); | ||
| tr.status = const SpanStatus.ok(); | ||
| print('OK'); | ||
| } catch (e, s) { | ||
| tr.throwable = e; | ||
| tr.status = const SpanStatus.internalError(); | ||
| await Sentry.captureException(e, stackTrace: s); | ||
| print('FAILED: $e'); | ||
| } finally { | ||
| await tr.finish(); | ||
| } | ||
| } | ||
|
|
||
| Future<void> _dummyUnaryCall(_GrpcBinClient client) async { | ||
| print('--- GRPCBin/DummyUnary ---'); | ||
| final tr = Sentry.startTransaction( | ||
| 'grpcb-dummy-unary', | ||
| 'grpc.client', | ||
| bindToScope: true, | ||
| ); | ||
| try { | ||
| final response = await client.dummyUnary( | ||
| _encodeDummyMessage('hello from sentry_grpc'), | ||
| ); | ||
| tr.status = const SpanStatus.ok(); | ||
| print('echo: "${_decodeDummyFString(response)}"'); | ||
| } catch (e, s) { | ||
| tr.throwable = e; | ||
| tr.status = const SpanStatus.internalError(); | ||
| await Sentry.captureException(e, stackTrace: s); | ||
| print('FAILED: $e'); | ||
| } finally { | ||
| await tr.finish(); | ||
| } | ||
| } | ||
|
|
||
| Future<void> _randomErrorCall(_GrpcBinClient client) async { | ||
| print('--- GRPCBin/RandomError ---'); | ||
| final tr = Sentry.startTransaction( | ||
| 'grpcb-random-error', | ||
| 'grpc.client', | ||
| bindToScope: true, | ||
| ); | ||
| try { | ||
| await client.randomError(); | ||
| tr.status = const SpanStatus.ok(); | ||
| print('OK'); | ||
| } catch (e, s) { | ||
| tr.throwable = e; | ||
| tr.status = const SpanStatus.internalError(); | ||
| await Sentry.captureException(e, stackTrace: s); | ||
| print('FAILED: $e'); | ||
| } finally { | ||
| await tr.finish(); | ||
| } | ||
| } | ||
|
|
||
| List<int> _encodeDummyMessage(String value) { | ||
| final bytes = utf8.encode(value); | ||
| return [0x0A, bytes.length, ...bytes]; | ||
| } | ||
|
|
||
| String _decodeDummyFString(List<int> bytes) { | ||
| if (bytes.length < 2 || bytes[0] != 0x0A) return '(no f_string)'; | ||
| final len = bytes[1]; | ||
| if (bytes.length < 2 + len) return '(truncated)'; | ||
| return utf8.decode(bytes.sublist(2, 2 + len)); | ||
| } | ||
|
|
||
| class _GrpcBinClient extends Client { | ||
| static final _emptyCall = ClientMethod<List<int>, List<int>>( | ||
| '/grpcbin.GRPCBin/Empty', | ||
| (data) => data, | ||
| (data) => data, | ||
| ); | ||
|
|
||
| static final _dummyUnaryCall = ClientMethod<List<int>, List<int>>( | ||
| '/grpcbin.GRPCBin/DummyUnary', | ||
| (data) => data, | ||
| (data) => data, | ||
| ); | ||
|
|
||
| static final _randomErrorCall = ClientMethod<List<int>, List<int>>( | ||
| '/grpcbin.GRPCBin/RandomError', | ||
| (data) => data, | ||
| (data) => data, | ||
| ); | ||
|
|
||
| _GrpcBinClient(super.channel, {super.interceptors}); | ||
|
|
||
| ResponseFuture<List<int>> empty() => | ||
| $createUnaryCall(_emptyCall, const <int>[]); | ||
|
|
||
| ResponseFuture<List<int>> dummyUnary(List<int> request) => | ||
| $createUnaryCall(_dummyUnaryCall, request); | ||
|
|
||
| ResponseFuture<List<int>> randomError() => | ||
| $createUnaryCall(_randomErrorCall, const <int>[]); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| name: sentry_grpc_example_grpc4 | ||
| description: sentry_grpc example using grpc 4.x | ||
| version: 1.0.0+1 | ||
| publish_to: none | ||
|
|
||
| environment: | ||
| sdk: '>=3.5.0 <4.0.0' | ||
|
|
||
| dependencies: | ||
| grpc: '>=4.0.4 <5.0.0' | ||
| sentry: 9.20.0 | ||
| sentry_grpc: | ||
| path: ../.. | ||
|
|
||
| dev_dependencies: | ||
| lints: ^5.0.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| dependency_overrides: | ||
| sentry: | ||
| path: ../../../dart | ||
| sentry_grpc: | ||
| path: ../.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rpcSystemNamestring value diverges from OTel spec ('rpc.system.name'vs'rpc.system')The constant value is
'rpc.system.name'but the OpenTelemetry RPC semantic convention (cited in the doc comment) defines this attribute asrpc.system. Spans emitted with this key will not be recognized by OTel-compatible backends (Grafana, Jaeger, etc.) that filter or group byrpc.system.Evidence
rpc.system, notrpc.system.name.'rpc.system.name'is incorrect._attachRpcAttributesinsentry_grpc_interceptor.dartcallsspan.setData(SemanticAttributesConstants.rpcSystemName, 'grpc'), so spans in production will carry the non-standard key.SemanticAttributesConstants.rpcSystemName(line 115 in test file), so they pass but validate the wrong key.Suggested fix: Change the string value to the correct OTel attribute name.
Identified by Warden find-bugs ยท U3Z-TQL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rpc.namedoesn't exist