tonic-rails 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -2
  3. data/Cargo.lock +5 -5
  4. data/README.md +204 -13
  5. data/ext/tonic_rails_native/Cargo.toml +2 -2
  6. data/ext/tonic_rails_native/src/body.rs +113 -49
  7. data/ext/tonic_rails_native/src/dispatch.rs +67 -8
  8. data/ext/tonic_rails_native/src/framing.rs +50 -0
  9. data/ext/tonic_rails_native/src/gvl.rs +109 -21
  10. data/ext/tonic_rails_native/src/lib.rs +6 -1
  11. data/ext/tonic_rails_native/src/log.rs +131 -0
  12. data/ext/tonic_rails_native/src/net.rs +7 -1
  13. data/ext/tonic_rails_native/src/server.rs +67 -6
  14. data/lib/generators/tonic_rails/install/install_generator.rb +45 -0
  15. data/lib/generators/tonic_rails/install/templates/tonic_rails.rb.tt +21 -0
  16. data/lib/generators/tonic_rails/service/service_generator.rb +110 -0
  17. data/lib/generators/tonic_rails/service/templates/handler.rb.tt +38 -0
  18. data/lib/generators/tonic_rails/service/templates/service.proto.tt +16 -0
  19. data/lib/generators/tonic_rails/service/templates/service_spec.rb.tt +17 -0
  20. data/lib/generators/tonic_rails/service/templates/service_test.rb.tt +14 -0
  21. data/lib/puma/plugin/tonic_rails.rb +116 -0
  22. data/lib/tonic/rails/base.rb +169 -23
  23. data/lib/tonic/rails/configuration.rb +27 -4
  24. data/lib/tonic/rails/health.rb +105 -0
  25. data/lib/tonic/rails/instrumentation.rb +78 -0
  26. data/lib/tonic/rails/metadata.rb +50 -0
  27. data/lib/tonic/rails/peer.rb +60 -0
  28. data/lib/tonic/rails/railtie.rb +2 -2
  29. data/lib/tonic/rails/router.rb +1 -0
  30. data/lib/tonic/rails/rpc_error.rb +73 -6
  31. data/lib/tonic/rails/server.rb +82 -12
  32. data/lib/tonic/rails/tasks/tonic-rails.rake +13 -1
  33. data/lib/tonic/rails/testing/errors.rb +47 -0
  34. data/lib/tonic/rails/testing/minitest.rb +44 -0
  35. data/lib/tonic/rails/testing/rspec.rb +53 -0
  36. data/lib/tonic/rails/testing.rb +35 -4
  37. data/lib/tonic/rails/version.rb +1 -1
  38. data/lib/tonic-rails.rb +4 -0
  39. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6c9da1fa1dde14dab1bd46c64821154a392dd9e5b5bc31faf396cf8e51dfc28
4
- data.tar.gz: f0988c4f232fb3016b205d094ea0970085e7515f71f6845e55bd8bc6d43c7d70
3
+ metadata.gz: c1578d591791ff33ce8365ae3fe5b5a6bb5dfbac8759e4f9762c7c511e71057a
4
+ data.tar.gz: e23782ccd2cf964d5db2cf25c98547780525e20d7880f0dab85a0c4c956b45cd
5
5
  SHA512:
6
- metadata.gz: 82a6eb4a8da4daaf1f91e6ead904b1223c13cc5b02f90e9bebf810ca7fc25bd71cb30cce07e539fde05b81b12fd89e84d540fc5aa5a22d38d7697aacb746a1c0
7
- data.tar.gz: 87dbd3f3abcbdefd3d6f47ad8b93ea992672efe67c643bcf47e75e255e920983ab6695ecfea8eb4273db390f57e6de419b9c369ad5067f0e07a0d086a7c9e218
6
+ metadata.gz: 616f736a4e29cd3fcf8fd4297d6159d9abb145f3973b27a08f6f2c153bf7f5ae87422870ef304dab552e62f8ef7b3037716432c89bfd5d12088ef97e1ae58166
7
+ data.tar.gz: 07a9749e9188700473883761291731a3f0b947bcdf4d986cd55df81dcb658229863149cfda1174a186a0cfe321e8e09810b7c1113232fefdc38c0ca2f1bba962
data/CHANGELOG.md CHANGED
@@ -7,7 +7,75 @@ minor versions may contain breaking changes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.1.0]
10
+ ## [0.3.0] - 2026-10-11
11
+
12
+ ### Added
13
+
14
+ - Generators: `rails g tonic_rails:install` (initializer, proto and
15
+ generated-code directories, Puma plugin) and `rails g tonic_rails:service
16
+ Item GetItem ListItems:server` (proto, handler with a stub per RPC,
17
+ registration, and an RSpec or Minitest test).
18
+ - Controller-style callbacks for handlers: `before_rpc`, `after_rpc` and
19
+ `around_rpc` with `only:` / `except:`, plus `current_rpc` and `request`
20
+ accessors.
21
+ - `rescue_from` (ActiveSupport::Rescuable) with a `code:` shorthand that maps
22
+ an exception class to a gRPC status.
23
+ - `peer` in handlers: the client's address and, with mutual TLS, its
24
+ certificate (`peer.certificate`, an `OpenSSL::X509::Certificate`).
25
+ `Testing.call`/`invoke` accept `peer:`.
26
+ - App-controlled health: `config.health_check` (polled every
27
+ `health_check_interval` seconds inside the Rails executor) and
28
+ `Tonic::Rails.server.serving!` / `not_serving!` for the whole server or
29
+ individual services, so readiness probes reflect real dependency health.
30
+ - Test helpers: the `raise_grpc_error(code).with_message(...)` RSpec matcher
31
+ (`require "tonic/rails/testing/rspec"`) and `assert_grpc_error` for Minitest
32
+ (`Tonic::Rails::Testing::Assertions`). Both accept `Tonic::Rails::RpcError`
33
+ and the grpc gem's `GRPC::BadStatus`.
34
+
35
+ ### Changed
36
+
37
+ - `tonic_rails:compile_protos` requires protoc 22 or newer and says so,
38
+ instead of generating code google-protobuf 4 can't load (older protoc,
39
+ such as Ubuntu's 3.21 package, emits a Ruby DSL that google-protobuf 4
40
+ removed).
41
+
42
+ ## [0.2.0] - 2026-10-11
43
+
44
+ ### Added
45
+
46
+ - Instrumentation: every RPC publishes a `process_rpc.tonic_rails`
47
+ ActiveSupport::Notifications event (path, service, rpc, metadata, status,
48
+ code) for APM agents, OpenTelemetry and custom subscribers. With no
49
+ subscribers it costs nothing measurable.
50
+ - Response metadata: handlers set `response_metadata` (headers) and
51
+ `trailing_metadata` (trailers, sent on success or failure).
52
+ - Rich errors: `RpcError.new(..., metadata:, details:)` sends trailing
53
+ metadata and a `google.rpc.Status` with packed details in
54
+ `grpc-status-details-bin`, which official gRPC clients decode.
55
+ - Puma plugin: `plugin :tonic_rails` in `config/puma.rb` runs the gRPC
56
+ server as a child process of Puma, in single and cluster mode, with
57
+ graceful stop and restart, and a shutdown if either side dies.
58
+ - Windows: precompiled `x64-mingw-ucrt` gems, tested in CI.
59
+ - `Tonic::Rails::Testing.invoke` returns the response together with its
60
+ headers and trailers.
61
+
62
+ ### Changed
63
+
64
+ - Response headers are sent with the handler's first output instead of
65
+ before the handler runs, so handlers can set them. The client deadline
66
+ applies while waiting.
67
+ - `log_requests` logs one line per RPC through the configured (Rails)
68
+ logger instead of stderr.
69
+ - Transport events from the native extension (rejected calls, expired
70
+ deadlines, refused connections, faults) go through the configured (Rails)
71
+ logger with severities instead of stderr. Handler outcomes, such as a
72
+ `NOT_FOUND` raised by a handler, are no longer logged by the transport;
73
+ use `log_requests` or the instrumentation event for those.
74
+ - `Server#stop` signals shutdown through a queue instead of a pipe, so
75
+ signal handling works the same on every platform.
76
+ - Dependencies: magnus 0.9, google-protobuf 4.36.
77
+
78
+ ## [0.1.0] - 2026-10-10
11
79
 
12
80
  First public release.
13
81
 
@@ -48,5 +116,7 @@ First public release.
48
116
  - Precompiled native gems for Linux (x86_64, aarch64; glibc and musl) and
49
117
  macOS (x86_64, arm64), Ruby 3.3 to 4.0.
50
118
 
51
- [Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.1.0...HEAD
119
+ [Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.3.0...HEAD
120
+ [0.3.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.2.0...v0.3.0
121
+ [0.2.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.1.0...v0.2.0
52
122
  [0.1.0]: https://github.com/CodingAnarchy/tonic-rails/releases/tag/v0.1.0
data/Cargo.lock CHANGED
@@ -469,9 +469,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
469
469
 
470
470
  [[package]]
471
471
  name = "magnus"
472
- version = "0.8.3"
472
+ version = "0.9.2"
473
473
  source = "registry+https://github.com/rust-lang/crates.io-index"
474
- checksum = "86a4f30dd400bf26769435129d7ecc529f56e3d45012ee6c394d6dbff3296bf9"
474
+ checksum = "b9068710e00d21be762edca59b9c4c8946dbdb29820d4f76ad559afea07982c9"
475
475
  dependencies = [
476
476
  "magnus-macros",
477
477
  "rb-sys",
@@ -481,9 +481,9 @@ dependencies = [
481
481
 
482
482
  [[package]]
483
483
  name = "magnus-macros"
484
- version = "0.8.0"
484
+ version = "0.9.1"
485
485
  source = "registry+https://github.com/rust-lang/crates.io-index"
486
- checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
486
+ checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
487
487
  dependencies = [
488
488
  "proc-macro2",
489
489
  "quote",
@@ -1023,7 +1023,7 @@ dependencies = [
1023
1023
 
1024
1024
  [[package]]
1025
1025
  name = "tonic_rails_native"
1026
- version = "0.1.0"
1026
+ version = "0.3.0"
1027
1027
  dependencies = [
1028
1028
  "axum",
1029
1029
  "bytes",
data/README.md CHANGED
@@ -30,10 +30,11 @@ end
30
30
 
31
31
  ## Why tonic-rails
32
32
 
33
- - **Fast where it counts.** Transport overhead is small: an empty RPC
34
- runs at roughly 75–80% of a pure-Rust tonic server's throughput. I/O-bound
35
- handlers scale with dispatch threads because Ruby releases the GVL
36
- while waiting. See [PERF.md](PERF.md).
33
+ - **Fast where it counts.** Inside a Rails app, light RPCs ran at 5–6×
34
+ the throughput of an equivalent Rails JSON endpoint and 1.4–3× the
35
+ official `grpc` gem, and server streaming at 4–7× the `grpc` gem.
36
+ Handlers dominated by slow queries or heavy object building see smaller
37
+ gains. See [PERF.md](PERF.md).
37
38
  - **All four RPC kinds.** Unary, server streaming, client streaming and
38
39
  bidirectional streaming, with backpressure in both directions.
39
40
  - **Production defaults.** Graceful draining on SIGTERM, health checks
@@ -44,7 +45,7 @@ end
44
45
  Reflection is off unless you turn it on. TLS problems stop the server
45
46
  instead of silently falling back to plaintext.
46
47
  - **No Rust toolchain needed** on the platforms we ship precompiled gems
47
- for (Linux and macOS, x86_64 and arm64).
48
+ for: Linux and macOS (x86_64 and arm64) and Windows (x64).
48
49
 
49
50
  ## Installation
50
51
 
@@ -59,10 +60,31 @@ On platforms without a precompiled gem, installation builds the
59
60
  extension from source, which needs a [Rust toolchain](https://rustup.rs).
60
61
 
61
62
  To generate Ruby classes from your `.proto` files you also need `protoc`
62
- (the Protocol Buffers compiler) during development.
63
+ (the Protocol Buffers compiler) **22.0 or newer** during development.
64
+ Older versions, such as Ubuntu's `protobuf-compiler` package (3.21),
65
+ generate code that google-protobuf 4 can't load; install a current
66
+ release from the [protobuf releases page](https://github.com/protocolbuffers/protobuf/releases)
67
+ or Homebrew.
63
68
 
64
69
  ## Quick start
65
70
 
71
+ The generators set everything up:
72
+
73
+ ```sh
74
+ bin/rails generate tonic_rails:install
75
+ bin/rails generate tonic_rails:service Item GetItem ListItems:server
76
+ bin/rails tonic_rails:compile_protos
77
+ ```
78
+
79
+ `install` adds `config/initializers/tonic_rails.rb` (load path for
80
+ generated code, configuration, handler registration), `app/protos/`,
81
+ `lib/grpc/`, and the Puma plugin. `service` writes the `.proto`, a handler
82
+ in `app/grpc/` with a stub per RPC, its registration, and a spec (or
83
+ Minitest test). RPCs take an optional `:server`, `:client` or `:bidi`
84
+ streaming kind.
85
+
86
+ The rest of this section shows the same steps by hand.
87
+
66
88
  ### 1. Define the service
67
89
 
68
90
  ```protobuf
@@ -139,7 +161,8 @@ bin/rails tonic_rails:routes # lists registered methods
139
161
  ```
140
162
 
141
163
  Run it as its own process (a separate Procfile entry, container or
142
- Kubernetes deployment) next to your web server.
164
+ Kubernetes deployment) next to your web server, or let Puma run it for
165
+ you (see [Running with Puma](#running-with-puma)).
143
166
 
144
167
  The [example app](example/) is a complete, runnable version of all of the
145
168
  above.
@@ -234,11 +257,54 @@ Any other exception becomes `INTERNAL` with the message `internal error`.
234
257
  The real exception is logged and passed to `Rails.error.report`, so your
235
258
  error tracker sees it.
236
259
 
260
+ ### Callbacks and rescue_from
261
+
262
+ Handlers get controller-style callbacks and exception mapping:
263
+
264
+ ```ruby
265
+ class ItemService
266
+ include Tonic::Rails::Base
267
+ rpc_service "demo.v1.ItemService"
268
+
269
+ before_rpc :authenticate, except: :ListItems
270
+ around_rpc :with_tenant
271
+ after_rpc { StatsD.increment("grpc.#{current_rpc}") }
272
+
273
+ rescue_from ActiveRecord::RecordNotFound, code: :not_found
274
+ rescue_from ActiveRecord::RecordInvalid, code: :invalid_argument, message: ->(e) { e.record.errors.full_messages.join(", ") }
275
+ rescue_from Pundit::NotAuthorizedError, with: :forbidden
276
+
277
+ private
278
+
279
+ def authenticate
280
+ @user = User.from_token(grpc_metadata["authorization"])
281
+ raise Tonic::Rails::RpcError.new("invalid token", code: :unauthenticated) unless @user
282
+ end
283
+
284
+ def with_tenant(&) = Tenant.switch(@user.tenant, &)
285
+
286
+ def forbidden(_e) = raise(Tonic::Rails::RpcError.new("not allowed", code: :permission_denied))
287
+ end
288
+ ```
289
+
290
+ - `before_rpc`, `after_rpc` and `around_rpc` accept method names or a
291
+ block, plus `only:` / `except:` with RPC names (`:GetItem` or
292
+ `:get_item`). Raising in a `before_rpc` stops the call.
293
+ - `current_rpc` is the RPC being handled and `request` the decoded
294
+ request, so callbacks can inspect it.
295
+ - `after_rpc` runs once a streamed response has been fully sent.
296
+ - `rescue_from` covers the handler and its callbacks. With `code:`, the
297
+ exception's message is sent to the client unless you pass `message:`.
298
+ Handlers given with `with:` or a block must raise an `RpcError`.
299
+ Anything unmapped becomes `INTERNAL`, as before.
300
+
237
301
  ### Metadata, deadlines and shutdown
238
302
 
239
303
  ```ruby
240
304
  def get_item(request)
241
305
  token = grpc_metadata["authorization"] # request headers, lower-case
306
+ peer.ip # client address ("10.0.4.17")
307
+ peer.certificate&.subject # client certificate, with mutual TLS
242
308
  deadline_ms # ms left before the client's deadline, or nil
243
309
  deadline_exceeded? # true once it has passed
244
310
  cancelled? # true while the server shuts down
@@ -248,6 +314,39 @@ end
248
314
  When the deadline passes, the client gets `DEADLINE_EXCEEDED` right away;
249
315
  check `deadline_exceeded?` in long loops to stop wasted work.
250
316
 
317
+ ### Response metadata
318
+
319
+ Set response headers and trailers from a handler:
320
+
321
+ ```ruby
322
+ def get_item(request)
323
+ response_metadata["x-cache"] = "hit" # headers
324
+ trailing_metadata["x-cost-ms"] = "3" # trailers, sent with the status
325
+ response_metadata["x-trace-bin"] = trace_bytes # -bin names take binary values
326
+ Demo::V1::Item.new(id: request.id)
327
+ end
328
+ ```
329
+
330
+ For streaming responses, set headers before the first message is sent.
331
+ Trailers are sent whether the call succeeds or fails.
332
+
333
+ ### Rich errors
334
+
335
+ `RpcError` can carry trailing metadata and
336
+ [rich error details](https://grpc.io/docs/guides/error/#richer-error-model):
337
+ protobuf messages packed into a `google.rpc.Status` in the standard
338
+ `grpc-status-details-bin` trailer, which official gRPC clients decode for
339
+ you.
340
+
341
+ ```ruby
342
+ raise Tonic::Rails::RpcError.new(
343
+ "invalid item",
344
+ code: :invalid_argument,
345
+ metadata: { "x-request-id" => request_id },
346
+ details: [Google::Rpc::BadRequest.new(field_violations: violations)],
347
+ )
348
+ ```
349
+
251
350
  ### Interceptors
252
351
 
253
352
  Interceptors wrap every call, outermost first. Call `next_call` to
@@ -280,6 +379,35 @@ expect { Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 0 }) }
280
379
 
281
380
  # Streaming: pass an Array of requests, get an Array of responses
282
381
  Tonic::Rails::Testing.call(ItemService, :ImportItems, [{ name: "a" }, { name: "b" }])
382
+
383
+ # Response metadata
384
+ result = Tonic::Rails::Testing.invoke(ItemService, :GetItem, { id: 1 })
385
+ result.headers["x-cache"] # => "hit"
386
+ result.trailers["x-cost-ms"] # => "3"
387
+ ```
388
+
389
+ Matchers for gRPC statuses work with `Testing.call` and with the official
390
+ `grpc` client's errors:
391
+
392
+ ```ruby
393
+ # spec/rails_helper.rb
394
+ require "tonic/rails/testing/rspec"
395
+
396
+ expect { Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 0 }) }
397
+ .to raise_grpc_error(:not_found).with_message(/0/)
398
+ ```
399
+
400
+ ```ruby
401
+ # Minitest
402
+ require "tonic/rails/testing/minitest"
403
+
404
+ class ItemServiceTest < ActiveSupport::TestCase
405
+ include Tonic::Rails::Testing::Assertions
406
+
407
+ test "missing items" do
408
+ assert_grpc_error(:not_found) { Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 0 }) }
409
+ end
410
+ end
283
411
  ```
284
412
 
285
413
  ## Configuration
@@ -313,11 +441,13 @@ end
313
441
  | `tls_client_ca_path` | `nil` | PEM CA bundle; requires client certificates (mutual TLS) |
314
442
  | `drain_timeout` | `10` | Seconds to let in-flight calls finish on shutdown |
315
443
  | `keepalive_interval`, `keepalive_timeout` | `15`, `5` | HTTP/2 PING interval and timeout, seconds |
444
+ | `health_check` | `nil` | Callable; while false or raising, health reports `NOT_SERVING` |
445
+ | `health_check_interval` | `5` | Seconds between health checks |
316
446
  | `compression` | `false` | gzip responses over 1 KiB for clients that accept it |
317
447
  | `reflection` | `false` | Serve `grpc.reflection.v1`; exposes your schema |
318
448
  | `interceptors` | `[]` | See [Interceptors](#interceptors) |
319
- | `log_requests` | `false` | Log every call to stderr (errors are always logged) |
320
- | `auto_start` | `false` | Start inside `rails server` (see below) |
449
+ | `log_requests` | `false` | Log one line per call through `logger` (unexpected exceptions are always logged) |
450
+ | `auto_start` | `false` | Start in a thread of `rails server`; for development (see [auto_start](#auto_start)) |
321
451
 
322
452
  ### Sizing threads
323
453
 
@@ -328,31 +458,92 @@ concurrency your I/O needs, size the database pool to match, and add
328
458
  processes for CPU-heavy services: the listener uses `SO_REUSEPORT`, so
329
459
  several processes can bind the same port.
330
460
 
461
+ ### Running with Puma
462
+
463
+ Add the plugin to `config/puma.rb`:
464
+
465
+ ```ruby
466
+ plugin :tonic_rails
467
+ ```
468
+
469
+ Puma then runs the gRPC server as one child process, in single and cluster
470
+ mode alike, so gRPC gets its own process and GVL instead of competing with
471
+ web workers. It starts after Puma boots and drains gracefully when Puma
472
+ stops or restarts. If the gRPC process dies, Puma shuts down too, so your
473
+ supervisor restarts both. It works with `rails server`, plain `puma`, and
474
+ `preload_app!`. (Not on Windows, which has no `fork()`: run
475
+ `tonic_rails:serve` as its own process there.)
476
+
477
+ For larger deployments, a dedicated `bin/rails tonic_rails:serve` process
478
+ (scaled separately) is still the most flexible option.
479
+
331
480
  ### auto_start
332
481
 
333
482
  `auto_start = true` starts the gRPC server in a background thread of
334
483
  `rails server` (never in consoles, rake tasks or tests). It's handy in
335
- development. In production prefer a dedicated `tonic_rails:serve`
336
- process: forking servers such as Puma in cluster mode would need to
337
- start one per worker.
484
+ development; in production use the Puma plugin or a dedicated process.
485
+ Don't combine it with the plugin.
338
486
 
339
487
  ## Operations
340
488
 
341
489
  - **Health checks.** `grpc.health.v1.Health` reports `SERVING` for the
342
490
  server (`""`) and every registered service, and flips to `NOT_SERVING`
343
491
  as soon as shutdown begins. Point your load balancer or Kubernetes
344
- gRPC probes at it.
492
+ gRPC probes at it. To make readiness reflect your dependencies, add a
493
+ check that runs every `health_check_interval` seconds (default 5):
494
+
495
+ ```ruby
496
+ config.health_check = -> { ActiveRecord::Base.connection.active? }
497
+ ```
498
+
499
+ While it returns false or raises, the server reports `NOT_SERVING`
500
+ (calls are still served). You can also mark the server or a service
501
+ yourself, for example during maintenance:
502
+ `Tonic::Rails.server.not_serving!("demo.v1.ItemService")` and
503
+ `serving!`. A service is `SERVING` only while the check passes and it
504
+ isn't marked otherwise.
345
505
  - **Graceful shutdown.** On SIGTERM or SIGINT the server stops
346
506
  accepting connections, tells clients to go away (HTTP/2 GOAWAY), lets
347
507
  in-flight calls finish for up to `drain_timeout` seconds, then exits.
348
508
  - **Load shedding.** When every dispatch thread is busy and the queue is
349
509
  full, new calls fail fast with `RESOURCE_EXHAUSTED` instead of piling up.
510
+ - **Instrumentation.** Every RPC publishes a `process_rpc.tonic_rails`
511
+ event through `ActiveSupport::Notifications`, so APM agents,
512
+ OpenTelemetry and your own subscribers can trace calls. See
513
+ [Instrumentation](#instrumentation).
514
+ - **Logs.** Everything goes through your (Rails) logger. With
515
+ `log_requests = true`, each RPC logs one line, e.g.
516
+ `gRPC /demo.v1.ItemService/GetItem OK in 1.4ms`. Calls the transport
517
+ rejects before reaching Ruby (unknown methods, a full queue, expired
518
+ deadlines, oversized messages, refused connections) are always logged,
519
+ at info for client-caused problems, warn for overload and error for faults.
350
520
  - **Stats.** `Tonic::Rails.server.stats` returns counters for totals,
351
521
  successes, errors, cancellations, deadline and resource errors, active
352
522
  connections and queued calls.
353
523
  - **Reflection.** Turn on `reflection` in development to use
354
524
  [grpcurl](https://github.com/fullstorydev/grpcurl) without proto files.
355
525
 
526
+ ## Instrumentation
527
+
528
+ Every RPC is wrapped in an `ActiveSupport::Notifications` event, inside the
529
+ Rails executor, so subscribers see the same context as your handler:
530
+
531
+ ```ruby
532
+ ActiveSupport::Notifications.subscribe("process_rpc.tonic_rails") do |event|
533
+ event.payload[:path] # "/demo.v1.ItemService/GetItem"
534
+ event.payload[:service] # "demo.v1.ItemService"
535
+ event.payload[:rpc] # "GetItem"
536
+ event.payload[:metadata] # request metadata
537
+ event.payload[:status] # :ok, :not_found, :internal, ...
538
+ event.payload[:code] # numeric gRPC status
539
+ event.duration # milliseconds
540
+ end
541
+ ```
542
+
543
+ When a call fails, `event.payload[:exception_object]` is the
544
+ `Tonic::Rails::RpcError` sent to the client. If a handler raised something
545
+ unexpected, that error's `cause` is the original exception.
546
+
356
547
  ## How it works
357
548
 
358
549
  ```
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tonic_rails_native"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  publish = false
@@ -23,7 +23,7 @@ crate-type = ["cdylib"]
23
23
  #
24
24
  # socket2 enables SO_REUSEPORT for multi-process (Puma worker) scaling.
25
25
  [dependencies]
26
- magnus = "0.8"
26
+ magnus = "0.9"
27
27
  rb-sys = "0.9"
28
28
  tonic = { version = "0.14", default-features = false, features = ["transport", "router", "server", "tls-ring"] }
29
29
  tonic-reflection = { version = "0.14", default-features = false, features = ["server"] }