istox_gruf 2.7.1

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