gruf 1.1.0 → 1.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.
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
@@ -17,16 +17,21 @@
17
17
  module Gruf
18
18
  module Hooks
19
19
  ##
20
- # Base class for a hook. Define before, around, or after methods to utilize functionality.
20
+ # Base class for a hook. Define before, around, outer_around, or after methods to utilize functionality.
21
21
  #
22
22
  class Base
23
23
  include Gruf::Loggable
24
24
 
25
- attr_reader :options, :service
25
+ # @return [Gruf::Service] service The service to perform the hook against
26
+ attr_reader :service
27
+ # @return [Hash] options Options to use for the hook
28
+ attr_reader :options
26
29
 
27
30
  ##
28
- # @param [Gruf::Service] service
29
- # @param [Hash] options
31
+ # Initialize the hook and run setup
32
+ #
33
+ # @param [Gruf::Service] service The gruf service that the hook will perform against
34
+ # @param [Hash] options (Optional) A hash of options for this hook
30
35
  #
31
36
  def initialize(service, options = {})
32
37
  @service = service
@@ -36,9 +41,26 @@ module Gruf
36
41
 
37
42
  ##
38
43
  # Method that can be used to setup the hook prior to running it
44
+ #
39
45
  def setup
40
46
  # noop
41
47
  end
48
+
49
+ ##
50
+ # @return [String] Returns the service name as a translated name separated by periods
51
+ #
52
+ def service_key
53
+ service.class.name.underscore.tr('/', '.')
54
+ end
55
+
56
+ ##
57
+ # Parse the method signature into a service.method name format
58
+ #
59
+ # @return [String] The parsed service method name
60
+ #
61
+ def method_key(call_signature)
62
+ "#{service_key}.#{call_signature.to_s.gsub('_without_intercept', '')}"
63
+ end
42
64
  end
43
65
  end
44
66
  end
@@ -23,15 +23,19 @@ module Gruf
23
23
  # Registry of all hooks added
24
24
  #
25
25
  class Registry
26
+ ##
27
+ # Error class that represents when a gruf hook does not extend the base class
28
+ #
26
29
  class HookDescendantError < StandardError; end
27
30
 
28
31
  class << self
29
32
  ##
30
33
  # Add an authentication strategy, either through a class or a block
31
34
  #
32
- # @param [String] name
33
- # @param [Gruf::Hooks::Base|NilClass] hook
34
- # @return [Class]
35
+ # @param [String] name The name to represent the hook as
36
+ # @param [Gruf::Hooks::Base|NilClass] hook The strategy class to add. If nil, will expect
37
+ # a block that can be built as a hook instead
38
+ # @return [Class] The hook that was added
35
39
  #
36
40
  def add(name, hook = nil, &block)
37
41
  base = Gruf::Hooks::Base
@@ -47,7 +51,9 @@ module Gruf
47
51
  end
48
52
 
49
53
  ##
50
- # Return a strategy type registry via a hash accessor syntax
54
+ # Return a hook via a hash accessor syntax
55
+ #
56
+ # @return [Gruf::Instrumentation::Base|NilClass] The requested hook, if exists
51
57
  #
52
58
  def [](name)
53
59
  _registry[name.to_sym]
@@ -63,28 +69,28 @@ module Gruf
63
69
  end
64
70
 
65
71
  ##
66
- # @return [Hash<Class>]
72
+ # @return [Hash<Class>] Return the registry represented as a Hash object
67
73
  #
68
74
  def to_h
69
75
  _registry
70
76
  end
71
77
 
72
78
  ##
73
- # @return [Boolean]
79
+ # @return [Boolean] Return true if there are any registered hooks
74
80
  #
75
81
  def any?
76
82
  count > 0
77
83
  end
78
84
 
79
85
  ##
80
- # @return [Integer]
86
+ # @return [Integer] Return the number of registered hooks
81
87
  #
82
88
  def count
83
89
  to_h.keys.count
84
90
  end
85
91
 
86
92
  ##
87
- # @return [Hash]
93
+ # @return [Hash] Clear all existing hooks from the registry
88
94
  #
89
95
  def clear
90
96
  @_registry = {}
@@ -93,7 +99,7 @@ module Gruf
93
99
  private
94
100
 
95
101
  ##
96
- # @return [Hash<Class>]
102
+ # @return [Hash<Class>] Return the current registry
97
103
  #
98
104
  def _registry
99
105
  @_registry ||= {}
@@ -14,23 +14,27 @@
14
14
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
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
+ require_relative 'request_context'
18
+
17
19
  module Gruf
18
20
  module Instrumentation
19
21
  ##
20
- # Base class for a hook. Define before, around, or after methods to utilize functionality.
22
+ # Base class for an instrumentation strategy. Define a call method to utilize functionality.
21
23
  #
22
24
  class Base
23
25
  include Gruf::Loggable
24
26
 
25
- attr_reader :options, :service, :response, :request, :execution_time, :call_signature, :active_call
27
+ # @return [Gruf::Service] service The service to instrument
28
+ attr_reader :service
29
+ # @return [Hash] options Options to use when instrumenting the call
30
+ attr_reader :options
26
31
 
27
- def initialize(service, request, response, execution_time, call_signature, active_call, options = {})
32
+ ##
33
+ # @param [Gruf::Service] service The service to instrument
34
+ # @param [Hash] options (Optional) Options to use when instrumenting the call
35
+ #
36
+ def initialize(service, options = {})
28
37
  @service = service
29
- @request = request
30
- @response = response
31
- @execution_time = execution_time
32
- @call_signature = call_signature.to_s.gsub('_without_intercept', '').to_sym
33
- @active_call = active_call
34
38
  @options = options
35
39
  setup
36
40
  end
@@ -43,17 +47,68 @@ module Gruf
43
47
  end
44
48
 
45
49
  ##
46
- # @return [Boolean]
50
+ # Was this call a success? If a response is a GRPC::BadStatus object, we assume that it was unsuccessful
47
51
  #
48
- def success?
52
+ # @param [Object] response The gRPC response object
53
+ # @return [Boolean] True if was a successful call
54
+ #
55
+ def success?(response)
49
56
  !response.is_a?(GRPC::BadStatus)
50
57
  end
51
58
 
52
59
  ##
60
+ # Abstract method that is required for implementing an instrumentation strategy.
61
+ #
62
+ # @abstract
63
+ # @param [Gruf::Instrumentation::RequestContext] _rc The current request context for the call
53
64
  #
54
- def call
65
+ def call(_rc)
55
66
  raise NotImplementedError
56
67
  end
68
+
69
+ ##
70
+ # Hook into the outer_around call to time the request, and pass that to the call method
71
+ #
72
+ # @param [Symbol] call_signature The method being called
73
+ # @param [Object] request The request object
74
+ # @param [GRPC::ActiveCall] active_call The gRPC active call object
75
+ # @param [Proc] &_block The execution block for the call
76
+ # @return [Object] result The result of the block that was called
77
+ #
78
+ def outer_around(call_signature, request, active_call, &_block)
79
+ timed = Timer.time do
80
+ yield
81
+ end
82
+ rc = RequestContext.new(
83
+ service: service,
84
+ request: request,
85
+ response: timed.result,
86
+ execution_time: timed.time,
87
+ call_signature: call_signature,
88
+ active_call: active_call
89
+ )
90
+ call(rc)
91
+ raise rc.response unless rc.success?
92
+ rc.response
93
+ end
94
+
95
+ ##
96
+ # @return [String] Returns the service name as a translated name separated by periods
97
+ #
98
+ def service_key
99
+ service.class.name.underscore.tr('/', '.')
100
+ end
101
+
102
+ ##
103
+ # Parse the method signature into a service.method name format
104
+ #
105
+ # @param [Symbol] call_signature The method call signature
106
+ # @param [String] delimiter The delimiter to separate service and method keys
107
+ # @return [String] The parsed service method name
108
+ #
109
+ def method_key(call_signature, delimiter: '.')
110
+ "#{service_key}#{delimiter}#{call_signature}"
111
+ end
57
112
  end
58
113
  end
59
114
  end
@@ -23,14 +23,17 @@ module Gruf
23
23
  ##
24
24
  # Handle the instrumented response. Note: this will only instrument timings of _successful_ responses.
25
25
  #
26
- def call
27
- active_call.output_metadata.update(metadata_key => execution_time.to_s)
26
+ # @param [Gruf::Instrumentation::RequestContext] rc The current request context for the call
27
+ # @return [Hash] The resulting output metadata with the timer attached
28
+ #
29
+ def call(rc)
30
+ rc.active_call.output_metadata.update(metadata_key => rc.execution_time.to_s)
28
31
  end
29
32
 
30
33
  private
31
34
 
32
35
  ##
33
- # @return [Symbol]
36
+ # @return [Symbol] The metadata key that the time result should be set to
34
37
  #
35
38
  def metadata_key
36
39
  options.fetch(:output_metadata_timer, {}).fetch(:metadata_key, :timer).to_sym
@@ -17,6 +17,7 @@
17
17
  require_relative 'base'
18
18
  require_relative 'statsd'
19
19
  require_relative 'output_metadata_timer'
20
+ require_relative 'request_logging/hook'
20
21
 
21
22
  module Gruf
22
23
  module Instrumentation
@@ -24,29 +25,37 @@ module Gruf
24
25
  # Registry of all hooks added
25
26
  #
26
27
  class Registry
27
- class HookDescendantError < StandardError; end
28
+ ##
29
+ # Error class that represents when a instrumentation strategy does not extend the base class
30
+ #
31
+ class StrategyDescendantError < StandardError; end
28
32
 
29
33
  class << self
30
34
  ##
31
35
  # Add an authentication strategy, either through a class or a block
32
36
  #
33
- # @param [String] name
34
- # @param [Gruf::Hooks::Base|NilClass] hook
35
- # @return [Class]
37
+ # Gruf::Instrumentation::Registry.add(:my_instrumentor, MyInstrumentor)
36
38
  #
37
- def add(name, hook = nil, &block)
39
+ # @param [String] name The name to represent the strategy as
40
+ # @param [Gruf::Hooks::Base|NilClass] strategy (Optional) The strategy class to add. If nil, will expect
41
+ # a block that can be built as a strategy instead
42
+ # @return [Class] The strategy that was added
43
+ #
44
+ def add(name, strategy = nil, &block)
38
45
  base = Gruf::Instrumentation::Base
39
- hook ||= Class.new(base)
40
- hook.class_eval(&block) if block_given?
46
+ strategy ||= Class.new(base)
47
+ strategy.class_eval(&block) if block_given?
41
48
 
42
- raise HookDescendantError, "Hooks must descend from #{base}" unless hook.ancestors.include?(base)
49
+ raise StrategyDescendantError, "Hooks must descend from #{base}" unless strategy.ancestors.include?(base)
43
50
 
44
- _registry[name.to_sym] = hook
51
+ _registry[name.to_sym] = strategy
45
52
  end
46
53
 
47
54
  ##
48
55
  # Return a strategy type registry via a hash accessor syntax
49
56
  #
57
+ # @return [Gruf::Instrumentation::Base|NilClass] The requested strategy, if exists
58
+ #
50
59
  def [](name)
51
60
  _registry[name.to_sym]
52
61
  end
@@ -61,21 +70,21 @@ module Gruf
61
70
  end
62
71
 
63
72
  ##
64
- # @return [Hash<Class>]
73
+ # @return [Hash<Class>] Return the registry represented as a Hash object
65
74
  #
66
75
  def to_h
67
76
  _registry
68
77
  end
69
78
 
70
79
  ##
71
- # @return [Boolean]
80
+ # @return [Boolean] Return true if there are any registered instrumentation strategies
72
81
  #
73
82
  def any?
74
83
  to_h.keys.count > 0
75
84
  end
76
85
 
77
86
  ##
78
- # @return [Hash]
87
+ # @return [Hash] Clear all existing strategies from the registry
79
88
  #
80
89
  def clear
81
90
  @_registry = {}
@@ -84,7 +93,7 @@ module Gruf
84
93
  private
85
94
 
86
95
  ##
87
- # @return [Hash<Class>]
96
+ # @return [Hash<Class>] Return the current registry
88
97
  #
89
98
  def _registry
90
99
  @_registry ||= {}
@@ -0,0 +1,66 @@
1
+ module Gruf
2
+ module Instrumentation
3
+ ##
4
+ # Represents a request context for a given incoming gRPC request. This represents an injection layer that is used
5
+ # to pass to instrumentation strategies safely across thread boundaries.
6
+ #
7
+ class RequestContext
8
+ # @return [Gruf::Service] service The service to instrument
9
+ attr_reader :service
10
+ # @return [Object] request The protobuf request object
11
+ attr_reader :request
12
+ # @return [Object] response The protobuf response object
13
+ attr_reader :response
14
+ # @return [Symbol] call_signature The gRPC method on the service that was called
15
+ attr_reader :call_signature
16
+ # @return [GRPC::ActiveCall] active_call The gRPC core active call object, which represents marshalled data for
17
+ # the call itself
18
+ attr_reader :active_call
19
+ # @return [Time] execution_time The execution time, in ms, of the request
20
+ attr_reader :execution_time
21
+
22
+ ##
23
+ # Initialize the request context given the request and response
24
+ #
25
+ def initialize(
26
+ service:,
27
+ request:,
28
+ response:,
29
+ call_signature:,
30
+ active_call:,
31
+ execution_time: 0.00
32
+ )
33
+ @service = service
34
+ @request = request
35
+ @response = response
36
+ @call_signature = call_signature
37
+ @active_call = active_call
38
+ @execution_time = execution_time
39
+ end
40
+
41
+ ##
42
+ # @return [Boolean] True if the response is successful
43
+ #
44
+ def success?
45
+ !response.is_a?(GRPC::BadStatus)
46
+ end
47
+
48
+ ##
49
+ # @return [String] Return the response class name
50
+ #
51
+ def response_class_name
52
+ response.class.name
53
+ end
54
+
55
+ ##
56
+ # Return the execution time rounded to a specified precision
57
+ #
58
+ # @param [Integer] precision The amount of decimal places to round to
59
+ # @return [Float] The execution time rounded to the appropriate decimal point
60
+ #
61
+ def execution_time_rounded(precision: 2)
62
+ execution_time.to_f.round(precision)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
5
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
6
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
7
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
8
+ #
9
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
10
+ # Software.
11
+ #
12
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
13
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
14
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
15
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
+ #
17
+ module Gruf
18
+ module Instrumentation
19
+ module RequestLogging
20
+ module Formatters
21
+ ##
22
+ # Base class for request log formatting
23
+ #
24
+ class Base
25
+ ##
26
+ # Format the parameters into a loggable string. Must be implemented in every derivative class
27
+ #
28
+ # @param [Hash] _payload The incoming request payload
29
+ # @return [String] The formatted string
30
+ #
31
+ def format(_payload)
32
+ raise NotImplementedError
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end