gruf 1.1.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -19
- data/README.md +71 -21
- data/lib/gruf.rb +3 -0
- data/lib/gruf/authentication.rb +9 -1
- data/lib/gruf/authentication/base.rb +16 -8
- data/lib/gruf/authentication/basic.rb +6 -6
- data/lib/gruf/authentication/none.rb +2 -2
- data/lib/gruf/authentication/strategies.rb +20 -7
- data/lib/gruf/client.rb +73 -11
- data/lib/gruf/configuration.rb +9 -6
- data/lib/gruf/error.rb +49 -15
- data/lib/gruf/errors/debug_info.rb +10 -5
- data/lib/gruf/errors/field.rb +12 -5
- data/lib/gruf/hooks/active_record/connection_reset.rb +15 -1
- data/lib/gruf/hooks/base.rb +26 -4
- data/lib/gruf/hooks/registry.rb +15 -9
- data/lib/gruf/instrumentation/base.rb +66 -11
- data/lib/gruf/instrumentation/output_metadata_timer.rb +6 -3
- data/lib/gruf/instrumentation/registry.rb +22 -13
- data/lib/gruf/instrumentation/request_context.rb +66 -0
- data/lib/gruf/instrumentation/request_logging/formatters/base.rb +38 -0
- data/lib/gruf/instrumentation/request_logging/formatters/logstash.rb +40 -0
- data/lib/gruf/instrumentation/request_logging/formatters/plain.rb +45 -0
- data/lib/gruf/instrumentation/request_logging/hook.rb +145 -0
- data/lib/gruf/instrumentation/statsd.rb +19 -13
- data/lib/gruf/loggable.rb +4 -1
- data/lib/gruf/logging.rb +19 -0
- data/lib/gruf/response.rb +19 -4
- data/lib/gruf/serializers/errors/base.rb +10 -3
- data/lib/gruf/serializers/errors/json.rb +5 -2
- data/lib/gruf/server.rb +10 -2
- data/lib/gruf/service.rb +32 -22
- data/lib/gruf/timer.rb +26 -5
- data/lib/gruf/version.rb +1 -1
- metadata +7 -2
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
5
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
6
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
7
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
#
|
9
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
10
|
+
# Software.
|
11
|
+
#
|
12
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
13
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
14
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
15
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
16
|
+
#
|
17
|
+
require 'json'
|
18
|
+
|
19
|
+
module Gruf
|
20
|
+
module Instrumentation
|
21
|
+
module RequestLogging
|
22
|
+
module Formatters
|
23
|
+
##
|
24
|
+
# Formats logging for gruf services into a Logstash-friendly JSON format
|
25
|
+
#
|
26
|
+
class Logstash < Base
|
27
|
+
##
|
28
|
+
# Format the request into a JSON-friendly payload
|
29
|
+
#
|
30
|
+
# @param [Hash] payload The incoming request payload
|
31
|
+
# @return [String] The JSON representation of the payload
|
32
|
+
#
|
33
|
+
def format(payload)
|
34
|
+
payload.merge(format: 'json').to_json
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
5
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
6
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
7
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
#
|
9
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
10
|
+
# Software.
|
11
|
+
#
|
12
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
13
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
14
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
15
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
16
|
+
#
|
17
|
+
module Gruf
|
18
|
+
module Instrumentation
|
19
|
+
module RequestLogging
|
20
|
+
module Formatters
|
21
|
+
##
|
22
|
+
# Formats the request into plaintext logging
|
23
|
+
#
|
24
|
+
class Plain < Base
|
25
|
+
##
|
26
|
+
# Format the request by only outputting the message body and params (if set to log params)
|
27
|
+
#
|
28
|
+
# @param [Hash] payload The incoming request payload
|
29
|
+
# @return [String] The formatted string
|
30
|
+
#
|
31
|
+
def format(payload)
|
32
|
+
time = payload.fetch(:duration, 0)
|
33
|
+
grpc_status = payload.fetch(:grpc_status, 'GRPC::Ok')
|
34
|
+
route_key = payload.fetch(:method, 'unknown')
|
35
|
+
body = payload.fetch(:message, '')
|
36
|
+
|
37
|
+
msg = "[#{grpc_status}] (#{route_key}) [#{time}ms] #{body}".strip
|
38
|
+
msg += " Parameters: #{payload[:params].to_h}" if payload.key?(:params)
|
39
|
+
msg.strip
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
5
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
6
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
7
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
#
|
9
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
10
|
+
# Software.
|
11
|
+
#
|
12
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
13
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
14
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
15
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
16
|
+
#
|
17
|
+
require 'socket'
|
18
|
+
require_relative 'formatters/base'
|
19
|
+
require_relative 'formatters/logstash'
|
20
|
+
require_relative 'formatters/plain'
|
21
|
+
|
22
|
+
module Gruf
|
23
|
+
module Instrumentation
|
24
|
+
module RequestLogging
|
25
|
+
##
|
26
|
+
# Represents an error if the formatter does not extend the base formatter
|
27
|
+
#
|
28
|
+
class InvalidFormatterError < StandardError; end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Handles Rails-style request logging for gruf services.
|
32
|
+
#
|
33
|
+
# This is added by default to gruf servers; if you have `Gruf.use_default_hooks = false`, you can add it back
|
34
|
+
# manually by doing:
|
35
|
+
#
|
36
|
+
# Gruf::Instrumentation::Registry.add(:request_logging, Gruf::Instrumentation::RequestLogging::Hook)
|
37
|
+
#
|
38
|
+
class Hook < ::Gruf::Instrumentation::Base
|
39
|
+
|
40
|
+
###
|
41
|
+
# Log the request, sending it to the appropriate formatter
|
42
|
+
#
|
43
|
+
# @param [Gruf::Instrumentation::RequestContext] rc The current request context for the call
|
44
|
+
# @return [String]
|
45
|
+
#
|
46
|
+
def call(rc)
|
47
|
+
if rc.success?
|
48
|
+
type = :info
|
49
|
+
status_name = 'GRPC::Ok'
|
50
|
+
else
|
51
|
+
type = :error
|
52
|
+
status_name = rc.response_class_name
|
53
|
+
end
|
54
|
+
|
55
|
+
payload = {}
|
56
|
+
payload[:params] = sanitize(rc.request.to_h) if options.fetch(:log_parameters, false)
|
57
|
+
payload[:message] = message(rc)
|
58
|
+
payload[:service] = service_key
|
59
|
+
payload[:method] = rc.call_signature
|
60
|
+
payload[:action] = rc.call_signature
|
61
|
+
payload[:grpc_status] = status_name
|
62
|
+
payload[:duration] = rc.execution_time_rounded
|
63
|
+
payload[:status] = status(rc.response, rc.success?)
|
64
|
+
payload[:thread_id] = Thread.current.object_id
|
65
|
+
payload[:time] = Time.now.to_s
|
66
|
+
payload[:host] = Socket.gethostname
|
67
|
+
|
68
|
+
::Gruf.logger.send(type, formatter.format(payload))
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
##
|
74
|
+
# Return an appropriate log message dependent on the status
|
75
|
+
#
|
76
|
+
# @param [RequestContext] rc The current request context
|
77
|
+
# @return [String] The appropriate message body
|
78
|
+
#
|
79
|
+
def message(rc)
|
80
|
+
if rc.success?
|
81
|
+
"[GRPC::Ok] (#{service_key}.#{rc.call_signature})"
|
82
|
+
else
|
83
|
+
"[#{rc.response_class_name}] (#{service_key}.#{rc.call_signature}) #{rc.response.message}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Return the proper status code for the response
|
89
|
+
#
|
90
|
+
# @param [Object] response The response object
|
91
|
+
# @param [Boolean] successful If the response was successful
|
92
|
+
# @return [Boolean] The proper status code
|
93
|
+
#
|
94
|
+
def status(response, successful)
|
95
|
+
successful ? GRPC::Core::StatusCodes::OK : response.code
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Determine the appropriate formatter for the request logging
|
100
|
+
#
|
101
|
+
# @return [Gruf::Instrumentation::RequestLogging::Formatters::Base]
|
102
|
+
#
|
103
|
+
def formatter
|
104
|
+
unless @formatter
|
105
|
+
fmt = options.fetch(:formatter, :plain)
|
106
|
+
@formatter = case fmt
|
107
|
+
when Symbol
|
108
|
+
klass = "Gruf::Instrumentation::RequestLogging::Formatters::#{fmt.to_s.capitalize}"
|
109
|
+
fmt = klass.constantize.new
|
110
|
+
when Class
|
111
|
+
fmt = fmt.new
|
112
|
+
else
|
113
|
+
fmt
|
114
|
+
end
|
115
|
+
raise Gruf::Instrumentation::RequestLogging::InvalidFormatterError unless fmt.is_a?(Gruf::Instrumentation::RequestLogging::Formatters::Base)
|
116
|
+
end
|
117
|
+
@formatter
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Redact any blacklisted params and return an updated hash
|
122
|
+
#
|
123
|
+
# @param [Hash] params The hash of parameters to sanitize
|
124
|
+
# @return [Hash] The sanitized params in hash form
|
125
|
+
#
|
126
|
+
def sanitize(params = {})
|
127
|
+
blacklist = options.fetch(:blacklist, []).map(&:to_s)
|
128
|
+
redacted_string = options.fetch(:redacted_string, 'REDACTED')
|
129
|
+
params.each do |param, _value|
|
130
|
+
params[param] = redacted_string if blacklist.include?(param.to_s)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Fetch the options for this hook
|
136
|
+
#
|
137
|
+
# @return [Hash] Return a hash of options for this hook
|
138
|
+
#
|
139
|
+
def options
|
140
|
+
super().fetch(:request_logging, {})
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -23,10 +23,14 @@ module Gruf
|
|
23
23
|
##
|
24
24
|
# Push data to StatsD, only doing so if a client is set
|
25
25
|
#
|
26
|
-
|
26
|
+
# @param [Gruf::Instrumentation::RequestContext] rc The current request context for the call
|
27
|
+
#
|
28
|
+
def call(rc)
|
27
29
|
if client
|
28
|
-
|
29
|
-
client.
|
30
|
+
rk = route_key(rc.call_signature)
|
31
|
+
client.increment(rk)
|
32
|
+
client.increment("#{rk}.#{postfix(rc.success?)}")
|
33
|
+
client.timing(rk, rc.execution_time)
|
30
34
|
else
|
31
35
|
Gruf.logger.error 'Statsd module loaded, but no client configured!'
|
32
36
|
end
|
@@ -35,21 +39,23 @@ module Gruf
|
|
35
39
|
private
|
36
40
|
|
37
41
|
##
|
38
|
-
# @
|
42
|
+
# @param [Boolean] successful Whether or not the request was successful
|
43
|
+
# @return [String] The appropriate postfix for the key dependent on response status
|
39
44
|
#
|
40
|
-
def
|
41
|
-
|
45
|
+
def postfix(successful)
|
46
|
+
successful ? 'success' : 'failure'
|
42
47
|
end
|
43
48
|
|
44
49
|
##
|
45
|
-
# @
|
50
|
+
# @param [Symbol] call_signature The method call signature for the handler
|
51
|
+
# @return [String] Return a composed route key that is used in the statsd metric
|
46
52
|
#
|
47
|
-
def
|
48
|
-
|
53
|
+
def route_key(call_signature)
|
54
|
+
"#{key_prefix}#{method_key(call_signature)}"
|
49
55
|
end
|
50
56
|
|
51
57
|
##
|
52
|
-
# @return [String]
|
58
|
+
# @return [String] Return the sanitized key prefix for the statsd metric key
|
53
59
|
#
|
54
60
|
def key_prefix
|
55
61
|
prefix = options.fetch(:prefix, '').to_s
|
@@ -57,17 +63,17 @@ module Gruf
|
|
57
63
|
end
|
58
64
|
|
59
65
|
##
|
60
|
-
# @return [::Statsd]
|
66
|
+
# @return [::Statsd] Return the given StatsD client
|
61
67
|
#
|
62
68
|
def client
|
63
69
|
@client ||= options.fetch(:client, nil)
|
64
70
|
end
|
65
71
|
|
66
72
|
##
|
67
|
-
# @return [Hash]
|
73
|
+
# @return [Hash] Return a hash of options for this hook
|
68
74
|
#
|
69
75
|
def options
|
70
|
-
|
76
|
+
super().fetch(:statsd, {})
|
71
77
|
end
|
72
78
|
end
|
73
79
|
end
|
data/lib/gruf/loggable.rb
CHANGED
@@ -15,9 +15,12 @@
|
|
15
15
|
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
16
16
|
#
|
17
17
|
module Gruf
|
18
|
+
##
|
19
|
+
# Mixin that allows any Gruf class to have easy access to the Gruf logger
|
20
|
+
#
|
18
21
|
module Loggable
|
19
22
|
##
|
20
|
-
# @return [Logger]
|
23
|
+
# @return [Logger] The set logger for Gruf
|
21
24
|
#
|
22
25
|
def logger
|
23
26
|
Gruf.logger
|
data/lib/gruf/logging.rb
CHANGED
@@ -15,19 +15,38 @@
|
|
15
15
|
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
16
16
|
#
|
17
17
|
module Gruf
|
18
|
+
##
|
19
|
+
# Handles internal gruf logging requests
|
20
|
+
#
|
18
21
|
module Logger
|
22
|
+
##
|
23
|
+
# Return the current Gruf logger
|
24
|
+
#
|
25
|
+
# @return [Logger]
|
26
|
+
#
|
19
27
|
def logger
|
20
28
|
Gruf.logger
|
21
29
|
end
|
22
30
|
end
|
23
31
|
|
32
|
+
##
|
33
|
+
# Handles grpc internal logging requests
|
34
|
+
#
|
24
35
|
module GrpcLogger
|
36
|
+
##
|
37
|
+
# Return the current Gruf gRPC core logger
|
38
|
+
#
|
39
|
+
# @return [Logger]
|
40
|
+
#
|
25
41
|
def logger
|
26
42
|
Gruf.grpc_logger
|
27
43
|
end
|
28
44
|
end
|
29
45
|
end
|
30
46
|
|
47
|
+
##
|
48
|
+
# Implements gruf's gRPC logger into the gRPC library logger
|
49
|
+
#
|
31
50
|
module GRPC
|
32
51
|
extend Gruf::GrpcLogger
|
33
52
|
end
|
data/lib/gruf/response.rb
CHANGED
@@ -19,11 +19,24 @@ module Gruf
|
|
19
19
|
# Wraps the active call operation to provide metadata and timing around the request
|
20
20
|
#
|
21
21
|
class Response
|
22
|
-
|
22
|
+
# @return [GRPC::ActiveCall::Operation] The operation that was executed for the given request
|
23
|
+
attr_reader :operation
|
24
|
+
# @return [Hash] The metadata that was attached to the operation
|
25
|
+
attr_reader :metadata
|
26
|
+
# @return [Hash] The trailing metadata that the service returned
|
27
|
+
attr_reader :trailing_metadata
|
28
|
+
# @return [Time] The set deadline on the call
|
29
|
+
attr_reader :deadline
|
30
|
+
# @return [Boolean] Whether or not the operation was cancelled
|
31
|
+
attr_reader :cancelled
|
32
|
+
# @return [Float] The time that the request took to execute
|
33
|
+
attr_reader :execution_time
|
23
34
|
|
24
35
|
##
|
25
|
-
#
|
26
|
-
#
|
36
|
+
# Initialize a response object with the given gRPC operation
|
37
|
+
#
|
38
|
+
# @param [GRPC::ActiveCall::Operation] op The given operation for the current call
|
39
|
+
# @param [Float] execution_time The amount of time that the response took to occur
|
27
40
|
#
|
28
41
|
def initialize(op, execution_time = nil)
|
29
42
|
@operation = op
|
@@ -38,6 +51,8 @@ module Gruf
|
|
38
51
|
##
|
39
52
|
# Return the message returned by the request
|
40
53
|
#
|
54
|
+
# @return [Object] The protobuf response message
|
55
|
+
#
|
41
56
|
def message
|
42
57
|
@message ||= op.execute
|
43
58
|
end
|
@@ -45,7 +60,7 @@ module Gruf
|
|
45
60
|
##
|
46
61
|
# Return execution time of the call internally on the server in ms
|
47
62
|
#
|
48
|
-
# @return [
|
63
|
+
# @return [Float] The execution time of the response
|
49
64
|
#
|
50
65
|
def internal_execution_time
|
51
66
|
key = Gruf.instrumentation_options.fetch(:output_metadata_timer, {}).fetch(:metadata_key, 'timer')
|
@@ -21,24 +21,31 @@ module Gruf
|
|
21
21
|
# Base class for serialization of errors for transport across the grpc protocol
|
22
22
|
#
|
23
23
|
class Base
|
24
|
+
# @return [Gruf::Error|String] The error being serialized
|
24
25
|
attr_reader :error
|
25
26
|
|
26
27
|
##
|
27
|
-
# @param [Gruf::Error|String] err
|
28
|
+
# @param [Gruf::Error|String] err The error to serialize
|
28
29
|
#
|
29
30
|
def initialize(err)
|
30
31
|
@error = err
|
31
32
|
end
|
32
33
|
|
33
34
|
##
|
34
|
-
#
|
35
|
+
# Must be implemented in a derived class. This method should serialize the error into a transportable String
|
36
|
+
# that can be pushed into GRPC metadata across the wire.
|
37
|
+
#
|
38
|
+
# @return [String] The serialized error
|
35
39
|
#
|
36
40
|
def serialize
|
37
41
|
raise NotImplementedError
|
38
42
|
end
|
39
43
|
|
40
44
|
##
|
41
|
-
#
|
45
|
+
# Must be implemented in a derived class. This method should deserialize the error object that is transported
|
46
|
+
# over the gRPC trailing metadata payload.
|
47
|
+
#
|
48
|
+
# @return [Object|Hash] The deserialized error object
|
42
49
|
#
|
43
50
|
def deserialize
|
44
51
|
raise NotImplementedError
|
@@ -19,16 +19,19 @@ require 'json'
|
|
19
19
|
module Gruf
|
20
20
|
module Serializers
|
21
21
|
module Errors
|
22
|
+
##
|
23
|
+
# Serializes the error via JSON for transport
|
24
|
+
#
|
22
25
|
class Json < Base
|
23
26
|
##
|
24
|
-
# @return [String]
|
27
|
+
# @return [String] The serialized JSON string
|
25
28
|
#
|
26
29
|
def serialize
|
27
30
|
@error.to_h.to_json
|
28
31
|
end
|
29
32
|
|
30
33
|
##
|
31
|
-
# @return [
|
34
|
+
# @return [Hash] A hash deserialized from the inputted JSON
|
32
35
|
#
|
33
36
|
def deserialize
|
34
37
|
JSON.parse(@error)
|