gruf 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|