gruf 1.2.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +98 -119
  4. data/bin/gruf +9 -3
  5. data/lib/gruf.rb +4 -4
  6. data/lib/gruf/configuration.rb +11 -20
  7. data/lib/gruf/controllers/base.rb +82 -0
  8. data/lib/gruf/controllers/request.rb +96 -0
  9. data/lib/gruf/controllers/service_binder.rb +86 -0
  10. data/lib/gruf/error.rb +9 -0
  11. data/lib/gruf/errors/helpers.rb +40 -0
  12. data/lib/gruf/{hooks → interceptors}/active_record/connection_reset.rb +4 -10
  13. data/lib/gruf/interceptors/authentication/basic.rb +80 -0
  14. data/lib/gruf/interceptors/base.rb +51 -0
  15. data/lib/gruf/{instrumentation/output_metadata_timer.rb → interceptors/context.rb} +25 -15
  16. data/lib/gruf/interceptors/instrumentation/output_metadata_timer.rb +59 -0
  17. data/lib/gruf/{instrumentation → interceptors/instrumentation}/request_logging/formatters/base.rb +15 -13
  18. data/lib/gruf/{instrumentation → interceptors/instrumentation}/request_logging/formatters/logstash.rb +15 -13
  19. data/lib/gruf/{instrumentation → interceptors/instrumentation}/request_logging/formatters/plain.rb +21 -19
  20. data/lib/gruf/interceptors/instrumentation/request_logging/interceptor.rb +191 -0
  21. data/lib/gruf/interceptors/instrumentation/statsd.rb +80 -0
  22. data/lib/gruf/interceptors/registry.rb +131 -0
  23. data/lib/gruf/{authentication/none.rb → interceptors/server_interceptor.rb} +8 -7
  24. data/lib/gruf/interceptors/timer.rb +79 -0
  25. data/lib/gruf/response.rb +1 -2
  26. data/lib/gruf/server.rb +40 -25
  27. data/lib/gruf/version.rb +1 -1
  28. metadata +19 -20
  29. data/lib/gruf/authentication.rb +0 -65
  30. data/lib/gruf/authentication/base.rb +0 -65
  31. data/lib/gruf/authentication/basic.rb +0 -74
  32. data/lib/gruf/authentication/strategies.rb +0 -107
  33. data/lib/gruf/hooks/base.rb +0 -66
  34. data/lib/gruf/hooks/registry.rb +0 -110
  35. data/lib/gruf/instrumentation/base.rb +0 -114
  36. data/lib/gruf/instrumentation/registry.rb +0 -104
  37. data/lib/gruf/instrumentation/request_context.rb +0 -82
  38. data/lib/gruf/instrumentation/request_logging/hook.rb +0 -185
  39. data/lib/gruf/instrumentation/statsd.rb +0 -80
  40. data/lib/gruf/service.rb +0 -333
@@ -15,30 +15,40 @@
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
- module Instrumentation
18
+ module Interceptors
19
19
  ##
20
- # Appends the timer metadata to the active call output metadata
20
+ # Runs interceptors in a given request context
21
21
  #
22
- class OutputMetadataTimer < Gruf::Instrumentation::Base
22
+ class Context
23
+ include Gruf::Loggable
24
+
23
25
  ##
24
- # Handle the instrumented response. Note: this will only instrument timings of _successful_ responses.
26
+ # Initialize the interception context
25
27
  #
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
+ # @param [Array<Gruf::Interceptors::ServerInterceptor>] interceptors
28
29
  #
29
- def call(rc)
30
- return unless rc.active_call && rc.active_call.respond_to?(:output_metadata)
31
-
32
- rc.active_call.output_metadata.update(metadata_key => rc.execution_time.to_s)
30
+ def initialize(interceptors = [])
31
+ @interceptors = interceptors
33
32
  end
34
33
 
35
- private
36
-
37
34
  ##
38
- # @return [Symbol] The metadata key that the time result should be set to
35
+ # Intercept the given request and run interceptors in a FIFO execution order
39
36
  #
40
- def metadata_key
41
- options.fetch(:output_metadata_timer, {}).fetch(:metadata_key, :timer).to_sym
37
+ def intercept!
38
+ return yield if @interceptors.none?
39
+
40
+ i = @interceptors.pop
41
+ return yield unless i
42
+
43
+ logger.debug "Intercepting request with interceptor: #{i.class}"
44
+
45
+ i.call do
46
+ if @interceptors.any?
47
+ intercept! { yield }
48
+ else
49
+ yield
50
+ end
51
+ end
42
52
  end
43
53
  end
44
54
  end
@@ -0,0 +1,59 @@
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 Interceptors
19
+ module Instrumentation
20
+ ##
21
+ # Appends the timer metadata to the active call output metadata
22
+ #
23
+ class OutputMetadataTimer < ::Gruf::Interceptors::ServerInterceptor
24
+ delegate :active_call, to: :request
25
+
26
+ ##
27
+ # Handle the instrumented response. Note: this will only instrument timings of _successful_ responses.
28
+ #
29
+ def call
30
+ return unless active_call.respond_to?(:output_metadata)
31
+
32
+ result = Gruf::Interceptors::Timer.time do
33
+ yield
34
+ end
35
+ output_metadata.update(metadata_key => result.elapsed.to_s)
36
+
37
+ raise result.message unless result.successful?
38
+ result.message
39
+ end
40
+
41
+ private
42
+
43
+ ##
44
+ # @return [Symbol] The metadata key that the time result should be set to
45
+ #
46
+ def metadata_key
47
+ options.fetch(:metadata_key, :timer).to_sym
48
+ end
49
+
50
+ ##
51
+ # @return [Hash]
52
+ #
53
+ def output_metadata
54
+ active_call.output_metadata
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -15,21 +15,23 @@
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
- module Instrumentation
19
- module RequestLogging
20
- module Formatters
21
- ##
22
- # Base class for request log formatting
23
- #
24
- class Base
18
+ module Interceptors
19
+ module Instrumentation
20
+ module RequestLogging
21
+ module Formatters
25
22
  ##
26
- # Format the parameters into a loggable string. Must be implemented in every derivative class
23
+ # Base class for request log formatting
27
24
  #
28
- # @param [Hash] _payload The incoming request payload
29
- # @return [String] The formatted string
30
- #
31
- def format(_payload)
32
- raise NotImplementedError
25
+ class Base
26
+ ##
27
+ # Format the parameters into a loggable string. Must be implemented in every derivative class
28
+ #
29
+ # @param [Hash] _payload The incoming request payload
30
+ # @return [String] The formatted string
31
+ #
32
+ def format(_payload)
33
+ raise NotImplementedError
34
+ end
33
35
  end
34
36
  end
35
37
  end
@@ -17,21 +17,23 @@
17
17
  require 'json'
18
18
 
19
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
20
+ module Interceptors
21
+ module Instrumentation
22
+ module RequestLogging
23
+ module Formatters
27
24
  ##
28
- # Format the request into a JSON-friendly payload
25
+ # Formats logging for gruf services into a Logstash-friendly JSON format
29
26
  #
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
27
+ class Logstash < Base
28
+ ##
29
+ # Format the request into a JSON-friendly payload
30
+ #
31
+ # @param [Hash] payload The incoming request payload
32
+ # @return [String] The JSON representation of the payload
33
+ #
34
+ def format(payload)
35
+ payload.merge(format: 'json').to_json
36
+ end
35
37
  end
36
38
  end
37
39
  end
@@ -15,28 +15,30 @@
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
- module Instrumentation
19
- module RequestLogging
20
- module Formatters
21
- ##
22
- # Formats the request into plaintext logging
23
- #
24
- class Plain < Base
18
+ module Interceptors
19
+ module Instrumentation
20
+ module RequestLogging
21
+ module Formatters
25
22
  ##
26
- # Format the request by only outputting the message body and params (if set to log params)
23
+ # Formats the request into plaintext logging
27
24
  #
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, '')
25
+ class Plain < Base
26
+ ##
27
+ # Format the request by only outputting the message body and params (if set to log params)
28
+ #
29
+ # @param [Hash] payload The incoming request payload
30
+ # @return [String] The formatted string
31
+ #
32
+ def format(payload)
33
+ time = payload.fetch(:duration, 0)
34
+ grpc_status = payload.fetch(:grpc_status, 'GRPC::Ok')
35
+ route_key = payload.fetch(:method, 'unknown')
36
+ body = payload.fetch(:message, '')
36
37
 
37
- msg = "[#{grpc_status}] (#{route_key}) [#{time}ms] #{body}".strip
38
- msg += " Parameters: #{payload[:params].to_h}" if payload.key?(:params)
39
- msg.strip
38
+ msg = "[#{grpc_status}] (#{route_key}) [#{time}ms] #{body}".strip
39
+ msg += " Parameters: #{payload[:params].to_h}" if payload.key?(:params)
40
+ msg.strip
41
+ end
40
42
  end
41
43
  end
42
44
  end
@@ -0,0 +1,191 @@
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 Interceptors
24
+ module Instrumentation
25
+ module RequestLogging
26
+ ##
27
+ # Represents an error if the formatter does not extend the base formatter
28
+ #
29
+ class InvalidFormatterError < StandardError; end
30
+
31
+ ##
32
+ # Handles Rails-style request logging for gruf services.
33
+ #
34
+ # This is added by default to gruf servers; if you have `Gruf.use_default_hooks = false`, you can add it back
35
+ # manually by doing:
36
+ #
37
+ # Gruf::Instrumentation::Registry.add(:request_logging, Gruf::Instrumentation::RequestLogging::Hook)
38
+ #
39
+ class Interceptor < ::Gruf::Interceptors::ServerInterceptor
40
+
41
+ ###
42
+ # Log the request, sending it to the appropriate formatter
43
+ #
44
+ # @return [String]
45
+ #
46
+ def call
47
+ result = Gruf::Interceptors::Timer.time do
48
+ yield
49
+ end
50
+
51
+ if result.successful?
52
+ type = options.fetch(:success_log_level, :info).to_sym
53
+ status_name = 'GRPC::Ok'
54
+ else
55
+ type = options.fetch(:failure_log_level, :error).to_sym
56
+ status_name = result.message_class_name
57
+ end
58
+
59
+ payload = {}
60
+ if !request.client_streamer?
61
+ payload[:params] = sanitize(request.message.to_h) if options.fetch(:log_parameters, false)
62
+ payload[:message] = message(request, result)
63
+ payload[:status] = status(result.message, result.successful?)
64
+ else
65
+ payload[:params] = {}
66
+ payload[:message] = ''
67
+ payload[:status] = GRPC::Core::StatusCodes::OK
68
+ end
69
+ payload[:service] = request.service_key
70
+ payload[:method] = request.method_key
71
+ payload[:action] = request.method_key
72
+ payload[:grpc_status] = status_name
73
+ payload[:duration] = result.elapsed_rounded
74
+ payload[:thread_id] = Thread.current.object_id
75
+ payload[:time] = Time.now.to_s
76
+ payload[:host] = Socket.gethostname
77
+
78
+ ::Gruf.logger.send(type, formatter.format(payload))
79
+
80
+ raise result.message unless result.successful?
81
+ result.message
82
+ end
83
+
84
+ private
85
+
86
+ ##
87
+ # Return an appropriate log message dependent on the status
88
+ #
89
+ # @param [Gruf::Controllers::Request] request
90
+ # @param [Gruf::Interceptors::Timer::Result] result
91
+ # @return [String] The appropriate message body
92
+ #
93
+ def message(request, result)
94
+ if result.successful?
95
+ "[GRPC::Ok] (#{request.method_name})"
96
+ else
97
+ "[#{result.message_class_name}] (#{request.method_name}) #{result.message.message}"
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Return the proper status code for the response
103
+ #
104
+ # @param [Object] response The response object
105
+ # @param [Boolean] successful If the response was successful
106
+ # @return [Boolean] The proper status code
107
+ #
108
+ def status(response, successful)
109
+ successful ? GRPC::Core::StatusCodes::OK : response.code
110
+ end
111
+
112
+ ##
113
+ # Determine the appropriate formatter for the request logging
114
+ #
115
+ # @return [Gruf::Instrumentation::RequestLogging::Formatters::Base]
116
+ #
117
+ def formatter
118
+ unless @formatter
119
+ fmt = options.fetch(:formatter, :plain)
120
+ @formatter = case fmt
121
+ when Symbol
122
+ klass = "Gruf::Interceptors::Instrumentation::RequestLogging::Formatters::#{fmt.to_s.capitalize}"
123
+ fmt = klass.constantize.new
124
+ when Class
125
+ fmt = fmt.new
126
+ else
127
+ fmt
128
+ end
129
+ raise InvalidFormatterError unless fmt.is_a?(Formatters::Base)
130
+ end
131
+ @formatter
132
+ end
133
+
134
+ ##
135
+ # Redact any blacklisted params and return an updated hash
136
+ #
137
+ # @param [Hash] params The hash of parameters to sanitize
138
+ # @return [Hash] The sanitized params in hash form
139
+ #
140
+ def sanitize(params = {})
141
+ blacklists = options.fetch(:blacklist, []).map(&:to_s)
142
+ redacted_string = options.fetch(:redacted_string, 'REDACTED')
143
+ blacklists.each do |blacklist|
144
+ parts = blacklist.split('.').map(&:to_sym)
145
+ redact!(parts, 0, params, redacted_string)
146
+ end
147
+ params
148
+ end
149
+
150
+ ##
151
+ # Helper method to recursively redact based on the black list
152
+ #
153
+ # @param [Array] parts The blacklist. ex. 'data.schema' -> [:data, :schema]
154
+ # @param [Integer] idx The current index of the blacklist
155
+ # @param [Hash] params The hash of parameters to sanitize
156
+ # @param [String] redacted_string The custom redact string
157
+ #
158
+ def redact!(parts = [], idx = 0, params = {}, redacted_string = 'REDACTED')
159
+ return unless parts.is_a?(Array) && params.is_a?(Hash)
160
+ return if idx >= parts.size || !params.key?(parts[idx])
161
+ if idx == parts.size - 1
162
+ if params[parts[idx]].is_a? Hash
163
+ hash_deep_redact!(params[parts[idx]], redacted_string)
164
+ else
165
+ params[parts[idx]] = redacted_string
166
+ end
167
+ return
168
+ end
169
+ redact!(parts, idx + 1, params[parts[idx]], redacted_string)
170
+ end
171
+
172
+ ##
173
+ # Helper method to recursively redact the value of all hash keys
174
+ #
175
+ # @param [Hash] hash Part of the hash of parameters to sanitize
176
+ # @param [String] redacted_string The custom redact string
177
+ #
178
+ def hash_deep_redact!(hash, redacted_string)
179
+ hash.keys.each do |key|
180
+ if hash[key].is_a? Hash
181
+ hash_deep_redact!(hash[key], redacted_string)
182
+ else
183
+ hash[key] = redacted_string
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,80 @@
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 Interceptors
19
+ module Instrumentation
20
+ ##
21
+ # Adds increment and timing stats to gRPC routes, pushing data to StatsD
22
+ #
23
+ class Statsd < Gruf::Interceptors::ServerInterceptor
24
+ ##
25
+ # Push data to StatsD, only doing so if a client is set
26
+ #
27
+ def call
28
+ unless client
29
+ Gruf.logger.error 'Statsd module loaded, but no client configured!'
30
+ return yield
31
+ end
32
+
33
+ client.increment(route_key)
34
+
35
+ result = Gruf::Interceptors::Timer.time do
36
+ yield
37
+ end
38
+
39
+ client.increment("#{route_key}.#{postfix(result.successful?)}")
40
+ client.timing(route_key, result.elapsed)
41
+
42
+ raise result.message unless result.successful?
43
+ result.message
44
+ end
45
+
46
+ private
47
+
48
+ ##
49
+ # @param [Boolean] successful Whether or not the request was successful
50
+ # @return [String] The appropriate postfix for the key dependent on response status
51
+ #
52
+ def postfix(successful)
53
+ successful ? 'success' : 'failure'
54
+ end
55
+
56
+ ##
57
+ # @return [String] Return a composed route key that is used in the statsd metric
58
+ #
59
+ def route_key
60
+ "#{key_prefix}#{request.method_name}"
61
+ end
62
+
63
+ ##
64
+ # @return [String] Return the sanitized key prefix for the statsd metric key
65
+ #
66
+ def key_prefix
67
+ prefix = options.fetch(:prefix, '').to_s
68
+ prefix.empty? ? '' : "#{prefix}."
69
+ end
70
+
71
+ ##
72
+ # @return [::Statsd] Return the given StatsD client
73
+ #
74
+ def client
75
+ @client ||= options.fetch(:client, nil)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end