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
@@ -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