tonic-rails 0.1.0 → 0.2.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.
@@ -111,6 +111,21 @@ module Tonic
111
111
  # @return [Hash{String=>String}]
112
112
  attr_reader :grpc_metadata
113
113
 
114
+ # Response headers (initial metadata) for this call. For streaming
115
+ # responses, set them before the first message; for unary RPCs,
116
+ # anywhere in the handler. `-bin` names take binary values.
117
+ # @return [Hash]
118
+ def response_metadata
119
+ @response_metadata ||= {}
120
+ end
121
+
122
+ # Trailing metadata, sent with the final status, whether the call
123
+ # succeeds or fails.
124
+ # @return [Hash]
125
+ def trailing_metadata
126
+ @trailing_metadata ||= {}
127
+ end
128
+
114
129
  # Milliseconds left before the client's deadline, or nil if the
115
130
  # client did not set one.
116
131
  # @return [Integer, nil]
@@ -143,39 +158,52 @@ module Tonic
143
158
  raise RpcError.new("method #{rpc_name} not implemented", code: :unimplemented) unless respond_to?(spec[:handler])
144
159
 
145
160
  @grpc_metadata = metadata
161
+ @grpc_sender = sender
162
+ @grpc_headers_sent = false
146
163
  @grpc_deadline_at = timeout_ms && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + (timeout_ms / 1000.0))
147
164
 
148
- request = if spec[:request_stream]
149
- Enumerator.new do |y|
150
- while (message = read_request(reader, spec))
151
- y << message
152
- end
153
- end
154
- else
155
- read_request(reader, spec) || raise(RpcError.new("missing request message", code: :invalid_argument))
156
- end
157
-
158
- result = public_send(spec[:handler], request)
159
-
160
- if spec[:response_stream]
161
- unless result.respond_to?(:each)
162
- raise TypeError, "streaming handler #{self.class}##{spec[:handler]} must return an Enumerable, got #{result.class}"
163
- end
164
-
165
- result.each { |item| push_response(sender, encode_response(item, spec)) }
166
- nil
167
- else
168
- encode_response(result, spec)
169
- end
165
+ result = public_send(spec[:handler], build_request(reader, spec))
166
+ respond(result, spec, sender)
170
167
  rescue RpcError
171
168
  raise
172
169
  rescue StandardError => e
173
170
  Tonic::Rails.log_exception(e, "#{self.class}##{rpc_name}")
174
171
  raise RpcError.new("internal error", code: :internal)
172
+ ensure
173
+ attach_trailing_metadata
175
174
  end
176
175
 
177
176
  private
178
177
 
178
+ def build_request(reader, spec)
179
+ unless spec[:request_stream]
180
+ return read_request(reader, spec) || raise(RpcError.new("missing request message", code: :invalid_argument))
181
+ end
182
+
183
+ Enumerator.new do |y|
184
+ while (message = read_request(reader, spec))
185
+ y << message
186
+ end
187
+ end
188
+ end
189
+
190
+ # Unary: the encoded response. Streaming: nil, after pushing each
191
+ # message through the sender.
192
+ def respond(result, spec, sender)
193
+ unless spec[:response_stream]
194
+ encoded = encode_response(result, spec)
195
+ send_response_headers
196
+ return encoded
197
+ end
198
+
199
+ unless result.respond_to?(:each)
200
+ raise TypeError, "streaming handler #{self.class}##{spec[:handler]} must return an Enumerable, got #{result.class}"
201
+ end
202
+
203
+ result.each { |item| push_response(sender, encode_response(item, spec)) }
204
+ nil
205
+ end
206
+
179
207
  def read_request(reader, spec)
180
208
  bytes = reader.next
181
209
  if bytes.nil?
@@ -205,11 +233,30 @@ module Tonic
205
233
  end
206
234
 
207
235
  def push_response(sender, bytes)
236
+ send_response_headers
208
237
  sender.push(bytes)
209
238
  rescue IOError
210
239
  # The client went away; nothing left to deliver to.
211
240
  raise RpcError.new("client cancelled the stream", code: :cancelled)
212
241
  end
242
+
243
+ def send_response_headers
244
+ return if @grpc_headers_sent
245
+
246
+ @grpc_headers_sent = true
247
+ return if @response_metadata.nil? || @response_metadata.empty? || @grpc_sender.nil?
248
+
249
+ @grpc_sender.send_headers(Metadata.encode(@response_metadata))
250
+ end
251
+
252
+ # Runs in `ensure`, so it must never raise over the call's outcome.
253
+ def attach_trailing_metadata
254
+ return if @trailing_metadata.nil? || @trailing_metadata.empty? || @grpc_sender.nil?
255
+
256
+ @grpc_sender.add_trailers(Metadata.encode(@trailing_metadata))
257
+ rescue StandardError => e
258
+ Tonic::Rails.log_exception(e, "#{self.class} trailing metadata")
259
+ end
213
260
  end
214
261
  end
215
262
  end
@@ -47,7 +47,8 @@ module Tonic
47
47
  # Concurrent HTTP/2 streams per connection. 0 = unlimited. Default 32.
48
48
  attr_accessor :concurrency_limit_per_connection
49
49
 
50
- # Log every request, not just errors, to stderr. Default false.
50
+ # Log one line per RPC (path, status, duration) through `logger`.
51
+ # Unexpected exceptions are always logged. Default false.
51
52
  attr_accessor :log_requests
52
53
 
53
54
  # Callables wrapping every RPC, outermost first:
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/notifications"
5
+
6
+ module Tonic
7
+ module Rails
8
+ # Every RPC is wrapped in an ActiveSupport::Notifications event, so APM
9
+ # agents, OpenTelemetry and your own subscribers can observe it:
10
+ #
11
+ # ActiveSupport::Notifications.subscribe("process_rpc.tonic_rails") do |event|
12
+ # event.payload[:path] # "/demo.v1.ItemService/GetItem"
13
+ # event.payload[:service] # "demo.v1.ItemService"
14
+ # event.payload[:rpc] # "GetItem"
15
+ # event.payload[:metadata] # request metadata Hash
16
+ # event.payload[:code] # gRPC status code, 0 = OK
17
+ # event.payload[:status] # :ok, :not_found, :internal, ...
18
+ # event.duration # milliseconds
19
+ # end
20
+ #
21
+ # When a call fails, :exception_object holds the RpcError. If a handler
22
+ # raised something unexpected, that RpcError's #cause is the original
23
+ # exception.
24
+ module Instrumentation
25
+ EVENT = "process_rpc.tonic_rails"
26
+
27
+ module_function
28
+
29
+ # @api private
30
+ def instrument(path, metadata)
31
+ # Without subscribers, skip building the payload: this runs on
32
+ # every RPC.
33
+ return yield unless ActiveSupport::Notifications.notifier.listening?(EVENT)
34
+
35
+ service, rpc = path.delete_prefix("/").split("/", 2)
36
+ payload = { path: path, service: service, rpc: rpc, metadata: metadata, code: nil, status: nil }
37
+ ActiveSupport::Notifications.instrument(EVENT, payload) do
38
+ result = yield
39
+ payload[:code] = 0
40
+ payload[:status] = :ok
41
+ result
42
+ rescue RpcError => e
43
+ payload[:code] = e.code
44
+ payload[:status] = e.code_name
45
+ raise
46
+ rescue StandardError
47
+ payload[:code] = RpcError::CODES[:internal]
48
+ payload[:status] = :internal
49
+ raise
50
+ end
51
+ end
52
+
53
+ # Logs one line per RPC through the configured logger, e.g.
54
+ # gRPC /demo.v1.ItemService/GetItem OK in 1.4ms
55
+ # gRPC /demo.v1.ItemService/GetItem NOT_FOUND in 0.9ms: item 7 not found
56
+ # Enabled by `config.log_requests = true`.
57
+ class RequestLogger
58
+ def initialize(logger)
59
+ @logger = logger
60
+ end
61
+
62
+ def call(event)
63
+ payload = event.payload
64
+ status = (payload[:status] || :unknown).to_s.upcase
65
+ line = format("gRPC %<path>s %<status>s in %<ms>.1fms", path: payload[:path], status: status, ms: event.duration)
66
+ error = payload[:exception_object]
67
+ line = "#{line}: #{error.message}" if error && payload[:code] != 0
68
+ @logger.info(line)
69
+ end
70
+
71
+ # @return [Object] the subscription, for #unsubscribe
72
+ def subscribe
73
+ ActiveSupport::Notifications.subscribe(EVENT, self)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tonic
4
+ module Rails
5
+ # Converts handler-supplied gRPC metadata into header pairs.
6
+ #
7
+ # Names are lower-cased and must be valid gRPC metadata names; names
8
+ # ending in "-bin" carry binary values, which are base64-encoded as the
9
+ # gRPC spec requires. Other values must be printable ASCII.
10
+ module Metadata
11
+ NAME = /\A[0-9a-z_.-]+\z/
12
+ PRINTABLE = /\A[\x20-\x7e]*\z/
13
+ RESERVED = %w[content-type te user-agent content-length connection transfer-encoding].freeze
14
+
15
+ module_function
16
+
17
+ # @param metadata [Hash{String,Symbol=>String,Array<String>}]
18
+ # @return [Hash{String=>String}]
19
+ # @raise [ArgumentError] for invalid or reserved names and values
20
+ def encode(metadata)
21
+ metadata.each_with_object({}) do |(name, value), out|
22
+ name = name.to_s.downcase
23
+ validate_name!(name)
24
+ out[name] = encode_value(name, value)
25
+ end
26
+ end
27
+
28
+ def validate_name!(name)
29
+ raise ArgumentError, "invalid gRPC metadata name #{name.inspect}" unless NAME.match?(name)
30
+ return unless RESERVED.include?(name) || (name.start_with?("grpc-") && name != "grpc-status-details-bin")
31
+
32
+ raise ArgumentError, "gRPC metadata name #{name.inspect} is reserved"
33
+ end
34
+
35
+ def encode_value(name, value)
36
+ values = Array(value).map(&:to_s)
37
+ if name.end_with?("-bin")
38
+ values.map { |v| [v].pack("m0") }.join(",")
39
+ else
40
+ values.each do |v|
41
+ next if PRINTABLE.match?(v)
42
+
43
+ raise ArgumentError, "gRPC metadata #{name} must be printable ASCII; use a -bin name for binary"
44
+ end
45
+ values.join(",")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -28,8 +28,8 @@ module Tonic
28
28
  config.after_initialize do
29
29
  configuration = Tonic::Rails.configuration
30
30
  # Only inside an actual web server process — not consoles, rake
31
- # tasks, or test runs. Forking servers (Puma cluster mode) must
32
- # instead start the server per worker, e.g. in `on_worker_boot`.
31
+ # tasks, or test runs. With Puma, prefer `plugin :tonic_rails`
32
+ # (lib/puma/plugin/tonic_rails.rb), which also covers cluster mode.
33
33
  if configuration.auto_start && defined?(::Rails::Server)
34
34
  server = Tonic::Rails.server
35
35
  thread = Thread.new do
@@ -2,13 +2,23 @@
2
2
 
3
3
  module Tonic
4
4
  module Rails
5
- # Raise from a handler to end the RPC with a gRPC status.
5
+ # Raise from a handler (or interceptor) to end the RPC with a gRPC status.
6
6
  #
7
- # raise Tonic::Rails::RpcError.new("product not found", code: :not_found)
7
+ # raise Tonic::Rails::RpcError.new("item not found", code: :not_found)
8
8
  #
9
- # The message is sent to the client, so keep internal details out of
10
- # it. Any other exception becomes INTERNAL with a generic message
11
- # and is logged server-side.
9
+ # Optionally attach trailing metadata and rich error details (protobuf
10
+ # messages such as google.rpc.BadRequest, sent in the standard
11
+ # grpc-status-details-bin trailer as a google.rpc.Status):
12
+ #
13
+ # raise Tonic::Rails::RpcError.new(
14
+ # "invalid item", code: :invalid_argument,
15
+ # metadata: { "x-request-id" => request_id },
16
+ # details: [Google::Rpc::BadRequest.new(field_violations: [...])],
17
+ # )
18
+ #
19
+ # The message, metadata and details go to the client, so keep internal
20
+ # details out of them. Any other exception becomes INTERNAL with a
21
+ # generic message and is logged server-side.
12
22
  class RpcError < Error
13
23
  CODES = {
14
24
  ok: 0,
@@ -35,13 +45,70 @@ module Tonic
35
45
  # @return [Integer] numeric gRPC status code
36
46
  attr_reader :code
37
47
 
48
+ # @return [Hash] trailing metadata sent with the status
49
+ attr_reader :metadata
50
+
51
+ # @return [Array<Google::Protobuf::AbstractMessage>] rich error details
52
+ attr_reader :details
53
+
38
54
  # @param message [String] status message sent to the client
39
55
  # @param code [Symbol, Integer] gRPC status code (see CODES)
40
- def initialize(message = nil, code: :unknown)
56
+ # @param metadata [Hash] trailing metadata (see Tonic::Rails::Metadata)
57
+ # @param details [Array<Google::Protobuf::AbstractMessage>] messages
58
+ # packed into google.rpc.Status.details
59
+ def initialize(message = nil, code: :unknown, metadata: {}, details: [])
41
60
  @code = self.class.normalize_code(code)
61
+ @metadata = metadata
62
+ @details = Array(details)
42
63
  super(message || code_name.to_s)
43
64
  end
44
65
 
66
+ # @api private
67
+ # @return [Hash{String=>String}] trailers to send with this status
68
+ def trailers
69
+ trailers = Metadata.encode(metadata)
70
+ trailers["grpc-status-details-bin"] = [status_proto].pack("m0") unless details.empty?
71
+ trailers
72
+ end
73
+
74
+ # The encoded google.rpc.Status (code = 1, message = 2, details = 3 as
75
+ # google.protobuf.Any), built directly so googleapis protos aren't a
76
+ # dependency.
77
+ def status_proto
78
+ require "google/protobuf/well_known_types"
79
+ buf = (+"").b
80
+ buf << field_key(1, 0) << varint(code)
81
+ buf << length_delimited(2, message.b)
82
+ details.each do |detail|
83
+ any = detail.is_a?(Google::Protobuf::Any) ? detail : Google::Protobuf::Any.pack(detail)
84
+ buf << length_delimited(3, Google::Protobuf::Any.encode(any))
85
+ end
86
+ buf
87
+ end
88
+
89
+ private
90
+
91
+ def field_key(field, wire_type)
92
+ varint((field << 3) | wire_type)
93
+ end
94
+
95
+ def length_delimited(field, bytes)
96
+ field_key(field, 2) + varint(bytes.bytesize) + bytes
97
+ end
98
+
99
+ def varint(value)
100
+ bytes = []
101
+ loop do
102
+ byte = value & 0x7f
103
+ value >>= 7
104
+ bytes << (value.zero? ? byte : byte | 0x80)
105
+ break if value.zero?
106
+ end
107
+ bytes.pack("C*")
108
+ end
109
+
110
+ public
111
+
45
112
  # @return [Symbol] e.g. :not_found
46
113
  def code_name
47
114
  CODE_NAMES.fetch(@code)
@@ -20,8 +20,7 @@ module Tonic
20
20
  @router = Router.new
21
21
  @native = nil
22
22
  @state = :stopped
23
- @stop_reader = nil
24
- @stop_writer = nil
23
+ @stop_signal = nil
25
24
  @stop_requested = false
26
25
  @lock = Mutex.new
27
26
  end
@@ -51,12 +50,17 @@ module Tonic
51
50
  @configuration.validate!
52
51
  Tonic::Rails.native_extension!
53
52
  Tonic::Rails._set_shutting_down(false)
54
- @stop_reader, @stop_writer = IO.pipe
53
+ # A Queue rather than a self-pipe: pushing to it is safe from trap
54
+ # handlers, and unlike pipes it can be waited on with a timeout
55
+ # on every platform (Windows select() only handles sockets).
56
+ @stop_signal = Thread::Queue.new
55
57
  stop if @stop_requested # requested while we were starting up
56
58
  @native = build_native_server
57
59
  @native.start_server
60
+ log_pump = spawn_log_pump
58
61
  @state = :running
59
62
 
63
+ request_log = subscribe_request_logger
60
64
  previous_handlers = install_signal_handlers ? trap_signals : {}
61
65
  workers = Array.new(@configuration.dispatch_threads) { |i| spawn_worker(i) }
62
66
  log_startup
@@ -65,11 +69,12 @@ module Tonic
65
69
  wait_for_stop(workers)
66
70
  shutdown(workers)
67
71
  ensure
72
+ ActiveSupport::Notifications.unsubscribe(request_log) if request_log
68
73
  previous_handlers&.each { |signal, handler| Signal.trap(signal, handler || "DEFAULT") }
69
74
  @native&.stop_server
75
+ stop_log_pump(log_pump)
70
76
  @native = nil
71
- [@stop_reader, @stop_writer].each { |io| io.close if io && !io.closed? }
72
- @stop_reader = @stop_writer = nil
77
+ @stop_signal = nil
73
78
  Tonic::Rails._set_shutting_down(false)
74
79
  @state = :stopped
75
80
  end
@@ -79,9 +84,7 @@ module Tonic
79
84
  # other threads; returns immediately.
80
85
  def stop
81
86
  @stop_requested = true
82
- @stop_writer&.write_nonblock(".", exception: false)
83
- rescue IOError
84
- # Already stopped.
87
+ @stop_signal&.push(:stop)
85
88
  end
86
89
 
87
90
  def running?
@@ -113,7 +116,6 @@ module Tonic
113
116
  native.set_worker_threads(config.worker_threads || 0)
114
117
  native.set_keepalive(config.keepalive_interval.to_f, config.keepalive_timeout.to_f)
115
118
  native.set_dispatch_queue_size(config.dispatch_queue_size)
116
- native.set_log_requests(config.log_requests ? true : false)
117
119
  native.set_compression(config.compression ? true : false)
118
120
  native.set_reflection(config.reflection ? true : false)
119
121
  native.set_reflection_descriptors(Reflection.file_descriptors(@router.service_names)) if config.reflection
@@ -133,8 +135,9 @@ module Tonic
133
135
  end
134
136
 
135
137
  guarded = lambda do |path, metadata, transport|
136
- call.call(path, metadata, transport)
137
- rescue RpcError
138
+ Instrumentation.instrument(path, metadata) { call.call(path, metadata, transport) }
139
+ rescue RpcError => e
140
+ attach_error_trailers(transport, e)
138
141
  raise
139
142
  rescue StandardError => e
140
143
  Tonic::Rails.log_exception(e, path)
@@ -144,6 +147,15 @@ module Tonic
144
147
  wrap_with_rails_executor(guarded)
145
148
  end
146
149
 
150
+ # An RpcError's own metadata and rich details travel as trailers.
151
+ def attach_error_trailers(transport, error)
152
+ return if error.metadata.empty? && error.details.empty?
153
+
154
+ transport[:sender]&.add_trailers(error.trailers)
155
+ rescue StandardError => e
156
+ Tonic::Rails.log_exception(e, "RpcError trailers")
157
+ end
158
+
147
159
  # Each RPC runs inside the Rails executor (or reloader, when code
148
160
  # reloading is on): ActiveRecord connections are returned, query
149
161
  # caches and CurrentAttributes are reset — the same isolation Rack
@@ -156,6 +168,41 @@ module Tonic
156
168
  ->(path, metadata, transport) { wrapper.wrap { callback.call(path, metadata, transport) } }
157
169
  end
158
170
 
171
+ def subscribe_request_logger
172
+ return unless @configuration.log_requests && @configuration.logger
173
+
174
+ Instrumentation::RequestLogger.new(@configuration.logger).subscribe
175
+ end
176
+
177
+ # Transport events from the Rust side (rejected calls, expired
178
+ # deadlines, refused connections, faults) can't call Ruby directly;
179
+ # this thread drains them into the configured logger.
180
+ def spawn_log_pump
181
+ native = @native
182
+ logger = @configuration.logger
183
+ @log_pump_running = true
184
+ Thread.new do
185
+ Thread.current.name = "tonic-rails-log"
186
+ write_transport_logs(logger, native.next_log_events(0.25)) while @log_pump_running
187
+ write_transport_logs(logger, native.next_log_events(0)) # whatever shutdown produced
188
+ rescue StandardError => e
189
+ warn("tonic-rails: log thread stopped: #{e.class}: #{e.message}")
190
+ end
191
+ end
192
+
193
+ def stop_log_pump(thread)
194
+ return unless thread
195
+
196
+ @log_pump_running = false
197
+ thread.join(2)
198
+ end
199
+
200
+ def write_transport_logs(logger, events)
201
+ return unless logger
202
+
203
+ events.each { |level, message| logger.public_send(level, "tonic-rails: #{message}") }
204
+ end
205
+
159
206
  def spawn_worker(index)
160
207
  native = @native
161
208
  Thread.new do
@@ -174,7 +221,7 @@ module Tonic
174
221
 
175
222
  def wait_for_stop(workers)
176
223
  loop do
177
- break if @stop_reader.wait_readable(1)
224
+ break if @stop_signal.pop(timeout: 1)
178
225
 
179
226
  dead = workers.reject(&:alive?)
180
227
  unless dead.empty?
@@ -28,19 +28,35 @@ module Tonic
28
28
  attr_reader :error
29
29
  end
30
30
 
31
- # Response side of a call: collects encoded messages.
31
+ # Response side of a call: collects encoded messages and metadata.
32
32
  class Sender
33
- attr_reader :messages
33
+ attr_reader :messages, :headers, :trailers
34
34
 
35
35
  def initialize
36
36
  @messages = []
37
+ @headers = {}
38
+ @trailers = {}
37
39
  end
38
40
 
39
41
  def push(bytes)
40
42
  @messages << bytes
41
43
  end
44
+
45
+ def send_headers(headers)
46
+ raise "response headers must be sent before any response message" unless @messages.empty?
47
+
48
+ @headers.merge!(headers)
49
+ end
50
+
51
+ def add_trailers(trailers)
52
+ @trailers.merge!(trailers)
53
+ end
42
54
  end
43
55
 
56
+ # What a call produced: the response (or responses, for streaming),
57
+ # plus the response headers and trailers as they'd go on the wire.
58
+ Result = Struct.new(:response, :headers, :trailers)
59
+
44
60
  module_function
45
61
 
46
62
  # @param service_class [Class] handler class
@@ -53,6 +69,18 @@ module Tonic
53
69
  # an Array of responses for server- and bidi-streaming RPCs
54
70
  # @raise [RpcError] when the handler ends the call with a status
55
71
  def call(service_class, rpc_name, request, metadata: {}, timeout_ms: nil)
72
+ invoke(service_class, rpc_name, request, metadata: metadata, timeout_ms: timeout_ms).response
73
+ end
74
+
75
+ # Like #call, but returns a Result with the response metadata too:
76
+ #
77
+ # result = Tonic::Rails::Testing.invoke(ItemService, :GetItem, { id: 1 })
78
+ # result.response.name # => "Widget"
79
+ # result.headers["x-cache"] # => "hit"
80
+ # result.trailers["x-cost-ms"] # => "3"
81
+ #
82
+ # @return [Result]
83
+ def invoke(service_class, rpc_name, request, metadata: {}, timeout_ms: nil)
56
84
  spec = service_class.grpc_rpcs.fetch(rpc_name.to_s) do
57
85
  raise ArgumentError, "#{service_class} has no RPC #{rpc_name}"
58
86
  end
@@ -69,11 +97,12 @@ module Tonic
69
97
  timeout_ms: timeout_ms,
70
98
  )
71
99
 
72
- if spec[:response_stream]
100
+ response = if spec[:response_stream]
73
101
  sender.messages.map { |bytes| spec[:response].decode(bytes) }
74
102
  else
75
103
  spec[:response].decode(result)
76
104
  end
105
+ Result.new(response, sender.headers, sender.trailers)
77
106
  end
78
107
 
79
108
  def encode(klass, message)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tonic
4
4
  module Rails
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/tonic-rails.rb CHANGED
@@ -10,9 +10,11 @@ module Tonic
10
10
  end
11
11
 
12
12
  require "tonic/rails/version"
13
+ require "tonic/rails/metadata"
13
14
  require "tonic/rails/rpc_error"
14
15
  require "tonic/rails/configuration"
15
16
  require "tonic/rails/router"
17
+ require "tonic/rails/instrumentation"
16
18
  require "tonic/rails/base"
17
19
  require "tonic/rails/reflection"
18
20
  require "tonic/rails/server"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tonic-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Tanous
@@ -77,11 +77,15 @@ files:
77
77
  - ext/tonic_rails_native/src/framing.rs
78
78
  - ext/tonic_rails_native/src/gvl.rs
79
79
  - ext/tonic_rails_native/src/lib.rs
80
+ - ext/tonic_rails_native/src/log.rs
80
81
  - ext/tonic_rails_native/src/net.rs
81
82
  - ext/tonic_rails_native/src/server.rs
83
+ - lib/puma/plugin/tonic_rails.rb
82
84
  - lib/tonic-rails.rb
83
85
  - lib/tonic/rails/base.rb
84
86
  - lib/tonic/rails/configuration.rb
87
+ - lib/tonic/rails/instrumentation.rb
88
+ - lib/tonic/rails/metadata.rb
85
89
  - lib/tonic/rails/railtie.rb
86
90
  - lib/tonic/rails/reflection.rb
87
91
  - lib/tonic/rails/router.rb