istox_gruf 2.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +246 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/README.md +544 -0
- data/bin/gruf +29 -0
- data/lib/gruf.rb +50 -0
- data/lib/gruf/cli/executor.rb +99 -0
- data/lib/gruf/client.rb +217 -0
- data/lib/gruf/client/error.rb +66 -0
- data/lib/gruf/client/error_factory.rb +105 -0
- data/lib/gruf/configuration.rb +137 -0
- data/lib/gruf/controllers/base.rb +102 -0
- data/lib/gruf/controllers/request.rb +121 -0
- data/lib/gruf/controllers/service_binder.rb +117 -0
- data/lib/gruf/error.rb +230 -0
- data/lib/gruf/errors/debug_info.rb +56 -0
- data/lib/gruf/errors/field.rb +56 -0
- data/lib/gruf/errors/helpers.rb +44 -0
- data/lib/gruf/hooks/base.rb +34 -0
- data/lib/gruf/hooks/executor.rb +47 -0
- data/lib/gruf/hooks/registry.rb +159 -0
- data/lib/gruf/instrumentable_grpc_server.rb +64 -0
- data/lib/gruf/integrations/rails/railtie.rb +10 -0
- data/lib/gruf/interceptors/active_record/connection_reset.rb +48 -0
- data/lib/gruf/interceptors/authentication/basic.rb +87 -0
- data/lib/gruf/interceptors/base.rb +53 -0
- data/lib/gruf/interceptors/client_interceptor.rb +125 -0
- data/lib/gruf/interceptors/context.rb +56 -0
- data/lib/gruf/interceptors/instrumentation/output_metadata_timer.rb +61 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/base.rb +41 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/logstash.rb +43 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/plain.rb +48 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/interceptor.rb +225 -0
- data/lib/gruf/interceptors/instrumentation/statsd.rb +82 -0
- data/lib/gruf/interceptors/registry.rb +161 -0
- data/lib/gruf/interceptors/server_interceptor.rb +34 -0
- data/lib/gruf/interceptors/timer.rb +85 -0
- data/lib/gruf/loggable.rb +30 -0
- data/lib/gruf/logging.rb +53 -0
- data/lib/gruf/outbound/request_context.rb +71 -0
- data/lib/gruf/response.rb +71 -0
- data/lib/gruf/serializers/errors/base.rb +57 -0
- data/lib/gruf/serializers/errors/json.rb +43 -0
- data/lib/gruf/server.rb +294 -0
- data/lib/gruf/synchronized_client.rb +97 -0
- data/lib/gruf/timer.rb +78 -0
- data/lib/gruf/version.rb +20 -0
- 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
|