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
@@ -0,0 +1,40 @@
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
+ require 'json'
18
+
19
+ module Gruf
20
+ module Instrumentation
21
+ module RequestLogging
22
+ module Formatters
23
+ ##
24
+ # Formats logging for gruf services into a Logstash-friendly JSON format
25
+ #
26
+ class Logstash < Base
27
+ ##
28
+ # Format the request into a JSON-friendly payload
29
+ #
30
+ # @param [Hash] payload The incoming request payload
31
+ # @return [String] The JSON representation of the payload
32
+ #
33
+ def format(payload)
34
+ payload.merge(format: 'json').to_json
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,45 @@
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
+ # Formats the request into plaintext logging
23
+ #
24
+ class Plain < Base
25
+ ##
26
+ # Format the request by only outputting the message body and params (if set to log params)
27
+ #
28
+ # @param [Hash] payload The incoming request payload
29
+ # @return [String] The formatted string
30
+ #
31
+ def format(payload)
32
+ time = payload.fetch(:duration, 0)
33
+ grpc_status = payload.fetch(:grpc_status, 'GRPC::Ok')
34
+ route_key = payload.fetch(:method, 'unknown')
35
+ body = payload.fetch(:message, '')
36
+
37
+ msg = "[#{grpc_status}] (#{route_key}) [#{time}ms] #{body}".strip
38
+ msg += " Parameters: #{payload[:params].to_h}" if payload.key?(:params)
39
+ msg.strip
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,145 @@
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
+ require 'socket'
18
+ require_relative 'formatters/base'
19
+ require_relative 'formatters/logstash'
20
+ require_relative 'formatters/plain'
21
+
22
+ module Gruf
23
+ module Instrumentation
24
+ module RequestLogging
25
+ ##
26
+ # Represents an error if the formatter does not extend the base formatter
27
+ #
28
+ class InvalidFormatterError < StandardError; end
29
+
30
+ ##
31
+ # Handles Rails-style request logging for gruf services.
32
+ #
33
+ # This is added by default to gruf servers; if you have `Gruf.use_default_hooks = false`, you can add it back
34
+ # manually by doing:
35
+ #
36
+ # Gruf::Instrumentation::Registry.add(:request_logging, Gruf::Instrumentation::RequestLogging::Hook)
37
+ #
38
+ class Hook < ::Gruf::Instrumentation::Base
39
+
40
+ ###
41
+ # Log the request, sending it to the appropriate formatter
42
+ #
43
+ # @param [Gruf::Instrumentation::RequestContext] rc The current request context for the call
44
+ # @return [String]
45
+ #
46
+ def call(rc)
47
+ if rc.success?
48
+ type = :info
49
+ status_name = 'GRPC::Ok'
50
+ else
51
+ type = :error
52
+ status_name = rc.response_class_name
53
+ end
54
+
55
+ payload = {}
56
+ payload[:params] = sanitize(rc.request.to_h) if options.fetch(:log_parameters, false)
57
+ payload[:message] = message(rc)
58
+ payload[:service] = service_key
59
+ payload[:method] = rc.call_signature
60
+ payload[:action] = rc.call_signature
61
+ payload[:grpc_status] = status_name
62
+ payload[:duration] = rc.execution_time_rounded
63
+ payload[:status] = status(rc.response, rc.success?)
64
+ payload[:thread_id] = Thread.current.object_id
65
+ payload[:time] = Time.now.to_s
66
+ payload[:host] = Socket.gethostname
67
+
68
+ ::Gruf.logger.send(type, formatter.format(payload))
69
+ end
70
+
71
+ private
72
+
73
+ ##
74
+ # Return an appropriate log message dependent on the status
75
+ #
76
+ # @param [RequestContext] rc The current request context
77
+ # @return [String] The appropriate message body
78
+ #
79
+ def message(rc)
80
+ if rc.success?
81
+ "[GRPC::Ok] (#{service_key}.#{rc.call_signature})"
82
+ else
83
+ "[#{rc.response_class_name}] (#{service_key}.#{rc.call_signature}) #{rc.response.message}"
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Return the proper status code for the response
89
+ #
90
+ # @param [Object] response The response object
91
+ # @param [Boolean] successful If the response was successful
92
+ # @return [Boolean] The proper status code
93
+ #
94
+ def status(response, successful)
95
+ successful ? GRPC::Core::StatusCodes::OK : response.code
96
+ end
97
+
98
+ ##
99
+ # Determine the appropriate formatter for the request logging
100
+ #
101
+ # @return [Gruf::Instrumentation::RequestLogging::Formatters::Base]
102
+ #
103
+ def formatter
104
+ unless @formatter
105
+ fmt = options.fetch(:formatter, :plain)
106
+ @formatter = case fmt
107
+ when Symbol
108
+ klass = "Gruf::Instrumentation::RequestLogging::Formatters::#{fmt.to_s.capitalize}"
109
+ fmt = klass.constantize.new
110
+ when Class
111
+ fmt = fmt.new
112
+ else
113
+ fmt
114
+ end
115
+ raise Gruf::Instrumentation::RequestLogging::InvalidFormatterError unless fmt.is_a?(Gruf::Instrumentation::RequestLogging::Formatters::Base)
116
+ end
117
+ @formatter
118
+ end
119
+
120
+ ##
121
+ # Redact any blacklisted params and return an updated hash
122
+ #
123
+ # @param [Hash] params The hash of parameters to sanitize
124
+ # @return [Hash] The sanitized params in hash form
125
+ #
126
+ def sanitize(params = {})
127
+ blacklist = options.fetch(:blacklist, []).map(&:to_s)
128
+ redacted_string = options.fetch(:redacted_string, 'REDACTED')
129
+ params.each do |param, _value|
130
+ params[param] = redacted_string if blacklist.include?(param.to_s)
131
+ end
132
+ end
133
+
134
+ ##
135
+ # Fetch the options for this hook
136
+ #
137
+ # @return [Hash] Return a hash of options for this hook
138
+ #
139
+ def options
140
+ super().fetch(:request_logging, {})
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -23,10 +23,14 @@ module Gruf
23
23
  ##
24
24
  # Push data to StatsD, only doing so if a client is set
25
25
  #
26
- def call
26
+ # @param [Gruf::Instrumentation::RequestContext] rc The current request context for the call
27
+ #
28
+ def call(rc)
27
29
  if client
28
- client.increment(route_key)
29
- client.timing(route_key, execution_time)
30
+ rk = route_key(rc.call_signature)
31
+ client.increment(rk)
32
+ client.increment("#{rk}.#{postfix(rc.success?)}")
33
+ client.timing(rk, rc.execution_time)
30
34
  else
31
35
  Gruf.logger.error 'Statsd module loaded, but no client configured!'
32
36
  end
@@ -35,21 +39,23 @@ module Gruf
35
39
  private
36
40
 
37
41
  ##
38
- # @return [String]
42
+ # @param [Boolean] successful Whether or not the request was successful
43
+ # @return [String] The appropriate postfix for the key dependent on response status
39
44
  #
40
- def route_key
41
- "#{key_prefix}#{service_key}.#{call_signature}"
45
+ def postfix(successful)
46
+ successful ? 'success' : 'failure'
42
47
  end
43
48
 
44
49
  ##
45
- # @return [String]
50
+ # @param [Symbol] call_signature The method call signature for the handler
51
+ # @return [String] Return a composed route key that is used in the statsd metric
46
52
  #
47
- def service_key
48
- service.class.name.underscore.tr('/', '.')
53
+ def route_key(call_signature)
54
+ "#{key_prefix}#{method_key(call_signature)}"
49
55
  end
50
56
 
51
57
  ##
52
- # @return [String]
58
+ # @return [String] Return the sanitized key prefix for the statsd metric key
53
59
  #
54
60
  def key_prefix
55
61
  prefix = options.fetch(:prefix, '').to_s
@@ -57,17 +63,17 @@ module Gruf
57
63
  end
58
64
 
59
65
  ##
60
- # @return [::Statsd]
66
+ # @return [::Statsd] Return the given StatsD client
61
67
  #
62
68
  def client
63
69
  @client ||= options.fetch(:client, nil)
64
70
  end
65
71
 
66
72
  ##
67
- # @return [Hash]
73
+ # @return [Hash] Return a hash of options for this hook
68
74
  #
69
75
  def options
70
- @options.fetch(:statsd, {})
76
+ super().fetch(:statsd, {})
71
77
  end
72
78
  end
73
79
  end
@@ -15,9 +15,12 @@
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
+ # Mixin that allows any Gruf class to have easy access to the Gruf logger
20
+ #
18
21
  module Loggable
19
22
  ##
20
- # @return [Logger]
23
+ # @return [Logger] The set logger for Gruf
21
24
  #
22
25
  def logger
23
26
  Gruf.logger
@@ -15,19 +15,38 @@
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
+ # Handles internal gruf logging requests
20
+ #
18
21
  module Logger
22
+ ##
23
+ # Return the current Gruf logger
24
+ #
25
+ # @return [Logger]
26
+ #
19
27
  def logger
20
28
  Gruf.logger
21
29
  end
22
30
  end
23
31
 
32
+ ##
33
+ # Handles grpc internal logging requests
34
+ #
24
35
  module GrpcLogger
36
+ ##
37
+ # Return the current Gruf gRPC core logger
38
+ #
39
+ # @return [Logger]
40
+ #
25
41
  def logger
26
42
  Gruf.grpc_logger
27
43
  end
28
44
  end
29
45
  end
30
46
 
47
+ ##
48
+ # Implements gruf's gRPC logger into the gRPC library logger
49
+ #
31
50
  module GRPC
32
51
  extend Gruf::GrpcLogger
33
52
  end
@@ -19,11 +19,24 @@ module Gruf
19
19
  # Wraps the active call operation to provide metadata and timing around the request
20
20
  #
21
21
  class Response
22
- attr_reader :operation, :metadata, :trailing_metadata, :deadline, :cancelled, :execution_time
22
+ # @return [GRPC::ActiveCall::Operation] The operation that was executed for the given request
23
+ attr_reader :operation
24
+ # @return [Hash] The metadata that was attached to the operation
25
+ attr_reader :metadata
26
+ # @return [Hash] The trailing metadata that the service returned
27
+ attr_reader :trailing_metadata
28
+ # @return [Time] The set deadline on the call
29
+ attr_reader :deadline
30
+ # @return [Boolean] Whether or not the operation was cancelled
31
+ attr_reader :cancelled
32
+ # @return [Float] The time that the request took to execute
33
+ attr_reader :execution_time
23
34
 
24
35
  ##
25
- # @param [GRPC::ActiveCall::Operation] op
26
- # @param [Float] execution_time
36
+ # Initialize a response object with the given gRPC operation
37
+ #
38
+ # @param [GRPC::ActiveCall::Operation] op The given operation for the current call
39
+ # @param [Float] execution_time The amount of time that the response took to occur
27
40
  #
28
41
  def initialize(op, execution_time = nil)
29
42
  @operation = op
@@ -38,6 +51,8 @@ module Gruf
38
51
  ##
39
52
  # Return the message returned by the request
40
53
  #
54
+ # @return [Object] The protobuf response message
55
+ #
41
56
  def message
42
57
  @message ||= op.execute
43
58
  end
@@ -45,7 +60,7 @@ module Gruf
45
60
  ##
46
61
  # Return execution time of the call internally on the server in ms
47
62
  #
48
- # @return [Integer]
63
+ # @return [Float] The execution time of the response
49
64
  #
50
65
  def internal_execution_time
51
66
  key = Gruf.instrumentation_options.fetch(:output_metadata_timer, {}).fetch(:metadata_key, 'timer')
@@ -21,24 +21,31 @@ module Gruf
21
21
  # Base class for serialization of errors for transport across the grpc protocol
22
22
  #
23
23
  class Base
24
+ # @return [Gruf::Error|String] The error being serialized
24
25
  attr_reader :error
25
26
 
26
27
  ##
27
- # @param [Gruf::Error|String] err
28
+ # @param [Gruf::Error|String] err The error to serialize
28
29
  #
29
30
  def initialize(err)
30
31
  @error = err
31
32
  end
32
33
 
33
34
  ##
34
- # @return [String]
35
+ # Must be implemented in a derived class. This method should serialize the error into a transportable String
36
+ # that can be pushed into GRPC metadata across the wire.
37
+ #
38
+ # @return [String] The serialized error
35
39
  #
36
40
  def serialize
37
41
  raise NotImplementedError
38
42
  end
39
43
 
40
44
  ##
41
- # @return [Object|Hash]
45
+ # Must be implemented in a derived class. This method should deserialize the error object that is transported
46
+ # over the gRPC trailing metadata payload.
47
+ #
48
+ # @return [Object|Hash] The deserialized error object
42
49
  #
43
50
  def deserialize
44
51
  raise NotImplementedError
@@ -19,16 +19,19 @@ require 'json'
19
19
  module Gruf
20
20
  module Serializers
21
21
  module Errors
22
+ ##
23
+ # Serializes the error via JSON for transport
24
+ #
22
25
  class Json < Base
23
26
  ##
24
- # @return [String]
27
+ # @return [String] The serialized JSON string
25
28
  #
26
29
  def serialize
27
30
  @error.to_h.to_json
28
31
  end
29
32
 
30
33
  ##
31
- # @return [Object|Hash]
34
+ # @return [Hash] A hash deserialized from the inputted JSON
32
35
  #
33
36
  def deserialize
34
37
  JSON.parse(@error)