gruf 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -19
  3. data/README.md +71 -21
  4. data/lib/gruf.rb +3 -0
  5. data/lib/gruf/authentication.rb +9 -1
  6. data/lib/gruf/authentication/base.rb +16 -8
  7. data/lib/gruf/authentication/basic.rb +6 -6
  8. data/lib/gruf/authentication/none.rb +2 -2
  9. data/lib/gruf/authentication/strategies.rb +20 -7
  10. data/lib/gruf/client.rb +73 -11
  11. data/lib/gruf/configuration.rb +9 -6
  12. data/lib/gruf/error.rb +49 -15
  13. data/lib/gruf/errors/debug_info.rb +10 -5
  14. data/lib/gruf/errors/field.rb +12 -5
  15. data/lib/gruf/hooks/active_record/connection_reset.rb +15 -1
  16. data/lib/gruf/hooks/base.rb +26 -4
  17. data/lib/gruf/hooks/registry.rb +15 -9
  18. data/lib/gruf/instrumentation/base.rb +66 -11
  19. data/lib/gruf/instrumentation/output_metadata_timer.rb +6 -3
  20. data/lib/gruf/instrumentation/registry.rb +22 -13
  21. data/lib/gruf/instrumentation/request_context.rb +66 -0
  22. data/lib/gruf/instrumentation/request_logging/formatters/base.rb +38 -0
  23. data/lib/gruf/instrumentation/request_logging/formatters/logstash.rb +40 -0
  24. data/lib/gruf/instrumentation/request_logging/formatters/plain.rb +45 -0
  25. data/lib/gruf/instrumentation/request_logging/hook.rb +145 -0
  26. data/lib/gruf/instrumentation/statsd.rb +19 -13
  27. data/lib/gruf/loggable.rb +4 -1
  28. data/lib/gruf/logging.rb +19 -0
  29. data/lib/gruf/response.rb +19 -4
  30. data/lib/gruf/serializers/errors/base.rb +10 -3
  31. data/lib/gruf/serializers/errors/json.rb +5 -2
  32. data/lib/gruf/server.rb +10 -2
  33. data/lib/gruf/service.rb +32 -22
  34. data/lib/gruf/timer.rb +26 -5
  35. data/lib/gruf/version.rb +1 -1
  36. metadata +7 -2
@@ -15,22 +15,56 @@
15
15
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
16
  #
17
17
  module Gruf
18
+ ##
19
+ # Abstracts out the calling interface for interacting with gRPC clients. Streamlines calling and provides
20
+ # instrumented response objects that also can contain deserialized error messages via serialized objects transported
21
+ # via the service's trailing metadata.
22
+ #
23
+ # begin
24
+ # client = ::Gruf::Client.new(service: ::Demo::ThingService)
25
+ # response = client.call(:GetMyThing, id: 123)
26
+ # puts response.message.inspect
27
+ # rescue Gruf::Client::Error => e
28
+ # puts e.error.inspect
29
+ # end
30
+ #
31
+ # Utilizes SimpleDelegator to wrap the given service that the client is connecting to, which allows a clean interface
32
+ # to the underlying RPC descriptors and methods.
33
+ #
18
34
  class Client < SimpleDelegator
19
35
  include Gruf::Loggable
20
36
 
37
+ ##
38
+ # Represents an error that was returned from the server's trailing metadata. Used as a custom exception object
39
+ # that is instead raised in the case of the service returning serialized error data, as opposed to the normal
40
+ # GRPC::BadStatus error
41
+ #
21
42
  class Error < StandardError
43
+ # @return [Object] error The deserialized error
22
44
  attr_reader :error
23
45
 
46
+ ##
47
+ # Initialize the client error
48
+ #
49
+ # @param [Object] error The deserialized error
50
+ #
24
51
  def initialize(error)
25
52
  @error = error
26
53
  end
27
54
  end
28
55
 
29
- attr_reader :base_klass, :service_klass, :opts
56
+ # @return [Class] The base, friendly name of the service being requested
57
+ attr_reader :base_klass
58
+ # @return [Class] The class name of the gRPC service being requested
59
+ attr_reader :service_klass
60
+ # @return [Hash] A hash of options for the client
61
+ attr_reader :opts
30
62
 
31
63
  ##
64
+ # Initialize the client and setup the stub
65
+ #
32
66
  # @param [Module] service The namespace of the client Stub that is desired to load
33
- # @param [Hash] options
67
+ # @param [Hash] options A hash of options for the client
34
68
  # @option options [String] :password The password for basic authentication for the service.
35
69
  # @option options [String] :hostname The hostname of the service. Defaults to linkerd.
36
70
  #
@@ -47,7 +81,16 @@ module Gruf
47
81
  ##
48
82
  # Call the client's method with given params
49
83
  #
84
+ # @param [String|Symbol] request_method The method that is being requested on the service
85
+ # @param [Hash] params (Optional) A hash of parameters that will be inserted into the gRPC request message that is required
86
+ # for the given above call
87
+ # @param [Hash] metadata (Optional) A hash of metadata key/values that are transported with the client request
88
+ # @param [Hash] opts (Optional) A hash of options to send to the gRPC request_response method
89
+ # @return [Gruf::Response] The response from the server
90
+ # @raise [Gruf::Client::Error|GRPC::BadStatus] If an error occurs, an exception will be raised according to the
91
+ # error type that was returned
50
92
  def call(request_method, params = {}, metadata = {}, opts = {})
93
+ request_method = request_method.to_sym
51
94
  req = request_object(request_method, params)
52
95
  md = build_metadata(metadata)
53
96
  call_sig = call_signature(request_method)
@@ -64,11 +107,14 @@ module Gruf
64
107
  private
65
108
 
66
109
  ##
110
+ # Execute the given request to the service
111
+ #
67
112
  # @param [Symbol] call_sig The call signature being executed
68
- # @param [Object] req
69
- # @param [Hash] md
113
+ # @param [Object] req (Optional) The protobuf request message to send
114
+ # @param [Hash] md (Optional) A hash of metadata key/values that are transported with the client request
115
+ # @param [Hash] opts (Optional) A hash of options to send to the gRPC request_response method
70
116
  #
71
- def execute(call_sig, req, md, opts)
117
+ def execute(call_sig, req, md, opts = {})
72
118
  timed = Timer.time do
73
119
  opts[:return_op] = true
74
120
  opts[:metadata] = md
@@ -78,14 +124,21 @@ module Gruf
78
124
  end
79
125
 
80
126
  ##
81
- # @return [Struct<GRPC::RpcDesc>]
127
+ # Get the appropriate RPC descriptor given the method on the service being called
128
+ #
129
+ # @param [Symbol] request_method The method name being called on the remote service
130
+ # @return [Struct<GRPC::RpcDesc>] Return the given RPC descriptor given the method on the service being called
82
131
  #
83
132
  def rpc_desc(request_method)
84
- service_klass.rpc_descs[request_method.to_sym]
133
+ service_klass.rpc_descs[request_method]
85
134
  end
86
135
 
87
136
  ##
88
- # @return [Class] The request object
137
+ # Get the appropriate protobuf request message for the given request method on the service being called
138
+ #
139
+ # @param [Symbol] request_method The method name being called on the remote service
140
+ # @param [Hash] params (Optional) A hash of parameters that will populate the request object
141
+ # @return [Class] The request object that corresponds to the method being called
89
142
  #
90
143
  def request_object(request_method, params = {})
91
144
  desc = rpc_desc(request_method)
@@ -93,6 +146,8 @@ module Gruf
93
146
  end
94
147
 
95
148
  ##
149
+ # Properly find the appropriate call signature for the GRPC::GenericService given the request method name
150
+ #
96
151
  # @return [Symbol]
97
152
  #
98
153
  def call_signature(request_method)
@@ -101,7 +156,10 @@ module Gruf
101
156
  end
102
157
 
103
158
  ##
104
- # @return [Hash]
159
+ # Build a sanitized, authenticated metadata hash for the given request
160
+ #
161
+ # @param [Hash] metadata A base metadata hash to build from
162
+ # @return [Hash] The compiled metadata hash that is ready to be transported over the wire
105
163
  #
106
164
  def build_metadata(metadata = {})
107
165
  unless opts[:password].empty?
@@ -114,7 +172,9 @@ module Gruf
114
172
  end
115
173
 
116
174
  ##
117
- # @return [Symbol|GRPC::Core::ChannelCredentials]
175
+ # Build the SSL/TLS credentials for the outbound gRPC request
176
+ #
177
+ # @return [Symbol|GRPC::Core::ChannelCredentials] The generated SSL credentials for the outbound gRPC request
118
178
  #
119
179
  # :nocov:
120
180
  def build_ssl_credentials
@@ -130,7 +190,9 @@ module Gruf
130
190
  # :nocov:
131
191
 
132
192
  ##
133
- # @return [Class]
193
+ # Return the specified error deserializer class by the configuration
194
+ #
195
+ # @return [Class] The proper error deserializer class. Defaults to JSON.
134
196
  #
135
197
  def error_deserializer_class
136
198
  if Gruf.error_serializer
@@ -38,7 +38,9 @@ module Gruf
38
38
  authorization_metadata_key: 'authorization',
39
39
  append_server_errors_to_trailing_metadata: true,
40
40
  use_default_hooks: true,
41
- backtrace_on_error: false
41
+ backtrace_on_error: false,
42
+ use_exception_message: true,
43
+ internal_error_message: 'Internal Server Error'
42
44
  }.freeze
43
45
 
44
46
  attr_accessor *VALID_CONFIG_KEYS.keys
@@ -53,8 +55,8 @@ module Gruf
53
55
  ##
54
56
  # Yield self for ruby-style initialization
55
57
  #
56
- # @yields [Gruf::Configuration]
57
- # @return [Gruf::Configuration]
58
+ # @yields [Gruf::Configuration] The configuration object for gruf
59
+ # @return [Gruf::Configuration] The configuration object for gruf
58
60
  #
59
61
  def configure
60
62
  yield self
@@ -63,7 +65,7 @@ module Gruf
63
65
  ##
64
66
  # Return the current configuration options as a Hash
65
67
  #
66
- # @return [Hash]
68
+ # @return [Hash] The configuration for gruf, represented as a Hash
67
69
  #
68
70
  def options
69
71
  opts = {}
@@ -80,7 +82,7 @@ module Gruf
80
82
  #
81
83
  def reset
82
84
  VALID_CONFIG_KEYS.each do |k, v|
83
- send((k.to_s + '=').to_sym, v)
85
+ send((k.to_s + '='), v)
84
86
  end
85
87
  if defined?(Rails) && Rails.logger
86
88
  self.root_path = Rails.root
@@ -102,6 +104,7 @@ module Gruf
102
104
  if use_default_hooks
103
105
  Gruf::Hooks::Registry.add(:ar_connection_reset, Gruf::Hooks::ActiveRecord::ConnectionReset)
104
106
  Gruf::Instrumentation::Registry.add(:output_metadata_timer, Gruf::Instrumentation::OutputMetadataTimer)
107
+ Gruf::Instrumentation::Registry.add(:request_logging, Gruf::Instrumentation::RequestLogging::Hook)
105
108
  end
106
109
  options
107
110
  end
@@ -111,7 +114,7 @@ module Gruf
111
114
  ##
112
115
  # Automatically determine environment
113
116
  #
114
- # @return [String]
117
+ # @return [String] The current Ruby environment
115
118
  #
116
119
  def environment
117
120
  if defined?(Rails)
@@ -28,6 +28,7 @@ module Gruf
28
28
  class Error
29
29
  include Gruf::Loggable
30
30
 
31
+ # @return [Hash<GRPC::BadStatus>] A hash mapping of gRPC BadStatus codes to error symbols
31
32
  TYPES = {
32
33
  ok: GRPC::Ok,
33
34
  cancelled: GRPC::Cancelled,
@@ -50,15 +51,30 @@ module Gruf
50
51
  data_loss: GRPC::DataLoss
51
52
  }.freeze
52
53
 
53
- attr_accessor :code, :app_code, :message, :field_errors, :debug_info, :grpc_error
54
+ # @return [Symbol] The given internal gRPC code for the error
55
+ attr_accessor :code
56
+ # @return [Symbol] An arbitrary application code that can be used for logical processing of the error by the client
57
+ attr_accessor :app_code
58
+ # @return [String] The error message returned by the server
59
+ attr_accessor :message
60
+ # @return [Array] An array of field errors that can be returned by the server
61
+ attr_accessor :field_errors
62
+ # @return [Object] A hash of debugging information, such as a stack trace and exception name, that can be used to
63
+ # debug an given error response. This is sent by the server over the trailing metadata.
64
+ attr_accessor :debug_info
65
+ # @return [GRPC::BadStatus] The gRPC BadStatus error object that was generated
66
+ attr_accessor :grpc_error
67
+ # @return [Hash] The trailing metadata that was attached to the error
54
68
  attr_reader :metadata
55
69
 
56
70
  ##
57
71
  # Initialize the error, setting default values
58
72
  #
73
+ # @param [Hash] args (Optional) An optional hash of arguments that will set fields on the error object
74
+ #
59
75
  def initialize(args = {})
60
76
  args.each do |k, v|
61
- send("#{k.to_sym}=", v) if respond_to?(k.to_sym)
77
+ send("#{k}=", v) if respond_to?(k)
62
78
  end
63
79
  @field_errors = []
64
80
  end
@@ -66,24 +82,29 @@ module Gruf
66
82
  ##
67
83
  # Add a field error to this error package
68
84
  #
69
- # @param [Symbol] field_name
70
- # @param [Symbol] error_code
71
- # @param [String] message
85
+ # @param [Symbol] field_name The field name for the error
86
+ # @param [Symbol] error_code The application error code for the error; e.g. :job_not_found
87
+ # @param [String] message The application error message for the error; e.g. "Job not found with ID 123"
72
88
  #
73
89
  def add_field_error(field_name, error_code, message = '')
74
90
  @field_errors << Errors::Field.new(field_name, error_code, message)
75
91
  end
76
92
 
77
93
  ##
78
- # @param [String] detail
79
- # @param [Array<String>] stack_trace
94
+ # Set the debugging information for the error message
95
+ #
96
+ # @param [String] detail The detailed message generated by the exception
97
+ # @param [Array<String>] stack_trace An array of strings that represents the exception backtrace generated by the
98
+ # service
80
99
  #
81
100
  def set_debug_info(detail, stack_trace = [])
82
101
  @debug_info = Errors::DebugInfo.new(detail, stack_trace)
83
102
  end
84
103
 
85
104
  ##
86
- # Ensure all metadata values are strings
105
+ # Ensure all metadata values are strings as HTTP/2 requires string values for transport
106
+ #
107
+ # @return [Hash] The newly set metadata
87
108
  #
88
109
  def metadata=(md)
89
110
  @metadata = md.map { |k, str| [k, str.to_s] }.to_h
@@ -92,7 +113,7 @@ module Gruf
92
113
  ##
93
114
  # Serialize the error for transport
94
115
  #
95
- # @return [String]
116
+ # @return [String] The serialized error message
96
117
  #
97
118
  def serialize
98
119
  serializer = serializer_class.new(self)
@@ -100,8 +121,10 @@ module Gruf
100
121
  end
101
122
 
102
123
  ##
103
- # @param [GRPC::ActiveCall]
104
- # @return [Error]
124
+ # Append any appropriate errors to the gRPC call and properly update the output metadata
125
+ #
126
+ # @param [GRPC::ActiveCall] active_call The marshalled gRPC call
127
+ # @return [Error] Return the error itself with the GRPC::ActiveCall attached and error metadata appended
105
128
  #
106
129
  def attach_to_call(active_call)
107
130
  metadata[Gruf.error_metadata_key.to_sym] = serialize if Gruf.append_server_errors_to_trailing_metadata
@@ -112,15 +135,20 @@ module Gruf
112
135
  end
113
136
 
114
137
  ##
115
- # @param [GRPC::ActiveCall]
116
- # @return [GRPC::BadStatus]
138
+ # Fail the current gRPC call with the given error, properly attaching it to the call and raising the appropriate
139
+ # gRPC BadStatus code.
140
+ #
141
+ # @param [GRPC::ActiveCall] active_call The marshalled gRPC call
142
+ # @return [GRPC::BadStatus] The gRPC BadStatus code this error is mapped to
117
143
  #
118
144
  def fail!(active_call)
119
145
  raise attach_to_call(active_call).grpc_error
120
146
  end
121
147
 
122
148
  ##
123
- # @return [Hash]
149
+ # Return the error represented in Hash form
150
+ #
151
+ # @return [Hash] The error as a hash
124
152
  #
125
153
  def to_h
126
154
  {
@@ -133,6 +161,8 @@ module Gruf
133
161
  end
134
162
 
135
163
  ##
164
+ # Return the appropriately mapped GRPC::BadStatus error object for this error
165
+ #
136
166
  # @return [GRPC::BadStatus]
137
167
  #
138
168
  def grpc_error
@@ -142,6 +172,8 @@ module Gruf
142
172
  private
143
173
 
144
174
  ##
175
+ # Return the error serializer being used for gruf
176
+ #
145
177
  # @return [Gruf::Serializers::Errors::Base]
146
178
  #
147
179
  def serializer_class
@@ -153,7 +185,9 @@ module Gruf
153
185
  end
154
186
 
155
187
  ##
156
- # @return [Class]
188
+ # Return the appropriate gRPC class for the given error code
189
+ #
190
+ # @return [Class] The gRPC error class
157
191
  #
158
192
  def grpc_class
159
193
  TYPES[code]
@@ -17,14 +17,17 @@
17
17
  module Gruf
18
18
  module Errors
19
19
  ##
20
- # Represents debugging information
20
+ # Represents debugging information for an exception that occurred in a gRPC service
21
21
  #
22
22
  class DebugInfo
23
- attr_reader :detail, :stack_trace
23
+ # @return [String] The detail message of the exception
24
+ attr_reader :detail
25
+ # @return [Array<String>] The stack trace generated by the exception as an array of strings
26
+ attr_reader :stack_trace
24
27
 
25
28
  ##
26
- # @param [String] detail
27
- # @param [Array<String>] stack_trace
29
+ # @param [String] detail The detail message of the exception
30
+ # @param [Array<String>] stack_trace The stack trace generated by the exception as an array of strings
28
31
  #
29
32
  def initialize(detail, stack_trace = [])
30
33
  @detail = detail
@@ -32,7 +35,9 @@ module Gruf
32
35
  end
33
36
 
34
37
  ##
35
- # @return [Hash]
38
+ # Return this object marshalled into a hash
39
+ #
40
+ # @return [Hash] The debug info represented as a has
36
41
  #
37
42
  def to_h
38
43
  {
@@ -20,12 +20,17 @@ module Gruf
20
20
  # Represents a field-specific error
21
21
  #
22
22
  class Field
23
- attr_reader :field_name, :error_code, :message
23
+ # @return [Symbol] The name of the field as a Symbol
24
+ attr_reader :field_name
25
+ # @return [Symbol] The application error code for the field, e.g. :job_not_found
26
+ attr_reader :error_code
27
+ # @return [String] The error message for the field, e.g. "Job with ID 123 not found"
28
+ attr_reader :message
24
29
 
25
30
  ##
26
- # @param [Symbol] field_name
27
- # @param [Symbol] error_code
28
- # @param [String] message
31
+ # @param [Symbol] field_name The name of the field as a Symbol
32
+ # @param [Symbol] error_code The application error code for the field, e.g. :job_not_found
33
+ # @param [String] message (Optional) The error message for the field, e.g. "Job with ID 123 not found"
29
34
  #
30
35
  def initialize(field_name, error_code, message = '')
31
36
  @field_name = field_name
@@ -34,7 +39,9 @@ module Gruf
34
39
  end
35
40
 
36
41
  ##
37
- # @return [Hash]
42
+ # Return the field error represented as a hash
43
+ #
44
+ # @return [Hash] The error represented as a hash
38
45
  #
39
46
  def to_h
40
47
  {
@@ -21,12 +21,26 @@ module Gruf
21
21
  # Resets the ActiveRecord connection to maintain accurate connected state in the thread pool
22
22
  #
23
23
  class ConnectionReset < Gruf::Hooks::Base
24
- def after(_success, _response, _call_signature, _req, _call)
24
+ ##
25
+ # Reset any ActiveRecord connections after a gRPC service is called. Because of the way gRPC manages its
26
+ # connection pool, we need to ensure that this is done to properly
27
+ #
28
+ # @param [Boolean] _success Whether or not the call was successful
29
+ # @param [Object] _response The protobuf response object
30
+ # @param [Symbol] _call_signature The gRPC method on the service that was called
31
+ # @param [Object] _request The protobuf request object
32
+ # @param [GRPC::ActiveCall] _call the gRPC core active call object, which represents marshalled data for
33
+ # the call itself
34
+ #
35
+ def after(_success, _response, _call_signature, _request, _call)
25
36
  ::ActiveRecord::Base.clear_active_connections! if enabled?
26
37
  end
27
38
 
28
39
  private
29
40
 
41
+ ##
42
+ # @return [Boolean] If AR is loaded, we can enable this hook safely
43
+ #
30
44
  def enabled?
31
45
  defined?(::ActiveRecord::Base)
32
46
  end