istox_gruf 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +246 -0
  3. data/CODE_OF_CONDUCT.md +46 -0
  4. data/README.md +544 -0
  5. data/bin/gruf +29 -0
  6. data/lib/gruf.rb +50 -0
  7. data/lib/gruf/cli/executor.rb +99 -0
  8. data/lib/gruf/client.rb +217 -0
  9. data/lib/gruf/client/error.rb +66 -0
  10. data/lib/gruf/client/error_factory.rb +105 -0
  11. data/lib/gruf/configuration.rb +137 -0
  12. data/lib/gruf/controllers/base.rb +102 -0
  13. data/lib/gruf/controllers/request.rb +121 -0
  14. data/lib/gruf/controllers/service_binder.rb +117 -0
  15. data/lib/gruf/error.rb +230 -0
  16. data/lib/gruf/errors/debug_info.rb +56 -0
  17. data/lib/gruf/errors/field.rb +56 -0
  18. data/lib/gruf/errors/helpers.rb +44 -0
  19. data/lib/gruf/hooks/base.rb +34 -0
  20. data/lib/gruf/hooks/executor.rb +47 -0
  21. data/lib/gruf/hooks/registry.rb +159 -0
  22. data/lib/gruf/instrumentable_grpc_server.rb +64 -0
  23. data/lib/gruf/integrations/rails/railtie.rb +10 -0
  24. data/lib/gruf/interceptors/active_record/connection_reset.rb +48 -0
  25. data/lib/gruf/interceptors/authentication/basic.rb +87 -0
  26. data/lib/gruf/interceptors/base.rb +53 -0
  27. data/lib/gruf/interceptors/client_interceptor.rb +125 -0
  28. data/lib/gruf/interceptors/context.rb +56 -0
  29. data/lib/gruf/interceptors/instrumentation/output_metadata_timer.rb +61 -0
  30. data/lib/gruf/interceptors/instrumentation/request_logging/formatters/base.rb +41 -0
  31. data/lib/gruf/interceptors/instrumentation/request_logging/formatters/logstash.rb +43 -0
  32. data/lib/gruf/interceptors/instrumentation/request_logging/formatters/plain.rb +48 -0
  33. data/lib/gruf/interceptors/instrumentation/request_logging/interceptor.rb +225 -0
  34. data/lib/gruf/interceptors/instrumentation/statsd.rb +82 -0
  35. data/lib/gruf/interceptors/registry.rb +161 -0
  36. data/lib/gruf/interceptors/server_interceptor.rb +34 -0
  37. data/lib/gruf/interceptors/timer.rb +85 -0
  38. data/lib/gruf/loggable.rb +30 -0
  39. data/lib/gruf/logging.rb +53 -0
  40. data/lib/gruf/outbound/request_context.rb +71 -0
  41. data/lib/gruf/response.rb +71 -0
  42. data/lib/gruf/serializers/errors/base.rb +57 -0
  43. data/lib/gruf/serializers/errors/json.rb +43 -0
  44. data/lib/gruf/server.rb +294 -0
  45. data/lib/gruf/synchronized_client.rb +97 -0
  46. data/lib/gruf/timer.rb +78 -0
  47. data/lib/gruf/version.rb +20 -0
  48. metadata +203 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Interceptors
20
+ ##
21
+ # Base class for interception requests
22
+ #
23
+ class Base
24
+ # @var [Gruf::Controllers::Request] request
25
+ attr_reader :request
26
+ # @var [Gruf::Error] error
27
+ attr_reader :error
28
+ # @var [Hash] options
29
+ attr_reader :options
30
+
31
+ ##
32
+ # @param [Gruf::Controllers::Request] request
33
+ # @param [Gruf::Error] error
34
+ # @param [Hash] options
35
+ #
36
+ def initialize(request, error, options = {})
37
+ @request = request
38
+ @error = error
39
+ @options = options || {}
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ require_relative 'client_interceptor'
46
+ require_relative 'server_interceptor'
47
+ require_relative 'context'
48
+ require_relative 'timer'
49
+ require_relative 'active_record/connection_reset'
50
+ require_relative 'authentication/basic'
51
+ require_relative 'instrumentation/statsd'
52
+ require_relative 'instrumentation/output_metadata_timer'
53
+ require_relative 'instrumentation/request_logging/interceptor'
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Interceptors
20
+ ##
21
+ # Intercepts outbound client requests to provide a unified interface and request context
22
+ #
23
+ class ClientInterceptor < GRPC::ClientInterceptor
24
+ include Gruf::Loggable
25
+
26
+ ##
27
+ # Handles interception of outbound calls. Implement this in your derivative interceptor implementation.
28
+ #
29
+ # @param [Gruf::Outbound::RequestContext] request_context The context of the outbound request
30
+ # @return [Object] This method must return the response from the yielded block
31
+ #
32
+ def call(request_context:)
33
+ logger.debug "Logging client interceptor for request: #{request_context.method}"
34
+ yield
35
+ end
36
+
37
+ ##
38
+ # Call the interceptor from the request_response call
39
+ #
40
+ # @param [Object] request The request being sent
41
+ # @param [GRPC::ActiveCall] call The GRPC ActiveCall object
42
+ # @param [Method] method The method being called
43
+ # @param [Hash] metadata A hash of outgoing metadata
44
+ # @return [Object] The response message
45
+ #
46
+ def request_response(request: nil, call: nil, method: nil, metadata: nil)
47
+ rc = Gruf::Outbound::RequestContext.new(
48
+ type: :request_response,
49
+ requests: [request],
50
+ call: call,
51
+ method: method,
52
+ metadata: metadata
53
+ )
54
+ call(request_context: rc) do
55
+ yield
56
+ end
57
+ end
58
+
59
+ ##
60
+ # Call the interceptor from the client_streamer call
61
+ #
62
+ # @param [Enumerable] requests An enumerable of requests being sent
63
+ # @param [GRPC::ActiveCall] call The GRPC ActiveCall object
64
+ # @param [Method] method The method being called
65
+ # @param [Hash] metadata A hash of outgoing metadata
66
+ # @return [Object] The response message
67
+ #
68
+ def client_streamer(requests: nil, call: nil, method: nil, metadata: nil)
69
+ rc = Gruf::Outbound::RequestContext.new(
70
+ type: :client_streamer,
71
+ requests: requests,
72
+ call: call,
73
+ method: method,
74
+ metadata: metadata
75
+ )
76
+ call(request_context: rc) do
77
+ yield
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Call the interceptor from the server_streamer call
83
+ #
84
+ # @param [Object] request The request being sent
85
+ # @param [GRPC::ActiveCall] call The GRPC ActiveCall object
86
+ # @param [Method] method The method being called
87
+ # @param [Hash] metadata A hash of outgoing metadata
88
+ # @return [Object] The response message
89
+ #
90
+ def server_streamer(request: nil, call: nil, method: nil, metadata: nil)
91
+ rc = Gruf::Outbound::RequestContext.new(
92
+ type: :server_streamer,
93
+ requests: [request],
94
+ call: call,
95
+ method: method,
96
+ metadata: metadata
97
+ )
98
+ call(request_context: rc) do
99
+ yield
100
+ end
101
+ end
102
+
103
+ ##
104
+ # Call the interceptor from the bidi_streamer call
105
+ #
106
+ # @param [Enumerable] requests An enumerable of requests being sent
107
+ # @param [GRPC::ActiveCall] call The GRPC ActiveCall object
108
+ # @param [Method] method The method being called
109
+ # @param [Hash] metadata A hash of outgoing metadata
110
+ #
111
+ def bidi_streamer(requests: nil, call: nil, method: nil, metadata: nil)
112
+ rc = Gruf::Outbound::RequestContext.new(
113
+ type: :bidi_streamer,
114
+ requests: requests,
115
+ call: call,
116
+ method: method,
117
+ metadata: metadata
118
+ )
119
+ call(request_context: rc) do
120
+ yield
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Interceptors
20
+ ##
21
+ # Runs interceptors in a given request context
22
+ #
23
+ class Context
24
+ include Gruf::Loggable
25
+
26
+ ##
27
+ # Initialize the interception context
28
+ #
29
+ # @param [Array<Gruf::Interceptors::ServerInterceptor>] interceptors
30
+ #
31
+ def initialize(interceptors = [])
32
+ @interceptors = interceptors
33
+ end
34
+
35
+ ##
36
+ # Intercept the given request and run interceptors in a FIFO execution order
37
+ #
38
+ def intercept!
39
+ return yield if @interceptors.none?
40
+
41
+ i = @interceptors.pop
42
+ return yield unless i
43
+
44
+ logger.debug "Intercepting request with interceptor: #{i.class}"
45
+
46
+ i.call do
47
+ if @interceptors.any?
48
+ intercept! { yield }
49
+ else
50
+ yield
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Interceptors
20
+ module Instrumentation
21
+ ##
22
+ # Appends the timer metadata to the active call output metadata
23
+ #
24
+ class OutputMetadataTimer < ::Gruf::Interceptors::ServerInterceptor
25
+ delegate :active_call, to: :request
26
+
27
+ ##
28
+ # Handle the instrumented response. Note: this will only instrument timings of _successful_ responses.
29
+ #
30
+ def call
31
+ return unless active_call.respond_to?(:output_metadata)
32
+
33
+ result = Gruf::Interceptors::Timer.time do
34
+ yield
35
+ end
36
+ output_metadata.update(metadata_key => result.elapsed.to_s)
37
+
38
+ raise result.message unless result.successful?
39
+
40
+ result.message
41
+ end
42
+
43
+ private
44
+
45
+ ##
46
+ # @return [Symbol] The metadata key that the time result should be set to
47
+ #
48
+ def metadata_key
49
+ options.fetch(:metadata_key, :timer).to_sym
50
+ end
51
+
52
+ ##
53
+ # @return [Hash]
54
+ #
55
+ def output_metadata
56
+ active_call.output_metadata
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Interceptors
20
+ module Instrumentation
21
+ module RequestLogging
22
+ module Formatters
23
+ ##
24
+ # Base class for request log formatting
25
+ #
26
+ class Base
27
+ ##
28
+ # Format the parameters into a loggable string. Must be implemented in every derivative class
29
+ #
30
+ # @param [Hash] _payload The incoming request payload
31
+ # @return [String] The formatted string
32
+ #
33
+ def format(_payload)
34
+ raise NotImplementedError
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ require 'json'
19
+
20
+ module Gruf
21
+ module Interceptors
22
+ module Instrumentation
23
+ module RequestLogging
24
+ module Formatters
25
+ ##
26
+ # Formats logging for gruf services into a Logstash-friendly JSON format
27
+ #
28
+ class Logstash < Base
29
+ ##
30
+ # Format the request into a JSON-friendly payload
31
+ #
32
+ # @param [Hash] payload The incoming request payload
33
+ # @return [String] The JSON representation of the payload
34
+ #
35
+ def format(payload)
36
+ payload.merge(format: 'json').to_json
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Interceptors
20
+ module Instrumentation
21
+ module RequestLogging
22
+ module Formatters
23
+ ##
24
+ # Formats the request into plaintext logging
25
+ #
26
+ class Plain < Base
27
+ ##
28
+ # Format the request by only outputting the message body and params (if set to log params)
29
+ #
30
+ # @param [Hash] payload The incoming request payload
31
+ # @return [String] The formatted string
32
+ #
33
+ def format(payload)
34
+ time = payload.fetch(:duration, 0)
35
+ grpc_status = payload.fetch(:grpc_status, 'GRPC::Ok')
36
+ route_key = payload.fetch(:method, 'unknown')
37
+ body = payload.fetch(:message, '')
38
+
39
+ msg = "[#{grpc_status}] (#{route_key}) [#{time}ms] #{body}".strip
40
+ msg += " Parameters: #{payload[:params].to_h}" if payload.key?(:params)
41
+ msg.strip
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ require 'socket'
19
+ require_relative 'formatters/base'
20
+ require_relative 'formatters/logstash'
21
+ require_relative 'formatters/plain'
22
+
23
+ module Gruf
24
+ module Interceptors
25
+ module Instrumentation
26
+ module RequestLogging
27
+ ##
28
+ # Represents an error if the formatter does not extend the base formatter
29
+ #
30
+ class InvalidFormatterError < StandardError; end
31
+
32
+ ##
33
+ # Handles Rails-style request logging for gruf services.
34
+ #
35
+ # This is added by default to gruf servers; if you have `Gruf.use_default_hooks = false`, you can add it back
36
+ # manually by doing:
37
+ #
38
+ # Gruf::Instrumentation::Registry.add(:request_logging, Gruf::Instrumentation::RequestLogging::Hook)
39
+ #
40
+ class Interceptor < ::Gruf::Interceptors::ServerInterceptor
41
+ # Default mappings of codes to log levels...
42
+ LOG_LEVEL_MAP = {
43
+ 'GRPC::Ok' => :debug,
44
+ 'GRPC::InvalidArgument' => :debug,
45
+ 'GRPC::NotFound' => :debug,
46
+ 'GRPC::AlreadyExists' => :debug,
47
+ 'GRPC::OutOfRange' => :debug,
48
+ 'GRPC::Unauthenticated' => :warn,
49
+ 'GRPC::PermissionDenied' => :warn,
50
+ 'GRPC::Unknown' => :error,
51
+ 'GRPC::Internal' => :error,
52
+ 'GRPC::DataLoss' => :error,
53
+ 'GRPC::FailedPrecondition' => :error,
54
+ 'GRPC::Unavailable' => :error,
55
+ 'GRPC::DeadlineExceeded' => :error,
56
+ 'GRPC::Cancelled' => :error
57
+ }.freeze
58
+
59
+ ###
60
+ # Log the request, sending it to the appropriate formatter
61
+ #
62
+ # @return [String]
63
+ #
64
+ def call
65
+ return yield if options.fetch(:ignore_methods, []).include?(request.method_name)
66
+
67
+ result = Gruf::Interceptors::Timer.time do
68
+ yield
69
+ end
70
+
71
+ # Fetch log level options and merge with default...
72
+ log_level_map = LOG_LEVEL_MAP.merge(options.fetch(:log_levels, {}))
73
+
74
+ # A result is either successful, or, some level of feedback handled in the else block...
75
+ if result.successful?
76
+ type = log_level_map['GRPC::Ok'] || :debug
77
+ status_name = 'GRPC::Ok'
78
+ else
79
+ type = log_level_map[result.message_class_name] || :error
80
+ status_name = result.message_class_name
81
+ end
82
+
83
+ payload = {}
84
+ if !request.client_streamer? && !request.bidi_streamer?
85
+ payload[:params] = sanitize(request.message.to_h) if options.fetch(:log_parameters, false)
86
+ payload[:message] = message(request, result)
87
+ payload[:status] = status(result.message, result.successful?)
88
+ else
89
+ payload[:params] = {}
90
+ payload[:message] = ''
91
+ payload[:status] = GRPC::Core::StatusCodes::OK
92
+ end
93
+
94
+ payload[:service] = request.service_key
95
+ payload[:method] = request.method_key
96
+ payload[:action] = request.method_key
97
+ payload[:grpc_status] = status_name
98
+ payload[:duration] = result.elapsed_rounded
99
+ payload[:thread_id] = Thread.current.object_id
100
+ payload[:time] = Time.now.to_s
101
+ payload[:host] = Socket.gethostname
102
+
103
+ logger.send(type, formatter.format(payload))
104
+
105
+ raise result.message unless result.successful?
106
+
107
+ result.message
108
+ end
109
+
110
+ private
111
+
112
+ ##
113
+ # @return [::Gruf::Logger]
114
+ #
115
+ def logger
116
+ @logger ||= options.fetch(:logger, ::Gruf.logger)
117
+ end
118
+
119
+ ##
120
+ # Return an appropriate log message dependent on the status
121
+ #
122
+ # @param [Gruf::Controllers::Request] request
123
+ # @param [Gruf::Interceptors::Timer::Result] result
124
+ # @return [String] The appropriate message body
125
+ #
126
+ def message(request, result)
127
+ return "[GRPC::Ok] (#{request.method_name})" if result.successful?
128
+
129
+ "[#{result.message_class_name}] (#{request.method_name}) #{result.message.message}"
130
+ end
131
+
132
+ ##
133
+ # Return the proper status code for the response
134
+ #
135
+ # @param [Object] response The response object
136
+ # @param [Boolean] successful If the response was successful
137
+ # @return [Boolean] The proper status code
138
+ #
139
+ def status(response, successful)
140
+ successful ? GRPC::Core::StatusCodes::OK : response.code
141
+ end
142
+
143
+ ##
144
+ # Determine the appropriate formatter for the request logging
145
+ #
146
+ # @return [Gruf::Instrumentation::RequestLogging::Formatters::Base]
147
+ #
148
+ def formatter
149
+ unless @formatter
150
+ fmt = options.fetch(:formatter, :plain)
151
+ @formatter = case fmt
152
+ when Symbol
153
+ prefix = 'Gruf::Interceptors::Instrumentation::RequestLogging::Formatters::'
154
+ klass = "#{prefix}#{fmt.to_s.capitalize}"
155
+ fmt = klass.constantize.new
156
+ when Class
157
+ fmt = fmt.new
158
+ else
159
+ fmt
160
+ end
161
+ raise InvalidFormatterError unless fmt.is_a?(Formatters::Base)
162
+ end
163
+ @formatter
164
+ end
165
+
166
+ ##
167
+ # Redact any blacklisted params and return an updated hash
168
+ #
169
+ # @param [Hash] params The hash of parameters to sanitize
170
+ # @return [Hash] The sanitized params in hash form
171
+ #
172
+ def sanitize(params = {})
173
+ blacklists = options.fetch(:blacklist, []).map(&:to_s)
174
+ redacted_string = options.fetch(:redacted_string, 'REDACTED')
175
+ blacklists.each do |blacklist|
176
+ parts = blacklist.split('.').map(&:to_sym)
177
+ redact!(parts, 0, params, redacted_string)
178
+ end
179
+ params
180
+ end
181
+
182
+ ##
183
+ # Helper method to recursively redact based on the black list
184
+ #
185
+ # @param [Array] parts The blacklist. ex. 'data.schema' -> [:data, :schema]
186
+ # @param [Integer] idx The current index of the blacklist
187
+ # @param [Hash] params The hash of parameters to sanitize
188
+ # @param [String] redacted_string The custom redact string
189
+ #
190
+ def redact!(parts = [], idx = 0, params = {}, redacted_string = 'REDACTED')
191
+ return unless parts.is_a?(Array) && params.is_a?(Hash)
192
+
193
+ return if idx >= parts.size || !params.key?(parts[idx])
194
+
195
+ if idx == parts.size - 1
196
+ if params[parts[idx]].is_a? Hash
197
+ hash_deep_redact!(params[parts[idx]], redacted_string)
198
+ else
199
+ params[parts[idx]] = redacted_string
200
+ end
201
+ return
202
+ end
203
+ redact!(parts, idx + 1, params[parts[idx]], redacted_string)
204
+ end
205
+
206
+ ##
207
+ # Helper method to recursively redact the value of all hash keys
208
+ #
209
+ # @param [Hash] hash Part of the hash of parameters to sanitize
210
+ # @param [String] redacted_string The custom redact string
211
+ #
212
+ def hash_deep_redact!(hash, redacted_string)
213
+ hash.each_key do |key|
214
+ if hash[key].is_a? Hash
215
+ hash_deep_redact!(hash[key], redacted_string)
216
+ else
217
+ hash[key] = redacted_string
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end