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